@luckystack/core 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +148 -107
  2. package/CLAUDE.md +245 -245
  3. package/LICENSE +21 -21
  4. package/README.md +93 -93
  5. package/dist/{chunk-WFABOO6J.js → chunk-AFG7KKMS.js} +3 -3
  6. package/dist/chunk-AFG7KKMS.js.map +1 -0
  7. package/dist/{chunk-3AWP2YEA.js → chunk-J3UOWYT4.js} +2 -28
  8. package/dist/chunk-J3UOWYT4.js.map +1 -0
  9. package/dist/{chunk-LU3IPFTR.js → chunk-LTCDQU5Z.js} +1 -1
  10. package/dist/chunk-LTCDQU5Z.js.map +1 -0
  11. package/dist/chunk-R7IRJFVC.js +67 -0
  12. package/dist/chunk-R7IRJFVC.js.map +1 -0
  13. package/dist/client.d.ts +3 -2
  14. package/dist/client.js +4 -4
  15. package/dist/client.js.map +1 -1
  16. package/dist/config-DZCzBNwP.d.ts +122 -0
  17. package/dist/config.d.ts +2 -0
  18. package/dist/config.js +31 -0
  19. package/dist/eslint/index.js.map +1 -1
  20. package/dist/index.d.ts +21 -149
  21. package/dist/index.js +66 -70
  22. package/dist/index.js.map +1 -1
  23. package/dist/pageRouteValidation-sJJCX-YV.d.ts +270 -0
  24. package/dist/{projectConfig-XL5VB76A.js → projectConfig-DBLSQ3NY.js} +2 -2
  25. package/dist/{sentrySetup-4NTWLN2A.js → sentrySetup-MF6JSVWA.js} +3 -3
  26. package/dist/sentrySetup-MF6JSVWA.js.map +1 -0
  27. package/dist/{pageRouteValidation-gqktrJci.d.ts → sessionTypes-Dzen_rPK.d.ts} +23 -265
  28. package/docs/app-bootstrap.md +418 -418
  29. package/docs/config-registry.md +348 -348
  30. package/docs/csrf-config.md +88 -88
  31. package/docs/error-tracker-registry.md +194 -174
  32. package/docs/hooks.md +298 -298
  33. package/docs/rate-limit-strategy.md +170 -170
  34. package/docs/redis-adapter.md +111 -111
  35. package/docs/session-types.md +180 -180
  36. package/docs/socket-bootstrap.md +366 -366
  37. package/package.json +6 -2
  38. package/dist/chunk-3AWP2YEA.js.map +0 -1
  39. package/dist/chunk-LU3IPFTR.js.map +0 -1
  40. package/dist/chunk-WFABOO6J.js.map +0 -1
  41. /package/dist/{projectConfig-XL5VB76A.js.map → config.js.map} +0 -0
  42. /package/dist/{sentrySetup-4NTWLN2A.js.map → projectConfig-DBLSQ3NY.js.map} +0 -0
@@ -1,88 +1,88 @@
1
- # CSRF Config
2
-
3
- > Customise the framework's CSRF cookie name, header name, token length and cookie options without forking `@luckystack/server`. Source: `packages/core/src/csrfConfig.ts`. Bijgewerkt: 2026-05-21.
4
-
5
- ## Overview
6
-
7
- LuckyStack's CSRF defense is double-submit: on first session write, the login package mints a per-session token; the server reads it from the session record on every state-changing request and compares it against the value the browser sends in a request header. Consumers who need to integrate with an external framework expectation (legacy `x-xsrf-token`, a custom auth gateway that rewrites cookies, FIPS-grade token length) override the defaults via `registerCsrfConfig({ ... })`.
8
-
9
- | Setting | Default | Where it is consumed |
10
- |---|---|---|
11
- | `cookieName` | `'csrf-token'` | Reserved for future cookie-issued tokens (current double-submit uses session record). |
12
- | `headerName` | `'x-csrf-token'` | Server: `csrfMiddleware.ts` reads `req.headers[headerName]`. Client: `httpFetch` attaches `headerName` on state-changing requests. |
13
- | `tokenLength` | `32` (bytes) | Session mint in `packages/login/src/session.ts` (`randomBytes(tokenLength).toString('hex')`). |
14
- | `cookieOptions.sameSite` | `'lax'` | Cookie attributes if/when the framework migrates to cookie-issued tokens. |
15
- | `cookieOptions.secure` | `true` | Same. |
16
- | `cookieOptions.httpOnly` | `false` | MUST be false for CSRF cookies — clients need to read the value. |
17
- | `cookieOptions.path` | `'/'` | Same. |
18
- | `cookieOptions.maxAgeMs` | `86_400_000` (1 day) | Same. |
19
-
20
- ## API Reference
21
-
22
- ### `registerCsrfConfig(input: Partial<CsrfConfig>): void`
23
-
24
- **Signature:**
25
- ```typescript
26
- export interface CsrfConfig {
27
- cookieName: string;
28
- headerName: string;
29
- tokenLength: number;
30
- cookieOptions: CsrfCookieOptions;
31
- }
32
- export const registerCsrfConfig = (input: Partial<CsrfConfig>): void
33
- ```
34
-
35
- **Behavior:** Shallow-merges `input` over the current config. `cookieOptions` is deep-merged so a partial override of `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.
36
-
37
- **Example — rename the header for an external gateway:**
38
- ```typescript
39
- import { registerCsrfConfig } from '@luckystack/core';
40
-
41
- registerCsrfConfig({
42
- headerName: 'x-xsrf-token',
43
- });
44
- ```
45
-
46
- **Example — FIPS-grade token length + strict same-site:**
47
- ```typescript
48
- import { registerCsrfConfig } from '@luckystack/core';
49
-
50
- registerCsrfConfig({
51
- tokenLength: 64,
52
- cookieOptions: { sameSite: 'strict' },
53
- });
54
- ```
55
-
56
- ### `getCsrfConfig(): CsrfConfig`
57
-
58
- Read the active config at call time. Framework code uses this — never read at module load.
59
-
60
- ### `DEFAULT_CSRF_CONFIG: CsrfConfig`
61
-
62
- Exported so consumers can spread + override structurally:
63
-
64
- ```typescript
65
- import { DEFAULT_CSRF_CONFIG, registerCsrfConfig } from '@luckystack/core';
66
-
67
- registerCsrfConfig({
68
- ...DEFAULT_CSRF_CONFIG,
69
- headerName: 'x-app-csrf',
70
- });
71
- ```
72
-
73
- ### `resetCsrfConfigForTests(): void`
74
-
75
- Test-only helper. Restore defaults between scenarios. Not part of the runtime contract.
76
-
77
- ## What this does NOT change
78
-
79
- - The double-submit comparison logic itself (`csrfSession.csrfToken === provided`).
80
- - Token-mode session behaviour — token-mode sessions skip CSRF entirely (cross-origin requests don't auto-attach `sessionStorage`).
81
- - The framework's HTTP route paths (`GET /auth/csrf` is still the issuing endpoint).
82
- - The `csrfMismatch` hook fan-out — payload shape and timing are unchanged.
83
-
84
- ## Related
85
-
86
- - Function INDEX: `packages/core/CLAUDE.md`
87
- - Source: `packages/core/src/csrfConfig.ts`, `packages/core/src/csrf.ts`, `packages/server/src/httpRoutes/csrfMiddleware.ts`, `packages/login/src/session.ts`
88
- - Architecture: `docs/ARCHITECTURE_EXTENSION_POINTS.md`, `packages/server/docs/security-defaults.md`
1
+ # CSRF Config
2
+
3
+ > Customise the framework's CSRF cookie name, header name, token length and cookie options without forking `@luckystack/server`. Source: `packages/core/src/csrfConfig.ts`. Bijgewerkt: 2026-05-21.
4
+
5
+ ## Overview
6
+
7
+ LuckyStack's CSRF defense is double-submit: on first session write, the login package mints a per-session token; the server reads it from the session record on every state-changing request and compares it against the value the browser sends in a request header. Consumers who need to integrate with an external framework expectation (legacy `x-xsrf-token`, a custom auth gateway that rewrites cookies, FIPS-grade token length) override the defaults via `registerCsrfConfig({ ... })`.
8
+
9
+ | Setting | Default | Where it is consumed |
10
+ |---|---|---|
11
+ | `cookieName` | `'csrf-token'` | Reserved for future cookie-issued tokens (current double-submit uses session record). |
12
+ | `headerName` | `'x-csrf-token'` | Server: `csrfMiddleware.ts` reads `req.headers[headerName]`. Client: `httpFetch` attaches `headerName` on state-changing requests. |
13
+ | `tokenLength` | `32` (bytes) | Session mint in `packages/login/src/session.ts` (`randomBytes(tokenLength).toString('hex')`). |
14
+ | `cookieOptions.sameSite` | `'lax'` | Cookie attributes if/when the framework migrates to cookie-issued tokens. |
15
+ | `cookieOptions.secure` | `true` | Same. |
16
+ | `cookieOptions.httpOnly` | `false` | MUST be false for CSRF cookies — clients need to read the value. |
17
+ | `cookieOptions.path` | `'/'` | Same. |
18
+ | `cookieOptions.maxAgeMs` | `86_400_000` (1 day) | Same. |
19
+
20
+ ## API Reference
21
+
22
+ ### `registerCsrfConfig(input: Partial<CsrfConfig>): void`
23
+
24
+ **Signature:**
25
+ ```typescript
26
+ export interface CsrfConfig {
27
+ cookieName: string;
28
+ headerName: string;
29
+ tokenLength: number;
30
+ cookieOptions: CsrfCookieOptions;
31
+ }
32
+ export const registerCsrfConfig = (input: Partial<CsrfConfig>): void
33
+ ```
34
+
35
+ **Behavior:** Shallow-merges `input` over the current config. `cookieOptions` is deep-merged so a partial override of `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.
36
+
37
+ **Example — rename the header for an external gateway:**
38
+ ```typescript
39
+ import { registerCsrfConfig } from '@luckystack/core';
40
+
41
+ registerCsrfConfig({
42
+ headerName: 'x-xsrf-token',
43
+ });
44
+ ```
45
+
46
+ **Example — FIPS-grade token length + strict same-site:**
47
+ ```typescript
48
+ import { registerCsrfConfig } from '@luckystack/core';
49
+
50
+ registerCsrfConfig({
51
+ tokenLength: 64,
52
+ cookieOptions: { sameSite: 'strict' },
53
+ });
54
+ ```
55
+
56
+ ### `getCsrfConfig(): CsrfConfig`
57
+
58
+ Read the active config at call time. Framework code uses this — never read at module load.
59
+
60
+ ### `DEFAULT_CSRF_CONFIG: CsrfConfig`
61
+
62
+ Exported so consumers can spread + override structurally:
63
+
64
+ ```typescript
65
+ import { DEFAULT_CSRF_CONFIG, registerCsrfConfig } from '@luckystack/core';
66
+
67
+ registerCsrfConfig({
68
+ ...DEFAULT_CSRF_CONFIG,
69
+ headerName: 'x-app-csrf',
70
+ });
71
+ ```
72
+
73
+ ### `resetCsrfConfigForTests(): void`
74
+
75
+ Test-only helper. Restore defaults between scenarios. Not part of the runtime contract.
76
+
77
+ ## What this does NOT change
78
+
79
+ - The double-submit comparison logic itself (`csrfSession.csrfToken === provided`).
80
+ - Token-mode session behaviour — token-mode sessions skip CSRF entirely (cross-origin requests don't auto-attach `sessionStorage`).
81
+ - The framework's HTTP route paths (`GET /auth/csrf` is still the issuing endpoint).
82
+ - The `csrfMismatch` hook fan-out — payload shape and timing are unchanged.
83
+
84
+ ## Related
85
+
86
+ - Function INDEX: `packages/core/CLAUDE.md`
87
+ - Source: `packages/core/src/csrfConfig.ts`, `packages/core/src/csrf.ts`, `packages/server/src/httpRoutes/csrfMiddleware.ts`, `packages/login/src/session.ts`
88
+ - Architecture: `docs/ARCHITECTURE_EXTENSION_POINTS.md`, `packages/server/docs/security-defaults.md`
@@ -1,174 +1,194 @@
1
- # Error Tracker Registry
2
-
3
- > Deep specs for the backend-agnostic error tracker registry + legacy Sentry slot. Source: `packages/core/src/errorTrackerRegistry.ts`, `sentrySetup.ts`. Bijgewerkt: 2026-05-20.
4
-
5
- ## Overview
6
-
7
- The error tracker registry is the seam between framework code (which never imports a specific observability backend) and adapters for Sentry / Datadog / PostHog / etc. The slot lives in `@luckystack/core`; adapter implementations ship in `@luckystack/error-tracking`. Multiple adapters can be active at once via `registerErrorTrackers([...])` — every event fans out to all of them, and per-adapter errors are swallowed so one buggy tracker can't break the chain.
8
-
9
- The legacy `initSharedSentry(instance)` + `captureException` / `captureMessage` / `setSentryUser` / `startSpan` API predates the multi-adapter registry. It is kept for backwards compatibility and dual-fans events through both the legacy slot AND any adapters registered with the new API, so a project can migrate incrementally.
10
-
11
- `startSpanAcrossTrackers` deliberately invokes only the first adapter that supports `startSpan` — spans don't fan out cleanly because they're nested execution scopes.
12
-
13
- ## Types
14
-
15
- ```typescript
16
- export interface ErrorTrackerContext {
17
- [key: string]: unknown;
18
- }
19
-
20
- export interface ErrorTrackerUser {
21
- id?: string;
22
- email?: string;
23
- username?: string;
24
- [key: string]: unknown;
25
- }
26
-
27
- export interface ErrorTrackerEvent {
28
- forwarded: boolean;
29
- kind: 'exception' | 'message';
30
- payload: Record<string, unknown>;
31
- }
32
-
33
- export type SpanResult<T> = T;
34
-
35
- export interface ErrorTracker {
36
- name: string;
37
- captureException: (error: unknown, context?: ErrorTrackerContext) => void;
38
- captureMessage: (
39
- message: string,
40
- level: 'info' | 'warning' | 'error' | 'fatal',
41
- context?: ErrorTrackerContext,
42
- ) => void;
43
- setUser: (user: ErrorTrackerUser | null) => void;
44
- setContext?: (key: string, context: ErrorTrackerContext | null) => void;
45
- startSpan?: <T>(name: string, op: string, fn: () => T) => T;
46
- recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;
47
- beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;
48
- }
49
- ```
50
-
51
- `SpanResult<T>` is an alias for `T` — the conditional unwrap collapsed in both branches and `tsup`'s dts emit choked on the widening. Kept as an exported alias so adapter authors can still annotate intent.
52
-
53
- ## API Reference — Multi-Adapter Registry
54
-
55
- ### `registerErrorTracker(tracker: ErrorTracker): void`
56
-
57
- **Behavior:** Replaces the active list with `[tracker]`. Use this for single-tracker setups.
58
-
59
- ### `registerErrorTrackers(trackers: ErrorTracker[]): void`
60
-
61
- **Behavior:** Replaces the active list with a copy of the input array (`[...trackers]`). Use for multi-backend setups (Sentry + Datadog, etc.).
62
-
63
- ### `getActiveErrorTrackers(): ErrorTracker[]`
64
-
65
- Returns the current array (the internal reference — treat as read-only).
66
-
67
- ### `captureExceptionAcrossTrackers(error, context?): void`
68
-
69
- **Behavior:** For each active tracker, calls `tracker.captureException(error, context)` inside a per-tracker `try / catch` that silently swallows failures.
70
-
71
- ### `captureMessageAcrossTrackers(message, level, context?): void`
72
-
73
- Same fan-out shape as `captureExceptionAcrossTrackers` for messages.
74
-
75
- ### `setErrorTrackerUser(user: ErrorTrackerUser | null): void`
76
-
77
- Calls `tracker.setUser(user)` on every active tracker (errors swallowed).
78
-
79
- ### `recordMetricAcrossTrackers(name, value, tags?): void`
80
-
81
- **Behavior:** Skips trackers that don't define `recordMetric`. Errors are swallowed.
82
-
83
- ### `startSpanAcrossTrackers<T>(name, op, fn): T`
84
-
85
- **Behavior:**
86
- - Finds the first tracker with a `startSpan` method.
87
- - If found, returns `tracker.startSpan(name, op, fn)` cast to `T`.
88
- - If none exist, returns `fn()` directly (no instrumentation).
89
-
90
- **Why not fan-out:** Spans are nested execution scopes — invoking multiple `startSpan` implementations would nest each other and produce confusing traces.
91
-
92
- ### Example — register multiple adapters
93
-
94
- ```typescript
95
- import { registerErrorTrackers, type ErrorTracker } from '@luckystack/core';
96
-
97
- const sentryAdapter: ErrorTracker = { /* ... */ };
98
- const datadogAdapter: ErrorTracker = { /* ... */ };
99
-
100
- registerErrorTrackers([sentryAdapter, datadogAdapter]);
101
- ```
102
-
103
- ## API Reference — Legacy Sentry Slot
104
-
105
- ### `initSharedSentry(instance: SentryInstance): void`
106
-
107
- **Signature:**
108
- ```typescript
109
- interface SentryInstance {
110
- captureException: (exception: unknown, ...args: unknown[]) => string;
111
- captureMessage: (message: string, ...args: unknown[]) => string;
112
- setUser: (user: unknown) => void;
113
- setContext: (key: string, context: unknown) => void;
114
- startInactiveSpan?: (context: unknown) => unknown;
115
- }
116
-
117
- export const initSharedSentry = (instance: SentryInstance): void
118
- ```
119
-
120
- **Behavior:** Stores the Sentry SDK instance in a module-level slot. New code should `registerErrorTracker(...)` from `@luckystack/error-tracking` instead.
121
-
122
- ### `captureException(error, context?): void`
123
-
124
- **Behavior:**
125
- - When the legacy Sentry slot is populated, calls `sentry.captureException(error, { extra: context })` (or no second arg when no context).
126
- - ALSO calls `captureExceptionAcrossTrackers(error, context)` so any modern adapters receive the same event.
127
-
128
- **Concurrency note:** Context is passed inline via the hint-shaped second argument so contexts don't leak across concurrent captures. The previous implementation used `sentry.setContext('additional', context)` which was process-global and leaked under concurrent captures.
129
-
130
- ### `captureMessage(message, level?, context?): void`
131
-
132
- **Behavior:** Mirror of `captureException` for messages. Level defaults to `'info'`.
133
-
134
- ### `setSentryUser(user): void`
135
-
136
- Calls `sentry?.setUser(user)` and fans out via `setErrorTrackerUser(user)`.
137
-
138
- ### `startSpan(name, op): unknown`
139
-
140
- **Behavior:**
141
- - If `sentry?.startInactiveSpan` exists, returns `sentry.startInactiveSpan({ name, op })`.
142
- - Otherwise falls back to `startSpanAcrossTrackers(name, op, () => undefined)` — useful when the project uses an adapter-only setup that supports spans.
143
-
144
- ## Edge cases
145
-
146
- - All multi-adapter fan-out paths swallow per-tracker errors. There is no way to surface them — by design, since framework hot paths (`tryCatch`, hook dispatch) must never crash because of a misbehaving tracker.
147
- - `registerErrorTracker` and `registerErrorTrackers` replace the active list — call once at boot. There is no `unregister` API.
148
- - `beforeSend` and `forwarded` on `ErrorTrackerEvent` are advisory for adapter authors. Core does not call `beforeSend` itself; the adapter implementation is expected to honor it.
149
-
150
- ## Example — minimal custom adapter
151
-
152
- ```typescript
153
- import { registerErrorTracker, type ErrorTracker } from '@luckystack/core';
154
-
155
- const stdoutAdapter: ErrorTracker = {
156
- name: 'stdout',
157
- captureException: (error, context) => {
158
- console.error('exception:', error, context);
159
- },
160
- captureMessage: (message, level, context) => {
161
- console.log(`[${level}] ${message}`, context);
162
- },
163
- setUser: () => { /* no-op */ },
164
- };
165
-
166
- registerErrorTracker(stdoutAdapter);
167
- ```
168
-
169
- ## Related
170
-
171
- - Function INDEX: `packages/core/CLAUDE.md`
172
- - Architecture: `docs/ARCHITECTURE_EXTENSION_POINTS.md`
173
- - README: `packages/core/README.md`, `packages/error-tracking/README.md`
174
- - Source: `packages/core/src/errorTrackerRegistry.ts`, `sentrySetup.ts`
1
+ # Error Tracker Registry
2
+
3
+ > Deep specs for the backend-agnostic error tracker registry + legacy Sentry slot. Source: `packages/core/src/errorTrackerRegistry.ts`, `errorTrackerIdentity.ts`, `sentrySetup.ts`. Bijgewerkt: 2026-07-16.
4
+
5
+ ## Overview
6
+
7
+ The error tracker registry is the seam between framework code (which never imports a specific observability backend) and adapters for Sentry / Datadog / PostHog / etc. The slot lives in `@luckystack/core`; adapter implementations ship in `@luckystack/error-tracking`. Multiple adapters can be active at once via `registerErrorTrackers([...])` — every event fans out to all of them, and per-adapter errors are swallowed so one buggy tracker can't break the chain.
8
+
9
+ The legacy `initSharedSentry(instance)` + `captureException` / `captureMessage` / `setSentryUser` / `startSpan` API predates the multi-adapter registry. It is kept for backwards compatibility and dual-fans events through both the legacy slot AND any adapters registered with the new API, so a project can migrate incrementally.
10
+
11
+ `startSpanAcrossTrackers` deliberately invokes only the first adapter that supports `startSpan` — spans don't fan out cleanly because they're nested execution scopes.
12
+
13
+ The fan-out registry and per-request identity scope are separate modules. The registry
14
+ is browser-safe because `@luckystack/core/client` reaches it through its lazy capture
15
+ path. `errorTrackerIdentity.ts` is server-only and owns `node:async_hooks`; API/sync
16
+ handlers and server adapters reach those identity exports through the main core barrel.
17
+ The core package build walks the emitted `dist/client.js` chunk graph and fails if any
18
+ Node builtin becomes reachable, because source-level import checks cannot see tsup's
19
+ cross-entry chunk coalescing.
20
+
21
+ ## Types
22
+
23
+ ```typescript
24
+ export interface ErrorTrackerContext {
25
+ [key: string]: unknown;
26
+ }
27
+
28
+ export interface ErrorTrackerUser {
29
+ id?: string;
30
+ email?: string;
31
+ username?: string;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export interface ErrorTrackerEvent {
36
+ forwarded: boolean;
37
+ kind: 'exception' | 'message';
38
+ payload: Record<string, unknown>;
39
+ }
40
+
41
+ export type SpanResult<T> = T;
42
+
43
+ export interface ErrorTracker {
44
+ name: string;
45
+ captureException: (error: unknown, context?: ErrorTrackerContext) => void;
46
+ captureMessage: (
47
+ message: string,
48
+ level: 'info' | 'warning' | 'error' | 'fatal',
49
+ context?: ErrorTrackerContext,
50
+ ) => void;
51
+ setUser: (user: ErrorTrackerUser | null) => void;
52
+ setContext?: (key: string, context: ErrorTrackerContext | null) => void;
53
+ startSpan?: <T>(name: string, op: string, fn: () => T) => T;
54
+ recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;
55
+ beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;
56
+ }
57
+ ```
58
+
59
+ `SpanResult<T>` is an alias for `T` — the conditional unwrap collapsed in both branches and `tsup`'s dts emit choked on the widening. Kept as an exported alias so adapter authors can still annotate intent.
60
+
61
+ ## API Reference — Multi-Adapter Registry
62
+
63
+ ### `registerErrorTracker(tracker: ErrorTracker): void`
64
+
65
+ **Behavior:** Replaces the active list with `[tracker]`. Use this for single-tracker setups.
66
+
67
+ ### `registerErrorTrackers(trackers: ErrorTracker[]): void`
68
+
69
+ **Behavior:** Replaces the active list with a copy of the input array (`[...trackers]`). Use for multi-backend setups (Sentry + Datadog, etc.).
70
+
71
+ ### `getActiveErrorTrackers(): ErrorTracker[]`
72
+
73
+ Returns the current array (the internal reference — treat as read-only).
74
+
75
+ ### `captureExceptionAcrossTrackers(error, context?): void`
76
+
77
+ **Behavior:** For each active tracker, calls `tracker.captureException(error, context)` inside a per-tracker `try / catch` that silently swallows failures.
78
+
79
+ ### `captureMessageAcrossTrackers(message, level, context?): void`
80
+
81
+ Same fan-out shape as `captureExceptionAcrossTrackers` for messages.
82
+
83
+ ### `setErrorTrackerUser(user: ErrorTrackerUser | null): void`
84
+
85
+ Calls `tracker.setUser(user)` on every active tracker (errors swallowed).
86
+
87
+ ### `recordMetricAcrossTrackers(name, value, tags?): void`
88
+
89
+ **Behavior:** Skips trackers that don't define `recordMetric`. Errors are swallowed.
90
+
91
+ ### `startSpanAcrossTrackers<T>(name, op, fn): T`
92
+
93
+ **Behavior:**
94
+ - Finds the first tracker with a `startSpan` method.
95
+ - If found, returns `tracker.startSpan(name, op, fn)` cast to `T`.
96
+ - If none exist, returns `fn()` directly (no instrumentation).
97
+
98
+ **Why not fan-out:** Spans are nested execution scopes — invoking multiple `startSpan` implementations would nest each other and produce confusing traces.
99
+
100
+ ### Example — register multiple adapters
101
+
102
+ ```typescript
103
+ import { registerErrorTrackers, type ErrorTracker } from '@luckystack/core';
104
+
105
+ const sentryAdapter: ErrorTracker = { /* ... */ };
106
+ const datadogAdapter: ErrorTracker = { /* ... */ };
107
+
108
+ registerErrorTrackers([sentryAdapter, datadogAdapter]);
109
+ ```
110
+
111
+ ## API Reference — Per-request identity
112
+
113
+ `runWithErrorTrackerIdentityScope(fn)` opens an empty AsyncLocalStorage scope before
114
+ session lookup. `setCurrentErrorTrackerIdentity(user)` fills the mutable box once the
115
+ session is known; `getCurrentErrorTrackerIdentity()` lets adapters read it without a
116
+ process-global user. `runWithErrorTrackerIdentity(user, fn)` is the convenience form
117
+ when the user is already known. Outside a scope, reads return `null` and writes are
118
+ no-ops.
119
+
120
+ These functions are server-only even though they are exported from the main core
121
+ barrel. They are intentionally absent from `@luckystack/core/client`.
122
+
123
+ ## API Reference — Legacy Sentry Slot
124
+
125
+ ### `initSharedSentry(instance: SentryInstance): void`
126
+
127
+ **Signature:**
128
+ ```typescript
129
+ interface SentryInstance {
130
+ captureException: (exception: unknown, ...args: unknown[]) => string;
131
+ captureMessage: (message: string, ...args: unknown[]) => string;
132
+ setUser: (user: unknown) => void;
133
+ setContext: (key: string, context: unknown) => void;
134
+ startInactiveSpan?: (context: unknown) => unknown;
135
+ }
136
+
137
+ export const initSharedSentry = (instance: SentryInstance): void
138
+ ```
139
+
140
+ **Behavior:** Stores the Sentry SDK instance in a module-level slot. New code should `registerErrorTracker(...)` from `@luckystack/error-tracking` instead.
141
+
142
+ ### `captureException(error, context?): void`
143
+
144
+ **Behavior:**
145
+ - When the legacy Sentry slot is populated, calls `sentry.captureException(error, { extra: context })` (or no second arg when no context).
146
+ - ALSO calls `captureExceptionAcrossTrackers(error, context)` so any modern adapters receive the same event.
147
+
148
+ **Concurrency note:** Context is passed inline via the hint-shaped second argument so contexts don't leak across concurrent captures. The previous implementation used `sentry.setContext('additional', context)` which was process-global and leaked under concurrent captures.
149
+
150
+ ### `captureMessage(message, level?, context?): void`
151
+
152
+ **Behavior:** Mirror of `captureException` for messages. Level defaults to `'info'`.
153
+
154
+ ### `setSentryUser(user): void`
155
+
156
+ Calls `sentry?.setUser(user)` and fans out via `setErrorTrackerUser(user)`.
157
+
158
+ ### `startSpan(name, op): unknown`
159
+
160
+ **Behavior:**
161
+ - If `sentry?.startInactiveSpan` exists, returns `sentry.startInactiveSpan({ name, op })`.
162
+ - Otherwise falls back to `startSpanAcrossTrackers(name, op, () => undefined)` — useful when the project uses an adapter-only setup that supports spans.
163
+
164
+ ## Edge cases
165
+
166
+ - All multi-adapter fan-out paths swallow per-tracker errors. There is no way to surface them — by design, since framework hot paths (`tryCatch`, hook dispatch) must never crash because of a misbehaving tracker.
167
+ - `registerErrorTracker` and `registerErrorTrackers` replace the active list — call once at boot. There is no `unregister` API.
168
+ - `beforeSend` and `forwarded` on `ErrorTrackerEvent` are advisory for adapter authors. Core does not call `beforeSend` itself; the adapter implementation is expected to honor it.
169
+
170
+ ## Example — minimal custom adapter
171
+
172
+ ```typescript
173
+ import { registerErrorTracker, type ErrorTracker } from '@luckystack/core';
174
+
175
+ const stdoutAdapter: ErrorTracker = {
176
+ name: 'stdout',
177
+ captureException: (error, context) => {
178
+ console.error('exception:', error, context);
179
+ },
180
+ captureMessage: (message, level, context) => {
181
+ console.log(`[${level}] ${message}`, context);
182
+ },
183
+ setUser: () => { /* no-op */ },
184
+ };
185
+
186
+ registerErrorTracker(stdoutAdapter);
187
+ ```
188
+
189
+ ## Related
190
+
191
+ - Function INDEX: `packages/core/CLAUDE.md`
192
+ - Architecture: `docs/ARCHITECTURE_EXTENSION_POINTS.md`
193
+ - README: `packages/core/README.md`, `packages/error-tracking/README.md`
194
+ - Source: `packages/core/src/errorTrackerRegistry.ts`, `sentrySetup.ts`