@serialsubscriptions/platform-integration 0.0.8-5.1

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