@lunora/runtime 1.0.0-alpha.13 → 1.0.0-alpha.15
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/dist/index.d.mts +116 -16
- package/dist/index.d.ts +116 -16
- package/dist/index.mjs +2 -2
- package/dist/packem_shared/{composeWorker-BOB2YZ6v.mjs → composeWorker-BUf56-tZ.mjs} +196 -43
- package/dist/packem_shared/{decorateResponse-DbISh_Wi.mjs → decorateResponse-HRXo-oOD.mjs} +40 -8
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -446,6 +446,72 @@ interface IdentityContractLike {
|
|
|
446
446
|
* the wrapped resolver; the admin path keeps the raw one (admin is gated by the
|
|
447
447
|
* bearer / Access, not the app's identity contract).
|
|
448
448
|
*/
|
|
449
|
+
/** One KV namespace as the studio's KV browser surfaces it. */
|
|
450
|
+
interface KvNamespaceSummary {
|
|
451
|
+
/** The wrangler/env binding name, e.g. `"MY_KV"`. */
|
|
452
|
+
binding: string;
|
|
453
|
+
}
|
|
454
|
+
/** One key entry as the KV admin browser surfaces it. */
|
|
455
|
+
interface KvKeyEntry {
|
|
456
|
+
/** Absolute expiration (Unix seconds), when set. */
|
|
457
|
+
expiration?: number;
|
|
458
|
+
/** Per-key metadata set at write time, or absent when none. */
|
|
459
|
+
metadata?: unknown;
|
|
460
|
+
/** The key name. */
|
|
461
|
+
name: string;
|
|
462
|
+
}
|
|
463
|
+
/** A paginated page of KV keys as the admin browser returns it. */
|
|
464
|
+
interface KvKeyListResult {
|
|
465
|
+
/** Opaque cursor for the next page; absent when the listing is complete. */
|
|
466
|
+
cursor?: string;
|
|
467
|
+
/** The keys on this page. */
|
|
468
|
+
keys: KvKeyEntry[];
|
|
469
|
+
/** True when this is the final page. */
|
|
470
|
+
listComplete: boolean;
|
|
471
|
+
}
|
|
472
|
+
/** A KV value together with its stored metadata. */
|
|
473
|
+
interface KvValueResult {
|
|
474
|
+
/** Per-key metadata, or `null` when none. */
|
|
475
|
+
metadata: unknown;
|
|
476
|
+
/** The stored value as a string, or `null` when the key is absent. */
|
|
477
|
+
value: null | string;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The introspector the worker wires for the studio's KV browser. Build it from
|
|
481
|
+
* the env's bound KV namespaces. Omit it and the `/_lunora/admin/kv/*`
|
|
482
|
+
* endpoints respond `KV_NOT_CONFIGURED`.
|
|
483
|
+
*/
|
|
484
|
+
interface KvIntrospector {
|
|
485
|
+
/** Delete a key from a namespace. No-op when the key is absent. */
|
|
486
|
+
deleteKey: (options: {
|
|
487
|
+
key: string;
|
|
488
|
+
namespace: string;
|
|
489
|
+
}) => Promise<void>;
|
|
490
|
+
/** Read a value (as text) and its metadata from a namespace key. */
|
|
491
|
+
getValue: (options: {
|
|
492
|
+
key: string;
|
|
493
|
+
namespace: string;
|
|
494
|
+
}) => Promise<KvValueResult>;
|
|
495
|
+
/** List keys in a namespace, optionally filtered by prefix and paginated. */
|
|
496
|
+
listKeys: (options: {
|
|
497
|
+
cursor?: string;
|
|
498
|
+
limit?: number;
|
|
499
|
+
namespace: string;
|
|
500
|
+
prefix?: string;
|
|
501
|
+
}) => Promise<KvKeyListResult>;
|
|
502
|
+
/** List the registered KV namespaces (binding names). */
|
|
503
|
+
listNamespaces: () => Promise<KvNamespaceSummary[]>;
|
|
504
|
+
/** Write a value (as text) with optional absolute expiration / relative TTL and metadata. */
|
|
505
|
+
putValue: (options: {
|
|
506
|
+
expiration?: number;
|
|
507
|
+
expirationTtl?: number;
|
|
508
|
+
key: string;
|
|
509
|
+
metadata?: unknown;
|
|
510
|
+
namespace: string;
|
|
511
|
+
value: string;
|
|
512
|
+
}) => Promise<void>;
|
|
513
|
+
}
|
|
514
|
+
/** The worker internals the KV routes reach through injection rather than closure. */
|
|
449
515
|
/**
|
|
450
516
|
* Observability hooks for the Lunora runtime.
|
|
451
517
|
*
|
|
@@ -1150,9 +1216,13 @@ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => Quer
|
|
|
1150
1216
|
interface SecurityHeadersOptions {
|
|
1151
1217
|
/**
|
|
1152
1218
|
* `Content-Security-Policy`. Omitted (`undefined`) applies a restrictive
|
|
1153
|
-
* default to **non-HTML** responses
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1219
|
+
* `default-src 'none'` policy to **non-HTML** responses, and a conservative
|
|
1220
|
+
* hardening policy to **HTML** responses (`base-uri 'none'; frame-ancestors
|
|
1221
|
+
* 'self'; object-src 'none'`) — this does NOT set `default-src`/`script-src`,
|
|
1222
|
+
* so it never blocks an SSR page's own scripts/styles/images/fetches, it only
|
|
1223
|
+
* locks down the `base` element, framing (mirroring the `SAMEORIGIN`
|
|
1224
|
+
* X-Frame-Options default), and legacy plugins. Pass a string to apply that
|
|
1225
|
+
* exact policy to every response (HTML included); `false` to never send one.
|
|
1156
1226
|
*/
|
|
1157
1227
|
csp?: string | false;
|
|
1158
1228
|
/** `X-Frame-Options` (clickjacking). Defaults to `SAMEORIGIN`. `false` omits it. */
|
|
@@ -1201,7 +1271,7 @@ interface SecurityOptions {
|
|
|
1201
1271
|
interface ResolvedHeaders {
|
|
1202
1272
|
coop: string | undefined;
|
|
1203
1273
|
csp: {
|
|
1204
|
-
|
|
1274
|
+
htmlValue: string | undefined;
|
|
1205
1275
|
value: string;
|
|
1206
1276
|
} | undefined;
|
|
1207
1277
|
enabled: boolean;
|
|
@@ -1264,6 +1334,24 @@ declare const resolveSecurity: (security: SecurityOptions | undefined, env?: Rec
|
|
|
1264
1334
|
* @returns a `403` Response when the origin is untrusted, or `undefined` when the request is allowed.
|
|
1265
1335
|
*/
|
|
1266
1336
|
declare const enforceOrigin: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
|
|
1337
|
+
/**
|
|
1338
|
+
* CSRF defense for the WebSocket upgrade (Cross-Site WebSocket Hijacking).
|
|
1339
|
+
*
|
|
1340
|
+
* A WS handshake is an HTTP `GET`, so {@link enforceOrigin}'s safe-method
|
|
1341
|
+
* exemption never fires for it — yet the browser auto-attaches the session
|
|
1342
|
+
* cookie to the handshake and WebSocket connections are NOT governed by
|
|
1343
|
+
* CORS/SOP. Without an explicit `Origin` check any page can open
|
|
1344
|
+
* `wss://app/_lunora/ws`, authenticate as the logged-in victim, and read the
|
|
1345
|
+
* victim's live queries + issue mutations as them. This guard closes that hole.
|
|
1346
|
+
*
|
|
1347
|
+
* Scoped to cookie-bearing upgrades — the only vector CSWSH can ride (a browser
|
|
1348
|
+
* attaches cookies but never a bearer token). Bearer/token/server-to-server
|
|
1349
|
+
* upgrades (no `Cookie`) are exempt. A browser ALWAYS sends `Origin` on a WS
|
|
1350
|
+
* handshake, so a missing/untrusted `Origin` on a cookie-bearing upgrade fails
|
|
1351
|
+
* closed (mirrors {@link enforceOrigin}).
|
|
1352
|
+
* @returns a `403` Response when the upgrade origin is untrusted, or `undefined` when it is allowed.
|
|
1353
|
+
*/
|
|
1354
|
+
|
|
1267
1355
|
/**
|
|
1268
1356
|
* Answer a CORS preflight (`OPTIONS` carrying `Access-Control-Request-Method`)
|
|
1269
1357
|
* for an allowlisted origin with a `204`. Returns `undefined` for non-preflight
|
|
@@ -1718,17 +1806,21 @@ interface WorkerOptions {
|
|
|
1718
1806
|
*/
|
|
1719
1807
|
adminToken?: string;
|
|
1720
1808
|
/**
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
* `
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* configured.
|
|
1809
|
+
* Opt into an authorization-open posture for sharded and fan-out access.
|
|
1810
|
+
*
|
|
1811
|
+
* By default (this flag unset/`false`) the runtime FAILS CLOSED: when
|
|
1812
|
+
* neither {@link WorkerOptions.authorizeShard} nor {@link WorkerOptions.authorizeFanOut}
|
|
1813
|
+
* is configured, naming a non-default shard (a potential cross-tenant hop)
|
|
1814
|
+
* or sending a fan-out envelope is rejected with a `403`
|
|
1815
|
+
* (`FORBIDDEN_SHARD`/`FORBIDDEN_FANOUT`). Set this to `true` to allow such
|
|
1816
|
+
* requests from any caller (including unauthenticated ones) — appropriate
|
|
1817
|
+
* only when every table is protected by per-row RLS. The runtime then emits
|
|
1818
|
+
* a single `console.warn` so the open posture stays visible in logs. Has no
|
|
1819
|
+
* effect once an `authorize*` callback is configured (those gate directly).
|
|
1820
|
+
*
|
|
1821
|
+
* NOTE: this is a behaviour change from earlier alphas, where the same
|
|
1822
|
+
* situation was warn-once-then-allow. Apps that relied on client-chosen
|
|
1823
|
+
* shard keys without an `authorize*` callback must set this flag explicitly.
|
|
1732
1824
|
*/
|
|
1733
1825
|
allowUnauthenticatedShardAccess?: boolean;
|
|
1734
1826
|
/**
|
|
@@ -1932,6 +2024,14 @@ interface WorkerOptions {
|
|
|
1932
2024
|
*/
|
|
1933
2025
|
jurisdiction?: DurableObjectJurisdiction;
|
|
1934
2026
|
/**
|
|
2027
|
+
* Introspector for Workers KV namespaces, backing the studio's KV browser
|
|
2028
|
+
* via `GET /_lunora/admin/kv/namespaces`, `GET /_lunora/admin/kv/keys`,
|
|
2029
|
+
* `GET|PUT|DELETE /_lunora/admin/kv/value`. Build it from the env's bound
|
|
2030
|
+
* KV namespaces with `createKvIntrospector` from `@lunora/bindings/kv`.
|
|
2031
|
+
* Omit it and those endpoints respond `KV_NOT_CONFIGURED`.
|
|
2032
|
+
*/
|
|
2033
|
+
kvIntrospector?: KvIntrospector;
|
|
2034
|
+
/**
|
|
1935
2035
|
* Optional telemetry sink. When supplied, the worker emits one
|
|
1936
2036
|
* `onRpc` event per dispatched RPC (single-shard forward or fan-out)
|
|
1937
2037
|
* with duration / ok / error / shardKey or fanOut metadata. Sink
|
|
@@ -2515,4 +2615,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
|
|
|
2515
2615
|
*/
|
|
2516
2616
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2517
2617
|
declare const VERSION: string;
|
|
2518
|
-
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthImpersonation, type AuthIntrospector, type AuthPage, type AuthSession, type AuthUser, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
|
2618
|
+
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthImpersonation, type AuthIntrospector, type AuthPage, type AuthSession, type AuthUser, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
package/dist/index.d.ts
CHANGED
|
@@ -446,6 +446,72 @@ interface IdentityContractLike {
|
|
|
446
446
|
* the wrapped resolver; the admin path keeps the raw one (admin is gated by the
|
|
447
447
|
* bearer / Access, not the app's identity contract).
|
|
448
448
|
*/
|
|
449
|
+
/** One KV namespace as the studio's KV browser surfaces it. */
|
|
450
|
+
interface KvNamespaceSummary {
|
|
451
|
+
/** The wrangler/env binding name, e.g. `"MY_KV"`. */
|
|
452
|
+
binding: string;
|
|
453
|
+
}
|
|
454
|
+
/** One key entry as the KV admin browser surfaces it. */
|
|
455
|
+
interface KvKeyEntry {
|
|
456
|
+
/** Absolute expiration (Unix seconds), when set. */
|
|
457
|
+
expiration?: number;
|
|
458
|
+
/** Per-key metadata set at write time, or absent when none. */
|
|
459
|
+
metadata?: unknown;
|
|
460
|
+
/** The key name. */
|
|
461
|
+
name: string;
|
|
462
|
+
}
|
|
463
|
+
/** A paginated page of KV keys as the admin browser returns it. */
|
|
464
|
+
interface KvKeyListResult {
|
|
465
|
+
/** Opaque cursor for the next page; absent when the listing is complete. */
|
|
466
|
+
cursor?: string;
|
|
467
|
+
/** The keys on this page. */
|
|
468
|
+
keys: KvKeyEntry[];
|
|
469
|
+
/** True when this is the final page. */
|
|
470
|
+
listComplete: boolean;
|
|
471
|
+
}
|
|
472
|
+
/** A KV value together with its stored metadata. */
|
|
473
|
+
interface KvValueResult {
|
|
474
|
+
/** Per-key metadata, or `null` when none. */
|
|
475
|
+
metadata: unknown;
|
|
476
|
+
/** The stored value as a string, or `null` when the key is absent. */
|
|
477
|
+
value: null | string;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The introspector the worker wires for the studio's KV browser. Build it from
|
|
481
|
+
* the env's bound KV namespaces. Omit it and the `/_lunora/admin/kv/*`
|
|
482
|
+
* endpoints respond `KV_NOT_CONFIGURED`.
|
|
483
|
+
*/
|
|
484
|
+
interface KvIntrospector {
|
|
485
|
+
/** Delete a key from a namespace. No-op when the key is absent. */
|
|
486
|
+
deleteKey: (options: {
|
|
487
|
+
key: string;
|
|
488
|
+
namespace: string;
|
|
489
|
+
}) => Promise<void>;
|
|
490
|
+
/** Read a value (as text) and its metadata from a namespace key. */
|
|
491
|
+
getValue: (options: {
|
|
492
|
+
key: string;
|
|
493
|
+
namespace: string;
|
|
494
|
+
}) => Promise<KvValueResult>;
|
|
495
|
+
/** List keys in a namespace, optionally filtered by prefix and paginated. */
|
|
496
|
+
listKeys: (options: {
|
|
497
|
+
cursor?: string;
|
|
498
|
+
limit?: number;
|
|
499
|
+
namespace: string;
|
|
500
|
+
prefix?: string;
|
|
501
|
+
}) => Promise<KvKeyListResult>;
|
|
502
|
+
/** List the registered KV namespaces (binding names). */
|
|
503
|
+
listNamespaces: () => Promise<KvNamespaceSummary[]>;
|
|
504
|
+
/** Write a value (as text) with optional absolute expiration / relative TTL and metadata. */
|
|
505
|
+
putValue: (options: {
|
|
506
|
+
expiration?: number;
|
|
507
|
+
expirationTtl?: number;
|
|
508
|
+
key: string;
|
|
509
|
+
metadata?: unknown;
|
|
510
|
+
namespace: string;
|
|
511
|
+
value: string;
|
|
512
|
+
}) => Promise<void>;
|
|
513
|
+
}
|
|
514
|
+
/** The worker internals the KV routes reach through injection rather than closure. */
|
|
449
515
|
/**
|
|
450
516
|
* Observability hooks for the Lunora runtime.
|
|
451
517
|
*
|
|
@@ -1150,9 +1216,13 @@ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => Quer
|
|
|
1150
1216
|
interface SecurityHeadersOptions {
|
|
1151
1217
|
/**
|
|
1152
1218
|
* `Content-Security-Policy`. Omitted (`undefined`) applies a restrictive
|
|
1153
|
-
* default to **non-HTML** responses
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1219
|
+
* `default-src 'none'` policy to **non-HTML** responses, and a conservative
|
|
1220
|
+
* hardening policy to **HTML** responses (`base-uri 'none'; frame-ancestors
|
|
1221
|
+
* 'self'; object-src 'none'`) — this does NOT set `default-src`/`script-src`,
|
|
1222
|
+
* so it never blocks an SSR page's own scripts/styles/images/fetches, it only
|
|
1223
|
+
* locks down the `base` element, framing (mirroring the `SAMEORIGIN`
|
|
1224
|
+
* X-Frame-Options default), and legacy plugins. Pass a string to apply that
|
|
1225
|
+
* exact policy to every response (HTML included); `false` to never send one.
|
|
1156
1226
|
*/
|
|
1157
1227
|
csp?: string | false;
|
|
1158
1228
|
/** `X-Frame-Options` (clickjacking). Defaults to `SAMEORIGIN`. `false` omits it. */
|
|
@@ -1201,7 +1271,7 @@ interface SecurityOptions {
|
|
|
1201
1271
|
interface ResolvedHeaders {
|
|
1202
1272
|
coop: string | undefined;
|
|
1203
1273
|
csp: {
|
|
1204
|
-
|
|
1274
|
+
htmlValue: string | undefined;
|
|
1205
1275
|
value: string;
|
|
1206
1276
|
} | undefined;
|
|
1207
1277
|
enabled: boolean;
|
|
@@ -1264,6 +1334,24 @@ declare const resolveSecurity: (security: SecurityOptions | undefined, env?: Rec
|
|
|
1264
1334
|
* @returns a `403` Response when the origin is untrusted, or `undefined` when the request is allowed.
|
|
1265
1335
|
*/
|
|
1266
1336
|
declare const enforceOrigin: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
|
|
1337
|
+
/**
|
|
1338
|
+
* CSRF defense for the WebSocket upgrade (Cross-Site WebSocket Hijacking).
|
|
1339
|
+
*
|
|
1340
|
+
* A WS handshake is an HTTP `GET`, so {@link enforceOrigin}'s safe-method
|
|
1341
|
+
* exemption never fires for it — yet the browser auto-attaches the session
|
|
1342
|
+
* cookie to the handshake and WebSocket connections are NOT governed by
|
|
1343
|
+
* CORS/SOP. Without an explicit `Origin` check any page can open
|
|
1344
|
+
* `wss://app/_lunora/ws`, authenticate as the logged-in victim, and read the
|
|
1345
|
+
* victim's live queries + issue mutations as them. This guard closes that hole.
|
|
1346
|
+
*
|
|
1347
|
+
* Scoped to cookie-bearing upgrades — the only vector CSWSH can ride (a browser
|
|
1348
|
+
* attaches cookies but never a bearer token). Bearer/token/server-to-server
|
|
1349
|
+
* upgrades (no `Cookie`) are exempt. A browser ALWAYS sends `Origin` on a WS
|
|
1350
|
+
* handshake, so a missing/untrusted `Origin` on a cookie-bearing upgrade fails
|
|
1351
|
+
* closed (mirrors {@link enforceOrigin}).
|
|
1352
|
+
* @returns a `403` Response when the upgrade origin is untrusted, or `undefined` when it is allowed.
|
|
1353
|
+
*/
|
|
1354
|
+
|
|
1267
1355
|
/**
|
|
1268
1356
|
* Answer a CORS preflight (`OPTIONS` carrying `Access-Control-Request-Method`)
|
|
1269
1357
|
* for an allowlisted origin with a `204`. Returns `undefined` for non-preflight
|
|
@@ -1718,17 +1806,21 @@ interface WorkerOptions {
|
|
|
1718
1806
|
*/
|
|
1719
1807
|
adminToken?: string;
|
|
1720
1808
|
/**
|
|
1721
|
-
*
|
|
1722
|
-
*
|
|
1723
|
-
*
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
* `
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
* configured.
|
|
1809
|
+
* Opt into an authorization-open posture for sharded and fan-out access.
|
|
1810
|
+
*
|
|
1811
|
+
* By default (this flag unset/`false`) the runtime FAILS CLOSED: when
|
|
1812
|
+
* neither {@link WorkerOptions.authorizeShard} nor {@link WorkerOptions.authorizeFanOut}
|
|
1813
|
+
* is configured, naming a non-default shard (a potential cross-tenant hop)
|
|
1814
|
+
* or sending a fan-out envelope is rejected with a `403`
|
|
1815
|
+
* (`FORBIDDEN_SHARD`/`FORBIDDEN_FANOUT`). Set this to `true` to allow such
|
|
1816
|
+
* requests from any caller (including unauthenticated ones) — appropriate
|
|
1817
|
+
* only when every table is protected by per-row RLS. The runtime then emits
|
|
1818
|
+
* a single `console.warn` so the open posture stays visible in logs. Has no
|
|
1819
|
+
* effect once an `authorize*` callback is configured (those gate directly).
|
|
1820
|
+
*
|
|
1821
|
+
* NOTE: this is a behaviour change from earlier alphas, where the same
|
|
1822
|
+
* situation was warn-once-then-allow. Apps that relied on client-chosen
|
|
1823
|
+
* shard keys without an `authorize*` callback must set this flag explicitly.
|
|
1732
1824
|
*/
|
|
1733
1825
|
allowUnauthenticatedShardAccess?: boolean;
|
|
1734
1826
|
/**
|
|
@@ -1932,6 +2024,14 @@ interface WorkerOptions {
|
|
|
1932
2024
|
*/
|
|
1933
2025
|
jurisdiction?: DurableObjectJurisdiction;
|
|
1934
2026
|
/**
|
|
2027
|
+
* Introspector for Workers KV namespaces, backing the studio's KV browser
|
|
2028
|
+
* via `GET /_lunora/admin/kv/namespaces`, `GET /_lunora/admin/kv/keys`,
|
|
2029
|
+
* `GET|PUT|DELETE /_lunora/admin/kv/value`. Build it from the env's bound
|
|
2030
|
+
* KV namespaces with `createKvIntrospector` from `@lunora/bindings/kv`.
|
|
2031
|
+
* Omit it and those endpoints respond `KV_NOT_CONFIGURED`.
|
|
2032
|
+
*/
|
|
2033
|
+
kvIntrospector?: KvIntrospector;
|
|
2034
|
+
/**
|
|
1935
2035
|
* Optional telemetry sink. When supplied, the worker emits one
|
|
1936
2036
|
* `onRpc` event per dispatched RPC (single-shard forward or fan-out)
|
|
1937
2037
|
* with duration / ok / error / shardKey or fanOut metadata. Sink
|
|
@@ -2515,4 +2615,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
|
|
|
2515
2615
|
*/
|
|
2516
2616
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2517
2617
|
declare const VERSION: string;
|
|
2518
|
-
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthImpersonation, type AuthIntrospector, type AuthPage, type AuthSession, type AuthUser, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
|
2618
|
+
export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthImpersonation, type AuthIntrospector, type AuthPage, type AuthSession, type AuthUser, type BackupManifest, type BackupStore, type ComposeIdentityResolversErrorMode, type ComposeIdentityResolversOptions, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type IdentityContractLike, type IdentityResolver, type IdentityValidation, type ImportFanOutRequest, type ImportFanOutResult, type KvIntrospector, type KvKeyEntry, type KvKeyListResult, type KvNamespaceSummary, type KvValueResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeIdentityResolvers, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, routeIdentityResolvers, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { toAirbyteMessages, toFivetranResponse } from './packem_shared/toAirbyteMessages-DrHdplb4.mjs';
|
|
2
|
-
export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-
|
|
2
|
+
export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-BUf56-tZ.mjs';
|
|
3
3
|
export { createCrossShardRelationCapabilities } from './packem_shared/createCrossShardRelationCapabilities-C0KOf7er.mjs';
|
|
4
4
|
export { DEFAULT_REGISTRY_CACHE_TTL_MS, SHARD_REGISTRY_DO_NAME, createDynamicShardRegistry } from './packem_shared/DEFAULT_REGISTRY_CACHE_TTL_MS-ocax8v0n.mjs';
|
|
5
5
|
export { LunoraError, toErrorResponse } from './packem_shared/LunoraError-CL0aOtpo.mjs';
|
|
@@ -7,7 +7,7 @@ export { emitLogEvent, emitRpcEvent } from './packem_shared/emitLogEvent-pEdtqAK
|
|
|
7
7
|
export { analyticsEngineSink, combineSinks, consoleSink, sentrySink, webhookSink } from './packem_shared/analyticsEngineSink-DqEvrQs0.mjs';
|
|
8
8
|
export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggregate } from './packem_shared/createQueryCoordinator-ZeZYUPNu.mjs';
|
|
9
9
|
export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
|
|
10
|
-
export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-
|
|
10
|
+
export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-HRXo-oOD.mjs';
|
|
11
11
|
export { NOOP_EXECUTION_CONTEXT } from './packem_shared/NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
|
|
12
12
|
export { composeIdentityResolvers, routeIdentityResolvers } from './packem_shared/composeIdentityResolvers-YjvUKisc.mjs';
|
|
13
13
|
|
|
@@ -4,7 +4,7 @@ import { wrapResolverWithContract } from './composeIdentityResolvers-YjvUKisc.mj
|
|
|
4
4
|
export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-YjvUKisc.mjs';
|
|
5
5
|
import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
|
|
6
6
|
import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
|
|
7
|
-
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-
|
|
7
|
+
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-HRXo-oOD.mjs';
|
|
8
8
|
|
|
9
9
|
const RELAY_NAME_INFIX = "::relay::";
|
|
10
10
|
const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
|
|
@@ -250,15 +250,15 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
250
250
|
}
|
|
251
251
|
const candidate = error;
|
|
252
252
|
const code = typeof candidate.code === "string" ? candidate.code : "AUTH_ADMIN_ERROR";
|
|
253
|
-
|
|
254
|
-
throw new LunoraError(
|
|
253
|
+
console.error("[lunora] auth admin operation failed:", error);
|
|
254
|
+
throw new LunoraError("auth admin operation failed", { code, status: AUTH_ADMIN_ERROR_STATUS[code] ?? 400 });
|
|
255
255
|
}
|
|
256
256
|
};
|
|
257
257
|
const handle = async (request, descriptor) => {
|
|
258
|
+
deps.assertAdmin(request);
|
|
258
259
|
if (request.method !== descriptor.http) {
|
|
259
260
|
throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
260
261
|
}
|
|
261
|
-
deps.assertAdmin(request);
|
|
262
262
|
const admin = deps.getAuthAdmin();
|
|
263
263
|
if (admin === void 0) {
|
|
264
264
|
throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
|
|
@@ -380,9 +380,9 @@ const readBodyBytesWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
|
380
380
|
}
|
|
381
381
|
return out.buffer;
|
|
382
382
|
};
|
|
383
|
-
const readJsonBodyWithLimit = async (request) => {
|
|
383
|
+
const readJsonBodyWithLimit = async (request, limit = MAX_BODY_BYTES) => {
|
|
384
384
|
try {
|
|
385
|
-
const text = await readBodyTextWithLimit(request);
|
|
385
|
+
const text = await readBodyTextWithLimit(request, limit);
|
|
386
386
|
return text === "" ? {} : JSON.parse(text);
|
|
387
387
|
} catch (error) {
|
|
388
388
|
if (error instanceof LunoraError) {
|
|
@@ -1011,6 +1011,128 @@ const buildIntrospectionAdminRoutes = (deps) => {
|
|
|
1011
1011
|
};
|
|
1012
1012
|
};
|
|
1013
1013
|
|
|
1014
|
+
const KV_NAMESPACES_PATH = "/_lunora/admin/kv/namespaces";
|
|
1015
|
+
const KV_KEYS_PATH = "/_lunora/admin/kv/keys";
|
|
1016
|
+
const KV_VALUE_PATH = "/_lunora/admin/kv/value";
|
|
1017
|
+
const KV_VALUE_MAX_BODY_BYTES = 32 * 1048576;
|
|
1018
|
+
const KV_MIN_EXPIRATION_SECONDS = 60;
|
|
1019
|
+
const buildKvAdminRoutes = (deps) => {
|
|
1020
|
+
const { readJsonBody, requireAdminOption } = deps;
|
|
1021
|
+
const gate = (request) => requireAdminOption(request, deps.kvIntrospector, {
|
|
1022
|
+
code: "KV_NOT_CONFIGURED",
|
|
1023
|
+
message: "KV endpoints require a `kvIntrospector` on the worker"
|
|
1024
|
+
});
|
|
1025
|
+
const ok = (payload) => Response.json(payload, { headers: { "content-type": "application/json" }, status: 200 });
|
|
1026
|
+
const requireNamespaceAndKey = (request, verb) => {
|
|
1027
|
+
const url = new URL(request.url);
|
|
1028
|
+
const namespace = url.searchParams.get("namespace") ?? "";
|
|
1029
|
+
const key = url.searchParams.get("key") ?? "";
|
|
1030
|
+
if (namespace === "") {
|
|
1031
|
+
throw new LunoraError(`KV-value ${verb} request requires a \`namespace\` query parameter`, { code: "BAD_REQUEST", status: 400 });
|
|
1032
|
+
}
|
|
1033
|
+
if (key === "") {
|
|
1034
|
+
throw new LunoraError(`KV-value ${verb} request requires a \`key\` query parameter`, { code: "BAD_REQUEST", status: 400 });
|
|
1035
|
+
}
|
|
1036
|
+
return { key, namespace };
|
|
1037
|
+
};
|
|
1038
|
+
const requireKnownNamespace = async (introspector, namespace) => {
|
|
1039
|
+
const namespaces = await introspector.listNamespaces();
|
|
1040
|
+
if (!namespaces.some((entry) => entry.binding === namespace)) {
|
|
1041
|
+
throw new LunoraError(`Unknown KV namespace binding \`${namespace}\``, { code: "NOT_FOUND", status: 404 });
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
const handleKvNamespaces = async (request) => {
|
|
1045
|
+
if (request.method !== "GET") {
|
|
1046
|
+
throw new LunoraError("KV-namespaces endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1047
|
+
}
|
|
1048
|
+
return ok({ namespaces: await gate(request).listNamespaces() });
|
|
1049
|
+
};
|
|
1050
|
+
const handleKvKeys = async (request) => {
|
|
1051
|
+
if (request.method !== "GET") {
|
|
1052
|
+
throw new LunoraError("KV-keys endpoint requires GET", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1053
|
+
}
|
|
1054
|
+
const introspector = gate(request);
|
|
1055
|
+
const url = new URL(request.url);
|
|
1056
|
+
const namespace = url.searchParams.get("namespace") ?? "";
|
|
1057
|
+
if (namespace === "") {
|
|
1058
|
+
throw new LunoraError("KV-keys request requires a `namespace` query parameter", { code: "BAD_REQUEST", status: 400 });
|
|
1059
|
+
}
|
|
1060
|
+
const prefix = url.searchParams.get("prefix") ?? void 0;
|
|
1061
|
+
const cursor = url.searchParams.get("cursor") ?? void 0;
|
|
1062
|
+
const limitRaw = url.searchParams.get("limit");
|
|
1063
|
+
const parsedLimit = limitRaw === null ? void 0 : Number.parseInt(limitRaw, 10);
|
|
1064
|
+
if (parsedLimit !== void 0 && (!Number.isInteger(parsedLimit) || parsedLimit < 1)) {
|
|
1065
|
+
throw new LunoraError("KV-keys `limit` must be a positive integer", { code: "BAD_REQUEST", status: 400 });
|
|
1066
|
+
}
|
|
1067
|
+
const limit = parsedLimit === void 0 ? void 0 : Math.min(parsedLimit, 1e3);
|
|
1068
|
+
await requireKnownNamespace(introspector, namespace);
|
|
1069
|
+
return ok(await introspector.listKeys({ cursor, limit, namespace, prefix }));
|
|
1070
|
+
};
|
|
1071
|
+
const handleKvValueGet = async (request) => {
|
|
1072
|
+
const introspector = gate(request);
|
|
1073
|
+
const params = requireNamespaceAndKey(request, "GET");
|
|
1074
|
+
await requireKnownNamespace(introspector, params.namespace);
|
|
1075
|
+
return ok(await introspector.getValue(params));
|
|
1076
|
+
};
|
|
1077
|
+
const handleKvValuePut = async (request) => {
|
|
1078
|
+
const introspector = gate(request);
|
|
1079
|
+
const candidate = await readJsonBody(request, KV_VALUE_MAX_BODY_BYTES);
|
|
1080
|
+
if (typeof candidate.namespace !== "string" || candidate.namespace === "") {
|
|
1081
|
+
throw new LunoraError("KV-value PUT request requires a `namespace` string", { code: "BAD_REQUEST", status: 400 });
|
|
1082
|
+
}
|
|
1083
|
+
if (typeof candidate.key !== "string" || candidate.key === "") {
|
|
1084
|
+
throw new LunoraError("KV-value PUT request requires a `key` string", { code: "BAD_REQUEST", status: 400 });
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof candidate.value !== "string") {
|
|
1087
|
+
throw new LunoraError("KV-value PUT request requires a `value` string", { code: "BAD_REQUEST", status: 400 });
|
|
1088
|
+
}
|
|
1089
|
+
if (candidate.expirationTtl !== void 0 && (typeof candidate.expirationTtl !== "number" || !Number.isInteger(candidate.expirationTtl) || candidate.expirationTtl < KV_MIN_EXPIRATION_SECONDS)) {
|
|
1090
|
+
throw new LunoraError("KV-value PUT `expirationTtl` must be an integer ≥ 60", { code: "BAD_REQUEST", status: 400 });
|
|
1091
|
+
}
|
|
1092
|
+
const minExpiration = Math.floor(Date.now() / 1e3) + KV_MIN_EXPIRATION_SECONDS;
|
|
1093
|
+
if (candidate.expiration !== void 0 && (typeof candidate.expiration !== "number" || !Number.isInteger(candidate.expiration) || candidate.expiration < minExpiration)) {
|
|
1094
|
+
throw new LunoraError("KV-value PUT `expiration` must be a Unix-seconds timestamp at least 60 seconds in the future", {
|
|
1095
|
+
code: "BAD_REQUEST",
|
|
1096
|
+
status: 400
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
await requireKnownNamespace(introspector, candidate.namespace);
|
|
1100
|
+
await introspector.putValue({
|
|
1101
|
+
expiration: candidate.expiration,
|
|
1102
|
+
expirationTtl: candidate.expirationTtl,
|
|
1103
|
+
key: candidate.key,
|
|
1104
|
+
metadata: candidate.metadata,
|
|
1105
|
+
namespace: candidate.namespace,
|
|
1106
|
+
value: candidate.value
|
|
1107
|
+
});
|
|
1108
|
+
return ok({ ok: true });
|
|
1109
|
+
};
|
|
1110
|
+
const handleKvValueDelete = async (request) => {
|
|
1111
|
+
const introspector = gate(request);
|
|
1112
|
+
const params = requireNamespaceAndKey(request, "DELETE");
|
|
1113
|
+
await requireKnownNamespace(introspector, params.namespace);
|
|
1114
|
+
await introspector.deleteKey(params);
|
|
1115
|
+
return ok({ deleted: true });
|
|
1116
|
+
};
|
|
1117
|
+
const kvValueHandlers = {
|
|
1118
|
+
DELETE: handleKvValueDelete,
|
|
1119
|
+
GET: handleKvValueGet,
|
|
1120
|
+
PUT: handleKvValuePut
|
|
1121
|
+
};
|
|
1122
|
+
const handleKvValue = (request) => {
|
|
1123
|
+
const handler = kvValueHandlers[request.method];
|
|
1124
|
+
if (!handler) {
|
|
1125
|
+
throw new LunoraError("KV-value endpoint requires GET, PUT, or DELETE", { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
1126
|
+
}
|
|
1127
|
+
return handler(request);
|
|
1128
|
+
};
|
|
1129
|
+
return {
|
|
1130
|
+
[KV_NAMESPACES_PATH]: handleKvNamespaces,
|
|
1131
|
+
[KV_KEYS_PATH]: handleKvKeys,
|
|
1132
|
+
[KV_VALUE_PATH]: handleKvValue
|
|
1133
|
+
};
|
|
1134
|
+
};
|
|
1135
|
+
|
|
1014
1136
|
const MIGRATE_PATH$1 = "/_lunora/migrate";
|
|
1015
1137
|
const PITR_PATH = "/_lunora/admin/pitr";
|
|
1016
1138
|
const RANK_PATH = "/_lunora/admin/rank";
|
|
@@ -1744,9 +1866,18 @@ const parseEnvelope = async (request) => {
|
|
|
1744
1866
|
throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
|
|
1745
1867
|
}
|
|
1746
1868
|
const envelope = body;
|
|
1869
|
+
const fanOut = validateFanOut(envelope.fanOut);
|
|
1870
|
+
const args = envelope.args ?? {};
|
|
1871
|
+
if (fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
1872
|
+
const requestedTable = args.table;
|
|
1873
|
+
if (typeof requestedTable === "string" && requestedTable !== fanOut.table) {
|
|
1874
|
+
throw new LunoraError("RPC `args.table` must match the authorized `fanOut.table` for a relation fan-out", { code: "BAD_REQUEST", status: 400 });
|
|
1875
|
+
}
|
|
1876
|
+
args.table = fanOut.table;
|
|
1877
|
+
}
|
|
1747
1878
|
return {
|
|
1748
|
-
args
|
|
1749
|
-
fanOut
|
|
1879
|
+
args,
|
|
1880
|
+
fanOut,
|
|
1750
1881
|
functionPath: envelope.functionPath,
|
|
1751
1882
|
shardKey: envelope.shardKey
|
|
1752
1883
|
};
|
|
@@ -1864,18 +1995,24 @@ const createWorker = (options) => {
|
|
|
1864
1995
|
}
|
|
1865
1996
|
return context;
|
|
1866
1997
|
};
|
|
1867
|
-
const hasAnyShardAuth = Boolean(options.authorizeShard) || Boolean(options.authorizeFanOut);
|
|
1868
1998
|
let warnedUnauthenticatedShardAccess = false;
|
|
1869
|
-
const
|
|
1870
|
-
if (
|
|
1999
|
+
const guardUnauthenticatedShardAccess = (kind) => {
|
|
2000
|
+
if (!options.allowUnauthenticatedShardAccess) {
|
|
2001
|
+
const callback = kind === "fan-out" ? "authorizeFanOut" : "authorizeShard";
|
|
2002
|
+
throw new LunoraError(
|
|
2003
|
+
`${kind} access is default-denied: configure \`${callback}\` on the worker, or set \`allowUnauthenticatedShardAccess: true\` to explicitly allow unauthenticated ${kind} access (relying solely on per-row RLS).`,
|
|
2004
|
+
{ code: kind === "fan-out" ? "FORBIDDEN_FANOUT" : "FORBIDDEN_SHARD", status: 403 }
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
if (warnedUnauthenticatedShardAccess) {
|
|
1871
2008
|
return;
|
|
1872
2009
|
}
|
|
1873
2010
|
warnedUnauthenticatedShardAccess = true;
|
|
1874
2011
|
console.warn(
|
|
1875
2012
|
[
|
|
1876
|
-
`[lunora] SECURITY:
|
|
2013
|
+
`[lunora] SECURITY: serving ${kind} access with \`allowUnauthenticatedShardAccess: true\` and no \`authorizeShard\`/\`authorizeFanOut\` — `,
|
|
1877
2014
|
`any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
|
|
1878
|
-
`
|
|
2015
|
+
`This is safe only if every table is protected by per-row RLS. Configure \`authorizeShard\`/\`authorizeFanOut\` to gate it.`
|
|
1879
2016
|
].join("")
|
|
1880
2017
|
);
|
|
1881
2018
|
};
|
|
@@ -1887,20 +2024,20 @@ const createWorker = (options) => {
|
|
|
1887
2024
|
resolveForwardContext: resolveAdminForwardContext,
|
|
1888
2025
|
shardDO
|
|
1889
2026
|
});
|
|
1890
|
-
const dispatchToShard = async (functionPath, args, shardKey) => {
|
|
2027
|
+
const dispatchToShard = async (functionPath, args, shardKey, mutationId) => {
|
|
1891
2028
|
if (options.authorizeShard) {
|
|
1892
2029
|
const allowed = await options.authorizeShard(null, shardKey);
|
|
1893
2030
|
if (!allowed) {
|
|
1894
2031
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
1895
2032
|
}
|
|
1896
2033
|
}
|
|
2034
|
+
const headers = { "content-type": "application/json", "x-lunora-system": "1" };
|
|
2035
|
+
if (mutationId !== void 0 && mutationId.length > 0) {
|
|
2036
|
+
headers["x-lunora-mutation-id"] = mutationId;
|
|
2037
|
+
}
|
|
1897
2038
|
const forwarded = new Request("https://shard.internal/rpc", {
|
|
1898
|
-
// `x-lunora-system` marks this as a trusted server-initiated dispatch
|
|
1899
|
-
// so the shard may run `internal` functions (scheduled/cron jobs are
|
|
1900
|
-
// typically internal). Authorization was already enforced above; this
|
|
1901
|
-
// header is set only here, never on the client RPC path.
|
|
1902
2039
|
body: JSON.stringify({ args, functionPath }),
|
|
1903
|
-
headers
|
|
2040
|
+
headers,
|
|
1904
2041
|
method: "POST"
|
|
1905
2042
|
});
|
|
1906
2043
|
return forwardToShard(shardDO, shardKey, forwarded);
|
|
@@ -2016,7 +2153,8 @@ const createWorker = (options) => {
|
|
|
2016
2153
|
}
|
|
2017
2154
|
const args = candidate.args ?? {};
|
|
2018
2155
|
const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
|
|
2019
|
-
const
|
|
2156
|
+
const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
|
|
2157
|
+
const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId);
|
|
2020
2158
|
await releasePoolSlot(candidate);
|
|
2021
2159
|
return response;
|
|
2022
2160
|
};
|
|
@@ -2097,6 +2235,11 @@ const createWorker = (options) => {
|
|
|
2097
2235
|
requireAdminOption,
|
|
2098
2236
|
vectorIntrospector: options.vectorIntrospector
|
|
2099
2237
|
});
|
|
2238
|
+
const kvAdminRoutes = buildKvAdminRoutes({
|
|
2239
|
+
kvIntrospector: options.kvIntrospector,
|
|
2240
|
+
readJsonBody: readJsonBodyWithLimit,
|
|
2241
|
+
requireAdminOption
|
|
2242
|
+
});
|
|
2100
2243
|
const introspectionAdminRoutes = buildIntrospectionAdminRoutes({
|
|
2101
2244
|
assertAdmin: assertAdminAuthorized,
|
|
2102
2245
|
options: {
|
|
@@ -2159,6 +2302,10 @@ const createWorker = (options) => {
|
|
|
2159
2302
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
2160
2303
|
throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
|
|
2161
2304
|
}
|
|
2305
|
+
const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
|
|
2306
|
+
if (blockedUpgrade) {
|
|
2307
|
+
return blockedUpgrade;
|
|
2308
|
+
}
|
|
2162
2309
|
const shardKey = url.searchParams.get("shard") ?? defaultShard;
|
|
2163
2310
|
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2164
2311
|
if (options.authorizeShard) {
|
|
@@ -2167,7 +2314,7 @@ const createWorker = (options) => {
|
|
|
2167
2314
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2168
2315
|
}
|
|
2169
2316
|
} else if (shardKey !== defaultShard) {
|
|
2170
|
-
|
|
2317
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2171
2318
|
}
|
|
2172
2319
|
const upgradeHeaders = new Headers(request.headers);
|
|
2173
2320
|
upgradeHeaders.delete("x-lunora-userid");
|
|
@@ -2196,29 +2343,34 @@ const createWorker = (options) => {
|
|
|
2196
2343
|
}
|
|
2197
2344
|
return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
|
|
2198
2345
|
};
|
|
2346
|
+
const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
|
|
2347
|
+
if (options.authorizeFanOut) {
|
|
2348
|
+
const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
|
|
2349
|
+
if (!allowed) {
|
|
2350
|
+
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2351
|
+
}
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
if (functionPath.startsWith("__lunora_relation__:")) {
|
|
2355
|
+
throw new LunoraError("reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker", {
|
|
2356
|
+
code: "FORBIDDEN_FANOUT",
|
|
2357
|
+
status: 403
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
if (options.authorizeShard) {
|
|
2361
|
+
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2362
|
+
code: "FORBIDDEN_FANOUT",
|
|
2363
|
+
status: 403
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
guardUnauthenticatedShardAccess("fan-out");
|
|
2367
|
+
};
|
|
2199
2368
|
const authorizeRpcEnvelope = async (envelope, identity) => {
|
|
2369
|
+
if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_admin__:")) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2200
2372
|
if (envelope.fanOut) {
|
|
2201
|
-
|
|
2202
|
-
const allowed = await options.authorizeFanOut(identity, envelope.fanOut.table, envelope.functionPath);
|
|
2203
|
-
if (!allowed) {
|
|
2204
|
-
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2205
|
-
}
|
|
2206
|
-
} else if (envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
2207
|
-
throw new LunoraError(
|
|
2208
|
-
"reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker",
|
|
2209
|
-
{
|
|
2210
|
-
code: "FORBIDDEN_FANOUT",
|
|
2211
|
-
status: 403
|
|
2212
|
-
}
|
|
2213
|
-
);
|
|
2214
|
-
} else if (options.authorizeShard) {
|
|
2215
|
-
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2216
|
-
code: "FORBIDDEN_FANOUT",
|
|
2217
|
-
status: 403
|
|
2218
|
-
});
|
|
2219
|
-
} else {
|
|
2220
|
-
warnUnauthenticatedShardAccessOnce("fan-out");
|
|
2221
|
-
}
|
|
2373
|
+
await authorizeFanOutEnvelope(envelope.fanOut, envelope.functionPath, identity);
|
|
2222
2374
|
return;
|
|
2223
2375
|
}
|
|
2224
2376
|
if (options.authorizeShard) {
|
|
@@ -2228,7 +2380,7 @@ const createWorker = (options) => {
|
|
|
2228
2380
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2229
2381
|
}
|
|
2230
2382
|
} else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
|
|
2231
|
-
|
|
2383
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2232
2384
|
}
|
|
2233
2385
|
};
|
|
2234
2386
|
const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
|
|
@@ -2631,6 +2783,7 @@ const createWorker = (options) => {
|
|
|
2631
2783
|
...workflowsAdminRoutes,
|
|
2632
2784
|
...storageAdminRoutes,
|
|
2633
2785
|
...vectorAdminRoutes,
|
|
2786
|
+
...kvAdminRoutes,
|
|
2634
2787
|
...introspectionAdminRoutes,
|
|
2635
2788
|
// `/_lunora/admin/auth/*` — the whole user-management plane, one route per
|
|
2636
2789
|
// `AuthAdmin` op, dispatched by the descriptor table in `./auth-admin-routes`.
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
const DEFAULT_CSP = "default-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'none'";
|
|
2
|
+
const htmlCspFor = (frameOptions) => {
|
|
3
|
+
const parts = ["base-uri 'none'", "object-src 'none'"];
|
|
4
|
+
if (frameOptions === "DENY") {
|
|
5
|
+
parts.push("frame-ancestors 'none'");
|
|
6
|
+
} else if (frameOptions === "SAMEORIGIN") {
|
|
7
|
+
parts.push("frame-ancestors 'self'");
|
|
8
|
+
}
|
|
9
|
+
return parts.join("; ");
|
|
10
|
+
};
|
|
2
11
|
const DEFAULT_PERMISSIONS_POLICY = "accelerometer=(), autoplay=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()";
|
|
3
12
|
const DEFAULT_CORS_HEADERS = ["Authorization", "Content-Type", "X-D1-Bookmark", "X-Lunora-Mutation-Id"];
|
|
4
13
|
const DEFAULT_CORS_METHODS = ["DELETE", "GET", "HEAD", "PATCH", "POST", "PUT"];
|
|
@@ -13,14 +22,14 @@ const resolveHstsHeader = (hsts) => {
|
|
|
13
22
|
const includeSubDomains = config.includeSubDomains ?? true;
|
|
14
23
|
return `max-age=${String(maxAge)}${includeSubDomains ? "; includeSubDomains" : ""}${config.preload ? "; preload" : ""}`;
|
|
15
24
|
};
|
|
16
|
-
const resolveCspHeader = (csp) => {
|
|
25
|
+
const resolveCspHeader = (csp, htmlDefault) => {
|
|
17
26
|
if (csp === false) {
|
|
18
27
|
return void 0;
|
|
19
28
|
}
|
|
20
29
|
if (typeof csp === "string") {
|
|
21
|
-
return {
|
|
30
|
+
return { htmlValue: csp, value: csp };
|
|
22
31
|
}
|
|
23
|
-
return {
|
|
32
|
+
return { htmlValue: htmlDefault, value: DEFAULT_CSP };
|
|
24
33
|
};
|
|
25
34
|
const resolveHeaders = (input) => {
|
|
26
35
|
if (input === false) {
|
|
@@ -35,11 +44,12 @@ const resolveHeaders = (input) => {
|
|
|
35
44
|
};
|
|
36
45
|
}
|
|
37
46
|
const options = input === void 0 || input === true ? {} : input;
|
|
47
|
+
const frameOptions = options.frameOptions === false ? void 0 : options.frameOptions ?? "SAMEORIGIN";
|
|
38
48
|
return {
|
|
39
49
|
coop: "same-origin",
|
|
40
|
-
csp: resolveCspHeader(options.csp),
|
|
50
|
+
csp: resolveCspHeader(options.csp, htmlCspFor(frameOptions)),
|
|
41
51
|
enabled: true,
|
|
42
|
-
frameOptions
|
|
52
|
+
frameOptions,
|
|
43
53
|
hsts: resolveHstsHeader(options.hsts),
|
|
44
54
|
permissionsPolicy: options.permissionsPolicy === false ? void 0 : options.permissionsPolicy ?? DEFAULT_PERMISSIONS_POLICY,
|
|
45
55
|
referrerPolicy: options.referrerPolicy === false ? void 0 : options.referrerPolicy ?? "strict-origin-when-cross-origin"
|
|
@@ -65,6 +75,11 @@ const resolveCors = (input) => {
|
|
|
65
75
|
if (typeof origins === "function") {
|
|
66
76
|
isAllowed = origins;
|
|
67
77
|
isExplicitlyAllowed = origins;
|
|
78
|
+
if (allowCredentials) {
|
|
79
|
+
console.warn(
|
|
80
|
+
"@lunora/runtime: security.cors combines a custom `allowedOrigins` predicate with `allowCredentials: true`. Ensure the predicate matches ONLY trusted origins by exact equality — an over-broad predicate (e.g. `() => true`, or `endsWith`/`includes` checks) reflects any origin with credentials, defeating the allowlist and the CSRF guard."
|
|
81
|
+
);
|
|
82
|
+
}
|
|
68
83
|
} else {
|
|
69
84
|
const originsList = origins;
|
|
70
85
|
if (originsList.includes("*") && allowCredentials) {
|
|
@@ -152,6 +167,20 @@ const enforceOrigin = (request, resolved) => {
|
|
|
152
167
|
{ headers: { "content-type": "application/json" }, status: 403 }
|
|
153
168
|
);
|
|
154
169
|
};
|
|
170
|
+
const enforceWebSocketOrigin = (request, resolved) => {
|
|
171
|
+
if (!resolved.csrf.enabled || !request.headers.get("cookie")) {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
const selfOrigin = new URL(request.url).origin;
|
|
175
|
+
const source = originOf(request.headers.get("origin"));
|
|
176
|
+
if (source !== void 0 && isTrustedOrigin(source, selfOrigin, resolved)) {
|
|
177
|
+
return void 0;
|
|
178
|
+
}
|
|
179
|
+
return Response.json(
|
|
180
|
+
{ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin websocket upgrade rejected" } },
|
|
181
|
+
{ headers: { "content-type": "application/json" }, status: 403 }
|
|
182
|
+
);
|
|
183
|
+
};
|
|
155
184
|
const corsResponseHeaders = (origin, cors) => {
|
|
156
185
|
const headers = new Headers();
|
|
157
186
|
headers.set("access-control-allow-origin", origin);
|
|
@@ -199,8 +228,11 @@ const applyBaselineHeaders = (headers, request, response, config) => {
|
|
|
199
228
|
if (config.coop !== void 0) {
|
|
200
229
|
setIfAbsent(headers, "cross-origin-opener-policy", config.coop);
|
|
201
230
|
}
|
|
202
|
-
if (config.csp !== void 0
|
|
203
|
-
|
|
231
|
+
if (config.csp !== void 0) {
|
|
232
|
+
const cspValue = isHtmlResponse(response) ? config.csp.htmlValue : config.csp.value;
|
|
233
|
+
if (cspValue !== void 0) {
|
|
234
|
+
setIfAbsent(headers, "content-security-policy", cspValue);
|
|
235
|
+
}
|
|
204
236
|
}
|
|
205
237
|
};
|
|
206
238
|
const applyCorsHeaders = (headers, request, cors) => {
|
|
@@ -230,4 +262,4 @@ const decorateResponse = (response, request, resolved) => {
|
|
|
230
262
|
return new Response(response.body, { headers, status: response.status, statusText: response.statusText });
|
|
231
263
|
};
|
|
232
264
|
|
|
233
|
-
export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity };
|
|
265
|
+
export { decorateResponse, enforceOrigin, enforceWebSocketOrigin, handleCorsPreflight, resolveSecurity };
|