diff --git a/packages/cli/src/auth/credential-manager.ts b/packages/cli/src/auth/credential-manager.ts index 647d09e8..19469272 100644 --- a/packages/cli/src/auth/credential-manager.ts +++ b/packages/cli/src/auth/credential-manager.ts @@ -58,15 +58,15 @@ export interface FileCredentialManagerOptions { } /** Which credential this process acts as, decided at the first - * activeCredential() read. The decision is what is pinned; the + * activeCredential() read. The decision is what is held; the * material behind it is re-read on every call. */ -type Pin = +type ActingAs = | { readonly kind: "unresolved" } | { readonly kind: "environment" } | { readonly kind: "session"; readonly workspaceId: string } | { readonly kind: "none" }; -type ResolvedPin = Exclude; +type ResolvedActingAs = Exclude; /** * The memory-backed storage, for a credential with no home record: a @@ -106,7 +106,7 @@ function memoryBackedStorage( /** * The credential manager over one state file. Sessions are keyed by - * workspace id; which credential this process acts as is pinned once; + * workspace id; which credential this process acts as is decided once; * every mutation takes a short file lock, re-reads, applies its slice, * and writes atomically. Reads never write and take no lock. */ @@ -116,11 +116,11 @@ export class FileCredentialManager implements CredentialManager { readonly #debug: DebugLog; readonly #fetchWorkspaceName: FetchWorkspaceName | undefined; readonly #refreshCredential: CredentialRefresher | undefined; - #pin: Pin = { kind: "unresolved" }; - /** Built for one pinned credential. Every mutation that moves the - * pin discards it, so a command that mutates and then reaches for - * ctx.api cannot be handed storage for the credential it used to be - * acting as. */ + #actingAs: ActingAs = { kind: "unresolved" }; + /** Built for the credential the process acts as. Every mutation that + * changes that discards it, so a command that mutates and then + * reaches for ctx.api cannot be handed storage for the credential it + * used to be acting as. */ #activeStorage: TokenStorage | undefined; #refreshLock: Promise = Promise.resolve(); @@ -138,20 +138,20 @@ export class FileCredentialManager implements CredentialManager { } async activeCredential(): Promise { - const pin = await this.#resolvePin(); + const actingAs = await this.#resolveActingAs(); - if (pin.kind === "environment") { + if (actingAs.kind === "environment") { return environmentCredential(this.#requireEnvironmentToken()); } const state = await readCredentialState(this.#filePath); - if (pin.kind === "none") { + if (actingAs.kind === "none") { if (state.sessions.length > 0) { throw credentialsRequiredError("sessions-held-none-selected"); } return null; } const record = state.sessions.find( - (session) => session.workspaceId === pin.workspaceId, + (session) => session.workspaceId === actingAs.workspaceId, ); if (record === undefined) { throw credentialsRequiredError("session-ended"); @@ -204,7 +204,7 @@ export class FileCredentialManager implements CredentialManager { }); if (!environmentInForce) { - this.#repin({ kind: "session", workspaceId }); + this.#actAs({ kind: "session", workspaceId }); } const name = await this.#lookUpWorkspaceName(credential, workspaceId); @@ -238,7 +238,7 @@ export class FileCredentialManager implements CredentialManager { return { state: next, result: toSession(record) }; }); if (!environmentInForce) { - this.#repin({ kind: "session", workspaceId }); + this.#actAs({ kind: "session", workspaceId }); } return selected; } @@ -254,8 +254,11 @@ export class FileCredentialManager implements CredentialManager { : { result: undefined }, ); - if (this.#pin.kind === "session" && this.#pin.workspaceId === workspaceId) { - this.#repin({ kind: "none" }); + if ( + this.#actingAs.kind === "session" && + this.#actingAs.workspaceId === workspaceId + ) { + this.#actAs({ kind: "none" }); } } @@ -270,7 +273,7 @@ export class FileCredentialManager implements CredentialManager { await this.#reapLegacyContextFile(); await this.#reapOrphanedWrites(); if (!environmentInForce) { - this.#repin({ kind: "none" }); + this.#actAs({ kind: "none" }); } } @@ -294,11 +297,12 @@ export class FileCredentialManager implements CredentialManager { return readActiveAccessToken(storage, this.#refreshCredential, options); } - /** §11.2: which storage is chosen once, when the pin resolves. Each + /** §11.2: which storage is chosen once, when the acting-as decision + * resolves. Each * has exactly one source of truth — the file, or process memory. */ #buildActiveStorage(): TokenStorage { - const pin = this.#pin; - if (pin.kind === "environment") { + const actingAs = this.#actingAs; + if (actingAs.kind === "environment") { return memoryBackedStorage( { token: this.#requireEnvironmentToken(), @@ -308,8 +312,8 @@ export class FileCredentialManager implements CredentialManager { (fn) => this.#withRefreshLock(fn), ); } - if (pin.kind === "session") { - return this.#fileBackedStorage(pin.workspaceId); + if (actingAs.kind === "session") { + return this.#fileBackedStorage(actingAs.workspaceId); } throw new Error( "@prisma/cli: activeCredentialStorage() is only valid once activeCredential() has returned non-null", @@ -433,33 +437,30 @@ export class FileCredentialManager implements CredentialManager { return run; } - async #resolvePin(): Promise { - const pinned = this.#pin; - if (pinned.kind !== "unresolved") return pinned; + async #resolveActingAs(): Promise { + const decided = this.#actingAs; + if (decided.kind !== "unresolved") return decided; if (this.#environmentToken() !== undefined) { - this.#debug("pinned to the environment credential"); - return this.#pinTo({ kind: "environment" }); + this.#debug("acting as the environment credential"); + this.#actingAs = { kind: "environment" }; + return { kind: "environment" }; } const state = await readCredentialState(this.#filePath); const selected = resolvedMarker(state); - this.#debug(`pinned to session ${selected ?? "(none)"}`); - return this.#pinTo( + this.#debug(`acting as session ${selected ?? "(none)"}`); + const resolved: ResolvedActingAs = selected === null ? { kind: "none" } - : { kind: "session", workspaceId: selected }, - ); - } - - #pinTo(pin: ResolvedPin): ResolvedPin { - this.#pin = pin; - return pin; + : { kind: "session", workspaceId: selected }; + this.#actingAs = resolved; + return resolved; } - /** Moves the pin after a mutation, discarding storage built for the - * credential this process was acting as before. */ - #repin(pin: ResolvedPin): void { - this.#pin = pin; + /** Changes which credential the process acts as after a mutation, + * discarding storage built for the previous one. */ + #actAs(next: ResolvedActingAs): void { + this.#actingAs = next; this.#activeStorage = undefined; } diff --git a/packages/cli/tests/credential-manager-processes.test.ts b/packages/cli/tests/credential-manager-processes.test.ts index 61ca307a..77c7d3de 100644 --- a/packages/cli/tests/credential-manager-processes.test.ts +++ b/packages/cli/tests/credential-manager-processes.test.ts @@ -1,7 +1,7 @@ /** * The credential manager across real processes on a real filesystem: * the short lock prevents lost updates, a crashed holder's lock is - * taken over, and a new process picks up the marker this one pinned + * taken over, and a new process picks up the marker this one moved * away from. */ import { spawn } from "node:child_process"; @@ -291,7 +291,7 @@ describe("across processes", () => { expect((await readCredentialState(stateFilePath)).sessions).toHaveLength(2); }, 30_000); - it("gives a new process the marker this process pinned away from", async () => { + it("gives a new process the marker this process moved away from", async () => { await runWorker("create", WORKSPACE_A, mintToken(WORKSPACE_A), "refresh-a"); await runWorker("create", WORKSPACE_B, mintToken(WORKSPACE_B), "refresh-b"); diff --git a/packages/cli/tests/credential-manager.test.ts b/packages/cli/tests/credential-manager.test.ts index 13c7f292..1b3abfcf 100644 --- a/packages/cli/tests/credential-manager.test.ts +++ b/packages/cli/tests/credential-manager.test.ts @@ -1,6 +1,6 @@ /** * The credential manager over its state file: the file format and its - * atomicity, process pinning, idempotent removal, what an environment + * atomicity, the acting-as decision, idempotent removal, what an environment * credential can and cannot reach, and the two token storages. */ import nodeFs from "node:fs"; @@ -453,14 +453,14 @@ describe("the state file", () => { }); }); -describe("process pinning", () => { - it("pins the credential at the first read and keeps it when another process moves the selection", async () => { +describe("which credential the process acts as", () => { + it("decides at the first read and keeps acting as that credential when another process moves the selection", async () => { await seedTwoSessions(); const manager = makeManager(); await makeManager().selectSession(WORKSPACE_A); - const pinned = await manager.activeCredential(); - expect(pinned?.workspaceId).toBe(WORKSPACE_A); + const first = await manager.activeCredential(); + expect(first?.workspaceId).toBe(WORKSPACE_A); await makeManager().selectSession(WORKSPACE_B); @@ -470,7 +470,7 @@ describe("process pinning", () => { ); }); - it("moves the pin on this process's own mutations", async () => { + it("acts as the new credential after this process's own mutations", async () => { await seedTwoSessions(); const manager = makeManager(); expect((await manager.activeCredential())?.workspaceId).toBe(WORKSPACE_B); @@ -503,7 +503,7 @@ describe("process pinning", () => { ); }); - it("fails with the session-ended error when another process ends the pinned session", async () => { + it("fails with the session-ended error when another process ends the session this one acts as", async () => { await seedTwoSessions(); const manager = makeManager(); await manager.activeCredential(); @@ -578,7 +578,7 @@ describe("mutations while an environment credential is in force", () => { /** Design §11.7 and §11.10 test 8: the refusals are gone. Selecting or * ending a stored session changes stored state; this process keeps * authenticating as the environment credential either way. */ - it("lets every mutation through and leaves the pin on the environment credential", async () => { + it("lets every mutation through and keeps acting as the environment credential", async () => { await seedTwoSessions(); const manager = makeManager({ env: { PRISMA_SERVICE_TOKEN: mintToken(WORKSPACE_C) }, @@ -609,7 +609,7 @@ describe("mutations while an environment credential is in force", () => { expect((await manager.activeCredential())?.workspaceId).toBe(WORKSPACE_C); }); - it("moves the stored selection without moving the pin", async () => { + it("moves the stored selection without changing what this process acts as", async () => { await seedTwoSessions(); const manager = makeManager({ env: { PRISMA_SERVICE_TOKEN: mintToken(WORKSPACE_C) }, @@ -971,7 +971,7 @@ describe("the file-backed TokenStorage", () => { expect(state.currentWorkspaceId).toBeNull(); }); - it("clearTokens removes only the pinned record", async () => { + it("clearTokens removes only the record this process acts as", async () => { const manager = makeManager(); await manager.createSession(credentialFor(WORKSPACE_A), WORKSPACE_A); await makeManager().createSession(credentialFor(WORKSPACE_B), WORKSPACE_B);