@luckystack/core 0.7.3 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.4] - 2026-07-22
11
+
12
+ ### Added
13
+
14
+ - `EmailSender.send(message, context?)` now receives a cooperative abort signal
15
+ and optional stable idempotency key. Failed `EmailResult`s can distinguish
16
+ definitive `not-sent` from an `unknown` post-dispatch outcome.
17
+ - Deploy routing config now includes `trustedProxyCidrs`, the explicit
18
+ immediate-peer trust boundary used by the HTTP and WebSocket router paths.
19
+
20
+ ### Fixed
21
+
22
+ - Automatic Redis rebuilds after secret resolution now replace only a
23
+ framework-owned default client. A consumer registered through
24
+ `registerRedisClient(customClient)` keeps precedence across rotation instead
25
+ of being disconnected and downgraded to the host/port default.
26
+ - Dev OAuth callback rewriting now distinguishes the intended pre-listen port from
27
+ the actually-bound port. Auto-derived direct loopback callbacks follow a port
28
+ hop (including IPv6 `[::1]`), while an explicitly configured localhost
29
+ router/reverse-proxy ingress is preserved.
30
+ - Added `registerBoundAddress(...)` so `getBindAddress()` can expose the real
31
+ `node:http` address without discarding the intended-port baseline.
32
+
10
33
  ## [0.7.3] - 2026-07-20
11
34
 
12
35
  ### Added
package/CLAUDE.md CHANGED
@@ -41,8 +41,10 @@ Foundation package for LuckyStack. Owns the socket-first transport contracts (`a
41
41
  | `registerAvatarConfig(config: AvatarConfigInput): void` | Override avatar disk format(s) + Cache-Control header. | -> docs/config-registry.md |
42
42
  | `getAvatarConfig(): AvatarConfig` | Read active avatar config. | -> docs/config-registry.md |
43
43
  | `DEFAULT_AVATAR_CONFIG: AvatarConfig` | Default formats `[{ extension: 'webp', contentType: 'image/webp' }]` + 24h cache. | -> docs/config-registry.md |
44
- | `registerBindAddress(address: { ip: string; port: number }): void` | `createLuckyStackServer` writes the resolved listen address here at boot. | -> docs/app-bootstrap.md |
45
- | `getBindAddress(): { ip: string; port: string }` | Resolve at call time (registry -> `SERVER_IP`/`SERVER_PORT` -> `'127.0.0.1'`/`''`). | -> docs/app-bootstrap.md |
44
+ | `registerBindAddress(address: { ip: string; port: number }): void` | Store the intended pre-listen address; resets the OAuth pre-hop baseline. | -> docs/app-bootstrap.md |
45
+ | `registerBoundAddress(address: { ip: string; port: number }): void` | Store the address reported by `node:http` after bind while preserving the intended baseline. | -> docs/app-bootstrap.md |
46
+ | `getBindAddress(): { ip: string; port: string }` | Resolve the actually-bound address at call time (registry -> env -> fallback). | -> docs/app-bootstrap.md |
47
+ | `resolveDevCallbackUrl(callbackUrl: string): string` | In non-prod, rewrite a loopback OAuth callback from the intended port to the actually-bound port; preserves explicit local ingress ports. | -> docs/app-bootstrap.md |
46
48
  | `registerRuntimeMapsProvider(provider: RuntimeMapsProvider): void` | DI slot for generated api/sync maps; called by project `server/prod/runtimeMaps.ts`. | -> docs/app-bootstrap.md |
47
49
  | `getRuntimeApiMaps(): Promise<RuntimeApiMapsResult>` | Async accessor for `{ apisObject, functionsObject }`. | -> docs/app-bootstrap.md |
48
50
  | `getRuntimeSyncMaps(): Promise<RuntimeSyncMapsResult>` | Async accessor for `{ syncObject, functionsObject }`. | -> docs/app-bootstrap.md |
@@ -51,7 +53,7 @@ Foundation package for LuckyStack. Owns the socket-first transport contracts (`a
51
53
  | `getRegisteredApiMethod(pagePath: string, apiName: string, version: string): HttpMethodLiteral \| undefined` | Lookup helper used by `isGetMethod`. | -> docs/app-bootstrap.md |
52
54
  | `isApiMethodMapRegistered(): boolean` | Detect whether the prefix-heuristic fallback is active. | -> docs/app-bootstrap.md |
53
55
  | `registerPrismaClient(client: PrismaClient, key?: string): PrismaClient` | Register a Prisma client into a slot (default `'default'`). Pass a `key` for graded credentials (e.g. `'ro'`/`'rw'`) or per-tenant clients. | -> docs/app-bootstrap.md |
54
- | `registerRedisClient(client: RedisClient, key?: string): RedisClient` | Register an ioredis client into a slot (default `'default'`). | -> docs/app-bootstrap.md |
56
+ | `registerRedisClient(client: RedisClient, key?: string): RedisClient` | Register an ioredis client into a slot (default `'default'`). A consumer-owned default keeps precedence across automatic secret refresh; rebuild it from your own secrets-resolved listener when needed. | -> docs/app-bootstrap.md |
55
57
  | `getPrismaClient(): PrismaClient` | Read the `'default'` slot (registered client or lazy default resolver). | -> docs/app-bootstrap.md |
56
58
  | `getRedisClient(): RedisClient` | Read the `'default'` slot (registered client or lazy default resolver). | -> docs/app-bootstrap.md |
57
59
  | `getPrismaClientFor(key?: string): PrismaClient` | Read a specific slot. `'default'` falls back to the resolver; any other unregistered slot throws (never silently returns the privileged default). | -> docs/app-bootstrap.md |
@@ -85,7 +87,7 @@ Foundation package for LuckyStack. Owns the socket-first transport contracts (`a
85
87
  | `registerNotifier(notifier: Notifier): void` | DI for client-side toast notifier (success/error/info/warning). | -> docs/app-bootstrap.md |
86
88
  | `getNotifier(): Notifier` | Read active notifier (no-op default). | -> docs/app-bootstrap.md |
87
89
  | `notify: Notifier` | Delegating wrapper used by framework hot paths. | -> docs/app-bootstrap.md |
88
- | `registerEmailSender(sender: EmailSender): void` | Legacy single-sender registration; mirrors into `default` slot. | -> docs/app-bootstrap.md |
90
+ | `registerEmailSender(sender: EmailSender): void` | Legacy single-sender registration; mirrors into `default` slot. `send(message, context?)` may honor `AbortSignal` + stable idempotency key; timeout outcomes can be explicitly unknown. | -> docs/app-bootstrap.md |
89
91
  | `registerEmailSenders(senders: EmailSenderRegistry): void` | Multi-adapter registration (`'default'`, `'transactional'`, `'marketing'`, custom). | -> docs/app-bootstrap.md |
90
92
  | `getEmailSender(): EmailSender \| null` | Read legacy/`default` sender. | -> docs/app-bootstrap.md |
91
93
  | `getEmailSenderByName(name: string): EmailSender \| null` | Read a specific slot (falls back to legacy sender for `'default'`). | -> docs/app-bootstrap.md |
package/README.md CHANGED
@@ -60,7 +60,7 @@ import { socket, isOnline, enqueueApiRequest } from '@luckystack/core/client';
60
60
  | 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`. |
61
61
  | Redacted log keys | `registerRedactedLogKeys`, `getRedactedLogKeys`, `isRedactedLogKey`, `resetRedactedLogKeysForTests` |
62
62
  | 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. |
63
- | Bind address | `registerBindAddress({ ip, port })`, `getBindAddress() → { ip, port: string }`. `createLuckyStackServer` registers the resolved listen address at boot so framework code (e.g. `checkOrigin` building the same-origin entry) reads it from the registry instead of `SERVER_IP` / `SERVER_PORT` env vars — no more drift when consumers configure programmatically via `createLuckyStackServer({ ip, port })`. Resolution at call time: (1) `registerBindAddress(...)` value, (2) `process.env.SERVER_IP` / `SERVER_PORT` (legacy), (3) `'127.0.0.1'` / `''` fallback. |
63
+ | 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. |
64
64
  | Locale reloader | `registerLocaleReloader`, `getLocaleReloader`. Type: `LocaleReloader` |
65
65
  | 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. |
66
66
  | 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. |
@@ -64,4 +64,4 @@ export {
64
64
  registerSecretsResolvedListener,
65
65
  notifySecretsResolved
66
66
  };
67
- //# sourceMappingURL=chunk-R7IRJFVC.js.map
67
+ //# sourceMappingURL=chunk-DVGSWLLS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/deployConfigRegistry.ts","../src/servicesConfigRegistry.ts","../src/secretsResolved.ts"],"sourcesContent":["//? Deploy-time topology registered by the project at boot. Mirrors the\n//? `projectConfig` / `runtimeMapsRegistry` / `notifier` pattern: framework\n//? packages read deploy data through `getDeployConfig()` so they don't\n//? `import '../../../deploy.config'`.\n//?\n//? Only the fields core actually consumes are declared here. The project's\n//? richer `DeployConfig` (environments, bindings, routing, etc.) stays in\n//? `deploy.config.ts` and is consumed directly by the router package, which\n//? is project-glue (tier-B) and intentionally stays project-coupled.\n//?\n//? Project entrypoint registers via the direct file path\n//? (`./packages/core/src/deployConfigRegistry`) — same Vite-bundle rule we\n//? already use for projectConfig: importing the core barrel from a\n//? client-bundled file pulls server-only modules (Redis, paths, etc.).\n\nexport interface DeployResourceShape {\n type: 'redis' | 'mongo';\n urlEnvKey: string;\n synchronizedEnvKeys?: string[];\n}\n\nexport interface DeployEnvironmentShape {\n redis: string;\n mongo: string;\n fallback?: string;\n bindings: Record<string, string>;\n}\n\nexport interface DeployRoutingShape {\n onMissingService?: 'hard-error' | 'proxy-fallback';\n missingServiceErrorCode?: string;\n enableUnhealthyFallback?: boolean;\n strictBootHandshake?: boolean;\n /**\n * Opt INTO the boot-handshake's synchronized-env (cookie/session secret) drift\n * check. DEFAULT false — the router stays a dumb proxy that holds NO backend\n * secrets. Comparing synchronized-env hashes requires the router process to\n * carry the same SESSION_SECRET etc. as the backends, which most deployments\n * deliberately avoid; the always-on shared-Redis handshake is unaffected. Set\n * true only when you intentionally give the router the synchronized secrets.\n */\n verifySynchronizedEnv?: boolean;\n /** Default port the router listens on when ROUTER_PORT env var is unset (default 4000). */\n defaultRouterPort?: number;\n /** Default ms between health polls when not specified per-call (default 5000). */\n defaultHealthPollMs?: number;\n /** ms before the fallback `/_health` probe times out (default 3000). */\n healthProbeTimeoutMs?: number;\n /** Seconds the boot-UUID Redis key lives for (default 3600 = 1h). */\n bootKeyTtlSeconds?: number;\n /**\n * ms before the router gives up proxying to an upstream backend (SEC-30 /\n * router-DeployRoutingShape). Guards against slow-loris upstreams holding a\n * router worker open. The router reads this when building its proxy request.\n * DEFAULT undefined → router falls back to its own built-in default.\n */\n upstreamTimeoutMs?: number;\n /**\n * Name of the service that terminates websocket (Socket.io) traffic. The\n * router pins WS upgrade requests to this service (they cannot be load-balanced\n * round-robin without sticky sessions). DEFAULT undefined → router uses its\n * `system` convention.\n */\n websocketService?: string;\n /**\n * Immediate TLS proxy addresses/CIDRs allowed to assert\n * `x-forwarded-proto: https`. Empty/undefined trusts nobody, so a client\n * directly reaching the router cannot spoof secure-scheme backend logic.\n * Examples: `['127.0.0.1/32', '::1/128']` for same-host nginx/Caddy, or the\n * private subnet of a managed load balancer.\n */\n trustedProxyCidrs?: string[];\n /**\n * Path the router exposes for its OWN health/liveness (distinct from the\n * backend `/_health` boot-handshake endpoint). DEFAULT undefined → router\n * uses its built-in default.\n */\n routerHealthPath?: string;\n /**\n * Max request body size (bytes) the router will buffer/forward before\n * rejecting with 413. DEFAULT undefined → router uses its built-in default.\n */\n maxRequestBodyBytes?: number;\n /**\n * Max size (bytes) of the upgrade `head` buffer the WS proxy forwards to the\n * upstream during a WebSocket handshake. An over-cap head is rejected (the\n * client socket is destroyed) BEFORE the upstream leg opens, so a client\n * cannot push an unbounded pre-upgrade buffer through the router. DEFAULT\n * undefined → router uses its built-in default (64 KiB).\n */\n wsMaxHeadBytes?: number;\n /**\n * Idle-timeout (ms) for an UPGRADED WebSocket pipe: when no bytes flow in\n * either direction for this long, the router tears the proxied pair down.\n * Bounds an otherwise-unbounded pre/post-auth pipe a client could hold open\n * against the router. The timer resets on any activity. DEFAULT undefined →\n * router uses its built-in default (120000 ms = 2 min). Set `0` to disable.\n */\n wsIdleTimeoutMs?: number;\n /**\n * Per-connection byte budget for an UPGRADED WebSocket pipe: when the total\n * bytes piped across both legs exceed this cap, the router tears the proxied\n * pair down. Bounds how much a single connection can stream through the\n * router. DEFAULT undefined → router uses its built-in default (100 MiB). Set\n * `false` to disable the cap (unbounded).\n */\n wsMaxBytesPerConnection?: number | false;\n}\n\nexport interface DeployDevelopmentShape {\n enableFallbackRouting?: boolean;\n healthPollMs?: number;\n switchNewTrafficToLocalWhenHealthy?: boolean;\n}\n\nexport interface DeployConfigShape {\n resources: Record<string, DeployResourceShape>;\n environments?: Record<string, DeployEnvironmentShape>;\n routing?: DeployRoutingShape;\n development?: DeployDevelopmentShape;\n}\n\nimport { createRegistry } from './createRegistry';\n\nconst DEFAULT_DEPLOY_CONFIG: DeployConfigShape = {\n resources: {},\n};\n\nconst registry = createRegistry<DeployConfigShape>(DEFAULT_DEPLOY_CONFIG);\n\nexport const registerDeployConfig = (config: DeployConfigShape): DeployConfigShape =>\n registry.register(config);\n\nexport const getDeployConfig = (): DeployConfigShape => registry.get();\n\nexport const isDeployConfigRegistered = (): boolean => registry.isRegistered();\n","//? Services config registry. The router (and presetLoader) consume the\n//? consumer's services topology — which services exist, which presets bundle\n//? them together — through this registry instead of a relative\n//? `../../../services.config` import.\n//?\n//? The consumer's `services.config.ts` registers its config at module import\n//? (side-effect pattern, identical to `deploy.config.ts:151`). Router code\n//? calls `getServicesConfig()` at request time.\n\nexport interface ServiceDefinition {\n /** 'root' -> reserved for the system service. Otherwise a folder name under srcDir. */\n // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents -- 'root' is documented as the reserved value; the union surfaces it in autocomplete\n source: 'root' | string;\n}\n\nexport interface PresetDefinition {\n description?: string;\n /** Services that are bundled together into one backend artifact. */\n services: string[];\n}\n\nexport interface ServicesConfigShape {\n services: Record<string, ServiceDefinition>;\n presets: Record<string, PresetDefinition>;\n}\n\nimport { createRegistry } from './createRegistry';\n\nconst registry = createRegistry<ServicesConfigShape>(\n //? Unregistered sentinel — `get` never returns this because `resolveDefault`\n //? throws first. Typed as the real shape so the slot stays well-typed.\n { services: {}, presets: {} },\n {\n resolveDefault: (): ServicesConfigShape => {\n throw new Error(\n '@luckystack/core: services config has not been registered. Call ' +\n '`registerServicesConfig({...})` from your `services.config.ts` (which should be ' +\n 'imported as a side-effect during boot).'\n );\n },\n },\n);\n\nexport const registerServicesConfig = (config: ServicesConfigShape): ServicesConfigShape =>\n registry.register(config);\n\nexport const getServicesConfig = (): ServicesConfigShape => registry.get();\n\nexport const isServicesConfigRegistered = (): boolean => registry.isRegistered();\n","//? Decoupled \"secrets were (re)resolved\" notification channel.\n//?\n//? A secret resolver fires `notifySecretsResolved(changedKeys)` right after it\n//? overwrites `process.env` with the real values (at boot AND on rotation).\n//? `@luckystack/secret-manager` fires it AUTOMATICALLY after every resolve (via\n//? the global-symbol channel at the bottom of this file — no import edge, no\n//? `onApplied` wiring needed).\n//? Listeners then drop any client they built from the PRE-resolution env so the\n//? next use rebuilds with the resolved secret.\n//?\n//? This is deliberately generic and lives in core with ZERO dependency on\n//? secret-manager: core self-registers a Redis listener (see `redis.ts`), and\n//? any other cached-client owner (a Prisma pool, an SDK client) can subscribe\n//? the same way without coupling to the resolver package.\n\nexport type SecretsResolvedListener = (changedKeys: readonly string[] | undefined) => void;\n\nconst listeners = new Set<SecretsResolvedListener>();\n\n//? Subscribe to secret-resolution events. Returns an unsubscribe function.\nexport const registerSecretsResolvedListener = (listener: SecretsResolvedListener): (() => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\n//? Fire all listeners. `changedKeys` = the env NAMES whose value actually\n//? changed (never the secret values). Omit it (`undefined`) when the caller\n//? doesn't know which keys changed — listeners then treat it as \"assume\n//? relevant\" and reset defensively.\nexport const notifySecretsResolved = (changedKeys?: readonly string[]): void => {\n for (const listener of listeners) {\n try {\n listener(changedKeys);\n } catch {\n //? A misbehaving listener must never break the resolve/boot path.\n }\n }\n};\n\n//? Cross-PACKAGE channel: publish THIS core instance's `notifySecretsResolved`\n//? on a well-known global-symbol array so a decoupled package that must not\n//? depend on `@luckystack/core` — notably `@luckystack/secret-manager`, the\n//? zero-dependency resolver — can fire it right after it resolves secrets, with\n//? NO import edge. An ARRAY (not a single slot) so that if the process somehow\n//? has more than one `@luckystack/core` instance (dual-package / mixed src+dist\n//? resolution — a real footgun that also causes the env-revert), EVERY instance's\n//? redis registry gets the rebuilt client, not just the last one loaded.\nconst GLOBAL_LISTENERS_SYMBOL = Symbol.for('luckystack.secretsResolved.listeners');\n\nconst getGlobalSecretsResolvedListeners = (): SecretsResolvedListener[] => {\n const existing: unknown = Reflect.get(globalThis, GLOBAL_LISTENERS_SYMBOL);\n if (Array.isArray(existing)) return existing as SecretsResolvedListener[];\n const created: SecretsResolvedListener[] = [];\n Reflect.set(globalThis, GLOBAL_LISTENERS_SYMBOL, created);\n return created;\n};\n\ngetGlobalSecretsResolvedListeners().push(notifySecretsResolved);\n"],"mappings":";;;;;AA4HA,IAAM,wBAA2C;AAAA,EAC/C,WAAW,CAAC;AACd;AAEA,IAAM,WAAW,eAAkC,qBAAqB;AAEjE,IAAM,uBAAuB,CAAC,WACnC,SAAS,SAAS,MAAM;AAEnB,IAAM,kBAAkB,MAAyB,SAAS,IAAI;AAE9D,IAAM,2BAA2B,MAAe,SAAS,aAAa;;;AC3G7E,IAAMA,YAAW;AAAA;AAAA;AAAA,EAGf,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC,EAAE;AAAA,EAC5B;AAAA,IACE,gBAAgB,MAA2B;AACzC,YAAM,IAAI;AAAA,QACR;AAAA,MAGF;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,yBAAyB,CAAC,WACrCA,UAAS,SAAS,MAAM;AAEnB,IAAM,oBAAoB,MAA2BA,UAAS,IAAI;AAElE,IAAM,6BAA6B,MAAeA,UAAS,aAAa;;;AC/B/E,IAAM,YAAY,oBAAI,IAA6B;AAG5C,IAAM,kCAAkC,CAAC,aAAoD;AAClG,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAMO,IAAM,wBAAwB,CAAC,gBAA0C;AAC9E,aAAW,YAAY,WAAW;AAChC,QAAI;AACF,eAAS,WAAW;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AACF;AAUA,IAAM,0BAA0B,uBAAO,IAAI,sCAAsC;AAEjF,IAAM,oCAAoC,MAAiC;AACzE,QAAM,WAAoB,QAAQ,IAAI,YAAY,uBAAuB;AACzE,MAAI,MAAM,QAAQ,QAAQ,EAAG,QAAO;AACpC,QAAM,UAAqC,CAAC;AAC5C,UAAQ,IAAI,YAAY,yBAAyB,OAAO;AACxD,SAAO;AACT;AAEA,kCAAkC,EAAE,KAAK,qBAAqB;","names":["registry"]}
@@ -47,6 +47,14 @@ interface DeployRoutingShape {
47
47
  * `system` convention.
48
48
  */
49
49
  websocketService?: string;
50
+ /**
51
+ * Immediate TLS proxy addresses/CIDRs allowed to assert
52
+ * `x-forwarded-proto: https`. Empty/undefined trusts nobody, so a client
53
+ * directly reaching the router cannot spoof secure-scheme backend logic.
54
+ * Examples: `['127.0.0.1/32', '::1/128']` for same-host nginx/Caddy, or the
55
+ * private subnet of a managed load balancer.
56
+ */
57
+ trustedProxyCidrs?: string[];
50
58
  /**
51
59
  * Path the router exposes for its OWN health/liveness (distinct from the
52
60
  * backend `/_health` boot-handshake endpoint). DEFAULT undefined → router
package/dist/config.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { A as AppConfig, a as AuthProps, B as BaseSessionLayout, f as DEFAULT_PROJECT_CONFIG, J as Jsonify, L as LoggingConfig, P as ProjectConfig, n as ProjectConfigInput, R as RateLimitingConfig, S as SessionConfig, b as SessionLocation, g as getProjectConfig, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-Dzen_rPK.js';
2
- export { S as SecretsResolvedListener, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-DZCzBNwP.js';
2
+ export { S as SecretsResolvedListener, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-Dl2CBINY.js';
package/dist/config.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerDeployConfig,
8
8
  registerSecretsResolvedListener,
9
9
  registerServicesConfig
10
- } from "./chunk-R7IRJFVC.js";
10
+ } from "./chunk-DVGSWLLS.js";
11
11
  import {
12
12
  DEFAULT_PROJECT_CONFIG,
13
13
  getProjectConfig,
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { H as HealthHashConfig, D as DeepPartial, B as BaseSessionLayout, a as A
4
4
  export { c as ApiConfig, A as AppConfig, d as AuthConfig, e as AuthRateLimitConfig, C as CorsConfig, f as DEFAULT_PROJECT_CONFIG, h as DevConfig, j as HttpConfig, k as HttpStreamConfig, J as Jsonify, L as LoggingConfig, O as OfflineQueueConfig, l as PasswordPolicyConfig, m as PathsConfig, P as ProjectConfig, n as ProjectConfigInput, o as RateLimitIdentity, p as RateLimitIdentityParams, R as RateLimitingConfig, q as SecurityHeadersConfig, S as SessionConfig, b as SessionLocation, s as SocketConfig, t as SyncConfig, u as SyncFlushPressureConfig, v as SyncStreamThrottleConfig, V as ValidationConfig, w as deepMerge, g as getProjectConfig, x as getProjectName, y as isPlainObject, i as isProjectConfigRegistered, r as registerProjectConfig } from './sessionTypes-Dzen_rPK.js';
5
5
  export { ApiTypeMap, StreamPayload, SyncTypeMap } from './apiTypeStubs.js';
6
6
  import { Server, Socket } from 'socket.io';
7
- export { D as DeployConfigShape, a as DeployDevelopmentShape, b as DeployEnvironmentShape, c as DeployResourceShape, d as DeployRoutingShape, P as PresetDefinition, S as SecretsResolvedListener, e as ServiceDefinition, f as ServicesConfigShape, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-DZCzBNwP.js';
7
+ export { D as DeployConfigShape, a as DeployDevelopmentShape, b as DeployEnvironmentShape, c as DeployResourceShape, d as DeployRoutingShape, P as PresetDefinition, S as SecretsResolvedListener, e as ServiceDefinition, f as ServicesConfigShape, g as getDeployConfig, h as getServicesConfig, i as isDeployConfigRegistered, j as isServicesConfigRegistered, n as notifySecretsResolved, r as registerDeployConfig, k as registerSecretsResolvedListener, l as registerServicesConfig } from './config-Dl2CBINY.js';
8
8
  import { z } from 'zod';
9
9
  import * as _prisma_client_runtime_library from '@prisma/client/runtime/library';
10
10
  import * as _prisma_client from '@prisma/client';
@@ -320,6 +320,7 @@ interface EmailMessage {
320
320
  */
321
321
  headers?: Record<string, string>;
322
322
  }
323
+ type EmailDeliveryOutcome = 'not-sent' | 'unknown';
323
324
  type EmailResult = {
324
325
  ok: true;
325
326
  id: string;
@@ -327,11 +328,27 @@ type EmailResult = {
327
328
  ok: false;
328
329
  reason: string;
329
330
  cause?: unknown;
331
+ /**
332
+ * `unknown` means the caller stopped waiting after dispatch began; retry
333
+ * only with the same idempotency key. Omitted on legacy/provider failures
334
+ * whose delivery semantics are adapter-specific.
335
+ */
336
+ deliveryOutcome?: EmailDeliveryOutcome;
330
337
  };
338
+ interface EmailSendContext {
339
+ /** Cooperative cancellation. Custom adapters should abort provider I/O when supported. */
340
+ signal: AbortSignal;
341
+ /** Stable caller-provided key reused across retries of the same logical email. */
342
+ idempotencyKey?: string;
343
+ }
331
344
  interface EmailSender {
332
345
  /** Adapter identifier for logs + diagnostics ("console", "resend", "smtp", etc.). */
333
346
  name: string;
334
- send: (message: EmailMessage) => Promise<EmailResult>;
347
+ /**
348
+ * The optional context preserves source compatibility with pre-0.7 adapters.
349
+ * New adapters should honor `signal` and provider-native idempotency where available.
350
+ */
351
+ send: (message: EmailMessage, context?: EmailSendContext) => Promise<EmailResult>;
335
352
  }
336
353
  /**
337
354
  * Registry keyed by adapter-slot name. Reserved slots used by the
@@ -946,6 +963,7 @@ interface BindAddress {
946
963
  port: number;
947
964
  }
948
965
  declare const registerBindAddress: (address: BindAddress) => void;
966
+ declare const registerBoundAddress: (address: BindAddress) => void;
949
967
  declare const getBindAddress: () => {
950
968
  ip: string;
951
969
  port: string;
@@ -1766,4 +1784,4 @@ declare const clearAllHooks: () => void;
1766
1784
  declare const registerSyncHook: <TName extends SyncHookName>(name: TName, handler: SyncHookHandler<SyncHookPayloads[TName]>) => (() => void);
1767
1785
  declare const dispatchSyncHook: <TName extends SyncHookName>(name: TName, payload: SyncHookPayloads[TName]) => void;
1768
1786
 
1769
- export { type ApiAuthRejectedPayload, type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type CheckRateLimitParams, type CookiePrefixConstraints, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, DEFAULT_REDACTED_LOG_KEYS, DEPTH_TRUNCATED_PLACEHOLDER, type DbHealthCheck, type DbHealthResult, DeepPartial, type DispatchResult, type EmailAttachment, type EmailMessage, type EmailResult, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, type FormatterEnvelope, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, HealthHashConfig, type HealthHashDescriptor, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, type OneTimeTokenHandle, PUBLIC_DIR, type PeerRequire, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostAvatarServePayload, type PostErrorNormalizePayload, type PostHttpRequestPayload, type PostSessionRefreshPayload, type PostSyncAuthorizePayload, type PostSyncExecutePayload, type PostSyncFanoutPayload, type PostSyncValidatePayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreAvatarServePayload, type PreCaptureFilter, type PreErrorNormalizePayload, type PreHttpRequestPayload, type PreServerStopPayload, type PreSessionRefreshPayload, type PreSocketMessagePayload, type PreSyncAuthorizePayload, type PreSyncExecutePayload, type PreSyncFanoutPayload, type PreSyncRecipientPayload, type PreSyncValidatePayload, type ProcessUploadInput, type ProcessUploadResult, REDACTED_PLACEHOLDER, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type Registry, type RegistryOptions, type ResolveClientIpParams, type RoomNameFormatter, type RoomNameFormatterContext, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type SessionCreatedPayload, type SessionLogoutInput, type SessionProvider, type SessionRevokedPayload, type SessionSaveResult, type SocketMiddleware, type SpanHandle, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UNKNOWN_CLIENT_IP, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, appendErrorTracker, applyCookiePrefixConstraints, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, consumeOneTimeToken, consumeOneTimeTokenJson, createLocalizedNormalizer, createRegistry, defaultRateLimitStrategy, defaultRedisKeyFormatter, defaultRoomNameFormatter, deriveTokenBucketId, describeHealthHashConfig, dispatchHook, dispatchSyncHook, ensurePeerDepInstalled, env, escapeHtml, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, flushErrorTrackers, formatKey, formatRoomName, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getCurrentErrorTrackerIdentity, getDbHealthCheck, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnv, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRegisteredSessionProvider, getRoomNameFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, hashSynchronizedValueWith, inferHttpMethod, initConsolelog, initSharedSentry, isDbHealthCheckRegistered, isEmailSenderRegistered, isFalsy, isGetMethodName, isLocalizedNormalizerRegistered, isLoopbackIp, isMethodAllowed, isPrismaClientRegistered, isPrismaClientResolvable, isProduction, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isSessionProviderRegistered, issueOneTimeToken, listEmailSenderNames, loadEnvFiles, loadPeer, normalizeErrorResponse, normalizeOrigin, oneTimeTokenKey, performLogout, prisma, processUpload, readBootUuid, readSession, rebuildDefaultRedisClient, recordMetricAcrossTrackers, redis, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerCsrfConfig, registerDbHealthCheck, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPreCaptureFilter, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRoomNameFormatter, registerRuntimeMapsProvider, registerSessionProvider, registerSocketMiddleware, registerStrayPrefixCommand, registerSyncAbortController, registerSyncHook, releaseLease, removeSession, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetDbHealthCheckForTests, resetDefaultRedisClient, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resetSessionProviderForTests, resolveClientIp, resolveDevCallbackUrl, resolveEnvKey, resolveErrorMessage, resolveFromRoot, resolveHealthHashConfig, resolveHealthHashConfigFromDescriptor, runWithErrorTrackerIdentity, runWithErrorTrackerIdentityScope, sanitizeErrorString, sanitizeErrorStrings, sanitizeForLog, serveAvatar, setCurrentErrorTrackerIdentity, setErrorTrackerUser, setIoInstance, setSentryUser, startSpan, startSpanAcrossTrackers, startSpanHandle, type syncMessage, tryCatch, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid, writeSession };
1787
+ export { type ApiAuthRejectedPayload, type ApiErrorPayload, type ApiResponseEnvelope, AuthProps, type AvatarConfig, type AvatarConfigInput, BOOT_KEY_PREFIX, BaseSessionLayout, type CheckRateLimitParams, type CookiePrefixConstraints, type CorsRejectedPayload, type CreateLocalizedNormalizerInput, type CsrfConfig, type CsrfCookieOptions, type CsrfMismatchPayload, DEFAULT_AVATAR_CONFIG, DEFAULT_CLIENT_KEY, DEFAULT_CSRF_CONFIG, DEFAULT_ENV_FILES, DEFAULT_REDACTED_LOG_KEYS, DEPTH_TRUNCATED_PLACEHOLDER, type DbHealthCheck, type DbHealthResult, DeepPartial, type DispatchResult, type EmailAttachment, type EmailDeliveryOutcome, type EmailMessage, type EmailResult, type EmailSendContext, type EmailSender, type EmailSenderRegistry, type ErrorFormatter, type ErrorFormatterContext, ErrorParam, ErrorResponseInput, type ErrorTracker, type ErrorTrackerContext, type ErrorTrackerEvent, type ErrorTrackerUser, type FormatterEnvelope, GENERATED_API_DOCS_PATH, GENERATED_API_SCHEMAS_PATH, GENERATED_SOCKET_TYPES_PATH, HealthHashConfig, type HealthHashDescriptor, type HookHandler, type HookName, type HookPayloads, type HookResult, type HookSessionShape, type HookStopSignal, type HttpMethod, type LanguageCode, type LocaleReloader, type LocalizedNormalizer, type NormalizeErrorResponseInput, NormalizedErrorResponse, type OnUploadCompletePayload, type OnUploadStartPayload, type OneTimeTokenHandle, PUBLIC_DIR, type PeerRequire, type PostApiExecutePayload, type PostApiRespondPayload, type PostApiValidatePayload, type PostAvatarServePayload, type PostErrorNormalizePayload, type PostHttpRequestPayload, type PostSessionRefreshPayload, type PostSyncAuthorizePayload, type PostSyncExecutePayload, type PostSyncFanoutPayload, type PostSyncValidatePayload, type PreApiExecutePayload, type PreApiRespondPayload, type PreApiValidatePayload, type PreAvatarServePayload, type PreCaptureFilter, type PreErrorNormalizePayload, type PreHttpRequestPayload, type PreServerStopPayload, type PreSessionRefreshPayload, type PreSocketMessagePayload, type PreSyncAuthorizePayload, type PreSyncExecutePayload, type PreSyncFanoutPayload, type PreSyncRecipientPayload, type PreSyncValidatePayload, type ProcessUploadInput, type ProcessUploadResult, REDACTED_PLACEHOLDER, ROOT_DIR, type RateLimitExceededPayload, type RateLimitResult, type RateLimitStrategy, type RedisConnectionOptions, type RedisKeyFormatter, type Registry, type RegistryOptions, type ResolveClientIpParams, type RoomNameFormatter, type RoomNameFormatterContext, type RuntimeApiMapsResult, type RuntimeEnv, type RuntimeMapsProvider, type RuntimeSyncMapsResult, SERVER_DIR, SERVER_FUNCTIONS_DIR, SHARED_DIR, SRC_DIR, type SessionCreatedPayload, type SessionLogoutInput, type SessionProvider, type SessionRevokedPayload, type SessionSaveResult, type SocketMiddleware, type SpanHandle, type SpanResult, type SyncErrorPayload, type SyncHookHandler, type SyncHookName, type SyncHookPayloads, TSCONFIG_ALIAS_FILES, type TranslateFunction, type TranslateInput, UNKNOWN_CLIENT_IP, UPLOADS_DIR, type ValidationResult$1 as ValidationResult, abortAllForSocket, abortApiByResponseIndex, abortSyncByCb, acquireLease, allowedOrigin, type apiMessage, appendErrorTracker, applyCookiePrefixConstraints, applyErrorFormatter, applySocketMiddlewares, applyStrayKeyPrefix, attachSocketRedisAdapter, bootstrapEnv, captureException, captureExceptionAcrossTrackers, captureMessage, captureMessageAcrossTrackers, checkRateLimit, clearAllHooks, clearAllRateLimits, clearRateLimit, clearSocketMiddlewares, collectSynchronizedEnvKeys, computeSynchronizedEnvHashes, consumeOneTimeToken, consumeOneTimeTokenJson, createLocalizedNormalizer, createRegistry, defaultRateLimitStrategy, defaultRedisKeyFormatter, defaultRoomNameFormatter, deriveTokenBucketId, describeHealthHashConfig, dispatchHook, dispatchSyncHook, ensurePeerDepInstalled, env, escapeHtml, extractLanguageFromHeader, extractTokenFromRequest, extractTokenFromSocket, flushErrorTrackers, formatKey, formatRoomName, getActiveErrorTrackers, getAvatarConfig, getBindAddress, getCookieValue, getCsrfConfig, getCurrentErrorTrackerIdentity, getDbHealthCheck, getEffectiveHttpMethod, getEmailSender, getEmailSenderByName, getEnv, getEnvFiles, getErrorFormatter, getGeneratedApiDocsPath, getGeneratedApiSchemasPath, getGeneratedSocketTypesPath, getIoInstance, getLocaleReloader, getLocalizedNormalizer, getParams, getPrismaClient, getPrismaClientFor, getPrismaClientKeys, getPublicDir, getRateLimitStatus, getRateLimitStrategy, getRedactedLogKeys, getRedisClient, getRedisClientFor, getRedisClientKeys, getRedisConnectionOptions, getRedisKeyFormatter, getRegisteredSessionProvider, getRoomNameFormatter, getRuntimeApiMaps, getRuntimeSyncMaps, getServerDir, getServerFunctionDirs, getServerFunctionsDir, getSharedDir, getSocketMiddlewares, getSrcDir, getUploadsDir, hasCookie, hashSynchronizedValue, hashSynchronizedValueWith, inferHttpMethod, initConsolelog, initSharedSentry, isDbHealthCheckRegistered, isEmailSenderRegistered, isFalsy, isGetMethodName, isLocalizedNormalizerRegistered, isLoopbackIp, isMethodAllowed, isPrismaClientRegistered, isPrismaClientResolvable, isProduction, isRedactedLogKey, isRedisClientRegistered, isRuntimeMapsProviderRegistered, isSessionProviderRegistered, issueOneTimeToken, listEmailSenderNames, loadEnvFiles, loadPeer, normalizeErrorResponse, normalizeOrigin, oneTimeTokenKey, performLogout, prisma, processUpload, readBootUuid, readSession, rebuildDefaultRedisClient, recordMetricAcrossTrackers, redis, registerApiAbortController, registerAvatarConfig, registerBindAddress, registerBoundAddress, registerCsrfConfig, registerDbHealthCheck, registerEmailSender, registerEmailSenders, registerErrorFormatter, registerErrorTracker, registerErrorTrackers, registerHook, registerLocaleReloader, registerLocalizedNormalizer, registerPreCaptureFilter, registerPrismaClient, registerRateLimitStrategy, registerRedactedLogKeys, registerRedisClient, registerRedisKeyFormatter, registerRoomNameFormatter, registerRuntimeMapsProvider, registerSessionProvider, registerSocketMiddleware, registerStrayPrefixCommand, registerSyncAbortController, registerSyncHook, releaseLease, removeSession, renewLease, resetClientsForTests, resetCsrfConfigForTests, resetDbHealthCheckForTests, resetDefaultRedisClient, resetRedactedLogKeysForTests, resetRedisKeyFormatterForTests, resetSessionProviderForTests, resolveClientIp, resolveDevCallbackUrl, resolveEnvKey, resolveErrorMessage, resolveFromRoot, resolveHealthHashConfig, resolveHealthHashConfigFromDescriptor, runWithErrorTrackerIdentity, runWithErrorTrackerIdentityScope, sanitizeErrorString, sanitizeErrorStrings, sanitizeForLog, serveAvatar, setCurrentErrorTrackerIdentity, setErrorTrackerUser, setIoInstance, setSentryUser, startSpan, startSpanAcrossTrackers, startSpanHandle, type syncMessage, tryCatch, unregisterApiAbortController, unregisterSyncAbortController, validateInputByType, validateRequest, writeBootUuid, writeSession };
package/dist/index.js CHANGED
@@ -59,7 +59,7 @@ import {
59
59
  registerDeployConfig,
60
60
  registerSecretsResolvedListener,
61
61
  registerServicesConfig
62
- } from "./chunk-R7IRJFVC.js";
62
+ } from "./chunk-DVGSWLLS.js";
63
63
  import {
64
64
  DEFAULT_REDACTED_LOG_KEYS,
65
65
  DEPTH_TRUNCATED_PLACEHOLDER,
@@ -618,6 +618,7 @@ var applyStrayKeyPrefix = (key) => key.includes(":") ? key : `${getProjectName()
618
618
 
619
619
  // src/redis.ts
620
620
  var cachedDefault = null;
621
+ var frameworkRegisteredDefault = null;
621
622
  var DEFAULT_MAX_REDIS_RECONNECT_ATTEMPTS = 50;
622
623
  var DEFAULT_MAX_REDIS_BACKOFF_MS = 2e3;
623
624
  var readPositiveIntEnv = (name, fallback) => {
@@ -683,6 +684,7 @@ var rebuildDefaultRedisClient = () => {
683
684
  resetDefaultRedisClient();
684
685
  const client = constructRedisClient();
685
686
  registerRedisClient(client);
687
+ frameworkRegisteredDefault = client;
686
688
  if (previousRegistered && previousRegistered !== client) {
687
689
  try {
688
690
  previousRegistered.disconnect();
@@ -693,9 +695,12 @@ var rebuildDefaultRedisClient = () => {
693
695
  };
694
696
  setDefaultRedisResolver(buildDefaultRedisClient);
695
697
  registerSecretsResolvedListener((changedKeys) => {
696
- if (changedKeys === void 0 || changedKeys.some((key) => /^REDIS_/i.test(key))) {
697
- rebuildDefaultRedisClient();
698
+ if (changedKeys !== void 0 && !changedKeys.some((key) => /^REDIS_/i.test(key))) return;
699
+ const registered2 = isRedisClientRegistered() ? getRedisClient() : null;
700
+ if (registered2 && registered2 !== frameworkRegisteredDefault) {
701
+ return;
698
702
  }
703
+ rebuildDefaultRedisClient();
699
704
  });
700
705
  var getRedisConnectionOptions = () => ({
701
706
  host: readRedisHost(),
@@ -1170,8 +1175,14 @@ var processUpload = async (input) => {
1170
1175
  };
1171
1176
 
1172
1177
  // src/bindAddress.ts
1178
+ var intended = null;
1173
1179
  var registered = null;
1174
1180
  var registerBindAddress = (address) => {
1181
+ intended = address;
1182
+ registered = address;
1183
+ };
1184
+ var registerBoundAddress = (address) => {
1185
+ intended ??= address;
1175
1186
  registered = address;
1176
1187
  };
1177
1188
  var getBindAddress = () => {
@@ -1192,11 +1203,12 @@ var resolveDevCallbackUrl = (callbackUrl) => {
1192
1203
  } catch {
1193
1204
  return callbackUrl;
1194
1205
  }
1195
- if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1") return callbackUrl;
1206
+ if (!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) return callbackUrl;
1196
1207
  const boundPort = getBindAddress().port;
1197
- if (!boundPort) return callbackUrl;
1208
+ const intendedPort = intended ? String(intended.port) : "";
1209
+ if (!boundPort || !intendedPort) return callbackUrl;
1198
1210
  const currentPort = url.port || defaultPortForProtocol(url.protocol);
1199
- if (currentPort === boundPort) return callbackUrl;
1211
+ if (currentPort === boundPort || currentPort !== intendedPort) return callbackUrl;
1200
1212
  url.port = boundPort === defaultPortForProtocol(url.protocol) ? "" : boundPort;
1201
1213
  return url.toString();
1202
1214
  };
@@ -2341,6 +2353,7 @@ export {
2341
2353
  registerApiMethodMap,
2342
2354
  registerAvatarConfig,
2343
2355
  registerBindAddress,
2356
+ registerBoundAddress,
2344
2357
  registerCsrfConfig,
2345
2358
  registerDbHealthCheck,
2346
2359
  registerDeployConfig,