@c1m-public/platform-auth 0.8.4 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,534 @@
1
- export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, isAuthErrorCode, isHandoffErrorCode, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath } from './chunk-Q7YI7RMG.js';
2
- export { decodeBase64Url, decodeDefaultSession, decodeTokenPayload, hasTokenAudience, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, tokenMaxAgeSeconds } from './chunk-FBC2NUH7.js';
3
- export { applyForwardedClientHeaders, forwardedRequestHeaders } from './chunk-ZWSJVE5S.js';
1
+ // src/token.ts
2
+ function readStringClaim(value, fallback = "") {
3
+ return typeof value === "string" ? value : fallback;
4
+ }
5
+ function readBooleanClaim(value, fallback) {
6
+ return typeof value === "boolean" ? value : fallback;
7
+ }
8
+ function decodeBase64Url(input) {
9
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
10
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
11
+ try {
12
+ const binary = globalThis.atob(padded);
13
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
14
+ return new TextDecoder().decode(bytes);
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ function decodeTokenPayload(token) {
20
+ const payloadSegment = token.split(".")[1];
21
+ if (!payloadSegment) return null;
22
+ const decodedJson = decodeBase64Url(payloadSegment);
23
+ if (!decodedJson) return null;
24
+ try {
25
+ const parsed = JSON.parse(decodedJson);
26
+ return parsed && typeof parsed === "object" ? parsed : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function hasTokenAudience(token, requiredAudience) {
32
+ const audience = decodeTokenPayload(token)?.aud;
33
+ return typeof audience === "string" ? audience === requiredAudience : Array.isArray(audience) && audience.includes(requiredAudience);
34
+ }
35
+ function nowInSeconds() {
36
+ return Math.floor(Date.now() / 1e3);
37
+ }
38
+ function tokenMaxAgeSeconds(token, configuredMaxAge) {
39
+ const decoded = decodeTokenPayload(token);
40
+ if (!decoded?.exp) return configuredMaxAge;
41
+ return Math.max(0, Math.min(configuredMaxAge, decoded.exp - nowInSeconds()));
42
+ }
43
+ function isTokenExpired(token) {
44
+ const decoded = decodeTokenPayload(token);
45
+ if (!decoded?.exp) return true;
46
+ return decoded.exp <= nowInSeconds();
47
+ }
48
+ function isTokenExpiringSoon(token, bufferSeconds = 120) {
49
+ const decoded = decodeTokenPayload(token);
50
+ if (!decoded?.exp) return true;
51
+ return decoded.exp < nowInSeconds() + bufferSeconds;
52
+ }
53
+ function decodeDefaultSession(token) {
54
+ const decoded = decodeTokenPayload(token);
55
+ if (!decoded) return null;
56
+ const actorId = decoded.effective_user_id || decoded.actor_id || decoded.customer_id || decoded.user_id || (typeof decoded.sub === "string" ? decoded.sub : void 0);
57
+ if (!actorId) return null;
58
+ return {
59
+ user: {
60
+ id: String(actorId),
61
+ email: readStringClaim(decoded.email),
62
+ name: readStringClaim(decoded.name),
63
+ role: readStringClaim(decoded.role, "user"),
64
+ is_active: readBooleanClaim(decoded.is_active, true),
65
+ email_verified: readBooleanClaim(decoded.email_verified, false),
66
+ created_at: readStringClaim(decoded.created_at),
67
+ has_passkey: readBooleanClaim(decoded.has_passkeys, false)
68
+ },
69
+ token,
70
+ exp: decoded.exp || 0,
71
+ iat: decoded.iat || 0
72
+ };
73
+ }
74
+ function parseAuthResponse(value) {
75
+ if (!value || typeof value !== "object") return null;
76
+ const candidate = value;
77
+ if (typeof candidate.access_token !== "string" || typeof candidate.refresh_token !== "string" || typeof candidate.token_type !== "string" || typeof candidate.expires_at !== "string" || !candidate.user || typeof candidate.user !== "object") {
78
+ return null;
79
+ }
80
+ return candidate;
81
+ }
82
+
83
+ // src/return-path.ts
84
+ function safeInternalPath(raw) {
85
+ if (raw == null) return null;
86
+ const candidate = raw.trim();
87
+ if (candidate.length === 0) return null;
88
+ if (!candidate.startsWith("/")) return null;
89
+ if (candidate.startsWith("//") || candidate.includes("://")) return null;
90
+ if (/%(?:2f|5c)/i.test(candidate)) return null;
91
+ if (/[\x00-\x1f\x7f]/.test(candidate)) return null;
92
+ try {
93
+ const parsed = new URL(candidate, "https://internal.invalid");
94
+ if (parsed.origin !== "https://internal.invalid") return null;
95
+ const roundtrip = `${parsed.pathname}${parsed.search}${parsed.hash}`;
96
+ if (roundtrip !== candidate) return null;
97
+ return candidate;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ // ../transport/src/forwarding.ts
104
+ function applyForwardedClientHeaders(incoming, headers = new Headers()) {
105
+ const forwardedFor = incoming.get("x-forwarded-for");
106
+ const realIp = incoming.get("x-real-ip") ?? forwardedFor?.split(",")[0]?.trim();
107
+ if (realIp && !headers.has("X-Client-IP")) headers.set("X-Client-IP", realIp);
108
+ if (realIp && !headers.has("X-Real-IP")) headers.set("X-Real-IP", realIp);
109
+ if (forwardedFor && !headers.has("X-Forwarded-For")) headers.set("X-Forwarded-For", forwardedFor);
110
+ const country = incoming.get("cf-ipcountry") ?? incoming.get("x-vercel-ip-country") ?? incoming.get("x-geo-country") ?? incoming.get("x-country-code");
111
+ if (country && country !== "XX" && !headers.has("X-Geo-Country")) {
112
+ headers.set("X-Geo-Country", country);
113
+ }
114
+ const host = incoming.get("host");
115
+ if (host && !headers.has("X-Forwarded-Host")) headers.set("X-Forwarded-Host", host);
116
+ const proto = incoming.get("x-forwarded-proto");
117
+ if (proto && !headers.has("X-Forwarded-Proto")) headers.set("X-Forwarded-Proto", proto);
118
+ const userAgent = incoming.get("user-agent");
119
+ if (userAgent && !headers.has("X-Client-User-Agent")) headers.set("X-Client-User-Agent", userAgent);
120
+ return headers;
121
+ }
122
+ function forwardedRequestHeaders(request, init) {
123
+ return applyForwardedClientHeaders(request.headers, new Headers(init));
124
+ }
125
+
126
+ // src/cookies.ts
127
+ function readCookie(request, name) {
128
+ const header = request.headers.get("cookie");
129
+ if (!header) return null;
130
+ for (const part of header.split(";")) {
131
+ const separator = part.indexOf("=");
132
+ if (separator < 0) continue;
133
+ if (part.slice(0, separator).trim() !== name) continue;
134
+ const raw = part.slice(separator + 1).trim();
135
+ try {
136
+ return decodeURIComponent(raw);
137
+ } catch {
138
+ return raw;
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ function serializeCookie(name, value, options = {}) {
144
+ const parts = [`${name}=${encodeURIComponent(value)}`];
145
+ parts.push(`Path=${options.path ?? "/"}`);
146
+ if (options.maxAge !== void 0) parts.push(`Max-Age=${Math.max(0, Math.floor(options.maxAge))}`);
147
+ if (options.httpOnly ?? true) parts.push("HttpOnly");
148
+ if (options.secure ?? name.startsWith("__Host-")) parts.push("Secure");
149
+ const sameSite = options.sameSite ?? "lax";
150
+ parts.push(`SameSite=${sameSite.charAt(0).toUpperCase()}${sameSite.slice(1)}`);
151
+ return parts.join("; ");
152
+ }
153
+ function setCookie(response, name, value, options) {
154
+ response.headers.append("Set-Cookie", serializeCookie(name, value, options));
155
+ return response;
156
+ }
157
+ function deleteCookie(response, name, options) {
158
+ response.headers.append("Set-Cookie", serializeCookie(name, "", { ...options, maxAge: 0 }));
159
+ return response;
160
+ }
161
+
162
+ // src/device-cookie.ts
163
+ var DEFAULT_DEVICE_MAX_AGE = 365 * 24 * 60 * 60;
164
+ function createDeviceIdentity(config) {
165
+ const maxAge = config.maxAgeSeconds ?? DEFAULT_DEVICE_MAX_AGE;
166
+ return {
167
+ read(request) {
168
+ const existing = readCookie(request, config.cookieName)?.trim();
169
+ return existing ? { value: existing, created: false } : { value: crypto.randomUUID(), created: true };
170
+ },
171
+ persist(response, identity) {
172
+ if (!identity.created) return response;
173
+ return setCookie(response, config.cookieName, identity.value, {
174
+ httpOnly: true,
175
+ secure: config.secure ?? true,
176
+ sameSite: "lax",
177
+ path: "/",
178
+ maxAge
179
+ });
180
+ }
181
+ };
182
+ }
183
+
184
+ // src/handoff-errors.ts
185
+ var HANDOFF_ERROR_MESSAGES = {
186
+ // backend — handoff broker
187
+ handoff_unknown_app: "That application is not registered for sign-in handoff.",
188
+ handoff_not_configured: "Sign-in handoff is not configured for this application yet.",
189
+ handoff_source_not_allowed: "Your current session cannot open that application. Sign in there directly.",
190
+ handoff_invalid: "That sign-in handoff expired or was already used. Start again from where you came from.",
191
+ handoff_device_required: "The handoff could not identify this browser. Please try again.",
192
+ handoff_request_invalid: "The sign-in handoff request was malformed. Please try again.",
193
+ handoff_failed: "Sign-in handoff failed. Sign in with an email code instead.",
194
+ // backend — target admission (same codes the target's own login returns)
195
+ knowledge_library_access_required: "Your account has not been granted Knowledge Library access. Ask a platform admin to enable it.",
196
+ admin_center_access_required: "Your account has not been granted Admin Center access.",
197
+ platform_admin_surface_required: "Platform operator accounts sign in through the Platform Admin Center.",
198
+ account_disabled: "This account is disabled.",
199
+ // BFF — target callback
200
+ handoff_state_invalid: "Sign-in attempt expired \u2014 please try again.",
201
+ handoff_exchange_failed: "Sign-in could not be completed. Please try again.",
202
+ // BFF — source start
203
+ handoff_start_failed: "Could not start the sign-in handoff. Please try again.",
204
+ handoff_redirect_invalid: "The sign-in handoff returned an unexpected destination and was stopped.",
205
+ // BFF — surface bounces
206
+ admin_surface: "Platform operators sign in here; SaaSDash sent you to the Admin Center."
207
+ };
208
+ var HANDOFF_ERROR_FALLBACK = "Sign-in failed \u2014 please try again.";
209
+ var SSO_ERROR_MESSAGES = {
210
+ sso_cancelled: "Sign-in was cancelled.",
211
+ sso_denied: "Sign-in was refused by the provider.",
212
+ sso_state_invalid: "Sign-in attempt expired \u2014 please try again.",
213
+ sso_start_failed: "Could not start sign-in with that provider. Please try again.",
214
+ sso_complete_failed: "Sign-in could not be completed. Please try again.",
215
+ sso_unknown_operator: "No operator account matches that identity. Ask an admin for an invite.",
216
+ sso_provider_not_found: "That sign-in provider is not available.",
217
+ sso_not_configured: "That sign-in provider is not fully configured."
218
+ };
219
+ var AUTH_ERROR_MESSAGES = {
220
+ ...HANDOFF_ERROR_MESSAGES,
221
+ ...SSO_ERROR_MESSAGES
222
+ };
223
+ function handoffErrorMessage(code) {
224
+ if (!code) return HANDOFF_ERROR_FALLBACK;
225
+ return HANDOFF_ERROR_MESSAGES[code] ?? HANDOFF_ERROR_FALLBACK;
226
+ }
227
+ function isHandoffErrorCode(code) {
228
+ return !!code && Object.prototype.hasOwnProperty.call(HANDOFF_ERROR_MESSAGES, code);
229
+ }
230
+ function authErrorMessage(code, overrides) {
231
+ if (!code) return HANDOFF_ERROR_FALLBACK;
232
+ return overrides?.[code] ?? AUTH_ERROR_MESSAGES[code] ?? HANDOFF_ERROR_FALLBACK;
233
+ }
234
+ function isAuthErrorCode(code, overrides) {
235
+ if (!code) return false;
236
+ return Object.prototype.hasOwnProperty.call(AUTH_ERROR_MESSAGES, code) || !!overrides && Object.prototype.hasOwnProperty.call(overrides, code);
237
+ }
238
+
239
+ // src/internal.ts
240
+ function resolveValue(value) {
241
+ return typeof value === "function" ? value() : value;
242
+ }
243
+ function resolveAsync(value) {
244
+ return typeof value === "function" ? value() : value;
245
+ }
246
+ var isPromise = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
247
+ function resolveAll(...values) {
248
+ const resolved = values.map((value) => resolveAsync(value));
249
+ return resolved.some(isPromise) ? Promise.all(resolved) : resolved;
250
+ }
251
+ var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
252
+ async function timedFetch(url, options = {}) {
253
+ const doFetch = options.fetch ?? options.fetchImpl ?? fetch;
254
+ const controller = options.timeoutMs ? new AbortController() : void 0;
255
+ const timer = controller ? setTimeout(() => controller.abort(), options.timeoutMs) : void 0;
256
+ try {
257
+ return await doFetch(url, {
258
+ ...options.init,
259
+ cache: "no-store",
260
+ ...controller ? { signal: controller.signal } : {}
261
+ });
262
+ } finally {
263
+ if (timer) clearTimeout(timer);
264
+ }
265
+ }
266
+
267
+ // src/handoff.ts
268
+ var HANDOFF_COOKIE_TTL_SECONDS = 120;
269
+ var CHALLENGE_PATTERN = /^[A-Za-z0-9_-]{43}$/;
270
+ function sourceStartPath(app) {
271
+ return `/api/auth/handoffs/${encodeURIComponent(app)}/start`;
272
+ }
273
+ function brokerPaths(app) {
274
+ const key = encodeURIComponent(app);
275
+ return {
276
+ start: `/api/v1/platform/auth/handoffs/${key}/start`,
277
+ exchange: `/api/v1/platform/auth/handoffs/${key}/exchange`
278
+ };
279
+ }
280
+ function originOf(value) {
281
+ try {
282
+ return new URL(value).origin;
283
+ } catch {
284
+ return null;
285
+ }
286
+ }
287
+ function base64Url(bytes) {
288
+ let binary = "";
289
+ for (const byte of bytes) binary += String.fromCharCode(byte);
290
+ return globalThis.btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
291
+ }
292
+ function randomToken(bytes) {
293
+ const raw = new Uint8Array(bytes);
294
+ crypto.getRandomValues(raw);
295
+ return base64Url(raw);
296
+ }
297
+ async function s256(verifier) {
298
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
299
+ return base64Url(new Uint8Array(digest));
300
+ }
301
+ function validState(state) {
302
+ return state.length >= 16 && state.length <= 256 && !/[\r\n]/.test(state);
303
+ }
304
+ function redirect(url, status = 302) {
305
+ return new Response(null, { status, headers: { Location: url.toString(), "Cache-Control": "no-store" } });
306
+ }
307
+ function loginErrorRedirect(origin, loginPath, code) {
308
+ const target = new URL(loginPath, origin);
309
+ target.searchParams.set("error", code);
310
+ return redirect(target);
311
+ }
312
+ function createHandoffTarget(config) {
313
+ const secure = config.secureCookies ?? true;
314
+ const prefix = `${secure ? "__Host-" : ""}${config.cookiePrefix}_handoff_`;
315
+ const cookies = { state: `${prefix}state`, verifier: `${prefix}verifier`, from: `${prefix}from` };
316
+ const cookieOptions = { httpOnly: true, secure, sameSite: "lax", path: "/", maxAge: HANDOFF_COOKIE_TTL_SECONDS };
317
+ const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
318
+ const clearHandoffCookies = (response) => {
319
+ for (const name of Object.values(cookies)) deleteCookie(response, name, { path: "/", secure });
320
+ return response;
321
+ };
322
+ const fail = (origin, code) => clearHandoffCookies(loginErrorRedirect(origin, config.loginPath, code));
323
+ return {
324
+ cookies,
325
+ /** GET `?source=&from=` */
326
+ async start(request) {
327
+ const url = new URL(request.url);
328
+ const sourceKey = url.searchParams.get("source")?.trim() || config.defaultSource;
329
+ const source = config.sources[sourceKey];
330
+ if (!source) return fail(url.origin, "handoff_request_invalid");
331
+ const sourceOrigin = originOf(resolveValue(source));
332
+ if (!sourceOrigin) return fail(url.origin, "handoff_not_configured");
333
+ const verifier = randomToken(48);
334
+ const state = randomToken(32);
335
+ const start = new URL(sourceStartPath(config.app), sourceOrigin);
336
+ start.searchParams.set("code_challenge", await s256(verifier));
337
+ start.searchParams.set("state", state);
338
+ const response = redirect(start);
339
+ setCookie(response, cookies.verifier, verifier, cookieOptions);
340
+ setCookie(response, cookies.state, state, cookieOptions);
341
+ const from = safeInternalPath(url.searchParams.get("from"));
342
+ if (from) setCookie(response, cookies.from, from, cookieOptions);
343
+ return response;
344
+ },
345
+ /** GET `?code=&state=` */
346
+ async callback(request) {
347
+ const url = new URL(request.url);
348
+ const code = url.searchParams.get("code") ?? "";
349
+ const state = url.searchParams.get("state") ?? "";
350
+ const expectedState = readCookie(request, cookies.state) ?? "";
351
+ const verifier = readCookie(request, cookies.verifier) ?? "";
352
+ if (!code || !state || !expectedState || state !== expectedState || !verifier) {
353
+ return fail(url.origin, "handoff_state_invalid");
354
+ }
355
+ const device = config.device.read(request);
356
+ const respond = (response) => config.device.persist(response, device);
357
+ try {
358
+ const headers = forwardedRequestHeaders(request, {
359
+ "Content-Type": "application/json",
360
+ "X-Client-Type": config.clientType,
361
+ "X-Client-Device-ID": device.value
362
+ });
363
+ const response = await timedFetch(`${resolveValue(config.apiUrl)}${brokerPaths(config.app).exchange}`, { init: { method: "POST", headers, body: JSON.stringify({ code, code_verifier: verifier }) }, timeoutMs: timeout, fetch: config.fetch });
364
+ const payload = await response.json().catch(() => ({}));
365
+ if (!response.ok) {
366
+ const backendCode = payload?.code;
367
+ return respond(fail(url.origin, typeof backendCode === "string" ? backendCode : "handoff_exchange_failed"));
368
+ }
369
+ const session = parseAuthResponse(payload);
370
+ if (!session || !hasTokenAudience(session.access_token, config.audience)) {
371
+ return respond(fail(url.origin, "handoff_exchange_failed"));
372
+ }
373
+ const from = safeInternalPath(readCookie(request, cookies.from)) ?? config.defaultReturnPath;
374
+ const established = await config.establishSession(session, redirect(new URL(from, url.origin)), request) ?? redirect(new URL(from, url.origin));
375
+ return respond(clearHandoffCookies(established));
376
+ } catch {
377
+ return respond(fail(url.origin, "handoff_exchange_failed"));
378
+ }
379
+ }
380
+ };
381
+ }
382
+ var DEFAULT_TARGET_CALLBACK = "/api/auth/platform-sso/callback";
383
+ var DEFAULT_TARGET_LOGIN = "/login";
384
+ function createHandoffSource(config) {
385
+ const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
386
+ return {
387
+ /** GET `/api/auth/handoffs/{app}/start?code_challenge=&state=` */
388
+ async start(request, app) {
389
+ const target = config.targets[app];
390
+ if (!target) return new Response("unknown handoff application", { status: 404, headers: { "Cache-Control": "no-store" } });
391
+ const targetOrigin = originOf(resolveValue(target.origin));
392
+ const targetLogin = target.loginPath ?? DEFAULT_TARGET_LOGIN;
393
+ if (!targetOrigin) return new Response("handoff target not configured", { status: 503, headers: { "Cache-Control": "no-store" } });
394
+ const targetError = (code) => loginErrorRedirect(targetOrigin, targetLogin, code);
395
+ const url = new URL(request.url);
396
+ const challenge = url.searchParams.get("code_challenge")?.trim() ?? "";
397
+ const state = url.searchParams.get("state")?.trim() ?? "";
398
+ if (!CHALLENGE_PATTERN.test(challenge) || !validState(state)) return targetError("handoff_request_invalid");
399
+ const sourceLogin = () => {
400
+ const login = new URL(config.loginPath, url.origin);
401
+ login.searchParams.set("from", `${url.pathname}${url.search}`);
402
+ return redirect(login);
403
+ };
404
+ const token = await config.getAccessToken(request);
405
+ if (!token) return sourceLogin();
406
+ try {
407
+ const headers = forwardedRequestHeaders(request, {
408
+ "Content-Type": "application/json",
409
+ "X-Client-Type": config.clientType,
410
+ Authorization: `Bearer ${token}`
411
+ });
412
+ const response = await timedFetch(`${resolveValue(config.apiUrl)}${brokerPaths(app).start}`, { init: { method: "POST", headers, body: JSON.stringify({ code_challenge: challenge, state }) }, timeoutMs: timeout, fetch: config.fetch });
413
+ if (response.status === 401) return sourceLogin();
414
+ const payload = await response.json().catch(() => ({}));
415
+ if (!response.ok) {
416
+ const backendCode = payload?.code;
417
+ return targetError(typeof backendCode === "string" ? backendCode : "handoff_start_failed");
418
+ }
419
+ const redirectUrl = payload?.redirect_url;
420
+ if (typeof redirectUrl !== "string") return targetError("handoff_start_failed");
421
+ let destination;
422
+ try {
423
+ destination = new URL(redirectUrl);
424
+ } catch {
425
+ return targetError("handoff_redirect_invalid");
426
+ }
427
+ if (destination.origin !== targetOrigin || destination.pathname !== (target.callbackPath ?? DEFAULT_TARGET_CALLBACK)) {
428
+ return targetError("handoff_redirect_invalid");
429
+ }
430
+ return redirect(destination);
431
+ } catch {
432
+ return targetError("handoff_start_failed");
433
+ }
434
+ }
435
+ };
436
+ }
437
+
438
+ // src/session-refresh.ts
439
+ var DEFAULT_TTL_MS = 5e3;
440
+ var DEFAULT_MAX = 1e3;
441
+ function parseRefreshEnvelope(value) {
442
+ if (!value || typeof value !== "object") return null;
443
+ const outer = value;
444
+ const payload = outer.data && typeof outer.data === "object" ? outer.data : value;
445
+ if (typeof payload.access_token !== "string" || !payload.access_token) return null;
446
+ return {
447
+ accessToken: payload.access_token,
448
+ refreshToken: typeof payload.refresh_token === "string" && payload.refresh_token ? payload.refresh_token : void 0
449
+ };
450
+ }
451
+ function createSessionRefresher(config) {
452
+ const ttl = config.singleflightTtlMs ?? DEFAULT_TTL_MS;
453
+ const max = config.maxSingleflights ?? DEFAULT_MAX;
454
+ const timeoutMs = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
455
+ const inflight = /* @__PURE__ */ new Map();
456
+ async function perform(refreshToken) {
457
+ try {
458
+ const pending = resolveAll(config.apiUrl, config.headers);
459
+ const [origin, extra] = pending instanceof Promise ? await pending : pending;
460
+ const headers = new Headers(extra);
461
+ headers.set("Content-Type", "application/json");
462
+ headers.set("X-Client-Type", config.clientType);
463
+ const response = await timedFetch(`${origin}${config.refreshPath}`, {
464
+ init: {
465
+ method: "POST",
466
+ headers,
467
+ body: JSON.stringify({ refresh_token: refreshToken })
468
+ },
469
+ timeoutMs,
470
+ fetch: config.fetch
471
+ });
472
+ if (!response.ok) return null;
473
+ const refreshed = parseRefreshEnvelope(await response.json());
474
+ return refreshed && hasTokenAudience(refreshed.accessToken, config.audience) ? refreshed : null;
475
+ } catch {
476
+ return null;
477
+ }
478
+ }
479
+ function prune(now) {
480
+ for (const [token, entry] of inflight) {
481
+ if (entry.expiresAt <= now) inflight.delete(token);
482
+ }
483
+ while (inflight.size >= max) {
484
+ const oldest = inflight.keys().next().value;
485
+ if (typeof oldest !== "string") break;
486
+ inflight.delete(oldest);
487
+ }
488
+ }
489
+ return {
490
+ refresh(refreshToken) {
491
+ const now = Date.now();
492
+ prune(now);
493
+ const existing = inflight.get(refreshToken);
494
+ if (existing && existing.expiresAt > now) return existing.promise;
495
+ const promise = perform(refreshToken).then((result) => {
496
+ if (!result) inflight.delete(refreshToken);
497
+ return result;
498
+ });
499
+ inflight.set(refreshToken, { expiresAt: now + ttl, promise });
500
+ return promise;
501
+ },
502
+ size() {
503
+ return inflight.size;
504
+ }
505
+ };
506
+ }
507
+
508
+ // src/session-gate.ts
509
+ function hasSessionShape(token) {
510
+ if (!token) return false;
511
+ const payload = decodeTokenPayload(token);
512
+ return Boolean(payload?.user_id || payload?.actor_id || payload?.customer_id || payload?.sub);
513
+ }
514
+ async function evaluateSessionGate(input) {
515
+ const { accessToken, refreshToken, audience, refresher, isPublic, isLoginPath, isApiPath } = input;
516
+ const live = !!accessToken && hasSessionShape(accessToken) && hasTokenAudience(accessToken, audience) && !isTokenExpired(accessToken);
517
+ if (live) {
518
+ if (isPublic && isLoginPath) return { kind: "redirect_home" };
519
+ if (refreshToken && isTokenExpiringSoon(accessToken, input.expiringSoonBufferSeconds)) {
520
+ const refreshed = await refresher.refresh(refreshToken);
521
+ if (refreshed) return { kind: "pass", refreshed };
522
+ }
523
+ return { kind: "pass" };
524
+ }
525
+ if (refreshToken) {
526
+ const refreshed = await refresher.refresh(refreshToken);
527
+ if (refreshed) return isPublic ? { kind: "redirect_home", refreshed } : { kind: "pass", refreshed };
528
+ }
529
+ if (isPublic) return { kind: "anonymous_public" };
530
+ if (isApiPath) return { kind: "unauthorized_api" };
531
+ return { kind: "redirect_login" };
532
+ }
533
+
534
+ export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, applyForwardedClientHeaders, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
package/dist/next.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { SessionData, CookieOptions } from './index.js';
2
- export { AUTH_ERROR_MESSAGES, AuthResponse, DeviceIdentity, DeviceIdentityConfig, DeviceIdentityStore, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, HandoffSource, HandoffSourceConfig, HandoffSourceTarget, HandoffTarget, HandoffTargetConfig, RefreshedSession, SSO_ERROR_MESSAGES, SessionGateDecision, SessionGateInput, SessionRefresher, SessionRefresherConfig, SessionUser, TokenPayload, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds } from './index.js';
1
+ import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-BnchfjP_.js';
2
+ export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshedSession, l as SSO_ERROR_MESSAGES, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as authErrorMessage, s as brokerPaths, t as createDeviceIdentity, u as createHandoffSource, v as createHandoffTarget, w as createSessionRefresher, x as decodeBase64Url, y as decodeDefaultSession, z as decodeTokenPayload, B as deleteCookie, E as evaluateSessionGate, F as handoffErrorMessage, G as hasSessionShape, I as hasTokenAudience, J as isAuthErrorCode, K as isHandoffErrorCode, L as isTokenExpired, M as isTokenExpiringSoon, N as parseAuthResponse, O as parseRefreshEnvelope, P as readCookie, Q as safeInternalPath, U as serializeCookie, V as setCookie, W as sourceStartPath, X as tokenMaxAgeSeconds } from './index-BnchfjP_.js';
3
3
  export { applyForwardedClientHeaders, forwardedRequestHeaders } from '@c1m-public/transport';
4
- import { R as Resolvable, A as AsyncResolvable } from './internal-D6RafCm2.js';
5
4
 
6
5
  /**
7
6
  * The error vocabulary every first-party platform surface uses at its edge.