@prisma/cli 8.0.0-rc.2-dev.49 → 8.0.0-rc.2-dev.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +102 -30
  2. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import process$1 from "node:process";
4
- import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, telemetryCommandGroup } from "@prisma/cli-engine";
4
+ import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, claimedIdentity, createCli, credentialWorkspaceId, credentialWorkspaceMismatchError, credentialsRequiredError, defineCommand, defineCommandFamily, defineSessionCommand, emptyServiceTokenError, flag, loadConfig, noSessionForWorkspaceError, positional, readActiveAccessToken, telemetryCommandGroup } from "@prisma/cli-engine";
5
5
  import { createComposerFamily } from "@prisma/composer/family";
6
6
  import { ormCommandFamily } from "@prisma/orm-toolchain/cli";
7
7
  import { CliStructuredError, notOk, ok } from "@prisma/cli-engine/protocol";
@@ -848,9 +848,9 @@ function sameWorkspaceId(left, right) {
848
848
  }
849
849
  //#endregion
850
850
  //#region src/auth/token-storage.ts
851
- const REFRESH_LOCK_RETRY_MS = 100;
852
- const REFRESH_LOCK_STALE_MS = 3e4;
853
- const REFRESH_LOCK_WAIT_TIMEOUT_MS = 25e3;
851
+ const REFRESH_LOCK_RETRY_MS$1 = 100;
852
+ const REFRESH_LOCK_STALE_MS$1 = 3e4;
853
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS$1 = 25e3;
854
854
  const EMPTY_AUTH_CONTEXT = {
855
855
  activeWorkspaceId: null,
856
856
  workspaces: {}
@@ -1097,8 +1097,8 @@ var FileTokenStorage = class {
1097
1097
  async acquireRefreshLock() {
1098
1098
  const lockId = randomUUID();
1099
1099
  const startedAt = Date.now();
1100
- const retryMs = this.options.lockRetryMs ?? REFRESH_LOCK_RETRY_MS;
1101
- const waitTimeoutMs = this.options.lockWaitTimeoutMs ?? REFRESH_LOCK_WAIT_TIMEOUT_MS;
1100
+ const retryMs = this.options.lockRetryMs ?? REFRESH_LOCK_RETRY_MS$1;
1101
+ const waitTimeoutMs = this.options.lockWaitTimeoutMs ?? REFRESH_LOCK_WAIT_TIMEOUT_MS$1;
1102
1102
  this.signal?.throwIfAborted();
1103
1103
  await fs.mkdir(path.dirname(this.lockFilePath), { recursive: true });
1104
1104
  while (true) {
@@ -1152,7 +1152,7 @@ var FileTokenStorage = class {
1152
1152
  const stats = await fs.stat(this.lockFilePath).catch(() => null);
1153
1153
  this.signal?.throwIfAborted();
1154
1154
  if (!stats) return null;
1155
- const staleMs = this.options.lockStaleMs ?? REFRESH_LOCK_STALE_MS;
1155
+ const staleMs = this.options.lockStaleMs ?? REFRESH_LOCK_STALE_MS$1;
1156
1156
  return Date.now() - stats.mtimeMs > staleMs ? lockId : null;
1157
1157
  }
1158
1158
  async releaseRefreshLock(lockId) {
@@ -14315,6 +14315,19 @@ const FILE_MODE = 384;
14315
14315
  const LOCK_STALE_MS = 5e3;
14316
14316
  const LOCK_RETRY_MS = 10;
14317
14317
  const LOCK_WAIT_TIMEOUT_MS = 1e4;
14318
+ const REFRESH_LOCK_STALE_MS = 3e4;
14319
+ const REFRESH_LOCK_RETRY_MS = 100;
14320
+ const REFRESH_LOCK_WAIT_TIMEOUT_MS = 3e4;
14321
+ const STATE_LOCK_TIMINGS = {
14322
+ staleMs: LOCK_STALE_MS,
14323
+ retryMs: LOCK_RETRY_MS,
14324
+ waitTimeoutMs: LOCK_WAIT_TIMEOUT_MS
14325
+ };
14326
+ const REFRESH_LOCK_TIMINGS = {
14327
+ staleMs: REFRESH_LOCK_STALE_MS,
14328
+ retryMs: REFRESH_LOCK_RETRY_MS,
14329
+ waitTimeoutMs: REFRESH_LOCK_WAIT_TIMEOUT_MS
14330
+ };
14318
14331
  const EMPTY_STATE = {
14319
14332
  version: 1,
14320
14333
  sessions: [],
@@ -14417,9 +14430,9 @@ async function writeCredentialState(filePath, state) {
14417
14430
  await fs.chmod(filePath, FILE_MODE).catch(() => {});
14418
14431
  }
14419
14432
  var StateLockTimeoutError = class extends CliStructuredError {
14420
- constructor(lockPath) {
14433
+ constructor(lockPath, waitTimeoutMs) {
14421
14434
  super("CLI.CREDENTIALS_LOCKED", "Another prisma process is still updating your credentials.", {
14422
- why: `The credentials lock at ${lockPath} was held for longer than ${LOCK_WAIT_TIMEOUT_MS}ms.`,
14435
+ why: `The credentials lock at ${lockPath} was held for longer than ${waitTimeoutMs}ms.`,
14423
14436
  nextActions: [{
14424
14437
  kind: "user-choice",
14425
14438
  label: "Wait for the other command to finish, then try again."
@@ -14435,8 +14448,20 @@ var StateLockTimeoutError = class extends CliStructuredError {
14435
14448
  * small fixed staleness threshold.
14436
14449
  */
14437
14450
  async function withStateLock(filePath, debug, run) {
14438
- const lockPath = `${filePath}.lock`;
14439
- const lockId = await acquireStateLock(lockPath, debug);
14451
+ return withFileLock(`${filePath}.lock`, debug, STATE_LOCK_TIMINGS, run);
14452
+ }
14453
+ /**
14454
+ * The cross-process lock the delegated refresh holds for its whole
14455
+ * read → exchange → write sequence, so two processes never spend the
14456
+ * same refresh token. Distinct from the state lock: it IS held across
14457
+ * network I/O, so its staleness and wait budgets are larger, and it
14458
+ * uses its own lock path so short mutations are not queued behind it.
14459
+ */
14460
+ async function withRefreshFileLock(filePath, debug, run) {
14461
+ return withFileLock(`${filePath}.refresh-lock`, debug, REFRESH_LOCK_TIMINGS, run);
14462
+ }
14463
+ async function withFileLock(lockPath, debug, timings, run) {
14464
+ const lockId = await acquireStateLock(lockPath, debug, timings);
14440
14465
  debug(`lock acquired ${lockPath}`);
14441
14466
  try {
14442
14467
  return await run();
@@ -14445,15 +14470,15 @@ async function withStateLock(filePath, debug, run) {
14445
14470
  debug(`lock released ${lockPath}`);
14446
14471
  }
14447
14472
  }
14448
- async function acquireStateLock(lockPath, debug) {
14473
+ async function acquireStateLock(lockPath, debug, timings) {
14449
14474
  const lockId = randomUUID();
14450
14475
  const startedAt = Date.now();
14451
14476
  await fs.mkdir(path.dirname(lockPath), { recursive: true });
14452
14477
  while (true) {
14453
14478
  if (await tryCreateStateLock(lockPath, lockId)) return lockId;
14454
- const tookOver = await takeOverStaleStateLock(lockPath, debug);
14455
- if (Date.now() - startedAt >= LOCK_WAIT_TIMEOUT_MS) throw new StateLockTimeoutError(lockPath);
14456
- if (!tookOver) await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
14479
+ const tookOver = await takeOverStaleStateLock(lockPath, debug, timings);
14480
+ if (Date.now() - startedAt >= timings.waitTimeoutMs) throw new StateLockTimeoutError(lockPath, timings.waitTimeoutMs);
14481
+ if (!tookOver) await new Promise((resolve) => setTimeout(resolve, timings.retryMs));
14457
14482
  }
14458
14483
  }
14459
14484
  async function tryCreateStateLock(lockPath, lockId) {
@@ -14479,10 +14504,10 @@ async function tryCreateStateLock(lockPath, lockId) {
14479
14504
  * then run their read-modify-write at once and one update is lost,
14480
14505
  * which is the very thing the lock exists to prevent.
14481
14506
  */
14482
- async function takeOverStaleStateLock(lockPath, debug) {
14507
+ async function takeOverStaleStateLock(lockPath, debug, timings) {
14483
14508
  const stale = await fs.stat(lockPath).catch(() => null);
14484
14509
  if (stale === null) return true;
14485
- if (Date.now() - stale.mtimeMs <= LOCK_STALE_MS) return false;
14510
+ if (Date.now() - stale.mtimeMs <= timings.staleMs) return false;
14486
14511
  const takenPath = `${lockPath}.${randomUUID()}.stale`;
14487
14512
  try {
14488
14513
  await fs.rename(lockPath, takenPath);
@@ -14520,12 +14545,16 @@ function memoryBackedStorage(credential, withRefreshLock) {
14520
14545
  let tokens = {
14521
14546
  workspaceId: credentialWorkspaceId(credential.token) ?? NO_WORKSPACE_CLAIMED,
14522
14547
  accessToken: credential.token,
14523
- refreshToken: credential.refreshToken
14548
+ refreshToken: credential.refreshToken,
14549
+ expiresAt: claimedExpiresAt(credential.token) ?? credential.expiresAt
14524
14550
  };
14525
14551
  return {
14526
14552
  getTokens: async () => tokens,
14527
- setTokens: async (rotated) => {
14528
- tokens = rotated;
14553
+ setTokens: async (rotated, expiresAt) => {
14554
+ tokens = {
14555
+ ...rotated,
14556
+ expiresAt: claimedExpiresAt(rotated.accessToken) ?? expiresAt ?? tokens?.expiresAt
14557
+ };
14529
14558
  },
14530
14559
  clearTokens: async () => {
14531
14560
  tokens = null;
@@ -14544,6 +14573,7 @@ var FileCredentialManager = class {
14544
14573
  #filePath;
14545
14574
  #debug;
14546
14575
  #fetchWorkspaceName;
14576
+ #refreshCredential;
14547
14577
  #pin = { kind: "unresolved" };
14548
14578
  /** Built for one pinned credential. Every mutation that moves the
14549
14579
  * pin discards it, so a command that mutates and then reaches for
@@ -14556,6 +14586,7 @@ var FileCredentialManager = class {
14556
14586
  this.#filePath = resolveStateFilePath(options.env).filePath;
14557
14587
  this.#debug = makeDebugLog(options.env, options.debugWrite);
14558
14588
  this.#fetchWorkspaceName = options.fetchWorkspaceName;
14589
+ this.#refreshCredential = options.refreshCredential;
14559
14590
  this.#debug(`state file ${this.#filePath}`);
14560
14591
  }
14561
14592
  get stateFilePath() {
@@ -14666,14 +14697,13 @@ var FileCredentialManager = class {
14666
14697
  this.#activeStorage ??= this.#buildActiveStorage();
14667
14698
  return this.#activeStorage;
14668
14699
  }
14669
- /** The spawn path's read: the active credential's access token,
14700
+ /** The delegated path's read: the active credential's access token,
14670
14701
  * fresh on every call, never the refresh token. Null when there is
14671
14702
  * no active credential to read — storage exists only once
14672
14703
  * activeCredential() has returned non-null. */
14673
- async activeAccessToken() {
14704
+ async activeAccessToken(options) {
14674
14705
  if (await this.activeCredential() === null) return null;
14675
- const tokens = await (await this.activeCredentialStorage()).getTokens();
14676
- return tokens === null ? null : tokens.accessToken;
14706
+ return readActiveAccessToken(await this.activeCredentialStorage(), this.#refreshCredential, options);
14677
14707
  }
14678
14708
  /** §11.2: which storage is chosen once, when the pin resolves. Each
14679
14709
  * has exactly one source of truth — the file, or process memory. */
@@ -14701,10 +14731,11 @@ var FileCredentialManager = class {
14701
14731
  return {
14702
14732
  workspaceId,
14703
14733
  accessToken: record.token,
14704
- ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken }
14734
+ ...record.refreshToken === void 0 ? {} : { refreshToken: record.refreshToken },
14735
+ ...record.expiresAt === void 0 ? {} : { expiresAt: new Date(record.expiresAt) }
14705
14736
  };
14706
14737
  },
14707
- setTokens: async (tokens) => {
14738
+ setTokens: async (tokens, expiresAt) => {
14708
14739
  this.#debug(`rotation write for session ${workspaceId}`);
14709
14740
  const claimed = credentialWorkspaceId(tokens.accessToken);
14710
14741
  if (claimed !== void 0 && claimed !== workspaceId) throw credentialWorkspaceMismatchError(workspaceId);
@@ -14716,7 +14747,7 @@ var FileCredentialManager = class {
14716
14747
  ...record.name === void 0 ? {} : { name: record.name },
14717
14748
  token: tokens.accessToken,
14718
14749
  ...tokens.refreshToken === void 0 ? {} : { refreshToken: tokens.refreshToken },
14719
- ...expiresAtSlice(tokens.accessToken, void 0)
14750
+ ...expiresAtSlice(tokens.accessToken, expiresAt ?? (record.expiresAt === void 0 ? void 0 : new Date(record.expiresAt)))
14720
14751
  };
14721
14752
  return {
14722
14753
  state: {
@@ -14745,7 +14776,7 @@ var FileCredentialManager = class {
14745
14776
  };
14746
14777
  });
14747
14778
  },
14748
- withRefreshLock: (fn) => this.#withRefreshLock(fn)
14779
+ withRefreshLock: (fn) => this.#withRefreshLock(() => withRefreshFileLock(this.#filePath, this.#debug, fn))
14749
14780
  };
14750
14781
  }
14751
14782
  #withRefreshLock(fn) {
@@ -14881,6 +14912,45 @@ function environmentCredential(token) {
14881
14912
  };
14882
14913
  }
14883
14914
  //#endregion
14915
+ //#region src/auth/refresh.ts
14916
+ const TRAILING_SLASH = /\/$/;
14917
+ const CREDENTIAL_REFRESH_TIMEOUT_MS = 1e4;
14918
+ /** The dumb HTTP adapter behind the engine's delegated-credential policy. */
14919
+ function makeCredentialRefresher(authBaseUrl) {
14920
+ const endpoint = `${authBaseUrl.replace(TRAILING_SLASH, "")}/token`;
14921
+ return async ({ refreshToken, signal }) => {
14922
+ signal.throwIfAborted();
14923
+ const refreshSignal = AbortSignal.any([signal, AbortSignal.timeout(CREDENTIAL_REFRESH_TIMEOUT_MS)]);
14924
+ const response = await fetch(endpoint, {
14925
+ method: "POST",
14926
+ headers: { "content-type": "application/x-www-form-urlencoded" },
14927
+ body: new URLSearchParams({
14928
+ grant_type: "refresh_token",
14929
+ refresh_token: refreshToken,
14930
+ client_id: CLIENT_ID
14931
+ }),
14932
+ signal: refreshSignal
14933
+ });
14934
+ const body = await readBody(response);
14935
+ if (response.status >= 400 && response.status < 500 && body?.error === "invalid_grant") return { kind: "invalid" };
14936
+ if (!response.ok || typeof body?.access_token !== "string" || typeof body.refresh_token !== "string" || typeof body.expires_in !== "number" || !Number.isFinite(body.expires_in) || body.expires_in < 0) throw new Error(`OAuth token refresh failed (status ${String(response.status)})`);
14937
+ return {
14938
+ kind: "success",
14939
+ accessToken: body.access_token,
14940
+ refreshToken: body.refresh_token,
14941
+ expiresAt: new Date(Date.now() + body.expires_in * 1e3)
14942
+ };
14943
+ };
14944
+ }
14945
+ async function readBody(response) {
14946
+ try {
14947
+ const body = await response.json();
14948
+ return typeof body === "object" && body !== null ? body : null;
14949
+ } catch {
14950
+ return null;
14951
+ }
14952
+ }
14953
+ //#endregion
14884
14954
  //#region src/auth/workspace-name.ts
14885
14955
  /** The manager's injected name lookup: a static-token client over the
14886
14956
  * credential just minted. The manager constructs no API client and
@@ -15092,6 +15162,7 @@ async function assembleRuntime(proc) {
15092
15162
  };
15093
15163
  warnOnDeprecatedStateFileEnvVar(proc);
15094
15164
  const apiBaseUrl = getApiBaseUrl(proc.env);
15165
+ const authBaseUrl = getAuthBaseUrl(proc.env);
15095
15166
  return {
15096
15167
  stdout: { write: (text) => {
15097
15168
  proc.stdout.write(text);
@@ -15118,13 +15189,14 @@ async function assembleRuntime(proc) {
15118
15189
  loadConfig: (configPath) => loadConfig(proc.cwd(), configPath),
15119
15190
  credentialManager: new FileCredentialManager({
15120
15191
  env: proc.env,
15121
- fetchWorkspaceName: fetchWorkspaceName(apiBaseUrl)
15192
+ fetchWorkspaceName: fetchWorkspaceName(apiBaseUrl),
15193
+ refreshCredential: makeCredentialRefresher(authBaseUrl)
15122
15194
  }),
15123
15195
  managementApiClientConfig: {
15124
15196
  clientId: CLIENT_ID,
15125
15197
  redirectUri: DEFAULT_REDIRECT_URI,
15126
15198
  apiBaseUrl,
15127
- authBaseUrl: getAuthBaseUrl(proc.env)
15199
+ authBaseUrl
15128
15200
  },
15129
15201
  spawn: spawnChild,
15130
15202
  /** The engine has already decided and composed; the bin only forks
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "8.0.0-rc.2-dev.49",
3
+ "version": "8.0.0-rc.2-dev.50",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -51,9 +51,9 @@
51
51
  "open": "^11.0.0"
52
52
  },
53
53
  "devDependencies": {
54
- "@repo/cli-conformance": "8.0.0-rc.2-dev.49",
55
- "@repo/cli-telemetry": "8.0.0-rc.2-dev.49",
56
- "@repo/tsconfig": "8.0.0-rc.2-dev.49",
54
+ "@repo/cli-conformance": "8.0.0-rc.2-dev.50",
55
+ "@repo/cli-telemetry": "8.0.0-rc.2-dev.50",
56
+ "@repo/tsconfig": "8.0.0-rc.2-dev.50",
57
57
  "@types/node": "^22.19.19",
58
58
  "tsdown": "^0.21.10",
59
59
  "tsx": "^4.22.4",