@ory/argus 0.13.9 → 1.0.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.
- package/README.md +31 -45
- package/assets/commands/temporal-up.md +1 -1
- package/assets/skills/auth-setup/SKILL.md +1 -1
- package/assets/skills/local-dev/SKILL.md +17 -7
- package/assets/skills/ory-build-agent/SKILL.md +43 -97
- package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -18
- package/assets/skills/ory-temporal-worker/SKILL.md +38 -42
- package/assets/skills/permissions-onboarding/SKILL.md +131 -104
- package/dist/adapters.d.ts +93 -30
- package/dist/adapters.js +464 -136
- package/dist/agent-auth.d.ts +258 -68
- package/dist/agent-auth.js +998 -202
- package/dist/auth-store.d.ts +37 -2
- package/dist/auth-store.js +37 -3
- package/dist/auth.d.ts +40 -4
- package/dist/auth.js +247 -19
- package/dist/bash-parser.d.ts +98 -0
- package/dist/bash-parser.js +396 -0
- package/dist/branding.d.ts +128 -0
- package/dist/branding.js +151 -0
- package/dist/build-info.json +4 -4
- package/dist/cli-invocation.d.ts +1 -1
- package/dist/cli-invocation.js +2 -1
- package/dist/cli.d.ts +20 -29
- package/dist/cli.js +271 -278
- package/dist/client.d.ts +175 -138
- package/dist/client.js +672 -391
- package/dist/config.d.ts +249 -57
- package/dist/config.js +486 -62
- package/dist/context.d.ts +10 -0
- package/dist/context.js +21 -0
- package/dist/contract-suite.d.ts +8 -8
- package/dist/contract-suite.js +88 -69
- package/dist/denial.d.ts +36 -3
- package/dist/denial.js +79 -10
- package/dist/event-reporter.d.ts +77 -0
- package/dist/event-reporter.js +776 -0
- package/dist/external-registrations-main.d.ts +10 -0
- package/dist/external-registrations-main.js +38 -0
- package/dist/external-registrations.d.ts +79 -0
- package/dist/external-registrations.js +188 -0
- package/dist/help-cli.d.ts +39 -0
- package/dist/help-cli.js +55 -0
- package/dist/hook-timeout.d.ts +64 -0
- package/dist/hook-timeout.js +88 -0
- package/dist/index.d.ts +31 -19
- package/dist/index.js +182 -31
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +38 -6
- package/dist/local/cli.js +11 -6
- package/dist/local/configs.d.ts +74 -18
- package/dist/local/configs.js +291 -84
- package/dist/local/health.d.ts +14 -0
- package/dist/local/health.js +50 -4
- package/dist/local/index.d.ts +2 -2
- package/dist/local/index.js +24 -10
- package/dist/local/manager.d.ts +20 -1
- package/dist/local/manager.js +160 -39
- package/dist/local/ports.d.ts +158 -0
- package/dist/local/ports.js +443 -0
- package/dist/local/seed.d.ts +22 -25
- package/dist/local/seed.js +88 -56
- package/dist/logger.d.ts +54 -25
- package/dist/logger.js +329 -63
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +10 -5
- package/dist/mirror-bootstrap.d.ts +48 -0
- package/dist/mirror-bootstrap.js +254 -0
- package/dist/opl.d.ts +289 -0
- package/dist/opl.js +446 -0
- package/dist/permission-mode.d.ts +87 -0
- package/dist/permission-mode.js +307 -0
- package/dist/permissions-cli.d.ts +13 -49
- package/dist/permissions-cli.js +154 -348
- package/dist/permissions.d.ts +148 -38
- package/dist/permissions.js +591 -45
- package/dist/post-install.d.ts +33 -0
- package/dist/post-install.js +127 -0
- package/dist/read-credential.d.ts +65 -0
- package/dist/read-credential.js +86 -0
- package/dist/registry/cli.js +5 -2
- package/dist/registry/config.d.ts +0 -17
- package/dist/registry/config.js +0 -23
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +2 -2
- package/dist/registry/manager.d.ts +4 -21
- package/dist/registry/manager.js +83 -55
- package/dist/runtime-credential.d.ts +140 -0
- package/dist/runtime-credential.js +572 -0
- package/dist/runtime.d.ts +408 -0
- package/dist/runtime.js +748 -0
- package/dist/setup.d.ts +23 -28
- package/dist/setup.js +57 -84
- package/dist/status-cli.d.ts +29 -13
- package/dist/status-cli.js +124 -144
- package/dist/status-data.d.ts +195 -0
- package/dist/status-data.js +333 -0
- package/dist/status-system.d.ts +24 -0
- package/dist/status-system.js +56 -0
- package/dist/subject.d.ts +126 -20
- package/dist/subject.js +215 -30
- package/dist/testing.d.ts +74 -38
- package/dist/testing.js +185 -68
- package/dist/tool-catalog.d.ts +53 -11
- package/dist/tool-catalog.js +164 -13
- package/dist/tool-metadata.d.ts +7 -6
- package/dist/tool-metadata.js +6 -5
- package/dist/types.d.ts +11 -1
- package/dist/uninstall.d.ts +74 -19
- package/dist/uninstall.js +224 -49
- package/dist/user-login.d.ts +22 -16
- package/dist/user-login.js +67 -96
- package/dist/watch-cli.d.ts +6 -0
- package/dist/watch-cli.js +217 -0
- package/package.json +3 -11
- package/dist/dev.d.ts +0 -103
- package/dist/dev.js +0 -584
- package/dist/interactive-setup.d.ts +0 -165
- package/dist/interactive-setup.js +0 -1546
- package/dist/local/jaeger-main.d.ts +0 -13
- package/dist/local/jaeger-main.js +0 -85
- package/dist/local/jaeger.d.ts +0 -50
- package/dist/local/jaeger.js +0 -162
- package/dist/otel/exporter.d.ts +0 -17
- package/dist/otel/exporter.js +0 -12
- package/dist/otel/index.d.ts +0 -2
- package/dist/otel/index.js +0 -8
- package/dist/otel/otlp.d.ts +0 -103
- package/dist/otel/otlp.js +0 -385
- package/dist/tracer.d.ts +0 -190
- package/dist/tracer.js +0 -481
- package/dist/watch-sandbox.d.ts +0 -9
- package/dist/watch-sandbox.js +0 -81
|
@@ -1,1546 +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, 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.
|
|
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.runPostInstall = runPostInstall;
|
|
69
|
-
exports.runInteractiveSetup = runInteractiveSetup;
|
|
70
|
-
exports.projectUrlFromSlug = projectUrlFromSlug;
|
|
71
|
-
exports.createProjectApiKeyViaConsole = createProjectApiKeyViaConsole;
|
|
72
|
-
exports.apiKeyAuthenticates = apiKeyAuthenticates;
|
|
73
|
-
const node_child_process_1 = require("node:child_process");
|
|
74
|
-
const fs = __importStar(require("node:fs"));
|
|
75
|
-
const os = __importStar(require("node:os"));
|
|
76
|
-
const path = __importStar(require("node:path"));
|
|
77
|
-
const tty = __importStar(require("node:tty"));
|
|
78
|
-
const readline = __importStar(require("node:readline"));
|
|
79
|
-
const config_js_1 = require("./config.js");
|
|
80
|
-
const setup_js_1 = require("./setup.js");
|
|
81
|
-
const cli_js_1 = require("./cli.js");
|
|
82
|
-
const cli_invocation_js_1 = require("./cli-invocation.js");
|
|
83
|
-
const auth_js_1 = require("./auth.js");
|
|
84
|
-
const client_js_1 = require("./client.js");
|
|
85
|
-
const logger_js_1 = require("./logger.js");
|
|
86
|
-
const user_login_js_1 = require("./user-login.js");
|
|
87
|
-
const agent_auth_js_1 = require("./agent-auth.js");
|
|
88
|
-
const tool_catalog_js_1 = require("./tool-catalog.js");
|
|
89
|
-
const index_js_1 = require("./local/index.js");
|
|
90
|
-
const subject_js_1 = require("./subject.js");
|
|
91
|
-
const permissions_cli_js_1 = require("./permissions-cli.js");
|
|
92
|
-
const version_cli_js_1 = require("./version-cli.js");
|
|
93
|
-
const ui = __importStar(require("./ui.js"));
|
|
94
|
-
/** Loopback redirect URIs the PKCE user login expects on the OAuth2 client. */
|
|
95
|
-
exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`);
|
|
96
|
-
/**
|
|
97
|
-
* Build a runner bound to a specific `ory` binary (a bare command resolved on
|
|
98
|
-
* `PATH`, or an absolute path to a plugin-installed binary). {@link defaultRunner}
|
|
99
|
-
* uses the bare `"ory"`; {@link installOryCli} returns one bound to the binary
|
|
100
|
-
* it just installed.
|
|
101
|
-
*/
|
|
102
|
-
function createOryRunner(oryBin) {
|
|
103
|
-
return {
|
|
104
|
-
exec(args, opts) {
|
|
105
|
-
const r = (0, node_child_process_1.spawnSync)(oryBin, args, {
|
|
106
|
-
encoding: "utf-8",
|
|
107
|
-
...(opts?.input !== undefined ? { input: opts.input } : {}),
|
|
108
|
-
});
|
|
109
|
-
return {
|
|
110
|
-
status: r.status,
|
|
111
|
-
stdout: r.stdout ?? "",
|
|
112
|
-
stderr: r.stderr ?? "",
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
execInteractive(args) {
|
|
116
|
-
const r = (0, node_child_process_1.spawnSync)(oryBin, args, { stdio: "inherit" });
|
|
117
|
-
return r.status ?? 1;
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
const defaultRunner = createOryRunner("ory");
|
|
122
|
-
/**
|
|
123
|
-
* npm spec for the Ory CLI installed on demand when the `ory` binary isn't on
|
|
124
|
-
* `PATH`. `@ory/cli` ships the platform `ory` binary and exposes it as its
|
|
125
|
-
* `bin`, so an isolated `npm install` gives us a working CLI without touching
|
|
126
|
-
* the user's global environment. Pinned to a major to stay reproducible while
|
|
127
|
-
* still picking up patch/minor fixes.
|
|
128
|
-
*/
|
|
129
|
-
exports.ORY_CLI_NPM_SPEC = "@ory/cli@^1";
|
|
130
|
-
/**
|
|
131
|
-
* Best-effort install of the Ory CLI into a plugin-managed directory under the
|
|
132
|
-
* shared data dir, returning a runner bound to the installed binary (or null
|
|
133
|
-
* on any failure). Uses `npm install --prefix` so nothing global is modified;
|
|
134
|
-
* the binary lands at `<dataDir>/cli/node_modules/.bin/ory`. Never throws.
|
|
135
|
-
*/
|
|
136
|
-
async function installOryCli() {
|
|
137
|
-
const dir = path.join((0, config_js_1.getDataDir)(), "cli");
|
|
138
|
-
try {
|
|
139
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
140
|
-
}
|
|
141
|
-
catch {
|
|
142
|
-
return null;
|
|
143
|
-
}
|
|
144
|
-
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
145
|
-
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"] });
|
|
146
|
-
if (r.status !== 0)
|
|
147
|
-
return null;
|
|
148
|
-
const binName = process.platform === "win32" ? "ory.cmd" : "ory";
|
|
149
|
-
const bin = path.join(dir, "node_modules", ".bin", binName);
|
|
150
|
-
if (!fs.existsSync(bin))
|
|
151
|
-
return null;
|
|
152
|
-
const runner = createOryRunner(bin);
|
|
153
|
-
// Confirm the freshly installed binary actually runs before handing it back.
|
|
154
|
-
return oryCliPresent(runner) ? runner : null;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* The shared post-install entry point every harness's `install` command
|
|
158
|
-
* calls. Runs the interactive setup wizard, then (using whatever identity the
|
|
159
|
-
* wizard cached) bootstraps permissions and prints the onboarding banner.
|
|
160
|
-
*
|
|
161
|
-
* This is the single seam that centralizes what each harness previously
|
|
162
|
-
* open-coded as a local `postInstallPermissions` helper.
|
|
163
|
-
*/
|
|
164
|
-
async function runPostInstall(binName, harness, args = [], deps = {}) {
|
|
165
|
-
// Surface the running CLI version up front. `npx -p @ory/<harness>` (no
|
|
166
|
-
// version pin) will happily reuse a previously-cached install rather than
|
|
167
|
-
// re-resolve to latest, so a user can silently be running an old CLI whose
|
|
168
|
-
// install flow predates newer behavior (e.g. the interactive setup wizard)
|
|
169
|
-
// while believing they're on the current release. Printing the version makes
|
|
170
|
-
// that mismatch visible at a glance; the README's troubleshooting note
|
|
171
|
-
// explains how to clear a stale npx cache.
|
|
172
|
-
const cliVersion = (0, version_cli_js_1.resolveCoreVersion)();
|
|
173
|
-
if (cliVersion !== "unknown") {
|
|
174
|
-
ui.info(`${binName} v${cliVersion}`);
|
|
175
|
-
}
|
|
176
|
-
const result = await runInteractiveSetup(binName, harness, args, deps);
|
|
177
|
-
// The Ory Network wizard path bootstraps permissions itself (via the
|
|
178
|
-
// admin-authenticated `ory` CLI). Only fall back to the DCR-token bootstrap
|
|
179
|
-
// when the wizard didn't handle it — otherwise we'd emit 403s on a hosted
|
|
180
|
-
// project whose relation-tuple API rejects the agent/user token.
|
|
181
|
-
const bootstrapped = result.skipAutoBootstrap
|
|
182
|
-
? false
|
|
183
|
-
: await (0, permissions_cli_js_1.maybeAutoBootstrap)(binName, harness);
|
|
184
|
-
(0, permissions_cli_js_1.printPermissionsOnboardingHelp)(binName, harness, {
|
|
185
|
-
bootstrappedAutomatically: bootstrapped,
|
|
186
|
-
});
|
|
187
|
-
// Flush any next-steps guidance the harness's install() deferred, so it
|
|
188
|
-
// lands at the very end of the interactive flow rather than above the
|
|
189
|
-
// wizard's first prompt. When the wizard already connected Ory, the renderer
|
|
190
|
-
// prints a short launch summary instead of the manual setup steps — those
|
|
191
|
-
// would only contradict the values the wizard just saved.
|
|
192
|
-
(0, setup_js_1.emitDeferredNextSteps)(isConfiguredOutcome(result.outcome));
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Outcomes where Ory is already wired up by the time the wizard returns —
|
|
196
|
-
* either the user connected it now (network / local / audit-only) or it was
|
|
197
|
-
* configured on a previous run. For these the closing next-steps should be a
|
|
198
|
-
* short "launch the harness" summary, not the manual env-var guidance.
|
|
199
|
-
*/
|
|
200
|
-
function isConfiguredOutcome(outcome) {
|
|
201
|
-
return (outcome === "network_configured" ||
|
|
202
|
-
outcome === "local_configured" ||
|
|
203
|
-
outcome === "audit_only" ||
|
|
204
|
-
outcome === "skipped_configured");
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Run the interactive setup wizard. Always resolves; never throws.
|
|
208
|
-
*/
|
|
209
|
-
async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
|
|
210
|
-
const runner = deps.runner ?? defaultRunner;
|
|
211
|
-
const ttyAvailable = deps.isTtyAvailableFn ?? cli_js_1.isTtyAvailable;
|
|
212
|
-
const noConfigure = args.includes("--no-configure");
|
|
213
|
-
const reconfigure = args.includes("--reconfigure");
|
|
214
|
-
if (noConfigure) {
|
|
215
|
-
return { outcome: "skipped_flag" };
|
|
216
|
-
}
|
|
217
|
-
if (!ttyAvailable()) {
|
|
218
|
-
// Non-interactive (CI/headless) — preserve the pre-wizard behavior and
|
|
219
|
-
// point the operator at the manual, scriptable path.
|
|
220
|
-
printManualSetup(binName);
|
|
221
|
-
return { outcome: "skipped_no_tty" };
|
|
222
|
-
}
|
|
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
|
-
// The wizard prompts several times in a row. Opening `/dev/tty` afresh for
|
|
238
|
-
// each prompt (as `promptOnTty` does) leaves a blocking read stuck in
|
|
239
|
-
// libuv's threadpool per call, and after a few prompts the pool is
|
|
240
|
-
// exhausted and the next prompt hangs. So when we're driving real TTY input
|
|
241
|
-
// (no injected promptFn), open one readline interface and reuse it for the
|
|
242
|
-
// whole flow. Tests inject their own promptFn and skip this entirely.
|
|
243
|
-
// One persistent tty prompter, but recreated around any interactive child
|
|
244
|
-
// process. A readline over `/dev/tty` that stays open across a
|
|
245
|
-
// `spawnSync(..., { stdio: "inherit" })` (how `execInteractive` runs the
|
|
246
|
-
// `ory auth` browser login) stops receiving input once the child returns —
|
|
247
|
-
// the terminal is left in a state the parent readline no longer reads from,
|
|
248
|
-
// so the *next* prompt (e.g. workspace selection) hangs forever with no
|
|
249
|
-
// output. `runInteractive` closes the prompter before the child and reopens
|
|
250
|
-
// a fresh one after, which reliably restores line input.
|
|
251
|
-
let ttyPrompter = deps.promptFn ? null : createTtyPrompter();
|
|
252
|
-
const prompt = (q) => (deps.promptFn ?? ttyPrompter?.prompt ?? cli_js_1.promptOnTty)(q);
|
|
253
|
-
const runInteractive = (fn) => {
|
|
254
|
-
if (deps.promptFn || !ttyPrompter)
|
|
255
|
-
return fn();
|
|
256
|
-
ttyPrompter.close();
|
|
257
|
-
ttyPrompter = null;
|
|
258
|
-
try {
|
|
259
|
-
return fn();
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
ttyPrompter = createTtyPrompter();
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
// Quiet the structured stderr firehose for the duration of the wizard so
|
|
266
|
-
// `[ory-agent] {...}` debug lines don't interleave with the human-facing UI
|
|
267
|
-
// when a session runs with ORY_AGENT_DEBUG=true (the dev launcher always
|
|
268
|
-
// sets it). Debug still lands in the log file.
|
|
269
|
-
try {
|
|
270
|
-
return await (0, logger_js_1.withQuietStderr)(() => runWizard(binName, harness, { runner, prompt, deps, runInteractive }));
|
|
271
|
-
}
|
|
272
|
-
catch (err) {
|
|
273
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
274
|
-
console.warn("");
|
|
275
|
-
ui.warning(`Interactive setup did not complete: ${msg}`);
|
|
276
|
-
printManualSetup(binName);
|
|
277
|
-
return { outcome: "network_fallback" };
|
|
278
|
-
}
|
|
279
|
-
finally {
|
|
280
|
-
ttyPrompter?.close();
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Open `/dev/tty` once and wrap it in a single readline interface, reused for
|
|
285
|
-
* every prompt in the wizard.
|
|
286
|
-
*
|
|
287
|
-
* Crucially this uses `tty.ReadStream`, not `fs.createReadStream`. Reading a
|
|
288
|
-
* tty via an fs stream issues a *blocking* `read()` in libuv's threadpool that
|
|
289
|
-
* can't be cancelled; a pending one exhausts the pool (hanging mid-flow) and,
|
|
290
|
-
* worse, blocks `process.exit()` from ever completing (hanging at the very
|
|
291
|
-
* end). `tty.ReadStream` uses libuv's non-blocking tty handle instead, which is
|
|
292
|
-
* event-loop integrated and tears down cleanly on exit. Returns null if
|
|
293
|
-
* `/dev/tty` can't be opened (caller falls back to `promptOnTty`).
|
|
294
|
-
*/
|
|
295
|
-
function createTtyPrompter() {
|
|
296
|
-
let fd;
|
|
297
|
-
try {
|
|
298
|
-
fd = fs.openSync("/dev/tty", "r");
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
return null;
|
|
302
|
-
}
|
|
303
|
-
let input;
|
|
304
|
-
try {
|
|
305
|
-
input = new tty.ReadStream(fd);
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
try {
|
|
309
|
-
fs.closeSync(fd);
|
|
310
|
-
}
|
|
311
|
-
catch {
|
|
312
|
-
/* ignore */
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
const rl = readline.createInterface({ input, output: process.stderr });
|
|
317
|
-
return {
|
|
318
|
-
prompt: (question) => new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim()))),
|
|
319
|
-
close: () => {
|
|
320
|
-
rl.close();
|
|
321
|
-
input.destroy();
|
|
322
|
-
try {
|
|
323
|
-
fs.closeSync(fd);
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
/* fd may already be closed by the stream */
|
|
327
|
-
}
|
|
328
|
-
},
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
async function runWizard(binName, harness, ctx) {
|
|
332
|
-
ui.banner("Ory Agent Plugin", "Connect your agent to Ory");
|
|
333
|
-
ui.heading("How do you want to connect?");
|
|
334
|
-
ui.blank();
|
|
335
|
-
ui.menu([
|
|
336
|
-
{
|
|
337
|
-
key: "1",
|
|
338
|
-
label: "Ory Network",
|
|
339
|
-
description: "sign in (or create an account) and connect a hosted project",
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
key: "2",
|
|
343
|
-
label: "Local stack",
|
|
344
|
-
description: "self-hosted Ory via `local up` (localhost:4000), seeded for you",
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
key: "3",
|
|
348
|
-
label: "Audit-only",
|
|
349
|
-
description: "no Ory project (traces tool calls locally, no auth or checks)",
|
|
350
|
-
},
|
|
351
|
-
]);
|
|
352
|
-
ui.blank();
|
|
353
|
-
const choice = await ctx.prompt(ui.promptLine("Enter 1, 2 or 3", { hint: "[1]" }));
|
|
354
|
-
// Default to the Ory Network path (empty / "1"). "2" wires up the local
|
|
355
|
-
// stack — where every value comes from the seeded stack, no prompts needed.
|
|
356
|
-
// "3" is the no-project audit-only fallback.
|
|
357
|
-
if (isAuditChoice(choice)) {
|
|
358
|
-
return configureAuditOnly(binName);
|
|
359
|
-
}
|
|
360
|
-
if (isLocalChoice(choice)) {
|
|
361
|
-
return configureLocalStack(binName, harness, ctx);
|
|
362
|
-
}
|
|
363
|
-
return configureNetwork(binName, harness, ctx);
|
|
364
|
-
}
|
|
365
|
-
function configureAuditOnly(binName) {
|
|
366
|
-
(0, config_js_1.saveConfig)({ auditOnly: true });
|
|
367
|
-
ui.heading("Audit-only mode");
|
|
368
|
-
ui.success("Configured for local / audit-only mode.");
|
|
369
|
-
ui.hint("Tool calls are traced locally; no Ory project, auth, or permission checks.");
|
|
370
|
-
ui.summary([["Config", (0, config_js_1.getConfigPath)()]]);
|
|
371
|
-
ui.blank();
|
|
372
|
-
ui.hint(`To connect to Ory later, re-run install with ${ui.inlineCommand("--reconfigure")}, or run:`);
|
|
373
|
-
ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
|
|
374
|
-
return { outcome: "audit_only" };
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* Configure the plugin against the local Ory stack. Every value the plugin
|
|
378
|
-
* needs — the project URL (`http://localhost:4000`), the public OAuth2 client
|
|
379
|
-
* for the PKCE login (the seeded `ory-user-local`), and the tool permissions —
|
|
380
|
-
* is supplied by the stack itself, so this path takes **no** prompts: the local
|
|
381
|
-
* stack is the default source of otherwise-unprovided values.
|
|
382
|
-
*
|
|
383
|
-
* The flow brings the stack up (idempotent; Docker required), seeds the test
|
|
384
|
-
* identity + `use` permissions, persists the local connection details, and runs
|
|
385
|
-
* the login + agent DCR so the next session is fully wired. Every step is
|
|
386
|
-
* fail-open: if Docker isn't available we still persist the local config and
|
|
387
|
-
* tell the user to run `local up` when they can, so the plugin is pointed at
|
|
388
|
-
* the local stack either way.
|
|
389
|
-
*/
|
|
390
|
-
async function configureLocalStack(binName, harness, ctx) {
|
|
391
|
-
const bringUp = ctx.deps.localStackFn ?? index_js_1.ensureLocalOryStack;
|
|
392
|
-
const seed = ctx.deps.seedFn ?? index_js_1.seedLocalEnvironment;
|
|
393
|
-
ui.heading("Local Ory stack");
|
|
394
|
-
ui.step("Setting up the local Ory stack (Kratos, Keto, Hydra, gateway on :4000).");
|
|
395
|
-
ui.hint("This requires Docker and may take a minute on first run.");
|
|
396
|
-
let stackStatus;
|
|
397
|
-
let stackDetail;
|
|
398
|
-
try {
|
|
399
|
-
const stack = await bringUp();
|
|
400
|
-
stackStatus = stack.status;
|
|
401
|
-
stackDetail = stack.detail;
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
stackStatus = "compose-failed";
|
|
405
|
-
stackDetail = err instanceof Error ? err.message : String(err);
|
|
406
|
-
}
|
|
407
|
-
const running = stackStatus === "started" || stackStatus === "already-running";
|
|
408
|
-
// Point the plugin at the local stack regardless of whether it came up: the
|
|
409
|
-
// seeded PKCE client id is stable, so this config is valid the moment the
|
|
410
|
-
// stack is running (now, or after a later `local up`). Persist the seeded
|
|
411
|
-
// subject namespace too so permission checks address the user as the same
|
|
412
|
-
// SubjectSet (`User:<id>`) the seed writes — without it, enforce mode would
|
|
413
|
-
// never match the seeded tuples.
|
|
414
|
-
(0, config_js_1.saveConfig)({
|
|
415
|
-
projectUrl: index_js_1.GATEWAY_URL,
|
|
416
|
-
oauth2ClientId: index_js_1.USER_CLIENT_ID,
|
|
417
|
-
userSubjectNamespace: index_js_1.USER_SUBJECT_NAMESPACE,
|
|
418
|
-
userLogin: true,
|
|
419
|
-
auditOnly: false,
|
|
420
|
-
});
|
|
421
|
-
if (!running) {
|
|
422
|
-
console.warn("");
|
|
423
|
-
ui.warning(`Could not start the local stack: ${stackDetail ?? stackStatus}.`);
|
|
424
|
-
ui.warnDetail("The plugin is still configured for the local stack — start it when Docker is ready:");
|
|
425
|
-
ui.warnDetail(`${(0, cli_invocation_js_1.oryNpx)(binName)} local up`);
|
|
426
|
-
printLocalStackSummary();
|
|
427
|
-
return {
|
|
428
|
-
outcome: "local_configured",
|
|
429
|
-
projectUrl: index_js_1.GATEWAY_URL,
|
|
430
|
-
oauth2ClientId: index_js_1.USER_CLIENT_ID,
|
|
431
|
-
userAuthenticated: false,
|
|
432
|
-
// The stack seeds its own permissions on `local up`; never run the
|
|
433
|
-
// hosted-project admin bootstrap against localhost.
|
|
434
|
-
skipAutoBootstrap: true,
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
// Seed the identity + `use` permissions into the local stack. Best-effort:
|
|
438
|
-
// observe mode (the install default) keeps tools working even if this fails.
|
|
439
|
-
let credentials = null;
|
|
440
|
-
try {
|
|
441
|
-
const result = await seed();
|
|
442
|
-
credentials = {
|
|
443
|
-
email: result.user.identity.email,
|
|
444
|
-
password: result.user.password,
|
|
445
|
-
};
|
|
446
|
-
ui.success(`Seeded ${result.permissions.tuples} permissions in '${result.permissions.namespace}' for ${result.permissions.subject}.`);
|
|
447
|
-
}
|
|
448
|
-
catch (err) {
|
|
449
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
450
|
-
ui.warning(`Could not seed the local stack: ${msg}`);
|
|
451
|
-
ui.warnDetail(`Seed it later with: ${(0, cli_invocation_js_1.oryNpx)(binName)} local seed`);
|
|
452
|
-
}
|
|
453
|
-
printLocalStackSummary();
|
|
454
|
-
if (credentials) {
|
|
455
|
-
ui.heading("Sign in to the browser login with the seeded test user:");
|
|
456
|
-
ui.summary([
|
|
457
|
-
["Email", credentials.email],
|
|
458
|
-
["Password", credentials.password],
|
|
459
|
-
]);
|
|
460
|
-
}
|
|
461
|
-
const { authenticated } = await completeFullSetup(binName, harness, ctx);
|
|
462
|
-
return {
|
|
463
|
-
outcome: "local_configured",
|
|
464
|
-
projectUrl: index_js_1.GATEWAY_URL,
|
|
465
|
-
oauth2ClientId: index_js_1.USER_CLIENT_ID,
|
|
466
|
-
userAuthenticated: authenticated,
|
|
467
|
-
skipAutoBootstrap: true,
|
|
468
|
-
};
|
|
469
|
-
}
|
|
470
|
-
function printLocalStackSummary() {
|
|
471
|
-
ui.heading("Local stack configured:");
|
|
472
|
-
ui.summary([
|
|
473
|
-
["Project URL", index_js_1.GATEWAY_URL],
|
|
474
|
-
["OAuth2 client id", index_js_1.USER_CLIENT_ID],
|
|
475
|
-
["Subject namespace", `${index_js_1.USER_SUBJECT_NAMESPACE} (matches seeded permissions)`],
|
|
476
|
-
["Config", (0, config_js_1.getConfigPath)()],
|
|
477
|
-
]);
|
|
478
|
-
}
|
|
479
|
-
async function configureNetwork(binName, harness, ctx) {
|
|
480
|
-
const { prompt } = ctx;
|
|
481
|
-
// 1. Ensure the `ory` CLI is available — offer to install it if it isn't,
|
|
482
|
-
// so a from-scratch user (no CLI on PATH) doesn't dead-end here. On accept
|
|
483
|
-
// we install `@ory/cli` into a plugin-managed dir and switch the runner to
|
|
484
|
-
// the freshly installed binary; on decline/failure we fall back to manual.
|
|
485
|
-
let runner = ctx.runner;
|
|
486
|
-
if (!oryCliPresent(runner)) {
|
|
487
|
-
const installed = await maybeInstallOryCli(ctx, prompt);
|
|
488
|
-
if (!installed) {
|
|
489
|
-
printOryCliMissing();
|
|
490
|
-
printManualSetup(binName);
|
|
491
|
-
return { outcome: "network_fallback" };
|
|
492
|
-
}
|
|
493
|
-
runner = installed;
|
|
494
|
-
}
|
|
495
|
-
// 2. Ensure the user is signed in. `ory` persists its own session, so probe
|
|
496
|
-
// first (listing workspaces) and only launch the browser sign-in when the
|
|
497
|
-
// probe fails — otherwise we'd promise a browser that never opens because
|
|
498
|
-
// the user is already authenticated.
|
|
499
|
-
let wsList = runner.exec(["list", "workspaces", "--format", "json"]);
|
|
500
|
-
if (wsList.status !== 0) {
|
|
501
|
-
ui.heading("Sign in to Ory Network");
|
|
502
|
-
ui.info("A browser window will open to sign in or create an account.");
|
|
503
|
-
ui.hint("If it doesn't, watch this terminal for a URL to open manually.");
|
|
504
|
-
ui.hint("The browser handles sign-in only — this terminal drives the rest of setup,");
|
|
505
|
-
ui.hint("so once the browser says you're signed in, come back here to continue.");
|
|
506
|
-
const loginCode = ctx.runInteractive(() => runner.execInteractive(["auth"]));
|
|
507
|
-
// The `ory auth` browser page is rendered by the Ory CLI, so we can't tell
|
|
508
|
-
// the user there to return to the terminal. Ring the bell instead — their
|
|
509
|
-
// focus is still on the browser, and this pulls it back to the terminal
|
|
510
|
-
// (dock bounce / tab badge) where the next steps live.
|
|
511
|
-
ui.bell();
|
|
512
|
-
if (loginCode !== 0) {
|
|
513
|
-
console.warn("");
|
|
514
|
-
ui.warning("Ory Network sign-in did not complete.");
|
|
515
|
-
printManualSetup(binName);
|
|
516
|
-
return { outcome: "network_fallback" };
|
|
517
|
-
}
|
|
518
|
-
ui.success("Signed in to Ory Network — back in the terminal now.");
|
|
519
|
-
ui.step("Next, choose your workspace and project here. Loading your workspaces…");
|
|
520
|
-
wsList = runner.exec(["list", "workspaces", "--format", "json"]);
|
|
521
|
-
}
|
|
522
|
-
else {
|
|
523
|
-
ui.heading("Ory Network");
|
|
524
|
-
ui.success("Already signed in to Ory Network.");
|
|
525
|
-
}
|
|
526
|
-
// 3. Pick a workspace (offer to create one when the account has none — a
|
|
527
|
-
// brand-new account may have no workspace yet).
|
|
528
|
-
let workspaces = wsList.status === 0 ? parseList(wsList.stdout, "workspaces") : null;
|
|
529
|
-
if (!workspaces || workspaces.length === 0) {
|
|
530
|
-
const created = await maybeCreateWorkspace(runner, prompt, harness);
|
|
531
|
-
if (created)
|
|
532
|
-
workspaces = [created];
|
|
533
|
-
}
|
|
534
|
-
if (!workspaces || workspaces.length === 0) {
|
|
535
|
-
console.warn("");
|
|
536
|
-
ui.warning("No Ory Network workspace selected.");
|
|
537
|
-
ui.warnDetail("Create one at https://console.ory.sh/ and re-run install with --reconfigure.");
|
|
538
|
-
printManualSetup(binName);
|
|
539
|
-
return { outcome: "network_fallback" };
|
|
540
|
-
}
|
|
541
|
-
const workspace = await selectFromList(prompt, "workspace", workspaces, (w) => w.name ?? w.id);
|
|
542
|
-
if (!workspace) {
|
|
543
|
-
printManualSetup(binName);
|
|
544
|
-
return { outcome: "network_fallback" };
|
|
545
|
-
}
|
|
546
|
-
// 4. Pick a project. A brand-new workspace with none gets the create prompt;
|
|
547
|
-
// otherwise the existing projects are listed for selection, and while the
|
|
548
|
-
// workspace is still under the Developer-plan cap of two we also offer to
|
|
549
|
-
// create a fresh project dedicated to the agent plugin — so a single-project
|
|
550
|
-
// account isn't forced to reuse its existing project.
|
|
551
|
-
ui.step("Loading projects…");
|
|
552
|
-
const existingProjects = listProjects(runner, workspace.id);
|
|
553
|
-
const project = !existingProjects || existingProjects.length === 0
|
|
554
|
-
? await maybeCreateProject(runner, prompt, workspace, harness)
|
|
555
|
-
: await selectOrCreateProject(runner, prompt, workspace, harness, existingProjects);
|
|
556
|
-
if (!project) {
|
|
557
|
-
console.warn("");
|
|
558
|
-
ui.warning("No Ory Network project selected.");
|
|
559
|
-
printManualSetup(binName);
|
|
560
|
-
return { outcome: "network_fallback" };
|
|
561
|
-
}
|
|
562
|
-
// The SDK URL is built from the project *slug*. Require a real slug — a
|
|
563
|
-
// missing one, or a value that looks like the project id (UUID), can't
|
|
564
|
-
// produce a correct URL, so fall back to manual setup rather than guess.
|
|
565
|
-
if (!project.slug || (0, config_js_1.looksLikeProjectId)(project.slug)) {
|
|
566
|
-
console.warn("");
|
|
567
|
-
ui.warning("Could not resolve the selected project's slug (needed for the SDK URL).");
|
|
568
|
-
printManualSetup(binName);
|
|
569
|
-
return { outcome: "network_fallback" };
|
|
570
|
-
}
|
|
571
|
-
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");
|
|
582
|
-
let clientId = findExistingOAuth2Client(runner, project.id);
|
|
583
|
-
if (clientId) {
|
|
584
|
-
ui.step(`Reusing the existing OAuth2 client for user login (${clientId}).`);
|
|
585
|
-
}
|
|
586
|
-
else {
|
|
587
|
-
ui.step("Creating the OAuth2 client for user login...");
|
|
588
|
-
clientId = createOAuth2Client(runner, project.id);
|
|
589
|
-
}
|
|
590
|
-
if (!clientId) {
|
|
591
|
-
ui.warning("Could not create the OAuth2 client automatically.");
|
|
592
|
-
ui.warnDetail(`Project URL resolved: ${projectUrl}`);
|
|
593
|
-
printManualOAuth2ClientHelp(binName, project.id);
|
|
594
|
-
// Still persist the project URL + id so the user only needs the client id.
|
|
595
|
-
(0, config_js_1.saveConfig)({ projectUrl, projectId: project.id, auditOnly: false });
|
|
596
|
-
return { outcome: "network_fallback", projectUrl };
|
|
597
|
-
}
|
|
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.
|
|
601
|
-
(0, config_js_1.saveConfig)({
|
|
602
|
-
projectUrl,
|
|
603
|
-
projectId: project.id,
|
|
604
|
-
oauth2ClientId: clientId,
|
|
605
|
-
userLogin: true,
|
|
606
|
-
auditOnly: false,
|
|
607
|
-
});
|
|
608
|
-
ui.heading("Ory Network connected:");
|
|
609
|
-
ui.success("Project connected and OAuth2 client created.");
|
|
610
|
-
ui.summary([
|
|
611
|
-
["Project URL", projectUrl],
|
|
612
|
-
["OAuth2 client id", clientId],
|
|
613
|
-
["Config", (0, config_js_1.getConfigPath)()],
|
|
614
|
-
]);
|
|
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.
|
|
621
|
-
if (subject) {
|
|
622
|
-
// Persist the subject *shape* this grant used. If the install resolved a
|
|
623
|
-
// SubjectSet (ORY_USER_SUBJECT_NAMESPACE was set), a later runtime session
|
|
624
|
-
// that lacks the env var would otherwise resolve a direct subject_id and
|
|
625
|
-
// check/write a divergent tuple for the same user. Persisting the namespace
|
|
626
|
-
// keeps install-time and runtime writes on one canonical subject shape.
|
|
627
|
-
if ("subjectSet" in subject) {
|
|
628
|
-
(0, config_js_1.saveConfig)({ userSubjectNamespace: subject.subjectSet.namespace });
|
|
629
|
-
}
|
|
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);
|
|
638
|
-
return {
|
|
639
|
-
outcome: "network_configured",
|
|
640
|
-
projectUrl,
|
|
641
|
-
oauth2ClientId: clientId,
|
|
642
|
-
userAuthenticated: authenticated,
|
|
643
|
-
// We just handled bootstrap with admin scope; don't let runPostInstall
|
|
644
|
-
// retry it with the scope-less DCR token (which would only 403).
|
|
645
|
-
skipAutoBootstrap: true,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
async function completeFullSetup(binName, harness, ctx) {
|
|
649
|
-
const clientFactory = ctx.deps.clientFactory ?? client_js_1.OryAgentClient.fromEnv;
|
|
650
|
-
const userLogin = ctx.deps.userLoginFn ?? user_login_js_1.ensureUserAuthenticated;
|
|
651
|
-
const agentGate = ctx.deps.agentGateFn ?? agent_auth_js_1.ensureAgentIdentity;
|
|
652
|
-
try {
|
|
653
|
-
const client = clientFactory(harness);
|
|
654
|
-
ui.heading("Signing you in to the project as the permission subject.");
|
|
655
|
-
ui.hint("If a sign-in is needed, a browser will open (watch for a URL here too).");
|
|
656
|
-
const decision = await userLogin(client, {
|
|
657
|
-
binName,
|
|
658
|
-
harness,
|
|
659
|
-
allowBlock: false,
|
|
660
|
-
});
|
|
661
|
-
// Resolve the agent identity regardless — it bootstraps from the user
|
|
662
|
-
// token when present and no-ops otherwise. Never blocks.
|
|
663
|
-
await agentGate(client, { harness });
|
|
664
|
-
const authenticated = decision.proceed && !!decision.subject;
|
|
665
|
-
const subject = authenticated ? (0, subject_js_1.resolveUserSubject)(client) : null;
|
|
666
|
-
return {
|
|
667
|
-
authenticated,
|
|
668
|
-
subject: subject && (0, subject_js_1.subjectLabel)(subject) !== "agent:unknown" ? subject : null,
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
catch (err) {
|
|
672
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
673
|
-
ui.warning(`Skipping automatic login (will run on first session): ${msg}`);
|
|
674
|
-
return { authenticated: false, subject: null };
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
/**
|
|
678
|
-
* Grant the signed-in user `use` on every built-in tool by writing relation
|
|
679
|
-
* tuples through the admin-authenticated `ory` CLI (`ory create
|
|
680
|
-
* relationships`). This is the one credential in the flow with project-admin
|
|
681
|
-
* scope — the agent's DCR token and the user's PKCE token can't write tuples
|
|
682
|
-
* on a hosted Ory Network project.
|
|
683
|
-
*
|
|
684
|
-
* The tuples reference the permission namespace (default `AgentTools`), which
|
|
685
|
-
* must be defined in the project's permission model or the write fails with
|
|
686
|
-
* `NotFound`. So we inspect the model first (see {@link inspectPermissionModel})
|
|
687
|
-
* and act on what's actually there. We provision in every case *except* when
|
|
688
|
-
* the namespace already exists:
|
|
689
|
-
*
|
|
690
|
-
* - **Namespace already defined** → grant directly, touch nothing.
|
|
691
|
-
* - **No model yet** (a freshly created Ory Network project) → offer to
|
|
692
|
-
* provision a minimal `<namespace>` model (`ory update opl`), then grant.
|
|
693
|
-
* - **Model exists (OPL source fetchable) but lacks `<namespace>`** → offer
|
|
694
|
-
* to *merge* the namespace into the existing OPL and re-upload it, leaving
|
|
695
|
-
* the other namespaces untouched, then grant. Ory Network serves the OPL
|
|
696
|
-
* source at a `location` URL in the permission config, which makes this
|
|
697
|
-
* non-destructive merge possible.
|
|
698
|
-
* - **Model provably exists but its source couldn't be read** → we can't
|
|
699
|
-
* merge and `ory update opl` overwrites the whole OPL, so we refuse to
|
|
700
|
-
* clobber it: print the exact snippet to add and skip the grant.
|
|
701
|
-
*
|
|
702
|
-
* Best-effort throughout: any failure prints a short, actionable note rather
|
|
703
|
-
* than a wall of errors. Observe mode (the install default) keeps tools working
|
|
704
|
-
* regardless, so this is an optimization, not a hard requirement.
|
|
705
|
-
*/
|
|
706
|
-
async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, prompt) {
|
|
707
|
-
const namespace = process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
708
|
-
const tools = (0, tool_catalog_js_1.getToolCatalog)(harness);
|
|
709
|
-
if (tools.length === 0)
|
|
710
|
-
return;
|
|
711
|
-
// Ensure the namespace the tuples reference exists, or the write NotFounds.
|
|
712
|
-
// 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.
|
|
717
|
-
const model = await inspectPermissionModel(runner, projectId, namespace);
|
|
718
|
-
if (model.kind === "merge") {
|
|
719
|
-
// 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
|
-
}
|
|
731
|
-
if (!provisionPermissionModel(runner, projectId, namespace, model.opl)) {
|
|
732
|
-
ui.warning(`Could not add '${namespace}' to the permission model automatically.`);
|
|
733
|
-
printPermissionModelHelp(namespace);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
ui.success(`Added '${namespace}' to the permission model.`);
|
|
737
|
-
}
|
|
738
|
-
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
|
-
}
|
|
749
|
-
if (!provisionPermissionModel(runner, projectId, namespace)) {
|
|
750
|
-
ui.warning(`Could not create the '${namespace}' permission model automatically.`);
|
|
751
|
-
printPermissionModelHelp(namespace);
|
|
752
|
-
return;
|
|
753
|
-
}
|
|
754
|
-
ui.success(`Created the '${namespace}' permission model.`);
|
|
755
|
-
}
|
|
756
|
-
else if (model.kind === "blocked") {
|
|
757
|
-
// A model provably exists but we couldn't fetch its source, so we can't
|
|
758
|
-
// 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.");
|
|
761
|
-
printPermissionModelHelp(namespace);
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
// "present" (namespace already defined) falls through and grants directly.
|
|
765
|
-
const subjectPatch = "subjectSet" in subject
|
|
766
|
-
? {
|
|
767
|
-
subject_set: {
|
|
768
|
-
namespace: subject.subjectSet.namespace,
|
|
769
|
-
object: subject.subjectSet.object,
|
|
770
|
-
relation: subject.subjectSet.relation,
|
|
771
|
-
},
|
|
772
|
-
}
|
|
773
|
-
: { subject_id: subject.subjectId };
|
|
774
|
-
const tuples = tools.map((object) => ({
|
|
775
|
-
namespace,
|
|
776
|
-
object,
|
|
777
|
-
relation: "use",
|
|
778
|
-
...subjectPatch,
|
|
779
|
-
}));
|
|
780
|
-
ui.heading("Permissions");
|
|
781
|
-
ui.step(`Granting ${(0, subject_js_1.subjectLabel)(subject)} 'use' on ${tools.length} tools (via the Ory CLI)...`);
|
|
782
|
-
const r = runner.exec(["create", "relationships", "--project", projectId, "--format", "json", "-f", "-", "--yes"], { input: JSON.stringify(tuples) });
|
|
783
|
-
if (r.status === 0) {
|
|
784
|
-
ui.success(`Granted permissions for ${tools.length} tools in '${namespace}'.`);
|
|
785
|
-
return;
|
|
786
|
-
}
|
|
787
|
-
ui.warning("Could not pre-grant permissions automatically.");
|
|
788
|
-
const detail = r.stderr.trim();
|
|
789
|
-
if (detail)
|
|
790
|
-
ui.warnDetail(detail.split("\n")[0]);
|
|
791
|
-
printPermissionModelHelp(namespace);
|
|
792
|
-
}
|
|
793
|
-
/**
|
|
794
|
-
* Inspect the project's permission model. Ory Network reports the config in one
|
|
795
|
-
* of two shapes: `{ namespaces: [] }` (or `null`) for a project with no model,
|
|
796
|
-
* and `{ namespaces: { location: "https://….txt" } }` for an OPL-configured
|
|
797
|
-
* one — where the `.txt` is the full OPL *source*. We fetch that source so a
|
|
798
|
-
* missing namespace can be merged in rather than clobbering the whole model.
|
|
799
|
-
*/
|
|
800
|
-
async function inspectPermissionModel(runner, projectId, namespace) {
|
|
801
|
-
const r = runner.exec([
|
|
802
|
-
"get",
|
|
803
|
-
"permission-config",
|
|
804
|
-
"--project",
|
|
805
|
-
projectId,
|
|
806
|
-
"--format",
|
|
807
|
-
"json",
|
|
808
|
-
]);
|
|
809
|
-
if (r.status !== 0)
|
|
810
|
-
return { kind: "unknown" };
|
|
811
|
-
const cfg = safeJsonParse(r.stdout);
|
|
812
|
-
if (!cfg || typeof cfg !== "object")
|
|
813
|
-
return { kind: "unknown" };
|
|
814
|
-
const ns = cfg.namespaces;
|
|
815
|
-
// A fresh project reports no model as null / an absent key.
|
|
816
|
-
if (ns == null)
|
|
817
|
-
return { kind: "empty" };
|
|
818
|
-
// OPL-configured form: { namespaces: { location: "https://….txt" } }.
|
|
819
|
-
// The .txt holds the full OPL source, so we can merge into it.
|
|
820
|
-
if (!Array.isArray(ns) && typeof ns === "object") {
|
|
821
|
-
const location = ns.location;
|
|
822
|
-
if (typeof location !== "string" || !location)
|
|
823
|
-
return { kind: "empty" };
|
|
824
|
-
const opl = await fetchOplSource(location);
|
|
825
|
-
// A model exists but its source is unreadable — can't merge, mustn't clobber.
|
|
826
|
-
if (opl == null)
|
|
827
|
-
return { kind: "blocked" };
|
|
828
|
-
return oplDefinesClass(opl, namespace)
|
|
829
|
-
? { kind: "present" }
|
|
830
|
-
: { kind: "merge", opl };
|
|
831
|
-
}
|
|
832
|
-
// Inline compiled form: { namespaces: [{ name, id }, …] } (legacy / self-hosted).
|
|
833
|
-
// We only have names, not source, so a missing namespace can't be merged.
|
|
834
|
-
if (Array.isArray(ns)) {
|
|
835
|
-
if (ns.length === 0)
|
|
836
|
-
return { kind: "empty" };
|
|
837
|
-
const names = ns
|
|
838
|
-
.map((n) => n && typeof n === "object"
|
|
839
|
-
? n.name
|
|
840
|
-
: undefined)
|
|
841
|
-
.filter((n) => typeof n === "string");
|
|
842
|
-
return names.includes(namespace) ? { kind: "present" } : { kind: "blocked" };
|
|
843
|
-
}
|
|
844
|
-
return { kind: "unknown" };
|
|
845
|
-
}
|
|
846
|
-
/** Fetch the OPL source served at a permission-config `location` URL. */
|
|
847
|
-
async function fetchOplSource(url) {
|
|
848
|
-
try {
|
|
849
|
-
const res = await fetch(url);
|
|
850
|
-
if (!res.ok)
|
|
851
|
-
return null;
|
|
852
|
-
return await res.text();
|
|
853
|
-
}
|
|
854
|
-
catch {
|
|
855
|
-
return null;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
/** Whether an OPL source defines `class <name>`. */
|
|
859
|
-
function oplDefinesClass(opl, name) {
|
|
860
|
-
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
861
|
-
return new RegExp(`\\bclass\\s+${escaped}\\b`).test(opl);
|
|
862
|
-
}
|
|
863
|
-
/**
|
|
864
|
-
* Provision a permission model that defines `<namespace>` with a `use`
|
|
865
|
-
* relation, via `ory update opl`. The namespace becomes an OPL class, so it
|
|
866
|
-
* must be a valid identifier; if not, we bail (caller guides instead).
|
|
867
|
-
*
|
|
868
|
-
* When `existingOpl` is given, the namespace is appended to that source
|
|
869
|
-
* (preserving the model's other namespaces); otherwise a fresh minimal model
|
|
870
|
-
* is generated. Writes the OPL to a temp file because `ory update opl` reads
|
|
871
|
-
* `--file` only (no stdin). Returns true on success.
|
|
872
|
-
*/
|
|
873
|
-
function provisionPermissionModel(runner, projectId, namespace, existingOpl) {
|
|
874
|
-
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace))
|
|
875
|
-
return false;
|
|
876
|
-
const opl = existingOpl !== undefined
|
|
877
|
-
? mergeNamespaceIntoOpl(existingOpl, namespace)
|
|
878
|
-
: freshOpl(namespace);
|
|
879
|
-
let dir;
|
|
880
|
-
try {
|
|
881
|
-
dir = fs.mkdtempSync(path.join(os.tmpdir(), "ory-opl-"));
|
|
882
|
-
}
|
|
883
|
-
catch {
|
|
884
|
-
return false;
|
|
885
|
-
}
|
|
886
|
-
const file = path.join(dir, "namespace_config.ts");
|
|
887
|
-
try {
|
|
888
|
-
fs.writeFileSync(file, opl);
|
|
889
|
-
const r = runner.exec([
|
|
890
|
-
"update",
|
|
891
|
-
"opl",
|
|
892
|
-
"--project",
|
|
893
|
-
projectId,
|
|
894
|
-
"-f",
|
|
895
|
-
file,
|
|
896
|
-
"--format",
|
|
897
|
-
"json",
|
|
898
|
-
"--yes",
|
|
899
|
-
]);
|
|
900
|
-
return r.status === 0;
|
|
901
|
-
}
|
|
902
|
-
catch {
|
|
903
|
-
return false;
|
|
904
|
-
}
|
|
905
|
-
finally {
|
|
906
|
-
try {
|
|
907
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
908
|
-
}
|
|
909
|
-
catch {
|
|
910
|
-
/* ignore */
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
/** A minimal standalone OPL defining `User` and `<namespace>` with `use`. */
|
|
915
|
-
function freshOpl(namespace) {
|
|
916
|
-
return [
|
|
917
|
-
'import { Namespace } from "@ory/permission-namespace-types"',
|
|
918
|
-
"",
|
|
919
|
-
"class User implements Namespace {}",
|
|
920
|
-
"",
|
|
921
|
-
`class ${namespace} implements Namespace {`,
|
|
922
|
-
" related: {",
|
|
923
|
-
" use: User[]",
|
|
924
|
-
" }",
|
|
925
|
-
"}",
|
|
926
|
-
"",
|
|
927
|
-
].join("\n");
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Append a `<namespace>` class (with a `use: User[]` relation) to an existing
|
|
931
|
-
* OPL source, leaving every other namespace untouched. Adds a `User` class too
|
|
932
|
-
* when the source doesn't already define one, since `use` is typed `User[]`.
|
|
933
|
-
*/
|
|
934
|
-
function mergeNamespaceIntoOpl(existingOpl, namespace) {
|
|
935
|
-
let opl = existingOpl.replace(/\s*$/, "\n");
|
|
936
|
-
if (!oplDefinesClass(opl, "User")) {
|
|
937
|
-
opl += "\nclass User implements Namespace {}\n";
|
|
938
|
-
}
|
|
939
|
-
opl +=
|
|
940
|
-
`\nclass ${namespace} implements Namespace {\n` +
|
|
941
|
-
" related: {\n" +
|
|
942
|
-
" use: User[]\n" +
|
|
943
|
-
" }\n" +
|
|
944
|
-
"}\n";
|
|
945
|
-
return opl;
|
|
946
|
-
}
|
|
947
|
-
function printPermissionModelHelp(namespace) {
|
|
948
|
-
// When the namespace is a valid OPL class name, show the exact snippet to
|
|
949
|
-
// paste into the model — that's the concrete "how" behind "define it". The
|
|
950
|
-
// 'use' relation is typed `User[]`, so `User` is a compile-time dependency:
|
|
951
|
-
// include it too, since an existing model may not already define it.
|
|
952
|
-
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace)) {
|
|
953
|
-
ui.warnDetail(`Add the '${namespace}' namespace to your Ory Permissions model. Its 'use'`);
|
|
954
|
-
ui.warnDetail("relation is typed `User[]`, so `User` must be defined too:");
|
|
955
|
-
console.warn("");
|
|
956
|
-
console.warn(ui.style.gray(" class User implements Namespace {}"));
|
|
957
|
-
console.warn("");
|
|
958
|
-
console.warn(ui.style.gray(` class ${namespace} implements Namespace {`));
|
|
959
|
-
console.warn(ui.style.gray(" related: { use: User[] }"));
|
|
960
|
-
console.warn(ui.style.gray(" }"));
|
|
961
|
-
console.warn("");
|
|
962
|
-
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.");
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
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.");
|
|
969
|
-
}
|
|
970
|
-
// ─── ory CLI helpers ──────────────────────────────────────────────────────
|
|
971
|
-
function oryCliPresent(runner) {
|
|
972
|
-
return runner.exec(["version"]).status === 0;
|
|
973
|
-
}
|
|
974
|
-
/**
|
|
975
|
-
* The `ory` binary isn't on `PATH`. Offer (prompted, default yes) to install it
|
|
976
|
-
* automatically so the Ory Network path doesn't dead-end for a from-scratch
|
|
977
|
-
* user. On accept, install `@ory/cli` into a plugin-managed dir and return a
|
|
978
|
-
* runner bound to it; on decline or failure, return null so the caller falls
|
|
979
|
-
* back to the manual instructions. Never throws.
|
|
980
|
-
*/
|
|
981
|
-
async function maybeInstallOryCli(ctx, prompt) {
|
|
982
|
-
ui.heading("Ory CLI");
|
|
983
|
-
ui.info("Connecting to Ory Network needs the Ory CLI, which isn't installed yet.");
|
|
984
|
-
ui.blank();
|
|
985
|
-
const answer = await prompt(ui.promptLine("Install it now (via npm, no system changes)?", { hint: "[Y/n]" }));
|
|
986
|
-
if (isNo(answer))
|
|
987
|
-
return null;
|
|
988
|
-
ui.step(`Installing ${exports.ORY_CLI_NPM_SPEC} — this can take a moment...`);
|
|
989
|
-
const install = ctx.deps.installOryCliFn ?? installOryCli;
|
|
990
|
-
let runner;
|
|
991
|
-
try {
|
|
992
|
-
runner = await install();
|
|
993
|
-
}
|
|
994
|
-
catch {
|
|
995
|
-
runner = null;
|
|
996
|
-
}
|
|
997
|
-
if (!runner) {
|
|
998
|
-
ui.warning("Could not install the Ory CLI automatically.");
|
|
999
|
-
return null;
|
|
1000
|
-
}
|
|
1001
|
-
ui.success("Ory CLI installed.");
|
|
1002
|
-
return runner;
|
|
1003
|
-
}
|
|
1004
|
-
function listProjects(runner, workspaceId) {
|
|
1005
|
-
const r = runner.exec([
|
|
1006
|
-
"list",
|
|
1007
|
-
"projects",
|
|
1008
|
-
"--workspace",
|
|
1009
|
-
workspaceId,
|
|
1010
|
-
"--format",
|
|
1011
|
-
"json",
|
|
1012
|
-
]);
|
|
1013
|
-
if (r.status !== 0)
|
|
1014
|
-
return null;
|
|
1015
|
-
return parseList(r.stdout, "projects");
|
|
1016
|
-
}
|
|
1017
|
-
async function maybeCreateWorkspace(runner, prompt, harness) {
|
|
1018
|
-
ui.heading("Your Ory Network account has no workspaces yet.");
|
|
1019
|
-
ui.blank();
|
|
1020
|
-
const answer = await prompt(ui.promptLine("Create a new workspace now?", { hint: "[Y/n]" }));
|
|
1021
|
-
if (isNo(answer))
|
|
1022
|
-
return null;
|
|
1023
|
-
const defaultName = `${harness}-workspace`;
|
|
1024
|
-
const nameInput = await prompt(ui.promptLine("Workspace name", { hint: `[${defaultName}]` }));
|
|
1025
|
-
const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
|
|
1026
|
-
const r = runner.exec([
|
|
1027
|
-
"create",
|
|
1028
|
-
"workspace",
|
|
1029
|
-
"--name",
|
|
1030
|
-
name,
|
|
1031
|
-
"--format",
|
|
1032
|
-
"json",
|
|
1033
|
-
"--yes",
|
|
1034
|
-
]);
|
|
1035
|
-
if (r.status !== 0) {
|
|
1036
|
-
ui.warning(`Could not create the workspace: ${r.stderr.trim() || "unknown error"}`);
|
|
1037
|
-
return null;
|
|
1038
|
-
}
|
|
1039
|
-
const workspace = parseObject(r.stdout);
|
|
1040
|
-
if (!workspace || !workspace.id)
|
|
1041
|
-
return null;
|
|
1042
|
-
ui.success(`Created workspace "${workspace.name ?? name}".`);
|
|
1043
|
-
return workspace;
|
|
1044
|
-
}
|
|
1045
|
-
async function maybeCreateProject(runner, prompt, workspace, harness) {
|
|
1046
|
-
const wsLabel = workspace.name ?? workspace.id;
|
|
1047
|
-
ui.heading(`Workspace "${wsLabel}" has no projects yet.`);
|
|
1048
|
-
ui.blank();
|
|
1049
|
-
const answer = await prompt(ui.promptLine("Create a new project now?", { hint: "[Y/n]" }));
|
|
1050
|
-
if (isNo(answer))
|
|
1051
|
-
return null;
|
|
1052
|
-
return createProjectInteractive(runner, prompt, workspace, harness);
|
|
1053
|
-
}
|
|
1054
|
-
/**
|
|
1055
|
-
* Choose the project to wire up when the workspace already has at least one.
|
|
1056
|
-
* Lists the existing projects for selection and — while the workspace is still
|
|
1057
|
-
* under the Ory Network Developer-plan cap of two projects — offers a "create a
|
|
1058
|
-
* new project" option, so an account with a single project can dedicate a fresh
|
|
1059
|
-
* one to the agent plugin instead of being forced onto the existing project.
|
|
1060
|
-
* Returns null if the user cancels.
|
|
1061
|
-
*/
|
|
1062
|
-
async function selectOrCreateProject(runner, prompt, workspace, harness, projects) {
|
|
1063
|
-
// Developer plans cap a workspace at two projects; only offer to create while
|
|
1064
|
-
// there's still headroom.
|
|
1065
|
-
const PROJECT_CAP = 2;
|
|
1066
|
-
const canCreate = projects.length < PROJECT_CAP;
|
|
1067
|
-
ui.heading("Select a project:");
|
|
1068
|
-
ui.blank();
|
|
1069
|
-
const entries = projects.map((p, i) => ({
|
|
1070
|
-
key: String(i + 1),
|
|
1071
|
-
label: `${p.name ?? p.slug ?? p.id}${p.slug ? ` (${p.slug})` : ""}`,
|
|
1072
|
-
}));
|
|
1073
|
-
if (canCreate) {
|
|
1074
|
-
entries.push({
|
|
1075
|
-
key: String(projects.length + 1),
|
|
1076
|
-
label: "Create a new project for the agent plugin",
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
ui.menu(entries);
|
|
1080
|
-
ui.blank();
|
|
1081
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1082
|
-
const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${entries.length}]` }));
|
|
1083
|
-
if (answer === null || answer.trim() === "")
|
|
1084
|
-
return null;
|
|
1085
|
-
const n = Number.parseInt(answer.trim(), 10);
|
|
1086
|
-
if (Number.isInteger(n) && n >= 1 && n <= projects.length) {
|
|
1087
|
-
return projects[n - 1];
|
|
1088
|
-
}
|
|
1089
|
-
if (canCreate && n === projects.length + 1) {
|
|
1090
|
-
return createProjectInteractive(runner, prompt, workspace, harness);
|
|
1091
|
-
}
|
|
1092
|
-
ui.warning(`"${answer}" is not a valid choice.`);
|
|
1093
|
-
}
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
/**
|
|
1097
|
-
* Prompt for a project name (defaulting to `<harness>-agent`) and create it in
|
|
1098
|
-
* the given workspace. Shared by the empty-workspace path and the "create a new
|
|
1099
|
-
* project" menu option. Returns null on failure.
|
|
1100
|
-
*/
|
|
1101
|
-
async function createProjectInteractive(runner, prompt, workspace, harness) {
|
|
1102
|
-
const defaultName = `${harness}-agent`;
|
|
1103
|
-
const nameInput = await prompt(ui.promptLine("New project name", { hint: `[${defaultName}]` }));
|
|
1104
|
-
const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
|
|
1105
|
-
const r = runner.exec([
|
|
1106
|
-
"create",
|
|
1107
|
-
"project",
|
|
1108
|
-
"--name",
|
|
1109
|
-
name,
|
|
1110
|
-
"--workspace",
|
|
1111
|
-
workspace.id,
|
|
1112
|
-
"--format",
|
|
1113
|
-
"json",
|
|
1114
|
-
"--yes",
|
|
1115
|
-
]);
|
|
1116
|
-
if (r.status !== 0) {
|
|
1117
|
-
ui.warning(`Could not create the project: ${r.stderr.trim() || "unknown error"}`);
|
|
1118
|
-
return null;
|
|
1119
|
-
}
|
|
1120
|
-
const project = parseObject(r.stdout);
|
|
1121
|
-
if (!project || !project.id)
|
|
1122
|
-
return null;
|
|
1123
|
-
ui.success(`Created project "${project.name ?? name}".`);
|
|
1124
|
-
return project;
|
|
1125
|
-
}
|
|
1126
|
-
/** JSON-Patch path (relative to the oauth2 service config) that toggles DCR. */
|
|
1127
|
-
const DCR_ENABLED_PATH = "/oidc/dynamic_client_registration/enabled";
|
|
1128
|
-
/**
|
|
1129
|
-
* Read the project's current DCR state via `ory get oauth2-config`.
|
|
1130
|
-
* Returns `true`/`false` when the value is known, or `null` when it can't be
|
|
1131
|
-
* determined (CLI error, unparseable output) so the caller falls open and
|
|
1132
|
-
* prompts anyway rather than assuming a state.
|
|
1133
|
-
*/
|
|
1134
|
-
function dcrEnabled(runner, projectId) {
|
|
1135
|
-
const r = runner.exec([
|
|
1136
|
-
"get",
|
|
1137
|
-
"oauth2-config",
|
|
1138
|
-
"--project",
|
|
1139
|
-
projectId,
|
|
1140
|
-
"--format",
|
|
1141
|
-
"json",
|
|
1142
|
-
]);
|
|
1143
|
-
if (r.status !== 0)
|
|
1144
|
-
return null;
|
|
1145
|
-
const config = parseObject(r.stdout);
|
|
1146
|
-
if (!config)
|
|
1147
|
-
return null;
|
|
1148
|
-
const enabled = config.oidc?.dynamic_client_registration?.enabled;
|
|
1149
|
-
return typeof enabled === "boolean" ? enabled : null;
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
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.
|
|
1157
|
-
*/
|
|
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
|
-
}
|
|
1173
|
-
const r = runner.exec([
|
|
1174
|
-
"patch",
|
|
1175
|
-
"oauth2-config",
|
|
1176
|
-
"--project",
|
|
1177
|
-
projectId,
|
|
1178
|
-
"--replace",
|
|
1179
|
-
`${DCR_ENABLED_PATH}=true`,
|
|
1180
|
-
"--format",
|
|
1181
|
-
"json",
|
|
1182
|
-
"--yes",
|
|
1183
|
-
]);
|
|
1184
|
-
if (r.status === 0) {
|
|
1185
|
-
ui.success("Dynamic Client Registration enabled.");
|
|
1186
|
-
}
|
|
1187
|
-
else {
|
|
1188
|
-
ui.warning(`Could not enable DCR automatically: ${r.stderr.trim() || "unknown error"}`);
|
|
1189
|
-
ui.warnDetail(`Enable it later with: ${manual}`);
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
/** The `client_name` every plugin-provisioned public PKCE client carries. */
|
|
1193
|
-
const OAUTH2_CLIENT_NAME = "ory-agent-plugin";
|
|
1194
|
-
/**
|
|
1195
|
-
* 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.
|
|
1199
|
-
*/
|
|
1200
|
-
function findExistingOAuth2Client(runner, projectId) {
|
|
1201
|
-
const r = runner.exec([
|
|
1202
|
-
"list",
|
|
1203
|
-
"oauth2-clients",
|
|
1204
|
-
"--project",
|
|
1205
|
-
projectId,
|
|
1206
|
-
"--format",
|
|
1207
|
-
"json",
|
|
1208
|
-
]);
|
|
1209
|
-
if (r.status !== 0)
|
|
1210
|
-
return null;
|
|
1211
|
-
const clients = parseList(r.stdout, "items");
|
|
1212
|
-
if (!clients)
|
|
1213
|
-
return null;
|
|
1214
|
-
const match = clients.find((c) => c.client_name === OAUTH2_CLIENT_NAME && c.client_id);
|
|
1215
|
-
return match?.client_id ?? null;
|
|
1216
|
-
}
|
|
1217
|
-
function createOAuth2Client(runner, projectId) {
|
|
1218
|
-
const args = [
|
|
1219
|
-
"create",
|
|
1220
|
-
"oauth2-client",
|
|
1221
|
-
"--project",
|
|
1222
|
-
projectId,
|
|
1223
|
-
"--name",
|
|
1224
|
-
OAUTH2_CLIENT_NAME,
|
|
1225
|
-
"--grant-type",
|
|
1226
|
-
"authorization_code,refresh_token",
|
|
1227
|
-
"--response-type",
|
|
1228
|
-
"code",
|
|
1229
|
-
"--scope",
|
|
1230
|
-
"openid,offline_access",
|
|
1231
|
-
"--token-endpoint-auth-method",
|
|
1232
|
-
"none",
|
|
1233
|
-
"--format",
|
|
1234
|
-
"json",
|
|
1235
|
-
"--yes",
|
|
1236
|
-
];
|
|
1237
|
-
for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
|
|
1238
|
-
args.push("--redirect-uri", uri);
|
|
1239
|
-
}
|
|
1240
|
-
const r = runner.exec(args);
|
|
1241
|
-
if (r.status !== 0)
|
|
1242
|
-
return null;
|
|
1243
|
-
const client = parseObject(r.stdout);
|
|
1244
|
-
return client?.client_id ?? null;
|
|
1245
|
-
}
|
|
1246
|
-
// ─── parsing helpers ────────────────────────────────────────────────────────
|
|
1247
|
-
/**
|
|
1248
|
-
* Parse a JSON list from `ory --format json` output. Tolerates both a bare
|
|
1249
|
-
* array and the `{ <key>: [...] }` envelope the Console list endpoints use.
|
|
1250
|
-
*/
|
|
1251
|
-
function parseList(stdout, key) {
|
|
1252
|
-
const parsed = safeJsonParse(stdout);
|
|
1253
|
-
if (parsed === null)
|
|
1254
|
-
return null;
|
|
1255
|
-
if (Array.isArray(parsed))
|
|
1256
|
-
return parsed;
|
|
1257
|
-
if (typeof parsed === "object") {
|
|
1258
|
-
const value = parsed[key];
|
|
1259
|
-
if (Array.isArray(value))
|
|
1260
|
-
return value;
|
|
1261
|
-
}
|
|
1262
|
-
return null;
|
|
1263
|
-
}
|
|
1264
|
-
function parseObject(stdout) {
|
|
1265
|
-
const parsed = safeJsonParse(stdout);
|
|
1266
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1267
|
-
return parsed;
|
|
1268
|
-
}
|
|
1269
|
-
return null;
|
|
1270
|
-
}
|
|
1271
|
-
function safeJsonParse(stdout) {
|
|
1272
|
-
const trimmed = stdout.trim();
|
|
1273
|
-
if (!trimmed)
|
|
1274
|
-
return null;
|
|
1275
|
-
try {
|
|
1276
|
-
return JSON.parse(trimmed);
|
|
1277
|
-
}
|
|
1278
|
-
catch {
|
|
1279
|
-
return null;
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
/**
|
|
1283
|
-
* Derive the canonical Ory Network project SDK/API URL from a project **slug**.
|
|
1284
|
-
* The subdomain is always the slug — never the project id. A project id (a
|
|
1285
|
-
* UUID) reaching here means a slug and an id were confused upstream, which
|
|
1286
|
-
* would build a URL that points nowhere, so we refuse it loudly rather than
|
|
1287
|
-
* silently misconfigure the plugin.
|
|
1288
|
-
*/
|
|
1289
|
-
function projectUrlFromSlug(slug) {
|
|
1290
|
-
if ((0, config_js_1.looksLikeProjectId)(slug)) {
|
|
1291
|
-
throw new Error(`Refusing to build an SDK URL from "${slug}": that is a project id (UUID), not a slug.`);
|
|
1292
|
-
}
|
|
1293
|
-
return `https://${slug}.projects.oryapis.com`;
|
|
1294
|
-
}
|
|
1295
|
-
// ─── prompt helpers ─────────────────────────────────────────────────────────
|
|
1296
|
-
/**
|
|
1297
|
-
* Present a numbered list and prompt for a selection. Auto-selects when there
|
|
1298
|
-
* is exactly one item. Returns null if the user cancels or gives up.
|
|
1299
|
-
*/
|
|
1300
|
-
async function selectFromList(prompt, label, items, render) {
|
|
1301
|
-
if (items.length === 1) {
|
|
1302
|
-
ui.info(`Using the only ${label}: ${ui.style.bold(render(items[0]))}`);
|
|
1303
|
-
return items[0];
|
|
1304
|
-
}
|
|
1305
|
-
ui.heading(`Select a ${label}:`);
|
|
1306
|
-
ui.blank();
|
|
1307
|
-
ui.menu(items.map((item, i) => ({ key: String(i + 1), label: render(item) })));
|
|
1308
|
-
ui.blank();
|
|
1309
|
-
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1310
|
-
const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${items.length}]` }));
|
|
1311
|
-
if (answer === null || answer.trim() === "")
|
|
1312
|
-
return null;
|
|
1313
|
-
const n = Number.parseInt(answer.trim(), 10);
|
|
1314
|
-
if (Number.isInteger(n) && n >= 1 && n <= items.length) {
|
|
1315
|
-
return items[n - 1];
|
|
1316
|
-
}
|
|
1317
|
-
ui.warning(`"${answer}" is not a valid choice.`);
|
|
1318
|
-
}
|
|
1319
|
-
return null;
|
|
1320
|
-
}
|
|
1321
|
-
function isLocalChoice(answer) {
|
|
1322
|
-
if (answer === null)
|
|
1323
|
-
return false;
|
|
1324
|
-
const v = answer.trim().toLowerCase();
|
|
1325
|
-
return v === "2" || v === "local" || v === "local-stack" || v === "localstack";
|
|
1326
|
-
}
|
|
1327
|
-
function isAuditChoice(answer) {
|
|
1328
|
-
if (answer === null)
|
|
1329
|
-
return false;
|
|
1330
|
-
const v = answer.trim().toLowerCase();
|
|
1331
|
-
return v === "3" || v === "audit" || v === "audit-only";
|
|
1332
|
-
}
|
|
1333
|
-
function isNo(answer) {
|
|
1334
|
-
if (answer === null)
|
|
1335
|
-
return false;
|
|
1336
|
-
const v = answer.trim().toLowerCase();
|
|
1337
|
-
return v === "n" || v === "no";
|
|
1338
|
-
}
|
|
1339
|
-
// ─── banners ────────────────────────────────────────────────────────────────
|
|
1340
|
-
function printOryCliMissing() {
|
|
1341
|
-
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:");
|
|
1343
|
-
ui.summary([
|
|
1344
|
-
["macOS/Linux (Homebrew)", "brew install ory/tap/cli"],
|
|
1345
|
-
[
|
|
1346
|
-
"macOS/Linux (script)",
|
|
1347
|
-
"bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b . ory",
|
|
1348
|
-
],
|
|
1349
|
-
["Docs", "https://www.ory.com/docs/guides/cli/installation"],
|
|
1350
|
-
]);
|
|
1351
|
-
}
|
|
1352
|
-
function printManualSetup(binName) {
|
|
1353
|
-
ui.heading("You can connect to Ory at any time:");
|
|
1354
|
-
ui.blank();
|
|
1355
|
-
ui.bullet("Connect to an Ory project (enables auth & permission checks):");
|
|
1356
|
-
ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
|
|
1357
|
-
ui.blank();
|
|
1358
|
-
ui.bullet("Or enable audit logging only (no auth or permission checks):");
|
|
1359
|
-
ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --audit-only`);
|
|
1360
|
-
ui.blank();
|
|
1361
|
-
ui.hint(`Config is saved to ${(0, config_js_1.getConfigPath)()} and shared across all agent plugins.`);
|
|
1362
|
-
}
|
|
1363
|
-
function printManualOAuth2ClientHelp(binName, projectId) {
|
|
1364
|
-
ui.heading("Create the public OAuth2 client by hand, then supply its id:");
|
|
1365
|
-
ui.blank();
|
|
1366
|
-
console.log(ui.style.gray(` ory create oauth2-client --project ${projectId} \\`));
|
|
1367
|
-
console.log(ui.style.gray(` --name "ory-agent-plugin" \\`));
|
|
1368
|
-
console.log(ui.style.gray(` --grant-type authorization_code,refresh_token \\`));
|
|
1369
|
-
console.log(ui.style.gray(` --response-type code \\`));
|
|
1370
|
-
console.log(ui.style.gray(` --scope openid,offline_access \\`));
|
|
1371
|
-
console.log(ui.style.gray(` --token-endpoint-auth-method none \\`));
|
|
1372
|
-
for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
|
|
1373
|
-
console.log(ui.style.gray(` --redirect-uri ${uri} \\`));
|
|
1374
|
-
}
|
|
1375
|
-
console.log(ui.style.gray(` --format json`));
|
|
1376
|
-
ui.blank();
|
|
1377
|
-
ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --oauth2-client-id <CLIENT_ID>`);
|
|
1378
|
-
}
|
|
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
|
-
/**
|
|
1442
|
-
* Best-effort check that an already-stored project API key still authenticates
|
|
1443
|
-
* against Keto. Runs one permission check with the key: if it completes (any
|
|
1444
|
-
* `allowed` value) the key is valid; an auth rejection (`session_inactive` /
|
|
1445
|
-
* `forbidden` / `session_aal2_required`) means it was revoked or deleted, so we
|
|
1446
|
-
* return false and the caller re-provisions. Transient failures (network /
|
|
1447
|
-
* rate-limit / unknown) return true so we don't re-mint on a blip. Never throws.
|
|
1448
|
-
*/
|
|
1449
|
-
async function apiKeyAuthenticates(apiKey, projectUrl) {
|
|
1450
|
-
try {
|
|
1451
|
-
// Direct construction (no fromEnv) seeds the key as both agent token and
|
|
1452
|
-
// admin key and skips DCR, so the probe uses exactly this key. No trace
|
|
1453
|
-
// file is configured, so nothing is written to disk.
|
|
1454
|
-
const probe = new client_js_1.OryAgentClient({ projectUrl, apiKey, harness: "install-probe" });
|
|
1455
|
-
await probe.checkPermission({
|
|
1456
|
-
namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
|
|
1457
|
-
object: "__ory_agent_key_probe__",
|
|
1458
|
-
relation: "__ory_agent_key_probe__",
|
|
1459
|
-
subjectId: "__ory_agent_key_probe__",
|
|
1460
|
-
});
|
|
1461
|
-
return true;
|
|
1462
|
-
}
|
|
1463
|
-
catch (err) {
|
|
1464
|
-
const code = err?.code;
|
|
1465
|
-
if (code === "session_inactive" || code === "forbidden" || code === "session_aal2_required") {
|
|
1466
|
-
return false;
|
|
1467
|
-
}
|
|
1468
|
-
return true;
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
/**
|
|
1472
|
-
* Manual fallback steps for the project API key, shown when auto-provisioning
|
|
1473
|
-
* is declined or fails. The `configure --api-key` command is the stable part;
|
|
1474
|
-
* the Console link is a convenience.
|
|
1475
|
-
*/
|
|
1476
|
-
function printManualApiKeySteps(binName, projectId) {
|
|
1477
|
-
ui.info("Create one in the Ory Console (Project settings → API keys):");
|
|
1478
|
-
ui.command(`https://console.ory.sh/projects/${projectId}/settings`);
|
|
1479
|
-
ui.info("then store it so permission checks authenticate with it:");
|
|
1480
|
-
ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --api-key <ory_pat_…>`);
|
|
1481
|
-
ui.hint("Observe mode works without it; enforce mode needs it to allow tools.");
|
|
1482
|
-
}
|
|
1483
|
-
/**
|
|
1484
|
-
* Provision the project API key that runtime Keto permission checks
|
|
1485
|
-
* authenticate with. The agent's DCR OAuth2 token can't — Ory Network rejects
|
|
1486
|
-
* it — so `enforce` mode needs a project API key. Best-effort and skippable:
|
|
1487
|
-
* offers to mint one via the Console API and persist it, and on decline / no
|
|
1488
|
-
* TTY / any failure falls back to manual guidance. No-ops when a key is already
|
|
1489
|
-
* configured (env or config file).
|
|
1490
|
-
*/
|
|
1491
|
-
async function maybeProvisionProjectApiKey(binName, harness, projectId, projectUrl, prompt, createFn, validateFn) {
|
|
1492
|
-
const resolved = (0, config_js_1.resolveConfig)();
|
|
1493
|
-
if (resolved.apiKey) {
|
|
1494
|
-
// An operator-set env key is authoritative — never touch it.
|
|
1495
|
-
if (resolved.apiKeySource === "env")
|
|
1496
|
-
return;
|
|
1497
|
-
// A stored (config-file) key that no longer authenticates — revoked or
|
|
1498
|
-
// deleted — must not wedge setup: validate it and re-provision if it's
|
|
1499
|
-
// dead. Presence alone isn't enough (a dangling key still fails at runtime
|
|
1500
|
-
// with session_inactive).
|
|
1501
|
-
if (await validateFn(resolved.apiKey, projectUrl))
|
|
1502
|
-
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;
|
|
1518
|
-
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) });
|
|
1523
|
-
}
|
|
1524
|
-
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;
|
|
1531
|
-
}
|
|
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();
|
|
1541
|
-
}
|
|
1542
|
-
catch {
|
|
1543
|
-
/* keep fallback */
|
|
1544
|
-
}
|
|
1545
|
-
return `ory-agent-plugins ${harness} ${host} ${new Date().toISOString()}`;
|
|
1546
|
-
}
|