@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.
- package/AGENTS.md +11 -0
- package/CHANGELOG.md +7 -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/client.d.ts +46 -335
- package/dist/client.js +2 -2
- 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/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/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.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.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 +2 -2
- package/CLAUDE.md +0 -256
- 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
|
-
|
|
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;
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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;
|