@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.
- package/dist/cjs/HttpService.js +142 -33
- package/dist/cjs/api/accounts.js +49 -10
- package/dist/cjs/api/apps.js +2 -2
- package/dist/cjs/api/auth.js +24 -3
- package/dist/cjs/api/billing.js +35 -0
- package/dist/cjs/boot/sessionColdBoot.js +28 -57
- package/dist/cjs/inference/OxyInferenceClient.js +23 -15
- package/dist/cjs/server/OxyServer.js +107 -51
- package/dist/cjs/server/agentAccount.js +19 -0
- package/dist/cjs/server/auth.js +19 -0
- package/dist/cjs/server/capabilityTicket.js +3 -41
- package/dist/cjs/server/index.js +10 -2
- package/dist/cjs/server/liveCapabilityTicket.js +48 -0
- package/dist/cjs/server/middleware.js +60 -18
- package/dist/cjs/server/namespaces.js +57 -0
- package/dist/cjs/server/serviceTokenLifetime.js +13 -0
- package/dist/cjs/session/SessionClient.js +90 -12
- package/dist/cjs/session/authStateStore.js +130 -39
- package/dist/cjs/session/refresh.js +152 -30
- package/dist/cjs/session/sharedDeviceCredential.js +17 -1
- package/dist/cjs/utils/officialOrigins.js +7 -18
- package/dist/esm/HttpService.js +143 -34
- package/dist/esm/api/accounts.js +38 -0
- package/dist/esm/api/apps.js +2 -2
- package/dist/esm/api/auth.js +23 -3
- package/dist/esm/api/billing.js +35 -0
- package/dist/esm/boot/sessionColdBoot.js +29 -58
- package/dist/esm/inference/OxyInferenceClient.js +21 -13
- package/dist/esm/server/OxyServer.js +107 -51
- package/dist/esm/server/agentAccount.js +16 -0
- package/dist/esm/server/auth.js +18 -0
- package/dist/esm/server/capabilityTicket.js +1 -39
- package/dist/esm/server/index.js +4 -1
- package/dist/esm/server/liveCapabilityTicket.js +45 -0
- package/dist/esm/server/middleware.js +60 -18
- package/dist/esm/server/namespaces.js +57 -0
- package/dist/esm/server/serviceTokenLifetime.js +9 -0
- package/dist/esm/session/SessionClient.js +90 -12
- package/dist/esm/session/authStateStore.js +128 -38
- package/dist/esm/session/refresh.js +151 -30
- package/dist/esm/session/sharedDeviceCredential.js +17 -1
- package/dist/esm/utils/officialOrigins.js +2 -13
- package/dist/types/HttpService.d.ts +26 -12
- package/dist/types/api/accounts.d.ts +38 -0
- package/dist/types/api/agency.d.ts +3 -1
- package/dist/types/api/apps.d.ts +3 -1
- package/dist/types/api/auth.d.ts +12 -1
- package/dist/types/api/billing.d.ts +12 -0
- package/dist/types/api/session.d.ts +3 -1
- package/dist/types/boot/sessionColdBoot.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/inference/OxyInferenceClient.d.ts +9 -0
- package/dist/types/inference/index.d.ts +1 -1
- package/dist/types/server/OxyServer.d.ts +16 -9
- package/dist/types/server/agentAccount.d.ts +14 -0
- package/dist/types/server/auth.d.ts +18 -0
- package/dist/types/server/capabilityTicket.d.ts +2 -4
- package/dist/types/server/index.d.ts +7 -1
- package/dist/types/server/liveCapabilityTicket.d.ts +15 -0
- package/dist/types/server/middleware.d.ts +15 -6
- package/dist/types/server/namespaces.d.ts +78 -2
- package/dist/types/server/serviceTokenLifetime.d.ts +7 -0
- package/dist/types/session/SessionClient.d.ts +8 -0
- package/dist/types/session/authStateStore.d.ts +15 -0
- package/dist/types/session/index.d.ts +1 -1
- package/dist/types/session/refresh.d.ts +32 -7
- package/dist/types/session/sharedDeviceCredential.d.ts +3 -1
- package/dist/types/utils/officialOrigins.d.ts +1 -5
- package/package.json +6 -6
- package/src/HttpService.ts +138 -50
- package/src/api/accounts.ts +22 -0
- package/src/api/agency.ts +3 -1
- package/src/api/apps.ts +2 -2
- package/src/api/auth.ts +31 -2
- package/src/api/billing.ts +71 -9
- package/src/api/session.ts +3 -1
- package/src/boot/sessionColdBoot.ts +27 -72
- package/src/index.ts +1 -1
- package/src/inference/OxyInferenceClient.ts +19 -5
- package/src/inference/index.ts +1 -0
- package/src/server/OxyServer.ts +114 -53
- package/src/server/agentAccount.ts +28 -0
- package/src/server/auth.ts +25 -0
- package/src/server/capabilityTicket.ts +2 -42
- package/src/server/index.ts +13 -0
- package/src/server/liveCapabilityTicket.ts +47 -0
- package/src/server/middleware.ts +72 -25
- package/src/server/namespaces.ts +99 -2
- package/src/server/serviceTokenLifetime.ts +13 -0
- package/src/session/SessionClient.ts +85 -12
- package/src/session/authStateStore.ts +130 -46
- package/src/session/index.ts +1 -0
- package/src/session/refresh.ts +144 -28
- package/src/session/sharedDeviceCredential.ts +14 -2
- 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
|
|
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
|
-
/**
|
|
61
|
-
const ACTING_AS_GRANT_TTL_MS =
|
|
62
|
-
/**
|
|
63
|
-
const ACTING_AS_DENIAL_TTL_MS =
|
|
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
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
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
|
|
284
|
-
const
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
this.
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/cjs/server/auth.js
CHANGED
|
@@ -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; } });
|
package/dist/cjs/server/index.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* ```
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.
|
|
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
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|