@ory/argus 0.13.9 → 0.14.0

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 (65) hide show
  1. package/README.md +2 -1
  2. package/assets/commands/dashboard.md +34 -0
  3. package/assets/skills/ory-build-agent/SKILL.md +11 -13
  4. package/assets/skills/ory-e2b-sandbox/SKILL.md +0 -1
  5. package/assets/skills/ory-temporal-worker/SKILL.md +8 -10
  6. package/assets/skills/permissions-onboarding/SKILL.md +3 -3
  7. package/dist/adapters.d.ts +12 -5
  8. package/dist/adapters.js +59 -10
  9. package/dist/agent-auth.d.ts +34 -3
  10. package/dist/agent-auth.js +58 -8
  11. package/dist/auth.d.ts +7 -0
  12. package/dist/auth.js +90 -5
  13. package/dist/branding.d.ts +67 -0
  14. package/dist/branding.js +81 -0
  15. package/dist/build-info.json +4 -4
  16. package/dist/cli.d.ts +3 -3
  17. package/dist/cli.js +13 -52
  18. package/dist/config.d.ts +48 -19
  19. package/dist/config.js +31 -26
  20. package/dist/contract-suite.d.ts +5 -3
  21. package/dist/contract-suite.js +13 -22
  22. package/dist/dashboard-cli.d.ts +8 -0
  23. package/dist/dashboard-cli.js +70 -0
  24. package/dist/dev.js +3 -4
  25. package/dist/index.d.ts +11 -4
  26. package/dist/index.js +39 -3
  27. package/dist/interactive-setup.d.ts +144 -23
  28. package/dist/interactive-setup.js +412 -224
  29. package/dist/local/health.d.ts +14 -0
  30. package/dist/local/health.js +46 -0
  31. package/dist/local/manager.js +1 -3
  32. package/dist/local/seed.d.ts +9 -20
  33. package/dist/local/seed.js +26 -18
  34. package/dist/permissions-cli.js +1 -1
  35. package/dist/project-api-key.d.ts +69 -0
  36. package/dist/project-api-key.js +147 -0
  37. package/dist/registry/manager.js +62 -18
  38. package/dist/setup-actions.d.ts +232 -0
  39. package/dist/setup-actions.js +507 -0
  40. package/dist/setup.d.ts +19 -0
  41. package/dist/setup.js +44 -14
  42. package/dist/skills.js +7 -0
  43. package/dist/status-cli.d.ts +2 -2
  44. package/dist/status-cli.js +4 -30
  45. package/dist/status-data.d.ts +96 -0
  46. package/dist/status-data.js +250 -0
  47. package/dist/status-system.d.ts +24 -0
  48. package/dist/status-system.js +56 -0
  49. package/dist/uninstall.d.ts +33 -15
  50. package/dist/uninstall.js +65 -22
  51. package/dist/user-login.d.ts +16 -9
  52. package/dist/user-login.js +18 -28
  53. package/dist/web/api.d.ts +33 -0
  54. package/dist/web/api.js +294 -0
  55. package/dist/web/launch.d.ts +11 -0
  56. package/dist/web/launch.js +96 -0
  57. package/dist/web/server.d.ts +20 -0
  58. package/dist/web/server.js +233 -0
  59. package/dist/web/types.d.ts +65 -0
  60. package/dist/web/types.js +2 -0
  61. package/dist/webapp/assets/index-Wucl4SZs.css +1 -0
  62. package/dist/webapp/assets/index-m-GtEdq0.js +49 -0
  63. package/dist/webapp/favicon.ico +0 -0
  64. package/dist/webapp/index.html +15 -0
  65. package/package.json +17 -2
@@ -15,10 +15,10 @@
15
15
  * caller bootstrap permissions from the freshly-cached user identity.
16
16
  *
17
17
  * The whole wizard is **fail-open and skippable**: on a missing TTY, a
18
- * `--no-configure` flag, an already-configured plugin (without
19
- * `--reconfigure`), a missing `ory` CLI, or any `ory` failure it prints the
20
- * manual `configure` instructions and returns without throwing. Install must
21
- * never crash because setup was declined or the network was unreachable.
18
+ * `--no-configure` flag, a declined reconfigure prompt on an already-configured
19
+ * plugin, a missing `ory` CLI, or any `ory` failure it prints the manual
20
+ * `configure` instructions and returns without throwing. Install must never
21
+ * crash because setup was declined or the network was unreachable.
22
22
  *
23
23
  * The Ory Network integration deliberately reuses the official `ory` CLI
24
24
  * rather than re-implementing the Console API: the CLI already owns Network
@@ -65,10 +65,23 @@ var __importStar = (this && this.__importStar) || (function () {
65
65
  Object.defineProperty(exports, "__esModule", { value: true });
66
66
  exports.ORY_CLI_NPM_SPEC = exports.LOOPBACK_REDIRECT_URIS = void 0;
67
67
  exports.createOryRunner = createOryRunner;
68
+ exports.installOryCli = installOryCli;
68
69
  exports.runPostInstall = runPostInstall;
69
70
  exports.runInteractiveSetup = runInteractiveSetup;
71
+ exports.inspectPermissionModel = inspectPermissionModel;
72
+ exports.provisionPermissionModel = provisionPermissionModel;
73
+ exports.oryCliPresent = oryCliPresent;
74
+ exports.listProjects = listProjects;
75
+ exports.dcrEnabled = dcrEnabled;
76
+ exports.enableDcr = enableDcr;
77
+ exports.projectLoginUiState = projectLoginUiState;
78
+ exports.useManagedAccountExperience = useManagedAccountExperience;
79
+ exports.findExistingOAuth2Client = findExistingOAuth2Client;
80
+ exports.createOAuth2Client = createOAuth2Client;
81
+ exports.parseList = parseList;
82
+ exports.parseObject = parseObject;
70
83
  exports.projectUrlFromSlug = projectUrlFromSlug;
71
- exports.createProjectApiKeyViaConsole = createProjectApiKeyViaConsole;
84
+ exports.isLocalProjectUrl = isLocalProjectUrl;
72
85
  exports.apiKeyAuthenticates = apiKeyAuthenticates;
73
86
  const node_child_process_1 = require("node:child_process");
74
87
  const fs = __importStar(require("node:fs"));
@@ -88,8 +101,12 @@ const agent_auth_js_1 = require("./agent-auth.js");
88
101
  const tool_catalog_js_1 = require("./tool-catalog.js");
89
102
  const index_js_1 = require("./local/index.js");
90
103
  const subject_js_1 = require("./subject.js");
104
+ const branding_js_1 = require("./branding.js");
105
+ const project_api_key_js_1 = require("./project-api-key.js");
91
106
  const permissions_cli_js_1 = require("./permissions-cli.js");
92
107
  const version_cli_js_1 = require("./version-cli.js");
108
+ const launch_js_1 = require("./web/launch.js");
109
+ const setup_actions_js_1 = require("./setup-actions.js");
93
110
  const ui = __importStar(require("./ui.js"));
94
111
  /** Loopback redirect URIs the PKCE user login expects on the OAuth2 client. */
95
112
  exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`);
@@ -101,6 +118,7 @@ exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://
101
118
  */
102
119
  function createOryRunner(oryBin) {
103
120
  return {
121
+ bin: oryBin,
104
122
  exec(args, opts) {
105
123
  const r = (0, node_child_process_1.spawnSync)(oryBin, args, {
106
124
  encoding: "utf-8",
@@ -153,6 +171,20 @@ async function installOryCli() {
153
171
  // Confirm the freshly installed binary actually runs before handing it back.
154
172
  return oryCliPresent(runner) ? runner : null;
155
173
  }
174
+ /** Project the wizard's dep bag onto the subset the headless web actions use. */
175
+ function toSetupDeps(deps) {
176
+ return {
177
+ runner: deps.runner,
178
+ installOryCliFn: deps.installOryCliFn,
179
+ clientFactory: deps.clientFactory,
180
+ userLoginFn: deps.userLoginFn,
181
+ agentGateFn: deps.agentGateFn,
182
+ localStackFn: deps.localStackFn,
183
+ seedFn: deps.seedFn,
184
+ createProjectApiKeyFn: deps.createProjectApiKeyFn,
185
+ validateApiKeyFn: deps.validateApiKeyFn,
186
+ };
187
+ }
156
188
  /**
157
189
  * The shared post-install entry point every harness's `install` command
158
190
  * calls. Runs the interactive setup wizard, then (using whatever identity the
@@ -173,6 +205,35 @@ async function runPostInstall(binName, harness, args = [], deps = {}) {
173
205
  if (cliVersion !== "unknown") {
174
206
  ui.info(`${binName} v${cliVersion}`);
175
207
  }
208
+ // Prefer the local web installer when a browser is available and the user
209
+ // didn't opt out. It drives the same walkthrough (and writes its own
210
+ // permissions via the API), so on success we skip the TTY wizard and the
211
+ // DCR-token bootstrap, then print the closing onboarding help. Falls through
212
+ // to the terminal wizard when a browser can't be opened, `--no-web` /
213
+ // `--no-configure` is set, tests inject a prompt, or the launch fails.
214
+ //
215
+ // We always launch when a browser is available — even when Ory is already
216
+ // configured. The web app opens on its "current configuration" review
217
+ // screen and offers to reconfigure or keep the existing setup, so there's no
218
+ // separate `--reconfigure` gate here.
219
+ const noConfigure = args.includes("--no-configure");
220
+ const noWeb = args.includes("--no-web");
221
+ if (!noConfigure && !noWeb && !deps.promptFn && (0, launch_js_1.canLaunchBrowser)()) {
222
+ let launched = false;
223
+ try {
224
+ launched = await (0, launch_js_1.launchWebInstaller)(binName, harness, toSetupDeps(deps));
225
+ }
226
+ catch {
227
+ launched = false;
228
+ }
229
+ if (launched) {
230
+ // The web installer already walked the user through permissions in the
231
+ // browser, so skip the terminal permissions onboarding banner here and
232
+ // print only the closing next-steps summary.
233
+ (0, setup_js_1.emitDeferredNextSteps)(true);
234
+ return;
235
+ }
236
+ }
176
237
  const result = await runInteractiveSetup(binName, harness, args, deps);
177
238
  // The Ory Network wizard path bootstraps permissions itself (via the
178
239
  // admin-authenticated `ory` CLI). Only fall back to the DCR-token bootstrap
@@ -220,20 +281,6 @@ async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
220
281
  printManualSetup(binName);
221
282
  return { outcome: "skipped_no_tty" };
222
283
  }
223
- const resolved = (0, config_js_1.resolveConfig)();
224
- if ((resolved.projectUrl || resolved.auditOnly) && !reconfigure) {
225
- ui.banner("Ory Agent Plugin", "Already connected to Ory");
226
- ui.blank();
227
- ui.success("Ory is already configured for the agent plugins.");
228
- ui.summary([
229
- ["Mode", resolved.auditOnly ? "audit-only (no project)" : "connected"],
230
- ["Project URL", resolved.auditOnly ? undefined : resolved.projectUrl],
231
- ["Config", (0, config_js_1.getConfigPath)()],
232
- ]);
233
- ui.blank();
234
- ui.hint(`Re-run with ${ui.inlineCommand("--reconfigure")} to change it.`);
235
- return { outcome: "skipped_configured" };
236
- }
237
284
  // The wizard prompts several times in a row. Opening `/dev/tty` afresh for
238
285
  // each prompt (as `promptOnTty` does) leaves a blocking read stuck in
239
286
  // libuv's threadpool per call, and after a few prompts the pool is
@@ -262,6 +309,39 @@ async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
262
309
  ttyPrompter = createTtyPrompter();
263
310
  }
264
311
  };
312
+ // When Ory is already configured, always surface the current configuration
313
+ // first, then offer to reconfigure — a plain re-run of install shows what's
314
+ // set and asks, rather than silently no-oping. This replaces the old
315
+ // `--reconfigure` gate; the flag still works as a shortcut that skips the
316
+ // confirmation and drops straight into the wizard.
317
+ const resolved = (0, config_js_1.resolveConfig)();
318
+ if (resolved.projectUrl || resolved.auditOnly) {
319
+ ui.banner("Ory Agent Plugin", "Already connected to Ory");
320
+ ui.blank();
321
+ ui.success("Ory is already configured for the agent plugins.");
322
+ ui.summary([
323
+ [
324
+ "Mode",
325
+ resolved.auditOnly
326
+ ? "audit-only (no project)"
327
+ : isLocalProjectUrl(resolved.projectUrl)
328
+ ? "local stack"
329
+ : "connected",
330
+ ],
331
+ ["Project URL", resolved.auditOnly ? undefined : resolved.projectUrl],
332
+ ["Config", (0, config_js_1.getConfigPath)()],
333
+ ]);
334
+ ui.blank();
335
+ if (!reconfigure) {
336
+ const answer = await prompt(ui.promptLine("Reconfigure?", { hint: "[y/N]" }));
337
+ if (!isYes(answer)) {
338
+ ui.hint("Keeping the current configuration.");
339
+ ttyPrompter?.close();
340
+ return { outcome: "skipped_configured" };
341
+ }
342
+ ui.blank();
343
+ }
344
+ }
265
345
  // Quiet the structured stderr firehose for the duration of the wizard so
266
346
  // `[ory-agent] {...}` debug lines don't interleave with the human-facing UI
267
347
  // when a session runs with ORY_AGENT_DEBUG=true (the dev launcher always
@@ -369,7 +449,7 @@ function configureAuditOnly(binName) {
369
449
  ui.hint("Tool calls are traced locally; no Ory project, auth, or permission checks.");
370
450
  ui.summary([["Config", (0, config_js_1.getConfigPath)()]]);
371
451
  ui.blank();
372
- ui.hint(`To connect to Ory later, re-run install with ${ui.inlineCommand("--reconfigure")}, or run:`);
452
+ ui.hint(`To connect to Ory later, re-run install, or run:`);
373
453
  ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
374
454
  return { outcome: "audit_only" };
375
455
  }
@@ -415,7 +495,6 @@ async function configureLocalStack(binName, harness, ctx) {
415
495
  projectUrl: index_js_1.GATEWAY_URL,
416
496
  oauth2ClientId: index_js_1.USER_CLIENT_ID,
417
497
  userSubjectNamespace: index_js_1.USER_SUBJECT_NAMESPACE,
418
- userLogin: true,
419
498
  auditOnly: false,
420
499
  });
421
500
  if (!running) {
@@ -458,7 +537,10 @@ async function configureLocalStack(binName, harness, ctx) {
458
537
  ["Password", credentials.password],
459
538
  ]);
460
539
  }
461
- const { authenticated } = await completeFullSetup(binName, harness, ctx);
540
+ const { authenticated } = await completeFullSetup(binName, harness, ctx, {
541
+ projectUrl: index_js_1.GATEWAY_URL,
542
+ oauth2ClientId: index_js_1.USER_CLIENT_ID,
543
+ });
462
544
  return {
463
545
  outcome: "local_configured",
464
546
  projectUrl: index_js_1.GATEWAY_URL,
@@ -534,7 +616,7 @@ async function configureNetwork(binName, harness, ctx) {
534
616
  if (!workspaces || workspaces.length === 0) {
535
617
  console.warn("");
536
618
  ui.warning("No Ory Network workspace selected.");
537
- ui.warnDetail("Create one at https://console.ory.sh/ and re-run install with --reconfigure.");
619
+ ui.warnDetail("Create one at https://console.ory.sh/ and re-run install.");
538
620
  printManualSetup(binName);
539
621
  return { outcome: "network_fallback" };
540
622
  }
@@ -569,23 +651,55 @@ async function configureNetwork(binName, harness, ctx) {
569
651
  return { outcome: "network_fallback" };
570
652
  }
571
653
  const projectUrl = projectUrlFromSlug(project.slug);
572
- // 5. Enable OAuth2 Dynamic Client Registration so the agent identity can
573
- // self-register at runtime. Ory Network projects ship with DCR disabled,
574
- // so without this the default agent-identity path would never resolve.
575
- await maybeEnableDcr(runner, prompt, project.id);
576
- // 6. Provision the public OAuth2 client for the PKCE user login. Reuse an
577
- // existing one on this project first — a re-run (`install --reconfigure`)
578
- // against the same project must not mint a duplicate public client and
579
- // orphan the old one (the CLI-created client is never cleaned up by
580
- // uninstall). Only create when no prior client is found.
581
- ui.heading("OAuth2 client");
654
+ // ── Set up the project ────────────────────────────────────────────────────
655
+ // Everything from here needs no decision from the user, so it runs as a
656
+ // single automated phase (mirroring the web installer's one "Set up" step):
657
+ // enable DCR, fix the login UI, provision the OAuth2 client, save, log in,
658
+ // grant permissions, and mint the project API key. Each action prints a
659
+ // one-line result; none of them prompt.
660
+ ui.heading("Setting up your project");
661
+ // Enable OAuth2 Dynamic Client Registration so the agent identity can
662
+ // self-register at runtime. Ory Network projects ship with DCR disabled, so
663
+ // without this the default agent-identity path would never resolve — it's
664
+ // required, so we enable it silently rather than asking.
665
+ const dcr = dcrEnabled(runner, project.id);
666
+ if (dcr === true) {
667
+ ui.success("Dynamic Client Registration already enabled.");
668
+ }
669
+ else if (enableDcr(runner, project.id)) {
670
+ ui.success("Enabled Dynamic Client Registration.");
671
+ }
672
+ else {
673
+ ui.warning("Could not enable Dynamic Client Registration automatically.");
674
+ ui.warnDetail(`Enable it later: ory patch oauth2-config --project ${project.id} --replace '${DCR_ENABLED_PATH}=true'`);
675
+ ui.warnDetail("or supply static agent credentials (ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET).");
676
+ }
677
+ // Make sure the browser-login UI is the Ory Managed Account Experience. A
678
+ // CLI-created project defaults the Kratos self-service and OAuth2
679
+ // login/consent URLs to the localhost:3000 quickstart sample, so the PKCE
680
+ // user login below would redirect the browser to a page nothing is serving.
681
+ // Best-effort; never clobbers a deliberate custom UI.
682
+ const loginUi = projectLoginUiState(runner, project.id);
683
+ if (loginUi === "loopback") {
684
+ useManagedAccountExperience(runner, project.id);
685
+ ui.success("Switched the login UI to the Ory Account Experience.");
686
+ }
687
+ else if (loginUi === "custom_external") {
688
+ ui.hint("This project uses a custom login UI; the browser login will use it.");
689
+ }
690
+ // Provision the public OAuth2 client for the PKCE user login. Reuse an
691
+ // existing one on this project first — a re-run (`install --reconfigure`)
692
+ // against the same project must not mint a duplicate public client and orphan
693
+ // the old one (the CLI-created client is never cleaned up by uninstall). Only
694
+ // create when no prior client is found.
582
695
  let clientId = findExistingOAuth2Client(runner, project.id);
583
696
  if (clientId) {
584
- ui.step(`Reusing the existing OAuth2 client for user login (${clientId}).`);
697
+ ui.success(`Reusing the OAuth2 client for user login (${clientId}).`);
585
698
  }
586
699
  else {
587
- ui.step("Creating the OAuth2 client for user login...");
588
700
  clientId = createOAuth2Client(runner, project.id);
701
+ if (clientId)
702
+ ui.success("Created the OAuth2 client for user login.");
589
703
  }
590
704
  if (!clientId) {
591
705
  ui.warning("Could not create the OAuth2 client automatically.");
@@ -595,29 +709,33 @@ async function configureNetwork(binName, harness, ctx) {
595
709
  (0, config_js_1.saveConfig)({ projectUrl, projectId: project.id, auditOnly: false });
596
710
  return { outcome: "network_fallback", projectUrl };
597
711
  }
598
- // 7. Persist and enable user login. The project id is stored alongside the
599
- // URL (it's the `ORY_PROJECT_ID` value) so later admin operations and
600
- // `status` have it without re-selecting a project.
712
+ // Persist the connection. The project id is stored alongside the URL (it's
713
+ // the `ORY_PROJECT_ID` value) so later admin operations and `status` have it
714
+ // without re-selecting a project.
601
715
  (0, config_js_1.saveConfig)({
602
716
  projectUrl,
603
717
  projectId: project.id,
718
+ projectName: project.name ?? project.slug,
719
+ workspaceName: workspace.name,
604
720
  oauth2ClientId: clientId,
605
- userLogin: true,
606
721
  auditOnly: false,
607
722
  });
608
- ui.heading("Ory Network connected:");
609
- ui.success("Project connected and OAuth2 client created.");
723
+ ui.success("Saved the Ory Network connection.");
610
724
  ui.summary([
611
725
  ["Project URL", projectUrl],
612
726
  ["OAuth2 client id", clientId],
613
727
  ["Config", (0, config_js_1.getConfigPath)()],
614
728
  ]);
615
- // 8. Best-effort full setup: log the user in now and resolve the agent
616
- // identity, so the very next session is fully wired.
617
- const { authenticated, subject } = await completeFullSetup(binName, harness, ctx);
618
- // 9. Bootstrap permissions via the admin-authenticated `ory` CLI. The
619
- // runtime agent/user tokens can't write relation tuples on a hosted
620
- // project (that's a project-admin operation), but the `ory` session can.
729
+ // Log the user in now and resolve the agent identity, so the very next
730
+ // session is fully wired.
731
+ const { authenticated, subject } = await completeFullSetup(binName, harness, ctx, {
732
+ projectUrl,
733
+ oauth2ClientId: clientId,
734
+ });
735
+ // Grant permissions via the admin-authenticated `ory` CLI. The runtime
736
+ // agent/user tokens can't write relation tuples on a hosted project (that's a
737
+ // project-admin operation), but the `ory` session can. The permission model
738
+ // is provisioned automatically if the project doesn't define the namespace.
621
739
  if (subject) {
622
740
  // Persist the subject *shape* this grant used. If the install resolved a
623
741
  // SubjectSet (ORY_USER_SUBJECT_NAMESPACE was set), a later runtime session
@@ -627,14 +745,13 @@ async function configureNetwork(binName, harness, ctx) {
627
745
  if ("subjectSet" in subject) {
628
746
  (0, config_js_1.saveConfig)({ userSubjectNamespace: subject.subjectSet.namespace });
629
747
  }
630
- await bootstrapPermissionsViaOry(runner, project.id, harness, subject, prompt);
631
- }
632
- // 10. Provision the project API key runtime permission *checks* need. Same
633
- // root cause as bootstrap's admin path — the DCR OAuth2 token can't
634
- // authenticate Ory Network's Permission API — but this credential is used
635
- // at runtime, so it's persisted to config. Placed last so its prompt
636
- // doesn't consume answers meant for earlier steps.
637
- await maybeProvisionProjectApiKey(binName, harness, project.id, projectUrl, prompt, ctx.deps.createProjectApiKeyFn ?? createProjectApiKeyViaConsole, ctx.deps.validateApiKeyFn ?? apiKeyAuthenticates);
748
+ await bootstrapPermissionsViaOry(runner, project.id, harness, subject);
749
+ }
750
+ // Provision the project API key runtime permission *checks* need. Same root
751
+ // cause as the bootstrap's admin path — the DCR OAuth2 token can't
752
+ // authenticate Ory Network's Permission API — but this credential is used at
753
+ // runtime, so it's persisted to config. Minted automatically (no prompt).
754
+ await maybeProvisionProjectApiKey(binName, project.id, projectUrl, ctx.deps.createProjectApiKeyFn ?? project_api_key_js_1.createProjectApiKeyViaConsole, ctx.deps.validateApiKeyFn ?? apiKeyAuthenticates, subject ? (0, setup_actions_js_1.resourceUserLabel)(runner, project.id, subject) : undefined);
638
755
  return {
639
756
  outcome: "network_configured",
640
757
  projectUrl,
@@ -645,10 +762,17 @@ async function configureNetwork(binName, harness, ctx) {
645
762
  skipAutoBootstrap: true,
646
763
  };
647
764
  }
648
- async function completeFullSetup(binName, harness, ctx) {
765
+ async function completeFullSetup(binName, harness, ctx, target = {}) {
649
766
  const clientFactory = ctx.deps.clientFactory ?? client_js_1.OryAgentClient.fromEnv;
650
767
  const userLogin = ctx.deps.userLoginFn ?? user_login_js_1.ensureUserAuthenticated;
651
768
  const agentGate = ctx.deps.agentGateFn ?? agent_auth_js_1.ensureAgentIdentity;
769
+ // Pin the project the wizard just selected so a stale ORY_PROJECT_URL /
770
+ // ORY_SDK_URL / ORY_OAUTH2_CLIENT_ID in the user's shell can't override it and
771
+ // launch the login at the wrong URL (the login gate resolves these env-first).
772
+ // Scoped to the login call and restored after: the later install steps
773
+ // address the project by id (CLI `--project`) rather than the env, so the
774
+ // pin only needs to hold for the browser login.
775
+ const restoreEnv = pinProjectEnv(target);
652
776
  try {
653
777
  const client = clientFactory(harness);
654
778
  ui.heading("Signing you in to the project as the permission subject.");
@@ -656,7 +780,6 @@ async function completeFullSetup(binName, harness, ctx) {
656
780
  const decision = await userLogin(client, {
657
781
  binName,
658
782
  harness,
659
- allowBlock: false,
660
783
  });
661
784
  // Resolve the agent identity regardless — it bootstraps from the user
662
785
  // token when present and no-ops otherwise. Never blocks.
@@ -673,6 +796,32 @@ async function completeFullSetup(binName, harness, ctx) {
673
796
  ui.warning(`Skipping automatic login (will run on first session): ${msg}`);
674
797
  return { authenticated: false, subject: null };
675
798
  }
799
+ finally {
800
+ restoreEnv();
801
+ }
802
+ }
803
+ /**
804
+ * Set ORY_PROJECT_URL / ORY_OAUTH2_CLIENT_ID to the wizard-selected values and
805
+ * return a function that restores their prior values. Used to pin the browser
806
+ * login to the just-selected project without leaking the override past it.
807
+ */
808
+ function pinProjectEnv(target) {
809
+ const prevUrl = process.env.ORY_PROJECT_URL;
810
+ const prevClient = process.env.ORY_OAUTH2_CLIENT_ID;
811
+ if (target.projectUrl)
812
+ process.env.ORY_PROJECT_URL = target.projectUrl;
813
+ if (target.oauth2ClientId)
814
+ process.env.ORY_OAUTH2_CLIENT_ID = target.oauth2ClientId;
815
+ return () => {
816
+ if (prevUrl === undefined)
817
+ delete process.env.ORY_PROJECT_URL;
818
+ else
819
+ process.env.ORY_PROJECT_URL = prevUrl;
820
+ if (prevClient === undefined)
821
+ delete process.env.ORY_OAUTH2_CLIENT_ID;
822
+ else
823
+ process.env.ORY_OAUTH2_CLIENT_ID = prevClient;
824
+ };
676
825
  }
677
826
  /**
678
827
  * Grant the signed-in user `use` on every built-in tool by writing relation
@@ -703,31 +852,22 @@ async function completeFullSetup(binName, harness, ctx) {
703
852
  * than a wall of errors. Observe mode (the install default) keeps tools working
704
853
  * regardless, so this is an optimization, not a hard requirement.
705
854
  */
706
- async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, prompt) {
855
+ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject) {
707
856
  const namespace = process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
708
857
  const tools = (0, tool_catalog_js_1.getToolCatalog)(harness);
709
858
  if (tools.length === 0)
710
859
  return;
711
860
  // Ensure the namespace the tuples reference exists, or the write NotFounds.
712
861
  // We only skip provisioning when the namespace is already defined; every
713
- // other state provisions. When we can fetch the existing OPL source we merge
714
- // our namespace into it (non-destructive); otherwise we create a fresh
715
- // minimal model. The one case we refuse is a model that provably exists but
716
- // whose source we couldn't read — overwriting it would clobber it.
862
+ // other state provisions automatically (no prompt). When we can fetch the
863
+ // existing OPL source we merge our namespace into it (non-destructive);
864
+ // otherwise we create a fresh minimal model. The one case we refuse is a
865
+ // model that provably exists but whose source we couldn't read — overwriting
866
+ // it would clobber it.
717
867
  const model = await inspectPermissionModel(runner, projectId, namespace);
718
868
  if (model.kind === "merge") {
719
869
  // A model exists and we have its OPL source — append our namespace without
720
- // disturbing the others (prompted).
721
- ui.heading(`This project has a permission model, but it doesn't define '${namespace}'.`);
722
- ui.hint("It can be added to the existing model without changing the other namespaces.");
723
- ui.blank();
724
- const answer = await prompt(ui.promptLine(`Add '${namespace}' to this project's permission model now?`, {
725
- hint: "[Y/n]",
726
- }));
727
- if (isNo(answer)) {
728
- printPermissionModelHelp(namespace);
729
- return;
730
- }
870
+ // disturbing the others.
731
871
  if (!provisionPermissionModel(runner, projectId, namespace, model.opl)) {
732
872
  ui.warning(`Could not add '${namespace}' to the permission model automatically.`);
733
873
  printPermissionModelHelp(namespace);
@@ -736,16 +876,8 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
736
876
  ui.success(`Added '${namespace}' to the permission model.`);
737
877
  }
738
878
  else if (model.kind === "empty" || model.kind === "unknown") {
739
- // No model (or one we couldn't read at all) — provision a minimal one
740
- // (prompted). Reading the config failing is treated as "nothing there".
741
- ui.heading(`This project has no '${namespace}' permission model yet —`);
742
- ui.hint("without it, permission checks can't be granted or enforced.");
743
- ui.blank();
744
- const answer = await prompt(ui.promptLine(`Create a minimal '${namespace}' permission model on this project now?`, { hint: "[Y/n]" }));
745
- if (isNo(answer)) {
746
- printPermissionModelHelp(namespace);
747
- return;
748
- }
879
+ // No model (or one we couldn't read at all) — provision a minimal one.
880
+ // Reading the config failing is treated as "nothing there".
749
881
  if (!provisionPermissionModel(runner, projectId, namespace)) {
750
882
  ui.warning(`Could not create the '${namespace}' permission model automatically.`);
751
883
  printPermissionModelHelp(namespace);
@@ -756,8 +888,7 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
756
888
  else if (model.kind === "blocked") {
757
889
  // A model provably exists but we couldn't fetch its source, so we can't
758
890
  // merge and `ory update opl` would overwrite it. Guide instead of clobber.
759
- ui.heading(`This project has a permission model, but it doesn't define '${namespace}'`);
760
- ui.hint("and its source couldn't be read, so adding it automatically might overwrite it.");
891
+ ui.warning(`This project's permission model couldn't be read; '${namespace}' wasn't added.`);
761
892
  printPermissionModelHelp(namespace);
762
893
  return;
763
894
  }
@@ -777,7 +908,6 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
777
908
  relation: "use",
778
909
  ...subjectPatch,
779
910
  }));
780
- ui.heading("Permissions");
781
911
  ui.step(`Granting ${(0, subject_js_1.subjectLabel)(subject)} 'use' on ${tools.length} tools (via the Ory CLI)...`);
782
912
  const r = runner.exec(["create", "relationships", "--project", projectId, "--format", "json", "-f", "-", "--yes"], { input: JSON.stringify(tuples) });
783
913
  if (r.status === 0) {
@@ -960,12 +1090,12 @@ function printPermissionModelHelp(namespace) {
960
1090
  console.warn(ui.style.gray(" }"));
961
1091
  console.warn("");
962
1092
  ui.warnDetail("(drop the `User` class if your model already defines it), then re-run");
963
- ui.warnDetail("install with --reconfigure to grant. You're in observe mode, so tools");
964
- ui.warnDetail("keep working meanwhile.");
1093
+ ui.warnDetail("install to grant. You're in observe mode, so tools keep working");
1094
+ ui.warnDetail("meanwhile.");
965
1095
  return;
966
1096
  }
967
1097
  ui.warnDetail(`Define the '${namespace}' namespace in Ory Permissions, then re-run install`);
968
- ui.warnDetail("with --reconfigure to grant. You're in observe mode, so tools keep working.");
1098
+ ui.warnDetail("to grant. You're in observe mode, so tools keep working.");
969
1099
  }
970
1100
  // ─── ory CLI helpers ──────────────────────────────────────────────────────
971
1101
  function oryCliPresent(runner) {
@@ -1099,7 +1229,7 @@ async function selectOrCreateProject(runner, prompt, workspace, harness, project
1099
1229
  * project" menu option. Returns null on failure.
1100
1230
  */
1101
1231
  async function createProjectInteractive(runner, prompt, workspace, harness) {
1102
- const defaultName = `${harness}-agent`;
1232
+ const defaultName = branding_js_1.DEFAULT_PROJECT_NAME;
1103
1233
  const nameInput = await prompt(ui.promptLine("New project name", { hint: `[${defaultName}]` }));
1104
1234
  const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
1105
1235
  const r = runner.exec([
@@ -1149,27 +1279,11 @@ function dcrEnabled(runner, projectId) {
1149
1279
  return typeof enabled === "boolean" ? enabled : null;
1150
1280
  }
1151
1281
  /**
1152
- * Enable OAuth2 Dynamic Client Registration on the project so the agent
1153
- * identity can self-register (RFC 7591) at runtime. First probes the project's
1154
- * current DCR state and skips silently when it's already enabled. Otherwise
1155
- * prompts (default yes) since it changes project-wide config; best-effort and
1156
- * fail-open — a decline or failure just prints the manual command and continues.
1282
+ * Patch the project's oauth2-config to enable Dynamic Client Registration.
1283
+ * Prompt-free; returns true on success. Shared by the TTY wizard's
1284
+ * {@link maybeEnableDcr} and the web installer API.
1157
1285
  */
1158
- async function maybeEnableDcr(runner, prompt, projectId) {
1159
- if (dcrEnabled(runner, projectId) === true)
1160
- return;
1161
- ui.heading("Dynamic Client Registration");
1162
- ui.info("The agent identity registers itself via OAuth2 Dynamic Client Registration");
1163
- ui.hint("(DCR), which Ory Network projects disable by default.");
1164
- ui.blank();
1165
- const answer = await prompt(ui.promptLine("Enable Dynamic Client Registration on this project?", { hint: "[Y/n]" }));
1166
- const manual = `ory patch oauth2-config --project ${projectId} --replace '${DCR_ENABLED_PATH}=true'`;
1167
- if (isNo(answer)) {
1168
- ui.hint("Skipped. Enable it later with:");
1169
- ui.command(manual);
1170
- ui.hint("or supply static agent credentials (ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET).");
1171
- return;
1172
- }
1286
+ function enableDcr(runner, projectId) {
1173
1287
  const r = runner.exec([
1174
1288
  "patch",
1175
1289
  "oauth2-config",
@@ -1181,21 +1295,138 @@ async function maybeEnableDcr(runner, prompt, projectId) {
1181
1295
  "json",
1182
1296
  "--yes",
1183
1297
  ]);
1184
- if (r.status === 0) {
1185
- ui.success("Dynamic Client Registration enabled.");
1298
+ return r.status === 0;
1299
+ }
1300
+ /** Read the OAuth2 (Hydra) login/consent/logout/registration provider URLs. */
1301
+ function readOauth2UiUrls(runner, projectId) {
1302
+ const r = runner.exec(["get", "oauth2-config", "--project", projectId, "--format", "json"]);
1303
+ if (r.status !== 0)
1304
+ return null;
1305
+ const config = parseObject(r.stdout);
1306
+ if (!config)
1307
+ return null;
1308
+ const urls = config.urls ?? {};
1309
+ const out = [];
1310
+ for (const key of ["login", "consent", "logout", "registration"]) {
1311
+ const u = urls[key];
1312
+ if (typeof u === "string" && u.trim())
1313
+ out.push({ path: `/urls/${key}`, url: u.trim() });
1314
+ }
1315
+ return out;
1316
+ }
1317
+ /** Read the Kratos self-service flow UI URLs (where the browser login lands). */
1318
+ function readIdentityUiUrls(runner, projectId) {
1319
+ const r = runner.exec(["get", "identity-config", "--project", projectId, "--format", "json"]);
1320
+ if (r.status !== 0)
1321
+ return null;
1322
+ const config = parseObject(r.stdout);
1323
+ if (!config)
1324
+ return null;
1325
+ const flows = config.selfservice?.flows ?? {};
1326
+ const out = [];
1327
+ for (const flow of ["login", "registration", "settings", "recovery", "verification", "error"]) {
1328
+ const u = flows[flow]?.ui_url;
1329
+ if (typeof u === "string" && u.trim())
1330
+ out.push({ path: `/selfservice/flows/${flow}/ui_url`, url: u.trim() });
1331
+ }
1332
+ return out;
1333
+ }
1334
+ /**
1335
+ * Classify a single UI URL:
1336
+ * - `managed` — a relative path (`/ui/login`) or an `oryapis`/`ory.sh` host:
1337
+ * the Ory Managed Account Experience, which works for a hosted project.
1338
+ * - `loopback` — a `localhost`/`127.0.0.1` host: Ory's quickstart sample,
1339
+ * reachable only on the machine that ran it, so the hosted login dies here.
1340
+ * - `custom` — any other absolute URL: a deliberate custom UI; leave it alone.
1341
+ */
1342
+ function classifyUiUrl(url) {
1343
+ const u = url.trim();
1344
+ if (!u || u.startsWith("/"))
1345
+ return "managed";
1346
+ let host;
1347
+ try {
1348
+ host = new URL(u).hostname;
1186
1349
  }
1187
- else {
1188
- ui.warning(`Could not enable DCR automatically: ${r.stderr.trim() || "unknown error"}`);
1189
- ui.warnDetail(`Enable it later with: ${manual}`);
1350
+ catch {
1351
+ return "managed"; // unparseable don't touch it
1190
1352
  }
1353
+ if (host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1")
1354
+ return "loopback";
1355
+ if (host === "oryapis.com" || host.endsWith(".oryapis.com") || host.endsWith(".ory.sh"))
1356
+ return "managed";
1357
+ return "custom";
1358
+ }
1359
+ /**
1360
+ * Classify the project's browser-login UI across the identity-config
1361
+ * (self-service) and oauth2-config (login/consent) URLs:
1362
+ * - `loopback` — at least one URL points at a `localhost` sample (needs fixing).
1363
+ * - `custom_external` — a deliberate custom UI is set and none are loopback.
1364
+ * - `managed` — everything is empty / relative / on the managed AX.
1365
+ * - `unknown` — neither config could be read.
1366
+ */
1367
+ function projectLoginUiState(runner, projectId) {
1368
+ const oauth2 = readOauth2UiUrls(runner, projectId);
1369
+ const identity = readIdentityUiUrls(runner, projectId);
1370
+ if (oauth2 === null && identity === null)
1371
+ return "unknown";
1372
+ const entries = [...(oauth2 ?? []), ...(identity ?? [])];
1373
+ const kinds = entries.map((e) => classifyUiUrl(e.url));
1374
+ if (kinds.includes("loopback"))
1375
+ return "loopback";
1376
+ if (kinds.includes("custom"))
1377
+ return "custom_external";
1378
+ return "managed";
1379
+ }
1380
+ /** Clear the loopback UI URLs in one config so it falls back to the managed AX. */
1381
+ function clearLoopbackUiUrls(runner, projectId, config, entries) {
1382
+ if (entries === null)
1383
+ return true; // couldn't read → nothing to clear (best-effort)
1384
+ const ops = [];
1385
+ for (const e of entries) {
1386
+ if (classifyUiUrl(e.url) === "loopback")
1387
+ ops.push("--replace", `${e.path}=`);
1388
+ }
1389
+ if (ops.length === 0)
1390
+ return true;
1391
+ const r = runner.exec([
1392
+ "patch",
1393
+ config,
1394
+ "--project",
1395
+ projectId,
1396
+ ...ops,
1397
+ "--format",
1398
+ "json",
1399
+ "--yes",
1400
+ ]);
1401
+ return r.status === 0;
1191
1402
  }
1192
- /** The `client_name` every plugin-provisioned public PKCE client carries. */
1193
- const OAUTH2_CLIENT_NAME = "ory-agent-plugin";
1403
+ /**
1404
+ * Point the project's browser-login UI at the Ory Managed Account Experience by
1405
+ * clearing any loopback (localhost sample) URLs in *both* the identity-config
1406
+ * (self-service flows) and the oauth2-config (login/consent). Clearing a UI URL
1407
+ * makes Ory Network fall back to the managed AX. Best-effort; returns true when
1408
+ * the patches succeeded (or there was nothing to clear). Never touches a
1409
+ * deliberate non-loopback custom UI.
1410
+ */
1411
+ function useManagedAccountExperience(runner, projectId) {
1412
+ const oauth2Ok = clearLoopbackUiUrls(runner, projectId, "oauth2-config", readOauth2UiUrls(runner, projectId));
1413
+ const identityOk = clearLoopbackUiUrls(runner, projectId, "identity-config", readIdentityUiUrls(runner, projectId));
1414
+ return oauth2Ok && identityOk;
1415
+ }
1416
+ /**
1417
+ * Enable OAuth2 Dynamic Client Registration on the project so the agent
1418
+ * identity can self-register (RFC 7591) at runtime. First probes the project's
1419
+ * current DCR state and skips silently when it's already enabled. Otherwise
1420
+ * prompts (default yes) since it changes project-wide config; best-effort and
1421
+ * fail-open — a decline or failure just prints the manual command and continues.
1422
+ */
1194
1423
  /**
1195
1424
  * Look for an already-provisioned public PKCE client on the project so a
1196
- * re-run reuses it instead of minting a duplicate. Matches by the fixed
1197
- * `client_name` {@link createOAuth2Client} assigns. Best-effort: any CLI or
1198
- * parse failure returns null and the caller falls back to creating one.
1425
+ * re-run or another user installing against the same shared project — reuses
1426
+ * the one client instead of minting a duplicate. Matches the current
1427
+ * {@link USER_LOGIN_CLIENT_NAME} *and* any legacy name an older install may
1428
+ * have used, so a rename never orphans a still-valid client. Best-effort: any
1429
+ * CLI or parse failure returns null and the caller falls back to creating one.
1199
1430
  */
1200
1431
  function findExistingOAuth2Client(runner, projectId) {
1201
1432
  const r = runner.exec([
@@ -1211,7 +1442,7 @@ function findExistingOAuth2Client(runner, projectId) {
1211
1442
  const clients = parseList(r.stdout, "items");
1212
1443
  if (!clients)
1213
1444
  return null;
1214
- const match = clients.find((c) => c.client_name === OAUTH2_CLIENT_NAME && c.client_id);
1445
+ const match = clients.find((c) => c.client_id && c.client_name && branding_js_1.USER_LOGIN_CLIENT_NAMES.includes(c.client_name));
1215
1446
  return match?.client_id ?? null;
1216
1447
  }
1217
1448
  function createOAuth2Client(runner, projectId) {
@@ -1221,7 +1452,7 @@ function createOAuth2Client(runner, projectId) {
1221
1452
  "--project",
1222
1453
  projectId,
1223
1454
  "--name",
1224
- OAUTH2_CLIENT_NAME,
1455
+ branding_js_1.USER_LOGIN_CLIENT_NAME,
1225
1456
  "--grant-type",
1226
1457
  "authorization_code,refresh_token",
1227
1458
  "--response-type",
@@ -1336,10 +1567,33 @@ function isNo(answer) {
1336
1567
  const v = answer.trim().toLowerCase();
1337
1568
  return v === "n" || v === "no";
1338
1569
  }
1570
+ /** Yes only on an explicit y/yes; empty / null / anything else is No. */
1571
+ function isYes(answer) {
1572
+ if (answer === null)
1573
+ return false;
1574
+ const v = answer.trim().toLowerCase();
1575
+ return v === "y" || v === "yes";
1576
+ }
1577
+ /**
1578
+ * A project URL on a loopback host is the self-hosted local stack (the dev
1579
+ * launcher's gateway on localhost), not a hosted Ory Network project — so the
1580
+ * "Mode" summary should read "local stack" for it.
1581
+ */
1582
+ function isLocalProjectUrl(url) {
1583
+ if (!url)
1584
+ return false;
1585
+ try {
1586
+ const host = new URL(url).hostname;
1587
+ return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0" || host === "::1";
1588
+ }
1589
+ catch {
1590
+ return false;
1591
+ }
1592
+ }
1339
1593
  // ─── banners ────────────────────────────────────────────────────────────────
1340
1594
  function printOryCliMissing() {
1341
1595
  ui.heading("The Ory CLI (`ory`) is required to connect to Ory Network but was not found.");
1342
- ui.hint("Install it, then re-run install with --reconfigure:");
1596
+ ui.hint("Install it, then re-run install:");
1343
1597
  ui.summary([
1344
1598
  ["macOS/Linux (Homebrew)", "brew install ory/tap/cli"],
1345
1599
  [
@@ -1364,7 +1618,7 @@ function printManualOAuth2ClientHelp(binName, projectId) {
1364
1618
  ui.heading("Create the public OAuth2 client by hand, then supply its id:");
1365
1619
  ui.blank();
1366
1620
  console.log(ui.style.gray(` ory create oauth2-client --project ${projectId} \\`));
1367
- console.log(ui.style.gray(` --name "ory-agent-plugin" \\`));
1621
+ console.log(ui.style.gray(` --name "${branding_js_1.USER_LOGIN_CLIENT_NAME}" \\`));
1368
1622
  console.log(ui.style.gray(` --grant-type authorization_code,refresh_token \\`));
1369
1623
  console.log(ui.style.gray(` --response-type code \\`));
1370
1624
  console.log(ui.style.gray(` --scope openid,offline_access \\`));
@@ -1376,68 +1630,6 @@ function printManualOAuth2ClientHelp(binName, projectId) {
1376
1630
  ui.blank();
1377
1631
  ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --oauth2-client-id <CLIENT_ID>`);
1378
1632
  }
1379
- /** Ory Console API base. The `ory` CLI has no project-API-key command, so key
1380
- * provisioning talks to the Console API directly, authenticated with the CLI's
1381
- * stored Ory Network session. Override with `ORY_CONSOLE_API_URL`. */
1382
- const CONSOLE_API_URL = process.env.ORY_CONSOLE_API_URL?.trim() || "https://api.console.ory.sh";
1383
- /** Path to the session the `ory` CLI persists after `ory auth`. */
1384
- const ORY_CLI_SESSION_FILE = ".ory-cloud.json";
1385
- /**
1386
- * Read the Ory Network session bearer the `ory` CLI stores at
1387
- * `~/.ory-cloud.json`. Returns null when the file is absent, unparseable, has
1388
- * no token, or the token has expired. Never throws.
1389
- */
1390
- function readConsoleSessionToken() {
1391
- try {
1392
- const raw = fs.readFileSync(path.join(os.homedir(), ORY_CLI_SESSION_FILE), "utf-8");
1393
- const parsed = JSON.parse(raw);
1394
- const token = parsed.access_token?.access_token;
1395
- if (!token)
1396
- return null;
1397
- const expiry = parsed.access_token?.expiry;
1398
- if (expiry) {
1399
- const expMs = Date.parse(expiry);
1400
- if (Number.isFinite(expMs) && expMs <= Date.now())
1401
- return null;
1402
- }
1403
- return token;
1404
- }
1405
- catch {
1406
- return null;
1407
- }
1408
- }
1409
- /**
1410
- * Mint an Ory Network **project API key** via the Console API
1411
- * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
1412
- * Returns the `ory_pat_…` value, or null on any failure (no session, non-2xx,
1413
- * missing value). Never throws — provisioning is best-effort.
1414
- *
1415
- * This is the one place the wizard reaches past the `ory` CLI to the Console
1416
- * API: the CLI (as of v1.3) exposes no project-API-key command, and Keto
1417
- * permission checks can't authenticate without one.
1418
- */
1419
- async function createProjectApiKeyViaConsole(args) {
1420
- const token = readConsoleSessionToken();
1421
- if (!token)
1422
- return null;
1423
- try {
1424
- const res = await fetch(`${CONSOLE_API_URL}/projects/${args.projectId}/tokens`, {
1425
- method: "POST",
1426
- headers: {
1427
- Authorization: `Bearer ${token}`,
1428
- "Content-Type": "application/json",
1429
- },
1430
- body: JSON.stringify({ name: args.name }),
1431
- });
1432
- if (!res.ok)
1433
- return null;
1434
- const body = (await res.json());
1435
- return typeof body.value === "string" && body.value.length > 0 ? body.value : null;
1436
- }
1437
- catch {
1438
- return null;
1439
- }
1440
- }
1441
1633
  /**
1442
1634
  * Best-effort check that an already-stored project API key still authenticates
1443
1635
  * against Keto. Runs one permission check with the key: if it completes (any
@@ -1488,7 +1680,7 @@ function printManualApiKeySteps(binName, projectId) {
1488
1680
  * TTY / any failure falls back to manual guidance. No-ops when a key is already
1489
1681
  * configured (env or config file).
1490
1682
  */
1491
- async function maybeProvisionProjectApiKey(binName, harness, projectId, projectUrl, prompt, createFn, validateFn) {
1683
+ async function maybeProvisionProjectApiKey(binName, projectId, projectUrl, createFn, validateFn, userLabel) {
1492
1684
  const resolved = (0, config_js_1.resolveConfig)();
1493
1685
  if (resolved.apiKey) {
1494
1686
  // An operator-set env key is authoritative — never touch it.
@@ -1498,49 +1690,45 @@ async function maybeProvisionProjectApiKey(binName, harness, projectId, projectU
1498
1690
  // deleted — must not wedge setup: validate it and re-provision if it's
1499
1691
  // dead. Presence alone isn't enough (a dangling key still fails at runtime
1500
1692
  // with session_inactive).
1501
- if (await validateFn(resolved.apiKey, projectUrl))
1693
+ if (await validateFn(resolved.apiKey, projectUrl)) {
1694
+ ui.success("Stored project API key is valid.");
1502
1695
  return;
1503
- ui.heading("Project API key");
1504
- ui.warning("The stored project API key is no longer valid (revoked or deleted).");
1505
- }
1506
- else {
1507
- ui.heading("Project API key");
1508
- ui.info("Runtime permission checks call Ory's Permission API, which authenticates");
1509
- ui.hint("with a project API key — the agent's OAuth2 token isn't accepted there.");
1510
- ui.hint("(Observe mode works without it; enforce mode needs it to allow tools.)");
1511
- }
1512
- ui.blank();
1513
- const answer = await prompt(ui.promptLine("Create a project API key now and store it?", { hint: "[Y/n]" }));
1514
- // `null` = no TTY / cancelled; treat as skip so we never mint without consent.
1515
- if (answer !== null && !isNo(answer)) {
1516
- ui.step("Creating a project API key via the Ory Console…");
1517
- let key = null;
1696
+ }
1697
+ ui.warning("The stored project API key is no longer valid (revoked or deleted) — replacing it.");
1698
+ }
1699
+ // Runtime permission checks call Ory's Permission API, which authenticates
1700
+ // with a project API key (the agent's OAuth2 token isn't accepted there), so
1701
+ // one is minted automatically. Observe mode works without it; enforce mode
1702
+ // needs it to allow tools.
1703
+ ui.step("Creating a project API key via the Ory Console…");
1704
+ let created = null;
1705
+ try {
1706
+ // Name the key for the signed-in user + host so that, on a shared project
1707
+ // where every user mints their own, a team admin can tell whose key is
1708
+ // whose and prune stale ones. The Console's "Date Added" column already
1709
+ // shows when each key was created, so no timestamp in the name.
1710
+ let host = "unknown-host";
1518
1711
  try {
1519
- // Name each key uniquely (harness + host + timestamp) so keys are
1520
- // distinguishable in the Console and a re-provision never collides with
1521
- // an existing entry.
1522
- key = await createFn({ projectId, name: projectApiKeyName(harness) });
1712
+ host = os.hostname();
1523
1713
  }
1524
1714
  catch {
1525
- key = null;
1526
- }
1527
- if (key) {
1528
- (0, config_js_1.saveConfig)({ apiKey: key });
1529
- ui.success("Project API key created and stored — permission checks will use it.");
1530
- return;
1715
+ /* keep fallback */
1531
1716
  }
1532
- ui.warning("Couldn't create a project API key automatically (Console session may be missing or expired).");
1533
- }
1534
- printManualApiKeySteps(binName, projectId);
1535
- }
1536
- /** Distinguishable, collision-free label for a provisioned project API key. */
1537
- function projectApiKeyName(harness) {
1538
- let host = "unknown-host";
1539
- try {
1540
- host = os.hostname();
1717
+ created = await createFn({
1718
+ projectId,
1719
+ name: (0, branding_js_1.projectApiKeyName)({ host, user: userLabel }),
1720
+ });
1541
1721
  }
1542
1722
  catch {
1543
- /* keep fallback */
1723
+ created = null;
1544
1724
  }
1545
- return `ory-agent-plugins ${harness} ${host} ${new Date().toISOString()}`;
1725
+ if (created) {
1726
+ // Persist the token id alongside the secret so uninstall can delete the key
1727
+ // server-side (deletion addresses the token by id, not by its secret value).
1728
+ (0, config_js_1.saveConfig)({ apiKey: created.value, apiKeyId: created.id ?? null });
1729
+ ui.success("Project API key created and stored — permission checks will use it.");
1730
+ return;
1731
+ }
1732
+ ui.warning("Couldn't create a project API key automatically (Console session may be missing or expired).");
1733
+ printManualApiKeySteps(binName, projectId);
1546
1734
  }