@oxy.so/core 4.0.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 +53 -20
  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 +46 -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 +12 -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 +50 -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,21 +64,8 @@
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
+ const contracts_2 = require("@oxy.so/contracts");
68
69
  /** The base URL of the Oxy API, when a caller names none. */
69
70
  exports.OXY_INFERENCE_BASE_URL = 'https://api.oxy.so';
70
71
  /**
@@ -175,7 +176,7 @@ class OxyInferenceClient {
175
176
  * "no such model".
176
177
  */
177
178
  async getModel(modelId, options = {}) {
178
- const parsed = contracts_1.modelIdSchema.safeParse(modelId);
179
+ const parsed = contracts_2.modelIdSchema.safeParse(modelId);
179
180
  if (!parsed.success) {
180
181
  throw new Error(`Not a canonical model id: ${modelId}. Expected <publisher>/<model>, e.g. acme/some-model.`);
181
182
  }
@@ -212,6 +213,23 @@ class OxyInferenceClient {
212
213
  : { delegatedUserId: options.delegatedUserId }),
213
214
  });
214
215
  }
216
+ /** Typed nonstreaming decisions. Idempotency conflicts are never replayed or retried. */
217
+ async decide(request, options = {}) {
218
+ const input = contracts_1.decisionRequestSchema.parse(request);
219
+ const payload = await __classPrivateFieldGet(this, _OxyInferenceClient_instances, "m", _OxyInferenceClient_request).call(this, 'POST', '/v1/decisions', {
220
+ body: input,
221
+ bindRequestId: true,
222
+ signal: options.signal,
223
+ idempotencyKey: options.idempotencyKey,
224
+ delegatedUserId: options.delegatedUserId,
225
+ });
226
+ const parsed = contracts_1.decisionSuccessSchema.safeParse(payload);
227
+ if (!parsed.success || parsed.data.model !== input.model ||
228
+ !(0, contracts_1.decisionAnswersMatch)(input, parsed.data.data)) {
229
+ throw new OxyInferenceProtocolError('Invalid or mismatched decisions response.');
230
+ }
231
+ return parsed.data;
232
+ }
215
233
  /** Generate audio bytes through Oxy's authenticated inference edge. */
216
234
  async speech(request, options = {}) {
217
235
  const response = await __classPrivateFieldGet(this, _OxyInferenceClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _OxyInferenceClient_baseURL, "f")}/v1/audio/speech`, {
@@ -232,7 +250,7 @@ class OxyInferenceClient {
232
250
  if (!response.ok) {
233
251
  throw toInferenceError(await response.json().catch(() => undefined), response.status, requestId);
234
252
  }
235
- const mediaType = contracts_1.inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
253
+ const mediaType = contracts_2.inferenceAudioMediaTypeSchema.safeParse(mediaTypeOf(response));
236
254
  if (!mediaType.success || !response.body) {
237
255
  await cancelUnreadResponse(response);
238
256
  throw protocolError('The inference API returned no supported audio body.', requestId);
@@ -249,7 +267,7 @@ class OxyInferenceClient {
249
267
  break;
250
268
  }
251
269
  size += next.value.byteLength;
252
- if (size > contracts_1.MAX_INFERENCE_AUDIO_BYTES)
270
+ if (size > contracts_2.MAX_INFERENCE_AUDIO_BYTES)
253
271
  throw protocolError('The inference audio exceeds the response limit.', requestId);
254
272
  chunks.push(next.value);
255
273
  }
@@ -332,7 +350,7 @@ class OxyInferenceClient {
332
350
  catch (_error) {
333
351
  throw protocolError(`The inference API sent a ${frame.name || 'unnamed'} SSE event that is not JSON.`, requestId);
334
352
  }
335
- const parsed = contracts_1.inferenceStreamEventSchema.safeParse(payload);
353
+ const parsed = contracts_2.inferenceStreamEventSchema.safeParse(payload);
336
354
  if (!parsed.success) {
337
355
  const path = parsed.error.issues[0]?.path;
338
356
  throw protocolError(`The inference API sent an event outside the published stream contract at ${path === undefined || path.length === 0 ? 'an unknown field' : path.join('.')}.`, requestId);
@@ -366,7 +384,15 @@ class OxyInferenceClient {
366
384
  * somebody else.
367
385
  */
368
386
  async getGeneration(id, options = {}) {
369
- 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);
370
396
  return body.data;
371
397
  }
372
398
  }
@@ -428,6 +454,13 @@ async function _OxyInferenceClient_request(method, path, options) {
428
454
  if (!response.ok) {
429
455
  throw toInferenceError(payload, response.status, response.headers.get('X-Oxy-Request-Id'));
430
456
  }
457
+ if (options.bindRequestId) {
458
+ const requestId = response.headers.get('X-Oxy-Request-Id');
459
+ if (!requestId || typeof payload !== 'object' || payload === null ||
460
+ !('requestId' in payload) || payload.requestId !== requestId) {
461
+ throw new OxyInferenceProtocolError('Decisions response request ID does not match its header.', requestId ?? undefined);
462
+ }
463
+ }
431
464
  return payload;
432
465
  };
433
466
  /** Bound one public SSE event before parsing it as JSON. */
@@ -574,7 +607,7 @@ const STATUS_FALLBACK_CODE = {
574
607
  * together — so an unrecognised one falls back to the status map instead of
575
608
  * being asserted into the type.
576
609
  */
577
- const INFERENCE_ERROR_CODE_SET = new Set(contracts_1.INFERENCE_ERROR_CODES);
610
+ const INFERENCE_ERROR_CODE_SET = new Set(contracts_2.INFERENCE_ERROR_CODES);
578
611
  /** Read whichever error shape arrived into the one this client throws. */
579
612
  function toInferenceError(payload, status, requestIdHeader) {
580
613
  const body = (payload ?? {});
@@ -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
+ }