@palbase/web 7.3.9 → 7.4.0

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.
Files changed (47) hide show
  1. package/dist/{analytics-facade-BER0EyYT.d.ts → analytics-facade-But6gu2r.d.ts} +135 -16
  2. package/dist/{analytics-facade-CQXCQSoF.d.cts → analytics-facade-f8IVujJW.d.cts} +135 -16
  3. package/dist/{chunk-EAOROUSW.js → chunk-4WMJTUDN.js} +26 -17
  4. package/dist/chunk-4WMJTUDN.js.map +1 -0
  5. package/dist/{chunk-Q5DUDL77.js → chunk-G56VGUYZ.js} +1 -1
  6. package/dist/chunk-G56VGUYZ.js.map +1 -0
  7. package/dist/{chunk-FCIMEU2E.js → chunk-VHUCJBBQ.js} +2 -2
  8. package/dist/chunk-VHUCJBBQ.js.map +1 -0
  9. package/dist/{chunk-H6YSCGQC.js → chunk-WXBOPPHU.js} +502 -38
  10. package/dist/chunk-WXBOPPHU.js.map +1 -0
  11. package/dist/gen/cli.cjs +1 -1
  12. package/dist/gen/cli.cjs.map +1 -1
  13. package/dist/gen/cli.js +2 -2
  14. package/dist/gen/cli.js.map +1 -1
  15. package/dist/index.cjs +514 -50
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +3 -3
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +3 -3
  20. package/dist/internal.cjs +524 -51
  21. package/dist/internal.cjs.map +1 -1
  22. package/dist/internal.d.cts +24 -4
  23. package/dist/internal.d.ts +24 -4
  24. package/dist/internal.js +3 -3
  25. package/dist/next/client.cjs +518 -48
  26. package/dist/next/client.cjs.map +1 -1
  27. package/dist/next/client.js +3 -3
  28. package/dist/next/index.cjs +524 -51
  29. package/dist/next/index.cjs.map +1 -1
  30. package/dist/next/index.d.cts +2 -2
  31. package/dist/next/index.d.ts +2 -2
  32. package/dist/next/index.js +4 -4
  33. package/dist/next/index.js.map +1 -1
  34. package/dist/next/proxy.cjs.map +1 -1
  35. package/dist/next/proxy.js +3 -3
  36. package/dist/{pb-WAQvwCfM.d.cts → pb-9MMDNQ9p.d.cts} +6 -1
  37. package/dist/{pb-BRlmBIAm.d.ts → pb-DRmdWQzG.d.ts} +6 -1
  38. package/dist/react/index.cjs +22 -19
  39. package/dist/react/index.cjs.map +1 -1
  40. package/dist/react/index.d.cts +1 -1
  41. package/dist/react/index.d.ts +1 -1
  42. package/dist/react/index.js +3 -3
  43. package/package.json +1 -1
  44. package/dist/chunk-EAOROUSW.js.map +0 -1
  45. package/dist/chunk-FCIMEU2E.js.map +0 -1
  46. package/dist/chunk-H6YSCGQC.js.map +0 -1
  47. package/dist/chunk-Q5DUDL77.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { P as PB } from '../pb-WAQvwCfM.cjs';
3
- import '../analytics-facade-CQXCQSoF.cjs';
2
+ import { P as PB } from '../pb-9MMDNQ9p.cjs';
3
+ import '../analytics-facade-f8IVujJW.cjs';
4
4
  import '../storage-BPaeSG8K.cjs';
5
5
  import './pooled-flags-4GtDtsiu.js';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { P as PB } from '../pb-BRlmBIAm.js';
3
- import '../analytics-facade-BER0EyYT.js';
2
+ import { P as PB } from '../pb-DRmdWQzG.js';
3
+ import '../analytics-facade-But6gu2r.js';
4
4
  import '../storage-BPaeSG8K.js';
5
5
  import './pooled-flags-4GtDtsiu.js';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  importNextServer
3
- } from "../chunk-FCIMEU2E.js";
3
+ } from "../chunk-VHUCJBBQ.js";
4
4
  import {
5
5
  SESSION_COOKIE_ATTRS,
6
6
  clearedSessionCookieNames,
@@ -14,14 +14,14 @@ import {
14
14
  buildRuntime,
15
15
  createBoundClient,
16
16
  getRuntime
17
- } from "../chunk-H6YSCGQC.js";
17
+ } from "../chunk-WXBOPPHU.js";
18
18
  import {
19
19
  BackendError
20
- } from "../chunk-EAOROUSW.js";
20
+ } from "../chunk-4WMJTUDN.js";
21
21
  import {
22
22
  environmentRefFromApiKey,
23
23
  environmentRefFromPublishableApiKey
24
- } from "../chunk-Q5DUDL77.js";
24
+ } from "../chunk-G56VGUYZ.js";
25
25
  import "../chunk-PZ5AY32C.js";
26
26
 
27
27
  // src/next/global-config.ts
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/global-config.ts","../../src/next/callback.ts","../../src/next/server.ts"],"sourcesContent":["/**\n * Global-config lookup for the palbe/next surfaces that run inside the\n * SERVER module graph (pbServer, handleAuthCallback) — the graph the root\n * layout's `import './palbe.gen'` configures.\n *\n * This module reaches `../internal.js` (and therefore the whole runtime\n * graph: livekit-client + the MLS WASM loader) on purpose — those surfaces\n * build a real runtime anyway. It is kept OUT of ./shared.ts so the\n * middleware entry, which must stay Edge-lean, never pulls it in.\n */\nimport type { PalbeConfig } from '../config.js';\nimport { BackendError } from '../errors.js';\nimport { getRuntime } from '../internal.js';\n\n/**\n * Global config with a surface-specific guided error when absent. Each\n * surface names the import location that configures ITS module graph.\n */\nexport function requireGlobalConfig(hint: string): PalbeConfig {\n try {\n return getRuntime().config;\n } catch {\n throw new BackendError('notConfigured', {\n code: 'not_configured',\n message: `Palbe is not configured in this module graph. Run 'palbase web link' and ${hint}`,\n });\n }\n}\n","/**\n * handleAuthCallback — the OAuth completion route handler factory.\n *\n * The app's redirect_uri (registered when calling signInWithOAuth with\n * `redirectTo: '<app>/auth/callback?provider=google&next=/dash'`) lands\n * here with `?code&state` from the provider. The handler exchanges them\n * SERVER-SIDE against palauth (PKCE verifier + state live server-side; the\n * browser never holds them), writes the session cookies onto a redirect\n * response, and sends the user on.\n *\n * ```ts\n * // app/auth/callback/route.ts\n * import { handleAuthCallback } from '@palbase/web/next';\n * export const GET = handleAuthCallback({ defaultNext: '/' });\n * ```\n *\n * MODULE-GRAPH RULE (same as ./index.ts): 'next/server' only lazily; the\n * type-only NextRequest import erases at build time.\n */\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport type { NextRequest } from 'next/server';\nimport { asWireAuthResult } from '../auth-wire.js';\nimport { encodeSessionCookiesDecoded, SESSION_COOKIE_ATTRS } from './cookie-codec.js';\nimport { requireGlobalConfig } from './global-config.js';\nimport { importNextServer } from './shared.js';\n\nexport interface HandleAuthCallbackOptions {\n /** Where to land when the callback carries no (valid) `next` param — and on every error/MFA redirect. Default '/'. */\n defaultNext?: string;\n}\n\n/**\n * Open-redirect guard: `next` rides the OAuth redirect querystring, so it is\n * attacker-influencable — only same-origin, path-absolute relative paths are\n * honored, and the post-auth victim is never sent off-origin.\n *\n * A leading-'/' heuristic is NOT enough. The value is later resolved with\n * `new URL(next, base)`, and the WHATWG URL parser STRIPS ASCII tab (0x09),\n * LF (0x0A) and CR (0x0D) before parsing — so `/<TAB>/evil.com` slips past a\n * \"next[1] is not '/'\" check yet parses to `//evil.com` → `https://evil.com`\n * (CWE-601 open redirect). We defend with TWO independent gates that must\n * BOTH pass, matching OWASP unvalidated-redirect guidance:\n *\n * 1. RAW-STRING gate (reject before normalization can hide the payload):\n * - reject any C0 control char (\\x00-\\x1F, incl. \\t \\n \\r) or other\n * whitespace — these are exactly what URL parsing strips/normalizes\n * away;\n * - require a path-absolute prefix: a single '/' NOT followed by '/' or\n * '\\\\' (blocks '//host' protocol-relative and '/\\\\host' backslash\n * tricks);\n * - reject a scheme: any ':' appearing before the first '/' ('javascript:',\n * 'https:'). Path-absolute inputs start with '/', so this also rejects\n * bare 'foo:bar'.\n * 2. CANONICAL gate: resolve `new URL(next, origin)` and require the result\n * to land on the SAME origin. This is the authoritative check — it sees\n * exactly what the browser will, after normalization.\n *\n * Anything ambiguous returns null so the caller falls back to a safe default.\n */\nfunction safeNextPath(next: string | null, origin: string): string | null {\n if (!next) return null;\n // 1a. No control chars / whitespace — URL parsing would strip these and\n // change what the path means (the \\t \\n \\r open-redirect class).\n // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting C0 is the point of the guard.\n if (/[\\u0000-\\u0020\\u007f\\s]/.test(next)) return null;\n // 1b. Must be path-absolute: a single leading '/', not '//' or '/\\\\'.\n if (next[0] !== '/' || next[1] === '/' || next[1] === '\\\\') return null;\n // 1c. No scheme before the first path separator (defense in depth; a\n // path-absolute string can't carry one, but reject explicitly).\n const firstSlash = next.indexOf('/');\n const firstColon = next.indexOf(':');\n if (firstColon !== -1 && firstColon < firstSlash) return null;\n // 2. Canonical same-origin check — resolve as the browser/redirect helper\n // will and confirm we never leave this origin.\n try {\n if (new URL(next, origin).origin !== origin) return null;\n } catch {\n return null;\n }\n return next;\n}\n\nfunction wireErrorCode(raw: unknown): string {\n if (typeof raw === 'object' && raw !== null) {\n const code = (raw as Record<string, unknown>).error;\n if (typeof code === 'string' && code !== '') return code;\n }\n return 'oauth_exchange_failed';\n}\n\n/**\n * Build the callback route handler. Outcomes (all 307 redirects, all with\n * `Cache-Control: private, no-store` — auth outcomes must never cache):\n * - provider `error` param → `?auth_error=<error>` (+ `auth_error_description`)\n * - missing code/state/provider → `?auth_error=invalid_callback`\n * - exchanged AuthResult → session cookies on the redirect to `next ?? defaultNext ?? '/'`\n * - MFA union → `?mfa_token=…&mfa_factors=a,b` on defaultNext, NO cookies\n * - exchange failure → `?auth_error=<wire code | oauth_exchange_failed>`\n */\nexport function handleAuthCallback(\n opts?: HandleAuthCallbackOptions,\n): (request: NextRequest) => Promise<Response> {\n return async (request) => {\n const config = requireGlobalConfig(\n 'import the generated palbe.gen.ts once in app/layout.tsx — route handlers share the server module graph with the root layout.',\n );\n const { NextResponse } = await importNextServer('handleAuthCallback');\n const fallback = opts?.defaultNext ?? '/';\n\n const redirect = (path: string, query?: Record<string, string>) => {\n const url = new URL(path, request.nextUrl);\n for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);\n const response = NextResponse.redirect(url); // 307 — the callback GET stays a GET\n response.headers.set('cache-control', 'private, no-store');\n return response;\n };\n\n const params = request.nextUrl.searchParams;\n const providerError = params.get('error');\n if (providerError) {\n const query: Record<string, string> = { auth_error: providerError };\n const description = params.get('error_description');\n if (description) query.auth_error_description = description;\n return redirect(fallback, query);\n }\n\n const code = params.get('code');\n const state = params.get('state');\n const provider = params.get('provider');\n if (!code || !state || !provider) {\n return redirect(fallback, { auth_error: 'invalid_callback' });\n }\n\n let exchange: Response;\n try {\n exchange = await fetch(\n `${config.url}/auth/oauth/${encodeURIComponent(provider)}/callback` +\n `?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,\n { headers: { apikey: config.apiKey } },\n );\n } catch {\n return redirect(fallback, { auth_error: 'oauth_exchange_failed' });\n }\n let raw: unknown = null;\n try {\n raw = await exchange.json();\n } catch {\n // malformed body — the shape checks below turn this into auth_error\n }\n if (!exchange.ok) return redirect(fallback, { auth_error: wireErrorCode(raw) });\n\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n // MFA union: relay the challenge to the app — NO cookies until it completes.\n if (obj.mfa_required === true && typeof obj.mfa_token === 'string') {\n const factors = Array.isArray(obj.mfa_factors)\n ? obj.mfa_factors.filter((f): f is string => typeof f === 'string')\n : [];\n return redirect(fallback, { mfa_token: obj.mfa_token, mfa_factors: factors.join(',') });\n }\n }\n\n // Zombie-session guard (shared with the auth facade): cookies are only\n // ever written for a FULLY-proven AuthResult.\n const wire = asWireAuthResult(raw);\n if (!wire) return redirect(fallback, { auth_error: 'oauth_exchange_failed' });\n\n const response = redirect(safeNextPath(params.get('next'), request.nextUrl.origin) ?? fallback);\n // DECODED codec values: NextResponse.cookies percent-encodes on write,\n // producing exactly the shared-codec wire bytes (encodeSessionCookiesDecoded).\n const write = encodeSessionCookiesDecoded(environmentRefFromPublishableApiKey(config.apiKey), {\n accessToken: wire.access_token,\n refreshToken: wire.refresh_token,\n expiresAt: Date.now() + wire.expires_in * 1000,\n });\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Codec overflow guard: expire one-past-the-end of the written run.\n for (const name of write.clear) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n return response;\n };\n}\n","/**\n * pbServer — the per-request server client of the Next.js adapter.\n *\n * Every call builds a FRESH runtime over the request's cookie store and\n * returns a runtime-BOUND `PB` client: typed namespaces, `call`, `upload`\n * and `auth` all resolve that runtime, never the process-global slot — two\n * bound clients serving concurrent requests cannot leak each other's\n * sessions (see createBoundClient). The store itself is read lazily per\n * adapter operation, so nothing is snapshotted beyond the call.\n *\n * MODULE-GRAPH RULE (same as ./index.ts): 'next/headers' is imported ONLY\n * lazily inside the default-store path — importing this module, and using\n * pbServer with an injected store, must work without next installed.\n */\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport { buildRuntime, createBoundClient } from '../internal.js';\nimport type { PB } from '../pb.js';\nimport type { PersistedSession, SessionStorageAdapter } from '../storage.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n} from './cookie-codec.js';\nimport { requireGlobalConfig } from './global-config.js';\n\n/**\n * Structural slice of Next's request cookie store (`await cookies()`); any\n * object honoring it works — the seam for tests and non-Next servers.\n * `set`/`delete` are optional because RSC-scope stores are read-only.\n */\nexport interface CookieStoreLike {\n get(name: string): { name: string; value: string } | undefined;\n getAll?(): Array<{ name: string; value: string }>;\n set?(name: string, value: string, options?: Record<string, unknown>): void;\n delete?(name: string): void;\n}\n\nfunction nextRequired(cause: unknown): BackendError {\n const detail = cause instanceof Error && cause.message ? ` (${cause.message})` : '';\n return new BackendError('validation', {\n code: 'next_required',\n message:\n 'pbServer() could not resolve a Next.js request cookie store: call it inside a Server Component, Server Action or Route Handler — or pass a store explicitly via pbServer({ cookies })' +\n `${detail}.`,\n });\n}\n\n/**\n * Next CONTROL-FLOW errors must pass through UNWRAPPED. During static\n * prerender (`next build`) `cookies()` throws DynamicServerError (digest\n * 'DYNAMIC_SERVER_USAGE'), which Next catches upstream to mark the route\n * dynamic — wrapping it into a BackendError would turn that bailout signal\n * into a real prerender error and FAIL THE BUILD of every page calling\n * pbServer (caught live in the P3 Next smoke). 'NEXT_'-prefixed digests are\n * the same sentinel family (redirect / not-found / static-gen bailout).\n * Genuine outside-request-scope errors carry no digest and still map to\n * next_required.\n */\nexport function isNextControlFlowError(err: unknown): boolean {\n if (typeof err !== 'object' || err === null) return false;\n const digest = (err as { digest?: unknown }).digest;\n return (\n typeof digest === 'string' && (digest === 'DYNAMIC_SERVER_USAGE' || digest.startsWith('NEXT_'))\n );\n}\n\n/** Lazy default store: `await (await import('next/headers')).cookies()`. */\nasync function nextCookieStore(): Promise<CookieStoreLike> {\n let cookies: () => Promise<CookieStoreLike>;\n try {\n ({ cookies } = await import('next/headers'));\n } catch (cause) {\n throw nextRequired(cause); // next not installed / not resolvable\n }\n try {\n return await cookies();\n } catch (cause) {\n if (isNextControlFlowError(cause)) throw cause; // static-prerender bailout etc.\n throw nextRequired(cause); // installed, but outside a request scope\n }\n}\n\n/** Delete one cookie: set-with-maxAge-0 when the store can set (parity with the browser side), else delete, else swallow (read-only RSC store). */\nfunction removeCookie(store: CookieStoreLike, name: string): void {\n try {\n if (store.set) store.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n else store.delete?.(name);\n } catch {\n // RSC read-only store — cookie mutation is the middleware's job there\n }\n}\n\n/**\n * SessionStorageAdapter over the request cookie store.\n *\n * SAVE SEMANTICS (decision, documented): only FULL sessions (access token +\n * expiry present) are written; a refresh-only save is SKIPPED with no throw\n * rather than written as {a:'',e:0}. The cookie is SHARED with the browser —\n * degrading it would drop an access token the browser may still be using and\n * force a refresh per page-load. The refresh-only branch is a defensive\n * legacy path anyway: every successful refresh saves a full session.\n *\n * All writes swallow per-cookie failures: in RSC scope Next's store throws\n * on mutation; the request still proceeds with the in-memory session but the\n * rotated cookie is NOT persisted. palbeMiddleware is therefore REQUIRED —\n * not optional — for session-bearing RSC apps: without it every RSC render\n * re-refreshes from the SAME cookie refresh token, and once two such\n * refreshes land more than 30s apart (outside palauth's rotation grace) the\n * reuse detector REVOKES THE WHOLE TOKEN FAMILY — the user is\n * force-logged-out everywhere.\n *\n * Values are written DECODED (encodeSessionCookiesDecoded): Next's real\n * cookies() store percent-encodes on serialization, yielding exactly the\n * shared-codec wire bytes; a raw store (tests, custom servers) persists the\n * bare JSON, which the load path's decode tolerates identically (tokens are\n * %-free base64url, so decodeURIComponent is the identity on them).\n */\nfunction serverCookieAdapter(\n store: CookieStoreLike,\n environmentRef: string,\n): SessionStorageAdapter {\n const present = (name: string): boolean => store.get(name) !== undefined;\n return {\n load(): PersistedSession | null {\n const stored = decodeSessionCookies((name) => store.get(name)?.value, environmentRef);\n if (!stored) return null;\n // A browser-side refresh-only write round-trips as a:'' / e:0 —\n // normalize to the legacy shape so hydration takes the expired-trick.\n return stored.accessToken && stored.expiresAt > 0\n ? {\n refreshToken: stored.refreshToken,\n accessToken: stored.accessToken,\n expiresAt: stored.expiresAt,\n }\n : { refreshToken: stored.refreshToken };\n },\n save(session: PersistedSession): void {\n if (!session.accessToken || !session.expiresAt) return; // see SAVE SEMANTICS above\n for (const name of clearedSessionCookieNames(environmentRef, present)) {\n removeCookie(store, name);\n }\n const { set, clear } = encodeSessionCookiesDecoded(environmentRef, {\n accessToken: session.accessToken,\n refreshToken: session.refreshToken,\n expiresAt: session.expiresAt,\n });\n for (const { name, value } of set) {\n try {\n store.set?.(name, value, { ...SESSION_COOKIE_ATTRS });\n } catch {\n // RSC read-only store\n }\n }\n // Overflow guard (codec contract): delete one-past-the-end so a stale\n // orphan chunk behind a gap can never join a future chunk run.\n for (const name of clear) removeCookie(store, name);\n },\n clear(): void {\n for (const name of clearedSessionCookieNames(environmentRef, present)) {\n removeCookie(store, name);\n }\n },\n };\n}\n\n/**\n * Build a per-request `PB` client over the request's cookies. Reads the\n * session the browser SDK wrote (shared cookie codec), refreshes pre-flight\n * when expired, and — where the store is writable (Server Actions, Route\n * Handlers) — persists the rotated session back.\n *\n * Each call builds a fresh runtime (a handful of small objects, no I/O) —\n * cheap enough to call per data access; code paths invoking it many times\n * within one request MAY memoize per-request (e.g. React's `cache()`), but\n * don't have to.\n *\n * ```ts\n * const pb = await pbServer();\n * const todos = await pb.todos.list();\n * ```\n */\nexport async function pbServer(opts?: { cookies?: CookieStoreLike }): Promise<PB> {\n const config = requireGlobalConfig(\n 'import the generated palbe.gen.ts once in app/layout.tsx — the root-layout import configures Server Components and Route Handlers too.',\n );\n const store = opts?.cookies ?? (await nextCookieStore());\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n const rt = buildRuntime({ ...config, storage: serverCookieAdapter(store, ref) });\n return createBoundClient(rt);\n}\n"],"mappings":"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","names":["response"]}
1
+ {"version":3,"sources":["../../src/next/global-config.ts","../../src/next/callback.ts","../../src/next/server.ts"],"sourcesContent":["/**\n * Global-config lookup for the palbe/next surfaces that run inside the\n * SERVER module graph (pbServer, handleAuthCallback) — the graph the root\n * layout's `import './palbe.gen'` configures.\n *\n * This module reaches `../internal.js` (and therefore the whole runtime\n * graph: livekit-client + the MLS WASM loader) on purpose — those surfaces\n * build a real runtime anyway. It is kept OUT of ./shared.ts so the\n * middleware entry, which must stay Edge-lean, never pulls it in.\n */\nimport type { PalbeConfig } from '../config.js';\nimport { BackendError } from '../errors.js';\nimport { getRuntime } from '../internal.js';\n\n/**\n * Global config with a surface-specific guided error when absent. Each\n * surface names the import location that configures ITS module graph.\n */\nexport function requireGlobalConfig(hint: string): PalbeConfig {\n try {\n return getRuntime().config;\n } catch {\n throw new BackendError('notConfigured', {\n code: 'not_configured',\n message: `Palbe is not configured in this module graph. Run 'palbase web link' and ${hint}`,\n });\n }\n}\n","/**\n * handleAuthCallback — the OAuth completion route handler factory.\n *\n * The app's redirect_uri (registered when calling signInWithOAuth with\n * `redirectTo: '<app>/auth/callback?provider=google&next=/dash'`) lands\n * here with `?code&state` from the provider. The handler exchanges them\n * SERVER-SIDE against palauth (PKCE verifier + state live server-side; the\n * browser never holds them), writes the session cookies onto a redirect\n * response, and sends the user on.\n *\n * ```ts\n * // app/auth/callback/route.ts\n * import { handleAuthCallback } from '@palbase/web/next';\n * export const GET = handleAuthCallback({ defaultNext: '/' });\n * ```\n *\n * MODULE-GRAPH RULE (same as ./index.ts): 'next/server' only lazily; the\n * type-only NextRequest import erases at build time.\n */\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport type { NextRequest } from 'next/server';\nimport { asWireAuthResult } from '../auth-wire.js';\nimport { encodeSessionCookiesDecoded, SESSION_COOKIE_ATTRS } from './cookie-codec.js';\nimport { requireGlobalConfig } from './global-config.js';\nimport { importNextServer } from './shared.js';\n\nexport interface HandleAuthCallbackOptions {\n /** Where to land when the callback carries no (valid) `next` param — and on every error/MFA redirect. Default '/'. */\n defaultNext?: string;\n}\n\n/**\n * Open-redirect guard: `next` rides the OAuth redirect querystring, so it is\n * attacker-influencable — only same-origin, path-absolute relative paths are\n * honored, and the post-auth victim is never sent off-origin.\n *\n * A leading-'/' heuristic is NOT enough. The value is later resolved with\n * `new URL(next, base)`, and the WHATWG URL parser STRIPS ASCII tab (0x09),\n * LF (0x0A) and CR (0x0D) before parsing — so `/<TAB>/evil.com` slips past a\n * \"next[1] is not '/'\" check yet parses to `//evil.com` → `https://evil.com`\n * (CWE-601 open redirect). We defend with TWO independent gates that must\n * BOTH pass, matching OWASP unvalidated-redirect guidance:\n *\n * 1. RAW-STRING gate (reject before normalization can hide the payload):\n * - reject any C0 control char (\\x00-\\x1F, incl. \\t \\n \\r) or other\n * whitespace — these are exactly what URL parsing strips/normalizes\n * away;\n * - require a path-absolute prefix: a single '/' NOT followed by '/' or\n * '\\\\' (blocks '//host' protocol-relative and '/\\\\host' backslash\n * tricks);\n * - reject a scheme: any ':' appearing before the first '/' ('javascript:',\n * 'https:'). Path-absolute inputs start with '/', so this also rejects\n * bare 'foo:bar'.\n * 2. CANONICAL gate: resolve `new URL(next, origin)` and require the result\n * to land on the SAME origin. This is the authoritative check — it sees\n * exactly what the browser will, after normalization.\n *\n * Anything ambiguous returns null so the caller falls back to a safe default.\n */\nfunction safeNextPath(next: string | null, origin: string): string | null {\n if (!next) return null;\n // 1a. No control chars / whitespace — URL parsing would strip these and\n // change what the path means (the \\t \\n \\r open-redirect class).\n // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting C0 is the point of the guard.\n if (/[\\u0000-\\u0020\\u007f\\s]/.test(next)) return null;\n // 1b. Must be path-absolute: a single leading '/', not '//' or '/\\\\'.\n if (next[0] !== '/' || next[1] === '/' || next[1] === '\\\\') return null;\n // 1c. No scheme before the first path separator (defense in depth; a\n // path-absolute string can't carry one, but reject explicitly).\n const firstSlash = next.indexOf('/');\n const firstColon = next.indexOf(':');\n if (firstColon !== -1 && firstColon < firstSlash) return null;\n // 2. Canonical same-origin check — resolve as the browser/redirect helper\n // will and confirm we never leave this origin.\n try {\n if (new URL(next, origin).origin !== origin) return null;\n } catch {\n return null;\n }\n return next;\n}\n\nfunction wireErrorCode(raw: unknown): string {\n if (typeof raw === 'object' && raw !== null) {\n const code = (raw as Record<string, unknown>).error;\n if (typeof code === 'string' && code !== '') return code;\n }\n return 'oauth_exchange_failed';\n}\n\n/**\n * Build the callback route handler. Outcomes (all 307 redirects, all with\n * `Cache-Control: private, no-store` — auth outcomes must never cache):\n * - provider `error` param → `?auth_error=<error>` (+ `auth_error_description`)\n * - missing code/state/provider → `?auth_error=invalid_callback`\n * - exchanged AuthResult → session cookies on the redirect to `next ?? defaultNext ?? '/'`\n * - MFA union → `?mfa_token=…&mfa_factors=a,b` on defaultNext, NO cookies\n * - exchange failure → `?auth_error=<wire code | oauth_exchange_failed>`\n */\nexport function handleAuthCallback(\n opts?: HandleAuthCallbackOptions,\n): (request: NextRequest) => Promise<Response> {\n return async (request) => {\n const config = requireGlobalConfig(\n 'import the generated palbe.gen.ts once in app/layout.tsx — route handlers share the server module graph with the root layout.',\n );\n const { NextResponse } = await importNextServer('handleAuthCallback');\n const fallback = opts?.defaultNext ?? '/';\n\n const redirect = (path: string, query?: Record<string, string>) => {\n const url = new URL(path, request.nextUrl);\n for (const [key, value] of Object.entries(query ?? {})) url.searchParams.set(key, value);\n const response = NextResponse.redirect(url); // 307 — the callback GET stays a GET\n response.headers.set('cache-control', 'private, no-store');\n return response;\n };\n\n const params = request.nextUrl.searchParams;\n const providerError = params.get('error');\n if (providerError) {\n const query: Record<string, string> = { auth_error: providerError };\n const description = params.get('error_description');\n if (description) query.auth_error_description = description;\n return redirect(fallback, query);\n }\n\n const code = params.get('code');\n const state = params.get('state');\n const provider = params.get('provider');\n if (!code || !state || !provider) {\n return redirect(fallback, { auth_error: 'invalid_callback' });\n }\n\n let exchange: Response;\n try {\n exchange = await fetch(\n `${config.url}/auth/oauth/${encodeURIComponent(provider)}/callback` +\n `?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,\n { headers: { apikey: config.apiKey } },\n );\n } catch {\n return redirect(fallback, { auth_error: 'oauth_exchange_failed' });\n }\n let raw: unknown = null;\n try {\n raw = await exchange.json();\n } catch {\n // malformed body — the shape checks below turn this into auth_error\n }\n if (!exchange.ok) return redirect(fallback, { auth_error: wireErrorCode(raw) });\n\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n // MFA union: relay the challenge to the app — NO cookies until it completes.\n if (obj.mfa_required === true && typeof obj.mfa_token === 'string') {\n const factors = Array.isArray(obj.mfa_factors)\n ? obj.mfa_factors.filter((f): f is string => typeof f === 'string')\n : [];\n return redirect(fallback, { mfa_token: obj.mfa_token, mfa_factors: factors.join(',') });\n }\n }\n\n // Zombie-session guard (shared with the auth facade): cookies are only\n // ever written for a FULLY-proven AuthResult.\n const wire = asWireAuthResult(raw);\n if (!wire) return redirect(fallback, { auth_error: 'oauth_exchange_failed' });\n\n const response = redirect(safeNextPath(params.get('next'), request.nextUrl.origin) ?? fallback);\n // DECODED codec values: NextResponse.cookies percent-encodes on write,\n // producing exactly the shared-codec wire bytes (encodeSessionCookiesDecoded).\n const write = encodeSessionCookiesDecoded(environmentRefFromPublishableApiKey(config.apiKey), {\n accessToken: wire.access_token,\n refreshToken: wire.refresh_token,\n expiresAt: Date.now() + wire.expires_in * 1000,\n });\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Codec overflow guard: expire one-past-the-end of the written run.\n for (const name of write.clear) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n return response;\n };\n}\n","/**\n * pbServer — the per-request server client of the Next.js adapter.\n *\n * Every call builds a FRESH runtime over the request's cookie store and\n * returns a runtime-BOUND `PB` client: typed namespaces, `call`, `upload`\n * and `auth` all resolve that runtime, never the process-global slot — two\n * bound clients serving concurrent requests cannot leak each other's\n * sessions (see createBoundClient). The store itself is read lazily per\n * adapter operation, so nothing is snapshotted beyond the call.\n *\n * MODULE-GRAPH RULE (same as ./index.ts): 'next/headers' is imported ONLY\n * lazily inside the default-store path — importing this module, and using\n * pbServer with an injected store, must work without next installed.\n */\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport { buildRuntime, createBoundClient } from '../internal.js';\nimport type { PB } from '../pb.js';\nimport type { PersistedSession, SessionStorageAdapter } from '../storage.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n} from './cookie-codec.js';\nimport { requireGlobalConfig } from './global-config.js';\n\n/**\n * Structural slice of Next's request cookie store (`await cookies()`); any\n * object honoring it works — the seam for tests and non-Next servers.\n * `set`/`delete` are optional because RSC-scope stores are read-only.\n */\nexport interface CookieStoreLike {\n get(name: string): { name: string; value: string } | undefined;\n getAll?(): Array<{ name: string; value: string }>;\n set?(name: string, value: string, options?: Record<string, unknown>): void;\n delete?(name: string): void;\n}\n\nfunction nextRequired(cause: unknown): BackendError {\n const detail = cause instanceof Error && cause.message ? ` (${cause.message})` : '';\n return new BackendError('validation', {\n code: 'next_required',\n message:\n 'pbServer() could not resolve a Next.js request cookie store: call it inside a Server Component, Server Action or Route Handler — or pass a store explicitly via pbServer({ cookies })' +\n `${detail}.`,\n });\n}\n\n/**\n * Next CONTROL-FLOW errors must pass through UNWRAPPED. During static\n * prerender (`next build`) `cookies()` throws DynamicServerError (digest\n * 'DYNAMIC_SERVER_USAGE'), which Next catches upstream to mark the route\n * dynamic — wrapping it into a BackendError would turn that bailout signal\n * into a real prerender error and FAIL THE BUILD of every page calling\n * pbServer (caught live in the P3 Next smoke). 'NEXT_'-prefixed digests are\n * the same sentinel family (redirect / not-found / static-gen bailout).\n * Genuine outside-request-scope errors carry no digest and still map to\n * next_required.\n */\nexport function isNextControlFlowError(err: unknown): boolean {\n if (typeof err !== 'object' || err === null) return false;\n const digest = (err as { digest?: unknown }).digest;\n return (\n typeof digest === 'string' && (digest === 'DYNAMIC_SERVER_USAGE' || digest.startsWith('NEXT_'))\n );\n}\n\n/** Lazy default store: `await (await import('next/headers')).cookies()`. */\nasync function nextCookieStore(): Promise<CookieStoreLike> {\n let cookies: () => Promise<CookieStoreLike>;\n try {\n ({ cookies } = await import('next/headers'));\n } catch (cause) {\n throw nextRequired(cause); // next not installed / not resolvable\n }\n try {\n return await cookies();\n } catch (cause) {\n if (isNextControlFlowError(cause)) throw cause; // static-prerender bailout etc.\n throw nextRequired(cause); // installed, but outside a request scope\n }\n}\n\n/** Delete one cookie: set-with-maxAge-0 when the store can set (parity with the browser side), else delete, else swallow (read-only RSC store). */\nfunction removeCookie(store: CookieStoreLike, name: string): void {\n try {\n if (store.set) store.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n else store.delete?.(name);\n } catch {\n // RSC read-only store — cookie mutation is the middleware's job there\n }\n}\n\n/**\n * SessionStorageAdapter over the request cookie store.\n *\n * SAVE SEMANTICS (decision, documented): only FULL sessions (access token +\n * expiry present) are written; a refresh-only save is SKIPPED with no throw\n * rather than written as {a:'',e:0}. The cookie is SHARED with the browser —\n * degrading it would drop an access token the browser may still be using and\n * force a refresh per page-load. The refresh-only branch is a defensive\n * legacy path anyway: every successful refresh saves a full session.\n *\n * All writes swallow per-cookie failures: in RSC scope Next's store throws\n * on mutation; the request still proceeds with the in-memory session but the\n * rotated cookie is NOT persisted. palbeProxy is therefore REQUIRED —\n * not optional — for session-bearing RSC apps: without it every RSC render\n * re-refreshes from the SAME cookie refresh token, and once two such\n * refreshes land more than 30s apart (outside palauth's rotation grace) the\n * reuse detector REVOKES THE WHOLE TOKEN FAMILY — the user is\n * force-logged-out everywhere.\n *\n * Values are written DECODED (encodeSessionCookiesDecoded): Next's real\n * cookies() store percent-encodes on serialization, yielding exactly the\n * shared-codec wire bytes; a raw store (tests, custom servers) persists the\n * bare JSON, which the load path's decode tolerates identically (tokens are\n * %-free base64url, so decodeURIComponent is the identity on them).\n */\nfunction serverCookieAdapter(\n store: CookieStoreLike,\n environmentRef: string,\n): SessionStorageAdapter {\n const present = (name: string): boolean => store.get(name) !== undefined;\n return {\n load(): PersistedSession | null {\n const stored = decodeSessionCookies((name) => store.get(name)?.value, environmentRef);\n if (!stored) return null;\n // A browser-side refresh-only write round-trips as a:'' / e:0 —\n // normalize to the legacy shape so hydration takes the expired-trick.\n return stored.accessToken && stored.expiresAt > 0\n ? {\n refreshToken: stored.refreshToken,\n accessToken: stored.accessToken,\n expiresAt: stored.expiresAt,\n }\n : { refreshToken: stored.refreshToken };\n },\n save(session: PersistedSession): void {\n if (!session.accessToken || !session.expiresAt) return; // see SAVE SEMANTICS above\n for (const name of clearedSessionCookieNames(environmentRef, present)) {\n removeCookie(store, name);\n }\n const { set, clear } = encodeSessionCookiesDecoded(environmentRef, {\n accessToken: session.accessToken,\n refreshToken: session.refreshToken,\n expiresAt: session.expiresAt,\n });\n for (const { name, value } of set) {\n try {\n store.set?.(name, value, { ...SESSION_COOKIE_ATTRS });\n } catch {\n // RSC read-only store\n }\n }\n // Overflow guard (codec contract): delete one-past-the-end so a stale\n // orphan chunk behind a gap can never join a future chunk run.\n for (const name of clear) removeCookie(store, name);\n },\n clear(): void {\n for (const name of clearedSessionCookieNames(environmentRef, present)) {\n removeCookie(store, name);\n }\n },\n };\n}\n\n/**\n * Build a per-request `PB` client over the request's cookies. Reads the\n * session the browser SDK wrote (shared cookie codec), refreshes pre-flight\n * when expired, and — where the store is writable (Server Actions, Route\n * Handlers) — persists the rotated session back.\n *\n * Each call builds a fresh runtime (a handful of small objects, no I/O) —\n * cheap enough to call per data access; code paths invoking it many times\n * within one request MAY memoize per-request (e.g. React's `cache()`), but\n * don't have to.\n *\n * ```ts\n * const pb = await pbServer();\n * const todos = await pb.todos.list();\n * ```\n */\nexport async function pbServer(opts?: { cookies?: CookieStoreLike }): Promise<PB> {\n const config = requireGlobalConfig(\n 'import the generated palbe.gen.ts once in app/layout.tsx — the root-layout import configures Server Components and Route Handlers too.',\n );\n const store = opts?.cookies ?? (await nextCookieStore());\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n const rt = buildRuntime({ ...config, storage: serverCookieAdapter(store, ref) });\n return createBoundClient(rt);\n}\n"],"mappings":"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","names":["response"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/next/proxy.ts","../../src/api-key.ts","../../src/errors.ts","../../src/next/cookie-codec.ts","../../src/next/shared.ts"],"sourcesContent":["/**\n * '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js\n * proxy (the file convention that replaced `middleware` in Next 16; this\n * package's peer range is `next >= 16`).\n *\n * REQUIRED — not optional — for session-bearing RSC apps. Server Components\n * cannot write cookies, so pbServer's reactive refresh cannot PERSIST a\n * rotated session there: every RSC render would re-refresh from the SAME\n * cookie refresh token, and once two of those land more than 30s apart\n * (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE\n * TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes\n * the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid\n * session and never burns the token itself.\n *\n * BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config\n * instead of reading the process-global one like pbServer does:\n *\n * Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts\n * imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from\n * proxy.ts to fix that would drag `__configure` → buildRuntime →\n * messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB\n * where this entry ships 168 KB. (Under the pre-16 `middleware` convention it\n * was worse than size — that bundle was Edge, and webpack failed the build\n * outright with \"Dynamic Code Evaluation ... not allowed in Edge Runtime\".\n * `proxy` runs on nodejs, which is not configurable, so the hard error is\n * gone and the weight is not.)\n *\n * So the proxy never touches the runtime: it takes the two published identity\n * values it actually needs and speaks the one auth wire directly. Nothing on\n * this module's import graph may reach ../runtime.js —\n * __tests__/next-proxy-bundle.test.ts locks that against the real build\n * output.\n *\n * 'next/server' is imported only lazily (the type-only NextRequest/\n * NextResponse imports erase at build time).\n *\n * ```ts\n * // proxy.ts\n * import { palbeProxy } from '@palbase/web/next/proxy';\n * import type { NextRequest } from 'next/server';\n *\n * export function proxy(request: NextRequest) {\n * return palbeProxy(request, {\n * url: 'https://abc12345m.palbase.studio',\n * apiKey: 'pb_abc12345m_c...',\n * });\n * }\n *\n * // `config` MUST be declared here, in this file: Next reads it off THIS\n * // file's AST (export const + literal initializer). A re-exported or\n * // imported config is invisible and silently degrades to a catch-all.\n * export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };\n * ```\n */\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n type StoredSession,\n} from './cookie-codec.js';\nimport { importNextServer } from './shared.js';\n\nconst DEFAULT_REFRESH_MARGIN_MS = 60_000;\n\n/**\n * The published identity palbeProxy needs — the same two values\n * `palbase web link` bakes into palbe.gen.ts. Both are publishable (the\n * anon key already ships in every browser bundle); a service_role key is\n * REJECTED, it has no business in a request-path bundle.\n *\n * A `PalbeConfig` is structurally assignable, so apps that already hold one\n * can pass it straight through.\n */\nexport interface PalbeProxyConfig {\n /** Environment URL, e.g. `https://abc12345m.palbase.studio`. */\n url: string;\n /** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */\n apiKey: string;\n}\n\nexport interface PalbeProxyOptions {\n /** Refresh when the access token expires within this window. Default 60s. */\n refreshMarginMs?: number;\n /**\n * Pre-built response to decorate (e.g. from your own chained proxy logic) instead of\n * building `NextResponse.next({ request })`. Pass-through cases return it\n * UNMODIFIED. Caveat: a pre-built response snapshotted the request headers\n * at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)\n * but not the downstream RSC render of THIS request — prefer omitting this\n * and building your response after palbeProxy when possible.\n */\n response?: NextResponse;\n}\n\ntype RefreshOutcome =\n | { kind: 'rotated'; session: StoredSession }\n | { kind: 'terminal' } // 400/401/403 — refresh token dead, sign out\n | { kind: 'transient' }; // network / 5xx / malformed — keep the session\n\n/**\n * Per-process single-flight: concurrent proxy invocations holding the\n * SAME refresh token (parallel tabs / prefetches) share one refresh call —\n * a per-token thundering herd would race palauth rotation. Multi-pod\n * concurrency is covered by palauth's 30s rotation grace (concurrent\n * refreshes of one token all succeed); per-process dedup is all that's\n * needed here (Studio precedent — don't over-engineer).\n */\nconst inflightRefreshes = new Map<string, Promise<RefreshOutcome>>();\n\nfunction refreshSingleFlight(\n config: PalbeProxyConfig,\n refreshToken: string,\n): Promise<RefreshOutcome> {\n const existing = inflightRefreshes.get(refreshToken);\n if (existing) return existing;\n const pending = refresh(config, refreshToken).finally(() => {\n inflightRefreshes.delete(refreshToken);\n });\n inflightRefreshes.set(refreshToken, pending);\n return pending;\n}\n\n/** Direct palauth refresh call (no runtime: the proxy needs only this one wire). */\nasync function refresh(config: PalbeProxyConfig, refreshToken: string): Promise<RefreshOutcome> {\n try {\n const res = await fetch(`${config.url}/auth/token/refresh`, {\n method: 'POST',\n headers: { apikey: config.apiKey, 'content-type': 'application/json' },\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n if (res.status === 400 || res.status === 401 || res.status === 403) {\n return { kind: 'terminal' };\n }\n if (!res.ok) return { kind: 'transient' };\n const raw: unknown = await res.json();\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n if (\n typeof obj.access_token === 'string' &&\n typeof obj.refresh_token === 'string' &&\n typeof obj.expires_in === 'number'\n ) {\n return {\n kind: 'rotated',\n session: {\n accessToken: obj.access_token,\n refreshToken: obj.refresh_token,\n expiresAt: Date.now() + obj.expires_in * 1000,\n },\n };\n }\n }\n return { kind: 'transient' }; // malformed 200 — never destroy a session over it\n } catch {\n return { kind: 'transient' }; // network failure\n }\n}\n\n/**\n * Keep the session cookie fresh for the request. No session / still-fresh\n * session → untouched pass-through (no cache-control changes). Stale or\n * expired → single-flight refresh, then the rotated (or cleared, on a dead\n * token) cookies are written onto BOTH the request (for the downstream RSC\n * render via `NextResponse.next({ request })`) and the response (for the\n * browser), with `Cache-Control: private, no-store` on touched responses.\n *\n * Cookie writes go through Next's cookie APIs with the DECODED codec values:\n * Next percent-encodes on serialization, so the wire bytes come out exactly\n * as the shared codec's (see encodeSessionCookiesDecoded). NextRequest's\n * cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie\n * form), hence the direct codec calls here instead of the pbServer adapter.\n */\nexport async function palbeProxy(\n request: NextRequest,\n config: PalbeProxyConfig,\n opts?: PalbeProxyOptions,\n): Promise<NextResponse> {\n // Fail LOUDLY on a bad key. A silently empty ref would name the cookie\n // `palbe-session-` — a name the browser SDK never writes — so every\n // request would look session-less and the proxy would degrade to a\n // no-op, which is exactly the token-family revocation this exists to stop.\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n if (ref === '') {\n throw new BackendError('validation', {\n code: 'validation_error',\n message:\n 'palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c…). Re-run `palbase web link` and copy the generated values.',\n });\n }\n const { NextResponse } = await importNextServer('palbeProxy');\n const passThrough = (): NextResponse => opts?.response ?? NextResponse.next({ request });\n\n const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);\n if (!session) return passThrough();\n if (session.expiresAt - Date.now() > (opts?.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS)) {\n return passThrough();\n }\n\n const outcome = await refreshSingleFlight(config, session.refreshToken);\n if (outcome.kind === 'transient') return passThrough(); // keep the session over blips\n\n // Session-cookie names in the incoming jar — the browser's state mirror.\n const staleNames = clearedSessionCookieNames(ref, (name) => request.cookies.has(name));\n\n if (outcome.kind === 'terminal') {\n for (const name of staleNames) request.cookies.delete(name);\n const response = opts?.response ?? NextResponse.next({ request });\n for (const name of staleNames) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n }\n\n const write = encodeSessionCookiesDecoded(ref, outcome.session);\n for (const name of staleNames) request.cookies.delete(name);\n for (const { name, value } of write.set) request.cookies.set(name, value);\n for (const name of write.clear) request.cookies.delete(name);\n // Build AFTER the request-cookie mutation: NextResponse.next({ request })\n // snapshots the request headers for the downstream RSC render.\n const response = opts?.response ?? NextResponse.next({ request });\n const written = new Set(write.set.map((c) => c.name));\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Browser-side cleanup: stale names not part of the new run (a shrunken /\n // grown chunk layout) + the codec's one-past-the-end overflow guard.\n for (const name of new Set([...write.clear, ...staleNames])) {\n if (!written.has(name)) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n}\n","/**\n * `pb_<environmentRef>_<scope><random>` → environmentRef ('' if malformed).\n * Utility for callers that only have a raw key. App-bound runtime storage and\n * palbe/next cookie naming use the explicit `PalbeConfig.environmentRef` emitted\n * by `palbe-gen`.\n *\n * The key is shape-validated (prefix `pb`, a 4-24 character lowercase ASCII\n * alphanumeric environmentRef, and a `<scope><at-least-20-base62-random>` secret\n * segment where scope ∈ {c,s}) before a ref is returned.\n *\n * THE SECRET'S LENGTH IS NOT PART OF THE CONTRACT — only its floor is. Two\n * minters ship two lengths (the stack's own `initenv` writes 20 base62, the\n * cloud control plane writes 32), and the gate that actually admits a request\n * says so outright: *\"a shorter or longer secret is not a security property\n * this door can rule on, and refusing one would only turn off a stack whose key\n * works everywhere else\"* (v2/internal/platform/identitymw.go: parseAPIKey).\n * Pinning EXACTLY 20 here made this SDK refuse every key the cloud has ever\n * minted (measured 2026-08-25: `buildRuntime` threw and palbe-gen never ran).\n * The floor stays, because the short/loose body is what the finding below was\n * about; the ceiling was never anyone's rule. A structurally-invalid key returns\n * '' so session storage stays\n * UNSCOPED rather than getting bound to a partially-parsed ref — a loose parse\n * (e.g. `pb_victim_cXX`) must not scope persisted sessions to `victim` and\n * hydrate a session belonging to a different, real key. The backend remains the\n * authority that rejects a bad key on the first request; this keeps the client's\n * local session-storage scoping honest. (Audit finding apikey-ref-mismatch.)\n */\nconst API_KEY_RE = /^pb_([a-z0-9]{4,24})_[cs][A-Za-z0-9]{20,}$/;\nconst PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20,}$/;\n\nexport function environmentRefFromApiKey(apiKey: string): string {\n const m = API_KEY_RE.exec(apiKey);\n return m ? (m[1] ?? '') : '';\n}\n\n/** Internal app-runtime parser: browser config accepts publishable keys only. */\nexport function environmentRefFromPublishableApiKey(apiKey: string): string {\n const match = PUBLISHABLE_API_KEY_RE.exec(apiKey);\n return match ? (match[1] ?? '') : '';\n}\n","import { PalbaseError, type PalbaseResponse } from '@palbase/core';\n\nexport interface FieldError {\n field: string;\n message: string;\n}\n\nexport type BackendErrorKind =\n | 'notConfigured'\n | 'validation'\n | 'unauthorized'\n | 'rateLimited'\n | 'server'\n | 'network'\n | 'decode';\n\ninterface BackendErrorParams {\n code: string;\n message: string;\n status?: number;\n requestId?: string;\n fields?: FieldError[];\n retryAfter?: number;\n data?: unknown;\n}\n\nexport class BackendError extends Error {\n readonly kind: BackendErrorKind;\n readonly code: string;\n readonly status: number;\n readonly requestId?: string;\n readonly fields?: FieldError[];\n readonly retryAfter?: number;\n readonly data?: unknown;\n\n constructor(kind: BackendErrorKind, params: BackendErrorParams) {\n super(params.message);\n this.name = 'BackendError';\n this.kind = kind;\n this.code = params.code;\n this.status = params.status ?? 0;\n this.requestId = params.requestId;\n this.fields = params.fields;\n this.retryAfter = params.retryAfter;\n this.data = params.data;\n }\n\n static notConfigured(): BackendError {\n return new BackendError('notConfigured', {\n code: 'not_configured',\n message:\n \"Palbe is not configured. Run 'palbase web link' in your project and make sure palbe.gen.ts is imported once at app startup.\",\n });\n }\n}\n\nfunction isFieldErrorArray(value: unknown): value is FieldError[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every(\n (v) =>\n typeof v === 'object' &&\n v !== null &&\n typeof (v as Record<string, unknown>).field === 'string' &&\n typeof (v as Record<string, unknown>).message === 'string',\n )\n );\n}\n\nfunction pickField(value: unknown, key: string): unknown {\n if (typeof value === 'object' && value !== null) {\n return (value as Record<string, unknown>)[key];\n }\n return undefined;\n}\n\nfunction pickNumber(value: unknown, key: string): number | undefined {\n const n = pickField(value, key);\n return typeof n === 'number' ? n : undefined;\n}\n\nfunction pickString(value: unknown, key: string): string | undefined {\n const s = pickField(value, key);\n return typeof s === 'string' ? s : undefined;\n}\n\nexport function fromPalbaseError(err: PalbaseError): BackendError {\n // HttpClient stores the WHOLE wire envelope as err.details:\n // { error, error_description, status, request_id, retry_after?, details?, data? }\n const base: BackendErrorParams = {\n code: err.code,\n message: err.message,\n status: err.status,\n requestId: pickString(err.details, 'request_id'),\n data: pickField(err.details, 'data'),\n };\n if (err.code === 'network_error') return new BackendError('network', base);\n if (err.status === 401) return new BackendError('unauthorized', base);\n if (err.status === 429)\n return new BackendError('rateLimited', {\n ...base,\n retryAfter: pickNumber(err.details, 'retry_after'),\n });\n // Field-error array lives at the envelope's nested `details` key.\n const nested = pickField(err.details, 'details');\n if (err.status === 400 && isFieldErrorArray(nested))\n return new BackendError('validation', { ...base, fields: nested });\n // Fallthrough also covers status-0 non-network errors (e.g. auth client's\n // synthetic 'no_refresh_token' with status 0) — those intentionally map to 'server'.\n return new BackendError('server', base);\n}\n\n/** Decode a raw wire body (used by paths that bypass HttpClient, e.g. upload). */\nexport function fromEnvelope(status: number, body: unknown): BackendError {\n const code = pickString(body, 'error') ?? 'http_error';\n const message = pickString(body, 'error_description') ?? `HTTP ${status}`;\n const requestId = pickString(body, 'request_id');\n const details = pickField(body, 'details');\n const params: BackendErrorParams = {\n code,\n message,\n status,\n requestId,\n data: pickField(body, 'data'),\n };\n if (status === 401) return new BackendError('unauthorized', params);\n if (status === 429)\n return new BackendError('rateLimited', {\n ...params,\n // Real 429 wire body has TOP-LEVEL retry_after; nested details is a fallback.\n retryAfter: pickNumber(body, 'retry_after') ?? pickNumber(details, 'retry_after'),\n });\n if (status === 400 && isFieldErrorArray(details))\n return new BackendError('validation', { ...params, fields: details });\n return new BackendError('server', params);\n}\n\n/**\n * Type guard for BackendError that survives module-identity splits.\n * This package ships dual ESM + CJS builds; if both end up loaded (or the\n * package is installed twice), two distinct BackendError classes coexist and\n * `instanceof` fails for errors thrown by \"the other\" copy. Falls back to a\n * structural check on `name` + `kind`.\n */\nexport function isBackendError(e: unknown): e is BackendError {\n return (\n e instanceof BackendError ||\n (typeof e === 'object' &&\n e !== null &&\n (e as Record<string, unknown>).name === 'BackendError' &&\n typeof (e as Record<string, unknown>).kind === 'string')\n );\n}\n\n/**\n * Structural fallback after instanceof for PalbaseError — same dual ESM+CJS\n * identity-split rationale as isBackendError above: two loaded PalbaseError\n * classes break `instanceof` across copies; the name check bridges that.\n * Returns the error as PalbaseError, or null when it isn't one.\n */\nexport function asPalbaseError(e: unknown): PalbaseError | null {\n if (e instanceof PalbaseError) return e;\n if (e instanceof Error && e.name === 'PalbaseError') return e as PalbaseError;\n return null;\n}\n\n/** Convert an internal {data,error} envelope into data-or-throw. */\nexport function unwrap<T>(res: PalbaseResponse<T>): T {\n if (res.error) throw fromPalbaseError(res.error);\n return res.data as T;\n}\n","/**\n * Session-cookie codec shared by EVERY palbe/next surface (browser storage\n * adapter, pbServer cookie adapter, middleware, auth callback). Pure\n * functions — no 'next' imports, no palbe runtime imports — so both sides\n * provably read and write the SAME bytes.\n *\n * Format (P3 design contract):\n * - name: `palbe-session-<environmentRef>`; overflow chunk names append `.0`,\n * `.1`, … (cookie names otherwise avoid dots — tooling friendliness).\n * - value: `encodeURIComponent(JSON.stringify({ a, r, e }))` with a=access\n * token, r=refresh token, e=expiry (ms epoch).\n * - one cookie when the encoded value fits MAX_COOKIE_VALUE chars, else\n * chunks ONLY (the base name must not coexist with chunks — callers clear\n * stale names via clearedSessionCookieNames before writing, then honor the\n * encode result's `clear` overflow guard after writing).\n */\n\nexport { environmentRefFromApiKey } from '../api-key.js';\n\nexport interface StoredSession {\n accessToken: string;\n refreshToken: string;\n /** ms epoch */\n expiresAt: number;\n}\n\n/** Browsers cap a cookie at ~4096 bytes incl. name + attributes; 3500 leaves headroom. */\nconst MAX_COOKIE_VALUE = 3500;\n\n/**\n * The P3 cookie contract attributes, shared by every server-side writer\n * (pbServer adapter, middleware, callback); the browser writer mirrors them\n * in document.cookie string form (next/client.ts). NOT HttpOnly by design —\n * the browser SDK must read/write the session (Supabase-paradigm tradeoff);\n * Max-Age = 30d = the refresh-token TTL.\n */\nexport const SESSION_COOKIE_ATTRS = {\n path: '/',\n sameSite: 'lax',\n secure: true,\n maxAge: 2_592_000,\n} as const;\n\nexport function sessionCookieName(environmentRef: string): string {\n return `palbe-session-${environmentRef}`;\n}\n\nexport interface SessionCookieWrite {\n /** Cookies to SET, in order. */\n set: Array<{ name: string; value: string }>;\n /**\n * Names to DELETE alongside the set: the one-past-the-end OVERFLOW GUARD\n * (base write → `.0`; chunks `.0..n` → `.(n+1)`). Decode stops at the\n * first missing chunk, so deleting exactly one-past-the-end guarantees a\n * stale orphan chunk (left behind a gap, where presence-probing cleanup\n * cannot see it) can never concatenate into the freshly written run.\n * Writers MUST honor both lists.\n */\n clear: string[];\n}\n\nexport function encodeSessionCookies(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const name = sessionCookieName(environmentRef);\n const value = encodeURIComponent(\n JSON.stringify({ a: session.accessToken, r: session.refreshToken, e: session.expiresAt }),\n );\n if (value.length <= MAX_COOKIE_VALUE) {\n return { set: [{ name, value }], clear: [`${name}.0`] };\n }\n const chunks: Array<{ name: string; value: string }> = [];\n // Chunk WITHOUT splitting a %XX escape (every '%' in the encoded value\n // starts one — encodeURIComponent never emits a bare '%'). Next's cookie\n // layer percent-DECODES values on read and silently DROPS a cookie whose\n // value is an invalid escape sequence, so a split escape would lose that\n // chunk server-side; independent decodability also lets\n // encodeSessionCookiesDecoded mirror this run byte-exactly.\n for (let start = 0; start < value.length; ) {\n let end = Math.min(start + MAX_COOKIE_VALUE, value.length);\n if (end < value.length) {\n if (value[end - 1] === '%') end -= 1;\n else if (value[end - 2] === '%') end -= 2;\n }\n chunks.push({ name: `${name}.${chunks.length}`, value: value.slice(start, end) });\n start = end;\n }\n return { set: chunks, clear: [`${name}.${chunks.length}`] };\n}\n\n/**\n * encodeSessionCookies for percent-ENCODING cookie stores. Next's request\n * and response cookie APIs apply encodeURIComponent to values when\n * serializing (and decodeURIComponent on read) — handing them the codec's\n * already-encoded values would DOUBLE-encode the wire bytes and break the\n * browser-side reader. This variant returns the same cookie run with\n * DECODED values: `encodeURIComponent(value)` reproduces the\n * encodeSessionCookies bytes exactly (chunk boundaries never split an\n * escape), so an encoding store writes byte-identical cookies to a raw\n * writer like document.cookie.\n */\nexport function encodeSessionCookiesDecoded(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const { set, clear } = encodeSessionCookies(environmentRef, session);\n return {\n set: set.map(({ name, value }) => ({ name, value: decodeURIComponent(value) })),\n clear,\n };\n}\n\n/**\n * Read the session back from a cookie getter: the base name wins; otherwise\n * `.0`, `.1`, … are concatenated until the first missing chunk. Tolerant:\n * malformed URI escapes / JSON / shape → null (treated as \"no session\").\n *\n * Accepts BOTH value forms. Next's cookie stores percent-DECODE values once\n * on read, so server-side callers (pbServer adapter, middleware) hand us the\n * already-decoded JSON; raw readers (document.cookie) hand us the encoded\n * form. The raw (already-decoded) parse runs FIRST because it is\n * unambiguous — an encoded value can never raw-parse as JSON\n * (encodeURIComponent escapes `{` itself, so it starts with `%7B`) — whereas\n * decode-first would silently corrupt a literal `%XX` run inside a token\n * (decodeURIComponent('%41') → 'A', no throw) or throw on an invalid escape.\n */\nexport function decodeSessionCookies(\n get: (name: string) => string | undefined,\n environmentRef: string,\n): StoredSession | null {\n const base = sessionCookieName(environmentRef);\n let encoded = get(base);\n if (encoded === undefined) {\n let joined = '';\n for (let i = 0; ; i++) {\n const part = get(`${base}.${i}`);\n if (part === undefined) break;\n joined += part;\n }\n if (joined === '') return null;\n encoded = joined;\n }\n const raw = parseStoredSession(encoded);\n if (raw !== null) return raw;\n try {\n return parseStoredSession(decodeURIComponent(encoded));\n } catch {\n return null;\n }\n}\n\n/** JSON-parse + shape-check one candidate value; malformed → null. */\nfunction parseStoredSession(json: string): StoredSession | null {\n try {\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.a === 'string' && typeof obj.r === 'string' && typeof obj.e === 'number') {\n return { accessToken: obj.a, refreshToken: obj.r, expiresAt: obj.e };\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Names a writer must delete before/while writing: the base name (when\n * present) plus every existing `.N` chunk, probed in order until the first\n * missing one (writes are always a contiguous `.0..n` run, so a gap means\n * the end of palbe-owned chunks).\n */\nexport function clearedSessionCookieNames(\n environmentRef: string,\n present: (name: string) => boolean,\n): string[] {\n const base = sessionCookieName(environmentRef);\n const names: string[] = [];\n if (present(base)) names.push(base);\n for (let i = 0; ; i++) {\n const chunk = `${base}.${i}`;\n if (!present(chunk)) break;\n names.push(chunk);\n }\n return names;\n}\n\n/**\n * Parse the canonical chunk index out of `palbe-session-<ref>.N`; non-chunk\n * names (the bare base, a foreign ref, junk suffixes like `.x` / `.01` /\n * `.-1`) → null. The index is a non-negative integer with NO leading zeros\n * (the encoder emits `.0`, `.1`, … verbatim), so `String(n) === suffix`\n * round-trips it exactly.\n */\nfunction chunkIndex(base: string, name: string): number | null {\n if (!name.startsWith(`${base}.`)) return null;\n const suffix = name.slice(base.length + 1);\n if (!/^\\d+$/.test(suffix)) return null;\n const idx = Number.parseInt(suffix, 10);\n return String(idx) === suffix ? idx : null;\n}\n\n/**\n * getAll-powered orphan sweep — the deletion list a writer with the FULL\n * cookie jar should clear, catching crash-orphans that presence-probing\n * (clearedSessionCookieNames, which stops at the first gap) CANNOT see.\n *\n * Given every cookie name in the jar, returns the `palbe-session-<ref>`-family\n * names that are NOT part of a single valid session:\n * - A valid session is EITHER the bare base alone, OR the contiguous chunk\n * run `.0, .1, …, n` (starting at `.0`, no gaps).\n * - Any `.N` beyond the first gap from `.0` is an orphan (incl. ALL chunks\n * when `.0` is absent — no run exists).\n * - The base coexisting with ANY chunk is inconsistent (the contract forbids\n * it; decode lets the base win and silently strands the chunks): the base\n * is kept, the chunks are swept (a fresh write replaces the base in place).\n *\n * Only canonical `.N` names count; foreign refs and malformed suffixes are\n * ignored. Output order: base (if inconsistent) first, then chunks ascending.\n */\nexport function orphanSessionCookieNames(environmentRef: string, allNames: string[]): string[] {\n const base = sessionCookieName(environmentRef);\n const hasBase = allNames.includes(base);\n const indices = new Set<number>();\n for (const name of allNames) {\n const idx = chunkIndex(base, name);\n if (idx !== null) indices.add(idx);\n }\n if (indices.size === 0) return []; // unchunked (base alone) or no palbe cookies\n\n // Length of the contiguous run from .0 (0 when .0 is missing).\n let runLen = 0;\n while (indices.has(runLen)) runLen++;\n\n const orphans: string[] = [];\n // Base coexisting with chunks: base wins on decode → chunks are stale.\n // Sweep the base too only when there are NO chunks behind the run to keep\n // (the contract forbids the mix; clearing chunks alone resolves it, but if\n // the run itself is invalid—no .0—the base is the real session and stays).\n // Decision: when base coexists with ANY chunk, the chunks are orphans; the\n // base is the live session and is kept — UNLESS the chunk set forms a valid\n // run AND there is also a gap-orphan, in which case both base+gap-orphan are\n // inconsistent leftovers. Concretely: keep it simple and SAFE — the base is\n // only swept when it sits alongside a partial/gapped chunk set (a crash mid\n // base→chunk migration), never when chunks form a clean run the base shadows.\n const everyChunkContiguous = indices.size === runLen && runLen > 0;\n if (hasBase && !everyChunkContiguous) orphans.push(base);\n\n for (const idx of [...indices].sort((a, b) => a - b)) {\n if (idx >= runLen) orphans.push(`${base}.${idx}`);\n }\n // Base + clean contiguous run: base wins on decode, chunks are stranded.\n if (hasBase && everyChunkContiguous) {\n for (let i = 0; i < runLen; i++) orphans.push(`${base}.${i}`);\n }\n return orphans;\n}\n","/**\n * The lazy 'next/server' import, shared by every palbe/next surface that\n * builds a NextResponse (palbeMiddleware, handleAuthCallback).\n *\n * MODULE-GRAPH RULE: this module must stay reachable from the middleware\n * entry, which Next compiles into its own Edge bundle — so it may import\n * NOTHING that leads to ../runtime.js. Keep it at errors.js and below.\n * (Global-config lookup, which does reach the runtime, lives in\n * ./global-config.ts.)\n */\nimport { BackendError } from '../errors.js';\n\nlet nextServerModule: Promise<typeof import('next/server')> | undefined;\n\n/**\n * Lazy 'next/server' import (MODULE-GRAPH RULE: never top-level — importing\n * palbe/next must work without next installed) with a guided failure.\n * The module promise is memoized: bundler dynamic-import machinery can take\n * macrotask-scale hops PER CALL, so without the cache two near-simultaneous\n * middleware invocations could straddle a refresh — every call after the\n * first now resolves in pure microtasks.\n */\nexport async function importNextServer(caller: string): Promise<typeof import('next/server')> {\n nextServerModule ??= import('next/server');\n try {\n return await nextServerModule;\n } catch (cause) {\n nextServerModule = undefined; // never poison later calls with a cached rejection\n const detail = cause instanceof Error && cause.message ? ` (${cause.message})` : '';\n throw new BackendError('validation', {\n code: 'next_required',\n message: `${caller}() requires Next.js — 'next/server' could not be resolved${detail}.`,\n });\n }\n}\n"],"mappings":"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","names":["response"]}
1
+ {"version":3,"sources":["../../src/next/proxy.ts","../../src/api-key.ts","../../src/errors.ts","../../src/next/cookie-codec.ts","../../src/next/shared.ts"],"sourcesContent":["/**\n * '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js\n * proxy (the file convention that replaced `middleware` in Next 16; this\n * package's peer range is `next >= 16`).\n *\n * REQUIRED — not optional — for session-bearing RSC apps. Server Components\n * cannot write cookies, so pbServer's reactive refresh cannot PERSIST a\n * rotated session there: every RSC render would re-refresh from the SAME\n * cookie refresh token, and once two of those land more than 30s apart\n * (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE\n * TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes\n * the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid\n * session and never burns the token itself.\n *\n * BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config\n * instead of reading the process-global one like pbServer does:\n *\n * Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts\n * imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from\n * proxy.ts to fix that would drag `__configure` → buildRuntime →\n * messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB\n * where this entry ships 168 KB. (Under the pre-16 `middleware` convention it\n * was worse than size — that bundle was Edge, and webpack failed the build\n * outright with \"Dynamic Code Evaluation ... not allowed in Edge Runtime\".\n * `proxy` runs on nodejs, which is not configurable, so the hard error is\n * gone and the weight is not.)\n *\n * So the proxy never touches the runtime: it takes the two published identity\n * values it actually needs and speaks the one auth wire directly. Nothing on\n * this module's import graph may reach ../runtime.js —\n * __tests__/next-proxy-bundle.test.ts locks that against the real build\n * output.\n *\n * 'next/server' is imported only lazily (the type-only NextRequest/\n * NextResponse imports erase at build time).\n *\n * ```ts\n * // proxy.ts\n * import { palbeProxy } from '@palbase/web/next/proxy';\n * import type { NextRequest } from 'next/server';\n *\n * export function proxy(request: NextRequest) {\n * return palbeProxy(request, {\n * url: 'https://abc12345m.palbase.studio',\n * apiKey: 'pb_abc12345m_c...',\n * });\n * }\n *\n * // `config` MUST be declared here, in this file: Next reads it off THIS\n * // file's AST (export const + literal initializer). A re-exported or\n * // imported config is invisible and silently degrades to a catch-all.\n * export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };\n * ```\n */\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n type StoredSession,\n} from './cookie-codec.js';\nimport { importNextServer } from './shared.js';\n\nconst DEFAULT_REFRESH_MARGIN_MS = 60_000;\n\n/**\n * The published identity palbeProxy needs — the same two values\n * `palbase web link` bakes into palbe.gen.ts. Both are publishable (the\n * anon key already ships in every browser bundle); a service_role key is\n * REJECTED, it has no business in a request-path bundle.\n *\n * A `PalbeConfig` is structurally assignable, so apps that already hold one\n * can pass it straight through.\n */\nexport interface PalbeProxyConfig {\n /** Environment URL, e.g. `https://abc12345m.palbase.studio`. */\n url: string;\n /** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */\n apiKey: string;\n}\n\nexport interface PalbeProxyOptions {\n /** Refresh when the access token expires within this window. Default 60s. */\n refreshMarginMs?: number;\n /**\n * Pre-built response to decorate (e.g. from your own chained proxy logic) instead of\n * building `NextResponse.next({ request })`. Pass-through cases return it\n * UNMODIFIED. Caveat: a pre-built response snapshotted the request headers\n * at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)\n * but not the downstream RSC render of THIS request — prefer omitting this\n * and building your response after palbeProxy when possible.\n */\n response?: NextResponse;\n}\n\ntype RefreshOutcome =\n | { kind: 'rotated'; session: StoredSession }\n | { kind: 'terminal' } // 400/401/403 — refresh token dead, sign out\n | { kind: 'transient' }; // network / 5xx / malformed — keep the session\n\n/**\n * Per-process single-flight: concurrent proxy invocations holding the\n * SAME refresh token (parallel tabs / prefetches) share one refresh call —\n * a per-token thundering herd would race palauth rotation. Multi-pod\n * concurrency is covered by palauth's 30s rotation grace (concurrent\n * refreshes of one token all succeed); per-process dedup is all that's\n * needed here (Studio precedent — don't over-engineer).\n */\nconst inflightRefreshes = new Map<string, Promise<RefreshOutcome>>();\n\nfunction refreshSingleFlight(\n config: PalbeProxyConfig,\n refreshToken: string,\n): Promise<RefreshOutcome> {\n const existing = inflightRefreshes.get(refreshToken);\n if (existing) return existing;\n const pending = refresh(config, refreshToken).finally(() => {\n inflightRefreshes.delete(refreshToken);\n });\n inflightRefreshes.set(refreshToken, pending);\n return pending;\n}\n\n/** Direct palauth refresh call (no runtime: the proxy needs only this one wire). */\nasync function refresh(config: PalbeProxyConfig, refreshToken: string): Promise<RefreshOutcome> {\n try {\n const res = await fetch(`${config.url}/auth/token/refresh`, {\n method: 'POST',\n headers: { apikey: config.apiKey, 'content-type': 'application/json' },\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n if (res.status === 400 || res.status === 401 || res.status === 403) {\n return { kind: 'terminal' };\n }\n if (!res.ok) return { kind: 'transient' };\n const raw: unknown = await res.json();\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n if (\n typeof obj.access_token === 'string' &&\n typeof obj.refresh_token === 'string' &&\n typeof obj.expires_in === 'number'\n ) {\n return {\n kind: 'rotated',\n session: {\n accessToken: obj.access_token,\n refreshToken: obj.refresh_token,\n expiresAt: Date.now() + obj.expires_in * 1000,\n },\n };\n }\n }\n return { kind: 'transient' }; // malformed 200 — never destroy a session over it\n } catch {\n return { kind: 'transient' }; // network failure\n }\n}\n\n/**\n * Keep the session cookie fresh for the request. No session / still-fresh\n * session → untouched pass-through (no cache-control changes). Stale or\n * expired → single-flight refresh, then the rotated (or cleared, on a dead\n * token) cookies are written onto BOTH the request (for the downstream RSC\n * render via `NextResponse.next({ request })`) and the response (for the\n * browser), with `Cache-Control: private, no-store` on touched responses.\n *\n * Cookie writes go through Next's cookie APIs with the DECODED codec values:\n * Next percent-encodes on serialization, so the wire bytes come out exactly\n * as the shared codec's (see encodeSessionCookiesDecoded). NextRequest's\n * cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie\n * form), hence the direct codec calls here instead of the pbServer adapter.\n */\nexport async function palbeProxy(\n request: NextRequest,\n config: PalbeProxyConfig,\n opts?: PalbeProxyOptions,\n): Promise<NextResponse> {\n // Fail LOUDLY on a bad key. A silently empty ref would name the cookie\n // `palbe-session-` — a name the browser SDK never writes — so every\n // request would look session-less and the proxy would degrade to a\n // no-op, which is exactly the token-family revocation this exists to stop.\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n if (ref === '') {\n throw new BackendError('validation', {\n code: 'validation_error',\n message:\n 'palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c…). Re-run `palbase web link` and copy the generated values.',\n });\n }\n const { NextResponse } = await importNextServer('palbeProxy');\n const passThrough = (): NextResponse => opts?.response ?? NextResponse.next({ request });\n\n const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);\n if (!session) return passThrough();\n if (session.expiresAt - Date.now() > (opts?.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS)) {\n return passThrough();\n }\n\n const outcome = await refreshSingleFlight(config, session.refreshToken);\n if (outcome.kind === 'transient') return passThrough(); // keep the session over blips\n\n // Session-cookie names in the incoming jar — the browser's state mirror.\n const staleNames = clearedSessionCookieNames(ref, (name) => request.cookies.has(name));\n\n if (outcome.kind === 'terminal') {\n for (const name of staleNames) request.cookies.delete(name);\n const response = opts?.response ?? NextResponse.next({ request });\n for (const name of staleNames) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n }\n\n const write = encodeSessionCookiesDecoded(ref, outcome.session);\n for (const name of staleNames) request.cookies.delete(name);\n for (const { name, value } of write.set) request.cookies.set(name, value);\n for (const name of write.clear) request.cookies.delete(name);\n // Build AFTER the request-cookie mutation: NextResponse.next({ request })\n // snapshots the request headers for the downstream RSC render.\n const response = opts?.response ?? NextResponse.next({ request });\n const written = new Set(write.set.map((c) => c.name));\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Browser-side cleanup: stale names not part of the new run (a shrunken /\n // grown chunk layout) + the codec's one-past-the-end overflow guard.\n for (const name of new Set([...write.clear, ...staleNames])) {\n if (!written.has(name)) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n}\n","/**\n * `pb_<environmentRef>_<scope><random>` → environmentRef ('' if malformed).\n *\n * THE KEY IS THE ONLY SOURCE. There is no `environmentRef` on `PalbeConfig` to\n * prefer over this parse: the field was a second copy of what the key already\n * carries, `palbase link` was measured writing one value while minting a key\n * carrying another, and it was removed everywhere (see `runtime.ts`'s note).\n * So app-bound runtime storage (`runtime.ts`) and palbe/next cookie naming\n * (client, server, proxy, callback) all reach the ref through the publishable\n * parser below, off `config.apiKey`.\n *\n * The key is shape-validated (prefix `pb`, a 4-24 character lowercase ASCII\n * alphanumeric environmentRef, and a `<scope><at-least-20-base62-random>` secret\n * segment where scope ∈ {c,s}) before a ref is returned.\n *\n * THE SECRET'S LENGTH IS NOT PART OF THE CONTRACT — only its floor is. Two\n * minters ship two lengths (the stack's own `initenv` writes 20 base62, the\n * cloud control plane writes 32), and the gate that actually admits a request\n * says so outright: *\"a shorter or longer secret is not a security property\n * this door can rule on, and refusing one would only turn off a stack whose key\n * works everywhere else\"* (v2/internal/platform/identitymw.go: parseAPIKey).\n * Pinning EXACTLY 20 here made this SDK refuse every key the cloud has ever\n * minted (measured 2026-08-25: `buildRuntime` threw and palbe-gen never ran).\n * The floor stays, because the short/loose body is what the finding below was\n * about; the ceiling was never anyone's rule. A structurally-invalid key returns\n * '' so session storage stays\n * UNSCOPED rather than getting bound to a partially-parsed ref — a loose parse\n * (e.g. `pb_victim_cXX`) must not scope persisted sessions to `victim` and\n * hydrate a session belonging to a different, real key. The backend remains the\n * authority that rejects a bad key on the first request; this keeps the client's\n * local session-storage scoping honest. (Audit finding apikey-ref-mismatch.)\n */\nconst API_KEY_RE = /^pb_([a-z0-9]{4,24})_[cs][A-Za-z0-9]{20,}$/;\nconst PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20,}$/;\n\nexport function environmentRefFromApiKey(apiKey: string): string {\n const m = API_KEY_RE.exec(apiKey);\n return m ? (m[1] ?? '') : '';\n}\n\n/** Internal app-runtime parser: browser config accepts publishable keys only. */\nexport function environmentRefFromPublishableApiKey(apiKey: string): string {\n const match = PUBLISHABLE_API_KEY_RE.exec(apiKey);\n return match ? (match[1] ?? '') : '';\n}\n","import { PalbaseError, type PalbaseResponse } from '@palbase/core';\n\nexport interface FieldError {\n field: string;\n message: string;\n}\n\nexport type BackendErrorKind =\n | 'notConfigured'\n | 'validation'\n | 'unauthorized'\n | 'rateLimited'\n | 'server'\n | 'network'\n | 'decode';\n\ninterface BackendErrorParams {\n code: string;\n message: string;\n status?: number;\n requestId?: string;\n fields?: FieldError[];\n retryAfter?: number;\n data?: unknown;\n}\n\nexport class BackendError extends Error {\n readonly kind: BackendErrorKind;\n readonly code: string;\n readonly status: number;\n readonly requestId?: string;\n readonly fields?: FieldError[];\n readonly retryAfter?: number;\n readonly data?: unknown;\n\n constructor(kind: BackendErrorKind, params: BackendErrorParams) {\n super(params.message);\n this.name = 'BackendError';\n this.kind = kind;\n this.code = params.code;\n this.status = params.status ?? 0;\n this.requestId = params.requestId;\n this.fields = params.fields;\n this.retryAfter = params.retryAfter;\n this.data = params.data;\n }\n\n static notConfigured(): BackendError {\n return new BackendError('notConfigured', {\n code: 'not_configured',\n message:\n \"Palbe is not configured. Run 'palbase web link' in your project and make sure palbe.gen.ts is imported once at app startup.\",\n });\n }\n}\n\nfunction isFieldErrorArray(value: unknown): value is FieldError[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every(\n (v) =>\n typeof v === 'object' &&\n v !== null &&\n typeof (v as Record<string, unknown>).field === 'string' &&\n typeof (v as Record<string, unknown>).message === 'string',\n )\n );\n}\n\n/**\n * Find the field-error array in a wire envelope.\n *\n * `data.fields` IS THE CONTRACT: the backend SDK declares\n * `bad_request: { fields: [{ field, message }] }` in its error registry, so\n * codegen types it as `error.data.fields`, `throw new BadRequest({ fields })`\n * answers in that shape, and — since 22.1.0 — so do the engine's own automatic\n * `@Body`/`@QueryParams`/`@Headers` refusals.\n *\n * The top-level `fields` is the SAME array as emitted by backend 22.0.x and\n * earlier, which is still what every tenant deployed before that release\n * answers. A browser gets the SDK long before its tenant redeploys, so this\n * reader covers both until the fleet has rolled; drop it once it has.\n *\n * A nested `details: [{ field, message }]` was read here until 8.0.0 and is NOT\n * read any more: no Palbase surface has ever emitted it. Believing in it is why\n * `kind: 'validation'` was unreachable for every real 400 the platform sends.\n */\nfunction fieldErrorsOf(envelope: unknown): FieldError[] | undefined {\n const scoped = pickField(pickField(envelope, 'data'), 'fields');\n if (isFieldErrorArray(scoped)) return scoped;\n const flat = pickField(envelope, 'fields');\n return isFieldErrorArray(flat) ? flat : undefined;\n}\n\n/**\n * Find the rate-limit hint in a wire envelope.\n *\n * Two producers, both real and both measured. palauth answers a lockout with a\n * top-level `retry_after` (`auth/internal/ratelimit/limiter.go`). Everything\n * built on the backend SDK — the engine's per-route limiter, and\n * `throw new TooManyRequests({ retryAfter })` — answers with `data.retryAfter`,\n * the name its error registry publishes and codegen types.\n */\nfunction retryAfterOf(envelope: unknown): number | undefined {\n return (\n pickNumber(envelope, 'retry_after') ?? pickNumber(pickField(envelope, 'data'), 'retryAfter')\n );\n}\n\nfunction pickField(value: unknown, key: string): unknown {\n if (typeof value === 'object' && value !== null) {\n return (value as Record<string, unknown>)[key];\n }\n return undefined;\n}\n\nfunction pickNumber(value: unknown, key: string): number | undefined {\n const n = pickField(value, key);\n return typeof n === 'number' ? n : undefined;\n}\n\nfunction pickString(value: unknown, key: string): string | undefined {\n const s = pickField(value, key);\n return typeof s === 'string' ? s : undefined;\n}\n\nexport function fromPalbaseError(err: PalbaseError): BackendError {\n // HttpClient stores the WHOLE wire envelope as err.details:\n // { error, error_description, status, request_id, retry_after?, data? }\n const base: BackendErrorParams = {\n code: err.code,\n message: err.message,\n status: err.status,\n requestId: pickString(err.details, 'request_id'),\n data: pickField(err.details, 'data'),\n };\n if (err.code === 'network_error') return new BackendError('network', base);\n if (err.status === 401) return new BackendError('unauthorized', base);\n if (err.status === 429)\n return new BackendError('rateLimited', { ...base, retryAfter: retryAfterOf(err.details) });\n const fields = fieldErrorsOf(err.details);\n if (err.status === 400 && fields) return new BackendError('validation', { ...base, fields });\n // Fallthrough also covers status-0 non-network errors (e.g. auth client's\n // synthetic 'no_refresh_token' with status 0) — those intentionally map to 'server'.\n return new BackendError('server', base);\n}\n\n/** Decode a raw wire body (used by paths that bypass HttpClient, e.g. upload). */\nexport function fromEnvelope(status: number, body: unknown): BackendError {\n const code = pickString(body, 'error') ?? 'http_error';\n const message = pickString(body, 'error_description') ?? `HTTP ${status}`;\n const requestId = pickString(body, 'request_id');\n const params: BackendErrorParams = {\n code,\n message,\n status,\n requestId,\n data: pickField(body, 'data'),\n };\n if (status === 401) return new BackendError('unauthorized', params);\n if (status === 429)\n return new BackendError('rateLimited', { ...params, retryAfter: retryAfterOf(body) });\n const fields = fieldErrorsOf(body);\n if (status === 400 && fields) return new BackendError('validation', { ...params, fields });\n return new BackendError('server', params);\n}\n\n/**\n * Type guard for BackendError that survives module-identity splits.\n * This package ships dual ESM + CJS builds; if both end up loaded (or the\n * package is installed twice), two distinct BackendError classes coexist and\n * `instanceof` fails for errors thrown by \"the other\" copy. Falls back to a\n * structural check on `name` + `kind`.\n */\nexport function isBackendError(e: unknown): e is BackendError {\n return (\n e instanceof BackendError ||\n (typeof e === 'object' &&\n e !== null &&\n (e as Record<string, unknown>).name === 'BackendError' &&\n typeof (e as Record<string, unknown>).kind === 'string')\n );\n}\n\n/**\n * Structural fallback after instanceof for PalbaseError — same dual ESM+CJS\n * identity-split rationale as isBackendError above: two loaded PalbaseError\n * classes break `instanceof` across copies; the name check bridges that.\n * Returns the error as PalbaseError, or null when it isn't one.\n */\nexport function asPalbaseError(e: unknown): PalbaseError | null {\n if (e instanceof PalbaseError) return e;\n if (e instanceof Error && e.name === 'PalbaseError') return e as PalbaseError;\n return null;\n}\n\n/** Convert an internal {data,error} envelope into data-or-throw. */\nexport function unwrap<T>(res: PalbaseResponse<T>): T {\n if (res.error) throw fromPalbaseError(res.error);\n return res.data as T;\n}\n","/**\n * Session-cookie codec shared by EVERY palbe/next surface (browser storage\n * adapter, pbServer cookie adapter, middleware, auth callback). Pure\n * functions — no 'next' imports, no palbe runtime imports — so both sides\n * provably read and write the SAME bytes.\n *\n * Format (P3 design contract):\n * - name: `palbe-session-<environmentRef>`; overflow chunk names append `.0`,\n * `.1`, … (cookie names otherwise avoid dots — tooling friendliness).\n * - value: `encodeURIComponent(JSON.stringify({ a, r, e }))` with a=access\n * token, r=refresh token, e=expiry (ms epoch).\n * - one cookie when the encoded value fits MAX_COOKIE_VALUE chars, else\n * chunks ONLY (the base name must not coexist with chunks — callers clear\n * stale names via clearedSessionCookieNames before writing, then honor the\n * encode result's `clear` overflow guard after writing).\n */\n\nexport { environmentRefFromApiKey } from '../api-key.js';\n\nexport interface StoredSession {\n accessToken: string;\n refreshToken: string;\n /** ms epoch */\n expiresAt: number;\n}\n\n/** Browsers cap a cookie at ~4096 bytes incl. name + attributes; 3500 leaves headroom. */\nconst MAX_COOKIE_VALUE = 3500;\n\n/**\n * The P3 cookie contract attributes, shared by every server-side writer\n * (pbServer adapter, middleware, callback); the browser writer mirrors them\n * in document.cookie string form (next/client.ts). NOT HttpOnly by design —\n * the browser SDK must read/write the session (Supabase-paradigm tradeoff);\n * Max-Age = 30d = the refresh-token TTL.\n */\nexport const SESSION_COOKIE_ATTRS = {\n path: '/',\n sameSite: 'lax',\n secure: true,\n maxAge: 2_592_000,\n} as const;\n\nexport function sessionCookieName(environmentRef: string): string {\n return `palbe-session-${environmentRef}`;\n}\n\nexport interface SessionCookieWrite {\n /** Cookies to SET, in order. */\n set: Array<{ name: string; value: string }>;\n /**\n * Names to DELETE alongside the set: the one-past-the-end OVERFLOW GUARD\n * (base write → `.0`; chunks `.0..n` → `.(n+1)`). Decode stops at the\n * first missing chunk, so deleting exactly one-past-the-end guarantees a\n * stale orphan chunk (left behind a gap, where presence-probing cleanup\n * cannot see it) can never concatenate into the freshly written run.\n * Writers MUST honor both lists.\n */\n clear: string[];\n}\n\nexport function encodeSessionCookies(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const name = sessionCookieName(environmentRef);\n const value = encodeURIComponent(\n JSON.stringify({ a: session.accessToken, r: session.refreshToken, e: session.expiresAt }),\n );\n if (value.length <= MAX_COOKIE_VALUE) {\n return { set: [{ name, value }], clear: [`${name}.0`] };\n }\n const chunks: Array<{ name: string; value: string }> = [];\n // Chunk WITHOUT splitting a %XX escape (every '%' in the encoded value\n // starts one — encodeURIComponent never emits a bare '%'). Next's cookie\n // layer percent-DECODES values on read and silently DROPS a cookie whose\n // value is an invalid escape sequence, so a split escape would lose that\n // chunk server-side; independent decodability also lets\n // encodeSessionCookiesDecoded mirror this run byte-exactly.\n for (let start = 0; start < value.length; ) {\n let end = Math.min(start + MAX_COOKIE_VALUE, value.length);\n if (end < value.length) {\n if (value[end - 1] === '%') end -= 1;\n else if (value[end - 2] === '%') end -= 2;\n }\n chunks.push({ name: `${name}.${chunks.length}`, value: value.slice(start, end) });\n start = end;\n }\n return { set: chunks, clear: [`${name}.${chunks.length}`] };\n}\n\n/**\n * encodeSessionCookies for percent-ENCODING cookie stores. Next's request\n * and response cookie APIs apply encodeURIComponent to values when\n * serializing (and decodeURIComponent on read) — handing them the codec's\n * already-encoded values would DOUBLE-encode the wire bytes and break the\n * browser-side reader. This variant returns the same cookie run with\n * DECODED values: `encodeURIComponent(value)` reproduces the\n * encodeSessionCookies bytes exactly (chunk boundaries never split an\n * escape), so an encoding store writes byte-identical cookies to a raw\n * writer like document.cookie.\n */\nexport function encodeSessionCookiesDecoded(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const { set, clear } = encodeSessionCookies(environmentRef, session);\n return {\n set: set.map(({ name, value }) => ({ name, value: decodeURIComponent(value) })),\n clear,\n };\n}\n\n/**\n * Read the session back from a cookie getter: the base name wins; otherwise\n * `.0`, `.1`, … are concatenated until the first missing chunk. Tolerant:\n * malformed URI escapes / JSON / shape → null (treated as \"no session\").\n *\n * Accepts BOTH value forms. Next's cookie stores percent-DECODE values once\n * on read, so server-side callers (pbServer adapter, middleware) hand us the\n * already-decoded JSON; raw readers (document.cookie) hand us the encoded\n * form. The raw (already-decoded) parse runs FIRST because it is\n * unambiguous — an encoded value can never raw-parse as JSON\n * (encodeURIComponent escapes `{` itself, so it starts with `%7B`) — whereas\n * decode-first would silently corrupt a literal `%XX` run inside a token\n * (decodeURIComponent('%41') → 'A', no throw) or throw on an invalid escape.\n */\nexport function decodeSessionCookies(\n get: (name: string) => string | undefined,\n environmentRef: string,\n): StoredSession | null {\n const base = sessionCookieName(environmentRef);\n let encoded = get(base);\n if (encoded === undefined) {\n let joined = '';\n for (let i = 0; ; i++) {\n const part = get(`${base}.${i}`);\n if (part === undefined) break;\n joined += part;\n }\n if (joined === '') return null;\n encoded = joined;\n }\n const raw = parseStoredSession(encoded);\n if (raw !== null) return raw;\n try {\n return parseStoredSession(decodeURIComponent(encoded));\n } catch {\n return null;\n }\n}\n\n/** JSON-parse + shape-check one candidate value; malformed → null. */\nfunction parseStoredSession(json: string): StoredSession | null {\n try {\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.a === 'string' && typeof obj.r === 'string' && typeof obj.e === 'number') {\n return { accessToken: obj.a, refreshToken: obj.r, expiresAt: obj.e };\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Names a writer must delete before/while writing: the base name (when\n * present) plus every existing `.N` chunk, probed in order until the first\n * missing one (writes are always a contiguous `.0..n` run, so a gap means\n * the end of palbe-owned chunks).\n */\nexport function clearedSessionCookieNames(\n environmentRef: string,\n present: (name: string) => boolean,\n): string[] {\n const base = sessionCookieName(environmentRef);\n const names: string[] = [];\n if (present(base)) names.push(base);\n for (let i = 0; ; i++) {\n const chunk = `${base}.${i}`;\n if (!present(chunk)) break;\n names.push(chunk);\n }\n return names;\n}\n\n/**\n * Parse the canonical chunk index out of `palbe-session-<ref>.N`; non-chunk\n * names (the bare base, a foreign ref, junk suffixes like `.x` / `.01` /\n * `.-1`) → null. The index is a non-negative integer with NO leading zeros\n * (the encoder emits `.0`, `.1`, … verbatim), so `String(n) === suffix`\n * round-trips it exactly.\n */\nfunction chunkIndex(base: string, name: string): number | null {\n if (!name.startsWith(`${base}.`)) return null;\n const suffix = name.slice(base.length + 1);\n if (!/^\\d+$/.test(suffix)) return null;\n const idx = Number.parseInt(suffix, 10);\n return String(idx) === suffix ? idx : null;\n}\n\n/**\n * getAll-powered orphan sweep — the deletion list a writer with the FULL\n * cookie jar should clear, catching crash-orphans that presence-probing\n * (clearedSessionCookieNames, which stops at the first gap) CANNOT see.\n *\n * Given every cookie name in the jar, returns the `palbe-session-<ref>`-family\n * names that are NOT part of a single valid session:\n * - A valid session is EITHER the bare base alone, OR the contiguous chunk\n * run `.0, .1, …, n` (starting at `.0`, no gaps).\n * - Any `.N` beyond the first gap from `.0` is an orphan (incl. ALL chunks\n * when `.0` is absent — no run exists).\n * - The base coexisting with ANY chunk is inconsistent (the contract forbids\n * it; decode lets the base win and silently strands the chunks): the base\n * is kept, the chunks are swept (a fresh write replaces the base in place).\n *\n * Only canonical `.N` names count; foreign refs and malformed suffixes are\n * ignored. Output order: base (if inconsistent) first, then chunks ascending.\n */\nexport function orphanSessionCookieNames(environmentRef: string, allNames: string[]): string[] {\n const base = sessionCookieName(environmentRef);\n const hasBase = allNames.includes(base);\n const indices = new Set<number>();\n for (const name of allNames) {\n const idx = chunkIndex(base, name);\n if (idx !== null) indices.add(idx);\n }\n if (indices.size === 0) return []; // unchunked (base alone) or no palbe cookies\n\n // Length of the contiguous run from .0 (0 when .0 is missing).\n let runLen = 0;\n while (indices.has(runLen)) runLen++;\n\n const orphans: string[] = [];\n // Base coexisting with chunks: base wins on decode → chunks are stale.\n // Sweep the base too only when there are NO chunks behind the run to keep\n // (the contract forbids the mix; clearing chunks alone resolves it, but if\n // the run itself is invalid—no .0—the base is the real session and stays).\n // Decision: when base coexists with ANY chunk, the chunks are orphans; the\n // base is the live session and is kept — UNLESS the chunk set forms a valid\n // run AND there is also a gap-orphan, in which case both base+gap-orphan are\n // inconsistent leftovers. Concretely: keep it simple and SAFE — the base is\n // only swept when it sits alongside a partial/gapped chunk set (a crash mid\n // base→chunk migration), never when chunks form a clean run the base shadows.\n const everyChunkContiguous = indices.size === runLen && runLen > 0;\n if (hasBase && !everyChunkContiguous) orphans.push(base);\n\n for (const idx of [...indices].sort((a, b) => a - b)) {\n if (idx >= runLen) orphans.push(`${base}.${idx}`);\n }\n // Base + clean contiguous run: base wins on decode, chunks are stranded.\n if (hasBase && everyChunkContiguous) {\n for (let i = 0; i < runLen; i++) orphans.push(`${base}.${i}`);\n }\n return orphans;\n}\n","/**\n * The lazy 'next/server' import, shared by every palbe/next surface that\n * builds a NextResponse (palbeProxy, handleAuthCallback).\n *\n * MODULE-GRAPH RULE: this module must stay reachable from the proxy entry,\n * which Next compiles into its own separate bundle — so it may import\n * NOTHING that leads to ../runtime.js. Keep it at errors.js and below.\n * (Global-config lookup, which does reach the runtime, lives in\n * ./global-config.ts.)\n */\nimport { BackendError } from '../errors.js';\n\nlet nextServerModule: Promise<typeof import('next/server')> | undefined;\n\n/**\n * Lazy 'next/server' import (MODULE-GRAPH RULE: never top-level — importing\n * palbe/next must work without next installed) with a guided failure.\n * The module promise is memoized: bundler dynamic-import machinery can take\n * macrotask-scale hops PER CALL, so without the cache two near-simultaneous\n * middleware invocations could straddle a refresh — every call after the\n * first now resolves in pure microtasks.\n */\nexport async function importNextServer(caller: string): Promise<typeof import('next/server')> {\n nextServerModule ??= import('next/server');\n try {\n return await nextServerModule;\n } catch (cause) {\n nextServerModule = undefined; // never poison later calls with a cached rejection\n const detail = cause instanceof Error && cause.message ? ` (${cause.message})` : '';\n throw new BackendError('validation', {\n code: 'next_required',\n message: `${caller}() requires Next.js — 'next/server' could not be resolved${detail}.`,\n });\n }\n}\n"],"mappings":"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","names":["response"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  importNextServer
3
- } from "../chunk-FCIMEU2E.js";
3
+ } from "../chunk-VHUCJBBQ.js";
4
4
  import {
5
5
  SESSION_COOKIE_ATTRS,
6
6
  clearedSessionCookieNames,
@@ -9,10 +9,10 @@ import {
9
9
  } from "../chunk-6VGKMNXO.js";
10
10
  import {
11
11
  BackendError
12
- } from "../chunk-EAOROUSW.js";
12
+ } from "../chunk-4WMJTUDN.js";
13
13
  import {
14
14
  environmentRefFromPublishableApiKey
15
- } from "../chunk-Q5DUDL77.js";
15
+ } from "../chunk-G56VGUYZ.js";
16
16
  import "../chunk-PZ5AY32C.js";
17
17
 
18
18
  // src/next/proxy.ts
@@ -1,4 +1,4 @@
1
- import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, X as PalbePerf, d as PalbeRuntime } from './analytics-facade-CQXCQSoF.cjs';
1
+ import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, Y as PalbePerf, d as PalbeRuntime } from './analytics-facade-f8IVujJW.cjs';
2
2
 
3
3
  interface CallOptions {
4
4
  headers?: Record<string, string>;
@@ -18,6 +18,11 @@ interface UploadOptions {
18
18
  file: Blob;
19
19
  filename?: string;
20
20
  contentType?: string;
21
+ /**
22
+ * The request body sent beside the file. Folded into the single `body` part
23
+ * storage reads and handed to the route's schema — pass a pre-encoded JSON
24
+ * string as `body` to own the envelope yourself.
25
+ */
21
26
  fields?: Record<string, string>;
22
27
  onProgress?: (progress: UploadProgress) => void;
23
28
  signal?: AbortSignal;
@@ -1,4 +1,4 @@
1
- import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, X as PalbePerf, d as PalbeRuntime } from './analytics-facade-BER0EyYT.js';
1
+ import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, Y as PalbePerf, d as PalbeRuntime } from './analytics-facade-But6gu2r.js';
2
2
 
3
3
  interface CallOptions {
4
4
  headers?: Record<string, string>;
@@ -18,6 +18,11 @@ interface UploadOptions {
18
18
  file: Blob;
19
19
  filename?: string;
20
20
  contentType?: string;
21
+ /**
22
+ * The request body sent beside the file. Folded into the single `body` part
23
+ * storage reads and handed to the route's schema — pass a pre-encoded JSON
24
+ * string as `body` to own the envelope yourself.
25
+ */
21
26
  fields?: Record<string, string>;
22
27
  onProgress?: (progress: UploadProgress) => void;
23
28
  signal?: AbortSignal;
@@ -185,6 +185,15 @@ function isFieldErrorArray(value) {
185
185
  (v) => typeof v === "object" && v !== null && typeof v.field === "string" && typeof v.message === "string"
186
186
  );
187
187
  }
188
+ function fieldErrorsOf(envelope) {
189
+ const scoped = pickField(pickField(envelope, "data"), "fields");
190
+ if (isFieldErrorArray(scoped)) return scoped;
191
+ const flat = pickField(envelope, "fields");
192
+ return isFieldErrorArray(flat) ? flat : void 0;
193
+ }
194
+ function retryAfterOf(envelope) {
195
+ return pickNumber(envelope, "retry_after") ?? pickNumber(pickField(envelope, "data"), "retryAfter");
196
+ }
188
197
  function pickField(value, key) {
189
198
  if (typeof value === "object" && value !== null) {
190
199
  return value[key];
@@ -210,20 +219,15 @@ function fromPalbaseError(err) {
210
219
  if (err.code === "network_error") return new BackendError("network", base);
211
220
  if (err.status === 401) return new BackendError("unauthorized", base);
212
221
  if (err.status === 429)
213
- return new BackendError("rateLimited", {
214
- ...base,
215
- retryAfter: pickNumber(err.details, "retry_after")
216
- });
217
- const nested = pickField(err.details, "details");
218
- if (err.status === 400 && isFieldErrorArray(nested))
219
- return new BackendError("validation", { ...base, fields: nested });
222
+ return new BackendError("rateLimited", { ...base, retryAfter: retryAfterOf(err.details) });
223
+ const fields = fieldErrorsOf(err.details);
224
+ if (err.status === 400 && fields) return new BackendError("validation", { ...base, fields });
220
225
  return new BackendError("server", base);
221
226
  }
222
227
  function fromEnvelope(status, body) {
223
228
  const code = pickString(body, "error") ?? "http_error";
224
229
  const message = pickString(body, "error_description") ?? `HTTP ${status}`;
225
230
  const requestId = pickString(body, "request_id");
226
- const details = pickField(body, "details");
227
231
  const params = {
228
232
  code,
229
233
  message,
@@ -233,13 +237,9 @@ function fromEnvelope(status, body) {
233
237
  };
234
238
  if (status === 401) return new BackendError("unauthorized", params);
235
239
  if (status === 429)
236
- return new BackendError("rateLimited", {
237
- ...params,
238
- // Real 429 wire body has TOP-LEVEL retry_after; nested details is a fallback.
239
- retryAfter: pickNumber(body, "retry_after") ?? pickNumber(details, "retry_after")
240
- });
241
- if (status === 400 && isFieldErrorArray(details))
242
- return new BackendError("validation", { ...params, fields: details });
240
+ return new BackendError("rateLimited", { ...params, retryAfter: retryAfterOf(body) });
241
+ const fields = fieldErrorsOf(body);
242
+ if (status === 400 && fields) return new BackendError("validation", { ...params, fields });
243
243
  return new BackendError("server", params);
244
244
  }
245
245
  function isBackendError(e) {
@@ -466,7 +466,7 @@ function applyBrowserOriginHeader(headers) {
466
466
  }
467
467
 
468
468
  // src/version.ts
469
- var VERSION = "7.3.9";
469
+ var VERSION = "7.4.0";
470
470
 
471
471
  // src/call.ts
472
472
  async function callEndpoint(resolveRt, name, input, options) {
@@ -486,6 +486,7 @@ async function callEndpoint(resolveRt, name, input, options) {
486
486
  }
487
487
 
488
488
  // src/upload.ts
489
+ var UPLOAD_PATH = "/v1/storage/upload";
489
490
  function abortError() {
490
491
  return new BackendError("network", { code: "aborted", message: "Upload aborted" });
491
492
  }
@@ -539,7 +540,9 @@ async function buildHeaders(rt, extra) {
539
540
  }
540
541
  function buildForm(options) {
541
542
  const form = new FormData();
542
- for (const [k, v] of Object.entries(options.fields ?? {})) form.append(k, v);
543
+ const { body, ...loose } = options.fields ?? {};
544
+ if (body !== void 0) form.append("body", body);
545
+ else if (Object.keys(loose).length > 0) form.append("body", JSON.stringify(loose));
543
546
  const file = options.contentType && options.file.type !== options.contentType ? new Blob([options.file], { type: options.contentType }) : options.file;
544
547
  const filename = options.filename ?? (typeof File !== "undefined" && options.file instanceof File ? options.file.name : "file");
545
548
  form.append("file", file, filename);
@@ -613,8 +616,8 @@ async function uploadViaFetch(url, form, headers, options) {
613
616
  async function uploadEndpoint(resolveRt, name, options) {
614
617
  const rt = resolveRt();
615
618
  checkConstraints(options);
616
- const path = name.startsWith("/") ? name : `/${name}`;
617
- const url = `${rt.config.url}${path}`;
619
+ const route = name.startsWith("/") ? name : `/${name}`;
620
+ const url = `${rt.config.url}${UPLOAD_PATH}${route}`;
618
621
  const headers = await buildHeaders(rt, options.headers);
619
622
  const form = buildForm(options);
620
623
  const useXHR = options.onProgress !== void 0 && typeof XMLHttpRequest !== "undefined";