@luckystack/core 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +245 -243
- package/LICENSE +21 -21
- package/README.md +93 -93
- package/dist/{chunk-H64EC7Q6.js → chunk-A3VMC576.js} +3 -3
- package/dist/chunk-A3VMC576.js.map +1 -0
- package/dist/{chunk-6BBJ2Q6I.js → chunk-ECWDUWBG.js} +1 -1
- package/dist/chunk-ECWDUWBG.js.map +1 -0
- package/dist/{chunk-TGYBUT4N.js → chunk-ELHTLIEX.js} +1 -1
- package/dist/chunk-ELHTLIEX.js.map +1 -0
- package/dist/client.js +4 -4
- package/dist/client.js.map +1 -1
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +38 -6
- package/dist/index.js.map +1 -1
- package/dist/{projectConfig-FITPOAMC.js → projectConfig-P2AHHQVW.js} +2 -2
- package/dist/{sentrySetup-MHAHHITT.js → sentrySetup-552JVLHJ.js} +2 -2
- package/docs/app-bootstrap.md +418 -418
- package/docs/config-registry.md +348 -348
- package/docs/csrf-config.md +88 -88
- package/docs/error-tracker-registry.md +174 -174
- package/docs/hooks.md +298 -298
- package/docs/rate-limit-strategy.md +170 -170
- package/docs/redis-adapter.md +111 -111
- package/docs/session-types.md +180 -180
- package/docs/socket-bootstrap.md +366 -366
- package/package.json +4 -1
- package/dist/chunk-6BBJ2Q6I.js.map +0 -1
- package/dist/chunk-H64EC7Q6.js.map +0 -1
- package/dist/chunk-TGYBUT4N.js.map +0 -1
- /package/dist/{projectConfig-FITPOAMC.js.map → projectConfig-P2AHHQVW.js.map} +0 -0
- /package/dist/{sentrySetup-MHAHHITT.js.map → sentrySetup-552JVLHJ.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errorTrackerRegistry.ts","../src/redactedLogKeys.ts","../src/loggerRegistry.ts","../src/sentrySetup.ts"],"sourcesContent":["//? Backend-agnostic error-tracker registry. Lives in @luckystack/core\r\n//? (not @luckystack/error-tracking) because framework code in core /\r\n//? api / sync needs to call it without taking a hard dep on a specific\r\n//? observability backend. The error-tracking package provides adapter\r\n//? IMPLEMENTATIONS (Sentry, Datadog, PostHog); the slot lives here.\r\n//?\r\n//? Multiple adapters can be registered at once via `registerErrorTrackers([...])` —\r\n//? every event fans out to all of them. Per-adapter errors are swallowed\r\n//? so one buggy tracker can't break the chain.\r\n\r\n//? SERVER-ONLY import. This static import of `node:async_hooks` is intentional\r\n//? and MUST remain externalized in any client-bundle build config (Vite/esbuild\r\n//? `external: ['node:async_hooks']`). Although AsyncLocalStorage is never\r\n//? constructed at module-eval time (the lazy `getIdentityStore()` guard handles\r\n//? that), the import itself will cause a runtime error in the browser if the\r\n//? bundler does not externalize it. Do NOT statically import this file from\r\n//? browser-bundle entry points — use `@luckystack/core/client` instead.\r\nimport * as nodeAsyncHooks from 'node:async_hooks';\r\nimport { getRedactedLogKeys, REDACTED_PLACEHOLDER, sanitizeForLog } from './redactedLogKeys';\r\nimport { getLogger } from './loggerRegistry';\r\n\r\nexport type ErrorTrackerContext = Record<string, unknown>;\r\n\r\n//? SYNC-17 defense-in-depth: scrub registered redacted keys (tokens, passwords,\r\n//? auth/cookie headers) from the context object before it fans out to any\r\n//? adapter, so a raw token nested in a capture context never reaches Sentry /\r\n//? Datadog / PostHog breadcrumbs. Returns the original reference when there is\r\n//? nothing to sanitize (no context) to avoid needless allocation on the hot path.\r\nconst sanitizeContext = (context?: ErrorTrackerContext): ErrorTrackerContext | undefined => {\r\n if (!context) return context;\r\n return sanitizeForLog(context) as ErrorTrackerContext;\r\n};\r\n\r\n//? ET-O2 value-level scrub: error.message and error.stack can contain\r\n//? interpolated secrets (\"token=abc123\") that bypass the key-based context\r\n//? scrub. Replace `key=value` and `key: value` patterns for every registered\r\n//? redacted key so the string never reaches an adapter verbatim.\r\n//? Runs at capture time (not on every Error construction) to stay off the\r\n//? hot path; the result is cached on the sanitized context so each adapter\r\n//? fan-out step reads the same pre-scrubbed string.\r\n//? BOOT-TIME ONLY: `registerRedactedLogKeys` must be called at boot with a\r\n//? static key set (framework package init / project boot). Calling it with\r\n//? dynamically generated strings (route names, tenant IDs, etc.) at request\r\n//? time will cause this cache to grow without bound. A hard cap (200 entries)\r\n//? logs a warning when exceeded so misuse is immediately diagnosable.\r\nconst REDACTED_VALUE_RE_CACHE_MAX = 200;\r\ninterface ScrubPatternPair {\r\n urlParam: RegExp; // matches <key>=<value> (URL-param style)\r\n logLabel: RegExp; // matches <key>: <value> (log-label style)\r\n}\r\nconst REDACTED_VALUE_RE_CACHE = new Map<string, ScrubPatternPair>();\r\n\r\nconst buildScrubPattern = (key: string): ScrubPatternPair => {\r\n const cached = REDACTED_VALUE_RE_CACHE.get(key);\r\n if (cached) return cached;\r\n if (REDACTED_VALUE_RE_CACHE.size >= REDACTED_VALUE_RE_CACHE_MAX) {\r\n try {\r\n getLogger().warn(`errorTracker: REDACTED_VALUE_RE_CACHE exceeded ${String(REDACTED_VALUE_RE_CACHE_MAX)} entries — registerRedactedLogKeys must only be called at boot with a static key set, not with dynamic/runtime-varying strings.`);\r\n } catch {\r\n // last-resort swallow\r\n }\r\n }\r\n const escaped = key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\r\n //? Two separate passes — one for `key=value` (URL-param style), one for\r\n //? `key: value` (log-label style) — to avoid alternation complexity and the\r\n //? subtle behaviours that arise when mixing capturing and non-capturing groups\r\n //? inside a single pattern (ET-O2 alternation fix).\r\n //? `+` (not `*`) for the value portion so `key=` with an empty value is NOT\r\n //? replaced (empty parameters are not secrets worth scrubbing and a `*` match\r\n //? would turn innocent `foo=` markers into `[redacted]`).\r\n const pair: ScrubPatternPair = {\r\n urlParam: new RegExp(String.raw`${escaped}=[^\\s,;\"'&]+`, 'gi'),\r\n logLabel: new RegExp(String.raw`${escaped}:\\s*[^\\s,;\"']+`, 'gi'),\r\n };\r\n REDACTED_VALUE_RE_CACHE.set(key, pair);\r\n return pair;\r\n};\r\n\r\nexport const sanitizeErrorString = (value: string): string => {\r\n let result = value;\r\n // Scrub matching `key=value` / `key: value` patterns for every registered\r\n // redacted key so interpolated secrets (e.g. \"token=abc\") never reach an adapter.\r\n for (const key of getRedactedLogKeys()) {\r\n const { urlParam, logLabel } = buildScrubPattern(key);\r\n result = result.replace(urlParam, REDACTED_PLACEHOLDER).replace(logLabel, REDACTED_PLACEHOLDER);\r\n }\r\n return result;\r\n};\r\n\r\n//? Produce a sanitized version of an Error's message and stack so adapters\r\n//? can emit pre-scrubbed strings without touching the original Error object.\r\nexport const sanitizeErrorStrings = (\r\n error: unknown,\r\n): { message: string; stack: string | undefined } | null => {\r\n if (!(error instanceof Error)) return null;\r\n return {\r\n message: sanitizeErrorString(error.message),\r\n stack: error.stack === undefined ? undefined : sanitizeErrorString(error.stack),\r\n };\r\n};\r\n\r\nexport interface ErrorTrackerUser {\r\n id?: string;\r\n email?: string;\r\n username?: string;\r\n [key: string]: unknown;\r\n}\r\n\r\nexport interface ErrorTrackerEvent {\r\n /** When false, the adapter must not forward this event (beforeSend opt-out). */\r\n forwarded: boolean;\r\n kind: 'exception' | 'message';\r\n payload: Record<string, unknown>;\r\n}\r\n\r\n//? `SpanResult<T>` was a conditional unwrap of Promise. It's now an alias\r\n//? for `T` directly because the conditional collapsed to T in both branches\r\n//? and tsup's dts emit choked on the conditional widening. Kept as an\r\n//? exported alias so adapter authors can still annotate with `SpanResult<T>`\r\n//? for documentation intent.\r\nexport type SpanResult<T> = T;\r\n\r\nexport interface ErrorTracker {\r\n /** Human-readable identifier (logs, diagnostics). */\r\n name: string;\r\n captureException: (error: unknown, context?: ErrorTrackerContext) => void;\r\n captureMessage: (\r\n message: string,\r\n level: 'info' | 'warning' | 'error' | 'fatal',\r\n context?: ErrorTrackerContext,\r\n ) => void;\r\n setUser: (user: ErrorTrackerUser | null) => void;\r\n setContext?: (key: string, context: ErrorTrackerContext | null) => void;\r\n startSpan?: <T>(name: string, op: string, fn: () => T) => T;\r\n /**\r\n * Optional handle-style span (#62): open a span NOW and close it later via the\r\n * returned handle's `finish()`. Unlike {@link startSpan} (which wraps a single\r\n * synchronous `fn`), this fits an open-at-preExecute / close-at-postExecute\r\n * request lifecycle that spans awaits. When present, the registry-level\r\n * {@link startSpanHandle} delegates to the FIRST registered adapter that\r\n * implements it so the request span reaches an adapter-only backend\r\n * (Datadog/PostHog/custom), not just the legacy Sentry SDK path.\r\n */\r\n startSpanHandle?: (name: string, op: string) => { finish: () => void };\r\n recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;\r\n beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;\r\n /**\r\n * Optional flush lifecycle hook (error-tracking ET batch). Called by\r\n * {@link flushErrorTrackers} on graceful shutdown so a buffered adapter\r\n * (PostHog batch, Sentry transport) can drain in-flight events before exit.\r\n * Returns a promise that resolves when the adapter has flushed.\r\n */\r\n flush?: () => Promise<void>;\r\n}\r\n\r\n//? ET-02 fix seam: per-event identity carried in AsyncLocalStorage instead of a\r\n//? process-global mutable `currentDistinctId`. The framework's API + sync request\r\n//? handlers open an identity SCOPE at request entry (`runWithErrorTrackerIdentityScope`)\r\n//? — BEFORE the first await that could interleave with another concurrent request —\r\n//? then write the resolved session into it via `setCurrentErrorTrackerIdentity(user)`\r\n//? once `readSession` resolves. Adapters read `getCurrentErrorTrackerIdentity()` at\r\n//? capture time, so two concurrent requests with different users can't cross-attribute\r\n//? events. Lives in core so the registry (and any adapter) can read it without a\r\n//? back-dependency on error-tracking.\r\n//?\r\n//? The store holds a MUTABLE box (not the user directly) so the identity can be set\r\n//? AFTER the scope is entered: AsyncLocalStorage propagates the same box reference to\r\n//? every async child of the scope, and each request gets its own box — so mutating\r\n//? `box.user` post-`await` is visible to that request's captures only, never another's.\r\ninterface IdentityBox {\r\n user: ErrorTrackerUser | null;\r\n}\r\n\r\n//? BROWSER-SAFE LAZY ALS: this registry is reachable from the client bundle, but\r\n//? `node:async_hooks` only exists on the server — vite externalizes it and THROWS\r\n//? on any property access in the browser. So we never construct/touch\r\n//? AsyncLocalStorage at module-eval; we resolve it lazily behind a server guard\r\n//? (`typeof window === 'undefined'`). In the browser the store is null and every\r\n//? identity helper degrades to a no-op / null (the client never needs per-request\r\n//? identity). On the server, behaviour is identical to a top-level store (ET-02).\r\ntype IdentityStore = nodeAsyncHooks.AsyncLocalStorage<IdentityBox>;\r\nlet resolvedIdentityStore: IdentityStore | null | undefined;\r\nconst getIdentityStore = (): IdentityStore | null => {\r\n if (resolvedIdentityStore !== undefined) return resolvedIdentityStore;\r\n //? Server detection via property presence — `'window' in globalThis` is false\r\n //? in Node and avoids the DOM-lib typing that makes `globalThis.window === undefined`\r\n //? a \"no-overlap\" lint error (and avoids a ReferenceError on a bare `window`).\r\n resolvedIdentityStore =\r\n 'window' in globalThis ? null : new nodeAsyncHooks.AsyncLocalStorage<IdentityBox>();\r\n return resolvedIdentityStore;\r\n};\r\n\r\n/**\r\n * Open a per-request error-tracker identity scope and run `fn` inside it. Call at\r\n * request ENTRY (before any await that could interleave with another request); the\r\n * identity starts null and is filled in later via {@link setCurrentErrorTrackerIdentity}\r\n * once the session is known. Each invocation gets an isolated box (ET-02).\r\n */\r\nexport const runWithErrorTrackerIdentityScope = <T>(fn: () => T): T => {\r\n const store = getIdentityStore();\r\n return store ? store.run({ user: null }, fn) : fn();\r\n};\r\n\r\n/**\r\n * Run `fn` with `user` bound as the ambient error-tracker identity (ET-02). Convenience\r\n * wrapper that opens a scope and immediately sets the identity — used where the user is\r\n * already known up front (and by the adapter regression tests).\r\n */\r\nexport const runWithErrorTrackerIdentity = <T>(user: ErrorTrackerUser | null, fn: () => T): T => {\r\n const store = getIdentityStore();\r\n return store ? store.run({ user }, fn) : fn();\r\n};\r\n\r\n/**\r\n * Write `user` into the active identity box opened by {@link runWithErrorTrackerIdentityScope}\r\n * / {@link runWithErrorTrackerIdentity}. No-op when called outside any scope (a background /\r\n * non-request capture has no per-request box; adapters fall back to their own global).\r\n */\r\nexport const setCurrentErrorTrackerIdentity = (user: ErrorTrackerUser | null): void => {\r\n const box = getIdentityStore()?.getStore();\r\n if (box) box.user = user;\r\n};\r\n\r\n/** Read the ambient per-event identity for the active request scope, if any. */\r\nexport const getCurrentErrorTrackerIdentity = (): ErrorTrackerUser | null =>\r\n getIdentityStore()?.getStore()?.user ?? null;\r\n\r\n//? Pre-capture filter (error-tracking ET batch). A registered filter runs on\r\n//? EVERY event just before fan-out; returning `false` DROPS the event entirely\r\n//? (e.g. suppress known-noisy errors, sample, or honour a per-event opt-out).\r\n//? Distinct from a per-adapter `beforeSend` (which transforms a single adapter's\r\n//? payload) — this gates the whole fan-out. Last-write-wins; `null` clears it.\r\nexport type PreCaptureFilter = (event: ErrorTrackerEvent) => boolean;\r\nlet preCaptureFilter: PreCaptureFilter | null = null;\r\n\r\nexport const registerPreCaptureFilter = (filter: PreCaptureFilter | null): void => {\r\n preCaptureFilter = filter;\r\n};\r\n\r\nconst passesPreCaptureFilter = (kind: 'exception' | 'message', payload: Record<string, unknown>): boolean => {\r\n if (!preCaptureFilter) return true;\r\n try {\r\n return preCaptureFilter({ forwarded: true, kind, payload });\r\n } catch {\r\n //? A throwing filter must not swallow telemetry — fail OPEN (forward).\r\n return true;\r\n }\r\n};\r\n\r\n//? ET-O6: the pre-capture filter payload now includes the error / message\r\n//? so filters can drop noisy errors by type/message (the documented use case).\r\n//? Separate helpers per kind so the call site stays type-safe.\r\nconst exceptionFilterPayload = (\r\n error: unknown,\r\n context: ErrorTrackerContext | undefined,\r\n): Record<string, unknown> => ({ error, context: context ?? null });\r\n\r\nconst messageFilterPayload = (\r\n message: string,\r\n level: string,\r\n context: ErrorTrackerContext | undefined,\r\n): Record<string, unknown> => ({ message, level, context: context ?? null });\r\n\r\nlet activeTrackers: ErrorTracker[] = [];\r\n\r\n//? REPLACE semantics — last-write-wins (ET-24 confirmed standard). Use\r\n//? `appendErrorTracker` when you need accumulate-not-replace (ET-05).\r\nexport const registerErrorTracker = (tracker: ErrorTracker): void => {\r\n activeTrackers = [tracker];\r\n};\r\n\r\nexport const registerErrorTrackers = (trackers: ErrorTracker[]): void => {\r\n activeTrackers = [...trackers];\r\n};\r\n\r\n/**\r\n * APPEND primitive (ET-05): add a tracker WITHOUT clobbering already-registered\r\n * ones. Fixes the async-PostHog-vs-consumer-overlay race where the loser of\r\n * `registerErrorTracker`'s replace silently vanished. De-duplicates by `name`\r\n * (re-appending the same adapter name replaces that one entry in place) so a\r\n * lazy adapter that registers a proxy then the real client doesn't double-fire.\r\n */\r\nexport const appendErrorTracker = (tracker: ErrorTracker): void => {\r\n const withoutSameName = activeTrackers.filter((t) => t.name !== tracker.name);\r\n activeTrackers = [...withoutSameName, tracker];\r\n};\r\n\r\nexport const getActiveErrorTrackers = (): ErrorTracker[] => activeTrackers;\r\n\r\n/**\r\n * Flush lifecycle (error-tracking ET batch): drain every adapter that exposes a\r\n * `flush()` before shutdown. Per-adapter failures are swallowed + logged so one\r\n * stuck adapter can't block graceful shutdown. Safe to call when no adapter has\r\n * `flush`.\r\n */\r\nexport const flushErrorTrackers = async (): Promise<void> => {\r\n await Promise.all(\r\n activeTrackers.map(async (tracker) => {\r\n if (!tracker.flush) return;\r\n try {\r\n await tracker.flush();\r\n } catch (error) {\r\n //? ET-O7: route to the shared failure logger (but never re-throw — a\r\n //? failing flush must not block graceful shutdown).\r\n logTrackerFailure(tracker.name, 'flush', error);\r\n }\r\n }),\r\n );\r\n};\r\n\r\nexport const captureExceptionAcrossTrackers = (\r\n error: unknown,\r\n context?: ErrorTrackerContext,\r\n): void => {\r\n const safeContext = sanitizeContext(context);\r\n if (!passesPreCaptureFilter('exception', exceptionFilterPayload(error, safeContext))) return;\r\n for (const tracker of activeTrackers) {\r\n try {\r\n tracker.captureException(error, safeContext);\r\n } catch (trackerError) {\r\n //? Fan-out error logging (error-tracking ET batch): a buggy tracker must\r\n //? not break the chain, but its failure was previously invisible. Log it\r\n //? (best-effort) so a misbehaving adapter is diagnosable.\r\n logTrackerFailure(tracker.name, 'captureException', trackerError);\r\n }\r\n }\r\n};\r\n\r\nexport const captureMessageAcrossTrackers = (\r\n message: string,\r\n level: 'info' | 'warning' | 'error' | 'fatal',\r\n context?: ErrorTrackerContext,\r\n): void => {\r\n const safeContext = sanitizeContext(context);\r\n if (!passesPreCaptureFilter('message', messageFilterPayload(message, level, safeContext))) return;\r\n for (const tracker of activeTrackers) {\r\n try {\r\n tracker.captureMessage(message, level, safeContext);\r\n } catch (trackerError) {\r\n logTrackerFailure(tracker.name, 'captureMessage', trackerError);\r\n }\r\n }\r\n};\r\n\r\n//? Best-effort fan-out failure logger. Wrapped in its own try/catch because the\r\n//? logger itself could throw on a broken sink, and a capture path must never\r\n//? throw.\r\nconst logTrackerFailure = (trackerName: string, op: string, error: unknown): void => {\r\n try {\r\n getLogger().warn(`errorTracker: \"${trackerName}\" threw in ${op}`, { error });\r\n } catch {\r\n // Last-resort swallow.\r\n }\r\n};\r\n\r\nexport const setErrorTrackerUser = (user: ErrorTrackerUser | null): void => {\r\n for (const tracker of activeTrackers) {\r\n try {\r\n tracker.setUser(user);\r\n } catch (error) {\r\n //? ET-O7: route to the shared failure logger so a misbehaving tracker is diagnosable.\r\n logTrackerFailure(tracker.name, 'setUser', error);\r\n }\r\n }\r\n};\r\n\r\nexport const recordMetricAcrossTrackers = (\r\n name: string,\r\n value: number,\r\n tags?: Record<string, string>,\r\n): void => {\r\n for (const tracker of activeTrackers) {\r\n if (tracker.recordMetric) {\r\n try {\r\n tracker.recordMetric(name, value, tags);\r\n } catch (error) {\r\n //? ET-O7: route to the shared failure logger so a misbehaving tracker is diagnosable.\r\n logTrackerFailure(tracker.name, 'recordMetric', error);\r\n }\r\n }\r\n }\r\n};\r\n\r\nexport const startSpanAcrossTrackers = <T>(name: string, op: string, fn: () => T): T => {\r\n //? Spans don't fan out cleanly to multiple backends — they're nested\r\n //? execution scopes. We only invoke the FIRST registered tracker's\r\n //? startSpan (others get notified via captureException paths if they\r\n //? want to instrument). When no tracker supports spans, run the fn directly.\r\n const first = activeTrackers.find((t) => t.startSpan);\r\n if (!first?.startSpan) return fn();\r\n return first.startSpan(name, op, fn);\r\n};\r\n\r\n/**\r\n * Handle-style span (error-tracking ET batch): for callers that can't wrap the\r\n * measured work in a single synchronous `fn` (streaming, cross-await spans).\r\n * Returns a handle whose `finish()` resolves the span's duration. The default\r\n * implementation is a lightweight wall-clock timer (works with any backend);\r\n * an adapter can supply a richer span via the existing `startSpan` callback\r\n * form when it needs native instrumentation. `finish()` is idempotent.\r\n */\r\nexport interface SpanHandle {\r\n /** Stop the span; safe to call more than once (subsequent calls are no-ops). */\r\n finish: () => void;\r\n /** Milliseconds elapsed at the moment `finish()` first ran (0 before). */\r\n readonly durationMs: number;\r\n}\r\n\r\n//? #62: open a handle-style span on the FIRST registered adapter that implements\r\n//? `startSpanHandle`, swallowing a throwing adapter so the wall-clock handle still\r\n//? works (telemetry must never break the request path). Returns `null` when no\r\n//? adapter supports handle-style spans.\r\nconst openDelegateSpanHandle = (name: string, op: string): { finish: () => void } | null => {\r\n const tracker = activeTrackers.find((t) => t.startSpanHandle);\r\n if (!tracker?.startSpanHandle) return null;\r\n try {\r\n return tracker.startSpanHandle(name, op);\r\n } catch (error) {\r\n logTrackerFailure(tracker.name, 'startSpanHandle', error);\r\n return null;\r\n }\r\n};\r\n\r\n//? #62: delegate the handle to the first adapter that supports `startSpanHandle`\r\n//? (so an adapter-only consumer — Datadog/PostHog/custom, no legacy Sentry SDK —\r\n//? actually gets request-timing spans), and ALWAYS keep the wall-clock timer as a\r\n//? backend-agnostic fallback + `durationMs` source. `finish()` is idempotent and\r\n//? closes the delegated span exactly once.\r\n//? ET-O9: removed the per-call `recordMetricAcrossTrackers` that fired on every\r\n//? startSpanHandle invocation — `durationMs` is available for callers that want to\r\n//? emit their own metric.\r\nexport const startSpanHandle = (name: string, op: string): SpanHandle => {\r\n const startedAt = Date.now();\r\n let finishedAt: number | null = null;\r\n const delegate = openDelegateSpanHandle(name, op);\r\n return {\r\n finish: () => {\r\n if (finishedAt !== null) return;\r\n finishedAt = Date.now();\r\n if (delegate) {\r\n try {\r\n delegate.finish();\r\n } catch (error) {\r\n logTrackerFailure('startSpanHandle', 'finish', error);\r\n }\r\n }\r\n },\r\n get durationMs() {\r\n return finishedAt === null ? 0 : finishedAt - startedAt;\r\n },\r\n };\r\n};\r\n","//? Registry of log keys that get masked when the framework prints request /\r\n//? response payloads. Feature packages register their domain-specific\r\n//? sensitive keys at boot (login → `password`, email → `apiKey`, etc.) so a\r\n//? consumer of the package never has to remember to extend a static list in\r\n//? core to keep their own data out of logs.\r\n//?\r\n//? Lookups are case-insensitive. Keys are merged with the framework defaults;\r\n//? `registerRedactedLogKeys` is additive — duplicate calls are idempotent.\r\n\r\nexport const DEFAULT_REDACTED_LOG_KEYS: readonly string[] = [\r\n 'password',\r\n 'confirmpassword',\r\n 'token',\r\n 'newtoken',\r\n 'authorization',\r\n 'cookie',\r\n 'set-cookie',\r\n 'csrftoken',\r\n 'apikey',\r\n 'secret',\r\n];\r\n\r\nconst redactedKeys = new Set<string>(DEFAULT_REDACTED_LOG_KEYS.map((key) => key.toLowerCase()));\r\n\r\n//? Sensitive SUFFIXES (0.2.0 widening). Exact-match alone missed compound keys\r\n//? a project naturally produces — `targetToken`, `sessionToken`, `clientSecret`,\r\n//? `stripeApiKey` — leaving real secrets in logs/breadcrumbs. A key whose\r\n//? lowercased form ENDS WITH one of these is redacted in addition to the exact\r\n//? set above. Suffix (not arbitrary substring) is deliberate: it catches the\r\n//? `<qualifier><Sensitive>` naming convention while a plain `substring` test\r\n//? would over-redact benign keys (e.g. a `tokenCount` metric, a `secretSanta`\r\n//? field). Kept conservative on purpose — register extra keys explicitly via\r\n//? `registerRedactedLogKeys` for project-specific names that don't fit.\r\nconst SENSITIVE_KEY_SUFFIXES: readonly string[] = ['token', 'secret', 'apikey', 'password'];\r\n\r\nexport const registerRedactedLogKeys = (keys: readonly string[]): void => {\r\n for (const key of keys) {\r\n redactedKeys.add(key.toLowerCase());\r\n }\r\n};\r\n\r\nexport const getRedactedLogKeys = (): readonly string[] => [...redactedKeys];\r\n\r\nexport const isRedactedLogKey = (key: string): boolean => {\r\n const lower = key.toLowerCase();\r\n if (redactedKeys.has(lower)) return true;\r\n return SENSITIVE_KEY_SUFFIXES.some((suffix) => lower.endsWith(suffix));\r\n};\r\n\r\n//? Test-only helper — restore the default seed set between integration tests.\r\n//? Never call from production code; framework packages register their own\r\n//? keys at boot and clearing would re-expose them.\r\nexport const resetRedactedLogKeysForTests = (): void => {\r\n redactedKeys.clear();\r\n for (const key of DEFAULT_REDACTED_LOG_KEYS) {\r\n redactedKeys.add(key.toLowerCase());\r\n }\r\n};\r\n\r\n/** Placeholder substituted for a redacted value. */\r\nexport const REDACTED_PLACEHOLDER = '[redacted]';\r\n\r\n//? Distinct from REDACTED_PLACEHOLDER so an over-deep BENIGN value (a numeric id,\r\n//? a timestamp) isn't mislabeled as a masked secret — the marker reads as\r\n//? structural truncation, not redaction.\r\nexport const DEPTH_TRUNCATED_PLACEHOLDER = '[truncated: max depth]';\r\n\r\n//? Depth cap so a self-referential / pathologically deep object can't make the\r\n//? sanitize pass blow the stack on a hot log/capture path.\r\nconst MAX_SANITIZE_DEPTH = 8;\r\n\r\n/**\r\n * Recursively deep-copy `value`, replacing any property whose key matches the\r\n * registered redacted-keys set (case-insensitive) with {@link REDACTED_PLACEHOLDER}\r\n * (SYNC-17 defense-in-depth). Use it at raw log / `captureException` context\r\n * sites so a token/password nested anywhere in a payload never lands verbatim in\r\n * a logger sink or error-tracker breadcrumb.\r\n *\r\n * - Primitives pass through unchanged.\r\n * - Arrays are mapped element-wise (array indices are never \"keys\", so an\r\n * element is only redacted by its own nested object keys).\r\n * - Cycles / over-deep graphs are cut at {@link MAX_SANITIZE_DEPTH} with\r\n * {@link DEPTH_TRUNCATED_PLACEHOLDER}; the input is never mutated.\r\n */\r\nexport const sanitizeForLog = (value: unknown, depth = 0, _seen?: WeakSet<object>): unknown => {\r\n if (depth > MAX_SANITIZE_DEPTH) return DEPTH_TRUNCATED_PLACEHOLDER;\r\n if (value === null || typeof value !== 'object') return value;\r\n\r\n //? Cycle detection: if we have already visited this reference higher up in the\r\n //? call stack, replace it with a marker rather than recursing into it — this\r\n //? prevents a stack overflow when `value` contains a circular reference (e.g.\r\n //? an Error whose `.cause` links back to itself, or a Node IncomingMessage\r\n //? with circular HTTP-socket references).\r\n const seen = _seen ?? new WeakSet<object>();\r\n if (seen.has(value)) return '[circular]';\r\n seen.add(value);\r\n\r\n if (Array.isArray(value)) {\r\n return value.map((item) => sanitizeForLog(item, depth + 1, seen));\r\n }\r\n\r\n const out: Record<string, unknown> = {};\r\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\r\n out[key] = isRedactedLogKey(key) ? REDACTED_PLACEHOLDER : sanitizeForLog(nested, depth + 1, seen);\r\n }\r\n return out;\r\n};\r\n","//? Pluggable logger so framework code does not have to write directly to\r\n//? stdout. The default implementation uses `console.{debug,info,warn,error}`\r\n//? directly with no trailing color-code argument — that argument was only\r\n//? meaningful when `initConsolelog()` (a dev-only monkey-patch) had been\r\n//? installed, but the default logger needs to be safe in production too.\r\n//?\r\n//? Installers route framework logs to Pino / Winston / Datadog / etc. by\r\n//? calling `registerLogger({ debug, info, warn, error })` once at boot,\r\n//? mirroring the rest of the registry pattern in this package. Dev installs\r\n//? that want colored output should call `registerLogger(createDevLogger())`.\r\n//?\r\n//? Read at call-time via `getLogger()` so registration order does not matter\r\n//? — same contract as `getProjectConfig()` and friends.\r\n\r\nimport { createRegistry } from './createRegistry';\r\n\r\nexport type LoggerContext = Record<string, unknown>;\r\n\r\nexport interface Logger {\r\n debug: (message: string, context?: LoggerContext) => void;\r\n info: (message: string, context?: LoggerContext) => void;\r\n warn: (message: string, context?: LoggerContext) => void;\r\n error: (message: string, error?: unknown, context?: LoggerContext) => void;\r\n}\r\n\r\nconst defaultLogger: Logger = {\r\n debug: (message, context) => {\r\n if (context === undefined) {console.debug(message);}\r\n else {console.debug(message, context);}\r\n },\r\n info: (message, context) => {\r\n if (context === undefined) {console.info(message);}\r\n else {console.info(message, context);}\r\n },\r\n warn: (message, context) => {\r\n if (context === undefined) {console.warn(message);}\r\n else {console.warn(message, context);}\r\n },\r\n error: (message, error, context) => {\r\n if (error !== undefined && context !== undefined) console.error(message, error, context);\r\n else if (error !== undefined) console.error(message, error);\r\n else if (context === undefined) {console.error(message);}\r\n else {console.error(message, context);}\r\n },\r\n};\r\n\r\n//? Dev-mode logger factory: wraps the default behavior with the colored\r\n//? terminal output that `initConsolelog()` produces. Install via\r\n//? `registerLogger(createDevLogger())` from a dev-only entry. Note: this\r\n//? still depends on `initConsolelog()` having been called to interpret the\r\n//? trailing color string — without it, the codes will appear as literal\r\n//? text. Use the default logger when running in production or unsure.\r\nexport const createDevLogger = (): Logger => ({\r\n debug: (message, context) => {\r\n if (context === undefined) {console.log(message, 'cyan');}\r\n else {console.log(message, context, 'cyan');}\r\n },\r\n info: (message, context) => {\r\n if (context === undefined) {console.log(message);}\r\n else {console.log(message, context);}\r\n },\r\n warn: (message, context) => {\r\n if (context === undefined) {console.log(message, 'yellow');}\r\n else {console.log(message, context, 'yellow');}\r\n },\r\n error: (message, error, context) => {\r\n if (error !== undefined && context !== undefined) console.log(message, error, context, 'red');\r\n else if (error !== undefined) console.log(message, error, 'red');\r\n else if (context === undefined) {console.log(message, 'red');}\r\n else {console.log(message, context, 'red');}\r\n },\r\n});\r\n\r\nconst registry = createRegistry<Logger>(defaultLogger);\r\n\r\nexport const registerLogger = (logger: Logger): void => {\r\n registry.register(logger);\r\n};\r\n\r\nexport const getLogger = (): Logger => registry.get();\r\n\r\nexport const isLoggerRegistered = (): boolean => registry.isRegistered();\r\n\r\n//? Test-only helper — restore the default logger between integration tests.\r\n//? Never call from production code.\r\nexport const resetLoggerForTests = (): void => {\r\n registry.reset();\r\n};\r\n","//? Legacy Sentry single-instance slot. Predates the multi-adapter\r\n//? ErrorTracker registry. New code should `registerErrorTracker(...)`\r\n//? from `@luckystack/error-tracking`; this file keeps backwards\r\n//? compatibility AND fans out events through any registered adapters so\r\n//? a project can mix legacy `initSharedSentry` + new `registerErrorTracker`\r\n//? during migration.\r\n//?\r\n//? The framework's own call sites (`captureException`, `captureMessage`,\r\n//? `setSentryUser`, `startSpan`) keep working unchanged — they now route\r\n//? through both the legacy slot and the new adapter list.\r\n\r\nimport {\r\n captureExceptionAcrossTrackers,\r\n captureMessageAcrossTrackers,\r\n setErrorTrackerUser,\r\n startSpanAcrossTrackers,\r\n} from './errorTrackerRegistry';\r\nimport { sanitizeForLog } from './redactedLogKeys';\r\n\r\ninterface SentryInstance {\r\n captureException: (exception: unknown, ...args: unknown[]) => string;\r\n captureMessage: (message: string, ...args: unknown[]) => string;\r\n setUser: (user: unknown) => void;\r\n setContext: (key: string, context: unknown) => void;\r\n startInactiveSpan?: (context: unknown) => unknown;\r\n}\r\n\r\nlet sentry: SentryInstance | undefined;\r\n\r\n/**\r\n * @deprecated Use `registerErrorTracker` from `@luckystack/error-tracking` instead.\r\n * Will be removed in a future major version.\r\n */\r\nexport const initSharedSentry = (instance: SentryInstance): void => {\r\n sentry = instance;\r\n};\r\n\r\n//? Pass `extra` inline via Sentry's hint-shaped second argument so contexts\r\n//? don't leak across concurrent captures. Previously we called\r\n//? `sentry.setContext('additional', context)` which is process-global until\r\n//? cleared — under concurrent captures the first context could leak into the\r\n//? second's report.\r\n//? CORE-O4: sanitize context before passing to the legacy Sentry slot so raw\r\n//? tokens/passwords don't reach `extra` when a consumer mixes initSharedSentry\r\n//? with framework capture. The multi-adapter path already sanitizes in\r\n//? errorTrackerRegistry.ts; this brings the legacy path into parity.\r\nexport const captureException = (\r\n error: unknown,\r\n context?: Record<string, unknown>,\r\n): void => {\r\n if (sentry) {\r\n if (context) {\r\n sentry.captureException(error, { extra: sanitizeForLog(context) as Record<string, unknown> });\r\n } else {\r\n sentry.captureException(error);\r\n }\r\n }\r\n //? Multi-adapter fan-out. When no adapter is registered this is a no-op.\r\n captureExceptionAcrossTrackers(error, context);\r\n};\r\n\r\nexport const captureMessage = (\r\n message: string,\r\n level: 'info' | 'warning' | 'error' | 'fatal' = 'info',\r\n context?: Record<string, unknown>,\r\n): void => {\r\n if (sentry) {\r\n if (context) {\r\n sentry.captureMessage(message, { level, extra: sanitizeForLog(context) as Record<string, unknown> });\r\n } else {\r\n sentry.captureMessage(message, level);\r\n }\r\n }\r\n captureMessageAcrossTrackers(message, level, context);\r\n};\r\n\r\nexport const setSentryUser = (user: {\r\n id?: string;\r\n email?: string;\r\n username?: string;\r\n} | null): void => {\r\n sentry?.setUser(user);\r\n setErrorTrackerUser(user);\r\n};\r\n\r\nexport const startSpan = (name: string, op: string): unknown => {\r\n //? Multi-adapter span coordination: first registered adapter that\r\n //? supports spans wins (spans don't fan out cleanly). Legacy Sentry\r\n //? single-instance path still creates the inactive span for backwards\r\n //? compatibility on consumers that haven't migrated.\r\n if (sentry?.startInactiveSpan) {\r\n return sentry.startInactiveSpan({ name, op });\r\n }\r\n //? No-op span body is intentional — this is the legacy inactive-span shape\r\n //? consumers built against; the real span lifecycle is managed elsewhere.\r\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- legacy inactive-span shape\r\n startSpanAcrossTrackers(name, op, () => {});\r\n};\r\n"],"mappings":"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","names":[]}
|
package/dist/client.js
CHANGED
|
@@ -40,16 +40,16 @@ import {
|
|
|
40
40
|
tryCatchSync,
|
|
41
41
|
validatePagePath,
|
|
42
42
|
waitForSocket
|
|
43
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-A3VMC576.js";
|
|
44
44
|
import {
|
|
45
45
|
getProjectConfig,
|
|
46
46
|
isProjectConfigRegistered,
|
|
47
47
|
registerProjectConfig
|
|
48
|
-
} from "./chunk-
|
|
48
|
+
} from "./chunk-ECWDUWBG.js";
|
|
49
49
|
import {
|
|
50
50
|
getLogger,
|
|
51
51
|
registerLogger
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-ELHTLIEX.js";
|
|
53
53
|
import "./chunk-USQA6QTX.js";
|
|
54
54
|
|
|
55
55
|
// src/apiInterceptors.ts
|
|
@@ -506,7 +506,7 @@ async function tryCatch(func, params, context) {
|
|
|
506
506
|
const response = await func(params);
|
|
507
507
|
return [null, response];
|
|
508
508
|
} catch (error) {
|
|
509
|
-
const { captureException } = await import("./sentrySetup-
|
|
509
|
+
const { captureException } = await import("./sentrySetup-552JVLHJ.js");
|
|
510
510
|
captureException(error, context);
|
|
511
511
|
return [error instanceof Error ? error : new Error(String(error)), null];
|
|
512
512
|
}
|
package/dist/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/apiInterceptors.ts","../src/apiRequest.ts","../src/tryCatchClient.ts","../src/localesRegistry.ts","../src/middlewareRegistry.ts","../src/react/sessionContext.ts","../src/react/AvatarProvider.tsx","../src/react/useTheme.ts","../src/react/TranslationProvider.tsx","../src/react/notify.ts","../src/react/Middleware.tsx","../src/react/Router.tsx"],"sourcesContent":["//? EXT-03 — client-side request/response interceptor registry for `apiRequest`.\n//?\n//? `apiRequest` (apiRequest.ts) is a monolithic typed function and Rule 21 /\n//? the `no-unsafe-api-wrappers` lint forbid wrapping it (a wrapper erases the\n//? route/version inference). This registry is the sanctioned escape hatch: a\n//? consumer registers an interceptor here instead of wrapping the call, so the\n//? typed call site is untouched and route/version inference survives.\n//?\n//? Two seams, both fire-and-mutate (NOT replace — the dispatcher reserves a\n//? structured return for control flow):\n//? - request interceptors run just before the socket emit. A handler may\n//? mutate `ctx.data` in place (inject a correlation id / feature-flag\n//? context / tenant header-equivalent) and is observation-only otherwise.\n//? - response interceptors run when the response envelope arrives, before the\n//? awaiting caller resolves. Observation-only (metrics, breadcrumb, custom\n//? logging). The envelope is NOT mutated by this seam — response rewriting\n//? belongs on the server `preApiRespond` / `transformApiResponse` hooks.\n//?\n//? Handlers are best-effort: a throw / rejection is caught + logged and never\n//? breaks the request pipeline. Both `register*` functions return an\n//? unsubscribe closure, matching `registerClientHook`'s contract.\n\nexport interface ApiRequestInterceptorContext {\n /** Resolved route name (`<page>/<api>` or `system/<api>`). */\n name: string;\n /** Route version literal (e.g. `'v1'`). */\n version: string;\n /**\n * Outgoing request payload. Mutate this object in place to augment the\n * request (correlation id, feature-flag context, ...). Replacing the\n * reference has no effect — mutate keys on the existing object.\n */\n data: Record<string, unknown>;\n}\n\nexport interface ApiResponseInterceptorContext {\n name: string;\n version: string;\n /** The response envelope the caller is about to receive (read-only intent). */\n response: { status: 'success' | 'error'; [key: string]: unknown };\n}\n\nexport type ApiRequestInterceptor = (\n ctx: ApiRequestInterceptorContext,\n) => void | Promise<void>;\n\nexport type ApiResponseInterceptor = (\n ctx: ApiResponseInterceptorContext,\n) => void | Promise<void>;\n\nimport { getLogger } from './loggerRegistry';\n\nconst requestInterceptors = new Set<ApiRequestInterceptor>();\nconst responseInterceptors = new Set<ApiResponseInterceptor>();\n\n/**\n * Register an outgoing-request interceptor. Returns an unsubscribe function.\n * Multiple interceptors run in registration order; each may mutate `ctx.data`.\n */\nexport function registerApiRequestInterceptor(interceptor: ApiRequestInterceptor): () => void {\n requestInterceptors.add(interceptor);\n return () => {\n requestInterceptors.delete(interceptor);\n };\n}\n\n/**\n * Register an incoming-response interceptor. Returns an unsubscribe function.\n */\nexport function registerApiResponseInterceptor(interceptor: ApiResponseInterceptor): () => void {\n responseInterceptors.add(interceptor);\n return () => {\n responseInterceptors.delete(interceptor);\n };\n}\n\n//? Framework-internal — invoked by `apiRequest`. Awaited so a handler can do an\n//? async lookup (read a flag store, mint a trace id) before the emit. Snapshot\n//? the set so a handler unregistering mid-dispatch can't skip a sibling.\nexport async function dispatchApiRequestInterceptors(\n ctx: ApiRequestInterceptorContext,\n): Promise<void> {\n if (requestInterceptors.size === 0) return;\n //? Snapshot so a handler that unregisters itself (or a sibling) mid-dispatch\n //? doesn't skip an un-visited handler.\n const interceptors = [...requestInterceptors];\n for (const interceptor of interceptors) {\n try {\n await interceptor(ctx);\n } catch (error) {\n getLogger().error('[apiRequestInterceptor] handler threw', error);\n }\n }\n}\n\n//? Framework-internal — invoked by `apiRequest` when the response arrives.\n//? Fire-and-forget (the caller does not await) so an interceptor can't delay\n//? the resolve; async rejections are caught + logged.\nexport function dispatchApiResponseInterceptors(\n ctx: ApiResponseInterceptorContext,\n): void {\n if (responseInterceptors.size === 0) return;\n //? Snapshot (see `dispatchApiRequestInterceptors`).\n const interceptors = [...responseInterceptors];\n for (const interceptor of interceptors) {\n try {\n const result = interceptor(ctx);\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\n (result as Promise<unknown>).catch((error: unknown) => {\n getLogger().error('[apiResponseInterceptor] async handler rejected', error);\n });\n }\n } catch (error) {\n getLogger().error('[apiResponseInterceptor] handler threw', error);\n }\n }\n}\n\n/** Test helper — drop every registered interceptor. Not part of the public API. */\nexport function _resetApiInterceptorsForTests(): void {\n requestInterceptors.clear();\n responseInterceptors.clear();\n}\n","import { getProjectConfig } from \"./projectConfig\";\nimport { getLogger } from \"./loggerRegistry\";\nimport { getRegisteredApiMethod } from \"./apiMethodMapRegistry\";\nimport { isGetMethodName } from \"./httpApiUtils\";\nimport { incrementResponseIndex, socket, waitForSocket } from \"./socketState\";\nimport type { ApiTypeMap, StreamPayload } from './apiTypeStubs';\nimport { notify } from \"./notifier\";\nimport { enqueueApiRequest, isOnline, removeApiQueueItem } from \"./offlineQueue\";\nimport { Socket } from \"socket.io-client\";\nimport { normalizeErrorResponseCore } from \"./responseNormalizer\";\nimport { parseServiceRouteName } from \"./serviceRoute\";\nimport tryCatchSync from \"./tryCatchSync\";\nimport {\n buildApiResponseEventName,\n buildApiStreamEventName,\n socketEventNames,\n} from \"./socketEvents\";\nimport {\n dispatchApiRequestInterceptors,\n dispatchApiResponseInterceptors,\n} from \"./apiInterceptors\";\n\n//? Abort controller logic:\n//? - abortable: true → always use abort controller\n//? - abortable: false → never use abort controller\n//? - abortable: undefined → use abort controller for GET APIs (from generated types)\n//? Keyed by `fullName` + a stable hash of the request `data` so the\n//? replace-previous (dedupe) semantics only fire for an IDENTICAL in-flight\n//? call. Two components polling the same GET route with DIFFERENT params no\n//? longer abort each other (each gets its own bucket); a genuine duplicate\n//? still supersedes the prior one.\n//? Size-capped at 1000 entries. All normal code paths (resolve, abort, disconnect,\n//? timeout) call `cleanupAbortController()` which deletes the entry. The cap\n//? is a safety net for edge cases where cleanup is skipped (socket torn down\n//? without a `disconnect` event, or a future code path that forgets to clean up).\nconst ABORT_CONTROLLERS_MAX = 1000;\nconst abortControllers = new Map<string, AbortController>();\n\n//? Stable, order-insensitive JSON stringify so `{a:1,b:2}` and `{b:2,a:1}`\n//? hash to the same abort-controller key. Falls back to a non-deduping unique\n//? token when the payload can't be serialised (circular ref), which is the\n//? safe choice — an unserialisable payload should never silently abort a\n//? sibling request.\nconst stableStringify = (value: unknown): string => {\n const seen = new WeakSet<object>();\n const walk = (input: unknown): unknown => {\n if (input === null || typeof input !== 'object') return input;\n if (seen.has(input)) return '[circular]';\n seen.add(input);\n if (Array.isArray(input)) return input.map((entry) => walk(entry));\n const record = input as Record<string, unknown>;\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(record).toSorted()) {\n out[key] = walk(record[key]);\n }\n return out;\n };\n const [error, json] = tryCatchSync(() => JSON.stringify(walk(value)));\n if (error || typeof json !== 'string') {\n return `__unserializable__:${String(Date.now())}-${String(Math.random())}`;\n }\n return json;\n};\n\nconst buildAbortKey = (fullName: string, data: unknown): string =>\n `${fullName}::${stableStringify(data)}`;\n\nexport type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;\n\n/**\n * Check if an API is a GET method.\n *\n * Primary path: consult the registered `apiMethodMap` (generated from the\n * actual handler's `httpMethod` export or name-based inference at codegen\n * time). Falls back to name-prefix heuristic only when the map isn't\n * registered yet — typically because the consumer hasn't called\n * `registerApiMethodMap(...)` from their `socketInitializer.ts` boot file.\n */\nconst isGetMethodByPrefix = (apiName: string): boolean => isGetMethodName(apiName.toLowerCase());\n\nconst resolveGetMethod = (apiName: string, version: string): { isGet: boolean; usingHeuristic: boolean } => {\n //? Resolved-name shape: `pagePath/apiName`. apiMethodMap is nested by\n //? pagePath → apiName → version. Split at the last `/` so multi-segment\n //? page paths (e.g. 'admin/users') still resolve.\n const lastSlash = apiName.lastIndexOf('/');\n if (lastSlash > 0) {\n const pagePath = apiName.slice(0, lastSlash);\n const leaf = apiName.slice(lastSlash + 1);\n const method = getRegisteredApiMethod(pagePath, leaf, version);\n if (method) return { isGet: method === 'GET', usingHeuristic: false };\n }\n return { isGet: isGetMethodByPrefix(apiName), usingHeuristic: true };\n};\n\nconst canSendNow = (socketInstance: Socket) => {\n if (!socketInstance.connected) return false;\n return isOnline();\n};\n\nconst createQueueId = () => {\n return `${String(Date.now())}-${String(Math.random())}`;\n};\n\n//? Resolve logging flags at call time so registration order doesn't matter.\n//? `getLogging()` is called inside each handler instead of captured at load.\nconst getLogging = () => getProjectConfig().logging;\nconst shouldLogDev = () => getLogging().devLogs;\nconst shouldNotifyDev = () => getLogging().devNotifications;\nconst shouldLogStream = () => getLogging().stream;\n\nconst shouldUseAbortController = ({\n abortable,\n isGet,\n usingHeuristic,\n}: {\n abortable: boolean | undefined;\n isGet: boolean;\n usingHeuristic: boolean;\n}) => {\n if (abortable === true) return true;\n if (abortable === false) return false;\n //? CORE-O15: when `abortable` is unspecified and we are relying on the\n //? name-prefix heuristic (apiMethodMap not registered), default to\n //? non-abortable. A mis-named POST (e.g. `getReport`) would otherwise\n //? silently abort an identical in-flight call it has nothing to do with.\n //? Once `registerApiMethodMap` is called (normal boot), the generated map\n //? resolves the method and the heuristic path is never taken.\n if (usingHeuristic) return false;\n return isGet;\n};\n\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Type Helpers\n// ═══════════════════════════════════════════════════════════════════════════════\n\n// Check if data input is required (i.e., T does NOT allow empty object)\n// Unions like {a:1} | {b:1} do NOT allow {}, so data will be required\ntype DataRequired<T> = Record<string, never> extends T ? false : true;\n\ntype UnionToIntersection<U> =\n (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void)\n ? I\n : never;\n\n// ═══════════════════════════════════════════════════════════════════════════════\n// Global API Params - Union of ALL valid API calls with proper data enforcement\n// ═══════════════════════════════════════════════════════════════════════════════\ntype ApiRouteRecord = UnionToIntersection<{\n [P in keyof ApiTypeMap]: {\n [N in keyof ApiTypeMap[P] as P extends 'root'\n ? `system/${Extract<N, string>}`\n : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N]\n }\n}[keyof ApiTypeMap]>;\n\ntype ApiFullName = Extract<keyof ApiRouteRecord, string>;\ntype VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;\n\n// Force expansion of types to clear aliases in tooltips\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n// Get input type for an API name (union if exists on multiple pages)\ntype InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { input: infer I }\n ? I\n : never;\n\n// Get output type for an API name (union if exists on multiple pages)\ntype OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { output: infer O }\n ? O\n : never;\n\ntype StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { stream: infer S }\n ? S\n : never;\n\ntype ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> =\n [StreamForFullName<F, V>] extends [never]\n ? never\n : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;\n\n// Build params type for a specific API name\ntype ApiParamsForFullName<\n F extends ApiFullName,\n V extends VersionsForFullName<F>\n> = DataRequired<InputForFullName<F, V>> extends true\n ? { name: F; version: V; data: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; }\n : { name: F; version: V; data?: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; };\n\ninterface RuntimeApiParams {\n name?: string;\n version?: string;\n data?: unknown;\n abortable?: boolean;\n disableErrorMessage?: boolean;\n onStream?: (event: ApiStreamEvent) => void;\n signal?: AbortSignal;\n /** Per-call response timeout (ms). Overrides `api.requestTimeoutMs`. `false` disables. */\n timeoutMs?: number | false;\n}\n\nexport interface ApiErrorResponse extends Record<string, unknown> {\n status: 'error';\n httpStatus: number;\n message: string;\n errorCode: string;\n errorParams?: { key: string; value: string | number | boolean }[];\n}\n\ninterface ApiSuccessResponse extends Record<string, unknown> {\n status: 'success';\n httpStatus: number;\n}\n\ntype ApiResponse = ApiErrorResponse | ApiSuccessResponse;\n\nconst normalizeApiError = ({\n response,\n fallbackErrorCode,\n fallbackHttpStatus = 500,\n}: {\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number; message?: string };\n fallbackErrorCode: string;\n fallbackHttpStatus?: number;\n}): ApiErrorResponse => {\n const normalized = normalizeErrorResponseCore({\n response,\n fallbackErrorCode,\n fallbackHttpStatus,\n resolveMessage: ({ errorCode }) => {\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\n return response.message;\n }\n return errorCode;\n },\n });\n\n return {\n status: 'error',\n message: normalized.message,\n errorCode: normalized.errorCode,\n errorParams: normalized.errorParams,\n httpStatus: normalized.httpStatus ?? fallbackHttpStatus,\n };\n};\n\n/**\n * Type-safe API request function.\n * \n * @example\n * ```typescript\n * // Full name usage - includes page in the name\n * const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });\n * // result is typed correctly for publicApi\n * \n * // Global APIs use service-first naming\n * await apiRequest({ name: 'system/session', version: 'v1' });\n * ```\n */\n\nexport function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(\n params: ApiParamsForFullName<F, V>\n): Promise<Prettify<OutputForFullName<F, V>> | ApiErrorResponse> {\n type RequestOutput = Prettify<OutputForFullName<F, V> & ApiResponse>;\n const runtimeParams = params as RuntimeApiParams;\n const { name, version, disableErrorMessage = false, onStream, signal: externalSignal, timeoutMs } = runtimeParams;\n const payloadData = runtimeParams.data;\n\n return new Promise<RequestOutput>((resolve) => {\n void (async () => {\n //? B1 — if the consumer-supplied signal is already aborted at call\n //? time, short-circuit before we even touch the socket. Mirrors the\n //? `fetch(url, { signal })` contract.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n if (!name || typeof name !== \"string\") {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid name\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidName' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidName' },\n fallbackErrorCode: 'api.invalidName',\n }) as RequestOutput);\n return;\n }\n\n if (!version || typeof version !== 'string') {\n if (shouldLogDev()) {\n getLogger().error(\"apiRequest: Invalid version\");\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'api.invalidVersion' });\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.invalidVersion' },\n fallbackErrorCode: 'api.invalidVersion',\n }) as RequestOutput);\n return;\n }\n\n const parsedRoute = parseServiceRouteName(name);\n if (parsedRoute.status === 'error') {\n if (shouldLogDev()) {\n getLogger().error(`[apiRequest] Invalid service route name`, undefined, { name, reason: parsedRoute.reason });\n }\n if (shouldNotifyDev()) {\n notify.error({ key: 'routing.invalidServiceRouteName' });\n }\n resolve(normalizeErrorResponseCore({\n response: {\n status: 'error',\n errorCode: 'routing.invalidServiceRouteName',\n errorParams: [{ key: 'name', value: name }],\n },\n fallbackErrorCode: 'routing.invalidServiceRouteName',\n }) as RequestOutput);\n return;\n }\n\n const sanitizedName = parsedRoute.normalizedRouteName;\n\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\n\n if (!await waitForSocket()) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n if (!socket) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\n fallbackErrorCode: 'api.ioUnavailable',\n }) as RequestOutput);\n return;\n }\n\n //? Abort controller logic:\n //? - abortable: true → always use abort controller\n //? - abortable: false → never use abort controller\n //? - abortable: undefined → smart default (GET-like APIs get abort controller)\n //? Pass the full pagePath/apiName so the registry lookup can resolve via\n //? the generated apiMethodMap (falls back to the leaf-name prefix heuristic\n //? when the map isn't registered yet).\n const { isGet, usingHeuristic } = resolveGetMethod(sanitizedName, version);\n const useAbortController = shouldUseAbortController({\n abortable: runtimeParams.abortable,\n isGet,\n usingHeuristic,\n });\n const fullName = `api/${sanitizedName}/${version}`;\n //? Per-payload abort key: only an identical in-flight call to the same\n //? route supersedes the previous one (see `buildAbortKey` rationale).\n const abortKey = buildAbortKey(fullName, data);\n\n let signal: AbortSignal | null = null;\n let abortHandler: (() => void) | null = null;\n let queueId: string | null = null;\n let cleanupStreamListener: (() => void) | null = null;\n //? Response listener cleanup, registered once the `.once` handler is\n //? attached in `runRequest`. Lets the abort path proactively `off()` the\n //? lingering response listener instead of waiting for a response that was\n //? cancelled and will never arrive.\n let cleanupResponseListener: (() => void) | null = null;\n //? Response-timeout timer. Armed right after the request is emitted so a\n //? lost response (server restart/crash between emit and reply) settles the\n //? promise instead of hanging the awaiting caller forever.\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\n const clearResponseTimeout = () => {\n if (responseTimeout) {\n clearTimeout(responseTimeout);\n responseTimeout = null;\n }\n };\n\n const cleanupAbortController = () => {\n if (signal && abortHandler) {\n signal.removeEventListener(\"abort\", abortHandler);\n }\n abortControllers.delete(abortKey);\n };\n\n if (useAbortController) {\n if (abortControllers.has(abortKey)) {\n const prevAbortController = abortControllers.get(abortKey);\n prevAbortController?.abort();\n }\n //? Evict the oldest entry when the cap is reached (LRU-insert-order).\n //? Map preserves insertion order; the first key is the oldest in-flight entry.\n if (!abortControllers.has(abortKey) && abortControllers.size >= ABORT_CONTROLLERS_MAX) {\n const oldest = abortControllers.keys().next().value;\n if (oldest !== undefined) abortControllers.delete(oldest);\n }\n const abortController = new AbortController();\n abortControllers.set(abortKey, abortController);\n signal = abortController.signal;\n\n abortHandler = () => {\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n //? Resolve (not reject) with the same `request.aborted` envelope the\n //? external-signal abort path uses. Rejecting here produced an\n //? unhandled rejection for fire-and-forget GET calls that get\n //? superseded by the default replace-previous behaviour.\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n\n signal.addEventListener(\"abort\", abortHandler);\n }\n\n const runRequest = (socketInstance: Socket) => {\n if (!canSendNow(socketInstance)) {\n queueId ??= createQueueId();\n //? For the immediate reject-new path (`drop-newest` / `reject`\n //? policy) THIS item's `onDrop` fires SYNCHRONOUSLY inside\n //? `enqueueApiRequest`. We keep surfacing that as the historical\n //? `offline.queueFull` via the `!enqueued` branch, so suppress the\n //? `onDrop` resolve during the synchronous enqueue window and re-arm\n //? it only if the item was actually queued (so a LATER async eviction\n //? — drop-oldest by a future enqueue, or age expiry — still settles).\n let suppressOnDrop = true;\n //? `enqueueApiRequest` returns `false` when the offline queue is\n //? full and `dropPolicy: 'reject'` is active. Without honoring\n //? that signal the outer promise never settles and the caller\n //? awaits forever. Resolve with a normalized error envelope so\n //? callers can branch.\n const enqueued = enqueueApiRequest({\n id: queueId,\n key: fullName,\n run: (nextSocket) => {\n runRequest(nextSocket);\n },\n createdAt: Date.now(),\n //? CORE: mirror SYNC-09. Settle the awaiting caller when this\n //? QUEUED request is later EVICTED (drop-oldest by a future\n //? enqueue / age expiry) instead of ever running. Without it the\n //? promise hangs forever, since `run` is the only thing that could\n //? otherwise resolve it — `apiRequest` had drifted from the sibling\n //? `syncRequest` which already had this fix. Also tear down the\n //? abort-controller registry entry so it isn't leaked.\n onDrop: (reason) => {\n if (suppressOnDrop) return;\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\n fallbackErrorCode: 'offline.dropped',\n }) as RequestOutput);\n },\n });\n //? Past the synchronous enqueue window: if the item is sitting in the\n //? queue, a future eviction should settle via `onDrop`.\n suppressOnDrop = false;\n if (!enqueued) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'offline.queueFull', httpStatus: 503 },\n fallbackErrorCode: 'offline.queueFull',\n fallbackHttpStatus: 503,\n }) as RequestOutput);\n } else if (externalSignal && queueId) {\n //? CORE-O14: if an external AbortSignal fires while the item is\n //? sitting in the offline queue, its listener inside runRequest has\n //? not been registered yet. Register a one-shot handler here so the\n //? abort is not silently ignored and the queue slot is reclaimed.\n const capturedQueueId = queueId;\n const queueAbortHandler = () => {\n removeApiQueueItem(capturedQueueId);\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', queueAbortHandler, { once: true });\n }\n return;\n }\n\n if (signal?.aborted) {\n return;\n }\n\n const tempIndex = incrementResponseIndex();\n socketInstance.emit(socketEventNames.apiRequest, { name: fullName, data, responseIndex: tempIndex });\n\n //? B1 — bridge the consumer's AbortSignal to the server. When the\n //? signal fires we emit `apiCancel { responseIndex }` so the server\n //? handler stops emitting new chunks. We also resolve locally with\n //? `request.aborted` so the awaiting caller settles (vs awaiting\n //? a response that will never arrive because we cancelled it).\n let cleanupExternalAbort: (() => void) | null = null;\n if (externalSignal) {\n const externalAbortHandler = () => {\n clearResponseTimeout();\n socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupAbortController();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n };\n externalSignal.addEventListener('abort', externalAbortHandler);\n cleanupExternalAbort = () => {\n externalSignal.removeEventListener('abort', externalAbortHandler);\n };\n }\n\n //? Arm the response timeout. A per-call `timeoutMs` overrides the\n //? `api.requestTimeoutMs` config default; either may be `false` to\n //? disable. On expiry we tear down every listener/controller and settle\n //? with a 504 envelope so the awaiting caller never hangs.\n clearResponseTimeout();\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\n responseTimeout = setTimeout(() => {\n responseTimeout = null;\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n cleanupAbortController();\n if (queueId) {\n removeApiQueueItem(queueId);\n }\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.timeout', httpStatus: 504 },\n fallbackErrorCode: 'api.timeout',\n fallbackHttpStatus: 504,\n }) as RequestOutput);\n }, effectiveTimeoutMs);\n }\n\n if (typeof onStream === 'function') {\n const streamEventName = buildApiStreamEventName(tempIndex);\n const streamListener = (streamPayload: ApiStreamEvent) => {\n if (signal?.aborted) {\n return;\n }\n\n if (shouldLogStream()) {\n getLogger().debug(`Server API Stream(${String(tempIndex)})`, { APINAME: sanitizedName, streamPayload });\n }\n\n onStream(streamPayload);\n };\n\n socketInstance.on(streamEventName, streamListener);\n cleanupStreamListener = () => {\n socketInstance.off(streamEventName, streamListener);\n };\n }\n\n if (shouldLogDev()) {\n getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });\n }\n\n //? Inside the handler we type as the runtime envelope (ApiResponse).\n //? Project-specific output narrowing only matters at the call site,\n //? where `RequestOutput = OutputForFullName<F,V> & ApiResponse` is\n //? returned via `resolve(... as RequestOutput)`. When core is type-\n //? checked in isolation (e.g. per-package tsup dts build), ApiTypeMap\n //? is empty so OutputForFullName collapses to never; using ApiResponse\n //? here keeps the body type-safe in both cases.\n const responseEventName = buildApiResponseEventName(tempIndex);\n const responseListener = (response: ApiResponse) => {\n if (signal?.aborted) {\n clearResponseTimeout();\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n return;\n }\n\n clearResponseTimeout();\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n //? Call cleanup (not just null) so that any watcher layered on top of\n //? cleanupResponseListener (e.g. the CORE-O13 disconnect watcher) is\n //? properly torn down when a normal response arrives.\n cleanupResponseListener?.();\n cleanupResponseListener = null;\n\n const status = response.status;\n\n //? EXT-03 — fire client response interceptors when the envelope\n //? arrives (observation-only: metrics, breadcrumb, custom logging).\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\n\n if (shouldLogDev()) {\n getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });\n }\n\n if (status === \"error\") {\n const normalizedError = normalizeErrorResponseCore({ response });\n\n if (!disableErrorMessage) {\n if (normalizedError.errorCode) {\n notify.error({ key: normalizedError.errorCode, params: normalizedError.errorParams });\n } else {\n notify.error({ key: normalizedError.message });\n }\n }\n\n Object.assign(response, normalizedError);\n cleanupAbortController();\n resolve(response as RequestOutput);\n return;\n }\n\n cleanupAbortController();\n\n resolve(response as RequestOutput);\n };\n\n socketInstance.once(responseEventName, responseListener);\n cleanupResponseListener = () => {\n socketInstance.off(responseEventName, responseListener);\n };\n\n //? CORE-O13: when timeout is disabled AND there is no abort controller\n //? a server restart between emit and reply leaves this promise pending\n //? forever, leaking the response listener. The disconnect event is the\n //? authoritative signal that no reply will ever arrive on this socket.\n const disconnectHandler = () => {\n if (cleanupResponseListener) {\n cleanupResponseListener();\n cleanupResponseListener = null;\n }\n cleanupStreamListener?.();\n cleanupStreamListener = null;\n cleanupExternalAbort?.();\n cleanupExternalAbort = null;\n clearResponseTimeout();\n cleanupAbortController();\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'api.disconnected' },\n fallbackErrorCode: 'api.disconnected',\n }) as RequestOutput);\n };\n socketInstance.once('disconnect', disconnectHandler);\n //? Wrap the response listener to also remove the disconnect watcher on\n //? a normal reply (avoids a dangling listener on every successful call).\n const originalCleanupResponseListener = cleanupResponseListener;\n cleanupResponseListener = () => {\n socketInstance.off('disconnect', disconnectHandler);\n originalCleanupResponseListener();\n };\n };\n\n //? EXT-03 — fire client request interceptors just before the emit. They\n //? may mutate `data` in place (correlation id / feature-flag context).\n //? `data` is the same reference passed to `socketInstance.emit`, so the\n //? mutation is on the wire. Awaited so an async interceptor (flag lookup,\n //? trace-id mint) completes first; failures are caught + logged inside.\n await dispatchApiRequestInterceptors({\n name: sanitizedName,\n version,\n data: data as Record<string, unknown>,\n });\n\n //? CORE-O12: re-check the external abort signal after the interceptor\n //? await. A slow async interceptor adds uncounted latency; if the caller\n //? aborted during that window the signal is already fired but nothing has\n //? yet attached its listener (that happens inside runRequest). Settling\n //? here avoids emitting a request whose response will never be consumed.\n if (externalSignal?.aborted) {\n resolve(normalizeApiError({\n response: { status: 'error', errorCode: 'request.aborted' },\n fallbackErrorCode: 'request.aborted',\n }) as RequestOutput);\n return;\n }\n\n runRequest(socket);\n })();\n });\n}","//? Browser-safe sibling of `tryCatch`. Behaviourally identical (same tuple,\n//? same params, same auto-capture on the error path) but it does NOT statically\n//? import `./sentrySetup`. The server `tryCatch.ts` reaches `sentrySetup` →\n//? `errorTrackerRegistry` → `node:async_hooks` at module-eval; that static edge\n//? is harmless on the server but drags a `node:`-externalized module into the\n//? Vite CLIENT bundle whenever a client importer (LoginForm) pulls\n//? `shared/tryCatch.ts`. So the client-reachable shim resolves HERE instead, and\n//? the only path to capture is a LAZY dynamic import taken on the error branch —\n//? keeping the node-bearing module out of the client's static graph.\n//?\n//? The server's `tryCatch.ts` is intentionally left untouched (byte-for-byte) so\n//? the server capture path keeps its synchronous, statically-linked behaviour.\n//? On the client, capture is best-effort and async-via-microtask is fine.\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 //? Lazy capture: the import is only evaluated on the error branch, so the\n //? `node:async_hooks`-bearing module never enters the client's static graph.\n const { captureException } = await import('./sentrySetup');\n captureException(error, context);\n return [error instanceof Error ? error : new Error(String(error)), null];\n }\n}\n","//? Locales registry — lets the framework's TranslationProvider + notify\n//? read the consumer's translation JSON files without baking them into the\n//? package. Consumer overlay: `luckystack/i18n/locales.ts` calls\n//? `registerLocales({ en: enJson, nl: nlJson, ... })`.\n//?\n//? Language source: a separate registry — `registerLanguageSource(fn)` —\n//? lets the consumer wire up where the active language comes from\n//? (typically the SessionProvider's current `session.language`). When no\n//? source is registered, the framework falls back to\n//? `getProjectConfig().defaultLanguage`.\n\nimport { getProjectConfig } from './projectConfig';\n\nexport type LocalesMap = Record<string, unknown>;\nexport type LanguageSource = () => string | null | undefined;\n\nlet activeLocales: LocalesMap = {};\nlet activeLanguageSource: LanguageSource | null = null;\n\nexport const registerLocales = (locales: LocalesMap): void => {\n activeLocales = locales;\n};\n\nexport const getRegisteredLocales = (): LocalesMap => activeLocales;\n\nexport const getDefaultLocale = (): string => getProjectConfig().defaultLanguage || 'en';\n\nexport const registerLanguageSource = (fn: LanguageSource): void => {\n activeLanguageSource = fn;\n};\n\nexport const getActiveLanguage = (): string => {\n if (activeLanguageSource) {\n const lang = activeLanguageSource();\n if (typeof lang === 'string' && lang.length > 0) return lang;\n }\n return getDefaultLocale();\n};\n\n//? Lookup a language's translation tree by code. Falls back to default\n//? language when the code isn't registered. Used by TranslationProvider\n//? + notify to resolve the active translations.\nexport const getLocaleByCode = (code: string): unknown => {\n if (code in activeLocales) return activeLocales[code];\n const fallback = getDefaultLocale();\n if (fallback in activeLocales) return activeLocales[fallback];\n return {};\n};\n","//? Middleware handler registry. The framework's `Middleware` + `Router`\n//? components (in `./react/`) call the registered handler on every\n//? navigation change to decide whether the user is allowed on the target\n//? route. Per-page `export const middleware` (see `registerPageMiddleware`\n//? below) is the canonical path; a consumer wanting a cross-cutting GLOBAL\n//? guard registers it from the client bootstrap (typically `main.tsx`) via\n//? `registerMiddlewareHandler(...)`. No separate\n//? `src/_functions/middlewareHandler.ts` file is required.\n\nimport type { BaseSessionLayout } from './sessionTypes';\n\nexport interface MiddlewareInput {\n location: string;\n searchParams: Record<string, string>;\n session: BaseSessionLayout | null;\n}\n\nexport type MiddlewareResult =\n | { success: true; redirect?: undefined }\n | { success: false; redirect: string }\n | undefined;\n\nexport type MiddlewareHandler = (input: MiddlewareInput) => MiddlewareResult | Promise<MiddlewareResult>;\n\n//? Default handler: allow by default. Per-page `middleware` exports on each\n//? `page.tsx` are the canonical route-guard path (see `registerPageMiddleware`\n//? below); the global handler is only consulted as a fallback for pages\n//? that don't declare their own guard. Allowing-by-default keeps public\n//? routes (`/`, `/login`, `/register`, ...) working without forcing every\n//? consumer to ship a `src/_functions/middlewareHandler.ts` boilerplate\n//? file just to opt-in to \"let the user through\".\n//?\n//? Consumers who need a cross-cutting global guard (telemetry, server-\n//? reachability check, maintenance banner gate) call\n//? `registerMiddlewareHandler(...)` directly from `main.tsx` with their own\n//? function — no separate file required.\nconst DEFAULT_HANDLER: MiddlewareHandler = () => ({ success: true });\n\nlet activeHandler: MiddlewareHandler = DEFAULT_HANDLER;\n\nexport const registerMiddlewareHandler = (handler: MiddlewareHandler): void => {\n activeHandler = handler;\n};\n\nexport const getMiddlewareHandler = (): MiddlewareHandler => activeHandler;\n\n//? Per-page middleware registry. Counterpart to `registerMiddlewareHandler`\n//? for the common case where each page declares its own guard locally\n//? (`export const middleware = ({ session }) => {...}` from page.tsx).\n//? The framework's `<Middleware>` component checks this map FIRST and only\n//? falls back to the global handler when no per-page entry is registered.\n//?\n//? `PageMiddleware<TSession>` matches the existing `MiddlewareHandler`\n//? signature so any handler shape is reusable on either side. The generic\n//? is purely ergonomic for consumer-side type narrowing — module\n//? augmentation of `BaseSessionLayout` flows through.\n\nexport type PageMiddleware<TSession extends BaseSessionLayout = BaseSessionLayout> = (\n input: { location: string; searchParams: Record<string, string>; session: TSession | null },\n) => MiddlewareResult | Promise<MiddlewareResult>;\n\nconst pageMiddlewares = new Map<string, PageMiddleware>();\n\nexport const registerPageMiddleware = (path: string, middleware: PageMiddleware): void => {\n pageMiddlewares.set(path, middleware);\n};\n\nexport const getPageMiddleware = (path: string): PageMiddleware | undefined =>\n pageMiddlewares.get(path);\n\nexport const hasPageMiddleware = (path: string): boolean => pageMiddlewares.has(path);\n\n/** Test helper — drop every registered per-page middleware. */\nexport const clearPageMiddlewaresForTests = (): void => {\n pageMiddlewares.clear();\n};\n","//? Shared session context. Defined in core so framework-React components\n//? (Middleware, Router, useSession-aware UI) can consume it without\n//? pulling in app code. The consumer's `SessionProvider` (in their\n//? template's `_providers/`) does the actual fetch/socket/lifecycle work\n//? and writes to this context via `<SessionContext value={...}>`.\n//?\n//? The session shape inside the context is intentionally loose\n//? (`BaseSessionLayout | null`) so app code with extended session types\n//? can read it through a typed `useSession<MySession>()` cast.\n\nimport { createContext, use } from 'react';\nimport type { BaseSessionLayout } from '../sessionTypes';\nimport {\n dispatchClientHook,\n dispatchVetoableClientHook,\n type ClientHookStopSignal,\n} from '../clientHookBus';\n\nexport interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {\n session: TSession | null;\n sessionLoaded: boolean;\n}\n\nconst DEFAULT_VALUE: SessionContextValue = {\n session: null,\n sessionLoaded: false,\n};\n\nexport const SessionContext = createContext<SessionContextValue>(DEFAULT_VALUE);\n\nexport function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession> {\n return use(SessionContext) as SessionContextValue<TSession>;\n}\n\n//? Non-React accessor for code that runs outside the component tree\n//? (e.g. notify() called from a non-component). Kept in sync with the\n//? context by the consumer's SessionProvider via `setLatestSession()`.\nlet latestSession: BaseSessionLayout | null = null;\n\nexport const setLatestSession = (session: BaseSessionLayout | null): void => {\n const previous = latestSession;\n latestSession = session;\n\n //? Fire client-side lifecycle hooks on null↔non-null transitions. Covers\n //? both fresh login AND existing-session restore on page reload — both\n //? cases need the same downstream actions (room auto-join, analytics\n //? identify, etc). Same-id updates (avatar refresh, profile change) do\n //? NOT fire postLogin; if a `sessionUpdate` hook is needed later, add it\n //? as a third branch without breaking existing handlers.\n if (previous === null && session !== null) {\n dispatchClientHook('postLogin', { session });\n } else if (previous !== null && session === null) {\n dispatchClientHook('postLogout', { previousSession: previous });\n }\n};\n\n//? Caller-supplied `TSession` is used to narrow the return type at the\n//? callsite (consumers extend `BaseSessionLayout`). The lint rule sees it as\n//? \"used once\" but the parameter exists solely to surface the consumer's\n//? widened session type through the read API.\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-narrowing use\nexport const getCurrentSession = <TSession extends BaseSessionLayout = BaseSessionLayout>(): TSession | null =>\n latestSession as TSession | null;\n\n/**\n * Vetoable entry point for committing a null → session transition (i.e. a\n * fresh login or a page-load session restore). Dispatches the `preLogin`\n * client hook to every registered handler in parallel; if any returns a\n * stop signal, the transition is aborted and `{ committed: false, signal }`\n * is returned. Otherwise, `setLatestSession(session)` runs synchronously\n * (which fires `postLogin`) and `{ committed: true }` comes back.\n *\n * Consumer's `SessionProvider` should `await` this for non-null session\n * commits and roll local React state back to `null` when a veto comes\n * through, so the UI doesn't render a half-logged-in state.\n */\nexport type ProposeLoginResult =\n | { stopped: false; committed: true }\n | { stopped: true; signal: ClientHookStopSignal; committed: false };\n\nexport const proposeLogin = async (\n session: BaseSessionLayout,\n): Promise<ProposeLoginResult> => {\n const veto = await dispatchVetoableClientHook('preLogin', { candidateSession: session });\n if (veto.stopped) return { stopped: true, signal: veto.signal, committed: false };\n setLatestSession(session);\n return { stopped: false, committed: true };\n};\n","/* eslint-disable react-refresh/only-export-components */\nimport { createContext, use, useState, useMemo, useCallback, ReactNode } from 'react';\n\nexport type AvatarStatus = 'avatar' | 'fallback';\n\ninterface AvatarContextType {\n avatarStatuses: Record<string, AvatarStatus>;\n setAvatarStatus: (key: string, status: AvatarStatus) => void;\n}\n\nconst AvatarContext = createContext<AvatarContextType | null>(null);\n\nexport const AvatarProvider = ({ children }: { children: ReactNode }) => {\n const [avatarStatuses, setAvatarStatuses] = useState<Record<string, AvatarStatus>>({});\n\n const setAvatarStatus = useCallback((key: string, status: AvatarStatus) => {\n setAvatarStatuses(prev => ({ ...prev, [key]: status }));\n }, []);\n\n const contextValue = useMemo(() => ({\n avatarStatuses,\n setAvatarStatus,\n }), [avatarStatuses, setAvatarStatus]);\n\n return (\n <AvatarContext value={contextValue}>\n {children}\n </AvatarContext>\n );\n};\n\nexport function useAvatarContext() {\n const ctx = use(AvatarContext);\n if (!ctx) throw new Error('useAvatarContext must be used within AvatarProvider');\n return ctx;\n}\n","//? `useTheme()` — framework-owned theme state hook. Default value comes\n//? from `getProjectConfig().defaultTheme` (the union literal exported by\n//? the consumer's `config.ts`). Toggling sets the `.dark` class on\n//? `<html>`. Persistence (e.g. saving to `User.theme` via\n//? `settings/updateUser`) is up to the consumer — the hook only owns\n//? local state + DOM class.\n\nimport { useState, useCallback } from 'react';\nimport { getProjectConfig } from '../projectConfig';\n\nexport type Theme = 'light' | 'dark';\n\nconst applyDomClass = (theme: Theme): void => {\n if (typeof document === 'undefined') return;\n document.documentElement.classList.toggle('dark', theme === 'dark');\n};\n\nexport const useTheme = () => {\n const initialTheme = (getProjectConfig().defaultTheme) ?? 'light';\n const [theme, setThemeState] = useState<Theme>(initialTheme);\n\n const setTheme = useCallback((next: Theme) => {\n setThemeState(next);\n applyDomClass(next);\n }, []);\n\n return { theme, setTheme };\n};\n","/* eslint-disable react-refresh/only-export-components */\n//? Framework-owned translation provider. Reads the registered locales map\n//? (consumer registers via `registerLocales({...})` in their overlay) and\n//? the active language source (consumer registers via\n//? `registerLanguageSource(fn)`, typically `() => session?.language`).\n//? Default language falls back to `getProjectConfig().defaultLanguage`.\n\nimport { createContext, ReactNode, use, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';\nimport {\n getActiveLanguage,\n getDefaultLocale,\n getLocaleByCode,\n getRegisteredLocales,\n} from '../localesRegistry';\n\nexport type TranslationRecord = Record<string, unknown>;\n\ninterface TranslationContextValue {\n translations: TranslationRecord;\n setLanguage: Dispatch<SetStateAction<string>>;\n}\n\nconst TranslationContext = createContext<TranslationContextValue | null>(null);\n\nconst resolveTranslations = (code: string): TranslationRecord => {\n const value = getLocaleByCode(code);\n if (value && typeof value === 'object') return value as TranslationRecord;\n return {};\n};\n\nexport function TranslationProvider({ children }: { children: ReactNode }) {\n const [language, setLanguage] = useState<string>(() => getActiveLanguage());\n const translations = useMemo(() => resolveTranslations(language), [language]);\n\n //? Poll the registered language source so changes to session.language\n //? (or whatever the consumer wired) flip the active language without a\n //? full remount. Cheap: shallow string compare every ~250ms.\n useEffect(() => {\n const handle = window.setInterval(() => {\n const next = getActiveLanguage();\n if (next !== language) setLanguage(next);\n }, 250);\n return () => { window.clearInterval(handle); };\n }, [language]);\n\n const contextValue = useMemo(() => ({ translations, setLanguage }), [translations]);\n\n return (\n <TranslationContext value={contextValue}>\n {children}\n </TranslationContext>\n );\n}\n\nexport function useTranslation() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useTranslation must be used within a TranslationProvider');\n return context.translations;\n}\n\nexport function useUpdateLanguage() {\n const context = use(TranslationContext);\n if (!context) throw new Error('useUpdateLanguage must be used within a TranslationProvider');\n return context.setLanguage;\n}\n\nexport interface TranslateParam { key: string; value: string | number | boolean }\n\n//? Resolve `a.b.c` style dot-notation keys in a translation tree. Returns\n//? the key itself if it can't be resolved (so untranslated strings stand\n//? out in the UI rather than vanishing). Supports `{{param}}` substitution.\nexport function translate({ translationList, key, params }: {\n translationList: TranslationRecord;\n key: unknown;\n params?: TranslateParam[];\n}): string {\n if (typeof key !== 'string' || key.length === 0) return '';\n\n const parts = key.split('.');\n let result: unknown = translationList;\n for (const part of parts) {\n if (result && typeof result === 'object' && part in result) {\n result = (result as Record<string, unknown>)[part];\n } else {\n return key;\n }\n }\n if (typeof result !== 'string') return key;\n if (!Array.isArray(params) || params.length === 0) return result;\n\n let finalResult = result;\n for (const param of params) {\n if (!param.key) continue;\n //? Escape key before building the RegExp — a key containing regex\n //? meta-characters (e.g. `.`, `+`, `(`) would otherwise mis-substitute\n //? or cause ReDoS. Use a function-form replacer to prevent `$`-sequences\n //? in the translation value from being interpreted by String.prototype.replace.\n const escapedKey = param.key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n const regex = new RegExp(`\\\\{\\\\{${escapedKey}\\\\}\\\\}`, 'g');\n const replacement = String(param.value);\n finalResult = finalResult.replace(regex, () => replacement);\n }\n return finalResult;\n}\n\n//? Tiny convenience hook for the common case: pass `{ key, params }` and\n//? get the resolved string back.\nexport const useTranslator = () => {\n const translations = useTranslation();\n return ({ key, params }: { key: string; params?: TranslateParam[] }) =>\n translate({ translationList: translations, key, params });\n};\n\n//? Re-export helpers from the registry so consumers have a single import\n//? point for translation customization.\nexport { getActiveLanguage, getDefaultLocale, getRegisteredLocales };\n","//? Framework-owned i18n-backed toast notify. Reads the active language\n//? source + the registered locales map to translate `{ key, params }`\n//? before handing off to `sonner`. Registers itself with\n//? `registerNotifier` on import so framework packages (apiRequest,\n//? syncRequest) automatically emit toasts in the consumer's locale.\n//?\n//? Importing this module triggers the registration as a side effect —\n//? add `import { i18nNotify } from '@luckystack/core/client';` to the\n//? consumer's main entry once.\n\nimport { toast } from 'sonner';\nimport { registerNotifier } from '../notifier';\nimport { getActiveLanguage, getLocaleByCode } from '../localesRegistry';\nimport { translate, type TranslateParam } from './TranslationProvider';\n\ninterface NotifyInput {\n key: string;\n params?: TranslateParam[];\n}\n\nconst resolve = (input: NotifyInput): string => {\n const language = getActiveLanguage();\n const list = getLocaleByCode(language);\n return translate({\n translationList: (list && typeof list === 'object' ? list : {}) as Record<string, unknown>,\n key: input.key,\n params: input.params,\n });\n};\n\nconst notify = {\n success: (input: NotifyInput) => { toast.success(resolve(input)); },\n error: (input: NotifyInput) => { toast.error(resolve(input)); },\n info: (input: NotifyInput) => { toast.info(resolve(input)); },\n warning: (input: NotifyInput) => { toast.warning(resolve(input)); },\n};\n\n//? Side-effect: register with core so framework packages emit through\n//? this i18n-backed implementation.\nregisterNotifier(notify);\n\nexport default notify;\n","//? Framework-owned route guard. Wraps protected pages — runs the registered\r\n//? middleware handler (`registerMiddlewareHandler` in core) on every route\r\n//? change and renders children only when allowed. Redirects via React\r\n//? Router's `useNavigate` when the handler returns a redirect. Shows a\r\n//? loader only after a 200ms delay so fast checks don't flash a spinner.\r\n\r\nimport { ReactNode, useEffect, useRef, useState } from 'react';\r\nimport { useLocation, useNavigate } from 'react-router-dom';\r\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\r\nimport { useSession } from './sessionContext';\r\nimport { getProjectConfig } from '../projectConfig';\r\n\r\nconst LOADER_DELAY_MS = 200;\r\n\r\nfunction MiddlewareLoader() {\r\n const [show, setShow] = useState(false);\r\n\r\n useEffect(() => {\r\n const id = globalThis.setTimeout(() => { setShow(true); }, LOADER_DELAY_MS);\r\n return () => { globalThis.clearTimeout(id); };\r\n }, []);\r\n\r\n if (!show) return null;\r\n\r\n return (\r\n <div className=\"w-full h-full flex items-center justify-center\" aria-busy=\"true\" aria-live=\"polite\">\r\n <div\r\n className=\"w-8 h-8 rounded-full border-2 border-container2-border border-t-primary animate-spin\"\r\n role=\"status\"\r\n aria-label=\"Loading\"\r\n />\r\n </div>\r\n );\r\n}\r\n\r\nexport default function Middleware({ children }: { children: ReactNode }) {\r\n const [allowed, setAllowed] = useState(false);\r\n const [checking, setChecking] = useState(true);\r\n\r\n const location = useLocation();\r\n const navigate = useNavigate();\r\n const { session, sessionLoaded } = useSession();\r\n\r\n //? Track the guarded route so a SESSION-only change re-evaluates the guard\r\n //? WITHOUT unmounting the page. A session update (avatar, roomCodes,\r\n //? preferences pushed via `updateSession`) used to flip `checking` back to\r\n //? true on every change → the whole page unmounted + remounted, wiping page\r\n //? state and aborting in-flight work. Joining a room (which saves the session\r\n //? and broadcasts `updateSession`) therefore looked like a full page refresh.\r\n //? Now we only show the loader / hide the page on a real ROUTE change; a bare\r\n //? session change re-checks in the background and only navigates away if the\r\n //? new session is no longer allowed.\r\n const routeKey = `${location.pathname}\u0000${location.search}`;\r\n const guardedRouteRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n const isRouteChange = guardedRouteRef.current !== routeKey;\r\n guardedRouteRef.current = routeKey;\r\n if (isRouteChange) {\r\n setAllowed(false);\r\n setChecking(true);\r\n }\r\n\r\n void (async () => {\r\n const params = new URLSearchParams(location.search);\r\n const queryObject: Record<string, string> = {};\r\n for (const [key, value] of params.entries()) {\r\n queryObject[key] = value;\r\n }\r\n\r\n //? Wait for the SessionProvider to finish its initial fetch — up to\r\n //? 5 seconds. Without this, we'd evaluate the middleware against a\r\n //? not-yet-loaded session and bounce the user back to /login.\r\n let count = 0;\r\n while (!sessionLoaded) {\r\n await new Promise(res => setTimeout(res, 10));\r\n count++;\r\n if (count > 500) break;\r\n }\r\n\r\n //? Per-page middleware first (declared in each page.tsx via\r\n //? `export const middleware`), then the global fallback handler\r\n //? registered via `registerMiddlewareHandler`. This lets pages own\r\n //? their own guards while keeping a central catch-all for\r\n //? cross-cutting cases (server-reachability checks, analytics).\r\n const pageMw = getPageMiddleware(location.pathname);\r\n const handler = pageMw ?? getMiddlewareHandler();\r\n const result = await handler({\r\n location: location.pathname,\r\n searchParams: queryObject,\r\n session,\r\n });\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isMounted intentional, see comment below\r\n if (!isMounted) return;\r\n\r\n if (result?.success) {\r\n setAllowed(true);\r\n } else if (result?.redirect) {\r\n void navigate(result.redirect);\r\n } else {\r\n //? Navigate to a safe, deterministic fallback instead of `navigate(-1)`,\r\n //? which chains backwards out of the app on repeated denials (e.g. when\r\n //? history is shallow or the previous page was also guarded).\r\n void navigate(getProjectConfig().loginRedirectUrl ?? '/');\r\n }\r\n\r\n setChecking(false);\r\n })();\r\n\r\n //! isMounted check is load-bearing — without it, repeated middleware\r\n //? denials chain navigate(-1) and walk too far up history. Keep it.\r\n return () => {\r\n isMounted = false;\r\n };\r\n }, [routeKey, location.pathname, location.search, navigate, session, sessionLoaded]);\r\n\r\n if (checking) return <MiddlewareLoader />;\r\n if (!allowed) return null;\r\n return <div className=\"w-full h-full\">{children}</div>;\r\n}\r\n","//? Wrapper around react-router-dom's `useNavigate` that runs the\n//? registered middleware handler before navigating. Returns the guarded\n//? navigate function. Use everywhere instead of `useNavigate` directly so\n//? programmatic navigations honor the same auth/redirect rules as\n//? `<Middleware>`-wrapped page renders.\n\nimport { useLocation, useNavigate } from 'react-router-dom';\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\nimport { useSession } from './sessionContext';\n\nconst getParams = (locationSearch: string) => {\n const params = new URLSearchParams(locationSearch);\n const queryObject: Record<string, string> = {};\n for (const [key, value] of params.entries()) {\n queryObject[key] = value;\n }\n return queryObject;\n};\n\nexport default function useRouter() {\n const navigateHandler = useNavigate();\n const location = useLocation();\n const { session } = useSession();\n\n const navigate = async (path: string) => {\n const queryObject = getParams(location.search);\n //? Per-page middleware first (declared via `export const middleware`\n //? on the target page.tsx), then the global fallback. Mirrors the\n //? `<Middleware>` component's resolution order so programmatic\n //? navigations honor the same guards as direct URL hits.\n const pageMw = getPageMiddleware(path);\n const handler = pageMw ?? getMiddlewareHandler();\n const result = await handler({ location: path, searchParams: queryObject, session });\n\n if (result?.success) {\n return navigateHandler(path);\n }\n if (result && !result.success && result.redirect) {\n return navigateHandler(result.redirect);\n }\n return;\n };\n\n return navigate;\n}\n"],"mappings":"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","names":["resolve","createContext","use","useState","useCallback","useState","useCallback","createContext","use","useMemo","useState","jsx","createContext","useState","useMemo","use","notify","useEffect","useState","jsx","useState","useEffect","useLocation","useNavigate","useNavigate","useLocation"]}
|
|
1
|
+
{"version":3,"sources":["../src/apiInterceptors.ts","../src/apiRequest.ts","../src/tryCatchClient.ts","../src/localesRegistry.ts","../src/middlewareRegistry.ts","../src/react/sessionContext.ts","../src/react/AvatarProvider.tsx","../src/react/useTheme.ts","../src/react/TranslationProvider.tsx","../src/react/notify.ts","../src/react/Middleware.tsx","../src/react/Router.tsx"],"sourcesContent":["//? EXT-03 — client-side request/response interceptor registry for `apiRequest`.\r\n//?\r\n//? `apiRequest` (apiRequest.ts) is a monolithic typed function and Rule 21 /\r\n//? the `no-unsafe-api-wrappers` lint forbid wrapping it (a wrapper erases the\r\n//? route/version inference). This registry is the sanctioned escape hatch: a\r\n//? consumer registers an interceptor here instead of wrapping the call, so the\r\n//? typed call site is untouched and route/version inference survives.\r\n//?\r\n//? Two seams, both fire-and-mutate (NOT replace — the dispatcher reserves a\r\n//? structured return for control flow):\r\n//? - request interceptors run just before the socket emit. A handler may\r\n//? mutate `ctx.data` in place (inject a correlation id / feature-flag\r\n//? context / tenant header-equivalent) and is observation-only otherwise.\r\n//? - response interceptors run when the response envelope arrives, before the\r\n//? awaiting caller resolves. Observation-only (metrics, breadcrumb, custom\r\n//? logging). The envelope is NOT mutated by this seam — response rewriting\r\n//? belongs on the server `preApiRespond` / `transformApiResponse` hooks.\r\n//?\r\n//? Handlers are best-effort: a throw / rejection is caught + logged and never\r\n//? breaks the request pipeline. Both `register*` functions return an\r\n//? unsubscribe closure, matching `registerClientHook`'s contract.\r\n\r\nexport interface ApiRequestInterceptorContext {\r\n /** Resolved route name (`<page>/<api>` or `system/<api>`). */\r\n name: string;\r\n /** Route version literal (e.g. `'v1'`). */\r\n version: string;\r\n /**\r\n * Outgoing request payload. Mutate this object in place to augment the\r\n * request (correlation id, feature-flag context, ...). Replacing the\r\n * reference has no effect — mutate keys on the existing object.\r\n */\r\n data: Record<string, unknown>;\r\n}\r\n\r\nexport interface ApiResponseInterceptorContext {\r\n name: string;\r\n version: string;\r\n /** The response envelope the caller is about to receive (read-only intent). */\r\n response: { status: 'success' | 'error'; [key: string]: unknown };\r\n}\r\n\r\nexport type ApiRequestInterceptor = (\r\n ctx: ApiRequestInterceptorContext,\r\n) => void | Promise<void>;\r\n\r\nexport type ApiResponseInterceptor = (\r\n ctx: ApiResponseInterceptorContext,\r\n) => void | Promise<void>;\r\n\r\nimport { getLogger } from './loggerRegistry';\r\n\r\nconst requestInterceptors = new Set<ApiRequestInterceptor>();\r\nconst responseInterceptors = new Set<ApiResponseInterceptor>();\r\n\r\n/**\r\n * Register an outgoing-request interceptor. Returns an unsubscribe function.\r\n * Multiple interceptors run in registration order; each may mutate `ctx.data`.\r\n */\r\nexport function registerApiRequestInterceptor(interceptor: ApiRequestInterceptor): () => void {\r\n requestInterceptors.add(interceptor);\r\n return () => {\r\n requestInterceptors.delete(interceptor);\r\n };\r\n}\r\n\r\n/**\r\n * Register an incoming-response interceptor. Returns an unsubscribe function.\r\n */\r\nexport function registerApiResponseInterceptor(interceptor: ApiResponseInterceptor): () => void {\r\n responseInterceptors.add(interceptor);\r\n return () => {\r\n responseInterceptors.delete(interceptor);\r\n };\r\n}\r\n\r\n//? Framework-internal — invoked by `apiRequest`. Awaited so a handler can do an\r\n//? async lookup (read a flag store, mint a trace id) before the emit. Snapshot\r\n//? the set so a handler unregistering mid-dispatch can't skip a sibling.\r\nexport async function dispatchApiRequestInterceptors(\r\n ctx: ApiRequestInterceptorContext,\r\n): Promise<void> {\r\n if (requestInterceptors.size === 0) return;\r\n //? Snapshot so a handler that unregisters itself (or a sibling) mid-dispatch\r\n //? doesn't skip an un-visited handler.\r\n const interceptors = [...requestInterceptors];\r\n for (const interceptor of interceptors) {\r\n try {\r\n await interceptor(ctx);\r\n } catch (error) {\r\n getLogger().error('[apiRequestInterceptor] handler threw', error);\r\n }\r\n }\r\n}\r\n\r\n//? Framework-internal — invoked by `apiRequest` when the response arrives.\r\n//? Fire-and-forget (the caller does not await) so an interceptor can't delay\r\n//? the resolve; async rejections are caught + logged.\r\nexport function dispatchApiResponseInterceptors(\r\n ctx: ApiResponseInterceptorContext,\r\n): void {\r\n if (responseInterceptors.size === 0) return;\r\n //? Snapshot (see `dispatchApiRequestInterceptors`).\r\n const interceptors = [...responseInterceptors];\r\n for (const interceptor of interceptors) {\r\n try {\r\n const result = interceptor(ctx);\r\n if (result && typeof (result as Promise<unknown>).catch === 'function') {\r\n (result as Promise<unknown>).catch((error: unknown) => {\r\n getLogger().error('[apiResponseInterceptor] async handler rejected', error);\r\n });\r\n }\r\n } catch (error) {\r\n getLogger().error('[apiResponseInterceptor] handler threw', error);\r\n }\r\n }\r\n}\r\n\r\n/** Test helper — drop every registered interceptor. Not part of the public API. */\r\nexport function _resetApiInterceptorsForTests(): void {\r\n requestInterceptors.clear();\r\n responseInterceptors.clear();\r\n}\r\n","import { getProjectConfig } from \"./projectConfig\";\r\nimport { getLogger } from \"./loggerRegistry\";\r\nimport { getRegisteredApiMethod } from \"./apiMethodMapRegistry\";\r\nimport { isGetMethodName } from \"./httpApiUtils\";\r\nimport { incrementResponseIndex, socket, waitForSocket } from \"./socketState\";\r\nimport type { ApiTypeMap, StreamPayload } from './apiTypeStubs';\r\nimport { notify } from \"./notifier\";\r\nimport { enqueueApiRequest, isOnline, removeApiQueueItem } from \"./offlineQueue\";\r\nimport { Socket } from \"socket.io-client\";\r\nimport { normalizeErrorResponseCore } from \"./responseNormalizer\";\r\nimport { parseServiceRouteName } from \"./serviceRoute\";\r\nimport tryCatchSync from \"./tryCatchSync\";\r\nimport {\r\n buildApiResponseEventName,\r\n buildApiStreamEventName,\r\n socketEventNames,\r\n} from \"./socketEvents\";\r\nimport {\r\n dispatchApiRequestInterceptors,\r\n dispatchApiResponseInterceptors,\r\n} from \"./apiInterceptors\";\r\n\r\n//? Abort controller logic:\r\n//? - abortable: true → always use abort controller\r\n//? - abortable: false → never use abort controller\r\n//? - abortable: undefined → use abort controller for GET APIs (from generated types)\r\n//? Keyed by `fullName` + a stable hash of the request `data` so the\r\n//? replace-previous (dedupe) semantics only fire for an IDENTICAL in-flight\r\n//? call. Two components polling the same GET route with DIFFERENT params no\r\n//? longer abort each other (each gets its own bucket); a genuine duplicate\r\n//? still supersedes the prior one.\r\n//? Size-capped at 1000 entries. All normal code paths (resolve, abort, disconnect,\r\n//? timeout) call `cleanupAbortController()` which deletes the entry. The cap\r\n//? is a safety net for edge cases where cleanup is skipped (socket torn down\r\n//? without a `disconnect` event, or a future code path that forgets to clean up).\r\nconst ABORT_CONTROLLERS_MAX = 1000;\r\nconst abortControllers = new Map<string, AbortController>();\r\n\r\n//? Stable, order-insensitive JSON stringify so `{a:1,b:2}` and `{b:2,a:1}`\r\n//? hash to the same abort-controller key. Falls back to a non-deduping unique\r\n//? token when the payload can't be serialised (circular ref), which is the\r\n//? safe choice — an unserialisable payload should never silently abort a\r\n//? sibling request.\r\nconst stableStringify = (value: unknown): string => {\r\n const seen = new WeakSet<object>();\r\n const walk = (input: unknown): unknown => {\r\n if (input === null || typeof input !== 'object') return input;\r\n if (seen.has(input)) return '[circular]';\r\n seen.add(input);\r\n if (Array.isArray(input)) return input.map((entry) => walk(entry));\r\n const record = input as Record<string, unknown>;\r\n const out: Record<string, unknown> = {};\r\n for (const key of Object.keys(record).toSorted()) {\r\n out[key] = walk(record[key]);\r\n }\r\n return out;\r\n };\r\n const [error, json] = tryCatchSync(() => JSON.stringify(walk(value)));\r\n if (error || typeof json !== 'string') {\r\n return `__unserializable__:${String(Date.now())}-${String(Math.random())}`;\r\n }\r\n return json;\r\n};\r\n\r\nconst buildAbortKey = (fullName: string, data: unknown): string =>\r\n `${fullName}::${stableStringify(data)}`;\r\n\r\nexport type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;\r\n\r\n/**\r\n * Check if an API is a GET method.\r\n *\r\n * Primary path: consult the registered `apiMethodMap` (generated from the\r\n * actual handler's `httpMethod` export or name-based inference at codegen\r\n * time). Falls back to name-prefix heuristic only when the map isn't\r\n * registered yet — typically because the consumer hasn't called\r\n * `registerApiMethodMap(...)` from their `socketInitializer.ts` boot file.\r\n */\r\nconst isGetMethodByPrefix = (apiName: string): boolean => isGetMethodName(apiName.toLowerCase());\r\n\r\nconst resolveGetMethod = (apiName: string, version: string): { isGet: boolean; usingHeuristic: boolean } => {\r\n //? Resolved-name shape: `pagePath/apiName`. apiMethodMap is nested by\r\n //? pagePath → apiName → version. Split at the last `/` so multi-segment\r\n //? page paths (e.g. 'admin/users') still resolve.\r\n const lastSlash = apiName.lastIndexOf('/');\r\n if (lastSlash > 0) {\r\n const pagePath = apiName.slice(0, lastSlash);\r\n const leaf = apiName.slice(lastSlash + 1);\r\n const method = getRegisteredApiMethod(pagePath, leaf, version);\r\n if (method) return { isGet: method === 'GET', usingHeuristic: false };\r\n }\r\n return { isGet: isGetMethodByPrefix(apiName), usingHeuristic: true };\r\n};\r\n\r\nconst canSendNow = (socketInstance: Socket) => {\r\n if (!socketInstance.connected) return false;\r\n return isOnline();\r\n};\r\n\r\nconst createQueueId = () => {\r\n return `${String(Date.now())}-${String(Math.random())}`;\r\n};\r\n\r\n//? Resolve logging flags at call time so registration order doesn't matter.\r\n//? `getLogging()` is called inside each handler instead of captured at load.\r\nconst getLogging = () => getProjectConfig().logging;\r\nconst shouldLogDev = () => getLogging().devLogs;\r\nconst shouldNotifyDev = () => getLogging().devNotifications;\r\nconst shouldLogStream = () => getLogging().stream;\r\n\r\nconst shouldUseAbortController = ({\r\n abortable,\r\n isGet,\r\n usingHeuristic,\r\n}: {\r\n abortable: boolean | undefined;\r\n isGet: boolean;\r\n usingHeuristic: boolean;\r\n}) => {\r\n if (abortable === true) return true;\r\n if (abortable === false) return false;\r\n //? CORE-O15: when `abortable` is unspecified and we are relying on the\r\n //? name-prefix heuristic (apiMethodMap not registered), default to\r\n //? non-abortable. A mis-named POST (e.g. `getReport`) would otherwise\r\n //? silently abort an identical in-flight call it has nothing to do with.\r\n //? Once `registerApiMethodMap` is called (normal boot), the generated map\r\n //? resolves the method and the heuristic path is never taken.\r\n if (usingHeuristic) return false;\r\n return isGet;\r\n};\r\n\r\n\r\n// ═══════════════════════════════════════════════════════════════════════════════\r\n// Type Helpers\r\n// ═══════════════════════════════════════════════════════════════════════════════\r\n\r\n// Check if data input is required (i.e., T does NOT allow empty object)\r\n// Unions like {a:1} | {b:1} do NOT allow {}, so data will be required\r\ntype DataRequired<T> = Record<string, never> extends T ? false : true;\r\n\r\ntype UnionToIntersection<U> =\r\n (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void)\r\n ? I\r\n : never;\r\n\r\n// ═══════════════════════════════════════════════════════════════════════════════\r\n// Global API Params - Union of ALL valid API calls with proper data enforcement\r\n// ═══════════════════════════════════════════════════════════════════════════════\r\ntype ApiRouteRecord = UnionToIntersection<{\r\n [P in keyof ApiTypeMap]: {\r\n [N in keyof ApiTypeMap[P] as P extends 'root'\r\n ? `system/${Extract<N, string>}`\r\n : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N]\r\n }\r\n}[keyof ApiTypeMap]>;\r\n\r\ntype ApiFullName = Extract<keyof ApiRouteRecord, string>;\r\ntype VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;\r\n\r\n// Force expansion of types to clear aliases in tooltips\r\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\r\n\r\n// Get input type for an API name (union if exists on multiple pages)\r\ntype InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { input: infer I }\r\n ? I\r\n : never;\r\n\r\n// Get output type for an API name (union if exists on multiple pages)\r\ntype OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { output: infer O }\r\n ? O\r\n : never;\r\n\r\ntype StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends { stream: infer S }\r\n ? S\r\n : never;\r\n\r\ntype ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> =\r\n [StreamForFullName<F, V>] extends [never]\r\n ? never\r\n : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;\r\n\r\n// Build params type for a specific API name\r\ntype ApiParamsForFullName<\r\n F extends ApiFullName,\r\n V extends VersionsForFullName<F>\r\n> = DataRequired<InputForFullName<F, V>> extends true\r\n ? { name: F; version: V; data: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; }\r\n : { name: F; version: V; data?: Prettify<InputForFullName<F, V>>; abortable?: boolean; disableErrorMessage?: boolean; onStream?: ApiStreamCallbackForFullName<F, V>; signal?: AbortSignal; timeoutMs?: number | false; };\r\n\r\ninterface RuntimeApiParams {\r\n name?: string;\r\n version?: string;\r\n data?: unknown;\r\n abortable?: boolean;\r\n disableErrorMessage?: boolean;\r\n onStream?: (event: ApiStreamEvent) => void;\r\n signal?: AbortSignal;\r\n /** Per-call response timeout (ms). Overrides `api.requestTimeoutMs`. `false` disables. */\r\n timeoutMs?: number | false;\r\n}\r\n\r\nexport interface ApiErrorResponse extends Record<string, unknown> {\r\n status: 'error';\r\n httpStatus: number;\r\n message: string;\r\n errorCode: string;\r\n errorParams?: { key: string; value: string | number | boolean }[];\r\n}\r\n\r\ninterface ApiSuccessResponse extends Record<string, unknown> {\r\n status: 'success';\r\n httpStatus: number;\r\n}\r\n\r\ntype ApiResponse = ApiErrorResponse | ApiSuccessResponse;\r\n\r\nconst normalizeApiError = ({\r\n response,\r\n fallbackErrorCode,\r\n fallbackHttpStatus = 500,\r\n}: {\r\n response: { status: 'error'; errorCode?: string; errorParams?: { key: string; value: string | number | boolean }[]; httpStatus?: number; message?: string };\r\n fallbackErrorCode: string;\r\n fallbackHttpStatus?: number;\r\n}): ApiErrorResponse => {\r\n const normalized = normalizeErrorResponseCore({\r\n response,\r\n fallbackErrorCode,\r\n fallbackHttpStatus,\r\n resolveMessage: ({ errorCode }) => {\r\n if (typeof response.message === 'string' && response.message.trim().length > 0) {\r\n return response.message;\r\n }\r\n return errorCode;\r\n },\r\n });\r\n\r\n return {\r\n status: 'error',\r\n message: normalized.message,\r\n errorCode: normalized.errorCode,\r\n errorParams: normalized.errorParams,\r\n httpStatus: normalized.httpStatus ?? fallbackHttpStatus,\r\n };\r\n};\r\n\r\n/**\r\n * Type-safe API request function.\r\n * \r\n * @example\r\n * ```typescript\r\n * // Full name usage - includes page in the name\r\n * const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });\r\n * // result is typed correctly for publicApi\r\n * \r\n * // Global APIs use service-first naming\r\n * await apiRequest({ name: 'system/session', version: 'v1' });\r\n * ```\r\n */\r\n\r\nexport function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(\r\n params: ApiParamsForFullName<F, V>\r\n): Promise<Prettify<OutputForFullName<F, V>> | ApiErrorResponse> {\r\n type RequestOutput = Prettify<OutputForFullName<F, V> & ApiResponse>;\r\n const runtimeParams = params as RuntimeApiParams;\r\n const { name, version, disableErrorMessage = false, onStream, signal: externalSignal, timeoutMs } = runtimeParams;\r\n const payloadData = runtimeParams.data;\r\n\r\n return new Promise<RequestOutput>((resolve) => {\r\n void (async () => {\r\n //? B1 — if the consumer-supplied signal is already aborted at call\r\n //? time, short-circuit before we even touch the socket. Mirrors the\r\n //? `fetch(url, { signal })` contract.\r\n if (externalSignal?.aborted) {\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'request.aborted' },\r\n fallbackErrorCode: 'request.aborted',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n if (!name || typeof name !== \"string\") {\r\n if (shouldLogDev()) {\r\n getLogger().error(\"apiRequest: Invalid name\");\r\n }\r\n if (shouldNotifyDev()) {\r\n notify.error({ key: 'api.invalidName' });\r\n }\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.invalidName' },\r\n fallbackErrorCode: 'api.invalidName',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n\r\n if (!version || typeof version !== 'string') {\r\n if (shouldLogDev()) {\r\n getLogger().error(\"apiRequest: Invalid version\");\r\n }\r\n if (shouldNotifyDev()) {\r\n notify.error({ key: 'api.invalidVersion' });\r\n }\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.invalidVersion' },\r\n fallbackErrorCode: 'api.invalidVersion',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n\r\n const parsedRoute = parseServiceRouteName(name);\r\n if (parsedRoute.status === 'error') {\r\n if (shouldLogDev()) {\r\n getLogger().error(`[apiRequest] Invalid service route name`, undefined, { name, reason: parsedRoute.reason });\r\n }\r\n if (shouldNotifyDev()) {\r\n notify.error({ key: 'routing.invalidServiceRouteName' });\r\n }\r\n resolve(normalizeErrorResponseCore({\r\n response: {\r\n status: 'error',\r\n errorCode: 'routing.invalidServiceRouteName',\r\n errorParams: [{ key: 'name', value: name }],\r\n },\r\n fallbackErrorCode: 'routing.invalidServiceRouteName',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n\r\n const sanitizedName = parsedRoute.normalizedRouteName;\r\n\r\n const data = payloadData && typeof payloadData === \"object\" ? payloadData : {};\r\n\r\n if (!await waitForSocket()) {\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\r\n fallbackErrorCode: 'api.ioUnavailable',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n if (!socket) {\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.ioUnavailable' },\r\n fallbackErrorCode: 'api.ioUnavailable',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n\r\n //? Abort controller logic:\r\n //? - abortable: true → always use abort controller\r\n //? - abortable: false → never use abort controller\r\n //? - abortable: undefined → smart default (GET-like APIs get abort controller)\r\n //? Pass the full pagePath/apiName so the registry lookup can resolve via\r\n //? the generated apiMethodMap (falls back to the leaf-name prefix heuristic\r\n //? when the map isn't registered yet).\r\n const { isGet, usingHeuristic } = resolveGetMethod(sanitizedName, version);\r\n const useAbortController = shouldUseAbortController({\r\n abortable: runtimeParams.abortable,\r\n isGet,\r\n usingHeuristic,\r\n });\r\n const fullName = `api/${sanitizedName}/${version}`;\r\n //? Per-payload abort key: only an identical in-flight call to the same\r\n //? route supersedes the previous one (see `buildAbortKey` rationale).\r\n const abortKey = buildAbortKey(fullName, data);\r\n\r\n let signal: AbortSignal | null = null;\r\n let abortHandler: (() => void) | null = null;\r\n let queueId: string | null = null;\r\n let cleanupStreamListener: (() => void) | null = null;\r\n //? Response listener cleanup, registered once the `.once` handler is\r\n //? attached in `runRequest`. Lets the abort path proactively `off()` the\r\n //? lingering response listener instead of waiting for a response that was\r\n //? cancelled and will never arrive.\r\n let cleanupResponseListener: (() => void) | null = null;\r\n //? Response-timeout timer. Armed right after the request is emitted so a\r\n //? lost response (server restart/crash between emit and reply) settles the\r\n //? promise instead of hanging the awaiting caller forever.\r\n let responseTimeout: ReturnType<typeof setTimeout> | null = null;\r\n const clearResponseTimeout = () => {\r\n if (responseTimeout) {\r\n clearTimeout(responseTimeout);\r\n responseTimeout = null;\r\n }\r\n };\r\n\r\n const cleanupAbortController = () => {\r\n if (signal && abortHandler) {\r\n signal.removeEventListener(\"abort\", abortHandler);\r\n }\r\n abortControllers.delete(abortKey);\r\n };\r\n\r\n if (useAbortController) {\r\n if (abortControllers.has(abortKey)) {\r\n const prevAbortController = abortControllers.get(abortKey);\r\n prevAbortController?.abort();\r\n }\r\n //? Evict the oldest entry when the cap is reached (LRU-insert-order).\r\n //? Map preserves insertion order; the first key is the oldest in-flight entry.\r\n if (!abortControllers.has(abortKey) && abortControllers.size >= ABORT_CONTROLLERS_MAX) {\r\n const oldest = abortControllers.keys().next().value;\r\n if (oldest !== undefined) abortControllers.delete(oldest);\r\n }\r\n const abortController = new AbortController();\r\n abortControllers.set(abortKey, abortController);\r\n signal = abortController.signal;\r\n\r\n abortHandler = () => {\r\n clearResponseTimeout();\r\n cleanupStreamListener?.();\r\n cleanupStreamListener = null;\r\n cleanupResponseListener?.();\r\n cleanupResponseListener = null;\r\n cleanupAbortController();\r\n if (queueId) {\r\n removeApiQueueItem(queueId);\r\n }\r\n //? Resolve (not reject) with the same `request.aborted` envelope the\r\n //? external-signal abort path uses. Rejecting here produced an\r\n //? unhandled rejection for fire-and-forget GET calls that get\r\n //? superseded by the default replace-previous behaviour.\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'request.aborted' },\r\n fallbackErrorCode: 'request.aborted',\r\n }) as RequestOutput);\r\n };\r\n\r\n signal.addEventListener(\"abort\", abortHandler);\r\n }\r\n\r\n const runRequest = (socketInstance: Socket) => {\r\n if (!canSendNow(socketInstance)) {\r\n queueId ??= createQueueId();\r\n //? For the immediate reject-new path (`drop-newest` / `reject`\r\n //? policy) THIS item's `onDrop` fires SYNCHRONOUSLY inside\r\n //? `enqueueApiRequest`. We keep surfacing that as the historical\r\n //? `offline.queueFull` via the `!enqueued` branch, so suppress the\r\n //? `onDrop` resolve during the synchronous enqueue window and re-arm\r\n //? it only if the item was actually queued (so a LATER async eviction\r\n //? — drop-oldest by a future enqueue, or age expiry — still settles).\r\n let suppressOnDrop = true;\r\n //? `enqueueApiRequest` returns `false` when the offline queue is\r\n //? full and `dropPolicy: 'reject'` is active. Without honoring\r\n //? that signal the outer promise never settles and the caller\r\n //? awaits forever. Resolve with a normalized error envelope so\r\n //? callers can branch.\r\n const enqueued = enqueueApiRequest({\r\n id: queueId,\r\n key: fullName,\r\n run: (nextSocket) => {\r\n runRequest(nextSocket);\r\n },\r\n createdAt: Date.now(),\r\n //? CORE: mirror SYNC-09. Settle the awaiting caller when this\r\n //? QUEUED request is later EVICTED (drop-oldest by a future\r\n //? enqueue / age expiry) instead of ever running. Without it the\r\n //? promise hangs forever, since `run` is the only thing that could\r\n //? otherwise resolve it — `apiRequest` had drifted from the sibling\r\n //? `syncRequest` which already had this fix. Also tear down the\r\n //? abort-controller registry entry so it isn't leaked.\r\n onDrop: (reason) => {\r\n if (suppressOnDrop) return;\r\n cleanupAbortController();\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'offline.dropped', errorParams: [{ key: 'reason', value: reason }] },\r\n fallbackErrorCode: 'offline.dropped',\r\n }) as RequestOutput);\r\n },\r\n });\r\n //? Past the synchronous enqueue window: if the item is sitting in the\r\n //? queue, a future eviction should settle via `onDrop`.\r\n suppressOnDrop = false;\r\n if (!enqueued) {\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'offline.queueFull', httpStatus: 503 },\r\n fallbackErrorCode: 'offline.queueFull',\r\n fallbackHttpStatus: 503,\r\n }) as RequestOutput);\r\n } else if (externalSignal && queueId) {\r\n //? CORE-O14: if an external AbortSignal fires while the item is\r\n //? sitting in the offline queue, its listener inside runRequest has\r\n //? not been registered yet. Register a one-shot handler here so the\r\n //? abort is not silently ignored and the queue slot is reclaimed.\r\n const capturedQueueId = queueId;\r\n const queueAbortHandler = () => {\r\n removeApiQueueItem(capturedQueueId);\r\n cleanupAbortController();\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'request.aborted' },\r\n fallbackErrorCode: 'request.aborted',\r\n }) as RequestOutput);\r\n };\r\n externalSignal.addEventListener('abort', queueAbortHandler, { once: true });\r\n }\r\n return;\r\n }\r\n\r\n if (signal?.aborted) {\r\n return;\r\n }\r\n\r\n const tempIndex = incrementResponseIndex();\r\n socketInstance.emit(socketEventNames.apiRequest, { name: fullName, data, responseIndex: tempIndex });\r\n\r\n //? B1 — bridge the consumer's AbortSignal to the server. When the\r\n //? signal fires we emit `apiCancel { responseIndex }` so the server\r\n //? handler stops emitting new chunks. We also resolve locally with\r\n //? `request.aborted` so the awaiting caller settles (vs awaiting\r\n //? a response that will never arrive because we cancelled it).\r\n let cleanupExternalAbort: (() => void) | null = null;\r\n if (externalSignal) {\r\n const externalAbortHandler = () => {\r\n clearResponseTimeout();\r\n socketInstance.emit(socketEventNames.apiCancel, { responseIndex: tempIndex });\r\n cleanupStreamListener?.();\r\n cleanupStreamListener = null;\r\n cleanupResponseListener?.();\r\n cleanupResponseListener = null;\r\n cleanupAbortController();\r\n cleanupExternalAbort?.();\r\n cleanupExternalAbort = null;\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'request.aborted' },\r\n fallbackErrorCode: 'request.aborted',\r\n }) as RequestOutput);\r\n };\r\n externalSignal.addEventListener('abort', externalAbortHandler);\r\n cleanupExternalAbort = () => {\r\n externalSignal.removeEventListener('abort', externalAbortHandler);\r\n };\r\n }\r\n\r\n //? Arm the response timeout. A per-call `timeoutMs` overrides the\r\n //? `api.requestTimeoutMs` config default; either may be `false` to\r\n //? disable. On expiry we tear down every listener/controller and settle\r\n //? with a 504 envelope so the awaiting caller never hangs.\r\n clearResponseTimeout();\r\n const effectiveTimeoutMs = timeoutMs ?? getProjectConfig().api.requestTimeoutMs;\r\n if (typeof effectiveTimeoutMs === 'number' && effectiveTimeoutMs > 0) {\r\n responseTimeout = setTimeout(() => {\r\n responseTimeout = null;\r\n cleanupStreamListener?.();\r\n cleanupStreamListener = null;\r\n cleanupResponseListener?.();\r\n cleanupResponseListener = null;\r\n cleanupExternalAbort?.();\r\n cleanupExternalAbort = null;\r\n cleanupAbortController();\r\n if (queueId) {\r\n removeApiQueueItem(queueId);\r\n }\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.timeout', httpStatus: 504 },\r\n fallbackErrorCode: 'api.timeout',\r\n fallbackHttpStatus: 504,\r\n }) as RequestOutput);\r\n }, effectiveTimeoutMs);\r\n }\r\n\r\n if (typeof onStream === 'function') {\r\n const streamEventName = buildApiStreamEventName(tempIndex);\r\n const streamListener = (streamPayload: ApiStreamEvent) => {\r\n if (signal?.aborted) {\r\n return;\r\n }\r\n\r\n if (shouldLogStream()) {\r\n getLogger().debug(`Server API Stream(${String(tempIndex)})`, { APINAME: sanitizedName, streamPayload });\r\n }\r\n\r\n onStream(streamPayload);\r\n };\r\n\r\n socketInstance.on(streamEventName, streamListener);\r\n cleanupStreamListener = () => {\r\n socketInstance.off(streamEventName, streamListener);\r\n };\r\n }\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`Client API Request(${String(tempIndex)})`, { APINAME: sanitizedName, data });\r\n }\r\n\r\n //? Inside the handler we type as the runtime envelope (ApiResponse).\r\n //? Project-specific output narrowing only matters at the call site,\r\n //? where `RequestOutput = OutputForFullName<F,V> & ApiResponse` is\r\n //? returned via `resolve(... as RequestOutput)`. When core is type-\r\n //? checked in isolation (e.g. per-package tsup dts build), ApiTypeMap\r\n //? is empty so OutputForFullName collapses to never; using ApiResponse\r\n //? here keeps the body type-safe in both cases.\r\n const responseEventName = buildApiResponseEventName(tempIndex);\r\n const responseListener = (response: ApiResponse) => {\r\n if (signal?.aborted) {\r\n clearResponseTimeout();\r\n cleanupExternalAbort?.();\r\n cleanupExternalAbort = null;\r\n return;\r\n }\r\n\r\n clearResponseTimeout();\r\n cleanupStreamListener?.();\r\n cleanupStreamListener = null;\r\n cleanupExternalAbort?.();\r\n cleanupExternalAbort = null;\r\n //? Call cleanup (not just null) so that any watcher layered on top of\r\n //? cleanupResponseListener (e.g. the CORE-O13 disconnect watcher) is\r\n //? properly torn down when a normal response arrives.\r\n cleanupResponseListener?.();\r\n cleanupResponseListener = null;\r\n\r\n const status = response.status;\r\n\r\n //? EXT-03 — fire client response interceptors when the envelope\r\n //? arrives (observation-only: metrics, breadcrumb, custom logging).\r\n dispatchApiResponseInterceptors({ name: sanitizedName, version, response });\r\n\r\n if (shouldLogDev()) {\r\n getLogger().debug(`Server API Response(${String(tempIndex)})`, { ...response, APINAME: sanitizedName });\r\n }\r\n\r\n if (status === \"error\") {\r\n const normalizedError = normalizeErrorResponseCore({ response });\r\n\r\n if (!disableErrorMessage) {\r\n if (normalizedError.errorCode) {\r\n notify.error({ key: normalizedError.errorCode, params: normalizedError.errorParams });\r\n } else {\r\n notify.error({ key: normalizedError.message });\r\n }\r\n }\r\n\r\n Object.assign(response, normalizedError);\r\n cleanupAbortController();\r\n resolve(response as RequestOutput);\r\n return;\r\n }\r\n\r\n cleanupAbortController();\r\n\r\n resolve(response as RequestOutput);\r\n };\r\n\r\n socketInstance.once(responseEventName, responseListener);\r\n cleanupResponseListener = () => {\r\n socketInstance.off(responseEventName, responseListener);\r\n };\r\n\r\n //? CORE-O13: when timeout is disabled AND there is no abort controller\r\n //? a server restart between emit and reply leaves this promise pending\r\n //? forever, leaking the response listener. The disconnect event is the\r\n //? authoritative signal that no reply will ever arrive on this socket.\r\n const disconnectHandler = () => {\r\n if (cleanupResponseListener) {\r\n cleanupResponseListener();\r\n cleanupResponseListener = null;\r\n }\r\n cleanupStreamListener?.();\r\n cleanupStreamListener = null;\r\n cleanupExternalAbort?.();\r\n cleanupExternalAbort = null;\r\n clearResponseTimeout();\r\n cleanupAbortController();\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'api.disconnected' },\r\n fallbackErrorCode: 'api.disconnected',\r\n }) as RequestOutput);\r\n };\r\n socketInstance.once('disconnect', disconnectHandler);\r\n //? Wrap the response listener to also remove the disconnect watcher on\r\n //? a normal reply (avoids a dangling listener on every successful call).\r\n const originalCleanupResponseListener = cleanupResponseListener;\r\n cleanupResponseListener = () => {\r\n socketInstance.off('disconnect', disconnectHandler);\r\n originalCleanupResponseListener();\r\n };\r\n };\r\n\r\n //? EXT-03 — fire client request interceptors just before the emit. They\r\n //? may mutate `data` in place (correlation id / feature-flag context).\r\n //? `data` is the same reference passed to `socketInstance.emit`, so the\r\n //? mutation is on the wire. Awaited so an async interceptor (flag lookup,\r\n //? trace-id mint) completes first; failures are caught + logged inside.\r\n await dispatchApiRequestInterceptors({\r\n name: sanitizedName,\r\n version,\r\n data: data as Record<string, unknown>,\r\n });\r\n\r\n //? CORE-O12: re-check the external abort signal after the interceptor\r\n //? await. A slow async interceptor adds uncounted latency; if the caller\r\n //? aborted during that window the signal is already fired but nothing has\r\n //? yet attached its listener (that happens inside runRequest). Settling\r\n //? here avoids emitting a request whose response will never be consumed.\r\n if (externalSignal?.aborted) {\r\n resolve(normalizeApiError({\r\n response: { status: 'error', errorCode: 'request.aborted' },\r\n fallbackErrorCode: 'request.aborted',\r\n }) as RequestOutput);\r\n return;\r\n }\r\n\r\n runRequest(socket);\r\n })();\r\n });\r\n}","//? Browser-safe sibling of `tryCatch`. Behaviourally identical (same tuple,\r\n//? same params, same auto-capture on the error path) but it does NOT statically\r\n//? import `./sentrySetup`. The server `tryCatch.ts` reaches `sentrySetup` →\r\n//? `errorTrackerRegistry` → `node:async_hooks` at module-eval; that static edge\r\n//? is harmless on the server but drags a `node:`-externalized module into the\r\n//? Vite CLIENT bundle whenever a client importer (LoginForm) pulls\r\n//? `shared/tryCatch.ts`. So the client-reachable shim resolves HERE instead, and\r\n//? the only path to capture is a LAZY dynamic import taken on the error branch —\r\n//? keeping the node-bearing module out of the client's static graph.\r\n//?\r\n//? The server's `tryCatch.ts` is intentionally left untouched (byte-for-byte) so\r\n//? the server capture path keeps its synchronous, statically-linked behaviour.\r\n//? On the client, capture is best-effort and async-via-microtask is fine.\r\n\r\nexport default async function tryCatch<T, P = void>(\r\n func: (values: P) => Promise<T> | T,\r\n params?: P,\r\n context?: Record<string, unknown>\r\n): Promise<[Error | null, T | null]> {\r\n try {\r\n const response = await func(params as P);\r\n return [null, response];\r\n } catch (error) {\r\n //? Lazy capture: the import is only evaluated on the error branch, so the\r\n //? `node:async_hooks`-bearing module never enters the client's static graph.\r\n const { captureException } = await import('./sentrySetup');\r\n captureException(error, context);\r\n return [error instanceof Error ? error : new Error(String(error)), null];\r\n }\r\n}\r\n","//? Locales registry — lets the framework's TranslationProvider + notify\r\n//? read the consumer's translation JSON files without baking them into the\r\n//? package. Consumer overlay: `luckystack/i18n/locales.ts` calls\r\n//? `registerLocales({ en: enJson, nl: nlJson, ... })`.\r\n//?\r\n//? Language source: a separate registry — `registerLanguageSource(fn)` —\r\n//? lets the consumer wire up where the active language comes from\r\n//? (typically the SessionProvider's current `session.language`). When no\r\n//? source is registered, the framework falls back to\r\n//? `getProjectConfig().defaultLanguage`.\r\n\r\nimport { getProjectConfig } from './projectConfig';\r\n\r\nexport type LocalesMap = Record<string, unknown>;\r\nexport type LanguageSource = () => string | null | undefined;\r\n\r\nlet activeLocales: LocalesMap = {};\r\nlet activeLanguageSource: LanguageSource | null = null;\r\n\r\nexport const registerLocales = (locales: LocalesMap): void => {\r\n activeLocales = locales;\r\n};\r\n\r\nexport const getRegisteredLocales = (): LocalesMap => activeLocales;\r\n\r\nexport const getDefaultLocale = (): string => getProjectConfig().defaultLanguage || 'en';\r\n\r\nexport const registerLanguageSource = (fn: LanguageSource): void => {\r\n activeLanguageSource = fn;\r\n};\r\n\r\nexport const getActiveLanguage = (): string => {\r\n if (activeLanguageSource) {\r\n const lang = activeLanguageSource();\r\n if (typeof lang === 'string' && lang.length > 0) return lang;\r\n }\r\n return getDefaultLocale();\r\n};\r\n\r\n//? Lookup a language's translation tree by code. Falls back to default\r\n//? language when the code isn't registered. Used by TranslationProvider\r\n//? + notify to resolve the active translations.\r\nexport const getLocaleByCode = (code: string): unknown => {\r\n if (code in activeLocales) return activeLocales[code];\r\n const fallback = getDefaultLocale();\r\n if (fallback in activeLocales) return activeLocales[fallback];\r\n return {};\r\n};\r\n","//? Middleware handler registry. The framework's `Middleware` + `Router`\r\n//? components (in `./react/`) call the registered handler on every\r\n//? navigation change to decide whether the user is allowed on the target\r\n//? route. Per-page `export const middleware` (see `registerPageMiddleware`\r\n//? below) is the canonical path; a consumer wanting a cross-cutting GLOBAL\r\n//? guard registers it from the client bootstrap (typically `main.tsx`) via\r\n//? `registerMiddlewareHandler(...)`. No separate\r\n//? `src/_functions/middlewareHandler.ts` file is required.\r\n\r\nimport type { BaseSessionLayout } from './sessionTypes';\r\n\r\nexport interface MiddlewareInput {\r\n location: string;\r\n searchParams: Record<string, string>;\r\n session: BaseSessionLayout | null;\r\n}\r\n\r\nexport type MiddlewareResult =\r\n | { success: true; redirect?: undefined }\r\n | { success: false; redirect: string }\r\n | undefined;\r\n\r\nexport type MiddlewareHandler = (input: MiddlewareInput) => MiddlewareResult | Promise<MiddlewareResult>;\r\n\r\n//? Default handler: allow by default. Per-page `middleware` exports on each\r\n//? `page.tsx` are the canonical route-guard path (see `registerPageMiddleware`\r\n//? below); the global handler is only consulted as a fallback for pages\r\n//? that don't declare their own guard. Allowing-by-default keeps public\r\n//? routes (`/`, `/login`, `/register`, ...) working without forcing every\r\n//? consumer to ship a `src/_functions/middlewareHandler.ts` boilerplate\r\n//? file just to opt-in to \"let the user through\".\r\n//?\r\n//? Consumers who need a cross-cutting global guard (telemetry, server-\r\n//? reachability check, maintenance banner gate) call\r\n//? `registerMiddlewareHandler(...)` directly from `main.tsx` with their own\r\n//? function — no separate file required.\r\nconst DEFAULT_HANDLER: MiddlewareHandler = () => ({ success: true });\r\n\r\nlet activeHandler: MiddlewareHandler = DEFAULT_HANDLER;\r\n\r\nexport const registerMiddlewareHandler = (handler: MiddlewareHandler): void => {\r\n activeHandler = handler;\r\n};\r\n\r\nexport const getMiddlewareHandler = (): MiddlewareHandler => activeHandler;\r\n\r\n//? Per-page middleware registry. Counterpart to `registerMiddlewareHandler`\r\n//? for the common case where each page declares its own guard locally\r\n//? (`export const middleware = ({ session }) => {...}` from page.tsx).\r\n//? The framework's `<Middleware>` component checks this map FIRST and only\r\n//? falls back to the global handler when no per-page entry is registered.\r\n//?\r\n//? `PageMiddleware<TSession>` matches the existing `MiddlewareHandler`\r\n//? signature so any handler shape is reusable on either side. The generic\r\n//? is purely ergonomic for consumer-side type narrowing — module\r\n//? augmentation of `BaseSessionLayout` flows through.\r\n\r\nexport type PageMiddleware<TSession extends BaseSessionLayout = BaseSessionLayout> = (\r\n input: { location: string; searchParams: Record<string, string>; session: TSession | null },\r\n) => MiddlewareResult | Promise<MiddlewareResult>;\r\n\r\nconst pageMiddlewares = new Map<string, PageMiddleware>();\r\n\r\nexport const registerPageMiddleware = (path: string, middleware: PageMiddleware): void => {\r\n pageMiddlewares.set(path, middleware);\r\n};\r\n\r\nexport const getPageMiddleware = (path: string): PageMiddleware | undefined =>\r\n pageMiddlewares.get(path);\r\n\r\nexport const hasPageMiddleware = (path: string): boolean => pageMiddlewares.has(path);\r\n\r\n/** Test helper — drop every registered per-page middleware. */\r\nexport const clearPageMiddlewaresForTests = (): void => {\r\n pageMiddlewares.clear();\r\n};\r\n","//? Shared session context. Defined in core so framework-React components\r\n//? (Middleware, Router, useSession-aware UI) can consume it without\r\n//? pulling in app code. The consumer's `SessionProvider` (in their\r\n//? template's `_providers/`) does the actual fetch/socket/lifecycle work\r\n//? and writes to this context via `<SessionContext value={...}>`.\r\n//?\r\n//? The session shape inside the context is intentionally loose\r\n//? (`BaseSessionLayout | null`) so app code with extended session types\r\n//? can read it through a typed `useSession<MySession>()` cast.\r\n\r\nimport { createContext, use } from 'react';\r\nimport type { BaseSessionLayout } from '../sessionTypes';\r\nimport {\r\n dispatchClientHook,\r\n dispatchVetoableClientHook,\r\n type ClientHookStopSignal,\r\n} from '../clientHookBus';\r\n\r\nexport interface SessionContextValue<TSession extends BaseSessionLayout = BaseSessionLayout> {\r\n session: TSession | null;\r\n sessionLoaded: boolean;\r\n}\r\n\r\nconst DEFAULT_VALUE: SessionContextValue = {\r\n session: null,\r\n sessionLoaded: false,\r\n};\r\n\r\nexport const SessionContext = createContext<SessionContextValue>(DEFAULT_VALUE);\r\n\r\nexport function useSession<TSession extends BaseSessionLayout = BaseSessionLayout>(): SessionContextValue<TSession> {\r\n return use(SessionContext) as SessionContextValue<TSession>;\r\n}\r\n\r\n//? Non-React accessor for code that runs outside the component tree\r\n//? (e.g. notify() called from a non-component). Kept in sync with the\r\n//? context by the consumer's SessionProvider via `setLatestSession()`.\r\nlet latestSession: BaseSessionLayout | null = null;\r\n\r\nexport const setLatestSession = (session: BaseSessionLayout | null): void => {\r\n const previous = latestSession;\r\n latestSession = session;\r\n\r\n //? Fire client-side lifecycle hooks on null↔non-null transitions. Covers\r\n //? both fresh login AND existing-session restore on page reload — both\r\n //? cases need the same downstream actions (room auto-join, analytics\r\n //? identify, etc). Same-id updates (avatar refresh, profile change) do\r\n //? NOT fire postLogin; if a `sessionUpdate` hook is needed later, add it\r\n //? as a third branch without breaking existing handlers.\r\n if (previous === null && session !== null) {\r\n dispatchClientHook('postLogin', { session });\r\n } else if (previous !== null && session === null) {\r\n dispatchClientHook('postLogout', { previousSession: previous });\r\n }\r\n};\r\n\r\n//? Caller-supplied `TSession` is used to narrow the return type at the\r\n//? callsite (consumers extend `BaseSessionLayout`). The lint rule sees it as\r\n//? \"used once\" but the parameter exists solely to surface the consumer's\r\n//? widened session type through the read API.\r\n// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- callsite-narrowing use\r\nexport const getCurrentSession = <TSession extends BaseSessionLayout = BaseSessionLayout>(): TSession | null =>\r\n latestSession as TSession | null;\r\n\r\n/**\r\n * Vetoable entry point for committing a null → session transition (i.e. a\r\n * fresh login or a page-load session restore). Dispatches the `preLogin`\r\n * client hook to every registered handler in parallel; if any returns a\r\n * stop signal, the transition is aborted and `{ committed: false, signal }`\r\n * is returned. Otherwise, `setLatestSession(session)` runs synchronously\r\n * (which fires `postLogin`) and `{ committed: true }` comes back.\r\n *\r\n * Consumer's `SessionProvider` should `await` this for non-null session\r\n * commits and roll local React state back to `null` when a veto comes\r\n * through, so the UI doesn't render a half-logged-in state.\r\n */\r\nexport type ProposeLoginResult =\r\n | { stopped: false; committed: true }\r\n | { stopped: true; signal: ClientHookStopSignal; committed: false };\r\n\r\nexport const proposeLogin = async (\r\n session: BaseSessionLayout,\r\n): Promise<ProposeLoginResult> => {\r\n const veto = await dispatchVetoableClientHook('preLogin', { candidateSession: session });\r\n if (veto.stopped) return { stopped: true, signal: veto.signal, committed: false };\r\n setLatestSession(session);\r\n return { stopped: false, committed: true };\r\n};\r\n","/* eslint-disable react-refresh/only-export-components */\r\nimport { createContext, use, useState, useMemo, useCallback, ReactNode } from 'react';\r\n\r\nexport type AvatarStatus = 'avatar' | 'fallback';\r\n\r\ninterface AvatarContextType {\r\n avatarStatuses: Record<string, AvatarStatus>;\r\n setAvatarStatus: (key: string, status: AvatarStatus) => void;\r\n}\r\n\r\nconst AvatarContext = createContext<AvatarContextType | null>(null);\r\n\r\nexport const AvatarProvider = ({ children }: { children: ReactNode }) => {\r\n const [avatarStatuses, setAvatarStatuses] = useState<Record<string, AvatarStatus>>({});\r\n\r\n const setAvatarStatus = useCallback((key: string, status: AvatarStatus) => {\r\n setAvatarStatuses(prev => ({ ...prev, [key]: status }));\r\n }, []);\r\n\r\n const contextValue = useMemo(() => ({\r\n avatarStatuses,\r\n setAvatarStatus,\r\n }), [avatarStatuses, setAvatarStatus]);\r\n\r\n return (\r\n <AvatarContext value={contextValue}>\r\n {children}\r\n </AvatarContext>\r\n );\r\n};\r\n\r\nexport function useAvatarContext() {\r\n const ctx = use(AvatarContext);\r\n if (!ctx) throw new Error('useAvatarContext must be used within AvatarProvider');\r\n return ctx;\r\n}\r\n","//? `useTheme()` — framework-owned theme state hook. Default value comes\r\n//? from `getProjectConfig().defaultTheme` (the union literal exported by\r\n//? the consumer's `config.ts`). Toggling sets the `.dark` class on\r\n//? `<html>`. Persistence (e.g. saving to `User.theme` via\r\n//? `settings/updateUser`) is up to the consumer — the hook only owns\r\n//? local state + DOM class.\r\n\r\nimport { useState, useCallback } from 'react';\r\nimport { getProjectConfig } from '../projectConfig';\r\n\r\nexport type Theme = 'light' | 'dark';\r\n\r\nconst applyDomClass = (theme: Theme): void => {\r\n if (typeof document === 'undefined') return;\r\n document.documentElement.classList.toggle('dark', theme === 'dark');\r\n};\r\n\r\nexport const useTheme = () => {\r\n const initialTheme = (getProjectConfig().defaultTheme) ?? 'light';\r\n const [theme, setThemeState] = useState<Theme>(initialTheme);\r\n\r\n const setTheme = useCallback((next: Theme) => {\r\n setThemeState(next);\r\n applyDomClass(next);\r\n }, []);\r\n\r\n return { theme, setTheme };\r\n};\r\n","/* eslint-disable react-refresh/only-export-components */\r\n//? Framework-owned translation provider. Reads the registered locales map\r\n//? (consumer registers via `registerLocales({...})` in their overlay) and\r\n//? the active language source (consumer registers via\r\n//? `registerLanguageSource(fn)`, typically `() => session?.language`).\r\n//? Default language falls back to `getProjectConfig().defaultLanguage`.\r\n\r\nimport { createContext, ReactNode, use, useEffect, useMemo, useState, Dispatch, SetStateAction } from 'react';\r\nimport {\r\n getActiveLanguage,\r\n getDefaultLocale,\r\n getLocaleByCode,\r\n getRegisteredLocales,\r\n} from '../localesRegistry';\r\n\r\nexport type TranslationRecord = Record<string, unknown>;\r\n\r\ninterface TranslationContextValue {\r\n translations: TranslationRecord;\r\n setLanguage: Dispatch<SetStateAction<string>>;\r\n}\r\n\r\nconst TranslationContext = createContext<TranslationContextValue | null>(null);\r\n\r\nconst resolveTranslations = (code: string): TranslationRecord => {\r\n const value = getLocaleByCode(code);\r\n if (value && typeof value === 'object') return value as TranslationRecord;\r\n return {};\r\n};\r\n\r\nexport function TranslationProvider({ children }: { children: ReactNode }) {\r\n const [language, setLanguage] = useState<string>(() => getActiveLanguage());\r\n const translations = useMemo(() => resolveTranslations(language), [language]);\r\n\r\n //? Poll the registered language source so changes to session.language\r\n //? (or whatever the consumer wired) flip the active language without a\r\n //? full remount. Cheap: shallow string compare every ~250ms.\r\n useEffect(() => {\r\n const handle = window.setInterval(() => {\r\n const next = getActiveLanguage();\r\n if (next !== language) setLanguage(next);\r\n }, 250);\r\n return () => { window.clearInterval(handle); };\r\n }, [language]);\r\n\r\n const contextValue = useMemo(() => ({ translations, setLanguage }), [translations]);\r\n\r\n return (\r\n <TranslationContext value={contextValue}>\r\n {children}\r\n </TranslationContext>\r\n );\r\n}\r\n\r\nexport function useTranslation() {\r\n const context = use(TranslationContext);\r\n if (!context) throw new Error('useTranslation must be used within a TranslationProvider');\r\n return context.translations;\r\n}\r\n\r\nexport function useUpdateLanguage() {\r\n const context = use(TranslationContext);\r\n if (!context) throw new Error('useUpdateLanguage must be used within a TranslationProvider');\r\n return context.setLanguage;\r\n}\r\n\r\nexport interface TranslateParam { key: string; value: string | number | boolean }\r\n\r\n//? Resolve `a.b.c` style dot-notation keys in a translation tree. Returns\r\n//? the key itself if it can't be resolved (so untranslated strings stand\r\n//? out in the UI rather than vanishing). Supports `{{param}}` substitution.\r\nexport function translate({ translationList, key, params }: {\r\n translationList: TranslationRecord;\r\n key: unknown;\r\n params?: TranslateParam[];\r\n}): string {\r\n if (typeof key !== 'string' || key.length === 0) return '';\r\n\r\n const parts = key.split('.');\r\n let result: unknown = translationList;\r\n for (const part of parts) {\r\n if (result && typeof result === 'object' && part in result) {\r\n result = (result as Record<string, unknown>)[part];\r\n } else {\r\n return key;\r\n }\r\n }\r\n if (typeof result !== 'string') return key;\r\n if (!Array.isArray(params) || params.length === 0) return result;\r\n\r\n let finalResult = result;\r\n for (const param of params) {\r\n if (!param.key) continue;\r\n //? Escape key before building the RegExp — a key containing regex\r\n //? meta-characters (e.g. `.`, `+`, `(`) would otherwise mis-substitute\r\n //? or cause ReDoS. Use a function-form replacer to prevent `$`-sequences\r\n //? in the translation value from being interpreted by String.prototype.replace.\r\n const escapedKey = param.key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\r\n const regex = new RegExp(`\\\\{\\\\{${escapedKey}\\\\}\\\\}`, 'g');\r\n const replacement = String(param.value);\r\n finalResult = finalResult.replace(regex, () => replacement);\r\n }\r\n return finalResult;\r\n}\r\n\r\n//? Tiny convenience hook for the common case: pass `{ key, params }` and\r\n//? get the resolved string back.\r\nexport const useTranslator = () => {\r\n const translations = useTranslation();\r\n return ({ key, params }: { key: string; params?: TranslateParam[] }) =>\r\n translate({ translationList: translations, key, params });\r\n};\r\n\r\n//? Re-export helpers from the registry so consumers have a single import\r\n//? point for translation customization.\r\nexport { getActiveLanguage, getDefaultLocale, getRegisteredLocales };\r\n","//? Framework-owned i18n-backed toast notify. Reads the active language\r\n//? source + the registered locales map to translate `{ key, params }`\r\n//? before handing off to `sonner`. Registers itself with\r\n//? `registerNotifier` on import so framework packages (apiRequest,\r\n//? syncRequest) automatically emit toasts in the consumer's locale.\r\n//?\r\n//? Importing this module triggers the registration as a side effect —\r\n//? add `import { i18nNotify } from '@luckystack/core/client';` to the\r\n//? consumer's main entry once.\r\n\r\nimport { toast } from 'sonner';\r\nimport { registerNotifier } from '../notifier';\r\nimport { getActiveLanguage, getLocaleByCode } from '../localesRegistry';\r\nimport { translate, type TranslateParam } from './TranslationProvider';\r\n\r\ninterface NotifyInput {\r\n key: string;\r\n params?: TranslateParam[];\r\n}\r\n\r\nconst resolve = (input: NotifyInput): string => {\r\n const language = getActiveLanguage();\r\n const list = getLocaleByCode(language);\r\n return translate({\r\n translationList: (list && typeof list === 'object' ? list : {}) as Record<string, unknown>,\r\n key: input.key,\r\n params: input.params,\r\n });\r\n};\r\n\r\nconst notify = {\r\n success: (input: NotifyInput) => { toast.success(resolve(input)); },\r\n error: (input: NotifyInput) => { toast.error(resolve(input)); },\r\n info: (input: NotifyInput) => { toast.info(resolve(input)); },\r\n warning: (input: NotifyInput) => { toast.warning(resolve(input)); },\r\n};\r\n\r\n//? Side-effect: register with core so framework packages emit through\r\n//? this i18n-backed implementation.\r\nregisterNotifier(notify);\r\n\r\nexport default notify;\r\n","//? Framework-owned route guard. Wraps protected pages — runs the registered\r\n//? middleware handler (`registerMiddlewareHandler` in core) on every route\r\n//? change and renders children only when allowed. Redirects via React\r\n//? Router's `useNavigate` when the handler returns a redirect. Shows a\r\n//? loader only after a 200ms delay so fast checks don't flash a spinner.\r\n\r\nimport { ReactNode, useEffect, useRef, useState } from 'react';\r\nimport { useLocation, useNavigate } from 'react-router-dom';\r\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\r\nimport { useSession } from './sessionContext';\r\nimport { getProjectConfig } from '../projectConfig';\r\n\r\nconst LOADER_DELAY_MS = 200;\r\n\r\nfunction MiddlewareLoader() {\r\n const [show, setShow] = useState(false);\r\n\r\n useEffect(() => {\r\n const id = globalThis.setTimeout(() => { setShow(true); }, LOADER_DELAY_MS);\r\n return () => { globalThis.clearTimeout(id); };\r\n }, []);\r\n\r\n if (!show) return null;\r\n\r\n return (\r\n <div className=\"w-full h-full flex items-center justify-center\" aria-busy=\"true\" aria-live=\"polite\">\r\n <div\r\n className=\"w-8 h-8 rounded-full border-2 border-container2-border border-t-primary animate-spin\"\r\n role=\"status\"\r\n aria-label=\"Loading\"\r\n />\r\n </div>\r\n );\r\n}\r\n\r\nexport default function Middleware({ children }: { children: ReactNode }) {\r\n const [allowed, setAllowed] = useState(false);\r\n const [checking, setChecking] = useState(true);\r\n\r\n const location = useLocation();\r\n const navigate = useNavigate();\r\n const { session, sessionLoaded } = useSession();\r\n\r\n //? Track the guarded route so a SESSION-only change re-evaluates the guard\r\n //? WITHOUT unmounting the page. A session update (avatar, roomCodes,\r\n //? preferences pushed via `updateSession`) used to flip `checking` back to\r\n //? true on every change → the whole page unmounted + remounted, wiping page\r\n //? state and aborting in-flight work. Joining a room (which saves the session\r\n //? and broadcasts `updateSession`) therefore looked like a full page refresh.\r\n //? Now we only show the loader / hide the page on a real ROUTE change; a bare\r\n //? session change re-checks in the background and only navigates away if the\r\n //? new session is no longer allowed.\r\n const routeKey = `${location.pathname}\u0000${location.search}`;\r\n const guardedRouteRef = useRef<string | null>(null);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n const isRouteChange = guardedRouteRef.current !== routeKey;\r\n guardedRouteRef.current = routeKey;\r\n if (isRouteChange) {\r\n setAllowed(false);\r\n setChecking(true);\r\n }\r\n\r\n void (async () => {\r\n const params = new URLSearchParams(location.search);\r\n const queryObject: Record<string, string> = {};\r\n for (const [key, value] of params.entries()) {\r\n queryObject[key] = value;\r\n }\r\n\r\n //? Wait for the SessionProvider to finish its initial fetch — up to\r\n //? 5 seconds. Without this, we'd evaluate the middleware against a\r\n //? not-yet-loaded session and bounce the user back to /login.\r\n let count = 0;\r\n while (!sessionLoaded) {\r\n await new Promise(res => setTimeout(res, 10));\r\n count++;\r\n if (count > 500) break;\r\n }\r\n\r\n //? Per-page middleware first (declared in each page.tsx via\r\n //? `export const middleware`), then the global fallback handler\r\n //? registered via `registerMiddlewareHandler`. This lets pages own\r\n //? their own guards while keeping a central catch-all for\r\n //? cross-cutting cases (server-reachability checks, analytics).\r\n const pageMw = getPageMiddleware(location.pathname);\r\n const handler = pageMw ?? getMiddlewareHandler();\r\n const result = await handler({\r\n location: location.pathname,\r\n searchParams: queryObject,\r\n session,\r\n });\r\n\r\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- isMounted intentional, see comment below\r\n if (!isMounted) return;\r\n\r\n if (result?.success) {\r\n setAllowed(true);\r\n } else if (result?.redirect) {\r\n void navigate(result.redirect);\r\n } else {\r\n //? Navigate to a safe, deterministic fallback instead of `navigate(-1)`,\r\n //? which chains backwards out of the app on repeated denials (e.g. when\r\n //? history is shallow or the previous page was also guarded).\r\n void navigate(getProjectConfig().loginRedirectUrl ?? '/');\r\n }\r\n\r\n setChecking(false);\r\n })();\r\n\r\n //! isMounted check is load-bearing — without it, repeated middleware\r\n //? denials chain navigate(-1) and walk too far up history. Keep it.\r\n return () => {\r\n isMounted = false;\r\n };\r\n }, [routeKey, location.pathname, location.search, navigate, session, sessionLoaded]);\r\n\r\n if (checking) return <MiddlewareLoader />;\r\n if (!allowed) return null;\r\n return <div className=\"w-full h-full\">{children}</div>;\r\n}\r\n","//? Wrapper around react-router-dom's `useNavigate` that runs the\r\n//? registered middleware handler before navigating. Returns the guarded\r\n//? navigate function. Use everywhere instead of `useNavigate` directly so\r\n//? programmatic navigations honor the same auth/redirect rules as\r\n//? `<Middleware>`-wrapped page renders.\r\n\r\nimport { useLocation, useNavigate } from 'react-router-dom';\r\nimport { getMiddlewareHandler, getPageMiddleware } from '../middlewareRegistry';\r\nimport { useSession } from './sessionContext';\r\n\r\nconst getParams = (locationSearch: string) => {\r\n const params = new URLSearchParams(locationSearch);\r\n const queryObject: Record<string, string> = {};\r\n for (const [key, value] of params.entries()) {\r\n queryObject[key] = value;\r\n }\r\n return queryObject;\r\n};\r\n\r\nexport default function useRouter() {\r\n const navigateHandler = useNavigate();\r\n const location = useLocation();\r\n const { session } = useSession();\r\n\r\n const navigate = async (path: string) => {\r\n const queryObject = getParams(location.search);\r\n //? Per-page middleware first (declared via `export const middleware`\r\n //? on the target page.tsx), then the global fallback. Mirrors the\r\n //? `<Middleware>` component's resolution order so programmatic\r\n //? navigations honor the same guards as direct URL hits.\r\n const pageMw = getPageMiddleware(path);\r\n const handler = pageMw ?? getMiddlewareHandler();\r\n const result = await handler({ location: path, searchParams: queryObject, session });\r\n\r\n if (result?.success) {\r\n return navigateHandler(path);\r\n }\r\n if (result && !result.success && result.redirect) {\r\n return navigateHandler(result.redirect);\r\n }\r\n return;\r\n };\r\n\r\n return navigate;\r\n}\r\n"],"mappings":"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","names":["resolve","createContext","use","useState","useCallback","useState","useCallback","createContext","use","useMemo","useState","jsx","createContext","useState","useMemo","use","notify","useEffect","useState","jsx","useState","useEffect","useLocation","useNavigate","useNavigate","useLocation"]}
|