@oxy.so/core 1.3.0 → 1.4.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.
@@ -109,5 +109,46 @@ function OxyServicesAgencyMixin(Base) {
109
109
  throw this.handleError(error);
110
110
  }
111
111
  }
112
+ /**
113
+ * Product backend → Oxy: trade the signed-in requester's access token for a
114
+ * one-use assertion naming `agentId` (ADR 0025). Authenticated with THIS
115
+ * instance's service credential (`configureServiceAuth`). `subjectToken` is
116
+ * sent to Oxy only, in the body; never forward it anywhere else.
117
+ *
118
+ * Not retried: a refusal is an answer, and the caller mints per turn.
119
+ */
120
+ async mintRequesterAssertion(input) {
121
+ try {
122
+ const serviceToken = await this.getServiceToken();
123
+ return await this.makeRequest('POST', '/internal/native-agents/requester-assertions', { agentId: input.agentId, subjectToken: input.subjectToken }, {
124
+ cache: false,
125
+ retry: false,
126
+ timeout: 5000,
127
+ headers: { Authorization: `Bearer ${serviceToken}` },
128
+ });
129
+ }
130
+ catch (error) {
131
+ throw this.handleError(error);
132
+ }
133
+ }
134
+ /**
135
+ * Audience → Oxy: verify, live-revalidate and CONSUME an assertion. A second
136
+ * call with the same assertion answers `active: false`. Authenticated with
137
+ * the audience's own service credential.
138
+ */
139
+ async introspectRequesterAssertion(input) {
140
+ try {
141
+ const serviceToken = await this.getServiceToken();
142
+ return await this.makeRequest('POST', '/internal/native-agents/requester-assertions/introspect', { assertion: input.assertion, presenter: input.presenter }, {
143
+ cache: false,
144
+ retry: false,
145
+ timeout: 5000,
146
+ headers: { Authorization: `Bearer ${serviceToken}` },
147
+ });
148
+ }
149
+ catch (error) {
150
+ throw this.handleError(error);
151
+ }
152
+ }
112
153
  };
113
154
  }
@@ -16,7 +16,8 @@
16
16
  * ```
17
17
  */
18
18
  Object.defineProperty(exports, "__esModule", { value: true });
19
- exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = 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.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
19
+ 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 = 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.createOxyAuthMiddleware = exports.createOptionalOxyAuth = void 0;
20
+ exports.withoutNodeHttpObservation = exports.observeNodeHttp = exports.createEcosystemTraffic = exports.isOfficialWebOrigin = exports.registrableApex = exports.OXY_IDENTITY_CACHE_PREFIXES = exports.oxyUserByIdCacheKey = exports.evictOxyIdentityCache = exports.publishOxyUserInvalidation = exports.createOxyUserInvalidationHandler = exports.verifySecret = void 0;
20
21
  var auth_1 = require("./auth");
21
22
  Object.defineProperty(exports, "createOptionalOxyAuth", { enumerable: true, get: function () { return auth_1.createOptionalOxyAuth; } });
22
23
  Object.defineProperty(exports, "createOxyAuthMiddleware", { enumerable: true, get: function () { return auth_1.createOxyAuthMiddleware; } });
@@ -72,6 +73,20 @@ Object.defineProperty(exports, "inputSatisfiesCapabilityLimits", { enumerable: t
72
73
  Object.defineProperty(exports, "issueCapabilityTicket", { enumerable: true, get: function () { return capabilityTicket_1.issueCapabilityTicket; } });
73
74
  Object.defineProperty(exports, "readCapabilityAuthorization", { enumerable: true, get: function () { return capabilityTicket_1.readCapabilityAuthorization; } });
74
75
  Object.defineProperty(exports, "verifyCapabilityTicket", { enumerable: true, get: function () { return capabilityTicket_1.verifyCapabilityTicket; } });
76
+ // Present-requester assertions (ADR 0025): a product backend trades a signed-in
77
+ // person's live session with Oxy for a one-use assertion its audience consumes.
78
+ var requesterAssertion_1 = require("./requesterAssertion");
79
+ Object.defineProperty(exports, "createOxyJwksKeyResolver", { enumerable: true, get: function () { return requesterAssertion_1.createOxyJwksKeyResolver; } });
80
+ Object.defineProperty(exports, "createOxyRequesterAssertionAuth", { enumerable: true, get: function () { return requesterAssertion_1.createOxyRequesterAssertionAuth; } });
81
+ Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER; } });
82
+ Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_HEADER", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_HEADER; } });
83
+ Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS; } });
84
+ Object.defineProperty(exports, "OXY_REQUESTER_ASSERTION_TYPE", { enumerable: true, get: function () { return requesterAssertion_1.OXY_REQUESTER_ASSERTION_TYPE; } });
85
+ Object.defineProperty(exports, "OxyRequesterAssertionError", { enumerable: true, get: function () { return requesterAssertion_1.OxyRequesterAssertionError; } });
86
+ Object.defineProperty(exports, "oxyRequesterAssertionClaimsSchema", { enumerable: true, get: function () { return requesterAssertion_1.oxyRequesterAssertionClaimsSchema; } });
87
+ Object.defineProperty(exports, "readOxyRequesterAssertionKeyId", { enumerable: true, get: function () { return requesterAssertion_1.readOxyRequesterAssertionKeyId; } });
88
+ Object.defineProperty(exports, "signOxyRequesterAssertion", { enumerable: true, get: function () { return requesterAssertion_1.signOxyRequesterAssertion; } });
89
+ Object.defineProperty(exports, "verifyOxyRequesterAssertion", { enumerable: true, get: function () { return requesterAssertion_1.verifyOxyRequesterAssertion; } });
75
90
  // Constant-time secret comparison.
76
91
  var verifySecret_1 = require("./verifySecret");
77
92
  Object.defineProperty(exports, "verifySecret", { enumerable: true, get: function () { return verifySecret_1.verifySecret; } });
@@ -0,0 +1,362 @@
1
+ "use strict";
2
+ /**
3
+ * Present-requester assertions (ADR 0025).
4
+ *
5
+ * A first-party product backend holding a signed-in person's live Oxy session
6
+ * trades it with Oxy (`OxyServices.mintRequesterAssertion`) for a one-use,
7
+ * short-lived `OXY-REQUESTER+JWT`, and sends that — never the person's bearer —
8
+ * to the audience service (Alia) beside its own service token. The audience
9
+ * mounts {@link createOxyRequesterAssertionAuth} after `createOxyAuthMiddleware`
10
+ * and gets `req.userId` from the verified, introspected and consumed assertion.
11
+ *
12
+ * Signing and stateless verification live here so Oxy (the signer) and every
13
+ * verifier share one implementation of the format.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.OxyRequesterAssertionError = exports.oxyRequesterAssertionClaimsSchema = exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = exports.OXY_REQUESTER_ASSERTION_HEADER = exports.OXY_REQUESTER_ASSERTION_TYPE = void 0;
17
+ exports.signOxyRequesterAssertion = signOxyRequesterAssertion;
18
+ exports.readOxyRequesterAssertionKeyId = readOxyRequesterAssertionKeyId;
19
+ exports.verifyOxyRequesterAssertion = verifyOxyRequesterAssertion;
20
+ exports.createOxyJwksKeyResolver = createOxyJwksKeyResolver;
21
+ exports.createOxyRequesterAssertionAuth = createOxyRequesterAssertionAuth;
22
+ const node_crypto_1 = require("node:crypto");
23
+ const zod_1 = require("zod");
24
+ exports.OXY_REQUESTER_ASSERTION_TYPE = 'OXY-REQUESTER+JWT';
25
+ /** The request header an audience reads the assertion from. */
26
+ exports.OXY_REQUESTER_ASSERTION_HEADER = 'x-oxy-requester-assertion';
27
+ /** Oxy signs for 120 s; no verifier accepts a lifetime above this ceiling. */
28
+ exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS = 300;
29
+ exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER = 'https://api.oxy.so';
30
+ const ALGORITHM = 'EdDSA';
31
+ const CLOCK_SKEW_SECONDS = 5;
32
+ const KEY_ID_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
33
+ const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
34
+ const MAX_ASSERTION_BYTES = 4096;
35
+ const identifier = zod_1.z.string().min(1).max(128);
36
+ exports.oxyRequesterAssertionClaimsSchema = zod_1.z.object({
37
+ iss: zod_1.z.string().min(1).max(256),
38
+ aud: identifier,
39
+ /** The requester account. */
40
+ sub: identifier,
41
+ jti: zod_1.z.string().uuid(),
42
+ iat: zod_1.z.number().int().nonnegative(),
43
+ exp: zod_1.z.number().int().positive(),
44
+ /** The application the assertion was minted for, and the only valid presenter. */
45
+ azp: identifier,
46
+ /** The exact credential of that application. */
47
+ cid: identifier,
48
+ /** The one native agent this requester may reach with it. */
49
+ agentId: identifier,
50
+ }).strict();
51
+ class OxyRequesterAssertionError extends Error {
52
+ constructor(code) {
53
+ super(`Requester assertion rejected: ${code}`);
54
+ this.code = code;
55
+ this.name = 'OxyRequesterAssertionError';
56
+ }
57
+ }
58
+ exports.OxyRequesterAssertionError = OxyRequesterAssertionError;
59
+ function base64Url(value) {
60
+ return Buffer.from(value).toString('base64url');
61
+ }
62
+ function decodeObject(segment) {
63
+ if (!BASE64URL_PATTERN.test(segment))
64
+ throw new OxyRequesterAssertionError('malformed');
65
+ const bytes = Buffer.from(segment, 'base64url');
66
+ if (bytes.toString('base64url') !== segment)
67
+ throw new OxyRequesterAssertionError('malformed');
68
+ let value;
69
+ try {
70
+ value = JSON.parse(bytes.toString('utf8'));
71
+ }
72
+ catch {
73
+ throw new OxyRequesterAssertionError('malformed');
74
+ }
75
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
76
+ throw new OxyRequesterAssertionError('malformed');
77
+ }
78
+ return value;
79
+ }
80
+ /** Signs an assertion. Oxy is the only intended caller. */
81
+ function signOxyRequesterAssertion(claims, signing) {
82
+ const parsed = exports.oxyRequesterAssertionClaimsSchema.parse(claims);
83
+ if (parsed.exp <= parsed.iat || parsed.exp - parsed.iat > exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS) {
84
+ throw new OxyRequesterAssertionError('ttl_exceeded');
85
+ }
86
+ if (!KEY_ID_PATTERN.test(signing.keyId))
87
+ throw new OxyRequesterAssertionError('malformed');
88
+ if (signing.privateKey.asymmetricKeyType !== 'ed25519') {
89
+ throw new OxyRequesterAssertionError('unknown_key');
90
+ }
91
+ const header = { alg: ALGORITHM, typ: exports.OXY_REQUESTER_ASSERTION_TYPE, kid: signing.keyId };
92
+ const input = `${base64Url(JSON.stringify(header))}.${base64Url(JSON.stringify(parsed))}`;
93
+ return `${input}.${base64Url((0, node_crypto_1.sign)(null, Buffer.from(input), signing.privateKey))}`;
94
+ }
95
+ /** Reads the `kid` without trusting anything else in the token. */
96
+ function readOxyRequesterAssertionKeyId(token) {
97
+ if (token.length > MAX_ASSERTION_BYTES)
98
+ throw new OxyRequesterAssertionError('malformed');
99
+ const segments = token.split('.');
100
+ if (segments.length !== 3 || segments.some((segment) => segment.length === 0)) {
101
+ throw new OxyRequesterAssertionError('malformed');
102
+ }
103
+ const header = decodeObject(segments[0]);
104
+ if (Object.keys(header).length !== 3
105
+ || header.alg !== ALGORITHM
106
+ || header.typ !== exports.OXY_REQUESTER_ASSERTION_TYPE
107
+ || typeof header.kid !== 'string'
108
+ || !KEY_ID_PATTERN.test(header.kid)) {
109
+ throw new OxyRequesterAssertionError('malformed');
110
+ }
111
+ return header.kid;
112
+ }
113
+ /**
114
+ * Stateless verification: header, signature, claim shape, issuer, audience and
115
+ * lifetime. It does NOT prove the assertion is unused or that the requester's
116
+ * session is still live — only introspection does.
117
+ */
118
+ function verifyOxyRequesterAssertion(token, options) {
119
+ readOxyRequesterAssertionKeyId(token);
120
+ const [encodedHeader, encodedPayload, encodedSignature] = token.split('.');
121
+ const publicKey = options.publicKey;
122
+ if (!publicKey || publicKey.asymmetricKeyType !== 'ed25519') {
123
+ throw new OxyRequesterAssertionError('unknown_key');
124
+ }
125
+ if (!BASE64URL_PATTERN.test(encodedSignature))
126
+ throw new OxyRequesterAssertionError('malformed');
127
+ const signature = Buffer.from(encodedSignature, 'base64url');
128
+ if (signature.length !== 64 || signature.toString('base64url') !== encodedSignature) {
129
+ throw new OxyRequesterAssertionError('malformed');
130
+ }
131
+ if (!(0, node_crypto_1.verify)(null, Buffer.from(`${encodedHeader}.${encodedPayload}`), publicKey, signature)) {
132
+ throw new OxyRequesterAssertionError('invalid_signature');
133
+ }
134
+ const parsed = exports.oxyRequesterAssertionClaimsSchema.safeParse(decodeObject(encodedPayload));
135
+ if (!parsed.success)
136
+ throw new OxyRequesterAssertionError('invalid_claims');
137
+ const claims = parsed.data;
138
+ const now = Math.floor((options.now ?? new Date()).getTime() / 1000);
139
+ if (claims.iss !== options.issuer)
140
+ throw new OxyRequesterAssertionError('wrong_issuer');
141
+ if (claims.aud !== options.audience)
142
+ throw new OxyRequesterAssertionError('wrong_audience');
143
+ if (claims.iat > now + CLOCK_SKEW_SECONDS)
144
+ throw new OxyRequesterAssertionError('not_yet_valid');
145
+ if (claims.exp <= now)
146
+ throw new OxyRequesterAssertionError('expired');
147
+ if (claims.exp <= claims.iat || claims.exp - claims.iat > exports.OXY_REQUESTER_ASSERTION_MAX_TTL_SECONDS) {
148
+ throw new OxyRequesterAssertionError('ttl_exceeded');
149
+ }
150
+ return claims;
151
+ }
152
+ // ---------------------------------------------------------------------------
153
+ // JWKS
154
+ // ---------------------------------------------------------------------------
155
+ const JWKS_CACHE_MS = 5 * 60 * 1000;
156
+ const JWKS_REFRESH_FLOOR_MS = 60 * 1000;
157
+ const JWKS_FETCH_TIMEOUT_MS = 5000;
158
+ const JWKS_MAX_BYTES = 64 * 1024;
159
+ const JWKS_MAX_KEYS = 20;
160
+ function parseJwks(value) {
161
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
162
+ throw new Error('JWKS is malformed');
163
+ const keys = value.keys;
164
+ if (!Array.isArray(keys) || keys.length === 0 || keys.length > JWKS_MAX_KEYS)
165
+ throw new Error('JWKS has an invalid key set');
166
+ const result = new Map();
167
+ for (const entry of keys) {
168
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
169
+ throw new Error('JWKS key is malformed');
170
+ const key = entry;
171
+ if (key.kty !== 'OKP'
172
+ || key.crv !== 'Ed25519'
173
+ || key.alg !== 'EdDSA'
174
+ || key.use !== 'sig'
175
+ || typeof key.x !== 'string'
176
+ || typeof key.kid !== 'string'
177
+ || !KEY_ID_PATTERN.test(key.kid)
178
+ || Object.prototype.hasOwnProperty.call(key, 'd')
179
+ || result.has(key.kid))
180
+ throw new Error('JWKS key is unsupported');
181
+ const x = Buffer.from(key.x, 'base64url');
182
+ if (x.length !== 32 || x.toString('base64url') !== key.x)
183
+ throw new Error('JWKS key is not Ed25519');
184
+ result.set(key.kid, (0, node_crypto_1.createPublicKey)({ key: { kty: 'OKP', crv: 'Ed25519', x: key.x }, format: 'jwk' }));
185
+ }
186
+ return result;
187
+ }
188
+ /**
189
+ * Cached Ed25519 JWKS resolver. An unknown `kid` refreshes at most once a
190
+ * minute, so a flood of forged key ids cannot turn into a flood of fetches. A
191
+ * failed refresh keeps serving keys that are still inside their cache window.
192
+ */
193
+ function createOxyJwksKeyResolver(options) {
194
+ const fetchImpl = options.fetch ?? fetch;
195
+ const clock = options.clock ?? Date.now;
196
+ let keys = new Map();
197
+ let expiresAt = 0;
198
+ let lastAttemptAt = Number.NEGATIVE_INFINITY;
199
+ let pending;
200
+ const refresh = async () => {
201
+ if (pending)
202
+ return pending;
203
+ lastAttemptAt = clock();
204
+ pending = (async () => {
205
+ const controller = new AbortController();
206
+ const timer = setTimeout(() => controller.abort(), JWKS_FETCH_TIMEOUT_MS);
207
+ try {
208
+ const response = await fetchImpl(options.jwksUrl, {
209
+ method: 'GET',
210
+ headers: { accept: 'application/json' },
211
+ redirect: 'error',
212
+ signal: controller.signal,
213
+ });
214
+ if (!response.ok)
215
+ throw new Error(`JWKS returned HTTP ${response.status}`);
216
+ const body = await response.text();
217
+ if (Buffer.byteLength(body, 'utf8') > JWKS_MAX_BYTES)
218
+ throw new Error('JWKS exceeds the size limit');
219
+ keys = parseJwks(JSON.parse(body));
220
+ expiresAt = clock() + JWKS_CACHE_MS;
221
+ }
222
+ finally {
223
+ clearTimeout(timer);
224
+ }
225
+ })();
226
+ try {
227
+ await pending;
228
+ }
229
+ finally {
230
+ pending = undefined;
231
+ }
232
+ };
233
+ return async (keyId) => {
234
+ const now = clock();
235
+ const cached = keys.get(keyId);
236
+ if (cached && expiresAt > now)
237
+ return cached;
238
+ if (expiresAt <= now || now - lastAttemptAt >= JWKS_REFRESH_FLOOR_MS) {
239
+ try {
240
+ await refresh();
241
+ }
242
+ catch {
243
+ // Fall through: a still-valid cached key is served, anything else is unknown.
244
+ }
245
+ }
246
+ return expiresAt > clock() ? keys.get(keyId) : undefined;
247
+ };
248
+ }
249
+ function reject(req, res, options, status, error, code) {
250
+ options.onRejected?.({
251
+ code,
252
+ status,
253
+ applicationId: req.serviceApp?.appId ?? null,
254
+ });
255
+ res.status(status).json({ error, code, message: 'The requester assertion was not accepted', status });
256
+ }
257
+ function singleHeader(req, name) {
258
+ const value = req.headers[name];
259
+ if (value === undefined)
260
+ return null;
261
+ if (typeof value !== 'string' || value.length === 0 || value.length > MAX_ASSERTION_BYTES)
262
+ return 'invalid';
263
+ return value;
264
+ }
265
+ /**
266
+ * Accepts an `X-Oxy-Requester-Assertion` presented beside a VERIFIED service
267
+ * token. Mount it after `createOxyAuthMiddleware`. Without the header it does
268
+ * nothing, so it composes with every other lane.
269
+ *
270
+ * `introspector` must be an `OxyServices` configured with THIS service's own
271
+ * credential (`configureServiceAuth`): Oxy only lets the audience's application
272
+ * consume an assertion.
273
+ */
274
+ function createOxyRequesterAssertionAuth(introspector, options) {
275
+ const issuer = options.issuer ?? exports.OXY_REQUESTER_ASSERTION_DEFAULT_ISSUER;
276
+ let resolvePublicKey = options.resolvePublicKey;
277
+ const resolver = () => {
278
+ resolvePublicKey ?? (resolvePublicKey = createOxyJwksKeyResolver({
279
+ jwksUrl: options.jwksUrl
280
+ ?? new URL('/capabilities/.well-known/jwks.json', introspector.getBaseURL()).toString(),
281
+ }));
282
+ return resolvePublicKey;
283
+ };
284
+ return async (req, res, next) => {
285
+ const assertion = singleHeader(req, exports.OXY_REQUESTER_ASSERTION_HEADER);
286
+ if (assertion === null) {
287
+ next();
288
+ return;
289
+ }
290
+ if (assertion === 'invalid') {
291
+ reject(req, res, options, 401, 'REQUESTER_ASSERTION_INVALID', 'malformed');
292
+ return;
293
+ }
294
+ const request = req;
295
+ const serviceApp = request.serviceApp;
296
+ if (!serviceApp) {
297
+ reject(req, res, options, 401, 'REQUESTER_ASSERTION_REQUIRES_SERVICE_TOKEN', 'service_token_required');
298
+ return;
299
+ }
300
+ // One identity channel per request. An offline delegation header or an
301
+ // identity some earlier middleware attached must never be combined with, or
302
+ // silently replaced by, a present-requester assertion.
303
+ if (req.headers['x-oxy-user-id'] !== undefined
304
+ || request.serviceActingAs !== undefined
305
+ || (request.userId !== undefined && request.userId !== null)) {
306
+ reject(req, res, options, 400, 'REQUESTER_ASSERTION_CONFLICT', 'identity_conflict');
307
+ return;
308
+ }
309
+ let claims;
310
+ try {
311
+ const keyId = readOxyRequesterAssertionKeyId(assertion);
312
+ claims = verifyOxyRequesterAssertion(assertion, {
313
+ publicKey: await resolver()(keyId),
314
+ issuer,
315
+ audience: options.audience,
316
+ ...(options.now ? { now: options.now() } : {}),
317
+ });
318
+ }
319
+ catch (error) {
320
+ const code = error instanceof OxyRequesterAssertionError ? error.code : 'invalid_claims';
321
+ reject(req, res, options, 401, 'REQUESTER_ASSERTION_INVALID', code);
322
+ return;
323
+ }
324
+ // Bound to its presenter: a copy in anyone else's hands proves nothing,
325
+ // and checking before introspection means it cannot even be spent.
326
+ if (claims.azp !== serviceApp.appId || claims.cid !== serviceApp.credentialId) {
327
+ reject(req, res, options, 401, 'REQUESTER_ASSERTION_INVALID', 'presenter_mismatch');
328
+ return;
329
+ }
330
+ let introspection;
331
+ try {
332
+ introspection = await introspector.introspectRequesterAssertion({
333
+ assertion,
334
+ presenter: { applicationId: serviceApp.appId, credentialId: serviceApp.credentialId },
335
+ });
336
+ }
337
+ catch {
338
+ reject(req, res, options, 503, 'REQUESTER_ASSERTION_UNAVAILABLE', 'introspection_unavailable');
339
+ return;
340
+ }
341
+ if (introspection.active !== true
342
+ || introspection.requesterAccountId !== claims.sub
343
+ || introspection.agentId !== claims.agentId
344
+ || introspection.applicationId !== claims.azp
345
+ || introspection.credentialId !== claims.cid
346
+ || introspection.jti !== claims.jti) {
347
+ reject(req, res, options, 401, 'REQUESTER_ASSERTION_INACTIVE', 'inactive');
348
+ return;
349
+ }
350
+ request.userId = claims.sub;
351
+ request.user = { id: claims.sub };
352
+ request.oxyRequester = {
353
+ userId: claims.sub,
354
+ agentId: claims.agentId,
355
+ applicationId: claims.azp,
356
+ credentialId: claims.cid,
357
+ jti: claims.jti,
358
+ expiresAt: new Date(claims.exp * 1000).toISOString(),
359
+ };
360
+ next();
361
+ };
362
+ }