@lunora/runtime 1.0.0-alpha.12 → 1.0.0-alpha.14
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 +163 -45
- package/dist/index.d.ts +163 -45
- package/dist/index.mjs +3 -2
- package/dist/packem_shared/composeIdentityResolvers-YjvUKisc.mjs +55 -0
- package/dist/packem_shared/{composeWorker-BNYeYQqL.mjs → composeWorker-M4mPqTJx.mjs} +73 -45
- 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
|
@@ -337,6 +337,116 @@ interface FunctionArgumentDescriptor {
|
|
|
337
337
|
* kind only — enough for a signature view without a deep recursive walk.
|
|
338
338
|
*/
|
|
339
339
|
/**
|
|
340
|
+
* Identity resolved from the inbound request by `WorkerOptions.resolveIdentity`.
|
|
341
|
+
*
|
|
342
|
+
* The `userId` field is special — it becomes `ctx.auth.userId` inside the
|
|
343
|
+
* Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
|
|
344
|
+
* forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
|
|
345
|
+
*
|
|
346
|
+
* Return `null` to signal that the request is anonymous; the runtime will
|
|
347
|
+
* skip both `x-lunora-userid` and `x-lunora-identity` headers, and
|
|
348
|
+
* `ctx.auth.userId` will be `undefined` on the shard side.
|
|
349
|
+
*/
|
|
350
|
+
interface ResolvedIdentity {
|
|
351
|
+
/** Arbitrary additional claims. Must be JSON-serialisable. */
|
|
352
|
+
[key: string]: unknown;
|
|
353
|
+
/**
|
|
354
|
+
* JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
|
|
355
|
+
* absent), the runtime forwards it as the socket's credential expiry — the
|
|
356
|
+
* DO drops the socket once it lapses. Used only on the WebSocket path.
|
|
357
|
+
*/
|
|
358
|
+
exp?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
|
|
361
|
+
* both are present. Forwarded as the socket's expiry on the WebSocket path
|
|
362
|
+
* so the DO drops the socket once it lapses; omit for non-expiring sessions.
|
|
363
|
+
*/
|
|
364
|
+
expiresAtMs?: number;
|
|
365
|
+
/** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
|
|
366
|
+
userId: string;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* A verifier that turns an inbound request into a {@link ResolvedIdentity} (or
|
|
370
|
+
* `null` for anonymous). Structurally identical to `WorkerOptions.resolveIdentity`,
|
|
371
|
+
* so `.auth()`'s better-auth session resolver, a signed-preview-link verifier, a
|
|
372
|
+
* per-tenant bearer check, an upstream-JWT reader, … are all just `IdentityResolver`s
|
|
373
|
+
* — the identity layer is generic over every scheme, not coupled to any one.
|
|
374
|
+
*/
|
|
375
|
+
type IdentityResolver = (request: Request, env: unknown) => Promise<ResolvedIdentity | null> | ResolvedIdentity | null;
|
|
376
|
+
/** Error policy for {@link composeIdentityResolvers} when a participant resolver throws. */
|
|
377
|
+
type ComposeIdentityResolversErrorMode = "fail-closed" | "skip";
|
|
378
|
+
/** Options for {@link composeIdentityResolvers}. */
|
|
379
|
+
interface ComposeIdentityResolversOptions {
|
|
380
|
+
/**
|
|
381
|
+
* What to do when a resolver throws. `"fail-closed"` (default, safe)
|
|
382
|
+
* re-throws so a broken verifier fails the request rather than silently
|
|
383
|
+
* falling through to a weaker one; `"skip"` swallows the error and tries the
|
|
384
|
+
* next resolver (use only when a resolver's failure genuinely means "not my
|
|
385
|
+
* scheme").
|
|
386
|
+
*/
|
|
387
|
+
readonly onError?: ComposeIdentityResolversErrorMode;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Compose several {@link IdentityResolver}s into one, first-match-wins: each is
|
|
391
|
+
* tried in order and the first that returns a non-null identity short-circuits.
|
|
392
|
+
* Generic over every scheme — the better-auth session resolver (obtained via the
|
|
393
|
+
* builder's `derived.resolveIdentity` escape hatch) is just one entry in the list,
|
|
394
|
+
* so composition never means losing it.
|
|
395
|
+
*
|
|
396
|
+
* A resolver that throws is handled per {@link ComposeIdentityResolversOptions.onError}
|
|
397
|
+
* (default `"fail-closed"`: the error propagates).
|
|
398
|
+
*/
|
|
399
|
+
declare const composeIdentityResolvers: (resolvers: ReadonlyArray<IdentityResolver>, options?: ComposeIdentityResolversOptions) => IdentityResolver;
|
|
400
|
+
/**
|
|
401
|
+
* A thin, generic helper over {@link composeIdentityResolvers} for the per-route case:
|
|
402
|
+
* pick a resolver by `new URL(request.url).pathname`. Keys are matched by longest
|
|
403
|
+
* path prefix; `"*"` is the fallback. Still fully generic — a route→resolver map,
|
|
404
|
+
* with no portal / preview / tenant concepts baked in (those live in the app's
|
|
405
|
+
* own resolvers).
|
|
406
|
+
* @example
|
|
407
|
+
* routeIdentityResolvers({ "/admin": adminResolver, "/partner": partnerResolver, "*": sessionResolver })
|
|
408
|
+
*/
|
|
409
|
+
declare const routeIdentityResolvers: (routes: Record<string, IdentityResolver>) => IdentityResolver;
|
|
410
|
+
/** The result of validating a candidate identity against an {@link IdentityContractLike}. */
|
|
411
|
+
type IdentityValidation = {
|
|
412
|
+
ok: true;
|
|
413
|
+
} | {
|
|
414
|
+
error: string;
|
|
415
|
+
ok: false;
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* Structural view of `@lunora/server`'s `IdentityContract` (from `defineIdentity`).
|
|
419
|
+
* Kept structural so `@lunora/runtime` stays free of an `@lunora/server` dependency.
|
|
420
|
+
* Keep the `onInvalid` union and `validate`/`IdentityValidation` shapes in sync with
|
|
421
|
+
* `@lunora/server`'s `IdentityContract` — they are projected by hand, not imported.
|
|
422
|
+
* The generated worker entry passes the app's `defineIdentity(...)` result here;
|
|
423
|
+
* the worker validates every resolver's returned claims against it at the trust
|
|
424
|
+
* boundary before they become `ctx.auth`.
|
|
425
|
+
*/
|
|
426
|
+
interface IdentityContractLike {
|
|
427
|
+
/** Reject policy applied when validation fails: downgrade to anonymous, or reject the request (401). */
|
|
428
|
+
readonly onInvalid: "anonymous" | "reject";
|
|
429
|
+
/** Validate resolver-returned claims against the declared contract. */
|
|
430
|
+
validate: (identity: Record<string, unknown>) => IdentityValidation;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The trust-boundary identity gate. Given the worker's `resolveIdentity` and an
|
|
434
|
+
* optional `defineIdentity(...)` contract, return a resolver that validates every
|
|
435
|
+
* resolved identity against the declared claims BEFORE it becomes `ctx.auth`.
|
|
436
|
+
*
|
|
437
|
+
* Claims arrive from untrusted tokens; a forged / malformed set is either
|
|
438
|
+
* downgraded to anonymous (`onInvalid: "anonymous"`, the safe default — the bad
|
|
439
|
+
* identity never reaches a policy as valid) or rejected with a `401`
|
|
440
|
+
* (`onInvalid: "reject"`), rather than flowing in as an unchecked cast. A valid
|
|
441
|
+
* identity is returned unchanged, so undeclared claims are forwarded verbatim.
|
|
442
|
+
*
|
|
443
|
+
* When no contract is configured (or there is no `resolveIdentity`), the original
|
|
444
|
+
* resolver is returned untouched — zero overhead and byte-identical behaviour.
|
|
445
|
+
* Only the public data paths (RPC / WebSocket / HTTP-action / server-query) use
|
|
446
|
+
* the wrapped resolver; the admin path keeps the raw one (admin is gated by the
|
|
447
|
+
* bearer / Access, not the app's identity contract).
|
|
448
|
+
*/
|
|
449
|
+
/**
|
|
340
450
|
* Observability hooks for the Lunora runtime.
|
|
341
451
|
*
|
|
342
452
|
* A user-supplied {@link ObservabilitySink} receives one event per dispatched
|
|
@@ -1040,9 +1150,13 @@ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => Quer
|
|
|
1040
1150
|
interface SecurityHeadersOptions {
|
|
1041
1151
|
/**
|
|
1042
1152
|
* `Content-Security-Policy`. Omitted (`undefined`) applies a restrictive
|
|
1043
|
-
* default to **non-HTML** responses
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1153
|
+
* `default-src 'none'` policy to **non-HTML** responses, and a conservative
|
|
1154
|
+
* hardening policy to **HTML** responses (`base-uri 'none'; frame-ancestors
|
|
1155
|
+
* 'self'; object-src 'none'`) — this does NOT set `default-src`/`script-src`,
|
|
1156
|
+
* so it never blocks an SSR page's own scripts/styles/images/fetches, it only
|
|
1157
|
+
* locks down the `base` element, framing (mirroring the `SAMEORIGIN`
|
|
1158
|
+
* X-Frame-Options default), and legacy plugins. Pass a string to apply that
|
|
1159
|
+
* exact policy to every response (HTML included); `false` to never send one.
|
|
1046
1160
|
*/
|
|
1047
1161
|
csp?: string | false;
|
|
1048
1162
|
/** `X-Frame-Options` (clickjacking). Defaults to `SAMEORIGIN`. `false` omits it. */
|
|
@@ -1091,7 +1205,7 @@ interface SecurityOptions {
|
|
|
1091
1205
|
interface ResolvedHeaders {
|
|
1092
1206
|
coop: string | undefined;
|
|
1093
1207
|
csp: {
|
|
1094
|
-
|
|
1208
|
+
htmlValue: string | undefined;
|
|
1095
1209
|
value: string;
|
|
1096
1210
|
} | undefined;
|
|
1097
1211
|
enabled: boolean;
|
|
@@ -1154,6 +1268,24 @@ declare const resolveSecurity: (security: SecurityOptions | undefined, env?: Rec
|
|
|
1154
1268
|
* @returns a `403` Response when the origin is untrusted, or `undefined` when the request is allowed.
|
|
1155
1269
|
*/
|
|
1156
1270
|
declare const enforceOrigin: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
|
|
1271
|
+
/**
|
|
1272
|
+
* CSRF defense for the WebSocket upgrade (Cross-Site WebSocket Hijacking).
|
|
1273
|
+
*
|
|
1274
|
+
* A WS handshake is an HTTP `GET`, so {@link enforceOrigin}'s safe-method
|
|
1275
|
+
* exemption never fires for it — yet the browser auto-attaches the session
|
|
1276
|
+
* cookie to the handshake and WebSocket connections are NOT governed by
|
|
1277
|
+
* CORS/SOP. Without an explicit `Origin` check any page can open
|
|
1278
|
+
* `wss://app/_lunora/ws`, authenticate as the logged-in victim, and read the
|
|
1279
|
+
* victim's live queries + issue mutations as them. This guard closes that hole.
|
|
1280
|
+
*
|
|
1281
|
+
* Scoped to cookie-bearing upgrades — the only vector CSWSH can ride (a browser
|
|
1282
|
+
* attaches cookies but never a bearer token). Bearer/token/server-to-server
|
|
1283
|
+
* upgrades (no `Cookie`) are exempt. A browser ALWAYS sends `Origin` on a WS
|
|
1284
|
+
* handshake, so a missing/untrusted `Origin` on a cookie-bearing upgrade fails
|
|
1285
|
+
* closed (mirrors {@link enforceOrigin}).
|
|
1286
|
+
* @returns a `403` Response when the upgrade origin is untrusted, or `undefined` when it is allowed.
|
|
1287
|
+
*/
|
|
1288
|
+
|
|
1157
1289
|
/**
|
|
1158
1290
|
* Answer a CORS preflight (`OPTIONS` carrying `Access-Control-Request-Method`)
|
|
1159
1291
|
* for an allowlisted origin with a `204`. Returns `undefined` for non-preflight
|
|
@@ -1223,35 +1355,6 @@ interface HttpRouterLike {
|
|
|
1223
1355
|
fetch(request: Request, env?: unknown, context?: ExecutionContextLike): Promise<Response> | Response;
|
|
1224
1356
|
}
|
|
1225
1357
|
/**
|
|
1226
|
-
* Identity resolved from the inbound request by {@link WorkerOptions.resolveIdentity}.
|
|
1227
|
-
*
|
|
1228
|
-
* The `userId` field is special — it becomes `ctx.auth.userId` inside the
|
|
1229
|
-
* Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
|
|
1230
|
-
* forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
|
|
1231
|
-
*
|
|
1232
|
-
* Return `null` to signal that the request is anonymous; the runtime will
|
|
1233
|
-
* skip both `x-lunora-userid` and `x-lunora-identity` headers, and
|
|
1234
|
-
* `ctx.auth.userId` will be `undefined` on the shard side.
|
|
1235
|
-
*/
|
|
1236
|
-
interface ResolvedIdentity {
|
|
1237
|
-
/** Arbitrary additional claims. Must be JSON-serialisable. */
|
|
1238
|
-
[key: string]: unknown;
|
|
1239
|
-
/**
|
|
1240
|
-
* JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
|
|
1241
|
-
* absent), the runtime forwards it as the socket's credential expiry — the
|
|
1242
|
-
* DO drops the socket once it lapses. Used only on the WebSocket path.
|
|
1243
|
-
*/
|
|
1244
|
-
exp?: number;
|
|
1245
|
-
/**
|
|
1246
|
-
* Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
|
|
1247
|
-
* both are present. Forwarded as the socket's expiry on the WebSocket path
|
|
1248
|
-
* so the DO drops the socket once it lapses; omit for non-expiring sessions.
|
|
1249
|
-
*/
|
|
1250
|
-
expiresAtMs?: number;
|
|
1251
|
-
/** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
|
|
1252
|
-
userId: string;
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
1358
|
* Per-table sharding metadata the admin import endpoint needs to route rows.
|
|
1256
1359
|
* Structural so this package stays free of `@lunora/server`. The codegen-
|
|
1257
1360
|
* generated worker entry passes a thin projection of the user's schema.
|
|
@@ -1637,17 +1740,21 @@ interface WorkerOptions {
|
|
|
1637
1740
|
*/
|
|
1638
1741
|
adminToken?: string;
|
|
1639
1742
|
/**
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
* `
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
* configured.
|
|
1743
|
+
* Opt into an authorization-open posture for sharded and fan-out access.
|
|
1744
|
+
*
|
|
1745
|
+
* By default (this flag unset/`false`) the runtime FAILS CLOSED: when
|
|
1746
|
+
* neither {@link WorkerOptions.authorizeShard} nor {@link WorkerOptions.authorizeFanOut}
|
|
1747
|
+
* is configured, naming a non-default shard (a potential cross-tenant hop)
|
|
1748
|
+
* or sending a fan-out envelope is rejected with a `403`
|
|
1749
|
+
* (`FORBIDDEN_SHARD`/`FORBIDDEN_FANOUT`). Set this to `true` to allow such
|
|
1750
|
+
* requests from any caller (including unauthenticated ones) — appropriate
|
|
1751
|
+
* only when every table is protected by per-row RLS. The runtime then emits
|
|
1752
|
+
* a single `console.warn` so the open posture stays visible in logs. Has no
|
|
1753
|
+
* effect once an `authorize*` callback is configured (those gate directly).
|
|
1754
|
+
*
|
|
1755
|
+
* NOTE: this is a behaviour change from earlier alphas, where the same
|
|
1756
|
+
* situation was warn-once-then-allow. Apps that relied on client-chosen
|
|
1757
|
+
* shard keys without an `authorize*` callback must set this flag explicitly.
|
|
1651
1758
|
*/
|
|
1652
1759
|
allowUnauthenticatedShardAccess?: boolean;
|
|
1653
1760
|
/**
|
|
@@ -1814,6 +1921,17 @@ interface WorkerOptions {
|
|
|
1814
1921
|
*/
|
|
1815
1922
|
httpRouter?: HttpRouterLike;
|
|
1816
1923
|
/**
|
|
1924
|
+
* The declared identity claim contract (`defineIdentity(...)` from
|
|
1925
|
+
* `@lunora/server`), passed by the generated worker entry. When present, the
|
|
1926
|
+
* worker validates every `resolveIdentity` result against it at the trust
|
|
1927
|
+
* boundary (on the public data paths — RPC / WebSocket / HTTP-action /
|
|
1928
|
+
* server-query, never the admin path) *before* the claims become `ctx.auth`.
|
|
1929
|
+
* A resolver output that violates the contract is downgraded to anonymous or
|
|
1930
|
+
* rejected with a `401`, per the contract's `onInvalid`. Omitted → no
|
|
1931
|
+
* validation, and the identity stays the historical untyped claim bag.
|
|
1932
|
+
*/
|
|
1933
|
+
identity?: IdentityContractLike;
|
|
1934
|
+
/**
|
|
1817
1935
|
* Insert `.global()` rows for the admin import endpoint. When omitted,
|
|
1818
1936
|
* rows targeting global tables are reported as hard errors.
|
|
1819
1937
|
*/
|
|
@@ -2423,4 +2541,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
|
|
|
2423
2541
|
*/
|
|
2424
2542
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2425
2543
|
declare const VERSION: string;
|
|
2426
|
-
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 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 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, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
|
2544
|
+
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -337,6 +337,116 @@ interface FunctionArgumentDescriptor {
|
|
|
337
337
|
* kind only — enough for a signature view without a deep recursive walk.
|
|
338
338
|
*/
|
|
339
339
|
/**
|
|
340
|
+
* Identity resolved from the inbound request by `WorkerOptions.resolveIdentity`.
|
|
341
|
+
*
|
|
342
|
+
* The `userId` field is special — it becomes `ctx.auth.userId` inside the
|
|
343
|
+
* Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
|
|
344
|
+
* forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
|
|
345
|
+
*
|
|
346
|
+
* Return `null` to signal that the request is anonymous; the runtime will
|
|
347
|
+
* skip both `x-lunora-userid` and `x-lunora-identity` headers, and
|
|
348
|
+
* `ctx.auth.userId` will be `undefined` on the shard side.
|
|
349
|
+
*/
|
|
350
|
+
interface ResolvedIdentity {
|
|
351
|
+
/** Arbitrary additional claims. Must be JSON-serialisable. */
|
|
352
|
+
[key: string]: unknown;
|
|
353
|
+
/**
|
|
354
|
+
* JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
|
|
355
|
+
* absent), the runtime forwards it as the socket's credential expiry — the
|
|
356
|
+
* DO drops the socket once it lapses. Used only on the WebSocket path.
|
|
357
|
+
*/
|
|
358
|
+
exp?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
|
|
361
|
+
* both are present. Forwarded as the socket's expiry on the WebSocket path
|
|
362
|
+
* so the DO drops the socket once it lapses; omit for non-expiring sessions.
|
|
363
|
+
*/
|
|
364
|
+
expiresAtMs?: number;
|
|
365
|
+
/** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
|
|
366
|
+
userId: string;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* A verifier that turns an inbound request into a {@link ResolvedIdentity} (or
|
|
370
|
+
* `null` for anonymous). Structurally identical to `WorkerOptions.resolveIdentity`,
|
|
371
|
+
* so `.auth()`'s better-auth session resolver, a signed-preview-link verifier, a
|
|
372
|
+
* per-tenant bearer check, an upstream-JWT reader, … are all just `IdentityResolver`s
|
|
373
|
+
* — the identity layer is generic over every scheme, not coupled to any one.
|
|
374
|
+
*/
|
|
375
|
+
type IdentityResolver = (request: Request, env: unknown) => Promise<ResolvedIdentity | null> | ResolvedIdentity | null;
|
|
376
|
+
/** Error policy for {@link composeIdentityResolvers} when a participant resolver throws. */
|
|
377
|
+
type ComposeIdentityResolversErrorMode = "fail-closed" | "skip";
|
|
378
|
+
/** Options for {@link composeIdentityResolvers}. */
|
|
379
|
+
interface ComposeIdentityResolversOptions {
|
|
380
|
+
/**
|
|
381
|
+
* What to do when a resolver throws. `"fail-closed"` (default, safe)
|
|
382
|
+
* re-throws so a broken verifier fails the request rather than silently
|
|
383
|
+
* falling through to a weaker one; `"skip"` swallows the error and tries the
|
|
384
|
+
* next resolver (use only when a resolver's failure genuinely means "not my
|
|
385
|
+
* scheme").
|
|
386
|
+
*/
|
|
387
|
+
readonly onError?: ComposeIdentityResolversErrorMode;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Compose several {@link IdentityResolver}s into one, first-match-wins: each is
|
|
391
|
+
* tried in order and the first that returns a non-null identity short-circuits.
|
|
392
|
+
* Generic over every scheme — the better-auth session resolver (obtained via the
|
|
393
|
+
* builder's `derived.resolveIdentity` escape hatch) is just one entry in the list,
|
|
394
|
+
* so composition never means losing it.
|
|
395
|
+
*
|
|
396
|
+
* A resolver that throws is handled per {@link ComposeIdentityResolversOptions.onError}
|
|
397
|
+
* (default `"fail-closed"`: the error propagates).
|
|
398
|
+
*/
|
|
399
|
+
declare const composeIdentityResolvers: (resolvers: ReadonlyArray<IdentityResolver>, options?: ComposeIdentityResolversOptions) => IdentityResolver;
|
|
400
|
+
/**
|
|
401
|
+
* A thin, generic helper over {@link composeIdentityResolvers} for the per-route case:
|
|
402
|
+
* pick a resolver by `new URL(request.url).pathname`. Keys are matched by longest
|
|
403
|
+
* path prefix; `"*"` is the fallback. Still fully generic — a route→resolver map,
|
|
404
|
+
* with no portal / preview / tenant concepts baked in (those live in the app's
|
|
405
|
+
* own resolvers).
|
|
406
|
+
* @example
|
|
407
|
+
* routeIdentityResolvers({ "/admin": adminResolver, "/partner": partnerResolver, "*": sessionResolver })
|
|
408
|
+
*/
|
|
409
|
+
declare const routeIdentityResolvers: (routes: Record<string, IdentityResolver>) => IdentityResolver;
|
|
410
|
+
/** The result of validating a candidate identity against an {@link IdentityContractLike}. */
|
|
411
|
+
type IdentityValidation = {
|
|
412
|
+
ok: true;
|
|
413
|
+
} | {
|
|
414
|
+
error: string;
|
|
415
|
+
ok: false;
|
|
416
|
+
};
|
|
417
|
+
/**
|
|
418
|
+
* Structural view of `@lunora/server`'s `IdentityContract` (from `defineIdentity`).
|
|
419
|
+
* Kept structural so `@lunora/runtime` stays free of an `@lunora/server` dependency.
|
|
420
|
+
* Keep the `onInvalid` union and `validate`/`IdentityValidation` shapes in sync with
|
|
421
|
+
* `@lunora/server`'s `IdentityContract` — they are projected by hand, not imported.
|
|
422
|
+
* The generated worker entry passes the app's `defineIdentity(...)` result here;
|
|
423
|
+
* the worker validates every resolver's returned claims against it at the trust
|
|
424
|
+
* boundary before they become `ctx.auth`.
|
|
425
|
+
*/
|
|
426
|
+
interface IdentityContractLike {
|
|
427
|
+
/** Reject policy applied when validation fails: downgrade to anonymous, or reject the request (401). */
|
|
428
|
+
readonly onInvalid: "anonymous" | "reject";
|
|
429
|
+
/** Validate resolver-returned claims against the declared contract. */
|
|
430
|
+
validate: (identity: Record<string, unknown>) => IdentityValidation;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* The trust-boundary identity gate. Given the worker's `resolveIdentity` and an
|
|
434
|
+
* optional `defineIdentity(...)` contract, return a resolver that validates every
|
|
435
|
+
* resolved identity against the declared claims BEFORE it becomes `ctx.auth`.
|
|
436
|
+
*
|
|
437
|
+
* Claims arrive from untrusted tokens; a forged / malformed set is either
|
|
438
|
+
* downgraded to anonymous (`onInvalid: "anonymous"`, the safe default — the bad
|
|
439
|
+
* identity never reaches a policy as valid) or rejected with a `401`
|
|
440
|
+
* (`onInvalid: "reject"`), rather than flowing in as an unchecked cast. A valid
|
|
441
|
+
* identity is returned unchanged, so undeclared claims are forwarded verbatim.
|
|
442
|
+
*
|
|
443
|
+
* When no contract is configured (or there is no `resolveIdentity`), the original
|
|
444
|
+
* resolver is returned untouched — zero overhead and byte-identical behaviour.
|
|
445
|
+
* Only the public data paths (RPC / WebSocket / HTTP-action / server-query) use
|
|
446
|
+
* the wrapped resolver; the admin path keeps the raw one (admin is gated by the
|
|
447
|
+
* bearer / Access, not the app's identity contract).
|
|
448
|
+
*/
|
|
449
|
+
/**
|
|
340
450
|
* Observability hooks for the Lunora runtime.
|
|
341
451
|
*
|
|
342
452
|
* A user-supplied {@link ObservabilitySink} receives one event per dispatched
|
|
@@ -1040,9 +1150,13 @@ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => Quer
|
|
|
1040
1150
|
interface SecurityHeadersOptions {
|
|
1041
1151
|
/**
|
|
1042
1152
|
* `Content-Security-Policy`. Omitted (`undefined`) applies a restrictive
|
|
1043
|
-
* default to **non-HTML** responses
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1153
|
+
* `default-src 'none'` policy to **non-HTML** responses, and a conservative
|
|
1154
|
+
* hardening policy to **HTML** responses (`base-uri 'none'; frame-ancestors
|
|
1155
|
+
* 'self'; object-src 'none'`) — this does NOT set `default-src`/`script-src`,
|
|
1156
|
+
* so it never blocks an SSR page's own scripts/styles/images/fetches, it only
|
|
1157
|
+
* locks down the `base` element, framing (mirroring the `SAMEORIGIN`
|
|
1158
|
+
* X-Frame-Options default), and legacy plugins. Pass a string to apply that
|
|
1159
|
+
* exact policy to every response (HTML included); `false` to never send one.
|
|
1046
1160
|
*/
|
|
1047
1161
|
csp?: string | false;
|
|
1048
1162
|
/** `X-Frame-Options` (clickjacking). Defaults to `SAMEORIGIN`. `false` omits it. */
|
|
@@ -1091,7 +1205,7 @@ interface SecurityOptions {
|
|
|
1091
1205
|
interface ResolvedHeaders {
|
|
1092
1206
|
coop: string | undefined;
|
|
1093
1207
|
csp: {
|
|
1094
|
-
|
|
1208
|
+
htmlValue: string | undefined;
|
|
1095
1209
|
value: string;
|
|
1096
1210
|
} | undefined;
|
|
1097
1211
|
enabled: boolean;
|
|
@@ -1154,6 +1268,24 @@ declare const resolveSecurity: (security: SecurityOptions | undefined, env?: Rec
|
|
|
1154
1268
|
* @returns a `403` Response when the origin is untrusted, or `undefined` when the request is allowed.
|
|
1155
1269
|
*/
|
|
1156
1270
|
declare const enforceOrigin: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
|
|
1271
|
+
/**
|
|
1272
|
+
* CSRF defense for the WebSocket upgrade (Cross-Site WebSocket Hijacking).
|
|
1273
|
+
*
|
|
1274
|
+
* A WS handshake is an HTTP `GET`, so {@link enforceOrigin}'s safe-method
|
|
1275
|
+
* exemption never fires for it — yet the browser auto-attaches the session
|
|
1276
|
+
* cookie to the handshake and WebSocket connections are NOT governed by
|
|
1277
|
+
* CORS/SOP. Without an explicit `Origin` check any page can open
|
|
1278
|
+
* `wss://app/_lunora/ws`, authenticate as the logged-in victim, and read the
|
|
1279
|
+
* victim's live queries + issue mutations as them. This guard closes that hole.
|
|
1280
|
+
*
|
|
1281
|
+
* Scoped to cookie-bearing upgrades — the only vector CSWSH can ride (a browser
|
|
1282
|
+
* attaches cookies but never a bearer token). Bearer/token/server-to-server
|
|
1283
|
+
* upgrades (no `Cookie`) are exempt. A browser ALWAYS sends `Origin` on a WS
|
|
1284
|
+
* handshake, so a missing/untrusted `Origin` on a cookie-bearing upgrade fails
|
|
1285
|
+
* closed (mirrors {@link enforceOrigin}).
|
|
1286
|
+
* @returns a `403` Response when the upgrade origin is untrusted, or `undefined` when it is allowed.
|
|
1287
|
+
*/
|
|
1288
|
+
|
|
1157
1289
|
/**
|
|
1158
1290
|
* Answer a CORS preflight (`OPTIONS` carrying `Access-Control-Request-Method`)
|
|
1159
1291
|
* for an allowlisted origin with a `204`. Returns `undefined` for non-preflight
|
|
@@ -1223,35 +1355,6 @@ interface HttpRouterLike {
|
|
|
1223
1355
|
fetch(request: Request, env?: unknown, context?: ExecutionContextLike): Promise<Response> | Response;
|
|
1224
1356
|
}
|
|
1225
1357
|
/**
|
|
1226
|
-
* Identity resolved from the inbound request by {@link WorkerOptions.resolveIdentity}.
|
|
1227
|
-
*
|
|
1228
|
-
* The `userId` field is special — it becomes `ctx.auth.userId` inside the
|
|
1229
|
-
* Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
|
|
1230
|
-
* forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
|
|
1231
|
-
*
|
|
1232
|
-
* Return `null` to signal that the request is anonymous; the runtime will
|
|
1233
|
-
* skip both `x-lunora-userid` and `x-lunora-identity` headers, and
|
|
1234
|
-
* `ctx.auth.userId` will be `undefined` on the shard side.
|
|
1235
|
-
*/
|
|
1236
|
-
interface ResolvedIdentity {
|
|
1237
|
-
/** Arbitrary additional claims. Must be JSON-serialisable. */
|
|
1238
|
-
[key: string]: unknown;
|
|
1239
|
-
/**
|
|
1240
|
-
* JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
|
|
1241
|
-
* absent), the runtime forwards it as the socket's credential expiry — the
|
|
1242
|
-
* DO drops the socket once it lapses. Used only on the WebSocket path.
|
|
1243
|
-
*/
|
|
1244
|
-
exp?: number;
|
|
1245
|
-
/**
|
|
1246
|
-
* Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
|
|
1247
|
-
* both are present. Forwarded as the socket's expiry on the WebSocket path
|
|
1248
|
-
* so the DO drops the socket once it lapses; omit for non-expiring sessions.
|
|
1249
|
-
*/
|
|
1250
|
-
expiresAtMs?: number;
|
|
1251
|
-
/** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
|
|
1252
|
-
userId: string;
|
|
1253
|
-
}
|
|
1254
|
-
/**
|
|
1255
1358
|
* Per-table sharding metadata the admin import endpoint needs to route rows.
|
|
1256
1359
|
* Structural so this package stays free of `@lunora/server`. The codegen-
|
|
1257
1360
|
* generated worker entry passes a thin projection of the user's schema.
|
|
@@ -1637,17 +1740,21 @@ interface WorkerOptions {
|
|
|
1637
1740
|
*/
|
|
1638
1741
|
adminToken?: string;
|
|
1639
1742
|
/**
|
|
1640
|
-
*
|
|
1641
|
-
*
|
|
1642
|
-
*
|
|
1643
|
-
*
|
|
1644
|
-
*
|
|
1645
|
-
*
|
|
1646
|
-
* `
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
1649
|
-
*
|
|
1650
|
-
* configured.
|
|
1743
|
+
* Opt into an authorization-open posture for sharded and fan-out access.
|
|
1744
|
+
*
|
|
1745
|
+
* By default (this flag unset/`false`) the runtime FAILS CLOSED: when
|
|
1746
|
+
* neither {@link WorkerOptions.authorizeShard} nor {@link WorkerOptions.authorizeFanOut}
|
|
1747
|
+
* is configured, naming a non-default shard (a potential cross-tenant hop)
|
|
1748
|
+
* or sending a fan-out envelope is rejected with a `403`
|
|
1749
|
+
* (`FORBIDDEN_SHARD`/`FORBIDDEN_FANOUT`). Set this to `true` to allow such
|
|
1750
|
+
* requests from any caller (including unauthenticated ones) — appropriate
|
|
1751
|
+
* only when every table is protected by per-row RLS. The runtime then emits
|
|
1752
|
+
* a single `console.warn` so the open posture stays visible in logs. Has no
|
|
1753
|
+
* effect once an `authorize*` callback is configured (those gate directly).
|
|
1754
|
+
*
|
|
1755
|
+
* NOTE: this is a behaviour change from earlier alphas, where the same
|
|
1756
|
+
* situation was warn-once-then-allow. Apps that relied on client-chosen
|
|
1757
|
+
* shard keys without an `authorize*` callback must set this flag explicitly.
|
|
1651
1758
|
*/
|
|
1652
1759
|
allowUnauthenticatedShardAccess?: boolean;
|
|
1653
1760
|
/**
|
|
@@ -1814,6 +1921,17 @@ interface WorkerOptions {
|
|
|
1814
1921
|
*/
|
|
1815
1922
|
httpRouter?: HttpRouterLike;
|
|
1816
1923
|
/**
|
|
1924
|
+
* The declared identity claim contract (`defineIdentity(...)` from
|
|
1925
|
+
* `@lunora/server`), passed by the generated worker entry. When present, the
|
|
1926
|
+
* worker validates every `resolveIdentity` result against it at the trust
|
|
1927
|
+
* boundary (on the public data paths — RPC / WebSocket / HTTP-action /
|
|
1928
|
+
* server-query, never the admin path) *before* the claims become `ctx.auth`.
|
|
1929
|
+
* A resolver output that violates the contract is downgraded to anonymous or
|
|
1930
|
+
* rejected with a `401`, per the contract's `onInvalid`. Omitted → no
|
|
1931
|
+
* validation, and the identity stays the historical untyped claim bag.
|
|
1932
|
+
*/
|
|
1933
|
+
identity?: IdentityContractLike;
|
|
1934
|
+
/**
|
|
1817
1935
|
* Insert `.global()` rows for the admin import endpoint. When omitted,
|
|
1818
1936
|
* rows targeting global tables are reported as hard errors.
|
|
1819
1937
|
*/
|
|
@@ -2423,4 +2541,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
|
|
|
2423
2541
|
*/
|
|
2424
2542
|
declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
|
|
2425
2543
|
declare const VERSION: string;
|
|
2426
|
-
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 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 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, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };
|
|
2544
|
+
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 };
|
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-M4mPqTJx.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,8 +7,9 @@ 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
|
+
export { composeIdentityResolvers, routeIdentityResolvers } from './packem_shared/composeIdentityResolvers-YjvUKisc.mjs';
|
|
12
13
|
|
|
13
14
|
const VERSION = "0.0.0";
|
|
14
15
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { LunoraError } from './LunoraError-CL0aOtpo.mjs';
|
|
2
|
+
|
|
3
|
+
const composeIdentityResolvers = (resolvers, options = {}) => {
|
|
4
|
+
const onError = options.onError ?? "fail-closed";
|
|
5
|
+
return async (request, env) => {
|
|
6
|
+
for (const resolver of resolvers) {
|
|
7
|
+
let resolved;
|
|
8
|
+
try {
|
|
9
|
+
resolved = await resolver(request, env);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (onError === "skip") {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
if (resolved) {
|
|
17
|
+
return resolved;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
const routeIdentityResolvers = (routes) => {
|
|
24
|
+
const prefixes = Object.keys(routes).filter((key) => key !== "*").toSorted((a, b) => b.length - a.length);
|
|
25
|
+
return (request, env) => {
|
|
26
|
+
const { pathname } = new URL(request.url);
|
|
27
|
+
const matched = prefixes.find((prefix) => pathname === prefix || pathname.startsWith(prefix.endsWith("/") ? prefix : `${prefix}/`));
|
|
28
|
+
const resolver = matched === void 0 ? routes["*"] : routes[matched];
|
|
29
|
+
if (resolver === void 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return resolver(request, env);
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const wrapResolverWithContract = (baseResolveIdentity, contract) => {
|
|
36
|
+
if (contract === void 0 || baseResolveIdentity === void 0) {
|
|
37
|
+
return baseResolveIdentity;
|
|
38
|
+
}
|
|
39
|
+
return async (request, env) => {
|
|
40
|
+
const resolved = await baseResolveIdentity(request, env);
|
|
41
|
+
if (!resolved) {
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
|
+
const result = contract.validate(resolved);
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
return resolved;
|
|
47
|
+
}
|
|
48
|
+
if (contract.onInvalid === "reject") {
|
|
49
|
+
throw new LunoraError(`identity claims failed the declared contract: ${result.error}`, { code: "UNAUTHENTICATED", status: 401 });
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export { composeIdentityResolvers, routeIdentityResolvers, wrapResolverWithContract };
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NOOP_EXECUTION_CONTEXT } from './NOOP_EXECUTION_CONTEXT-CCTu0Bf1.mjs';
|
|
2
2
|
import { LunoraError, toErrorResponse, isStructuralLunoraError, isStructuralConflictError } from './LunoraError-CL0aOtpo.mjs';
|
|
3
|
+
import { wrapResolverWithContract } from './composeIdentityResolvers-YjvUKisc.mjs';
|
|
4
|
+
export { composeIdentityResolvers, routeIdentityResolvers } from './composeIdentityResolvers-YjvUKisc.mjs';
|
|
3
5
|
import { emitRpcEvent } from './emitLogEvent-pEdtqAK8.mjs';
|
|
4
6
|
import { resolveShard, applyJurisdiction } from './applyJurisdiction-BkZtTkct.mjs';
|
|
5
|
-
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-
|
|
7
|
+
import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse, enforceWebSocketOrigin } from './decorateResponse-HRXo-oOD.mjs';
|
|
6
8
|
|
|
7
9
|
const RELAY_NAME_INFIX = "::relay::";
|
|
8
10
|
const relayName = (ownerKey, index) => `${ownerKey}${RELAY_NAME_INFIX}${String(index)}`;
|
|
@@ -248,15 +250,15 @@ const buildAuthAdminRoutes = (deps) => {
|
|
|
248
250
|
}
|
|
249
251
|
const candidate = error;
|
|
250
252
|
const code = typeof candidate.code === "string" ? candidate.code : "AUTH_ADMIN_ERROR";
|
|
251
|
-
|
|
252
|
-
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 });
|
|
253
255
|
}
|
|
254
256
|
};
|
|
255
257
|
const handle = async (request, descriptor) => {
|
|
258
|
+
deps.assertAdmin(request);
|
|
256
259
|
if (request.method !== descriptor.http) {
|
|
257
260
|
throw new LunoraError(`Auth admin endpoint requires ${descriptor.http}`, { code: "METHOD_NOT_ALLOWED", status: 405 });
|
|
258
261
|
}
|
|
259
|
-
deps.assertAdmin(request);
|
|
260
262
|
const admin = deps.getAuthAdmin();
|
|
261
263
|
if (admin === void 0) {
|
|
262
264
|
throw new LunoraError("auth endpoints require an `authAdmin` on the worker", { code: "AUTH_NOT_CONFIGURED", status: 400 });
|
|
@@ -1742,9 +1744,18 @@ const parseEnvelope = async (request) => {
|
|
|
1742
1744
|
throw new LunoraError("RPC `shardKey` must be a string", { code: "BAD_REQUEST", status: 400 });
|
|
1743
1745
|
}
|
|
1744
1746
|
const envelope = body;
|
|
1747
|
+
const fanOut = validateFanOut(envelope.fanOut);
|
|
1748
|
+
const args = envelope.args ?? {};
|
|
1749
|
+
if (fanOut && envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
1750
|
+
const requestedTable = args.table;
|
|
1751
|
+
if (typeof requestedTable === "string" && requestedTable !== fanOut.table) {
|
|
1752
|
+
throw new LunoraError("RPC `args.table` must match the authorized `fanOut.table` for a relation fan-out", { code: "BAD_REQUEST", status: 400 });
|
|
1753
|
+
}
|
|
1754
|
+
args.table = fanOut.table;
|
|
1755
|
+
}
|
|
1745
1756
|
return {
|
|
1746
|
-
args
|
|
1747
|
-
fanOut
|
|
1757
|
+
args,
|
|
1758
|
+
fanOut,
|
|
1748
1759
|
functionPath: envelope.functionPath,
|
|
1749
1760
|
shardKey: envelope.shardKey
|
|
1750
1761
|
};
|
|
@@ -1836,6 +1847,7 @@ const checkAdminWsToken = (request, expected) => {
|
|
|
1836
1847
|
};
|
|
1837
1848
|
const createWorker = (options) => {
|
|
1838
1849
|
const defaultShard = options.defaultShardKey ?? "__root__";
|
|
1850
|
+
const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
|
|
1839
1851
|
const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
|
|
1840
1852
|
const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
|
|
1841
1853
|
let envAdminToken;
|
|
@@ -1861,18 +1873,24 @@ const createWorker = (options) => {
|
|
|
1861
1873
|
}
|
|
1862
1874
|
return context;
|
|
1863
1875
|
};
|
|
1864
|
-
const hasAnyShardAuth = Boolean(options.authorizeShard) || Boolean(options.authorizeFanOut);
|
|
1865
1876
|
let warnedUnauthenticatedShardAccess = false;
|
|
1866
|
-
const
|
|
1867
|
-
if (
|
|
1877
|
+
const guardUnauthenticatedShardAccess = (kind) => {
|
|
1878
|
+
if (!options.allowUnauthenticatedShardAccess) {
|
|
1879
|
+
const callback = kind === "fan-out" ? "authorizeFanOut" : "authorizeShard";
|
|
1880
|
+
throw new LunoraError(
|
|
1881
|
+
`${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).`,
|
|
1882
|
+
{ code: kind === "fan-out" ? "FORBIDDEN_FANOUT" : "FORBIDDEN_SHARD", status: 403 }
|
|
1883
|
+
);
|
|
1884
|
+
}
|
|
1885
|
+
if (warnedUnauthenticatedShardAccess) {
|
|
1868
1886
|
return;
|
|
1869
1887
|
}
|
|
1870
1888
|
warnedUnauthenticatedShardAccess = true;
|
|
1871
1889
|
console.warn(
|
|
1872
1890
|
[
|
|
1873
|
-
`[lunora] SECURITY:
|
|
1891
|
+
`[lunora] SECURITY: serving ${kind} access with \`allowUnauthenticatedShardAccess: true\` and no \`authorizeShard\`/\`authorizeFanOut\` — `,
|
|
1874
1892
|
`any caller (including unauthenticated ones) can target any shard / fan out across the table. `,
|
|
1875
|
-
`
|
|
1893
|
+
`This is safe only if every table is protected by per-row RLS. Configure \`authorizeShard\`/\`authorizeFanOut\` to gate it.`
|
|
1876
1894
|
].join("")
|
|
1877
1895
|
);
|
|
1878
1896
|
};
|
|
@@ -1884,20 +1902,20 @@ const createWorker = (options) => {
|
|
|
1884
1902
|
resolveForwardContext: resolveAdminForwardContext,
|
|
1885
1903
|
shardDO
|
|
1886
1904
|
});
|
|
1887
|
-
const dispatchToShard = async (functionPath, args, shardKey) => {
|
|
1905
|
+
const dispatchToShard = async (functionPath, args, shardKey, mutationId) => {
|
|
1888
1906
|
if (options.authorizeShard) {
|
|
1889
1907
|
const allowed = await options.authorizeShard(null, shardKey);
|
|
1890
1908
|
if (!allowed) {
|
|
1891
1909
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
1892
1910
|
}
|
|
1893
1911
|
}
|
|
1912
|
+
const headers = { "content-type": "application/json", "x-lunora-system": "1" };
|
|
1913
|
+
if (mutationId !== void 0 && mutationId.length > 0) {
|
|
1914
|
+
headers["x-lunora-mutation-id"] = mutationId;
|
|
1915
|
+
}
|
|
1894
1916
|
const forwarded = new Request("https://shard.internal/rpc", {
|
|
1895
|
-
// `x-lunora-system` marks this as a trusted server-initiated dispatch
|
|
1896
|
-
// so the shard may run `internal` functions (scheduled/cron jobs are
|
|
1897
|
-
// typically internal). Authorization was already enforced above; this
|
|
1898
|
-
// header is set only here, never on the client RPC path.
|
|
1899
1917
|
body: JSON.stringify({ args, functionPath }),
|
|
1900
|
-
headers
|
|
1918
|
+
headers,
|
|
1901
1919
|
method: "POST"
|
|
1902
1920
|
});
|
|
1903
1921
|
return forwardToShard(shardDO, shardKey, forwarded);
|
|
@@ -2013,7 +2031,8 @@ const createWorker = (options) => {
|
|
|
2013
2031
|
}
|
|
2014
2032
|
const args = candidate.args ?? {};
|
|
2015
2033
|
const shardKey = typeof candidate.shardKey === "string" && candidate.shardKey.length > 0 ? candidate.shardKey : defaultShard;
|
|
2016
|
-
const
|
|
2034
|
+
const mutationId = typeof candidate.id === "string" && candidate.id.length > 0 ? candidate.id : void 0;
|
|
2035
|
+
const response = await dispatchToShard(candidate.functionPath, args, shardKey, mutationId);
|
|
2017
2036
|
await releasePoolSlot(candidate);
|
|
2018
2037
|
return response;
|
|
2019
2038
|
};
|
|
@@ -2108,7 +2127,7 @@ const createWorker = (options) => {
|
|
|
2108
2127
|
requireAdminOption
|
|
2109
2128
|
});
|
|
2110
2129
|
const buildHttpActionContext = async (request, env) => {
|
|
2111
|
-
const { claims, headers, userId } = await resolveForwardContext(request, env,
|
|
2130
|
+
const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2112
2131
|
const run = async (reference, args = {}) => {
|
|
2113
2132
|
const functionPath = reference.__lunoraRef;
|
|
2114
2133
|
if (typeof functionPath !== "string") {
|
|
@@ -2156,15 +2175,19 @@ const createWorker = (options) => {
|
|
|
2156
2175
|
if (request.headers.get("Upgrade") !== "websocket") {
|
|
2157
2176
|
throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
|
|
2158
2177
|
}
|
|
2178
|
+
const blockedUpgrade = enforceWebSocketOrigin(request, resolvedSecurity);
|
|
2179
|
+
if (blockedUpgrade) {
|
|
2180
|
+
return blockedUpgrade;
|
|
2181
|
+
}
|
|
2159
2182
|
const shardKey = url.searchParams.get("shard") ?? defaultShard;
|
|
2160
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2183
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2161
2184
|
if (options.authorizeShard) {
|
|
2162
2185
|
const allowed = await options.authorizeShard(identity, shardKey);
|
|
2163
2186
|
if (!allowed) {
|
|
2164
2187
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2165
2188
|
}
|
|
2166
2189
|
} else if (shardKey !== defaultShard) {
|
|
2167
|
-
|
|
2190
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2168
2191
|
}
|
|
2169
2192
|
const upgradeHeaders = new Headers(request.headers);
|
|
2170
2193
|
upgradeHeaders.delete("x-lunora-userid");
|
|
@@ -2193,29 +2216,34 @@ const createWorker = (options) => {
|
|
|
2193
2216
|
}
|
|
2194
2217
|
return forwardToShard(shardDO, shardKey, new Request(request, { headers: upgradeHeaders }));
|
|
2195
2218
|
};
|
|
2219
|
+
const authorizeFanOutEnvelope = async (fanOut, functionPath, identity) => {
|
|
2220
|
+
if (options.authorizeFanOut) {
|
|
2221
|
+
const allowed = await options.authorizeFanOut(identity, fanOut.table, functionPath);
|
|
2222
|
+
if (!allowed) {
|
|
2223
|
+
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2224
|
+
}
|
|
2225
|
+
return;
|
|
2226
|
+
}
|
|
2227
|
+
if (functionPath.startsWith("__lunora_relation__:")) {
|
|
2228
|
+
throw new LunoraError("reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker", {
|
|
2229
|
+
code: "FORBIDDEN_FANOUT",
|
|
2230
|
+
status: 403
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
if (options.authorizeShard) {
|
|
2234
|
+
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2235
|
+
code: "FORBIDDEN_FANOUT",
|
|
2236
|
+
status: 403
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
guardUnauthenticatedShardAccess("fan-out");
|
|
2240
|
+
};
|
|
2196
2241
|
const authorizeRpcEnvelope = async (envelope, identity) => {
|
|
2242
|
+
if (!envelope.fanOut && envelope.functionPath.startsWith("__lunora_admin__:")) {
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2197
2245
|
if (envelope.fanOut) {
|
|
2198
|
-
|
|
2199
|
-
const allowed = await options.authorizeFanOut(identity, envelope.fanOut.table, envelope.functionPath);
|
|
2200
|
-
if (!allowed) {
|
|
2201
|
-
throw new LunoraError("Forbidden fan-out", { code: "FORBIDDEN_FANOUT", status: 403 });
|
|
2202
|
-
}
|
|
2203
|
-
} else if (envelope.functionPath.startsWith("__lunora_relation__:")) {
|
|
2204
|
-
throw new LunoraError(
|
|
2205
|
-
"reverse cross-backend relation reads (`__lunora_relation__:*`) require `authorizeFanOut` to be configured on the worker",
|
|
2206
|
-
{
|
|
2207
|
-
code: "FORBIDDEN_FANOUT",
|
|
2208
|
-
status: 403
|
|
2209
|
-
}
|
|
2210
|
-
);
|
|
2211
|
-
} else if (options.authorizeShard) {
|
|
2212
|
-
throw new LunoraError("Fan-out requires `authorizeFanOut` to be configured on the worker when `authorizeShard` is set", {
|
|
2213
|
-
code: "FORBIDDEN_FANOUT",
|
|
2214
|
-
status: 403
|
|
2215
|
-
});
|
|
2216
|
-
} else {
|
|
2217
|
-
warnUnauthenticatedShardAccessOnce("fan-out");
|
|
2218
|
-
}
|
|
2246
|
+
await authorizeFanOutEnvelope(envelope.fanOut, envelope.functionPath, identity);
|
|
2219
2247
|
return;
|
|
2220
2248
|
}
|
|
2221
2249
|
if (options.authorizeShard) {
|
|
@@ -2225,7 +2253,7 @@ const createWorker = (options) => {
|
|
|
2225
2253
|
throw new LunoraError("Forbidden shard", { code: "FORBIDDEN_SHARD", status: 403 });
|
|
2226
2254
|
}
|
|
2227
2255
|
} else if (envelope.shardKey !== void 0 && envelope.shardKey !== defaultShard) {
|
|
2228
|
-
|
|
2256
|
+
guardUnauthenticatedShardAccess("shard");
|
|
2229
2257
|
}
|
|
2230
2258
|
};
|
|
2231
2259
|
const dispatchSingleShard = async (functionPath, args, shardKey, forwardedHeaders, sinkContext) => {
|
|
@@ -2282,7 +2310,7 @@ const createWorker = (options) => {
|
|
|
2282
2310
|
status: 400
|
|
2283
2311
|
});
|
|
2284
2312
|
}
|
|
2285
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2313
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2286
2314
|
await authorizeRpcEnvelope(envelope, identity);
|
|
2287
2315
|
{
|
|
2288
2316
|
const rpcStartedAt = Date.now();
|
|
@@ -2457,7 +2485,7 @@ const createWorker = (options) => {
|
|
|
2457
2485
|
if (typeof functionPath !== "string") {
|
|
2458
2486
|
throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
|
|
2459
2487
|
}
|
|
2460
|
-
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env,
|
|
2488
|
+
const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
|
|
2461
2489
|
await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
|
|
2462
2490
|
const shardKey = callOptions.shardKey ?? defaultShard;
|
|
2463
2491
|
return await dispatchSingleShard(functionPath, args, shardKey, forwardedHeaders);
|
|
@@ -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 };
|