@serialsubscriptions/platform-integration 0.0.8-5.1 → 0.0.8-5.3

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.
@@ -23,8 +23,9 @@ export type SSIRequestConfig = {
23
23
  ssiClientId: string | null;
24
24
  };
25
25
  /**
26
- * Get the base URL from request headers or environment.
27
- * Uses x-forwarded-proto, x-forwarded-host, origin, host, and NEXT_PUBLIC_APP_URL.
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.
28
29
  */
29
30
  export declare function getBaseUrl(req: Request): string | null;
30
31
  /** When baseUrl is a single URL or comma-separated list, return the first URL. */
@@ -14,32 +14,26 @@ exports.toAuthConfig = toAuthConfig;
14
14
  exports.normalizeAuthConfig = normalizeAuthConfig;
15
15
  // --- Helpers used by getRequestConfig (env: NEXT_PUBLIC_APP_URL, SSI_*, request headers) ---
16
16
  /**
17
- * Get the base URL from request headers or environment.
18
- * Uses x-forwarded-proto, x-forwarded-host, origin, host, and NEXT_PUBLIC_APP_URL.
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.
19
20
  */
20
21
  function getBaseUrl(req) {
22
+ const fromEnv = process.env.NEXT_PUBLIC_APP_URL?.trim();
23
+ if (fromEnv)
24
+ return fromEnv;
21
25
  const proto = req.headers.get('x-forwarded-proto') || 'https';
22
- let baseUrl = process.env.NEXT_PUBLIC_APP_URL;
23
- if (!baseUrl) {
24
- const forwardedHost = req.headers.get('x-forwarded-host');
25
- if (forwardedHost) {
26
- baseUrl = `${proto}://${forwardedHost}`;
27
- }
28
- else {
29
- const origin = req.headers.get('origin');
30
- if (origin) {
31
- baseUrl = origin;
32
- }
33
- else {
34
- const host = req.headers.get('host');
35
- const hostWithoutPort = host ? host.split(':')[0] : undefined;
36
- if (hostWithoutPort) {
37
- baseUrl = `${proto}://${hostWithoutPort}`;
38
- }
39
- }
40
- }
41
- }
42
- return baseUrl || null;
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;
43
37
  }
44
38
  /** When baseUrl is a single URL or comma-separated list, return the first URL. */
45
39
  function firstBaseUrl(baseUrl) {
@@ -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.1",
3
+ "version": "0.0.85.3",
4
4
  "description": "Serial Subscriptions Libraries",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",