@luckystack/core 0.8.3 → 0.8.5

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 (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/CLAUDE.md +8 -8
  3. package/README.md +6 -8
  4. package/dist/{chunk-6HGH6S36.js → chunk-7VLA5TBT.js} +23 -2
  5. package/dist/chunk-7VLA5TBT.js.map +1 -0
  6. package/dist/{chunk-I4R7DMIX.js → chunk-IJYNVM2W.js} +2 -2
  7. package/dist/{chunk-RH65K3US.js → chunk-OWMII7WC.js} +1 -1
  8. package/dist/chunk-OWMII7WC.js.map +1 -0
  9. package/dist/{chunk-GZEHPARN.js → chunk-VRKJHE7E.js} +3 -3
  10. package/dist/{chunk-GZEHPARN.js.map → chunk-VRKJHE7E.js.map} +1 -1
  11. package/dist/client.d.ts +2 -2
  12. package/dist/client.js +4 -4
  13. package/dist/{config-DV8DGNt6.d.ts → config-CwW3KY9r.d.ts} +6 -2
  14. package/dist/config.d.ts +2 -2
  15. package/dist/config.js +10 -4
  16. package/dist/index.d.ts +23 -11
  17. package/dist/index.js +39 -13
  18. package/dist/index.js.map +1 -1
  19. package/dist/{projectConfig-PB5VK5AD.js → projectConfig-REH67TB2.js} +2 -2
  20. package/dist/{sentrySetup-G7NGISBY.js → sentrySetup-HNIURVUZ.js} +3 -3
  21. package/dist/{sessionTypes-iyUMf_pf.d.ts → sessionTypes-DanjuPaE.d.ts} +1 -1
  22. package/docs/app-bootstrap.md +10 -7
  23. package/docs/config-registry.md +2 -2
  24. package/docs/csrf-config.md +11 -11
  25. package/docs/hooks.md +1 -1
  26. package/docs/rate-limit-strategy.md +1 -1
  27. package/docs/redis-adapter.md +1 -1
  28. package/docs/session-types.md +1 -1
  29. package/docs/socket-bootstrap.md +3 -3
  30. package/package.json +1 -1
  31. package/dist/chunk-6HGH6S36.js.map +0 -1
  32. package/dist/chunk-RH65K3US.js.map +0 -1
  33. /package/dist/{chunk-I4R7DMIX.js.map → chunk-IJYNVM2W.js.map} +0 -0
  34. /package/dist/{projectConfig-PB5VK5AD.js.map → projectConfig-REH67TB2.js.map} +0 -0
  35. /package/dist/{sentrySetup-G7NGISBY.js.map → sentrySetup-HNIURVUZ.js.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/apiMethodMapRegistry.ts","../src/httpApiUtils.ts","../src/socketState.ts","../src/notifier.ts","../src/tryCatchSync.ts","../src/clientHookBus.ts","../src/offlineQueue.ts","../src/responseNormalizer.ts","../src/serviceRoute.ts","../src/socketEvents.ts","../src/csrfConfig.ts","../src/tryCatch.ts","../src/csrf.ts","../src/sleep.ts","../src/pageRouteValidation.ts"],"sourcesContent":["//? Generated `apiMethodMap` registry. Replaces the prefix-heuristic\n//? in `isGetMethod` (apiRequest.ts) so abort-controller selection uses\n//? the actual HTTP method declared on the handler instead of guessing\n//? from the name prefix.\n//?\n//? Wiring: the project's `src/_sockets/apiRequest.ts` wrapper imports the\n//? generated runtime map and calls `registerApiMethodMap(apiMethodMap)` once at\n//? module load, immediately before re-exporting the core request helper. Until then,\n//? lookups return `undefined` and `isGetMethod` falls back to the\n//? prefix heuristic (preserving previous behavior for transitional code).\n\nexport type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\nexport type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;\n\nimport { createRegistry } from './createRegistry';\n\nconst registry = createRegistry<ApiMethodMap | null>(null);\n\nexport const registerApiMethodMap = (map: ApiMethodMap): void => {\n registry.register(map);\n};\n\nexport const getRegisteredApiMethod = (\n pagePath: string,\n apiName: string,\n version: string,\n): HttpMethodLiteral | undefined => {\n const activeMap = registry.get();\n if (!activeMap) return undefined;\n //? The map shape is `Record<page, Record<name, Record<version, method>>>`;\n //? TS treats each indexed access as defined (no `noUncheckedIndexedAccess`),\n //? so the optional chains look redundant. They're not — a missing key returns\n //? undefined at runtime, which is the contract this function is documenting.\n /* eslint-disable @typescript-eslint/no-unnecessary-condition -- runtime-defensive index access */\n return activeMap[pagePath]?.[apiName]?.[version];\n /* eslint-enable @typescript-eslint/no-unnecessary-condition */\n};\n\nexport const isApiMethodMapRegistered = (): boolean => registry.isRegistered();\n","/**\n * HTTP API Utilities\n * \n * Helpers for HTTP API request handling including:\n * - Smart HTTP method detection based on API name\n * - Method validation\n */\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * Single source of truth for the GET-prefix naming heuristic\n * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the\n * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two\n * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.\n */\nexport const isGetMethodName = (methodNameLower: string): boolean =>\n methodNameLower.startsWith('get') ||\n methodNameLower.startsWith('fetch') ||\n methodNameLower.startsWith('list');\n\n/**\n * Infer the HTTP method from an API name based on common naming conventions.\n * \n * - Names starting with \"get\" → GET\n * - Names starting with \"delete\" or \"remove\" → DELETE\n * - Names starting with \"update\" or \"edit\" → PUT\n * - Everything else → POST (create operations, actions)\n * \n * @example\n * ```typescript\n * inferHttpMethod('getUserData') // → 'GET'\n * inferHttpMethod('deleteAccount') // → 'DELETE'\n * inferHttpMethod('updateProfile') // → 'PUT'\n * inferHttpMethod('createUser') // → 'POST'\n * inferHttpMethod('sendEmail') // → 'POST'\n * ```\n */\nexport const inferHttpMethod = (apiName: string): HttpMethod => {\n // Extract the method name from the full path (e.g., \"api/examples/getUserData\" → \"getUserData\")\n const nameParts = apiName.split('/');\n const lastPart = nameParts.at(-1)?.toLowerCase() ?? '';\n const methodName = /^v\\d+$/.test(lastPart)\n ? (nameParts.at(-2) ?? '').toLowerCase()\n : lastPart;\n\n if (isGetMethodName(methodName)) {\n return 'GET';\n }\n\n if (methodName.startsWith('delete') || methodName.startsWith('remove')) {\n return 'DELETE';\n }\n\n if (methodName.startsWith('update') || methodName.startsWith('edit') || methodName.startsWith('patch')) {\n return 'PUT';\n }\n\n // Default: POST for create operations and general actions\n return 'POST';\n};\n\n/**\n * Get the effective HTTP method for an API.\n * Uses explicit export if available, otherwise infers from name.\n */\nexport const getEffectiveHttpMethod = (\n apiConfig: { httpMethod?: HttpMethod },\n apiName: string\n): HttpMethod => {\n return apiConfig.httpMethod ?? inferHttpMethod(apiName);\n};\n\n/**\n * Check if an HTTP method is valid for an API.\n *\n * @param requestMethod - The method from the HTTP request\n * @param allowedMethod - The method the API accepts\n * @returns true only when the method matches the route's allowed method.\n *\n * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route\n * author wiring this into their dispatch must answer CORS preflights BEFORE the\n * route check — letting `OPTIONS` through would execute the handler on a request\n * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a\n * bypass on method-locked routes.\n */\nexport const isMethodAllowed = (\n requestMethod: string,\n allowedMethod: HttpMethod\n): boolean => {\n return requestMethod === allowedMethod;\n};\n","import type { Socket } from 'socket.io-client';\n\n// Shared, mutable socket-client state. Single source of truth for the socket\n// instance, consumed by transport helpers in @luckystack/core (apiRequest)\n// and @luckystack/sync (syncRequest), and written by the project's React\n// hook wiring in `src/_sockets/socketInitializer.ts` via `setSocket(io(...))`.\n//\n// The `export let socket` form gives importers a live binding — they always\n// see the current value after `setSocket` replaces it.\n\n// eslint-disable-next-line import-x/no-mutable-exports\nexport let socket: Socket | null = null;\n\n//? One-shot waiters: promises that resolve the moment a non-null socket is\n//? set. Replacing the original `setInterval` poll (10 ms × 500 iterations)\n//? with a subscribe-and-notify pattern eliminates the busy-loop overhead and\n//? guarantees sub-tick delivery rather than up-to-10 ms latency.\nlet waiters: ((s: Socket) => void)[] = [];\n\n//? Absolute deadline for `waitForSocket` — prevents a permanently-dangling\n//? promise in environments where the socket is never created (SSR, tests).\nconst WAIT_FOR_SOCKET_TIMEOUT_MS = 5000;\n\nexport const setSocket = (next: Socket | null): void => {\n socket = next;\n //? Resolve all pending waiters the moment a real socket arrives.\n if (next) {\n const pending = waiters;\n waiters = [];\n for (const resolve of pending) {\n resolve(next);\n }\n }\n};\n\nlet responseIndex = 0;\n\nexport const incrementResponseIndex = (): number => {\n responseIndex = responseIndex + 1;\n return responseIndex;\n};\n\nexport const waitForSocket = (): Promise<Socket | null> => {\n if (socket) return Promise.resolve(socket);\n return new Promise<Socket | null>((resolve) => {\n const timer = setTimeout(() => {\n //? Time out: remove our waiter slot and resolve with null so the\n //? caller surfaces a \"socket unavailable\" path instead of hanging.\n waiters = waiters.filter((w) => w !== onSocket);\n resolve(null);\n }, WAIT_FOR_SOCKET_TIMEOUT_MS);\n\n const onSocket = (s: Socket): void => {\n clearTimeout(timer);\n resolve(s);\n };\n\n waiters.push(onSocket);\n });\n};\n","//? Client-side notifier DI. Framework packages (apiRequest, syncRequest,\n//? offline queue flush) emit toasts on transport errors. Default is silent\n//? (no-op) so tests and SSR contexts don't need a toaster installed.\n//?\n//? The project's entry point registers a notifier backed by its UI toolkit\n//? (sonner + i18n) via `registerNotifier({ error, success, ... })`.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface NotifyParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface NotifyInput {\n key: string;\n params?: NotifyParam[];\n}\n\nexport interface Notifier {\n success: (input: NotifyInput) => void;\n error: (input: NotifyInput) => void;\n info: (input: NotifyInput) => void;\n warning: (input: NotifyInput) => void;\n}\n\nconst noopNotifier: Notifier = {\n success: () => { /* no-op */ },\n error: () => { /* no-op */ },\n info: () => { /* no-op */ },\n warning: () => { /* no-op */ },\n};\n\nconst registry = createRegistry<Notifier>(noopNotifier);\n\nexport const registerNotifier = (notifier: Notifier): void => {\n registry.register(notifier);\n};\n\nexport const getNotifier = (): Notifier => registry.get();\n\n//? Delegating wrapper matching the project's existing `notify` shape so the\n//? switchover inside framework packages is a one-line import change.\nexport const notify: Notifier = {\n success: (input) => { registry.get().success(input); },\n error: (input) => { registry.get().error(input); },\n info: (input) => { registry.get().info(input); },\n warning: (input) => { registry.get().warning(input); },\n};\n","//? Synchronous sibling of `tryCatch`. The async `tryCatch` returns a Promise\n//? and auto-captures to the error tracker — neither of which fits a hot,\n//? genuinely-synchronous path (error-formatter dispatch, cookie/CSRF parse)\n//? that must stay synchronous and decides its own logging.\n//?\n//? Unlike the async `tryCatch`, this helper does NOT auto-capture to the\n//? error tracker: the call sites that need it are synchronous fallbacks that\n//? already do their own contextual logging and deliberately keep the error\n//? path side-effect-light. Capture explicitly on the error tuple when needed.\n\n//? `P = void` mirrors the async `tryCatch<T, P = void>` so a params-less call\n//? can pass just the result type: `tryCatchSync<URL>(() => new URL(raw))`.\n//? Without the default TS demands both type args (TS2558).\nexport default function tryCatchSync<T, P = void>(\n func: (values: P) => T,\n params?: P,\n): [Error | null, T | null] {\n try {\n const response = func(params as P);\n return [null, response];\n } catch (error) {\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side hook bus. Counterpart to the server-side `registerHook` /\n//? `dispatchHook` system in `hookRegistry.ts`, but lives here in the\n//? browser-safe surface and listens for client-side lifecycle moments:\n//? session login, session logout (and room for `sessionUpdate` later).\n//?\n//? Fired from `react/sessionContext.ts` when `setLatestSession` observes\n//? a null → non-null (postLogin) or non-null → null (postLogout) transition.\n//? That makes the fire-point framework-controlled — consumers don't have to\n//? add their own useEffect in `SessionProvider` to detect these moments.\n//?\n//? Handlers are best-effort: errors are caught + logged but don't break the\n//? session pipeline. Async handlers fire-and-forget; the caller of\n//? `setLatestSession` does not await.\n//?\n//? Typical use:\n//? import { registerClientHook } from '@luckystack/core/client';\n//?\n//? registerClientHook('postLogin', ({ session }) => {\n//? const letter = session.name?.[0]?.toLowerCase();\n//? if (letter) joinRoom(`letter-${letter}`);\n//? });\n//?\n//? Consumers with extended session shapes get narrow typing via the same\n//? `BaseSessionLayout` module augmentation pattern the rest of the framework\n//? uses (see ARCHITECTURE_EXTENSION_POINTS.md `@luckystack/login` section).\n\nimport type { BaseSessionLayout } from './sessionTypes';\nimport { getLogger } from './loggerRegistry';\n\nexport interface ClientHookPayloadMap {\n //? Fires BEFORE the framework commits a null → session transition.\n //? Handlers may return a `ClientHookStopSignal` to veto the login (e.g.\n //? account suspended since last visit, feature-flag gate, geo block).\n //? Only fires when the consumer commits the transition through the\n //? vetoable entry point — see `proposeLogin` in `react/sessionContext.ts`.\n preLogin: { candidateSession: BaseSessionLayout };\n postLogin: { session: BaseSessionLayout };\n postLogout: { previousSession: BaseSessionLayout };\n //? Fires when the offline queue drops a request (full queue with a\n //? `drop-oldest`/`drop-newest`/`reject` policy, or an item past `maxAgeMs`).\n //? The in-memory queue is not durable across refreshes; this is the seam to\n //? surface a \"your change wasn't sent\" toast or to persist the item\n //? elsewhere. `queue` says which queue, `reason` why it was dropped.\n queueItemDropped: {\n queue: 'api' | 'sync';\n key: string;\n reason: 'queue-full' | 'expired';\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';\n };\n}\n\nexport type ClientHookName = keyof ClientHookPayloadMap;\n\n//? Stop signal returned by a `pre*` client hook handler to veto the\n//? framework's main flow. Mirrors the server-side `HookStopSignal` shape so\n//? `errorCode` flows through the same i18n / responseNormalizer pipeline a\n//? consumer already wires up.\nexport interface ClientHookStopSignal {\n stop: true;\n errorCode: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- mirrors server-side HookResult, see hooks/types.ts\nexport type ClientHookResult = void | undefined | ClientHookStopSignal;\n\nexport type ClientDispatchResult =\n | { stopped: false }\n | { stopped: true; signal: ClientHookStopSignal };\n\nexport type ClientHookHandler<N extends ClientHookName> = (\n payload: ClientHookPayloadMap[N],\n) => ClientHookResult | Promise<ClientHookResult>;\n\n//? Storage uses `unknown` handler sets so the generic `N` doesn't have to\n//? narrow at every access — the cast happens once at read/write inside the\n//? typed wrappers below, which is sound because each hook name's Set only\n//? ever sees handlers registered under that same name.\ntype AnyHandler = (payload: unknown) => ClientHookResult | Promise<ClientHookResult>;\nconst handlerSets: Partial<Record<ClientHookName, Set<AnyHandler>>> = {};\n\n/**\n * Subscribe to a client-side lifecycle hook. Returns an unsubscribe function\n * — call it to remove the handler (e.g. inside a React effect cleanup).\n *\n * Multiple handlers can be registered for the same hook; they all fire on\n * each dispatch. A handler that throws (sync) or rejects (async) is logged\n * to `console.error` and does not affect other handlers.\n */\nexport function registerClientHook<N extends ClientHookName>(\n name: N,\n handler: ClientHookHandler<N>,\n): () => void {\n const existing = handlerSets[name];\n const set = existing ?? new Set<AnyHandler>();\n if (!existing) handlerSets[name] = set;\n const wrapped = handler as AnyHandler;\n set.add(wrapped);\n return () => {\n set.delete(wrapped);\n };\n}\n\n/**\n * Fire a client-side hook (fire-and-forget). Framework-internal — consumers\n * should not call this directly. Used for `post*` hooks where any stop-signal\n * returned by a handler is intentionally ignored: a `postLogin` listener that\n * tried to \"stop\" the framework would be too late — the transition already\n * happened. Use `dispatchVetoableClientHook` for `pre*` hooks where the\n * caller actually awaits the veto decision.\n */\nexport function dispatchClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): void {\n const set = handlerSets[name];\n if (!set || set.size === 0) return;\n //? Snapshot the set before iterating so a handler that unregisters itself\n //? (or a sibling) mid-dispatch doesn't skip un-visited handlers — Set\n //? iteration honors mid-iter deletes per ECMAScript spec, which we don't\n //? want here.\n const handlers = [...set];\n for (const handler of handlers) {\n try {\n const result = handler(payload);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error(`[clientHook:${name}] async handler rejected`, error);\n });\n }\n } catch (error) {\n getLogger().error(`[clientHook:${name}] handler threw`, error);\n }\n }\n}\n\n/**\n * Awaits every registered handler for a `pre*` client hook. If any handler\n * returns a `ClientHookStopSignal`, dispatch short-circuits and returns\n * `{ stopped: true, signal }`; callers must respect the veto (skip the\n * pending action). Handlers that throw or reject are logged but treated as\n * `undefined` — a buggy handler must not silently block the user flow.\n *\n * Mirrors `dispatchHook` from `@luckystack/core` so consumer/framework code\n * can use the same stop-signal pattern on both sides of the wire.\n */\nexport async function dispatchVetoableClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): Promise<ClientDispatchResult> {\n const set = handlerSets[name];\n if (!set || set.size === 0) return { stopped: false };\n //? Snapshot the set (see `dispatchClientHook` for why).\n const handlers = [...set];\n for (const handler of handlers) {\n let result: ClientHookResult;\n try {\n result = await handler(payload);\n } catch (error) {\n getLogger().error(`[clientHook:${name}] vetoable handler threw`, error);\n continue;\n }\n if (result?.stop) {\n return { stopped: true, signal: result };\n }\n }\n return { stopped: false };\n}\n\n/** Test helper — drop every registered handler. Not part of the public API. */\nexport function _resetClientHooksForTests(): void {\n for (const key of Object.keys(handlerSets) as ClientHookName[]) {\n handlerSets[key]?.clear();\n }\n}\n","import { Socket } from \"socket.io-client\";\nimport { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport { dispatchClientHook } from \"./clientHookBus\";\n\n/** Why a queued item was dropped (handed to its `onDrop` callback). */\nexport type QueueDropReason = 'expired' | 'queue-full';\n\ninterface ApiQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See\n * `SyncQueueItem.dropPolicy` for details.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional callback invoked when THIS item is dropped (evicted for age or\n * because the queue was full / rejected) instead of ever running (SYNC-09).\n * The transport wires this to settle the pending `syncRequest`/`apiRequest`\n * promise with an error envelope, so an evicted offline request resolves\n * rather than hanging forever. Invoked at most once; failures are swallowed.\n */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\ninterface SyncQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the app\n * default stays `'reject'` for safer sends. When omitted, falls back to\n * the global config.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** See {@link ApiQueueItem.onDrop} (SYNC-09). */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\nconst apiQueue: ApiQueueItem[] = [];\nconst syncQueue: SyncQueueItem[] = [];\n\n//? Independent flush guards so a long api flush doesn't no-op a concurrent\n//? sync flush (or vice versa).\nlet isFlushingApi = false;\nlet isFlushingSync = false;\n\nexport const isOnline = () => {\n if (typeof navigator === \"undefined\") return true;\n return navigator.onLine;\n};\n\n//? Common drop shape — every queue item the framework drops carries these.\ninterface DroppableItem {\n createdAt: number;\n key: string;\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n onDrop?: (reason: QueueDropReason) => void;\n}\n\n//? Notify a dropped item's owner exactly once. Swallows callback errors so a\n//? throwing `onDrop` can't wedge the eviction/enqueue loop (SYNC-09).\nconst notifyDropped = (item: DroppableItem, reason: QueueDropReason): void => {\n if (!item.onDrop) return;\n tryCatchSync(() => { item.onDrop?.(reason); });\n};\n\n//? Dispatch the `queueItemDropped` client hook, guarded so a misbehaving\n//? handler set can't interrupt the eviction/enqueue loop (CORE-N8).\nconst safeDispatchDropHook = (\n queue: 'api' | 'sync',\n key: string,\n reason: QueueDropReason,\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject',\n): void => {\n tryCatchSync(() => {\n dispatchClientHook('queueItemDropped', { queue, key, reason, dropPolicy });\n });\n};\n\nconst evictExpired = (\n queue: DroppableItem[],\n label: 'api' | 'sync',\n): void => {\n const { maxAgeMs, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n if (maxAgeMs <= 0) return;\n const cutoff = Date.now() - maxAgeMs;\n for (let i = queue.length - 1; i >= 0; i -= 1) {\n const item = queue[i];\n if (item && item.createdAt < cutoff) {\n queue.splice(i, 1);\n notifyDropped(item, 'expired');\n safeDispatchDropHook(label, item.key, 'expired', item.dropPolicy ?? globalPolicy);\n }\n }\n};\n\nconst enqueueWithPolicy = <T extends DroppableItem>(\n queue: T[],\n item: T,\n label: 'api' | 'sync',\n): boolean => {\n evictExpired(queue, label);\n const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n //? Per-request override wins over the global policy. Lets specific\n //? syncs pick 'drop-oldest' for liveness while the app default stays\n //? 'reject' for safer sends.\n const effectivePolicy = item.dropPolicy ?? globalPolicy;\n if (queue.length < maxSize) {\n queue.push(item);\n return true;\n }\n if (effectivePolicy === 'drop-oldest') {\n const dropped = queue.shift();\n queue.push(item);\n getLogger().debug(`offlineQueue:${label} full — dropped oldest item (policy=${effectivePolicy})`);\n if (dropped) {\n notifyDropped(dropped, 'queue-full');\n safeDispatchDropHook(label, dropped.key, 'queue-full', effectivePolicy);\n }\n return true;\n }\n if (effectivePolicy === 'drop-newest') {\n getLogger().debug(`offlineQueue:${label} full — dropped newest item (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n }\n getLogger().warn(`offlineQueue:${label} full — rejecting enqueue (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n};\n\nexport const enqueueApiRequest = (item: ApiQueueItem): boolean =>\n enqueueWithPolicy(apiQueue, item, 'api');\n\nexport const enqueueSyncRequest = (item: SyncQueueItem): boolean =>\n enqueueWithPolicy(syncQueue, item, 'sync');\n\nexport const removeApiQueueItem = (id: string) => {\n const index = apiQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n apiQueue.splice(index, 1);\n }\n};\n\nexport const removeSyncQueueItem = (id: string) => {\n const index = syncQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n syncQueue.splice(index, 1);\n }\n};\n\nexport const removeApiQueueItemsByKey = (key: string) => {\n for (let i = apiQueue.length - 1; i >= 0; i -= 1) {\n if (apiQueue[i]?.key === key) {\n apiQueue.splice(i, 1);\n }\n }\n};\n\n//? Run one dequeued item. If `run` throws, requeue it at the FRONT (so the\n//? next flush retries it in order) and signal the caller to stop this flush —\n//? otherwise a throwing item is silently lost AND the `isFlushing*` guard\n//? would be left stuck `true`, wedging every future flush.\nconst runQueueItem = <T extends { run: (s: Socket) => void }>(\n queue: T[],\n item: T,\n socketInstance: Socket,\n label: string,\n): boolean => {\n const [error] = tryCatchSync(() => { item.run(socketInstance); });\n if (error) {\n queue.unshift(item);\n getLogger().warn(`offlineQueue:${label} run threw — requeued item, pausing flush`, { error });\n return false;\n }\n return true;\n};\n\nexport const flushApiQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingApi) return;\n evictExpired(apiQueue, 'api');\n if (apiQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingApi = true;\n while (apiQueue.length > 0) {\n if (!canRun()) break;\n const item = apiQueue.shift();\n if (item && !runQueueItem(apiQueue, item, socketInstance, 'api')) break;\n }\n isFlushingApi = false;\n};\n\nexport const flushSyncQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingSync) return;\n evictExpired(syncQueue, 'sync');\n if (syncQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingSync = true;\n while (syncQueue.length > 0) {\n if (!canRun()) break;\n const item = syncQueue.shift();\n if (item && !runQueueItem(syncQueue, item, socketInstance, 'sync')) break;\n }\n isFlushingSync = false;\n};\n\nexport const getApiQueueSize = () => apiQueue.length;\nexport const getSyncQueueSize = () => syncQueue.length;\n","export interface ErrorParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface ErrorResponseInput {\n status?: unknown;\n errorCode?: unknown;\n errorParams?: unknown;\n httpStatus?: unknown;\n}\n\nexport interface NormalizedErrorResponse {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: ErrorParam[];\n httpStatus?: number;\n}\n\nexport const INVALID_ERROR_RESPONSE_CODE = 'error.invalidResponse';\n\nexport const isErrorParamArray = (value: unknown): value is ErrorParam[] => {\n return Array.isArray(value) && value.every((item) => {\n return (\n !!item\n && typeof item === 'object'\n && typeof (item as ErrorParam).key === 'string'\n && ['string', 'number', 'boolean'].includes(typeof (item as ErrorParam).value)\n );\n });\n};\n\nexport const normalizeErrorResponseCore = ({\n response,\n fallbackHttpStatus,\n fallbackErrorCode,\n resolveMessage,\n}: {\n response: ErrorResponseInput;\n fallbackHttpStatus?: number;\n fallbackErrorCode?: string;\n resolveMessage?: (args: { errorCode: string; errorParams?: ErrorParam[] }) => string;\n}): NormalizedErrorResponse => {\n const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : undefined;\n const errorCodeValue = typeof response.errorCode === 'string' ? response.errorCode.trim() : '';\n const hasErrorCode = errorCodeValue.length > 0;\n const finalErrorCode = hasErrorCode ? errorCodeValue : (fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE);\n const providedHttpStatus = typeof response.httpStatus === 'number' ? response.httpStatus : undefined;\n\n const message = resolveMessage\n ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams })\n : finalErrorCode;\n\n return {\n status: 'error',\n message,\n errorCode: finalErrorCode,\n errorParams: normalizedParams,\n httpStatus: providedHttpStatus ?? fallbackHttpStatus,\n };\n};\n\nexport const defaultHttpStatusForResponse = ({\n status,\n explicitHttpStatus,\n fallbackErrorStatus = 400,\n}: {\n status: 'success' | 'error';\n explicitHttpStatus?: number;\n fallbackErrorStatus?: number;\n}): number => {\n if (typeof explicitHttpStatus === 'number') return explicitHttpStatus;\n return status === 'success' ? 200 : fallbackErrorStatus;\n};\n","const VERSION_SEGMENT_REGEX = /^v\\d+$/;\n\nexport interface ServiceRouteParseSuccess {\n status: 'success';\n normalizedRouteName: string;\n service: string;\n routeName: string;\n}\n\nexport interface ServiceRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;\n\nexport interface TransportRouteParseSuccess {\n status: 'success';\n normalizedFullName: string;\n version: string;\n serviceRoute: ServiceRouteParseSuccess;\n}\n\nexport interface TransportRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;\n\nconst normalizeRawRoute = (value: string): string => {\n return value.trim().replaceAll('\\\\', '/').replaceAll(/^\\/+/g, '').replaceAll(/\\/+$/g, '');\n};\n\nconst hasEmptySegments = (value: string): boolean => {\n return value.split('/').some((segment) => segment.length === 0);\n};\n\nexport const parseServiceRouteName = (value: string): ServiceRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Route name cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Route name cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n if (segments.length < 2) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n const [service, ...routeSegments] = segments;\n const routeName = routeSegments.join('/');\n\n if (!service || !routeName) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n return {\n status: 'success',\n normalizedRouteName: `${service}/${routeName}`,\n service,\n routeName,\n };\n};\n\nexport const parseTransportRouteName = ({\n value,\n prefix,\n}: {\n value: string;\n prefix: 'api' | 'sync';\n}): TransportRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Transport route cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Transport route cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;\n\n if (routeSegments.length < 3) {\n return {\n status: 'error',\n reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`,\n };\n }\n\n const version = routeSegments.at(-1);\n if (!version || !VERSION_SEGMENT_REGEX.test(version)) {\n return { status: 'error', reason: 'Route version segment must match v{number}.' };\n }\n\n const serviceRouteToken = routeSegments.slice(0, -1).join('/');\n const serviceRoute = parseServiceRouteName(serviceRouteToken);\n if (serviceRoute.status === 'error') {\n return {\n status: 'error',\n reason: serviceRoute.reason,\n };\n }\n\n return {\n status: 'success',\n normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,\n version,\n serviceRoute,\n };\n};\n","const withIndex = (prefix: string, responseIndex: number | string): string => {\n return `${prefix}${String(responseIndex)}`;\n};\n\nexport const socketEventNames = {\n apiRequest: 'apiRequest',\n apiCancel: 'apiCancel',\n sync: 'sync',\n syncCancel: 'syncCancel',\n joinRoom: 'joinRoom',\n leaveRoom: 'leaveRoom',\n getJoinedRooms: 'getJoinedRooms',\n updateLocation: 'updateLocation',\n updateSession: 'updateSession',\n sessionReplaced: 'sessionReplaced',\n logout: 'logout',\n\n intentionalDisconnect: 'intentionalDisconnect',\n intentionalReconnect: 'intentionalReconnect',\n //? Client -> server activity heartbeat (mouse/keyboard/touch), throttled.\n //? Drives the server-side activity sampler that fires registered activity\n //? events (e.g. the built-in AFK detector).\n activity: 'activity',\n userAfk: 'userAfk',\n userBack: 'userBack',\n //? Emitted to a room's remaining members when a peer leaves — hard disconnect\n //? (socket close) or grace-expiry (presence MIS-003). Lets clients prune a\n //? departed peer from their roster without polling. The presence package owns\n //? the dispatch; core owns the wire-event name so client + server can't drift.\n userLeft: 'userLeft',\n\n connect: 'connect',\n disconnect: 'disconnect',\n reconnectAttempt: 'reconnect_attempt',\n connectError: 'connect_error',\n\n apiResponsePrefix: 'apiResponse-',\n apiStreamPrefix: 'apiStream-',\n syncResponsePrefix: 'sync-',\n syncProgressPrefix: 'sync-progress-',\n joinRoomResponsePrefix: 'joinRoom-',\n leaveRoomResponsePrefix: 'leaveRoom-',\n getJoinedRoomsResponsePrefix: 'getJoinedRooms-',\n} as const;\n\nexport const buildApiResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiResponsePrefix, responseIndex);\n};\n\nexport const buildApiStreamEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiStreamPrefix, responseIndex);\n};\n\nexport const buildSyncResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncResponsePrefix, responseIndex);\n};\n\nexport const buildSyncProgressEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncProgressPrefix, responseIndex);\n};\n\nexport const buildJoinRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex);\n};\n\nexport const buildLeaveRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex);\n};\n\nexport const buildGetJoinedRoomsResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex);\n};\n","//? CSRF configuration registry. The framework's CSRF middleware\n//? (`packages/server/src/httpRoutes/csrfMiddleware.ts`) and the\n//? client-side fetch wrapper (`packages/core/src/csrf.ts`) both consult\n//? `getCsrfConfig()` so consumers can change the header name, cookie\n//? name, token length, or cookie options without forking.\n//?\n//? The default values match the framework's pre-registry hardcodes\n//? (header `x-csrf-token`, 32-byte hex token minted at session create) so\n//? consumers who never call `registerCsrfConfig` get the historical\n//? behaviour unchanged.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface CsrfCookieOptions {\n sameSite?: 'strict' | 'lax' | 'none';\n secure?: boolean;\n /**\n * MUST be false for CSRF cookies — the client needs to read the value\n * with JS to attach it as a request header. The framework reads the\n * value from the session record (Redis-side) rather than the cookie,\n * so this flag is documentation/intent only.\n */\n httpOnly?: boolean;\n path?: string;\n maxAgeMs?: number;\n}\n\nexport interface CsrfConfig {\n /** Cookie name used to deliver the CSRF token to the browser. */\n cookieName: string;\n /** Request-header name the client attaches on state-changing requests. */\n headerName: string;\n /** Length in bytes of the random token minted by the session layer. */\n tokenLength: number;\n cookieOptions: CsrfCookieOptions;\n}\n\nexport const DEFAULT_CSRF_CONFIG: CsrfConfig = {\n cookieName: 'csrf-token',\n headerName: 'x-csrf-token',\n tokenLength: 32,\n cookieOptions: {\n sameSite: 'lax',\n //? `secure` intentionally UNSET so it resolves per-environment at serialize\n //? time (csrfRoute.ts) the SAME way the session cookie does — emit `Secure` in\n //? prod (SECURE=true) but NOT over plain HTTP in dev. A hardcoded `true` made\n //? the browser drop the login-absent double-submit cookie over http://, so\n //? every state-changing request 403'd. Force it on explicitly via\n //? `registerCsrfConfig({ cookieOptions: { secure: true } })` if desired.\n httpOnly: false,\n path: '/',\n maxAgeMs: 24 * 60 * 60 * 1000,\n },\n};\n\nconst registry = createRegistry<CsrfConfig, Partial<CsrfConfig>>(DEFAULT_CSRF_CONFIG, {\n //? Merge shallowly with the CURRENT config; `cookieOptions` is deep-merged\n //? so a partial override of (say) `sameSite` does not clobber `path` /\n //? `maxAgeMs`. Last-write-wins, and successive calls accumulate.\n transform: (input, current) => ({\n ...current,\n ...input,\n cookieOptions: { ...current.cookieOptions, ...input.cookieOptions },\n }),\n});\n\n/**\n * Override one or more CSRF settings. Merges shallowly with the current\n * config; `cookieOptions` is deep-merged so a partial override of (say)\n * `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.\n */\nexport const registerCsrfConfig = (input: Partial<CsrfConfig>): void => {\n registry.register(input);\n};\n\n/** Read the active config at call time (never at module load). */\nexport const getCsrfConfig = (): CsrfConfig => registry.get();\n\n/** Test-only — restore defaults between scenarios. */\nexport const resetCsrfConfigForTests = (): void => {\n registry.reset();\n};\n","import { captureException } from './sentrySetup';\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side CSRF helper. Lazily fetches and caches the session's CSRF\n//? token from `/auth/csrf`, then attaches it as `x-csrf-token` on\n//? subsequent HTTP fetches in cookie mode.\n//?\n//? Token-mode sessions are CSRF-immune because cross-origin POSTs do not\n//? auto-attach the sessionStorage value; the cache simply stays empty in\n//? that mode.\n\nimport { getCsrfConfig } from './csrfConfig';\nimport { getProjectConfig } from './projectConfig';\nimport { socket } from './socketState';\nimport tryCatch from './tryCatch';\nimport tryCatchSync from './tryCatchSync';\n\n//? Tokens are scoped to the HTTP invocation origin. In routed-http mode the\n//? Socket.io connection may terminate on a remote `system` service while the\n//? invocation goes same-origin through a local router; a single socket-derived\n//? cache would fetch the CSRF token from the wrong ingress.\nconst cachedTokens = new Map<string, string>();\nconst inflightFetches = new Map<string, Promise<string | null>>();\n\nconst resolveSocketOrigin = (): string => socket?.io.opts.hostname\n ? `${socket.io.opts.secure ? 'https' : 'http'}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ''}`\n : '';\n\nconst resolveRequestOrigin = (input?: RequestInfo | URL): string => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const browserOrigin = globalThis.window !== undefined && globalThis.location?.origin\n ? globalThis.location.origin\n : '';\n if (input !== undefined) {\n let raw: string;\n if (typeof input === 'string') raw = input;\n else if (input instanceof URL) raw = input.href;\n else raw = input.url;\n const fallbackOrigin = browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n const [error, parsed] = tryCatchSync(() => new URL(raw, fallbackOrigin));\n if (!error && parsed) return parsed.origin;\n }\n return browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n};\n\nconst fetchCsrfToken = async (origin: string): Promise<string | null> => {\n const base = origin;\n const [error, token] = await tryCatch<string | null, undefined>(async () => {\n const response = await fetch(`${base}/auth/csrf`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!response.ok) return null;\n const body = (await response.json()) as { csrfToken?: string | null };\n return body.csrfToken ?? null;\n });\n if (error) return null;\n return token ?? null;\n};\n\n/**\n * Fetch (or return the cached) CSRF token for the active session. Returns\n * null in token mode or when no session exists. Callers should attach the\n * returned value as the `X-CSRF-Token` request header.\n */\nexport const getCsrfToken = async (origin = resolveRequestOrigin()): Promise<string | null> => {\n //? Token-mode sessions: skip entirely. The server doesn't enforce CSRF\n //? in token mode, and there's no value to attach.\n if (getProjectConfig().session.basedToken) {\n return null;\n }\n\n const cached = cachedTokens.get(origin);\n if (cached) return cached;\n const inflight = inflightFetches.get(origin);\n if (inflight) return inflight;\n\n const next = fetchCsrfToken(origin).then((token) => {\n if (token) cachedTokens.set(origin, token);\n inflightFetches.delete(origin);\n return token;\n });\n inflightFetches.set(origin, next);\n return next;\n};\n\n/** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */\nexport const clearCsrfToken = (): void => {\n cachedTokens.clear();\n inflightFetches.clear();\n};\n\nconst STATE_CHANGING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\nconst isCsrfMismatchResponse = async (response: Response): Promise<boolean> => {\n if (response.status !== 403) return false;\n const [error, isMismatch] = await tryCatch<boolean, undefined>(async () => {\n //? `response.clone()` is used here so the ORIGINAL response body stream is\n //? preserved for the caller (the clone is consumed, the original is not).\n //? This is important: `httpFetch` returns the original `response` reference\n //? to the caller so it can read its body, and cloning before reading is the\n //? correct way to peek at the body without consuming it.\n const cloned = response.clone();\n const body = (await cloned.json()) as { errorCode?: string };\n return body.errorCode === 'auth.csrfMismatch';\n });\n if (error) return false;\n return isMismatch ?? false;\n};\n\n/**\n * Drop-in replacement for `fetch` that automatically attaches the framework's\n * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in\n * cookie mode. Use this for any project-side HTTP call that hits a\n * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.\n *\n * Behavior:\n * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).\n * - Token mode: passes through without CSRF (immune by design).\n * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,\n * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`\n * response, clears the cache and retries once.\n * - `credentials: 'include'` is forced unless the caller overrode it.\n *\n * @example\n * import { httpFetch } from '@luckystack/core/client';\n * const res = await httpFetch('/api/system/myRoute/v1', {\n * method: 'POST',\n * body: JSON.stringify({ ... }),\n * });\n */\nexport const httpFetch: typeof fetch = async (input, init = {}) => {\n const method = (init.method ?? 'GET').toUpperCase();\n const headers = new Headers(init.headers);\n const config = getProjectConfig();\n if (config.session.basedToken && !headers.has('Authorization')) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const token = globalThis.window === undefined\n ? null\n : tryCatchSync(() => globalThis.sessionStorage.getItem('token'))[1];\n if (token) headers.set('Authorization', `Bearer ${token}`);\n }\n if (!headers.has('Content-Type') && init.body && typeof init.body === 'string') {\n headers.set('Content-Type', 'application/json');\n }\n\n const csrfHeaderName = getCsrfConfig().headerName;\n const send = async (csrfToken: string | null): Promise<Response> => {\n const finalHeaders = new Headers(headers);\n if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {\n finalHeaders.set(csrfHeaderName, csrfToken);\n }\n return fetch(input, {\n ...init,\n method,\n headers: finalHeaders,\n credentials: init.credentials ?? 'include',\n });\n };\n\n if (!STATE_CHANGING.has(method) || config.session.basedToken) {\n return send(null);\n }\n\n const requestOrigin = resolveRequestOrigin(input);\n const token = await getCsrfToken(requestOrigin);\n let response = await send(token);\n\n //? On csrfMismatch, the cached token is stale (session rotated). Clear and\n //? retry ONCE with a fresh fetch — covers the case where the user logged\n //? back in while a tab was idle. The retry is intentionally limited to one\n //? attempt: a second csrfMismatch means the session is broken in a way a\n //? fresh token won't fix (e.g. session expired), and retrying would loop.\n if (await isCsrfMismatchResponse(response)) {\n clearCsrfToken();\n const refreshed = await getCsrfToken(requestOrigin);\n if (refreshed) {\n response = await send(refreshed);\n }\n }\n\n return response;\n};\n","export default function sleep(ms: number) {\n return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","//? Pure page-route validator. Lives in @luckystack/core (no Node deps) so\n//? both the client-side router (`src/main.tsx`'s `getRoutes()`) AND the\n//? devkit-side scaffold CLI / hot-reload validator can consume it through\n//? the same single source of truth.\n//?\n//? Implements the invisible-parent folder convention: a folder starting\n//? with `privateFolderPrefix` is stripped from the URL but its CHILDREN\n//? remain routeable. A `page.tsx` placed directly inside such a folder\n//? (no non-private sibling segment) is invalid because it has no URL.\n//?\n//? Reserved framework folders (`_api`, `_sync`, `_components`, …) NEVER\n//? host a page, even nested — placing `page.tsx` anywhere under one of\n//? them is a hard error.\n\nexport interface PageRouteRules {\n /**\n * Single-character prefix that marks a folder as **invisible-parent** for\n * page routing. Default: `'_'`.\n */\n privateFolderPrefix: string;\n /**\n * Folder names that are reserved for framework-internal use and may NEVER\n * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit\n * a hard error when a page is placed inside one.\n */\n scaffoldIgnoredFolders: readonly string[];\n}\n\nexport const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules = {\n privateFolderPrefix: '_',\n scaffoldIgnoredFolders: [\n '_api',\n '_sync',\n '_function',\n '_functions',\n '_component',\n '_components',\n '_provider',\n '_providers',\n '_locale',\n '_locales',\n '_socket',\n '_sockets',\n '_shared',\n '_server',\n ],\n};\n\nexport interface PagePathValidationResult {\n /** `true` when the page file produces a routeable URL. */\n valid: boolean;\n /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */\n route?: string;\n /** Human-readable explanation when `!valid`. */\n reason?: string;\n}\n\n/**\n * Validate a `page.tsx` file path relative to `src/`. Accepts both\n * forward and back slashes, with or without a leading `./` or `/`.\n * The filename **must** be `page.tsx` (or `page.jsx`); other filenames\n * return `valid: false`.\n *\n * Examples (defaults):\n * admin/page.tsx -> { valid: true, route: '/admin' }\n * _housing/renting/page.tsx -> { valid: true, route: '/renting' }\n * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }\n * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }\n * page.tsx -> { valid: true, route: '/' }\n * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }\n */\nexport const validatePagePath = (\n srcRelativePath: string,\n rules: PageRouteRules = DEFAULT_PAGE_ROUTE_RULES,\n): PagePathValidationResult => {\n const normalized = srcRelativePath.replaceAll('\\\\', '/').replace(/^\\.\\//, '').replace(/^\\/+/, '');\n const segments = normalized.split('/').filter((segment) => segment.length > 0);\n\n if (segments.length === 0) {\n return { valid: false, reason: 'empty path' };\n }\n\n const filename = segments.at(-1);\n if (filename !== 'page.tsx' && filename !== 'page.jsx') {\n return { valid: false, reason: 'not a page file (expected page.tsx)' };\n }\n\n const folderSegments = segments.slice(0, -1);\n const prefix = rules.privateFolderPrefix;\n const reserved = rules.scaffoldIgnoredFolders;\n\n for (const reservedName of reserved) {\n if (folderSegments.includes(reservedName)) {\n return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };\n }\n }\n\n const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));\n\n if (folderSegments.length > 0 && visibleSegments.length === 0) {\n return { valid: false, reason: 'no URL segment left after stripping underscore folders' };\n }\n\n const route = visibleSegments.length === 0 ? '/' : `/${visibleSegments.join('/')}`;\n return { valid: true, route };\n};\n"],"mappings":"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","names":["registry","responseIndex","registry","token"]}
1
+ {"version":3,"sources":["../src/apiMethodMapRegistry.ts","../src/httpApiUtils.ts","../src/socketState.ts","../src/notifier.ts","../src/tryCatchSync.ts","../src/clientHookBus.ts","../src/offlineQueue.ts","../src/responseNormalizer.ts","../src/serviceRoute.ts","../src/socketEvents.ts","../src/csrfConfig.ts","../src/tryCatch.ts","../src/csrf.ts","../src/sleep.ts","../src/pageRouteValidation.ts"],"sourcesContent":["//? Generated `apiMethodMap` registry. Replaces the prefix-heuristic\n//? in `isGetMethod` (apiRequest.ts) so abort-controller selection uses\n//? the actual HTTP method declared on the handler instead of guessing\n//? from the name prefix.\n//?\n//? Wiring: the project's `src/_sockets/apiRequest.ts` wrapper imports the\n//? generated runtime map and calls `registerApiMethodMap(apiMethodMap)` once at\n//? module load, immediately before re-exporting the core request helper. Until then,\n//? lookups return `undefined` and `isGetMethod` falls back to the\n//? prefix heuristic (preserving previous behavior for transitional code).\n\nexport type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\nexport type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;\n\nimport { createRegistry } from './createRegistry';\n\nconst registry = createRegistry<ApiMethodMap | null>(null);\n\nexport const registerApiMethodMap = (map: ApiMethodMap): void => {\n registry.register(map);\n};\n\nexport const getRegisteredApiMethod = (\n pagePath: string,\n apiName: string,\n version: string,\n): HttpMethodLiteral | undefined => {\n const activeMap = registry.get();\n if (!activeMap) return undefined;\n //? The map shape is `Record<page, Record<name, Record<version, method>>>`;\n //? TS treats each indexed access as defined (no `noUncheckedIndexedAccess`),\n //? so the optional chains look redundant. They're not — a missing key returns\n //? undefined at runtime, which is the contract this function is documenting.\n /* eslint-disable @typescript-eslint/no-unnecessary-condition -- runtime-defensive index access */\n return activeMap[pagePath]?.[apiName]?.[version];\n /* eslint-enable @typescript-eslint/no-unnecessary-condition */\n};\n\nexport const isApiMethodMapRegistered = (): boolean => registry.isRegistered();\n","/**\n * HTTP API Utilities\n * \n * Helpers for HTTP API request handling including:\n * - Smart HTTP method detection based on API name\n * - Method validation\n */\n\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * Single source of truth for the GET-prefix naming heuristic\n * (`get` / `fetch` / `list`). Used by both `inferHttpMethod` here and the\n * client transport's `isGetMethod` fallback in `apiRequest.ts`, so the two\n * prefix heuristics can't drift apart. Expects an already-lowercased leaf name.\n */\nexport const isGetMethodName = (methodNameLower: string): boolean =>\n methodNameLower.startsWith('get') ||\n methodNameLower.startsWith('fetch') ||\n methodNameLower.startsWith('list');\n\n/**\n * Infer the HTTP method from an API name based on common naming conventions.\n * \n * - Names starting with \"get\" → GET\n * - Names starting with \"delete\" or \"remove\" → DELETE\n * - Names starting with \"update\" or \"edit\" → PUT\n * - Everything else → POST (create operations, actions)\n * \n * @example\n * ```typescript\n * inferHttpMethod('getUserData') // → 'GET'\n * inferHttpMethod('deleteAccount') // → 'DELETE'\n * inferHttpMethod('updateProfile') // → 'PUT'\n * inferHttpMethod('createUser') // → 'POST'\n * inferHttpMethod('sendEmail') // → 'POST'\n * ```\n */\nexport const inferHttpMethod = (apiName: string): HttpMethod => {\n // Extract the method name from the full path (e.g., \"api/examples/getUserData\" → \"getUserData\")\n const nameParts = apiName.split('/');\n const lastPart = nameParts.at(-1)?.toLowerCase() ?? '';\n const methodName = /^v\\d+$/.test(lastPart)\n ? (nameParts.at(-2) ?? '').toLowerCase()\n : lastPart;\n\n if (isGetMethodName(methodName)) {\n return 'GET';\n }\n\n if (methodName.startsWith('delete') || methodName.startsWith('remove')) {\n return 'DELETE';\n }\n\n if (methodName.startsWith('update') || methodName.startsWith('edit') || methodName.startsWith('patch')) {\n return 'PUT';\n }\n\n // Default: POST for create operations and general actions\n return 'POST';\n};\n\n/**\n * Get the effective HTTP method for an API.\n * Uses explicit export if available, otherwise infers from name.\n */\nexport const getEffectiveHttpMethod = (\n apiConfig: { httpMethod?: HttpMethod },\n apiName: string\n): HttpMethod => {\n return apiConfig.httpMethod ?? inferHttpMethod(apiName);\n};\n\n/**\n * Check if an HTTP method is valid for an API.\n *\n * @param requestMethod - The method from the HTTP request\n * @param allowedMethod - The method the API accepts\n * @returns true only when the method matches the route's allowed method.\n *\n * NOTE: `OPTIONS` is intentionally NOT treated as allowed here. A custom-route\n * author wiring this into their dispatch must answer CORS preflights BEFORE the\n * route check — letting `OPTIONS` through would execute the handler on a request\n * the CSRF middleware treats as non-state-changing (CSRF-exempt), opening a\n * bypass on method-locked routes.\n */\nexport const isMethodAllowed = (\n requestMethod: string,\n allowedMethod: HttpMethod\n): boolean => {\n return requestMethod === allowedMethod;\n};\n","import type { Socket } from 'socket.io-client';\n\n// Shared, mutable socket-client state. Single source of truth for the socket\n// instance, consumed by transport helpers in @luckystack/core (apiRequest)\n// and @luckystack/sync (syncRequest), and written by the project's React\n// hook wiring in `src/_sockets/socketInitializer.ts` via `setSocket(io(...))`.\n//\n// The `export let socket` form gives importers a live binding — they always\n// see the current value after `setSocket` replaces it.\n\n// eslint-disable-next-line import-x/no-mutable-exports\nexport let socket: Socket | null = null;\n\n//? One-shot waiters: promises that resolve the moment a non-null socket is\n//? set. Replacing the original `setInterval` poll (10 ms × 500 iterations)\n//? with a subscribe-and-notify pattern eliminates the busy-loop overhead and\n//? guarantees sub-tick delivery rather than up-to-10 ms latency.\nlet waiters: ((s: Socket) => void)[] = [];\n\n//? Absolute deadline for `waitForSocket` — prevents a permanently-dangling\n//? promise in environments where the socket is never created (SSR, tests).\nconst WAIT_FOR_SOCKET_TIMEOUT_MS = 5000;\n\nexport const setSocket = (next: Socket | null): void => {\n socket = next;\n //? Resolve all pending waiters the moment a real socket arrives.\n if (next) {\n const pending = waiters;\n waiters = [];\n for (const resolve of pending) {\n resolve(next);\n }\n }\n};\n\nlet responseIndex = 0;\n\nexport const incrementResponseIndex = (): number => {\n responseIndex = responseIndex + 1;\n return responseIndex;\n};\n\nexport const waitForSocket = (): Promise<Socket | null> => {\n if (socket) return Promise.resolve(socket);\n return new Promise<Socket | null>((resolve) => {\n const timer = setTimeout(() => {\n //? Time out: remove our waiter slot and resolve with null so the\n //? caller surfaces a \"socket unavailable\" path instead of hanging.\n waiters = waiters.filter((w) => w !== onSocket);\n resolve(null);\n }, WAIT_FOR_SOCKET_TIMEOUT_MS);\n\n const onSocket = (s: Socket): void => {\n clearTimeout(timer);\n resolve(s);\n };\n\n waiters.push(onSocket);\n });\n};\n","//? Client-side notifier DI. Framework packages (apiRequest, syncRequest,\n//? offline queue flush) emit toasts on transport errors. Default is silent\n//? (no-op) so tests and SSR contexts don't need a toaster installed.\n//?\n//? The project's entry point registers a notifier backed by its UI toolkit\n//? (sonner + i18n) via `registerNotifier({ error, success, ... })`.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface NotifyParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface NotifyInput {\n key: string;\n params?: NotifyParam[];\n}\n\nexport interface Notifier {\n success: (input: NotifyInput) => void;\n error: (input: NotifyInput) => void;\n info: (input: NotifyInput) => void;\n warning: (input: NotifyInput) => void;\n}\n\nconst noopNotifier: Notifier = {\n success: () => { /* no-op */ },\n error: () => { /* no-op */ },\n info: () => { /* no-op */ },\n warning: () => { /* no-op */ },\n};\n\nconst registry = createRegistry<Notifier>(noopNotifier);\n\nexport const registerNotifier = (notifier: Notifier): void => {\n registry.register(notifier);\n};\n\nexport const getNotifier = (): Notifier => registry.get();\n\n//? Delegating wrapper matching the project's existing `notify` shape so the\n//? switchover inside framework packages is a one-line import change.\nexport const notify: Notifier = {\n success: (input) => { registry.get().success(input); },\n error: (input) => { registry.get().error(input); },\n info: (input) => { registry.get().info(input); },\n warning: (input) => { registry.get().warning(input); },\n};\n","//? Synchronous sibling of `tryCatch`. The async `tryCatch` returns a Promise\n//? and auto-captures to the error tracker — neither of which fits a hot,\n//? genuinely-synchronous path (error-formatter dispatch, cookie/CSRF parse)\n//? that must stay synchronous and decides its own logging.\n//?\n//? Unlike the async `tryCatch`, this helper does NOT auto-capture to the\n//? error tracker: the call sites that need it are synchronous fallbacks that\n//? already do their own contextual logging and deliberately keep the error\n//? path side-effect-light. Capture explicitly on the error tuple when needed.\n\n//? `P = void` mirrors the async `tryCatch<T, P = void>` so a params-less call\n//? can pass just the result type: `tryCatchSync<URL>(() => new URL(raw))`.\n//? Without the default TS demands both type args (TS2558).\nexport default function tryCatchSync<T, P = void>(\n func: (values: P) => T,\n params?: P,\n): [Error | null, T | null] {\n try {\n const response = func(params as P);\n return [null, response];\n } catch (error) {\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side hook bus. Counterpart to the server-side `registerHook` /\n//? `dispatchHook` system in `hookRegistry.ts`, but lives here in the\n//? browser-safe surface and listens for client-side lifecycle moments:\n//? session login, session logout (and room for `sessionUpdate` later).\n//?\n//? Fired from `react/sessionContext.ts` when `setLatestSession` observes\n//? a null → non-null (postLogin) or non-null → null (postLogout) transition.\n//? That makes the fire-point framework-controlled — consumers don't have to\n//? add their own useEffect in `SessionProvider` to detect these moments.\n//?\n//? Handlers are best-effort: errors are caught + logged but don't break the\n//? session pipeline. Async handlers fire-and-forget; the caller of\n//? `setLatestSession` does not await.\n//?\n//? Typical use:\n//? import { registerClientHook } from '@luckystack/core/client';\n//?\n//? registerClientHook('postLogin', ({ session }) => {\n//? const letter = session.name?.[0]?.toLowerCase();\n//? if (letter) joinRoom(`letter-${letter}`);\n//? });\n//?\n//? Consumers with extended session shapes get narrow typing via the same\n//? `BaseSessionLayout` module augmentation pattern the rest of the framework\n//? uses (see ARCHITECTURE_EXTENSION_POINTS.md `@luckystack/login` section).\n\nimport type { BaseSessionLayout } from './sessionTypes';\nimport { getLogger } from './loggerRegistry';\n\nexport interface ClientHookPayloadMap {\n //? Fires BEFORE the framework commits a null → session transition.\n //? Handlers may return a `ClientHookStopSignal` to veto the login (e.g.\n //? account suspended since last visit, feature-flag gate, geo block).\n //? Only fires when the consumer commits the transition through the\n //? vetoable entry point — see `proposeLogin` in `react/sessionContext.ts`.\n preLogin: { candidateSession: BaseSessionLayout };\n postLogin: { session: BaseSessionLayout };\n postLogout: { previousSession: BaseSessionLayout };\n //? Fires when the offline queue drops a request (full queue with a\n //? `drop-oldest`/`drop-newest`/`reject` policy, or an item past `maxAgeMs`).\n //? The in-memory queue is not durable across refreshes; this is the seam to\n //? surface a \"your change wasn't sent\" toast or to persist the item\n //? elsewhere. `queue` says which queue, `reason` why it was dropped.\n queueItemDropped: {\n queue: 'api' | 'sync';\n key: string;\n reason: 'queue-full' | 'expired';\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject';\n };\n}\n\nexport type ClientHookName = keyof ClientHookPayloadMap;\n\n//? Stop signal returned by a `pre*` client hook handler to veto the\n//? framework's main flow. Mirrors the server-side `HookStopSignal` shape so\n//? `errorCode` flows through the same i18n / responseNormalizer pipeline a\n//? consumer already wires up.\nexport interface ClientHookStopSignal {\n stop: true;\n errorCode: string;\n}\n\n// eslint-disable-next-line @typescript-eslint/no-invalid-void-type -- mirrors server-side HookResult, see hooks/types.ts\nexport type ClientHookResult = void | undefined | ClientHookStopSignal;\n\nexport type ClientDispatchResult =\n | { stopped: false }\n | { stopped: true; signal: ClientHookStopSignal };\n\nexport type ClientHookHandler<N extends ClientHookName> = (\n payload: ClientHookPayloadMap[N],\n) => ClientHookResult | Promise<ClientHookResult>;\n\n//? Storage uses `unknown` handler sets so the generic `N` doesn't have to\n//? narrow at every access — the cast happens once at read/write inside the\n//? typed wrappers below, which is sound because each hook name's Set only\n//? ever sees handlers registered under that same name.\ntype AnyHandler = (payload: unknown) => ClientHookResult | Promise<ClientHookResult>;\nconst handlerSets: Partial<Record<ClientHookName, Set<AnyHandler>>> = {};\n\n/**\n * Subscribe to a client-side lifecycle hook. Returns an unsubscribe function\n * — call it to remove the handler (e.g. inside a React effect cleanup).\n *\n * Multiple handlers can be registered for the same hook; they all fire on\n * each dispatch. A handler that throws (sync) or rejects (async) is logged\n * to `console.error` and does not affect other handlers.\n */\nexport function registerClientHook<N extends ClientHookName>(\n name: N,\n handler: ClientHookHandler<N>,\n): () => void {\n const existing = handlerSets[name];\n const set = existing ?? new Set<AnyHandler>();\n if (!existing) handlerSets[name] = set;\n const wrapped = handler as AnyHandler;\n set.add(wrapped);\n return () => {\n set.delete(wrapped);\n };\n}\n\n/**\n * Fire a client-side hook (fire-and-forget). Framework-internal — consumers\n * should not call this directly. Used for `post*` hooks where any stop-signal\n * returned by a handler is intentionally ignored: a `postLogin` listener that\n * tried to \"stop\" the framework would be too late — the transition already\n * happened. Use `dispatchVetoableClientHook` for `pre*` hooks where the\n * caller actually awaits the veto decision.\n */\nexport function dispatchClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): void {\n const set = handlerSets[name];\n if (!set || set.size === 0) return;\n //? Snapshot the set before iterating so a handler that unregisters itself\n //? (or a sibling) mid-dispatch doesn't skip un-visited handlers — Set\n //? iteration honors mid-iter deletes per ECMAScript spec, which we don't\n //? want here.\n const handlers = [...set];\n for (const handler of handlers) {\n try {\n const result = handler(payload);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error(`[clientHook:${name}] async handler rejected`, error);\n });\n }\n } catch (error) {\n getLogger().error(`[clientHook:${name}] handler threw`, error);\n }\n }\n}\n\n/**\n * Awaits every registered handler for a `pre*` client hook. If any handler\n * returns a `ClientHookStopSignal`, dispatch short-circuits and returns\n * `{ stopped: true, signal }`; callers must respect the veto (skip the\n * pending action). Handlers that throw or reject are logged but treated as\n * `undefined` — a buggy handler must not silently block the user flow.\n *\n * Mirrors `dispatchHook` from `@luckystack/core` so consumer/framework code\n * can use the same stop-signal pattern on both sides of the wire.\n */\nexport async function dispatchVetoableClientHook<N extends ClientHookName>(\n name: N,\n payload: ClientHookPayloadMap[N],\n): Promise<ClientDispatchResult> {\n const set = handlerSets[name];\n if (!set || set.size === 0) return { stopped: false };\n //? Snapshot the set (see `dispatchClientHook` for why).\n const handlers = [...set];\n for (const handler of handlers) {\n let result: ClientHookResult;\n try {\n result = await handler(payload);\n } catch (error) {\n getLogger().error(`[clientHook:${name}] vetoable handler threw`, error);\n continue;\n }\n if (result?.stop) {\n return { stopped: true, signal: result };\n }\n }\n return { stopped: false };\n}\n\n/** Test helper — drop every registered handler. Not part of the public API. */\nexport function _resetClientHooksForTests(): void {\n for (const key of Object.keys(handlerSets) as ClientHookName[]) {\n handlerSets[key]?.clear();\n }\n}\n","import { Socket } from \"socket.io-client\";\nimport { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport { dispatchClientHook } from \"./clientHookBus\";\n\n/** Why a queued item was dropped (handed to its `onDrop` callback). */\nexport type QueueDropReason = 'expired' | 'queue-full';\n\ninterface ApiQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. See\n * `SyncQueueItem.dropPolicy` for details.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /**\n * Optional callback invoked when THIS item is dropped (evicted for age or\n * because the queue was full / rejected) instead of ever running (SYNC-09).\n * The transport wires this to settle the pending `syncRequest`/`apiRequest`\n * promise with an error envelope, so an evicted offline request resolves\n * rather than hanging forever. Invoked at most once; failures are swallowed.\n */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\ninterface SyncQueueItem {\n id: string;\n key: string;\n run: (socketInstance: Socket) => void;\n createdAt: number;\n /**\n * Per-request override of `projectConfig.offlineQueue.dropPolicy`. Lets a\n * specific sync (\"editor cursor move\") pick `'drop-oldest'` while the app\n * default stays `'reject'` for safer sends. When omitted, falls back to\n * the global config.\n */\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n /** See {@link ApiQueueItem.onDrop} (SYNC-09). */\n onDrop?: (reason: QueueDropReason) => void;\n}\n\nconst apiQueue: ApiQueueItem[] = [];\nconst syncQueue: SyncQueueItem[] = [];\n\n//? Independent flush guards so a long api flush doesn't no-op a concurrent\n//? sync flush (or vice versa).\nlet isFlushingApi = false;\nlet isFlushingSync = false;\n\nexport const isOnline = () => {\n if (typeof navigator === \"undefined\") return true;\n return navigator.onLine;\n};\n\n//? Common drop shape — every queue item the framework drops carries these.\ninterface DroppableItem {\n createdAt: number;\n key: string;\n dropPolicy?: 'drop-oldest' | 'drop-newest' | 'reject';\n onDrop?: (reason: QueueDropReason) => void;\n}\n\n//? Notify a dropped item's owner exactly once. Swallows callback errors so a\n//? throwing `onDrop` can't wedge the eviction/enqueue loop (SYNC-09).\nconst notifyDropped = (item: DroppableItem, reason: QueueDropReason): void => {\n if (!item.onDrop) return;\n tryCatchSync(() => { item.onDrop?.(reason); });\n};\n\n//? Dispatch the `queueItemDropped` client hook, guarded so a misbehaving\n//? handler set can't interrupt the eviction/enqueue loop (CORE-N8).\nconst safeDispatchDropHook = (\n queue: 'api' | 'sync',\n key: string,\n reason: QueueDropReason,\n dropPolicy: 'drop-oldest' | 'drop-newest' | 'reject',\n): void => {\n tryCatchSync(() => {\n dispatchClientHook('queueItemDropped', { queue, key, reason, dropPolicy });\n });\n};\n\nconst evictExpired = (\n queue: DroppableItem[],\n label: 'api' | 'sync',\n): void => {\n const { maxAgeMs, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n if (maxAgeMs <= 0) return;\n const cutoff = Date.now() - maxAgeMs;\n for (let i = queue.length - 1; i >= 0; i -= 1) {\n const item = queue[i];\n if (item && item.createdAt < cutoff) {\n queue.splice(i, 1);\n notifyDropped(item, 'expired');\n safeDispatchDropHook(label, item.key, 'expired', item.dropPolicy ?? globalPolicy);\n }\n }\n};\n\nconst enqueueWithPolicy = <T extends DroppableItem>(\n queue: T[],\n item: T,\n label: 'api' | 'sync',\n): boolean => {\n evictExpired(queue, label);\n const { maxSize, dropPolicy: globalPolicy } = getProjectConfig().offlineQueue;\n //? Per-request override wins over the global policy. Lets specific\n //? syncs pick 'drop-oldest' for liveness while the app default stays\n //? 'reject' for safer sends.\n const effectivePolicy = item.dropPolicy ?? globalPolicy;\n if (queue.length < maxSize) {\n queue.push(item);\n return true;\n }\n if (effectivePolicy === 'drop-oldest') {\n const dropped = queue.shift();\n queue.push(item);\n getLogger().debug(`offlineQueue:${label} full — dropped oldest item (policy=${effectivePolicy})`);\n if (dropped) {\n notifyDropped(dropped, 'queue-full');\n safeDispatchDropHook(label, dropped.key, 'queue-full', effectivePolicy);\n }\n return true;\n }\n if (effectivePolicy === 'drop-newest') {\n getLogger().debug(`offlineQueue:${label} full — dropped newest item (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n }\n getLogger().warn(`offlineQueue:${label} full — rejecting enqueue (policy=${effectivePolicy})`);\n notifyDropped(item, 'queue-full');\n safeDispatchDropHook(label, item.key, 'queue-full', effectivePolicy);\n return false;\n};\n\nexport const enqueueApiRequest = (item: ApiQueueItem): boolean =>\n enqueueWithPolicy(apiQueue, item, 'api');\n\nexport const enqueueSyncRequest = (item: SyncQueueItem): boolean =>\n enqueueWithPolicy(syncQueue, item, 'sync');\n\nexport const removeApiQueueItem = (id: string) => {\n const index = apiQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n apiQueue.splice(index, 1);\n }\n};\n\nexport const removeSyncQueueItem = (id: string) => {\n const index = syncQueue.findIndex((item) => item.id === id);\n if (index !== -1) {\n syncQueue.splice(index, 1);\n }\n};\n\nexport const removeApiQueueItemsByKey = (key: string) => {\n for (let i = apiQueue.length - 1; i >= 0; i -= 1) {\n if (apiQueue[i]?.key === key) {\n apiQueue.splice(i, 1);\n }\n }\n};\n\n//? Run one dequeued item. If `run` throws, requeue it at the FRONT (so the\n//? next flush retries it in order) and signal the caller to stop this flush —\n//? otherwise a throwing item is silently lost AND the `isFlushing*` guard\n//? would be left stuck `true`, wedging every future flush.\nconst runQueueItem = <T extends { run: (s: Socket) => void }>(\n queue: T[],\n item: T,\n socketInstance: Socket,\n label: string,\n): boolean => {\n const [error] = tryCatchSync(() => { item.run(socketInstance); });\n if (error) {\n queue.unshift(item);\n getLogger().warn(`offlineQueue:${label} run threw — requeued item, pausing flush`, { error });\n return false;\n }\n return true;\n};\n\nexport const flushApiQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingApi) return;\n evictExpired(apiQueue, 'api');\n if (apiQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingApi = true;\n while (apiQueue.length > 0) {\n if (!canRun()) break;\n const item = apiQueue.shift();\n if (item && !runQueueItem(apiQueue, item, socketInstance, 'api')) break;\n }\n isFlushingApi = false;\n};\n\nexport const flushSyncQueue = (canRun: () => boolean, socketInstance: Socket) => {\n if (isFlushingSync) return;\n evictExpired(syncQueue, 'sync');\n if (syncQueue.length === 0) return;\n if (!canRun()) return;\n\n isFlushingSync = true;\n while (syncQueue.length > 0) {\n if (!canRun()) break;\n const item = syncQueue.shift();\n if (item && !runQueueItem(syncQueue, item, socketInstance, 'sync')) break;\n }\n isFlushingSync = false;\n};\n\nexport const getApiQueueSize = () => apiQueue.length;\nexport const getSyncQueueSize = () => syncQueue.length;\n","export interface ErrorParam {\n key: string;\n value: string | number | boolean;\n}\n\nexport interface ErrorResponseInput {\n status?: unknown;\n errorCode?: unknown;\n errorParams?: unknown;\n httpStatus?: unknown;\n}\n\nexport interface NormalizedErrorResponse {\n status: 'error';\n message: string;\n errorCode: string;\n errorParams?: ErrorParam[];\n httpStatus?: number;\n}\n\nexport const INVALID_ERROR_RESPONSE_CODE = 'error.invalidResponse';\n\nexport const isErrorParamArray = (value: unknown): value is ErrorParam[] => {\n return Array.isArray(value) && value.every((item) => {\n return (\n !!item\n && typeof item === 'object'\n && typeof (item as ErrorParam).key === 'string'\n && ['string', 'number', 'boolean'].includes(typeof (item as ErrorParam).value)\n );\n });\n};\n\nexport const normalizeErrorResponseCore = ({\n response,\n fallbackHttpStatus,\n fallbackErrorCode,\n resolveMessage,\n}: {\n response: ErrorResponseInput;\n fallbackHttpStatus?: number;\n fallbackErrorCode?: string;\n resolveMessage?: (args: { errorCode: string; errorParams?: ErrorParam[] }) => string;\n}): NormalizedErrorResponse => {\n const normalizedParams = isErrorParamArray(response.errorParams) ? response.errorParams : undefined;\n const errorCodeValue = typeof response.errorCode === 'string' ? response.errorCode.trim() : '';\n const hasErrorCode = errorCodeValue.length > 0;\n const finalErrorCode = hasErrorCode ? errorCodeValue : (fallbackErrorCode ?? INVALID_ERROR_RESPONSE_CODE);\n const providedHttpStatus = typeof response.httpStatus === 'number' ? response.httpStatus : undefined;\n\n const message = resolveMessage\n ? resolveMessage({ errorCode: finalErrorCode, errorParams: normalizedParams })\n : finalErrorCode;\n\n return {\n status: 'error',\n message,\n errorCode: finalErrorCode,\n errorParams: normalizedParams,\n httpStatus: providedHttpStatus ?? fallbackHttpStatus,\n };\n};\n\nexport const defaultHttpStatusForResponse = ({\n status,\n explicitHttpStatus,\n fallbackErrorStatus = 400,\n}: {\n status: 'success' | 'error';\n explicitHttpStatus?: number;\n fallbackErrorStatus?: number;\n}): number => {\n if (typeof explicitHttpStatus === 'number') return explicitHttpStatus;\n return status === 'success' ? 200 : fallbackErrorStatus;\n};\n","const VERSION_SEGMENT_REGEX = /^v\\d+$/;\n\nexport interface ServiceRouteParseSuccess {\n status: 'success';\n normalizedRouteName: string;\n service: string;\n routeName: string;\n}\n\nexport interface ServiceRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type ServiceRouteParseResult = ServiceRouteParseSuccess | ServiceRouteParseError;\n\nexport interface TransportRouteParseSuccess {\n status: 'success';\n normalizedFullName: string;\n version: string;\n serviceRoute: ServiceRouteParseSuccess;\n}\n\nexport interface TransportRouteParseError {\n status: 'error';\n reason: string;\n}\n\nexport type TransportRouteParseResult = TransportRouteParseSuccess | TransportRouteParseError;\n\nconst normalizeRawRoute = (value: string): string => {\n return value.trim().replaceAll('\\\\', '/').replaceAll(/^\\/+/g, '').replaceAll(/\\/+$/g, '');\n};\n\nconst hasEmptySegments = (value: string): boolean => {\n return value.split('/').some((segment) => segment.length === 0);\n};\n\nexport const parseServiceRouteName = (value: string): ServiceRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Route name cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Route name cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n if (segments.length < 2) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n const [service, ...routeSegments] = segments;\n const routeName = routeSegments.join('/');\n\n if (!service || !routeName) {\n return { status: 'error', reason: 'Route name must include service and route segments.' };\n }\n\n return {\n status: 'success',\n normalizedRouteName: `${service}/${routeName}`,\n service,\n routeName,\n };\n};\n\nexport const parseTransportRouteName = ({\n value,\n prefix,\n}: {\n value: string;\n prefix: 'api' | 'sync';\n}): TransportRouteParseResult => {\n const normalized = normalizeRawRoute(value);\n if (!normalized) {\n return { status: 'error', reason: 'Transport route cannot be empty.' };\n }\n\n if (hasEmptySegments(normalized)) {\n return { status: 'error', reason: 'Transport route cannot contain empty segments.' };\n }\n\n const segments = normalized.split('/');\n const routeSegments = segments[0] === prefix ? segments.slice(1) : segments;\n\n if (routeSegments.length < 3) {\n return {\n status: 'error',\n reason: `Transport route must use ${prefix}/{service}/{name}/{version} format.`,\n };\n }\n\n const version = routeSegments.at(-1);\n if (!version || !VERSION_SEGMENT_REGEX.test(version)) {\n return { status: 'error', reason: 'Route version segment must match v{number}.' };\n }\n\n const serviceRouteToken = routeSegments.slice(0, -1).join('/');\n const serviceRoute = parseServiceRouteName(serviceRouteToken);\n if (serviceRoute.status === 'error') {\n return {\n status: 'error',\n reason: serviceRoute.reason,\n };\n }\n\n return {\n status: 'success',\n normalizedFullName: `${prefix}/${serviceRoute.normalizedRouteName}/${version}`,\n version,\n serviceRoute,\n };\n};\n","const withIndex = (prefix: string, responseIndex: number | string): string => {\n return `${prefix}${String(responseIndex)}`;\n};\n\nexport const socketEventNames = {\n apiRequest: 'apiRequest',\n apiCancel: 'apiCancel',\n sync: 'sync',\n syncCancel: 'syncCancel',\n joinRoom: 'joinRoom',\n leaveRoom: 'leaveRoom',\n getJoinedRooms: 'getJoinedRooms',\n updateLocation: 'updateLocation',\n updateSession: 'updateSession',\n sessionReplaced: 'sessionReplaced',\n logout: 'logout',\n\n intentionalDisconnect: 'intentionalDisconnect',\n intentionalReconnect: 'intentionalReconnect',\n //? Client -> server activity heartbeat (mouse/keyboard/touch), throttled.\n //? Drives the server-side activity sampler that fires registered activity\n //? events (e.g. the built-in AFK detector).\n activity: 'activity',\n userAfk: 'userAfk',\n userBack: 'userBack',\n //? Emitted to a room's remaining members when a peer leaves — hard disconnect\n //? (socket close) or grace-expiry (presence MIS-003). Lets clients prune a\n //? departed peer from their roster without polling. The presence package owns\n //? the dispatch; core owns the wire-event name so client + server can't drift.\n userLeft: 'userLeft',\n\n connect: 'connect',\n disconnect: 'disconnect',\n reconnectAttempt: 'reconnect_attempt',\n connectError: 'connect_error',\n\n apiResponsePrefix: 'apiResponse-',\n apiStreamPrefix: 'apiStream-',\n syncResponsePrefix: 'sync-',\n syncProgressPrefix: 'sync-progress-',\n joinRoomResponsePrefix: 'joinRoom-',\n leaveRoomResponsePrefix: 'leaveRoom-',\n getJoinedRoomsResponsePrefix: 'getJoinedRooms-',\n} as const;\n\nexport const buildApiResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiResponsePrefix, responseIndex);\n};\n\nexport const buildApiStreamEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.apiStreamPrefix, responseIndex);\n};\n\nexport const buildSyncResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncResponsePrefix, responseIndex);\n};\n\nexport const buildSyncProgressEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.syncProgressPrefix, responseIndex);\n};\n\nexport const buildJoinRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.joinRoomResponsePrefix, responseIndex);\n};\n\nexport const buildLeaveRoomResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.leaveRoomResponsePrefix, responseIndex);\n};\n\nexport const buildGetJoinedRoomsResponseEventName = (responseIndex: number | string): string => {\n return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex);\n};\n","//? CSRF configuration registry. The framework's CSRF middleware\n//? (`packages/server/src/httpRoutes/csrfMiddleware.ts`) and the\n//? client-side fetch wrapper (`packages/core/src/csrf.ts`) both consult\n//? `getCsrfConfig()` so consumers can change the header name, cookie\n//? name, token length, or cookie options without forking.\n//?\n//? The default values match the framework's pre-registry hardcodes\n//? (header `x-csrf-token`, 32-byte hex token minted at session create) so\n//? consumers who never call `registerCsrfConfig` get the historical\n//? behaviour unchanged.\n\nimport { createRegistry } from './createRegistry';\n\nexport interface CsrfCookieOptions {\n sameSite?: 'strict' | 'lax' | 'none';\n secure?: boolean;\n /**\n * Whether the login-absent double-submit cookie is HttpOnly. The client gets\n * the matching header value from the same-origin `GET /auth/csrf` JSON body,\n * so either value works; `false` remains the compatibility default.\n */\n httpOnly?: boolean;\n path?: string;\n maxAgeMs?: number;\n}\n\nexport interface CsrfConfig {\n /** Cookie name used to deliver the CSRF token to the browser. */\n cookieName: string;\n /** Request-header name the client attaches on state-changing requests. */\n headerName: string;\n /** Length in bytes of the random token minted by the session layer. */\n tokenLength: number;\n cookieOptions: CsrfCookieOptions;\n}\n\nexport const DEFAULT_CSRF_CONFIG: CsrfConfig = {\n cookieName: 'csrf-token',\n headerName: 'x-csrf-token',\n tokenLength: 32,\n cookieOptions: {\n sameSite: 'lax',\n //? `secure` intentionally UNSET so it resolves per-environment at serialize\n //? time (csrfRoute.ts) the SAME way the session cookie does — emit `Secure` in\n //? prod (SECURE=true) but NOT over plain HTTP in dev. A hardcoded `true` made\n //? the browser drop the login-absent double-submit cookie over http://, so\n //? every state-changing request 403'd. Force it on explicitly via\n //? `registerCsrfConfig({ cookieOptions: { secure: true } })` if desired.\n httpOnly: false,\n path: '/',\n maxAgeMs: 24 * 60 * 60 * 1000,\n },\n};\n\nconst registry = createRegistry<CsrfConfig, Partial<CsrfConfig>>(DEFAULT_CSRF_CONFIG, {\n //? Merge shallowly with the CURRENT config; `cookieOptions` is deep-merged\n //? so a partial override of (say) `sameSite` does not clobber `path` /\n //? `maxAgeMs`. Last-write-wins, and successive calls accumulate.\n transform: (input, current) => ({\n ...current,\n ...input,\n cookieOptions: { ...current.cookieOptions, ...input.cookieOptions },\n }),\n});\n\n/**\n * Override one or more CSRF settings. Merges shallowly with the current\n * config; `cookieOptions` is deep-merged so a partial override of (say)\n * `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.\n */\nexport const registerCsrfConfig = (input: Partial<CsrfConfig>): void => {\n registry.register(input);\n};\n\n/** Read the active config at call time (never at module load). */\nexport const getCsrfConfig = (): CsrfConfig => registry.get();\n\n/** Test-only — restore defaults between scenarios. */\nexport const resetCsrfConfigForTests = (): void => {\n registry.reset();\n};\n","import { captureException } from './sentrySetup';\n\nexport default async function tryCatch<T, P = void>(\n func: (values: P) => Promise<T> | T,\n params?: P,\n context?: Record<string, unknown>\n): Promise<[Error | null, T | null]> {\n try {\n const response = await func(params as P);\n return [null, response];\n } catch (error) {\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Client-side CSRF helper. Lazily fetches and caches the session's CSRF\n//? token from `/auth/csrf`, then attaches it as `x-csrf-token` on\n//? subsequent HTTP fetches in cookie mode.\n//?\n//? Token-mode sessions are CSRF-immune because cross-origin POSTs do not\n//? auto-attach the sessionStorage value; the cache simply stays empty in\n//? that mode.\n\nimport { getCsrfConfig } from './csrfConfig';\nimport { getProjectConfig } from './projectConfig';\nimport { socket } from './socketState';\nimport tryCatch from './tryCatch';\nimport tryCatchSync from './tryCatchSync';\n\n//? Tokens are scoped to the HTTP invocation origin. In routed-http mode the\n//? Socket.io connection may terminate on a remote `system` service while the\n//? invocation goes same-origin through a local router; a single socket-derived\n//? cache would fetch the CSRF token from the wrong ingress.\nconst cachedTokens = new Map<string, string>();\nconst inflightFetches = new Map<string, Promise<string | null>>();\n\nconst resolveSocketOrigin = (): string => socket?.io.opts.hostname\n ? `${socket.io.opts.secure ? 'https' : 'http'}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ''}`\n : '';\n\nconst resolveRequestOrigin = (input?: RequestInfo | URL): string => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const browserOrigin = globalThis.window !== undefined && globalThis.location?.origin\n ? globalThis.location.origin\n : '';\n if (input !== undefined) {\n let raw: string;\n if (typeof input === 'string') raw = input;\n else if (input instanceof URL) raw = input.href;\n else raw = input.url;\n const fallbackOrigin = browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n const [error, parsed] = tryCatchSync(() => new URL(raw, fallbackOrigin));\n if (!error && parsed) return parsed.origin;\n }\n return browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();\n};\n\nconst fetchCsrfToken = async (origin: string): Promise<string | null> => {\n const base = origin;\n const [error, token] = await tryCatch<string | null, undefined>(async () => {\n const response = await fetch(`${base}/auth/csrf`, {\n method: 'GET',\n credentials: 'include',\n });\n if (!response.ok) return null;\n const body = (await response.json()) as { csrfToken?: string | null };\n return body.csrfToken ?? null;\n });\n if (error) return null;\n return token ?? null;\n};\n\n/**\n * Fetch (or return the cached) CSRF token for the active session. Returns\n * null in token mode or when no session exists. Callers should attach the\n * returned value as the `X-CSRF-Token` request header.\n */\nexport const getCsrfToken = async (origin = resolveRequestOrigin()): Promise<string | null> => {\n //? Token-mode sessions: skip entirely. The server doesn't enforce CSRF\n //? in token mode, and there's no value to attach.\n if (getProjectConfig().session.basedToken) {\n return null;\n }\n\n const cached = cachedTokens.get(origin);\n if (cached) return cached;\n const inflight = inflightFetches.get(origin);\n if (inflight) return inflight;\n\n const next = fetchCsrfToken(origin).then((token) => {\n if (token) cachedTokens.set(origin, token);\n inflightFetches.delete(origin);\n return token;\n });\n inflightFetches.set(origin, next);\n return next;\n};\n\n/** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */\nexport const clearCsrfToken = (): void => {\n cachedTokens.clear();\n inflightFetches.clear();\n};\n\nconst STATE_CHANGING = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);\n\nconst isCsrfMismatchResponse = async (response: Response): Promise<boolean> => {\n if (response.status !== 403) return false;\n const [error, isMismatch] = await tryCatch<boolean, undefined>(async () => {\n //? `response.clone()` is used here so the ORIGINAL response body stream is\n //? preserved for the caller (the clone is consumed, the original is not).\n //? This is important: `httpFetch` returns the original `response` reference\n //? to the caller so it can read its body, and cloning before reading is the\n //? correct way to peek at the body without consuming it.\n const cloned = response.clone();\n const body = (await cloned.json()) as { errorCode?: string };\n return body.errorCode === 'auth.csrfMismatch';\n });\n if (error) return false;\n return isMismatch ?? false;\n};\n\n/**\n * Drop-in replacement for `fetch` that automatically attaches the framework's\n * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in\n * cookie mode. Use this for any project-side HTTP call that hits a\n * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.\n *\n * Behavior:\n * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).\n * - Token mode: passes through without CSRF (immune by design).\n * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,\n * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`\n * response, clears the cache and retries once.\n * - `credentials: 'include'` is forced unless the caller overrode it.\n *\n * @example\n * import { httpFetch } from '@luckystack/core/client';\n * const res = await httpFetch('/api/system/myRoute/v1', {\n * method: 'POST',\n * body: JSON.stringify({ ... }),\n * });\n */\nexport const httpFetch: typeof fetch = async (input, init = {}) => {\n const method = (init.method ?? 'GET').toUpperCase();\n const headers = new Headers(init.headers);\n const config = getProjectConfig();\n if (config.session.basedToken && !headers.has('Authorization')) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- SSR runtime guard\n const token = globalThis.window === undefined\n ? null\n : tryCatchSync(() => globalThis.sessionStorage.getItem('token'))[1];\n if (token) headers.set('Authorization', `Bearer ${token}`);\n }\n if (!headers.has('Content-Type') && init.body && typeof init.body === 'string') {\n headers.set('Content-Type', 'application/json');\n }\n\n const csrfHeaderName = getCsrfConfig().headerName;\n const send = async (csrfToken: string | null): Promise<Response> => {\n const finalHeaders = new Headers(headers);\n if (csrfToken && STATE_CHANGING.has(method) && !finalHeaders.has(csrfHeaderName)) {\n finalHeaders.set(csrfHeaderName, csrfToken);\n }\n return fetch(input, {\n ...init,\n method,\n headers: finalHeaders,\n credentials: init.credentials ?? 'include',\n });\n };\n\n if (!STATE_CHANGING.has(method) || config.session.basedToken) {\n return send(null);\n }\n\n const requestOrigin = resolveRequestOrigin(input);\n const token = await getCsrfToken(requestOrigin);\n let response = await send(token);\n\n //? On csrfMismatch, the cached token is stale (session rotated). Clear and\n //? retry ONCE with a fresh fetch — covers the case where the user logged\n //? back in while a tab was idle. The retry is intentionally limited to one\n //? attempt: a second csrfMismatch means the session is broken in a way a\n //? fresh token won't fix (e.g. session expired), and retrying would loop.\n if (await isCsrfMismatchResponse(response)) {\n clearCsrfToken();\n const refreshed = await getCsrfToken(requestOrigin);\n if (refreshed) {\n response = await send(refreshed);\n }\n }\n\n return response;\n};\n","export default function sleep(ms: number) {\n return new Promise<void>((resolve) => setTimeout(resolve, ms));\n}\n","//? Pure page-route validator. Lives in @luckystack/core (no Node deps) so\n//? both the client-side router (`src/main.tsx`'s `getRoutes()`) AND the\n//? devkit-side scaffold CLI / hot-reload validator can consume it through\n//? the same single source of truth.\n//?\n//? Implements the invisible-parent folder convention: a folder starting\n//? with `privateFolderPrefix` is stripped from the URL but its CHILDREN\n//? remain routeable. A `page.tsx` placed directly inside such a folder\n//? (no non-private sibling segment) is invalid because it has no URL.\n//?\n//? Reserved framework folders (`_api`, `_sync`, `_components`, …) NEVER\n//? host a page, even nested — placing `page.tsx` anywhere under one of\n//? them is a hard error.\n\nexport interface PageRouteRules {\n /**\n * Single-character prefix that marks a folder as **invisible-parent** for\n * page routing. Default: `'_'`.\n */\n privateFolderPrefix: string;\n /**\n * Folder names that are reserved for framework-internal use and may NEVER\n * host a `page.tsx`, nested or otherwise. Scaffold + page-discovery emit\n * a hard error when a page is placed inside one.\n */\n scaffoldIgnoredFolders: readonly string[];\n}\n\nexport const DEFAULT_PAGE_ROUTE_RULES: PageRouteRules = {\n privateFolderPrefix: '_',\n scaffoldIgnoredFolders: [\n '_api',\n '_sync',\n '_function',\n '_functions',\n '_component',\n '_components',\n '_provider',\n '_providers',\n '_locale',\n '_locales',\n '_socket',\n '_sockets',\n '_shared',\n '_server',\n ],\n};\n\nexport interface PagePathValidationResult {\n /** `true` when the page file produces a routeable URL. */\n valid: boolean;\n /** Computed URL when `valid`. Always begins with `/`. `/` for root page. */\n route?: string;\n /** Human-readable explanation when `!valid`. */\n reason?: string;\n}\n\n/**\n * Validate a `page.tsx` file path relative to `src/`. Accepts both\n * forward and back slashes, with or without a leading `./` or `/`.\n * The filename **must** be `page.tsx` (or `page.jsx`); other filenames\n * return `valid: false`.\n *\n * Examples (defaults):\n * admin/page.tsx -> { valid: true, route: '/admin' }\n * _housing/renting/page.tsx -> { valid: true, route: '/renting' }\n * _housing/page.tsx -> { valid: false, reason: 'no URL segment left after stripping underscore folders' }\n * _api/something/page.tsx -> { valid: false, reason: 'page.tsx cannot live inside reserved folder _api' }\n * page.tsx -> { valid: true, route: '/' }\n * some/route.tsx -> { valid: false, reason: 'not a page file (expected page.tsx)' }\n */\nexport const validatePagePath = (\n srcRelativePath: string,\n rules: PageRouteRules = DEFAULT_PAGE_ROUTE_RULES,\n): PagePathValidationResult => {\n const normalized = srcRelativePath.replaceAll('\\\\', '/').replace(/^\\.\\//, '').replace(/^\\/+/, '');\n const segments = normalized.split('/').filter((segment) => segment.length > 0);\n\n if (segments.length === 0) {\n return { valid: false, reason: 'empty path' };\n }\n\n const filename = segments.at(-1);\n if (filename !== 'page.tsx' && filename !== 'page.jsx') {\n return { valid: false, reason: 'not a page file (expected page.tsx)' };\n }\n\n const folderSegments = segments.slice(0, -1);\n const prefix = rules.privateFolderPrefix;\n const reserved = rules.scaffoldIgnoredFolders;\n\n for (const reservedName of reserved) {\n if (folderSegments.includes(reservedName)) {\n return { valid: false, reason: `page.tsx cannot live inside reserved folder ${reservedName}` };\n }\n }\n\n const visibleSegments = folderSegments.filter((segment) => !segment.startsWith(prefix));\n\n if (folderSegments.length > 0 && visibleSegments.length === 0) {\n return { valid: false, reason: 'no URL segment left after stripping underscore folders' };\n }\n\n const route = visibleSegments.length === 0 ? '/' : `/${visibleSegments.join('/')}`;\n return { valid: true, route };\n};\n"],"mappings":";;;;;;;;;;AAiBA,IAAM,WAAW,eAAoC,IAAI;AAElD,IAAM,uBAAuB,CAAC,QAA4B;AAC/D,WAAS,SAAS,GAAG;AACvB;AAEO,IAAM,yBAAyB,CACpC,UACA,SACA,YACkC;AAClC,QAAM,YAAY,SAAS,IAAI;AAC/B,MAAI,CAAC,UAAW,QAAO;AAMvB,SAAO,UAAU,QAAQ,IAAI,OAAO,IAAI,OAAO;AAEjD;AAEO,IAAM,2BAA2B,MAAe,SAAS,aAAa;;;ACvBtE,IAAM,kBAAkB,CAAC,oBAC9B,gBAAgB,WAAW,KAAK,KAChC,gBAAgB,WAAW,OAAO,KAClC,gBAAgB,WAAW,MAAM;AAmB5B,IAAM,kBAAkB,CAAC,YAAgC;AAE9D,QAAM,YAAY,QAAQ,MAAM,GAAG;AACnC,QAAM,WAAW,UAAU,GAAG,EAAE,GAAG,YAAY,KAAK;AACpD,QAAM,aAAa,SAAS,KAAK,QAAQ,KACpC,UAAU,GAAG,EAAE,KAAK,IAAI,YAAY,IACrC;AAEJ,MAAI,gBAAgB,UAAU,GAAG;AAC/B,WAAO;AAAA,EACT;AAEA,MAAI,WAAW,WAAW,QAAQ,KAAK,WAAW,WAAW,QAAQ,GAAG;AACtE,WAAO;AAAA,EACT;AAEA,MAAI,WAAW,WAAW,QAAQ,KAAK,WAAW,WAAW,MAAM,KAAK,WAAW,WAAW,OAAO,GAAG;AACtG,WAAO;AAAA,EACT;AAGA,SAAO;AACT;AAMO,IAAM,yBAAyB,CACpC,WACA,YACe;AACf,SAAO,UAAU,cAAc,gBAAgB,OAAO;AACxD;AAeO,IAAM,kBAAkB,CAC7B,eACA,kBACY;AACZ,SAAO,kBAAkB;AAC3B;;;AChFO,IAAI,SAAwB;AAMnC,IAAI,UAAmC,CAAC;AAIxC,IAAM,6BAA6B;AAE5B,IAAM,YAAY,CAAC,SAA8B;AACtD,WAAS;AAET,MAAI,MAAM;AACR,UAAM,UAAU;AAChB,cAAU,CAAC;AACX,eAAW,WAAW,SAAS;AAC7B,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AACF;AAEA,IAAI,gBAAgB;AAEb,IAAM,yBAAyB,MAAc;AAClD,kBAAgB,gBAAgB;AAChC,SAAO;AACT;AAEO,IAAM,gBAAgB,MAA8B;AACzD,MAAI,OAAQ,QAAO,QAAQ,QAAQ,MAAM;AACzC,SAAO,IAAI,QAAuB,CAAC,YAAY;AAC7C,UAAM,QAAQ,WAAW,MAAM;AAG7B,gBAAU,QAAQ,OAAO,CAAC,MAAM,MAAM,QAAQ;AAC9C,cAAQ,IAAI;AAAA,IACd,GAAG,0BAA0B;AAE7B,UAAM,WAAW,CAAC,MAAoB;AACpC,mBAAa,KAAK;AAClB,cAAQ,CAAC;AAAA,IACX;AAEA,YAAQ,KAAK,QAAQ;AAAA,EACvB,CAAC;AACH;;;ACjCA,IAAM,eAAyB;AAAA,EAC7B,SAAS,MAAM;AAAA,EAAc;AAAA,EAC7B,OAAO,MAAM;AAAA,EAAc;AAAA,EAC3B,MAAM,MAAM;AAAA,EAAc;AAAA,EAC1B,SAAS,MAAM;AAAA,EAAc;AAC/B;AAEA,IAAMA,YAAW,eAAyB,YAAY;AAE/C,IAAM,mBAAmB,CAAC,aAA6B;AAC5D,EAAAA,UAAS,SAAS,QAAQ;AAC5B;AAEO,IAAM,cAAc,MAAgBA,UAAS,IAAI;AAIjD,IAAM,SAAmB;AAAA,EAC9B,SAAS,CAAC,UAAU;AAAE,IAAAA,UAAS,IAAI,EAAE,QAAQ,KAAK;AAAA,EAAG;AAAA,EACrD,OAAO,CAAC,UAAU;AAAE,IAAAA,UAAS,IAAI,EAAE,MAAM,KAAK;AAAA,EAAG;AAAA,EACjD,MAAM,CAAC,UAAU;AAAE,IAAAA,UAAS,IAAI,EAAE,KAAK,KAAK;AAAA,EAAG;AAAA,EAC/C,SAAS,CAAC,UAAU;AAAE,IAAAA,UAAS,IAAI,EAAE,QAAQ,KAAK;AAAA,EAAG;AACvD;;;ACnCe,SAAR,aACL,MACA,QAC0B;AAC1B,MAAI;AACF,UAAM,WAAW,KAAK,MAAW;AACjC,WAAO,CAAC,MAAM,QAAQ;AAAA,EACxB,SAAS,OAAO;AACd,WAAO,CAAC,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,GAAG,IAAI;AAAA,EACzE;AACF;;;ACuDA,IAAM,cAAgE,CAAC;AAUhE,SAAS,mBACd,MACA,SACY;AACZ,QAAM,WAAW,YAAY,IAAI;AACjC,QAAM,MAAM,YAAY,oBAAI,IAAgB;AAC5C,MAAI,CAAC,SAAU,aAAY,IAAI,IAAI;AACnC,QAAM,UAAU;AAChB,MAAI,IAAI,OAAO;AACf,SAAO,MAAM;AACX,QAAI,OAAO,OAAO;AAAA,EACpB;AACF;AAUO,SAAS,mBACd,MACA,SACM;AACN,QAAM,MAAM,YAAY,IAAI;AAC5B,MAAI,CAAC,OAAO,IAAI,SAAS,EAAG;AAK5B,QAAM,WAAW,CAAC,GAAG,GAAG;AACxB,aAAW,WAAW,UAAU;AAC9B,QAAI;AACF,YAAM,SAAS,QAAQ,OAAO;AAC9B,UAAI,UAAU,OAAQ,OAA4B,UAAU,YAAY;AACtE,QAAC,OAA4B,MAAM,CAAC,UAAmB;AACrD,oBAAU,EAAE,MAAM,eAAe,IAAI,4BAA4B,KAAK;AAAA,QACxE,CAAC;AAAA,MACH;AAAA,IACF,SAAS,OAAO;AACd,gBAAU,EAAE,MAAM,eAAe,IAAI,mBAAmB,KAAK;AAAA,IAC/D;AAAA,EACF;AACF;AAYA,eAAsB,2BACpB,MACA,SAC+B;AAC/B,QAAM,MAAM,YAAY,IAAI;AAC5B,MAAI,CAAC,OAAO,IAAI,SAAS,EAAG,QAAO,EAAE,SAAS,MAAM;AAEpD,QAAM,WAAW,CAAC,GAAG,GAAG;AACxB,aAAW,WAAW,UAAU;AAC9B,QAAI;AACJ,QAAI;AACF,eAAS,MAAM,QAAQ,OAAO;AAAA,IAChC,SAAS,OAAO;AACd,gBAAU,EAAE,MAAM,eAAe,IAAI,4BAA4B,KAAK;AACtE;AAAA,IACF;AACA,QAAI,QAAQ,MAAM;AAChB,aAAO,EAAE,SAAS,MAAM,QAAQ,OAAO;AAAA,IACzC;AAAA,EACF;AACA,SAAO,EAAE,SAAS,MAAM;AAC1B;;;ACzHA,IAAM,WAA2B,CAAC;AAClC,IAAM,YAA6B,CAAC;AAIpC,IAAI,gBAAgB;AACpB,IAAI,iBAAiB;AAEd,IAAM,WAAW,MAAM;AAC5B,MAAI,OAAO,cAAc,YAAa,QAAO;AAC7C,SAAO,UAAU;AACnB;AAYA,IAAM,gBAAgB,CAAC,MAAqB,WAAkC;AAC5E,MAAI,CAAC,KAAK,OAAQ;AAClB,eAAa,MAAM;AAAE,SAAK,SAAS,MAAM;AAAA,EAAG,CAAC;AAC/C;AAIA,IAAM,uBAAuB,CAC3B,OACA,KACA,QACA,eACS;AACT,eAAa,MAAM;AACjB,uBAAmB,oBAAoB,EAAE,OAAO,KAAK,QAAQ,WAAW,CAAC;AAAA,EAC3E,CAAC;AACH;AAEA,IAAM,eAAe,CACnB,OACA,UACS;AACT,QAAM,EAAE,UAAU,YAAY,aAAa,IAAI,iBAAiB,EAAE;AAClE,MAAI,YAAY,EAAG;AACnB,QAAM,SAAS,KAAK,IAAI,IAAI;AAC5B,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7C,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,QAAQ,KAAK,YAAY,QAAQ;AACnC,YAAM,OAAO,GAAG,CAAC;AACjB,oBAAc,MAAM,SAAS;AAC7B,2BAAqB,OAAO,KAAK,KAAK,WAAW,KAAK,cAAc,YAAY;AAAA,IAClF;AAAA,EACF;AACF;AAEA,IAAM,oBAAoB,CACxB,OACA,MACA,UACY;AACZ,eAAa,OAAO,KAAK;AACzB,QAAM,EAAE,SAAS,YAAY,aAAa,IAAI,iBAAiB,EAAE;AAIjE,QAAM,kBAAkB,KAAK,cAAc;AAC3C,MAAI,MAAM,SAAS,SAAS;AAC1B,UAAM,KAAK,IAAI;AACf,WAAO;AAAA,EACT;AACA,MAAI,oBAAoB,eAAe;AACrC,UAAM,UAAU,MAAM,MAAM;AAC5B,UAAM,KAAK,IAAI;AACf,cAAU,EAAE,MAAM,gBAAgB,KAAK,4CAAuC,eAAe,GAAG;AAChG,QAAI,SAAS;AACX,oBAAc,SAAS,YAAY;AACnC,2BAAqB,OAAO,QAAQ,KAAK,cAAc,eAAe;AAAA,IACxE;AACA,WAAO;AAAA,EACT;AACA,MAAI,oBAAoB,eAAe;AACrC,cAAU,EAAE,MAAM,gBAAgB,KAAK,4CAAuC,eAAe,GAAG;AAChG,kBAAc,MAAM,YAAY;AAChC,yBAAqB,OAAO,KAAK,KAAK,cAAc,eAAe;AACnE,WAAO;AAAA,EACT;AACA,YAAU,EAAE,KAAK,gBAAgB,KAAK,0CAAqC,eAAe,GAAG;AAC7F,gBAAc,MAAM,YAAY;AAChC,uBAAqB,OAAO,KAAK,KAAK,cAAc,eAAe;AACnE,SAAO;AACT;AAEO,IAAM,oBAAoB,CAAC,SAChC,kBAAkB,UAAU,MAAM,KAAK;AAElC,IAAM,qBAAqB,CAAC,SACjC,kBAAkB,WAAW,MAAM,MAAM;AAEpC,IAAM,qBAAqB,CAAC,OAAe;AAChD,QAAM,QAAQ,SAAS,UAAU,CAAC,SAAS,KAAK,OAAO,EAAE;AACzD,MAAI,UAAU,IAAI;AAChB,aAAS,OAAO,OAAO,CAAC;AAAA,EAC1B;AACF;AAEO,IAAM,sBAAsB,CAAC,OAAe;AACjD,QAAM,QAAQ,UAAU,UAAU,CAAC,SAAS,KAAK,OAAO,EAAE;AAC1D,MAAI,UAAU,IAAI;AAChB,cAAU,OAAO,OAAO,CAAC;AAAA,EAC3B;AACF;AAEO,IAAM,2BAA2B,CAAC,QAAgB;AACvD,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAChD,QAAI,SAAS,CAAC,GAAG,QAAQ,KAAK;AAC5B,eAAS,OAAO,GAAG,CAAC;AAAA,IACtB;AAAA,EACF;AACF;AAMA,IAAM,eAAe,CACnB,OACA,MACA,gBACA,UACY;AACZ,QAAM,CAAC,KAAK,IAAI,aAAa,MAAM;AAAE,SAAK,IAAI,cAAc;AAAA,EAAG,CAAC;AAChE,MAAI,OAAO;AACT,UAAM,QAAQ,IAAI;AAClB,cAAU,EAAE,KAAK,gBAAgB,KAAK,kDAA6C,EAAE,MAAM,CAAC;AAC5F,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,IAAM,gBAAgB,CAAC,QAAuB,mBAA2B;AAC9E,MAAI,cAAe;AACnB,eAAa,UAAU,KAAK;AAC5B,MAAI,SAAS,WAAW,EAAG;AAC3B,MAAI,CAAC,OAAO,EAAG;AAEf,kBAAgB;AAChB,SAAO,SAAS,SAAS,GAAG;AAC1B,QAAI,CAAC,OAAO,EAAG;AACf,UAAM,OAAO,SAAS,MAAM;AAC5B,QAAI,QAAQ,CAAC,aAAa,UAAU,MAAM,gBAAgB,KAAK,EAAG;AAAA,EACpE;AACA,kBAAgB;AAClB;AAEO,IAAM,iBAAiB,CAAC,QAAuB,mBAA2B;AAC/E,MAAI,eAAgB;AACpB,eAAa,WAAW,MAAM;AAC9B,MAAI,UAAU,WAAW,EAAG;AAC5B,MAAI,CAAC,OAAO,EAAG;AAEf,mBAAiB;AACjB,SAAO,UAAU,SAAS,GAAG;AAC3B,QAAI,CAAC,OAAO,EAAG;AACf,UAAM,OAAO,UAAU,MAAM;AAC7B,QAAI,QAAQ,CAAC,aAAa,WAAW,MAAM,gBAAgB,MAAM,EAAG;AAAA,EACtE;AACA,mBAAiB;AACnB;AAEO,IAAM,kBAAkB,MAAM,SAAS;AACvC,IAAM,mBAAmB,MAAM,UAAU;;;ACtMzC,IAAM,8BAA8B;AAEpC,IAAM,oBAAoB,CAAC,UAA0C;AAC1E,SAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,CAAC,SAAS;AACnD,WACE,CAAC,CAAC,QACC,OAAO,SAAS,YAChB,OAAQ,KAAoB,QAAQ,YACpC,CAAC,UAAU,UAAU,SAAS,EAAE,SAAS,OAAQ,KAAoB,KAAK;AAAA,EAEjF,CAAC;AACH;AAEO,IAAM,6BAA6B,CAAC;AAAA,EACzC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAK+B;AAC7B,QAAM,mBAAmB,kBAAkB,SAAS,WAAW,IAAI,SAAS,cAAc;AAC1F,QAAM,iBAAiB,OAAO,SAAS,cAAc,WAAW,SAAS,UAAU,KAAK,IAAI;AAC5F,QAAM,eAAe,eAAe,SAAS;AAC7C,QAAM,iBAAiB,eAAe,iBAAkB,qBAAqB;AAC7E,QAAM,qBAAqB,OAAO,SAAS,eAAe,WAAW,SAAS,aAAa;AAE3F,QAAM,UAAU,iBACZ,eAAe,EAAE,WAAW,gBAAgB,aAAa,iBAAiB,CAAC,IAC3E;AAEJ,SAAO;AAAA,IACL,QAAQ;AAAA,IACR;AAAA,IACA,WAAW;AAAA,IACX,aAAa;AAAA,IACb,YAAY,sBAAsB;AAAA,EACpC;AACF;AAEO,IAAM,+BAA+B,CAAC;AAAA,EAC3C;AAAA,EACA;AAAA,EACA,sBAAsB;AACxB,MAIc;AACZ,MAAI,OAAO,uBAAuB,SAAU,QAAO;AACnD,SAAO,WAAW,YAAY,MAAM;AACtC;;;AC1EA,IAAM,wBAAwB;AA8B9B,IAAM,oBAAoB,CAAC,UAA0B;AACnD,SAAO,MAAM,KAAK,EAAE,WAAW,MAAM,GAAG,EAAE,WAAW,SAAS,EAAE,EAAE,WAAW,SAAS,EAAE;AAC1F;AAEA,IAAM,mBAAmB,CAAC,UAA2B;AACnD,SAAO,MAAM,MAAM,GAAG,EAAE,KAAK,CAAC,YAAY,QAAQ,WAAW,CAAC;AAChE;AAEO,IAAM,wBAAwB,CAAC,UAA2C;AAC/E,QAAM,aAAa,kBAAkB,KAAK;AAC1C,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,QAAQ,SAAS,QAAQ,8BAA8B;AAAA,EAClE;AAEA,MAAI,iBAAiB,UAAU,GAAG;AAChC,WAAO,EAAE,QAAQ,SAAS,QAAQ,4CAA4C;AAAA,EAChF;AAEA,QAAM,WAAW,WAAW,MAAM,GAAG;AACrC,MAAI,SAAS,SAAS,GAAG;AACvB,WAAO,EAAE,QAAQ,SAAS,QAAQ,sDAAsD;AAAA,EAC1F;AAEA,QAAM,CAAC,SAAS,GAAG,aAAa,IAAI;AACpC,QAAM,YAAY,cAAc,KAAK,GAAG;AAExC,MAAI,CAAC,WAAW,CAAC,WAAW;AAC1B,WAAO,EAAE,QAAQ,SAAS,QAAQ,sDAAsD;AAAA,EAC1F;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,qBAAqB,GAAG,OAAO,IAAI,SAAS;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACF;AAEO,IAAM,0BAA0B,CAAC;AAAA,EACtC;AAAA,EACA;AACF,MAGiC;AAC/B,QAAM,aAAa,kBAAkB,KAAK;AAC1C,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,QAAQ,SAAS,QAAQ,mCAAmC;AAAA,EACvE;AAEA,MAAI,iBAAiB,UAAU,GAAG;AAChC,WAAO,EAAE,QAAQ,SAAS,QAAQ,iDAAiD;AAAA,EACrF;AAEA,QAAM,WAAW,WAAW,MAAM,GAAG;AACrC,QAAM,gBAAgB,SAAS,CAAC,MAAM,SAAS,SAAS,MAAM,CAAC,IAAI;AAEnE,MAAI,cAAc,SAAS,GAAG;AAC5B,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ,4BAA4B,MAAM;AAAA,IAC5C;AAAA,EACF;AAEA,QAAM,UAAU,cAAc,GAAG,EAAE;AACnC,MAAI,CAAC,WAAW,CAAC,sBAAsB,KAAK,OAAO,GAAG;AACpD,WAAO,EAAE,QAAQ,SAAS,QAAQ,8CAA8C;AAAA,EAClF;AAEA,QAAM,oBAAoB,cAAc,MAAM,GAAG,EAAE,EAAE,KAAK,GAAG;AAC7D,QAAM,eAAe,sBAAsB,iBAAiB;AAC5D,MAAI,aAAa,WAAW,SAAS;AACnC,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,QAAQ,aAAa;AAAA,IACvB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,oBAAoB,GAAG,MAAM,IAAI,aAAa,mBAAmB,IAAI,OAAO;AAAA,IAC5E;AAAA,IACA;AAAA,EACF;AACF;;;AClHA,IAAM,YAAY,CAAC,QAAgBC,mBAA2C;AAC5E,SAAO,GAAG,MAAM,GAAG,OAAOA,cAAa,CAAC;AAC1C;AAEO,IAAM,mBAAmB;AAAA,EAC9B,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,WAAW;AAAA,EACX,gBAAgB;AAAA,EAChB,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,QAAQ;AAAA,EAER,uBAAuB;AAAA,EACvB,sBAAsB;AAAA;AAAA;AAAA;AAAA,EAItB,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAKV,UAAU;AAAA,EAEV,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,kBAAkB;AAAA,EAClB,cAAc;AAAA,EAEd,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,wBAAwB;AAAA,EACxB,yBAAyB;AAAA,EACzB,8BAA8B;AAChC;AAEO,IAAM,4BAA4B,CAACA,mBAA2C;AACnF,SAAO,UAAU,iBAAiB,mBAAmBA,cAAa;AACpE;AAEO,IAAM,0BAA0B,CAACA,mBAA2C;AACjF,SAAO,UAAU,iBAAiB,iBAAiBA,cAAa;AAClE;AAEO,IAAM,6BAA6B,CAACA,mBAA2C;AACpF,SAAO,UAAU,iBAAiB,oBAAoBA,cAAa;AACrE;AAEO,IAAM,6BAA6B,CAACA,mBAA2C;AACpF,SAAO,UAAU,iBAAiB,oBAAoBA,cAAa;AACrE;AAEO,IAAM,iCAAiC,CAACA,mBAA2C;AACxF,SAAO,UAAU,iBAAiB,wBAAwBA,cAAa;AACzE;AAEO,IAAM,kCAAkC,CAACA,mBAA2C;AACzF,SAAO,UAAU,iBAAiB,yBAAyBA,cAAa;AAC1E;AAEO,IAAM,uCAAuC,CAACA,mBAA2C;AAC9F,SAAO,UAAU,iBAAiB,8BAA8BA,cAAa;AAC/E;;;ACnCO,IAAM,sBAAkC;AAAA,EAC7C,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,aAAa;AAAA,EACb,eAAe;AAAA,IACb,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOV,UAAU;AAAA,IACV,MAAM;AAAA,IACN,UAAU,KAAK,KAAK,KAAK;AAAA,EAC3B;AACF;AAEA,IAAMC,YAAW,eAAgD,qBAAqB;AAAA;AAAA;AAAA;AAAA,EAIpF,WAAW,CAAC,OAAO,aAAa;AAAA,IAC9B,GAAG;AAAA,IACH,GAAG;AAAA,IACH,eAAe,EAAE,GAAG,QAAQ,eAAe,GAAG,MAAM,cAAc;AAAA,EACpE;AACF,CAAC;AAOM,IAAM,qBAAqB,CAAC,UAAqC;AACtE,EAAAA,UAAS,SAAS,KAAK;AACzB;AAGO,IAAM,gBAAgB,MAAkBA,UAAS,IAAI;AAGrD,IAAM,0BAA0B,MAAY;AACjD,EAAAA,UAAS,MAAM;AACjB;;;AC9EA,eAAO,SACL,MACA,QACA,SACmC;AACnC,MAAI;AACF,UAAM,WAAW,MAAM,KAAK,MAAW;AACvC,WAAO,CAAC,MAAM,QAAQ;AAAA,EACxB,SAAS,OAAO;AACd,qBAAiB,OAAO,OAAO;AAC/B,WAAO,CAAC,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,GAAG,IAAI;AAAA,EACzE;AACF;;;ACIA,IAAM,eAAe,oBAAI,IAAoB;AAC7C,IAAM,kBAAkB,oBAAI,IAAoC;AAEhE,IAAM,sBAAsB,MAAc,QAAQ,GAAG,KAAK,WACtD,GAAG,OAAO,GAAG,KAAK,SAAS,UAAU,MAAM,MAAM,OAAO,GAAG,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,OAAO,IAAI,OAAO,GAAG,KAAK,IAAI,KAAK,EAAE,KAC/H;AAEJ,IAAM,uBAAuB,CAAC,UAAsC;AAElE,QAAM,gBAAgB,WAAW,WAAW,UAAa,WAAW,UAAU,SAC1E,WAAW,SAAS,SACpB;AACJ,MAAI,UAAU,QAAW;AACvB,QAAI;AACJ,QAAI,OAAO,UAAU,SAAU,OAAM;AAAA,aAC5B,iBAAiB,IAAK,OAAM,MAAM;AAAA,QACtC,OAAM,MAAM;AACjB,UAAM,iBAAiB,cAAc,SAAS,IAAI,gBAAgB,oBAAoB;AACtF,UAAM,CAAC,OAAO,MAAM,IAAI,aAAa,MAAM,IAAI,IAAI,KAAK,cAAc,CAAC;AACvE,QAAI,CAAC,SAAS,OAAQ,QAAO,OAAO;AAAA,EACtC;AACA,SAAO,cAAc,SAAS,IAAI,gBAAgB,oBAAoB;AACxE;AAEA,IAAM,iBAAiB,OAAO,WAA2C;AACvE,QAAM,OAAO;AACb,QAAM,CAAC,OAAO,KAAK,IAAI,MAAM,SAAmC,YAAY;AAC1E,UAAM,WAAW,MAAM,MAAM,GAAG,IAAI,cAAc;AAAA,MAChD,QAAQ;AAAA,MACR,aAAa;AAAA,IACf,CAAC;AACD,QAAI,CAAC,SAAS,GAAI,QAAO;AACzB,UAAM,OAAQ,MAAM,SAAS,KAAK;AAClC,WAAO,KAAK,aAAa;AAAA,EAC3B,CAAC;AACD,MAAI,MAAO,QAAO;AAClB,SAAO,SAAS;AAClB;AAOO,IAAM,eAAe,OAAO,SAAS,qBAAqB,MAA8B;AAG7F,MAAI,iBAAiB,EAAE,QAAQ,YAAY;AACzC,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,aAAa,IAAI,MAAM;AACtC,MAAI,OAAQ,QAAO;AACnB,QAAM,WAAW,gBAAgB,IAAI,MAAM;AAC3C,MAAI,SAAU,QAAO;AAErB,QAAM,OAAO,eAAe,MAAM,EAAE,KAAK,CAAC,UAAU;AAClD,QAAI,MAAO,cAAa,IAAI,QAAQ,KAAK;AACzC,oBAAgB,OAAO,MAAM;AAC7B,WAAO;AAAA,EACT,CAAC;AACD,kBAAgB,IAAI,QAAQ,IAAI;AAChC,SAAO;AACT;AAGO,IAAM,iBAAiB,MAAY;AACxC,eAAa,MAAM;AACnB,kBAAgB,MAAM;AACxB;AAEA,IAAM,iBAAiB,oBAAI,IAAI,CAAC,QAAQ,OAAO,SAAS,QAAQ,CAAC;AAEjE,IAAM,yBAAyB,OAAO,aAAyC;AAC7E,MAAI,SAAS,WAAW,IAAK,QAAO;AACpC,QAAM,CAAC,OAAO,UAAU,IAAI,MAAM,SAA6B,YAAY;AAMzE,UAAM,SAAS,SAAS,MAAM;AAC9B,UAAM,OAAQ,MAAM,OAAO,KAAK;AAChC,WAAO,KAAK,cAAc;AAAA,EAC5B,CAAC;AACD,MAAI,MAAO,QAAO;AAClB,SAAO,cAAc;AACvB;AAuBO,IAAM,YAA0B,OAAO,OAAO,OAAO,CAAC,MAAM;AACjE,QAAM,UAAU,KAAK,UAAU,OAAO,YAAY;AAClD,QAAM,UAAU,IAAI,QAAQ,KAAK,OAAO;AACxC,QAAM,SAAS,iBAAiB;AAChC,MAAI,OAAO,QAAQ,cAAc,CAAC,QAAQ,IAAI,eAAe,GAAG;AAE9D,UAAMC,SAAQ,WAAW,WAAW,SAChC,OACA,aAAa,MAAM,WAAW,eAAe,QAAQ,OAAO,CAAC,EAAE,CAAC;AACpE,QAAIA,OAAO,SAAQ,IAAI,iBAAiB,UAAUA,MAAK,EAAE;AAAA,EAC3D;AACA,MAAI,CAAC,QAAQ,IAAI,cAAc,KAAK,KAAK,QAAQ,OAAO,KAAK,SAAS,UAAU;AAC9E,YAAQ,IAAI,gBAAgB,kBAAkB;AAAA,EAChD;AAEA,QAAM,iBAAiB,cAAc,EAAE;AACvC,QAAM,OAAO,OAAO,cAAgD;AAClE,UAAM,eAAe,IAAI,QAAQ,OAAO;AACxC,QAAI,aAAa,eAAe,IAAI,MAAM,KAAK,CAAC,aAAa,IAAI,cAAc,GAAG;AAChF,mBAAa,IAAI,gBAAgB,SAAS;AAAA,IAC5C;AACA,WAAO,MAAM,OAAO;AAAA,MAClB,GAAG;AAAA,MACH;AAAA,MACA,SAAS;AAAA,MACT,aAAa,KAAK,eAAe;AAAA,IACnC,CAAC;AAAA,EACH;AAEA,MAAI,CAAC,eAAe,IAAI,MAAM,KAAK,OAAO,QAAQ,YAAY;AAC5D,WAAO,KAAK,IAAI;AAAA,EAClB;AAEA,QAAM,gBAAgB,qBAAqB,KAAK;AAChD,QAAM,QAAQ,MAAM,aAAa,aAAa;AAC9C,MAAI,WAAW,MAAM,KAAK,KAAK;AAO/B,MAAI,MAAM,uBAAuB,QAAQ,GAAG;AAC1C,mBAAe;AACf,UAAM,YAAY,MAAM,aAAa,aAAa;AAClD,QAAI,WAAW;AACb,iBAAW,MAAM,KAAK,SAAS;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;;;ACnLe,SAAR,MAAuB,IAAY;AACxC,SAAO,IAAI,QAAc,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AAC/D;;;AC0BO,IAAM,2BAA2C;AAAA,EACtD,qBAAqB;AAAA,EACrB,wBAAwB;AAAA,IACtB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAyBO,IAAM,mBAAmB,CAC9B,iBACA,QAAwB,6BACK;AAC7B,QAAM,aAAa,gBAAgB,WAAW,MAAM,GAAG,EAAE,QAAQ,SAAS,EAAE,EAAE,QAAQ,QAAQ,EAAE;AAChG,QAAM,WAAW,WAAW,MAAM,GAAG,EAAE,OAAO,CAAC,YAAY,QAAQ,SAAS,CAAC;AAE7E,MAAI,SAAS,WAAW,GAAG;AACzB,WAAO,EAAE,OAAO,OAAO,QAAQ,aAAa;AAAA,EAC9C;AAEA,QAAM,WAAW,SAAS,GAAG,EAAE;AAC/B,MAAI,aAAa,cAAc,aAAa,YAAY;AACtD,WAAO,EAAE,OAAO,OAAO,QAAQ,sCAAsC;AAAA,EACvE;AAEA,QAAM,iBAAiB,SAAS,MAAM,GAAG,EAAE;AAC3C,QAAM,SAAS,MAAM;AACrB,QAAM,WAAW,MAAM;AAEvB,aAAW,gBAAgB,UAAU;AACnC,QAAI,eAAe,SAAS,YAAY,GAAG;AACzC,aAAO,EAAE,OAAO,OAAO,QAAQ,+CAA+C,YAAY,GAAG;AAAA,IAC/F;AAAA,EACF;AAEA,QAAM,kBAAkB,eAAe,OAAO,CAAC,YAAY,CAAC,QAAQ,WAAW,MAAM,CAAC;AAEtF,MAAI,eAAe,SAAS,KAAK,gBAAgB,WAAW,GAAG;AAC7D,WAAO,EAAE,OAAO,OAAO,QAAQ,yDAAyD;AAAA,EAC1F;AAEA,QAAM,QAAQ,gBAAgB,WAAW,IAAI,MAAM,IAAI,gBAAgB,KAAK,GAAG,CAAC;AAChF,SAAO,EAAE,OAAO,MAAM,MAAM;AAC9B;","names":["registry","responseIndex","registry","token"]}
package/dist/client.d.ts CHANGED
@@ -2,8 +2,8 @@ import { StreamPayload, ApiTypeMap } from './apiTypeStubs.js';
2
2
  export { SyncTypeMap } from './apiTypeStubs.js';
3
3
  import { H as HttpMethodLiteral } from './pageRouteValidation-bz2LFVQq.js';
4
4
  export { A as ApiMethodMap, D as DEFAULT_PAGE_ROUTE_RULES, L as Logger, a as LoggerContext, N as Notifier, b as NotifyInput, c as NotifyParam, P as PagePathValidationResult, d as PageRouteRules, S as SOCKETSTATUS, e as buildApiResponseEventName, f as buildApiStreamEventName, g as buildGetJoinedRoomsResponseEventName, h as buildJoinRoomResponseEventName, i as buildLeaveRoomResponseEventName, j as buildSyncProgressEventName, k as buildSyncResponseEventName, l as clearCsrfToken, m as enqueueApiRequest, n as enqueueSyncRequest, o as flushApiQueue, p as flushSyncQueue, q as getApiQueueSize, r as getCsrfToken, s as getLogger, t as getNotifier, u as getRegisteredApiMethod, v as getSyncQueueSize, w as httpFetch, x as incrementResponseIndex, y as isApiMethodMapRegistered, z as isOnline, B as normalizeErrorResponseCore, C as notify, E as parseServiceRouteName, F as registerApiMethodMap, G as registerLogger, I as registerNotifier, J as removeApiQueueItem, K as removeApiQueueItemsByKey, M as removeSyncQueueItem, O as setSocket, Q as sleep, R as socket, T as socketEventNames, U as statusContent, V as tryCatchSync, W as validatePagePath, X as waitForSocket } from './pageRouteValidation-bz2LFVQq.js';
5
- import { B as BaseSessionLayout } from './sessionTypes-iyUMf_pf.js';
6
- export { A as AppConfig, a as AuthProps, L as LoggingConfig, P as ProjectConfig, R as RateLimitingConfig, S as SessionConfig, b as SessionLocation, T as TransportConfig, g as getProjectConfig, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-iyUMf_pf.js';
5
+ import { B as BaseSessionLayout } from './sessionTypes-DanjuPaE.js';
6
+ export { A as AppConfig, a as AuthProps, L as LoggingConfig, P as ProjectConfig, R as RateLimitingConfig, S as SessionConfig, b as SessionLocation, T as TransportConfig, g as getProjectConfig, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-DanjuPaE.js';
7
7
  import * as react from 'react';
8
8
  import { ReactNode, Dispatch, SetStateAction } from 'react';
9
9
  import 'socket.io-client';
package/dist/client.js CHANGED
@@ -41,16 +41,16 @@ import {
41
41
  tryCatchSync,
42
42
  validatePagePath,
43
43
  waitForSocket
44
- } from "./chunk-GZEHPARN.js";
44
+ } from "./chunk-VRKJHE7E.js";
45
45
  import {
46
46
  getLogger,
47
47
  registerLogger
48
- } from "./chunk-I4R7DMIX.js";
48
+ } from "./chunk-IJYNVM2W.js";
49
49
  import {
50
50
  getProjectConfig,
51
51
  isProjectConfigRegistered,
52
52
  registerProjectConfig
53
- } from "./chunk-RH65K3US.js";
53
+ } from "./chunk-OWMII7WC.js";
54
54
 
55
55
  // src/apiInterceptors.ts
56
56
  var requestInterceptors = /* @__PURE__ */ new Set();
@@ -101,7 +101,7 @@ async function tryCatch(func, params, context) {
101
101
  const response = await func(params);
102
102
  return [null, response];
103
103
  } catch (error) {
104
- const { captureException } = await import("./sentrySetup-G7NGISBY.js");
104
+ const { captureException } = await import("./sentrySetup-HNIURVUZ.js");
105
105
  captureException(error, context);
106
106
  return [error instanceof Error ? error : new Error(String(error)), null];
107
107
  }
@@ -1,4 +1,8 @@
1
- import './sessionTypes-iyUMf_pf.js';
1
+ import './sessionTypes-DanjuPaE.js';
2
+
3
+ declare const registerPortOverride: (port: number | null) => void;
4
+ declare const getPortOverride: () => number | undefined;
5
+ declare const resetPortOverrideForTests: () => void;
2
6
 
3
7
  interface DeployResourceShape {
4
8
  type: 'redis' | 'mongo';
@@ -133,4 +137,4 @@ type SecretsResolvedListener = (changedKeys: readonly string[] | undefined) => v
133
137
  declare const registerSecretsResolvedListener: (listener: SecretsResolvedListener) => (() => void);
134
138
  declare const notifySecretsResolved: (changedKeys?: readonly string[]) => void;
135
139
 
136
- export { type DeployConfigShape as D, type PresetDefinition as P, type SecretsResolvedListener as S, type DeployDevelopmentShape as a, type DeployEnvironmentShape as b, type DeployResourceShape as c, type DeployRoutingShape as d, type ServiceDefinition as e, type ServicesConfigShape as f, getDeployConfig as g, getServicesConfig as h, isDeployConfigRegistered as i, isServicesConfigRegistered as j, registerSecretsResolvedListener as k, registerServicesConfig as l, notifySecretsResolved as n, registerDeployConfig as r };
140
+ export { type DeployConfigShape as D, type PresetDefinition as P, type SecretsResolvedListener as S, type DeployDevelopmentShape as a, type DeployEnvironmentShape as b, type DeployResourceShape as c, type DeployRoutingShape as d, type ServiceDefinition as e, type ServicesConfigShape as f, getDeployConfig as g, getPortOverride as h, getServicesConfig as i, isDeployConfigRegistered as j, isServicesConfigRegistered as k, registerPortOverride as l, registerSecretsResolvedListener as m, notifySecretsResolved as n, registerServicesConfig as o, resetPortOverrideForTests as p, registerDeployConfig as r };
package/dist/config.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { A as AppConfig, a as AuthProps, B as BaseSessionLayout, f as DEFAULT_PROJECT_CONFIG, J as Jsonify, L as LoggingConfig, P as ProjectConfig, n as ProjectConfigInput, R as RateLimitingConfig, S as SessionConfig, b as SessionLocation, T as TransportConfig, g as getProjectConfig, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-iyUMf_pf.js';
2
- export { S as SecretsResolvedListener, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-DV8DGNt6.js';
1
+ export { A as AppConfig, a as AuthProps, B as BaseSessionLayout, f as DEFAULT_PROJECT_CONFIG, J as Jsonify, L as LoggingConfig, P as ProjectConfig, n as ProjectConfigInput, R as RateLimitingConfig, S as SessionConfig, b as SessionLocation, T as TransportConfig, g as getProjectConfig, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-DanjuPaE.js';
2
+ export { S as SecretsResolvedListener, g as getDeployConfig, h as getPortOverride, i as getServicesConfig, j as isDeployConfigRegistered, k as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, l as registerPortOverride, m as registerSecretsResolvedListener, o as registerServicesConfig, p as resetPortOverrideForTests } from './config-CwW3KY9r.js';
package/dist/config.js CHANGED
@@ -1,22 +1,26 @@
1
1
  import {
2
2
  getDeployConfig,
3
+ getPortOverride,
3
4
  getServicesConfig,
4
5
  isDeployConfigRegistered,
5
6
  isServicesConfigRegistered,
6
7
  notifySecretsResolved,
7
8
  registerDeployConfig,
9
+ registerPortOverride,
8
10
  registerSecretsResolvedListener,
9
- registerServicesConfig
10
- } from "./chunk-6HGH6S36.js";
11
+ registerServicesConfig,
12
+ resetPortOverrideForTests
13
+ } from "./chunk-7VLA5TBT.js";
11
14
  import {
12
15
  DEFAULT_PROJECT_CONFIG,
13
16
  getProjectConfig,
14
17
  isProjectConfigRegistered,
15
18
  registerProjectConfig
16
- } from "./chunk-RH65K3US.js";
19
+ } from "./chunk-OWMII7WC.js";
17
20
  export {
18
21
  DEFAULT_PROJECT_CONFIG,
19
22
  getDeployConfig,
23
+ getPortOverride,
20
24
  getProjectConfig,
21
25
  getServicesConfig,
22
26
  isDeployConfigRegistered,
@@ -24,8 +28,10 @@ export {
24
28
  isServicesConfigRegistered,
25
29
  notifySecretsResolved,
26
30
  registerDeployConfig,
31
+ registerPortOverride,
27
32
  registerProjectConfig,
28
33
  registerSecretsResolvedListener,
29
- registerServicesConfig
34
+ registerServicesConfig,
35
+ resetPortOverrideForTests
30
36
  };
31
37
  //# sourceMappingURL=config.js.map
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { Y as ErrorParam, Z as ErrorResponseInput, _ as NormalizedErrorResponse } from './pageRouteValidation-bz2LFVQq.js';
2
2
  export { A as ApiMethodMap, D as DEFAULT_PAGE_ROUTE_RULES, H as HttpMethodLiteral, $ as INVALID_ERROR_RESPONSE_CODE, L as Logger, a as LoggerContext, N as Notifier, b as NotifyInput, c as NotifyParam, P as PagePathValidationResult, d as PageRouteRules, a0 as QueueDropReason, S as SOCKETSTATUS, a1 as ServiceRouteParseError, a2 as ServiceRouteParseResult, a3 as ServiceRouteParseSuccess, a4 as TransportRouteParseError, a5 as TransportRouteParseResult, a6 as TransportRouteParseSuccess, e as buildApiResponseEventName, f as buildApiStreamEventName, g as buildGetJoinedRoomsResponseEventName, h as buildJoinRoomResponseEventName, i as buildLeaveRoomResponseEventName, j as buildSyncProgressEventName, k as buildSyncResponseEventName, l as clearCsrfToken, a7 as createDevLogger, a8 as defaultHttpStatusForResponse, m as enqueueApiRequest, n as enqueueSyncRequest, o as flushApiQueue, p as flushSyncQueue, q as getApiQueueSize, r as getCsrfToken, s as getLogger, t as getNotifier, u as getRegisteredApiMethod, v as getSyncQueueSize, w as httpFetch, x as incrementResponseIndex, y as isApiMethodMapRegistered, a9 as isErrorParamArray, aa as isLoggerRegistered, z as isOnline, B as normalizeErrorResponseCore, C as notify, E as parseServiceRouteName, ab as parseTransportRouteName, F as registerApiMethodMap, G as registerLogger, I as registerNotifier, J as removeApiQueueItem, K as removeApiQueueItemsByKey, M as removeSyncQueueItem, ac as resetLoggerForTests, O as setSocket, Q as sleep, R as socket, T as socketEventNames, U as statusContent, V as tryCatchSync, W as validatePagePath, X as waitForSocket } from './pageRouteValidation-bz2LFVQq.js';
3
- import { H as HealthHashConfig, D as DeepPartial, B as BaseSessionLayout, a as AuthProps } from './sessionTypes-iyUMf_pf.js';
4
- export { c as ApiConfig, A as AppConfig, d as AuthConfig, e as AuthRateLimitConfig, C as CorsConfig, f as DEFAULT_PROJECT_CONFIG, h as DevConfig, j as HttpConfig, k as HttpStreamConfig, J as Jsonify, L as LoggingConfig, O as OfflineQueueConfig, l as PasswordPolicyConfig, m as PathsConfig, P as ProjectConfig, n as ProjectConfigInput, o as RateLimitIdentity, p as RateLimitIdentityParams, R as RateLimitingConfig, q as SecurityHeadersConfig, S as SessionConfig, b as SessionLocation, s as SocketConfig, t as SyncConfig, u as SyncFlushPressureConfig, v as SyncStreamThrottleConfig, V as ValidationConfig, w as deepMerge, g as getProjectConfig, x as getProjectName, y as isPlainObject, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-iyUMf_pf.js';
3
+ import { H as HealthHashConfig, D as DeepPartial, B as BaseSessionLayout, a as AuthProps } from './sessionTypes-DanjuPaE.js';
4
+ export { c as ApiConfig, A as AppConfig, d as AuthConfig, e as AuthRateLimitConfig, C as CorsConfig, f as DEFAULT_PROJECT_CONFIG, h as DevConfig, j as HttpConfig, k as HttpStreamConfig, J as Jsonify, L as LoggingConfig, O as OfflineQueueConfig, l as PasswordPolicyConfig, m as PathsConfig, P as ProjectConfig, n as ProjectConfigInput, o as RateLimitIdentity, p as RateLimitIdentityParams, R as RateLimitingConfig, q as SecurityHeadersConfig, S as SessionConfig, b as SessionLocation, s as SocketConfig, t as SyncConfig, u as SyncFlushPressureConfig, v as SyncStreamThrottleConfig, V as ValidationConfig, w as deepMerge, g as getProjectConfig, x as getProjectName, y as isPlainObject, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-DanjuPaE.js';
5
5
  export { ApiTypeMap, StreamPayload, SyncTypeMap } from './apiTypeStubs.js';
6
6
  import { Server, Socket } from 'socket.io';
7
- export { D as DeployConfigShape, a as DeployDevelopmentShape, b as DeployEnvironmentShape, c as DeployResourceShape, d as DeployRoutingShape, P as PresetDefinition, S as SecretsResolvedListener, e as ServiceDefinition, f as ServicesConfigShape, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-DV8DGNt6.js';
7
+ export { D as DeployConfigShape, a as DeployDevelopmentShape, b as DeployEnvironmentShape, c as DeployResourceShape, d as DeployRoutingShape, P as PresetDefinition, S as SecretsResolvedListener, e as ServiceDefinition, f as ServicesConfigShape, g as getDeployConfig, h as getPortOverride, i as getServicesConfig, j as isDeployConfigRegistered, k as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, l as registerPortOverride, m as registerSecretsResolvedListener, o as registerServicesConfig, p as resetPortOverrideForTests } from './config-CwW3KY9r.js';
8
8
  import { z } from 'zod';
9
9
  import * as _prisma_client_runtime_library from '@prisma/client/runtime/library';
10
10
  import * as _prisma_client from '@prisma/client';
@@ -86,6 +86,18 @@ declare const ensurePeerDepInstalled: (packageName: string, hint: string, requir
86
86
  */
87
87
  declare const loadPeer: <T>(packageName: string, hint: string, requireFn?: PeerRequire) => T;
88
88
 
89
+ /** Matches `<name>.test|tests|spec.<ts|tsx|js|jsx|mts|cts|mjs|cjs>`. */
90
+ declare const TEST_FILE_PATTERN: RegExp;
91
+ /** Directory names that conventionally hold tests and never runtime modules. */
92
+ declare const TEST_DIRECTORY_NAMES: Set<string>;
93
+ /**
94
+ * True when the given file name (or path — only the last segment is examined)
95
+ * is a test file by the conventions above.
96
+ */
97
+ declare const isTestFile: (fileNameOrPath: string) => boolean;
98
+ /** True when a directory name conventionally holds tests rather than runtime modules. */
99
+ declare const isTestDirectory: (directoryName: string) => boolean;
100
+
89
101
  interface RoomNameFormatterContext {
90
102
  /**
91
103
  * Which room FAMILY the name is being formatted for — a formatter may branch on
@@ -524,7 +536,6 @@ declare const EnvSchema: z.ZodObject<{
524
536
  test: "test";
525
537
  }>>;
526
538
  SERVER_IP: z.ZodDefault<z.ZodString>;
527
- SERVER_PORT: z.ZodDefault<z.ZodString>;
528
539
  SECURE: z.ZodDefault<z.ZodEnum<{
529
540
  true: "true";
530
541
  false: "false";
@@ -542,7 +553,6 @@ declare const env: {
542
553
  [x: string]: unknown;
543
554
  NODE_ENV: "development" | "production" | "test";
544
555
  SERVER_IP: string;
545
- SERVER_PORT: string;
546
556
  SECURE: "true" | "false";
547
557
  REDIS_HOST: string;
548
558
  REDIS_PORT: string;
@@ -903,10 +913,9 @@ interface CsrfCookieOptions {
903
913
  sameSite?: 'strict' | 'lax' | 'none';
904
914
  secure?: boolean;
905
915
  /**
906
- * MUST be false for CSRF cookies — the client needs to read the value
907
- * with JS to attach it as a request header. The framework reads the
908
- * value from the session record (Redis-side) rather than the cookie,
909
- * so this flag is documentation/intent only.
916
+ * Whether the login-absent double-submit cookie is HttpOnly. The client gets
917
+ * the matching header value from the same-origin `GET /auth/csrf` JSON body,
918
+ * so either value works; `false` remains the compatibility default.
910
919
  */
911
920
  httpOnly?: boolean;
912
921
  path?: string;
@@ -987,7 +996,10 @@ interface BindAddress {
987
996
  ip: string;
988
997
  port: number;
989
998
  }
990
- declare const registerBindAddress: (address: BindAddress) => void;
999
+ interface IntendedBindAddress extends BindAddress {
1000
+ configuredPort?: number;
1001
+ }
1002
+ declare const registerBindAddress: (address: IntendedBindAddress) => void;
991
1003
  declare const registerBoundAddress: (address: BindAddress) => void;
992
1004
  declare const getBindAddress: () => {
993
1005
  ip: string;
@@ -1809,4 +1821,4 @@ declare const clearAllHooks: () => void;
1809
1821
  declare const registerSyncHook: <TName extends SyncHookName>(name: TName, handler: SyncHookHandler<SyncHookPayloads[TName]>) => (() => void);
1810
1822
  declare const dispatchSyncHook: <TName extends SyncHookName>(name: TName, payload: SyncHookPayloads[TName]) => void;
1811
1823
 
1812
- export { type ApiAuthRejectedPayload, type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type BootUuidHeartbeat, type CheckRateLimitParams, type CookiePrefixConstraints, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, DEFAULT_REDACTED_LOG_KEYS, DEPTH_TRUNCATED_PLACEHOLDER, type DbHealthCheck, type DbHealthResult, DeepPartial, type DispatchResult, type EmailAttachment, type EmailDeliveryOutcome, type EmailMessage, type EmailResult, type EmailSendContext, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, type FormatterEnvelope, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, HealthHashConfig, type HealthHashDescriptor, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, type OneTimeTokenHandle, PUBLIC_DIR, type PeerRequire, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostAvatarServePayload, type PostErrorNormalizePayload, type PostHttpRequestPayload, type PostSessionRefreshPayload, type PostSyncAuthorizePayload, type PostSyncExecutePayload, type PostSyncFanoutPayload, type PostSyncValidatePayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreAvatarServePayload, type PreCaptureFilter, type PreErrorNormalizePayload, type PreHttpRequestPayload, type PreServerStopPayload, type PreSessionRefreshPayload, type PreSocketMessagePayload, type PreSyncAuthorizePayload, type PreSyncExecutePayload, type PreSyncFanoutPayload, type PreSyncRecipientPayload, type PreSyncValidatePayload, type ProcessUploadInput, type ProcessUploadResult, REDACTED_PLACEHOLDER, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type Registry, type RegistryOptions, type ResolveClientIpParams, type RoomNameFormatter, type RoomNameFormatterContext, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type SessionCreatedPayload, type SessionLogoutInput, type SessionProvider, type SessionRevokedPayload, type SessionSaveResult, type SocketMiddleware, type SpanHandle, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UNKNOWN_CLIENT_IP, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, appendErrorTracker, applyCookiePrefixConstraints, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, consumeOneTimeToken, consumeOneTimeTokenJson, createLocalizedNormalizer, createRegistry, defaultRateLimitStrategy, defaultRedisKeyFormatter, defaultRoomNameFormatter, deriveTokenBucketId, describeHealthHashConfig, dispatchHook, dispatchSyncHook, ensurePeerDepInstalled, env, escapeHtml, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, flushErrorTrackers, formatKey, formatRoomName, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getCurrentErrorTrackerIdentity, getDbHealthCheck, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnv, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRegisteredSessionProvider, getRoomNameFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, hashSynchronizedValueWith, inferHttpMethod, initConsolelog, initSharedSentry, isDbHealthCheckRegistered, isEmailSenderRegistered, isFalsy, isGetMethodName, isLocalizedNormalizerRegistered, isLoopbackIp, isMethodAllowed, isPrismaClientRegistered, isPrismaClientResolvable, isProduction, isProductionRuntime, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isSessionProviderRegistered, isTestRuntime, issueOneTimeToken, listEmailSenderNames, loadEnvFiles, loadPeer, normalizeErrorResponse, normalizeOrigin, oneTimeTokenKey, performLogout, prisma, processUpload, readBootUuid, readSession, rebuildDefaultRedisClient, recordMetricAcrossTrackers, redis, refreshBootUuid, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerBoundAddress, registerCsrfConfig, registerDbHealthCheck, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPreCaptureFilter, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRoomNameFormatter, registerRuntimeMapsProvider, registerSessionProvider, registerSocketMiddleware, registerStrayPrefixCommand, registerSyncAbortController, registerSyncHook, releaseLease, removeSession, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetDbHealthCheckForTests, resetDefaultRedisClient, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resetSessionProviderForTests, resolveClientIp, resolveDevCallbackUrl, resolveEnvKey, resolveErrorMessage, resolveFromRoot, resolveHealthHashConfig, resolveHealthHashConfigFromDescriptor, resolveRuntimeMode, runWithErrorTrackerIdentity, runWithErrorTrackerIdentityScope, sanitizeErrorString, sanitizeErrorStrings, sanitizeForLog, serveAvatar, setCurrentErrorTrackerIdentity, setErrorTrackerUser, setIoInstance, setSentryUser, startBootUuidHeartbeat, startSpan, startSpanAcrossTrackers, startSpanHandle, type syncMessage, tryCatch, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid, writeSession };
1824
+ export { type ApiAuthRejectedPayload, type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type BootUuidHeartbeat, type CheckRateLimitParams, type CookiePrefixConstraints, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, DEFAULT_REDACTED_LOG_KEYS, DEPTH_TRUNCATED_PLACEHOLDER, type DbHealthCheck, type DbHealthResult, DeepPartial, type DispatchResult, type EmailAttachment, type EmailDeliveryOutcome, type EmailMessage, type EmailResult, type EmailSendContext, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, type FormatterEnvelope, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, HealthHashConfig, type HealthHashDescriptor, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, type OneTimeTokenHandle, PUBLIC_DIR, type PeerRequire, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostAvatarServePayload, type PostErrorNormalizePayload, type PostHttpRequestPayload, type PostSessionRefreshPayload, type PostSyncAuthorizePayload, type PostSyncExecutePayload, type PostSyncFanoutPayload, type PostSyncValidatePayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreAvatarServePayload, type PreCaptureFilter, type PreErrorNormalizePayload, type PreHttpRequestPayload, type PreServerStopPayload, type PreSessionRefreshPayload, type PreSocketMessagePayload, type PreSyncAuthorizePayload, type PreSyncExecutePayload, type PreSyncFanoutPayload, type PreSyncRecipientPayload, type PreSyncValidatePayload, type ProcessUploadInput, type ProcessUploadResult, REDACTED_PLACEHOLDER, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type Registry, type RegistryOptions, type ResolveClientIpParams, type RoomNameFormatter, type RoomNameFormatterContext, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type SessionCreatedPayload, type SessionLogoutInput, type SessionProvider, type SessionRevokedPayload, type SessionSaveResult, type SocketMiddleware, type SpanHandle, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TEST_DIRECTORY_NAMES, TEST_FILE_PATTERN, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UNKNOWN_CLIENT_IP, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, appendErrorTracker, applyCookiePrefixConstraints, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, consumeOneTimeToken, consumeOneTimeTokenJson, createLocalizedNormalizer, createRegistry, defaultRateLimitStrategy, defaultRedisKeyFormatter, defaultRoomNameFormatter, deriveTokenBucketId, describeHealthHashConfig, dispatchHook, dispatchSyncHook, ensurePeerDepInstalled, env, escapeHtml, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, flushErrorTrackers, formatKey, formatRoomName, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getCurrentErrorTrackerIdentity, getDbHealthCheck, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnv, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRegisteredSessionProvider, getRoomNameFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, hashSynchronizedValueWith, inferHttpMethod, initConsolelog, initSharedSentry, isDbHealthCheckRegistered, isEmailSenderRegistered, isFalsy, isGetMethodName, isLocalizedNormalizerRegistered, isLoopbackIp, isMethodAllowed, isPrismaClientRegistered, isPrismaClientResolvable, isProduction, isProductionRuntime, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isSessionProviderRegistered, isTestDirectory, isTestFile, isTestRuntime, issueOneTimeToken, listEmailSenderNames, loadEnvFiles, loadPeer, normalizeErrorResponse, normalizeOrigin, oneTimeTokenKey, performLogout, prisma, processUpload, readBootUuid, readSession, rebuildDefaultRedisClient, recordMetricAcrossTrackers, redis, refreshBootUuid, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerBoundAddress, registerCsrfConfig, registerDbHealthCheck, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPreCaptureFilter, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRoomNameFormatter, registerRuntimeMapsProvider, registerSessionProvider, registerSocketMiddleware, registerStrayPrefixCommand, registerSyncAbortController, registerSyncHook, releaseLease, removeSession, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetDbHealthCheckForTests, resetDefaultRedisClient, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resetSessionProviderForTests, resolveClientIp, resolveDevCallbackUrl, resolveEnvKey, resolveErrorMessage, resolveFromRoot, resolveHealthHashConfig, resolveHealthHashConfigFromDescriptor, resolveRuntimeMode, runWithErrorTrackerIdentity, runWithErrorTrackerIdentityScope, sanitizeErrorString, sanitizeErrorStrings, sanitizeForLog, serveAvatar, setCurrentErrorTrackerIdentity, setErrorTrackerUser, setIoInstance, setSentryUser, startBootUuidHeartbeat, startSpan, startSpanAcrossTrackers, startSpanHandle, type syncMessage, tryCatch, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid, writeSession };