@prisma/cli 3.0.0-dev.97.1 → 8.0.0-rc.1

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 (99) hide show
  1. package/README.md +3 -14
  2. package/dist/adapters/local-state.js +14 -3
  3. package/dist/{lib/auth → auth}/client.js +11 -3
  4. package/dist/auth/errors.js +50 -0
  5. package/dist/{lib/auth → auth}/guard.js +7 -6
  6. package/dist/{lib/auth → auth}/login.js +147 -6
  7. package/dist/{lib/auth/auth-ops.js → auth/operations.js} +63 -13
  8. package/dist/auth/recipient.js +42 -0
  9. package/dist/{adapters → auth}/token-storage.js +28 -7
  10. package/dist/auth/workspaces.js +171 -0
  11. package/dist/cli-command.js +14 -0
  12. package/dist/cli-name.js +12 -0
  13. package/dist/cli.js +1 -1
  14. package/dist/cli2.js +25 -8
  15. package/dist/command-arguments.js +12 -0
  16. package/dist/commands/agent/index.js +60 -0
  17. package/dist/commands/app/index.js +11 -6
  18. package/dist/commands/auth/index.js +2 -3
  19. package/dist/commands/bucket/index.js +123 -0
  20. package/dist/commands/build/index.js +29 -0
  21. package/dist/commands/database/index.js +92 -2
  22. package/dist/commands/feedback/index.js +20 -0
  23. package/dist/commands/git/index.js +1 -1
  24. package/dist/commands/init/index.js +33 -0
  25. package/dist/commands/project/index.js +54 -5
  26. package/dist/controllers/agent-setup.js +52 -0
  27. package/dist/controllers/agent.js +228 -0
  28. package/dist/controllers/app-env-api.js +3 -2
  29. package/dist/controllers/app-env-file.js +1 -1
  30. package/dist/controllers/app-env.js +5 -5
  31. package/dist/controllers/app.js +423 -358
  32. package/dist/controllers/auth.js +62 -254
  33. package/dist/controllers/branch.js +5 -16
  34. package/dist/controllers/bucket.js +184 -0
  35. package/dist/controllers/build.js +88 -0
  36. package/dist/controllers/database.js +230 -85
  37. package/dist/controllers/feedback.js +90 -0
  38. package/dist/controllers/init.js +814 -0
  39. package/dist/controllers/project.js +345 -184
  40. package/dist/controllers/select-prompt-port.js +1 -0
  41. package/dist/{shell/errors.js → errors.js} +8 -40
  42. package/dist/lib/agent/cli-command.js +20 -0
  43. package/dist/lib/agent/constants.js +12 -0
  44. package/dist/lib/agent/package-manager.js +99 -0
  45. package/dist/lib/agent/setup-status.js +83 -0
  46. package/dist/lib/app/app-provider.js +168 -95
  47. package/dist/lib/app/branch-database-deploy.js +70 -42
  48. package/dist/lib/app/branch-database.js +39 -20
  49. package/dist/lib/app/build-settings.js +8 -3
  50. package/dist/lib/app/build.js +16 -14
  51. package/dist/lib/app/bun-project.js +1 -1
  52. package/dist/lib/app/compute-config.js +29 -31
  53. package/dist/lib/app/deploy-plan.js +1 -0
  54. package/dist/lib/app/deploy-progress.js +7 -7
  55. package/dist/lib/app/env-config.js +1 -1
  56. package/dist/lib/app/env-file.js +2 -2
  57. package/dist/lib/app/env-vars.js +1 -1
  58. package/dist/lib/app/local-dev.js +1 -1
  59. package/dist/lib/app/production-deploy-gate.js +3 -2
  60. package/dist/lib/bucket/provider.js +139 -0
  61. package/dist/lib/database/provider.js +235 -17
  62. package/dist/lib/diagnostics.js +2 -1
  63. package/dist/lib/feedback.js +15 -0
  64. package/dist/lib/git/local-branch.js +1 -1
  65. package/dist/lib/project/interactive-setup.js +6 -5
  66. package/dist/lib/project/local-pin.js +1 -1
  67. package/dist/lib/project/provider.js +93 -0
  68. package/dist/lib/project/resolution.js +11 -8
  69. package/dist/lib/project/setup.js +9 -6
  70. package/dist/lib/version.js +3 -2
  71. package/dist/lib/workspace-id.js +18 -0
  72. package/dist/payload-B88Qvzxk-CrtTXSOe.js +34 -0
  73. package/dist/presenters/agent.js +74 -0
  74. package/dist/presenters/app.js +33 -7
  75. package/dist/presenters/auth.js +3 -2
  76. package/dist/presenters/bucket.js +174 -0
  77. package/dist/presenters/database.js +193 -5
  78. package/dist/presenters/feedback.js +26 -0
  79. package/dist/presenters/init.js +30 -0
  80. package/dist/presenters/project.js +98 -4
  81. package/dist/shell/cli-command.js +2 -0
  82. package/dist/shell/command-meta.js +303 -4
  83. package/dist/shell/command-runner.js +13 -6
  84. package/dist/shell/help.js +6 -5
  85. package/dist/shell/output.js +65 -3
  86. package/dist/shell/prompt.js +12 -5
  87. package/dist/shell/runtime.js +4 -21
  88. package/dist/state-dir.js +12 -0
  89. package/dist/{shell/update-check.js → update-check.js} +6 -5
  90. package/dist/v8/cli.js +14699 -0
  91. package/dist/v8/sender.js +192 -0
  92. package/package.json +21 -13
  93. package/dist/adapters/mock-api.js +0 -136
  94. package/dist/lib/app/app-interaction.js +0 -5
  95. package/dist/shell/command-arguments.js +0 -6
  96. package/dist/use-cases/auth.js +0 -145
  97. package/dist/use-cases/branch.js +0 -47
  98. package/dist/use-cases/create-cli-gateways.js +0 -68
  99. package/dist/use-cases/project.js +0 -30
package/README.md CHANGED
@@ -76,6 +76,8 @@ The beta package exposes `prisma-cli` so it can coexist with the existing
76
76
  | `project` | List projects, show the resolved project, and manage project environment variables. |
77
77
  | `git` | Connect or disconnect a project from a GitHub repository. |
78
78
  | `branch` | List Prisma branches for the resolved project. |
79
+ | `database` | Create, inspect, and remove Prisma Postgres databases and their connection strings. |
80
+ | `bucket` | Create, list, and delete Tigris object-store buckets and their access keys. |
79
81
  | `app` | Build, run, deploy, inspect, open, stream logs, promote, roll back, and remove apps. |
80
82
 
81
83
  Common examples:
@@ -98,24 +100,11 @@ npx prisma-cli app promote <deployment-id>
98
100
  - Stable command groups, flags, and error codes for scripts and agents.
99
101
  - Environment variable values are not printed back to the terminal.
100
102
 
101
- ### Agent skills
102
-
103
- For agent-guided Next.js deploys, install the Prisma CLI skill cluster at the
104
- project level. Match the skill ref to the installed CLI version:
105
-
106
- ```bash
107
- npx prisma-cli version
108
- pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v<cli-version> --all
109
- ```
110
-
111
- The skills teach agents how to prepare a Next.js app, run `app deploy`, verify
112
- the result, and route CLI / Compute feedback to the right Prisma channel.
113
-
114
103
  ---
115
104
 
116
105
  ## Beta notes
117
106
 
118
- - Requires Node.js 22.12 or newer.
107
+ - Requires Node.js 22.18 or newer.
119
108
  - This is a beta package and may change quickly.
120
109
  - Official beta releases publish as `@prisma/cli`.
121
110
  - The package binary is `prisma-cli`, not `prisma`, during beta.
@@ -1,5 +1,5 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
1
  import path from "node:path";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  //#region src/adapters/local-state.ts
4
4
  const DEFAULT_STATE = {
5
5
  auth: null,
@@ -12,7 +12,8 @@ const DEFAULT_STATE = {
12
12
  app: {
13
13
  selectedByProject: {},
14
14
  knownLiveDeploymentByProject: {}
15
- }
15
+ },
16
+ agent: { setupPromptDismissedAt: null }
16
17
  };
17
18
  const DEFAULT_STATE_FILE_NAME = "state.json";
18
19
  function resolveLocalStateFilePath(stateDir) {
@@ -43,7 +44,8 @@ var LocalStateStore = class {
43
44
  app: {
44
45
  selectedByProject: parsed.app?.selectedByProject ?? {},
45
46
  knownLiveDeploymentByProject: parsed.app?.knownLiveDeploymentByProject ?? {}
46
- }
47
+ },
48
+ agent: { setupPromptDismissedAt: parsed.agent?.setupPromptDismissedAt ?? null }
47
49
  };
48
50
  } catch (error) {
49
51
  if (error.code === "ENOENT") return structuredClone(DEFAULT_STATE);
@@ -139,6 +141,15 @@ var LocalStateStore = class {
139
141
  await this.write(state);
140
142
  return state;
141
143
  }
144
+ async readAgentSetupPromptDismissedAt() {
145
+ return (await this.read()).agent.setupPromptDismissedAt;
146
+ }
147
+ async setAgentSetupPromptDismissedAt(dismissedAt) {
148
+ const state = await this.read();
149
+ state.agent.setupPromptDismissedAt = dismissedAt;
150
+ await this.write(state);
151
+ return state;
152
+ }
142
153
  };
143
154
  //#endregion
144
155
  export { LocalStateStore, resolveLocalStateFilePath };
@@ -1,15 +1,23 @@
1
1
  import path from "node:path";
2
2
  import os from "node:os";
3
- //#region src/lib/auth/client.ts
3
+ //#region src/auth/client.ts
4
4
  const CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw";
5
- const SERVICE_TOKEN_ENV_VAR = "PRISMA_SERVICE_TOKEN";
6
5
  const AUTH_FILE_ENV_VAR = "PRISMA_COMPUTE_AUTH_FILE";
6
+ /**
7
+ * The SDK's config demands a redirect URI even for a client that only
8
+ * ever makes API calls with tokens it already has. Port 0 is the
9
+ * honest value: no browser is ever sent here, and nothing listens.
10
+ */
11
+ const UNUSED_REDIRECT_URI = "http://localhost:0/auth/callback";
7
12
  function getApiBaseUrl(env = process.env) {
8
13
  return env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io";
9
14
  }
10
15
  function getAuthFilePath(env = process.env) {
11
16
  const configured = env[AUTH_FILE_ENV_VAR];
12
17
  if (configured?.trim()) return path.resolve(configured);
18
+ return defaultAuthFilePath(env);
19
+ }
20
+ function defaultAuthFilePath(env = process.env) {
13
21
  if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "prisma", "auth.json");
14
22
  if (process.platform === "win32") {
15
23
  const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
@@ -19,4 +27,4 @@ function getAuthFilePath(env = process.env) {
19
27
  return path.join(xdgConfigHome, "prisma", "auth.json");
20
28
  }
21
29
  //#endregion
22
- export { CLIENT_ID, SERVICE_TOKEN_ENV_VAR, getApiBaseUrl, getAuthFilePath };
30
+ export { CLIENT_ID, UNUSED_REDIRECT_URI, getApiBaseUrl, getAuthFilePath };
@@ -0,0 +1,50 @@
1
+ import { CliError } from "../errors.js";
2
+ //#region src/auth/errors.ts
3
+ /**
4
+ * Auth-specific error constructors, owned by the auth module. The
5
+ * CliError base class still lives in the legacy shell (`shell/errors`);
6
+ * it is the one remaining legacy dependency of this module and is
7
+ * named as an S2d survivor until the operations layer throws structured
8
+ * errors directly. The shell re-exports these constructors so legacy
9
+ * imports keep working.
10
+ */
11
+ function workspaceSwitchUnavailableError() {
12
+ return new CliError({
13
+ code: "WORKSPACE_SWITCH_UNAVAILABLE",
14
+ domain: "auth",
15
+ summary: "Workspace switching is unavailable",
16
+ why: "PRISMA_SERVICE_TOKEN is set, so authenticated commands use that token instead of local OAuth workspaces.",
17
+ fix: "Unset PRISMA_SERVICE_TOKEN to switch between local OAuth workspaces, or use a token for the workspace you want.",
18
+ exitCode: 1,
19
+ nextSteps: ["unset PRISMA_SERVICE_TOKEN", "prisma-cli auth workspace list"]
20
+ });
21
+ }
22
+ function workspaceNotAuthenticatedError(workspaceRef) {
23
+ return new CliError({
24
+ code: "WORKSPACE_NOT_AUTHENTICATED",
25
+ domain: "auth",
26
+ summary: "Workspace is not authenticated",
27
+ why: `No stored OAuth session matched "${workspaceRef}".`,
28
+ fix: "Run prisma-cli auth login and authorize that workspace, then switch to it.",
29
+ meta: { workspaceRef },
30
+ exitCode: 1,
31
+ nextSteps: ["prisma-cli auth workspace list", "prisma-cli auth login"]
32
+ });
33
+ }
34
+ function workspaceAmbiguousError(workspaceRef, matches) {
35
+ return new CliError({
36
+ code: "WORKSPACE_AMBIGUOUS",
37
+ domain: "auth",
38
+ summary: "Workspace name is ambiguous",
39
+ why: `Multiple authenticated workspaces matched "${workspaceRef}".`,
40
+ fix: "Run prisma-cli auth workspace list and switch by workspace id.",
41
+ meta: {
42
+ workspaceRef,
43
+ matches
44
+ },
45
+ exitCode: 2,
46
+ nextSteps: ["prisma-cli auth workspace list"]
47
+ });
48
+ }
49
+ //#endregion
50
+ export { workspaceAmbiguousError, workspaceNotAuthenticatedError, workspaceSwitchUnavailableError };
@@ -1,7 +1,8 @@
1
- import { CLIENT_ID, SERVICE_TOKEN_ENV_VAR, getApiBaseUrl } from "./client.js";
2
- import { FileTokenStorage } from "../../adapters/token-storage.js";
1
+ import { CLIENT_ID, UNUSED_REDIRECT_URI, getApiBaseUrl } from "./client.js";
2
+ import { FileTokenStorage } from "./token-storage.js";
3
3
  import { createManagementApiClient, createManagementApiSdk } from "@prisma/management-api-sdk";
4
- //#region src/lib/auth/guard.ts
4
+ import { SERVICE_TOKEN_ENV_VAR } from "@prisma/cli-engine";
5
+ //#region src/auth/guard.ts
5
6
  /**
6
7
  * Resolve authentication and return a ManagementApiClient.
7
8
  *
@@ -11,7 +12,7 @@ import { createManagementApiClient, createManagementApiSdk } from "@prisma/manag
11
12
  *
12
13
  * Returns null if not authenticated.
13
14
  */
14
- async function requireComputeAuth(env = process.env, signal) {
15
+ async function authenticatedManagementApiClient(env = process.env, signal) {
15
16
  signal?.throwIfAborted();
16
17
  const rawToken = env[SERVICE_TOKEN_ENV_VAR];
17
18
  if (rawToken !== void 0) {
@@ -29,10 +30,10 @@ async function requireComputeAuth(env = process.env, signal) {
29
30
  if (!await tokenStorage.getTokens()) return null;
30
31
  return createManagementApiSdk({
31
32
  clientId: CLIENT_ID,
32
- redirectUri: "http://localhost:0/auth/callback",
33
+ redirectUri: UNUSED_REDIRECT_URI,
33
34
  tokenStorage,
34
35
  apiBaseUrl: getApiBaseUrl(env)
35
36
  }).client;
36
37
  }
37
38
  //#endregion
38
- export { requireComputeAuth };
39
+ export { authenticatedManagementApiClient };
@@ -1,11 +1,11 @@
1
1
  import { getApiBaseUrl } from "./client.js";
2
- import { FileTokenStorage } from "../../adapters/token-storage.js";
3
- import open from "open";
2
+ import { FileTokenStorage } from "./token-storage.js";
4
3
  import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
5
4
  import events from "node:events";
6
5
  import http from "node:http";
7
6
  import readline from "node:readline/promises";
8
- //#region src/lib/auth/login.ts
7
+ import open from "open";
8
+ //#region src/auth/login.ts
9
9
  var AuthError$1 = class extends Error {
10
10
  constructor(message) {
11
11
  super(message);
@@ -33,6 +33,7 @@ async function login(options = {}) {
33
33
  apiBaseUrl: options.apiBaseUrl,
34
34
  authBaseUrl: options.authBaseUrl,
35
35
  openUrl: options.openUrl,
36
+ onVerificationUrl: options.onVerificationUrl,
36
37
  env: options.env,
37
38
  signal: options.signal,
38
39
  output
@@ -78,8 +79,8 @@ async function login(options = {}) {
78
79
  settle(resolve);
79
80
  } catch (error) {
80
81
  res.statusCode = 400;
81
- const message = error instanceof Error ? error.message : String(error);
82
- res.end(message);
82
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
83
+ res.end("Sign-in could not be completed. Return to your terminal.");
83
84
  settle(() => reject(error));
84
85
  return;
85
86
  }
@@ -174,6 +175,9 @@ var LoginState = class {
174
175
  });
175
176
  this.latestState = state;
176
177
  this.latestVerifier = verifier;
178
+ try {
179
+ this.options.onVerificationUrl?.(url);
180
+ } catch {}
177
181
  this.options.signal?.throwIfAborted();
178
182
  if (interactive) this.printLoginInstructions(url);
179
183
  try {
@@ -242,6 +246,9 @@ function renderSuccessPage(workspaceName) {
242
246
  --foreground: #1f2430;
243
247
  --muted: #4f5665;
244
248
  --mark-color: #050812;
249
+ --surface: #f6f7fb;
250
+ --border: #e4e7ee;
251
+ --success: #15803d;
245
252
  }
246
253
 
247
254
  @media (prefers-color-scheme: dark) {
@@ -250,6 +257,9 @@ function renderSuccessPage(workspaceName) {
250
257
  --foreground: #f6f7fb;
251
258
  --muted: #c5cad6;
252
259
  --mark-color: #ffffff;
260
+ --surface: #0d1322;
261
+ --border: #232a3d;
262
+ --success: #4ade80;
253
263
  }
254
264
  }
255
265
 
@@ -303,6 +313,93 @@ function renderSuccessPage(workspaceName) {
303
313
  line-height: 1.55;
304
314
  letter-spacing: 0;
305
315
  }
316
+
317
+ .skills {
318
+ margin-top: 40px;
319
+ padding-top: 28px;
320
+ border-top: 1px solid var(--border);
321
+ text-align: left;
322
+ }
323
+
324
+ .skills-lead {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 8px;
328
+ margin: 0 0 12px;
329
+ font-size: 15px;
330
+ color: var(--foreground);
331
+ }
332
+
333
+ .skills-lead svg {
334
+ flex: none;
335
+ color: var(--muted);
336
+ }
337
+
338
+ .command {
339
+ display: flex;
340
+ align-items: center;
341
+ gap: 8px;
342
+ padding: 10px 8px 10px 16px;
343
+ background: var(--surface);
344
+ border: 1px solid var(--border);
345
+ border-radius: 8px;
346
+ }
347
+
348
+ .command code {
349
+ flex: 1;
350
+ overflow-x: auto;
351
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
352
+ font-size: 13.5px;
353
+ white-space: nowrap;
354
+ }
355
+
356
+ .command .prompt {
357
+ color: var(--muted);
358
+ user-select: none;
359
+ }
360
+
361
+ .copy {
362
+ flex: none;
363
+ display: grid;
364
+ place-items: center;
365
+ padding: 6px;
366
+ border: 0;
367
+ border-radius: 6px;
368
+ background: transparent;
369
+ color: var(--muted);
370
+ cursor: pointer;
371
+ }
372
+
373
+ .copy:hover {
374
+ background: var(--border);
375
+ color: var(--foreground);
376
+ }
377
+
378
+ .copy .icon-check,
379
+ .copy.copied .icon-copy {
380
+ display: none;
381
+ }
382
+
383
+ .copy.copied .icon-check {
384
+ display: block;
385
+ }
386
+
387
+ .copy.copied,
388
+ .copy.copied:hover {
389
+ color: var(--success);
390
+ }
391
+
392
+ .visually-hidden {
393
+ position: absolute;
394
+ width: 1px;
395
+ height: 1px;
396
+ margin: -1px;
397
+ padding: 0;
398
+ overflow: hidden;
399
+ clip: rect(0 0 0 0);
400
+ white-space: nowrap;
401
+ border: 0;
402
+ }
306
403
  </style>
307
404
  </head>
308
405
  <body>
@@ -310,7 +407,51 @@ function renderSuccessPage(workspaceName) {
310
407
  <main>
311
408
  <h1>You're all set.</h1>
312
409
  <p>${workspaceName ? `Your terminal is now connected to your ${escapeHtml(workspaceName)} workspace. Head back to your terminal to continue.` : "Your terminal is now connected to your Prisma workspace. Head back to your terminal to continue."}</p>
410
+ <section class="skills">
411
+ <div class="skills-lead">
412
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/></svg>
413
+ Using an AI coding agent? Add the Prisma skills:
414
+ </div>
415
+ <div class="command">
416
+ <code><span class="prompt">$ </span><span class="command-text">npx skills add prisma/skills</span></code>
417
+ <button class="copy" type="button" aria-label="Copy command to clipboard">
418
+ <svg class="icon-copy" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
419
+ <svg class="icon-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
420
+ </button>
421
+ </div>
422
+ <span class="visually-hidden skills-status" role="status"></span>
423
+ </section>
313
424
  </main>
425
+ <script>
426
+ (() => {
427
+ const command = "npx skills add prisma/skills";
428
+ const button = document.querySelector(".copy");
429
+ const status = document.querySelector(".skills-status");
430
+ let timer;
431
+ button.addEventListener("click", async () => {
432
+ try {
433
+ await navigator.clipboard.writeText(command);
434
+ } catch {
435
+ // Clipboard access denied: select the command so Cmd/Ctrl+C works.
436
+ const range = document.createRange();
437
+ range.selectNodeContents(document.querySelector(".command-text"));
438
+ const selection = window.getSelection();
439
+ selection.removeAllRanges();
440
+ selection.addRange(range);
441
+ status.textContent =
442
+ "Copying failed. The command is selected; press Ctrl+C or Cmd+C to copy it.";
443
+ return;
444
+ }
445
+ button.classList.add("copied");
446
+ status.textContent = "Command copied to clipboard.";
447
+ clearTimeout(timer);
448
+ timer = setTimeout(() => {
449
+ button.classList.remove("copied");
450
+ status.textContent = "";
451
+ }, 2000);
452
+ });
453
+ })();
454
+ <\/script>
314
455
  </body>
315
456
  </html>`;
316
457
  }
@@ -327,4 +468,4 @@ function escapeHtml(value) {
327
468
  });
328
469
  }
329
470
  //#endregion
330
- export { login };
471
+ export { AuthError$1 as AuthError, login };
@@ -1,9 +1,22 @@
1
- import { SERVICE_TOKEN_ENV_VAR } from "./client.js";
2
- import { FileTokenStorage } from "../../adapters/token-storage.js";
3
- import { requireComputeAuth } from "./guard.js";
4
- import { login } from "./login.js";
5
- //#region src/lib/auth/auth-ops.ts
1
+ import { FileTokenStorage } from "./token-storage.js";
2
+ import { authenticatedManagementApiClient } from "./guard.js";
3
+ import { AuthError, login } from "./login.js";
4
+ import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, credentialWorkspaceId } from "@prisma/cli-engine";
5
+ //#region src/auth/operations.ts
6
6
  const WORKSPACE_SUB_PREFIX = "workspace:";
7
+ /** Thrown when PRISMA_SERVICE_TOKEN is set to an empty/blank value. */
8
+ var EmptyServiceTokenError = class extends Error {
9
+ code = "EMPTY_SERVICE_TOKEN";
10
+ constructor() {
11
+ super(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
12
+ this.name = "EmptyServiceTokenError";
13
+ }
14
+ };
15
+ /** Matches by the code discriminator, not instanceof, so the check
16
+ * survives duplicate module instances. */
17
+ function isEmptyServiceTokenError(error) {
18
+ return typeof error === "object" && error !== null && error.code === "EMPTY_SERVICE_TOKEN";
19
+ }
7
20
  function decodeJwtPayload(token) {
8
21
  try {
9
22
  const payload = token.split(".")[1];
@@ -24,18 +37,55 @@ function workspaceIdFromClaims(claims) {
24
37
  const id = sub.slice(10).trim();
25
38
  return id.length > 0 ? id : null;
26
39
  }
27
- async function performLogin(env, signal) {
40
+ /** Holds the tokens the SDK writes at callback time and nothing else:
41
+ * minting and custody stay separate, so a login never writes through
42
+ * the credential manager. */
43
+ var ThrowawayTokenStorage = class {
44
+ tokens = null;
45
+ async getTokens() {
46
+ return this.tokens;
47
+ }
48
+ async setTokens(tokens) {
49
+ this.tokens = tokens;
50
+ }
51
+ async clearTokens() {
52
+ this.tokens = null;
53
+ }
54
+ };
55
+ /** Runs the browser consent flow and RETURNS the minted credential.
56
+ * Storing it is the caller's job. */
57
+ async function performLogin(env, signal, options) {
58
+ const tokenStorage = new ThrowawayTokenStorage();
28
59
  await login({
29
- tokenStorage: new FileTokenStorage(env, signal, { activateOnSetTokens: true }),
60
+ tokenStorage,
30
61
  env,
31
- signal
62
+ signal,
63
+ onVerificationUrl: options?.onVerificationUrl
64
+ });
65
+ const tokens = tokenStorage.tokens;
66
+ if (!tokens) throw new AuthError("Sign-in finished without producing a credential.");
67
+ return {
68
+ token: tokens.accessToken,
69
+ refreshToken: tokens.refreshToken,
70
+ expiresAt: claimedExpiresAt(tokens.accessToken)
71
+ };
72
+ }
73
+ /** Writes a minted credential into the legacy store, which the legacy
74
+ * shell still reads. Dies with the legacy shell. */
75
+ async function storeLegacyCredential(env, credential, signal) {
76
+ const workspaceId = credentialWorkspaceId(credential.token);
77
+ if (workspaceId === void 0) return;
78
+ await new FileTokenStorage(env, signal, { activateOnSetTokens: true }).setTokens({
79
+ workspaceId,
80
+ accessToken: credential.token,
81
+ ...credential.refreshToken === void 0 ? {} : { refreshToken: credential.refreshToken }
32
82
  });
33
83
  }
34
84
  async function readAuthState(env, signal) {
35
85
  const rawServiceToken = env[SERVICE_TOKEN_ENV_VAR];
36
86
  if (rawServiceToken !== void 0) {
37
87
  const serviceToken = rawServiceToken.trim();
38
- if (serviceToken.length === 0) throw new Error(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
88
+ if (serviceToken.length === 0) throw new EmptyServiceTokenError();
39
89
  return readServiceTokenAuthState(serviceToken, env, signal);
40
90
  }
41
91
  const tokenStorage = new FileTokenStorage(env, signal);
@@ -47,7 +97,7 @@ async function readAuthState(env, signal) {
47
97
  workspace: null,
48
98
  credential: null
49
99
  };
50
- const client = await requireComputeAuth(env, signal);
100
+ const client = await authenticatedManagementApiClient(env, signal);
51
101
  const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
52
102
  if (currentPrincipal) {
53
103
  if (currentPrincipal.authenticated && currentPrincipal.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, currentPrincipal.workspace);
@@ -65,7 +115,7 @@ async function readAuthState(env, signal) {
65
115
  return authState;
66
116
  }
67
117
  async function readServiceTokenAuthState(token, env, signal) {
68
- const client = await requireComputeAuth(env, signal);
118
+ const client = await authenticatedManagementApiClient(env, signal);
69
119
  const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
70
120
  if (currentPrincipal) return currentPrincipal;
71
121
  const claims = decodeJwtPayload(token);
@@ -88,7 +138,7 @@ async function readServiceTokenAuthState(token, env, signal) {
88
138
  async function buildAuthState({ workspaceIdFromCredential, claims, env, client, signal }) {
89
139
  let workspaceId = workspaceIdFromCredential;
90
140
  let workspaceName = workspaceIdFromCredential;
91
- client ??= await requireComputeAuth(env, signal);
141
+ client ??= await authenticatedManagementApiClient(env, signal);
92
142
  if (client) try {
93
143
  const { data, response } = await client.GET("/v1/workspaces/{id}", {
94
144
  params: { path: { id: workspaceIdFromCredential } },
@@ -155,4 +205,4 @@ async function performLogout(env, signal) {
155
205
  await new FileTokenStorage(env, signal).clearTokens();
156
206
  }
157
207
  //#endregion
158
- export { performLogin, performLogout, readAuthState };
208
+ export { isEmptyServiceTokenError, performLogin, performLogout, readAuthState, storeLegacyCredential };
@@ -0,0 +1,42 @@
1
+ import { getApiBaseUrl } from "./client.js";
2
+ import { FileTokenStorage } from "./token-storage.js";
3
+ import { createManagementApiSdk } from "@prisma/management-api-sdk";
4
+ //#region src/auth/recipient.ts
5
+ var RecipientSessionInvalidError = class extends Error {
6
+ constructor(workspaceRef) {
7
+ super(`The stored session for workspace "${workspaceRef}" could not be validated.`);
8
+ this.workspaceRef = workspaceRef;
9
+ this.name = "RecipientSessionInvalidError";
10
+ }
11
+ };
12
+ /**
13
+ * Resolve a locally stored OAuth workspace session and return a validated
14
+ * access token for it, refreshing through the SDK when the stored token has
15
+ * expired. The active workspace pointer is never touched.
16
+ *
17
+ * Throws WorkspaceSelectionError when the ref does not match exactly one
18
+ * stored session, and RecipientSessionInvalidError when the session cannot
19
+ * be validated or refreshed.
20
+ */
21
+ async function resolveRecipientWorkspaceSession(workspaceRef, env = process.env, signal) {
22
+ const workspace = await new FileTokenStorage(env, signal).resolveWorkspace(workspaceRef);
23
+ const pinnedStorage = new FileTokenStorage(env, signal, {
24
+ activateOnSetTokens: false,
25
+ lockSetTokens: false,
26
+ pinnedWorkspaceId: workspace.credentialWorkspaceId
27
+ });
28
+ if ((await createManagementApiSdk({
29
+ clientId: "cmm3lndn701oo0uefvxzo0ivw",
30
+ redirectUri: "http://localhost:0/auth/callback",
31
+ tokenStorage: pinnedStorage,
32
+ apiBaseUrl: getApiBaseUrl(env)
33
+ }).client.GET("/v1/workspaces", { signal })).error) throw new RecipientSessionInvalidError(workspaceRef);
34
+ const tokens = await pinnedStorage.getTokens();
35
+ if (!tokens) throw new RecipientSessionInvalidError(workspaceRef);
36
+ return {
37
+ workspace,
38
+ accessToken: tokens.accessToken
39
+ };
40
+ }
41
+ //#endregion
42
+ export { RecipientSessionInvalidError, resolveRecipientWorkspaceSession };
@@ -1,9 +1,10 @@
1
- import { getAuthFilePath } from "../lib/auth/client.js";
2
- import fs from "node:fs/promises";
1
+ import { getAuthFilePath } from "./client.js";
2
+ import { sameWorkspaceId } from "../lib/workspace-id.js";
3
3
  import path from "node:path";
4
+ import fs from "node:fs/promises";
4
5
  import { randomUUID } from "node:crypto";
5
6
  import { CredentialsStore } from "@prisma/credentials-store";
6
- //#region src/adapters/token-storage.ts
7
+ //#region src/auth/token-storage.ts
7
8
  const REFRESH_LOCK_RETRY_MS = 100;
8
9
  const REFRESH_LOCK_STALE_MS = 3e4;
9
10
  const REFRESH_LOCK_WAIT_TIMEOUT_MS = 25e3;
@@ -77,6 +78,7 @@ var FileTokenStorage = class {
77
78
  this.signal?.throwIfAborted();
78
79
  try {
79
80
  const credentials = await this.readCredentialsFromDisk();
81
+ if (this.options.pinnedWorkspaceId) return findTokensForWorkspace(credentials, this.options.pinnedWorkspaceId);
80
82
  const context = await this.readAuthContext();
81
83
  if (context.state.activeWorkspaceId) return findTokensForWorkspace(credentials, context.state.activeWorkspaceId);
82
84
  const latest = findLatestValidTokens(credentials);
@@ -138,6 +140,9 @@ var FileTokenStorage = class {
138
140
  this.signal?.throwIfAborted();
139
141
  const credentials = await this.readCredentialsFromDisk();
140
142
  const context = await this.ensureMigratedAuthContext(credentials);
143
+ return this.workspacesFromState(credentials, context);
144
+ }
145
+ workspacesFromState(credentials, context) {
141
146
  return credentials.map((credential) => storedCredentialToTokens(credential)).filter((tokens) => tokens !== null).map((tokens) => {
142
147
  const cached = context.state.workspaces[tokens.workspaceId];
143
148
  const id = typeof cached?.id === "string" && cached.id.trim().length > 0 ? cached.id.trim() : tokens.workspaceId;
@@ -159,6 +164,24 @@ var FileTokenStorage = class {
159
164
  async useWorkspace(workspaceRef) {
160
165
  return this.withRefreshLock(() => this.useWorkspaceUnlocked(workspaceRef));
161
166
  }
167
+ /**
168
+ * Resolve a workspace ref (id, credential workspace id, or cached name)
169
+ * against the locally stored sessions without changing the active
170
+ * workspace. Read-only counterpart of useWorkspace: it reads the auth
171
+ * context as-is and never runs the legacy-state migration, so it writes
172
+ * nothing.
173
+ */
174
+ async resolveWorkspace(workspaceRef) {
175
+ const ref = workspaceRef.trim();
176
+ if (!ref) throw new WorkspaceSelectionError("missing");
177
+ this.signal?.throwIfAborted();
178
+ const credentials = await this.readCredentialsFromDisk();
179
+ const context = await this.readAuthContext();
180
+ const matches = this.workspacesFromState(credentials, context).filter((workspace) => workspaceMatchesRef(workspace, ref));
181
+ if (matches.length === 0) throw new WorkspaceSelectionError("not-found", ref);
182
+ if (matches.length > 1) throw new WorkspaceSelectionError("ambiguous", ref, matches);
183
+ return matches[0];
184
+ }
162
185
  async useWorkspaceUnlocked(workspaceRef) {
163
186
  const ref = workspaceRef.trim();
164
187
  if (!ref) throw new WorkspaceSelectionError("missing");
@@ -327,6 +350,7 @@ var FileTokenStorage = class {
327
350
  await this.writeAuthContext(context.state);
328
351
  }
329
352
  async maybeActivateWorkspaceId(workspaceId) {
353
+ if (this.options.pinnedWorkspaceId) return;
330
354
  const context = await this.readAuthContext();
331
355
  if (this.options.activateOnSetTokens === false && context.exists && context.state.activeWorkspaceId && context.state.activeWorkspaceId !== workspaceId) return;
332
356
  context.state.activeWorkspaceId = workspaceId;
@@ -406,10 +430,7 @@ var RefreshLockTimeoutError = class extends Error {
406
430
  }
407
431
  };
408
432
  function workspaceMatchesRef(workspace, ref) {
409
- return workspace.credentialWorkspaceId === ref || workspace.id === ref || stripWorkspacePrefix(workspace.credentialWorkspaceId) === stripWorkspacePrefix(ref) || stripWorkspacePrefix(workspace.id) === stripWorkspacePrefix(ref) || workspace.name.toLowerCase() === ref.toLowerCase();
410
- }
411
- function stripWorkspacePrefix(value) {
412
- return value.startsWith("wksp_") ? value.slice(5) : value;
433
+ return sameWorkspaceId(workspace.credentialWorkspaceId, ref) || sameWorkspaceId(workspace.id, ref) || workspace.name.toLowerCase() === ref.toLowerCase();
413
434
  }
414
435
  function workspaceDisplayName(name, credentialWorkspaceId) {
415
436
  return name === credentialWorkspaceId ? UNKNOWN_WORKSPACE_NAME : name;