@c1m-public/platform-auth 0.9.0 → 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.
@@ -1,12 +1,342 @@
1
- import { decodeTokenPayload } from '../chunk-FBC2NUH7.js';
2
- import { AccessSnapshotError, fetchAccessSnapshot, isAccessSnapshotError, hasAnyPermission, hasAllPermissions, hasWorkspacePermission } from '../chunk-U67NBJND.js';
3
- export { AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, LEADBROKER_PERMISSIONS, PIPELINE_STAGES, PLATFORM_PERMISSIONS, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, appScopes, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPipelineStage, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor } from '../chunk-U67NBJND.js';
4
- import { resolveValue } from '../chunk-ZWSJVE5S.js';
5
1
  import 'server-only';
6
2
  import { cache } from 'react';
7
3
  import { forbidden, unauthorized } from 'next/navigation.js';
8
4
  import { updateTag } from 'next/cache.js';
9
5
 
6
+ // src/access/next.ts
7
+
8
+ // src/internal.ts
9
+ function resolveValue(value) {
10
+ return typeof value === "function" ? value() : value;
11
+ }
12
+ async function timedFetch(url, options = {}) {
13
+ const doFetch = options.fetch ?? options.fetchImpl ?? fetch;
14
+ const controller = options.timeoutMs ? new AbortController() : void 0;
15
+ const timer = controller ? setTimeout(() => controller.abort(), options.timeoutMs) : void 0;
16
+ try {
17
+ return await doFetch(url, {
18
+ ...options.init,
19
+ cache: "no-store",
20
+ ...controller ? { signal: controller.signal } : {}
21
+ });
22
+ } finally {
23
+ if (timer) clearTimeout(timer);
24
+ }
25
+ }
26
+
27
+ // src/token.ts
28
+ function decodeBase64Url(input) {
29
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
30
+ const padded = normalized.padEnd(normalized.length + (4 - normalized.length % 4) % 4, "=");
31
+ try {
32
+ const binary = globalThis.atob(padded);
33
+ const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
34
+ return new TextDecoder().decode(bytes);
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function decodeTokenPayload(token) {
40
+ const payloadSegment = token.split(".")[1];
41
+ if (!payloadSegment) return null;
42
+ const decodedJson = decodeBase64Url(payloadSegment);
43
+ if (!decodedJson) return null;
44
+ try {
45
+ const parsed = JSON.parse(decodedJson);
46
+ return parsed && typeof parsed === "object" ? parsed : null;
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // src/access/errors.ts
53
+ var AccessSnapshotError = class extends Error {
54
+ code;
55
+ status;
56
+ constructor(code, message, status) {
57
+ super(message);
58
+ this.name = "AccessSnapshotError";
59
+ this.code = code;
60
+ this.status = status;
61
+ }
62
+ };
63
+ function isAccessSnapshotError(error, code) {
64
+ return error instanceof AccessSnapshotError && (code === void 0 || error.code === code);
65
+ }
66
+
67
+ // ../transport/src/forwarding.ts
68
+ function applyForwardedClientHeaders(incoming, headers = new Headers()) {
69
+ const forwardedFor = incoming.get("x-forwarded-for");
70
+ const realIp = incoming.get("x-real-ip") ?? forwardedFor?.split(",")[0]?.trim();
71
+ if (realIp && !headers.has("X-Client-IP")) headers.set("X-Client-IP", realIp);
72
+ if (realIp && !headers.has("X-Real-IP")) headers.set("X-Real-IP", realIp);
73
+ if (forwardedFor && !headers.has("X-Forwarded-For")) headers.set("X-Forwarded-For", forwardedFor);
74
+ const country = incoming.get("cf-ipcountry") ?? incoming.get("x-vercel-ip-country") ?? incoming.get("x-geo-country") ?? incoming.get("x-country-code");
75
+ if (country && country !== "XX" && !headers.has("X-Geo-Country")) {
76
+ headers.set("X-Geo-Country", country);
77
+ }
78
+ const host = incoming.get("host");
79
+ if (host && !headers.has("X-Forwarded-Host")) headers.set("X-Forwarded-Host", host);
80
+ const proto = incoming.get("x-forwarded-proto");
81
+ if (proto && !headers.has("X-Forwarded-Proto")) headers.set("X-Forwarded-Proto", proto);
82
+ const userAgent = incoming.get("user-agent");
83
+ if (userAgent && !headers.has("X-Client-User-Agent")) headers.set("X-Client-User-Agent", userAgent);
84
+ return headers;
85
+ }
86
+ function forwardedRequestHeaders(request, init) {
87
+ return applyForwardedClientHeaders(request.headers, new Headers(init));
88
+ }
89
+
90
+ // src/access/snapshot.ts
91
+ var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "saas_app", "workspace"]);
92
+ function emptySnapshot(subjectId = "") {
93
+ return {
94
+ subject_id: subjectId,
95
+ access_version: 0,
96
+ is_owner: false,
97
+ active: false,
98
+ platform_role: "",
99
+ permissions: [],
100
+ dash_roles: {},
101
+ workspace_permissions: {},
102
+ workspace_roles: {}
103
+ };
104
+ }
105
+ function isRecord(value) {
106
+ return typeof value === "object" && value !== null && !Array.isArray(value);
107
+ }
108
+ function fail(field, why) {
109
+ throw new AccessSnapshotError("invalid_snapshot", `access snapshot: ${field} ${why}`);
110
+ }
111
+ function parseAccessSnapshot(input) {
112
+ if (!isRecord(input)) fail("payload", "is not an object");
113
+ const { subject_id, access_version, is_owner, active, platform_role, permissions, dash_roles, workspace_permissions, workspace_roles } = input;
114
+ if (typeof subject_id !== "string" || subject_id === "") fail("subject_id", "must be a non-empty string");
115
+ if (typeof access_version !== "number" || !Number.isInteger(access_version) || access_version < 0) fail("access_version", "must be a non-negative integer");
116
+ if (typeof is_owner !== "boolean") fail("is_owner", "must be a boolean");
117
+ if (typeof active !== "boolean") fail("active", "must be a boolean");
118
+ if (platform_role !== void 0 && platform_role !== null && typeof platform_role !== "string") fail("platform_role", "must be a string");
119
+ if (!Array.isArray(permissions)) fail("permissions", "must be an array");
120
+ const entries = permissions.map((raw, i) => {
121
+ if (!isRecord(raw)) fail(`permissions[${i}]`, "is not an object");
122
+ const { key, scope_type, scope_id } = raw;
123
+ if (typeof key !== "string" || key === "") fail(`permissions[${i}].key`, "must be a non-empty string");
124
+ if (typeof scope_type !== "string" || !SCOPE_TYPES.has(scope_type)) fail(`permissions[${i}].scope_type`, "must be platform|saas_app|workspace");
125
+ if (scope_id !== void 0 && scope_id !== null && typeof scope_id !== "string") fail(`permissions[${i}].scope_id`, "must be a string");
126
+ return { key, scope_type, scope_id: typeof scope_id === "string" ? scope_id : "" };
127
+ });
128
+ const dashRoles = {};
129
+ if (dash_roles !== void 0 && dash_roles !== null) {
130
+ if (!isRecord(dash_roles)) fail("dash_roles", "must be an object");
131
+ for (const [dash, role] of Object.entries(dash_roles)) {
132
+ if (typeof role !== "string") fail(`dash_roles[${dash}]`, "must be a string");
133
+ dashRoles[dash] = role;
134
+ }
135
+ }
136
+ const wsPerms = {};
137
+ if (workspace_permissions !== void 0 && workspace_permissions !== null) {
138
+ if (!isRecord(workspace_permissions)) fail("workspace_permissions", "must be an object");
139
+ for (const [ws, keys] of Object.entries(workspace_permissions)) {
140
+ if (!Array.isArray(keys) || keys.some((k) => typeof k !== "string")) fail(`workspace_permissions[${ws}]`, "must be a string array");
141
+ wsPerms[ws] = [...keys];
142
+ }
143
+ }
144
+ const wsRoles = {};
145
+ if (workspace_roles !== void 0 && workspace_roles !== null) {
146
+ if (!isRecord(workspace_roles)) fail("workspace_roles", "must be an object");
147
+ for (const [ws, role] of Object.entries(workspace_roles)) {
148
+ if (typeof role !== "string") fail(`workspace_roles[${ws}]`, "must be a string");
149
+ wsRoles[ws] = role;
150
+ }
151
+ }
152
+ return {
153
+ subject_id,
154
+ access_version,
155
+ is_owner,
156
+ active,
157
+ platform_role: typeof platform_role === "string" ? platform_role : "",
158
+ permissions: entries,
159
+ dash_roles: dashRoles,
160
+ workspace_permissions: wsPerms,
161
+ workspace_roles: wsRoles
162
+ };
163
+ }
164
+
165
+ // src/access/fetch.ts
166
+ var DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
167
+ async function fetchAccessSnapshot(opts) {
168
+ const url = `${resolveValue(opts.apiUrl).replace(/\/$/, "")}${opts.endpointPath ?? DEFAULT_ACCESS_ENDPOINT_PATH}`;
169
+ const headers = opts.request ? forwardedRequestHeaders(opts.request, opts.headers) : new Headers(opts.headers);
170
+ headers.set("Authorization", `Bearer ${opts.accessToken}`);
171
+ headers.set("Accept", "application/json");
172
+ let response;
173
+ try {
174
+ response = await timedFetch(url, {
175
+ init: { method: "GET", headers },
176
+ timeoutMs: opts.fetchTimeoutMs,
177
+ fetchImpl: opts.fetchImpl
178
+ });
179
+ } catch (error) {
180
+ throw new AccessSnapshotError("unavailable", `access snapshot request failed: ${error.message}`);
181
+ }
182
+ if (response.status === 401) throw new AccessSnapshotError("unauthenticated", "session expired", 401);
183
+ if (response.status === 403) throw new AccessSnapshotError("forbidden", "access snapshot refused", 403);
184
+ if (!response.ok) throw new AccessSnapshotError("unavailable", `access snapshot returned ${response.status}`, response.status);
185
+ let body;
186
+ try {
187
+ body = await response.json();
188
+ } catch {
189
+ throw new AccessSnapshotError("invalid_snapshot", "access snapshot body is not JSON", response.status);
190
+ }
191
+ return parseAccessSnapshot(body);
192
+ }
193
+
194
+ // src/access/predicates.ts
195
+ function entryCoversApp(entry, saasApp) {
196
+ switch (entry.scope_type) {
197
+ case "platform":
198
+ return true;
199
+ case "saas_app":
200
+ if (saasApp === void 0) return true;
201
+ return entry.scope_id === "*" || entry.scope_id.toLowerCase() === saasApp.toLowerCase();
202
+ default:
203
+ return false;
204
+ }
205
+ }
206
+ function hasPermission(snapshot, key, opts = {}) {
207
+ if (!snapshot.active) return false;
208
+ return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.saasApp));
209
+ }
210
+ function hasAnyPermission(snapshot, keys, opts = {}) {
211
+ return keys.some((key) => hasPermission(snapshot, key, opts));
212
+ }
213
+ function hasAllPermissions(snapshot, keys, opts = {}) {
214
+ return keys.every((key) => hasPermission(snapshot, key, opts));
215
+ }
216
+ function hasWorkspacePermission(snapshot, workspaceId, key) {
217
+ if (!snapshot.active) return false;
218
+ if (snapshot.is_owner) return true;
219
+ return (snapshot.workspace_permissions[workspaceId] ?? []).includes(key);
220
+ }
221
+ function managedDashes(snapshot) {
222
+ if (!snapshot.active) return [];
223
+ return Object.entries(snapshot.dash_roles).map(([saasApp, role]) => ({ saasApp, role })).sort((a, b) => a.saasApp.localeCompare(b.saasApp));
224
+ }
225
+ function dashRole(snapshot, saasApp) {
226
+ if (!snapshot.active) return "";
227
+ return snapshot.dash_roles[saasApp] ?? "";
228
+ }
229
+ function isPlatformOwner(snapshot) {
230
+ return snapshot.active && snapshot.is_owner;
231
+ }
232
+ function appScopes(snapshot, key) {
233
+ if (!snapshot.active) return { all: false, apps: [] };
234
+ const apps = /* @__PURE__ */ new Set();
235
+ for (const entry of snapshot.permissions) {
236
+ if (entry.key !== key) continue;
237
+ if (entry.scope_type === "platform") return { all: true, apps: [] };
238
+ if (entry.scope_type === "saas_app") {
239
+ if (entry.scope_id === "*") return { all: true, apps: [] };
240
+ apps.add(entry.scope_id.toLowerCase());
241
+ }
242
+ }
243
+ return { all: false, apps: [...apps].sort() };
244
+ }
245
+ function permissionsFor(snapshot, opts = {}) {
246
+ if (!snapshot.active) return [];
247
+ const keys = /* @__PURE__ */ new Set();
248
+ for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.saasApp)) keys.add(entry.key);
249
+ return [...keys].sort();
250
+ }
251
+
252
+ // src/access/generated/keys.ts
253
+ var PLATFORM_PERMISSIONS = [
254
+ "affiliates.finance.manage",
255
+ "affiliates.partners.manage",
256
+ "affiliates.programmes.manage",
257
+ "affiliates.view",
258
+ "linkservice.domains.manage",
259
+ "linkservice.links.manage",
260
+ "linkservice.qr.manage",
261
+ "linkservice.view",
262
+ "platform.admin.view",
263
+ "platform.audit.view",
264
+ "platform.auth.manage",
265
+ "platform.billing.manage",
266
+ "platform.channels.manage",
267
+ "platform.channels.view",
268
+ "platform.config.manage",
269
+ "platform.docs.manage",
270
+ "platform.docs.view",
271
+ "platform.observability.manage",
272
+ "platform.observability.view",
273
+ "platform.offboarding.view",
274
+ "platform.ops.manage",
275
+ "platform.support.impersonate",
276
+ "platform.support.sessions",
277
+ "platform.tenants.archive",
278
+ "platform.tenants.manage",
279
+ "platform.tenants.restore",
280
+ "platform.users.deactivate",
281
+ "platform.users.delete",
282
+ "platform.users.invite",
283
+ "platform.users.manage_access",
284
+ "platform.users.reactivate",
285
+ "platform.users.view"
286
+ ];
287
+ var DASH_PERMISSIONS = [
288
+ "dash.apps.manage",
289
+ "dash.billing.manage",
290
+ "dash.channels.manage",
291
+ "dash.customers.manage",
292
+ "dash.domains.manage",
293
+ "dash.members.manage",
294
+ "dash.ownership.transfer",
295
+ "dash.plans.manage",
296
+ "dash.publish",
297
+ "dash.settings.manage",
298
+ "dash.workspaces.admin",
299
+ "dash.workspaces.manage"
300
+ ];
301
+ var DASH_ROLES = [
302
+ "dash.admin",
303
+ "dash.owner"
304
+ ];
305
+ var LEADBROKER_PERMISSIONS = [
306
+ "leadbroker.finance.manage",
307
+ "leadbroker.leads.dispatch",
308
+ "leadbroker.leads.review",
309
+ "leadbroker.leads.view",
310
+ "leadbroker.policy.manage"
311
+ ];
312
+ var WORKSPACE_PERMISSIONS = [
313
+ "workspace.channels.manage",
314
+ "workspace.channels.secrets",
315
+ "workspace.channels.view",
316
+ "workspace.crm.manage",
317
+ "workspace.crm.view",
318
+ "workspace.customers.manage",
319
+ "workspace.customers.view",
320
+ "workspace.orders.manage",
321
+ "workspace.orders.view",
322
+ "workspace.payments.record",
323
+ "workspace.site.edit",
324
+ "workspace.site.view",
325
+ "workspace.staff.manage"
326
+ ];
327
+ var WORKSPACE_ROLES = [
328
+ "workspace.editor",
329
+ "workspace.frontdesk",
330
+ "workspace.manager"
331
+ ];
332
+
333
+ // src/access/generated/stages.ts
334
+ var PIPELINE_STAGES = ["created", "awaiting_payment", "provisioning", "attention", "active"];
335
+ function isPipelineStage(value) {
336
+ return typeof value === "string" && PIPELINE_STAGES.includes(value);
337
+ }
338
+
339
+ // src/access/next.ts
10
340
  function accessCacheTag(subjectId) {
11
341
  return `access:${subjectId}`;
12
342
  }
@@ -72,4 +402,4 @@ function createAccessLoader(options) {
72
402
  return { getAccess, getAccessOrNull, requireAccess, requireWorkspaceAccess, invalidateAccess, cacheTag: tag };
73
403
  }
74
404
 
75
- export { accessCacheTag, createAccessLoader };
405
+ export { AccessSnapshotError, DASH_PERMISSIONS, DASH_ROLES, DEFAULT_ACCESS_ENDPOINT_PATH, LEADBROKER_PERMISSIONS, PIPELINE_STAGES, PLATFORM_PERMISSIONS, WORKSPACE_PERMISSIONS, WORKSPACE_ROLES, accessCacheTag, appScopes, createAccessLoader, dashRole, emptySnapshot, entryCoversApp, fetchAccessSnapshot, hasAllPermissions, hasAnyPermission, hasPermission, hasWorkspacePermission, isAccessSnapshotError, isPipelineStage, isPlatformOwner, managedDashes, parseAccessSnapshot, permissionsFor };
@@ -0,0 +1,235 @@
1
+ import '@c1m-public/transport';
2
+
3
+ interface SessionUser {
4
+ id: string;
5
+ email: string;
6
+ name: string;
7
+ role: string;
8
+ is_active: boolean;
9
+ email_verified: boolean;
10
+ created_at: string;
11
+ last_login_at?: string;
12
+ has_passkey?: boolean;
13
+ }
14
+ interface SessionData {
15
+ user: SessionUser;
16
+ token: string;
17
+ exp: number;
18
+ iat: number;
19
+ }
20
+ type TokenPayload = Record<string, unknown> & {
21
+ sub?: string;
22
+ exp?: number;
23
+ iat?: number;
24
+ actor_id?: string;
25
+ effective_user_id?: string;
26
+ customer_id?: string;
27
+ user_id?: string;
28
+ aud?: string | string[];
29
+ email?: string;
30
+ name?: string;
31
+ role?: string;
32
+ is_active?: boolean;
33
+ email_verified?: boolean;
34
+ created_at?: string;
35
+ has_passkeys?: boolean;
36
+ };
37
+ declare function decodeBase64Url(input: string): string | null;
38
+ declare function decodeTokenPayload(token: string): TokenPayload | null;
39
+ declare function hasTokenAudience(token: string, requiredAudience: string): boolean;
40
+ /** Cookie max-age clamped to the token's remaining lifetime. */
41
+ declare function tokenMaxAgeSeconds(token: string, configuredMaxAge: number): number;
42
+ declare function isTokenExpired(token: string): boolean;
43
+ declare function isTokenExpiringSoon(token: string, bufferSeconds?: number): boolean;
44
+ declare function decodeDefaultSession(token: string): SessionData | null;
45
+ /** Backend auth response shape shared by verify-code, OIDC complete, and handoff exchange. */
46
+ interface AuthResponse {
47
+ access_token: string;
48
+ refresh_token: string;
49
+ token_type: string;
50
+ expires_at: string;
51
+ user: Record<string, unknown>;
52
+ }
53
+ declare function parseAuthResponse(value: unknown): AuthResponse | null;
54
+
55
+ /**
56
+ * Strict same-origin return path: must start with `/`, not `//` or contain `://`,
57
+ * no encoded slashes or control chars, and round-trip URL parsing unchanged.
58
+ * Returns the value verbatim (query/hash kept) or null.
59
+ */
60
+ declare function safeInternalPath(raw: string | null | undefined): string | null;
61
+
62
+ interface CookieOptions {
63
+ httpOnly?: boolean;
64
+ secure?: boolean;
65
+ sameSite?: "lax" | "strict" | "none";
66
+ path?: string;
67
+ maxAge?: number;
68
+ }
69
+ declare function readCookie(request: Request, name: string): string | null;
70
+ declare function serializeCookie(name: string, value: string, options?: CookieOptions): string;
71
+ declare function setCookie(response: Response, name: string, value: string, options?: CookieOptions): Response;
72
+ declare function deleteCookie(response: Response, name: string, options?: Pick<CookieOptions, "path" | "secure">): Response;
73
+
74
+ /**
75
+ * Per-browser identity sent as `X-Client-Device-ID`; c1m-server refuses to
76
+ * create a session without it. Informational only, no authority.
77
+ */
78
+ interface DeviceIdentity {
79
+ value: string;
80
+ created: boolean;
81
+ }
82
+ interface DeviceIdentityConfig {
83
+ cookieName: string;
84
+ secure?: boolean;
85
+ maxAgeSeconds?: number;
86
+ }
87
+ declare function createDeviceIdentity(config: DeviceIdentityConfig): {
88
+ read(request: Request): DeviceIdentity;
89
+ persist(response: Response, identity: DeviceIdentity): Response;
90
+ };
91
+ type DeviceIdentityStore = ReturnType<typeof createDeviceIdentity>;
92
+
93
+ declare const HANDOFF_ERROR_MESSAGES: Record<string, string>;
94
+ declare const HANDOFF_ERROR_FALLBACK = "Sign-in failed \u2014 please try again.";
95
+ declare const SSO_ERROR_MESSAGES: Record<string, string>;
96
+ /** Every sign-in failure code this package knows: handoff, admission, and SSO. */
97
+ declare const AUTH_ERROR_MESSAGES: Record<string, string>;
98
+ declare function handoffErrorMessage(code: string | null | undefined): string;
99
+ declare function isHandoffErrorCode(code: string | null | undefined): boolean;
100
+ /**
101
+ * Resolve any sign-in failure code to copy. `overrides` wins, so a surface can
102
+ * reword a shared code or add its own without forking the whole map.
103
+ */
104
+ declare function authErrorMessage(code: string | null | undefined, overrides?: Record<string, string>): string;
105
+ declare function isAuthErrorCode(code: string | null | undefined, overrides?: Record<string, string>): boolean;
106
+
107
+ /** A value that may be fixed or resolved per call (env read at request time). */
108
+ type Resolvable<T = string> = T | (() => T);
109
+ /** A value, a sync resolver, or an async resolver. */
110
+ type AsyncResolvable<T> = T | (() => T | Promise<T>);
111
+
112
+ declare function sourceStartPath(app: string): string;
113
+ declare function brokerPaths(app: string): {
114
+ readonly start: `/api/v1/platform/auth/handoffs/${string}/start`;
115
+ readonly exchange: `/api/v1/platform/auth/handoffs/${string}/exchange`;
116
+ };
117
+ interface HandoffTargetConfig {
118
+ app: string;
119
+ apiUrl: Resolvable;
120
+ audience: string;
121
+ clientType: string;
122
+ sources: Record<string, Resolvable>;
123
+ defaultSource: string;
124
+ cookiePrefix: string;
125
+ loginPath: string;
126
+ defaultReturnPath: string;
127
+ device: DeviceIdentityStore;
128
+ /** Write session cookies onto `response` (or return void if written via a framework store). */
129
+ establishSession: (session: AuthResponse, response: Response, request: Request) => Promise<Response | void> | Response | void;
130
+ secureCookies?: boolean;
131
+ timeoutMs?: number;
132
+ /** @deprecated use `timeoutMs`. */
133
+ fetchTimeoutMs?: number;
134
+ fetch?: typeof fetch;
135
+ }
136
+ declare function createHandoffTarget(config: HandoffTargetConfig): {
137
+ cookies: {
138
+ readonly state: `${string}state`;
139
+ readonly verifier: `${string}verifier`;
140
+ readonly from: `${string}from`;
141
+ };
142
+ /** GET `?source=&from=` */
143
+ start(request: Request): Promise<Response>;
144
+ /** GET `?code=&state=` */
145
+ callback(request: Request): Promise<Response>;
146
+ };
147
+ type HandoffTarget = ReturnType<typeof createHandoffTarget>;
148
+ interface HandoffSourceTarget {
149
+ origin: Resolvable;
150
+ callbackPath?: string;
151
+ loginPath?: string;
152
+ }
153
+ interface HandoffSourceConfig {
154
+ apiUrl: Resolvable;
155
+ clientType: string;
156
+ targets: Record<string, HandoffSourceTarget>;
157
+ /** Live bearer or null when signed out; refresh an expiring token before returning it. */
158
+ getAccessToken: (request: Request) => Promise<string | null> | string | null;
159
+ loginPath: string;
160
+ timeoutMs?: number;
161
+ /** @deprecated use `timeoutMs`. */
162
+ fetchTimeoutMs?: number;
163
+ fetch?: typeof fetch;
164
+ }
165
+ declare function createHandoffSource(config: HandoffSourceConfig): {
166
+ /** GET `/api/auth/handoffs/{app}/start?code_challenge=&state=` */
167
+ start(request: Request, app: string): Promise<Response>;
168
+ };
169
+ type HandoffSource = ReturnType<typeof createHandoffSource>;
170
+
171
+ /**
172
+ * Coalesced refresh-token exchange for a BFF proxy: same refresh token within
173
+ * a short window shares one backend call. Per-process best effort; backend
174
+ * reuse detection is the correctness guarantee.
175
+ */
176
+ interface RefreshedSession {
177
+ accessToken: string;
178
+ refreshToken?: string;
179
+ }
180
+ interface SessionRefresherConfig {
181
+ /** Backend origin, no /api/v1. May resolve asynchronously (a tenant-scoped origin). */
182
+ apiUrl: AsyncResolvable<string>;
183
+ /** e.g. /api/v1/platform/auth/refresh; empty when `apiUrl` already resolves to the full URL. */
184
+ refreshPath: string;
185
+ /** Extra request headers, e.g. a service credential and its tenant selector. */
186
+ headers?: AsyncResolvable<HeadersInit | undefined>;
187
+ clientType: string;
188
+ audience: string;
189
+ singleflightTtlMs?: number;
190
+ maxSingleflights?: number;
191
+ timeoutMs?: number;
192
+ /** @deprecated use `timeoutMs`. */
193
+ fetchTimeoutMs?: number;
194
+ fetch?: typeof fetch;
195
+ }
196
+ /**
197
+ * Two refresh envelopes exist: the platform one (flat, with token_type and
198
+ * expires_at) and the end-user one (`{ data: { access_token, refresh_token? } }`,
199
+ * where a rotated refresh token is optional). Both carry the access token the
200
+ * same way; that is all a refresh needs.
201
+ */
202
+ declare function parseRefreshEnvelope(value: unknown): RefreshedSession | null;
203
+ declare function createSessionRefresher(config: SessionRefresherConfig): {
204
+ refresh(refreshToken: string): Promise<RefreshedSession | null>;
205
+ size(): number;
206
+ };
207
+ type SessionRefresher = ReturnType<typeof createSessionRefresher>;
208
+
209
+ declare function hasSessionShape(token: string | undefined | null): boolean;
210
+ type SessionGateDecision = {
211
+ kind: "pass";
212
+ refreshed?: RefreshedSession;
213
+ } | {
214
+ kind: "redirect_home";
215
+ refreshed?: RefreshedSession;
216
+ } | {
217
+ kind: "anonymous_public";
218
+ } | {
219
+ kind: "unauthorized_api";
220
+ } | {
221
+ kind: "redirect_login";
222
+ };
223
+ interface SessionGateInput {
224
+ accessToken?: string | null;
225
+ refreshToken?: string | null;
226
+ audience: string;
227
+ refresher: Pick<SessionRefresher, "refresh">;
228
+ isPublic: boolean;
229
+ isLoginPath: boolean;
230
+ isApiPath: boolean;
231
+ expiringSoonBufferSeconds?: number;
232
+ }
233
+ declare function evaluateSessionGate(input: SessionGateInput): Promise<SessionGateDecision>;
234
+
235
+ export { type AsyncResolvable as A, deleteCookie as B, type CookieOptions as C, type DeviceIdentity as D, evaluateSessionGate as E, handoffErrorMessage as F, hasSessionShape as G, HANDOFF_ERROR_FALLBACK as H, hasTokenAudience as I, isAuthErrorCode as J, isHandoffErrorCode as K, isTokenExpired as L, isTokenExpiringSoon as M, parseAuthResponse as N, parseRefreshEnvelope as O, readCookie as P, safeInternalPath as Q, type Resolvable as R, type SessionData as S, type TokenPayload as T, serializeCookie as U, setCookie as V, sourceStartPath as W, tokenMaxAgeSeconds as X, AUTH_ERROR_MESSAGES as a, type AuthResponse as b, type DeviceIdentityConfig as c, type DeviceIdentityStore as d, HANDOFF_ERROR_MESSAGES as e, type HandoffSource as f, type HandoffSourceConfig as g, type HandoffSourceTarget as h, type HandoffTarget as i, type HandoffTargetConfig as j, type RefreshedSession as k, SSO_ERROR_MESSAGES as l, type SessionGateDecision as m, type SessionGateInput as n, type SessionRefresher as o, type SessionRefresherConfig as p, type SessionUser as q, authErrorMessage as r, brokerPaths as s, createDeviceIdentity as t, createHandoffSource as u, createHandoffTarget as v, createSessionRefresher as w, decodeBase64Url as x, decodeDefaultSession as y, decodeTokenPayload as z };