@lunora/runtime 1.0.0-alpha.12 → 1.0.0-alpha.13

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 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
@@ -1223,35 +1333,6 @@ interface HttpRouterLike {
1223
1333
  fetch(request: Request, env?: unknown, context?: ExecutionContextLike): Promise<Response> | Response;
1224
1334
  }
1225
1335
  /**
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
1336
  * Per-table sharding metadata the admin import endpoint needs to route rows.
1256
1337
  * Structural so this package stays free of `@lunora/server`. The codegen-
1257
1338
  * generated worker entry passes a thin projection of the user's schema.
@@ -1814,6 +1895,17 @@ interface WorkerOptions {
1814
1895
  */
1815
1896
  httpRouter?: HttpRouterLike;
1816
1897
  /**
1898
+ * The declared identity claim contract (`defineIdentity(...)` from
1899
+ * `@lunora/server`), passed by the generated worker entry. When present, the
1900
+ * worker validates every `resolveIdentity` result against it at the trust
1901
+ * boundary (on the public data paths — RPC / WebSocket / HTTP-action /
1902
+ * server-query, never the admin path) *before* the claims become `ctx.auth`.
1903
+ * A resolver output that violates the contract is downgraded to anonymous or
1904
+ * rejected with a `401`, per the contract's `onInvalid`. Omitted → no
1905
+ * validation, and the identity stays the historical untyped claim bag.
1906
+ */
1907
+ identity?: IdentityContractLike;
1908
+ /**
1817
1909
  * Insert `.global()` rows for the admin import endpoint. When omitted,
1818
1910
  * rows targeting global tables are reported as hard errors.
1819
1911
  */
@@ -2423,4 +2515,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
2423
2515
  */
2424
2516
  declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
2425
2517
  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 };
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 };
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
@@ -1223,35 +1333,6 @@ interface HttpRouterLike {
1223
1333
  fetch(request: Request, env?: unknown, context?: ExecutionContextLike): Promise<Response> | Response;
1224
1334
  }
1225
1335
  /**
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
1336
  * Per-table sharding metadata the admin import endpoint needs to route rows.
1256
1337
  * Structural so this package stays free of `@lunora/server`. The codegen-
1257
1338
  * generated worker entry passes a thin projection of the user's schema.
@@ -1814,6 +1895,17 @@ interface WorkerOptions {
1814
1895
  */
1815
1896
  httpRouter?: HttpRouterLike;
1816
1897
  /**
1898
+ * The declared identity claim contract (`defineIdentity(...)` from
1899
+ * `@lunora/server`), passed by the generated worker entry. When present, the
1900
+ * worker validates every `resolveIdentity` result against it at the trust
1901
+ * boundary (on the public data paths — RPC / WebSocket / HTTP-action /
1902
+ * server-query, never the admin path) *before* the claims become `ctx.auth`.
1903
+ * A resolver output that violates the contract is downgraded to anonymous or
1904
+ * rejected with a `401`, per the contract's `onInvalid`. Omitted → no
1905
+ * validation, and the identity stays the historical untyped claim bag.
1906
+ */
1907
+ identity?: IdentityContractLike;
1908
+ /**
1817
1909
  * Insert `.global()` rows for the admin import endpoint. When omitted,
1818
1910
  * rows targeting global tables are reported as hard errors.
1819
1911
  */
@@ -2423,4 +2515,4 @@ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => Obse
2423
2515
  */
2424
2516
  declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
2425
2517
  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 };
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 };
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-BNYeYQqL.mjs';
2
+ export { composeWorker, createLunoraHandler, createWorker, defineRpcEnvelope, resolveLunoraOptions, withFrameworkWorker } from './packem_shared/composeWorker-BOB2YZ6v.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';
@@ -9,6 +9,7 @@ export { createQueryCoordinator, createStaticShardRegistry, mergeStrategyForAggr
9
9
  export { applyJurisdiction, resolveShard } from './packem_shared/applyJurisdiction-BkZtTkct.mjs';
10
10
  export { decorateResponse, enforceOrigin, handleCorsPreflight, resolveSecurity } from './packem_shared/decorateResponse-DbISh_Wi.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,5 +1,7 @@
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
7
  import { resolveSecurity, handleCorsPreflight, enforceOrigin, decorateResponse } from './decorateResponse-DbISh_Wi.mjs';
@@ -1836,6 +1838,7 @@ const checkAdminWsToken = (request, expected) => {
1836
1838
  };
1837
1839
  const createWorker = (options) => {
1838
1840
  const defaultShard = options.defaultShardKey ?? "__root__";
1841
+ const publicResolveIdentity = wrapResolverWithContract(options.resolveIdentity, options.identity);
1839
1842
  const shardDO = applyJurisdiction(options.shardDO, options.jurisdiction);
1840
1843
  const schedulerDO = options.schedulerDO === void 0 ? void 0 : applyJurisdiction(options.schedulerDO, options.jurisdiction);
1841
1844
  let envAdminToken;
@@ -2108,7 +2111,7 @@ const createWorker = (options) => {
2108
2111
  requireAdminOption
2109
2112
  });
2110
2113
  const buildHttpActionContext = async (request, env) => {
2111
- const { claims, headers, userId } = await resolveForwardContext(request, env, options.resolveIdentity);
2114
+ const { claims, headers, userId } = await resolveForwardContext(request, env, publicResolveIdentity);
2112
2115
  const run = async (reference, args = {}) => {
2113
2116
  const functionPath = reference.__lunoraRef;
2114
2117
  if (typeof functionPath !== "string") {
@@ -2157,7 +2160,7 @@ const createWorker = (options) => {
2157
2160
  throw new LunoraError("WebSocket upgrade header missing", { code: "BAD_REQUEST", status: 426 });
2158
2161
  }
2159
2162
  const shardKey = url.searchParams.get("shard") ?? defaultShard;
2160
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2163
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2161
2164
  if (options.authorizeShard) {
2162
2165
  const allowed = await options.authorizeShard(identity, shardKey);
2163
2166
  if (!allowed) {
@@ -2282,7 +2285,7 @@ const createWorker = (options) => {
2282
2285
  status: 400
2283
2286
  });
2284
2287
  }
2285
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2288
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2286
2289
  await authorizeRpcEnvelope(envelope, identity);
2287
2290
  {
2288
2291
  const rpcStartedAt = Date.now();
@@ -2457,7 +2460,7 @@ const createWorker = (options) => {
2457
2460
  if (typeof functionPath !== "string") {
2458
2461
  throw new LunoraError("serverQuery: expected a function reference from the generated `api`", { code: "BAD_REQUEST", status: 400 });
2459
2462
  }
2460
- const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, options.resolveIdentity);
2463
+ const { headers: forwardedHeaders, identity } = await resolveForwardContext(request, env, publicResolveIdentity);
2461
2464
  await authorizeRpcEnvelope({ functionPath, shardKey: callOptions.shardKey }, identity);
2462
2465
  const shardKey = callOptions.shardKey ?? defaultShard;
2463
2466
  return await dispatchSingleShard(functionPath, args, shardKey, forwardedHeaders);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/runtime",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "Lunora Worker runtime: the RPC router, shard resolver, and query coordinator",
5
5
  "keywords": [
6
6
  "cloudflare",