@spine-event-engine/auth 2.0.0-snapshot.26 → 2.0.0-snapshot.28

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 (65) hide show
  1. package/README.md +102 -11
  2. package/REFERENCE.md +125 -3
  3. package/dist/index.d.ts +15 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +4 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/oidc/contracts.d.ts +18 -4
  8. package/dist/oidc/contracts.d.ts.map +1 -1
  9. package/dist/oidc/flow-store.d.ts +143 -0
  10. package/dist/oidc/flow-store.d.ts.map +1 -0
  11. package/dist/oidc/flow-store.js +184 -0
  12. package/dist/oidc/flow-store.js.map +1 -0
  13. package/dist/oidc/index.d.ts +29 -9
  14. package/dist/oidc/index.d.ts.map +1 -1
  15. package/dist/oidc/index.js +611 -152
  16. package/dist/oidc/index.js.map +1 -1
  17. package/dist/oidc/persistent-flow-store.d.ts +117 -0
  18. package/dist/oidc/persistent-flow-store.d.ts.map +1 -0
  19. package/dist/oidc/persistent-flow-store.js +413 -0
  20. package/dist/oidc/persistent-flow-store.js.map +1 -0
  21. package/dist/oidc/records.d.ts +15 -0
  22. package/dist/oidc/records.d.ts.map +1 -0
  23. package/dist/oidc/records.js +91 -0
  24. package/dist/oidc/records.js.map +1 -0
  25. package/dist/providers/index.d.ts +10 -5
  26. package/dist/providers/index.d.ts.map +1 -1
  27. package/dist/providers/index.js +103 -30
  28. package/dist/providers/index.js.map +1 -1
  29. package/dist/sessions/cookies.d.ts +46 -8
  30. package/dist/sessions/cookies.d.ts.map +1 -1
  31. package/dist/sessions/cookies.js +269 -47
  32. package/dist/sessions/cookies.js.map +1 -1
  33. package/dist/sessions/opaque-session-store.d.ts +143 -0
  34. package/dist/sessions/opaque-session-store.d.ts.map +1 -0
  35. package/dist/sessions/opaque-session-store.js +185 -0
  36. package/dist/sessions/opaque-session-store.js.map +1 -0
  37. package/dist/sessions/opaque.d.ts +93 -106
  38. package/dist/sessions/opaque.d.ts.map +1 -1
  39. package/dist/sessions/opaque.js +501 -232
  40. package/dist/sessions/opaque.js.map +1 -1
  41. package/dist/sessions/persistent-opaque-session-store.d.ts +105 -0
  42. package/dist/sessions/persistent-opaque-session-store.d.ts.map +1 -0
  43. package/dist/sessions/persistent-opaque-session-store.js +333 -0
  44. package/dist/sessions/persistent-opaque-session-store.js.map +1 -0
  45. package/dist/sessions/persistent-token-revocations.d.ts +90 -0
  46. package/dist/sessions/persistent-token-revocations.d.ts.map +1 -0
  47. package/dist/sessions/persistent-token-revocations.js +422 -0
  48. package/dist/sessions/persistent-token-revocations.js.map +1 -0
  49. package/dist/sessions/session-lifetime.d.ts +67 -0
  50. package/dist/sessions/session-lifetime.d.ts.map +1 -0
  51. package/dist/sessions/session-lifetime.js +165 -0
  52. package/dist/sessions/session-lifetime.js.map +1 -0
  53. package/dist/sessions/session-record.d.ts +15 -0
  54. package/dist/sessions/session-record.d.ts.map +1 -0
  55. package/dist/sessions/session-record.js +109 -0
  56. package/dist/sessions/session-record.js.map +1 -0
  57. package/dist/sessions/signed.d.ts +5 -4
  58. package/dist/sessions/signed.d.ts.map +1 -1
  59. package/dist/sessions/signed.js +212 -74
  60. package/dist/sessions/signed.js.map +1 -1
  61. package/dist/subscriptions/index.d.ts.map +1 -1
  62. package/dist/subscriptions/index.js +260 -26
  63. package/dist/subscriptions/index.js.map +1 -1
  64. package/dist/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +20 -6
@@ -0,0 +1,15 @@
1
+ import { type Timestamp } from "@bufbuild/protobuf/wkt";
2
+ import { type AuthenticatedPrincipal as ProtoPrincipal, type OpaqueSessionRecord } from "@spine-event-engine/proto/auth";
3
+ import type { AuthenticatedPrincipal } from "../index.js";
4
+ /**
5
+ * Domain conversion and exact time ordering for opaque session records.
6
+ */
7
+ export declare const SessionRecords: Readonly<{
8
+ deadline(value: Timestamp): Timestamp;
9
+ compare(left: Timestamp, right: Timestamp): number;
10
+ orderKey(value: Timestamp): Uint8Array;
11
+ principal(principal: AuthenticatedPrincipal): ProtoPrincipal;
12
+ publicPrincipal(record: OpaqueSessionRecord): AuthenticatedPrincipal;
13
+ expiry(record: OpaqueSessionRecord): Timestamp;
14
+ }>;
15
+ //# sourceMappingURL=session-record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-record.d.ts","sourceRoot":"","sources":["../../src/sessions/session-record.ts"],"names":[],"mappings":"AAeA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAGL,KAAK,sBAAsB,IAAI,cAAc,EAC7C,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACvC,SAAS,CAAC,SAAS,EAAE,sBAAsB,GAAG,cAAc,CAAC;IAC7D,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,sBAAsB,CAAC;IACrE,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC;CAChD,CA4FC,CAAC"}
@@ -0,0 +1,109 @@
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 { clone, create } from "@bufbuild/protobuf";
15
+ import { TimestampSchema } from "@bufbuild/protobuf/wkt";
16
+ import { AuthenticatedPrincipalSchema, PrincipalAttributesSchema, } from "@spine-event-engine/proto/auth";
17
+ /**
18
+ * Domain conversion and exact time ordering for opaque session records.
19
+ */
20
+ export const SessionRecords = Object.freeze({
21
+ /**
22
+ * Validates an exact Protobuf deadline.
23
+ *
24
+ * @param value Candidate deadline.
25
+ * @returns Detached validated deadline.
26
+ */
27
+ deadline(value) {
28
+ if (value.seconds < -62135596800n ||
29
+ value.seconds > 253402300799n ||
30
+ !Number.isInteger(value.nanos) ||
31
+ value.nanos < 0 ||
32
+ value.nanos >= 1_000_000_000)
33
+ throw new RangeError("Session deadline is outside the Protobuf Timestamp range.");
34
+ return clone(TimestampSchema, value);
35
+ },
36
+ /**
37
+ * Compares exact deadlines without millisecond truncation.
38
+ *
39
+ * @param left First deadline.
40
+ * @param right Second deadline.
41
+ * @returns Negative, zero, or positive order.
42
+ */
43
+ compare(left, right) {
44
+ if (left.seconds < right.seconds)
45
+ return -1;
46
+ if (left.seconds > right.seconds)
47
+ return 1;
48
+ return left.nanos - right.nanos;
49
+ },
50
+ /**
51
+ * Encodes a complete deadline for unsigned byte ordering.
52
+ *
53
+ * @param value Exact deadline.
54
+ * @returns Twelve ordered bytes.
55
+ */
56
+ orderKey(value) {
57
+ const deadline = SessionRecords.deadline(value);
58
+ const bytes = Buffer.alloc(12);
59
+ bytes.writeBigUInt64BE(BigInt.asUintN(64, deadline.seconds) ^ 0x8000000000000000n, 0);
60
+ bytes.writeUInt32BE(deadline.nanos, 8);
61
+ return bytes;
62
+ },
63
+ /**
64
+ * Copies a public principal into the domain message with attributes presence.
65
+ *
66
+ * @param principal Authenticated identity.
67
+ * @returns Complete Protobuf identity.
68
+ */
69
+ principal(principal) {
70
+ if (typeof principal.id !== "string")
71
+ throw new TypeError("principal.id must be a string.");
72
+ if (principal.attributes === undefined)
73
+ return create(AuthenticatedPrincipalSchema, { id: principal.id });
74
+ const values = Object.fromEntries(Object.entries(principal.attributes));
75
+ if (Object.values(values).some((value) => typeof value !== "string"))
76
+ throw new TypeError("Principal attributes must be strings.");
77
+ return create(AuthenticatedPrincipalSchema, {
78
+ id: principal.id,
79
+ attributes: create(PrincipalAttributesSchema, { values }),
80
+ });
81
+ },
82
+ /**
83
+ * Copies a stored identity to the public principal shape.
84
+ *
85
+ * @param record Complete session record.
86
+ * @returns Detached public identity.
87
+ */
88
+ publicPrincipal(record) {
89
+ const principal = record.principal;
90
+ if (principal === undefined)
91
+ throw new Error("Stored session has no principal.");
92
+ const attributes = principal.attributes;
93
+ return Object.freeze(attributes === undefined
94
+ ? { id: principal.id }
95
+ : { id: principal.id, attributes: Object.freeze({ ...attributes.values }) });
96
+ },
97
+ /**
98
+ * Reads the required exact deadline from a stored session.
99
+ *
100
+ * @param record Complete session record.
101
+ * @returns Validated deadline.
102
+ */
103
+ expiry(record) {
104
+ if (record.whenExpires === undefined)
105
+ throw new Error("Stored session has no expiry.");
106
+ return SessionRecords.deadline(record.whenExpires);
107
+ },
108
+ });
109
+ //# sourceMappingURL=session-record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-record.js","sourceRoot":"","sources":["../../src/sessions/session-record.ts"],"names":[],"mappings":"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"}
@@ -59,10 +59,10 @@ export interface SignedTokenRevocation {
59
59
  */
60
60
  isRevoked(jti: string): Promise<boolean>;
61
61
  /**
62
- * Stores the exact token ID through its Protobuf Timestamp expiry.
63
- * The application owns persistence, cleanup, availability, and atomicity.
62
+ * Stores the exact token ID through the inclusive last acceptance instant.
63
+ * The strategy includes its configured clock tolerance before calling this port.
64
64
  * @param jti The token identifier to retain.
65
- * @param expiresAt The expiry after which retention may end.
65
+ * @param expiresAt The full last-acceptance deadline, including clock tolerance.
66
66
  * @returns Completes after the token ID is retained.
67
67
  */
68
68
  revoke(jti: string, expiresAt: Timestamp): Promise<void>;
@@ -214,9 +214,10 @@ export declare class SignedSessions implements SessionResolver {
214
214
  * Cookie credentials and every malformed, invalid, expired, revoked, or
215
215
  * unavailable-revocation result return `undefined`.
216
216
  * @param credential The credential to resolve.
217
+ * @param lifetime Optional host operation; omission retains no monitor.
217
218
  * @returns The verified session, or undefined when it cannot be trusted.
218
219
  */
219
- resolve(credential: RequestCredential): Promise<ResolvedSession | undefined>;
220
+ resolve(credential: RequestCredential, lifetime?: AbortSignal): Promise<ResolvedSession | undefined>;
220
221
  /**
221
222
  * Updates the active P-256 key atomically.
222
223
  *
@@ -1 +1 @@
1
- {"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../src/sessions/signed.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../src/sessions/signed.ts"],"names":[],"mappings":"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"}
@@ -13,8 +13,10 @@
13
13
  */
14
14
  import { Time } from "@spine-event-engine/core/time";
15
15
  import { create } from "@bufbuild/protobuf";
16
- import { KeyObject, createPrivateKey, createPublicKey, randomBytes as nodeRandomBytes, sign, verify, } from "node:crypto";
16
+ import { KeyObject, createPrivateKey, createPublicKey, randomBytes as nodeRandomBytes, } from "node:crypto";
17
17
  import { TimestampSchema } from "@bufbuild/protobuf/wkt";
18
+ import { SignJWT, decodeProtectedHeader, jwtVerify } from "jose";
19
+ import { SessionLifetimes } from "./session-lifetime.js";
18
20
  /**
19
21
  * Finite, local ES256/JWT application sessions. This class has no remote key
20
22
  * discovery, persistence, OIDC behavior, or authorization policy. It imports
@@ -36,6 +38,10 @@ export class SignedSessions {
36
38
  #keys = new Map();
37
39
  #revocation;
38
40
  #closed = false;
41
+ #lifetimes = new SessionLifetimes(() => {
42
+ const now = this.#now();
43
+ return typeof now === "number" ? now : undefined;
44
+ });
39
45
  /**
40
46
  * Creates signed sessions with copied finite configuration and P-256 keys.
41
47
  * @param options The issuer, key ring, limits, clock, entropy, and revocation settings.
@@ -71,80 +77,156 @@ export class SignedSessions {
71
77
  * @param principal The authenticated principal to encode.
72
78
  * @returns The issuance outcome and, when accepted, credential and session.
73
79
  */
74
- issue(principal) {
80
+ async issue(principal) {
75
81
  if (this.#isClosed())
76
- return Promise.resolve(SignedSessionValues.rejected("closed"));
82
+ return SignedSessionValues.rejected("closed");
77
83
  const now = this.#now();
78
84
  if (typeof now !== "number")
79
- return Promise.resolve(SignedSessionValues.rejected(now.kind === "closed" ? "closed" : "clock-failure"));
85
+ return SignedSessionValues.rejected(now.kind === "closed" ? "closed" : "clock-failure");
80
86
  if (this.#isClosed())
81
- return Promise.resolve(SignedSessionValues.rejected("closed"));
87
+ return SignedSessionValues.rejected("closed");
82
88
  this.#sweep(now);
89
+ return this.#issuePrepared(principal);
90
+ }
91
+ /**
92
+ * Copies the principal and constructs bounded claims before signing.
93
+ *
94
+ * @param principal Authenticated principal supplied for issuance.
95
+ * @returns Issuance outcome after signing or a specific input failure.
96
+ */
97
+ async #issuePrepared(principal) {
83
98
  let copied;
84
99
  try {
85
100
  copied = SignedSessionValues.principalCopy(principal, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
86
101
  }
87
102
  catch {
88
- return Promise.resolve(SignedSessionValues.rejected("principal-invalid"));
103
+ return SignedSessionValues.rejected("principal-invalid");
89
104
  }
90
105
  if (copied === undefined)
91
- return Promise.resolve(SignedSessionValues.rejected("principal-invalid"));
106
+ return SignedSessionValues.rejected("principal-invalid");
92
107
  const jti = this.#jti();
93
108
  if (jti === undefined)
94
- return Promise.resolve(this.#isClosed()
109
+ return this.#isClosed()
95
110
  ? SignedSessionValues.rejected("closed")
96
- : SignedSessionValues.rejected("entropy-failure"));
111
+ : SignedSessionValues.rejected("entropy-failure");
97
112
  if (this.#isClosed())
98
- return Promise.resolve(SignedSessionValues.rejected("closed"));
113
+ return SignedSessionValues.rejected("closed");
99
114
  const again = this.#now();
100
115
  if (typeof again !== "number")
101
- return Promise.resolve(SignedSessionValues.rejected(again.kind === "closed" ? "closed" : "clock-failure"));
116
+ return SignedSessionValues.rejected(again.kind === "closed" ? "closed" : "clock-failure");
102
117
  if (this.#isClosed())
103
- return Promise.resolve(SignedSessionValues.rejected("closed"));
104
- const iat = Math.floor(again / 1000);
118
+ return SignedSessionValues.rejected("closed");
119
+ const claims = this.#issueClaims(copied, jti, again);
120
+ if (claims === undefined)
121
+ return SignedSessionValues.rejected("clock-failure");
122
+ return this.#signIssued(claims, copied);
123
+ }
124
+ /**
125
+ * Builds exact signed claims when their expiry fits the trusted clock domain.
126
+ *
127
+ * @param principal Detached authenticated principal.
128
+ * @param jti Fresh token identifier.
129
+ * @param now Trusted issuance time in milliseconds.
130
+ * @returns Claims, or undefined when expiry exceeds the Timestamp range.
131
+ */
132
+ #issueClaims(principal, jti, now) {
133
+ const iat = Math.floor(now / 1000);
105
134
  const exp = iat + this.#ttl;
106
135
  if (!Number.isSafeInteger(exp) || !SignedSessionValues.timeValid(exp * 1000))
107
- return Promise.resolve(SignedSessionValues.rejected("clock-failure"));
108
- const claims = {
136
+ return undefined;
137
+ return {
109
138
  iss: this.#issuer,
110
139
  aud: this.#audience,
111
- sub: copied.id,
140
+ sub: principal.id,
112
141
  iat,
113
142
  nbf: iat,
114
143
  exp,
115
144
  jti,
116
- ...(copied.attributes === undefined ? {} : { attributes: copied.attributes }),
145
+ ...(principal.attributes === undefined ? {} : { attributes: principal.attributes }),
117
146
  };
147
+ }
148
+ /**
149
+ * Signs claims and rechecks closure and time after the awaited signature.
150
+ *
151
+ * @param claims Validated claims to sign.
152
+ * @param principal Detached authenticated principal.
153
+ * @returns Issued credential or a failure without exposing a late token.
154
+ */
155
+ async #signIssued(claims, principal) {
118
156
  try {
119
- const token = this.#token(claims);
157
+ const token = await this.#token(claims);
120
158
  if (token.length > this.#maxToken)
121
- return Promise.resolve(SignedSessionValues.rejected("signing-failure"));
159
+ return SignedSessionValues.rejected("signing-failure");
122
160
  if (this.#isClosed())
123
- return Promise.resolve(SignedSessionValues.rejected("closed"));
124
- return Promise.resolve({
161
+ return SignedSessionValues.rejected("closed");
162
+ if (!this.#signedTimeValid(claims))
163
+ return SignedSessionValues.rejected("clock-failure");
164
+ if (this.#isClosed())
165
+ return SignedSessionValues.rejected("closed");
166
+ return {
125
167
  kind: "issued",
126
168
  credential: Object.freeze({ kind: "bearer", value: token }),
127
- session: SignedSessionValues.session(copied, exp),
128
- });
169
+ session: SignedSessionValues.session(principal, claims.exp),
170
+ };
129
171
  }
130
172
  catch {
131
- return Promise.resolve(SignedSessionValues.rejected("signing-failure"));
173
+ return this.#isClosed()
174
+ ? SignedSessionValues.rejected("closed")
175
+ : SignedSessionValues.rejected("signing-failure");
132
176
  }
133
177
  }
178
+ /**
179
+ * Checks issued claims against a fresh trusted clock reading.
180
+ *
181
+ * @param claims Claims signed by the active key.
182
+ * @returns Whether they still satisfy the exact application time policy.
183
+ */
184
+ #signedTimeValid(claims) {
185
+ const signedAt = this.#now();
186
+ return (typeof signedAt === "number" &&
187
+ SignedSessionValues.validClaims(claims, this.#issuer, this.#audience, this.#ttl, this.#skew, signedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars) !== undefined);
188
+ }
134
189
  /**
135
190
  * Resolves one locally verifiable bearer credential.
136
191
  *
137
192
  * Cookie credentials and every malformed, invalid, expired, revoked, or
138
193
  * unavailable-revocation result return `undefined`.
139
194
  * @param credential The credential to resolve.
195
+ * @param lifetime Optional host operation; omission retains no monitor.
140
196
  * @returns The verified session, or undefined when it cannot be trusted.
141
197
  */
142
- async resolve(credential) {
198
+ async resolve(credential, lifetime) {
143
199
  if (this.#isClosed() || credential.kind !== "bearer")
144
200
  return undefined;
145
- const claims = this.#verifiedClaims(credential.value);
146
- if (claims === undefined)
201
+ lifetime?.throwIfAborted();
202
+ const claims = await this.#verifiedClaims(credential.value);
203
+ if (claims === undefined || this.#isClosed())
147
204
  return undefined;
205
+ const monitor = lifetime === undefined
206
+ ? undefined
207
+ : this.#lifetimes.register(this.#identity(claims.jti), lifetime);
208
+ let retained = false;
209
+ try {
210
+ const resolved = await this.#resolveClaims(claims);
211
+ if (resolved === undefined || monitor === undefined)
212
+ return resolved;
213
+ if (!monitor.ready(resolved.expiresAt))
214
+ return undefined;
215
+ retained = true;
216
+ return { ...resolved, invalidated: monitor.signal };
217
+ }
218
+ finally {
219
+ if (!retained)
220
+ monitor?.release();
221
+ }
222
+ }
223
+ /**
224
+ * Checks authoritative revocation and time after monitored registration.
225
+ *
226
+ * @param claims Verified token claims.
227
+ * @returns Detached session if still valid.
228
+ */
229
+ async #resolveClaims(claims) {
148
230
  if (this.#revocation !== undefined)
149
231
  try {
150
232
  if (await this.#revocation.isRevoked(claims.jti))
@@ -153,13 +235,25 @@ export class SignedSessions {
153
235
  catch {
154
236
  return undefined;
155
237
  }
156
- if (this.#isClosed())
238
+ const checkedAt = this.#now();
239
+ if (typeof checkedAt !== "number" || this.#isClosed())
240
+ return undefined;
241
+ if (SignedSessionValues.validClaims(claims, this.#issuer, this.#audience, this.#ttl, this.#skew, checkedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars) === undefined)
157
242
  return undefined;
158
243
  return SignedSessionValues.session({
159
244
  id: claims.sub,
160
245
  ...(claims.attributes === undefined ? {} : { attributes: claims.attributes }),
161
246
  }, claims.exp);
162
247
  }
248
+ /**
249
+ * Scopes a token ID to this strategy's issuer and audience.
250
+ *
251
+ * @param jti Authenticated token ID.
252
+ * @returns Local monitor key.
253
+ */
254
+ #identity(jti) {
255
+ return `${this.#issuer}\u0000${this.#audience}\u0000${jti}`;
256
+ }
163
257
  /**
164
258
  * Updates the active P-256 key atomically.
165
259
  *
@@ -223,15 +317,20 @@ export class SignedSessions {
223
317
  const resolved = await this.#claimsForLogout(credential);
224
318
  if (resolved === undefined)
225
319
  return { kind: "expiryOnly" };
226
- if (this.#revocation === undefined)
320
+ if (this.#revocation === undefined) {
321
+ this.#lifetimes.invalidate(this.#identity(resolved.jti));
227
322
  return { kind: "expiryOnly" };
323
+ }
228
324
  try {
229
- await this.#revocation.revoke(resolved.jti, SignedSessionValues.timestamp(resolved.exp));
325
+ await this.#revocation.revoke(resolved.jti, SignedSessionValues.lastAcceptance(resolved.exp, this.#skew));
230
326
  return this.#closed ? { kind: "expiryOnly" } : { kind: "revoked" };
231
327
  }
232
328
  catch {
233
329
  return this.#closed ? { kind: "expiryOnly" } : { kind: "unavailable" };
234
330
  }
331
+ finally {
332
+ this.#lifetimes.invalidate(this.#identity(resolved.jti));
333
+ }
235
334
  }
236
335
  /**
237
336
  * Closes the resolver and clears active, verification-key, and revocation references.
@@ -240,6 +339,7 @@ export class SignedSessions {
240
339
  */
241
340
  close() {
242
341
  this.#closed = true;
342
+ this.#lifetimes.close();
243
343
  this.#keys.clear();
244
344
  this.#active = undefined;
245
345
  this.#revocation = undefined;
@@ -248,9 +348,15 @@ export class SignedSessions {
248
348
  #claimsForLogout(credential) {
249
349
  if (credential.kind !== "bearer" || this.#isClosed())
250
350
  return Promise.resolve(undefined);
251
- return Promise.resolve(this.#verifiedClaims(credential.value));
351
+ return this.#verifiedClaims(credential.value);
252
352
  }
253
- #verifiedClaims(value) {
353
+ /**
354
+ * Verifies a compact token and checks current key and claim policy again.
355
+ *
356
+ * @param value Compact bearer token.
357
+ * @returns Trusted claims, or undefined on any verification failure.
358
+ */
359
+ async #verifiedClaims(value) {
254
360
  const parsed = this.#parse(value);
255
361
  if (parsed === undefined)
256
362
  return undefined;
@@ -261,29 +367,63 @@ export class SignedSessions {
261
367
  const key = this.#keys.get(parsed.kid);
262
368
  if (key === undefined || (key.expiresAt !== undefined && now > key.expiresAt))
263
369
  return undefined;
370
+ const verified = await this.#verifyToken(value, key.publicKey, now);
371
+ if (verified === undefined)
372
+ return undefined;
373
+ const checkedAt = this.#now();
374
+ if (typeof checkedAt !== "number" || this.#isClosed())
375
+ return undefined;
376
+ this.#sweep(checkedAt);
377
+ if (!this.#keys.has(parsed.kid) || (key.expiresAt !== undefined && checkedAt > key.expiresAt))
378
+ return undefined;
379
+ return SignedSessionValues.validClaims(verified, this.#issuer, this.#audience, this.#ttl, this.#skew, checkedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
380
+ }
381
+ /**
382
+ * Runs jose JWT verification with preliminary time tolerance.
383
+ *
384
+ * @param value Compact bearer token.
385
+ * @param key Selected public verification key.
386
+ * @param now Trusted time sampled before verification.
387
+ * @returns Verified payload, or undefined when jose rejects the token.
388
+ */
389
+ async #verifyToken(value, key, now) {
264
390
  try {
265
- if (!verify("sha256", Buffer.from(parsed.input), { key: key.publicKey, dsaEncoding: "ieee-p1363" }, parsed.signature))
266
- return undefined;
391
+ const result = await jwtVerify(value, key, {
392
+ algorithms: ["ES256"],
393
+ typ: "JWT",
394
+ issuer: this.#issuer,
395
+ audience: this.#audience,
396
+ requiredClaims: ["sub", "iat", "nbf", "exp", "jti"],
397
+ maxTokenAge: this.#ttl,
398
+ clockTolerance: this.#skew + 1,
399
+ currentDate: new Date(now),
400
+ });
401
+ return result.payload;
267
402
  }
268
403
  catch {
269
404
  return undefined;
270
405
  }
271
- return SignedSessionValues.validClaims(parsed.claims, this.#issuer, this.#audience, this.#ttl, this.#skew, now, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
272
406
  }
407
+ /**
408
+ * Signs the exact JWT header and claims with the active P-256 key.
409
+ *
410
+ * @param claims Validated application claims.
411
+ * @returns Compact ES256 JWT from jose.
412
+ */
273
413
  #token(claims) {
274
414
  const active = this.#active;
275
415
  if (active === undefined)
276
416
  throw new Error("SignedSessions is closed");
277
- const header = SignedSessionValues.encode({ alg: "ES256", typ: "JWT", kid: active.kid });
278
- const payload = SignedSessionValues.encode(claims);
279
- const input = `${header}.${payload}`;
280
- const signature = sign("sha256", Buffer.from(input), {
281
- key: active.privateKey,
282
- dsaEncoding: "ieee-p1363",
283
- });
284
- if (signature.byteLength !== 64)
285
- throw new Error("invalid ES256 signature");
286
- return `${input}.${Buffer.from(signature).toString("base64url")}`;
417
+ return new SignJWT(claims.attributes === undefined ? {} : { attributes: claims.attributes })
418
+ .setProtectedHeader({ alg: "ES256", typ: "JWT", kid: active.kid })
419
+ .setIssuer(claims.iss)
420
+ .setAudience(claims.aud)
421
+ .setSubject(claims.sub)
422
+ .setIssuedAt(claims.iat)
423
+ .setNotBefore(claims.nbf)
424
+ .setExpirationTime(claims.exp)
425
+ .setJti(claims.jti)
426
+ .sign(active.privateKey);
287
427
  }
288
428
  #jti() {
289
429
  let bytes;
@@ -322,6 +462,12 @@ export class SignedSessions {
322
462
  if (key.expiresAt !== undefined && now > key.expiresAt)
323
463
  this.#keys.delete(kid);
324
464
  }
465
+ /**
466
+ * Checks compact encoding bounds and reads the exact protected header with jose.
467
+ *
468
+ * @param value Candidate compact bearer token.
469
+ * @returns Validated key ID, or undefined for an unsupported encoding or header.
470
+ */
325
471
  #parse(value) {
326
472
  if (typeof value !== "string" || value.length === 0 || value.length > this.#maxToken)
327
473
  return undefined;
@@ -331,9 +477,11 @@ export class SignedSessions {
331
477
  return undefined;
332
478
  const [encodedHeader, encodedClaims, encodedSignature] = segments;
333
479
  try {
334
- const header = SignedSessionValues.json(encodedHeader);
335
- if (!SignedSessionValues.plain(header) ||
336
- header.alg !== "ES256" ||
480
+ if (Buffer.byteLength(encodedHeader, "base64url") > 16_384 ||
481
+ Buffer.byteLength(encodedClaims, "base64url") > 16_384)
482
+ return undefined;
483
+ const header = decodeProtectedHeader(value);
484
+ if (header.alg !== "ES256" ||
337
485
  header.typ !== "JWT" ||
338
486
  typeof header.kid !== "string" ||
339
487
  Object.keys(header).length !== 3 ||
@@ -342,12 +490,7 @@ export class SignedSessions {
342
490
  const signature = Buffer.from(encodedSignature, "base64url");
343
491
  if (signature.byteLength !== 64 || signature.toString("base64url") !== encodedSignature)
344
492
  return undefined;
345
- return {
346
- kid: header.kid,
347
- input: `${encodedHeader}.${encodedClaims}`,
348
- signature,
349
- claims: SignedSessionValues.json(encodedClaims),
350
- };
493
+ return { kid: header.kid };
351
494
  }
352
495
  catch {
353
496
  return undefined;
@@ -357,6 +500,7 @@ export class SignedSessions {
357
500
  /**
358
501
  * Validates, serializes, and constructs finite signed-session values.
359
502
  */
503
+ const maxTimestampSecond = 253402300799n;
360
504
  const SignedSessionValues = Object.freeze({
361
505
  /**
362
506
  * Validates the issuer, audience, and finite token limits before key installation.
@@ -498,25 +642,6 @@ const SignedSessionValues = Object.freeze({
498
642
  base64url(value) {
499
643
  return value.length > 0 && /^[A-Za-z0-9_-]+$/.test(value) && value.length % 4 !== 1;
500
644
  },
501
- /**
502
- * Encodes a JSON value as URL-safe base64.
503
- * @param value The JSON value to encode.
504
- * @returns The encoded JSON string.
505
- */
506
- encode(value) {
507
- return Buffer.from(JSON.stringify(value)).toString("base64url");
508
- },
509
- /**
510
- * Decodes bounded URL-safe base64 JSON.
511
- * @param value The URL-safe base64 JSON string to decode.
512
- * @returns The decoded JSON value.
513
- */
514
- json(value) {
515
- const decoded = Buffer.from(value, "base64url");
516
- if (decoded.byteLength > 16_384)
517
- throw new Error();
518
- return JSON.parse(decoded.toString("utf8"));
519
- },
520
645
  /**
521
646
  * Checks whether a value is a plain object.
522
647
  * @param value The unknown value to test for plain-record shape.
@@ -619,6 +744,19 @@ const SignedSessionValues = Object.freeze({
619
744
  timestamp(seconds) {
620
745
  return create(TimestampSchema, { seconds: BigInt(seconds) });
621
746
  },
747
+ /**
748
+ * Calculates the final representable acceptance instant without unsafe addition.
749
+ *
750
+ * @param expiry Signed token expiry in epoch seconds.
751
+ * @param skew Accepted clock tolerance in seconds.
752
+ * @returns Exact inclusive deadline within the configured clock's Timestamp domain.
753
+ */
754
+ lastAcceptance(expiry, skew) {
755
+ const endpoint = BigInt(expiry) + BigInt(skew);
756
+ return endpoint > maxTimestampSecond
757
+ ? create(TimestampSchema, { seconds: maxTimestampSecond, nanos: 999_999_999 })
758
+ : create(TimestampSchema, { seconds: endpoint });
759
+ },
622
760
  /**
623
761
  * Creates a resolved session with a detached principal.
624
762
  * @param principal The principal to copy.
@@ -641,7 +779,7 @@ const SignedSessionValues = Object.freeze({
641
779
  */
642
780
  timeValid(milliseconds) {
643
781
  const seconds = Math.floor(milliseconds / 1000);
644
- return seconds >= -62_135_596_800 && seconds <= 253_402_300_799;
782
+ return seconds >= -62_135_596_800 && seconds <= Number(maxTimestampSecond);
645
783
  },
646
784
  /**
647
785
  * Checks a token identifier against expected random encoding.