@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.
Files changed (69) hide show
  1. package/README.md +2 -1
  2. package/assets/commands/dashboard.md +34 -0
  3. package/assets/skills/ory-build-agent/SKILL.md +11 -13
  4. package/assets/skills/ory-e2b-sandbox/SKILL.md +0 -1
  5. package/assets/skills/ory-temporal-worker/SKILL.md +8 -10
  6. package/assets/skills/permissions-onboarding/SKILL.md +3 -3
  7. package/dist/adapters.d.ts +27 -4
  8. package/dist/adapters.js +132 -32
  9. package/dist/agent-auth.d.ts +34 -3
  10. package/dist/agent-auth.js +58 -8
  11. package/dist/auth.d.ts +7 -0
  12. package/dist/auth.js +90 -5
  13. package/dist/branding.d.ts +67 -0
  14. package/dist/branding.js +81 -0
  15. package/dist/build-info.json +4 -4
  16. package/dist/cli.d.ts +3 -3
  17. package/dist/cli.js +13 -52
  18. package/dist/client.d.ts +31 -0
  19. package/dist/client.js +58 -0
  20. package/dist/config.d.ts +48 -19
  21. package/dist/config.js +31 -26
  22. package/dist/contract-suite.d.ts +5 -3
  23. package/dist/contract-suite.js +13 -22
  24. package/dist/dashboard-cli.d.ts +8 -0
  25. package/dist/dashboard-cli.js +70 -0
  26. package/dist/dev.js +3 -4
  27. package/dist/index.d.ts +12 -5
  28. package/dist/index.js +41 -3
  29. package/dist/interactive-setup.d.ts +144 -23
  30. package/dist/interactive-setup.js +412 -224
  31. package/dist/local/health.d.ts +14 -0
  32. package/dist/local/health.js +46 -0
  33. package/dist/local/manager.js +1 -3
  34. package/dist/local/seed.d.ts +9 -20
  35. package/dist/local/seed.js +26 -18
  36. package/dist/permissions-cli.js +12 -4
  37. package/dist/project-api-key.d.ts +69 -0
  38. package/dist/project-api-key.js +147 -0
  39. package/dist/registry/manager.js +62 -18
  40. package/dist/setup-actions.d.ts +232 -0
  41. package/dist/setup-actions.js +507 -0
  42. package/dist/setup.d.ts +19 -0
  43. package/dist/setup.js +44 -14
  44. package/dist/skills.js +7 -0
  45. package/dist/status-cli.d.ts +2 -2
  46. package/dist/status-cli.js +4 -30
  47. package/dist/status-data.d.ts +96 -0
  48. package/dist/status-data.js +250 -0
  49. package/dist/status-system.d.ts +24 -0
  50. package/dist/status-system.js +56 -0
  51. package/dist/testing.d.ts +8 -0
  52. package/dist/testing.js +20 -1
  53. package/dist/uninstall.d.ts +33 -15
  54. package/dist/uninstall.js +65 -22
  55. package/dist/user-login.d.ts +16 -9
  56. package/dist/user-login.js +18 -28
  57. package/dist/web/api.d.ts +33 -0
  58. package/dist/web/api.js +294 -0
  59. package/dist/web/launch.d.ts +11 -0
  60. package/dist/web/launch.js +96 -0
  61. package/dist/web/server.d.ts +20 -0
  62. package/dist/web/server.js +233 -0
  63. package/dist/web/types.d.ts +65 -0
  64. package/dist/web/types.js +2 -0
  65. package/dist/webapp/assets/index-Wucl4SZs.css +1 -0
  66. package/dist/webapp/assets/index-m-GtEdq0.js +49 -0
  67. package/dist/webapp/favicon.ico +0 -0
  68. package/dist/webapp/index.html +15 -0
  69. package/package.json +17 -2
@@ -19,6 +19,20 @@ export declare function checkAllServices(options?: {
19
19
  projectRoot?: string;
20
20
  gatewayUrl?: string;
21
21
  }): Promise<ServiceHealth[]>;
22
+ /**
23
+ * Wait until every backend service the seed writes to reports ready, polling
24
+ * at the given interval. Returns `{ ready: true }` on success, or
25
+ * `{ ready: false, pending }` listing the services that never came up within
26
+ * the timeout.
27
+ *
28
+ * Call this before seeding: it closes the race where the gateway is green and
29
+ * containers are "running" but Kratos / Keto / Hydra are not yet serving on
30
+ * their admin/write ports.
31
+ */
32
+ export declare function waitForSeedTargets(timeoutMs?: number, intervalMs?: number): Promise<{
33
+ ready: boolean;
34
+ pending: string[];
35
+ }>;
22
36
  /**
23
37
  * Wait for the gateway to become healthy, polling at the given interval.
24
38
  * Returns true if healthy within the timeout, false otherwise.
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.checkAllServices = checkAllServices;
7
+ exports.waitForSeedTargets = waitForSeedTargets;
7
8
  exports.waitForGateway = waitForGateway;
8
9
  const configs_js_1 = require("./configs.js");
9
10
  async function checkEndpoint(name, url) {
@@ -38,6 +39,51 @@ async function checkAllServices(options = {}) {
38
39
  ];
39
40
  return Promise.all(checks);
40
41
  }
42
+ /**
43
+ * Backend services the seed writes to, addressed at the exact host-published
44
+ * ports the seed uses (Kratos **admin**, Keto **write**, Hydra **admin**), so
45
+ * a green result means the seed's target APIs are actually accepting
46
+ * connections — not merely that the container is "running" or that the
47
+ * static nginx gateway `/health` is up.
48
+ *
49
+ * This is the readiness signal the gateway probe cannot provide: the gateway
50
+ * `/health` is a literal nginx `return 200` that never proxies a backend, and
51
+ * the Ory service containers declare no Docker healthcheck, so
52
+ * `docker compose up -d --wait` returns as soon as they are *started*, well
53
+ * before they are *serving*. Seeding against that gap intermittently fails to
54
+ * create identities and permission tuples on cold starts.
55
+ */
56
+ function seedTargetServices() {
57
+ return [
58
+ { name: "Kratos", url: `http://localhost:${configs_js_1.KRATOS_ADMIN_PORT}/health/ready` },
59
+ { name: "Keto", url: `http://localhost:${configs_js_1.KETO_WRITE_PORT}/health/ready` },
60
+ { name: "Hydra", url: `http://localhost:${configs_js_1.HYDRA_ADMIN_PORT}/health/ready` },
61
+ ];
62
+ }
63
+ /**
64
+ * Wait until every backend service the seed writes to reports ready, polling
65
+ * at the given interval. Returns `{ ready: true }` on success, or
66
+ * `{ ready: false, pending }` listing the services that never came up within
67
+ * the timeout.
68
+ *
69
+ * Call this before seeding: it closes the race where the gateway is green and
70
+ * containers are "running" but Kratos / Keto / Hydra are not yet serving on
71
+ * their admin/write ports.
72
+ */
73
+ async function waitForSeedTargets(timeoutMs = 60_000, intervalMs = 1000) {
74
+ const services = seedTargetServices();
75
+ const deadline = Date.now() + timeoutMs;
76
+ // eslint-disable-next-line no-constant-condition
77
+ while (true) {
78
+ const results = await Promise.all(services.map((s) => checkEndpoint(s.name, s.url)));
79
+ const pending = results.filter((r) => !r.healthy).map((r) => r.name);
80
+ if (pending.length === 0)
81
+ return { ready: true, pending: [] };
82
+ if (Date.now() >= deadline)
83
+ return { ready: false, pending };
84
+ await new Promise((r) => setTimeout(r, intervalMs));
85
+ }
86
+ }
41
87
  /**
42
88
  * Wait for the gateway to become healthy, polling at the given interval.
43
89
  * Returns true if healthy within the timeout, false otherwise.
@@ -719,8 +719,7 @@ function printSeedResult(result) {
719
719
  console.log("Seed complete!");
720
720
  console.log("==============");
721
721
  console.log("");
722
- console.log(` Agent identity: ${result.agent.identity.email} (${result.agent.identity.id})`);
723
- console.log(` (OAuth2 client self-registers on first run via DCR)`);
722
+ console.log(` Agent identity: self-registers on first run via DCR (nothing seeded)`);
724
723
  console.log("");
725
724
  console.log(` User identity: ${result.user.identity.email} (${result.user.identity.id})`);
726
725
  console.log(` User password: ${result.user.password}`);
@@ -731,7 +730,6 @@ function printSeedResult(result) {
731
730
  console.log("To use with any Ory agent plugin, set these environment variables:");
732
731
  console.log("");
733
732
  console.log(` export ORY_PROJECT_URL=${configs_js_1.GATEWAY_URL}`);
734
- console.log(` export ORY_USER_LOGIN=true`);
735
733
  console.log(` export ORY_OAUTH2_CLIENT_ID=${result.user.client.clientId}`);
736
734
  console.log(` export ORY_USER_SUBJECT_NAMESPACE=User`);
737
735
  console.log(` export ORY_USER_SUBJECT_ID=${result.user.identity.id}`);
@@ -1,18 +1,16 @@
1
1
  /**
2
2
  * Seed the local Ory environment with test data for development.
3
3
  *
4
- * Creates two principals:
4
+ * Creates the **User** (`user@ory-local.dev`) — a human identity with a
5
+ * known password (printed by the launcher) and an OAuth2
6
+ * `authorization_code` + PKCE client registered against the loopback
7
+ * redirect URIs. The launcher always runs the real PKCE browser flow; the
8
+ * user types these credentials into the login UI to complete the flow.
5
9
  *
6
- * 1. **Agent** (`agent@ory-local.dev`) a Kratos identity that the
7
- * harness will register a DCR OAuth2 client *for* on first session
8
- * start. No OAuth2 client is pre-registered here.
9
- *
10
- * 2. **User** (`user@ory-local.dev`) — a human identity with a known
11
- * password (printed by the launcher) and an OAuth2
12
- * `authorization_code` + PKCE client registered against the
13
- * loopback redirect URIs. The launcher always runs the real PKCE
14
- * browser flow; the user types these credentials into the login
15
- * UI to complete the flow.
10
+ * No agent principal is seeded: the harness self-registers a DCR OAuth2
11
+ * client for the agent on first session start (using the user's bearer as
12
+ * the initial access token), and permission checks are made against the
13
+ * user subject — so a pre-seeded agent Kratos identity would be unused.
16
14
  *
17
15
  * Permission tuples are written as SubjectSet `User:<identity.id>` so
18
16
  * the Console's *Add relationship* dialog (which always emits SubjectSet
@@ -39,15 +37,6 @@ export interface SeededOAuth2Client {
39
37
  clientSecret?: string;
40
38
  }
41
39
  export interface SeedResult {
42
- /**
43
- * Agent (machine) identity — Kratos identity only. The agent's OAuth2
44
- * client is *not* pre-registered: the harness self-registers via DCR
45
- * (RFC 7591) on first session start using the user's bearer as the
46
- * initial access token.
47
- */
48
- agent: {
49
- identity: SeededIdentity;
50
- };
51
40
  /** User (human) identity — subject of permission checks. */
52
41
  user: {
53
42
  identity: SeededIdentity;
@@ -2,18 +2,16 @@
2
2
  /**
3
3
  * Seed the local Ory environment with test data for development.
4
4
  *
5
- * Creates two principals:
5
+ * Creates the **User** (`user@ory-local.dev`) — a human identity with a
6
+ * known password (printed by the launcher) and an OAuth2
7
+ * `authorization_code` + PKCE client registered against the loopback
8
+ * redirect URIs. The launcher always runs the real PKCE browser flow; the
9
+ * user types these credentials into the login UI to complete the flow.
6
10
  *
7
- * 1. **Agent** (`agent@ory-local.dev`) a Kratos identity that the
8
- * harness will register a DCR OAuth2 client *for* on first session
9
- * start. No OAuth2 client is pre-registered here.
10
- *
11
- * 2. **User** (`user@ory-local.dev`) — a human identity with a known
12
- * password (printed by the launcher) and an OAuth2
13
- * `authorization_code` + PKCE client registered against the
14
- * loopback redirect URIs. The launcher always runs the real PKCE
15
- * browser flow; the user types these credentials into the login
16
- * UI to complete the flow.
11
+ * No agent principal is seeded: the harness self-registers a DCR OAuth2
12
+ * client for the agent on first session start (using the user's bearer as
13
+ * the initial access token), and permission checks are made against the
14
+ * user subject — so a pre-seeded agent Kratos identity would be unused.
17
15
  *
18
16
  * Permission tuples are written as SubjectSet `User:<identity.id>` so
19
17
  * the Console's *Add relationship* dialog (which always emits SubjectSet
@@ -30,11 +28,10 @@ exports.seedLocalEnvironment = seedLocalEnvironment;
30
28
  const auth_js_1 = require("../auth.js");
31
29
  const tool_catalog_js_1 = require("../tool-catalog.js");
32
30
  const configs_js_1 = require("./configs.js");
31
+ const health_js_1 = require("./health.js");
33
32
  const KRATOS_ADMIN = `http://localhost:${configs_js_1.KRATOS_ADMIN_PORT}`;
34
33
  const KETO_WRITE = `http://localhost:${configs_js_1.KETO_WRITE_PORT}`;
35
34
  const HYDRA_ADMIN = `http://localhost:${configs_js_1.HYDRA_ADMIN_PORT}`;
36
- const AGENT_EMAIL = "agent@ory-local.dev";
37
- const AGENT_PASSWORD = "ory-agent-local-dev-password!";
38
35
  const USER_EMAIL = "user@ory-local.dev";
39
36
  const USER_PASSWORD = "ory-user-local-dev-password!";
40
37
  const USER_CLIENT_NAME = "ory-agent-plugins-local-user";
@@ -179,10 +176,22 @@ function loopbackRedirectUris() {
179
176
  */
180
177
  async function seedLocalEnvironment(namespace = "AgentTools") {
181
178
  process.stderr.write("Seeding local Ory environment...\n\n");
182
- // 1. Identities
183
- process.stderr.write(" Creating agent identity...\n");
184
- const agentIdentity = await seedIdentity(AGENT_EMAIL, AGENT_PASSWORD, "Local Agent");
185
- process.stderr.write(` Agent: ${agentIdentity.id} (${agentIdentity.email})\n`);
179
+ // 0. Readiness barrier. The seed writes directly to Kratos (admin),
180
+ // Keto (write), and Hydra (admin). Neither the static nginx gateway
181
+ // `/health` nor `docker compose up -d --wait` (the Ory containers
182
+ // declare no healthcheck) proves those APIs are actually serving, so
183
+ // on a cold start the writes can race ahead of the services and
184
+ // silently create no identities or permission tuples. Block until all
185
+ // three report ready before touching them.
186
+ process.stderr.write(" Waiting for backend services to be ready...\n");
187
+ const readiness = await (0, health_js_1.waitForSeedTargets)();
188
+ if (!readiness.ready) {
189
+ throw new Error(`backend services not ready before seed: ${readiness.pending.join(", ")} ` +
190
+ `did not report ready within the timeout. The stack may still be starting — retry in a moment.`);
191
+ }
192
+ // 1. User identity. No agent identity is seeded — the harness
193
+ // self-registers a DCR OAuth2 client for the agent on first session
194
+ // start, and permission checks target the user subject.
186
195
  process.stderr.write(" Creating user identity...\n");
187
196
  const userIdentity = await seedIdentity(USER_EMAIL, USER_PASSWORD, "Local User");
188
197
  process.stderr.write(` User: ${userIdentity.id} (${userIdentity.email})\n`);
@@ -219,7 +228,6 @@ async function seedLocalEnvironment(namespace = "AgentTools") {
219
228
  process.stderr.write(` Warning: user OAuth2 client registration failed: ${msg}\n`);
220
229
  }
221
230
  return {
222
- agent: { identity: agentIdentity },
223
231
  user: { identity: userIdentity, password: USER_PASSWORD, client: userClient },
224
232
  permissions: { namespace, subject: subjectLabel, tuples: tupleCount },
225
233
  };
@@ -258,7 +258,7 @@ async function runPermissionsBootstrap(binName, harness, args) {
258
258
  console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
259
259
  console.error("");
260
260
  console.error("Either:");
261
- console.error(" - run the harness once with ORY_USER_LOGIN=true so a user token is cached, or");
261
+ console.error(" - run the harness once so the user login caches a token, or");
262
262
  console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
263
263
  return 1;
264
264
  }
@@ -288,9 +288,17 @@ async function runPermissionsBootstrap(binName, harness, args) {
288
288
  continue;
289
289
  }
290
290
  try {
291
- const res = await client.createRelationship(check, {
292
- spanAttributes: { toolName: tool, source: "permissions_bootstrap" },
293
- });
291
+ // Read-before-write: Keto's create API is not idempotent (every PUT
292
+ // inserts a new row and never 409s), so a plain createRelationship on
293
+ // re-install would duplicate the whole catalog. ensureRelationship
294
+ // probes for the exact tuple first and only writes when it is absent.
295
+ const spanAttributes = { toolName: tool, source: "permissions_bootstrap" };
296
+ let res = await client.ensureRelationship(check, { spanAttributes });
297
+ if (res.probeError) {
298
+ // Couldn't confirm existence — fall back to a direct write so a
299
+ // needed grant is still created (bootstrap's job is to guarantee it).
300
+ res = await client.createRelationship(check, { spanAttributes });
301
+ }
294
302
  if (res.alreadyExisted) {
295
303
  existed++;
296
304
  console.log(` = ${tool} (already exists)`);
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Ory Network **project API key** (project token) lifecycle via the Ory
3
+ * Console API.
4
+ *
5
+ * Runtime permission checks authenticate to Ory's Permission (Keto) API with a
6
+ * project API key — the agent's OAuth2 token isn't accepted there — so the
7
+ * installer mints one. The `ory` CLI (as of v1.3) exposes no project-API-key
8
+ * command, so this is the one place the plugin reaches past the CLI to the
9
+ * Console API directly, authenticating with the CLI's stored Ory Network
10
+ * session (`~/.ory-cloud.json`).
11
+ *
12
+ * These helpers own the full lifecycle so both the installer (create) and the
13
+ * uninstaller (delete) speak to the same endpoint with the same auth. Every
14
+ * function is best-effort and never throws — provisioning and teardown must not
15
+ * crash on a missing session or a network blip.
16
+ */
17
+ export interface CreateProjectApiKeyArgs {
18
+ projectId: string;
19
+ /** Human-readable label shown in the Console's API-keys list. */
20
+ name: string;
21
+ }
22
+ /**
23
+ * A minted project API key. `value` is the `ory_pat_…` secret (returned only at
24
+ * creation); `id` is the stable token id used to manage/delete the key later.
25
+ */
26
+ export interface ProjectApiKeyResult {
27
+ value: string;
28
+ /** Token id, when the Console returned one — needed for later deletion. */
29
+ id?: string;
30
+ }
31
+ /** Outcome of a best-effort server-side project-API-key deletion. */
32
+ export type ProjectApiKeyDeleteResult = {
33
+ status: "deleted";
34
+ }
35
+ /** Nothing to do server-side (no session, or no id to address the token). */
36
+ | {
37
+ status: "skipped";
38
+ message: string;
39
+ } | {
40
+ status: "failed";
41
+ message?: string;
42
+ httpStatus?: number;
43
+ };
44
+ /**
45
+ * Read the Ory Network session bearer the `ory` CLI stores at
46
+ * `~/.ory-cloud.json`. Returns null when the file is absent, unparseable, has
47
+ * no token, or the token has expired. Never throws.
48
+ */
49
+ export declare function readConsoleSessionToken(): string | null;
50
+ /**
51
+ * Mint an Ory Network **project API key** via the Console API
52
+ * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
53
+ * Returns the `{ value, id }` pair, or null on any failure (no session, non-2xx,
54
+ * missing value). Never throws — provisioning is best-effort.
55
+ */
56
+ export declare function createProjectApiKeyViaConsole(args: CreateProjectApiKeyArgs): Promise<ProjectApiKeyResult | null>;
57
+ /**
58
+ * Delete an Ory Network project API key via the Console API
59
+ * (`DELETE /projects/{projectId}/tokens/{tokenId}`), authenticated with the
60
+ * `ory` CLI session. Best-effort and never throws.
61
+ *
62
+ * A 404 (already gone) counts as `deleted` — the desired end state is reached.
63
+ * When no CLI session is available there is nothing we can do server-side, so
64
+ * the result is `skipped` (not a failure).
65
+ */
66
+ export declare function deleteProjectApiKeyViaConsole(args: {
67
+ projectId: string;
68
+ tokenId: string;
69
+ }): Promise<ProjectApiKeyDeleteResult>;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Ory Network **project API key** (project token) lifecycle via the Ory
4
+ * Console API.
5
+ *
6
+ * Runtime permission checks authenticate to Ory's Permission (Keto) API with a
7
+ * project API key — the agent's OAuth2 token isn't accepted there — so the
8
+ * installer mints one. The `ory` CLI (as of v1.3) exposes no project-API-key
9
+ * command, so this is the one place the plugin reaches past the CLI to the
10
+ * Console API directly, authenticating with the CLI's stored Ory Network
11
+ * session (`~/.ory-cloud.json`).
12
+ *
13
+ * These helpers own the full lifecycle so both the installer (create) and the
14
+ * uninstaller (delete) speak to the same endpoint with the same auth. Every
15
+ * function is best-effort and never throws — provisioning and teardown must not
16
+ * crash on a missing session or a network blip.
17
+ */
18
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ var desc = Object.getOwnPropertyDescriptor(m, k);
21
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
22
+ desc = { enumerable: true, get: function() { return m[k]; } };
23
+ }
24
+ Object.defineProperty(o, k2, desc);
25
+ }) : (function(o, m, k, k2) {
26
+ if (k2 === undefined) k2 = k;
27
+ o[k2] = m[k];
28
+ }));
29
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
30
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
31
+ }) : function(o, v) {
32
+ o["default"] = v;
33
+ });
34
+ var __importStar = (this && this.__importStar) || (function () {
35
+ var ownKeys = function(o) {
36
+ ownKeys = Object.getOwnPropertyNames || function (o) {
37
+ var ar = [];
38
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
39
+ return ar;
40
+ };
41
+ return ownKeys(o);
42
+ };
43
+ return function (mod) {
44
+ if (mod && mod.__esModule) return mod;
45
+ var result = {};
46
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
47
+ __setModuleDefault(result, mod);
48
+ return result;
49
+ };
50
+ })();
51
+ Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.readConsoleSessionToken = readConsoleSessionToken;
53
+ exports.createProjectApiKeyViaConsole = createProjectApiKeyViaConsole;
54
+ exports.deleteProjectApiKeyViaConsole = deleteProjectApiKeyViaConsole;
55
+ const fs = __importStar(require("node:fs"));
56
+ const os = __importStar(require("node:os"));
57
+ const path = __importStar(require("node:path"));
58
+ /** Base URL for the Ory Console API. Override with `ORY_CONSOLE_API_URL`. */
59
+ const CONSOLE_API_URL = process.env.ORY_CONSOLE_API_URL?.trim() || "https://api.console.ory.sh";
60
+ /** Path to the session the `ory` CLI persists after `ory auth`. */
61
+ const ORY_CLI_SESSION_FILE = ".ory-cloud.json";
62
+ /**
63
+ * Read the Ory Network session bearer the `ory` CLI stores at
64
+ * `~/.ory-cloud.json`. Returns null when the file is absent, unparseable, has
65
+ * no token, or the token has expired. Never throws.
66
+ */
67
+ function readConsoleSessionToken() {
68
+ try {
69
+ const raw = fs.readFileSync(path.join(os.homedir(), ORY_CLI_SESSION_FILE), "utf-8");
70
+ const parsed = JSON.parse(raw);
71
+ const token = parsed.access_token?.access_token;
72
+ if (!token)
73
+ return null;
74
+ const expiry = parsed.access_token?.expiry;
75
+ if (expiry) {
76
+ const expMs = Date.parse(expiry);
77
+ if (Number.isFinite(expMs) && expMs <= Date.now())
78
+ return null;
79
+ }
80
+ return token;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /**
87
+ * Mint an Ory Network **project API key** via the Console API
88
+ * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
89
+ * Returns the `{ value, id }` pair, or null on any failure (no session, non-2xx,
90
+ * missing value). Never throws — provisioning is best-effort.
91
+ */
92
+ async function createProjectApiKeyViaConsole(args) {
93
+ const token = readConsoleSessionToken();
94
+ if (!token)
95
+ return null;
96
+ try {
97
+ const res = await fetch(`${CONSOLE_API_URL}/projects/${args.projectId}/tokens`, {
98
+ method: "POST",
99
+ headers: {
100
+ Authorization: `Bearer ${token}`,
101
+ "Content-Type": "application/json",
102
+ },
103
+ body: JSON.stringify({ name: args.name }),
104
+ });
105
+ if (!res.ok)
106
+ return null;
107
+ const body = (await res.json());
108
+ const value = typeof body.value === "string" && body.value.length > 0 ? body.value : null;
109
+ if (!value)
110
+ return null;
111
+ const id = typeof body.id === "string" && body.id.length > 0 ? body.id : undefined;
112
+ return { value, id };
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ /**
119
+ * Delete an Ory Network project API key via the Console API
120
+ * (`DELETE /projects/{projectId}/tokens/{tokenId}`), authenticated with the
121
+ * `ory` CLI session. Best-effort and never throws.
122
+ *
123
+ * A 404 (already gone) counts as `deleted` — the desired end state is reached.
124
+ * When no CLI session is available there is nothing we can do server-side, so
125
+ * the result is `skipped` (not a failure).
126
+ */
127
+ async function deleteProjectApiKeyViaConsole(args) {
128
+ const token = readConsoleSessionToken();
129
+ if (!token) {
130
+ return {
131
+ status: "skipped",
132
+ message: "no Ory Console session (run `ory auth` to enable server-side removal)",
133
+ };
134
+ }
135
+ try {
136
+ const res = await fetch(`${CONSOLE_API_URL}/projects/${args.projectId}/tokens/${args.tokenId}`, {
137
+ method: "DELETE",
138
+ headers: { Authorization: `Bearer ${token}` },
139
+ });
140
+ if (res.ok || res.status === 404)
141
+ return { status: "deleted" };
142
+ return { status: "failed", httpStatus: res.status };
143
+ }
144
+ catch (err) {
145
+ return { status: "failed", message: err?.message };
146
+ }
147
+ }
@@ -139,32 +139,72 @@ function readPackageName(pkgJsonPath) {
139
139
  }
140
140
  }
141
141
  /**
142
- * Strip `publishConfig.registry` and `publishConfig.provenance` from every
143
- * `@ory/*` package.json for the duration of `fn`, then restore.
142
+ * Compute a unique local-dev version for a publish run: the base version's
143
+ * `major.minor.patch` with a `-dev.<timestamp>` prerelease suffix (e.g.
144
+ * `0.13.9-dev.1721710000000`). All `@ory/*` packages share one version (the
145
+ * changesets `fixed` group), so we read the base from the first package and
146
+ * stamp the same value onto every package — keeping `workspace:*` cross-deps
147
+ * resolvable to the just-published version.
144
148
  *
145
- * The OIDC trusted-publishing release workflow pins each package's
146
- * `publishConfig.registry` to `https://registry.npmjs.org/` with
147
- * `provenance: true`. `publishConfig` overrides both the `--registry` CLI
148
- * flag and the `npm_config_registry` env var on `pnpm publish`, so without
149
- * this transform `pnpm publish --registry http://localhost:4873` still
150
- * targets real npmjs.org and fails with `E404 undefined` (no auth) /
151
- * provenance errors. We unset the two fields only on disk for the publish
152
- * call, then put the files back exactly as they were so the CI release
153
- * workflow keeps working.
149
+ * Why a fresh version every publish: `npx`/pacote cache installed trees and
150
+ * tarballs by `name@version`. Republishing the *same* version over a rebuilt
151
+ * tarball lets a client's `~/.npm/_npx` or `_cacache` serve stale bytes even
152
+ * after Verdaccio has the fresh code. A new version each run guarantees a
153
+ * cache miss so `@latest` always resolves to what we just built.
154
+ */
155
+ function makeLocalDevVersion(pkgJsonFiles, stamp) {
156
+ let base = "0.0.0";
157
+ for (const file of pkgJsonFiles) {
158
+ try {
159
+ const v = JSON.parse(fs.readFileSync(file, "utf-8")).version;
160
+ if (v) {
161
+ base = v;
162
+ break;
163
+ }
164
+ }
165
+ catch {
166
+ // ignore unreadable package.json; fall back to the next one
167
+ }
168
+ }
169
+ // Strip any existing prerelease/build suffix so we don't stack `-dev` tags.
170
+ const core = base.split(/[-+]/)[0];
171
+ return `${core}-dev.${stamp}`;
172
+ }
173
+ /**
174
+ * Prepare every `@ory/*` package.json for a local publish for the duration of
175
+ * `fn`, then restore each file exactly as it was. Two transforms are applied:
176
+ *
177
+ * 1. **Strip `publishConfig.registry` / `publishConfig.provenance`.** The OIDC
178
+ * trusted-publishing release workflow pins each package's
179
+ * `publishConfig.registry` to `https://registry.npmjs.org/` with
180
+ * `provenance: true`. `publishConfig` overrides both the `--registry` CLI
181
+ * flag and the `npm_config_registry` env var, so without this transform a
182
+ * local publish still targets real npmjs.org and fails with `E404
183
+ * undefined` (no auth) / provenance errors.
184
+ *
185
+ * 2. **Stamp a unique `-dev.<timestamp>` version** (see
186
+ * {@link makeLocalDevVersion}) so `@latest` changes on every publish and
187
+ * clients can't serve a stale cached tarball for a version we already used.
188
+ *
189
+ * Both are on-disk only for the publish call; the files are put back exactly as
190
+ * they were so the CI release workflow (and git) keep working.
154
191
  */
155
192
  async function withLocalPublishConfig(workspaceRoot, fn) {
156
193
  const files = listWorkspaceScopedPackageJsons(workspaceRoot);
194
+ const stamp = Date.now();
195
+ const devVersion = makeLocalDevVersion(files, stamp);
157
196
  const backups = new Map();
158
197
  for (const file of files) {
159
198
  const original = fs.readFileSync(file, "utf-8");
160
199
  const pkg = JSON.parse(original);
161
- if (!pkg.publishConfig)
162
- continue;
163
- const stripped = { ...pkg.publishConfig };
164
- delete stripped.registry;
165
- delete stripped.provenance;
200
+ const next = { ...pkg, version: devVersion };
201
+ if (pkg.publishConfig) {
202
+ const stripped = { ...pkg.publishConfig };
203
+ delete stripped.registry;
204
+ delete stripped.provenance;
205
+ next.publishConfig = stripped;
206
+ }
166
207
  backups.set(file, original);
167
- const next = { ...pkg, publishConfig: stripped };
168
208
  fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
169
209
  }
170
210
  const restore = () => {
@@ -461,7 +501,7 @@ async function publishAllOryToLocal(opts = {}) {
461
501
  const { name, version } = readPackageNameVersion(pkgJsonPath);
462
502
  const pkgName = name ?? path.basename(pkgDir);
463
503
  if (!opts.quiet) {
464
- console.log(`Publishing ${pkgName}...`);
504
+ console.log(`Publishing ${pkgName}@${version}...`);
465
505
  }
466
506
  const packResult = (0, node_child_process_1.spawnSync)("pnpm", ["pack", "--pack-destination", packDir], {
467
507
  cwd: pkgDir,
@@ -495,6 +535,10 @@ async function publishAllOryToLocal(opts = {}) {
495
535
  config_js_1.REGISTRY_URL,
496
536
  ...scopeRegistryArg,
497
537
  "--access=public",
538
+ // Explicit so a `-dev.<ts>` prerelease still becomes `@latest` in the
539
+ // local registry (some npm versions won't auto-tag prereleases).
540
+ "--tag",
541
+ "latest",
498
542
  ], {
499
543
  cwd: pkgDir,
500
544
  stdio: opts.quiet ? "pipe" : "inherit",