@spine-event-engine/auth 2.0.0-snapshot.2

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 (57) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +112 -0
  3. package/REFERENCE.md +94 -0
  4. package/dist/gateway/dynamic-subscription-creator.d.ts +52 -0
  5. package/dist/gateway/dynamic-subscription-creator.d.ts.map +1 -0
  6. package/dist/gateway/dynamic-subscription-creator.js +71 -0
  7. package/dist/gateway/dynamic-subscription-creator.js.map +1 -0
  8. package/dist/gateway/dynamic-unary-forwarder.d.ts +130 -0
  9. package/dist/gateway/dynamic-unary-forwarder.d.ts.map +1 -0
  10. package/dist/gateway/dynamic-unary-forwarder.js +185 -0
  11. package/dist/gateway/dynamic-unary-forwarder.js.map +1 -0
  12. package/dist/gateway/index.d.ts +164 -0
  13. package/dist/gateway/index.d.ts.map +1 -0
  14. package/dist/gateway/index.js +195 -0
  15. package/dist/gateway/index.js.map +1 -0
  16. package/dist/index.d.ts +433 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +56 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/native/index.d.ts +182 -0
  21. package/dist/native/index.d.ts.map +1 -0
  22. package/dist/native/index.js +463 -0
  23. package/dist/native/index.js.map +1 -0
  24. package/dist/oidc/contracts.d.ts +334 -0
  25. package/dist/oidc/contracts.d.ts.map +1 -0
  26. package/dist/oidc/contracts.js +15 -0
  27. package/dist/oidc/contracts.js.map +1 -0
  28. package/dist/oidc/index.d.ts +41 -0
  29. package/dist/oidc/index.d.ts.map +1 -0
  30. package/dist/oidc/index.js +825 -0
  31. package/dist/oidc/index.js.map +1 -0
  32. package/dist/providers/index.d.ts +154 -0
  33. package/dist/providers/index.d.ts.map +1 -0
  34. package/dist/providers/index.js +574 -0
  35. package/dist/providers/index.js.map +1 -0
  36. package/dist/request/index.d.ts +10 -0
  37. package/dist/request/index.d.ts.map +1 -0
  38. package/dist/request/index.js +81 -0
  39. package/dist/request/index.js.map +1 -0
  40. package/dist/sessions/cookies.d.ts +104 -0
  41. package/dist/sessions/cookies.d.ts.map +1 -0
  42. package/dist/sessions/cookies.js +295 -0
  43. package/dist/sessions/cookies.js.map +1 -0
  44. package/dist/sessions/opaque.d.ts +163 -0
  45. package/dist/sessions/opaque.d.ts.map +1 -0
  46. package/dist/sessions/opaque.js +268 -0
  47. package/dist/sessions/opaque.js.map +1 -0
  48. package/dist/sessions/signed.d.ts +245 -0
  49. package/dist/sessions/signed.d.ts.map +1 -0
  50. package/dist/sessions/signed.js +534 -0
  51. package/dist/sessions/signed.js.map +1 -0
  52. package/dist/subscriptions/index.d.ts +463 -0
  53. package/dist/subscriptions/index.d.ts.map +1 -0
  54. package/dist/subscriptions/index.js +814 -0
  55. package/dist/subscriptions/index.js.map +1 -0
  56. package/dist/tsconfig.tsbuildinfo +1 -0
  57. package/package.json +37 -0
@@ -0,0 +1,825 @@
1
+ /*
2
+ * Copyright 2026, CodeMatters. All rights reserved.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5
+ * in compliance with the License. You may obtain a copy of the License at
6
+ *
7
+ * https://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
10
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11
+ * or implied. See the License for the specific language governing permissions and limitations under
12
+ * the License.
13
+ */
14
+ import { createHash, randomBytes as nodeRandomBytes, timingSafeEqual } from "node:crypto";
15
+ import { create } from "@bufbuild/protobuf";
16
+ import { TimestampSchema } from "@bufbuild/protobuf/wkt";
17
+ const RANDOM_BYTES = 32;
18
+ const DEFAULT_TRANSACTION_TTL = 5 * 60 * 1_000;
19
+ const DEFAULT_GRANT_TTL = 60 * 1_000;
20
+ const DEFAULT_CAPACITY = 1_000;
21
+ const DEFAULT_COLLISION_ATTEMPTS = 3;
22
+ const DEFAULT_TIMEOUT = 30 * 1_000;
23
+ const DEFAULT_MAX_URL = 4_096;
24
+ const MAX_TIMESTAMP_MILLISECONDS = 253_402_300_799_999;
25
+ const base64Url32 = /^[A-Za-z0-9_-]{43}$/;
26
+ const RFC7636_VERIFIER = /^[A-Za-z0-9\-._~]{43,128}$/;
27
+ /**
28
+ * Bounded, framework-neutral authorization-code transaction manager.
29
+ *
30
+ * It owns finite start, callback, and one-time application-session exchange
31
+ * transitions; HTTP endpoints and provider discovery remain application work.
32
+ */
33
+ export class OidcFlow {
34
+ #authorizationEndpoint;
35
+ #callbackUri;
36
+ #clientId;
37
+ #scopes;
38
+ #allowedPostLoginRedirects;
39
+ #provider;
40
+ #providerIssuer;
41
+ #identityMapping;
42
+ #sessionIssuer;
43
+ #clock;
44
+ #random;
45
+ #transactionTtlMilliseconds;
46
+ #grantTtlMilliseconds;
47
+ #maxTransactions;
48
+ #maxGrants;
49
+ #collisionAttempts;
50
+ #operationTimeoutMilliseconds;
51
+ #maxAuthorizationUrlLength;
52
+ #transactions = new Map();
53
+ #grants = new Map();
54
+ #callbacks = new Set();
55
+ #closed = false;
56
+ /**
57
+ * Creates a bounded authorization-code flow.
58
+ * @param options The trusted provider, callback, transaction, grant, and session settings.
59
+ */
60
+ constructor(options) {
61
+ this.#authorizationEndpoint = OidcFlowValues.strictHttpsUrl(options.authorizationEndpoint, "authorizationEndpoint");
62
+ OidcFlowValues.strictHttpsUrl(options.callbackUri, "callbackUri");
63
+ this.#callbackUri = options.callbackUri;
64
+ this.#clientId = OidcFlowValues.nonEmpty(options.clientId, "clientId");
65
+ this.#scopes = OidcFlowValues.validScopes(options.scopes);
66
+ this.#allowedPostLoginRedirects = OidcFlowValues.validRedirects(options.allowedPostLoginRedirects);
67
+ validateProvider(options.provider);
68
+ OidcFlowValues.validateFunction(options.identityMapping.resolve, "identityMapping.resolve");
69
+ OidcFlowValues.validateFunction(options.sessionIssuer.issue, "sessionIssuer.issue");
70
+ this.#provider = options.provider;
71
+ this.#providerIssuer = options.provider.issuer;
72
+ this.#identityMapping = options.identityMapping;
73
+ this.#sessionIssuer = options.sessionIssuer;
74
+ this.#clock = options.clock ?? { now: Date.now };
75
+ this.#random = options.randomBytes ?? nodeRandomBytes;
76
+ this.#transactionTtlMilliseconds = OidcFlowValues.positiveSafeInteger(options.transactionTtlMilliseconds ?? DEFAULT_TRANSACTION_TTL, "transactionTtlMilliseconds");
77
+ this.#grantTtlMilliseconds = OidcFlowValues.positiveSafeInteger(options.grantTtlMilliseconds ?? DEFAULT_GRANT_TTL, "grantTtlMilliseconds");
78
+ this.#maxTransactions = OidcFlowValues.positiveSafeInteger(options.maxTransactions ?? DEFAULT_CAPACITY, "maxTransactions");
79
+ this.#maxGrants = OidcFlowValues.positiveSafeInteger(options.maxGrants ?? DEFAULT_CAPACITY, "maxGrants");
80
+ this.#collisionAttempts = OidcFlowValues.positiveSafeInteger(options.collisionAttempts ?? DEFAULT_COLLISION_ATTEMPTS, "collisionAttempts");
81
+ this.#operationTimeoutMilliseconds = OidcFlowValues.positiveSafeInteger(options.operationTimeoutMilliseconds ?? DEFAULT_TIMEOUT, "operationTimeoutMilliseconds");
82
+ this.#maxAuthorizationUrlLength = OidcFlowValues.positiveSafeInteger(options.maxAuthorizationUrlLength ?? DEFAULT_MAX_URL, "maxAuthorizationUrlLength");
83
+ }
84
+ /**
85
+ * Starts one finite authorization-code transaction without exposing any application credential.
86
+ * @param input The browser redirect and requested scopes.
87
+ * @returns The authorization URL and state, or a rejection.
88
+ */
89
+ start(input) {
90
+ if (this.#isClosed())
91
+ return OidcFlowValues.rejected("closed");
92
+ const request = OidcFlowValues.snapshotStartInput(input);
93
+ if (request === undefined ||
94
+ !OidcFlowValues.validStartInput(request, this.#allowedPostLoginRedirects))
95
+ return OidcFlowValues.rejected("invalid-input");
96
+ const now = this.#now();
97
+ if (now === undefined)
98
+ return OidcFlowValues.rejected("closed");
99
+ if (this.#closed)
100
+ return OidcFlowValues.rejected("closed");
101
+ this.#sweepExpired(now);
102
+ if (this.#transactions.size >= this.#maxTransactions)
103
+ return OidcFlowValues.rejected("capacity-exceeded");
104
+ for (let attempt = 0; attempt < this.#collisionAttempts; attempt += 1) {
105
+ const material = this.#transactionMaterial();
106
+ if (material === undefined) {
107
+ if (this.#isClosed())
108
+ return OidcFlowValues.rejected("closed");
109
+ continue;
110
+ }
111
+ try {
112
+ const current = this.#now();
113
+ if (current === undefined)
114
+ return OidcFlowValues.rejected("closed");
115
+ this.#sweepExpired(current);
116
+ if (this.#isClosed())
117
+ return OidcFlowValues.rejected("closed");
118
+ if (this.#transactions.size >= this.#maxTransactions)
119
+ return OidcFlowValues.rejected("capacity-exceeded");
120
+ if (this.#transactions.has(material.state) || this.#transactionMaterialInUse(material))
121
+ continue;
122
+ const transactionExpiry = OidcFlowValues.expiryAt(current, this.#transactionTtlMilliseconds);
123
+ if (transactionExpiry === undefined) {
124
+ this.#failClosed();
125
+ return OidcFlowValues.rejected("clock-failure");
126
+ }
127
+ const authorizationUrl = this.#authorizationUrl(material.state, material.nonce, Buffer.from(material.providerVerifierBytes).toString("base64url"), request.browserCodeChallenge);
128
+ if (authorizationUrl === undefined)
129
+ return OidcFlowValues.rejected("invalid-input");
130
+ this.#transactions.set(material.state, {
131
+ nonce: material.nonce,
132
+ providerVerifier: Uint8Array.from(material.providerVerifierBytes),
133
+ browserCodeChallenge: request.browserCodeChallenge,
134
+ postLoginRedirect: request.postLoginRedirect,
135
+ expiresAt: transactionExpiry,
136
+ });
137
+ return Object.freeze({ kind: "started", authorizationUrl, expiresAt: transactionExpiry });
138
+ }
139
+ finally {
140
+ material.bytes.forEach((bytes) => bytes.fill(0));
141
+ }
142
+ }
143
+ return OidcFlowValues.rejected("entropy-exhausted");
144
+ }
145
+ /**
146
+ * Processes a callback state before provider verification or mapping.
147
+ * A rejected callback never restores its transaction.
148
+ * @param input The callback state and provider response.
149
+ * @returns The callback result, including a one-time grant when accepted.
150
+ */
151
+ async callback(input) {
152
+ if (this.#isClosed())
153
+ return OidcFlowValues.callbackRejected("closed");
154
+ const state = OidcFlowValues.snapshotCallbackState(input);
155
+ if (state === undefined)
156
+ return OidcFlowValues.callbackRejected("invalid-input");
157
+ const now = this.#now();
158
+ if (now === undefined)
159
+ return OidcFlowValues.callbackRejected("closed");
160
+ if (this.#isClosed())
161
+ return OidcFlowValues.callbackRejected("closed");
162
+ const transaction = this.#transactions.get(state);
163
+ if (transaction === undefined)
164
+ return OidcFlowValues.callbackRejected("not-found");
165
+ this.#transactions.delete(state);
166
+ const callback = OidcFlowValues.snapshotCallbackInput(input);
167
+ if (callback === undefined || !OidcFlowValues.validCallbackInput(callback)) {
168
+ transaction.providerVerifier.fill(0);
169
+ return OidcFlowValues.callbackRejected("invalid-input");
170
+ }
171
+ if (now >= transaction.expiresAt) {
172
+ transaction.providerVerifier.fill(0);
173
+ return OidcFlowValues.callbackRejected("expired");
174
+ }
175
+ if (callback.error !== undefined) {
176
+ transaction.providerVerifier.fill(0);
177
+ return OidcFlowValues.callbackRejected("provider-error");
178
+ }
179
+ if (callback.responseIssuer !== undefined && callback.responseIssuer !== this.#providerIssuer) {
180
+ transaction.providerVerifier.fill(0);
181
+ return OidcFlowValues.callbackRejected("issuer-mismatch");
182
+ }
183
+ const providerVerifier = Buffer.from(transaction.providerVerifier).toString("base64url");
184
+ transaction.providerVerifier.fill(0);
185
+ const identity = await this.#runBounded((signal) => this.#provider.exchangeAuthorizationCode({
186
+ code: callback.code,
187
+ clientId: this.#clientId,
188
+ callbackUri: this.#callbackUri,
189
+ providerCodeVerifier: providerVerifier,
190
+ expectedNonce: transaction.nonce,
191
+ signal,
192
+ }));
193
+ if (this.#isClosed())
194
+ return OidcFlowValues.callbackRejected("closed");
195
+ let verified;
196
+ try {
197
+ verified = OidcFlowValues.validExternalIdentity(identity, this.#providerIssuer);
198
+ }
199
+ catch {
200
+ verified = undefined;
201
+ }
202
+ if (verified === undefined)
203
+ return OidcFlowValues.callbackRejected("verification-failed");
204
+ const resolved = await this.#runBounded((signal) => this.#identityMapping.resolve(verified, signal));
205
+ if (this.#isClosed())
206
+ return OidcFlowValues.callbackRejected("closed");
207
+ const mapped = OidcFlowValues.snapshotResolvedIdentity(resolved, verified);
208
+ if (mapped === undefined)
209
+ return OidcFlowValues.callbackRejected("mapping-failed");
210
+ const current = this.#now();
211
+ if (current === undefined)
212
+ return OidcFlowValues.callbackRejected("closed");
213
+ this.#sweepGrants(current);
214
+ if (this.#isClosed())
215
+ return OidcFlowValues.callbackRejected("closed");
216
+ if (this.#grants.size >= this.#maxGrants)
217
+ return OidcFlowValues.callbackRejected("capacity-exceeded");
218
+ const expiry = OidcFlowValues.expiryAt(current, this.#grantTtlMilliseconds);
219
+ if (expiry === undefined) {
220
+ this.#failClosed();
221
+ return OidcFlowValues.callbackRejected("clock-failure");
222
+ }
223
+ const grant = this.#nextGrant();
224
+ if (grant === undefined)
225
+ return OidcFlowValues.callbackRejected(this.#isClosed() ? "closed" : "entropy-exhausted");
226
+ if (this.#isClosed() || this.#grants.size >= this.#maxGrants || this.#grants.has(grant))
227
+ return OidcFlowValues.callbackRejected(this.#isClosed() ? "closed" : "entropy-exhausted");
228
+ this.#grants.set(grant, {
229
+ identity: mapped,
230
+ browserCodeChallenge: transaction.browserCodeChallenge,
231
+ postLoginRedirect: transaction.postLoginRedirect,
232
+ expiresAt: expiry,
233
+ });
234
+ return Object.freeze({
235
+ kind: "granted",
236
+ grant,
237
+ postLoginRedirect: transaction.postLoginRedirect,
238
+ expiresAt: expiry,
239
+ });
240
+ }
241
+ /**
242
+ * Processes a grant before validating browser PKCE proof or issuing a session.
243
+ * All failures deliberately have the same result to avoid grant-state enumeration.
244
+ * @param input The grant and browser PKCE proof.
245
+ * @returns The issued application session or an enumeration-safe rejection.
246
+ */
247
+ async exchange(input) {
248
+ const grantRequest = OidcFlowValues.snapshotGrantExchangeInput(input);
249
+ if (this.#isClosed() ||
250
+ grantRequest === undefined ||
251
+ !OidcFlowValues.validGrant(grantRequest.grant))
252
+ return OidcFlowValues.exchangeRejected();
253
+ const now = this.#now();
254
+ if (now === undefined)
255
+ return OidcFlowValues.exchangeRejected();
256
+ const grant = this.#grants.get(grantRequest.grant);
257
+ if (grant === undefined)
258
+ return OidcFlowValues.exchangeRejected();
259
+ this.#grants.delete(grantRequest.grant);
260
+ const browserCodeVerifier = OidcFlowValues.snapshotBrowserCodeVerifier(input);
261
+ if (now >= grant.expiresAt ||
262
+ browserCodeVerifier === undefined ||
263
+ !OidcFlowValues.validBrowserCodeVerifier(browserCodeVerifier))
264
+ return OidcFlowValues.exchangeRejected();
265
+ if (!OidcFlowValues.constantTimeEquals(OidcFlowValues.sha256Base64Url(browserCodeVerifier), grant.browserCodeChallenge))
266
+ return OidcFlowValues.exchangeRejected();
267
+ const issued = await this.#runBounded((signal) => this.#sessionIssuer.issue(grant.identity.principal, signal));
268
+ const safeIssue = OidcFlowValues.snapshotSessionIssue(issued);
269
+ if (this.#isClosed() || safeIssue === undefined)
270
+ return OidcFlowValues.exchangeRejected();
271
+ return Object.freeze({
272
+ kind: "issued",
273
+ credential: safeIssue.credential,
274
+ session: safeIssue.session,
275
+ });
276
+ }
277
+ /**
278
+ * Closes the flow and discards retained OIDC transaction material.
279
+ */
280
+ close() {
281
+ if (this.#closed)
282
+ return;
283
+ this.#closed = true;
284
+ this.#clearTransactions();
285
+ this.#grants.clear();
286
+ this.#callbacks.forEach((controller) => {
287
+ controller.abort();
288
+ });
289
+ this.#callbacks.clear();
290
+ }
291
+ #isClosed() {
292
+ return this.#closed;
293
+ }
294
+ #authorizationUrl(state, nonce, providerVerifier, browserCodeChallenge) {
295
+ // The provider verifier is deliberately not serialized. Its S256 challenge is.
296
+ const url = new URL(this.#authorizationEndpoint);
297
+ const parameters = url.searchParams;
298
+ parameters.set("response_type", "code");
299
+ parameters.set("client_id", this.#clientId);
300
+ parameters.set("redirect_uri", this.#callbackUri);
301
+ parameters.set("scope", this.#scopes.join(" "));
302
+ parameters.set("state", state);
303
+ parameters.set("nonce", nonce);
304
+ parameters.set("code_challenge", OidcFlowValues.sha256Base64Url(providerVerifier));
305
+ parameters.set("code_challenge_method", "S256");
306
+ // The browser challenge is retained for the later, distinct application-session grant.
307
+ void browserCodeChallenge;
308
+ const serialized = url.toString();
309
+ return serialized.length <= this.#maxAuthorizationUrlLength ? serialized : undefined;
310
+ }
311
+ #transactionMaterial() {
312
+ const bytes = [];
313
+ try {
314
+ const stateBytes = this.#random(RANDOM_BYTES);
315
+ bytes.push(stateBytes);
316
+ if (!(stateBytes instanceof Uint8Array) || stateBytes.byteLength !== RANDOM_BYTES)
317
+ return undefined;
318
+ const nonceBytes = this.#random(RANDOM_BYTES);
319
+ bytes.push(nonceBytes);
320
+ if (!(nonceBytes instanceof Uint8Array) || nonceBytes.byteLength !== RANDOM_BYTES)
321
+ return undefined;
322
+ const verifierBytes = this.#random(RANDOM_BYTES);
323
+ bytes.push(verifierBytes);
324
+ if (!(verifierBytes instanceof Uint8Array) || verifierBytes.byteLength !== RANDOM_BYTES)
325
+ return undefined;
326
+ return {
327
+ state: Buffer.from(stateBytes).toString("base64url"),
328
+ nonce: Buffer.from(nonceBytes).toString("base64url"),
329
+ providerVerifierBytes: verifierBytes,
330
+ bytes,
331
+ };
332
+ }
333
+ catch {
334
+ return undefined;
335
+ }
336
+ finally {
337
+ if (bytes.length !== 3)
338
+ bytes.forEach((value) => value.fill(0));
339
+ }
340
+ }
341
+ async #runBounded(operation) {
342
+ const controller = new AbortController();
343
+ this.#callbacks.add(controller);
344
+ let timer;
345
+ try {
346
+ const aborted = new Promise((resolve) => {
347
+ controller.signal.addEventListener("abort", () => {
348
+ resolve(undefined);
349
+ }, { once: true });
350
+ });
351
+ const timeout = new Promise((resolve) => {
352
+ timer = setTimeout(() => {
353
+ controller.abort();
354
+ resolve(undefined);
355
+ }, this.#operationTimeoutMilliseconds);
356
+ });
357
+ return await Promise.race([operation(controller.signal), timeout, aborted]);
358
+ }
359
+ catch {
360
+ return undefined;
361
+ }
362
+ finally {
363
+ if (timer !== undefined)
364
+ clearTimeout(timer);
365
+ this.#callbacks.delete(controller);
366
+ controller.abort();
367
+ }
368
+ }
369
+ #nextGrant() {
370
+ for (let attempt = 0; attempt < this.#collisionAttempts; attempt += 1) {
371
+ let bytes;
372
+ try {
373
+ bytes = this.#random(RANDOM_BYTES);
374
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength !== RANDOM_BYTES)
375
+ continue;
376
+ const grant = Buffer.from(bytes).toString("base64url");
377
+ if (!this.#grants.has(grant))
378
+ return grant;
379
+ }
380
+ catch {
381
+ // A bounded retry handles entropy faults without retaining partial material.
382
+ }
383
+ finally {
384
+ bytes?.fill(0);
385
+ }
386
+ }
387
+ return undefined;
388
+ }
389
+ #transactionMaterialInUse(material) {
390
+ const verifier = Buffer.from(material.providerVerifierBytes).toString("base64url");
391
+ const challenge = OidcFlowValues.sha256Base64Url(verifier);
392
+ return [...this.#transactions.values()].some((transaction) => transaction.nonce === material.nonce ||
393
+ OidcFlowValues.sha256Base64Url(Buffer.from(transaction.providerVerifier).toString("base64url")) === challenge);
394
+ }
395
+ #now() {
396
+ try {
397
+ const now = this.#clock.now();
398
+ if (!OidcFlowValues.validTimestamp(now))
399
+ throw new Error("invalid clock");
400
+ return now;
401
+ }
402
+ catch {
403
+ this.#failClosed();
404
+ return undefined;
405
+ }
406
+ }
407
+ #sweepExpired(now) {
408
+ for (const [state, transaction] of this.#transactions)
409
+ if (now >= transaction.expiresAt)
410
+ this.#dropTransaction(state, transaction);
411
+ }
412
+ #sweepGrants(now) {
413
+ for (const [grant, record] of this.#grants)
414
+ if (now >= record.expiresAt)
415
+ this.#grants.delete(grant);
416
+ }
417
+ #dropTransaction(state, transaction) {
418
+ this.#transactions.delete(state);
419
+ transaction.providerVerifier.fill(0);
420
+ }
421
+ #clearTransactions() {
422
+ this.#transactions.forEach((transaction) => transaction.providerVerifier.fill(0));
423
+ this.#transactions.clear();
424
+ }
425
+ #failClosed() {
426
+ this.#closed = true;
427
+ this.#clearTransactions();
428
+ this.#grants.clear();
429
+ this.#callbacks.forEach((controller) => {
430
+ controller.abort();
431
+ });
432
+ this.#callbacks.clear();
433
+ }
434
+ }
435
+ const OidcFlowValues = Object.freeze({
436
+ rejected(reason) {
437
+ return Object.freeze({ kind: "rejected", reason });
438
+ },
439
+ callbackRejected(reason) {
440
+ return Object.freeze({ kind: "rejected", reason });
441
+ },
442
+ exchangeRejected() {
443
+ return Object.freeze({ kind: "rejected" });
444
+ },
445
+ strictHttpsUrl(value, name) {
446
+ const url = OidcFlowValues.parseUrl(value, name);
447
+ if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "")
448
+ throw new TypeError(`${name} must be an exact HTTPS URL without credentials or fragment`);
449
+ return url;
450
+ },
451
+ parseUrl(value, name) {
452
+ if (typeof value !== "string" || value.length === 0 || value.length > 4_096)
453
+ throw new TypeError(`${name} must be a bounded non-empty URL`);
454
+ try {
455
+ return new URL(value);
456
+ }
457
+ catch {
458
+ throw new TypeError(`${name} must be a URL`);
459
+ }
460
+ },
461
+ validScopes(scopes) {
462
+ if (!Array.isArray(scopes) || scopes.length === 0 || scopes.length > 64)
463
+ throw new TypeError("scopes must be a non-empty bounded list");
464
+ const copy = scopes.map((scope) => {
465
+ if (typeof scope !== "string" || scope.length === 0 || scope.length > 256 || /\s/.test(scope))
466
+ throw new TypeError("scopes must contain bounded non-empty tokens");
467
+ return scope;
468
+ });
469
+ if (new Set(copy).size !== copy.length || !copy.includes("openid"))
470
+ throw new TypeError("scopes must be unique and include openid");
471
+ return Object.freeze(copy);
472
+ },
473
+ validRedirects(redirects) {
474
+ if (!Array.isArray(redirects) || redirects.length === 0 || redirects.length > 1_000)
475
+ throw new TypeError("allowedPostLoginRedirects must be a non-empty bounded list");
476
+ const copy = redirects.map((redirect) => {
477
+ if (typeof redirect !== "string")
478
+ throw new TypeError("allowedPostLoginRedirects entries must be strings");
479
+ const url = OidcFlowValues.strictHttpsUrl(redirect, "allowedPostLoginRedirects entry");
480
+ return url.toString();
481
+ });
482
+ if (new Set(copy).size !== copy.length)
483
+ throw new TypeError("allowedPostLoginRedirects must be unique");
484
+ return new Set(copy);
485
+ },
486
+ validStartInput(input, redirects) {
487
+ if (typeof input.browserCodeChallenge !== "string" ||
488
+ !base64Url32.test(input.browserCodeChallenge))
489
+ return false;
490
+ if (typeof input.postLoginRedirect !== "string" || input.postLoginRedirect.length > 4_096)
491
+ return false;
492
+ try {
493
+ return redirects.has(OidcFlowValues.strictHttpsUrl(input.postLoginRedirect, "postLoginRedirect").toString());
494
+ }
495
+ catch {
496
+ return false;
497
+ }
498
+ },
499
+ snapshotStartInput(input) {
500
+ try {
501
+ if (!OidcFlowValues.plainRecord(input))
502
+ return undefined;
503
+ return Object.freeze({
504
+ browserCodeChallenge: input.browserCodeChallenge,
505
+ postLoginRedirect: input.postLoginRedirect,
506
+ });
507
+ }
508
+ catch {
509
+ return undefined;
510
+ }
511
+ },
512
+ snapshotCallbackState(input) {
513
+ try {
514
+ if (!OidcFlowValues.plainRecord(input))
515
+ return undefined;
516
+ const state = input.state;
517
+ return OidcFlowValues.validGrant(state) ? state : undefined;
518
+ }
519
+ catch {
520
+ return undefined;
521
+ }
522
+ },
523
+ snapshotCallbackInput(input) {
524
+ try {
525
+ if (!OidcFlowValues.plainRecord(input))
526
+ return undefined;
527
+ return Object.freeze({
528
+ code: input.code,
529
+ error: input.error,
530
+ responseIssuer: input.responseIssuer,
531
+ });
532
+ }
533
+ catch {
534
+ return undefined;
535
+ }
536
+ },
537
+ validCallbackInput(input) {
538
+ const codeValid = typeof input.code === "string" && OidcFlowValues.boundedNonEmpty(input.code);
539
+ const errorValid = typeof input.error === "string" && OidcFlowValues.boundedNonEmpty(input.error);
540
+ if (codeValid === errorValid)
541
+ return false;
542
+ if (input.responseIssuer === undefined)
543
+ return true;
544
+ return (typeof input.responseIssuer === "string" &&
545
+ input.responseIssuer.length <= 4_096 &&
546
+ OidcFlowValues.isStrictHttpsUrl(input.responseIssuer));
547
+ },
548
+ validGrant(value) {
549
+ return typeof value === "string" && base64Url32.test(value);
550
+ },
551
+ snapshotGrantExchangeInput(input) {
552
+ try {
553
+ if (!OidcFlowValues.plainRecord(input))
554
+ return undefined;
555
+ return Object.freeze({ grant: input.grant });
556
+ }
557
+ catch {
558
+ return undefined;
559
+ }
560
+ },
561
+ snapshotBrowserCodeVerifier(input) {
562
+ try {
563
+ if (!OidcFlowValues.plainRecord(input))
564
+ return undefined;
565
+ return input.browserCodeVerifier;
566
+ }
567
+ catch {
568
+ return undefined;
569
+ }
570
+ },
571
+ validBrowserCodeVerifier(value) {
572
+ return typeof value === "string" && RFC7636_VERIFIER.test(value);
573
+ },
574
+ validExternalIdentity(identity, issuer) {
575
+ try {
576
+ if (!OidcFlowValues.plainRecord(identity))
577
+ return undefined;
578
+ const actualIssuer = identity.issuer;
579
+ const subject = identity.subject;
580
+ const claims = identity.claims;
581
+ if (actualIssuer !== issuer || !OidcFlowValues.boundedNonEmpty(subject))
582
+ return undefined;
583
+ const copiedClaims = claims === undefined ? undefined : OidcFlowValues.copyBoundedRecord(claims, true);
584
+ if (claims !== undefined && copiedClaims === undefined)
585
+ return undefined;
586
+ return Object.freeze({
587
+ issuer,
588
+ subject,
589
+ ...(copiedClaims === undefined ? {} : { claims: copiedClaims }),
590
+ });
591
+ }
592
+ catch {
593
+ return undefined;
594
+ }
595
+ },
596
+ snapshotResolvedIdentity(identity, expected) {
597
+ try {
598
+ if (identity === undefined ||
599
+ identity === null ||
600
+ typeof identity !== "object" ||
601
+ Array.isArray(identity))
602
+ return undefined;
603
+ const candidate = identity;
604
+ const externalIdentity = candidate.externalIdentity;
605
+ const principal = candidate.principal;
606
+ if (!OidcFlowValues.plainRecord(externalIdentity) || !OidcFlowValues.plainRecord(principal))
607
+ return undefined;
608
+ const issuer = externalIdentity.issuer;
609
+ const subject = externalIdentity.subject;
610
+ const claims = externalIdentity.claims;
611
+ const id = principal.id;
612
+ const attributes = principal.attributes;
613
+ if (issuer !== expected.issuer ||
614
+ subject !== expected.subject ||
615
+ !OidcFlowValues.boundedNonEmpty(id))
616
+ return undefined;
617
+ const copiedClaims = claims === undefined ? undefined : OidcFlowValues.copyBoundedRecord(claims, true);
618
+ const copiedAttributes = attributes === undefined ? undefined : OidcFlowValues.copyBoundedRecord(attributes, false);
619
+ if ((claims !== undefined && copiedClaims === undefined) ||
620
+ (attributes !== undefined && copiedAttributes === undefined))
621
+ return undefined;
622
+ const mappedClaims = copiedClaims ?? {};
623
+ const expectedClaims = expected.claims ?? {};
624
+ const mappedClaimEntries = Object.entries(mappedClaims);
625
+ if (mappedClaimEntries.length !== Object.keys(expectedClaims).length ||
626
+ mappedClaimEntries.some(([name, value]) => expectedClaims[name] !== value))
627
+ return undefined;
628
+ const snapshot = {
629
+ externalIdentity: expected,
630
+ principal: Object.freeze({
631
+ id,
632
+ ...(copiedAttributes === undefined ? {} : { attributes: copiedAttributes }),
633
+ }),
634
+ };
635
+ return Object.freeze(snapshot);
636
+ }
637
+ catch {
638
+ return undefined;
639
+ }
640
+ },
641
+ copyBoundedRecord(value, rejectTokens) {
642
+ if (!OidcFlowValues.plainRecord(value))
643
+ return undefined;
644
+ const entries = Object.entries(value);
645
+ if (entries.length > 32)
646
+ return undefined;
647
+ let characters = 0;
648
+ const copy = {};
649
+ for (const [name, item] of entries) {
650
+ if (!OidcFlowValues.boundedNonEmpty(name) ||
651
+ !OidcFlowValues.boundedNonEmpty(item) ||
652
+ (rejectTokens && OidcFlowValues.tokenLikeClaim(name)))
653
+ return undefined;
654
+ characters += name.length + item.length;
655
+ if (characters > 4_096)
656
+ return undefined;
657
+ Object.defineProperty(copy, name, {
658
+ value: item,
659
+ enumerable: true,
660
+ configurable: true,
661
+ writable: true,
662
+ });
663
+ }
664
+ return Object.freeze(copy);
665
+ },
666
+ validPrincipal(principal) {
667
+ const candidate = principal;
668
+ if (principal === null ||
669
+ typeof principal !== "object" ||
670
+ !OidcFlowValues.boundedNonEmpty(candidate.id))
671
+ return false;
672
+ const attributes = candidate.attributes;
673
+ if (attributes === undefined)
674
+ return true;
675
+ let characters = 0;
676
+ const entries = Object.entries(attributes);
677
+ if (entries.length > 32)
678
+ return false;
679
+ return entries.every(([name, value]) => {
680
+ if (!OidcFlowValues.boundedNonEmpty(name) || !OidcFlowValues.boundedNonEmpty(value))
681
+ return false;
682
+ characters += name.length + value.length;
683
+ return characters <= 4_096;
684
+ });
685
+ },
686
+ validSessionIssue(issue) {
687
+ if (!OidcFlowValues.plainRecord(issue))
688
+ return false;
689
+ const credential = issue.credential;
690
+ const session = issue.session;
691
+ if (!OidcFlowValues.plainRecord(credential) || !OidcFlowValues.plainRecord(session))
692
+ return false;
693
+ return ((credential.kind === "bearer" || credential.kind === "cookie") &&
694
+ OidcFlowValues.boundedNonEmpty(credential.value) &&
695
+ OidcFlowValues.validPrincipal(session.principal) &&
696
+ OidcFlowValues.validSessionTimestamp(session.expiresAt));
697
+ },
698
+ snapshotSessionIssue(issue) {
699
+ try {
700
+ const rawIssue = issue;
701
+ const credential = rawIssue?.credential;
702
+ const session = rawIssue?.session;
703
+ const principal = session?.principal;
704
+ const expiry = session?.expiresAt;
705
+ const attributes = principal?.attributes;
706
+ if (attributes !== undefined && !OidcFlowValues.plainRecord(attributes))
707
+ return undefined;
708
+ const snapshot = {
709
+ credential: credential === undefined ? undefined : { kind: credential.kind, value: credential.value },
710
+ session: principal === undefined || expiry === undefined
711
+ ? undefined
712
+ : {
713
+ principal: {
714
+ id: principal.id,
715
+ attributes: attributes === undefined ? undefined : { ...attributes },
716
+ },
717
+ expiresAt: { seconds: expiry.seconds, nanos: expiry.nanos },
718
+ },
719
+ };
720
+ if (!OidcFlowValues.validSessionIssue(snapshot))
721
+ return undefined;
722
+ return Object.freeze({
723
+ credential: Object.freeze(snapshot.credential),
724
+ session: OidcFlowValues.copyResolvedSession(snapshot.session),
725
+ });
726
+ }
727
+ catch {
728
+ return undefined;
729
+ }
730
+ },
731
+ validSessionTimestamp(value) {
732
+ if (!OidcFlowValues.plainRecord(value))
733
+ return false;
734
+ return (typeof value.seconds === "bigint" &&
735
+ value.seconds >= -62135596800n &&
736
+ value.seconds <= 253402300799n &&
737
+ typeof value.nanos === "number" &&
738
+ Number.isSafeInteger(value.nanos) &&
739
+ value.nanos >= 0 &&
740
+ value.nanos < 1_000_000_000);
741
+ },
742
+ copyResolvedSession(session) {
743
+ const attributes = session.principal.attributes;
744
+ const principal = Object.freeze({
745
+ id: session.principal.id,
746
+ ...(attributes === undefined ? {} : { attributes: Object.freeze({ ...attributes }) }),
747
+ });
748
+ return Object.freeze({
749
+ principal,
750
+ expiresAt: create(TimestampSchema, {
751
+ seconds: session.expiresAt.seconds,
752
+ nanos: session.expiresAt.nanos,
753
+ }),
754
+ });
755
+ },
756
+ boundedNonEmpty(value) {
757
+ return typeof value === "string" && value.length > 0 && value.length <= 4_096;
758
+ },
759
+ plainRecord(value) {
760
+ if (value === null || typeof value !== "object" || Array.isArray(value))
761
+ return false;
762
+ const prototype = Reflect.getPrototypeOf(value);
763
+ return prototype === Object.prototype || prototype === null;
764
+ },
765
+ tokenLikeClaim(name) {
766
+ return /(^|[_-])(access[_-]?token|refresh[_-]?token|id[_-]?token|token)([_-]|$)/iu.test(name);
767
+ },
768
+ isStrictHttpsUrl(value) {
769
+ try {
770
+ OidcFlowValues.strictHttpsUrl(value, "issuer");
771
+ return true;
772
+ }
773
+ catch {
774
+ return false;
775
+ }
776
+ },
777
+ nonEmpty(value, name) {
778
+ if (typeof value !== "string" || value.length === 0 || value.length > 4_096)
779
+ throw new TypeError(`${name} must be a bounded non-empty string`);
780
+ return value;
781
+ },
782
+ validateProvider(provider) {
783
+ if (!OidcFlowValues.plainRecord(provider))
784
+ throw new TypeError("provider is required");
785
+ if (typeof provider.issuer !== "string")
786
+ throw new TypeError("provider.issuer is required");
787
+ OidcFlowValues.strictHttpsUrl(provider.issuer, "provider.issuer");
788
+ OidcFlowValues.validateFunction(provider.exchangeAuthorizationCode, "provider.exchangeAuthorizationCode");
789
+ },
790
+ validateFunction(value, name) {
791
+ if (typeof value !== "function")
792
+ throw new TypeError(`${name} must be a function`);
793
+ },
794
+ positiveSafeInteger(value, name) {
795
+ if (!Number.isSafeInteger(value) || value <= 0)
796
+ throw new TypeError(`${name} must be a positive safe integer`);
797
+ return value;
798
+ },
799
+ validTimestamp(value) {
800
+ return (Number.isSafeInteger(value) &&
801
+ value >= -62_135_596_800_000 &&
802
+ value <= MAX_TIMESTAMP_MILLISECONDS);
803
+ },
804
+ expiryAt(now, ttl) {
805
+ const value = now + ttl;
806
+ return OidcFlowValues.validTimestamp(value) ? value : undefined;
807
+ },
808
+ sha256Base64Url(value) {
809
+ // Node's synchronous hash keeps start() atomic and avoids retaining the verifier buffer.
810
+ return createHash("sha256").update(value, "ascii").digest("base64url");
811
+ },
812
+ constantTimeEquals(left, right) {
813
+ const leftBytes = Buffer.from(left, "ascii");
814
+ const rightBytes = Buffer.from(right, "ascii");
815
+ try {
816
+ return (leftBytes.byteLength === rightBytes.byteLength && timingSafeEqual(leftBytes, rightBytes));
817
+ }
818
+ finally {
819
+ leftBytes.fill(0);
820
+ rightBytes.fill(0);
821
+ }
822
+ },
823
+ });
824
+ const validateProvider = OidcFlowValues.validateProvider;
825
+ //# sourceMappingURL=index.js.map