@serialsubscriptions/platform-integration 0.0.85 → 0.85.4

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.
@@ -3,6 +3,14 @@ import type { AuthConfigInput } from "./requestConfig";
3
3
  export declare const scopes: {
4
4
  readonly defaultScopes: "openid profile email view_project create_project delete_project update_project view_subscribed_plan view_subscribed_feature view_subscribed_limit access_subscription_usage";
5
5
  };
6
+ /**
7
+ * Get client id and secret for an audience from env (for token endpoint use).
8
+ * If aud != SSI_CLIENT_ID, uses SSI_CLIENT_ID_<UPPERCASE(aud)> and SSI_CLIENT_SECRET_<UPPERCASE(aud)> when set.
9
+ */
10
+ export declare function getClientCredentialsForAudience(aud: string, defaultClientId: string, defaultClientSecret: string): {
11
+ clientId: string;
12
+ clientSecret: string;
13
+ };
6
14
  /**
7
15
  * Check if a JWT is expired (or within bufferSeconds of expiry) by decoding its payload.
8
16
  * Does not verify the signature — use only for expiry checks (e.g. "should I refresh?").
@@ -36,6 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  })();
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.AuthServer = exports.scopes = void 0;
39
+ exports.getClientCredentialsForAudience = getClientCredentialsForAudience;
39
40
  exports.isJwtExpired = isJwtExpired;
40
41
  exports.getJwtExpirySeconds = getJwtExpirySeconds;
41
42
  const crypto = __importStar(require("crypto"));
@@ -51,6 +52,61 @@ exports.scopes = {
51
52
  function normalizeIssuer(url) {
52
53
  return url.replace(/\/+$/, "");
53
54
  }
55
+ /**
56
+ * Build env key for alternate client id/secret by audience.
57
+ * e.g. aud "scheduled_job_client" -> "SSI_CLIENT_ID_SCHEDULED_JOB_CLIENT"
58
+ */
59
+ function audienceToEnvSuffix(aud) {
60
+ return String(aud).toUpperCase().replace(/-/g, "_");
61
+ }
62
+ /**
63
+ * Resolve allowed audiences for JWT verification.
64
+ * If token aud != default client id, check SSI_CLIENT_ID_<UPPERCASE(aud)>; if set, that value is accepted as audience.
65
+ */
66
+ function getAllowedAudiences(jwt, defaultClientId) {
67
+ const allowed = [defaultClientId];
68
+ let payload;
69
+ try {
70
+ payload = (0, jose_1.decodeJwt)(jwt);
71
+ }
72
+ catch {
73
+ return allowed;
74
+ }
75
+ const aud = payload.aud;
76
+ const audList = aud == null ? [] : Array.isArray(aud) ? aud.map(String) : [String(aud)];
77
+ for (const a of audList) {
78
+ if (a && a !== defaultClientId) {
79
+ const envKey = `SSI_CLIENT_ID_${audienceToEnvSuffix(a)}`;
80
+ const alt = process.env[envKey]?.trim();
81
+ if (alt) {
82
+ if (!allowed.includes(alt))
83
+ allowed.push(alt);
84
+ // Also accept the token's aud value so verification passes when issuer puts aud in token
85
+ if (a !== alt && !allowed.includes(a))
86
+ allowed.push(a);
87
+ }
88
+ }
89
+ }
90
+ return allowed;
91
+ }
92
+ /**
93
+ * Get client id and secret for an audience from env (for token endpoint use).
94
+ * If aud != SSI_CLIENT_ID, uses SSI_CLIENT_ID_<UPPERCASE(aud)> and SSI_CLIENT_SECRET_<UPPERCASE(aud)> when set.
95
+ */
96
+ function getClientCredentialsForAudience(aud, defaultClientId, defaultClientSecret) {
97
+ if (!aud || aud === defaultClientId)
98
+ return { clientId: defaultClientId, clientSecret: defaultClientSecret };
99
+ const suffix = audienceToEnvSuffix(aud);
100
+ const clientId = process.env[`SSI_CLIENT_ID_${suffix}`]?.trim();
101
+ const clientSecret = process.env[`SSI_CLIENT_SECRET_${suffix}`]?.trim();
102
+ if (clientId) {
103
+ return {
104
+ clientId,
105
+ clientSecret: clientSecret ?? defaultClientSecret,
106
+ };
107
+ }
108
+ return { clientId: defaultClientId, clientSecret: defaultClientSecret };
109
+ }
54
110
  /**
55
111
  * Check if a JWT is expired (or within bufferSeconds of expiry) by decoding its payload.
56
112
  * Does not verify the signature — use only for expiry checks (e.g. "should I refresh?").
@@ -442,6 +498,7 @@ class AuthServer {
442
498
  * @private
443
499
  */
444
500
  async verifyWithIssuer(jwt) {
501
+ const allowedAudiences = getAllowedAudiences(jwt, this.cfg.clientId);
445
502
  // If static JWKS provided, use jose's importJWK for each key
446
503
  if (this.cfg.jwks && Array.isArray(this.cfg.jwks.keys)) {
447
504
  // For static JWKS, we need to import and try each key
@@ -450,7 +507,7 @@ class AuthServer {
450
507
  const publicKey = await (0, jose_1.importJWK)(key, "RS256");
451
508
  const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
452
509
  issuer: this.cfg.issuerAccepted,
453
- audience: this.cfg.clientId,
510
+ audience: allowedAudiences,
454
511
  algorithms: ["RS256"],
455
512
  });
456
513
  return payload;
@@ -470,7 +527,7 @@ class AuthServer {
470
527
  const publicKey = await (0, jose_1.importJWK)(key, "RS256");
471
528
  const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
472
529
  issuer: this.cfg.issuerAccepted,
473
- audience: this.cfg.clientId,
530
+ audience: allowedAudiences,
474
531
  algorithms: ["RS256"],
475
532
  });
476
533
  return payload;
@@ -6,7 +6,7 @@
6
6
  * and other client code stay typed.
7
7
  *
8
8
  * - **baseUrl**: App origin (e.g. window.location.origin or NEXT_PUBLIC_APP_URL).
9
- * - **apiUrl**: App API base; where your apps routes live (e.g. /api/v1/auth/session).
9
+ * - **apiUrl**: App API base; where your app's routes live (e.g. /api/v1/auth/session).
10
10
  * Pass to SessionClient.getSessionClient(config.apiUrl ?? config.baseUrl).
11
11
  * - **ssiApiUrl**: SSI Platform API base (for links, account portal, etc.).
12
12
  * - **ssiIssuerBaseUrl**: SSI issuer URL (often same as ssiApiUrl; e.g. for /pricing links).
@@ -18,7 +18,25 @@ export type SSIClientConfig = {
18
18
  ssiIssuerBaseUrl?: string | null;
19
19
  };
20
20
  /**
21
- * Returns the URL to pass to SessionClient (app base so it can call the app’s session endpoint).
21
+ * Base app URL (client): NEXT_PUBLIC_APP_URL or (in browser) window.location.origin.
22
+ */
23
+ export declare function getClientBaseUrl(): string | null;
24
+ /**
25
+ * App API base URL (client): NEXT_PUBLIC_API_URL or baseUrl.
26
+ */
27
+ export declare function getClientApiUrl(baseUrl: string | null): string | null;
28
+ /**
29
+ * SSI Platform API base URL (client): NEXT_PUBLIC_SSI_API_BASE_URL or derived from baseUrl
30
+ * hostname (e.g. https://&lt;subdomain&gt;.cerealstackdev.com on known domains).
31
+ */
32
+ export declare function getClientSsiApiUrl(baseUrl: string | null): string | null;
33
+ /**
34
+ * Build SSIClientConfig from public env and (in browser) window.location.
35
+ * Use in client components; for SessionClient pass the result to getSessionClient(config).
36
+ */
37
+ export declare function getClientConfig(): SSIClientConfig;
38
+ /**
39
+ * Returns the URL to pass to SessionClient (app base so it can call the app's session endpoint).
22
40
  * Prefers apiUrl so the session route is resolved correctly when app and API differ.
23
41
  */
24
42
  export declare function getSessionClientBaseUrl(config: SSIClientConfig | undefined | null): string | undefined;
@@ -1,10 +1,75 @@
1
+ "use client";
1
2
  "use strict";
2
- // clientConfig.ts
3
- // Client-side config types and helpers. No server/auth dependencies so safe for frontend bundle.
4
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getClientBaseUrl = getClientBaseUrl;
5
+ exports.getClientApiUrl = getClientApiUrl;
6
+ exports.getClientSsiApiUrl = getClientSsiApiUrl;
7
+ exports.getClientConfig = getClientConfig;
5
8
  exports.getSessionClientBaseUrl = getSessionClientBaseUrl;
9
+ function trimTrailingSlash(url) {
10
+ return url.replace(/\/$/, "");
11
+ }
12
+ /**
13
+ * Base app URL (client): NEXT_PUBLIC_APP_URL or (in browser) window.location.origin.
14
+ */
15
+ function getClientBaseUrl() {
16
+ const baseUrl = process.env.NEXT_PUBLIC_APP_URL ||
17
+ (typeof window !== "undefined" ? window.location.origin : null);
18
+ return baseUrl ? trimTrailingSlash(baseUrl) : null;
19
+ }
20
+ /**
21
+ * App API base URL (client): NEXT_PUBLIC_API_URL or baseUrl.
22
+ */
23
+ function getClientApiUrl(baseUrl) {
24
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || (baseUrl ?? null);
25
+ return apiUrl ? trimTrailingSlash(apiUrl) : null;
26
+ }
27
+ /**
28
+ * SSI Platform API base URL (client): NEXT_PUBLIC_SSI_API_BASE_URL or derived from baseUrl
29
+ * hostname (e.g. https://&lt;subdomain&gt;.cerealstackdev.com on known domains).
30
+ */
31
+ function getClientSsiApiUrl(baseUrl) {
32
+ if (!baseUrl)
33
+ return null;
34
+ if (process.env.NEXT_PUBLIC_SSI_API_BASE_URL) {
35
+ return trimTrailingSlash(process.env.NEXT_PUBLIC_SSI_API_BASE_URL);
36
+ }
37
+ try {
38
+ const url = new URL(baseUrl);
39
+ const hostname = url.hostname;
40
+ const validDomain = hostname.endsWith(".cerealstackdev.com") ||
41
+ hostname.endsWith(".cerealstackqa.com") ||
42
+ hostname.endsWith(".cerealstack.com");
43
+ if (!validDomain)
44
+ return null;
45
+ const parts = hostname.split(".");
46
+ if (parts.length < 3)
47
+ return null;
48
+ const subdomain = parts[parts.length - 3];
49
+ const domain = parts.slice(-2).join(".");
50
+ return `https://${subdomain}.${domain}`;
51
+ }
52
+ catch {
53
+ return null;
54
+ }
55
+ }
56
+ /**
57
+ * Build SSIClientConfig from public env and (in browser) window.location.
58
+ * Use in client components; for SessionClient pass the result to getSessionClient(config).
59
+ */
60
+ function getClientConfig() {
61
+ const baseUrl = getClientBaseUrl();
62
+ const apiUrl = getClientApiUrl(baseUrl);
63
+ const ssiApiUrl = getClientSsiApiUrl(baseUrl);
64
+ return {
65
+ baseUrl,
66
+ apiUrl,
67
+ ssiApiUrl,
68
+ ssiIssuerBaseUrl: ssiApiUrl,
69
+ };
70
+ }
6
71
  /**
7
- * Returns the URL to pass to SessionClient (app base so it can call the apps session endpoint).
72
+ * Returns the URL to pass to SessionClient (app base so it can call the app's session endpoint).
8
73
  * Prefers apiUrl so the session route is resolved correctly when app and API differ.
9
74
  */
10
75
  function getSessionClientBaseUrl(config) {
@@ -1,3 +1,3 @@
1
1
  export { SessionClient } from './session/SessionClient';
2
2
  export type { SSIClientConfig } from '../clientConfig';
3
- export { getSessionClientBaseUrl } from '../clientConfig';
3
+ export { getClientConfig, getSessionClientBaseUrl, getClientBaseUrl, getClientApiUrl, getClientSsiApiUrl, } from '../clientConfig';
@@ -1,8 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSessionClientBaseUrl = exports.SessionClient = void 0;
3
+ exports.getClientSsiApiUrl = exports.getClientApiUrl = exports.getClientBaseUrl = exports.getSessionClientBaseUrl = exports.getClientConfig = exports.SessionClient = void 0;
4
4
  // Browser/middleware-safe exports only (no auth.server / requestConfig)
5
5
  var SessionClient_1 = require("./session/SessionClient");
6
6
  Object.defineProperty(exports, "SessionClient", { enumerable: true, get: function () { return SessionClient_1.SessionClient; } });
7
7
  var clientConfig_1 = require("../clientConfig");
8
+ Object.defineProperty(exports, "getClientConfig", { enumerable: true, get: function () { return clientConfig_1.getClientConfig; } });
8
9
  Object.defineProperty(exports, "getSessionClientBaseUrl", { enumerable: true, get: function () { return clientConfig_1.getSessionClientBaseUrl; } });
10
+ Object.defineProperty(exports, "getClientBaseUrl", { enumerable: true, get: function () { return clientConfig_1.getClientBaseUrl; } });
11
+ Object.defineProperty(exports, "getClientApiUrl", { enumerable: true, get: function () { return clientConfig_1.getClientApiUrl; } });
12
+ Object.defineProperty(exports, "getClientSsiApiUrl", { enumerable: true, get: function () { return clientConfig_1.getClientSsiApiUrl; } });
@@ -22,6 +22,29 @@ export type SSIRequestConfig = {
22
22
  ssiRedirectUri: string | null;
23
23
  ssiClientId: string | null;
24
24
  };
25
+ /**
26
+ * Get the base URL from environment or request headers.
27
+ * If NEXT_PUBLIC_APP_URL is set, returns it; otherwise derives from
28
+ * x-forwarded-proto, x-forwarded-host, origin, or host.
29
+ */
30
+ export declare function getBaseUrl(req: Request): string | null;
31
+ /** When baseUrl is a single URL or comma-separated list, return the first URL. */
32
+ export declare function firstBaseUrl(baseUrl: string | null): string | null;
33
+ /** App API base URL: NEXT_PUBLIC_API_URL or baseUrl. */
34
+ export declare function getBaseApiUrl(req: Request): string | null;
35
+ /** SSI client id: SSI_CLIENT_ID env or subdomain on known Cereal Stack domains. */
36
+ export declare function getSsiClientId(baseUrl: string): string | null;
37
+ /** SSI issuer base URL: SSI_ISSUER_BASE_URL env or derived from host (getSsiApiUrl). */
38
+ export declare function getIssuerBaseUrl(baseUrl: string): string | null;
39
+ /**
40
+ * SSI Platform API base URL: SSI_API_BASE_URL env or derived from host
41
+ * (e.g. https://&lt;subdomain&gt;.cerealstackdev.com on known domains).
42
+ */
43
+ export declare function getSsiApiUrl(baseUrl: string): string | null;
44
+ /** SSI redirect URI: SSI_REDIRECT_URI env or `${baseUrl}/api/v1/auth/callback`. */
45
+ export declare function getSsiRedirectUri(baseUrl: string): string | null;
46
+ export declare function getRequestConfig(req: Request): SSIRequestConfig;
47
+ export declare function getRequestConfig(headers: Headers): SSIRequestConfig;
25
48
  /**
26
49
  * Union type accepted anywhere AuthConfig is accepted (AuthServer, SessionManager).
27
50
  */
@@ -1,9 +1,120 @@
1
1
  "use strict";
2
2
  // requestConfig.ts
3
- // Types for config that host apps derive per-request and pass into AuthServer, SessionManager, and SSI API clients.
3
+ // Types and helpers for config that host apps derive per-request and pass into AuthServer, SessionManager, and SSI API clients.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.getBaseUrl = getBaseUrl;
6
+ exports.firstBaseUrl = firstBaseUrl;
7
+ exports.getBaseApiUrl = getBaseApiUrl;
8
+ exports.getSsiClientId = getSsiClientId;
9
+ exports.getIssuerBaseUrl = getIssuerBaseUrl;
10
+ exports.getSsiApiUrl = getSsiApiUrl;
11
+ exports.getSsiRedirectUri = getSsiRedirectUri;
12
+ exports.getRequestConfig = getRequestConfig;
5
13
  exports.toAuthConfig = toAuthConfig;
6
14
  exports.normalizeAuthConfig = normalizeAuthConfig;
15
+ // --- Helpers used by getRequestConfig (env: NEXT_PUBLIC_APP_URL, SSI_*, request headers) ---
16
+ /**
17
+ * Get the base URL from environment or request headers.
18
+ * If NEXT_PUBLIC_APP_URL is set, returns it; otherwise derives from
19
+ * x-forwarded-proto, x-forwarded-host, origin, or host.
20
+ */
21
+ function getBaseUrl(req) {
22
+ const fromEnv = process.env.NEXT_PUBLIC_APP_URL?.trim();
23
+ if (fromEnv)
24
+ return fromEnv;
25
+ const proto = req.headers.get('x-forwarded-proto') || 'https';
26
+ const forwardedHost = req.headers.get('x-forwarded-host');
27
+ if (forwardedHost)
28
+ return `${proto}://${forwardedHost}`;
29
+ const origin = req.headers.get('origin');
30
+ if (origin)
31
+ return origin;
32
+ const host = req.headers.get('host');
33
+ const hostWithoutPort = host ? host.split(':')[0] : undefined;
34
+ if (hostWithoutPort)
35
+ return `${proto}://${hostWithoutPort}`;
36
+ return null;
37
+ }
38
+ /** When baseUrl is a single URL or comma-separated list, return the first URL. */
39
+ function firstBaseUrl(baseUrl) {
40
+ if (!baseUrl)
41
+ return null;
42
+ const first = baseUrl.split(',')[0].trim();
43
+ return first || null;
44
+ }
45
+ /** App API base URL: NEXT_PUBLIC_API_URL or baseUrl. */
46
+ function getBaseApiUrl(req) {
47
+ return process.env.NEXT_PUBLIC_API_URL || getBaseUrl(req) || null;
48
+ }
49
+ /** SSI client id: SSI_CLIENT_ID env or subdomain on known Cereal Stack domains. */
50
+ function getSsiClientId(baseUrl) {
51
+ if (process.env.SSI_CLIENT_ID)
52
+ return process.env.SSI_CLIENT_ID;
53
+ try {
54
+ const url = new URL(baseUrl);
55
+ url.pathname = url.pathname.replace(/\/$/, '');
56
+ const hostname = url.hostname;
57
+ if (hostname.endsWith('.cerealstackdev.com') || hostname.endsWith('.cerealstackqa.com') || hostname.endsWith('.cerealstack.com')) {
58
+ const domainParts = hostname.split('.');
59
+ const subdomain = domainParts[domainParts.length - 3];
60
+ return subdomain ?? null;
61
+ }
62
+ }
63
+ catch {
64
+ // ignore
65
+ }
66
+ return null;
67
+ }
68
+ /** SSI issuer base URL: SSI_ISSUER_BASE_URL env or derived from host (getSsiApiUrl). */
69
+ function getIssuerBaseUrl(baseUrl) {
70
+ return process.env.SSI_ISSUER_BASE_URL || getSsiApiUrl(baseUrl) || null;
71
+ }
72
+ /**
73
+ * SSI Platform API base URL: SSI_API_BASE_URL env or derived from host
74
+ * (e.g. https://&lt;subdomain&gt;.cerealstackdev.com on known domains).
75
+ */
76
+ function getSsiApiUrl(baseUrl) {
77
+ if (process.env.SSI_API_BASE_URL)
78
+ return process.env.SSI_API_BASE_URL;
79
+ try {
80
+ const url = new URL(baseUrl);
81
+ url.pathname = url.pathname.replace(/\/$/, '');
82
+ const hostname = url.hostname;
83
+ if (hostname.endsWith('.cerealstackdev.com') || hostname.endsWith('.cerealstackqa.com') || hostname.endsWith('.cerealstack.com')) {
84
+ const domainParts = hostname.split('.');
85
+ const subdomain = domainParts[domainParts.length - 3];
86
+ const domain = domainParts[domainParts.length - 2] + '.' + domainParts[domainParts.length - 1];
87
+ return `https://${subdomain}.${domain}`;
88
+ }
89
+ }
90
+ catch {
91
+ // ignore
92
+ }
93
+ return null;
94
+ }
95
+ /** SSI redirect URI: SSI_REDIRECT_URI env or `${baseUrl}/api/v1/auth/callback`. */
96
+ function getSsiRedirectUri(baseUrl) {
97
+ return process.env.SSI_REDIRECT_URI || `${baseUrl}/api/v1/auth/callback`;
98
+ }
99
+ /**
100
+ * Build SSIRequestConfig from the current request (or headers).
101
+ * Uses env vars (NEXT_PUBLIC_APP_URL, SSI_*, etc.) and request headers for base URL and SSI URLs.
102
+ * Apps can use this as-is or extend the result with app-specific fields (e.g. prisma, dbUrl).
103
+ */
104
+ function getRequestConfig(arg) {
105
+ const req = arg instanceof Headers ? new Request('http://local', { headers: arg }) : arg;
106
+ const rawBaseUrl = getBaseUrl(req);
107
+ const baseUrl = firstBaseUrl(rawBaseUrl);
108
+ const fallback = baseUrl || '';
109
+ return {
110
+ baseUrl: baseUrl ?? null,
111
+ apiUrl: getBaseApiUrl(req),
112
+ ssiApiUrl: getSsiApiUrl(fallback),
113
+ ssiIssuerBaseUrl: getIssuerBaseUrl(fallback),
114
+ ssiRedirectUri: getSsiRedirectUri(fallback),
115
+ ssiClientId: getSsiClientId(fallback),
116
+ };
117
+ }
7
118
  function isSSIRequestConfig(x) {
8
119
  return x != null && typeof x === 'object' && 'ssiIssuerBaseUrl' in x;
9
120
  }
@@ -42,10 +42,18 @@ export declare class SessionManager {
42
42
  private auth?;
43
43
  private _sessionId?;
44
44
  private authConfig?;
45
+ private bearerSession?;
46
+ private pendingBearerToken?;
45
47
  private static inFlightRefresh;
46
48
  private freshnessOnce;
47
49
  /** Accepts AuthConfig or SSIRequestConfig (e.g. from getRequestConfig(req)). */
48
50
  constructor(storage: SSIStorage, authConfig?: AuthConfigInput);
51
+ /**
52
+ * Create a session from environment: cookie or Authorization: Bearer JWT.
53
+ * When a Request is passed and contains "Authorization: Bearer <token>", the token is verified
54
+ * (valid and not expired); a session id is generated but not cached. Cookie is used only when
55
+ * no Bearer header is present.
56
+ */
49
57
  static fromEnv(): SessionManager;
50
58
  static fromEnv(cookieHeader: string | null, authConfig?: AuthConfigInput): SessionManager;
51
59
  static fromEnv(request: Request, authConfig?: AuthConfigInput): SessionManager;
@@ -70,6 +78,17 @@ export declare class SessionManager {
70
78
  static fromEnvAsync(cookieHeader: string | null, authConfig?: AuthConfigInput): Promise<SessionManager>;
71
79
  static fromEnvAsync(request: Request, authConfig?: AuthConfigInput): Promise<SessionManager>;
72
80
  static fromEnvAsync(authConfig: AuthConfigInput): Promise<SessionManager>;
81
+ /**
82
+ * Parse Authorization: Bearer <token> from Request or Headers.
83
+ * Returns the JWT string or null if missing/invalid format.
84
+ */
85
+ private static parseBearerToken;
86
+ /**
87
+ * Verify the Bearer JWT and set ephemeral session (not cached).
88
+ * When token is provided (async path), generates a new sessionId.
89
+ * When called with no args, uses pendingBearerToken (sync path, lazy verification).
90
+ */
91
+ private verifyAndSetBearerSession;
73
92
  private parseCookies;
74
93
  get sessionId(): string | undefined;
75
94
  /** Cryptographically strong, URL-safe session id */
@@ -112,11 +131,13 @@ export declare class SessionManager {
112
131
  /**
113
132
  * Get session data by key. For 'claims', decodes the id_token payload
114
133
  * WITHOUT verification (use your verifier upstream for security).
134
+ * Bearer JWT sessions: returns from verified ephemeral data (not cached).
115
135
  */
116
136
  getSessionData(sessionId: string, dataKey: SessionDataKey): Promise<KVValue | null>;
117
137
  private ensureFreshOnce;
118
138
  /**
119
139
  * Delete all keys under this session object (id/access/refresh).
140
+ * For Bearer sessions, clears in-memory state only (nothing was cached).
120
141
  */
121
142
  clearSession(sessionId: string): Promise<string>;
122
143
  /**
@@ -57,9 +57,16 @@ class SessionManager {
57
57
  }
58
58
  const session = new SessionManager(SSIStorage_1.SSIStorage.fromEnv(), actualAuthConfig);
59
59
  if (cookieHeaderOrRequest instanceof Request) {
60
- const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
61
- if (cookieHeader) {
62
- session._sessionId = session.parseCookies(cookieHeader);
60
+ const bearerToken = SessionManager.parseBearerToken(cookieHeaderOrRequest);
61
+ if (bearerToken) {
62
+ session._sessionId = session.generateSessionId();
63
+ session.pendingBearerToken = bearerToken;
64
+ }
65
+ else {
66
+ const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
67
+ if (cookieHeader) {
68
+ session._sessionId = session.parseCookies(cookieHeader);
69
+ }
63
70
  }
64
71
  }
65
72
  else if (cookieHeaderOrRequest) {
@@ -84,34 +91,89 @@ class SessionManager {
84
91
  }
85
92
  const session = new SessionManager(SSIStorage_1.SSIStorage.fromEnv(), actualAuthConfig);
86
93
  if (cookieHeaderOrRequest instanceof Request) {
87
- const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
88
- if (cookieHeader) {
89
- session._sessionId = session.parseCookies(cookieHeader);
94
+ const bearerToken = SessionManager.parseBearerToken(cookieHeaderOrRequest);
95
+ if (bearerToken) {
96
+ await session.verifyAndSetBearerSession(bearerToken);
97
+ }
98
+ else {
99
+ const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
100
+ if (cookieHeader) {
101
+ session._sessionId = session.parseCookies(cookieHeader);
102
+ }
90
103
  }
91
104
  }
92
105
  else if (cookieHeaderOrRequest) {
93
106
  session._sessionId = session.parseCookies(cookieHeaderOrRequest);
94
107
  }
95
108
  else {
96
- // When called with no arguments, try to auto-detect cookie header from Next.js context
109
+ // When called with no arguments, try to auto-detect from Next.js context
97
110
  try {
98
- // Dynamic import of next/headers - only available in Next.js server context
99
111
  const { headers } = await import('next/headers');
100
112
  const headersList = await headers();
101
113
  if (headersList && typeof headersList.get === 'function') {
102
- const cookieHeader = headersList.get('cookie');
103
- if (cookieHeader) {
104
- session._sessionId = session.parseCookies(cookieHeader);
114
+ const authHeader = headersList.get('authorization');
115
+ const bearerToken = authHeader?.trim().toLowerCase().startsWith('bearer ')
116
+ ? authHeader.trim().slice(7).trim() || null
117
+ : null;
118
+ if (bearerToken) {
119
+ await session.verifyAndSetBearerSession(bearerToken);
120
+ }
121
+ else {
122
+ const cookieHeader = headersList.get('cookie');
123
+ if (cookieHeader) {
124
+ session._sessionId = session.parseCookies(cookieHeader);
125
+ }
105
126
  }
106
127
  }
107
128
  }
108
129
  catch {
109
130
  // Not in Next.js context or headers() not available - silently continue
110
- // This is expected when called outside of Next.js or when creating new sessions
111
131
  }
112
132
  }
113
133
  return session;
114
134
  }
135
+ /**
136
+ * Parse Authorization: Bearer <token> from Request or Headers.
137
+ * Returns the JWT string or null if missing/invalid format.
138
+ */
139
+ static parseBearerToken(requestOrHeaders) {
140
+ const auth = requestOrHeaders instanceof Request
141
+ ? requestOrHeaders.headers.get('authorization')
142
+ : requestOrHeaders.get('authorization');
143
+ if (!auth || typeof auth !== 'string')
144
+ return null;
145
+ const trimmed = auth.trim();
146
+ if (!trimmed.toLowerCase().startsWith('bearer '))
147
+ return null;
148
+ const token = trimmed.slice(7).trim();
149
+ return token || null;
150
+ }
151
+ /**
152
+ * Verify the Bearer JWT and set ephemeral session (not cached).
153
+ * When token is provided (async path), generates a new sessionId.
154
+ * When called with no args, uses pendingBearerToken (sync path, lazy verification).
155
+ */
156
+ async verifyAndSetBearerSession(token) {
157
+ const raw = token ?? this.pendingBearerToken;
158
+ if (!raw)
159
+ return;
160
+ const auth = this.getAuth();
161
+ let claims;
162
+ try {
163
+ claims = (await auth.verifyAndDecodeJwt(raw));
164
+ }
165
+ catch {
166
+ this._sessionId = undefined;
167
+ this.pendingBearerToken = undefined;
168
+ throw new Error('Invalid or expired Bearer JWT');
169
+ }
170
+ const exp = typeof claims.exp === 'number' ? claims.exp : 0;
171
+ const sessionId = token ? this.generateSessionId() : this._sessionId;
172
+ if (token)
173
+ this._sessionId = sessionId;
174
+ this.bearerSession = { sessionId, accessToken: raw, claims, exp };
175
+ this.pendingBearerToken = undefined;
176
+ }
115
177
  parseCookies(header) {
116
178
  const cookieName = process.env.SSI_COOKIE_NAME ?? 'ssi_session';
117
179
  for (const part of header.split(/;\s*/)) {
@@ -212,8 +274,43 @@ class SessionManager {
212
274
  /**
213
275
  * Get session data by key. For 'claims', decodes the id_token payload
214
276
  * WITHOUT verification (use your verifier upstream for security).
277
+ * Bearer JWT sessions: returns from verified ephemeral data (not cached).
215
278
  */
216
279
  async getSessionData(sessionId, dataKey) {
280
+ if (this.bearerSession?.sessionId === sessionId) {
281
+ const b = this.bearerSession;
282
+ if (dataKey === 'claims')
283
+ return b.claims;
284
+ if (dataKey === 'access_token')
285
+ return b.accessToken;
286
+ if (dataKey === 'id_token')
287
+ return b.accessToken;
288
+ if (dataKey === 'refresh_token')
289
+ return null;
290
+ return null;
291
+ }
292
+ if (this.pendingBearerToken && this._sessionId === sessionId) {
293
+ try {
294
+ await this.verifyAndSetBearerSession();
295
+ }
296
+ catch {
297
+ this._sessionId = undefined;
298
+ this.pendingBearerToken = undefined;
299
+ return null;
300
+ }
301
+ if (this.bearerSession?.sessionId === sessionId) {
302
+ const b = this.bearerSession;
303
+ if (dataKey === 'claims')
304
+ return b.claims;
305
+ if (dataKey === 'access_token')
306
+ return b.accessToken;
307
+ if (dataKey === 'id_token')
308
+ return b.accessToken;
309
+ if (dataKey === 'refresh_token')
310
+ return null;
311
+ return null;
312
+ }
313
+ }
217
314
  const keyToCheck = dataKey === 'claims' ? 'id_token' : dataKey;
218
315
  try {
219
316
  await this.ensureFreshOnce(sessionId);
@@ -249,6 +346,11 @@ class SessionManager {
249
346
  return value;
250
347
  }
251
348
  ensureFreshOnce(sessionId) {
349
+ if (this.bearerSession?.sessionId === sessionId)
350
+ return Promise.resolve();
351
+ if (this.pendingBearerToken && this._sessionId === sessionId) {
352
+ return this.verifyAndSetBearerSession();
353
+ }
252
354
  const existing = this.freshnessOnce.get(sessionId);
253
355
  if (existing)
254
356
  return existing;
@@ -319,9 +421,17 @@ class SessionManager {
319
421
  }
320
422
  /**
321
423
  * Delete all keys under this session object (id/access/refresh).
424
+ * For Bearer sessions, clears in-memory state only (nothing was cached).
322
425
  */
323
426
  async clearSession(sessionId) {
324
427
  const deleteCookieHeader = this.buildSessionCookie('', { maxAge: 0 });
428
+ if (this.bearerSession?.sessionId === sessionId || (this.pendingBearerToken && this._sessionId === sessionId)) {
429
+ this.bearerSession = undefined;
430
+ this.pendingBearerToken = undefined;
431
+ if (this._sessionId === sessionId)
432
+ this._sessionId = undefined;
433
+ return deleteCookieHeader;
434
+ }
325
435
  const keys = (await this.storage.keysForObject(sessionId)) ?? [];
326
436
  if (!keys.length)
327
437
  return deleteCookieHeader;
@@ -348,11 +458,33 @@ class SessionManager {
348
458
  }
349
459
  /** Returns remaining TTL in seconds for this session's id_token, or null if none */
350
460
  async getSessionTtlSeconds(sessionId) {
461
+ if (this.bearerSession?.sessionId === sessionId) {
462
+ const secs = this.bearerSession.exp - Math.floor(Date.now() / 1000);
463
+ return Math.max(0, secs);
464
+ }
465
+ if (this.pendingBearerToken && this._sessionId === sessionId) {
466
+ await this.verifyAndSetBearerSession();
467
+ if (this.bearerSession?.sessionId === sessionId) {
468
+ const secs = this.bearerSession.exp - Math.floor(Date.now() / 1000);
469
+ return Math.max(0, secs);
470
+ }
471
+ }
351
472
  return this.storage.getRemainingTtl(sessionId, 'id_token');
352
473
  }
353
474
  async getVerifiedClaims(sessionId) {
354
475
  if (!sessionId)
355
476
  return null;
477
+ if (this.bearerSession?.sessionId === sessionId)
478
+ return this.bearerSession.claims;
479
+ if (this.pendingBearerToken && this._sessionId === sessionId) {
480
+ try {
481
+ await this.verifyAndSetBearerSession();
482
+ return this.bearerSession?.sessionId === sessionId ? this.bearerSession.claims : null;
483
+ }
484
+ catch {
485
+ return null;
486
+ }
487
+ }
356
488
  try {
357
489
  await this.ensureFreshOnce(sessionId);
358
490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serialsubscriptions/platform-integration",
3
- "version": "0.0.85",
3
+ "version": "0.85.4",
4
4
  "description": "Serial Subscriptions Libraries",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",