@lunora/server 1.0.0-alpha.55 → 1.0.0-alpha.57

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.
Files changed (28) hide show
  1. package/dist/data-model.d.mts +21 -0
  2. package/dist/data-model.d.ts +21 -0
  3. package/dist/index.d.mts +708 -528
  4. package/dist/index.d.ts +708 -528
  5. package/dist/index.mjs +1 -1
  6. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-DO2-TdzC.mjs +1 -0
  7. package/dist/packem_shared/asBucketStorage-BthCnWop.mjs +1 -0
  8. package/dist/packem_shared/buildMaskRegistry-BCIzKxdK.mjs +1 -0
  9. package/dist/packem_shared/{buildRlsReadRegistry-WdiqSj87.mjs → buildRlsReadRegistry-CA-aUi7I.mjs} +1 -1
  10. package/dist/packem_shared/composePluginMiddleware-B67TQ409.mjs +1 -0
  11. package/dist/packem_shared/defineAggregateIndex-DMhN3nby.mjs +1 -0
  12. package/dist/packem_shared/initLunora-DXCOVntr.mjs +1 -0
  13. package/dist/packem_shared/mask-DF7mtxQe.mjs +1 -0
  14. package/dist/packem_shared/optional-writer-override-CXXO30sZ.mjs +1 -0
  15. package/dist/packem_shared/plugin-B5pDeN17.mjs +1 -0
  16. package/dist/packem_shared/rls-tRDkICWZ.mjs +1 -0
  17. package/dist/rls/testing.mjs +1 -1
  18. package/dist/types.d.mts +38 -9
  19. package/dist/types.d.ts +38 -9
  20. package/dist/types.mjs +1 -1
  21. package/package.json +4 -4
  22. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-C9RX3Cl6.mjs +0 -1
  23. package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +0 -1
  24. package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +0 -1
  25. package/dist/packem_shared/defineAggregateIndex-_gWNmkQZ.mjs +0 -1
  26. package/dist/packem_shared/initLunora-QNqOuG0A.mjs +0 -1
  27. package/dist/packem_shared/mask-C6Bi78qj.mjs +0 -1
  28. package/dist/packem_shared/rls-_iVsPvhX.mjs +0 -1
package/dist/index.d.mts CHANGED
@@ -10,19 +10,21 @@ import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D
10
10
  export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-C4CMJK8x.mjs";
11
11
  export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
12
12
  /**
13
- * Make any `config.storage` result bucket-aware so `ctx.storage.bucket(name)`
13
+ * Make any resolved storage capability bucket-aware so `ctx.storage.bucket(name)`
14
14
  * always resolves. A `createBucketStorage(...)` result already carries
15
15
  * `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
16
16
  * (or the no-storage stub) is tagged as the `"default"` bucket, where
17
17
  * `.bucket(name)` is the identity — single-bucket apps address one binding under
18
18
  * every name.
19
19
  *
20
- * This is the runtime counterpart the generated `_generated/shard.ts` imports to
21
- * wrap `ctx.storage`; it lives here (the single source) rather than being stamped
22
- * inline into every generated file, so the bucket-tagging behaviour has one home
23
- * alongside the storage ctx types. The input is genuinely heterogeneous (a thunk
24
- * result cast through `unknown`), so the signature is `unknown unknown`; the
25
- * generated caller casts the result to its storage type.
20
+ * Lives here rather than in `@lunora/server` because two packages need it and
21
+ * neither may depend on the other: `@lunora/server` re-exports it as the runtime
22
+ * counterpart `_generated/shard.ts` imports, and `@lunora/runtime` uses it to
23
+ * build `ctx.storage` for an HTTP action from the worker's own R2 bindings.
24
+ * Inlined into each `dist` by the bundler, so no dependency edge is created.
25
+ *
26
+ * The input is genuinely heterogeneous (a thunk result cast through `unknown`),
27
+ * so the signature is `unknown → unknown`; callers cast the result.
26
28
  */
27
29
  declare const asBucketStorage: (raw: unknown) => unknown;
28
30
  /** Builder discriminator. Codegen reads this kind. */
@@ -583,18 +585,29 @@ type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PU
583
585
  /**
584
586
  * Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
585
587
  * HTTP actions run in the worker (the "action runtime"), separate from the
586
- * transactional store, so there is no direct `db` / `vectors` / `storage`
587
- * surface — reach the data layer through `runQuery` / `runMutation` /
588
- * `runAction`, which forward to the owning shard.
588
+ * transactional store, so there is no direct `db` / `vectors` surface — reach the
589
+ * data layer through `runQuery` / `runMutation` / `runAction`, which forward to
590
+ * the owning shard. `db`'s absence is principled: an HTTP handler is not
591
+ * transactional.
592
+ *
593
+ * `scheduler` and `storage` ARE present, because neither needs the shard — the
594
+ * scheduler talks to the scheduler DO, and R2 is a worker binding an HTTP
595
+ * handler can reach where an action does. Both are optional: each exists only
596
+ * when the app declared the matching capability (`.scheduler(...)` /
597
+ * `.storage(...)`) on the generated app builder.
589
598
  *
590
- * `scheduler` IS present (it talks to the scheduler DO, not the shard) but is
591
- * optional: it exists only when the app declared `.scheduler(...)` on the
592
- * generated app builder. "Receive webhook enqueue the real work return 200"
593
- * is what HTTP actions are for, so omitting it forced every app to hand-roll a
594
- * hop through a mutation plus a closed allow-list of target strings.
599
+ * Omitting them was costly out of proportion to the gap. Without `scheduler`,
600
+ * "receive webhook enqueue the real work return 200" — the shape HTTP
601
+ * actions exist for forced a hop through a mutation plus a closed allow-list
602
+ * of target strings, because a function reference cannot cross the RPC boundary
603
+ * and a free-form target on an unauthenticated endpoint is a "call any internal
604
+ * function" primitive. Without `storage`, any helper the ctx was threaded into
605
+ * had to be typed for its storage-touching branch, so a handler was barred from
606
+ * the helper even on the branches that never went near storage.
595
607
  */
596
608
  type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery"> & {
597
609
  readonly scheduler?: ActionCtx["scheduler"];
610
+ readonly storage?: ActionCtx["storage"];
598
611
  };
599
612
  /** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
600
613
  type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
@@ -967,456 +980,197 @@ declare const clampLimit: (limit: number | undefined, fallback: number, maxLimit
967
980
  */
968
981
  declare const defineListArgs: <TDocument>() => <F extends ListFilterShape<TDocument>, O extends keyof TDocument & string>(config: DefineListArgsConfig<F, O>) => ListArgsSpec<TDocument, F, O>;
969
982
  /**
970
- * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
971
- * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one same identity
972
- * resolver, same `can(...)` permission check so an author can branch a mask
973
- * on the caller's role/permission. `row` is the full pre-mask row the column
974
- * belongs to; `column` is the column currently being masked. Both are absent
975
- * when the context is used for the procedure-wide `bypass` check (no specific
976
- * cell is in play yet).
977
- */
978
- interface MaskContext<Context = unknown> {
979
- readonly auth: {
980
- /** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
981
- readonly can: (permission: Permission | string) => boolean;
982
- readonly identity?: Record<string, unknown> | null;
983
- readonly roles: ReadonlyArray<string>;
984
- readonly userId: null | string;
985
- };
986
- /** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
987
- readonly column?: string;
988
- readonly ctx: Context;
989
- /** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
990
- readonly row?: Record<string, unknown>;
983
+ * Structural mirrors of `@lunora/shard-engine`'s rank-page-row shapes
984
+ * (`RankPageRowKey` / `RankPageRow` / `ShardRankPageResult`)the return type
985
+ * of the writer's `rankPageRows` seam, the cross-shard companion to
986
+ * `rankPage`.
987
+ *
988
+ * Shared by `../rls/middleware` and `../mask/middleware`: both wrap
989
+ * `rankPageRows` structurally (no `@lunora/shard-engine` import, mirroring how
990
+ * every other method on their `DatabaseWriterLike`/`MaskDatabase` projections
991
+ * is hand-mirrored rather than imported) and both need the exact same result
992
+ * shape to type their overrides. A single copy here means the two wrappers
993
+ * can't drift out of lockstep with each other see AGENTS.md's platform
994
+ * parity note on `ShardSqlExec` and the canonical binding `*Like` projections
995
+ * shipping wrong for exactly this reason (two hand-maintained mirrors of one
996
+ * upstream type).
997
+ */
998
+ /** Structural mirror of `@lunora/shard-engine`'s `RankPageRowKey`. */
999
+ interface RankPageRowKeyLike {
1000
+ partitionKey: string;
1001
+ rowId: string;
1002
+ sortValues: ReadonlyArray<unknown>;
1003
+ }
1004
+ /** Structural mirror of `@lunora/shard-engine`'s `RankPageRow`. */
1005
+ interface RankPageRowLike {
1006
+ doc: Record<string, unknown>;
1007
+ key: RankPageRowKeyLike;
1008
+ }
1009
+ /** Structural mirror of `@lunora/shard-engine`'s `ShardRankPageResult` — the `rankPageRows` return shape. */
1010
+ interface ShardRankPageResultLike {
1011
+ directions: ReadonlyArray<"asc" | "desc">;
1012
+ hasMore: boolean;
1013
+ rows: ReadonlyArray<RankPageRowLike>;
991
1014
  }
992
1015
  /**
993
- * A custom masking function. Receives the raw cell value and the
994
- * {@link MaskContext}, returns the value to surface. Use it for partial masks
995
- * (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
996
- * or format-preserving tokens. A function that **throws** fails closed — the
997
- * cell is redacted to `null`, never leaked raw.
1016
+ * The prefixed tables a single plugin `P` contributes, or an empty map when it
1017
+ * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
1018
+ * so {@link InstalledTables} can fold a tuple of plugins.
998
1019
  */
999
- type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
1020
+ type ExtensionTablesOf<P> = P extends {
1021
+ readonly extension: SchemaExtension<infer X> & {
1022
+ readonly key: infer K;
1023
+ };
1024
+ } ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
1000
1025
  /**
1001
- * How a column is masked:
1002
- *
1003
- * - `"redact"` drop the value to `null`. The simplest, safest strategy, and
1004
- * the right choice for any value that must actually be kept secret.
1005
- * - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
1006
- * same input always yields the same token (joinable/groupable client-side).
1007
- * **This is NOT a confidentiality control.** It is a non-cryptographic,
1008
- * unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
1009
- * phone numbers, SSNs) are brute-force-recoverable by the very caller you are
1010
- * masking from, and identical values always produce identical tokens across
1011
- * rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
1012
- * stable pseudonym for grouping/joining and leaking the value is acceptable —
1013
- * never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
1014
- * - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
1026
+ * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
1027
+ * auto-prefixed extension tables left-to-right — the type-level mirror of
1028
+ * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
1015
1029
  */
1016
- type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
1017
- /** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
1018
- type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
1030
+ type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
1019
1031
  /**
1020
- * The mask declaration passed to `mask(...)`: a table column strategy map.
1021
- * Deliberately a plain object literal so the codegen feeder can statically read
1022
- * which columns a procedure masks (powering the `mask_uncovered_pii_column`
1023
- * advisor lint), exactly as the RLS feeder reads policy tables.
1032
+ * Union every plugin's `ContextOut` in a tuple the type-level mirror of the
1033
+ * `ctx.api.&lt;key>` additions {@link composePluginMiddleware} accumulates as each
1034
+ * plugin middleware runs. Independent of the incoming context, which the builder
1035
+ * infers at the `.use(...)` site.
1024
1036
  */
1025
- type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
1037
+ type ComposedOut<Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? ComposedOut<Rest> & (Head extends Plugin<any, any, infer Out> ? Out : unknown) : unknown;
1026
1038
  /**
1027
- * Options for `mask(policies, options)`.
1028
- *
1029
- * - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
1030
- * inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
1031
- * not listed grants no permissions (fails closed for unknown roles).
1032
- * - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
1033
- * mask is skipped (the caller sees raw values). Use it for a privileged
1034
- * viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
1035
- * branching every column when an entire class of caller should see clear data.
1039
+ * Schema fragment a plugin contributes. Same shape as the `tables` map
1040
+ * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
1041
+ * `defineSchema` argument so a plugin can ship vector decls alongside its
1042
+ * tables.
1036
1043
  */
1037
- interface MaskOptions<Context = unknown> {
1038
- readonly bypass?: (context: MaskContext<Context>) => boolean;
1039
- readonly roles?: ReadonlyArray<Role>;
1040
- }
1041
- interface QueryPage$1 {
1042
- continueCursor: null | string;
1043
- isDone: boolean;
1044
- page: Record<string, unknown>[];
1044
+ interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
1045
+ /** Stable key identifying the plugin that owns this extension. */
1046
+ readonly key: string;
1047
+ /**
1048
+ * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
1049
+ * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
1050
+ * collide with an app table; do **not** namespace manually.
1051
+ */
1052
+ readonly tables: T;
1053
+ /**
1054
+ * Optional standalone vector indexes the plugin ships, keyed by index
1055
+ * name. Merged into the host schema's `vectorIndexes`; a key collision
1056
+ * with the base schema is a hard error (same policy as tables).
1057
+ */
1058
+ readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
1045
1059
  }
1046
- interface QueryArgs$1 {
1047
- baseWhere?: unknown;
1048
- cursor?: null | string;
1049
- limit?: number;
1050
- orderBy?: ReadonlyArray<Record<string, unknown>>;
1051
- where?: unknown;
1052
- with?: Record<string, unknown>;
1060
+ /**
1061
+ * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
1062
+ * error messages on collision) and a type-level brand.
1063
+ */
1064
+ declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
1065
+ tables: T;
1066
+ vectorIndexes?: Record<string, VectorIndexDefinition>;
1067
+ }) => SchemaExtension<T>;
1068
+ /**
1069
+ * A plugin packages an optional schema extension and optional middleware.
1070
+ * Both are independently usable: an app can install only the schema (e.g.
1071
+ * for plugins that ship background workers but no per-request behavior)
1072
+ * or only the middleware (plugins that augment ctx without persistent
1073
+ * state).
1074
+ */
1075
+ interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
1076
+ /**
1077
+ * Optional schema extension. Apps install via
1078
+ * `defineSchema(...).extend(plugin.extension)`.
1079
+ */
1080
+ readonly extension?: SchemaExtension<TExtension>;
1081
+ /** Stable key identifying the plugin. Matches `extension.key` when set. */
1082
+ readonly key: string;
1083
+ /**
1084
+ * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
1085
+ * The middleware can extend `ctx`; convention is to attach helpers under
1086
+ * `ctx.api.&lt;key>`, e.g.
1087
+ *
1088
+ * ```ts
1089
+ * middleware: ({ ctx, next }) =>
1090
+ * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
1091
+ * ```
1092
+ */
1093
+ readonly middleware?: Middleware<TContextIn, TContextOut>;
1053
1094
  }
1054
- interface AggregateArgs$1 {
1055
- field?: string;
1056
- op: string;
1057
- where?: unknown;
1095
+ /** Options to {@link definePlugin}. */
1096
+ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut> {
1097
+ extension?: SchemaExtension<TExtension>;
1098
+ middleware?: Middleware<TContextIn, TContextOut>;
1058
1099
  }
1059
- interface GroupByArgs$1 {
1060
- agg?: {
1061
- field?: string;
1062
- op: string;
1100
+ /**
1101
+ * Call signatures for {@link definePlugin}. When `extension` is supplied the
1102
+ * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1103
+ * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1104
+ * typechecks without a non-null assertion — the shape every scaffold template
1105
+ * ships. The bare-options signature keeps `extension` optional for plugins
1106
+ * that carry only middleware.
1107
+ */
1108
+ interface DefinePluginFunction {
1109
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1110
+ extension: SchemaExtension<TExtension>;
1111
+ }): Plugin<TExtension, TContextIn, TContextOut> & {
1112
+ readonly extension: SchemaExtension<TExtension>;
1063
1113
  };
1064
- by: ReadonlyArray<string>;
1065
- where?: unknown;
1114
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1066
1115
  }
1067
- interface TableReaderLike$1 {
1068
- collect: () => Promise<Record<string, unknown>[]>;
1069
- filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike$1;
1070
- first: () => Promise<Record<string, unknown> | null>;
1071
- order: (direction: "asc" | "desc") => TableReaderLike$1;
1072
- paginate: (options: {
1073
- cursor?: null | string;
1074
- numItems: number;
1075
- }) => Promise<QueryPage$1>;
1076
- take: (limit: number) => Promise<Record<string, unknown>[]>;
1077
- unique: () => Promise<Record<string, unknown> | null>;
1078
- withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
1079
- withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
1080
- withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
1116
+ /**
1117
+ * Package a schema extension + middleware as a reusable plugin. Either
1118
+ * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1119
+ */
1120
+ declare const definePlugin: DefinePluginFunction;
1121
+ /**
1122
+ * Bundle of registered functions a {@link Component} ships. Keys are the
1123
+ * function's local name (e.g. `check`, `reset`); the registered function
1124
+ * value carries its own kind / args / handler.
1125
+ *
1126
+ * Users re-export from their own lunora module so codegen picks them up:
1127
+ *
1128
+ * ```ts
1129
+ * // lunora/ratelimit.ts
1130
+ * import { ratelimit } from "@vendor/ratelimit-component";
1131
+ * export const { check, reset } = ratelimit.functions;
1132
+ * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1133
+ * ```
1134
+ *
1135
+ * Codegen follows the re-export back to the bundled `query/mutation/action`
1136
+ * call (property access or destructuring both work), so the functions land in
1137
+ * the generated `api` under the re-exporting file's namespace.
1138
+ */
1139
+ type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
1140
+ /**
1141
+ * Component = {@link Plugin} with a bundle of registered functions. The
1142
+ * extension + middleware + functions are independent: a component can ship
1143
+ * functions without a schema (e.g. a stateless utility), or a schema
1144
+ * without functions (e.g. shared table definitions), and any combination.
1145
+ */
1146
+ interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
1147
+ readonly functions: F;
1148
+ }
1149
+ interface DefineComponentOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut, F extends ComponentFunctions> extends DefinePluginOptions<TExtension, TContextIn, TContextOut> {
1150
+ /** Registered functions the component ships. Keys are the function's local name. */
1151
+ functions?: F;
1081
1152
  }
1082
1153
  /**
1083
- * Structural projection of the runtime ORM writer the same subset
1084
- * `../rls/middleware` mirrors, so the wrapper is interchangeable between
1085
- * `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
1086
- * inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
1087
- */
1088
- interface MaskDatabase {
1089
- aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
1090
- count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
1091
- delete: (id: string, expectedTable?: string) => Promise<void>;
1092
- deleteMany: (ids: ReadonlyArray<string>, options?: {
1093
- limit?: number;
1094
- }) => Promise<{
1095
- deleted: number;
1096
- }>;
1097
- deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
1098
- limit?: number;
1099
- }) => Promise<{
1100
- deleted: number;
1101
- }>;
1102
- findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
1103
- findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
1104
- findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
1105
- get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
1106
- groupBy: (tableName: string, options: GroupByArgs$1) => Promise<ReadonlyArray<{
1107
- key: Record<string, unknown>;
1108
- value: null | number;
1109
- }>>;
1110
- insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1111
- insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1112
- limit?: number;
1113
- skipDuplicates?: boolean;
1114
- }) => Promise<(string | null)[]>;
1115
- lookupById?: (id: string, expectedTable?: string) => Promise<null | {
1116
- row: Record<string, unknown>;
1117
- tableName: string;
1118
- }>;
1119
- patch: (id: string, patch: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1120
- patchMany: (patches: ReadonlyArray<{
1121
- id: string;
1122
- patch: Record<string, unknown>;
1123
- }>, options?: {
1124
- limit?: number;
1125
- }) => Promise<{
1126
- patched: number;
1127
- }>;
1128
- patchWhere?: (tableName: string, args: {
1129
- patch: Record<string, unknown>;
1130
- where: Record<string, unknown>;
1131
- }, options?: {
1132
- limit?: number;
1133
- }) => Promise<{
1134
- patched: number;
1135
- }>;
1136
- query: (tableName: string) => TableReaderLike$1;
1137
- rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
1138
- position: number;
1139
- total: number;
1140
- }>;
1141
- rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
1142
- before: number;
1143
- total: number;
1144
- }>;
1145
- rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage$1>;
1146
- replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1147
- }
1148
- /** Roles list source on the context. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
1149
- type AuthLike$1 = {
1150
- getIdentity?: () => Promise<Record<string, unknown> | null>;
1151
- roles?: ReadonlyArray<string>;
1152
- userId?: null | string;
1153
- };
1154
- interface MaskContextIn {
1155
- auth?: AuthLike$1;
1156
- db: MaskDatabase;
1157
- }
1158
- /**
1159
- * Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
1160
- * Closes over the policy map at builder-construction time; resolves identity +
1161
- * the `bypass` decision per call against the live ctx.
1162
- *
1163
- * IMPORTANT: a mask is in scope only for procedures whose builder chain
1164
- * includes this middleware — opt-in, never global (the same invariant as RLS).
1165
- */
1166
- declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
1167
- /** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
1168
- type MigrationDocument = Record<string, unknown>;
1169
- /**
1170
- * Transform applied to one document. Return a new document to rewrite the row,
1171
- * or `undefined` to leave it untouched (skipped, not counted as changed). The
1172
- * runner always preserves the original `_id` and `_creationTime`, so the
1173
- * returned document neither needs to nor should change row identity.
1174
- */
1175
- type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
1176
- interface MigrationDefinition {
1177
- /** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
1178
- readonly batchSize?: number;
1179
- /** Optional reverse transform, applied by `migrate down`. */
1180
- readonly down?: MigrationTransform;
1181
- /** Stable, unique identifier — the key per-shard run-state is tracked under. */
1182
- readonly id: string;
1183
- /** Table whose documents this migration iterates. */
1184
- readonly table: string;
1185
- /** Forward transform, applied to every row by `migrate up`. */
1186
- readonly up: MigrationTransform;
1187
- }
1188
- /** A {@link MigrationDefinition} plus the codegen discovery marker. */
1189
- interface RegisteredMigration extends MigrationDefinition {
1190
- readonly __lunoraMigration: true;
1191
- }
1192
- /** Declare an online data migration. See the module docs for runtime semantics. */
1193
- declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
1194
- /**
1195
- * A mutator declaration. `server` is authoritative; `client` is the optimistic
1196
- * twin (optional — omit it to let the optimistic write fall through to the
1197
- * server round-trip with no local preview). Both receive the same validated
1198
- * `args`.
1199
- */
1200
- interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
1201
- /**
1202
- * Validator for the mutator's arguments. Validated on the DO before `server`
1203
- * runs and (when present) on the client before `client` runs, so both impls
1204
- * see the same parsed shape. Omit for a parameterless mutator.
1205
- */
1206
- readonly args?: Args;
1207
- /**
1208
- * Optimistic client implementation. Runs in a TanStack DB transaction
1209
- * against the local collections; its writes are applied immediately and
1210
- * automatically rolled back / rebased as the authoritative result syncs
1211
- * back. Pure and side-effect-free beyond the local store. Omit to skip the
1212
- * local preview.
1213
- */
1214
- readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
1215
- /**
1216
- * Owner-scope the write: names the column carrying the row owner (e.g.
1217
- * `owner: "userId"`). Before `server` runs, the mutator requires a verified
1218
- * identity, rejects a client-supplied owner that disagrees with it, and sets
1219
- * the column to the verified value — so the impl reads `args[owner]` without
1220
- * trusting the client and never repeats the check by hand.
1221
- *
1222
- * This replaces the "every mutator opens with `assertOwner(ctx, args.userId)`"
1223
- * pattern, and is the write-side counterpart to an `owner`-scoped
1224
- * {@link import("./shapes").ShapeDefinition}. Unlike a shape it takes the column
1225
- * NAME rather than `true`: a shape is bound to one `table`, so the table's
1226
- * `.ownedBy(field)` resolves unambiguously, whereas one mutator may write
1227
- * several tables and has no single owning table to read it from.
1228
- *
1229
- * Declare the column `v.optional(...)` to leave it off the wire entirely; it is
1230
- * injected either way.
1231
- */
1232
- readonly owner?: string;
1233
- /**
1234
- * Authoritative server implementation. Runs inside the shard DO with a full
1235
- * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
1236
- * and poke back to subscribers. This is the source of truth — the client
1237
- * impl is only a prediction of it.
1238
- */
1239
- readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
1240
- }
1241
- /**
1242
- * A {@link MutatorDefinition} plus the codegen discovery marker and a
1243
- * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
1244
- * invokes a mutator exactly like a registered procedure.
1245
- */
1246
- interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
1247
- readonly __lunoraMutator: true;
1248
- /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
1249
- readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
1250
- /**
1251
- * Marks the dispatch kind so codegen can register the mutator in the same
1252
- * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
1253
- * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
1254
- * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
1255
- * ordinary `mutation`.
1256
- */
1257
- readonly kind: "mutation";
1258
- }
1259
- /** Declare a custom mutator. See the module docs for runtime semantics. */
1260
- declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
1261
- /**
1262
- * The prefixed tables a single plugin `P` contributes, or an empty map when it
1263
- * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
1264
- * so {@link InstalledTables} can fold a tuple of plugins.
1265
- */
1266
- type ExtensionTablesOf<P> = P extends {
1267
- readonly extension: SchemaExtension<infer X> & {
1268
- readonly key: infer K;
1269
- };
1270
- } ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
1271
- /**
1272
- * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
1273
- * auto-prefixed extension tables left-to-right — the type-level mirror of
1274
- * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
1275
- */
1276
- type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
1277
- /**
1278
- * Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
1279
- * `ctx.api.&lt;key>` additions {@link composePluginMiddleware} accumulates as each
1280
- * plugin middleware runs. Independent of the incoming context, which the builder
1281
- * infers at the `.use(...)` site.
1282
- */
1283
- type ComposedOut<Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? ComposedOut<Rest> & (Head extends Plugin<any, any, infer Out> ? Out : unknown) : unknown;
1284
- /**
1285
- * Schema fragment a plugin contributes. Same shape as the `tables` map
1286
- * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
1287
- * `defineSchema` argument so a plugin can ship vector decls alongside its
1288
- * tables.
1289
- */
1290
- interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
1291
- /** Stable key identifying the plugin that owns this extension. */
1292
- readonly key: string;
1293
- /**
1294
- * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
1295
- * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
1296
- * collide with an app table; do **not** namespace manually.
1297
- */
1298
- readonly tables: T;
1299
- /**
1300
- * Optional standalone vector indexes the plugin ships, keyed by index
1301
- * name. Merged into the host schema's `vectorIndexes`; a key collision
1302
- * with the base schema is a hard error (same policy as tables).
1303
- */
1304
- readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
1305
- }
1306
- /**
1307
- * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
1308
- * error messages on collision) and a type-level brand.
1309
- */
1310
- declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
1311
- tables: T;
1312
- vectorIndexes?: Record<string, VectorIndexDefinition>;
1313
- }) => SchemaExtension<T>;
1314
- /**
1315
- * A plugin packages an optional schema extension and optional middleware.
1316
- * Both are independently usable: an app can install only the schema (e.g.
1317
- * for plugins that ship background workers but no per-request behavior)
1318
- * or only the middleware (plugins that augment ctx without persistent
1319
- * state).
1320
- */
1321
- interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
1322
- /**
1323
- * Optional schema extension. Apps install via
1324
- * `defineSchema(...).extend(plugin.extension)`.
1325
- */
1326
- readonly extension?: SchemaExtension<TExtension>;
1327
- /** Stable key identifying the plugin. Matches `extension.key` when set. */
1328
- readonly key: string;
1329
- /**
1330
- * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
1331
- * The middleware can extend `ctx`; convention is to attach helpers under
1332
- * `ctx.api.&lt;key>`, e.g.
1333
- *
1334
- * ```ts
1335
- * middleware: ({ ctx, next }) =>
1336
- * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
1337
- * ```
1338
- */
1339
- readonly middleware?: Middleware<TContextIn, TContextOut>;
1340
- }
1341
- /** Options to {@link definePlugin}. */
1342
- interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut> {
1343
- extension?: SchemaExtension<TExtension>;
1344
- middleware?: Middleware<TContextIn, TContextOut>;
1345
- }
1346
- /**
1347
- * Call signatures for {@link definePlugin}. When `extension` is supplied the
1348
- * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1349
- * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1350
- * typechecks without a non-null assertion — the shape every scaffold template
1351
- * ships. The bare-options signature keeps `extension` optional for plugins
1352
- * that carry only middleware.
1353
- */
1354
- interface DefinePluginFunction {
1355
- <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1356
- extension: SchemaExtension<TExtension>;
1357
- }): Plugin<TExtension, TContextIn, TContextOut> & {
1358
- readonly extension: SchemaExtension<TExtension>;
1359
- };
1360
- <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1361
- }
1362
- /**
1363
- * Package a schema extension + middleware as a reusable plugin. Either
1364
- * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1365
- */
1366
- declare const definePlugin: DefinePluginFunction;
1367
- /**
1368
- * Bundle of registered functions a {@link Component} ships. Keys are the
1369
- * function's local name (e.g. `check`, `reset`); the registered function
1370
- * value carries its own kind / args / handler.
1371
- *
1372
- * Users re-export from their own lunora module so codegen picks them up:
1373
- *
1374
- * ```ts
1375
- * // lunora/ratelimit.ts
1376
- * import { ratelimit } from "@vendor/ratelimit-component";
1377
- * export const { check, reset } = ratelimit.functions;
1378
- * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1379
- * ```
1380
- *
1381
- * Codegen follows the re-export back to the bundled `query/mutation/action`
1382
- * call (property access or destructuring both work), so the functions land in
1383
- * the generated `api` under the re-exporting file's namespace.
1384
- */
1385
- type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
1386
- /**
1387
- * Component = {@link Plugin} with a bundle of registered functions. The
1388
- * extension + middleware + functions are independent: a component can ship
1389
- * functions without a schema (e.g. a stateless utility), or a schema
1390
- * without functions (e.g. shared table definitions), and any combination.
1391
- */
1392
- interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
1393
- readonly functions: F;
1394
- }
1395
- interface DefineComponentOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut, F extends ComponentFunctions> extends DefinePluginOptions<TExtension, TContextIn, TContextOut> {
1396
- /** Registered functions the component ships. Keys are the function's local name. */
1397
- functions?: F;
1398
- }
1399
- /**
1400
- * Convenience wrapper around {@link definePlugin} that also bundles a set
1401
- * of registered functions. The resulting `component.functions` object is a
1402
- * record of `name → registered query/mutation/action`; consumers
1403
- * re-export entries so codegen discovers them as user functions:
1404
- *
1405
- * ```ts
1406
- * export const ratelimit = defineComponent("ratelimit", {
1407
- * // Bare `buckets` merges in as `ratelimit_buckets`.
1408
- * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1409
- * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1410
- * functions: {
1411
- * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1412
- * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1413
- * },
1414
- * });
1415
- * ```
1416
- *
1417
- * Re-exporting an entry (by property access or destructuring) is enough for
1418
- * codegen to discover it in the host app's namespace — the discovery resolver
1419
- * chases the re-export back to the bundled registration call.
1154
+ * Convenience wrapper around {@link definePlugin} that also bundles a set
1155
+ * of registered functions. The resulting `component.functions` object is a
1156
+ * record of `name registered query/mutation/action`; consumers
1157
+ * re-export entries so codegen discovers them as user functions:
1158
+ *
1159
+ * ```ts
1160
+ * export const ratelimit = defineComponent("ratelimit", {
1161
+ * // Bare `buckets` merges in as `ratelimit_buckets`.
1162
+ * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1163
+ * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1164
+ * functions: {
1165
+ * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1166
+ * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1167
+ * },
1168
+ * });
1169
+ * ```
1170
+ *
1171
+ * Re-exporting an entry (by property access or destructuring) is enough for
1172
+ * codegen to discover it in the host app's namespace — the discovery resolver
1173
+ * chases the re-export back to the bundled registration call.
1420
1174
  */
1421
1175
  declare const defineComponent: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions>(key: string, options: DefineComponentOptions<TExtension, TContextIn, TContextOut, F>) => Component<TExtension, TContextIn, TContextOut, F>;
1422
1176
  /**
@@ -1439,6 +1193,16 @@ type PrefixedTables<X extends Record<string, TableDefinition>, Key extends strin
1439
1193
  * collisions are impossible. The only remaining hard error is two extensions
1440
1194
  * sharing the same `key` and producing the same prefixed table (or vector
1441
1195
  * index) name — silent shadow would let one plugin hijack another's data.
1196
+ *
1197
+ * Re-runs {@link validateIndexFields} against the merged table set before
1198
+ * returning: `defineSchema` only validates the tables it was called with, so
1199
+ * without this an extension-contributed index with a typo'd/out-of-shape
1200
+ * field (or a duplicate name within one kind) would never be checked at all.
1201
+ * Re-validating the whole merged set (base + prefixed extension tables) is
1202
+ * cheap and idempotent for the base tables, which already passed this same
1203
+ * check when the base schema was built. Both callers of this function —
1204
+ * `withExtend.extend()` (`./schema`) and `installPlugins` (below) — get the
1205
+ * re-validation for free from this single call site (plan 258 §4/§9 Q3).
1442
1206
  */
1443
1207
  declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
1444
1208
  readonly key: Key;
@@ -1570,7 +1334,7 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1570
1334
  backend?: GlobalBackend;
1571
1335
  }) => TableBuilder<Shape>;
1572
1336
  /** Add a secondary index. */
1573
- index: (name: string, fields: ReadonlyArray<string>, options?: {
1337
+ index: (name: string, fields: ReadonlyArray<(keyof Shape & string) | (typeof SYSTEM_INDEX_FIELDS)[number]>, options?: {
1574
1338
  unique?: boolean;
1575
1339
  }) => TableBuilder<Shape>;
1576
1340
  /**
@@ -1692,101 +1456,510 @@ declare const defineTable: <Shape extends Record<string, Validator>>(inputShape:
1692
1456
  */
1693
1457
  declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
1694
1458
  /**
1695
- * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1696
- * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1697
- * the owning table explicitly via `on` — handy when a single counter wants to
1698
- * live next to the schema map rather than inside a table chain.
1459
+ * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1460
+ * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1461
+ * the owning table explicitly via `on` — handy when a single counter wants to
1462
+ * live next to the schema map rather than inside a table chain.
1463
+ */
1464
+ interface AggregateIndexOptions {
1465
+ by?: ReadonlyArray<string>;
1466
+ field?: string;
1467
+ on: string;
1468
+ op?: AggregateOp;
1469
+ where?: Record<string, unknown>;
1470
+ }
1471
+ /**
1472
+ * Declare a standalone aggregate index. Pass the returned value to
1473
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
1474
+ * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1475
+ * (DO + D1) read every index uniformly off the table definition.
1476
+ */
1477
+ declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
1478
+ /**
1479
+ * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1480
+ * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1481
+ * `table` so it can sit next to the schema map.
1482
+ */
1483
+ interface RankIndexOptions {
1484
+ partitionBy?: ReadonlyArray<string>;
1485
+ sortBy: ReadonlyArray<{
1486
+ direction?: "asc" | "desc";
1487
+ field: string;
1488
+ }>;
1489
+ table: string;
1490
+ where?: Record<string, unknown>;
1491
+ }
1492
+ /**
1493
+ * Declare a standalone rank index. Pass the returned value to
1494
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1495
+ * by index name — the schema attaches it to `tables[on].rankIndexes`.
1496
+ */
1497
+ declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
1498
+ /**
1499
+ * Build the application schema. The first argument is the table map; the
1500
+ * optional second argument registers standalone `defineVectorIndex(...)`
1501
+ * declarations (DSL Shape B) keyed by index name. The optional third argument
1502
+ * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1503
+ * the optional fourth argument registers standalone `defineRankIndex(...)`
1504
+ * declarations. Both are folded into the matching `tables[on].*Indexes` array
1505
+ * so runtime backends read every index uniformly off the table definition.
1506
+ */
1507
+ /**
1508
+ * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1509
+ * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1510
+ *
1511
+ * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1512
+ * the merged tables. Extension tables are auto-namespaced by the extension
1513
+ * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1514
+ * prefixed names via {@link PrefixedTables}. Chains:
1515
+ * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1516
+ * `a`'s prefixed tables then `b`'s.
1517
+ */
1518
+ type ExtendableSchema<T extends Record<string, TableDefinition>> = {
1519
+ extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
1520
+ readonly key: Key;
1521
+ }) => ExtendableSchema<PrefixedTables<X, Key> & T>;
1522
+ /**
1523
+ * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1524
+ * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1525
+ * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1526
+ * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1527
+ * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1528
+ * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1529
+ *
1530
+ * ⚠️ **Set this once, before your first deploy — changing or removing it
1531
+ * strands data.** A Durable Object name maps to a *different* ID in each
1532
+ * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1533
+ * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1534
+ * old jurisdiction's DOs and is no longer reachable. There is no in-place
1535
+ * migration — you would have to export from the old jurisdiction and import
1536
+ * into the new one.
1537
+ *
1538
+ * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1539
+ * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1540
+ * settings, not this option.
1541
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1542
+ */
1543
+ jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
1544
+ /**
1545
+ * Turn on secure-by-default RLS for the whole schema. Every table is then
1546
+ * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1547
+ * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1548
+ * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1549
+ * mode, so `.rls("required")` composes with `.extend(...)` either order.
1550
+ */
1551
+ rls: (mode: "required") => ExtendableSchema<T>;
1552
+ } & Schema<T>;
1553
+ /**
1554
+ * Columns every row carries implicitly (never part of a table's declared
1555
+ * `shape`), so `.index()` may legitimately name them. The single source for
1556
+ * both the compile-time allow-list (`TableBuilder["index"]`'s `fields` type,
1557
+ * via `(typeof SYSTEM_INDEX_FIELDS)[number]`) and the runtime cross-check
1558
+ * below (via `SYSTEM_INDEX_FIELDS_SET`) — declared once so the two can't
1559
+ * drift apart.
1560
+ */
1561
+ declare const SYSTEM_INDEX_FIELDS: readonly ["_creationTime", "_id"];
1562
+ /**
1563
+ * Per-table, per-KIND index→declared-fields map: for each table, each index
1564
+ * KIND (`index` | `rank` | `geo`) that has at least one declared index maps
1565
+ * to a name→fields record for that kind only. Distilled by
1566
+ * {@link indexFieldsFromSchema}; this is the shape `mask()`'s
1567
+ * `MaskOptions.indexFields` expects (see `./mask/types`), so a table not
1568
+ * present here (no declared indexes of any kind) is simply absent from the
1569
+ * map rather than mapped to `{}`, and a kind with no declared indexes on a
1570
+ * table that HAS other kinds is simply absent from that table's entry.
1571
+ *
1572
+ * Kept per kind (rather than one flat name→fields record) because the engine
1573
+ * resolves `withIndex`/`withGeoIndex`/rank reads in THREE separate
1574
+ * namespaces (`tableDefinition.indexes` / `.geoIndexes` / `.rankIndexes` —
1575
+ * see `@lunora/shard-engine`'s `ctx-db.ts`), so the same name can legally and
1576
+ * unambiguously denote a different index per kind. A flat map would let one
1577
+ * kind's fields silently shadow another's for a colliding name, producing a
1578
+ * wrong-namespace answer from the mask guard (checking the wrong index's
1579
+ * fields) instead of the documented fail-open (missing lookup) — see plan 258.
1580
+ */
1581
+ type IndexFieldsByTable = Readonly<Record<string, {
1582
+ readonly geo?: Readonly<Record<string, ReadonlyArray<string>>>;
1583
+ readonly index?: Readonly<Record<string, ReadonlyArray<string>>>;
1584
+ readonly rank?: Readonly<Record<string, ReadonlyArray<string>>>;
1585
+ }>>;
1586
+ declare const indexFieldsFromSchema: (schema: Schema) => IndexFieldsByTable;
1587
+ declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
1588
+ /**
1589
+ * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
1590
+ * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
1591
+ * resolver, same `can(...)` permission check — so an author can branch a mask
1592
+ * on the caller's role/permission. `row` is the full pre-mask row the column
1593
+ * belongs to; `column` is the column currently being masked. Both are absent
1594
+ * when the context is used for the procedure-wide `bypass` check (no specific
1595
+ * cell is in play yet).
1596
+ */
1597
+ interface MaskContext<Context = unknown> {
1598
+ readonly auth: {
1599
+ /** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
1600
+ readonly can: (permission: Permission | string) => boolean;
1601
+ readonly identity?: Record<string, unknown> | null;
1602
+ readonly roles: ReadonlyArray<string>;
1603
+ readonly userId: null | string;
1604
+ };
1605
+ /** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
1606
+ readonly column?: string;
1607
+ readonly ctx: Context;
1608
+ /** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
1609
+ readonly row?: Record<string, unknown>;
1610
+ }
1611
+ /**
1612
+ * A custom masking function. Receives the raw cell value and the
1613
+ * {@link MaskContext}, returns the value to surface. Use it for partial masks
1614
+ * (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
1615
+ * or format-preserving tokens. A function that **throws** fails closed — the
1616
+ * cell is redacted to `null`, never leaked raw.
1617
+ */
1618
+ type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
1619
+ /**
1620
+ * How a column is masked:
1621
+ *
1622
+ * - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
1623
+ * the right choice for any value that must actually be kept secret.
1624
+ * - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
1625
+ * same input always yields the same token (joinable/groupable client-side).
1626
+ * **This is NOT a confidentiality control.** It is a non-cryptographic,
1627
+ * unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
1628
+ * phone numbers, SSNs) are brute-force-recoverable by the very caller you are
1629
+ * masking from, and identical values always produce identical tokens across
1630
+ * rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
1631
+ * stable pseudonym for grouping/joining and leaking the value is acceptable —
1632
+ * never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
1633
+ * - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
1634
+ */
1635
+ type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
1636
+ /** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
1637
+ type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
1638
+ /**
1639
+ * The mask declaration passed to `mask(...)`: a table → column → strategy map.
1640
+ * Deliberately a plain object literal so the codegen feeder can statically read
1641
+ * which columns a procedure masks (powering the `mask_uncovered_pii_column`
1642
+ * advisor lint), exactly as the RLS feeder reads policy tables.
1643
+ */
1644
+ type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
1645
+ /**
1646
+ * Options for `mask(policies, options)`.
1647
+ *
1648
+ * - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
1649
+ * inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
1650
+ * not listed grants no permissions (fails closed for unknown roles).
1651
+ * - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
1652
+ * mask is skipped (the caller sees raw values). Use it for a privileged
1653
+ * viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
1654
+ * branching every column when an entire class of caller should see clear data.
1655
+ * - `indexFields` closes the bare-index-scan / rank / geo position oracle (see
1656
+ * the `mask/middleware` module docblock's "Residual read-position oracles" section).
1657
+ */
1658
+ interface MaskOptions<Context = unknown> {
1659
+ readonly bypass?: (context: MaskContext<Context>) => boolean;
1660
+ /**
1661
+ * Per-table, per-KIND index→declared-fields map (regular index `fields`
1662
+ * under `index`; rank index `sortBy` ∪ `partitionBy` under `rank`; geo
1663
+ * index `field` under `geo`). Supplied to close the bare-index-scan /
1664
+ * rank / geo position oracle: a `withIndex(name)` with no range callback,
1665
+ * a `rank`/`rankPage`/`rankBefore` read, or a `withGeoIndex` read, over an
1666
+ * index whose DECLARED fields (for that read's own kind) intersect a
1667
+ * masked column, is rejected. Kept per kind — rather than one flat
1668
+ * name→fields map — because the engine resolves `withIndex` /
1669
+ * `withGeoIndex` / rank reads in three separate namespaces, so the same
1670
+ * index name can legally denote a different index per kind; a flat map
1671
+ * would let one kind's fields shadow another's for a colliding name.
1672
+ * OPTIONAL and additive — omit it and behaviour is unchanged (the oracle
1673
+ * stays open, exactly as before this option existed). Build it with
1674
+ * `indexFieldsFromSchema` (exported from `@lunora/server`):
1675
+ * `mask(policies, { indexFields: indexFieldsFromSchema(schema) })`.
1676
+ */
1677
+ readonly indexFields?: IndexFieldsByTable;
1678
+ readonly roles?: ReadonlyArray<Role>;
1679
+ }
1680
+ interface QueryPage$1 {
1681
+ continueCursor: null | string;
1682
+ isDone: boolean;
1683
+ page: Record<string, unknown>[];
1684
+ }
1685
+ interface QueryArgs$1 {
1686
+ baseWhere?: unknown;
1687
+ cursor?: null | string;
1688
+ limit?: number;
1689
+ orderBy?: ReadonlyArray<Record<string, unknown>>;
1690
+ where?: unknown;
1691
+ with?: Record<string, unknown>;
1692
+ }
1693
+ interface AggregateArgs$1 {
1694
+ field?: string;
1695
+ op: string;
1696
+ where?: unknown;
1697
+ }
1698
+ interface GroupByArgs$1 {
1699
+ agg?: {
1700
+ field?: string;
1701
+ op: string;
1702
+ };
1703
+ by: ReadonlyArray<string>;
1704
+ where?: unknown;
1705
+ }
1706
+ /** One row of a `.collectWithScores()` result — mirrors `@lunora/shard-engine`'s `ScoredDocument`. */
1707
+ type ScoredDocument = GeoScoredDocument | SearchScoredDocument;
1708
+ /** A `.withGeoIndex()` row — mirrors `@lunora/shard-engine`'s `GeoScoredDocument`. */
1709
+ interface GeoScoredDocument {
1710
+ distanceMeters: null | number;
1711
+ document: Record<string, unknown>;
1712
+ score?: never;
1713
+ }
1714
+ /** A `.withSearchIndex()` row — mirrors `@lunora/shard-engine`'s `SearchScoredDocument`. */
1715
+ interface SearchScoredDocument {
1716
+ distanceMeters?: never;
1717
+ document: Record<string, unknown>;
1718
+ score: number;
1719
+ }
1720
+ interface TableReaderLike$1 {
1721
+ [Symbol.asyncIterator]: () => AsyncIterator<Record<string, unknown>>;
1722
+ collect: () => Promise<Record<string, unknown>[]>;
1723
+ collectWithScores: () => Promise<ScoredDocument[]>;
1724
+ filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike$1;
1725
+ first: () => Promise<Record<string, unknown> | null>;
1726
+ order: (direction: "asc" | "desc") => TableReaderLike$1;
1727
+ paginate: (options: {
1728
+ cursor?: null | string;
1729
+ numItems: number;
1730
+ }) => Promise<QueryPage$1>;
1731
+ take: (limit: number) => Promise<Record<string, unknown>[]>;
1732
+ unique: () => Promise<Record<string, unknown> | null>;
1733
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
1734
+ withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
1735
+ withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
1736
+ }
1737
+ /**
1738
+ * Structural projection of the runtime ORM writer — the same subset
1739
+ * `../rls/middleware` mirrors, so the wrapper is interchangeable between
1740
+ * `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
1741
+ * inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
1699
1742
  */
1700
- interface AggregateIndexOptions {
1701
- by?: ReadonlyArray<string>;
1702
- field?: string;
1703
- on: string;
1704
- op?: AggregateOp;
1705
- where?: Record<string, unknown>;
1743
+ interface MaskDatabase {
1744
+ aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
1745
+ count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
1746
+ delete: (id: string, expectedTable?: string) => Promise<void>;
1747
+ deleteMany: (ids: ReadonlyArray<string>, options?: {
1748
+ limit?: number;
1749
+ }) => Promise<{
1750
+ deleted: number;
1751
+ }>;
1752
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
1753
+ limit?: number;
1754
+ }) => Promise<{
1755
+ deleted: number;
1756
+ }>;
1757
+ findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
1758
+ findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
1759
+ findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
1760
+ get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
1761
+ groupBy: (tableName: string, options: GroupByArgs$1) => Promise<ReadonlyArray<{
1762
+ key: Record<string, unknown>;
1763
+ value: null | number;
1764
+ }>>;
1765
+ insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1766
+ insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1767
+ limit?: number;
1768
+ skipDuplicates?: boolean;
1769
+ }) => Promise<(string | null)[]>;
1770
+ lookupById?: (id: string, expectedTable?: string) => Promise<null | {
1771
+ row: Record<string, unknown>;
1772
+ tableName: string;
1773
+ }>;
1774
+ patch: (id: string, patch: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1775
+ patchMany: (patches: ReadonlyArray<{
1776
+ id: string;
1777
+ patch: Record<string, unknown>;
1778
+ }>, options?: {
1779
+ limit?: number;
1780
+ }) => Promise<{
1781
+ patched: number;
1782
+ }>;
1783
+ patchWhere?: (tableName: string, args: {
1784
+ patch: Record<string, unknown>;
1785
+ where: Record<string, unknown>;
1786
+ }, options?: {
1787
+ limit?: number;
1788
+ }) => Promise<{
1789
+ patched: number;
1790
+ }>;
1791
+ query: (tableName: string) => TableReaderLike$1;
1792
+ rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
1793
+ position: number;
1794
+ total: number;
1795
+ }>;
1796
+ rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
1797
+ before: number;
1798
+ total: number;
1799
+ }>;
1800
+ rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage$1>;
1801
+ /** Cross-shard companion to `rankPage`, gated the same way `rankPage` is masked below. */
1802
+ rankPageRows?: (tableName: string, indexName: string, options?: unknown) => Promise<ShardRankPageResultLike>;
1803
+ replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1804
+ }
1805
+ /** Roles list source on the context. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
1806
+ type AuthLike$1 = {
1807
+ getIdentity?: () => Promise<Record<string, unknown> | null>;
1808
+ roles?: ReadonlyArray<string>;
1809
+ userId?: null | string;
1810
+ };
1811
+ interface MaskContextIn {
1812
+ auth?: AuthLike$1;
1813
+ db: MaskDatabase;
1706
1814
  }
1707
1815
  /**
1708
- * Declare a standalone aggregate index. Pass the returned value to
1709
- * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name
1710
- * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1711
- * (DO + D1) read every index uniformly off the table definition.
1816
+ * Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
1817
+ * Closes over the policy map at builder-construction time; resolves identity +
1818
+ * the `bypass` decision per call against the live ctx.
1819
+ *
1820
+ * IMPORTANT: a mask is in scope only for procedures whose builder chain
1821
+ * includes this middleware — opt-in, never global (the same invariant as RLS).
1712
1822
  */
1713
- declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
1823
+ declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
1824
+ /** Project-wide masked-column registry: every table any registered function masks, unioned. */
1825
+ type MaskRegistry = ReadonlyMap<string, ReadonlySet<string>>;
1714
1826
  /**
1715
- * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1716
- * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1717
- * `table` so it can sit next to the schema map.
1827
+ * Build the project-wide masked-column registry from the registered functions
1828
+ * (pass `Object.values(LUNORA_FUNCTIONS)`) the mask-column twin of
1829
+ * `buildRlsReadRegistry`. Unions every function's `.use(mask(...))` columns
1830
+ * per table: a column masked by ANY registered function counts as masked in
1831
+ * the registry — there is no "which procedure would this shape have gone
1832
+ * through" question to narrow by, so the union is the only safe answer.
1718
1833
  */
1719
- interface RankIndexOptions {
1720
- partitionBy?: ReadonlyArray<string>;
1721
- sortBy: ReadonlyArray<{
1722
- direction?: "asc" | "desc";
1723
- field: string;
1834
+ declare const buildMaskRegistry: (functions: Iterable<unknown>) => MaskRegistry;
1835
+ /** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
1836
+ type MigrationDocument = Record<string, unknown>;
1837
+ /**
1838
+ * The read surface a transform reaches through its `ctx`.
1839
+ *
1840
+ * Read-only by design: the runner accounts for exactly one rewrite per row read,
1841
+ * and a transform writing directly would make that count describe something
1842
+ * other than what happened. Scoped to the shard the runner is walking.
1843
+ */
1844
+ interface MigrationReader {
1845
+ count: (table: string, where?: Record<string, unknown>) => Promise<number>;
1846
+ findFirst: (table: string, args?: Record<string, unknown>) => Promise<MigrationDocument | null>;
1847
+ findMany: (table: string, args?: Record<string, unknown>) => Promise<{
1848
+ isDone: boolean;
1849
+ page: MigrationDocument[];
1724
1850
  }>;
1725
- table: string;
1726
- where?: Record<string, unknown>;
1851
+ get: (id: string, expectedTable?: string) => Promise<MigrationDocument | null>;
1852
+ }
1853
+ /** The context handed to a transform alongside the row. */
1854
+ interface MigrationCtx {
1855
+ db: MigrationReader;
1727
1856
  }
1728
1857
  /**
1729
- * Declare a standalone rank index. Pass the returned value to
1730
- * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1731
- * by index name the schema attaches it to `tables[on].rankIndexes`.
1732
- */
1733
- declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
1734
- /**
1735
- * Build the application schema. The first argument is the table map; the
1736
- * optional second argument registers standalone `defineVectorIndex(...)`
1737
- * declarations (DSL Shape B) keyed by index name. The optional third argument
1738
- * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1739
- * the optional fourth argument registers standalone `defineRankIndex(...)`
1740
- * declarations. Both are folded into the matching `tables[on].*Indexes` array
1741
- * so runtime backends read every index uniformly off the table definition.
1858
+ * Transform applied to one document. Return a new document to rewrite the row,
1859
+ * or `undefined` to leave it untouched (skipped, not counted as changed). The
1860
+ * runner always preserves the original `_id` and `_creationTime`, so the
1861
+ * returned document neither needs to nor should change row identity.
1862
+ *
1863
+ * The second parameter carries a shard-scoped reader. Without it a transform
1864
+ * could only rewrite the row it was handed enough for a backfill whose new
1865
+ * value is a pure function of the old row (`displayName = name ?? "Anonymous"`),
1866
+ * but not for the shape people actually write: read the parent, copy a field
1867
+ * down onto its children.
1868
+ *
1869
+ * May return a promise, since a cross-table read is asynchronous.
1870
+ *
1871
+ * **A shard key cannot be backfilled this way, even with a reader.** A row whose
1872
+ * shard-key field is unset does not belong to any shard, so a shard-scoped query
1873
+ * will not enumerate it; and writing the key would have to MOVE the row to a
1874
+ * different Durable Object, which a per-shard runner cannot do. Re-keying is an
1875
+ * export → transform → import, not a migration.
1742
1876
  */
1877
+ type MigrationTransform = (document: MigrationDocument, ctx: MigrationCtx) => MigrationDocument | Promise<MigrationDocument | undefined | void> | undefined | void;
1878
+ interface MigrationDefinition {
1879
+ /** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
1880
+ readonly batchSize?: number;
1881
+ /** Optional reverse transform, applied by `migrate down`. */
1882
+ readonly down?: MigrationTransform;
1883
+ /** Stable, unique identifier — the key per-shard run-state is tracked under. */
1884
+ readonly id: string;
1885
+ /** Table whose documents this migration iterates. */
1886
+ readonly table: string;
1887
+ /** Forward transform, applied to every row by `migrate up`. */
1888
+ readonly up: MigrationTransform;
1889
+ }
1890
+ /** A {@link MigrationDefinition} plus the codegen discovery marker. */
1891
+ interface RegisteredMigration extends MigrationDefinition {
1892
+ readonly __lunoraMigration: true;
1893
+ }
1894
+ /** Declare an online data migration. See the module docs for runtime semantics. */
1895
+ declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
1743
1896
  /**
1744
- * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1745
- * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1746
- *
1747
- * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1748
- * the merged tables. Extension tables are auto-namespaced by the extension
1749
- * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1750
- * prefixed names via {@link PrefixedTables}. Chains:
1751
- * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1752
- * `a`'s prefixed tables then `b`'s.
1897
+ * A mutator declaration. `server` is authoritative; `client` is the optimistic
1898
+ * twin (optional omit it to let the optimistic write fall through to the
1899
+ * server round-trip with no local preview). Both receive the same validated
1900
+ * `args`.
1753
1901
  */
1754
- type ExtendableSchema<T extends Record<string, TableDefinition>> = {
1755
- extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
1756
- readonly key: Key;
1757
- }) => ExtendableSchema<PrefixedTables<X, Key> & T>;
1902
+ interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
1758
1903
  /**
1759
- * Pin every Durable Object the app reaches shards, fan-out, subscriptions,
1760
- * the scheduler, and `ctx.containers` to a Cloudflare data-residency
1761
- * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1762
- * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1763
- * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1764
- * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1904
+ * Validator for the mutator's arguments. Validated on the DO before `server`
1905
+ * runs and (when present) on the client before `client` runs, so both impls
1906
+ * see the same parsed shape. Omit for a parameterless mutator.
1907
+ */
1908
+ readonly args?: Args;
1909
+ /**
1910
+ * Optimistic client implementation. Runs in a TanStack DB transaction
1911
+ * against the local collections; its writes are applied immediately and
1912
+ * automatically rolled back / rebased as the authoritative result syncs
1913
+ * back. Pure and side-effect-free beyond the local store. Omit to skip the
1914
+ * local preview.
1915
+ */
1916
+ readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
1917
+ /**
1918
+ * Owner-scope the write: names the column carrying the row owner (e.g.
1919
+ * `owner: "userId"`). Before `server` runs, the mutator requires a verified
1920
+ * identity, rejects a client-supplied owner that disagrees with it, and sets
1921
+ * the column to the verified value — so the impl reads `args[owner]` without
1922
+ * trusting the client and never repeats the check by hand.
1765
1923
  *
1766
- * ⚠️ **Set this once, before your first deploy — changing or removing it
1767
- * strands data.** A Durable Object name maps to a *different* ID in each
1768
- * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1769
- * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1770
- * old jurisdiction's DOs and is no longer reachable. There is no in-place
1771
- * migration you would have to export from the old jurisdiction and import
1772
- * into the new one.
1924
+ * This replaces the "every mutator opens with `assertOwner(ctx, args.userId)`"
1925
+ * pattern, and is the write-side counterpart to an `owner`-scoped
1926
+ * {@link import("./shapes").ShapeDefinition}. Unlike a shape it takes the column
1927
+ * NAME rather than `true`: a shape is bound to one `table`, so the table's
1928
+ * `.ownedBy(field)` resolves unambiguously, whereas one mutator may write
1929
+ * several tables and has no single owning table to read it from.
1773
1930
  *
1774
- * Note: this pins **DO-backed** state only. D1-backed state `.global()`
1775
- * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1776
- * settings, not this option.
1777
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1931
+ * Declare the column `v.optional(...)` to leave it off the wire entirely; it is
1932
+ * injected either way.
1778
1933
  */
1779
- jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
1934
+ readonly owner?: string;
1780
1935
  /**
1781
- * Turn on secure-by-default RLS for the whole schema. Every table is then
1782
- * protected the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1783
- * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1784
- * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1785
- * mode, so `.rls("required")` composes with `.extend(...)` either order.
1936
+ * Authoritative server implementation. Runs inside the shard DO with a full
1937
+ * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
1938
+ * and poke back to subscribers. This is the source of truth — the client
1939
+ * impl is only a prediction of it.
1786
1940
  */
1787
- rls: (mode: "required") => ExtendableSchema<T>;
1788
- } & Schema<T>;
1789
- declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
1941
+ readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
1942
+ }
1943
+ /**
1944
+ * A {@link MutatorDefinition} plus the codegen discovery marker and a
1945
+ * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
1946
+ * invokes a mutator exactly like a registered procedure.
1947
+ */
1948
+ interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
1949
+ readonly __lunoraMutator: true;
1950
+ /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
1951
+ readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
1952
+ /**
1953
+ * Marks the dispatch kind so codegen can register the mutator in the same
1954
+ * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
1955
+ * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
1956
+ * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
1957
+ * ordinary `mutation`.
1958
+ */
1959
+ readonly kind: "mutation";
1960
+ }
1961
+ /** Declare a custom mutator. See the module docs for runtime semantics. */
1962
+ declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
1790
1963
  /** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
1791
1964
  declare const DEFAULT_TTL_MS = 3e4;
1792
1965
  declare const PRESENCE_BARE_TABLE = "present";
@@ -2174,6 +2347,13 @@ interface DatabaseWriterLike {
2174
2347
  * Required for the same reason as `aggregate`.
2175
2348
  */
2176
2349
  rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
2350
+ /**
2351
+ * Cross-shard companion to `rankPage`: same ranked slice, but each row
2352
+ * keeps its rank-key tuple for the query coordinator's k-way merge. Same
2353
+ * count-of-partition RLS hazard as `rankPage` — failed closed under a read
2354
+ * policy for the identical reason (see `rankPage` above).
2355
+ */
2356
+ rankPageRows?: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<ShardRankPageResultLike>;
2177
2357
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
2178
2358
  restore?: (id: string, expectedTable?: string) => Promise<void>;
2179
2359
  /**
@@ -2455,4 +2635,4 @@ interface StorageContextIn {
2455
2635
  }
2456
2636
  declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
2457
2637
  declare const VERSION = "0.0.0";
2458
- export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, type DataModelInit, type DefineComponentOptions, type DefineIdentityOptions, type DefineListArgsConfig, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExposeConfig, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type IdentityContract, type IdentityRejectMode, type IdentityValidation, type InferArgs, type InferEnv, type InferIdentity, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type ListArgsSpec, type ListArgsValidators, type ListArgsValue, type ListFilterOperators, type ListOrderByEntry, type ListWhere, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationDefinition, type MigrationDocument, type MigrationTransform, type MutationBuilder, type MutationCtx, type MutatorDefinition, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, allowAll, asBucketStorage, bindOrm, bindTableFacade, buildRlsReadRegistry, clampLimit, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineIdentity, defineListArgs, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, deny, httpAction, httpRoute, httpRouter, initLunora, installPlugins, isDeny, isSafeHeaderValue, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules, toWhereInput };
2638
+ export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, type DataModelInit, type DefineComponentOptions, type DefineIdentityOptions, type DefineListArgsConfig, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExposeConfig, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type IdentityContract, type IdentityRejectMode, type IdentityValidation, type IndexFieldsByTable, type InferArgs, type InferEnv, type InferIdentity, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type ListArgsSpec, type ListArgsValidators, type ListArgsValue, type ListFilterOperators, type ListOrderByEntry, type ListWhere, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskRegistry, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationCtx, type MigrationDefinition, type MigrationDocument, type MigrationReader, type MigrationTransform, type MutationBuilder, type MutationCtx, type MutatorDefinition, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, allowAll, asBucketStorage, bindOrm, bindTableFacade, buildMaskRegistry, buildRlsReadRegistry, clampLimit, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineIdentity, defineListArgs, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, deny, httpAction, httpRoute, httpRouter, indexFieldsFromSchema, initLunora, installPlugins, isDeny, isSafeHeaderValue, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules, toWhereInput };