@luckystack/core 0.7.5 → 0.8.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/CHANGELOG.md +20 -0
- package/CLAUDE.md +5 -2
- package/dist/{chunk-DVGSWLLS.js → chunk-6HGH6S36.js} +2 -2
- package/dist/{chunk-DVGSWLLS.js.map → chunk-6HGH6S36.js.map} +1 -1
- package/dist/{chunk-AFG7KKMS.js → chunk-6K7MAXMT.js} +48 -33
- package/dist/{chunk-AFG7KKMS.js.map → chunk-6K7MAXMT.js.map} +1 -1
- package/dist/{chunk-J3UOWYT4.js → chunk-I4R7DMIX.js} +2 -2
- package/dist/{chunk-LTCDQU5Z.js → chunk-RH65K3US.js} +4 -1
- package/dist/{chunk-LTCDQU5Z.js.map → chunk-RH65K3US.js.map} +1 -1
- package/dist/client.d.ts +25 -4
- package/dist/client.js +223 -15
- package/dist/client.js.map +1 -1
- package/dist/{config-Dl2CBINY.d.ts → config-DV8DGNt6.d.ts} +7 -1
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -2
- package/dist/index.d.ts +26 -6
- package/dist/index.js +61 -5
- package/dist/index.js.map +1 -1
- package/dist/{pageRouteValidation-CWkPqPW3.d.ts → pageRouteValidation-bz2LFVQq.d.ts} +7 -7
- package/dist/{projectConfig-DBLSQ3NY.js → projectConfig-PB5VK5AD.js} +2 -2
- package/dist/{sentrySetup-MF6JSVWA.js → sentrySetup-G7NGISBY.js} +3 -3
- package/dist/{sessionTypes-Dzen_rPK.d.ts → sessionTypes-iyUMf_pf.d.ts} +12 -1
- package/docs/app-bootstrap.md +14 -0
- package/docs/session-types.md +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-J3UOWYT4.js.map → chunk-I4R7DMIX.js.map} +0 -0
- /package/dist/{projectConfig-DBLSQ3NY.js.map → projectConfig-PB5VK5AD.js.map} +0 -0
- /package/dist/{sentrySetup-MF6JSVWA.js.map → sentrySetup-G7NGISBY.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-07-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `transport.invocation` can opt typed API/sync calls into routed HTTP/SSE while the existing Socket.io connection remains responsible for realtime delivery.
|
|
15
|
+
- Browser-safe routed invocation now preserves timeout, cancellation, streaming envelopes, typed GET payloads, bearer auth and origin-scoped CSRF.
|
|
16
|
+
- Service topology can declare pure-data `customRoutes` ownership for non-`/api`/`/sync` paths.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Runtime input validation now recognizes finite numeric TypeScript literals in object fields and unions, including negative and decimal values, instead of rejecting them as unvalidatable before the handler runs.
|
|
21
|
+
|
|
22
|
+
## [0.7.6] - 2026-07-23
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- Boot UUIDs can now be renewed without rotating their value, and a missing key
|
|
27
|
+
is recreated after Redis recovery. The non-overlapping, unref'd heartbeat API
|
|
28
|
+
prevents the default one-hour TTL from making healthy servers not-ready.
|
|
29
|
+
|
|
10
30
|
## [0.7.4] - 2026-07-22
|
|
11
31
|
|
|
12
32
|
### Added
|
package/CLAUDE.md
CHANGED
|
@@ -150,7 +150,8 @@ Foundation package for LuckyStack. Owns the socket-first transport contracts (`a
|
|
|
150
150
|
| `HookSessionShape`, `HookName`, `HookHandler`, `HookResult`, `HookStopSignal`, `HookPayloads` (types) | Hook contract types (augmentable via TS module augmentation). | -> docs/hooks.md |
|
|
151
151
|
| `preServerStop` hook + `PreServerStopPayload` (type) | Graceful-shutdown lifecycle hook — `@luckystack/server` dispatches it once on SIGTERM/SIGINT before the server stops accepting connections. Payload `{ reason, timeoutMs? }`. Best-effort (a stop signal does NOT abort shutdown); use to flush trackers / drain queues / close pools. | -> docs/hooks.md |
|
|
152
152
|
| `isOnline()` / `enqueueApiRequest` / `enqueueSyncRequest` / `removeApiQueueItem` / `removeSyncQueueItem` / `removeApiQueueItemsByKey` / `flushApiQueue` / `flushSyncQueue` / `getApiQueueSize` / `getSyncQueueSize` | Client-side offline queue with per-item `dropPolicy` and global max-size/max-age caps. | -> docs/socket-bootstrap.md |
|
|
153
|
-
| `getCsrfToken()`, `clearCsrfToken()`, `httpFetch(...)` | CSRF-aware fetch wrapper
|
|
153
|
+
| `getCsrfToken(origin?)`, `clearCsrfToken()`, `httpFetch(...)` | Origin-scoped CSRF-aware fetch wrapper; routed token-mode calls also attach bearer auth. | -> docs/socket-bootstrap.md |
|
|
154
|
+
| `invokeRoutedHttp(...)` (`/client`) | Browser-safe JSON/SSE invocation primitive with streaming, timeout and composed AbortSignals; used by typed API/sync routed mode. | -> docs/socket-bootstrap.md |
|
|
154
155
|
| `issueOneTimeToken(namespace, ttlSeconds, payload): OneTimeTokenHandle` | Mint a single-use Redis-backed token (returns `{ token, store() }`). HASHED AT REST — only `sha256(token)` is stored as the key, never the raw token. Used by `@luckystack/login` for password-reset + email-change links. | -> docs/app-bootstrap.md |
|
|
155
156
|
| `consumeOneTimeToken(namespace, token): Promise<string \| null>` | Atomically validate + consume a one-time token (single `MULTI` GET+DEL → at-most-once). Returns the stored payload string, or null on miss/expired/reused. | -> docs/app-bootstrap.md |
|
|
156
157
|
| `consumeOneTimeTokenJson<T>(namespace, token): Promise<T \| null>` | `consumeOneTimeToken` + JSON-parse; null on miss OR malformed payload. | -> docs/app-bootstrap.md |
|
|
@@ -165,6 +166,8 @@ Foundation package for LuckyStack. Owns the socket-first transport contracts (`a
|
|
|
165
166
|
| `applySocketMiddlewares(io: SocketIOServer): void` | Wire every registered middleware into the running Socket.io server (called from `@luckystack/server`'s `loadSocket`). | -> docs/socket-bootstrap.md |
|
|
166
167
|
| `writeBootUuid(envKey?): Promise<string>` | Write a fresh UUID to `luckystack:boot:<envKey>` with the configured TTL. | -> docs/app-bootstrap.md |
|
|
167
168
|
| `readBootUuid(envKey?): Promise<string \| null>` | Read the boot UUID (router cross-checks against `/_health`). | -> docs/app-bootstrap.md |
|
|
169
|
+
| `refreshBootUuid(envKey?): Promise<void>` | Extend the existing environment UUID without rotating it; recreate it when Redis recovered after expiry. | -> docs/app-bootstrap.md |
|
|
170
|
+
| `startBootUuidHeartbeat(envKey?): BootUuidHeartbeat` | Start a non-overlapping, unref'd TTL/3 refresh loop; `stop()` cancels it during server shutdown. | -> docs/app-bootstrap.md |
|
|
168
171
|
| `resolveEnvKey(): string` | `LUCKYSTACK_ENV` -> `NODE_ENV` -> `'development'`. | -> docs/app-bootstrap.md |
|
|
169
172
|
| `BOOT_KEY_PREFIX: 'luckystack:boot:'` | Constant — single source of truth so router can't drift. | -> docs/app-bootstrap.md |
|
|
170
173
|
| `collectSynchronizedEnvKeys()` / `computeSynchronizedEnvHashes(bootUuid?)` / `hashSynchronizedValue(value, bootUuid?)` | Cross-env drift detection helpers for the router boot handshake. Both hash helpers now honour `http.healthHash` (default `'plain'` = byte-identical to before); the optional `bootUuid?` arg is only needed when `http.healthHash.salt === '@bootUuid'`. Zero-arg callers unchanged. | -> docs/app-bootstrap.md |
|
|
@@ -218,7 +221,7 @@ Env vars read directly by core (via `env.ts` and call-time helpers):
|
|
|
218
221
|
| `LUCKYSTACK_ENV` | (unset) | `resolveEnvKey()` first preference (boot UUID, router handshake). |
|
|
219
222
|
| `LUCKYSTACK_ENV_FILES` | `.env,.env.local` | Ambient override for `getEnvFiles()` / `loadEnvFiles()` — comma-separated list of env files to load, "later overrides earlier". |
|
|
220
223
|
|
|
221
|
-
`registerProjectConfig` slots (see `ProjectConfig` for full surface): `app.publicUrl`, `logging.*`, `rateLimiting.{enabled, store, redisKeyPrefix, defaultApiLimit, defaultIpLimit, windowMs, cleanupIntervalMs, onStoreError, skipLoopbackInDev, identity, auth}`, `session.{basedToken, expiryDays, perUser, maxConcurrentPerUser, onConflict, notifyOldDeviceOnRevoke, projectName}`, `http.{sessionCookie*, sessionCookieDomain, sessionCookiePrefix, sessionCookieSecure, requestBodyMaxBytes, trustProxy, trustedProxyHopCount, acceptBearerInCookieMode, healthEndpoint, liveEndpoint, readyEndpoint, testResetEndpoint, healthHash, stream, securityHeaders, cors}`, `auth.{credentials, oauthStateTtlSeconds, passwordPolicy, emailMaxLength, nameMaxLength, bcryptRounds, providerAccountStrategy, forgotPassword, passwordResetTtlSeconds, passwordResetBrand, emailChangeTtlSeconds, allowRegistration, passwordResetPath, emailChangeConfirmPath}`, `socket.{maxHttpBufferSize, pingTimeout, pingInterval, activityHeartbeatThrottleMs}`, `api.{requestTimeoutMs}`, `validation.{runtimeMode}`, `sync.{streamThrottle, fanoutYieldEvery, fanoutYieldMs, requestTimeoutMs, allowClientReceiverAll, requireRoomMembership, flushPressure}`, `offlineQueue.{maxSize, maxAgeMs, dropPolicy}`, `dev.{hotReloadDebounceMs, watcherStabilityThresholdMs, watcherPollIntervalMs, warnOnMissingInputType}`, `deploy.routing.{upstreamTimeoutMs, websocketService, routerHealthPath, maxRequestBodyBytes}` (via `registerDeployConfig`), `paths.*`, `defaultLanguage`, `defaultTheme`, `socketActivityBroadcaster`, `socketStatusIndicator`, `locationProviderEnabled`, `loginRedirectUrl`, `oauthCallbackBase`.
|
|
224
|
+
`registerProjectConfig` slots (see `ProjectConfig` for full surface): `app.publicUrl`, `transport.invocation` (`'socket'` default or `'routed-http'`), `logging.*`, `rateLimiting.{enabled, store, redisKeyPrefix, defaultApiLimit, defaultIpLimit, windowMs, cleanupIntervalMs, onStoreError, skipLoopbackInDev, identity, auth}`, `session.{basedToken, expiryDays, perUser, maxConcurrentPerUser, onConflict, notifyOldDeviceOnRevoke, projectName}`, `http.{sessionCookie*, sessionCookieDomain, sessionCookiePrefix, sessionCookieSecure, requestBodyMaxBytes, trustProxy, trustedProxyHopCount, acceptBearerInCookieMode, healthEndpoint, liveEndpoint, readyEndpoint, testResetEndpoint, healthHash, stream, securityHeaders, cors}`, `auth.{credentials, oauthStateTtlSeconds, passwordPolicy, emailMaxLength, nameMaxLength, bcryptRounds, providerAccountStrategy, forgotPassword, passwordResetTtlSeconds, passwordResetBrand, emailChangeTtlSeconds, allowRegistration, passwordResetPath, emailChangeConfirmPath}`, `socket.{maxHttpBufferSize, pingTimeout, pingInterval, activityHeartbeatThrottleMs}`, `api.{requestTimeoutMs}`, `validation.{runtimeMode}`, `sync.{streamThrottle, fanoutYieldEvery, fanoutYieldMs, requestTimeoutMs, allowClientReceiverAll, requireRoomMembership, flushPressure}`, `offlineQueue.{maxSize, maxAgeMs, dropPolicy}`, `dev.{hotReloadDebounceMs, watcherStabilityThresholdMs, watcherPollIntervalMs, warnOnMissingInputType}`, `deploy.routing.{upstreamTimeoutMs, websocketService, routerHealthPath, maxRequestBodyBytes}` (via `registerDeployConfig`), `paths.*`, `defaultLanguage`, `defaultTheme`, `socketActivityBroadcaster`, `socketStatusIndicator`, `locationProviderEnabled`, `loginRedirectUrl`, `oauthCallbackBase`.
|
|
222
225
|
|
|
223
226
|
> **New 0.2.0 keys (all additive — a missing key keeps prior behavior EXCEPT `validation.runtimeMode`):** `validation.runtimeMode` (`'enforce'` default — prod input validation now ACTUALLY runs; set `'off'` to restore the old prod no-op). `rateLimiting.skipLoopbackInDev` (default `false`; skip the cross-route IP cap for loopback in dev), `rateLimiting.identity` (callback overriding the per-route bucket basis), `rateLimiting.auth` (`{ enabled false, maxAttempts 5, maxAttemptsPerAccount 50, windowMs 900000 }` — dual lockout counter: per-IP `maxAttempts` + cross-IP `maxAttemptsPerAccount`, ADR 0015`) — per-account login lockout slot). **BREAKING — `sync.allowClientReceiverAll` (default now `false`, was `true`) + `sync.requireRoomMembership` (default now `true`, was `false`): a client can no longer broadcast to `'all'` nor target an unjoined room by default — join the room, approve via `preSyncAuthorize`, or opt back into the permissive values.** `sync.flushPressure` (`{ highWaterMarkChunks 1000, lowWaterMarkChunks 250, maxBufferedBytes 5242880 }`). **BREAKING — `http.healthHash` (`{ mode: 'plain'|'salted'|'hmac', salt: string }`) now DEFAULTS to `{ mode: 'hmac', salt: '@bootUuid' }`: `/_health` no longer exposes a stable, unsalted `sha256(secret)` — the synchronized-env fingerprint is HMAC-keyed on the per-boot UUID (rotates each restart). When no boot UUID is available the `'@bootUuid'` sentinel collapses to `'plain'` so the boot handshake never silently diverges. Set a non-empty `salt` to pin a stable key, or `mode:'plain'` to restore legacy wire output.** `http.sessionCookieDomain`/`sessionCookiePrefix` (`'__Host-'`/`'__Secure-'`)/`sessionCookieSecure`. `http.trustedProxyHopCount` (default `1` — when `trustProxy` is on, the resolved client IP is now the entry that many hops in from the RIGHT of `X-Forwarded-For` (the rightmost hop is the immediate upstream proxy); the leftmost, client-controlled hop is never trusted — CORE-O3 leftmost-spoof fix; clamped to the list length). `http.acceptBearerInCookieMode` (default `false` — in cookie-mode (`session.basedToken:false`) the framework now reads ONLY the session cookie and IGNORES any `Authorization: Bearer` / `handshake.auth.token` fallback, closing the CORE-O10 CSRF-bypass; set `true` to restore the legacy cookie-then-bearer fallback. Token-mode is unaffected). `socket.activityHeartbeatThrottleMs` (default `10000`). `auth.allowRegistration` (default `true`), `auth.passwordResetPath` (default `'/reset-password'`), `auth.emailChangeConfirmPath` (default `'/confirm-email-change'`). `deploy.routing.{upstreamTimeoutMs, websocketService, routerHealthPath, maxRequestBodyBytes}` (all optional — undefined uses the router built-in default).
|
|
224
227
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createRegistry
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-RH65K3US.js";
|
|
4
4
|
|
|
5
5
|
// src/deployConfigRegistry.ts
|
|
6
6
|
var DEFAULT_DEPLOY_CONFIG = {
|
|
@@ -64,4 +64,4 @@ export {
|
|
|
64
64
|
registerSecretsResolvedListener,
|
|
65
65
|
notifySecretsResolved
|
|
66
66
|
};
|
|
67
|
-
//# sourceMappingURL=chunk-
|
|
67
|
+
//# sourceMappingURL=chunk-6HGH6S36.js.map
|
|
@@ -1 +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"]}
|
|
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 * Pure-data ownership manifest for HTTP routes outside `/api/*` and `/sync/*`.\n * Keys are absolute path prefixes; values are service keys. The router uses\n * longest-prefix matching, so `/webhooks/admin` may override `/webhooks`.\n */\n customRoutes?: Record<string, string>;\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;;;ACrG7E,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;;;ACrC/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"]}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
captureException,
|
|
3
3
|
getLogger
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-I4R7DMIX.js";
|
|
5
5
|
import {
|
|
6
6
|
createRegistry,
|
|
7
7
|
getProjectConfig
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-RH65K3US.js";
|
|
9
9
|
|
|
10
10
|
// src/apiMethodMapRegistry.ts
|
|
11
11
|
var registry = createRegistry(null);
|
|
@@ -459,11 +459,6 @@ var buildGetJoinedRoomsResponseEventName = (responseIndex2) => {
|
|
|
459
459
|
return withIndex(socketEventNames.getJoinedRoomsResponsePrefix, responseIndex2);
|
|
460
460
|
};
|
|
461
461
|
|
|
462
|
-
// src/sleep.ts
|
|
463
|
-
function sleep(ms) {
|
|
464
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
462
|
// src/csrfConfig.ts
|
|
468
463
|
var DEFAULT_CSRF_CONFIG = {
|
|
469
464
|
cookieName: "csrf-token",
|
|
@@ -512,18 +507,24 @@ async function tryCatch(func, params, context) {
|
|
|
512
507
|
}
|
|
513
508
|
|
|
514
509
|
// src/csrf.ts
|
|
515
|
-
var
|
|
516
|
-
var
|
|
517
|
-
var
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
510
|
+
var cachedTokens = /* @__PURE__ */ new Map();
|
|
511
|
+
var inflightFetches = /* @__PURE__ */ new Map();
|
|
512
|
+
var resolveSocketOrigin = () => socket?.io.opts.hostname ? `${socket.io.opts.secure ? "https" : "http"}://${socket.io.opts.hostname}${socket.io.opts.port ? `:${socket.io.opts.port}` : ""}` : "";
|
|
513
|
+
var resolveRequestOrigin = (input) => {
|
|
514
|
+
const browserOrigin = globalThis.window !== void 0 && globalThis.location?.origin ? globalThis.location.origin : "";
|
|
515
|
+
if (input !== void 0) {
|
|
516
|
+
let raw;
|
|
517
|
+
if (typeof input === "string") raw = input;
|
|
518
|
+
else if (input instanceof URL) raw = input.href;
|
|
519
|
+
else raw = input.url;
|
|
520
|
+
const fallbackOrigin = browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();
|
|
521
|
+
const [error, parsed] = tryCatchSync(() => new URL(raw, fallbackOrigin));
|
|
522
|
+
if (!error && parsed) return parsed.origin;
|
|
523
|
+
}
|
|
524
|
+
return browserOrigin.length > 0 ? browserOrigin : resolveSocketOrigin();
|
|
525
|
+
};
|
|
526
|
+
var fetchCsrfToken = async (origin) => {
|
|
527
|
+
const base = origin;
|
|
527
528
|
const [error, token] = await tryCatch(async () => {
|
|
528
529
|
const response = await fetch(`${base}/auth/csrf`, {
|
|
529
530
|
method: "GET",
|
|
@@ -536,22 +537,25 @@ var fetchCsrfToken = async () => {
|
|
|
536
537
|
if (error) return null;
|
|
537
538
|
return token ?? null;
|
|
538
539
|
};
|
|
539
|
-
var getCsrfToken = async () => {
|
|
540
|
+
var getCsrfToken = async (origin = resolveRequestOrigin()) => {
|
|
540
541
|
if (getProjectConfig().session.basedToken) {
|
|
541
542
|
return null;
|
|
542
543
|
}
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
544
|
+
const cached = cachedTokens.get(origin);
|
|
545
|
+
if (cached) return cached;
|
|
546
|
+
const inflight = inflightFetches.get(origin);
|
|
547
|
+
if (inflight) return inflight;
|
|
548
|
+
const next = fetchCsrfToken(origin).then((token) => {
|
|
549
|
+
if (token) cachedTokens.set(origin, token);
|
|
550
|
+
inflightFetches.delete(origin);
|
|
548
551
|
return token;
|
|
549
552
|
});
|
|
550
|
-
|
|
553
|
+
inflightFetches.set(origin, next);
|
|
554
|
+
return next;
|
|
551
555
|
};
|
|
552
556
|
var clearCsrfToken = () => {
|
|
553
|
-
|
|
554
|
-
|
|
557
|
+
cachedTokens.clear();
|
|
558
|
+
inflightFetches.clear();
|
|
555
559
|
};
|
|
556
560
|
var STATE_CHANGING = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
557
561
|
var isCsrfMismatchResponse = async (response) => {
|
|
@@ -567,6 +571,11 @@ var isCsrfMismatchResponse = async (response) => {
|
|
|
567
571
|
var httpFetch = async (input, init = {}) => {
|
|
568
572
|
const method = (init.method ?? "GET").toUpperCase();
|
|
569
573
|
const headers = new Headers(init.headers);
|
|
574
|
+
const config = getProjectConfig();
|
|
575
|
+
if (config.session.basedToken && !headers.has("Authorization")) {
|
|
576
|
+
const token2 = globalThis.window === void 0 ? null : tryCatchSync(() => globalThis.sessionStorage.getItem("token"))[1];
|
|
577
|
+
if (token2) headers.set("Authorization", `Bearer ${token2}`);
|
|
578
|
+
}
|
|
570
579
|
if (!headers.has("Content-Type") && init.body && typeof init.body === "string") {
|
|
571
580
|
headers.set("Content-Type", "application/json");
|
|
572
581
|
}
|
|
@@ -583,14 +592,15 @@ var httpFetch = async (input, init = {}) => {
|
|
|
583
592
|
credentials: init.credentials ?? "include"
|
|
584
593
|
});
|
|
585
594
|
};
|
|
586
|
-
if (!STATE_CHANGING.has(method) ||
|
|
595
|
+
if (!STATE_CHANGING.has(method) || config.session.basedToken) {
|
|
587
596
|
return send(null);
|
|
588
597
|
}
|
|
589
|
-
const
|
|
598
|
+
const requestOrigin = resolveRequestOrigin(input);
|
|
599
|
+
const token = await getCsrfToken(requestOrigin);
|
|
590
600
|
let response = await send(token);
|
|
591
601
|
if (await isCsrfMismatchResponse(response)) {
|
|
592
602
|
clearCsrfToken();
|
|
593
|
-
const refreshed = await getCsrfToken();
|
|
603
|
+
const refreshed = await getCsrfToken(requestOrigin);
|
|
594
604
|
if (refreshed) {
|
|
595
605
|
response = await send(refreshed);
|
|
596
606
|
}
|
|
@@ -598,6 +608,11 @@ var httpFetch = async (input, init = {}) => {
|
|
|
598
608
|
return response;
|
|
599
609
|
};
|
|
600
610
|
|
|
611
|
+
// src/sleep.ts
|
|
612
|
+
function sleep(ms) {
|
|
613
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
614
|
+
}
|
|
615
|
+
|
|
601
616
|
// src/pageRouteValidation.ts
|
|
602
617
|
var DEFAULT_PAGE_ROUTE_RULES = {
|
|
603
618
|
privateFolderPrefix: "_",
|
|
@@ -687,7 +702,6 @@ export {
|
|
|
687
702
|
buildJoinRoomResponseEventName,
|
|
688
703
|
buildLeaveRoomResponseEventName,
|
|
689
704
|
buildGetJoinedRoomsResponseEventName,
|
|
690
|
-
sleep,
|
|
691
705
|
DEFAULT_CSRF_CONFIG,
|
|
692
706
|
registerCsrfConfig,
|
|
693
707
|
getCsrfConfig,
|
|
@@ -696,7 +710,8 @@ export {
|
|
|
696
710
|
getCsrfToken,
|
|
697
711
|
clearCsrfToken,
|
|
698
712
|
httpFetch,
|
|
713
|
+
sleep,
|
|
699
714
|
DEFAULT_PAGE_ROUTE_RULES,
|
|
700
715
|
validatePagePath
|
|
701
716
|
};
|
|
702
|
-
//# sourceMappingURL=chunk-
|
|
717
|
+
//# sourceMappingURL=chunk-6K7MAXMT.js.map
|