@luckystack/core 0.6.2 → 0.6.3

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 CHANGED
@@ -7,8 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - **Timestamps in the built-in loggers.** The default `console.*` logger and
13
+ `createDevLogger` now prefix each line with an ISO-8601 UTC timestamp
14
+ (`[2026-07-13T15:20:01.123Z] Connected to Redis`), controlled by a new
15
+ `logging.timestamps` config key (default `true`; set `false` under a log
16
+ aggregator that stamps its own time). Only the message is prefixed — context /
17
+ error args stay separate. A registered custom logger owns its own formatting.
18
+ - **Decoupled secrets-resolved hook** (ADR 0026): `notifySecretsResolved(changedKeys?)`
19
+ + `registerSecretsResolvedListener(fn)`. A secret resolver (e.g.
20
+ `@luckystack/secret-manager` via `onApplied: notifySecretsResolved`) fires it after
21
+ overwriting `process.env`; `redis.ts` self-registers a listener that drops the cached
22
+ default client when a `REDIS_` credential changed, so Redis auth via a secret-manager
23
+ POINTER survives boot AND rotation with no hand-wiring. Generic — Prisma pools / SDK
24
+ clients can subscribe too. Also adds an optional structural `secretManager?` field on
25
+ `ProjectConfig` (`SecretManagerConfigRef`) so the server boot can detect a resolver.
26
+
10
27
  ### Fixed
11
28
 
29
+ - **Redis secret-manager pointer boot** (ADR 0026): the default Redis client no longer
30
+ fails auth with a baked-in `REDIS_PASSWORD_V<n>` pointer when it was built (during an
31
+ early import) before secrets resolved — the decoupled hook above + the server-boot
32
+ reset rebuild it from the resolved env. The `WRONGPASS` diagnostic now points at the
33
+ automatic handling + the `envNames` allowlist requirement.
12
34
  - **CORE-2** — `tryCatchSync<T, P = void>` now mirrors `tryCatch`'s `P` default,
13
35
  so a params-less call can pass only the result type (`tryCatchSync<URL>(() =>
14
36
  new URL(raw))`) instead of failing with TS2558.
@@ -1,6 +1,7 @@
1
1
  import {
2
- createRegistry
3
- } from "./chunk-USQA6QTX.js";
2
+ createRegistry,
3
+ getProjectConfig
4
+ } from "./chunk-ZHNQOQBR.js";
4
5
 
5
6
  // src/errorTrackerRegistry.ts
6
7
  import * as nodeAsyncHooks from "async_hooks";
@@ -62,67 +63,68 @@ var sanitizeForLog = (value, depth = 0, _seen) => {
62
63
  };
63
64
 
64
65
  // src/loggerRegistry.ts
66
+ var withTimestamp = (message) => getProjectConfig().logging.timestamps ? `[${(/* @__PURE__ */ new Date()).toISOString()}] ${message}` : message;
65
67
  var defaultLogger = {
66
68
  debug: (message, context) => {
67
69
  if (context === void 0) {
68
- console.debug(message);
70
+ console.debug(withTimestamp(message));
69
71
  } else {
70
- console.debug(message, context);
72
+ console.debug(withTimestamp(message), context);
71
73
  }
72
74
  },
73
75
  info: (message, context) => {
74
76
  if (context === void 0) {
75
- console.info(message);
77
+ console.info(withTimestamp(message));
76
78
  } else {
77
- console.info(message, context);
79
+ console.info(withTimestamp(message), context);
78
80
  }
79
81
  },
80
82
  warn: (message, context) => {
81
83
  if (context === void 0) {
82
- console.warn(message);
84
+ console.warn(withTimestamp(message));
83
85
  } else {
84
- console.warn(message, context);
86
+ console.warn(withTimestamp(message), context);
85
87
  }
86
88
  },
87
89
  error: (message, error, context) => {
88
- if (error !== void 0 && context !== void 0) console.error(message, error, context);
89
- else if (error !== void 0) console.error(message, error);
90
+ if (error !== void 0 && context !== void 0) console.error(withTimestamp(message), error, context);
91
+ else if (error !== void 0) console.error(withTimestamp(message), error);
90
92
  else if (context === void 0) {
91
- console.error(message);
93
+ console.error(withTimestamp(message));
92
94
  } else {
93
- console.error(message, context);
95
+ console.error(withTimestamp(message), context);
94
96
  }
95
97
  }
96
98
  };
97
99
  var createDevLogger = () => ({
98
100
  debug: (message, context) => {
99
101
  if (context === void 0) {
100
- console.log(message, "cyan");
102
+ console.log(withTimestamp(message), "cyan");
101
103
  } else {
102
- console.log(message, context, "cyan");
104
+ console.log(withTimestamp(message), context, "cyan");
103
105
  }
104
106
  },
105
107
  info: (message, context) => {
106
108
  if (context === void 0) {
107
- console.log(message);
109
+ console.log(withTimestamp(message));
108
110
  } else {
109
- console.log(message, context);
111
+ console.log(withTimestamp(message), context);
110
112
  }
111
113
  },
112
114
  warn: (message, context) => {
113
115
  if (context === void 0) {
114
- console.log(message, "yellow");
116
+ console.log(withTimestamp(message), "yellow");
115
117
  } else {
116
- console.log(message, context, "yellow");
118
+ console.log(withTimestamp(message), context, "yellow");
117
119
  }
118
120
  },
119
121
  error: (message, error, context) => {
120
- if (error !== void 0 && context !== void 0) console.log(message, error, context, "red");
121
- else if (error !== void 0) console.log(message, error, "red");
122
+ if (error !== void 0 && context !== void 0) console.log(withTimestamp(message), error, context, "red");
123
+ else if (error !== void 0) console.log(withTimestamp(message), error, "red");
122
124
  else if (context === void 0) {
123
- console.log(message, "red");
125
+ console.log(withTimestamp(message), "red");
124
126
  } else {
125
- console.log(message, context, "red");
127
+ console.log(withTimestamp(message), context, "red");
126
128
  }
127
129
  }
128
130
  });
@@ -392,4 +394,4 @@ export {
392
394
  setSentryUser,
393
395
  startSpan
394
396
  };
395
- //# sourceMappingURL=chunk-KPQX46TT.js.map
397
+ //# sourceMappingURL=chunk-PIUX2DOF.js.map
@@ -1 +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 //? 2FA (ADR 0024). `totpSecret` already matches the `secret` suffix, but\r\n //? `recoveryCodes` matches neither the exact set nor any suffix — list it\r\n //? explicitly so the log-redaction floor mirrors the session sanitizer,\r\n //? which strips both `totpSecret` and `recoveryCodes`.\r\n 'recoverycodes',\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":[]}
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 //? 2FA (ADR 0024). `totpSecret` already matches the `secret` suffix, but\r\n //? `recoveryCodes` matches neither the exact set nor any suffix — list it\r\n //? explicitly so the log-redaction floor mirrors the session sanitizer,\r\n //? which strips both `totpSecret` and `recoveryCodes`.\r\n 'recoverycodes',\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\nimport { getProjectConfig } from './projectConfig';\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\n//? Prefix a log message with an ISO-8601 UTC timestamp when\r\n//? `logging.timestamps` is on (the default). Read at call time so a consumer's\r\n//? config registration order doesn't matter, and so it can be toggled off under\r\n//? a log aggregator that stamps its own time. Only the framework's BUILT-IN\r\n//? loggers use this — a registered custom logger owns its own formatting.\r\nconst withTimestamp = (message: string): string =>\r\n getProjectConfig().logging.timestamps ? `[${new Date().toISOString()}] ${message}` : message;\r\n\r\nconst defaultLogger: Logger = {\r\n debug: (message, context) => {\r\n if (context === undefined) {console.debug(withTimestamp(message));}\r\n else {console.debug(withTimestamp(message), context);}\r\n },\r\n info: (message, context) => {\r\n if (context === undefined) {console.info(withTimestamp(message));}\r\n else {console.info(withTimestamp(message), context);}\r\n },\r\n warn: (message, context) => {\r\n if (context === undefined) {console.warn(withTimestamp(message));}\r\n else {console.warn(withTimestamp(message), context);}\r\n },\r\n error: (message, error, context) => {\r\n if (error !== undefined && context !== undefined) console.error(withTimestamp(message), error, context);\r\n else if (error !== undefined) console.error(withTimestamp(message), error);\r\n else if (context === undefined) {console.error(withTimestamp(message));}\r\n else {console.error(withTimestamp(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(withTimestamp(message), 'cyan');}\r\n else {console.log(withTimestamp(message), context, 'cyan');}\r\n },\r\n info: (message, context) => {\r\n if (context === undefined) {console.log(withTimestamp(message));}\r\n else {console.log(withTimestamp(message), context);}\r\n },\r\n warn: (message, context) => {\r\n if (context === undefined) {console.log(withTimestamp(message), 'yellow');}\r\n else {console.log(withTimestamp(message), context, 'yellow');}\r\n },\r\n error: (message, error, context) => {\r\n if (error !== undefined && context !== undefined) console.log(withTimestamp(message), error, context, 'red');\r\n else if (error !== undefined) console.log(withTimestamp(message), error, 'red');\r\n else if (context === undefined) {console.log(withTimestamp(message), 'red');}\r\n else {console.log(withTimestamp(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":[]}
@@ -1,13 +1,11 @@
1
- import {
2
- getProjectConfig
3
- } from "./chunk-3HQDGXMX.js";
4
1
  import {
5
2
  captureException,
6
3
  getLogger
7
- } from "./chunk-KPQX46TT.js";
4
+ } from "./chunk-PIUX2DOF.js";
8
5
  import {
9
- createRegistry
10
- } from "./chunk-USQA6QTX.js";
6
+ createRegistry,
7
+ getProjectConfig
8
+ } from "./chunk-ZHNQOQBR.js";
11
9
 
12
10
  // src/apiMethodMapRegistry.ts
13
11
  var registry = createRegistry(null);
@@ -701,4 +699,4 @@ export {
701
699
  DEFAULT_PAGE_ROUTE_RULES,
702
700
  validatePagePath
703
701
  };
704
- //# sourceMappingURL=chunk-O457EZZQ.js.map
702
+ //# sourceMappingURL=chunk-WBKHSJID.js.map