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