@luckystack/core 0.10.2 → 0.11.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 (122) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +2 -88
  4. package/dist/apiInterceptors.d.ts +36 -0
  5. package/dist/apiMethodMapRegistry.d.ts +5 -0
  6. package/dist/apiRequest.d.ts +71 -0
  7. package/dist/apiTypeStubs.d.ts +3 -5
  8. package/dist/avatarConfig.d.ts +22 -0
  9. package/dist/bindAddress.d.ts +15 -0
  10. package/dist/bootUuid.d.ts +24 -0
  11. package/dist/cancelRegistry.d.ts +7 -0
  12. package/dist/checkOrigin.d.ts +6 -0
  13. package/dist/client.d.ts +46 -335
  14. package/dist/client.js +2 -2
  15. package/dist/client.js.map +1 -1
  16. package/dist/clientHookBus.d.ts +62 -0
  17. package/dist/clients.d.ts +16 -0
  18. package/dist/config.d.ts +8 -2
  19. package/dist/configUtils.d.ts +16 -0
  20. package/dist/consoleLog.d.ts +1 -0
  21. package/dist/cookies.d.ts +28 -0
  22. package/dist/createRegistry.d.ts +39 -0
  23. package/dist/csrf.d.ts +30 -0
  24. package/dist/csrfConfig.d.ts +32 -0
  25. package/dist/db.d.ts +12 -0
  26. package/dist/dbHealthCheck.d.ts +6 -0
  27. package/dist/{config-CwW3KY9r.d.ts → deployConfigRegistry.d.ts} +8 -43
  28. package/dist/emailRegistry.d.ts +139 -0
  29. package/dist/env.d.ts +38 -0
  30. package/dist/errorFormatterRegistry.d.ts +54 -0
  31. package/dist/errorTrackerIdentity.d.ts +14 -0
  32. package/dist/errorTrackerRegistry.d.ts +85 -0
  33. package/dist/escapeHtml.d.ts +6 -0
  34. package/dist/eslint/index.d.ts +5 -8
  35. package/dist/eslint/internal/hasPackage.d.ts +1 -0
  36. package/dist/eslint/internal/ruleTypes.d.ts +4 -0
  37. package/dist/eslint/rules/no-arbitrary-tailwind-color.d.ts +3 -0
  38. package/dist/eslint/rules/no-direct-prisma-import-in-components.d.ts +3 -0
  39. package/dist/eslint/rules/no-local-socket-enumeration.d.ts +3 -0
  40. package/dist/eslint/rules/no-raw-fetch-in-src.d.ts +3 -0
  41. package/dist/eslint/rules/no-raw-try-catch.d.ts +3 -0
  42. package/dist/eslint/rules/no-unsafe-api-wrappers.d.ts +3 -0
  43. package/dist/eslint/rules/no-unsafe-sync-wrappers.d.ts +3 -0
  44. package/dist/eslint/rules/prefer-luckystack-confirm.d.ts +3 -0
  45. package/dist/eslint/rules/prefer-luckystack-dropdown.d.ts +3 -0
  46. package/dist/eslint/rules/prefer-luckystack-notify.d.ts +3 -0
  47. package/dist/extractToken.d.ts +12 -0
  48. package/dist/extractTokenFromRequest.d.ts +12 -0
  49. package/dist/getParams.d.ts +9 -0
  50. package/dist/hooks/registry.d.ts +12 -0
  51. package/dist/hooks/types.d.ts +446 -0
  52. package/dist/httpApiUtils.d.ts +54 -0
  53. package/dist/index.d.ts +110 -1892
  54. package/dist/index.js.map +1 -1
  55. package/dist/lease.d.ts +18 -0
  56. package/dist/localSocketEnumerationGuard.d.ts +22 -0
  57. package/dist/localeReloader.d.ts +3 -0
  58. package/dist/localesRegistry.d.ts +8 -0
  59. package/dist/localizedNormalizer.d.ts +50 -0
  60. package/dist/loggerRegistry.d.ts +12 -0
  61. package/dist/middlewareRegistry.d.ts +45 -0
  62. package/dist/notifier.d.ts +17 -0
  63. package/dist/offlineQueue.d.ts +48 -0
  64. package/dist/oneTimeToken.d.ts +43 -0
  65. package/dist/pageRouteValidation.d.ts +37 -0
  66. package/dist/paths.d.ts +35 -0
  67. package/dist/peerDeps.d.ts +23 -0
  68. package/dist/portOverrideRegistry.d.ts +3 -0
  69. package/dist/processUpload.d.ts +44 -0
  70. package/dist/{sessionTypes-DanjuPaE.d.ts → projectConfig.d.ts} +32 -129
  71. package/dist/rateLimiter.d.ts +90 -0
  72. package/dist/react/AvatarProvider.d.ts +11 -0
  73. package/dist/react/Middleware.d.ts +10 -0
  74. package/dist/react/Router.d.ts +1 -0
  75. package/dist/react/TranslationProvider.d.ts +22 -0
  76. package/dist/react/notify.d.ts +12 -0
  77. package/dist/react/sessionContext.d.ts +31 -0
  78. package/dist/react/useTheme.d.ts +5 -0
  79. package/dist/redactedLogKeys.d.ts +22 -0
  80. package/dist/redis.d.ts +15 -0
  81. package/dist/redisKeyFormatter.d.ts +7 -0
  82. package/dist/resolveClientIp.d.ts +51 -0
  83. package/dist/responseNormalizer.d.ts +33 -0
  84. package/dist/roomNameFormatterRegistry.d.ts +36 -0
  85. package/dist/roomSockets.d.ts +23 -0
  86. package/dist/routedDataQuery.d.ts +7 -0
  87. package/dist/routedHttpInvocation.d.ts +20 -0
  88. package/dist/runtimeMapsRegistry.d.ts +18 -0
  89. package/dist/runtimeTypeValidation.d.ts +13 -0
  90. package/dist/secretsResolved.d.ts +3 -0
  91. package/dist/sentrySetup.d.ts +21 -0
  92. package/dist/serveAvatars.d.ts +5 -0
  93. package/dist/serviceRoute.d.ts +27 -0
  94. package/dist/servicesConfigRegistry.d.ts +22 -0
  95. package/dist/sessionProviderRegistry.d.ts +46 -0
  96. package/dist/sessionTypes.d.ts +79 -0
  97. package/dist/sleep.d.ts +1 -0
  98. package/dist/socketEvents.d.ts +37 -0
  99. package/dist/socketMiddlewareRegistry.d.ts +20 -0
  100. package/dist/socketRedisAdapter.d.ts +21 -0
  101. package/dist/socketState.d.ts +5 -0
  102. package/dist/socketStatusTypes.d.ts +6 -0
  103. package/dist/socketTypes.d.ts +34 -0
  104. package/dist/synchronizedEnvHashes.d.ts +66 -0
  105. package/dist/testFileConvention.d.ts +11 -0
  106. package/dist/testResetTargets.d.ts +5 -0
  107. package/dist/tryCatch.d.ts +1 -0
  108. package/dist/tryCatchClient.d.ts +1 -0
  109. package/dist/tryCatchSync.d.ts +1 -0
  110. package/dist/validateRequest.d.ts +50 -0
  111. package/docs/app-bootstrap.md +4 -449
  112. package/docs/config-registry.md +3 -346
  113. package/docs/csrf-config.md +3 -86
  114. package/docs/error-tracker-registry.md +3 -192
  115. package/docs/hooks.md +2 -295
  116. package/docs/rate-limit-strategy.md +3 -168
  117. package/docs/redis-adapter.md +5 -110
  118. package/docs/session-types.md +3 -178
  119. package/docs/socket-bootstrap.md +5 -385
  120. package/package.json +2 -2
  121. package/CLAUDE.md +0 -256
  122. package/dist/pageRouteValidation-B4Cmtfjv.d.ts +0 -280
@@ -0,0 +1,16 @@
1
+ export type DeepPartial<T> = {
2
+ [K in keyof T]?: T[K] extends object | undefined ? DeepPartial<NonNullable<T[K]>> : T[K];
3
+ };
4
+ /**
5
+ * True when `value` is a plain object literal (prototype is `Object.prototype`
6
+ * or `null`). Arrays, class instances, Maps, Dates, and `null` are rejected so
7
+ * they are treated as opaque replacement values during a merge.
8
+ */
9
+ export declare const isPlainObject: (value: unknown) => value is Record<string, unknown>;
10
+ /**
11
+ * Deep-merge a deep-partial `override` over a fully-populated `base`,
12
+ * returning a new object. See the module header for the exact behaviour
13
+ * contract. Arrays and non-plain-object values are replaced wholesale; nested
14
+ * plain objects merge recursively; `undefined` override values are ignored.
15
+ */
16
+ export declare const deepMerge: <T>(base: T, override: DeepPartial<T> | undefined) => T;
@@ -0,0 +1 @@
1
+ export declare const initConsolelog: () => void;
@@ -0,0 +1,28 @@
1
+ export declare const getCookieValue: (cookieHeader: string | undefined, cookieName: string) => string | null;
2
+ export declare const hasCookie: (cookieHeader: string | undefined, cookieName: string) => boolean;
3
+ /** Attributes a cookie-builder must honour, after applying any name prefix. */
4
+ export interface CookiePrefixConstraints {
5
+ /** Effective cookie name (prefix prepended when applicable). */
6
+ name: string;
7
+ /** Whether `Secure` must be set (forced true by either prefix). */
8
+ secure: boolean;
9
+ /** Whether `Path=/` is forced (true under `__Host-`). */
10
+ forcePathRoot: boolean;
11
+ /** Whether a `Domain` attribute is FORBIDDEN (true under `__Host-`). */
12
+ forbidDomain: boolean;
13
+ }
14
+ /**
15
+ * Compute the attribute constraints a `__Host-` / `__Secure-` cookie-name prefix
16
+ * imposes (CORE-10/39), so the server's session + CSRF cookie builders apply the
17
+ * browser-enforced rules consistently:
18
+ *
19
+ * - `'__Host-'` → name prefixed, `Secure` forced, `Path=/` forced, `Domain` forbidden.
20
+ * - `'__Secure-'` → name prefixed, `Secure` forced.
21
+ * - `undefined` → name unchanged, no forced attributes (today's behavior).
22
+ *
23
+ * Pure + deterministic — the builder still owns serialization; this only
24
+ * resolves the constraints so server and any other cookie-site can't drift.
25
+ * `secureOverride` lets the caller force `Secure` independent of a prefix
26
+ * (mirrors `http.sessionCookieSecure`).
27
+ */
28
+ export declare const applyCookiePrefixConstraints: (baseName: string, prefix: '__Host-' | '__Secure-' | undefined, secureOverride?: boolean) => CookiePrefixConstraints;
@@ -0,0 +1,39 @@
1
+ type RequireTransform<TInput, TStored> = [TInput] extends [TStored] ? {
2
+ transform?: (input: TInput, current: TStored) => TStored;
3
+ } : {
4
+ transform: (input: TInput, current: TStored) => TStored;
5
+ };
6
+ interface RegistryOptionsBase<TStored> {
7
+ /**
8
+ * Called once per `register` with the freshly-stored value — used for the
9
+ * rate-limiter's `[RateLimiter] active strategy → …` debug line. Runs after
10
+ * the slot is updated so a logger registry can log through itself.
11
+ */
12
+ onRegister?: (stored: TStored) => void;
13
+ /**
14
+ * Resolve the value `get` returns when nothing has been registered yet.
15
+ * Defaults to returning `defaultValue`. `servicesConfig` throws here.
16
+ */
17
+ resolveDefault?: () => TStored;
18
+ }
19
+ export type RegistryOptions<TInput, TStored> = RegistryOptionsBase<TStored> & RequireTransform<TInput, TStored>;
20
+ export interface Registry<TInput, TStored> {
21
+ /** Store a value (after the optional `transform`) and mark the slot registered. */
22
+ register: (input: TInput) => TStored;
23
+ /** Read the active value at call time (registered value, else the default). */
24
+ get: () => TStored;
25
+ /** True once `register` has been called at least once. */
26
+ isRegistered: () => boolean;
27
+ /** Restore the unregistered state (test-only). */
28
+ reset: () => void;
29
+ }
30
+ /**
31
+ * Create a single-slot registry. `defaultValue` is returned by `get` until
32
+ * `register` is called (unless `resolveDefault` overrides that). When
33
+ * `TInput` differs from `TStored`, the `transform` option is mandatory (the
34
+ * `RequireTransform` constraint enforces it); when they match it is optional
35
+ * and defaults to identity. The second positional argument is likewise
36
+ * optional only in the identity case.
37
+ */
38
+ export declare function createRegistry<TStored, TInput = TStored>(defaultValue: TStored, ...optionsArg: [TInput] extends [TStored] ? [options?: RegistryOptions<TInput, TStored>] : [options: RegistryOptions<TInput, TStored>]): Registry<TInput, TStored>;
39
+ export {};
package/dist/csrf.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fetch (or return the cached) CSRF token for the active session. Returns
3
+ * null in token mode or when no session exists. Callers should attach the
4
+ * returned value as the `X-CSRF-Token` request header.
5
+ */
6
+ export declare const getCsrfToken: (origin?: string) => Promise<string | null>;
7
+ /** Drop the cached token. Call this on logout or when a 403 csrfMismatch is seen. */
8
+ export declare const clearCsrfToken: () => void;
9
+ /**
10
+ * Drop-in replacement for `fetch` that automatically attaches the framework's
11
+ * `X-CSRF-Token` and `X-Request-Id` headers to state-changing requests in
12
+ * cookie mode. Use this for any project-side HTTP call that hits a
13
+ * LuckyStack `/api/*`, `/sync/*`, or `/auth/api/*` endpoint.
14
+ *
15
+ * Behavior:
16
+ * - GET / OPTIONS / HEAD: passes through without CSRF (read-only).
17
+ * - Token mode: passes through without CSRF (immune by design).
18
+ * - Cookie mode + state-changing method: lazily fetches `/auth/csrf` once,
19
+ * caches, attaches as `x-csrf-token`. On a 403 `auth.csrfMismatch`
20
+ * response, clears the cache and retries once.
21
+ * - `credentials: 'include'` is forced unless the caller overrode it.
22
+ *
23
+ * @example
24
+ * import { httpFetch } from '@luckystack/core/client';
25
+ * const res = await httpFetch('/api/system/myRoute/v1', {
26
+ * method: 'POST',
27
+ * body: JSON.stringify({ ... }),
28
+ * });
29
+ */
30
+ export declare const httpFetch: typeof fetch;
@@ -0,0 +1,32 @@
1
+ export interface CsrfCookieOptions {
2
+ sameSite?: 'strict' | 'lax' | 'none';
3
+ secure?: boolean;
4
+ /**
5
+ * Whether the login-absent double-submit cookie is HttpOnly. The client gets
6
+ * the matching header value from the same-origin `GET /auth/csrf` JSON body,
7
+ * so either value works; `false` remains the compatibility default.
8
+ */
9
+ httpOnly?: boolean;
10
+ path?: string;
11
+ maxAgeMs?: number;
12
+ }
13
+ export interface CsrfConfig {
14
+ /** Cookie name used to deliver the CSRF token to the browser. */
15
+ cookieName: string;
16
+ /** Request-header name the client attaches on state-changing requests. */
17
+ headerName: string;
18
+ /** Length in bytes of the random token minted by the session layer. */
19
+ tokenLength: number;
20
+ cookieOptions: CsrfCookieOptions;
21
+ }
22
+ export declare const DEFAULT_CSRF_CONFIG: CsrfConfig;
23
+ /**
24
+ * Override one or more CSRF settings. Merges shallowly with the current
25
+ * config; `cookieOptions` is deep-merged so a partial override of (say)
26
+ * `sameSite` does not clobber `path` / `maxAgeMs`. Last-write-wins.
27
+ */
28
+ export declare const registerCsrfConfig: (input: Partial<CsrfConfig>) => void;
29
+ /** Read the active config at call time (never at module load). */
30
+ export declare const getCsrfConfig: () => CsrfConfig;
31
+ /** Test-only — restore defaults between scenarios. */
32
+ export declare const resetCsrfConfigForTests: () => void;
package/dist/db.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import './env.js';
2
+ import type { PrismaClient } from '@prisma/client';
3
+ declare global {
4
+ var __luckystackPrisma: PrismaClient | undefined;
5
+ }
6
+ /**
7
+ * Is a generated `@prisma/client` present in this install? Used by the
8
+ * default resolver's error path and by `/readyz` to decide whether a missing
9
+ * DB ping means "broken" or "this project has no Prisma by design".
10
+ */
11
+ export declare const isPrismaClientResolvable: () => boolean;
12
+ export declare const prisma: PrismaClient;
@@ -0,0 +1,6 @@
1
+ export type DbHealthResult = boolean | 'skipped';
2
+ export type DbHealthCheck = () => Promise<DbHealthResult> | DbHealthResult;
3
+ export declare const registerDbHealthCheck: (check: DbHealthCheck) => void;
4
+ export declare const getDbHealthCheck: () => DbHealthCheck | null;
5
+ export declare const isDbHealthCheckRegistered: () => boolean;
6
+ export declare const resetDbHealthCheckForTests: () => void;
@@ -1,21 +1,15 @@
1
- import './sessionTypes-DanjuPaE.js';
2
-
3
- declare const registerPortOverride: (port: number | null) => void;
4
- declare const getPortOverride: () => number | undefined;
5
- declare const resetPortOverrideForTests: () => void;
6
-
7
- interface DeployResourceShape {
1
+ export interface DeployResourceShape {
8
2
  type: 'redis' | 'mongo';
9
3
  urlEnvKey: string;
10
4
  synchronizedEnvKeys?: string[];
11
5
  }
12
- interface DeployEnvironmentShape {
6
+ export interface DeployEnvironmentShape {
13
7
  redis: string;
14
8
  mongo: string;
15
9
  fallback?: string;
16
10
  bindings: Record<string, string>;
17
11
  }
18
- interface DeployRoutingShape {
12
+ export interface DeployRoutingShape {
19
13
  onMissingService?: 'hard-error' | 'proxy-fallback';
20
14
  missingServiceErrorCode?: string;
21
15
  enableUnhealthyFallback?: boolean;
@@ -95,46 +89,17 @@ interface DeployRoutingShape {
95
89
  */
96
90
  wsMaxBytesPerConnection?: number | false;
97
91
  }
98
- interface DeployDevelopmentShape {
92
+ export interface DeployDevelopmentShape {
99
93
  enableFallbackRouting?: boolean;
100
94
  healthPollMs?: number;
101
95
  switchNewTrafficToLocalWhenHealthy?: boolean;
102
96
  }
103
- interface DeployConfigShape {
97
+ export interface DeployConfigShape {
104
98
  resources: Record<string, DeployResourceShape>;
105
99
  environments?: Record<string, DeployEnvironmentShape>;
106
100
  routing?: DeployRoutingShape;
107
101
  development?: DeployDevelopmentShape;
108
102
  }
109
- declare const registerDeployConfig: (config: DeployConfigShape) => DeployConfigShape;
110
- declare const getDeployConfig: () => DeployConfigShape;
111
- declare const isDeployConfigRegistered: () => boolean;
112
-
113
- interface ServiceDefinition {
114
- /** 'root' -> reserved for the system service. Otherwise a folder name under srcDir. */
115
- source: 'root' | string;
116
- }
117
- interface PresetDefinition {
118
- description?: string;
119
- /** Services that are bundled together into one backend artifact. */
120
- services: string[];
121
- }
122
- interface ServicesConfigShape {
123
- services: Record<string, ServiceDefinition>;
124
- presets: Record<string, PresetDefinition>;
125
- /**
126
- * Pure-data ownership manifest for HTTP routes outside `/api/*` and `/sync/*`.
127
- * Keys are absolute path prefixes; values are service keys. The router uses
128
- * longest-prefix matching, so `/webhooks/admin` may override `/webhooks`.
129
- */
130
- customRoutes?: Record<string, string>;
131
- }
132
- declare const registerServicesConfig: (config: ServicesConfigShape) => ServicesConfigShape;
133
- declare const getServicesConfig: () => ServicesConfigShape;
134
- declare const isServicesConfigRegistered: () => boolean;
135
-
136
- type SecretsResolvedListener = (changedKeys: readonly string[] | undefined) => void;
137
- declare const registerSecretsResolvedListener: (listener: SecretsResolvedListener) => (() => void);
138
- declare const notifySecretsResolved: (changedKeys?: readonly string[]) => void;
139
-
140
- export { type DeployConfigShape as D, type PresetDefinition as P, type SecretsResolvedListener as S, type DeployDevelopmentShape as a, type DeployEnvironmentShape as b, type DeployResourceShape as c, type DeployRoutingShape as d, type ServiceDefinition as e, type ServicesConfigShape as f, getDeployConfig as g, getPortOverride as h, getServicesConfig as i, isDeployConfigRegistered as j, isServicesConfigRegistered as k, registerPortOverride as l, registerSecretsResolvedListener as m, notifySecretsResolved as n, registerServicesConfig as o, resetPortOverrideForTests as p, registerDeployConfig as r };
103
+ export declare const registerDeployConfig: (config: DeployConfigShape) => DeployConfigShape;
104
+ export declare const getDeployConfig: () => DeployConfigShape;
105
+ export declare const isDeployConfigRegistered: () => boolean;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * A single email attachment (email F2). Adapters (Resend, SMTP/Nodemailer, SES)
3
+ * thread these into their provider payload. `content` is the raw bytes
4
+ * (`Buffer`/`Uint8Array`) or a base64 string; provide a `path`/`href` instead
5
+ * for adapters that fetch externally. Shapes are intentionally permissive so a
6
+ * single attachment object works across providers — each adapter maps the
7
+ * fields it supports.
8
+ */
9
+ export interface EmailAttachment {
10
+ /** File name shown to the recipient (e.g. `invoice.pdf`). */
11
+ filename: string;
12
+ /** Inline bytes or base64 string. Mutually exclusive with `path`/`href`. */
13
+ content?: Buffer | Uint8Array | string;
14
+ /**
15
+ * Local file path the adapter reads (when it supports path-based sends).
16
+ *
17
+ * @security Trusted-input field — do NOT derive from client-supplied data
18
+ * without validation. Framework-level path-traversal or safe-directory checks
19
+ * are NOT applied automatically; callers must reject paths that escape the
20
+ * intended directory (e.g. containing `..` or absolute paths outside a known
21
+ * root) before passing them here.
22
+ */
23
+ path?: string;
24
+ /**
25
+ * Remote URL the adapter fetches (when it supports href-based sends).
26
+ *
27
+ * @security Trusted-input field — do NOT derive from client-supplied data
28
+ * without validation. Only `https:` and `http:` URLs are safe for
29
+ * framework-internal use; arbitrary schemes (e.g. `file:`, `data:`) may be
30
+ * forwarded to nodemailer/Resend without restriction. Validate against an
31
+ * allowlist before passing here.
32
+ */
33
+ href?: string;
34
+ /** MIME type (e.g. `application/pdf`). Adapter may infer from `filename` if omitted. */
35
+ contentType?: string;
36
+ /** `Content-ID` for inline/embedded images referenced by `cid:` in the HTML. */
37
+ cid?: string;
38
+ }
39
+ export interface EmailMessage {
40
+ to: string | string[];
41
+ subject: string;
42
+ html: string;
43
+ text?: string;
44
+ from?: string;
45
+ replyTo?: string;
46
+ cc?: string | string[];
47
+ bcc?: string | string[];
48
+ /**
49
+ * File attachments (email F2). Owned by core (not `@luckystack/email`) so the
50
+ * type is the single source of truth across the framework and every adapter;
51
+ * adapters thread these into their provider's attachment payload. Optional —
52
+ * a message without attachments behaves exactly as before.
53
+ */
54
+ attachments?: EmailAttachment[];
55
+ /**
56
+ * Custom message headers (email F2) — e.g. `X-Entity-Ref-ID`,
57
+ * `List-Unsubscribe`, idempotency keys. Adapters merge these over the headers
58
+ * they set themselves (adapter-reserved headers win to avoid breaking
59
+ * delivery). Optional.
60
+ *
61
+ * @security CR and LF characters in header keys or values enable SMTP header
62
+ * injection. `sendEmail` strips `\r` and `\n` from every key and value before
63
+ * passing them to the adapter (EMAIL-O7). Do NOT derive keys or values from
64
+ * unsanitized client input.
65
+ */
66
+ headers?: Record<string, string>;
67
+ }
68
+ export type EmailDeliveryOutcome = 'not-sent' | 'unknown';
69
+ export type EmailResult = {
70
+ ok: true;
71
+ id: string;
72
+ } | {
73
+ ok: false;
74
+ reason: string;
75
+ cause?: unknown;
76
+ /**
77
+ * `unknown` means the caller stopped waiting after dispatch began; retry
78
+ * only with the same idempotency key. Omitted on legacy/provider failures
79
+ * whose delivery semantics are adapter-specific.
80
+ */
81
+ deliveryOutcome?: EmailDeliveryOutcome;
82
+ };
83
+ export interface EmailSendContext {
84
+ /** Cooperative cancellation. Custom adapters should abort provider I/O when supported. */
85
+ signal: AbortSignal;
86
+ /** Stable caller-provided key reused across retries of the same logical email. */
87
+ idempotencyKey?: string;
88
+ }
89
+ export interface EmailSender {
90
+ /** Adapter identifier for logs + diagnostics ("console", "resend", "smtp", etc.). */
91
+ name: string;
92
+ /**
93
+ * The optional context preserves source compatibility with pre-0.7 adapters.
94
+ * New adapters should honor `signal` and provider-native idempotency where available.
95
+ */
96
+ send: (message: EmailMessage, context?: EmailSendContext) => Promise<EmailResult>;
97
+ }
98
+ /**
99
+ * Registry keyed by adapter-slot name. Reserved slots used by the
100
+ * framework:
101
+ *
102
+ * - `'default'` — fallback when no specific slot matches.
103
+ * - `'transactional'` — login password-reset, account-confirmation,
104
+ * invoice-receipt, security-critical emails.
105
+ * - `'marketing'` — newsletters, announcements; consumers MAY route
106
+ * bulk sends here.
107
+ * - `'diagnostics'` — diagnostic-test endpoints (e.g. playground).
108
+ *
109
+ * Custom slot names ('billing', 'support', etc.) are allowed; consumers
110
+ * resolve them explicitly via `sendEmail({ adapter: 'billing', ... })`.
111
+ */
112
+ export type EmailSenderRegistry = Partial<Record<string, EmailSender>>;
113
+ /**
114
+ * Register a single global email sender. Kept for backwards compatibility.
115
+ * For new code, prefer `registerEmailSenders({ ... })` which lets the
116
+ * framework route transactional vs. marketing through different adapters.
117
+ */
118
+ export declare const registerEmailSender: (sender: EmailSender) => void;
119
+ /**
120
+ * Register multiple named email senders. Replaces the entire registry
121
+ * (last-write-wins). Use this when your app routes different message
122
+ * types through different adapters — e.g. Resend for marketing and SMTP
123
+ * for transactional. The framework picks adapter slots by convention
124
+ * (see `EmailSenderRegistry`); consumer code can pick explicitly via
125
+ * `sendEmail({ adapter: 'foo', ... })`.
126
+ */
127
+ export declare const registerEmailSenders: (senders: EmailSenderRegistry) => void;
128
+ /** Read the legacy single sender (or the `default` slot of the registry). */
129
+ export declare const getEmailSender: () => EmailSender | null;
130
+ /**
131
+ * Read a specific named sender. Falls back to the legacy single sender
132
+ * when the requested slot is absent. Returns null when neither exists.
133
+ * Pass the convention-based slots (`'transactional'`, `'marketing'`,
134
+ * `'diagnostics'`, `'default'`) or any custom slot you registered.
135
+ */
136
+ export declare const getEmailSenderByName: (name: string) => EmailSender | null;
137
+ /** Read every registered slot name. */
138
+ export declare const listEmailSenderNames: () => string[];
139
+ export declare const isEmailSenderRegistered: () => boolean;
package/dist/env.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ declare const EnvSchema: z.ZodObject<{
3
+ NODE_ENV: z.ZodDefault<z.ZodEnum<{
4
+ development: "development";
5
+ production: "production";
6
+ test: "test";
7
+ }>>;
8
+ SERVER_IP: z.ZodDefault<z.ZodString>;
9
+ SECURE: z.ZodDefault<z.ZodEnum<{
10
+ false: "false";
11
+ true: "true";
12
+ }>>;
13
+ REDIS_HOST: z.ZodDefault<z.ZodString>;
14
+ REDIS_PORT: z.ZodDefault<z.ZodString>;
15
+ PROJECT_NAME: z.ZodDefault<z.ZodString>;
16
+ }, z.core.$loose>;
17
+ export type RuntimeEnv = z.infer<typeof EnvSchema>;
18
+ export declare const DEFAULT_ENV_FILES: string[];
19
+ export declare const getEnvFiles: () => string[];
20
+ export declare const loadEnvFiles: () => void;
21
+ export declare const bootstrapEnv: () => RuntimeEnv;
22
+ export declare const env: {
23
+ [x: string]: unknown;
24
+ NODE_ENV: "development" | "production" | "test";
25
+ SERVER_IP: string;
26
+ SECURE: "false" | "true";
27
+ REDIS_HOST: string;
28
+ REDIS_PORT: string;
29
+ PROJECT_NAME: string;
30
+ };
31
+ export declare const isProduction: boolean;
32
+ export declare const getEnv: () => RuntimeEnv;
33
+ /** Application runtime mode. Keep separate from `resolveEnvKey()`, which is a
34
+ * deploy-topology identity and may be `staging`, `dockerSplit`, etc. */
35
+ export declare const resolveRuntimeMode: () => RuntimeEnv['NODE_ENV'];
36
+ export declare const isProductionRuntime: () => boolean;
37
+ export declare const isTestRuntime: () => boolean;
38
+ export {};
@@ -0,0 +1,54 @@
1
+ export interface ErrorFormatterContext {
2
+ /** Resolved route name (`api/billing/getInvoice/v1`, `sync/...`). */
3
+ routeName: string;
4
+ /** Transport that triggered the error. */
5
+ transport: 'socket' | 'http';
6
+ /** Session id when available. */
7
+ userId?: string | null;
8
+ }
9
+ /** The normalized error envelope handed to every formatter. */
10
+ export interface FormatterErrorEnvelope {
11
+ status: 'error';
12
+ errorCode?: string;
13
+ message?: string;
14
+ httpStatus?: number;
15
+ [key: string]: unknown;
16
+ }
17
+ export type ErrorFormatter = (error: FormatterErrorEnvelope, ctx: ErrorFormatterContext) => Record<string, unknown>;
18
+ /**
19
+ * Minimal structural contract every formatter-eligible envelope satisfies:
20
+ * a discriminant `status` plus an open string-index so formatters can read /
21
+ * add arbitrary fields. Generic helpers below constrain to this so transport
22
+ * handlers no longer need `as unknown as` double-casts at the formatter
23
+ * boundary — see `applyErrorFormatter` / `buildFormattedError`.
24
+ */
25
+ export interface FormatterEnvelope {
26
+ status?: string;
27
+ [key: string]: unknown;
28
+ }
29
+ /**
30
+ * Register a global error-response formatter. Receives the normalized
31
+ * error envelope + context; return a (possibly extended) object that the
32
+ * framework will emit. Pass `null` to unregister.
33
+ *
34
+ * For per-endpoint overrides, export `errorFormatter` from your
35
+ * `_api/foo_v1.ts` file directly — the api/sync handler imports + invokes
36
+ * it through `applyErrorFormatter`.
37
+ */
38
+ export declare const registerErrorFormatter: (formatter: ErrorFormatter | null) => void;
39
+ /** Read the active global formatter (or null). */
40
+ export declare const getErrorFormatter: () => ErrorFormatter | null;
41
+ /**
42
+ * Apply per-route → global → identity formatter chain to a normalized
43
+ * error envelope. Returns the envelope unchanged when `status !== 'error'`
44
+ * so it's safe to wrap every emit call. Errors thrown inside a formatter
45
+ * are caught and logged; the un-formatted envelope is returned in that
46
+ * case to keep the error path crash-resistant.
47
+ */
48
+ export declare const applyErrorFormatter: <T extends FormatterEnvelope>(input: {
49
+ response: T;
50
+ routeName: string;
51
+ transport: 'socket' | 'http';
52
+ userId?: string | null;
53
+ perRouteFormatter?: ErrorFormatter | undefined | null;
54
+ }) => T;
@@ -0,0 +1,14 @@
1
+ export interface ErrorTrackerUser {
2
+ id?: string;
3
+ email?: string;
4
+ username?: string;
5
+ [key: string]: unknown;
6
+ }
7
+ /** Open an isolated request identity scope before the first async boundary. */
8
+ export declare const runWithErrorTrackerIdentityScope: <T>(fn: () => T) => T;
9
+ /** Open an isolated scope with a user that is already known. */
10
+ export declare const runWithErrorTrackerIdentity: <T>(user: ErrorTrackerUser | null, fn: () => T) => T;
11
+ /** Set the user inside the active request scope; no-op outside a scope. */
12
+ export declare const setCurrentErrorTrackerIdentity: (user: ErrorTrackerUser | null) => void;
13
+ /** Read the current request identity, or null outside a scope. */
14
+ export declare const getCurrentErrorTrackerIdentity: () => ErrorTrackerUser | null;
@@ -0,0 +1,85 @@
1
+ import type { ErrorTrackerUser } from './errorTrackerIdentity.js';
2
+ export type { ErrorTrackerUser } from './errorTrackerIdentity.js';
3
+ export type ErrorTrackerContext = Record<string, unknown>;
4
+ export declare const sanitizeErrorString: (value: string) => string;
5
+ export declare const sanitizeErrorStrings: (error: unknown) => {
6
+ message: string;
7
+ stack: string | undefined;
8
+ } | null;
9
+ export interface ErrorTrackerEvent {
10
+ /** When false, the adapter must not forward this event (beforeSend opt-out). */
11
+ forwarded: boolean;
12
+ kind: 'exception' | 'message';
13
+ payload: Record<string, unknown>;
14
+ }
15
+ export type SpanResult<T> = T;
16
+ export interface ErrorTracker {
17
+ /** Human-readable identifier (logs, diagnostics). */
18
+ name: string;
19
+ captureException: (error: unknown, context?: ErrorTrackerContext) => void;
20
+ captureMessage: (message: string, level: 'info' | 'warning' | 'error' | 'fatal', context?: ErrorTrackerContext) => void;
21
+ setUser: (user: ErrorTrackerUser | null) => void;
22
+ setContext?: (key: string, context: ErrorTrackerContext | null) => void;
23
+ startSpan?: <T>(name: string, op: string, fn: () => T) => T;
24
+ /**
25
+ * Optional handle-style span (#62): open a span NOW and close it later via the
26
+ * returned handle's `finish()`. Unlike {@link startSpan} (which wraps a single
27
+ * synchronous `fn`), this fits an open-at-preExecute / close-at-postExecute
28
+ * request lifecycle that spans awaits. When present, the registry-level
29
+ * {@link startSpanHandle} delegates to the FIRST registered adapter that
30
+ * implements it so the request span reaches an adapter-only backend
31
+ * (Datadog/PostHog/custom), not just the legacy Sentry SDK path.
32
+ */
33
+ startSpanHandle?: (name: string, op: string) => {
34
+ finish: () => void;
35
+ };
36
+ recordMetric?: (name: string, value: number, tags?: Record<string, string>) => void;
37
+ beforeSend?: (event: ErrorTrackerEvent) => ErrorTrackerEvent | null;
38
+ /**
39
+ * Optional flush lifecycle hook (error-tracking ET batch). Called by
40
+ * {@link flushErrorTrackers} on graceful shutdown so a buffered adapter
41
+ * (PostHog batch, Sentry transport) can drain in-flight events before exit.
42
+ * Returns a promise that resolves when the adapter has flushed.
43
+ */
44
+ flush?: () => Promise<void>;
45
+ }
46
+ export type PreCaptureFilter = (event: ErrorTrackerEvent) => boolean;
47
+ export declare const registerPreCaptureFilter: (filter: PreCaptureFilter | null) => void;
48
+ export declare const registerErrorTracker: (tracker: ErrorTracker) => void;
49
+ export declare const registerErrorTrackers: (trackers: ErrorTracker[]) => void;
50
+ /**
51
+ * APPEND primitive (ET-05): add a tracker WITHOUT clobbering already-registered
52
+ * ones. Fixes the async-PostHog-vs-consumer-overlay race where the loser of
53
+ * `registerErrorTracker`'s replace silently vanished. De-duplicates by `name`
54
+ * (re-appending the same adapter name replaces that one entry in place) so a
55
+ * lazy adapter that registers a proxy then the real client doesn't double-fire.
56
+ */
57
+ export declare const appendErrorTracker: (tracker: ErrorTracker) => void;
58
+ export declare const getActiveErrorTrackers: () => ErrorTracker[];
59
+ /**
60
+ * Flush lifecycle (error-tracking ET batch): drain every adapter that exposes a
61
+ * `flush()` before shutdown. Per-adapter failures are swallowed + logged so one
62
+ * stuck adapter can't block graceful shutdown. Safe to call when no adapter has
63
+ * `flush`.
64
+ */
65
+ export declare const flushErrorTrackers: () => Promise<void>;
66
+ export declare const captureExceptionAcrossTrackers: (error: unknown, context?: ErrorTrackerContext) => void;
67
+ export declare const captureMessageAcrossTrackers: (message: string, level: 'info' | 'warning' | 'error' | 'fatal', context?: ErrorTrackerContext) => void;
68
+ export declare const setErrorTrackerUser: (user: ErrorTrackerUser | null) => void;
69
+ export declare const recordMetricAcrossTrackers: (name: string, value: number, tags?: Record<string, string>) => void;
70
+ export declare const startSpanAcrossTrackers: <T>(name: string, op: string, fn: () => T) => T;
71
+ /**
72
+ * Handle-style span (error-tracking ET batch): for callers that can't wrap the
73
+ * measured work in a single synchronous `fn` (streaming, cross-await spans).
74
+ * Returns a handle whose `finish()` resolves the span's duration. The default
75
+ * implementation is a lightweight wall-clock timer (works with any backend);
76
+ * an adapter can supply a richer span via the existing `startSpan` callback
77
+ * form when it needs native instrumentation. `finish()` is idempotent.
78
+ */
79
+ export interface SpanHandle {
80
+ /** Stop the span; safe to call more than once (subsequent calls are no-ops). */
81
+ finish: () => void;
82
+ /** Milliseconds elapsed at the moment `finish()` first ran (0 before). */
83
+ readonly durationMs: number;
84
+ }
85
+ export declare const startSpanHandle: (name: string, op: string) => SpanHandle;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Escape `&`, `<`, `>`, `"`, `'` for safe interpolation into HTML text nodes
3
+ * and quoted attribute values. Does NOT protect against CSS/JS/URL-context
4
+ * injection — use a dedicated sanitizer for those contexts.
5
+ */
6
+ export declare const escapeHtml: (str: string) => string;
@@ -1,10 +1,7 @@
1
- import { Rule, Linter } from 'eslint';
2
-
3
- type EslintRule = Rule.RuleModule;
4
-
5
- declare const hasPackage: (name: string) => boolean;
6
-
1
+ import type { Linter } from 'eslint';
2
+ import type { EslintRule } from './internal/ruleTypes.js';
7
3
  declare const allRules: Record<string, EslintRule>;
8
4
  declare const luckystackConfig: Linter.Config[];
9
-
10
- export { luckystackConfig as default, hasPackage, allRules as rules };
5
+ export default luckystackConfig;
6
+ export { allRules as rules };
7
+ export { hasPackage } from './internal/hasPackage.js';
@@ -0,0 +1 @@
1
+ export declare const hasPackage: (name: string) => boolean;
@@ -0,0 +1,4 @@
1
+ import type { Rule } from 'eslint';
2
+ export type EslintRule = Rule.RuleModule;
3
+ export type RuleContext = Rule.RuleContext;
4
+ export type RuleListener = Rule.RuleListener;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;
@@ -0,0 +1,3 @@
1
+ import type { EslintRule } from '../internal/ruleTypes.js';
2
+ declare const rule: EslintRule;
3
+ export default rule;