@luckystack/core 0.10.2 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +18 -0
- package/README.md +2 -88
- package/dist/apiInterceptors.d.ts +36 -0
- package/dist/apiMethodMapRegistry.d.ts +5 -0
- package/dist/apiRequest.d.ts +71 -0
- package/dist/apiTypeStubs.d.ts +3 -5
- package/dist/avatarConfig.d.ts +22 -0
- package/dist/bindAddress.d.ts +15 -0
- package/dist/bootUuid.d.ts +24 -0
- package/dist/cancelRegistry.d.ts +7 -0
- package/dist/checkOrigin.d.ts +6 -0
- package/dist/{chunk-IJYNVM2W.js → chunk-P3SSLA3E.js} +2 -2
- package/dist/chunk-P3SSLA3E.js.map +1 -0
- package/dist/{chunk-5FXBTGYL.js → chunk-XDX5MV54.js} +2 -2
- package/dist/chunk-XDX5MV54.js.map +1 -0
- package/dist/client.d.ts +46 -335
- package/dist/client.js +5 -5
- package/dist/client.js.map +1 -1
- package/dist/clientHookBus.d.ts +62 -0
- package/dist/clients.d.ts +16 -0
- package/dist/config.d.ts +8 -2
- package/dist/configUtils.d.ts +16 -0
- package/dist/consoleLog.d.ts +1 -0
- package/dist/cookies.d.ts +28 -0
- package/dist/createRegistry.d.ts +39 -0
- package/dist/csrf.d.ts +30 -0
- package/dist/csrfConfig.d.ts +32 -0
- package/dist/db.d.ts +12 -0
- package/dist/dbHealthCheck.d.ts +6 -0
- package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
- package/dist/emailRegistry.d.ts +139 -0
- package/dist/env.d.ts +38 -0
- package/dist/errorFormatterRegistry.d.ts +54 -0
- package/dist/errorTrackerIdentity.d.ts +14 -0
- package/dist/errorTrackerRegistry.d.ts +85 -0
- package/dist/escapeHtml.d.ts +6 -0
- package/dist/eslint/index.d.ts +5 -8
- package/dist/eslint/index.js +125 -8
- package/dist/eslint/index.js.map +1 -1
- package/dist/eslint/internal/hasPackage.d.ts +1 -0
- package/dist/eslint/internal/ruleTypes.d.ts +4 -0
- package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
- package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
- package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
- package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
- package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
- package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
- package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
- package/dist/eslint/rules/no-untranslated-jsx-text.d.ts +4 -0
- package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
- package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
- package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
- package/dist/extractToken.d.ts +12 -0
- package/dist/extractTokenFromRequest.d.ts +12 -0
- package/dist/getParams.d.ts +9 -0
- package/dist/hooks/registry.d.ts +12 -0
- package/dist/hooks/types.d.ts +446 -0
- package/dist/httpApiUtils.d.ts +54 -0
- package/dist/index.d.ts +110 -1892
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/lease.d.ts +18 -0
- package/dist/localSocketEnumerationGuard.d.ts +22 -0
- package/dist/localeReloader.d.ts +3 -0
- package/dist/localesRegistry.d.ts +8 -0
- package/dist/localizedNormalizer.d.ts +50 -0
- package/dist/loggerRegistry.d.ts +12 -0
- package/dist/middlewareRegistry.d.ts +45 -0
- package/dist/notifier.d.ts +17 -0
- package/dist/offlineQueue.d.ts +48 -0
- package/dist/oneTimeToken.d.ts +43 -0
- package/dist/pageRouteValidation.d.ts +37 -0
- package/dist/paths.d.ts +35 -0
- package/dist/peerDeps.d.ts +23 -0
- package/dist/portOverrideRegistry.d.ts +3 -0
- package/dist/processUpload.d.ts +44 -0
- package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
- package/dist/rateLimiter.d.ts +90 -0
- package/dist/react/AvatarProvider.d.ts +11 -0
- package/dist/react/Middleware.d.ts +10 -0
- package/dist/react/Router.d.ts +1 -0
- package/dist/react/TranslationProvider.d.ts +22 -0
- package/dist/react/notify.d.ts +12 -0
- package/dist/react/sessionContext.d.ts +31 -0
- package/dist/react/useTheme.d.ts +5 -0
- package/dist/redactedLogKeys.d.ts +22 -0
- package/dist/redis.d.ts +15 -0
- package/dist/redisKeyFormatter.d.ts +7 -0
- package/dist/resolveClientIp.d.ts +51 -0
- package/dist/responseNormalizer.d.ts +33 -0
- package/dist/roomNameFormatterRegistry.d.ts +36 -0
- package/dist/roomSockets.d.ts +23 -0
- package/dist/routedDataQuery.d.ts +7 -0
- package/dist/routedHttpInvocation.d.ts +20 -0
- package/dist/runtimeMapsRegistry.d.ts +18 -0
- package/dist/runtimeTypeValidation.d.ts +13 -0
- package/dist/secretsResolved.d.ts +3 -0
- package/dist/{sentrySetup-HNIURVUZ.js → sentrySetup-OLZ4BO7Q.js} +2 -2
- package/dist/sentrySetup.d.ts +21 -0
- package/dist/serveAvatars.d.ts +5 -0
- package/dist/serviceRoute.d.ts +27 -0
- package/dist/servicesConfigRegistry.d.ts +22 -0
- package/dist/sessionProviderRegistry.d.ts +46 -0
- package/dist/sessionTypes.d.ts +79 -0
- package/dist/sleep.d.ts +1 -0
- package/dist/socketEvents.d.ts +37 -0
- package/dist/socketMiddlewareRegistry.d.ts +20 -0
- package/dist/socketRedisAdapter.d.ts +21 -0
- package/dist/socketState.d.ts +5 -0
- package/dist/socketStatusTypes.d.ts +6 -0
- package/dist/socketTypes.d.ts +34 -0
- package/dist/synchronizedEnvHashes.d.ts +66 -0
- package/dist/testFileConvention.d.ts +11 -0
- package/dist/testResetTargets.d.ts +5 -0
- package/dist/tryCatch.d.ts +1 -0
- package/dist/tryCatchClient.d.ts +1 -0
- package/dist/tryCatchSync.d.ts +1 -0
- package/dist/validateRequest.d.ts +50 -0
- package/docs/app-bootstrap.md +4 -449
- package/docs/config-registry.md +3 -346
- package/docs/csrf-config.md +3 -86
- package/docs/error-tracker-registry.md +3 -192
- package/docs/hooks.md +2 -295
- package/docs/rate-limit-strategy.md +3 -168
- package/docs/redis-adapter.md +5 -110
- package/docs/session-types.md +3 -178
- package/docs/socket-bootstrap.md +5 -385
- package/package.json +4 -4
- package/CLAUDE.md +0 -256
- package/dist/chunk-5FXBTGYL.js.map +0 -1
- package/dist/chunk-IJYNVM2W.js.map +0 -1
- package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
- /package/dist/{sentrySetup-HNIURVUZ.js.map → sentrySetup-OLZ4BO7Q.js.map} +0 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @luckystack/core — design context
|
|
2
|
+
|
|
3
|
+
Core owns the shared transport contracts and registration seams used by framework packages. Register infrastructure at boot and read it at call time; do not copy config defaults into consumers or bypass registries. Source: `src/projectConfig.ts`, `src/createRegistry.ts`, `src/index.ts`, `src/client.ts`.
|
|
4
|
+
|
|
5
|
+
Keep the server barrel out of browser bundles; use `@luckystack/core/client`. Extend session types in the consumer rather than editing shared base types. Do not let observability adapters break request handling; redact sensitive fields before emitting context. Source: `src/errorTrackerRegistry.ts`, `src/redactedLogKeys.ts`.
|
|
6
|
+
|
|
7
|
+
Security boundaries: cookie sessions use CSRF protection; token-mode bearer credentials are supplied explicitly. Do not enable bearer fallback in cookie mode without reviewing CSRF implications (`src/extractToken.ts`). Trust forwarded IP headers only when the reverse proxy and hop count are configured (`src/resolveClientIp.ts`); rate limits depend on this identity. Never relax origin checks for convenience (`src/checkOrigin.ts`). Redis keys and socket room names must remain isolated across tenants (`src/redisKeyFormatter.ts`, `src/roomNameFormatterRegistry.ts`).
|
|
8
|
+
|
|
9
|
+
Multi-instance: the per-instance socket maps must stay unreachable from consumer code by default — use the cross-instance room helper, and take the raw server only for deliberate per-instance work with a lint opt-out that says why (ADR 0061, `src/roomSockets.ts`). A room-name formatter's `userId` is context, never part of a content room's physical name (ADR 0060). The Redis-adapter key, not the Redis server, defines the socket cluster; it separates environments but not their data (`src/socketRedisAdapter.ts`).
|
|
10
|
+
|
|
11
|
+
Design discussions: `docs/` in this package and repository `docs/ARCHITECTURE_AUTH.md`, `docs/ARCHITECTURE_MULTI_INSTANCE.md`. Signatures and defaults live in source, not this file.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ All notable changes to `@luckystack/core` are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.12.0] - 2026-10-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **ESLint rule `luckystack/no-untranslated-jsx-text`** (`@luckystack/core/eslint`, on as a warning): text in JSX goes through the project translator. It replaces `react/jsx-no-literals` from `eslint-plugin-react`, which stopped at ESLint 9 and was kept for this one rule. Same behaviour as the options the scaffold ran it with: JSX text and string/template literals in a child `{…}` are reported, attribute values are not, and punctuation, digits and units such as `px` are allowed. `allowedStrings` replaces that allow-list. A disable for `react/jsx-no-literals` becomes `luckystack/no-untranslated-jsx-text`.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- The optional `eslint` peer accepts ESLint 9 and 10 (`^9.0.0 || ^10.0.0`), so a project already on ESLint 10 installs without a peer conflict. The bundled rules use no API that ESLint 10 removed.
|
|
17
|
+
- Requires Node.js 22.13 or newer (was 20). Node 20 stopped receiving security updates in April 2026, and the scaffold's lint toolchain (ESLint 10) needs 22.13.
|
|
18
|
+
|
|
19
|
+
## [0.11.0] - 2026-10-14
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Type declarations are emitted per source file by TypeScript 7 (`tsc -b`) instead of one bundled `.d.ts` per entry, so the framework build no longer runs package by package. The exported types are unchanged; relative imports inside the declarations carry `.js`, so `moduleResolution: "node16"`/`"nodenext"` projects resolve them as before. The `prisma` export carries an explicit `PrismaClient` type, so its declaration references `@prisma/client` instead of Prisma's generated `.prisma/client` folder.
|
|
24
|
+
- Package design guidance now ships as `AGENTS.md`, not `CLAUDE.md`. Handwritten docs focus on rationale and safety; exact API contracts remain in installed declarations/source.
|
|
25
|
+
|
|
8
26
|
## [0.10.2] - 2026-10-09
|
|
9
27
|
|
|
10
28
|
### Added
|
package/README.md
CHANGED
|
@@ -1,91 +1,5 @@
|
|
|
1
1
|
# @luckystack/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Shared foundation for LuckyStack transport, configuration, session types and framework extension points. Install with `npm install @luckystack/core ioredis socket.io socket.io-client zod`. Add the optional `@prisma/client` only when using the default Prisma integration (see `package.json`). Import browser code from `@luckystack/core/client`, never the server barrel.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @luckystack/core
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Required peer dependencies you must install separately:
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install ioredis socket.io socket.io-client zod
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
`@prisma/client` is an optional peer. Install it only when the project uses the default Prisma client; a registered custom database client or a DB-less project does not need it.
|
|
20
|
-
|
|
21
|
-
## Quickstart
|
|
22
|
-
|
|
23
|
-
The core package is mostly consumed transitively through `@luckystack/server`, `@luckystack/api`, etc. You typically only touch it directly to register your project config and to call low-level utilities.
|
|
24
|
-
|
|
25
|
-
```ts
|
|
26
|
-
import { registerProjectConfig, getProjectConfig, notify, redis, prisma } from '@luckystack/core';
|
|
27
|
-
|
|
28
|
-
registerProjectConfig({
|
|
29
|
-
defaultLanguage: 'en',
|
|
30
|
-
session: { expiryDays: 30, basedToken: false, perUser: 'multiple' },
|
|
31
|
-
auth: {
|
|
32
|
-
emailMaxLength: 254,
|
|
33
|
-
passwordMinLength: 8,
|
|
34
|
-
passwordMaxLength: 128,
|
|
35
|
-
nameMaxLength: 64,
|
|
36
|
-
oauthStateTtlSeconds: 600,
|
|
37
|
-
},
|
|
38
|
-
// ...see ProjectConfig type for the full surface
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const config = getProjectConfig();
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Subpath: `@luckystack/core/client`
|
|
45
|
-
|
|
46
|
-
Browser-safe surface for the React/Vite bundle. Re-exports the pieces consumed by `@luckystack/sync`'s client transport (`projectConfig`, `notifier`, `socketState`, `offlineQueue`, `responseNormalizer`, `serviceRoute`, `socketEvents`, `socketStatusTypes`, `apiTypeStubs`, `sessionTypes`).
|
|
47
|
-
|
|
48
|
-
```ts
|
|
49
|
-
import { socket, isOnline, enqueueApiRequest } from '@luckystack/core/client';
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Public API (server entry)
|
|
53
|
-
|
|
54
|
-
| Group | Exports |
|
|
55
|
-
| --- | --- |
|
|
56
|
-
| Project config | `registerProjectConfig`, `getProjectConfig`, `getProjectName`, `isProjectConfigRegistered`, `DEFAULT_PROJECT_CONFIG`. Types: `ProjectConfig`, `ProjectConfigInput`, `LoggingConfig`, `RateLimitingConfig`, `SessionConfig`, `SentryConfig`, `SentrySampleRates`, `HttpConfig`, `HttpStreamConfig`, `SecurityHeadersConfig`, `CorsConfig`, `AuthConfig`, `SocketConfig`, `SyncConfig`, `SyncStreamThrottleConfig`, `OfflineQueueConfig`, `DevConfig`, `PathsConfig`, `EmailConfig`, `EmailLoggingConfig`, `EmailEnvVarsConfig`, `EmailDefaultsConfig`. **`getProjectName()`** is the canonical way to read the project's Redis-prefix string — it resolves at call time in this order: (1) `projectConfig.session.projectName` if a consumer set it, (2) `process.env.PROJECT_NAME` (read after `dotenv` has loaded), (3) literal `'luckystack'` as the absolute fallback. Reach for it from any framework or project code that needs the prefix instead of duplicating the env-read pattern. |
|
|
57
|
-
| Deploy + services config | `registerDeployConfig`, `getDeployConfig`, `isDeployConfigRegistered`, `registerServicesConfig`, `getServicesConfig`, `isServicesConfigRegistered`. Types: `DeployConfigShape`, `DeployResourceShape`, `DeployEnvironmentShape`, `DeployRoutingShape`, `DeployDevelopmentShape`, `ServicesConfigShape`, `ServiceDefinition`, `PresetDefinition` |
|
|
58
|
-
| Runtime maps | `registerRuntimeMapsProvider`, `getRuntimeApiMaps`, `getRuntimeSyncMaps`. Types: `RuntimeMapsProvider`, `RuntimeApiMapsResult`, `RuntimeSyncMapsResult` |
|
|
59
|
-
| DI clients | `registerPrismaClient`, `registerRedisClient`, `getPrismaClient`, `getRedisClient`, `isPrismaClientRegistered`, `isRedisClientRegistered` |
|
|
60
|
-
| Notifier | `registerNotifier`, `getNotifier`, `notify`. Types: `Notifier`, `NotifyInput`, `NotifyParam` |
|
|
61
|
-
| Email registry | `registerEmailSender`, `getEmailSender`, `isEmailSenderRegistered`. Types: `EmailSender`, `EmailMessage`, `EmailResult` (re-exported from `@luckystack/email` for convenience) |
|
|
62
|
-
| Logger | `registerLogger`, `getLogger`, `isLoggerRegistered`, `resetLoggerForTests`, `createDevLogger`. Types: `Logger`, `LoggerContext`. Default logger is a thin wrapper over `console.{debug,info,warn,error}` with no color codes — safe in production. Call `registerLogger(createDevLogger())` from a dev-only entry to opt into the colored terminal output (which still depends on `initConsolelog()` to render the trailing color string). Plug in Pino / Winston / Datadog by registering an object that satisfies `Logger`. |
|
|
63
|
-
| Redacted log keys | `registerRedactedLogKeys`, `getRedactedLogKeys`, `isRedactedLogKey`, `resetRedactedLogKeysForTests` |
|
|
64
|
-
| Avatar / uploads | `registerAvatarConfig`, `getAvatarConfig`, `DEFAULT_AVATAR_CONFIG`, `serveAvatar`, `processUpload`. Types: `AvatarConfig`, `AvatarConfigInput`, `ProcessUploadInput`, `ProcessUploadResult`. **Path-traversal guard:** `serveAvatar` rejects any `fileId` that does not match `/^[A-Za-z0-9_-]{1,128}$/` with a 400 before touching the filesystem — `..`, slashes, and other path-separator characters cannot escape the configured upload directory. **`processUpload`** wraps the project-supplied encode/save callback with the framework's `onUploadStart` (stoppable) and `onUploadComplete` hook contract so consumer upload routes don't need to plumb the hooks themselves. Returns `{ status: 'success', sizeBytes }`, `{ status: 'rejected', errorCode }` (when `onUploadStart` returned a stop signal), or `{ status: 'error', reason, cause? }`. See [`src/settings/_api/updateUser_v1.ts`](../../src/settings/_api/updateUser_v1.ts) for the canonical avatar usage. |
|
|
65
|
-
| Bind address | `registerBindAddress({ ip, port })` stores the intended pre-listen address; `registerBoundAddress({ ip, port })` stores `node:http`'s successful address; `getBindAddress() → { ip, port: string }` returns the actually-bound value. `resolveDevCallbackUrl(url)` rewrites only a stale direct loopback OAuth callback from intended → bound port, preserving explicit local router/reverse-proxy ingress. Env values remain legacy fallbacks. |
|
|
66
|
-
| Locale reloader | `registerLocaleReloader`, `getLocaleReloader`. Type: `LocaleReloader` |
|
|
67
|
-
| Hooks (async) | `registerHook`, `dispatchHook`, `clearAllHooks`. Types: `HookName`, `HookHandler`, `HookResult`, `HookStopSignal`, `HookPayloads`, `DispatchResult`, `HookSessionShape`. Canonical payload types: `PreApiValidatePayload`, `PostApiValidatePayload`, `PreApiExecutePayload`, `PostApiExecutePayload`, `PreApiRespondPayload`, `PostApiRespondPayload`, `ApiResponseEnvelope`, `PreSyncFanoutPayload`, `PostSyncFanoutPayload`, `ApiErrorPayload`, `SyncErrorPayload`, `RateLimitExceededPayload`, `CorsRejectedPayload`, `CsrfMismatchPayload`, `PreSessionRefreshPayload`, `PostSessionRefreshPayload`, `OnUploadStartPayload`, `OnUploadCompletePayload`. Feature-package payloads (`Pre/PostLogin`, `Pre/PostRegister`, `Pre/PostLogout`, `Pre/PostSessionCreate`, `Pre/PostSessionDelete`, `Pre/PostEmailSend`, `Pre/PostPresenceUpdate`, socket lifecycle, etc.) augment `HookPayloads` from their owning packages — see those packages' READMEs. |
|
|
68
|
-
| Hooks (sync, mutators) | `registerSyncHook`, `dispatchSyncHook`. Types: `SyncHookName`, `SyncHookHandler`, `SyncHookPayloads`, `PreErrorNormalizePayload`, `PostErrorNormalizePayload`. Sync hook handlers must be synchronous (no `async`), mutate the payload object in place, and cannot stop the flow. Use them only for hot-path mutators like error-code remapping. |
|
|
69
|
-
| Session types | `BaseSessionLayout`, `SessionLocation`, `AuthProps` |
|
|
70
|
-
| Socket | `setIoInstance`, `getIoInstance`, `attachSocketRedisAdapter`, `socket`, `setSocket`, `incrementResponseIndex`, `waitForSocket`. Types: `SOCKETSTATUS`, `statusContent`, `apiMessage`, `syncMessage` |
|
|
71
|
-
| Auth helpers | `extractTokenFromSocket`, `extractTokenFromRequest`, `validateRequest`, `isFalsy`, `allowedOrigin`. Types: `ValidationResult` |
|
|
72
|
-
| Rate limiting | `checkRateLimit`, `getRateLimitStatus`, `clearRateLimit`, `clearAllRateLimits` |
|
|
73
|
-
| Validation | `validateInputByType` (resolves to `@luckystack/devkit` lazily in dev) |
|
|
74
|
-
| Persistence + env | `redis`, `prisma`, `db` exports, `env` exports |
|
|
75
|
-
| Boot | `writeBootUuid`, `readBootUuid`, `resolveEnvKey`, `collectSynchronizedEnvKeys`, `computeSynchronizedEnvHashes`, `hashSynchronizedValue`, `initConsolelog` |
|
|
76
|
-
| Offline queue | `isOnline`, `enqueueApiRequest`, `enqueueSyncRequest`, `removeApiQueueItem`, `removeSyncQueueItem`, `removeApiQueueItemsByKey`, `flushApiQueue`, `flushSyncQueue`, `getApiQueueSize`, `getSyncQueueSize` |
|
|
77
|
-
| CSRF | `getCsrfToken`, `clearCsrfToken`, `httpFetch` |
|
|
78
|
-
| Cookies + paths | All exports of `./cookies`, `./httpApiUtils`, and `./paths` (`getGeneratedApiDocsPath`, `getApiMethodMapPath`, etc.) |
|
|
79
|
-
| Misc | `tryCatch`, `sleep`, `getParams`, response normalizers (`normalizeErrorResponse`, `extractLanguageFromHeader`, …), service-route helpers, socket-event constants |
|
|
80
|
-
|
|
81
|
-
> Note: `apiRequest` is exported from `@luckystack/core/client`, not from the server entry. The server barrel does not pull React-coupled code.
|
|
82
|
-
|
|
83
|
-
## Dependencies
|
|
84
|
-
|
|
85
|
-
- Runtime: `@socket.io/redis-adapter`, `dotenv`
|
|
86
|
-
- Required peers: `ioredis@^5.10.0`, `socket.io@^4.8.0`, `socket.io-client@^4.8.0`, `zod@^4.0.0`
|
|
87
|
-
- Optional peer: `@prisma/client@^6.19.0` — only needed when the default Prisma client is used.
|
|
88
|
-
|
|
89
|
-
## License
|
|
90
|
-
|
|
91
|
-
MIT — see [LICENSE](../../LICENSE).
|
|
5
|
+
Register project infrastructure before starting the server; let `@luckystack/server` handle ordinary bootstrapping. Keep cookie and bearer session modes distinct: cookie-backed writes require origin and CSRF protection. See [design context](./AGENTS.md), [configuration rationale](./docs/config-registry.md), and [security rationale](./docs/socket-bootstrap.md). Types, exports and defaults are in `src/` and `package.json`.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface ApiRequestInterceptorContext {
|
|
2
|
+
/** Resolved route name (`<page>/<api>` or `system/<api>`). */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Route version literal (e.g. `'v1'`). */
|
|
5
|
+
version: string;
|
|
6
|
+
/**
|
|
7
|
+
* Outgoing request payload. Mutate this object in place to augment the
|
|
8
|
+
* request (correlation id, feature-flag context, ...). Replacing the
|
|
9
|
+
* reference has no effect — mutate keys on the existing object.
|
|
10
|
+
*/
|
|
11
|
+
data: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface ApiResponseInterceptorContext {
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
/** The response envelope the caller is about to receive (read-only intent). */
|
|
17
|
+
response: {
|
|
18
|
+
status: 'success' | 'error';
|
|
19
|
+
[key: string]: unknown;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export type ApiRequestInterceptor = (ctx: ApiRequestInterceptorContext) => void | Promise<void>;
|
|
23
|
+
export type ApiResponseInterceptor = (ctx: ApiResponseInterceptorContext) => void | Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Register an outgoing-request interceptor. Returns an unsubscribe function.
|
|
26
|
+
* Multiple interceptors run in registration order; each may mutate `ctx.data`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registerApiRequestInterceptor(interceptor: ApiRequestInterceptor): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Register an incoming-response interceptor. Returns an unsubscribe function.
|
|
31
|
+
*/
|
|
32
|
+
export declare function registerApiResponseInterceptor(interceptor: ApiResponseInterceptor): () => void;
|
|
33
|
+
export declare function dispatchApiRequestInterceptors(ctx: ApiRequestInterceptorContext): Promise<void>;
|
|
34
|
+
export declare function dispatchApiResponseInterceptors(ctx: ApiResponseInterceptorContext): void;
|
|
35
|
+
/** Test helper — drop every registered interceptor. Not part of the public API. */
|
|
36
|
+
export declare function _resetApiInterceptorsForTests(): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type HttpMethodLiteral = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
2
|
+
export type ApiMethodMap = Record<string, Record<string, Record<string, HttpMethodLiteral>>>;
|
|
3
|
+
export declare const registerApiMethodMap: (map: ApiMethodMap) => void;
|
|
4
|
+
export declare const getRegisteredApiMethod: (pagePath: string, apiName: string, version: string) => HttpMethodLiteral | undefined;
|
|
5
|
+
export declare const isApiMethodMapRegistered: () => boolean;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ApiTypeMap, StreamPayload } from './apiTypeStubs.js';
|
|
2
|
+
export type ApiStreamEvent<T extends StreamPayload = StreamPayload> = T;
|
|
3
|
+
export declare const resolveApiRequestHttpMethod: (apiName: string, version: string) => "DELETE" | "GET" | "POST" | "PUT";
|
|
4
|
+
type DataRequired<T> = Record<string, never> extends T ? false : true;
|
|
5
|
+
type UnionToIntersection<U> = (U extends unknown ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
|
|
6
|
+
type ApiRouteRecord = UnionToIntersection<{
|
|
7
|
+
[P in keyof ApiTypeMap]: {
|
|
8
|
+
[N in keyof ApiTypeMap[P] as P extends 'root' ? `system/${Extract<N, string>}` : `${Extract<P, string>}/${Extract<N, string>}`]: ApiTypeMap[P][N];
|
|
9
|
+
};
|
|
10
|
+
}[keyof ApiTypeMap]>;
|
|
11
|
+
type ApiFullName = Extract<keyof ApiRouteRecord, string>;
|
|
12
|
+
type VersionsForFullName<F extends ApiFullName> = keyof ApiRouteRecord[F] & string;
|
|
13
|
+
type Prettify<T> = {
|
|
14
|
+
[K in keyof T]: T[K];
|
|
15
|
+
} & {};
|
|
16
|
+
type InputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
|
|
17
|
+
input: infer I;
|
|
18
|
+
} ? I : never;
|
|
19
|
+
type OutputForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
|
|
20
|
+
output: infer O;
|
|
21
|
+
} ? O : never;
|
|
22
|
+
type StreamForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = ApiRouteRecord[F][V] extends {
|
|
23
|
+
stream: infer S;
|
|
24
|
+
} ? S : never;
|
|
25
|
+
type ApiStreamCallbackForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = [
|
|
26
|
+
StreamForFullName<F, V>
|
|
27
|
+
] extends [never] ? never : (event: ApiStreamEvent<Prettify<StreamForFullName<F, V> extends StreamPayload ? StreamForFullName<F, V> : StreamPayload>>) => void;
|
|
28
|
+
type ApiParamsForFullName<F extends ApiFullName, V extends VersionsForFullName<F>> = DataRequired<InputForFullName<F, V>> extends true ? {
|
|
29
|
+
name: F;
|
|
30
|
+
version: V;
|
|
31
|
+
data: Prettify<InputForFullName<F, V>>;
|
|
32
|
+
abortable?: boolean;
|
|
33
|
+
disableErrorMessage?: boolean;
|
|
34
|
+
onStream?: ApiStreamCallbackForFullName<F, V>;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
timeoutMs?: number | false;
|
|
37
|
+
} : {
|
|
38
|
+
name: F;
|
|
39
|
+
version: V;
|
|
40
|
+
data?: Prettify<InputForFullName<F, V>>;
|
|
41
|
+
abortable?: boolean;
|
|
42
|
+
disableErrorMessage?: boolean;
|
|
43
|
+
onStream?: ApiStreamCallbackForFullName<F, V>;
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
timeoutMs?: number | false;
|
|
46
|
+
};
|
|
47
|
+
export interface ApiErrorResponse extends Record<string, unknown> {
|
|
48
|
+
status: 'error';
|
|
49
|
+
httpStatus: number;
|
|
50
|
+
message: string;
|
|
51
|
+
errorCode: string;
|
|
52
|
+
errorParams?: {
|
|
53
|
+
key: string;
|
|
54
|
+
value: string | number | boolean;
|
|
55
|
+
}[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Type-safe API request function.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* // Full name usage - includes page in the name
|
|
63
|
+
* const result = await apiRequest({ name: 'examples/publicApi', version: 'v1', data: { message: 'hello' } });
|
|
64
|
+
* // result is typed correctly for publicApi
|
|
65
|
+
*
|
|
66
|
+
* // Global APIs use service-first naming
|
|
67
|
+
* await apiRequest({ name: 'system/session', version: 'v1' });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare function apiRequest<F extends ApiFullName, V extends VersionsForFullName<F>>(params: ApiParamsForFullName<F, V>): Promise<Prettify<OutputForFullName<F, V>> | ApiErrorResponse>;
|
|
71
|
+
export {};
|
package/dist/apiTypeStubs.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
interface ApiTypeMap {
|
|
1
|
+
export interface ApiTypeMap {
|
|
2
2
|
}
|
|
3
|
-
interface SyncTypeMap {
|
|
3
|
+
export interface SyncTypeMap {
|
|
4
4
|
}
|
|
5
|
-
type StreamPayload = Record<string, unknown>;
|
|
6
|
-
|
|
7
|
-
export type { ApiTypeMap, StreamPayload, SyncTypeMap };
|
|
5
|
+
export type StreamPayload = Record<string, unknown>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DeepPartial } from './configUtils.js';
|
|
2
|
+
export interface AvatarConfig {
|
|
3
|
+
/**
|
|
4
|
+
* File format(s) the framework will look for on disk, in order. The first
|
|
5
|
+
* one that exists wins. Each entry is `{ extension, contentType }` so the
|
|
6
|
+
* `Content-Type` response header matches what the file actually is.
|
|
7
|
+
* Default: `[{ extension: 'webp', contentType: 'image/webp' }]`.
|
|
8
|
+
*/
|
|
9
|
+
formats: {
|
|
10
|
+
extension: string;
|
|
11
|
+
contentType: string;
|
|
12
|
+
}[];
|
|
13
|
+
/**
|
|
14
|
+
* `Cache-Control` value sent with the avatar response.
|
|
15
|
+
* Default: `'public, max-age=86400'` (24 hours).
|
|
16
|
+
*/
|
|
17
|
+
cacheControl: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_AVATAR_CONFIG: AvatarConfig;
|
|
20
|
+
export type AvatarConfigInput = DeepPartial<AvatarConfig>;
|
|
21
|
+
export declare const registerAvatarConfig: (config: AvatarConfigInput) => void;
|
|
22
|
+
export declare const getAvatarConfig: () => AvatarConfig;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface BindAddress {
|
|
2
|
+
ip: string;
|
|
3
|
+
port: number;
|
|
4
|
+
}
|
|
5
|
+
interface IntendedBindAddress extends BindAddress {
|
|
6
|
+
configuredPort?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare const registerBindAddress: (address: IntendedBindAddress) => void;
|
|
9
|
+
export declare const registerBoundAddress: (address: BindAddress) => void;
|
|
10
|
+
export declare const getBindAddress: () => {
|
|
11
|
+
ip: string;
|
|
12
|
+
port: string;
|
|
13
|
+
};
|
|
14
|
+
export declare const resolveDevCallbackUrl: (callbackUrl: string) => string;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const BOOT_KEY_PREFIX = "luckystack:boot:";
|
|
2
|
+
export declare const resolveEnvKey: () => string;
|
|
3
|
+
export declare const writeBootUuid: (envKey?: string) => Promise<string>;
|
|
4
|
+
export declare const readBootUuid: (envKey?: string) => Promise<string | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Extends the current environment boot UUID without rotating its value.
|
|
7
|
+
*
|
|
8
|
+
* Every healthy backend may renew the environment-level key, so multi-instance
|
|
9
|
+
* deployments do not depend on whichever process happened to write it last. If
|
|
10
|
+
* Redis recovered after the key expired, the first refresher writes a new UUID.
|
|
11
|
+
*/
|
|
12
|
+
export declare const refreshBootUuid: (envKey?: string) => Promise<void>;
|
|
13
|
+
export interface BootUuidHeartbeat {
|
|
14
|
+
stop: () => void;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Keeps the Redis boot UUID alive for as long as this backend is running.
|
|
18
|
+
*
|
|
19
|
+
* The self-rescheduling timeout waits for each Redis operation to settle before
|
|
20
|
+
* scheduling another, so an outage cannot accumulate overlapping refreshes.
|
|
21
|
+
* The timer is unref'd and therefore never keeps an otherwise-finished process
|
|
22
|
+
* alive. Failures are logged and retried on the next interval.
|
|
23
|
+
*/
|
|
24
|
+
export declare const startBootUuidHeartbeat: (envKey?: string) => BootUuidHeartbeat;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const registerSyncAbortController: (socketId: string, cb: string, controller: AbortController) => string;
|
|
2
|
+
export declare const unregisterSyncAbortController: (key: string) => void;
|
|
3
|
+
export declare const abortSyncByCb: (socketId: string, cb: string) => boolean;
|
|
4
|
+
export declare const registerApiAbortController: (socketId: string, responseIndex: number | string, controller: AbortController) => string;
|
|
5
|
+
export declare const unregisterApiAbortController: (key: string) => void;
|
|
6
|
+
export declare const abortApiByResponseIndex: (socketId: string, responseIndex: number | string) => boolean;
|
|
7
|
+
export declare const abortAllForSocket: (socketId: string) => void;
|
|
@@ -163,7 +163,7 @@ var sanitizeErrorString = (value) => {
|
|
|
163
163
|
let result = value;
|
|
164
164
|
for (const key of getRedactedLogKeys()) {
|
|
165
165
|
const { urlParam, logLabel } = buildScrubPattern(key);
|
|
166
|
-
result = result.replace(urlParam, REDACTED_PLACEHOLDER).replace(logLabel, REDACTED_PLACEHOLDER);
|
|
166
|
+
result = result.replace(urlParam, () => REDACTED_PLACEHOLDER).replace(logLabel, () => REDACTED_PLACEHOLDER);
|
|
167
167
|
}
|
|
168
168
|
return result;
|
|
169
169
|
};
|
|
@@ -368,4 +368,4 @@ export {
|
|
|
368
368
|
setSentryUser,
|
|
369
369
|
startSpan
|
|
370
370
|
};
|
|
371
|
-
//# sourceMappingURL=chunk-
|
|
371
|
+
//# sourceMappingURL=chunk-P3SSLA3E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/redactedLogKeys.ts","../src/loggerRegistry.ts","../src/errorTrackerRegistry.ts","../src/sentrySetup.ts"],"sourcesContent":["//? Registry of log keys that get masked when the framework prints request /\n//? response payloads. Feature packages register their domain-specific\n//? sensitive keys at boot (login → `password`, email → `apiKey`, etc.) so a\n//? consumer of the package never has to remember to extend a static list in\n//? core to keep their own data out of logs.\n//?\n//? Lookups are case-insensitive. Keys are merged with the framework defaults;\n//? `registerRedactedLogKeys` is additive — duplicate calls are idempotent.\n\nexport const DEFAULT_REDACTED_LOG_KEYS: readonly string[] = [\n 'password',\n 'confirmpassword',\n 'token',\n 'newtoken',\n 'authorization',\n 'cookie',\n 'set-cookie',\n 'csrftoken',\n 'apikey',\n 'secret',\n //? 2FA (ADR 0024). `totpSecret` already matches the `secret` suffix, but\n //? `recoveryCodes` matches neither the exact set nor any suffix — list it\n //? explicitly so the log-redaction floor mirrors the session sanitizer,\n //? which strips both `totpSecret` and `recoveryCodes`.\n 'recoverycodes',\n];\n\nconst redactedKeys = new Set<string>(DEFAULT_REDACTED_LOG_KEYS.map((key) => key.toLowerCase()));\n\n//? Sensitive SUFFIXES (0.2.0 widening). Exact-match alone missed compound keys\n//? a project naturally produces — `targetToken`, `sessionToken`, `clientSecret`,\n//? `stripeApiKey` — leaving real secrets in logs/breadcrumbs. A key whose\n//? lowercased form ENDS WITH one of these is redacted in addition to the exact\n//? set above. Suffix (not arbitrary substring) is deliberate: it catches the\n//? `<qualifier><Sensitive>` naming convention while a plain `substring` test\n//? would over-redact benign keys (e.g. a `tokenCount` metric, a `secretSanta`\n//? field). Kept conservative on purpose — register extra keys explicitly via\n//? `registerRedactedLogKeys` for project-specific names that don't fit.\nconst SENSITIVE_KEY_SUFFIXES: readonly string[] = ['token', 'secret', 'apikey', 'password'];\n\nexport const registerRedactedLogKeys = (keys: readonly string[]): void => {\n for (const key of keys) {\n redactedKeys.add(key.toLowerCase());\n }\n};\n\nexport const getRedactedLogKeys = (): readonly string[] => [...redactedKeys];\n\nexport const isRedactedLogKey = (key: string): boolean => {\n const lower = key.toLowerCase();\n if (redactedKeys.has(lower)) return true;\n return SENSITIVE_KEY_SUFFIXES.some((suffix) => lower.endsWith(suffix));\n};\n\n//? Test-only helper — restore the default seed set between integration tests.\n//? Never call from production code; framework packages register their own\n//? keys at boot and clearing would re-expose them.\nexport const resetRedactedLogKeysForTests = (): void => {\n redactedKeys.clear();\n for (const key of DEFAULT_REDACTED_LOG_KEYS) {\n redactedKeys.add(key.toLowerCase());\n }\n};\n\n/** Placeholder substituted for a redacted value. */\nexport const REDACTED_PLACEHOLDER = '[redacted]';\n\n//? Distinct from REDACTED_PLACEHOLDER so an over-deep BENIGN value (a numeric id,\n//? a timestamp) isn't mislabeled as a masked secret — the marker reads as\n//? structural truncation, not redaction.\nexport const DEPTH_TRUNCATED_PLACEHOLDER = '[truncated: max depth]';\n\n//? Depth cap so a self-referential / pathologically deep object can't make the\n//? sanitize pass blow the stack on a hot log/capture path.\nconst MAX_SANITIZE_DEPTH = 8;\n\n/**\n * Recursively deep-copy `value`, replacing any property whose key matches the\n * registered redacted-keys set (case-insensitive) with {@link REDACTED_PLACEHOLDER}\n * (SYNC-17 defense-in-depth). Use it at raw log / `captureException` context\n * sites so a token/password nested anywhere in a payload never lands verbatim in\n * a logger sink or error-tracker breadcrumb.\n *\n * - Primitives pass through unchanged.\n * - Arrays are mapped element-wise (array indices are never \"keys\", so an\n * element is only redacted by its own nested object keys).\n * - Cycles / over-deep graphs are cut at {@link MAX_SANITIZE_DEPTH} with\n * {@link DEPTH_TRUNCATED_PLACEHOLDER}; the input is never mutated.\n */\nexport const sanitizeForLog = (value: unknown, depth = 0, _seen?: WeakSet<object>): unknown => {\n if (depth > MAX_SANITIZE_DEPTH) return DEPTH_TRUNCATED_PLACEHOLDER;\n if (value === null || typeof value !== 'object') return value;\n\n //? Cycle detection: if we have already visited this reference higher up in the\n //? call stack, replace it with a marker rather than recursing into it — this\n //? prevents a stack overflow when `value` contains a circular reference (e.g.\n //? an Error whose `.cause` links back to itself, or a Node IncomingMessage\n //? with circular HTTP-socket references).\n const seen = _seen ?? new WeakSet<object>();\n if (seen.has(value)) return '[circular]';\n seen.add(value);\n\n if (Array.isArray(value)) {\n return value.map((item) => sanitizeForLog(item, depth + 1, seen));\n }\n\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {\n out[key] = isRedactedLogKey(key) ? REDACTED_PLACEHOLDER : sanitizeForLog(nested, depth + 1, seen);\n }\n return out;\n};\n","//? Pluggable logger so framework code does not have to write directly to\n//? stdout. The default implementation uses `console.{debug,info,warn,error}`\n//? directly with no trailing color-code argument — that argument was only\n//? meaningful when `initConsolelog()` (a dev-only monkey-patch) had been\n//? installed, but the default logger needs to be safe in production too.\n//?\n//? Installers route framework logs to Pino / Winston / Datadog / etc. by\n//? calling `registerLogger({ debug, info, warn, error })` once at boot,\n//? mirroring the rest of the registry pattern in this package. Dev installs\n//? that want colored output should call `registerLogger(createDevLogger())`.\n//?\n//? Read at call-time via `getLogger()` so registration order does not matter\n//? — same contract as `getProjectConfig()` and friends.\n\nimport { createRegistry } from './createRegistry';\nimport { getProjectConfig } from './projectConfig';\n\nexport type LoggerContext = Record<string, unknown>;\n\nexport interface Logger {\n debug: (message: string, context?: LoggerContext) => void;\n info: (message: string, context?: LoggerContext) => void;\n warn: (message: string, context?: LoggerContext) => void;\n error: (message: string, error?: unknown, context?: LoggerContext) => void;\n}\n\n//? Prefix a log message with an ISO-8601 UTC timestamp when\n//? `logging.timestamps` is on (the default). Read at call time so a consumer's\n//? config registration order doesn't matter, and so it can be toggled off under\n//? a log aggregator that stamps its own time. Only the framework's BUILT-IN\n//? loggers use this — a registered custom logger owns its own formatting.\nconst withTimestamp = (message: string): string =>\n getProjectConfig().logging.timestamps ? `[${new Date().toISOString()}] ${message}` : message;\n\nconst defaultLogger: Logger = {\n debug: (message, context) => {\n if (context === undefined) {console.debug(withTimestamp(message));}\n else {console.debug(withTimestamp(message), context);}\n },\n info: (message, context) => {\n if (context === undefined) {console.info(withTimestamp(message));}\n else {console.info(withTimestamp(message), context);}\n },\n warn: (message, context) => {\n if (context === undefined) {console.warn(withTimestamp(message));}\n else {console.warn(withTimestamp(message), context);}\n },\n error: (message, error, context) => {\n if (error !== undefined && context !== undefined) console.error(withTimestamp(message), error, context);\n else if (error !== undefined) console.error(withTimestamp(message), error);\n else if (context === undefined) {console.error(withTimestamp(message));}\n else {console.error(withTimestamp(message), context);}\n },\n};\n\n//? Dev-mode logger factory: wraps the default behavior with the colored\n//? terminal output that `initConsolelog()` produces. Install via\n//? `registerLogger(createDevLogger())` from a dev-only entry. Note: this\n//? still depends on `initConsolelog()` having been called to interpret the\n//? trailing color string — without it, the codes will appear as literal\n//? text. Use the default logger when running in production or unsure.\nexport const createDevLogger = (): Logger => ({\n debug: (message, context) => {\n if (context === undefined) {console.log(withTimestamp(message), 'cyan');}\n else {console.log(withTimestamp(message), context, 'cyan');}\n },\n info: (message, context) => {\n if (context === undefined) {console.log(withTimestamp(message));}\n else {console.log(withTimestamp(message), context);}\n },\n warn: (message, context) => {\n if (context === undefined) {console.log(withTimestamp(message), 'yellow');}\n else {console.log(withTimestamp(message), context, 'yellow');}\n },\n error: (message, error, context) => {\n if (error !== undefined && context !== undefined) console.log(withTimestamp(message), error, context, 'red');\n else if (error !== undefined) console.log(withTimestamp(message), error, 'red');\n else if (context === undefined) {console.log(withTimestamp(message), 'red');}\n else {console.log(withTimestamp(message), context, 'red');}\n },\n});\n\nconst registry = createRegistry<Logger>(defaultLogger);\n\nexport const registerLogger = (logger: Logger): void => {\n registry.register(logger);\n};\n\nexport const getLogger = (): Logger => registry.get();\n\nexport const isLoggerRegistered = (): boolean => registry.isRegistered();\n\n//? Test-only helper — restore the default logger between integration tests.\n//? Never call from production code.\nexport const resetLoggerForTests = (): void => {\n registry.reset();\n};\n","//? Backend-agnostic error-tracker registry. Lives in @luckystack/core\n//? (not @luckystack/error-tracking) because framework code in core /\n//? api / sync needs to call it without taking a hard dep on a specific\n//? observability backend. The error-tracking package provides adapter\n//? IMPLEMENTATIONS (Sentry, Datadog, PostHog); the slot lives here.\n//?\n//? Multiple adapters can be registered at once via `registerErrorTrackers([...])` —\n//? every event fans out to all of them. Per-adapter errors are swallowed\n//? so one buggy tracker can't break the chain.\n\n//? The AsyncLocalStorage identity scope deliberately lives in the separate\n//? server-only errorTrackerIdentity.ts module. This registry is also reached by\n//? the browser-safe legacy capture seam, so it must remain free of node:* imports.\nimport type { ErrorTrackerUser } from './errorTrackerIdentity';\nimport { getRedactedLogKeys, REDACTED_PLACEHOLDER, sanitizeForLog } from './redactedLogKeys';\nimport { getLogger } from './loggerRegistry';\n\nexport type { ErrorTrackerUser } from './errorTrackerIdentity';\n\nexport type ErrorTrackerContext = Record<string, unknown>;\n\n//? SYNC-17 defense-in-depth: scrub registered redacted keys (tokens, passwords,\n//? auth/cookie headers) from the context object before it fans out to any\n//? adapter, so a raw token nested in a capture context never reaches Sentry /\n//? Datadog / PostHog breadcrumbs. Returns the original reference when there is\n//? nothing to sanitize (no context) to avoid needless allocation on the hot path.\nconst sanitizeContext = (context?: ErrorTrackerContext): ErrorTrackerContext | undefined => {\n if (!context) return context;\n return sanitizeForLog(context) as ErrorTrackerContext;\n};\n\n//? ET-O2 value-level scrub: error.message and error.stack can contain\n//? interpolated secrets (\"token=abc123\") that bypass the key-based context\n//? scrub. Replace `key=value` and `key: value` patterns for every registered\n//? redacted key so the string never reaches an adapter verbatim.\n//? Runs at capture time (not on every Error construction) to stay off the\n//? hot path; the result is cached on the sanitized context so each adapter\n//? fan-out step reads the same pre-scrubbed string.\n//? BOOT-TIME ONLY: `registerRedactedLogKeys` must be called at boot with a\n//? static key set (framework package init / project boot). Calling it with\n//? dynamically generated strings (route names, tenant IDs, etc.) at request\n//? time will cause this cache to grow without bound. A hard cap (200 entries)\n//? logs a warning when exceeded so misuse is immediately diagnosable.\nconst REDACTED_VALUE_RE_CACHE_MAX = 200;\ninterface ScrubPatternPair {\n urlParam: RegExp; // matches <key>=<value> (URL-param style)\n logLabel: RegExp; // matches <key>: <value> (log-label style)\n}\nconst REDACTED_VALUE_RE_CACHE = new Map<string, ScrubPatternPair>();\n\nconst buildScrubPattern = (key: string): ScrubPatternPair => {\n const cached = REDACTED_VALUE_RE_CACHE.get(key);\n if (cached) return cached;\n if (REDACTED_VALUE_RE_CACHE.size >= REDACTED_VALUE_RE_CACHE_MAX) {\n try {\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.`);\n } catch {\n // last-resort swallow\n }\n }\n const escaped = key.replaceAll(/[.*+?^${}()|[\\]\\\\]/g, String.raw`\\$&`);\n //? Two separate passes — one for `key=value` (URL-param style), one for\n //? `key: value` (log-label style) — to avoid alternation complexity and the\n //? subtle behaviours that arise when mixing capturing and non-capturing groups\n //? inside a single pattern (ET-O2 alternation fix).\n //? `+` (not `*`) for the value portion so `key=` with an empty value is NOT\n //? replaced (empty parameters are not secrets worth scrubbing and a `*` match\n //? would turn innocent `foo=` markers into `[redacted]`).\n const pair: ScrubPatternPair = {\n urlParam: new RegExp(String.raw`${escaped}=[^\\s,;\"'&]+`, 'gi'),\n logLabel: new RegExp(String.raw`${escaped}:\\s*[^\\s,;\"']+`, 'gi'),\n };\n REDACTED_VALUE_RE_CACHE.set(key, pair);\n return pair;\n};\n\nexport const sanitizeErrorString = (value: string): string => {\n let result = value;\n // Scrub matching `key=value` / `key: value` patterns for every registered\n // redacted key so interpolated secrets (e.g. \"token=abc\") never reach an adapter.\n for (const key of getRedactedLogKeys()) {\n const { urlParam, logLabel } = buildScrubPattern(key);\n result = result.replace(urlParam, () => REDACTED_PLACEHOLDER).replace(logLabel, () => REDACTED_PLACEHOLDER);\n }\n return result;\n};\n\n//? Produce a sanitized version of an Error's message and stack so adapters\n//? can emit pre-scrubbed strings without touching the original Error object.\nexport const sanitizeErrorStrings = (\n error: unknown,\n): { message: string; stack: string | undefined } | null => {\n if (!(error instanceof Error)) return null;\n return {\n message: sanitizeErrorString(error.message),\n stack: error.stack === undefined ? undefined : sanitizeErrorString(error.stack),\n };\n};\n\nexport interface ErrorTrackerEvent {\n /** When false, the adapter must not forward this event (beforeSend opt-out). */\n forwarded: boolean;\n kind: 'exception' | 'message';\n payload: Record<string, unknown>;\n}\n\n//? `SpanResult<T>` was a conditional unwrap of Promise. It's now an alias\n//? for `T` directly because the conditional collapsed to T in both branches\n//? and tsup's dts emit choked on the conditional widening. Kept as an\n//? exported alias so adapter authors can still annotate with `SpanResult<T>`\n//? for documentation intent.\nexport type SpanResult<T> = T;\n\nexport interface ErrorTracker {\n /** Human-readable identifier (logs, diagnostics). */\n name: string;\n captureException: (error: unknown, context?: ErrorTrackerContext) => void;\n captureMessage: (\n message: string,\n level: 'info' | 'warning' | 'error' | 'fatal',\n context?: ErrorTrackerContext,\n ) => void;\n setUser: (user: ErrorTrackerUser | null) => void;\n setContext?: (key: string, context: ErrorTrackerContext | null) => void;\n startSpan?: <T>(name: string, op: string, fn: () => T) => T;\n /**\n * Optional handle-style span (#62): open a span NOW and close it later via the\n * returned handle's `finish()`. Unlike {@link startSpan} (which wraps a single\n * synchronous `fn`), this fits an open-at-preExecute / close-at-postExecute\n * request lifecycle that spans awaits. When present, the registry-level\n * {@link startSpanHandle} delegates to the FIRST registered adapter that\n * implements it so the request span reaches an adapter-only backend\n * (Datadog/PostHog/custom), not just the legacy Sentry SDK path.\n */\n startSpanHandle?: (name: string, op: string) => { finish: () => void };\n recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;\n beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;\n /**\n * Optional flush lifecycle hook (error-tracking ET batch). Called by\n * {@link flushErrorTrackers} on graceful shutdown so a buffered adapter\n * (PostHog batch, Sentry transport) can drain in-flight events before exit.\n * Returns a promise that resolves when the adapter has flushed.\n */\n flush?: () => Promise<void>;\n}\n\n//? Pre-capture filter (error-tracking ET batch). A registered filter runs on\n//? EVERY event just before fan-out; returning `false` DROPS the event entirely\n//? (e.g. suppress known-noisy errors, sample, or honour a per-event opt-out).\n//? Distinct from a per-adapter `beforeSend` (which transforms a single adapter's\n//? payload) — this gates the whole fan-out. Last-write-wins; `null` clears it.\nexport type PreCaptureFilter = (event: ErrorTrackerEvent) => boolean;\nlet preCaptureFilter: PreCaptureFilter | null = null;\n\nexport const registerPreCaptureFilter = (filter: PreCaptureFilter | null): void => {\n preCaptureFilter = filter;\n};\n\nconst passesPreCaptureFilter = (kind: 'exception' | 'message', payload: Record<string, unknown>): boolean => {\n if (!preCaptureFilter) return true;\n try {\n return preCaptureFilter({ forwarded: true, kind, payload });\n } catch {\n //? A throwing filter must not swallow telemetry — fail OPEN (forward).\n return true;\n }\n};\n\n//? ET-O6: the pre-capture filter payload now includes the error / message\n//? so filters can drop noisy errors by type/message (the documented use case).\n//? Separate helpers per kind so the call site stays type-safe.\nconst exceptionFilterPayload = (\n error: unknown,\n context: ErrorTrackerContext | undefined,\n): Record<string, unknown> => ({ error, context: context ?? null });\n\nconst messageFilterPayload = (\n message: string,\n level: string,\n context: ErrorTrackerContext | undefined,\n): Record<string, unknown> => ({ message, level, context: context ?? null });\n\nlet activeTrackers: ErrorTracker[] = [];\n\n//? REPLACE semantics — last-write-wins (ET-24 confirmed standard). Use\n//? `appendErrorTracker` when you need accumulate-not-replace (ET-05).\nexport const registerErrorTracker = (tracker: ErrorTracker): void => {\n activeTrackers = [tracker];\n};\n\nexport const registerErrorTrackers = (trackers: ErrorTracker[]): void => {\n activeTrackers = [...trackers];\n};\n\n/**\n * APPEND primitive (ET-05): add a tracker WITHOUT clobbering already-registered\n * ones. Fixes the async-PostHog-vs-consumer-overlay race where the loser of\n * `registerErrorTracker`'s replace silently vanished. De-duplicates by `name`\n * (re-appending the same adapter name replaces that one entry in place) so a\n * lazy adapter that registers a proxy then the real client doesn't double-fire.\n */\nexport const appendErrorTracker = (tracker: ErrorTracker): void => {\n const withoutSameName = activeTrackers.filter((t) => t.name !== tracker.name);\n activeTrackers = [...withoutSameName, tracker];\n};\n\nexport const getActiveErrorTrackers = (): ErrorTracker[] => activeTrackers;\n\n/**\n * Flush lifecycle (error-tracking ET batch): drain every adapter that exposes a\n * `flush()` before shutdown. Per-adapter failures are swallowed + logged so one\n * stuck adapter can't block graceful shutdown. Safe to call when no adapter has\n * `flush`.\n */\nexport const flushErrorTrackers = async (): Promise<void> => {\n await Promise.all(\n activeTrackers.map(async (tracker) => {\n if (!tracker.flush) return;\n try {\n await tracker.flush();\n } catch (error) {\n //? ET-O7: route to the shared failure logger (but never re-throw — a\n //? failing flush must not block graceful shutdown).\n logTrackerFailure(tracker.name, 'flush', error);\n }\n }),\n );\n};\n\nexport const captureExceptionAcrossTrackers = (\n error: unknown,\n context?: ErrorTrackerContext,\n): void => {\n const safeContext = sanitizeContext(context);\n if (!passesPreCaptureFilter('exception', exceptionFilterPayload(error, safeContext))) return;\n for (const tracker of activeTrackers) {\n try {\n tracker.captureException(error, safeContext);\n } catch (trackerError) {\n //? Fan-out error logging (error-tracking ET batch): a buggy tracker must\n //? not break the chain, but its failure was previously invisible. Log it\n //? (best-effort) so a misbehaving adapter is diagnosable.\n logTrackerFailure(tracker.name, 'captureException', trackerError);\n }\n }\n};\n\nexport const captureMessageAcrossTrackers = (\n message: string,\n level: 'info' | 'warning' | 'error' | 'fatal',\n context?: ErrorTrackerContext,\n): void => {\n const safeContext = sanitizeContext(context);\n if (!passesPreCaptureFilter('message', messageFilterPayload(message, level, safeContext))) return;\n for (const tracker of activeTrackers) {\n try {\n tracker.captureMessage(message, level, safeContext);\n } catch (trackerError) {\n logTrackerFailure(tracker.name, 'captureMessage', trackerError);\n }\n }\n};\n\n//? Best-effort fan-out failure logger. Wrapped in its own try/catch because the\n//? logger itself could throw on a broken sink, and a capture path must never\n//? throw.\nconst logTrackerFailure = (trackerName: string, op: string, error: unknown): void => {\n try {\n getLogger().warn(`errorTracker: \"${trackerName}\" threw in ${op}`, { error });\n } catch {\n // Last-resort swallow.\n }\n};\n\nexport const setErrorTrackerUser = (user: ErrorTrackerUser | null): void => {\n for (const tracker of activeTrackers) {\n try {\n tracker.setUser(user);\n } catch (error) {\n //? ET-O7: route to the shared failure logger so a misbehaving tracker is diagnosable.\n logTrackerFailure(tracker.name, 'setUser', error);\n }\n }\n};\n\nexport const recordMetricAcrossTrackers = (\n name: string,\n value: number,\n tags?: Record<string, string>,\n): void => {\n for (const tracker of activeTrackers) {\n if (tracker.recordMetric) {\n try {\n tracker.recordMetric(name, value, tags);\n } catch (error) {\n //? ET-O7: route to the shared failure logger so a misbehaving tracker is diagnosable.\n logTrackerFailure(tracker.name, 'recordMetric', error);\n }\n }\n }\n};\n\nexport const startSpanAcrossTrackers = <T>(name: string, op: string, fn: () => T): T => {\n //? Spans don't fan out cleanly to multiple backends — they're nested\n //? execution scopes. We only invoke the FIRST registered tracker's\n //? startSpan (others get notified via captureException paths if they\n //? want to instrument). When no tracker supports spans, run the fn directly.\n const first = activeTrackers.find((t) => t.startSpan);\n if (!first?.startSpan) return fn();\n return first.startSpan(name, op, fn);\n};\n\n/**\n * Handle-style span (error-tracking ET batch): for callers that can't wrap the\n * measured work in a single synchronous `fn` (streaming, cross-await spans).\n * Returns a handle whose `finish()` resolves the span's duration. The default\n * implementation is a lightweight wall-clock timer (works with any backend);\n * an adapter can supply a richer span via the existing `startSpan` callback\n * form when it needs native instrumentation. `finish()` is idempotent.\n */\nexport interface SpanHandle {\n /** Stop the span; safe to call more than once (subsequent calls are no-ops). */\n finish: () => void;\n /** Milliseconds elapsed at the moment `finish()` first ran (0 before). */\n readonly durationMs: number;\n}\n\n//? #62: open a handle-style span on the FIRST registered adapter that implements\n//? `startSpanHandle`, swallowing a throwing adapter so the wall-clock handle still\n//? works (telemetry must never break the request path). Returns `null` when no\n//? adapter supports handle-style spans.\nconst openDelegateSpanHandle = (name: string, op: string): { finish: () => void } | null => {\n const tracker = activeTrackers.find((t) => t.startSpanHandle);\n if (!tracker?.startSpanHandle) return null;\n try {\n return tracker.startSpanHandle(name, op);\n } catch (error) {\n logTrackerFailure(tracker.name, 'startSpanHandle', error);\n return null;\n }\n};\n\n//? #62: delegate the handle to the first adapter that supports `startSpanHandle`\n//? (so an adapter-only consumer — Datadog/PostHog/custom, no legacy Sentry SDK —\n//? actually gets request-timing spans), and ALWAYS keep the wall-clock timer as a\n//? backend-agnostic fallback + `durationMs` source. `finish()` is idempotent and\n//? closes the delegated span exactly once.\n//? ET-O9: removed the per-call `recordMetricAcrossTrackers` that fired on every\n//? startSpanHandle invocation — `durationMs` is available for callers that want to\n//? emit their own metric.\nexport const startSpanHandle = (name: string, op: string): SpanHandle => {\n const startedAt = Date.now();\n let finishedAt: number | null = null;\n const delegate = openDelegateSpanHandle(name, op);\n return {\n finish: () => {\n if (finishedAt !== null) return;\n finishedAt = Date.now();\n if (delegate) {\n try {\n delegate.finish();\n } catch (error) {\n logTrackerFailure('startSpanHandle', 'finish', error);\n }\n }\n },\n get durationMs() {\n return finishedAt === null ? 0 : finishedAt - startedAt;\n },\n };\n};\n","//? Legacy Sentry single-instance slot. Predates the multi-adapter\n//? ErrorTracker registry. New code should `registerErrorTracker(...)`\n//? from `@luckystack/error-tracking`; this file keeps backwards\n//? compatibility AND fans out events through any registered adapters so\n//? a project can mix legacy `initSharedSentry` + new `registerErrorTracker`\n//? during migration.\n//?\n//? The framework's own call sites (`captureException`, `captureMessage`,\n//? `setSentryUser`, `startSpan`) keep working unchanged — they now route\n//? through both the legacy slot and the new adapter list.\n\nimport {\n captureExceptionAcrossTrackers,\n captureMessageAcrossTrackers,\n setErrorTrackerUser,\n startSpanAcrossTrackers,\n} from './errorTrackerRegistry';\nimport { sanitizeForLog } from './redactedLogKeys';\n\ninterface SentryInstance {\n captureException: (exception: unknown, ...args: unknown[]) => string;\n captureMessage: (message: string, ...args: unknown[]) => string;\n setUser: (user: unknown) => void;\n setContext: (key: string, context: unknown) => void;\n startInactiveSpan?: (context: unknown) => unknown;\n}\n\nlet sentry: SentryInstance | undefined;\n\n/**\n * @deprecated Use `registerErrorTracker` from `@luckystack/error-tracking` instead.\n * Will be removed in a future major version.\n */\nexport const initSharedSentry = (instance: SentryInstance): void => {\n sentry = instance;\n};\n\n//? Pass `extra` inline via Sentry's hint-shaped second argument so contexts\n//? don't leak across concurrent captures. Previously we called\n//? `sentry.setContext('additional', context)` which is process-global until\n//? cleared — under concurrent captures the first context could leak into the\n//? second's report.\n//? CORE-O4: sanitize context before passing to the legacy Sentry slot so raw\n//? tokens/passwords don't reach `extra` when a consumer mixes initSharedSentry\n//? with framework capture. The multi-adapter path already sanitizes in\n//? errorTrackerRegistry.ts; this brings the legacy path into parity.\nexport const captureException = (\n error: unknown,\n context?: Record<string, unknown>,\n): void => {\n if (sentry) {\n if (context) {\n sentry.captureException(error, { extra: sanitizeForLog(context) as Record<string, unknown> });\n } else {\n sentry.captureException(error);\n }\n }\n //? Multi-adapter fan-out. When no adapter is registered this is a no-op.\n captureExceptionAcrossTrackers(error, context);\n};\n\nexport const captureMessage = (\n message: string,\n level: 'info' | 'warning' | 'error' | 'fatal' = 'info',\n context?: Record<string, unknown>,\n): void => {\n if (sentry) {\n if (context) {\n sentry.captureMessage(message, { level, extra: sanitizeForLog(context) as Record<string, unknown> });\n } else {\n sentry.captureMessage(message, level);\n }\n }\n captureMessageAcrossTrackers(message, level, context);\n};\n\nexport const setSentryUser = (user: {\n id?: string;\n email?: string;\n username?: string;\n} | null): void => {\n sentry?.setUser(user);\n setErrorTrackerUser(user);\n};\n\nexport const startSpan = (name: string, op: string): unknown => {\n //? Multi-adapter span coordination: first registered adapter that\n //? supports spans wins (spans don't fan out cleanly). Legacy Sentry\n //? single-instance path still creates the inactive span for backwards\n //? compatibility on consumers that haven't migrated.\n if (sentry?.startInactiveSpan) {\n return sentry.startInactiveSpan({ name, op });\n }\n //? No-op span body is intentional — this is the legacy inactive-span shape\n //? consumers built against; the real span lifecycle is managed elsewhere.\n // eslint-disable-next-line @typescript-eslint/no-empty-function -- legacy inactive-span shape\n startSpanAcrossTrackers(name, op, () => {});\n};\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
captureException,
|
|
3
3
|
getLogger
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-P3SSLA3E.js";
|
|
5
5
|
import {
|
|
6
6
|
createRegistry,
|
|
7
7
|
getProjectConfig
|
|
@@ -726,4 +726,4 @@ export {
|
|
|
726
726
|
DEFAULT_PAGE_ROUTE_RULES,
|
|
727
727
|
validatePagePath
|
|
728
728
|
};
|
|
729
|
-
//# sourceMappingURL=chunk-
|
|
729
|
+
//# sourceMappingURL=chunk-XDX5MV54.js.map
|