@oxy.so/core 4.1.0 → 4.2.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 (95) hide show
  1. package/dist/cjs/HttpService.js +142 -33
  2. package/dist/cjs/api/accounts.js +49 -10
  3. package/dist/cjs/api/apps.js +2 -2
  4. package/dist/cjs/api/auth.js +24 -3
  5. package/dist/cjs/api/billing.js +35 -0
  6. package/dist/cjs/boot/sessionColdBoot.js +28 -57
  7. package/dist/cjs/inference/OxyInferenceClient.js +23 -15
  8. package/dist/cjs/server/OxyServer.js +107 -51
  9. package/dist/cjs/server/agentAccount.js +19 -0
  10. package/dist/cjs/server/auth.js +19 -0
  11. package/dist/cjs/server/capabilityTicket.js +3 -41
  12. package/dist/cjs/server/index.js +10 -2
  13. package/dist/cjs/server/liveCapabilityTicket.js +48 -0
  14. package/dist/cjs/server/middleware.js +60 -18
  15. package/dist/cjs/server/namespaces.js +57 -0
  16. package/dist/cjs/server/serviceTokenLifetime.js +13 -0
  17. package/dist/cjs/session/SessionClient.js +90 -12
  18. package/dist/cjs/session/authStateStore.js +130 -39
  19. package/dist/cjs/session/refresh.js +152 -30
  20. package/dist/cjs/session/sharedDeviceCredential.js +17 -1
  21. package/dist/cjs/utils/officialOrigins.js +7 -18
  22. package/dist/esm/HttpService.js +143 -34
  23. package/dist/esm/api/accounts.js +38 -0
  24. package/dist/esm/api/apps.js +2 -2
  25. package/dist/esm/api/auth.js +23 -3
  26. package/dist/esm/api/billing.js +35 -0
  27. package/dist/esm/boot/sessionColdBoot.js +29 -58
  28. package/dist/esm/inference/OxyInferenceClient.js +21 -13
  29. package/dist/esm/server/OxyServer.js +107 -51
  30. package/dist/esm/server/agentAccount.js +16 -0
  31. package/dist/esm/server/auth.js +18 -0
  32. package/dist/esm/server/capabilityTicket.js +1 -39
  33. package/dist/esm/server/index.js +4 -1
  34. package/dist/esm/server/liveCapabilityTicket.js +45 -0
  35. package/dist/esm/server/middleware.js +60 -18
  36. package/dist/esm/server/namespaces.js +57 -0
  37. package/dist/esm/server/serviceTokenLifetime.js +9 -0
  38. package/dist/esm/session/SessionClient.js +90 -12
  39. package/dist/esm/session/authStateStore.js +128 -38
  40. package/dist/esm/session/refresh.js +151 -30
  41. package/dist/esm/session/sharedDeviceCredential.js +17 -1
  42. package/dist/esm/utils/officialOrigins.js +2 -13
  43. package/dist/types/HttpService.d.ts +26 -12
  44. package/dist/types/api/accounts.d.ts +38 -0
  45. package/dist/types/api/agency.d.ts +3 -1
  46. package/dist/types/api/apps.d.ts +3 -1
  47. package/dist/types/api/auth.d.ts +12 -1
  48. package/dist/types/api/billing.d.ts +12 -0
  49. package/dist/types/api/session.d.ts +3 -1
  50. package/dist/types/boot/sessionColdBoot.d.ts +1 -1
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/types/inference/OxyInferenceClient.d.ts +9 -0
  53. package/dist/types/inference/index.d.ts +1 -1
  54. package/dist/types/server/OxyServer.d.ts +16 -9
  55. package/dist/types/server/agentAccount.d.ts +14 -0
  56. package/dist/types/server/auth.d.ts +18 -0
  57. package/dist/types/server/capabilityTicket.d.ts +2 -4
  58. package/dist/types/server/index.d.ts +7 -1
  59. package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
  60. package/dist/types/server/middleware.d.ts +15 -6
  61. package/dist/types/server/namespaces.d.ts +78 -2
  62. package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
  63. package/dist/types/session/SessionClient.d.ts +8 -0
  64. package/dist/types/session/authStateStore.d.ts +15 -0
  65. package/dist/types/session/index.d.ts +1 -1
  66. package/dist/types/session/refresh.d.ts +32 -7
  67. package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
  68. package/dist/types/utils/officialOrigins.d.ts +1 -5
  69. package/package.json +6 -6
  70. package/src/HttpService.ts +138 -50
  71. package/src/api/accounts.ts +22 -0
  72. package/src/api/agency.ts +3 -1
  73. package/src/api/apps.ts +2 -2
  74. package/src/api/auth.ts +31 -2
  75. package/src/api/billing.ts +71 -9
  76. package/src/api/session.ts +3 -1
  77. package/src/boot/sessionColdBoot.ts +27 -72
  78. package/src/index.ts +1 -1
  79. package/src/inference/OxyInferenceClient.ts +19 -5
  80. package/src/inference/index.ts +1 -0
  81. package/src/server/OxyServer.ts +114 -53
  82. package/src/server/agentAccount.ts +28 -0
  83. package/src/server/auth.ts +25 -0
  84. package/src/server/capabilityTicket.ts +2 -42
  85. package/src/server/index.ts +13 -0
  86. package/src/server/liveCapabilityTicket.ts +47 -0
  87. package/src/server/middleware.ts +72 -25
  88. package/src/server/namespaces.ts +99 -2
  89. package/src/server/serviceTokenLifetime.ts +13 -0
  90. package/src/session/SessionClient.ts +85 -12
  91. package/src/session/authStateStore.ts +130 -46
  92. package/src/session/index.ts +1 -0
  93. package/src/session/refresh.ts +144 -28
  94. package/src/session/sharedDeviceCredential.ts +14 -2
  95. package/src/utils/officialOrigins.ts +2 -13
@@ -1,4 +1,18 @@
1
1
  "use strict";
2
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
3
+ if (kind === "m") throw new TypeError("Private method is not writable");
4
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
5
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
6
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
7
+ };
8
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
9
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
10
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
+ };
13
+ var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_headers, _OxyInferenceClient_request;
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.OxyInferenceClient = exports.OxyInferenceProtocolError = exports.OxyInferenceError = exports.OXY_INFERENCE_BASE_URL = void 0;
2
16
  /**
3
17
  * The Oxy inference client — one surface, two credential lanes (issue #972,
4
18
  * workstream 15).
@@ -50,20 +64,6 @@
50
64
  * schema, so a rename on either side fails a build rather than a customer's
51
65
  * request.
52
66
  */
53
- var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
54
- if (kind === "m") throw new TypeError("Private method is not writable");
55
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
56
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
57
- return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
58
- };
59
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
60
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
61
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
62
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
63
- };
64
- var _OxyInferenceClient_instances, _OxyInferenceClient_baseURL, _OxyInferenceClient_credential, _OxyInferenceClient_fetch, _OxyInferenceClient_bearer, _OxyInferenceClient_headers, _OxyInferenceClient_request;
65
- Object.defineProperty(exports, "__esModule", { value: true });
66
- exports.OxyInferenceClient = exports.OxyInferenceProtocolError = exports.OxyInferenceError = exports.OXY_INFERENCE_BASE_URL = void 0;
67
67
  const contracts_1 = require("@oxy.so/contracts");
68
68
  const contracts_2 = require("@oxy.so/contracts");
69
69
  /** The base URL of the Oxy API, when a caller names none. */
@@ -384,7 +384,15 @@ class OxyInferenceClient {
384
384
  * somebody else.
385
385
  */
386
386
  async getGeneration(id, options = {}) {
387
- const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, { ...(options.signal === undefined ? {} : { signal: options.signal }) });
387
+ const record = await this.getGenerationRecord(id, options);
388
+ if (record.schemaVersion !== 1) {
389
+ throw new OxyInferenceProtocolError('Internal usage has no financial receipt. Use getGenerationRecord().', record.requestId);
390
+ }
391
+ return record;
392
+ }
393
+ /** Read a financial receipt (v1) or an internal technical record (v2). */
394
+ async getGenerationRecord(id, options = {}) {
395
+ const body = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'GET', `/v1/generations/${encodeURIComponent(id)}`, options);
388
396
  return body.data;
389
397
  }
390
398
  }
@@ -29,6 +29,18 @@ exports.OxyServer = exports.ServiceCredentialMismatchError = exports.ANONYMOUS_S
29
29
  * Node only.
30
30
  */
31
31
  const jwt_decode_1 = require("jwt-decode");
32
+ const serviceTokenLifetime_1 = require("./serviceTokenLifetime");
33
+ /** Unverified cache metadata only: an old lifetime forces a new signed mint. */
34
+ function reusableServiceToken(token) {
35
+ try {
36
+ const claims = (0, jwt_decode_1.jwtDecode)(token);
37
+ return (0, serviceTokenLifetime_1.hasBoundedServiceTokenLifetime)(claims) && typeof claims.exp === 'number'
38
+ && claims.exp > Math.floor(Date.now() / 1000);
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
32
44
  const protocol_1 = require("@oxy.so/protocol");
33
45
  const OxyServices_1 = require("../OxyServices");
34
46
  const logger_1 = require("../logger");
@@ -57,10 +69,10 @@ const WORKLOAD_CACHE_KEY = 'workload-identity';
57
69
  const TOKEN_REUSE_MARGIN_MS = 60000;
58
70
  /** Bound on remembered `appId:userId` delegation answers. */
59
71
  const ACTING_AS_CACHE_MAX = 1000;
60
- /** A positive grant is reused for 5 minutes — the revocation latency window. */
61
- const ACTING_AS_GRANT_TTL_MS = 5 * 60 * 1000;
62
- /** A negative answer (or a failed lookup) is reused for 1 minute. */
63
- const ACTING_AS_DENIAL_TTL_MS = 60 * 1000;
72
+ /** Explicit read reuse only; effects always perform a fresh lookup. */
73
+ const ACTING_AS_GRANT_TTL_MS = 60000;
74
+ /** Explicit authoritative read denials last ten seconds; failures are never cached. */
75
+ const ACTING_AS_DENIAL_TTL_MS = 10000;
64
76
  class OxyServer extends OxyServices_1.OxyServices {
65
77
  constructor(config) {
66
78
  super(config);
@@ -69,6 +81,7 @@ class OxyServer extends OxyServices_1.OxyServices {
69
81
  this.serviceApiSecret = null;
70
82
  this.actingAs = new Map();
71
83
  this.actingAsPending = new Map();
84
+ this.actingAsGenerations = new Map();
72
85
  /** Public keys only; never private material. */
73
86
  this.jwksCache = { keys: new Map(), expiresAt: 0, lastAttemptAt: 0 };
74
87
  /**
@@ -122,7 +135,7 @@ class OxyServer extends OxyServices_1.OxyServices {
122
135
  },
123
136
  jwksCache: this.jwksCache,
124
137
  validateSession: (sessionId, options) => this.session.validate(sessionId, options),
125
- verifyActingAs: (appId, userId) => this.verifyActingAs(appId, userId),
138
+ verifyActingAs: (appId, userId, options) => this.verifyActingAs(appId, userId, options),
126
139
  });
127
140
  }
128
141
  /** Files, plus the service-only metadata and linked-URL lookups. */
@@ -208,7 +221,7 @@ class OxyServer extends OxyServices_1.OxyServices {
208
221
  });
209
222
  throw new ServiceCredentialMismatchError();
210
223
  }
211
- if (entry.token && entry.expiresAt > now + TOKEN_REUSE_MARGIN_MS) {
224
+ if (entry.token && reusableServiceToken(entry.token) && entry.expiresAt > now + TOKEN_REUSE_MARGIN_MS) {
212
225
  return entry.token;
213
226
  }
214
227
  if (entry.pending) {
@@ -270,32 +283,49 @@ class OxyServer extends OxyServices_1.OxyServices {
270
283
  }
271
284
  // ── Delegation and account events ────────────────────────────────────────
272
285
  /**
273
- * Whether service app `appId` holds an active delegation grant to act for
274
- * `userId`; the grant's scopes, or `null`. Used by `middleware.auth()` for
275
- * `X-Oxy-User-Id`.
276
- *
277
- * Answers are cached per `appId:userId` (a grant for 5 minutes — the
278
- * revocation window; a denial or failed lookup for 1 minute), bounded to the
279
- * 1000 most recent pairs, and concurrent lookups of one pair share a single
280
- * request.
286
+ * Resolve authority live by default. Explicit read-only reuse lasts at most
287
+ * 60s positive / 10s negative, anchored to request start; errors never cache.
288
+ * Effects do not share an older in-flight read. Durable epoch plus local
289
+ * completion ordering prevents ABA and late positive replies overwriting a
290
+ * newer refusal even when app/credential revocation leaves epoch unchanged.
281
291
  */
282
- async verifyActingAs(appId, userId) {
283
- const cacheKey = `${appId}:${userId}`;
284
- const cached = this.actingAs.get(cacheKey);
285
- if (cached && cached.expiresAt > Date.now()) {
286
- // Refresh recency for the LRU bound.
287
- this.actingAs.delete(cacheKey);
288
- this.actingAs.set(cacheKey, cached);
289
- return cached.result;
290
- }
291
- const inflight = this.actingAsPending.get(cacheKey);
292
- if (inflight)
293
- return inflight;
294
- const lookup = this.lookupActingAs(appId, userId).finally(() => {
295
- this.actingAsPending.delete(cacheKey);
296
- });
292
+ async verifyActingAs(appId, userId, options = {}) {
293
+ const pair = JSON.stringify([appId, userId]);
294
+ const cacheKey = JSON.stringify([appId, userId, options.credentialId, options.ownerAccountId, options.environment]);
295
+ if (options.cache === true) {
296
+ const cached = this.actingAs.get(cacheKey);
297
+ if (cached && cached.expiresAt > Date.now()) {
298
+ this.actingAs.delete(cacheKey);
299
+ this.actingAs.set(cacheKey, cached);
300
+ return cached.result;
301
+ }
302
+ const pending = this.actingAsPending.get(cacheKey);
303
+ if (pending)
304
+ return pending;
305
+ }
306
+ let generation = this.actingAsGenerations.get(pair);
307
+ if (!generation) {
308
+ generation = { epoch: 0n, issued: 0, completed: 0 };
309
+ this.actingAsGenerations.set(pair, generation);
310
+ if (this.actingAsGenerations.size > ACTING_AS_CACHE_MAX) {
311
+ const oldest = this.actingAsGenerations.keys().next().value;
312
+ if (oldest !== undefined)
313
+ this.actingAsGenerations.delete(oldest);
314
+ }
315
+ }
316
+ const serial = ++generation.issued;
317
+ const startedAt = Date.now();
318
+ const lookup = this.lookupActingAs(appId, userId, options, pair, cacheKey, generation, serial, startedAt);
319
+ if (options.cache !== true)
320
+ return lookup;
297
321
  this.actingAsPending.set(cacheKey, lookup);
298
- return lookup;
322
+ try {
323
+ return await lookup;
324
+ }
325
+ finally {
326
+ if (this.actingAsPending.get(cacheKey) === lookup)
327
+ this.actingAsPending.delete(cacheKey);
328
+ }
299
329
  }
300
330
  // ── Internals ────────────────────────────────────────────────────────────
301
331
  /**
@@ -351,7 +381,7 @@ class OxyServer extends OxyServices_1.OxyServices {
351
381
  /** A token obtained by attestation, cached like a credential's (one identity per process). */
352
382
  async workloadServiceToken() {
353
383
  const entry = this.serviceTokens.get(WORKLOAD_CACHE_KEY);
354
- if (entry?.token && entry.expiresAt > Date.now() + TOKEN_REUSE_MARGIN_MS)
384
+ if (entry?.token && reusableServiceToken(entry.token) && entry.expiresAt > Date.now() + TOKEN_REUSE_MARGIN_MS)
355
385
  return entry.token;
356
386
  if (entry?.pending)
357
387
  return entry.pending;
@@ -382,36 +412,62 @@ class OxyServer extends OxyServices_1.OxyServices {
382
412
  settled.pending = null;
383
413
  }
384
414
  }
385
- async lookupActingAs(appId, userId) {
415
+ async lookupActingAs(appId, userId, options, pair, cacheKey, generation, serial, startedAt) {
386
416
  try {
387
- // The verify endpoint admits only a platform-trusted caller, so this
388
- // carries the VERIFIER's own service token. No retry and a short timeout:
389
- // it runs inside request-handling middleware, and a retry loop would
390
- // multiply the latency of every delegated request. A verifier with no
391
- // credential throws here and is refused below — a host that cannot prove
392
- // who it is has no business learning who delegated to whom.
393
417
  const serviceToken = await this.serviceToken();
394
- const result = await this.request('GET', '/internal/service-acting-as/verify', { appId, userId }, { cache: false, retry: false, timeout: 5000, headers: { Authorization: `Bearer ${serviceToken}` } });
395
- const verified = result?.authorized
396
- ? { authorized: true, scopes: Array.isArray(result.scopes) ? result.scopes : [] }
397
- : null;
398
- this.rememberActingAs(`${appId}:${userId}`, verified, ACTING_AS_GRANT_TTL_MS);
418
+ const result = await this.request('GET', '/internal/service-acting-as/verify', { appId, userId, ...(options.credentialId ? { credentialId: options.credentialId,
419
+ ownerAccountId: options.ownerAccountId, environment: options.environment } : {}) }, { cache: false, retry: false, timeout: 5000, headers: { Authorization: `Bearer ${serviceToken}` } });
420
+ // A deadline bounds how long an already old response can authorize. It
421
+ // is not by itself a measurement of end-to-end revocation latency.
422
+ if (this.actingAsGenerations.get(pair) !== generation || serial < generation.completed)
423
+ return null;
424
+ generation.completed = serial;
425
+ this.clearActingAsPair(appId, userId);
426
+ if (Date.now() - startedAt >= 5000)
427
+ return null;
428
+ const rawEpoch = result?.epoch;
429
+ if (typeof rawEpoch !== 'string' || !/^(?:0|[1-9][0-9]{0,18})$/.test(rawEpoch))
430
+ return null;
431
+ const epoch = BigInt(rawEpoch);
432
+ if (epoch > 9223372036854775807n)
433
+ return null;
434
+ if (typeof result.authorized !== 'boolean' || !Array.isArray(result.scopes)
435
+ || !result.scopes.every((scope) => typeof scope === 'string' && scope.length > 0 && scope.trim() === scope)
436
+ || new Set(result.scopes).size !== result.scopes.length
437
+ || (!result.authorized && result.scopes.length !== 0))
438
+ return null;
439
+ const older = epoch < generation.epoch;
440
+ if (epoch > generation.epoch)
441
+ generation.epoch = epoch;
442
+ if (result.authorized && older)
443
+ return null;
444
+ const verified = result.authorized === true
445
+ ? { authorized: true, scopes: result.scopes, epoch: rawEpoch } : null;
446
+ if (options.cache === true)
447
+ this.rememberActingAs(cacheKey, verified, startedAt + (verified ? ACTING_AS_GRANT_TTL_MS : ACTING_AS_DENIAL_TTL_MS));
399
448
  return verified;
400
449
  }
401
450
  catch (error) {
402
- logger_1.logger.warn('[oxy.auth] verifyActingAs lookup failed — caching negative result', {
403
- component: 'auth',
404
- method: 'verifyActingAs',
405
- appId,
406
- userId,
451
+ if (this.actingAsGenerations.get(pair) === generation && serial >= generation.completed) {
452
+ generation.completed = serial;
453
+ this.clearActingAsPair(appId, userId);
454
+ }
455
+ logger_1.logger.warn('[oxy.auth] verifyActingAs lookup failed — not cached', {
456
+ component: 'auth', method: 'verifyActingAs', appId, userId,
407
457
  }, error);
408
- this.rememberActingAs(`${appId}:${userId}`, null, ACTING_AS_DENIAL_TTL_MS);
409
458
  return null;
410
459
  }
411
460
  }
412
- rememberActingAs(cacheKey, result, ttlMs) {
461
+ clearActingAsPair(appId, userId) {
462
+ for (const key of this.actingAs.keys()) {
463
+ const [cachedApp, cachedUser] = JSON.parse(key);
464
+ if (cachedApp === appId && cachedUser === userId)
465
+ this.actingAs.delete(key);
466
+ }
467
+ }
468
+ rememberActingAs(cacheKey, result, expiresAt) {
413
469
  this.actingAs.delete(cacheKey);
414
- this.actingAs.set(cacheKey, { result, expiresAt: Date.now() + ttlMs });
470
+ this.actingAs.set(cacheKey, { result, expiresAt });
415
471
  if (this.actingAs.size > ACTING_AS_CACHE_MAX) {
416
472
  const oldest = this.actingAs.keys().next().value;
417
473
  if (oldest !== undefined)
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signInAgentAccount = signInAgentAccount;
4
+ /** Autonomous entry helper. The signer owns its private key (for example in a KMS). */
5
+ const contracts_1 = require("@oxy.so/contracts");
6
+ const secp256k1_1 = require("@oxy.so/protocol/secp256k1");
7
+ /** Explicit sign-in; no timer, automatic escalation or service credential lane. */
8
+ async function signInAgentAccount(options) {
9
+ const { client, accountId, signer } = options;
10
+ const publicKey = (0, secp256k1_1.normalizeSecp256k1PublicKey)(signer.publicKey.trim());
11
+ const claims = await client.auth.agent.requestChallenge(publicKey);
12
+ const timestamp = Date.now();
13
+ if (claims.action !== 'agent_signin' || claims.accountId !== accountId || claims.actorId !== accountId
14
+ || (0, secp256k1_1.normalizeSecp256k1PublicKey)(claims.publicKey) !== publicKey || !claims.authMethodId || claims.expiresAt <= timestamp) {
15
+ throw new Error('Agent challenge does not match this account and signer');
16
+ }
17
+ const signature = await signer.signMessage((0, contracts_1.buildAgentProofMessage)(claims, timestamp));
18
+ return client.auth.agent.verify(publicKey, { challenge: claims.challenge, timestamp, signature });
19
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OXY_SERVICE_ENVIRONMENTS = void 0;
4
4
  exports.getOxyUserId = getOxyUserId;
5
+ exports.getOxyActor = getOxyActor;
5
6
  exports.isOxyAuthenticated = isOxyAuthenticated;
6
7
  exports.getOxyBillingPrincipal = getOxyBillingPrincipal;
7
8
  exports.getRequiredOxyBillingPrincipal = getRequiredOxyBillingPrincipal;
@@ -39,6 +40,24 @@ function getOxyUserId(req) {
39
40
  normalizeId(authReq.user?.id) ??
40
41
  normalizeId(authReq.user?._id));
41
42
  }
43
+ /**
44
+ * Who acted, and as whom, on this user session (issue #1520), or `null` when
45
+ * the session authority did not report it.
46
+ *
47
+ * An audit records `actorAccountId` as the actor and `effectiveAccountId`
48
+ * (equal to {@link getOxyUserId}) as the account spoken as. A bot acting with
49
+ * nobody operating it is its own actor; a person acting as an organization or
50
+ * a bot is the actor of a `delegated` chain. Never substitute the effective
51
+ * account's owner for either: when this is `null`, record the actor as unknown.
52
+ */
53
+ function getOxyActor(req) {
54
+ const actor = req.oxyActor;
55
+ if (!actor)
56
+ return null;
57
+ // Defence in depth: a chain that does not describe the authenticated
58
+ // subject is not this request's chain.
59
+ return actor.effectiveAccountId === getOxyUserId(req) ? actor : null;
60
+ }
42
61
  function isOxyAuthenticated(req) {
43
62
  return getOxyUserId(req) !== null;
44
63
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CapabilityTicketError = void 0;
4
- exports.inputSatisfiesCapabilityLimits = inputSatisfiesCapabilityLimits;
3
+ exports.inputSatisfiesCapabilityLimits = exports.CapabilityTicketError = void 0;
5
4
  exports.issueCapabilityTicket = issueCapabilityTicket;
6
5
  exports.verifyCapabilityTicket = verifyCapabilityTicket;
7
6
  exports.readCapabilityAuthorization = readCapabilityAuthorization;
@@ -30,45 +29,6 @@ function decodeJsonSegment(segment) {
30
29
  throw new CapabilityTicketError('malformed', 'Capability ticket is not valid base64url JSON');
31
30
  }
32
31
  }
33
- function valuesAtPath(input, path) {
34
- let current = [input];
35
- for (const segment of path.split('.')) {
36
- const next = [];
37
- for (const value of current) {
38
- const records = Array.isArray(value) ? value : [value];
39
- for (const record of records) {
40
- if (typeof record !== 'object' || record === null || Array.isArray(record))
41
- continue;
42
- if (Object.prototype.hasOwnProperty.call(record, segment)) {
43
- next.push(record[segment]);
44
- }
45
- }
46
- }
47
- if (next.length === 0)
48
- return [];
49
- current = next;
50
- }
51
- return current.flatMap((value) => Array.isArray(value) ? value : [value]);
52
- }
53
- /** Enforces the signed per-action bounds before a domain handler runs. */
54
- function inputSatisfiesCapabilityLimits(tool, input, limits) {
55
- for (const limit of limits) {
56
- if (limit.tool !== tool)
57
- return false;
58
- const actualValues = valuesAtPath(input, limit.key);
59
- if (actualValues.length === 0)
60
- return false;
61
- if (typeof limit.value === 'number') {
62
- const maximum = limit.value;
63
- if (!actualValues.every((actual) => (typeof actual === 'number' && Number.isFinite(actual) && actual <= maximum)))
64
- return false;
65
- continue;
66
- }
67
- if (!actualValues.every((actual) => actual === limit.value))
68
- return false;
69
- }
70
- return true;
71
- }
72
32
  function issueCapabilityTicket(claims, options) {
73
33
  const ttlSeconds = options.ttlSeconds ?? 60;
74
34
  if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1 || ttlSeconds > DEFAULT_MAX_TTL_SECONDS) {
@@ -190,3 +150,5 @@ function createCapabilityTicketMiddleware(options) {
190
150
  }
191
151
  };
192
152
  }
153
+ var contracts_2 = require("@oxy.so/contracts");
154
+ Object.defineProperty(exports, "inputSatisfiesCapabilityLimits", { enumerable: true, get: function () { return contracts_2.inputSatisfiesCapabilityLimits; } });
@@ -16,8 +16,8 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.verifyCapabilityTicket = exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
- exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = void 0;
19
+ exports.readCapabilityAuthorization = exports.issueCapabilityTicket = exports.inputSatisfiesCapabilityLimits = exports.createCapabilityTicketMiddleware = exports.CapabilityTicketError = exports.OXY_CSP_BASELINE = exports.inlineScriptCspHash = exports.formatOxyCspPolicy = exports.extractInlineScripts = exports.cspSourcesFor = exports.createOxySecurityHeaders = exports.buildOxyPagesHeaders = exports.buildOxyCspDirectives = exports.createOxyCors = exports.UPSTREAM_HEADERS_TIMEOUT_MS = exports.MAX_URL_LENGTH = exports.MAX_REDIRECTS = exports.DEFAULT_USER_AGENT = exports.BLOCKED_HOSTNAMES = exports.ALLOWED_PROTOCOLS = exports.ALLOWED_PORTS = exports.UpstreamError = exports.SsrfRejection = exports.safeFetch = exports.isBlockedIp = exports.assertSafePublicUrl = exports.createOxyRateLimit = exports.OXY_SERVICE_ENVIRONMENTS = exports.requireOxyAuth = exports.isOxyAuthenticated = exports.getRequiredOxyUserId = exports.getRequiredOxyBillingPrincipal = exports.getOxyUserId = exports.getOxyRequestAttribution = exports.getOxyDelegatedUserId = exports.getOxyBillingPrincipal = exports.getOxyAuthRefusal = exports.getOxyActor = exports.createOxyAuthMiddleware = exports.createOptionalOxyAuth = exports.OXY_ACCOUNT_DELETED_EVENT_URI = exports.OxyAccountEventError = exports.ServerReputationApi = exports.ServerNotificationsApi = exports.ServerLinkedAccountsApi = exports.ServerAssetsApi = exports.ServerAgencyApi = exports.ANONYMOUS_SERVICE_TOKEN_RETRY_MS = exports.ServiceCredentialMismatchError = exports.OxyServer = void 0;
20
+ exports.signInAgentAccount = exports.hasBoundedServiceTokenLifetime = exports.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS = exports.requestWorkloadServiceToken = exports.canAttestWorkloadIdentity = exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isAllowedDeviceJoinOrigin = exports.isOfficialWebOrigin = exports.isLoopbackOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.createLiveCapabilityTicketVerifier = exports.verifySecret = exports.verifyOxyRequesterAssertion = exports.signOxyRequesterAssertion = exports.readOxyRequesterAssertionKeyId = exports.oxyRequesterAssertionClaimsSchema = exports.OxyRequesterAssertionError = exports.OXY_REQUESTER_ASSERTION_TYPE = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.createOxyRequesterAssertionAuth = exports.createOxyJwksKeyResolver = exports.verifyCapabilityTicket = void 0;
21
21
  // The backend client: OxyServices plus the service-token lane, middleware,
22
22
  // account events and the service-only namespace methods.
23
23
  var OxyServer_1 = require("./OxyServer");
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "OXY_ACCOUNT_DELETED_EVENT_URI", { enumerable: tr
36
36
  var auth_1 = require("./auth");
37
37
  Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
38
38
  Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
39
+ Object.defineProperty(exports, "getOxyActor", { enumerable: true, get: function () { return auth_1.getOxyActor; } });
39
40
  Object.defineProperty(exports, "getOxyAuthRefusal", { enumerable: true, get: function () { return auth_1.getOxyAuthRefusal; } });
40
41
  Object.defineProperty(exports, "getOxyBillingPrincipal", { enumerable: true, get: function () { return auth_1.getOxyBillingPrincipal; } });
41
42
  Object.defineProperty(exports, "getOxyDelegatedUserId", { enumerable: true, get: function () { return auth_1.getOxyDelegatedUserId; } });
@@ -106,6 +107,8 @@ Object.defineProperty(exports, "verifyOxyRequesterAssertion", { enumerable: true
106
107
  // Constant-time secret comparison.
107
108
  var verifySecret_1 = require("./verifySecret");
108
109
  Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
110
+ var liveCapabilityTicket_1 = require("./liveCapabilityTicket");
111
+ Object.defineProperty(exports, "createLiveCapabilityTicketVerifier", { enumerable: true, get: function () { return liveCapabilityTicket_1.createLiveCapabilityTicketVerifier; } });
109
112
  // Cross-service user-invalidation signal: oxy-api publishes when identity
110
113
  // changes, every consuming backend sweeps its caches instead of waiting out a TTL.
111
114
  var userInvalidation_1 = require("./userInvalidation");
@@ -142,3 +145,8 @@ Object.defineProperty(exports, "withoutNodeHttpObservation", { enumerable: true,
142
145
  var workloadIdentity_1 = require("./workloadIdentity");
143
146
  Object.defineProperty(exports, "canAttestWorkloadIdentity", { enumerable: true, get: function () { return workloadIdentity_1.canAttestWorkloadIdentity; } });
144
147
  Object.defineProperty(exports, "requestWorkloadServiceToken", { enumerable: true, get: function () { return workloadIdentity_1.requestWorkloadServiceToken; } });
148
+ var serviceTokenLifetime_1 = require("./serviceTokenLifetime");
149
+ Object.defineProperty(exports, "OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS", { enumerable: true, get: function () { return serviceTokenLifetime_1.OXY_SERVICE_TOKEN_MAX_LIFETIME_SECONDS; } });
150
+ Object.defineProperty(exports, "hasBoundedServiceTokenLifetime", { enumerable: true, get: function () { return serviceTokenLifetime_1.hasBoundedServiceTokenLifetime; } });
151
+ var agentAccount_1 = require("./agentAccount");
152
+ Object.defineProperty(exports, "signInAgentAccount", { enumerable: true, get: function () { return agentAccount_1.signInAgentAccount; } });
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createLiveCapabilityTicketVerifier = createLiveCapabilityTicketVerifier;
4
+ const contracts_1 = require("@oxy.so/contracts");
5
+ const capabilityTicket_1 = require("./capabilityTicket");
6
+ /** Local signature + live receiving-service introspection; no cache or retry. */
7
+ function createLiveCapabilityTicketVerifier(options) {
8
+ if (typeof options.issuer !== 'string' || options.issuer.trim() === '')
9
+ throw new Error('A trusted Capability issuer is required');
10
+ const timeoutMs = options.timeoutMs ?? 5000;
11
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000) {
12
+ throw new Error('Capability introspection timeout must be between 1 and 60000ms');
13
+ }
14
+ return async (ticket, input = {}) => {
15
+ input.signal?.throwIfAborted();
16
+ const signed = (0, capabilityTicket_1.verifyCapabilityTicket)(ticket, options);
17
+ if (!signed.catalog)
18
+ throw new Error('Internal MCP requires a signed catalogue binding');
19
+ const controller = new AbortController();
20
+ const abort = () => controller.abort(input.signal?.reason);
21
+ input.signal?.addEventListener('abort', abort, { once: true });
22
+ const timer = setTimeout(() => controller.abort(new Error('Capability introspection timed out')), timeoutMs);
23
+ let rejectAbort;
24
+ try {
25
+ const interrupted = new Promise((_resolve, reject) => {
26
+ rejectAbort = () => reject(controller.signal.reason ?? new Error('Capability introspection aborted'));
27
+ controller.signal.addEventListener('abort', rejectAbort, { once: true });
28
+ if (controller.signal.aborted)
29
+ rejectAbort();
30
+ });
31
+ const result = await Promise.race([options.introspect(ticket, { signal: controller.signal }), interrupted]);
32
+ controller.signal.throwIfAborted();
33
+ if (result.active !== true)
34
+ throw new Error('Capability ticket is inactive');
35
+ const live = contracts_1.capabilityTicketClaimsSchema.parse(result.claims);
36
+ if ((0, contracts_1.canonicalCapabilityJson)(live) !== (0, contracts_1.canonicalCapabilityJson)(signed))
37
+ throw new Error('Capability introspection claims mismatch');
38
+ // An authority response delayed past expiry cannot extend the signed grant.
39
+ return (0, capabilityTicket_1.verifyCapabilityTicket)(ticket, options);
40
+ }
41
+ finally {
42
+ clearTimeout(timer);
43
+ input.signal?.removeEventListener('abort', abort);
44
+ if (rejectAbort)
45
+ controller.signal.removeEventListener('abort', rejectAbort);
46
+ }
47
+ };
48
+ }
@@ -4,6 +4,8 @@ exports.OXY_JWT_ISSUER = exports.OxyAccountEventError = exports.OXY_ACCOUNT_DELE
4
4
  exports.createOxyMiddleware = createOxyMiddleware;
5
5
  exports.parseJsonSegment = parseJsonSegment;
6
6
  exports.resolveServiceTokenPublicKey = resolveServiceTokenPublicKey;
7
+ const serviceTokenLifetime_1 = require("./serviceTokenLifetime");
8
+ const contracts_1 = require("@oxy.so/contracts");
7
9
  const jwt_decode_1 = require("jwt-decode");
8
10
  const protocol_1 = require("@oxy.so/protocol");
9
11
  const logger_1 = require("../logger");
@@ -412,22 +414,18 @@ function createOxyMiddleware(host) {
412
414
  return onError(error);
413
415
  return res.status(401).json(error);
414
416
  }
415
- // One of Oxy's own applications acts for a user without a grant:
416
- // inside the ecosystem that is trust, not consent. It still acts
417
- // with the USER's authority only — what that user may do is for the
418
- // receiving service to check.
419
- if (oxyUserId && tier === 'internal') {
420
- req.userId = oxyUserId;
421
- req.user = { id: oxyUserId };
422
- req.serviceActingAs = { userId: oxyUserId, scopes: [] };
423
- }
424
- else if (oxyUserId) {
425
- // C3: an EXTERNAL service may only act as a user when an explicit
417
+ // A service credential plus a user header is never a present user
418
+ // session, even for an internal tier. Offline delegation is explicit.
419
+ if (oxyUserId) {
420
+ // C3: every service may only act as a user when an explicit
426
421
  // ServiceActingAs grant exists for that (appId, userId) pair.
427
422
  // Without the grant we MUST refuse — silently attaching
428
423
  // `req.userId = oxyUserId` would let any service impersonate
429
424
  // any user simply by setting the header.
430
- const grant = await host.verifyActingAs(appId, oxyUserId);
425
+ const grant = await host.verifyActingAs(appId, oxyUserId, {
426
+ cache: false, credentialId: decoded.credentialId,
427
+ ownerAccountId: decoded.ownerAccountId, environment: decoded.environment,
428
+ });
431
429
  if (!grant || !grant.authorized) {
432
430
  logger_1.logger.warn('[oxy.auth] Service token rejected — no delegation grant', {
433
431
  component: 'auth',
@@ -647,7 +645,43 @@ function createOxyMiddleware(host) {
647
645
  return onError(error);
648
646
  return res.status(401).json(error);
649
647
  }
648
+ // Who acted, and as whom — from the session authority's answer
649
+ // (#1520), never from a header or this undecoded token. An API that
650
+ // predates the field answers without it, and that is `null` here:
651
+ // "not reported", never a guess that the subject acted for itself.
652
+ // A chain naming a DIFFERENT effective account than the session
653
+ // resolved to has no benign cause and is refused like a user
654
+ // mismatch.
655
+ const actor = readActorChain(validationResult.actor);
656
+ if (actor === 'invalid' || (actor && actor.effectiveAccountId !== validatedUserId)) {
657
+ logger_1.logger.warn('[oxy.auth] Session rejected — actor chain does not describe the session', {
658
+ component: 'auth',
659
+ method: 'auth',
660
+ validatedUserId,
661
+ });
662
+ recordRefusal(req, {
663
+ code: 'SESSION_ACTOR_MISMATCH',
664
+ stage: 'session',
665
+ reason: 'Session actor chain does not describe the validated session',
666
+ status: 401,
667
+ });
668
+ if (optional) {
669
+ req.userId = null;
670
+ req.user = null;
671
+ return next();
672
+ }
673
+ const error = {
674
+ error: 'SESSION_ACTOR_MISMATCH',
675
+ message: 'Session actor does not match the session',
676
+ code: 'SESSION_ACTOR_MISMATCH',
677
+ status: 401
678
+ };
679
+ if (onError)
680
+ return onError(error);
681
+ return res.status(401).json(error);
682
+ }
650
683
  req.userId = validatedUserId;
684
+ req.oxyActor = actor;
651
685
  req.accessToken = token;
652
686
  req.sessionId = sessionId;
653
687
  // Session validation already returned the full user, so `loadUser`
@@ -909,12 +943,6 @@ function createOxyMiddleware(host) {
909
943
  });
910
944
  return;
911
945
  }
912
- // Oxy's own applications are trusted app to app; scopes are the
913
- // external lane.
914
- if (req.serviceApp.tier === 'internal') {
915
- next();
916
- return;
917
- }
918
946
  const appHasScope = appScopes.includes(scope);
919
947
  const delegationHasScope = delegatedScopes.includes(scope);
920
948
  const hasRequiredScope = req.serviceActingAs
@@ -1130,6 +1158,9 @@ function getUserIdentityId(user) {
1130
1158
  * service token because no claim binding existed.
1131
1159
  */
1132
1160
  function verifyServiceTokenClaims(decoded, expected) {
1161
+ if (!(0, serviceTokenLifetime_1.hasBoundedServiceTokenLifetime)(decoded)) {
1162
+ throw new ServiceTokenClaimError('Service token lifetime must be positive and at most 300 seconds');
1163
+ }
1133
1164
  if (decoded.type !== 'service') {
1134
1165
  throw new ServiceTokenClaimError(`Service token has unexpected type '${String(decoded.type)}'`);
1135
1166
  }
@@ -1146,3 +1177,14 @@ function verifyServiceTokenClaims(decoded, expected) {
1146
1177
  throw new ServiceTokenClaimError(`Service token audience mismatch: expected '${expected.audience}', got '${String(aud)}'`);
1147
1178
  }
1148
1179
  }
1180
+ /**
1181
+ * Parse the session authority's actor chain. `null` when the API did not send
1182
+ * one; `'invalid'` when it sent something that is not a chain, which the caller
1183
+ * refuses rather than ignores.
1184
+ */
1185
+ function readActorChain(raw) {
1186
+ if (raw === undefined || raw === null)
1187
+ return null;
1188
+ const parsed = contracts_1.accountActorChainSchema.safeParse(raw);
1189
+ return parsed.success ? parsed.data : 'invalid';
1190
+ }