@serialsubscriptions/platform-integration 0.0.79

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 (60) hide show
  1. package/README.md +1 -0
  2. package/lib/SSIProject.d.ts +343 -0
  3. package/lib/SSIProject.js +429 -0
  4. package/lib/SSIProjectApi.d.ts +384 -0
  5. package/lib/SSIProjectApi.js +534 -0
  6. package/lib/SSISubscribedFeatureApi.d.ts +387 -0
  7. package/lib/SSISubscribedFeatureApi.js +511 -0
  8. package/lib/SSISubscribedLimitApi.d.ts +384 -0
  9. package/lib/SSISubscribedLimitApi.js +534 -0
  10. package/lib/SSISubscribedPlanApi.d.ts +384 -0
  11. package/lib/SSISubscribedPlanApi.js +537 -0
  12. package/lib/SubscribedPlanManager.d.ts +380 -0
  13. package/lib/SubscribedPlanManager.js +288 -0
  14. package/lib/UsageApi.d.ts +128 -0
  15. package/lib/UsageApi.js +224 -0
  16. package/lib/auth.server.d.ts +192 -0
  17. package/lib/auth.server.js +579 -0
  18. package/lib/cache/SSICache.d.ts +40 -0
  19. package/lib/cache/SSICache.js +134 -0
  20. package/lib/cache/backends/MemoryCacheBackend.d.ts +15 -0
  21. package/lib/cache/backends/MemoryCacheBackend.js +46 -0
  22. package/lib/cache/backends/RedisCacheBackend.d.ts +27 -0
  23. package/lib/cache/backends/RedisCacheBackend.js +95 -0
  24. package/lib/cache/constants.d.ts +7 -0
  25. package/lib/cache/constants.js +10 -0
  26. package/lib/cache/types.d.ts +27 -0
  27. package/lib/cache/types.js +2 -0
  28. package/lib/frontend/index.d.ts +1 -0
  29. package/lib/frontend/index.js +6 -0
  30. package/lib/frontend/session/SessionClient.d.ts +24 -0
  31. package/lib/frontend/session/SessionClient.js +145 -0
  32. package/lib/index.d.ts +15 -0
  33. package/lib/index.js +38 -0
  34. package/lib/lib/session/SessionClient.d.ts +11 -0
  35. package/lib/lib/session/SessionClient.js +47 -0
  36. package/lib/lib/session/index.d.ts +3 -0
  37. package/lib/lib/session/index.js +3 -0
  38. package/lib/lib/session/stores/MemoryStore.d.ts +7 -0
  39. package/lib/lib/session/stores/MemoryStore.js +23 -0
  40. package/lib/lib/session/stores/index.d.ts +1 -0
  41. package/lib/lib/session/stores/index.js +1 -0
  42. package/lib/lib/session/types.d.ts +37 -0
  43. package/lib/lib/session/types.js +1 -0
  44. package/lib/session/SessionClient.d.ts +19 -0
  45. package/lib/session/SessionClient.js +132 -0
  46. package/lib/session/SessionManager.d.ts +139 -0
  47. package/lib/session/SessionManager.js +443 -0
  48. package/lib/stateStore.d.ts +5 -0
  49. package/lib/stateStore.js +9 -0
  50. package/lib/storage/SSIStorage.d.ts +24 -0
  51. package/lib/storage/SSIStorage.js +117 -0
  52. package/lib/storage/backends/MemoryBackend.d.ts +10 -0
  53. package/lib/storage/backends/MemoryBackend.js +44 -0
  54. package/lib/storage/backends/PostgresBackend.d.ts +24 -0
  55. package/lib/storage/backends/PostgresBackend.js +106 -0
  56. package/lib/storage/backends/RedisBackend.d.ts +19 -0
  57. package/lib/storage/backends/RedisBackend.js +78 -0
  58. package/lib/storage/types.d.ts +27 -0
  59. package/lib/storage/types.js +2 -0
  60. package/package.json +71 -0
@@ -0,0 +1,579 @@
1
+ "use strict";
2
+ // packages/platform-integration/src/auth.server.ts
3
+ // Production-grade JWT verification using jose library
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.AuthServer = exports.scopes = void 0;
39
+ const crypto = __importStar(require("crypto"));
40
+ const jose_1 = require("jose");
41
+ const stateStore_1 = require("./stateStore");
42
+ const SSICache_1 = require("./cache/SSICache");
43
+ const constants_1 = require("./cache/constants");
44
+ exports.scopes = {
45
+ defaultScopes: "openid profile email view_project create_project delete_project update_project view_subscribed_plan view_subscribed_feature view_subscribed_limit access_subscription_usage"
46
+ };
47
+ // Helper to normalize issuer URLs by removing trailing slashes
48
+ function normalizeIssuer(url) {
49
+ return url.replace(/\/+$/, "");
50
+ }
51
+ class AuthServer {
52
+ constructor(config) {
53
+ // Use empty object as default if config is not provided
54
+ const cfg = config ?? {};
55
+ let scopesString;
56
+ if (cfg.scopes === undefined || cfg.scopes === null) {
57
+ scopesString = exports.scopes.defaultScopes;
58
+ }
59
+ else if (Array.isArray(cfg.scopes)) {
60
+ scopesString = cfg.scopes.length > 0 ? cfg.scopes.join(" ") : exports.scopes.defaultScopes;
61
+ }
62
+ else {
63
+ scopesString = cfg.scopes.trim() || exports.scopes.defaultScopes;
64
+ }
65
+ // Get values from config or fall back to environment variables
66
+ const issuerBaseUrlRaw = cfg.issuerBaseUrl ?? process.env.SSI_ISSUER_BASE_URL ?? "";
67
+ const clientId = cfg.clientId ?? process.env.SSI_CLIENT_ID ?? "";
68
+ const clientSecret = cfg.clientSecret ?? process.env.SSI_CLIENT_SECRET ?? "";
69
+ const redirectUriRaw = cfg.redirectUri ?? process.env.SSI_REDIRECT_URI ?? "";
70
+ const jwksPath = cfg.jwksPath ?? process.env.SSI_ISSUER_JWKS_PATH ?? "/.well-known/jwks.json";
71
+ // Validate required values
72
+ if (!issuerBaseUrlRaw) {
73
+ throw new Error("issuerBaseUrl is required. Provide it in config or set SSI_ISSUER_BASE_URL environment variable.");
74
+ }
75
+ if (!clientId) {
76
+ throw new Error("clientId is required. Provide it in config or set SSI_CLIENT_ID environment variable.");
77
+ }
78
+ // Create both normalized forms (with and without trailing slash)
79
+ // This allows JWT validation to succeed regardless of how the issuer formats their iss claim
80
+ const issuerNoSlash = normalizeIssuer(issuerBaseUrlRaw);
81
+ const issuerWithSlash = issuerNoSlash + "/";
82
+ const issuerAccepted = [issuerNoSlash, issuerWithSlash];
83
+ // Normalize redirectUri using URL class
84
+ const redirectUri = redirectUriRaw ? new URL(redirectUriRaw).toString() : "";
85
+ // Normalize jwksUri if provided, otherwise construct it
86
+ const jwksUri = cfg.jwksUri
87
+ ? (cfg.jwksUri.startsWith('http://') || cfg.jwksUri.startsWith('https://'))
88
+ ? new URL(cfg.jwksUri).toString()
89
+ : new URL(cfg.jwksUri, issuerNoSlash).toString()
90
+ : `${issuerNoSlash}${jwksPath.startsWith("/") ? jwksPath : `/${jwksPath}`}`;
91
+ // Resolve logout endpoint: config > full URL env > issuer + path env > default /user/logout
92
+ const logoutPathEnv = process.env.SSI_ISSUER_LOGOUT_PATH?.trim();
93
+ const logoutEndpointEnv = process.env.SSI_LOGOUT_ENDPOINT?.trim();
94
+ const resolvedLogoutEndpointRaw = cfg.logoutEndpoint
95
+ ?? (logoutEndpointEnv || `${issuerNoSlash}${logoutPathEnv ? (logoutPathEnv.startsWith('/') ? logoutPathEnv : `/${logoutPathEnv}`) : '/user/logout'}`);
96
+ // Normalize logout endpoint using URL class (handle relative paths by constructing relative to issuer)
97
+ const resolvedLogoutEndpoint = resolvedLogoutEndpointRaw
98
+ ? (resolvedLogoutEndpointRaw.startsWith('http://') || resolvedLogoutEndpointRaw.startsWith('https://'))
99
+ ? new URL(resolvedLogoutEndpointRaw).toString()
100
+ : new URL(resolvedLogoutEndpointRaw, issuerNoSlash).toString()
101
+ : "";
102
+ // Normalize authorization and token endpoints if provided, otherwise construct them
103
+ const authorizationEndpoint = cfg.authorizationEndpoint
104
+ ? (cfg.authorizationEndpoint.startsWith('http://') || cfg.authorizationEndpoint.startsWith('https://'))
105
+ ? new URL(cfg.authorizationEndpoint).toString()
106
+ : new URL(cfg.authorizationEndpoint, issuerNoSlash).toString()
107
+ : `${issuerNoSlash}/oauth/authorize`;
108
+ const tokenEndpoint = cfg.tokenEndpoint
109
+ ? (cfg.tokenEndpoint.startsWith('http://') || cfg.tokenEndpoint.startsWith('https://'))
110
+ ? new URL(cfg.tokenEndpoint).toString()
111
+ : new URL(cfg.tokenEndpoint, issuerNoSlash).toString()
112
+ : `${issuerNoSlash}/oauth/token`;
113
+ this.cfg = {
114
+ issuerBaseUrl: issuerBaseUrlRaw, // keep as provided for URLs we build
115
+ authorizationEndpoint,
116
+ tokenEndpoint,
117
+ logoutEndpoint: resolvedLogoutEndpoint,
118
+ jwksUri,
119
+ jwks: cfg.jwks,
120
+ clientId,
121
+ clientSecret,
122
+ redirectUri,
123
+ audience: cfg.audience ?? "",
124
+ scopes: scopesString,
125
+ issuerAccepted, // Both forms accepted for JWT validation
126
+ };
127
+ // Initialize state storage: use provided storage or default from environment
128
+ this.stateStorage = cfg.storage
129
+ ? cfg.storage.withClass('state')
130
+ : stateStore_1.stateStorage;
131
+ // Initialize JWKS cache with "jwks" prefix
132
+ this.jwksCache = SSICache_1.SSICache.fromEnv().withPrefix('jwks');
133
+ }
134
+ /**
135
+ * Build the authorization URL and persist a CSRF state for the callback.
136
+ * Returns: { url, stateKey, stateValue }
137
+ */
138
+ async getLoginUrl(opts = {}) {
139
+ const stateKey = opts.stateKey ?? crypto.randomUUID();
140
+ const stateValue = crypto.randomUUID(); // CSRF token value
141
+ await this.stateStorage.set(stateKey, 'value', stateValue, opts.stateTtlSeconds ?? 600);
142
+ const u = new URL(this.cfg.authorizationEndpoint);
143
+ u.searchParams.set("response_type", "code");
144
+ u.searchParams.set("client_id", this.cfg.clientId);
145
+ u.searchParams.set("redirect_uri", this.cfg.redirectUri);
146
+ u.searchParams.set("scope", this.cfg.scopes);
147
+ u.searchParams.set("state", `${stateKey}:${stateValue}`);
148
+ if (this.cfg.audience)
149
+ u.searchParams.set("audience", this.cfg.audience);
150
+ if (opts.extraParams) {
151
+ for (const [k, v] of Object.entries(opts.extraParams))
152
+ u.searchParams.set(k, v);
153
+ }
154
+ return { url: u.toString(), stateKey, stateValue };
155
+ }
156
+ /**
157
+ * Build the logout URL for RP-initiated logout.
158
+ * Common params: id_token_hint, post_logout_redirect_uri
159
+ */
160
+ getLogoutUrl(opts = {}) {
161
+ // Ensure logout URL uses the same origin as the authorization endpoint
162
+ const authOrigin = new URL(this.cfg.authorizationEndpoint).origin;
163
+ const configured = this.cfg.logoutEndpoint;
164
+ let u;
165
+ if (configured && /^https?:\/\//i.test(configured)) {
166
+ const tmp = new URL(configured);
167
+ u = new URL(tmp.pathname + tmp.search, authOrigin);
168
+ }
169
+ else {
170
+ const path = configured && configured.length > 0
171
+ ? (configured.startsWith('/') ? configured : `/${configured}`)
172
+ : '/user/logout';
173
+ u = new URL(path, authOrigin);
174
+ }
175
+ if (opts.idTokenHint)
176
+ u.searchParams.set('id_token_hint', opts.idTokenHint);
177
+ // Use provided postLogoutRedirectUri, or default to NEXT_PUBLIC_APP_URL if available
178
+ const postLogoutRedirectUriRaw = opts.postLogoutRedirectUri ?? process.env.NEXT_PUBLIC_APP_URL;
179
+ if (postLogoutRedirectUriRaw) {
180
+ // Normalize the URL using URL class
181
+ const normalizedPostLogoutUri = new URL(postLogoutRedirectUriRaw).toString();
182
+ u.searchParams.set('post_logout_redirect_uri', normalizedPostLogoutUri);
183
+ }
184
+ if (opts.extraParams) {
185
+ for (const [k, v] of Object.entries(opts.extraParams))
186
+ u.searchParams.set(k, v);
187
+ }
188
+ return { url: u.toString() };
189
+ }
190
+ /**
191
+ * Handle the OAuth callback: validates state and exchanges the code for tokens.
192
+ * Returns TokenResponse (and includes raw for debugging).
193
+ */
194
+ async handleCallback(params) {
195
+ const code = params.code ?? "";
196
+ const state = params.state ?? "";
197
+ if (!code || !state) {
198
+ throw new Error("Missing code or state.");
199
+ }
200
+ // State validation
201
+ // Expect state to be `${stateKey}:${stateValue}`
202
+ // Use split(":", 2) to only split on the first colon, in case stateValue somehow contains colons
203
+ const parts = state.split(":", 2);
204
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
205
+ throw new Error(`Invalid state format. Expected "stateKey:stateValue", got: ${state.substring(0, 100)}`);
206
+ }
207
+ const [stateKey, stateValue] = parts;
208
+ const storedValue = await this.stateStorage.get(stateKey, 'value');
209
+ const expected = storedValue === null ? undefined : storedValue;
210
+ if (!expected) {
211
+ throw new Error(`Invalid or expired state. State key "${stateKey}" not found in store. This usually means the state has expired or was never stored.`);
212
+ }
213
+ if (expected !== stateValue) {
214
+ throw new Error(`Invalid state. State value mismatch for key "${stateKey}". Expected "${expected}", got "${stateValue}".`);
215
+ }
216
+ await this.stateStorage.del(stateKey, 'value');
217
+ // Token exchange (authorization_code)
218
+ const body = new URLSearchParams({
219
+ grant_type: "authorization_code",
220
+ code,
221
+ redirect_uri: this.cfg.redirectUri,
222
+ client_id: this.cfg.clientId,
223
+ });
224
+ // Confidential client uses client_secret_post for PoC
225
+ if (this.cfg.clientSecret) {
226
+ body.set("client_secret", this.cfg.clientSecret);
227
+ }
228
+ const resp = await fetch(this.cfg.tokenEndpoint, {
229
+ method: "POST",
230
+ headers: { "content-type": "application/x-www-form-urlencoded" },
231
+ body,
232
+ });
233
+ if (!resp.ok) {
234
+ const txt = await resp.text().catch(() => "");
235
+ throw new Error(`Token endpoint error (${resp.status}): ${txt}`);
236
+ }
237
+ const json = await resp.json();
238
+ // Verify ID token if present
239
+ let idClaims;
240
+ if (json.id_token) {
241
+ idClaims = await this.verifyWithIssuer(json.id_token);
242
+ }
243
+ // Verify access token if it's a JWT (3 parts)
244
+ let accessClaims;
245
+ if (json.access_token && json.access_token.split(".").length === 3) {
246
+ try {
247
+ accessClaims = await this.verifyWithIssuer(json.access_token);
248
+ }
249
+ catch {
250
+ // opaque or differently signed; ignore for now
251
+ }
252
+ }
253
+ const out = {
254
+ access_token: json.access_token,
255
+ id_token: json.id_token,
256
+ token_type: json.token_type,
257
+ refresh_token: json.refresh_token,
258
+ expires_in: json.expires_in,
259
+ scope: json.scope,
260
+ raw: json,
261
+ id_claims: idClaims,
262
+ access_claims: accessClaims,
263
+ };
264
+ this.lastTokens = out;
265
+ return out;
266
+ }
267
+ /**
268
+ * Exchange a refresh_token for new tokens.
269
+ * Verifies any returned JWTs and returns a TokenResponse.
270
+ *
271
+ * @param refreshToken - The refresh token to exchange for new tokens
272
+ * @param options - Optional parameters for the refresh request
273
+ * @param options.scope - Optional scope parameter (cannot exceed originally granted scope)
274
+ * @param options.useAuthHeader - Use Authorization header instead of client_secret in body
275
+ * @returns Promise<TokenResponse> - New tokens with verified claims
276
+ */
277
+ async refreshTokens(refreshToken, options = {}) {
278
+ if (!refreshToken)
279
+ throw new Error("Missing refresh_token");
280
+ const body = new URLSearchParams({
281
+ grant_type: "refresh_token",
282
+ refresh_token: refreshToken,
283
+ client_id: this.cfg.clientId,
284
+ });
285
+ // Add scope if provided
286
+ if (options.scope) {
287
+ body.set("scope", options.scope);
288
+ }
289
+ // Prepare headers
290
+ const headers = {
291
+ "content-type": "application/x-www-form-urlencoded"
292
+ };
293
+ // Handle client authentication
294
+ if (this.cfg.clientSecret) {
295
+ if (options.useAuthHeader) {
296
+ // Use Authorization header (Basic auth with base64 encoded client_id:client_secret)
297
+ const credentials = Buffer.from(`${this.cfg.clientId}:${this.cfg.clientSecret}`).toString('base64');
298
+ headers["authorization"] = `Basic ${credentials}`;
299
+ }
300
+ else {
301
+ // Use client_secret in body (default behavior)
302
+ body.set("client_secret", this.cfg.clientSecret);
303
+ }
304
+ }
305
+ const resp = await fetch(this.cfg.tokenEndpoint, {
306
+ method: "POST",
307
+ headers,
308
+ body,
309
+ });
310
+ if (!resp.ok) {
311
+ const txt = await resp.text().catch(() => "");
312
+ let errorMessage = `Token endpoint error (${resp.status}): ${txt}`;
313
+ // Try to parse OAuth error response for better error messages
314
+ try {
315
+ const errorJson = JSON.parse(txt);
316
+ if (errorJson.error) {
317
+ errorMessage = `OAuth error (${errorJson.error}): ${errorJson.error_description || errorJson.error}`;
318
+ // Handle specific OAuth errors
319
+ if (errorJson.error === "invalid_grant") {
320
+ errorMessage += " - The refresh token is invalid or expired. User needs to re-authenticate.";
321
+ }
322
+ else if (errorJson.error === "invalid_client") {
323
+ errorMessage += " - Client authentication failed. Check client_id and client_secret.";
324
+ }
325
+ else if (errorJson.error === "invalid_scope") {
326
+ errorMessage += " - The requested scope is invalid or exceeds the originally granted scope.";
327
+ }
328
+ }
329
+ }
330
+ catch {
331
+ // If JSON parsing fails, use the original error message
332
+ }
333
+ throw new Error(errorMessage);
334
+ }
335
+ const json = await resp.json();
336
+ // Verify ID token if present
337
+ let idClaims;
338
+ if (json.id_token) {
339
+ try {
340
+ idClaims = await this.verifyWithIssuer(json.id_token);
341
+ }
342
+ catch (error) {
343
+ throw new Error(`ID token verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
344
+ }
345
+ }
346
+ // Verify access token if it's a JWT (3 parts)
347
+ let accessClaims;
348
+ if (json.access_token && json.access_token.split(".").length === 3) {
349
+ try {
350
+ accessClaims = await this.verifyWithIssuer(json.access_token);
351
+ }
352
+ catch (error) {
353
+ // Log warning but don't fail - access token might be opaque
354
+ console.warn(`Access token verification failed (may be opaque): ${error instanceof Error ? error.message : 'Unknown error'}`);
355
+ }
356
+ }
357
+ const out = {
358
+ access_token: json.access_token,
359
+ id_token: json.id_token,
360
+ token_type: json.token_type,
361
+ refresh_token: json.refresh_token,
362
+ expires_in: json.expires_in,
363
+ scope: json.scope,
364
+ raw: json,
365
+ id_claims: idClaims,
366
+ access_claims: accessClaims,
367
+ };
368
+ this.lastTokens = out;
369
+ return out;
370
+ }
371
+ /**
372
+ * Fetch and cache JWKS from the remote endpoint.
373
+ * Uses SSICache to cache the JWKS response.
374
+ * @private
375
+ */
376
+ async fetchAndCacheJWKS() {
377
+ const cacheKey = this.cfg.jwksUri;
378
+ // Use remember to get from cache or fetch and cache
379
+ const jwks = await this.jwksCache.remember(cacheKey, constants_1.TTL_JWKS, async () => {
380
+ // Fetch JWKS from remote endpoint
381
+ const response = await fetch(this.cfg.jwksUri);
382
+ if (!response.ok) {
383
+ throw new Error(`Failed to fetch JWKS from ${this.cfg.jwksUri}: ${response.status} ${response.statusText}`);
384
+ }
385
+ const jwksData = await response.json();
386
+ // Validate JWKS structure
387
+ if (!jwksData || !Array.isArray(jwksData.keys)) {
388
+ throw new Error(`Invalid JWKS structure from ${this.cfg.jwksUri}`);
389
+ }
390
+ return jwksData;
391
+ });
392
+ return jwks;
393
+ }
394
+ /**
395
+ * Verify JWT signature and claims using jose library.
396
+ * Handles JWKS fetching, caching, kid selection, and algorithm validation.
397
+ * @private
398
+ */
399
+ async verifyWithIssuer(jwt) {
400
+ // If static JWKS provided, use jose's importJWK for each key
401
+ if (this.cfg.jwks && Array.isArray(this.cfg.jwks.keys)) {
402
+ // For static JWKS, we need to import and try each key
403
+ for (const key of this.cfg.jwks.keys) {
404
+ try {
405
+ const publicKey = await (0, jose_1.importJWK)(key, "RS256");
406
+ const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
407
+ issuer: this.cfg.issuerAccepted,
408
+ audience: this.cfg.clientId,
409
+ algorithms: ["RS256"],
410
+ });
411
+ return payload;
412
+ }
413
+ catch {
414
+ // Try next key
415
+ continue;
416
+ }
417
+ }
418
+ throw new Error("No valid JWK found in static JWKS");
419
+ }
420
+ // Fetch and cache JWKS from remote endpoint
421
+ const jwks = await this.fetchAndCacheJWKS();
422
+ // Try each key in the JWKS
423
+ for (const key of jwks.keys) {
424
+ try {
425
+ const publicKey = await (0, jose_1.importJWK)(key, "RS256");
426
+ const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
427
+ issuer: this.cfg.issuerAccepted,
428
+ audience: this.cfg.clientId,
429
+ algorithms: ["RS256"],
430
+ });
431
+ return payload;
432
+ }
433
+ catch {
434
+ // Try next key
435
+ continue;
436
+ }
437
+ }
438
+ throw new Error("No valid JWK found in JWKS to verify the JWT");
439
+ }
440
+ /**
441
+ * Public method to verify a JWT.
442
+ * Use this in middleware or anywhere you need to validate tokens.
443
+ */
444
+ async verifyJwt(jwt) {
445
+ const claims = await this.verifyWithIssuer(jwt);
446
+ return claims;
447
+ }
448
+ /**
449
+ * Verify and decode a JWT before returning its claims.
450
+ * Throws if the JWT is invalid, expired, or fails signature verification.
451
+ *
452
+ * This method is safe to use in middleware or debugging contexts when you
453
+ * simply need the decoded, verified claims from a JWT string.
454
+ */
455
+ async verifyAndDecodeJwt(jwt) {
456
+ if (!jwt)
457
+ throw new Error("Missing JWT");
458
+ const parts = jwt.split(".");
459
+ if (parts.length !== 3)
460
+ throw new Error("Invalid JWT format");
461
+ // Reuse the same cryptographic verification logic
462
+ const claims = await this.verifyWithIssuer(jwt);
463
+ // If verifyWithIssuer() throws, this will never return
464
+ return claims;
465
+ }
466
+ /**
467
+ * Automatically refresh tokens if they are expired or about to expire.
468
+ * This is a convenience method that handles the refresh logic automatically.
469
+ *
470
+ * @param refreshToken - The refresh token to use for refreshing
471
+ * @param options - Optional parameters for the refresh request
472
+ * @param options.bufferSeconds - How many seconds before expiry to consider tokens as "about to expire" (default: 60)
473
+ * @param options.scope - Optional scope parameter
474
+ * @param options.useAuthHeader - Use Authorization header instead of client_secret in body
475
+ * @returns Promise<TokenResponse | null> - New tokens if refreshed, null if not needed
476
+ */
477
+ async autoRefreshTokens(refreshToken, options = {}) {
478
+ const bufferSeconds = options.bufferSeconds ?? 60;
479
+ // Check if we have current tokens and if they need refreshing
480
+ if (this.lastTokens?.access_token && this.lastTokens?.expires_in) {
481
+ const now = Math.floor(Date.now() / 1000);
482
+ const expiresAt = now + this.lastTokens.expires_in;
483
+ const refreshThreshold = now + bufferSeconds;
484
+ // If tokens are still valid for more than bufferSeconds, no need to refresh
485
+ if (expiresAt > refreshThreshold) {
486
+ return null;
487
+ }
488
+ }
489
+ try {
490
+ return await this.refreshTokens(refreshToken, {
491
+ scope: options.scope,
492
+ useAuthHeader: options.useAuthHeader
493
+ });
494
+ }
495
+ catch (error) {
496
+ // If refresh fails, clear the last tokens as they're likely invalid
497
+ this.lastTokens = undefined;
498
+ throw error;
499
+ }
500
+ }
501
+ /**
502
+ * Check if the current tokens are expired or about to expire.
503
+ *
504
+ * @param bufferSeconds - How many seconds before expiry to consider tokens as "about to expire" (default: 60)
505
+ * @returns boolean - true if tokens need refreshing, false otherwise
506
+ */
507
+ needsTokenRefresh(bufferSeconds = 60) {
508
+ if (!this.lastTokens?.access_token || !this.lastTokens?.expires_in) {
509
+ return true; // No tokens or missing expiry info
510
+ }
511
+ const now = Math.floor(Date.now() / 1000);
512
+ const expiresAt = now + this.lastTokens.expires_in;
513
+ const refreshThreshold = now + bufferSeconds;
514
+ return expiresAt <= refreshThreshold;
515
+ }
516
+ /**
517
+ * Get the time until the current access token expires.
518
+ *
519
+ * @returns number - Seconds until expiry, or 0 if no token or expired
520
+ */
521
+ getTokenExpirySeconds() {
522
+ if (!this.lastTokens?.expires_in) {
523
+ return 0;
524
+ }
525
+ const now = Math.floor(Date.now() / 1000);
526
+ const expiresAt = now + this.lastTokens.expires_in;
527
+ const timeLeft = expiresAt - now;
528
+ return Math.max(0, timeLeft);
529
+ }
530
+ /** Returns the last TokenResponse produced by handleCallback/refreshTokens in this instance */
531
+ getLastTokenResponse() {
532
+ return this.lastTokens;
533
+ }
534
+ }
535
+ exports.AuthServer = AuthServer;
536
+ /**
537
+ * USAGE EXAMPLES
538
+ *
539
+ * // Basic token refresh
540
+ * const auth = new AuthServer(config);
541
+ * const newTokens = await auth.refreshTokens(refreshToken);
542
+ *
543
+ * // Refresh with Authorization header instead of client_secret in body
544
+ * const newTokens = await auth.refreshTokens(refreshToken, {
545
+ * useAuthHeader: true
546
+ * });
547
+ *
548
+ * // Refresh with specific scope (cannot exceed originally granted scope)
549
+ * const newTokens = await auth.refreshTokens(refreshToken, {
550
+ * scope: "openid profile email offline_access"
551
+ * });
552
+ *
553
+ * // Automatic refresh - only refreshes if tokens are expired or about to expire
554
+ * const newTokens = await auth.autoRefreshTokens(refreshToken, {
555
+ * bufferSeconds: 120, // Refresh if expires within 2 minutes
556
+ * useAuthHeader: true
557
+ * });
558
+ *
559
+ * // Check if tokens need refreshing
560
+ * if (auth.needsTokenRefresh(60)) {
561
+ * const newTokens = await auth.refreshTokens(refreshToken);
562
+ * }
563
+ *
564
+ * // Get time until token expires
565
+ * const secondsUntilExpiry = auth.getTokenExpirySeconds();
566
+ * console.log(`Token expires in ${secondsUntilExpiry} seconds`);
567
+ *
568
+ * // Error handling
569
+ * try {
570
+ * const newTokens = await auth.refreshTokens(refreshToken);
571
+ * // Use new tokens...
572
+ * } catch (error) {
573
+ * if (error.message.includes('invalid_grant')) {
574
+ * // Refresh token is invalid/expired - redirect to login
575
+ * return Response.redirect('/login');
576
+ * }
577
+ * // Handle other errors...
578
+ * }
579
+ */
@@ -0,0 +1,40 @@
1
+ import type { CacheInitOpts } from './types';
2
+ export declare class SSICache {
3
+ private backend;
4
+ private container;
5
+ private prefix?;
6
+ private static __shared?;
7
+ private constructor();
8
+ static fromEnv(): SSICache;
9
+ static init(opts: CacheInitOpts): SSICache;
10
+ /**
11
+ * Returns a cache instance pinned to a prefix ("jwks", "oidc", "rl", etc.).
12
+ */
13
+ withPrefix(prefix?: string): SSICache;
14
+ private k;
15
+ get<T = unknown>(key: string): Promise<T | null>;
16
+ set<T = unknown>(key: string, value: T, ttlSec: number): Promise<void>;
17
+ del(key: string): Promise<void>;
18
+ mget<T = unknown>(keys: string[]): Promise<(T | null)[]>;
19
+ mset<T = unknown>(entries: Array<{
20
+ key: string;
21
+ value: T;
22
+ ttlSec: number;
23
+ }>): Promise<void>;
24
+ incrby(key: string, by?: number, ttlSec?: number): Promise<number>;
25
+ ttl(key: string): Promise<number | null>;
26
+ /**
27
+ * compute-if-absent: get from cache or compute and cache the value
28
+ */
29
+ remember<T>(key: string, ttlSec: number, loader: () => Promise<T>): Promise<T>;
30
+ /**
31
+ * Simple distributed lock (best-effort): returns lock token if acquired
32
+ * Only works properly with Redis backend; memory backend returns a naive token
33
+ */
34
+ acquireLock(key: string, ttlMs: number): Promise<string | null>;
35
+ /**
36
+ * Release a lock acquired via acquireLock
37
+ */
38
+ releaseLock(key: string, token: string): Promise<void>;
39
+ close(): Promise<void>;
40
+ }