@ory/argus 0.13.6 → 0.13.7

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.
@@ -48,6 +48,14 @@ export interface AgentCredentials {
48
48
  * via deprecated alias). Never blocks resolution.
49
49
  */
50
50
  warnings: string[];
51
+ /**
52
+ * Set on a `kind: "none"` result when the token endpoint rejected the
53
+ * credentials for an auth reason (401/403) — i.e. the client is genuinely
54
+ * dead (revoked/invalid). A transient failure (network, rate-limit, 5xx)
55
+ * leaves this false so callers don't discard otherwise-good persisted
56
+ * credentials. See the persisted-DCR branch in {@link resolveAgentCredentials}.
57
+ */
58
+ authRejected?: boolean;
51
59
  }
52
60
  export interface ResolveAgentCredentialsOptions {
53
61
  /** Ory project URL — required for client_credentials grant + DCR. */
@@ -275,10 +275,20 @@ async function resolveAgentCredentials(options = {}) {
275
275
  });
276
276
  if (result.kind !== "none")
277
277
  return result;
278
- // Server rejected the persisted credsclear and fall through to
279
- // a fresh registration attempt.
280
- warnings.push(`Persisted dynamic agent credentials rejected (${result.reason}); re-registering.`);
281
- clearDynamicFn();
278
+ // Only a genuine auth rejection (401/403 client revoked/invalid) means
279
+ // the persisted creds are dead. Clear them and fall through to a fresh
280
+ // registration. A transient failure (network, rate-limit, 5xx) must NOT
281
+ // discard good creds: doing so would orphan the server-side client
282
+ // (clearDynamicFn never revokes it) and register a brand-new one on every
283
+ // blip. Keep the persisted creds and report the transient failure instead.
284
+ if (result.authRejected) {
285
+ warnings.push(`Persisted dynamic agent credentials rejected (${result.reason}); re-registering.`);
286
+ clearDynamicFn();
287
+ }
288
+ else {
289
+ warnings.push(`Persisted dynamic agent credentials temporarily unavailable (${result.reason}); keeping them for the next session.`);
290
+ return { ...result, warnings };
291
+ }
282
292
  }
283
293
  }
284
294
  // 4. Fresh dynamic registration — bootstrap using the user's bearer
@@ -377,11 +387,27 @@ async function resolveClientCredentials(args) {
377
387
  catch (err) {
378
388
  return {
379
389
  kind: "none",
390
+ authRejected: isAuthRejectionError(err),
380
391
  reason: `${reasonNoun} client_credentials grant failed: ${err instanceof Error ? err.message : String(err)}`,
381
392
  warnings: args.warnings,
382
393
  };
383
394
  }
384
395
  }
396
+ /**
397
+ * Whether a token-grant error is an auth rejection (401/403) — meaning the
398
+ * credentials themselves are dead — as opposed to a transient failure
399
+ * (network, rate-limit, 5xx). Reads a `status` property when the thrower
400
+ * attaches one ({@link fetchClientCredentialsToken} does), and falls back to
401
+ * matching `HTTP 401`/`HTTP 403` in the message so injected/stubbed grant
402
+ * functions that throw a plain `Error` are classified correctly too.
403
+ */
404
+ function isAuthRejectionError(err) {
405
+ const status = err?.status;
406
+ if (status === 401 || status === 403)
407
+ return true;
408
+ const msg = err instanceof Error ? err.message : String(err);
409
+ return /\bHTTP 40[13]\b/.test(msg);
410
+ }
385
411
  // Kept as raw fetch (rather than @ory/client's OAuth2Api.oauth2TokenExchange)
386
412
  // because the SDK method's signature does not expose client_secret, audience,
387
413
  // or scope — all of which this grant needs. Ory's own SDK docstring on that
@@ -404,7 +430,9 @@ async function fetchClientCredentialsToken(args) {
404
430
  body: body.toString(),
405
431
  });
406
432
  if (!res.ok) {
407
- throw new Error(`HTTP ${res.status}`);
433
+ const err = new Error(`HTTP ${res.status}`);
434
+ err.status = res.status;
435
+ throw err;
408
436
  }
409
437
  const json = (await res.json());
410
438
  const accessToken = typeof json.access_token === "string" ? json.access_token : "";
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "255043db833b8ebda7cbec0dee740eda045f064e",
4
- "commitShort": "255043d",
3
+ "commit": "badfe6ff1166227b0eefaeb54018482631e0ce7b",
4
+ "commitShort": "badfe6f",
5
5
  "branch": "main",
6
- "commitDate": "2026-07-15T09:35:21-07:00",
6
+ "commitDate": "2026-07-15T11:49:39-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-07-15T16:39:31.503Z"
8
+ "builtAt": "2026-07-15T18:53:45.680Z"
9
9
  }
@@ -568,10 +568,20 @@ async function configureNetwork(binName, harness, ctx) {
568
568
  // self-register at runtime. Ory Network projects ship with DCR disabled,
569
569
  // so without this the default agent-identity path would never resolve.
570
570
  await maybeEnableDcr(runner, prompt, project.id);
571
- // 6. Create the public OAuth2 client for the PKCE user login.
571
+ // 6. Provision the public OAuth2 client for the PKCE user login. Reuse an
572
+ // existing one on this project first — a re-run (`install --reconfigure`)
573
+ // against the same project must not mint a duplicate public client and
574
+ // orphan the old one (the CLI-created client is never cleaned up by
575
+ // uninstall). Only create when no prior client is found.
572
576
  ui.heading("OAuth2 client");
573
- ui.step("Creating the OAuth2 client for user login...");
574
- const clientId = createOAuth2Client(runner, project.id);
577
+ let clientId = findExistingOAuth2Client(runner, project.id);
578
+ if (clientId) {
579
+ ui.step(`Reusing the existing OAuth2 client for user login (${clientId}).`);
580
+ }
581
+ else {
582
+ ui.step("Creating the OAuth2 client for user login...");
583
+ clientId = createOAuth2Client(runner, project.id);
584
+ }
575
585
  if (!clientId) {
576
586
  ui.warning("Could not create the OAuth2 client automatically.");
577
587
  ui.warnDetail(`Project URL resolved: ${projectUrl}`);
@@ -604,6 +614,14 @@ async function configureNetwork(binName, harness, ctx) {
604
614
  // runtime agent/user tokens can't write relation tuples on a hosted
605
615
  // project (that's a project-admin operation), but the `ory` session can.
606
616
  if (subject) {
617
+ // Persist the subject *shape* this grant used. If the install resolved a
618
+ // SubjectSet (ORY_USER_SUBJECT_NAMESPACE was set), a later runtime session
619
+ // that lacks the env var would otherwise resolve a direct subject_id and
620
+ // check/write a divergent tuple for the same user. Persisting the namespace
621
+ // keeps install-time and runtime writes on one canonical subject shape.
622
+ if ("subjectSet" in subject) {
623
+ (0, config_js_1.saveConfig)({ userSubjectNamespace: subject.subjectSet.namespace });
624
+ }
607
625
  await bootstrapPermissionsViaOry(runner, project.id, harness, subject, prompt);
608
626
  }
609
627
  // 10. Provision the project API key runtime permission *checks* need. Same
@@ -1166,6 +1184,31 @@ async function maybeEnableDcr(runner, prompt, projectId) {
1166
1184
  ui.warnDetail(`Enable it later with: ${manual}`);
1167
1185
  }
1168
1186
  }
1187
+ /** The `client_name` every plugin-provisioned public PKCE client carries. */
1188
+ const OAUTH2_CLIENT_NAME = "ory-agent-plugin";
1189
+ /**
1190
+ * Look for an already-provisioned public PKCE client on the project so a
1191
+ * re-run reuses it instead of minting a duplicate. Matches by the fixed
1192
+ * `client_name` {@link createOAuth2Client} assigns. Best-effort: any CLI or
1193
+ * parse failure returns null and the caller falls back to creating one.
1194
+ */
1195
+ function findExistingOAuth2Client(runner, projectId) {
1196
+ const r = runner.exec([
1197
+ "list",
1198
+ "oauth2-clients",
1199
+ "--project",
1200
+ projectId,
1201
+ "--format",
1202
+ "json",
1203
+ ]);
1204
+ if (r.status !== 0)
1205
+ return null;
1206
+ const clients = parseList(r.stdout, "items");
1207
+ if (!clients)
1208
+ return null;
1209
+ const match = clients.find((c) => c.client_name === OAUTH2_CLIENT_NAME && c.client_id);
1210
+ return match?.client_id ?? null;
1211
+ }
1169
1212
  function createOAuth2Client(runner, projectId) {
1170
1213
  const args = [
1171
1214
  "create",
@@ -1173,7 +1216,7 @@ function createOAuth2Client(runner, projectId) {
1173
1216
  "--project",
1174
1217
  projectId,
1175
1218
  "--name",
1176
- "ory-agent-plugin",
1219
+ OAUTH2_CLIENT_NAME,
1177
1220
  "--grant-type",
1178
1221
  "authorization_code,refresh_token",
1179
1222
  "--response-type",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.13.6",
3
+ "version": "0.13.7",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",