@ory/argus 0.14.0 → 1.0.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 (150) hide show
  1. package/README.md +31 -46
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +39 -91
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
  7. package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +88 -32
  10. package/dist/adapters.js +443 -164
  11. package/dist/agent-auth.d.ts +226 -67
  12. package/dist/agent-auth.js +951 -205
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +33 -4
  16. package/dist/auth.js +163 -20
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +77 -16
  20. package/dist/branding.js +93 -23
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -239
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +217 -54
  29. package/dist/config.js +461 -42
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +3 -5
  33. package/dist/contract-suite.js +75 -47
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +28 -23
  47. package/dist/index.js +173 -58
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.js +7 -7
  54. package/dist/local/index.d.ts +2 -2
  55. package/dist/local/index.js +24 -10
  56. package/dist/local/manager.d.ts +20 -1
  57. package/dist/local/manager.js +159 -36
  58. package/dist/local/ports.d.ts +158 -0
  59. package/dist/local/ports.js +443 -0
  60. package/dist/local/seed.d.ts +13 -5
  61. package/dist/local/seed.js +62 -38
  62. package/dist/logger.d.ts +54 -25
  63. package/dist/logger.js +329 -63
  64. package/dist/mcp.d.ts +2 -2
  65. package/dist/mcp.js +10 -5
  66. package/dist/mirror-bootstrap.d.ts +48 -0
  67. package/dist/mirror-bootstrap.js +254 -0
  68. package/dist/opl.d.ts +289 -0
  69. package/dist/opl.js +446 -0
  70. package/dist/permission-mode.d.ts +87 -0
  71. package/dist/permission-mode.js +307 -0
  72. package/dist/permissions-cli.d.ts +13 -49
  73. package/dist/permissions-cli.js +154 -348
  74. package/dist/permissions.d.ts +148 -38
  75. package/dist/permissions.js +591 -45
  76. package/dist/post-install.d.ts +33 -0
  77. package/dist/post-install.js +127 -0
  78. package/dist/read-credential.d.ts +65 -0
  79. package/dist/read-credential.js +86 -0
  80. package/dist/registry/cli.js +5 -2
  81. package/dist/registry/config.d.ts +0 -17
  82. package/dist/registry/config.js +0 -23
  83. package/dist/registry/index.d.ts +1 -1
  84. package/dist/registry/index.js +2 -2
  85. package/dist/registry/manager.d.ts +4 -21
  86. package/dist/registry/manager.js +21 -37
  87. package/dist/runtime-credential.d.ts +140 -0
  88. package/dist/runtime-credential.js +572 -0
  89. package/dist/runtime.d.ts +413 -0
  90. package/dist/runtime.js +825 -0
  91. package/dist/setup.d.ts +23 -47
  92. package/dist/setup.js +59 -116
  93. package/dist/skills.js +0 -7
  94. package/dist/status-cli.d.ts +27 -11
  95. package/dist/status-cli.js +121 -115
  96. package/dist/status-data.d.ts +116 -17
  97. package/dist/status-data.js +121 -38
  98. package/dist/subject.d.ts +126 -20
  99. package/dist/subject.js +215 -30
  100. package/dist/testing.d.ts +74 -38
  101. package/dist/testing.js +185 -68
  102. package/dist/tool-catalog.d.ts +53 -11
  103. package/dist/tool-catalog.js +164 -13
  104. package/dist/tool-metadata.d.ts +7 -6
  105. package/dist/tool-metadata.js +6 -5
  106. package/dist/types.d.ts +11 -1
  107. package/dist/uninstall.d.ts +71 -34
  108. package/dist/uninstall.js +217 -85
  109. package/dist/user-login.d.ts +9 -10
  110. package/dist/user-login.js +56 -75
  111. package/dist/watch-cli.d.ts +6 -0
  112. package/dist/watch-cli.js +217 -0
  113. package/package.json +4 -27
  114. package/assets/commands/dashboard.md +0 -34
  115. package/dist/dashboard-cli.d.ts +0 -8
  116. package/dist/dashboard-cli.js +0 -70
  117. package/dist/dev.d.ts +0 -103
  118. package/dist/dev.js +0 -583
  119. package/dist/interactive-setup.d.ts +0 -286
  120. package/dist/interactive-setup.js +0 -1734
  121. package/dist/local/jaeger-main.d.ts +0 -13
  122. package/dist/local/jaeger-main.js +0 -85
  123. package/dist/local/jaeger.d.ts +0 -50
  124. package/dist/local/jaeger.js +0 -162
  125. package/dist/otel/exporter.d.ts +0 -17
  126. package/dist/otel/exporter.js +0 -12
  127. package/dist/otel/index.d.ts +0 -2
  128. package/dist/otel/index.js +0 -8
  129. package/dist/otel/otlp.d.ts +0 -103
  130. package/dist/otel/otlp.js +0 -385
  131. package/dist/project-api-key.d.ts +0 -69
  132. package/dist/project-api-key.js +0 -147
  133. package/dist/setup-actions.d.ts +0 -232
  134. package/dist/setup-actions.js +0 -507
  135. package/dist/tracer.d.ts +0 -190
  136. package/dist/tracer.js +0 -481
  137. package/dist/watch-sandbox.d.ts +0 -9
  138. package/dist/watch-sandbox.js +0 -81
  139. package/dist/web/api.d.ts +0 -33
  140. package/dist/web/api.js +0 -294
  141. package/dist/web/launch.d.ts +0 -11
  142. package/dist/web/launch.js +0 -96
  143. package/dist/web/server.d.ts +0 -20
  144. package/dist/web/server.js +0 -233
  145. package/dist/web/types.d.ts +0 -65
  146. package/dist/web/types.js +0 -2
  147. package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
  148. package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
  149. package/dist/webapp/favicon.ico +0 -0
  150. package/dist/webapp/index.html +0 -15
@@ -1,1734 +0,0 @@
1
- "use strict";
2
- /**
3
- * Interactive install wizard. Runs after a harness plugin has been
4
- * registered, stepping the user through connecting the plugin to Ory:
5
- *
6
- * 1. Choose a connection: **Ory Network** (browser login / account
7
- * creation) or **local / audit-only** (no project, audit logging only).
8
- * 2. Ory Network branch: shell out to the official `ory` CLI to log in,
9
- * pick a workspace, pick a project (→ `projectUrl`), and create the
10
- * public OAuth2 client the PKCE user login needs (→ `oauth2ClientId`).
11
- * 3. Persist the resolved values to the shared config file and enable the
12
- * interactive user login.
13
- * 4. Best-effort "full setup": run the PKCE user login now (we hold the
14
- * project URL + client id), resolve the agent DCR identity, and let the
15
- * caller bootstrap permissions from the freshly-cached user identity.
16
- *
17
- * The whole wizard is **fail-open and skippable**: on a missing TTY, a
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
- *
23
- * The Ory Network integration deliberately reuses the official `ory` CLI
24
- * rather than re-implementing the Console API: the CLI already owns Network
25
- * browser login + account creation, workspace/project listing, and OAuth2
26
- * client creation, and the client it creates is exactly the one the plugin
27
- * READMEs document for manual setup. The one exception is minting the project
28
- * API key that runtime permission checks need — the CLI exposes no command for
29
- * it, so that single step calls the Console API directly, reusing the CLI's
30
- * stored session for auth (see {@link createProjectApiKeyViaConsole}).
31
- */
32
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
- if (k2 === undefined) k2 = k;
34
- var desc = Object.getOwnPropertyDescriptor(m, k);
35
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
- desc = { enumerable: true, get: function() { return m[k]; } };
37
- }
38
- Object.defineProperty(o, k2, desc);
39
- }) : (function(o, m, k, k2) {
40
- if (k2 === undefined) k2 = k;
41
- o[k2] = m[k];
42
- }));
43
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
44
- Object.defineProperty(o, "default", { enumerable: true, value: v });
45
- }) : function(o, v) {
46
- o["default"] = v;
47
- });
48
- var __importStar = (this && this.__importStar) || (function () {
49
- var ownKeys = function(o) {
50
- ownKeys = Object.getOwnPropertyNames || function (o) {
51
- var ar = [];
52
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
53
- return ar;
54
- };
55
- return ownKeys(o);
56
- };
57
- return function (mod) {
58
- if (mod && mod.__esModule) return mod;
59
- var result = {};
60
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
61
- __setModuleDefault(result, mod);
62
- return result;
63
- };
64
- })();
65
- Object.defineProperty(exports, "__esModule", { value: true });
66
- exports.ORY_CLI_NPM_SPEC = exports.LOOPBACK_REDIRECT_URIS = void 0;
67
- exports.createOryRunner = createOryRunner;
68
- exports.installOryCli = installOryCli;
69
- exports.runPostInstall = runPostInstall;
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;
83
- exports.projectUrlFromSlug = projectUrlFromSlug;
84
- exports.isLocalProjectUrl = isLocalProjectUrl;
85
- exports.apiKeyAuthenticates = apiKeyAuthenticates;
86
- const node_child_process_1 = require("node:child_process");
87
- const fs = __importStar(require("node:fs"));
88
- const os = __importStar(require("node:os"));
89
- const path = __importStar(require("node:path"));
90
- const tty = __importStar(require("node:tty"));
91
- const readline = __importStar(require("node:readline"));
92
- const config_js_1 = require("./config.js");
93
- const setup_js_1 = require("./setup.js");
94
- const cli_js_1 = require("./cli.js");
95
- const cli_invocation_js_1 = require("./cli-invocation.js");
96
- const auth_js_1 = require("./auth.js");
97
- const client_js_1 = require("./client.js");
98
- const logger_js_1 = require("./logger.js");
99
- const user_login_js_1 = require("./user-login.js");
100
- const agent_auth_js_1 = require("./agent-auth.js");
101
- const tool_catalog_js_1 = require("./tool-catalog.js");
102
- const index_js_1 = require("./local/index.js");
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");
106
- const permissions_cli_js_1 = require("./permissions-cli.js");
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");
110
- const ui = __importStar(require("./ui.js"));
111
- /** Loopback redirect URIs the PKCE user login expects on the OAuth2 client. */
112
- exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`);
113
- /**
114
- * Build a runner bound to a specific `ory` binary (a bare command resolved on
115
- * `PATH`, or an absolute path to a plugin-installed binary). {@link defaultRunner}
116
- * uses the bare `"ory"`; {@link installOryCli} returns one bound to the binary
117
- * it just installed.
118
- */
119
- function createOryRunner(oryBin) {
120
- return {
121
- bin: oryBin,
122
- exec(args, opts) {
123
- const r = (0, node_child_process_1.spawnSync)(oryBin, args, {
124
- encoding: "utf-8",
125
- ...(opts?.input !== undefined ? { input: opts.input } : {}),
126
- });
127
- return {
128
- status: r.status,
129
- stdout: r.stdout ?? "",
130
- stderr: r.stderr ?? "",
131
- };
132
- },
133
- execInteractive(args) {
134
- const r = (0, node_child_process_1.spawnSync)(oryBin, args, { stdio: "inherit" });
135
- return r.status ?? 1;
136
- },
137
- };
138
- }
139
- const defaultRunner = createOryRunner("ory");
140
- /**
141
- * npm spec for the Ory CLI installed on demand when the `ory` binary isn't on
142
- * `PATH`. `@ory/cli` ships the platform `ory` binary and exposes it as its
143
- * `bin`, so an isolated `npm install` gives us a working CLI without touching
144
- * the user's global environment. Pinned to a major to stay reproducible while
145
- * still picking up patch/minor fixes.
146
- */
147
- exports.ORY_CLI_NPM_SPEC = "@ory/cli@^1";
148
- /**
149
- * Best-effort install of the Ory CLI into a plugin-managed directory under the
150
- * shared data dir, returning a runner bound to the installed binary (or null
151
- * on any failure). Uses `npm install --prefix` so nothing global is modified;
152
- * the binary lands at `<dataDir>/cli/node_modules/.bin/ory`. Never throws.
153
- */
154
- async function installOryCli() {
155
- const dir = path.join((0, config_js_1.getDataDir)(), "cli");
156
- try {
157
- fs.mkdirSync(dir, { recursive: true });
158
- }
159
- catch {
160
- return null;
161
- }
162
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
163
- const r = (0, node_child_process_1.spawnSync)(npm, ["install", "--prefix", dir, "--no-save", "--no-audit", "--no-fund", exports.ORY_CLI_NPM_SPEC], { encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"] });
164
- if (r.status !== 0)
165
- return null;
166
- const binName = process.platform === "win32" ? "ory.cmd" : "ory";
167
- const bin = path.join(dir, "node_modules", ".bin", binName);
168
- if (!fs.existsSync(bin))
169
- return null;
170
- const runner = createOryRunner(bin);
171
- // Confirm the freshly installed binary actually runs before handing it back.
172
- return oryCliPresent(runner) ? runner : null;
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
- }
188
- /**
189
- * The shared post-install entry point every harness's `install` command
190
- * calls. Runs the interactive setup wizard, then (using whatever identity the
191
- * wizard cached) bootstraps permissions and prints the onboarding banner.
192
- *
193
- * This is the single seam that centralizes what each harness previously
194
- * open-coded as a local `postInstallPermissions` helper.
195
- */
196
- async function runPostInstall(binName, harness, args = [], deps = {}) {
197
- // Surface the running CLI version up front. `npx -p @ory/<harness>` (no
198
- // version pin) will happily reuse a previously-cached install rather than
199
- // re-resolve to latest, so a user can silently be running an old CLI whose
200
- // install flow predates newer behavior (e.g. the interactive setup wizard)
201
- // while believing they're on the current release. Printing the version makes
202
- // that mismatch visible at a glance; the README's troubleshooting note
203
- // explains how to clear a stale npx cache.
204
- const cliVersion = (0, version_cli_js_1.resolveCoreVersion)();
205
- if (cliVersion !== "unknown") {
206
- ui.info(`${binName} v${cliVersion}`);
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
- }
237
- const result = await runInteractiveSetup(binName, harness, args, deps);
238
- // The Ory Network wizard path bootstraps permissions itself (via the
239
- // admin-authenticated `ory` CLI). Only fall back to the DCR-token bootstrap
240
- // when the wizard didn't handle it — otherwise we'd emit 403s on a hosted
241
- // project whose relation-tuple API rejects the agent/user token.
242
- const bootstrapped = result.skipAutoBootstrap
243
- ? false
244
- : await (0, permissions_cli_js_1.maybeAutoBootstrap)(binName, harness);
245
- (0, permissions_cli_js_1.printPermissionsOnboardingHelp)(binName, harness, {
246
- bootstrappedAutomatically: bootstrapped,
247
- });
248
- // Flush any next-steps guidance the harness's install() deferred, so it
249
- // lands at the very end of the interactive flow rather than above the
250
- // wizard's first prompt. When the wizard already connected Ory, the renderer
251
- // prints a short launch summary instead of the manual setup steps — those
252
- // would only contradict the values the wizard just saved.
253
- (0, setup_js_1.emitDeferredNextSteps)(isConfiguredOutcome(result.outcome));
254
- }
255
- /**
256
- * Outcomes where Ory is already wired up by the time the wizard returns —
257
- * either the user connected it now (network / local / audit-only) or it was
258
- * configured on a previous run. For these the closing next-steps should be a
259
- * short "launch the harness" summary, not the manual env-var guidance.
260
- */
261
- function isConfiguredOutcome(outcome) {
262
- return (outcome === "network_configured" ||
263
- outcome === "local_configured" ||
264
- outcome === "audit_only" ||
265
- outcome === "skipped_configured");
266
- }
267
- /**
268
- * Run the interactive setup wizard. Always resolves; never throws.
269
- */
270
- async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
271
- const runner = deps.runner ?? defaultRunner;
272
- const ttyAvailable = deps.isTtyAvailableFn ?? cli_js_1.isTtyAvailable;
273
- const noConfigure = args.includes("--no-configure");
274
- const reconfigure = args.includes("--reconfigure");
275
- if (noConfigure) {
276
- return { outcome: "skipped_flag" };
277
- }
278
- if (!ttyAvailable()) {
279
- // Non-interactive (CI/headless) — preserve the pre-wizard behavior and
280
- // point the operator at the manual, scriptable path.
281
- printManualSetup(binName);
282
- return { outcome: "skipped_no_tty" };
283
- }
284
- // The wizard prompts several times in a row. Opening `/dev/tty` afresh for
285
- // each prompt (as `promptOnTty` does) leaves a blocking read stuck in
286
- // libuv's threadpool per call, and after a few prompts the pool is
287
- // exhausted and the next prompt hangs. So when we're driving real TTY input
288
- // (no injected promptFn), open one readline interface and reuse it for the
289
- // whole flow. Tests inject their own promptFn and skip this entirely.
290
- // One persistent tty prompter, but recreated around any interactive child
291
- // process. A readline over `/dev/tty` that stays open across a
292
- // `spawnSync(..., { stdio: "inherit" })` (how `execInteractive` runs the
293
- // `ory auth` browser login) stops receiving input once the child returns —
294
- // the terminal is left in a state the parent readline no longer reads from,
295
- // so the *next* prompt (e.g. workspace selection) hangs forever with no
296
- // output. `runInteractive` closes the prompter before the child and reopens
297
- // a fresh one after, which reliably restores line input.
298
- let ttyPrompter = deps.promptFn ? null : createTtyPrompter();
299
- const prompt = (q) => (deps.promptFn ?? ttyPrompter?.prompt ?? cli_js_1.promptOnTty)(q);
300
- const runInteractive = (fn) => {
301
- if (deps.promptFn || !ttyPrompter)
302
- return fn();
303
- ttyPrompter.close();
304
- ttyPrompter = null;
305
- try {
306
- return fn();
307
- }
308
- finally {
309
- ttyPrompter = createTtyPrompter();
310
- }
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
- }
345
- // Quiet the structured stderr firehose for the duration of the wizard so
346
- // `[ory-agent] {...}` debug lines don't interleave with the human-facing UI
347
- // when a session runs with ORY_AGENT_DEBUG=true (the dev launcher always
348
- // sets it). Debug still lands in the log file.
349
- try {
350
- return await (0, logger_js_1.withQuietStderr)(() => runWizard(binName, harness, { runner, prompt, deps, runInteractive }));
351
- }
352
- catch (err) {
353
- const msg = err instanceof Error ? err.message : String(err);
354
- console.warn("");
355
- ui.warning(`Interactive setup did not complete: ${msg}`);
356
- printManualSetup(binName);
357
- return { outcome: "network_fallback" };
358
- }
359
- finally {
360
- ttyPrompter?.close();
361
- }
362
- }
363
- /**
364
- * Open `/dev/tty` once and wrap it in a single readline interface, reused for
365
- * every prompt in the wizard.
366
- *
367
- * Crucially this uses `tty.ReadStream`, not `fs.createReadStream`. Reading a
368
- * tty via an fs stream issues a *blocking* `read()` in libuv's threadpool that
369
- * can't be cancelled; a pending one exhausts the pool (hanging mid-flow) and,
370
- * worse, blocks `process.exit()` from ever completing (hanging at the very
371
- * end). `tty.ReadStream` uses libuv's non-blocking tty handle instead, which is
372
- * event-loop integrated and tears down cleanly on exit. Returns null if
373
- * `/dev/tty` can't be opened (caller falls back to `promptOnTty`).
374
- */
375
- function createTtyPrompter() {
376
- let fd;
377
- try {
378
- fd = fs.openSync("/dev/tty", "r");
379
- }
380
- catch {
381
- return null;
382
- }
383
- let input;
384
- try {
385
- input = new tty.ReadStream(fd);
386
- }
387
- catch {
388
- try {
389
- fs.closeSync(fd);
390
- }
391
- catch {
392
- /* ignore */
393
- }
394
- return null;
395
- }
396
- const rl = readline.createInterface({ input, output: process.stderr });
397
- return {
398
- prompt: (question) => new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim()))),
399
- close: () => {
400
- rl.close();
401
- input.destroy();
402
- try {
403
- fs.closeSync(fd);
404
- }
405
- catch {
406
- /* fd may already be closed by the stream */
407
- }
408
- },
409
- };
410
- }
411
- async function runWizard(binName, harness, ctx) {
412
- ui.banner("Ory Agent Plugin", "Connect your agent to Ory");
413
- ui.heading("How do you want to connect?");
414
- ui.blank();
415
- ui.menu([
416
- {
417
- key: "1",
418
- label: "Ory Network",
419
- description: "sign in (or create an account) and connect a hosted project",
420
- },
421
- {
422
- key: "2",
423
- label: "Local stack",
424
- description: "self-hosted Ory via `local up` (localhost:4000), seeded for you",
425
- },
426
- {
427
- key: "3",
428
- label: "Audit-only",
429
- description: "no Ory project (traces tool calls locally, no auth or checks)",
430
- },
431
- ]);
432
- ui.blank();
433
- const choice = await ctx.prompt(ui.promptLine("Enter 1, 2 or 3", { hint: "[1]" }));
434
- // Default to the Ory Network path (empty / "1"). "2" wires up the local
435
- // stack — where every value comes from the seeded stack, no prompts needed.
436
- // "3" is the no-project audit-only fallback.
437
- if (isAuditChoice(choice)) {
438
- return configureAuditOnly(binName);
439
- }
440
- if (isLocalChoice(choice)) {
441
- return configureLocalStack(binName, harness, ctx);
442
- }
443
- return configureNetwork(binName, harness, ctx);
444
- }
445
- function configureAuditOnly(binName) {
446
- (0, config_js_1.saveConfig)({ auditOnly: true });
447
- ui.heading("Audit-only mode");
448
- ui.success("Configured for local / audit-only mode.");
449
- ui.hint("Tool calls are traced locally; no Ory project, auth, or permission checks.");
450
- ui.summary([["Config", (0, config_js_1.getConfigPath)()]]);
451
- ui.blank();
452
- ui.hint(`To connect to Ory later, re-run install, or run:`);
453
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
454
- return { outcome: "audit_only" };
455
- }
456
- /**
457
- * Configure the plugin against the local Ory stack. Every value the plugin
458
- * needs — the project URL (`http://localhost:4000`), the public OAuth2 client
459
- * for the PKCE login (the seeded `ory-user-local`), and the tool permissions —
460
- * is supplied by the stack itself, so this path takes **no** prompts: the local
461
- * stack is the default source of otherwise-unprovided values.
462
- *
463
- * The flow brings the stack up (idempotent; Docker required), seeds the test
464
- * identity + `use` permissions, persists the local connection details, and runs
465
- * the login + agent DCR so the next session is fully wired. Every step is
466
- * fail-open: if Docker isn't available we still persist the local config and
467
- * tell the user to run `local up` when they can, so the plugin is pointed at
468
- * the local stack either way.
469
- */
470
- async function configureLocalStack(binName, harness, ctx) {
471
- const bringUp = ctx.deps.localStackFn ?? index_js_1.ensureLocalOryStack;
472
- const seed = ctx.deps.seedFn ?? index_js_1.seedLocalEnvironment;
473
- ui.heading("Local Ory stack");
474
- ui.step("Setting up the local Ory stack (Kratos, Keto, Hydra, gateway on :4000).");
475
- ui.hint("This requires Docker and may take a minute on first run.");
476
- let stackStatus;
477
- let stackDetail;
478
- try {
479
- const stack = await bringUp();
480
- stackStatus = stack.status;
481
- stackDetail = stack.detail;
482
- }
483
- catch (err) {
484
- stackStatus = "compose-failed";
485
- stackDetail = err instanceof Error ? err.message : String(err);
486
- }
487
- const running = stackStatus === "started" || stackStatus === "already-running";
488
- // Point the plugin at the local stack regardless of whether it came up: the
489
- // seeded PKCE client id is stable, so this config is valid the moment the
490
- // stack is running (now, or after a later `local up`). Persist the seeded
491
- // subject namespace too so permission checks address the user as the same
492
- // SubjectSet (`User:<id>`) the seed writes — without it, enforce mode would
493
- // never match the seeded tuples.
494
- (0, config_js_1.saveConfig)({
495
- projectUrl: index_js_1.GATEWAY_URL,
496
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
497
- userSubjectNamespace: index_js_1.USER_SUBJECT_NAMESPACE,
498
- auditOnly: false,
499
- });
500
- if (!running) {
501
- console.warn("");
502
- ui.warning(`Could not start the local stack: ${stackDetail ?? stackStatus}.`);
503
- ui.warnDetail("The plugin is still configured for the local stack — start it when Docker is ready:");
504
- ui.warnDetail(`${(0, cli_invocation_js_1.oryNpx)(binName)} local up`);
505
- printLocalStackSummary();
506
- return {
507
- outcome: "local_configured",
508
- projectUrl: index_js_1.GATEWAY_URL,
509
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
510
- userAuthenticated: false,
511
- // The stack seeds its own permissions on `local up`; never run the
512
- // hosted-project admin bootstrap against localhost.
513
- skipAutoBootstrap: true,
514
- };
515
- }
516
- // Seed the identity + `use` permissions into the local stack. Best-effort:
517
- // observe mode (the install default) keeps tools working even if this fails.
518
- let credentials = null;
519
- try {
520
- const result = await seed();
521
- credentials = {
522
- email: result.user.identity.email,
523
- password: result.user.password,
524
- };
525
- ui.success(`Seeded ${result.permissions.tuples} permissions in '${result.permissions.namespace}' for ${result.permissions.subject}.`);
526
- }
527
- catch (err) {
528
- const msg = err instanceof Error ? err.message : String(err);
529
- ui.warning(`Could not seed the local stack: ${msg}`);
530
- ui.warnDetail(`Seed it later with: ${(0, cli_invocation_js_1.oryNpx)(binName)} local seed`);
531
- }
532
- printLocalStackSummary();
533
- if (credentials) {
534
- ui.heading("Sign in to the browser login with the seeded test user:");
535
- ui.summary([
536
- ["Email", credentials.email],
537
- ["Password", credentials.password],
538
- ]);
539
- }
540
- const { authenticated } = await completeFullSetup(binName, harness, ctx, {
541
- projectUrl: index_js_1.GATEWAY_URL,
542
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
543
- });
544
- return {
545
- outcome: "local_configured",
546
- projectUrl: index_js_1.GATEWAY_URL,
547
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
548
- userAuthenticated: authenticated,
549
- skipAutoBootstrap: true,
550
- };
551
- }
552
- function printLocalStackSummary() {
553
- ui.heading("Local stack configured:");
554
- ui.summary([
555
- ["Project URL", index_js_1.GATEWAY_URL],
556
- ["OAuth2 client id", index_js_1.USER_CLIENT_ID],
557
- ["Subject namespace", `${index_js_1.USER_SUBJECT_NAMESPACE} (matches seeded permissions)`],
558
- ["Config", (0, config_js_1.getConfigPath)()],
559
- ]);
560
- }
561
- async function configureNetwork(binName, harness, ctx) {
562
- const { prompt } = ctx;
563
- // 1. Ensure the `ory` CLI is available — offer to install it if it isn't,
564
- // so a from-scratch user (no CLI on PATH) doesn't dead-end here. On accept
565
- // we install `@ory/cli` into a plugin-managed dir and switch the runner to
566
- // the freshly installed binary; on decline/failure we fall back to manual.
567
- let runner = ctx.runner;
568
- if (!oryCliPresent(runner)) {
569
- const installed = await maybeInstallOryCli(ctx, prompt);
570
- if (!installed) {
571
- printOryCliMissing();
572
- printManualSetup(binName);
573
- return { outcome: "network_fallback" };
574
- }
575
- runner = installed;
576
- }
577
- // 2. Ensure the user is signed in. `ory` persists its own session, so probe
578
- // first (listing workspaces) and only launch the browser sign-in when the
579
- // probe fails — otherwise we'd promise a browser that never opens because
580
- // the user is already authenticated.
581
- let wsList = runner.exec(["list", "workspaces", "--format", "json"]);
582
- if (wsList.status !== 0) {
583
- ui.heading("Sign in to Ory Network");
584
- ui.info("A browser window will open to sign in or create an account.");
585
- ui.hint("If it doesn't, watch this terminal for a URL to open manually.");
586
- ui.hint("The browser handles sign-in only — this terminal drives the rest of setup,");
587
- ui.hint("so once the browser says you're signed in, come back here to continue.");
588
- const loginCode = ctx.runInteractive(() => runner.execInteractive(["auth"]));
589
- // The `ory auth` browser page is rendered by the Ory CLI, so we can't tell
590
- // the user there to return to the terminal. Ring the bell instead — their
591
- // focus is still on the browser, and this pulls it back to the terminal
592
- // (dock bounce / tab badge) where the next steps live.
593
- ui.bell();
594
- if (loginCode !== 0) {
595
- console.warn("");
596
- ui.warning("Ory Network sign-in did not complete.");
597
- printManualSetup(binName);
598
- return { outcome: "network_fallback" };
599
- }
600
- ui.success("Signed in to Ory Network — back in the terminal now.");
601
- ui.step("Next, choose your workspace and project here. Loading your workspaces…");
602
- wsList = runner.exec(["list", "workspaces", "--format", "json"]);
603
- }
604
- else {
605
- ui.heading("Ory Network");
606
- ui.success("Already signed in to Ory Network.");
607
- }
608
- // 3. Pick a workspace (offer to create one when the account has none — a
609
- // brand-new account may have no workspace yet).
610
- let workspaces = wsList.status === 0 ? parseList(wsList.stdout, "workspaces") : null;
611
- if (!workspaces || workspaces.length === 0) {
612
- const created = await maybeCreateWorkspace(runner, prompt, harness);
613
- if (created)
614
- workspaces = [created];
615
- }
616
- if (!workspaces || workspaces.length === 0) {
617
- console.warn("");
618
- ui.warning("No Ory Network workspace selected.");
619
- ui.warnDetail("Create one at https://console.ory.sh/ and re-run install.");
620
- printManualSetup(binName);
621
- return { outcome: "network_fallback" };
622
- }
623
- const workspace = await selectFromList(prompt, "workspace", workspaces, (w) => w.name ?? w.id);
624
- if (!workspace) {
625
- printManualSetup(binName);
626
- return { outcome: "network_fallback" };
627
- }
628
- // 4. Pick a project. A brand-new workspace with none gets the create prompt;
629
- // otherwise the existing projects are listed for selection, and while the
630
- // workspace is still under the Developer-plan cap of two we also offer to
631
- // create a fresh project dedicated to the agent plugin — so a single-project
632
- // account isn't forced to reuse its existing project.
633
- ui.step("Loading projects…");
634
- const existingProjects = listProjects(runner, workspace.id);
635
- const project = !existingProjects || existingProjects.length === 0
636
- ? await maybeCreateProject(runner, prompt, workspace, harness)
637
- : await selectOrCreateProject(runner, prompt, workspace, harness, existingProjects);
638
- if (!project) {
639
- console.warn("");
640
- ui.warning("No Ory Network project selected.");
641
- printManualSetup(binName);
642
- return { outcome: "network_fallback" };
643
- }
644
- // The SDK URL is built from the project *slug*. Require a real slug — a
645
- // missing one, or a value that looks like the project id (UUID), can't
646
- // produce a correct URL, so fall back to manual setup rather than guess.
647
- if (!project.slug || (0, config_js_1.looksLikeProjectId)(project.slug)) {
648
- console.warn("");
649
- ui.warning("Could not resolve the selected project's slug (needed for the SDK URL).");
650
- printManualSetup(binName);
651
- return { outcome: "network_fallback" };
652
- }
653
- const projectUrl = projectUrlFromSlug(project.slug);
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.
695
- let clientId = findExistingOAuth2Client(runner, project.id);
696
- if (clientId) {
697
- ui.success(`Reusing the OAuth2 client for user login (${clientId}).`);
698
- }
699
- else {
700
- clientId = createOAuth2Client(runner, project.id);
701
- if (clientId)
702
- ui.success("Created the OAuth2 client for user login.");
703
- }
704
- if (!clientId) {
705
- ui.warning("Could not create the OAuth2 client automatically.");
706
- ui.warnDetail(`Project URL resolved: ${projectUrl}`);
707
- printManualOAuth2ClientHelp(binName, project.id);
708
- // Still persist the project URL + id so the user only needs the client id.
709
- (0, config_js_1.saveConfig)({ projectUrl, projectId: project.id, auditOnly: false });
710
- return { outcome: "network_fallback", projectUrl };
711
- }
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.
715
- (0, config_js_1.saveConfig)({
716
- projectUrl,
717
- projectId: project.id,
718
- projectName: project.name ?? project.slug,
719
- workspaceName: workspace.name,
720
- oauth2ClientId: clientId,
721
- auditOnly: false,
722
- });
723
- ui.success("Saved the Ory Network connection.");
724
- ui.summary([
725
- ["Project URL", projectUrl],
726
- ["OAuth2 client id", clientId],
727
- ["Config", (0, config_js_1.getConfigPath)()],
728
- ]);
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.
739
- if (subject) {
740
- // Persist the subject *shape* this grant used. If the install resolved a
741
- // SubjectSet (ORY_USER_SUBJECT_NAMESPACE was set), a later runtime session
742
- // that lacks the env var would otherwise resolve a direct subject_id and
743
- // check/write a divergent tuple for the same user. Persisting the namespace
744
- // keeps install-time and runtime writes on one canonical subject shape.
745
- if ("subjectSet" in subject) {
746
- (0, config_js_1.saveConfig)({ userSubjectNamespace: subject.subjectSet.namespace });
747
- }
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);
755
- return {
756
- outcome: "network_configured",
757
- projectUrl,
758
- oauth2ClientId: clientId,
759
- userAuthenticated: authenticated,
760
- // We just handled bootstrap with admin scope; don't let runPostInstall
761
- // retry it with the scope-less DCR token (which would only 403).
762
- skipAutoBootstrap: true,
763
- };
764
- }
765
- async function completeFullSetup(binName, harness, ctx, target = {}) {
766
- const clientFactory = ctx.deps.clientFactory ?? client_js_1.OryAgentClient.fromEnv;
767
- const userLogin = ctx.deps.userLoginFn ?? user_login_js_1.ensureUserAuthenticated;
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);
776
- try {
777
- const client = clientFactory(harness);
778
- ui.heading("Signing you in to the project as the permission subject.");
779
- ui.hint("If a sign-in is needed, a browser will open (watch for a URL here too).");
780
- const decision = await userLogin(client, {
781
- binName,
782
- harness,
783
- });
784
- // Resolve the agent identity regardless — it bootstraps from the user
785
- // token when present and no-ops otherwise. Never blocks.
786
- await agentGate(client, { harness });
787
- const authenticated = decision.proceed && !!decision.subject;
788
- const subject = authenticated ? (0, subject_js_1.resolveUserSubject)(client) : null;
789
- return {
790
- authenticated,
791
- subject: subject && (0, subject_js_1.subjectLabel)(subject) !== "agent:unknown" ? subject : null,
792
- };
793
- }
794
- catch (err) {
795
- const msg = err instanceof Error ? err.message : String(err);
796
- ui.warning(`Skipping automatic login (will run on first session): ${msg}`);
797
- return { authenticated: false, subject: null };
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
- };
825
- }
826
- /**
827
- * Grant the signed-in user `use` on every built-in tool by writing relation
828
- * tuples through the admin-authenticated `ory` CLI (`ory create
829
- * relationships`). This is the one credential in the flow with project-admin
830
- * scope — the agent's DCR token and the user's PKCE token can't write tuples
831
- * on a hosted Ory Network project.
832
- *
833
- * The tuples reference the permission namespace (default `AgentTools`), which
834
- * must be defined in the project's permission model or the write fails with
835
- * `NotFound`. So we inspect the model first (see {@link inspectPermissionModel})
836
- * and act on what's actually there. We provision in every case *except* when
837
- * the namespace already exists:
838
- *
839
- * - **Namespace already defined** → grant directly, touch nothing.
840
- * - **No model yet** (a freshly created Ory Network project) → offer to
841
- * provision a minimal `<namespace>` model (`ory update opl`), then grant.
842
- * - **Model exists (OPL source fetchable) but lacks `<namespace>`** → offer
843
- * to *merge* the namespace into the existing OPL and re-upload it, leaving
844
- * the other namespaces untouched, then grant. Ory Network serves the OPL
845
- * source at a `location` URL in the permission config, which makes this
846
- * non-destructive merge possible.
847
- * - **Model provably exists but its source couldn't be read** → we can't
848
- * merge and `ory update opl` overwrites the whole OPL, so we refuse to
849
- * clobber it: print the exact snippet to add and skip the grant.
850
- *
851
- * Best-effort throughout: any failure prints a short, actionable note rather
852
- * than a wall of errors. Observe mode (the install default) keeps tools working
853
- * regardless, so this is an optimization, not a hard requirement.
854
- */
855
- async function bootstrapPermissionsViaOry(runner, projectId, harness, subject) {
856
- const namespace = process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
857
- const tools = (0, tool_catalog_js_1.getToolCatalog)(harness);
858
- if (tools.length === 0)
859
- return;
860
- // Ensure the namespace the tuples reference exists, or the write NotFounds.
861
- // We only skip provisioning when the namespace is already defined; every
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.
867
- const model = await inspectPermissionModel(runner, projectId, namespace);
868
- if (model.kind === "merge") {
869
- // A model exists and we have its OPL source — append our namespace without
870
- // disturbing the others.
871
- if (!provisionPermissionModel(runner, projectId, namespace, model.opl)) {
872
- ui.warning(`Could not add '${namespace}' to the permission model automatically.`);
873
- printPermissionModelHelp(namespace);
874
- return;
875
- }
876
- ui.success(`Added '${namespace}' to the permission model.`);
877
- }
878
- else if (model.kind === "empty" || model.kind === "unknown") {
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".
881
- if (!provisionPermissionModel(runner, projectId, namespace)) {
882
- ui.warning(`Could not create the '${namespace}' permission model automatically.`);
883
- printPermissionModelHelp(namespace);
884
- return;
885
- }
886
- ui.success(`Created the '${namespace}' permission model.`);
887
- }
888
- else if (model.kind === "blocked") {
889
- // A model provably exists but we couldn't fetch its source, so we can't
890
- // merge and `ory update opl` would overwrite it. Guide instead of clobber.
891
- ui.warning(`This project's permission model couldn't be read; '${namespace}' wasn't added.`);
892
- printPermissionModelHelp(namespace);
893
- return;
894
- }
895
- // "present" (namespace already defined) falls through and grants directly.
896
- const subjectPatch = "subjectSet" in subject
897
- ? {
898
- subject_set: {
899
- namespace: subject.subjectSet.namespace,
900
- object: subject.subjectSet.object,
901
- relation: subject.subjectSet.relation,
902
- },
903
- }
904
- : { subject_id: subject.subjectId };
905
- const tuples = tools.map((object) => ({
906
- namespace,
907
- object,
908
- relation: "use",
909
- ...subjectPatch,
910
- }));
911
- ui.step(`Granting ${(0, subject_js_1.subjectLabel)(subject)} 'use' on ${tools.length} tools (via the Ory CLI)...`);
912
- const r = runner.exec(["create", "relationships", "--project", projectId, "--format", "json", "-f", "-", "--yes"], { input: JSON.stringify(tuples) });
913
- if (r.status === 0) {
914
- ui.success(`Granted permissions for ${tools.length} tools in '${namespace}'.`);
915
- return;
916
- }
917
- ui.warning("Could not pre-grant permissions automatically.");
918
- const detail = r.stderr.trim();
919
- if (detail)
920
- ui.warnDetail(detail.split("\n")[0]);
921
- printPermissionModelHelp(namespace);
922
- }
923
- /**
924
- * Inspect the project's permission model. Ory Network reports the config in one
925
- * of two shapes: `{ namespaces: [] }` (or `null`) for a project with no model,
926
- * and `{ namespaces: { location: "https://….txt" } }` for an OPL-configured
927
- * one — where the `.txt` is the full OPL *source*. We fetch that source so a
928
- * missing namespace can be merged in rather than clobbering the whole model.
929
- */
930
- async function inspectPermissionModel(runner, projectId, namespace) {
931
- const r = runner.exec([
932
- "get",
933
- "permission-config",
934
- "--project",
935
- projectId,
936
- "--format",
937
- "json",
938
- ]);
939
- if (r.status !== 0)
940
- return { kind: "unknown" };
941
- const cfg = safeJsonParse(r.stdout);
942
- if (!cfg || typeof cfg !== "object")
943
- return { kind: "unknown" };
944
- const ns = cfg.namespaces;
945
- // A fresh project reports no model as null / an absent key.
946
- if (ns == null)
947
- return { kind: "empty" };
948
- // OPL-configured form: { namespaces: { location: "https://….txt" } }.
949
- // The .txt holds the full OPL source, so we can merge into it.
950
- if (!Array.isArray(ns) && typeof ns === "object") {
951
- const location = ns.location;
952
- if (typeof location !== "string" || !location)
953
- return { kind: "empty" };
954
- const opl = await fetchOplSource(location);
955
- // A model exists but its source is unreadable — can't merge, mustn't clobber.
956
- if (opl == null)
957
- return { kind: "blocked" };
958
- return oplDefinesClass(opl, namespace)
959
- ? { kind: "present" }
960
- : { kind: "merge", opl };
961
- }
962
- // Inline compiled form: { namespaces: [{ name, id }, …] } (legacy / self-hosted).
963
- // We only have names, not source, so a missing namespace can't be merged.
964
- if (Array.isArray(ns)) {
965
- if (ns.length === 0)
966
- return { kind: "empty" };
967
- const names = ns
968
- .map((n) => n && typeof n === "object"
969
- ? n.name
970
- : undefined)
971
- .filter((n) => typeof n === "string");
972
- return names.includes(namespace) ? { kind: "present" } : { kind: "blocked" };
973
- }
974
- return { kind: "unknown" };
975
- }
976
- /** Fetch the OPL source served at a permission-config `location` URL. */
977
- async function fetchOplSource(url) {
978
- try {
979
- const res = await fetch(url);
980
- if (!res.ok)
981
- return null;
982
- return await res.text();
983
- }
984
- catch {
985
- return null;
986
- }
987
- }
988
- /** Whether an OPL source defines `class <name>`. */
989
- function oplDefinesClass(opl, name) {
990
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
991
- return new RegExp(`\\bclass\\s+${escaped}\\b`).test(opl);
992
- }
993
- /**
994
- * Provision a permission model that defines `<namespace>` with a `use`
995
- * relation, via `ory update opl`. The namespace becomes an OPL class, so it
996
- * must be a valid identifier; if not, we bail (caller guides instead).
997
- *
998
- * When `existingOpl` is given, the namespace is appended to that source
999
- * (preserving the model's other namespaces); otherwise a fresh minimal model
1000
- * is generated. Writes the OPL to a temp file because `ory update opl` reads
1001
- * `--file` only (no stdin). Returns true on success.
1002
- */
1003
- function provisionPermissionModel(runner, projectId, namespace, existingOpl) {
1004
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace))
1005
- return false;
1006
- const opl = existingOpl !== undefined
1007
- ? mergeNamespaceIntoOpl(existingOpl, namespace)
1008
- : freshOpl(namespace);
1009
- let dir;
1010
- try {
1011
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "ory-opl-"));
1012
- }
1013
- catch {
1014
- return false;
1015
- }
1016
- const file = path.join(dir, "namespace_config.ts");
1017
- try {
1018
- fs.writeFileSync(file, opl);
1019
- const r = runner.exec([
1020
- "update",
1021
- "opl",
1022
- "--project",
1023
- projectId,
1024
- "-f",
1025
- file,
1026
- "--format",
1027
- "json",
1028
- "--yes",
1029
- ]);
1030
- return r.status === 0;
1031
- }
1032
- catch {
1033
- return false;
1034
- }
1035
- finally {
1036
- try {
1037
- fs.rmSync(dir, { recursive: true, force: true });
1038
- }
1039
- catch {
1040
- /* ignore */
1041
- }
1042
- }
1043
- }
1044
- /** A minimal standalone OPL defining `User` and `<namespace>` with `use`. */
1045
- function freshOpl(namespace) {
1046
- return [
1047
- 'import { Namespace } from "@ory/permission-namespace-types"',
1048
- "",
1049
- "class User implements Namespace {}",
1050
- "",
1051
- `class ${namespace} implements Namespace {`,
1052
- " related: {",
1053
- " use: User[]",
1054
- " }",
1055
- "}",
1056
- "",
1057
- ].join("\n");
1058
- }
1059
- /**
1060
- * Append a `<namespace>` class (with a `use: User[]` relation) to an existing
1061
- * OPL source, leaving every other namespace untouched. Adds a `User` class too
1062
- * when the source doesn't already define one, since `use` is typed `User[]`.
1063
- */
1064
- function mergeNamespaceIntoOpl(existingOpl, namespace) {
1065
- let opl = existingOpl.replace(/\s*$/, "\n");
1066
- if (!oplDefinesClass(opl, "User")) {
1067
- opl += "\nclass User implements Namespace {}\n";
1068
- }
1069
- opl +=
1070
- `\nclass ${namespace} implements Namespace {\n` +
1071
- " related: {\n" +
1072
- " use: User[]\n" +
1073
- " }\n" +
1074
- "}\n";
1075
- return opl;
1076
- }
1077
- function printPermissionModelHelp(namespace) {
1078
- // When the namespace is a valid OPL class name, show the exact snippet to
1079
- // paste into the model — that's the concrete "how" behind "define it". The
1080
- // 'use' relation is typed `User[]`, so `User` is a compile-time dependency:
1081
- // include it too, since an existing model may not already define it.
1082
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace)) {
1083
- ui.warnDetail(`Add the '${namespace}' namespace to your Ory Permissions model. Its 'use'`);
1084
- ui.warnDetail("relation is typed `User[]`, so `User` must be defined too:");
1085
- console.warn("");
1086
- console.warn(ui.style.gray(" class User implements Namespace {}"));
1087
- console.warn("");
1088
- console.warn(ui.style.gray(` class ${namespace} implements Namespace {`));
1089
- console.warn(ui.style.gray(" related: { use: User[] }"));
1090
- console.warn(ui.style.gray(" }"));
1091
- console.warn("");
1092
- ui.warnDetail("(drop the `User` class if your model already defines it), then re-run");
1093
- ui.warnDetail("install to grant. You're in observe mode, so tools keep working");
1094
- ui.warnDetail("meanwhile.");
1095
- return;
1096
- }
1097
- ui.warnDetail(`Define the '${namespace}' namespace in Ory Permissions, then re-run install`);
1098
- ui.warnDetail("to grant. You're in observe mode, so tools keep working.");
1099
- }
1100
- // ─── ory CLI helpers ──────────────────────────────────────────────────────
1101
- function oryCliPresent(runner) {
1102
- return runner.exec(["version"]).status === 0;
1103
- }
1104
- /**
1105
- * The `ory` binary isn't on `PATH`. Offer (prompted, default yes) to install it
1106
- * automatically so the Ory Network path doesn't dead-end for a from-scratch
1107
- * user. On accept, install `@ory/cli` into a plugin-managed dir and return a
1108
- * runner bound to it; on decline or failure, return null so the caller falls
1109
- * back to the manual instructions. Never throws.
1110
- */
1111
- async function maybeInstallOryCli(ctx, prompt) {
1112
- ui.heading("Ory CLI");
1113
- ui.info("Connecting to Ory Network needs the Ory CLI, which isn't installed yet.");
1114
- ui.blank();
1115
- const answer = await prompt(ui.promptLine("Install it now (via npm, no system changes)?", { hint: "[Y/n]" }));
1116
- if (isNo(answer))
1117
- return null;
1118
- ui.step(`Installing ${exports.ORY_CLI_NPM_SPEC} — this can take a moment...`);
1119
- const install = ctx.deps.installOryCliFn ?? installOryCli;
1120
- let runner;
1121
- try {
1122
- runner = await install();
1123
- }
1124
- catch {
1125
- runner = null;
1126
- }
1127
- if (!runner) {
1128
- ui.warning("Could not install the Ory CLI automatically.");
1129
- return null;
1130
- }
1131
- ui.success("Ory CLI installed.");
1132
- return runner;
1133
- }
1134
- function listProjects(runner, workspaceId) {
1135
- const r = runner.exec([
1136
- "list",
1137
- "projects",
1138
- "--workspace",
1139
- workspaceId,
1140
- "--format",
1141
- "json",
1142
- ]);
1143
- if (r.status !== 0)
1144
- return null;
1145
- return parseList(r.stdout, "projects");
1146
- }
1147
- async function maybeCreateWorkspace(runner, prompt, harness) {
1148
- ui.heading("Your Ory Network account has no workspaces yet.");
1149
- ui.blank();
1150
- const answer = await prompt(ui.promptLine("Create a new workspace now?", { hint: "[Y/n]" }));
1151
- if (isNo(answer))
1152
- return null;
1153
- const defaultName = `${harness}-workspace`;
1154
- const nameInput = await prompt(ui.promptLine("Workspace name", { hint: `[${defaultName}]` }));
1155
- const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
1156
- const r = runner.exec([
1157
- "create",
1158
- "workspace",
1159
- "--name",
1160
- name,
1161
- "--format",
1162
- "json",
1163
- "--yes",
1164
- ]);
1165
- if (r.status !== 0) {
1166
- ui.warning(`Could not create the workspace: ${r.stderr.trim() || "unknown error"}`);
1167
- return null;
1168
- }
1169
- const workspace = parseObject(r.stdout);
1170
- if (!workspace || !workspace.id)
1171
- return null;
1172
- ui.success(`Created workspace "${workspace.name ?? name}".`);
1173
- return workspace;
1174
- }
1175
- async function maybeCreateProject(runner, prompt, workspace, harness) {
1176
- const wsLabel = workspace.name ?? workspace.id;
1177
- ui.heading(`Workspace "${wsLabel}" has no projects yet.`);
1178
- ui.blank();
1179
- const answer = await prompt(ui.promptLine("Create a new project now?", { hint: "[Y/n]" }));
1180
- if (isNo(answer))
1181
- return null;
1182
- return createProjectInteractive(runner, prompt, workspace, harness);
1183
- }
1184
- /**
1185
- * Choose the project to wire up when the workspace already has at least one.
1186
- * Lists the existing projects for selection and — while the workspace is still
1187
- * under the Ory Network Developer-plan cap of two projects — offers a "create a
1188
- * new project" option, so an account with a single project can dedicate a fresh
1189
- * one to the agent plugin instead of being forced onto the existing project.
1190
- * Returns null if the user cancels.
1191
- */
1192
- async function selectOrCreateProject(runner, prompt, workspace, harness, projects) {
1193
- // Developer plans cap a workspace at two projects; only offer to create while
1194
- // there's still headroom.
1195
- const PROJECT_CAP = 2;
1196
- const canCreate = projects.length < PROJECT_CAP;
1197
- ui.heading("Select a project:");
1198
- ui.blank();
1199
- const entries = projects.map((p, i) => ({
1200
- key: String(i + 1),
1201
- label: `${p.name ?? p.slug ?? p.id}${p.slug ? ` (${p.slug})` : ""}`,
1202
- }));
1203
- if (canCreate) {
1204
- entries.push({
1205
- key: String(projects.length + 1),
1206
- label: "Create a new project for the agent plugin",
1207
- });
1208
- }
1209
- ui.menu(entries);
1210
- ui.blank();
1211
- for (let attempt = 0; attempt < 3; attempt++) {
1212
- const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${entries.length}]` }));
1213
- if (answer === null || answer.trim() === "")
1214
- return null;
1215
- const n = Number.parseInt(answer.trim(), 10);
1216
- if (Number.isInteger(n) && n >= 1 && n <= projects.length) {
1217
- return projects[n - 1];
1218
- }
1219
- if (canCreate && n === projects.length + 1) {
1220
- return createProjectInteractive(runner, prompt, workspace, harness);
1221
- }
1222
- ui.warning(`"${answer}" is not a valid choice.`);
1223
- }
1224
- return null;
1225
- }
1226
- /**
1227
- * Prompt for a project name (defaulting to `<harness>-agent`) and create it in
1228
- * the given workspace. Shared by the empty-workspace path and the "create a new
1229
- * project" menu option. Returns null on failure.
1230
- */
1231
- async function createProjectInteractive(runner, prompt, workspace, harness) {
1232
- const defaultName = branding_js_1.DEFAULT_PROJECT_NAME;
1233
- const nameInput = await prompt(ui.promptLine("New project name", { hint: `[${defaultName}]` }));
1234
- const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
1235
- const r = runner.exec([
1236
- "create",
1237
- "project",
1238
- "--name",
1239
- name,
1240
- "--workspace",
1241
- workspace.id,
1242
- "--format",
1243
- "json",
1244
- "--yes",
1245
- ]);
1246
- if (r.status !== 0) {
1247
- ui.warning(`Could not create the project: ${r.stderr.trim() || "unknown error"}`);
1248
- return null;
1249
- }
1250
- const project = parseObject(r.stdout);
1251
- if (!project || !project.id)
1252
- return null;
1253
- ui.success(`Created project "${project.name ?? name}".`);
1254
- return project;
1255
- }
1256
- /** JSON-Patch path (relative to the oauth2 service config) that toggles DCR. */
1257
- const DCR_ENABLED_PATH = "/oidc/dynamic_client_registration/enabled";
1258
- /**
1259
- * Read the project's current DCR state via `ory get oauth2-config`.
1260
- * Returns `true`/`false` when the value is known, or `null` when it can't be
1261
- * determined (CLI error, unparseable output) so the caller falls open and
1262
- * prompts anyway rather than assuming a state.
1263
- */
1264
- function dcrEnabled(runner, projectId) {
1265
- const r = runner.exec([
1266
- "get",
1267
- "oauth2-config",
1268
- "--project",
1269
- projectId,
1270
- "--format",
1271
- "json",
1272
- ]);
1273
- if (r.status !== 0)
1274
- return null;
1275
- const config = parseObject(r.stdout);
1276
- if (!config)
1277
- return null;
1278
- const enabled = config.oidc?.dynamic_client_registration?.enabled;
1279
- return typeof enabled === "boolean" ? enabled : null;
1280
- }
1281
- /**
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.
1285
- */
1286
- function enableDcr(runner, projectId) {
1287
- const r = runner.exec([
1288
- "patch",
1289
- "oauth2-config",
1290
- "--project",
1291
- projectId,
1292
- "--replace",
1293
- `${DCR_ENABLED_PATH}=true`,
1294
- "--format",
1295
- "json",
1296
- "--yes",
1297
- ]);
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;
1349
- }
1350
- catch {
1351
- return "managed"; // unparseable — don't touch it
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;
1402
- }
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
- */
1423
- /**
1424
- * Look for an already-provisioned public PKCE client on the project so a
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.
1430
- */
1431
- function findExistingOAuth2Client(runner, projectId) {
1432
- const r = runner.exec([
1433
- "list",
1434
- "oauth2-clients",
1435
- "--project",
1436
- projectId,
1437
- "--format",
1438
- "json",
1439
- ]);
1440
- if (r.status !== 0)
1441
- return null;
1442
- const clients = parseList(r.stdout, "items");
1443
- if (!clients)
1444
- return null;
1445
- const match = clients.find((c) => c.client_id && c.client_name && branding_js_1.USER_LOGIN_CLIENT_NAMES.includes(c.client_name));
1446
- return match?.client_id ?? null;
1447
- }
1448
- function createOAuth2Client(runner, projectId) {
1449
- const args = [
1450
- "create",
1451
- "oauth2-client",
1452
- "--project",
1453
- projectId,
1454
- "--name",
1455
- branding_js_1.USER_LOGIN_CLIENT_NAME,
1456
- "--grant-type",
1457
- "authorization_code,refresh_token",
1458
- "--response-type",
1459
- "code",
1460
- "--scope",
1461
- "openid,offline_access",
1462
- "--token-endpoint-auth-method",
1463
- "none",
1464
- "--format",
1465
- "json",
1466
- "--yes",
1467
- ];
1468
- for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
1469
- args.push("--redirect-uri", uri);
1470
- }
1471
- const r = runner.exec(args);
1472
- if (r.status !== 0)
1473
- return null;
1474
- const client = parseObject(r.stdout);
1475
- return client?.client_id ?? null;
1476
- }
1477
- // ─── parsing helpers ────────────────────────────────────────────────────────
1478
- /**
1479
- * Parse a JSON list from `ory --format json` output. Tolerates both a bare
1480
- * array and the `{ <key>: [...] }` envelope the Console list endpoints use.
1481
- */
1482
- function parseList(stdout, key) {
1483
- const parsed = safeJsonParse(stdout);
1484
- if (parsed === null)
1485
- return null;
1486
- if (Array.isArray(parsed))
1487
- return parsed;
1488
- if (typeof parsed === "object") {
1489
- const value = parsed[key];
1490
- if (Array.isArray(value))
1491
- return value;
1492
- }
1493
- return null;
1494
- }
1495
- function parseObject(stdout) {
1496
- const parsed = safeJsonParse(stdout);
1497
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1498
- return parsed;
1499
- }
1500
- return null;
1501
- }
1502
- function safeJsonParse(stdout) {
1503
- const trimmed = stdout.trim();
1504
- if (!trimmed)
1505
- return null;
1506
- try {
1507
- return JSON.parse(trimmed);
1508
- }
1509
- catch {
1510
- return null;
1511
- }
1512
- }
1513
- /**
1514
- * Derive the canonical Ory Network project SDK/API URL from a project **slug**.
1515
- * The subdomain is always the slug — never the project id. A project id (a
1516
- * UUID) reaching here means a slug and an id were confused upstream, which
1517
- * would build a URL that points nowhere, so we refuse it loudly rather than
1518
- * silently misconfigure the plugin.
1519
- */
1520
- function projectUrlFromSlug(slug) {
1521
- if ((0, config_js_1.looksLikeProjectId)(slug)) {
1522
- throw new Error(`Refusing to build an SDK URL from "${slug}": that is a project id (UUID), not a slug.`);
1523
- }
1524
- return `https://${slug}.projects.oryapis.com`;
1525
- }
1526
- // ─── prompt helpers ─────────────────────────────────────────────────────────
1527
- /**
1528
- * Present a numbered list and prompt for a selection. Auto-selects when there
1529
- * is exactly one item. Returns null if the user cancels or gives up.
1530
- */
1531
- async function selectFromList(prompt, label, items, render) {
1532
- if (items.length === 1) {
1533
- ui.info(`Using the only ${label}: ${ui.style.bold(render(items[0]))}`);
1534
- return items[0];
1535
- }
1536
- ui.heading(`Select a ${label}:`);
1537
- ui.blank();
1538
- ui.menu(items.map((item, i) => ({ key: String(i + 1), label: render(item) })));
1539
- ui.blank();
1540
- for (let attempt = 0; attempt < 3; attempt++) {
1541
- const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${items.length}]` }));
1542
- if (answer === null || answer.trim() === "")
1543
- return null;
1544
- const n = Number.parseInt(answer.trim(), 10);
1545
- if (Number.isInteger(n) && n >= 1 && n <= items.length) {
1546
- return items[n - 1];
1547
- }
1548
- ui.warning(`"${answer}" is not a valid choice.`);
1549
- }
1550
- return null;
1551
- }
1552
- function isLocalChoice(answer) {
1553
- if (answer === null)
1554
- return false;
1555
- const v = answer.trim().toLowerCase();
1556
- return v === "2" || v === "local" || v === "local-stack" || v === "localstack";
1557
- }
1558
- function isAuditChoice(answer) {
1559
- if (answer === null)
1560
- return false;
1561
- const v = answer.trim().toLowerCase();
1562
- return v === "3" || v === "audit" || v === "audit-only";
1563
- }
1564
- function isNo(answer) {
1565
- if (answer === null)
1566
- return false;
1567
- const v = answer.trim().toLowerCase();
1568
- return v === "n" || v === "no";
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
- }
1593
- // ─── banners ────────────────────────────────────────────────────────────────
1594
- function printOryCliMissing() {
1595
- ui.heading("The Ory CLI (`ory`) is required to connect to Ory Network but was not found.");
1596
- ui.hint("Install it, then re-run install:");
1597
- ui.summary([
1598
- ["macOS/Linux (Homebrew)", "brew install ory/tap/cli"],
1599
- [
1600
- "macOS/Linux (script)",
1601
- "bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b . ory",
1602
- ],
1603
- ["Docs", "https://www.ory.com/docs/guides/cli/installation"],
1604
- ]);
1605
- }
1606
- function printManualSetup(binName) {
1607
- ui.heading("You can connect to Ory at any time:");
1608
- ui.blank();
1609
- ui.bullet("Connect to an Ory project (enables auth & permission checks):");
1610
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
1611
- ui.blank();
1612
- ui.bullet("Or enable audit logging only (no auth or permission checks):");
1613
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --audit-only`);
1614
- ui.blank();
1615
- ui.hint(`Config is saved to ${(0, config_js_1.getConfigPath)()} and shared across all agent plugins.`);
1616
- }
1617
- function printManualOAuth2ClientHelp(binName, projectId) {
1618
- ui.heading("Create the public OAuth2 client by hand, then supply its id:");
1619
- ui.blank();
1620
- console.log(ui.style.gray(` ory create oauth2-client --project ${projectId} \\`));
1621
- console.log(ui.style.gray(` --name "${branding_js_1.USER_LOGIN_CLIENT_NAME}" \\`));
1622
- console.log(ui.style.gray(` --grant-type authorization_code,refresh_token \\`));
1623
- console.log(ui.style.gray(` --response-type code \\`));
1624
- console.log(ui.style.gray(` --scope openid,offline_access \\`));
1625
- console.log(ui.style.gray(` --token-endpoint-auth-method none \\`));
1626
- for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
1627
- console.log(ui.style.gray(` --redirect-uri ${uri} \\`));
1628
- }
1629
- console.log(ui.style.gray(` --format json`));
1630
- ui.blank();
1631
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --oauth2-client-id <CLIENT_ID>`);
1632
- }
1633
- /**
1634
- * Best-effort check that an already-stored project API key still authenticates
1635
- * against Keto. Runs one permission check with the key: if it completes (any
1636
- * `allowed` value) the key is valid; an auth rejection (`session_inactive` /
1637
- * `forbidden` / `session_aal2_required`) means it was revoked or deleted, so we
1638
- * return false and the caller re-provisions. Transient failures (network /
1639
- * rate-limit / unknown) return true so we don't re-mint on a blip. Never throws.
1640
- */
1641
- async function apiKeyAuthenticates(apiKey, projectUrl) {
1642
- try {
1643
- // Direct construction (no fromEnv) seeds the key as both agent token and
1644
- // admin key and skips DCR, so the probe uses exactly this key. No trace
1645
- // file is configured, so nothing is written to disk.
1646
- const probe = new client_js_1.OryAgentClient({ projectUrl, apiKey, harness: "install-probe" });
1647
- await probe.checkPermission({
1648
- namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
1649
- object: "__ory_agent_key_probe__",
1650
- relation: "__ory_agent_key_probe__",
1651
- subjectId: "__ory_agent_key_probe__",
1652
- });
1653
- return true;
1654
- }
1655
- catch (err) {
1656
- const code = err?.code;
1657
- if (code === "session_inactive" || code === "forbidden" || code === "session_aal2_required") {
1658
- return false;
1659
- }
1660
- return true;
1661
- }
1662
- }
1663
- /**
1664
- * Manual fallback steps for the project API key, shown when auto-provisioning
1665
- * is declined or fails. The `configure --api-key` command is the stable part;
1666
- * the Console link is a convenience.
1667
- */
1668
- function printManualApiKeySteps(binName, projectId) {
1669
- ui.info("Create one in the Ory Console (Project settings → API keys):");
1670
- ui.command(`https://console.ory.sh/projects/${projectId}/settings`);
1671
- ui.info("then store it so permission checks authenticate with it:");
1672
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --api-key <ory_pat_…>`);
1673
- ui.hint("Observe mode works without it; enforce mode needs it to allow tools.");
1674
- }
1675
- /**
1676
- * Provision the project API key that runtime Keto permission checks
1677
- * authenticate with. The agent's DCR OAuth2 token can't — Ory Network rejects
1678
- * it — so `enforce` mode needs a project API key. Best-effort and skippable:
1679
- * offers to mint one via the Console API and persist it, and on decline / no
1680
- * TTY / any failure falls back to manual guidance. No-ops when a key is already
1681
- * configured (env or config file).
1682
- */
1683
- async function maybeProvisionProjectApiKey(binName, projectId, projectUrl, createFn, validateFn, userLabel) {
1684
- const resolved = (0, config_js_1.resolveConfig)();
1685
- if (resolved.apiKey) {
1686
- // An operator-set env key is authoritative — never touch it.
1687
- if (resolved.apiKeySource === "env")
1688
- return;
1689
- // A stored (config-file) key that no longer authenticates — revoked or
1690
- // deleted — must not wedge setup: validate it and re-provision if it's
1691
- // dead. Presence alone isn't enough (a dangling key still fails at runtime
1692
- // with session_inactive).
1693
- if (await validateFn(resolved.apiKey, projectUrl)) {
1694
- ui.success("Stored project API key is valid.");
1695
- return;
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";
1711
- try {
1712
- host = os.hostname();
1713
- }
1714
- catch {
1715
- /* keep fallback */
1716
- }
1717
- created = await createFn({
1718
- projectId,
1719
- name: (0, branding_js_1.projectApiKeyName)({ host, user: userLabel }),
1720
- });
1721
- }
1722
- catch {
1723
- created = null;
1724
- }
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);
1734
- }