@serialsubscriptions/platform-integration 0.0.84 → 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.
@@ -36,9 +36,16 @@ import { type JsonApiResource } from './SSISubscribedPlanApi';
36
36
  * Attributes for a subscribed_plan--subscribed_plan resource.
37
37
  * Based on your JSON sample; extra fields will still be present in the raw JSON:API resource.
38
38
  */
39
+ /**
40
+ * Known values for subscribed plan `status` (and for project `subscription_status`
41
+ * when synced from the platform). Document the full list in SUBSCRIBEDPLAN_MANAGER.md.
42
+ */
43
+ export declare const SUBSCRIPTION_STATUS: {};
44
+ export type SubscriptionStatusValue = (typeof SUBSCRIPTION_STATUS)[keyof typeof SUBSCRIPTION_STATUS] | string;
39
45
  export interface SubscribedPlanAttributes {
40
46
  drupal_internal__id: number;
41
47
  name: string;
48
+ /** Plan status from the platform. See SUBSCRIBEDPLAN_MANAGER.md for possible values. */
42
49
  status: string;
43
50
  subscription_period: string;
44
51
  name_subscript: string | null;
@@ -33,11 +33,26 @@
33
33
  * ```
34
34
  */
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.SubscribedPlanManager = void 0;
36
+ exports.SubscribedPlanManager = exports.SUBSCRIPTION_STATUS = void 0;
37
37
  const SSISubscribedPlanApi_1 = require("./SSISubscribedPlanApi");
38
38
  const SSISubscribedFeatureApi_1 = require("./SSISubscribedFeatureApi");
39
39
  const SSISubscribedLimitApi_1 = require("./SSISubscribedLimitApi");
40
40
  const SSICache_1 = require("./cache/SSICache");
41
+ /**
42
+ * Attributes for a subscribed_plan--subscribed_plan resource.
43
+ * Based on your JSON sample; extra fields will still be present in the raw JSON:API resource.
44
+ */
45
+ /**
46
+ * Known values for subscribed plan `status` (and for project `subscription_status`
47
+ * when synced from the platform). Document the full list in SUBSCRIBEDPLAN_MANAGER.md.
48
+ */
49
+ exports.SUBSCRIPTION_STATUS = {
50
+ // Add values as the platform defines them, e.g.:
51
+ // ACTIVE: 'active',
52
+ // CANCELLED: 'cancelled',
53
+ // TRIALING: 'trialing',
54
+ // PAST_DUE: 'past_due',
55
+ };
41
56
  /**
42
57
  * SubscribedPlanManager
43
58
  *
@@ -1,7 +1,16 @@
1
1
  import { SSIStorage } from "./storage/SSIStorage";
2
+ import type { AuthConfigInput } from "./requestConfig";
2
3
  export declare const scopes: {
3
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";
4
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
+ };
5
14
  /**
6
15
  * Check if a JWT is expired (or within bufferSeconds of expiry) by decoding its payload.
7
16
  * Does not verify the signature — use only for expiry checks (e.g. "should I refresh?").
@@ -67,7 +76,8 @@ export declare class AuthServer {
67
76
  private stateStorage;
68
77
  private jwksCache;
69
78
  private lastTokens?;
70
- constructor(config?: AuthConfig);
79
+ /** Accepts AuthConfig or SSIRequestConfig (e.g. from getRequestConfig(req)). */
80
+ constructor(config?: AuthConfigInput);
71
81
  /**
72
82
  * Build the authorization URL and persist a CSRF state for the callback.
73
83
  * Returns: { url, stateKey, stateValue }
@@ -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"));
@@ -43,6 +44,7 @@ const jose_1 = require("jose");
43
44
  const stateStore_1 = require("./stateStore");
44
45
  const SSICache_1 = require("./cache/SSICache");
45
46
  const constants_1 = require("./cache/constants");
47
+ const requestConfig_1 = require("./requestConfig");
46
48
  exports.scopes = {
47
49
  defaultScopes: "openid profile email view_project create_project delete_project update_project view_subscribed_plan view_subscribed_feature view_subscribed_limit access_subscription_usage"
48
50
  };
@@ -50,6 +52,61 @@ exports.scopes = {
50
52
  function normalizeIssuer(url) {
51
53
  return url.replace(/\/+$/, "");
52
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
+ }
53
110
  /**
54
111
  * Check if a JWT is expired (or within bufferSeconds of expiry) by decoding its payload.
55
112
  * Does not verify the signature — use only for expiry checks (e.g. "should I refresh?").
@@ -93,9 +150,9 @@ function getJwtExpirySeconds(jwt) {
93
150
  }
94
151
  }
95
152
  class AuthServer {
153
+ /** Accepts AuthConfig or SSIRequestConfig (e.g. from getRequestConfig(req)). */
96
154
  constructor(config) {
97
- // Use empty object as default if config is not provided
98
- const cfg = config ?? {};
155
+ const cfg = (0, requestConfig_1.normalizeAuthConfig)(config) ?? {};
99
156
  let scopesString;
100
157
  if (cfg.scopes === undefined || cfg.scopes === null) {
101
158
  scopesString = exports.scopes.defaultScopes;
@@ -441,6 +498,7 @@ class AuthServer {
441
498
  * @private
442
499
  */
443
500
  async verifyWithIssuer(jwt) {
501
+ const allowedAudiences = getAllowedAudiences(jwt, this.cfg.clientId);
444
502
  // If static JWKS provided, use jose's importJWK for each key
445
503
  if (this.cfg.jwks && Array.isArray(this.cfg.jwks.keys)) {
446
504
  // For static JWKS, we need to import and try each key
@@ -449,7 +507,7 @@ class AuthServer {
449
507
  const publicKey = await (0, jose_1.importJWK)(key, "RS256");
450
508
  const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
451
509
  issuer: this.cfg.issuerAccepted,
452
- audience: this.cfg.clientId,
510
+ audience: allowedAudiences,
453
511
  algorithms: ["RS256"],
454
512
  });
455
513
  return payload;
@@ -469,7 +527,7 @@ class AuthServer {
469
527
  const publicKey = await (0, jose_1.importJWK)(key, "RS256");
470
528
  const { payload } = await (0, jose_1.jwtVerify)(jwt, publicKey, {
471
529
  issuer: this.cfg.issuerAccepted,
472
- audience: this.cfg.clientId,
530
+ audience: allowedAudiences,
473
531
  algorithms: ["RS256"],
474
532
  });
475
533
  return payload;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Config shape that host apps use in the browser (e.g. from getClientConfig()).
3
+ *
4
+ * Derived from public env (NEXT_PUBLIC_*) and optionally window.location.
5
+ * Use this type for the return value of your client config helper so SessionClient
6
+ * and other client code stay typed.
7
+ *
8
+ * - **baseUrl**: App origin (e.g. window.location.origin or NEXT_PUBLIC_APP_URL).
9
+ * - **apiUrl**: App API base; where your app's routes live (e.g. /api/v1/auth/session).
10
+ * Pass to SessionClient.getSessionClient(config.apiUrl ?? config.baseUrl).
11
+ * - **ssiApiUrl**: SSI Platform API base (for links, account portal, etc.).
12
+ * - **ssiIssuerBaseUrl**: SSI issuer URL (often same as ssiApiUrl; e.g. for /pricing links).
13
+ */
14
+ export type SSIClientConfig = {
15
+ baseUrl: string | null;
16
+ apiUrl?: string | null;
17
+ ssiApiUrl?: string | null;
18
+ ssiIssuerBaseUrl?: string | null;
19
+ };
20
+ /**
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).
40
+ * Prefers apiUrl so the session route is resolved correctly when app and API differ.
41
+ */
42
+ export declare function getSessionClientBaseUrl(config: SSIClientConfig | undefined | null): string | undefined;
@@ -0,0 +1,80 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getClientBaseUrl = getClientBaseUrl;
5
+ exports.getClientApiUrl = getClientApiUrl;
6
+ exports.getClientSsiApiUrl = getClientSsiApiUrl;
7
+ exports.getClientConfig = getClientConfig;
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
+ }
71
+ /**
72
+ * Returns the URL to pass to SessionClient (app base so it can call the app's session endpoint).
73
+ * Prefers apiUrl so the session route is resolved correctly when app and API differ.
74
+ */
75
+ function getSessionClientBaseUrl(config) {
76
+ if (config == null)
77
+ return undefined;
78
+ const url = config.apiUrl ?? config.baseUrl;
79
+ return url != null && url !== "" ? url : undefined;
80
+ }
@@ -1 +1,3 @@
1
1
  export { SessionClient } from './session/SessionClient';
2
+ export type { SSIClientConfig } from '../clientConfig';
3
+ export { getClientConfig, getSessionClientBaseUrl, getClientBaseUrl, getClientApiUrl, getClientSsiApiUrl, } from '../clientConfig';
@@ -1,6 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SessionClient = void 0;
4
- // Browser/middleware-safe exports only
3
+ exports.getClientSsiApiUrl = exports.getClientApiUrl = exports.getClientBaseUrl = exports.getSessionClientBaseUrl = exports.getClientConfig = exports.SessionClient = void 0;
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
+ var clientConfig_1 = require("../clientConfig");
8
+ Object.defineProperty(exports, "getClientConfig", { enumerable: true, get: function () { return clientConfig_1.getClientConfig; } });
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; } });
@@ -1,13 +1,25 @@
1
+ import { type SSIClientConfig } from '../../clientConfig';
1
2
  export declare class SessionClient {
2
3
  private readonly cookieHeader?;
3
- private static readonly instances;
4
4
  private readonly baseUrl;
5
5
  private claims;
6
6
  private ttl;
7
7
  private initPromise;
8
8
  private static normalizeBaseUrl;
9
9
  constructor(baseUrl?: string, cookieHeader?: string | null | undefined);
10
- static getSessionClient(baseUrl?: string): SessionClient;
10
+ /**
11
+ * Returns a new SessionClient for this request. Call once per request (or per user
12
+ * context) so session is not shared across requests.
13
+ *
14
+ * Accepts a base URL string or an SSIClientConfig (e.g. from getClientConfig());
15
+ * when given config, uses config.apiUrl ?? config.baseUrl.
16
+ *
17
+ * In Next.js server components/route handlers you can omit cookieHeader: the client
18
+ * will automatically use the current request's session cookie (via next/headers).
19
+ * In other server contexts (e.g. middleware, non-Next), pass the request's Cookie
20
+ * header so this client is bound to that request's session.
21
+ */
22
+ static getSessionClient(baseUrlOrConfig?: string | SSIClientConfig, cookieHeader?: string | null): SessionClient;
11
23
  isLoggedIn(): Promise<boolean>;
12
24
  warmup(): void;
13
25
  getTtl(): Promise<number | null>;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SessionClient = void 0;
4
+ const clientConfig_1 = require("../../clientConfig");
5
+ /** Session cookie name; must match SessionManager (SSI_COOKIE_NAME env or default). */
6
+ const SESSION_COOKIE_NAME = process.env.SSI_COOKIE_NAME ?? "ssi_session";
4
7
  class SessionClient {
5
8
  static normalizeBaseUrl(baseUrl) {
6
9
  if (!baseUrl) {
@@ -30,16 +33,21 @@ class SessionClient {
30
33
  }
31
34
  this.initPromise = this.loadClaims();
32
35
  }
33
- static getSessionClient(baseUrl) {
34
- const resolvedBaseUrl = baseUrl ?? process.env.NEXT_PUBLIC_BASE_URL ?? "";
35
- const normalizedBaseUrl = SessionClient.normalizeBaseUrl(resolvedBaseUrl);
36
- if (!normalizedBaseUrl || !normalizedBaseUrl.startsWith("http://") && !normalizedBaseUrl.startsWith("https://")) {
37
- console.error(`You must set baseUrl to SessionClient or you must set env variable NEXT_PUBLIC_BASE_URL. Incorrect value \`${resolvedBaseUrl}\``);
38
- }
39
- if (!SessionClient.instances.has(normalizedBaseUrl)) {
40
- SessionClient.instances.set(normalizedBaseUrl, new SessionClient(baseUrl));
41
- }
42
- return SessionClient.instances.get(normalizedBaseUrl);
36
+ /**
37
+ * Returns a new SessionClient for this request. Call once per request (or per user
38
+ * context) so session is not shared across requests.
39
+ *
40
+ * Accepts a base URL string or an SSIClientConfig (e.g. from getClientConfig());
41
+ * when given config, uses config.apiUrl ?? config.baseUrl.
42
+ *
43
+ * In Next.js server components/route handlers you can omit cookieHeader: the client
44
+ * will automatically use the current request's session cookie (via next/headers).
45
+ * In other server contexts (e.g. middleware, non-Next), pass the request's Cookie
46
+ * header so this client is bound to that request's session.
47
+ */
48
+ static getSessionClient(baseUrlOrConfig, cookieHeader) {
49
+ const baseUrl = typeof baseUrlOrConfig === 'string' ? baseUrlOrConfig : (0, clientConfig_1.getSessionClientBaseUrl)(baseUrlOrConfig);
50
+ return new SessionClient(baseUrl, cookieHeader);
43
51
  }
44
52
  async isLoggedIn() {
45
53
  await this.initPromise;
@@ -87,14 +95,13 @@ class SessionClient {
87
95
  return init;
88
96
  }
89
97
  try {
90
- // Dynamic import of next/headers - only available in Next.js server context
91
- // @ts-expect-error - next/headers types are available at runtime but TypeScript can't resolve them during package build
98
+ // Dynamic import of next/headers - only available in Next.js server context (request-scoped)
92
99
  const { cookies } = await import("next/headers");
93
100
  const cookieStore = await cookies();
94
- const serializedCookies = cookieStore
95
- .getAll()
96
- .map((cookie) => `${cookie.name}=${cookie.value}`)
97
- .join("; ");
101
+ const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME);
102
+ const serializedCookies = sessionCookie
103
+ ? `${sessionCookie.name}=${sessionCookie.value}`
104
+ : "";
98
105
  if (!serializedCookies) {
99
106
  return init;
100
107
  }
@@ -141,5 +148,4 @@ class SessionClient {
141
148
  }
142
149
  }
143
150
  exports.SessionClient = SessionClient;
144
- SessionClient.instances = new Map();
145
151
  exports.default = SessionClient;
package/lib/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './auth.server';
2
+ export * from './requestConfig';
3
+ export * from './clientConfig';
2
4
  export * from './stateStore';
3
5
  export * from './storage/SSIStorage';
4
6
  export * from './storage/types';
package/lib/index.js CHANGED
@@ -16,6 +16,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.SSISubscribedPlanApi = exports.SSISubscribedLimitApi = exports.SSISubscribedFeatureApi = exports.SessionClient = void 0;
18
18
  __exportStar(require("./auth.server"), exports);
19
+ __exportStar(require("./requestConfig"), exports);
20
+ __exportStar(require("./clientConfig"), exports);
19
21
  __exportStar(require("./stateStore"), exports);
20
22
  __exportStar(require("./storage/SSIStorage"), exports);
21
23
  __exportStar(require("./storage/types"), exports);
@@ -0,0 +1,61 @@
1
+ import type { AuthConfig } from './auth.server';
2
+ /**
3
+ * SSI-related config that host apps typically derive from the current request
4
+ * (e.g. via getRequestConfig(req)) and pass to the library.
5
+ *
6
+ * Use this type when building a "request config" in your app so SessionManager,
7
+ * AuthServer, UsageApi, and SubscribedPlanManager get a consistent, typed shape.
8
+ *
9
+ * - **ssiIssuerBaseUrl**, **ssiRedirectUri**, **ssiClientId**: passed to
10
+ * AuthServer and SessionManager.fromEnv(cookieHeader, config).
11
+ * - **ssiApiUrl**: base URL for SSI Platform API (UsageApi, SubscribedPlanManager,
12
+ * SSIProjectApi, etc.).
13
+ * - **baseUrl** / **apiUrl**: optional; for app use (e.g. redirect after login).
14
+ * The library does not use these; they are included so apps can use this type
15
+ * as the SSI subset of a larger RequestConfig.
16
+ */
17
+ export type SSIRequestConfig = {
18
+ baseUrl?: string | null;
19
+ apiUrl?: string | null;
20
+ ssiApiUrl: string | null;
21
+ ssiIssuerBaseUrl: string | null;
22
+ ssiRedirectUri: string | null;
23
+ ssiClientId: string | null;
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;
48
+ /**
49
+ * Union type accepted anywhere AuthConfig is accepted (AuthServer, SessionManager).
50
+ */
51
+ export type AuthConfigInput = AuthConfig | SSIRequestConfig;
52
+ /**
53
+ * Converts SSIRequestConfig to AuthConfig for use with AuthServer and SessionManager.
54
+ * Drops null/undefined so the library can fall back to env vars where appropriate.
55
+ */
56
+ export declare function toAuthConfig(rc: SSIRequestConfig): AuthConfig;
57
+ /**
58
+ * Normalizes AuthConfigInput to AuthConfig. Use internally so AuthServer and SessionManager
59
+ * accept both AuthConfig and SSIRequestConfig.
60
+ */
61
+ export declare function normalizeAuthConfig(input: AuthConfigInput | undefined): AuthConfig | undefined;
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ // requestConfig.ts
3
+ // Types and helpers for config that host apps derive per-request and pass into AuthServer, SessionManager, and SSI API clients.
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;
13
+ exports.toAuthConfig = toAuthConfig;
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
+ }
118
+ function isSSIRequestConfig(x) {
119
+ return x != null && typeof x === 'object' && 'ssiIssuerBaseUrl' in x;
120
+ }
121
+ /**
122
+ * Converts SSIRequestConfig to AuthConfig for use with AuthServer and SessionManager.
123
+ * Drops null/undefined so the library can fall back to env vars where appropriate.
124
+ */
125
+ function toAuthConfig(rc) {
126
+ const auth = {};
127
+ if (rc.ssiIssuerBaseUrl != null && rc.ssiIssuerBaseUrl !== '')
128
+ auth.issuerBaseUrl = rc.ssiIssuerBaseUrl;
129
+ if (rc.ssiRedirectUri != null && rc.ssiRedirectUri !== '')
130
+ auth.redirectUri = rc.ssiRedirectUri;
131
+ if (rc.ssiClientId != null && rc.ssiClientId !== '')
132
+ auth.clientId = rc.ssiClientId;
133
+ return auth;
134
+ }
135
+ /**
136
+ * Normalizes AuthConfigInput to AuthConfig. Use internally so AuthServer and SessionManager
137
+ * accept both AuthConfig and SSIRequestConfig.
138
+ */
139
+ function normalizeAuthConfig(input) {
140
+ if (input == null)
141
+ return undefined;
142
+ if (isSSIRequestConfig(input))
143
+ return toAuthConfig(input);
144
+ return input;
145
+ }
@@ -1,6 +1,7 @@
1
1
  import { SSIStorage } from '../storage/SSIStorage';
2
2
  import type { KVValue } from '../storage/types';
3
- import { AuthServer, type TokenResponse, type AuthConfig } from '../auth.server';
3
+ import { AuthServer, type TokenResponse } from '../auth.server';
4
+ import { type AuthConfigInput } from '../requestConfig';
4
5
  export declare const sessionRoles: {
5
6
  allRoles: string[];
6
7
  adminRoles: string[];
@@ -41,13 +42,22 @@ export declare class SessionManager {
41
42
  private auth?;
42
43
  private _sessionId?;
43
44
  private authConfig?;
45
+ private bearerSession?;
46
+ private pendingBearerToken?;
44
47
  private static inFlightRefresh;
45
48
  private freshnessOnce;
46
- constructor(storage: SSIStorage, authConfig?: AuthConfig);
49
+ /** Accepts AuthConfig or SSIRequestConfig (e.g. from getRequestConfig(req)). */
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
+ */
47
57
  static fromEnv(): SessionManager;
48
- static fromEnv(cookieHeader: string | null, authConfig?: AuthConfig): SessionManager;
49
- static fromEnv(request: Request, authConfig?: AuthConfig): SessionManager;
50
- static fromEnv(authConfig: AuthConfig): SessionManager;
58
+ static fromEnv(cookieHeader: string | null, authConfig?: AuthConfigInput): SessionManager;
59
+ static fromEnv(request: Request, authConfig?: AuthConfigInput): SessionManager;
60
+ static fromEnv(authConfig: AuthConfigInput): SessionManager;
51
61
  /**
52
62
  * Async version that attempts to auto-detect the cookie header from Next.js context
53
63
  * when called with no arguments. Falls back gracefully if not in a Next.js context.
@@ -65,9 +75,20 @@ export declare class SessionManager {
65
75
  * const session = await SessionManager.fromEnvAsync(request, { clientId: 'my-client-id' });
66
76
  */
67
77
  static fromEnvAsync(): Promise<SessionManager>;
68
- static fromEnvAsync(cookieHeader: string | null, authConfig?: AuthConfig): Promise<SessionManager>;
69
- static fromEnvAsync(request: Request, authConfig?: AuthConfig): Promise<SessionManager>;
70
- static fromEnvAsync(authConfig: AuthConfig): Promise<SessionManager>;
78
+ static fromEnvAsync(cookieHeader: string | null, authConfig?: AuthConfigInput): Promise<SessionManager>;
79
+ static fromEnvAsync(request: Request, authConfig?: AuthConfigInput): Promise<SessionManager>;
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;
71
92
  private parseCookies;
72
93
  get sessionId(): string | undefined;
73
94
  /** Cryptographically strong, URL-safe session id */
@@ -110,11 +131,13 @@ export declare class SessionManager {
110
131
  /**
111
132
  * Get session data by key. For 'claims', decodes the id_token payload
112
133
  * WITHOUT verification (use your verifier upstream for security).
134
+ * Bearer JWT sessions: returns from verified ephemeral data (not cached).
113
135
  */
114
136
  getSessionData(sessionId: string, dataKey: SessionDataKey): Promise<KVValue | null>;
115
137
  private ensureFreshOnce;
116
138
  /**
117
139
  * Delete all keys under this session object (id/access/refresh).
140
+ * For Bearer sessions, clears in-memory state only (nothing was cached).
118
141
  */
119
142
  clearSession(sessionId: string): Promise<string>;
120
143
  /**
@@ -4,6 +4,7 @@ exports.SessionManager = exports.sessionRoles = void 0;
4
4
  const SSIStorage_1 = require("../storage/SSIStorage");
5
5
  const crypto_1 = require("crypto");
6
6
  const auth_server_1 = require("../auth.server");
7
+ const requestConfig_1 = require("../requestConfig");
7
8
  exports.sessionRoles = {
8
9
  allRoles: ['platform_admin', 'member', 'owner', 'admin', 'billing', 'readonly'],
9
10
  adminRoles: ['platform_admin', 'owner', 'admin'],
@@ -35,33 +36,37 @@ function resolveCookieDomain() {
35
36
  }
36
37
  }
37
38
  class SessionManager {
39
+ /** Accepts AuthConfig or SSIRequestConfig (e.g. from getRequestConfig(req)). */
38
40
  constructor(storage, authConfig) {
39
41
  // Per-request-instance guard so TTL/refresh is evaluated at most once per session
40
42
  this.freshnessOnce = new Map();
41
43
  this.storage = storage.withClass('session'); // pin class="session"
42
- this.authConfig = authConfig;
44
+ this.authConfig = (0, requestConfig_1.normalizeAuthConfig)(authConfig);
43
45
  }
44
46
  static fromEnv(cookieHeaderOrRequestOrConfig, authConfig) {
45
- // Handle case where first arg is AuthConfig (no cookie header/request)
46
47
  let actualAuthConfig;
47
48
  let cookieHeaderOrRequest;
48
- if (cookieHeaderOrRequestOrConfig &&
49
- typeof cookieHeaderOrRequestOrConfig === 'object' &&
50
- !(cookieHeaderOrRequestOrConfig instanceof Request) &&
51
- ('clientId' in cookieHeaderOrRequestOrConfig || 'issuerBaseUrl' in cookieHeaderOrRequestOrConfig)) {
52
- // First arg is AuthConfig
49
+ const isConfigLike = (x) => x != null && typeof x === 'object' && !(x instanceof Request) &&
50
+ ('clientId' in x || 'issuerBaseUrl' in x || 'ssiClientId' in x || 'ssiIssuerBaseUrl' in x);
51
+ if (cookieHeaderOrRequestOrConfig && isConfigLike(cookieHeaderOrRequestOrConfig)) {
53
52
  actualAuthConfig = cookieHeaderOrRequestOrConfig;
54
53
  }
55
54
  else {
56
- // First arg is cookie header/request, second is optional AuthConfig
57
55
  cookieHeaderOrRequest = cookieHeaderOrRequestOrConfig;
58
56
  actualAuthConfig = authConfig;
59
57
  }
60
58
  const session = new SessionManager(SSIStorage_1.SSIStorage.fromEnv(), actualAuthConfig);
61
59
  if (cookieHeaderOrRequest instanceof Request) {
62
- const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
63
- if (cookieHeader) {
64
- 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
+ }
65
70
  }
66
71
  }
67
72
  else if (cookieHeaderOrRequest) {
@@ -73,52 +78,102 @@ class SessionManager {
73
78
  return session;
74
79
  }
75
80
  static async fromEnvAsync(cookieHeaderOrRequestOrConfig, authConfig) {
76
- // Handle case where first arg is AuthConfig (no cookie header/request)
77
81
  let actualAuthConfig;
78
82
  let cookieHeaderOrRequest;
79
- if (cookieHeaderOrRequestOrConfig &&
80
- typeof cookieHeaderOrRequestOrConfig === 'object' &&
81
- !(cookieHeaderOrRequestOrConfig instanceof Request) &&
82
- ('clientId' in cookieHeaderOrRequestOrConfig || 'issuerBaseUrl' in cookieHeaderOrRequestOrConfig)) {
83
- // First arg is AuthConfig
83
+ const isConfigLike = (x) => x != null && typeof x === 'object' && !(x instanceof Request) &&
84
+ ('clientId' in x || 'issuerBaseUrl' in x || 'ssiClientId' in x || 'ssiIssuerBaseUrl' in x);
85
+ if (cookieHeaderOrRequestOrConfig && isConfigLike(cookieHeaderOrRequestOrConfig)) {
84
86
  actualAuthConfig = cookieHeaderOrRequestOrConfig;
85
87
  }
86
88
  else {
87
- // First arg is cookie header/request, second is optional AuthConfig
88
89
  cookieHeaderOrRequest = cookieHeaderOrRequestOrConfig;
89
90
  actualAuthConfig = authConfig;
90
91
  }
91
92
  const session = new SessionManager(SSIStorage_1.SSIStorage.fromEnv(), actualAuthConfig);
92
93
  if (cookieHeaderOrRequest instanceof Request) {
93
- const cookieHeader = cookieHeaderOrRequest.headers.get('cookie');
94
- if (cookieHeader) {
95
- 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
+ }
96
103
  }
97
104
  }
98
105
  else if (cookieHeaderOrRequest) {
99
106
  session._sessionId = session.parseCookies(cookieHeaderOrRequest);
100
107
  }
101
108
  else {
102
- // 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
103
110
  try {
104
- // Dynamic import of next/headers - only available in Next.js server context
105
- // @ts-expect-error - next/headers types are available at runtime but TypeScript can't resolve them during package build
106
111
  const { headers } = await import('next/headers');
107
112
  const headersList = await headers();
108
113
  if (headersList && typeof headersList.get === 'function') {
109
- const cookieHeader = headersList.get('cookie');
110
- if (cookieHeader) {
111
- 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
+ }
112
126
  }
113
127
  }
114
128
  }
115
129
  catch {
116
130
  // Not in Next.js context or headers() not available - silently continue
117
- // This is expected when called outside of Next.js or when creating new sessions
118
131
  }
119
132
  }
120
133
  return session;
121
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
+ }
122
177
  parseCookies(header) {
123
178
  const cookieName = process.env.SSI_COOKIE_NAME ?? 'ssi_session';
124
179
  for (const part of header.split(/;\s*/)) {
@@ -219,8 +274,43 @@ class SessionManager {
219
274
  /**
220
275
  * Get session data by key. For 'claims', decodes the id_token payload
221
276
  * WITHOUT verification (use your verifier upstream for security).
277
+ * Bearer JWT sessions: returns from verified ephemeral data (not cached).
222
278
  */
223
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
+ }
224
314
  const keyToCheck = dataKey === 'claims' ? 'id_token' : dataKey;
225
315
  try {
226
316
  await this.ensureFreshOnce(sessionId);
@@ -256,6 +346,11 @@ class SessionManager {
256
346
  return value;
257
347
  }
258
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
+ }
259
354
  const existing = this.freshnessOnce.get(sessionId);
260
355
  if (existing)
261
356
  return existing;
@@ -326,9 +421,17 @@ class SessionManager {
326
421
  }
327
422
  /**
328
423
  * Delete all keys under this session object (id/access/refresh).
424
+ * For Bearer sessions, clears in-memory state only (nothing was cached).
329
425
  */
330
426
  async clearSession(sessionId) {
331
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
+ }
332
435
  const keys = (await this.storage.keysForObject(sessionId)) ?? [];
333
436
  if (!keys.length)
334
437
  return deleteCookieHeader;
@@ -355,11 +458,33 @@ class SessionManager {
355
458
  }
356
459
  /** Returns remaining TTL in seconds for this session's id_token, or null if none */
357
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
+ }
358
472
  return this.storage.getRemainingTtl(sessionId, 'id_token');
359
473
  }
360
474
  async getVerifiedClaims(sessionId) {
361
475
  if (!sessionId)
362
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
+ }
363
488
  try {
364
489
  await this.ensureFreshOnce(sessionId);
365
490
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serialsubscriptions/platform-integration",
3
- "version": "0.0.84",
3
+ "version": "0.85.4",
4
4
  "description": "Serial Subscriptions Libraries",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -67,5 +67,8 @@
67
67
  "peerDependencies": {
68
68
  "next": "^15.0.0"
69
69
  },
70
+ "optionalDependencies": {
71
+ "next": "^15.0.0"
72
+ },
70
73
  "license": "MIT"
71
74
  }