@serialsubscriptions/platform-integration 0.0.8-5.2 → 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.
- package/lib/session/SessionManager.d.ts +21 -0
- package/lib/session/SessionManager.js +144 -12
- package/package.json +1 -1
|
@@ -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
|
|
61
|
-
if (
|
|
62
|
-
session._sessionId = session.
|
|
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
|
|
88
|
-
if (
|
|
89
|
-
|
|
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
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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
|
}
|