Skip to content

Commit 16e963d

Browse files
The credential manager says what it decides instead of calling it a pin (#201)
The operator's ask, twice over: first the banned word `#repin`, then the whole "pin" vocabulary — nobody knows what a pin is. The concept is **which credential this process acts as**, decided once at the first `activeCredential()` read. The private surface now says that: type `ActingAs`, field `#actingAs`, resolver `#resolveActingAs`, and `#actAs` for the mutation-time change that also discards storage built for the previous credential. Test descriptions use the same words. No behaviour change; all 978 CLI tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent d4c7bb7 commit 16e963d

3 files changed

Lines changed: 54 additions & 53 deletions

File tree

packages/cli/src/auth/credential-manager.ts

Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,15 @@ export interface FileCredentialManagerOptions {
5858
}
5959

6060
/** Which credential this process acts as, decided at the first
61-
* activeCredential() read. The decision is what is pinned; the
61+
* activeCredential() read. The decision is what is held; the
6262
* material behind it is re-read on every call. */
63-
type Pin =
63+
type ActingAs =
6464
| { readonly kind: "unresolved" }
6565
| { readonly kind: "environment" }
6666
| { readonly kind: "session"; readonly workspaceId: string }
6767
| { readonly kind: "none" };
6868

69-
type ResolvedPin = Exclude<Pin, { readonly kind: "unresolved" }>;
69+
type ResolvedActingAs = Exclude<ActingAs, { readonly kind: "unresolved" }>;
7070

7171
/**
7272
* The memory-backed storage, for a credential with no home record: a
@@ -106,7 +106,7 @@ function memoryBackedStorage(
106106

107107
/**
108108
* The credential manager over one state file. Sessions are keyed by
109-
* workspace id; which credential this process acts as is pinned once;
109+
* workspace id; which credential this process acts as is decided once;
110110
* every mutation takes a short file lock, re-reads, applies its slice,
111111
* and writes atomically. Reads never write and take no lock.
112112
*/
@@ -116,11 +116,11 @@ export class FileCredentialManager implements CredentialManager {
116116
readonly #debug: DebugLog;
117117
readonly #fetchWorkspaceName: FetchWorkspaceName | undefined;
118118
readonly #refreshCredential: CredentialRefresher | undefined;
119-
#pin: Pin = { kind: "unresolved" };
120-
/** Built for one pinned credential. Every mutation that moves the
121-
* pin discards it, so a command that mutates and then reaches for
122-
* ctx.api cannot be handed storage for the credential it used to be
123-
* acting as. */
119+
#actingAs: ActingAs = { kind: "unresolved" };
120+
/** Built for the credential the process acts as. Every mutation that
121+
* changes that discards it, so a command that mutates and then
122+
* reaches for ctx.api cannot be handed storage for the credential it
123+
* used to be acting as. */
124124
#activeStorage: TokenStorage | undefined;
125125
#refreshLock: Promise<unknown> = Promise.resolve();
126126

@@ -138,20 +138,20 @@ export class FileCredentialManager implements CredentialManager {
138138
}
139139

140140
async activeCredential(): Promise<ActiveCredential | null> {
141-
const pin = await this.#resolvePin();
141+
const actingAs = await this.#resolveActingAs();
142142

143-
if (pin.kind === "environment") {
143+
if (actingAs.kind === "environment") {
144144
return environmentCredential(this.#requireEnvironmentToken());
145145
}
146146
const state = await readCredentialState(this.#filePath);
147-
if (pin.kind === "none") {
147+
if (actingAs.kind === "none") {
148148
if (state.sessions.length > 0) {
149149
throw credentialsRequiredError("sessions-held-none-selected");
150150
}
151151
return null;
152152
}
153153
const record = state.sessions.find(
154-
(session) => session.workspaceId === pin.workspaceId,
154+
(session) => session.workspaceId === actingAs.workspaceId,
155155
);
156156
if (record === undefined) {
157157
throw credentialsRequiredError("session-ended");
@@ -204,7 +204,7 @@ export class FileCredentialManager implements CredentialManager {
204204
});
205205

206206
if (!environmentInForce) {
207-
this.#repin({ kind: "session", workspaceId });
207+
this.#actAs({ kind: "session", workspaceId });
208208
}
209209

210210
const name = await this.#lookUpWorkspaceName(credential, workspaceId);
@@ -238,7 +238,7 @@ export class FileCredentialManager implements CredentialManager {
238238
return { state: next, result: toSession(record) };
239239
});
240240
if (!environmentInForce) {
241-
this.#repin({ kind: "session", workspaceId });
241+
this.#actAs({ kind: "session", workspaceId });
242242
}
243243
return selected;
244244
}
@@ -254,8 +254,11 @@ export class FileCredentialManager implements CredentialManager {
254254
: { result: undefined },
255255
);
256256

257-
if (this.#pin.kind === "session" && this.#pin.workspaceId === workspaceId) {
258-
this.#repin({ kind: "none" });
257+
if (
258+
this.#actingAs.kind === "session" &&
259+
this.#actingAs.workspaceId === workspaceId
260+
) {
261+
this.#actAs({ kind: "none" });
259262
}
260263
}
261264

@@ -270,7 +273,7 @@ export class FileCredentialManager implements CredentialManager {
270273
await this.#reapLegacyContextFile();
271274
await this.#reapOrphanedWrites();
272275
if (!environmentInForce) {
273-
this.#repin({ kind: "none" });
276+
this.#actAs({ kind: "none" });
274277
}
275278
}
276279

@@ -294,11 +297,12 @@ export class FileCredentialManager implements CredentialManager {
294297
return readActiveAccessToken(storage, this.#refreshCredential, options);
295298
}
296299

297-
/** §11.2: which storage is chosen once, when the pin resolves. Each
300+
/** §11.2: which storage is chosen once, when the acting-as decision
301+
* resolves. Each
298302
* has exactly one source of truth — the file, or process memory. */
299303
#buildActiveStorage(): TokenStorage {
300-
const pin = this.#pin;
301-
if (pin.kind === "environment") {
304+
const actingAs = this.#actingAs;
305+
if (actingAs.kind === "environment") {
302306
return memoryBackedStorage(
303307
{
304308
token: this.#requireEnvironmentToken(),
@@ -308,8 +312,8 @@ export class FileCredentialManager implements CredentialManager {
308312
(fn) => this.#withRefreshLock(fn),
309313
);
310314
}
311-
if (pin.kind === "session") {
312-
return this.#fileBackedStorage(pin.workspaceId);
315+
if (actingAs.kind === "session") {
316+
return this.#fileBackedStorage(actingAs.workspaceId);
313317
}
314318
throw new Error(
315319
"@prisma/cli: activeCredentialStorage() is only valid once activeCredential() has returned non-null",
@@ -433,33 +437,30 @@ export class FileCredentialManager implements CredentialManager {
433437
return run;
434438
}
435439

436-
async #resolvePin(): Promise<ResolvedPin> {
437-
const pinned = this.#pin;
438-
if (pinned.kind !== "unresolved") return pinned;
440+
async #resolveActingAs(): Promise<ResolvedActingAs> {
441+
const decided = this.#actingAs;
442+
if (decided.kind !== "unresolved") return decided;
439443

440444
if (this.#environmentToken() !== undefined) {
441-
this.#debug("pinned to the environment credential");
442-
return this.#pinTo({ kind: "environment" });
445+
this.#debug("acting as the environment credential");
446+
this.#actingAs = { kind: "environment" };
447+
return { kind: "environment" };
443448
}
444449
const state = await readCredentialState(this.#filePath);
445450
const selected = resolvedMarker(state);
446-
this.#debug(`pinned to session ${selected ?? "(none)"}`);
447-
return this.#pinTo(
451+
this.#debug(`acting as session ${selected ?? "(none)"}`);
452+
const resolved: ResolvedActingAs =
448453
selected === null
449454
? { kind: "none" }
450-
: { kind: "session", workspaceId: selected },
451-
);
452-
}
453-
454-
#pinTo(pin: ResolvedPin): ResolvedPin {
455-
this.#pin = pin;
456-
return pin;
455+
: { kind: "session", workspaceId: selected };
456+
this.#actingAs = resolved;
457+
return resolved;
457458
}
458459

459-
/** Moves the pin after a mutation, discarding storage built for the
460-
* credential this process was acting as before. */
461-
#repin(pin: ResolvedPin): void {
462-
this.#pin = pin;
460+
/** Changes which credential the process acts as after a mutation,
461+
* discarding storage built for the previous one. */
462+
#actAs(next: ResolvedActingAs): void {
463+
this.#actingAs = next;
463464
this.#activeStorage = undefined;
464465
}
465466

packages/cli/tests/credential-manager-processes.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* The credential manager across real processes on a real filesystem:
33
* the short lock prevents lost updates, a crashed holder's lock is
4-
* taken over, and a new process picks up the marker this one pinned
4+
* taken over, and a new process picks up the marker this one moved
55
* away from.
66
*/
77
import { spawn } from "node:child_process";
@@ -291,7 +291,7 @@ describe("across processes", () => {
291291
expect((await readCredentialState(stateFilePath)).sessions).toHaveLength(2);
292292
}, 30_000);
293293

294-
it("gives a new process the marker this process pinned away from", async () => {
294+
it("gives a new process the marker this process moved away from", async () => {
295295
await runWorker("create", WORKSPACE_A, mintToken(WORKSPACE_A), "refresh-a");
296296
await runWorker("create", WORKSPACE_B, mintToken(WORKSPACE_B), "refresh-b");
297297

packages/cli/tests/credential-manager.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* The credential manager over its state file: the file format and its
3-
* atomicity, process pinning, idempotent removal, what an environment
3+
* atomicity, the acting-as decision, idempotent removal, what an environment
44
* credential can and cannot reach, and the two token storages.
55
*/
66
import nodeFs from "node:fs";
@@ -453,14 +453,14 @@ describe("the state file", () => {
453453
});
454454
});
455455

456-
describe("process pinning", () => {
457-
it("pins the credential at the first read and keeps it when another process moves the selection", async () => {
456+
describe("which credential the process acts as", () => {
457+
it("decides at the first read and keeps acting as that credential when another process moves the selection", async () => {
458458
await seedTwoSessions();
459459
const manager = makeManager();
460460
await makeManager().selectSession(WORKSPACE_A);
461461

462-
const pinned = await manager.activeCredential();
463-
expect(pinned?.workspaceId).toBe(WORKSPACE_A);
462+
const first = await manager.activeCredential();
463+
expect(first?.workspaceId).toBe(WORKSPACE_A);
464464

465465
await makeManager().selectSession(WORKSPACE_B);
466466

@@ -470,7 +470,7 @@ describe("process pinning", () => {
470470
);
471471
});
472472

473-
it("moves the pin on this process's own mutations", async () => {
473+
it("acts as the new credential after this process's own mutations", async () => {
474474
await seedTwoSessions();
475475
const manager = makeManager();
476476
expect((await manager.activeCredential())?.workspaceId).toBe(WORKSPACE_B);
@@ -503,7 +503,7 @@ describe("process pinning", () => {
503503
);
504504
});
505505

506-
it("fails with the session-ended error when another process ends the pinned session", async () => {
506+
it("fails with the session-ended error when another process ends the session this one acts as", async () => {
507507
await seedTwoSessions();
508508
const manager = makeManager();
509509
await manager.activeCredential();
@@ -578,7 +578,7 @@ describe("mutations while an environment credential is in force", () => {
578578
/** Design §11.7 and §11.10 test 8: the refusals are gone. Selecting or
579579
* ending a stored session changes stored state; this process keeps
580580
* authenticating as the environment credential either way. */
581-
it("lets every mutation through and leaves the pin on the environment credential", async () => {
581+
it("lets every mutation through and keeps acting as the environment credential", async () => {
582582
await seedTwoSessions();
583583
const manager = makeManager({
584584
env: { PRISMA_SERVICE_TOKEN: mintToken(WORKSPACE_C) },
@@ -609,7 +609,7 @@ describe("mutations while an environment credential is in force", () => {
609609
expect((await manager.activeCredential())?.workspaceId).toBe(WORKSPACE_C);
610610
});
611611

612-
it("moves the stored selection without moving the pin", async () => {
612+
it("moves the stored selection without changing what this process acts as", async () => {
613613
await seedTwoSessions();
614614
const manager = makeManager({
615615
env: { PRISMA_SERVICE_TOKEN: mintToken(WORKSPACE_C) },
@@ -971,7 +971,7 @@ describe("the file-backed TokenStorage", () => {
971971
expect(state.currentWorkspaceId).toBeNull();
972972
});
973973

974-
it("clearTokens removes only the pinned record", async () => {
974+
it("clearTokens removes only the record this process acts as", async () => {
975975
const manager = makeManager();
976976
await manager.createSession(credentialFor(WORKSPACE_A), WORKSPACE_A);
977977
await makeManager().createSession(credentialFor(WORKSPACE_B), WORKSPACE_B);

0 commit comments

Comments
 (0)