@lunora/server 1.0.0-alpha.8 → 1.0.0-alpha.81

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 (80) hide show
  1. package/LICENSE.md +219 -0
  2. package/README.md +46 -0
  3. package/dist/data-model.d.mts +260 -164
  4. package/dist/data-model.d.ts +260 -164
  5. package/dist/data-model.mjs +0 -1
  6. package/dist/drizzle.mjs +1 -1
  7. package/dist/index.d.mts +2023 -1175
  8. package/dist/index.d.ts +2023 -1175
  9. package/dist/index.mjs +1 -28
  10. package/dist/otel.d.mts +543 -0
  11. package/dist/otel.d.ts +543 -0
  12. package/dist/otel.mjs +1 -0
  13. package/dist/packem_shared/DEFAULT_LIMIT-DC-M6faS.mjs +1 -0
  14. package/dist/packem_shared/LunoraEnvError-ByvihJFy.mjs +3 -0
  15. package/dist/packem_shared/LunoraError-DcKdk9Ti.mjs +1 -0
  16. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BpSPSCwL.mjs +1 -0
  17. package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
  18. package/dist/packem_shared/apply-output-BG8Bd0Ug.mjs +1 -0
  19. package/dist/packem_shared/asBucketStorage-BthCnWop.mjs +1 -0
  20. package/dist/packem_shared/bindOrm-lWdeDi9P.mjs +1 -0
  21. package/dist/packem_shared/buildMaskRegistry-72SL8fJn.mjs +1 -0
  22. package/dist/packem_shared/buildRlsReadRegistry-Deq8inqS.mjs +1 -0
  23. package/dist/packem_shared/composePluginMiddleware-DQHF6mYr.mjs +1 -0
  24. package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
  25. package/dist/packem_shared/createPolicyDsl-DNfByAUX.mjs +1 -0
  26. package/dist/packem_shared/createSecrets-CyrEddTF.mjs +1 -0
  27. package/dist/packem_shared/defineAggregateIndex-BtVAy69G.mjs +1 -0
  28. package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
  29. package/dist/packem_shared/defineMigration-7GI9Qbtd.mjs +1 -0
  30. package/dist/packem_shared/defineMutator-DKy8UtbB.mjs +1 -0
  31. package/dist/packem_shared/defineShape-DYxbNl5W.mjs +1 -0
  32. package/dist/packem_shared/defineStorageRule-BvxHVq3b.mjs +1 -0
  33. package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
  34. package/dist/packem_shared/httpAction-D3b9-YiY.mjs +5 -0
  35. package/dist/packem_shared/initLunora-B2sfmFJ6.mjs +1 -0
  36. package/dist/packem_shared/mask-CfHNITgg.mjs +1 -0
  37. package/dist/packem_shared/middleware-DEW0yLvU.mjs +1 -0
  38. package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
  39. package/dist/packem_shared/onQueryChange-gepYD6KC.mjs +1 -0
  40. package/dist/packem_shared/plugin-DQcLxTx1.mjs +1 -0
  41. package/dist/packem_shared/policy-tag-D4RzgQnw.mjs +1 -0
  42. package/dist/packem_shared/protectPublic-Bo4G9xaV.mjs +1 -0
  43. package/dist/packem_shared/rls-B3mNGBuc.mjs +1 -0
  44. package/dist/packem_shared/run-middleware-B-JeGGp9.mjs +1 -0
  45. package/dist/packem_shared/storageRules-CyaiZ0av.mjs +1 -0
  46. package/dist/packem_shared/types.d-B-pIn8rE.d.mts +141 -0
  47. package/dist/packem_shared/types.d-CdPDEjhz.d.ts +141 -0
  48. package/dist/rls/testing.d.mts +31 -31
  49. package/dist/rls/testing.d.ts +31 -31
  50. package/dist/rls/testing.mjs +1 -49
  51. package/dist/types.d.mts +1594 -506
  52. package/dist/types.d.ts +1594 -506
  53. package/dist/types.mjs +1 -31
  54. package/package.json +17 -4
  55. package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +0 -187
  56. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  57. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-C7j9gY_d.mjs +0 -114
  58. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  59. package/dist/packem_shared/bindOrm-Ce57S3N9.mjs +0 -128
  60. package/dist/packem_shared/buildRlsReadRegistry-CkxuS69B.mjs +0 -107
  61. package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +0 -100
  62. package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +0 -29
  63. package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +0 -55
  64. package/dist/packem_shared/defineAggregateIndex-ZdyU78gh.mjs +0 -291
  65. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
  66. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
  67. package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +0 -17
  68. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
  69. package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
  70. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
  71. package/dist/packem_shared/initLunora-lxwHTEV3.mjs +0 -100
  72. package/dist/packem_shared/mask-eCUYOwhd.mjs +0 -211
  73. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  74. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
  75. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
  76. package/dist/packem_shared/rls-ClmjkkxZ.mjs +0 -569
  77. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
  78. package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
  79. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  80. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/dist/index.d.ts CHANGED
@@ -1,37 +1,48 @@
1
- import { Validator, Infer, ValidatorMap, InferValidatorMap, v } from '@lunora/values';
2
- export { type ColumnValidator, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
3
- import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.js";
4
- export { type AnyApi, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.js";
1
+ import { Validator, Infer, ValidatorMap, InferValidatorMap, ColumnValidator, v } from '@lunora/values';
2
+ export { type ColumnValidator, type GeoPoint, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
3
+ import { ArgsValidator, InferArgs, RegisteredAction, ExposeConfig, X402ProcedureConfig, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, DurableStreamOptions, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, ShardInitEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, SearchLanguage, SearchStrategy, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.js";
4
+ export { type AnyApi, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type GeoBoundingBox, type GeoFilterBuilder, type GeoIndexDefinition, type GeoPointInput, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LogFields, type LunoraLogMethod, type LunoraLogger, type LunoraMetrics, type LunoraTracer, type LunoraWideEvent, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type RestCacheConfig, type RunQueryOptions, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type SpanEvaluation, type SpanHandle, type SpanKind, type SpanLink, type SpanOptions, type Storage, type StorageMetadata, type StorageObjectHead, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type TtlDefinition, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowEventDefinition, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.js";
5
+ import { LunoraError as LunoraError$1, LunoraErrorCode } from '@lunora/errors';
6
+ export type { LunoraErrorCode } from '@lunora/errors';
5
7
  import { Context, Hono } from 'hono';
6
- import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D as DefinePolicyInput, W as WhereInput, c as RlsOptions } from "./packem_shared/types.d-BDY0FYHK.js";
7
- export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-BDY0FYHK.js";
8
+ import { WhereOperators, QueryArgs as QueryArgs$2 } from "./data-model.js";
9
+ import { W as WhereInput, a as Policy, b as RlsOptions, c as Permission, R as Role, T as TypedDefinePolicyInput, D as DefinePolicyInput } from "./packem_shared/types.d-CdPDEjhz.js";
10
+ export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-CdPDEjhz.js";
8
11
  export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
9
- import "./data-model.js";
10
- /**
11
- * Make any `config.storage` result bucket-aware so `ctx.storage.bucket(name)`
12
- * always resolves. A `createBucketStorage(...)` result already carries
13
- * `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
14
- * (or the no-storage stub) is tagged as the `"default"` bucket, where
15
- * `.bucket(name)` is the identity — single-bucket apps address one binding under
16
- * every name.
17
- *
18
- * This is the runtime counterpart the generated `_generated/shard.ts` imports to
19
- * wrap `ctx.storage`; it lives here (the single source) rather than being stamped
20
- * inline into every generated file, so the bucket-tagging behaviour has one home
21
- * alongside the storage ctx types. The input is genuinely heterogeneous (a thunk
22
- * result cast through `unknown`), so the signature is `unknown → unknown`; the
23
- * generated caller casts the result to its storage type.
24
- */
12
+ /**
13
+ * Make any resolved storage capability bucket-aware so `ctx.storage.bucket(name)`
14
+ * always resolves. A `createBucketStorage(...)` result already carries
15
+ * `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
16
+ * (or the no-storage stub) is tagged as the `"default"` bucket, where
17
+ * `.bucket(name)` is the identity single-bucket apps address one binding under
18
+ * every name.
19
+ *
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.
28
+ */
25
29
  declare const asBucketStorage: (raw: unknown) => unknown;
30
+ /**
31
+ * Options for the `.stream()` terminal. `durable: true` is shorthand for
32
+ * `durable: {}` — the runtime only ever sees the object form.
33
+ */
34
+ interface StreamOptions {
35
+ durable?: boolean | DurableStreamOptions;
36
+ }
26
37
  /** Builder discriminator. Codegen reads this kind. */
27
38
  type TerminalKind = FunctionKind;
28
39
  /** Initial (empty) accumulated args for a fresh builder. */
29
40
  type EmptyArgs = Record<never, never>;
30
41
  /**
31
- * `next()` advances the middleware chain. Called with no argument it forwards
32
- * the current context unchanged; called with `{ ctx }` it shallow-merges the
33
- * extension, and the result type reflects the widened context.
34
- */
42
+ * `next()` advances the middleware chain. Called with no argument it forwards
43
+ * the current context unchanged; called with `{ ctx }` it shallow-merges the
44
+ * extension, and the result type reflects the widened context.
45
+ */
35
46
  interface MiddlewareNext<ContextIn> {
36
47
  (): Promise<ContextIn>;
37
48
  <Extension extends Record<string, unknown>>(options: {
@@ -39,28 +50,46 @@ interface MiddlewareNext<ContextIn> {
39
50
  }): Promise<ContextIn & Extension>;
40
51
  }
41
52
  /**
42
- * A middleware receives the current context and a `next` continuation. Its
43
- * return type becomes the builder's new context, so `return next({ ctx })`
44
- * propagates the extension into every downstream `.use()` and the handler.
45
- */
53
+ * A middleware receives the current context and a `next` continuation. Its
54
+ * return type becomes the builder's new context, so `return next({ ctx })`
55
+ * propagates the extension into every downstream `.use()` and the handler.
56
+ */
46
57
  type Middleware<ContextIn, ContextOut> = (options: {
47
58
  ctx: ContextIn;
48
59
  next: MiddlewareNext<ContextIn>;
49
60
  }) => ContextOut | Promise<ContextOut>;
50
- /** Options accepted by `initLunora.dataModel&lt;DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
61
+ /** Options accepted by `initLunora.dataModel<DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
51
62
  type CreateOptions = Record<never, never>;
52
63
  /**
53
- * `Output` carries the type declared by `.output(validator)`. It defaults to
54
- * the `undefined` sentinel meaning "not declared": in that state the terminal
55
- * stays generic over the handler's own return type. Once `.output()` sets it to
56
- * a concrete type, the terminal requires the handler to return that type and
57
- * the registration is typed to it (the runtime parses the result through the
58
- * validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
59
- * `Output` doesn't distribute and so the test is for the exact sentinel.
60
- */
64
+ * `Output` carries the type declared by `.output(validator)`. It defaults to
65
+ * the `undefined` sentinel meaning "not declared": in that state the terminal
66
+ * stays generic over the handler's own return type. Once `.output()` sets it to
67
+ * a concrete type, the terminal requires the handler to return that type and
68
+ * the registration is typed to it (the runtime parses the result through the
69
+ * validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
70
+ * `Output` doesn't distribute and so the test is for the exact sentinel.
71
+ */
61
72
  interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
62
73
  readonly __lunoraProcedure: "query";
74
+ /**
75
+ * Publish this query on the opt-in public REST surface (plan 167) — the
76
+ * runtime mints `GET /_lunora/rest/<namespace>/<fn>` (and `POST`), dispatching
77
+ * THROUGH the procedure so `ctx.auth` / RLS / validators are enforced, and the
78
+ * generated OpenAPI describes it. Default-closed: omit to keep it RPC-only.
79
+ */
80
+ expose: (config: ExposeConfig) => QueryBuilder<Context, Args, Output>;
63
81
  input: <A extends ArgsValidator>(validators: A) => QueryBuilder<Context, A & Args, Output>;
82
+ /**
83
+ * Attach static, per-procedure metadata. Merges across calls, is readable
84
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
85
+ * `fn.meta` so codegen and other tooling can enumerate it.
86
+ *
87
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
88
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
89
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
90
+ * can only be executed. Mirrors tRPC's `.meta()`.
91
+ */
92
+ meta: (value: Record<string, unknown>) => QueryBuilder<Context, Args, Output>;
64
93
  output: <V extends Validator>(validator: V) => QueryBuilder<Context, Args, Infer<V>>;
65
94
  query: [Output] extends [undefined] ? <R>(handler: (options: {
66
95
  args: InferArgs<Args>;
@@ -70,23 +99,54 @@ interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined>
70
99
  ctx: Context;
71
100
  }) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
72
101
  /**
73
- * Terminal: declare this procedure as a streaming query. The handler is an
74
- * async generator (or any function returning an `AsyncIterable&lt;R>`) that
75
- * yields one chunk per server-pushed frame. The third `signal` argument is
76
- * tripped when the client cancels — break out of the loop or check
77
- * `signal.aborted` between yields. `.output()` does not apply: per-chunk
78
- * validation is opt-in via the handler itself.
79
- */
102
+ * Terminal: declare this procedure as a streaming query. The handler is an
103
+ * async generator (or any function returning an `AsyncIterable<R>`) that
104
+ * yields one chunk per server-pushed frame. The third `signal` argument is
105
+ * tripped when the client cancels — break out of the loop or check
106
+ * `signal.aborted` between yields. `.output()` does not apply: per-chunk
107
+ * validation is opt-in via the handler itself.
108
+ *
109
+ * Pass `{ durable: true }` to make the run outlive the socket that opened
110
+ * it: chunks are persisted as they are produced, so a reload resumes the
111
+ * same run from where it left off instead of dropping the work, and a
112
+ * second client with the same arguments attaches to the same transcript.
113
+ * That is what an LLM response wants; a progress ticker does not need it.
114
+ */
80
115
  stream: <R>(handler: (options: {
81
116
  args: InferArgs<Args>;
82
117
  ctx: Context;
83
118
  signal: AbortSignal;
84
- }) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
119
+ }) => AsyncGenerator<R, void, void> | AsyncIterable<R>, options?: StreamOptions) => RegisteredStream<Args, R>;
85
120
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => QueryBuilder<ContextOut, Args, Output>;
121
+ /**
122
+ * Mark this query as paid. The origin worker answers an unpaid client RPC
123
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
124
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
125
+ * recipient, and facilitator come from the worker-level x402 charge config.
126
+ */
127
+ x402: (config: X402ProcedureConfig) => QueryBuilder<Context, Args, Output>;
86
128
  }
87
129
  interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
88
130
  readonly __lunoraProcedure: "mutation";
131
+ /**
132
+ * Publish this mutation on the opt-in public REST surface (plan 167) — the
133
+ * runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
134
+ * procedure so `ctx.auth` / RLS / validators are enforced, and the generated
135
+ * OpenAPI describes it. Default-closed: omit to keep it RPC-only.
136
+ */
137
+ expose: (config: ExposeConfig) => MutationBuilder<Context, Args, Output>;
89
138
  input: <A extends ArgsValidator>(validators: A) => MutationBuilder<Context, A & Args, Output>;
139
+ /**
140
+ * Attach static, per-procedure metadata. Merges across calls, is readable
141
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
142
+ * `fn.meta` so codegen and other tooling can enumerate it.
143
+ *
144
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
145
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
146
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
147
+ * can only be executed. Mirrors tRPC's `.meta()`.
148
+ */
149
+ meta: (value: Record<string, unknown>) => MutationBuilder<Context, Args, Output>;
90
150
  mutation: [Output] extends [undefined] ? <R>(handler: (options: {
91
151
  args: InferArgs<Args>;
92
152
  ctx: Context;
@@ -96,6 +156,13 @@ interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefine
96
156
  }) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
97
157
  output: <V extends Validator>(validator: V) => MutationBuilder<Context, Args, Infer<V>>;
98
158
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => MutationBuilder<ContextOut, Args, Output>;
159
+ /**
160
+ * Mark this mutation as paid. The origin worker answers an unpaid client RPC
161
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
162
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
163
+ * recipient, and facilitator come from the worker-level x402 charge config.
164
+ */
165
+ x402: (config: X402ProcedureConfig) => MutationBuilder<Context, Args, Output>;
99
166
  }
100
167
  interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
101
168
  readonly __lunoraProcedure: "action";
@@ -106,20 +173,56 @@ interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined>
106
173
  args: InferArgs<Args>;
107
174
  ctx: Context;
108
175
  }) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
176
+ /**
177
+ * Publish this action on the opt-in public REST surface (plan 167) — the
178
+ * runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
179
+ * procedure so `ctx.auth` / RLS / validators are enforced, and the generated
180
+ * OpenAPI describes it. Default-closed: omit to keep it RPC-only.
181
+ */
182
+ expose: (config: ExposeConfig) => ActionBuilder<Context, Args, Output>;
109
183
  input: <A extends ArgsValidator>(validators: A) => ActionBuilder<Context, A & Args, Output>;
184
+ /**
185
+ * Attach static, per-procedure metadata. Merges across calls, is readable
186
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
187
+ * `fn.meta` so codegen and other tooling can enumerate it.
188
+ *
189
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
190
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
191
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
192
+ * can only be executed. Mirrors tRPC's `.meta()`.
193
+ */
194
+ meta: (value: Record<string, unknown>) => ActionBuilder<Context, Args, Output>;
110
195
  output: <V extends Validator>(validator: V) => ActionBuilder<Context, Args, Infer<V>>;
111
196
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => ActionBuilder<ContextOut, Args, Output>;
112
- }
113
- /**
114
- * Internal builder variants. Identical to their public counterparts but carry
115
- * the `__lunoraVisibility: "internal"` brand codegen keys off to route the
116
- * registration into the `internal` object (and keep it off `api`). `input`/`use`
117
- * return the internal builder type so the brand survives the whole chain.
118
- */
197
+ /**
198
+ * Mark this action as paid. The origin worker answers an unpaid client RPC
199
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
200
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
201
+ * recipient, and facilitator come from the worker-level x402 charge config.
202
+ */
203
+ x402: (config: X402ProcedureConfig) => ActionBuilder<Context, Args, Output>;
204
+ }
205
+ /**
206
+ * Internal builder variants. Identical to their public counterparts but carry
207
+ * the `__lunoraVisibility: "internal"` brand codegen keys off to route the
208
+ * registration into the `internal` object (and keep it off `api`). `input`/`use`
209
+ * return the internal builder type so the brand survives the whole chain.
210
+ */
119
211
  interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
120
212
  readonly __lunoraProcedure: "query";
121
213
  readonly __lunoraVisibility: "internal";
122
214
  input: <A extends ArgsValidator>(validators: A) => InternalQueryBuilder<Context, A & Args, Output>;
215
+ /**
216
+ * Attach static, per-procedure metadata. Merges across calls, is readable
217
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
218
+ * `fn.meta` so codegen and other tooling can enumerate it.
219
+ *
220
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
221
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
222
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
223
+ * can only be executed. Mirrors tRPC's `.meta()`.
224
+ */
225
+ meta: (value: Record<string, unknown>) => InternalQueryBuilder<Context, Args, Output>;
123
226
  output: <V extends Validator>(validator: V) => InternalQueryBuilder<Context, Args, Infer<V>>;
124
227
  query: [Output] extends [undefined] ? <R>(handler: (options: {
125
228
  args: InferArgs<Args>;
@@ -133,13 +236,24 @@ interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = und
133
236
  args: InferArgs<Args>;
134
237
  ctx: Context;
135
238
  signal: AbortSignal;
136
- }) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
239
+ }) => AsyncGenerator<R, void, void> | AsyncIterable<R>, options?: StreamOptions) => RegisteredStream<Args, R>;
137
240
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalQueryBuilder<ContextOut, Args, Output>;
138
241
  }
139
242
  interface InternalMutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
140
243
  readonly __lunoraProcedure: "mutation";
141
244
  readonly __lunoraVisibility: "internal";
142
245
  input: <A extends ArgsValidator>(validators: A) => InternalMutationBuilder<Context, A & Args, Output>;
246
+ /**
247
+ * Attach static, per-procedure metadata. Merges across calls, is readable
248
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
249
+ * `fn.meta` so codegen and other tooling can enumerate it.
250
+ *
251
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
252
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
253
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
254
+ * can only be executed. Mirrors tRPC's `.meta()`.
255
+ */
256
+ meta: (value: Record<string, unknown>) => InternalMutationBuilder<Context, Args, Output>;
143
257
  mutation: [Output] extends [undefined] ? <R>(handler: (options: {
144
258
  args: InferArgs<Args>;
145
259
  ctx: Context;
@@ -161,6 +275,17 @@ interface InternalActionBuilder<Context, Args extends ArgsValidator, Output = un
161
275
  ctx: Context;
162
276
  }) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
163
277
  input: <A extends ArgsValidator>(validators: A) => InternalActionBuilder<Context, A & Args, Output>;
278
+ /**
279
+ * Attach static, per-procedure metadata. Merges across calls, is readable
280
+ * from middleware as `ctx.meta`, and is stamped onto the registration as
281
+ * `fn.meta` so codegen and other tooling can enumerate it.
282
+ *
283
+ * The point is policy that is DATA rather than a call: `.meta({ rateLimit:
284
+ * "pins/create" })` can be walked to generate a rate-limit registry or docs,
285
+ * where the same policy expressed only as `.use(rateLimit("pins/create"))`
286
+ * can only be executed. Mirrors tRPC's `.meta()`.
287
+ */
288
+ meta: (value: Record<string, unknown>) => InternalActionBuilder<Context, Args, Output>;
164
289
  output: <V extends Validator>(validator: V) => InternalActionBuilder<Context, Args, Infer<V>>;
165
290
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalActionBuilder<ContextOut, Args, Output>;
166
291
  }
@@ -179,38 +304,38 @@ interface DataModelInit<DataModel> {
179
304
  create: (options?: CreateOptions) => LunoraBuilders;
180
305
  }
181
306
  /**
182
- * Entry point for the procedure builder. `dataModel&lt;DM>()` binds the generated
183
- * `DataModel` (phantom for now), and `.create()` yields the public root builders
184
- * plus their `internal*` counterparts.
185
- */
307
+ * Entry point for the procedure builder. `dataModel<DM>()` binds the generated
308
+ * `DataModel` (phantom for now), and `.create()` yields the public root builders
309
+ * plus their `internal*` counterparts.
310
+ */
186
311
  declare const initLunora: {
187
312
  dataModel: <DataModel>() => DataModelInit<DataModel>;
188
313
  };
189
314
  /**
190
- * Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
191
- * `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
192
- * absent or non-Secrets-Store binding throws a directed error pointing at the
193
- * wrangler config; the lookup is lazy, so an unused secret never resolves.
194
- */
315
+ * Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
316
+ * `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
317
+ * absent or non-Secrets-Store binding throws a directed error pointing at the
318
+ * wrangler config; the lookup is lazy, so an unused secret never resolves.
319
+ */
195
320
  declare const createSecrets: (env: Record<string, unknown>) => Secrets;
196
321
  /**
197
- * Redact secrets from a free-form message. Masks, in order: any quoted value
198
- * whose contents look like a credential (so a value surfaced as `received string
199
- * "sk_live_…"` is masked even though the surrounding text is not a token); a
200
- * `scheme://user:password@host` URL credential (the password segment); any
201
- * known-prefix credential token wherever it appears, at any length; any value
202
- * following a secret-named key in `KEY=value` / `KEY: value` form; and any
203
- * remaining bare high-entropy ≥24-char token run anywhere in the message.
204
- *
205
- * This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
206
- * secret under a non-secret-named key (and embedded credentials in shapes not
207
- * enumerated here) can still slip through. Treat it as a backstop — prefer
208
- * structured logging that never serializes raw env/secret fields in the first
209
- * place over relying on post-hoc scrubbing of untrusted data.
210
- *
211
- * Exported because it is independently useful — call it before logging anything
212
- * derived from `env`, request bodies, or thrown errors.
213
- */
322
+ * Redact secrets from a free-form message. Masks, in order: any quoted value
323
+ * whose contents look like a credential (so a value surfaced as `received string
324
+ * "sk_live_…"` is masked even though the surrounding text is not a token); a
325
+ * `scheme://user:password@host` URL credential (the password segment); any
326
+ * known-prefix credential token wherever it appears, at any length; any value
327
+ * following a secret-named key in `KEY=value` / `KEY: value` form; and any
328
+ * remaining bare high-entropy ≥24-char token run anywhere in the message.
329
+ *
330
+ * This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
331
+ * secret under a non-secret-named key (and embedded credentials in shapes not
332
+ * enumerated here) can still slip through. Treat it as a backstop — prefer
333
+ * structured logging that never serializes raw env/secret fields in the first
334
+ * place over relying on post-hoc scrubbing of untrusted data.
335
+ *
336
+ * Exported because it is independently useful — call it before logging anything
337
+ * derived from `env`, request bodies, or thrown errors.
338
+ */
214
339
  declare const redactSecrets: (message: string) => string;
215
340
  /** One key's validation failure, secrets already redacted out of `message`. */
216
341
  interface EnvKeyFailure {
@@ -220,32 +345,31 @@ interface EnvKeyFailure {
220
345
  message: string;
221
346
  }
222
347
  /**
223
- * Thrown when one or more env keys are missing or fail validation. Carries the
224
- * structured list of `failures` (each with the offending `key`) so callers can
225
- * react programmatically; `message` is the joined, secret-redacted summary.
226
- *
227
- * Named export only (no default) per the repo export convention.
228
- */
229
- declare class LunoraEnvError extends Error {
230
- override readonly name = "LunoraEnvError";
348
+ * Thrown when one or more env keys are missing or fail validation. Carries the
349
+ * structured list of `failures` (each with the offending `key`) so callers can
350
+ * react programmatically; `message` is the joined, secret-redacted summary.
351
+ *
352
+ * Named export only (no default) per the repo export convention.
353
+ */
354
+ declare class LunoraEnvError extends LunoraError$1 {
231
355
  readonly failures: ReadonlyArray<EnvKeyFailure>;
232
356
  constructor(failures: ReadonlyArray<EnvKeyFailure>);
233
357
  }
234
358
  /** A record of `v.*` validators describing the expected env shape. */
235
359
  type EnvShape = Record<string, Validator>;
236
360
  /**
237
- * The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
238
- * become optional keys; everything else is required. Mirrors how `InferArgs`
239
- * derives an args object from a validator map.
240
- */
241
- type InferEnv<S extends EnvShape> = { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]> } & { [K in keyof S as undefined extends Infer<S[K]> ? never : K]: Infer<S[K]> };
242
- /**
243
- * The accessor returned by {@link defineEnv}. A typed view over an `env` object
244
- * plus a `.parse(env)` escape hatch that validates every key eagerly.
245
- *
246
- * Call the accessor with the worker's `env` to get the typed, lazily-validated
247
- * proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
248
- */
361
+ * The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
362
+ * become optional keys; everything else is required. Mirrors how `InferArgs`
363
+ * derives an args object from a validator map.
364
+ */
365
+ type InferEnv<S extends EnvShape> = { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]>; } & { [K in keyof S as undefined extends Infer<S[K]> ? never : K]: Infer<S[K]>; };
366
+ /**
367
+ * The accessor returned by {@link defineEnv}. A typed view over an `env` object
368
+ * plus a `.parse(env)` escape hatch that validates every key eagerly.
369
+ *
370
+ * Call the accessor with the worker's `env` to get the typed, lazily-validated
371
+ * proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
372
+ */
249
373
  interface EnvAccessor<S extends EnvShape> {
250
374
  /** Validate every key eagerly and return the typed, plain (non-proxy) object. Use for fail-fast-at-boot. */
251
375
  parse: (env: unknown) => InferEnv<S>;
@@ -253,91 +377,40 @@ interface EnvAccessor<S extends EnvShape> {
253
377
  (env: unknown): InferEnv<S>;
254
378
  }
255
379
  /**
256
- * Define a typed, validated accessor over a Worker's `env`. Pass a record of
257
- * `v.*` validators; receive an accessor that validates lazily per key (cached
258
- * per `env` identity) and infers its output type from the validators.
259
- *
260
- * ```ts
261
- * import { defineEnv, v } from "@lunora/server";
262
- *
263
- * const config = defineEnv({
264
- * STRIPE_KEY: v.string(),
265
- * PORT: v.optional(v.number()),
266
- * });
267
- *
268
- * export default {
269
- * fetch(request, env) {
270
- * const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
271
- * // …
272
- * },
273
- * };
274
- * ```
275
- *
276
- * Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
277
- * invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
278
- */
380
+ * Define a typed, validated accessor over a Worker's `env`. Pass a record of
381
+ * `v.*` validators; receive an accessor that validates lazily per key (cached
382
+ * per `env` identity) and infers its output type from the validators.
383
+ *
384
+ * ```ts
385
+ * import { defineEnv, v } from "@lunora/server";
386
+ *
387
+ * const config = defineEnv({
388
+ * STRIPE_KEY: v.string(),
389
+ * PORT: v.optional(v.number()),
390
+ * });
391
+ *
392
+ * export default {
393
+ * fetch(request, env) {
394
+ * const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
395
+ * // …
396
+ * },
397
+ * };
398
+ * ```
399
+ *
400
+ * Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
401
+ * invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
402
+ */
279
403
  declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
404
+ declare class LunoraError extends LunoraError$1 {
405
+ constructor(code: LunoraErrorCode, message?: string, data?: unknown);
406
+ }
280
407
  /**
281
- * Canonical error type for Lunora procedures and middleware.
282
- *
283
- * The runtime's structural error mapper keys off `name === "LunoraError"` plus
284
- * the numeric `status`, so throwing one of these from a handler or middleware
285
- * yields the right RPC/HTTP status without any further wiring. `code` carries
286
- * the machine-readable reason for clients.
287
- */
288
- declare const CODE_STATUS: {
289
- readonly BAD_REQUEST: 400;
290
- readonly CONFLICT: 409;
291
- /**
292
- * `count()` invoked against a table whose context carries an active RLS
293
- * policy. The operation itself is unsupported in an RLS-restricted reader
294
- * (kitcn's documented constraint) — the request is well-formed and the
295
- * caller is authorized, so this is a 422 (semantic conflict) rather than a
296
- * 403 (policy denial).
297
- */
298
- readonly COUNT_RLS_UNSUPPORTED: 422;
299
- readonly FORBIDDEN: 403;
300
- readonly INTERNAL_SERVER_ERROR: 500;
301
- /**
302
- * An analytical reduction (`aggregate` / `groupBy`) was invoked over a
303
- * column that the procedure's `mask()` middleware redacts. A masked column
304
- * can't be summed, averaged, or grouped without leaking the very values the
305
- * mask hides (a group key *is* the raw value; an aggregate is computed from
306
- * it), so the operation fails closed. The request is well-formed and the
307
- * caller is authorized — this is a 422 (semantic conflict), mirroring
308
- * `COUNT_RLS_UNSUPPORTED`.
309
- */
310
- readonly MASK_UNSUPPORTED: 422;
311
- readonly NOT_FOUND: 404;
312
- readonly NOT_IMPLEMENTED: 501;
313
- /**
314
- * A write policy's `when` returned a relation-crossing predicate
315
- * (`some`/`none`/`every`/`is`/`isNot`). The in-memory write-policy evaluator
316
- * has no child fetcher and cannot resolve a relation node, so the policy is
317
- * unsupported as written. Relation predicates are valid in *read* policies
318
- * and query `where` clauses (the pre-resolver handles them there). The
319
- * request is well-formed; this is a 422 (semantic conflict), mirroring the
320
- * sibling `*_UNSUPPORTED` codes.
321
- */
322
- readonly RELATION_PREDICATE_UNSUPPORTED: 422;
323
- readonly TOO_MANY_REQUESTS: 429;
324
- readonly UNAUTHORIZED: 401;
325
- readonly UNPROCESSABLE: 422;
326
- };
327
- type LunoraErrorCode = keyof typeof CODE_STATUS;
328
- declare class LunoraError extends Error {
329
- override readonly name = "LunoraError";
330
- readonly code: LunoraErrorCode;
331
- readonly status: number;
332
- constructor(code: LunoraErrorCode, message?: string);
333
- }
334
- /**
335
- * Minimal structural writer the facade binds over. Declared with **method**
336
- * syntax (not arrow properties) so a more-specifically-typed writer — both
337
- * `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
338
- * stays assignable under bivariant parameter checking. That is the whole reason
339
- * the shared helper can serve both callers, hence the rule exemption.
340
- */
408
+ * Minimal structural writer the facade binds over. Declared with **method**
409
+ * syntax (not arrow properties) so a more-specifically-typed writer — both
410
+ * `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer
411
+ * stays assignable under bivariant parameter checking. That is the whole reason
412
+ * the shared helper can serve both callers, hence the rule exemption.
413
+ */
341
414
  interface FacadeWriterLike {
342
415
  aggregate(tableName: string, options: unknown): Promise<unknown>;
343
416
  count(tableName: string, where?: unknown): Promise<number>;
@@ -349,6 +422,11 @@ interface FacadeWriterLike {
349
422
  }, expectedTable?: string): Promise<{
350
423
  deleted: number;
351
424
  }>;
425
+ deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
426
+ limit?: number;
427
+ }): Promise<{
428
+ deleted: number;
429
+ }>;
352
430
  findFirst(tableName: string, args?: unknown): Promise<unknown>;
353
431
  findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
354
432
  findMany(tableName: string, args?: unknown): Promise<unknown>;
@@ -357,15 +435,27 @@ interface FacadeWriterLike {
357
435
  insert(tableName: string, document: Record<string, unknown>): Promise<string>;
358
436
  insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
359
437
  limit?: number;
360
- }): Promise<string[]>;
438
+ skipDuplicates?: boolean;
439
+ }): Promise<(string | null)[]>;
361
440
  patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
362
441
  patchMany?(patches: ReadonlyArray<{
363
442
  id: string;
364
443
  patch: Record<string, unknown>;
365
444
  }>, options?: {
366
445
  limit?: number;
367
- }, expectedTable?: string): Promise<void>;
446
+ }, expectedTable?: string): Promise<{
447
+ patched: number;
448
+ }>;
449
+ patchWhere?(tableName: string, args: {
450
+ patch: Record<string, unknown>;
451
+ where: Record<string, unknown>;
452
+ }, options?: {
453
+ limit?: number;
454
+ }): Promise<{
455
+ patched: number;
456
+ }>;
368
457
  query(tableName: string): {
458
+ withGeoIndex(indexName: string, build: (q: unknown) => unknown): unknown;
369
459
  withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
370
460
  };
371
461
  rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
@@ -378,11 +468,19 @@ interface FacadeEntry {
378
468
  aggregate: (options: unknown) => Promise<unknown>;
379
469
  count: (where?: unknown) => Promise<number>;
380
470
  delete: (id: string) => Promise<void>;
381
- deleteMany: (ids: ReadonlyArray<string>, options?: {
382
- limit?: number;
383
- }) => Promise<{
384
- deleted: number;
385
- }>;
471
+ deleteMany: {
472
+ (ids: ReadonlyArray<string>, options?: {
473
+ limit?: number;
474
+ }): Promise<{
475
+ deleted: number;
476
+ }>;
477
+ (args: {
478
+ limit?: number;
479
+ where: Record<string, unknown>;
480
+ }): Promise<{
481
+ deleted: number;
482
+ }>;
483
+ };
386
484
  /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
387
485
  exists: (where?: unknown) => Promise<boolean>;
388
486
  findFirst: (args?: unknown) => Promise<unknown>;
@@ -393,16 +491,34 @@ interface FacadeEntry {
393
491
  /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
394
492
  hardDelete: (id: string) => Promise<void>;
395
493
  insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
494
+ /**
495
+ * Insert many documents into this table in one call. With
496
+ * `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
497
+ * instead of failing the batch. The typed facade narrows the return to
498
+ * `Id<T>[]` when skipDuplicates is not requested.
499
+ */
396
500
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
397
501
  limit?: number;
398
- }) => Promise<string[]>;
502
+ skipDuplicates?: boolean;
503
+ }) => Promise<(string | null)[]>;
399
504
  patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
400
- patchMany: (patches: ReadonlyArray<{
401
- id: string;
402
- values: Record<string, unknown>;
403
- }>, options?: {
404
- limit?: number;
405
- }) => Promise<void>;
505
+ patchMany: {
506
+ (patches: ReadonlyArray<{
507
+ id: string;
508
+ values: Record<string, unknown>;
509
+ }>, options?: {
510
+ limit?: number;
511
+ }): Promise<{
512
+ patched: number;
513
+ }>;
514
+ (args: {
515
+ limit?: number;
516
+ values: Record<string, unknown>;
517
+ where: Record<string, unknown>;
518
+ }): Promise<{
519
+ patched: number;
520
+ }>;
521
+ };
406
522
  rank: (indexName: string, options: unknown) => Promise<unknown>;
407
523
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
408
524
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
@@ -412,15 +528,16 @@ interface FacadeEntry {
412
528
  upsert: (args: UpsertArgs) => Promise<UpsertResult>;
413
529
  /** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
414
530
  upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
531
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => unknown;
415
532
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
416
533
  }
417
534
  /** Options accepted by the per-table `insert` accessor. */
418
535
  interface FacadeInsertOptions {
419
536
  /**
420
- * When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
421
- * silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
422
- * other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
423
- */
537
+ * When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
538
+ * silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
539
+ * other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
540
+ */
424
541
  skipDuplicates?: boolean;
425
542
  }
426
543
  /** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
@@ -448,17 +565,17 @@ interface UpsertManyArgs {
448
565
  target: UpsertTarget;
449
566
  }
450
567
  /**
451
- * Bind a structural writer to one table, producing its `ctx.db` table accessor.
452
- *
453
- * The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
454
- * `tableName` as `expectedTable` so the underlying writer scopes its id lookup
455
- * to this table. Without it, a branded `Id&lt;"posts">` carrying another table's
456
- * id would resolve cross-table (the writer probes every table by id), letting
457
- * `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
458
- * mutate — a row in an unrelated table (IDOR). Writers that ignore the second
459
- * argument keep their previous global behaviour; the scoping is opt-in via this
460
- * forwarded name.
461
- */
568
+ * Bind a structural writer to one table, producing its `ctx.db` table accessor.
569
+ *
570
+ * The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
571
+ * `tableName` as `expectedTable` so the underlying writer scopes its id lookup
572
+ * to this table. Without it, a branded `Id<"posts">` carrying another table's
573
+ * id would resolve cross-table (the writer probes every table by id), letting
574
+ * `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
575
+ * mutate — a row in an unrelated table (IDOR). Writers that ignore the second
576
+ * argument keep their previous global behaviour; the scoping is opt-in via this
577
+ * forwarded name.
578
+ */
462
579
  declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) => FacadeEntry;
463
580
  /** The kitcn-style `ctx.orm` namespace over a per-table facade map. */
464
581
  interface OrmLike {
@@ -479,21 +596,40 @@ declare const bindOrm: (facade: Record<string, FacadeEntry>) => OrmLike;
479
596
  /** HTTP verbs the typed {@link httpRoute} builder can bind to. */
480
597
  type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
481
598
  /**
482
- * Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
483
- * HTTP actions run in the worker (the "action runtime"), separate from the
484
- * transactional store, so there is no direct `db` / `vectors` / `scheduler` /
485
- * `storage` surface — reach the data layer through `runQuery` / `runMutation` /
486
- * `runAction`, which forward to the owning shard.
487
- */
488
- type HttpActionCtx = Pick<ActionCtx, "auth" | "fetch" | "runAction" | "runMutation" | "runQuery">;
599
+ * Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
600
+ * HTTP actions run in the worker (the "action runtime"), separate from the
601
+ * transactional store, so there is no direct `db` / `vectors` surface reach the
602
+ * data layer through `runQuery` / `runMutation` / `runAction`, which forward to
603
+ * the owning shard. `db`'s absence is principled: an HTTP handler is not
604
+ * transactional.
605
+ *
606
+ * `scheduler` and `storage` ARE present, because neither needs the shard — the
607
+ * scheduler talks to the scheduler DO, and R2 is a worker binding an HTTP
608
+ * handler can reach where an action does. Both are optional: each exists only
609
+ * when the app declared the matching capability (`.scheduler(...)` /
610
+ * `.storage(...)`) on the generated app builder.
611
+ *
612
+ * Omitting them was costly out of proportion to the gap. Without `scheduler`,
613
+ * "receive webhook → enqueue the real work → return 200" — the shape HTTP
614
+ * actions exist for — forced a hop through a mutation plus a closed allow-list
615
+ * of target strings, because a function reference cannot cross the RPC boundary
616
+ * and a free-form target on an unauthenticated endpoint is a "call any internal
617
+ * function" primitive. Without `storage`, any helper the ctx was threaded into
618
+ * had to be typed for its storage-touching branch, so a handler was barred from
619
+ * the helper even on the branches that never went near storage.
620
+ */
621
+ type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery"> & {
622
+ readonly scheduler?: ActionCtx["scheduler"];
623
+ readonly storage?: ActionCtx["storage"];
624
+ };
489
625
  /** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
490
626
  type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
491
627
  /**
492
- * The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
493
- * The runtime injects the per-request {@link HttpActionCtx} on the private
494
- * `__lunoraCtx` binding; the router's lifting middleware promotes it to
495
- * `c.var.lunora` so handlers can read it as a typed variable.
496
- */
628
+ * The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
629
+ * The runtime injects the per-request {@link HttpActionCtx} on the private
630
+ * `__lunoraCtx` binding; the router's lifting middleware promotes it to
631
+ * `c.var.lunora` so handlers can read it as a typed variable.
632
+ */
497
633
  interface LunoraHttpEnv {
498
634
  Bindings: Record<string, unknown> & {
499
635
  __lunoraCtx?: HttpActionCtx;
@@ -507,31 +643,31 @@ type LunoraHttpApp = Hono<LunoraHttpEnv>;
507
643
  /** A compiled route handler: a hono handler that resolves to a raw {@link Response}. */
508
644
  type LunoraRouteHandler = (c: Context<LunoraHttpEnv>) => Promise<Response>;
509
645
  /**
510
- * Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
511
- * hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
512
- * runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
513
- * {@link httpRouter}; `request` is the underlying `c.req.raw`.
514
- */
646
+ * Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
647
+ * hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
648
+ * runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
649
+ * {@link httpRouter}; `request` is the underlying `c.req.raw`.
650
+ */
515
651
  declare const httpAction: (handler: HttpActionHandler) => LunoraRouteHandler;
516
652
  /**
517
- * Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
518
- * the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
519
- * {@link httpAction} and the typed {@link httpRoute} builder can read the action
520
- * context. The full hono surface is available — plugins, path params, `.route`:
521
- *
522
- * ```ts
523
- * const app = httpRouter();
524
- * app.use("*", cors());
525
- * app.post("/webhook", httpAction(onWebhook));
526
- * app.get("/users/:id", getUser);
527
- * export default createWorker({ httpRouter: app, ... });
528
- * ```
529
- *
530
- * The lifting middleware throws if the context is absent. `createWorker` injects
531
- * it on every request the router sees, so this only trips when the app is run
532
- * outside the runtime — a misconfiguration we surface loudly rather than let
533
- * `c.var.lunora` be silently `undefined` despite its non-optional type.
534
- */
653
+ * Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
654
+ * the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
655
+ * {@link httpAction} and the typed {@link httpRoute} builder can read the action
656
+ * context. The full hono surface is available — plugins, path params, `.route`:
657
+ *
658
+ * ```ts
659
+ * const app = httpRouter();
660
+ * app.use("*", cors());
661
+ * app.post("/webhook", httpAction(onWebhook));
662
+ * app.get("/users/:id", getUser);
663
+ * export default createWorker({ httpRouter: app, ... });
664
+ * ```
665
+ *
666
+ * The lifting middleware throws if the context is absent. `createWorker` injects
667
+ * it on every request the router sees, so this only trips when the app is run
668
+ * outside the runtime — a misconfiguration we surface loudly rather than let
669
+ * `c.var.lunora` be silently `undefined` despite its non-optional type.
670
+ */
535
671
  declare const httpRouter: () => LunoraHttpApp;
536
672
  /** The `{ ctx, searchParams, body, params }` a typed route handler receives. */
537
673
  interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator> {
@@ -541,11 +677,12 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
541
677
  searchParams: InferArgs<SearchParams>;
542
678
  }
543
679
  /**
544
- * The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
545
- * handler receives. There is no parsed `body` — streams are typically GET, and
546
- * the raw `request` is exposed if a handler needs to read the body itself.
547
- * `signal` is tripped when the client disconnects.
548
- */
680
+ * The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
681
+ * handler receives. There is no parsed `body` — streams are typically GET, and
682
+ * the raw `request` is exposed if a handler needs to read the body itself.
683
+ * `signal` is tripped when the client disconnects.
684
+ * @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
685
+ */
549
686
  interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
550
687
  ctx: HttpActionCtx;
551
688
  params: InferArgs<Params>;
@@ -554,35 +691,51 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
554
691
  signal: AbortSignal;
555
692
  }
556
693
  /**
557
- * A typed REST route under construction. `.searchParams()` / `.body()` /
558
- * `.params()` accumulate validator maps (later calls merge, a colliding key
559
- * wins) that decode the URL query, JSON body, and hono path params into the
560
- * handler's typed `searchParams` / `body` / `params`. Like the procedure
561
- * builder, `.output(validator)` defaults to the `undefined` sentinel — while
562
- * unset the handler is generic over its own return; once set the handler must
563
- * return that type and the result is parsed through the validator before
564
- * serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
565
- * `Output` doesn't distribute and the test is for the exact sentinel.
566
- *
567
- * The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
568
- * directly with `app.get(path, route)`.
569
- */
694
+ * A typed REST route under construction. `.searchParams()` / `.body()` /
695
+ * `.params()` accumulate validator maps (later calls merge, a colliding key
696
+ * wins) that decode the URL query, JSON body, and hono path params into the
697
+ * handler's typed `searchParams` / `body` / `params`. Like the procedure
698
+ * builder, `.output(validator)` defaults to the `undefined` sentinel — while
699
+ * unset the handler is generic over its own return; once set the handler must
700
+ * return that type and the result is parsed through the validator before
701
+ * serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
702
+ * `Output` doesn't distribute and the test is for the exact sentinel.
703
+ *
704
+ * The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
705
+ * directly with `app.get(path, route)`.
706
+ */
570
707
  interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
571
708
  body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
709
+ /**
710
+ * Attach a `Cache-Control` header to the response. Only meaningful when
711
+ * Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
712
+ */
713
+ cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
714
+ /**
715
+ * Attach a `Cache-Tag` header to the response for tag-based purging via
716
+ * `ctx.cache.purge({ tags: [...] })`.
717
+ */
718
+ cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
572
719
  handler: [Output] extends [undefined] ? <R>(handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Promise<R> | R) => LunoraRouteHandler : (handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Output | Promise<Output>) => LunoraRouteHandler;
573
720
  output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
574
721
  params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
575
722
  searchParams: <S extends ArgsValidator>(validators: S) => HttpRouteBuilder<S & SearchParams, Body, Params, Output>;
576
723
  /**
577
- * Terminal: declare this route as a streaming Server-Sent Events endpoint.
578
- * The handler is an async generator (or any function returning an
579
- * `AsyncIterable&lt;R>`) that yields one chunk per SSE `data:` frame; on
580
- * iterator completion the route writes a final `event: complete` frame; on
581
- * throw, an `event: error` frame is written with `{code, message}` before
582
- * the stream closes. The chunks are JSON-encoded; `R` is inferred from the
583
- * handler's yielded type.
584
- */
724
+ * Terminal: declare this route as a streaming Server-Sent Events endpoint.
725
+ * The handler is an async generator (or any function returning an
726
+ * `AsyncIterable<R>`) that yields one chunk per SSE `data:` frame; on
727
+ * iterator completion the route writes a final `event: complete` frame; on
728
+ * throw, an `event: error` frame is written with `{code, message}` before
729
+ * the stream closes. The chunks are JSON-encoded; `R` is inferred from the
730
+ * handler's yielded type.
731
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
732
+ */
585
733
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
734
+ /**
735
+ * Attach a `Vary` header to the response so Cloudflare stores separate
736
+ * cached variants per distinct value of the listed request headers.
737
+ */
738
+ vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
586
739
  }
587
740
  /** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
588
741
  type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
@@ -597,33 +750,33 @@ interface HttpRoute {
597
750
  put: HttpRouteFactory;
598
751
  }
599
752
  /**
600
- * Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
601
- * typed route and a hand-written {@link httpAction} are interchangeable when
602
- * mounted on {@link httpRouter}:
603
- *
604
- * ```ts
605
- * export const listTodos = httpRoute
606
- * .get("/api/todos")
607
- * .searchParams({ limit: v.number(), q: v.optional(v.string()) })
608
- * .output(v.array(v.object({ id: v.string(), text: v.string() })))
609
- * .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
610
- *
611
- * export const getTodo = httpRoute
612
- * .get("/api/todos/:id")
613
- * .params({ id: v.string() })
614
- * .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
615
- *
616
- * const app = httpRouter();
617
- * app.get("/api/todos", listTodos);
618
- * app.get("/api/todos/:id", getTodo);
619
- * ```
620
- */
753
+ * Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
754
+ * typed route and a hand-written {@link httpAction} are interchangeable when
755
+ * mounted on {@link httpRouter}:
756
+ *
757
+ * ```ts
758
+ * export const listTodos = httpRoute
759
+ * .get("/api/todos")
760
+ * .searchParams({ limit: v.number(), q: v.optional(v.string()) })
761
+ * .output(v.array(v.object({ id: v.string(), text: v.string() })))
762
+ * .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
763
+ *
764
+ * export const getTodo = httpRoute
765
+ * .get("/api/todos/:id")
766
+ * .params({ id: v.string() })
767
+ * .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
768
+ *
769
+ * const app = httpRouter();
770
+ * app.get("/api/todos", listTodos);
771
+ * app.get("/api/todos/:id", getTodo);
772
+ * ```
773
+ */
621
774
  declare const httpRoute: HttpRoute;
622
775
  /**
623
- * Structural view of an R2 object body, as returned by `@lunora/storage`'s
624
- * `download()`. Re-declared here (not imported) so `@lunora/server` takes no
625
- * runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
626
- */
776
+ * Structural view of an R2 object body, as returned by `@lunora/storage`'s
777
+ * `download()`. Re-declared here (not imported) so `@lunora/server` takes no
778
+ * runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
779
+ */
627
780
  interface StorageObjectBody {
628
781
  /** The object body stream (`null` for a zero-byte object). */
629
782
  body: ReadableStream | null;
@@ -643,8 +796,22 @@ interface StorageRange {
643
796
  length: number;
644
797
  offset: number;
645
798
  }
646
- /** The minimal storage surface {@link serveStorageObject} needs: a metadata-rich `download`. */
647
- interface StorageDownloader {
799
+ /**
800
+ * The minimal storage surface {@link serveStorageObject} needs: a metadata-rich
801
+ * `download`, plus the body-free `head` a range request resolves against.
802
+ *
803
+ * `head` is required rather than optional-with-a-fallback because the fallback
804
+ * is the bug: without it a ranged request has to start a full-object `download`
805
+ * just to learn the size, then throw that body away. `@lunora/storage`'s `head`
806
+ * already degrades internally to a 0-length ranged `get()` on a binding with no
807
+ * HEAD, so there is nothing a caller here could usefully do that it does not.
808
+ */
809
+ interface StorageHead {
810
+ /** Object metadata with no body. `size` is the FULL object size (mirrors R2). */
811
+ head: (key: string) => Promise<Omit<StorageObjectBody, "body"> | null>;
812
+ }
813
+ /** The storage surface {@link serveStorageObject} reads through. */
814
+ interface StorageDownloader extends StorageHead {
648
815
  download: (key: string, options?: {
649
816
  range?: StorageRange;
650
817
  }) => Promise<StorageObjectBody | null>;
@@ -654,148 +821,400 @@ interface ContextWithStorage {
654
821
  storage: StorageDownloader;
655
822
  }
656
823
  /**
657
- * Stream a stored object as an HTTP {@link Response} from an `httpAction`
658
- * handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
659
- * Honors a single-range `Range` request **206 Partial Content** with
660
- * `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
661
- * **404**; an out-of-bounds range is a **416** with a `Content-Range` of
662
- * `bytes` star-slash-size.
663
- *
664
- * A range request re-issues the `download()` with the resolved `{ offset, length }`
665
- * window so R2 streams only those bytes back to the Worker — the slice is never
666
- * buffered in the isolate. The first `download()` is used only for the object's
667
- * size + metadata (its body is left unread and cancelled). For very large
668
- * objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since the
669
- * client then ranges against R2/CDN directly with no Worker hop.
670
- */
824
+ * True when `value` is safe to use as an HTTP header field-value: no CR, LF, or
825
+ * NUL. Guards against response-header injection / `Headers`-construction throws
826
+ * when reflecting attacker-influenced object metadata (e.g. a stored
827
+ * `Content-Type`). Exported (see the `export {}` at the file end) so an `httpAction`
828
+ * handler can guard a request-derived header value before writing it the fix the
829
+ * `http_action_response_header_injection` advisor lint points to.
830
+ */
831
+ declare const isSafeHeaderValue: (value: string) => boolean;
832
+ /**
833
+ * Stream a stored object as an HTTP {@link Response} from an `httpAction`
834
+ * handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
835
+ * Honors a single-range `Range` request **206 Partial Content** with
836
+ * `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
837
+ * **404**; an out-of-bounds range is a **416** with a `Content-Range` of
838
+ * `bytes` star-slash-size.
839
+ *
840
+ * A range request resolves its window against a body-free `head()`, then issues
841
+ * ONE `download()` with the resolved `{ offset, length }` so R2 streams just
842
+ * those bytes — the slice is never buffered in the isolate, and no full-object
843
+ * body transfer is started only to be cancelled. A request that cannot produce a
844
+ * 206 at all (no `Range`, multi-range, malformed) skips the `head()` entirely and
845
+ * streams straight from a single `download()`. For very
846
+ * large objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since
847
+ * the client then ranges against R2/CDN directly with no Worker hop.
848
+ */
671
849
  declare const serveStorageObject: (context: ContextWithStorage, key: string, request: Request) => Promise<Response>;
850
+ /**
851
+ * What the worker does with a resolver's identity when it fails contract
852
+ * validation (a forged / malformed claim set arriving from an untrusted token).
853
+ * `"anonymous"` (default, safe) treats the request as anonymous, so the bad
854
+ * identity never reaches a policy as a valid identity (`ctx.auth.userId`
855
+ * becomes `undefined`). `"reject"` fails the request closed (a `401`) — use
856
+ * when a malformed credential should be a hard error, not a silent downgrade.
857
+ */
858
+ type IdentityRejectMode = "anonymous" | "reject";
859
+ /** Options for {@link defineIdentity}. */
860
+ interface DefineIdentityOptions {
861
+ /**
862
+ * How to handle a resolver identity that violates the contract at the trust
863
+ * boundary. Defaults to `"anonymous"` (a forged claim set is downgraded to
864
+ * anonymous rather than flowing in as an unchecked cast).
865
+ */
866
+ readonly onInvalid?: IdentityRejectMode;
867
+ }
868
+ /** Result of validating a candidate identity against the contract. */
869
+ type IdentityValidation = {
870
+ ok: true;
871
+ } | {
872
+ error: string;
873
+ ok: false;
874
+ };
875
+ /**
876
+ * A declared identity claim contract. Carries the codegen discovery brand, the
877
+ * declared claim validators, the reject policy, and a runtime `validate`. The
878
+ * `TClaims` type parameter is the inferred claim shape (always extending
879
+ * `{ userId: string }`); it is phantom (no runtime field) and exists so
880
+ * `@lunora/codegen` and {@link InferIdentity} can recover the type.
881
+ */
882
+ interface IdentityContract<TClaims extends {
883
+ userId: string;
884
+ } = {
885
+ userId: string;
886
+ }> {
887
+ /**
888
+ * Phantom carrier for the inferred claim type. Never populated at runtime
889
+ * (`undefined`); present only so the type flows to codegen / {@link InferIdentity}.
890
+ */
891
+ readonly __claimType?: TClaims;
892
+ readonly __lunoraIdentity: true;
893
+ /** The declared claim validators (a `@lunora/values` validator map). */
894
+ readonly claims: ValidatorMap;
895
+ /** Reject policy applied at the trust boundary. See {@link IdentityRejectMode}. */
896
+ readonly onInvalid: IdentityRejectMode;
897
+ /**
898
+ * Validate a resolver's returned identity against the declared claims. On
899
+ * success the caller keeps the original identity untouched (so undeclared
900
+ * claims are forwarded verbatim, preserving today's behaviour); on failure
901
+ * the worker applies the `onInvalid` policy.
902
+ */
903
+ validate: (identity: Record<string, unknown>) => IdentityValidation;
904
+ }
905
+ /** Recover the declared claim type from a {@link defineIdentity} contract. */
906
+ type InferIdentity<T> = T extends IdentityContract<infer TClaims> ? TClaims : never;
907
+ /**
908
+ * Declare the identity claim contract. `claims` is a `@lunora/values` validator
909
+ * map whose inferred type must extend `{ userId: string }` — if it does not
910
+ * (e.g. `userId` is missing or not a required string), the argument type
911
+ * collapses to `never` and the call fails to typecheck.
912
+ * @example
913
+ * export const identity = defineIdentity({ userId: v.string(), tenantId: v.optional(v.string()), scopes: v.optional(v.array(v.string())) });
914
+ */
915
+ declare const defineIdentity: <A extends ValidatorMap>(claims: InferValidatorMap<A> extends {
916
+ userId: string;
917
+ } ? A : never, options?: DefineIdentityOptions) => IdentityContract<InferValidatorMap<A> & {
918
+ userId: string;
919
+ }>;
672
920
  /** Handler for a connection-lifecycle hook. */
673
921
  type LifecycleHandler = (context: MutationCtx, event: LifecycleEvent) => Promise<void> | void;
922
+ /** Handler for a shard-init hook. */
923
+ type ShardInitHandler = (context: MutationCtx, event: ShardInitEvent) => Promise<void> | void;
674
924
  /** Register a hook that fires once when a client's WebSocket connects. */
675
925
  declare const onConnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
676
926
  /** Register a hook that fires once when a client's WebSocket disconnects. */
677
927
  declare const onDisconnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
678
928
  /**
679
- * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
680
- * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-onesame identity
681
- * resolver, same `can(...)` permission check — so an author can branch a mask
682
- * on the caller's role/permission. `row` is the full pre-mask row the column
683
- * belongs to; `column` is the column currently being masked. Both are absent
684
- * when the context is used for the procedure-wide `bypass` check (no specific
685
- * cell is in play yet).
686
- */
687
- interface MaskContext<Context = unknown> {
688
- readonly auth: {
689
- /** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
690
- readonly can: (permission: Permission | string) => boolean;
691
- readonly identity?: Record<string, unknown> | null;
692
- readonly roles: ReadonlyArray<string>;
693
- readonly userId: null | string;
694
- };
695
- /** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
696
- readonly column?: string;
697
- readonly ctx: Context;
698
- /** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
699
- readonly row?: Record<string, unknown>;
929
+ * Register a hook that fires ONCE per Durable Object instance, before any
930
+ * handler on that instance can runthe re-init half of `.memory()` tables.
931
+ *
932
+ * A shard is not a process that stays up. Cloudflare reconstructs the Durable
933
+ * Object after every eviction, and a shard whose sockets are hibernating is
934
+ * evicted routinely, so "cold start" is a steady-state event rather than a rare
935
+ * one. Everything the shard held in memory is gone at that moment: the JS heap,
936
+ * and every `.memory()` table, which the framework has already cleared by the
937
+ * time this hook runs.
938
+ *
939
+ * ```ts
940
+ * // lunora/init.ts
941
+ * import { onShardInit } from "@lunora/server";
942
+ *
943
+ * export const warm = onShardInit(async (ctx, event) => {
944
+ * // Rebuild ephemeral state from the durable tables that outlived us.
945
+ * for await (const member of ctx.db.roomMembers.iterate({ where: { roomId: event.shardKey } })) {
946
+ * await ctx.db.presence.insert({ userId: member.userId, status: "away" });
947
+ * }
948
+ * });
949
+ * ```
950
+ *
951
+ * **Ordering is the guarantee.** Memory tables are cleared, then every init hook
952
+ * runs to completion, and only then does the dispatch that triggered the cold
953
+ * start proceed. No handler, subscription refresh, alarm, or shape poke can
954
+ * observe a memory table in the gap. Hooks run sequentially in manifest order,
955
+ * so one may depend on state an earlier one wrote.
956
+ *
957
+ * **It is a mutation, and it runs on every cold start.** Keep it cheap and keep
958
+ * it idempotent: it is on the latency path of the request that woke the shard,
959
+ * and it will run again — many times — over the shard's life. Writing to durable
960
+ * tables from here is legal and occasionally right, but remember it is a
961
+ * rebuild, not a migration; use `defineMigration` for anything that should
962
+ * happen once.
963
+ *
964
+ * **No caller identity.** The hook dispatches as a trusted system call with no
965
+ * request identity — `ctx.auth` is anonymous and RLS does not apply even under
966
+ * `.rls("required")`, exactly as for a cron tick or a migration. RLS scopes rows
967
+ * to a user and an init hook has none, so `ctx.db` here sees every row: scope
968
+ * your reads yourself. (`onConnect`/`onDisconnect` are the opposite case — they
969
+ * carry the socket's verified identity and stay RLS-guarded.)
970
+ *
971
+ * A throw is logged and does NOT fail the dispatch that woke the shard — an init
972
+ * hook that cannot rebuild presence must not take the whole shard down with it.
973
+ * The table is then cleared but not refilled, so reads see nothing. A failure
974
+ * EARLIER, before the framework's clear runs, instead leaves the previous
975
+ * instance's rows in place: a memory table's rows live in SQLite until they are
976
+ * deleted, so an eviction on its own does not remove them.
977
+ */
978
+ declare const onShardInit: (handler: ShardInitHandler) => RegisteredLifecycleHook;
979
+ /** Default `limit` when the caller doesn't ask for one. */
980
+ declare const DEFAULT_LIMIT = 25;
981
+ /** Default ceiling on `limit`, so one request can't ask for an unbounded page. */
982
+ declare const DEFAULT_MAX_LIMIT = 100;
983
+ /**
984
+ * Per-field predicate accepted for a declared filter column. An alias of the
985
+ * `ctx.db` `where` DSL's own operator type rather than a copy, so the two cannot
986
+ * drift as operators are added.
987
+ */
988
+ type ListFilterOperators<T> = WhereOperators<T>;
989
+ /**
990
+ * The filter allow-list a caller may declare: a subset of the document's own
991
+ * columns, each with a validator for that column's type. Constraining the KEYS to
992
+ * `keyof Doc` is what turns a typo'd or renamed column into a compile error
993
+ * instead of a predicate that silently never matches.
994
+ */
995
+ type ListFilterShape<TDocument> = { [K in keyof TDocument & string]?: Validator<TDocument[K]>; };
996
+ /** The `where` argument: each declared filter column, optionally, as a bare value or an operator object. */
997
+ type ListWhere<F> = { [K in keyof F]?: Infer<NonNullable<F[K]>> | ListFilterOperators<Infer<NonNullable<F[K]>>>; };
998
+ /** One `orderBy` entry. `direction` defaults to `"asc"`. */
999
+ interface ListOrderByEntry<O extends string> {
1000
+ direction?: "asc" | "desc";
1001
+ field: O;
1002
+ }
1003
+ /** The decoded arguments a {@link defineListArgs} endpoint receives. */
1004
+ interface ListArgsValue<F, O extends string> {
1005
+ cursor?: null | number | string;
1006
+ limit?: number;
1007
+ orderBy?: ListOrderByEntry<O>[];
1008
+ where?: ListWhere<F>;
700
1009
  }
701
- /**
702
- * A custom masking function. Receives the raw cell value and the
703
- * {@link MaskContext}, returns the value to surface. Use it for partial masks
704
- * (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
705
- * or format-preserving tokens. A function that **throws** fails closed — the
706
- * cell is redacted to `null`, never leaked raw.
707
- */
708
- type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
709
- /**
710
- * How a column is masked:
711
- *
712
- * - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
713
- * the right choice for any value that must actually be kept secret.
714
- * - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
715
- * same input always yields the same token (joinable/groupable client-side).
716
- * **This is NOT a confidentiality control.** It is a non-cryptographic,
717
- * unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
718
- * phone numbers, SSNs) are brute-force-recoverable by the very caller you are
719
- * masking from, and identical values always produce identical tokens across
720
- * rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
721
- * stable pseudonym for grouping/joining and leaking the value is acceptable —
722
- * never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
723
- * - a {@link MaskFn} author-defined transform (partial mask, role-aware reveal).
724
- */
725
- type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
726
- /** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
727
- type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
728
- /**
729
- * The mask declaration passed to `mask(...)`: a table → column → strategy map.
730
- * Deliberately a plain object literal so the codegen feeder can statically read
731
- * which columns a procedure masks (powering the `mask_uncovered_pii_column`
732
- * advisor lint), exactly as the RLS feeder reads policy tables.
733
- */
734
- type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
735
- /**
736
- * Options for `mask(policies, options)`.
737
- *
738
- * - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
739
- * inside a {@link MaskFn} identical to `rls(policies, { roles })`. A role
740
- * not listed grants no permissions (fails closed for unknown roles).
741
- * - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
742
- * mask is skipped (the caller sees raw values). Use it for a privileged
743
- * viewer `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
744
- * branching every column when an entire class of caller should see clear data.
745
- */
746
- interface MaskOptions<Context = unknown> {
747
- readonly bypass?: (context: MaskContext<Context>) => boolean;
748
- readonly roles?: ReadonlyArray<Role>;
1010
+ interface DefineListArgsConfig<F, O extends string> {
1011
+ /** `limit` applied when the caller omits one. Defaults to 25. */
1012
+ readonly defaultLimit?: number;
1013
+ /**
1014
+ * Allow-list of filterable columns. Publish only columns an index can serve;
1015
+ * anything absent here is unreachable from the client.
1016
+ */
1017
+ readonly filter: F;
1018
+ /** Ceiling on `in` / `notIn` array length — one bound parameter each. Defaults to 100. */
1019
+ readonly maxInValues?: number;
1020
+ /** Ceiling on `limit`; a larger request is clamped down, not rejected. Defaults to 100. */
1021
+ readonly maxLimit?: number;
1022
+ /** Ceiling on how many `orderBy` entries a request may ask for. Defaults to 8. */
1023
+ readonly maxOrderBy?: number;
1024
+ /** Allow-list of sortable columns. Pass `[]` to fix the order server-side. */
1025
+ readonly orderBy: ReadonlyArray<O>;
1026
+ }
1027
+ /** The validator map handed to `.input()`. Typed precisely so `args` infers end-to-end. */
1028
+ interface ListArgsValidators<F, O extends string> {
1029
+ cursor: ColumnValidator<null | number | string | undefined, null | number | string | undefined>;
1030
+ limit: ColumnValidator<number | undefined, number | undefined>;
1031
+ orderBy: ColumnValidator<ListOrderByEntry<O>[] | undefined, ListOrderByEntry<O>[] | undefined>;
1032
+ where: ColumnValidator<ListWhere<F> | undefined, ListWhere<F> | undefined>;
1033
+ }
1034
+ interface ListArgsSpec<TDocument, F, O extends string> {
1035
+ /** Spread into `.input(...)` `{ cursor, limit, orderBy, where }`. */
1036
+ readonly args: ListArgsValidators<F, O>;
1037
+ /**
1038
+ * Translate the decoded arguments into the `findMany` options object:
1039
+ * `limit` clamped into `[1, maxLimit]`, `orderBy` reshaped from
1040
+ * `{ field, direction }[]` into `ctx.db`'s `{ column: direction }[]`.
1041
+ *
1042
+ * Returns `QueryArgs<Doc>` — bound to the table, not free — so a mismatch
1043
+ * between what this helper declares and what the table actually holds is a
1044
+ * compile error at the `findMany` call site.
1045
+ */
1046
+ readonly toQueryArgs: (args: ListArgsValue<F, O>) => QueryArgs$2<TDocument>;
1047
+ }
1048
+ /** Clamp a caller-supplied `limit` into `[1, maxLimit]`; a non-finite value falls back to `fallback`. */
1049
+ declare const clampLimit: (limit: number | undefined, fallback: number, maxLimit: number) => number;
1050
+ /**
1051
+ * Declare the filter / sort / page arguments for a list endpoint, plus the
1052
+ * translation into `ctx.db.<table>.findMany(...)` options. See the module docs
1053
+ * for the shape and the reasoning behind it.
1054
+ *
1055
+ * Curried on the document type: `defineListArgs<Doc<"messages">>()({ })`. The
1056
+ * extra `()` buys the thing that matters — with `Doc` bound, `filter` keys and
1057
+ * `orderBy` entries are checked against the table's real columns, so a typo or a
1058
+ * column renamed out from under the endpoint is a COMPILE error instead of a
1059
+ * predicate that silently matches nothing. TypeScript has no partial type-argument
1060
+ * inference, so binding `Doc` explicitly while still inferring `F` and `O` from
1061
+ * the config requires the second call.
1062
+ */
1063
+ declare const defineListArgs: <TDocument>() => <F extends ListFilterShape<TDocument>, O extends keyof TDocument & string>(config: DefineListArgsConfig<F, O>) => ListArgsSpec<TDocument, F, O>;
1064
+ /**
1065
+ * Structural mirrors of `@lunora/shard-engine`'s rank-page-row shapes
1066
+ * (`RankPageRowKey` / `RankPageRow` / `ShardRankPageResult`) — the return type
1067
+ * of the writer's `rankPageRows` seam, the cross-shard companion to
1068
+ * `rankPage`.
1069
+ *
1070
+ * Shared by `../rls/middleware` and `../mask/middleware`: both wrap
1071
+ * `rankPageRows` structurally (no `@lunora/shard-engine` import, mirroring how
1072
+ * every other method on their `DatabaseWriterLike`/`MaskDatabase` projections
1073
+ * is hand-mirrored rather than imported) and both need the exact same result
1074
+ * shape to type their overrides. A single copy here means the two wrappers
1075
+ * can't drift out of lockstep with each other — see AGENTS.md's platform
1076
+ * parity note on `ShardSqlExec` and the canonical binding `*Like` projections
1077
+ * shipping wrong for exactly this reason (two hand-maintained mirrors of one
1078
+ * upstream type).
1079
+ */
1080
+ /** Structural mirror of `@lunora/shard-engine`'s `RankPageRowKey`. */
1081
+ interface RankPageRowKeyLike {
1082
+ partitionKey: string;
1083
+ rowId: string;
1084
+ sortValues: ReadonlyArray<unknown>;
749
1085
  }
750
- interface QueryPage$1 {
751
- continueCursor: null | string;
752
- isDone: boolean;
753
- page: Record<string, unknown>[];
1086
+ /** Structural mirror of `@lunora/shard-engine`'s `RankPageRow`. */
1087
+ interface RankPageRowLike {
1088
+ doc: Record<string, unknown>;
1089
+ key: RankPageRowKeyLike;
754
1090
  }
1091
+ /** Structural mirror of `@lunora/shard-engine`'s `ShardRankPageResult` — the `rankPageRows` return shape. */
1092
+ interface ShardRankPageResultLike {
1093
+ directions: ReadonlyArray<"asc" | "desc">;
1094
+ hasMore: boolean;
1095
+ rows: ReadonlyArray<RankPageRowLike>;
1096
+ }
1097
+ /**
1098
+ * Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
1099
+ * runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
1100
+ * `restrictsCounts` straight off these option objects, so as long as the
1101
+ * fields here stay name-compatible the wrapper is portable across the two
1102
+ * dialects without an inter-package dependency.
1103
+ */
755
1104
  interface QueryArgs$1 {
756
- baseWhere?: unknown;
1105
+ baseWhere?: WhereInput;
757
1106
  cursor?: null | string;
758
1107
  limit?: number;
759
- where?: unknown;
1108
+ orderBy?: ReadonlyArray<unknown>;
1109
+ /**
1110
+ * Per-target-table read filter the RLS wrapper attaches so a `with` relation
1111
+ * is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
1112
+ * the top-level read: `(table) => readBase(table).baseWhere`.
1113
+ */
1114
+ relationBaseWhere?: (table: string) => undefined | WhereInput;
1115
+ restrictsCounts?: boolean;
1116
+ where?: WhereInput;
760
1117
  with?: Record<string, unknown>;
761
1118
  }
1119
+ interface CountArgs {
1120
+ baseWhere?: WhereInput;
1121
+ relationBaseWhere?: (table: string) => undefined | WhereInput;
1122
+ restrictsCounts?: boolean;
1123
+ where?: WhereInput;
1124
+ }
1125
+ /** Structural mirror of `@lunora/do`'s `AggregateOptions` — only the fields the wrapper touches. */
762
1126
  interface AggregateArgs$1 {
1127
+ baseWhere?: WhereInput;
763
1128
  field?: string;
764
1129
  op: string;
1130
+ relationBaseWhere?: (table: string) => undefined | WhereInput;
1131
+ restrictsCounts?: boolean;
1132
+ where?: WhereInput;
765
1133
  }
1134
+ /** Structural mirror of `@lunora/do`'s `GroupByOptions`. */
766
1135
  interface GroupByArgs$1 {
767
1136
  agg?: {
768
1137
  field?: string;
769
1138
  op: string;
770
1139
  };
1140
+ baseWhere?: WhereInput;
771
1141
  by: ReadonlyArray<string>;
1142
+ relationBaseWhere?: (table: string) => undefined | WhereInput;
1143
+ restrictsCounts?: boolean;
1144
+ where?: WhereInput;
1145
+ }
1146
+ /** Structural mirror of `@lunora/do`'s `RankOptions`. */
1147
+ interface RankArgs {
1148
+ baseWhere?: WhereInput;
1149
+ restrictsCounts?: boolean;
1150
+ row: Record<string, unknown> | string;
1151
+ where?: WhereInput;
1152
+ }
1153
+ /** Structural mirror of `@lunora/do`'s `RankBeforeOptions`. */
1154
+ interface RankBeforeArgs {
1155
+ partitionKey: string;
1156
+ restrictsCounts?: boolean;
1157
+ rowId: string;
1158
+ sortValues: ReadonlyArray<unknown>;
1159
+ }
1160
+ /** Structural mirror of `@lunora/do`'s `RankPageOptions`. */
1161
+ interface RankPageArgs {
1162
+ baseWhere?: WhereInput;
1163
+ cursor?: null | string;
1164
+ restrictsCounts?: boolean;
1165
+ take?: number;
1166
+ where?: WhereInput;
1167
+ }
1168
+ interface QueryPage$1 {
1169
+ continueCursor: null | string;
1170
+ isDone: boolean;
1171
+ page: Record<string, unknown>[];
772
1172
  }
773
1173
  interface TableReaderLike$1 {
774
1174
  collect: () => Promise<Record<string, unknown>[]>;
775
1175
  filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike$1;
776
1176
  first: () => Promise<Record<string, unknown> | null>;
777
- order: (direction: "asc" | "desc") => TableReaderLike$1;
778
1177
  paginate: (options: {
779
1178
  cursor?: null | string;
780
1179
  numItems: number;
781
1180
  }) => Promise<QueryPage$1>;
782
1181
  take: (limit: number) => Promise<Record<string, unknown>[]>;
783
- unique: () => Promise<Record<string, unknown> | null>;
1182
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
784
1183
  withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
785
1184
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
786
1185
  }
787
1186
  /**
788
- * Structural projection of the runtime ORM writer the same subset
789
- * `../rls/middleware` mirrors, so the wrapper is interchangeable between
790
- * `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
791
- * inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
792
- */
793
- interface MaskDatabase {
1187
+ * Structural projection of the runtime ORM writer. The wrapper relies only
1188
+ * on these fields, so it's interchangeable between `@lunora/do`'s
1189
+ * `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
1190
+ */
1191
+ interface DatabaseWriterLike {
1192
+ /**
1193
+ * Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
1194
+ * `baseWhere` into `options` so the reduction only sees policy-visible rows
1195
+ * (safe: an aggregate scoped to `where` never reveals a hidden row — see
1196
+ * `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
1197
+ * wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
1198
+ */
794
1199
  aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
795
- count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
796
- delete: (id: string, expectedTable?: string) => Promise<void>;
1200
+ count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
1201
+ delete: (id: string, expectedTable?: string, options?: {
1202
+ hard?: boolean;
1203
+ }) => Promise<void>;
1204
+ /** Uncapped, chunked erase of a whole table. The RLS wrapper gates each row like a single delete. */
1205
+ deleteAll?: (tableName: string, options?: {
1206
+ chunkSize?: number;
1207
+ hard?: boolean;
1208
+ }) => Promise<{
1209
+ deleted: number;
1210
+ }>;
797
1211
  deleteMany: (ids: ReadonlyArray<string>, options?: {
798
1212
  limit?: number;
1213
+ }, expectedTable?: string) => Promise<{
1214
+ deleted: number;
1215
+ }>;
1216
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
1217
+ limit?: number;
799
1218
  }) => Promise<{
800
1219
  deleted: number;
801
1220
  }>;
@@ -803,6 +1222,11 @@ interface MaskDatabase {
803
1222
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
804
1223
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
805
1224
  get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
1225
+ /**
1226
+ * Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
1227
+ * reduction is scoped to policy-visible rows, so a group count tallies only
1228
+ * rows the caller may read. Required for the same reason as `aggregate`.
1229
+ */
806
1230
  groupBy: (tableName: string, options: GroupByArgs$1) => Promise<ReadonlyArray<{
807
1231
  key: Record<string, unknown>;
808
1232
  value: null | number;
@@ -810,7 +1234,20 @@ interface MaskDatabase {
810
1234
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
811
1235
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
812
1236
  limit?: number;
1237
+ skipDuplicates?: boolean;
1238
+ }) => Promise<(string | null)[]>;
1239
+ insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1240
+ allowExplicitId?: boolean;
1241
+ limit?: number;
813
1242
  }) => Promise<string[]>;
1243
+ /**
1244
+ * Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
1245
+ * already knows the owning table of an id internally, so it can return
1246
+ * `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
1247
+ * uses it to collapse the per-call membership-probe fan-out (1 `get` + N
1248
+ * `findFirst` across every policy table) down to one lookup. Writers that
1249
+ * don't implement it fall back to the probe path.
1250
+ */
814
1251
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
815
1252
  row: Record<string, unknown>;
816
1253
  tableName: string;
@@ -821,207 +1258,162 @@ interface MaskDatabase {
821
1258
  patch: Record<string, unknown>;
822
1259
  }>, options?: {
823
1260
  limit?: number;
824
- }) => Promise<void>;
1261
+ }, expectedTable?: string) => Promise<{
1262
+ patched: number;
1263
+ }>;
1264
+ patchWhere?: (tableName: string, args: {
1265
+ patch: Record<string, unknown>;
1266
+ where: WhereInput;
1267
+ }, options?: {
1268
+ limit?: number;
1269
+ }) => Promise<{
1270
+ patched: number;
1271
+ }>;
825
1272
  query: (tableName: string) => TableReaderLike$1;
826
- rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
1273
+ /**
1274
+ * Rank a row within its partition. A position is a count-of-rows-before, so
1275
+ * — exactly like `count()` — it can't be trusted in an RLS-restricted
1276
+ * reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
1277
+ * for the same reason as `aggregate`.
1278
+ */
1279
+ rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
827
1280
  position: number;
828
1281
  total: number;
829
1282
  }>;
830
- rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
1283
+ /** Cross-shard rank primitive same count-of-before RLS hazard as `rank`; failed closed under a read policy. */
1284
+ rankBefore?: (tableName: string, indexName: string, options: RankBeforeArgs) => Promise<{
831
1285
  before: number;
832
1286
  total: number;
833
1287
  }>;
834
- rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage$1>;
1288
+ /**
1289
+ * Sorted pagination over a rank companion. The companion stores only the
1290
+ * partition + sort keys + id, so an arbitrary read `baseWhere` can't be
1291
+ * enforced against it (and re-filtering the fetched rows would break page
1292
+ * sizing). RLS therefore fails it closed rather than leak hidden rows.
1293
+ * Required for the same reason as `aggregate`.
1294
+ */
1295
+ rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage$1>;
1296
+ /**
1297
+ * Cross-shard companion to `rankPage`: same ranked slice, but each row
1298
+ * keeps its rank-key tuple for the query coordinator's k-way merge. Same
1299
+ * count-of-partition RLS hazard as `rankPage` — failed closed under a read
1300
+ * policy for the identical reason (see `rankPage` above).
1301
+ */
1302
+ rankPageRows?: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<ShardRankPageResultLike>;
835
1303
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1304
+ restore?: (id: string, expectedTable?: string) => Promise<void>;
1305
+ /**
1306
+ * Whole-shard erase. The RLS wrapper deliberately **fails this closed** rather
1307
+ * than wrapping it — see the wrapper's `wipeShard`.
1308
+ */
1309
+ wipeShard?: (options?: {
1310
+ chunkSize?: number;
1311
+ exclude?: ReadonlyArray<string>;
1312
+ tables?: ReadonlyArray<string>;
1313
+ }) => Promise<{
1314
+ deleted: number;
1315
+ tables: Record<string, number>;
1316
+ }>;
836
1317
  }
837
- /** Roles list source on the context. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
838
- type AuthLike$1 = {
1318
+ /**
1319
+ * What a procedure's `ctx.db` must structurally satisfy for the middleware
1320
+ * to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
1321
+ * rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
1322
+ * the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
1323
+ * and structural matching keeps this module free of an `@lunora/do`-typed
1324
+ * `ctx`.
1325
+ */
1326
+ type RlsDatabase = DatabaseWriterLike;
1327
+ /** Roles list source on the context. Tolerant of older auth states. */
1328
+ type AuthLike = {
839
1329
  getIdentity?: () => Promise<Record<string, unknown> | null>;
840
1330
  roles?: ReadonlyArray<string>;
841
1331
  userId?: null | string;
842
1332
  };
843
- interface MaskContextIn {
844
- auth?: AuthLike$1;
845
- db: MaskDatabase;
846
- }
847
- /**
848
- * Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
849
- * Closes over the policy map at builder-construction time; resolves identity +
850
- * the `bypass` decision per call against the live ctx.
851
- *
852
- * IMPORTANT: a mask is in scope only for procedures whose builder chain
853
- * includes this middleware — opt-in, never global (the same invariant as RLS).
854
- */
855
- declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
856
- /**
857
- * Online data-migration authoring API.
858
- *
859
- * `defineMigration` declares a per-document backfill over one table: `up`
860
- * transforms every existing row, `down` (optional) reverses it. Unlike the D1
861
- * SQL schema migrations in `@lunora/d1`, these run *inside each shard's*
862
- * Durable Object against live documents, in keyset batches, and are resumable —
863
- * the per-shard runner in `@lunora/do` tracks progress in a reserved
864
- * `__lunora_migrations` table so an interrupted run picks up where it stopped.
865
- *
866
- * The returned object carries a `__lunoraMigration` brand so codegen can
867
- * discover declarations through the type checker (mirroring the procedure
868
- * builder's `__lunoraProcedure` brand) and emit them into a `LUNORA_MIGRATIONS`
869
- * registry the DO and CLI look migrations up by id.
870
- */
871
- /** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
872
- type MigrationDocument = Record<string, unknown>;
873
- /**
874
- * Transform applied to one document. Return a new document to rewrite the row,
875
- * or `undefined` to leave it untouched (skipped, not counted as changed). The
876
- * runner always preserves the original `_id` and `_creationTime`, so the
877
- * returned document neither needs to nor should change row identity.
878
- */
879
- type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
880
- interface MigrationDefinition {
881
- /** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
882
- readonly batchSize?: number;
883
- /** Optional reverse transform, applied by `migrate down`. */
884
- readonly down?: MigrationTransform;
885
- /** Stable, unique identifier — the key per-shard run-state is tracked under. */
886
- readonly id: string;
887
- /** Table whose documents this migration iterates. */
888
- readonly table: string;
889
- /** Forward transform, applied to every row by `migrate up`. */
890
- readonly up: MigrationTransform;
891
- }
892
- /** A {@link MigrationDefinition} plus the codegen discovery marker. */
893
- interface RegisteredMigration extends MigrationDefinition {
894
- readonly __lunoraMigration: true;
895
- }
896
- /** Declare an online data migration. See the module docs for runtime semantics. */
897
- declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
898
- /**
899
- * A mutator declaration. `server` is authoritative; `client` is the optimistic
900
- * twin (optional — omit it to let the optimistic write fall through to the
901
- * server round-trip with no local preview). Both receive the same validated
902
- * `args`.
903
- */
904
- interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
905
- /**
906
- * Validator for the mutator's arguments. Validated on the DO before `server`
907
- * runs and (when present) on the client before `client` runs, so both impls
908
- * see the same parsed shape. Omit for a parameterless mutator.
909
- */
910
- readonly args?: Args;
911
- /**
912
- * Optimistic client implementation. Runs in a TanStack DB transaction
913
- * against the local collections; its writes are applied immediately and
914
- * automatically rolled back / rebased as the authoritative result syncs
915
- * back. Pure and side-effect-free beyond the local store. Omit to skip the
916
- * local preview.
917
- */
918
- readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
919
- /**
920
- * Authoritative server implementation. Runs inside the shard DO with a full
921
- * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
922
- * and poke back to subscribers. This is the source of truth — the client
923
- * impl is only a prediction of it.
924
- */
925
- readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
926
- }
927
- /**
928
- * A {@link MutatorDefinition} plus the codegen discovery marker and a
929
- * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
930
- * invokes a mutator exactly like a registered procedure.
931
- */
932
- interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
933
- readonly __lunoraMutator: true;
934
- /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
935
- readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
936
- /**
937
- * Marks the dispatch kind so codegen can register the mutator in the same
938
- * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
939
- * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
940
- * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
941
- * ordinary `mutation`.
942
- */
943
- readonly kind: "mutation";
1333
+ /** Minimal shape the middleware needs on the incoming ctx. */
1334
+ interface RlsContextIn {
1335
+ auth?: AuthLike;
1336
+ db: RlsDatabase;
944
1337
  }
945
- /** Declare a custom mutator. See the module docs for runtime semantics. */
946
- declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
1338
+ declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
947
1339
  /**
948
- * The prefixed tables a single plugin `P` contributes, or an empty map when it
949
- * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
950
- * so {@link InstalledTables} can fold a tuple of plugins.
951
- */
1340
+ * The prefixed tables a single plugin `P` contributes, or an empty map when it
1341
+ * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
1342
+ * so {@link InstalledTables} can fold a tuple of plugins.
1343
+ */
952
1344
  type ExtensionTablesOf<P> = P extends {
953
1345
  readonly extension: SchemaExtension<infer X> & {
954
1346
  readonly key: infer K;
955
1347
  };
956
1348
  } ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
957
1349
  /**
958
- * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
959
- * auto-prefixed extension tables left-to-right — the type-level mirror of
960
- * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
961
- */
1350
+ * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
1351
+ * auto-prefixed extension tables left-to-right — the type-level mirror of
1352
+ * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
1353
+ */
962
1354
  type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
963
1355
  /**
964
- * Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
965
- * `ctx.api.&lt;key>` additions {@link composePluginMiddleware} accumulates as each
966
- * plugin middleware runs. Independent of the incoming context, which the builder
967
- * infers at the `.use(...)` site.
968
- */
1356
+ * Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
1357
+ * `ctx.api.<key>` additions {@link composePluginMiddleware} accumulates as each
1358
+ * plugin middleware runs. Independent of the incoming context, which the builder
1359
+ * infers at the `.use(...)` site.
1360
+ */
969
1361
  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;
970
1362
  /**
971
- * Schema fragment a plugin contributes. Same shape as the `tables` map
972
- * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
973
- * `defineSchema` argument so a plugin can ship vector decls alongside its
974
- * tables.
975
- */
1363
+ * Schema fragment a plugin contributes. Same shape as the `tables` map
1364
+ * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
1365
+ * `defineSchema` argument so a plugin can ship vector decls alongside its
1366
+ * tables.
1367
+ */
976
1368
  interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
977
1369
  /** Stable key identifying the plugin that owns this extension. */
978
1370
  readonly key: string;
979
1371
  /**
980
- * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
981
- * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
982
- * collide with an app table; do **not** namespace manually.
983
- */
1372
+ * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
1373
+ * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
1374
+ * collide with an app table; do **not** namespace manually.
1375
+ */
984
1376
  readonly tables: T;
985
1377
  /**
986
- * Optional standalone vector indexes the plugin ships, keyed by index
987
- * name. Merged into the host schema's `vectorIndexes`; a key collision
988
- * with the base schema is a hard error (same policy as tables).
989
- */
1378
+ * Optional standalone vector indexes the plugin ships, keyed by index
1379
+ * name. Merged into the host schema's `vectorIndexes`; a key collision
1380
+ * with the base schema is a hard error (same policy as tables).
1381
+ */
990
1382
  readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
991
1383
  }
992
1384
  /**
993
- * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
994
- * error messages on collision) and a type-level brand.
995
- */
1385
+ * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
1386
+ * error messages on collision) and a type-level brand.
1387
+ */
996
1388
  declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
997
1389
  tables: T;
998
1390
  vectorIndexes?: Record<string, VectorIndexDefinition>;
999
1391
  }) => SchemaExtension<T>;
1000
1392
  /**
1001
- * A plugin packages an optional schema extension and optional middleware.
1002
- * Both are independently usable: an app can install only the schema (e.g.
1003
- * for plugins that ship background workers but no per-request behavior)
1004
- * or only the middleware (plugins that augment ctx without persistent
1005
- * state).
1006
- */
1393
+ * A plugin packages an optional schema extension and optional middleware.
1394
+ * Both are independently usable: an app can install only the schema (e.g.
1395
+ * for plugins that ship background workers but no per-request behavior)
1396
+ * or only the middleware (plugins that augment ctx without persistent
1397
+ * state).
1398
+ */
1007
1399
  interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
1008
1400
  /**
1009
- * Optional schema extension. Apps install via
1010
- * `defineSchema(...).extend(plugin.extension)`.
1011
- */
1401
+ * Optional schema extension. Apps install via
1402
+ * `defineSchema(...).extend(plugin.extension)`.
1403
+ */
1012
1404
  readonly extension?: SchemaExtension<TExtension>;
1013
1405
  /** Stable key identifying the plugin. Matches `extension.key` when set. */
1014
1406
  readonly key: string;
1015
1407
  /**
1016
- * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
1017
- * The middleware can extend `ctx`; convention is to attach helpers under
1018
- * `ctx.api.&lt;key>`, e.g.
1019
- *
1020
- * ```ts
1021
- * middleware: ({ ctx, next }) =>
1022
- * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
1023
- * ```
1024
- */
1408
+ * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
1409
+ * The middleware can extend `ctx`; convention is to attach helpers under
1410
+ * `ctx.api.<key>`, e.g.
1411
+ *
1412
+ * ```ts
1413
+ * middleware: ({ ctx, next }) =>
1414
+ * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
1415
+ * ```
1416
+ */
1025
1417
  readonly middleware?: Middleware<TContextIn, TContextOut>;
1026
1418
  }
1027
1419
  /** Options to {@link definePlugin}. */
@@ -1030,35 +1422,51 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
1030
1422
  middleware?: Middleware<TContextIn, TContextOut>;
1031
1423
  }
1032
1424
  /**
1033
- * Package a schema extension + middleware as a reusable plugin. Either
1034
- * field is optional `definePlugin("foo", {})` is valid but degenerate.
1035
- */
1036
- declare const definePlugin: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>) => Plugin<TExtension, TContextIn, TContextOut>;
1037
- /**
1038
- * Bundle of registered functions a {@link Component} ships. Keys are the
1039
- * function's local name (e.g. `check`, `reset`); the registered function
1040
- * value carries its own kind / args / handler.
1041
- *
1042
- * Users re-export from their own lunora module so codegen picks them up:
1043
- *
1044
- * ```ts
1045
- * // lunora/ratelimit.ts
1046
- * import { ratelimit } from "@vendor/ratelimit-component";
1047
- * export const { check, reset } = ratelimit.functions;
1048
- * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1049
- * ```
1050
- *
1051
- * Codegen follows the re-export back to the bundled `query/mutation/action`
1052
- * call (property access or destructuring both work), so the functions land in
1053
- * the generated `api` under the re-exporting file's namespace.
1054
- */
1425
+ * Call signatures for {@link definePlugin}. When `extension` is supplied the
1426
+ * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1427
+ * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1428
+ * typechecks without a non-null assertion the shape every scaffold template
1429
+ * ships. The bare-options signature keeps `extension` optional for plugins
1430
+ * that carry only middleware.
1431
+ */
1432
+ interface DefinePluginFunction {
1433
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1434
+ extension: SchemaExtension<TExtension>;
1435
+ }): Plugin<TExtension, TContextIn, TContextOut> & {
1436
+ readonly extension: SchemaExtension<TExtension>;
1437
+ };
1438
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1439
+ }
1440
+ /**
1441
+ * Package a schema extension + middleware as a reusable plugin. Either
1442
+ * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1443
+ */
1444
+ declare const definePlugin: DefinePluginFunction;
1445
+ /**
1446
+ * Bundle of registered functions a {@link Component} ships. Keys are the
1447
+ * function's local name (e.g. `check`, `reset`); the registered function
1448
+ * value carries its own kind / args / handler.
1449
+ *
1450
+ * Users re-export from their own lunora module so codegen picks them up:
1451
+ *
1452
+ * ```ts
1453
+ * // lunora/ratelimit.ts
1454
+ * import { ratelimit } from "@vendor/ratelimit-component";
1455
+ * export const { check, reset } = ratelimit.functions;
1456
+ * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1457
+ * ```
1458
+ *
1459
+ * Codegen follows the re-export back to the bundled `query/mutation/action`
1460
+ * call (property access or destructuring both work), so the functions land in
1461
+ * the generated `api` under the re-exporting file's namespace.
1462
+ */
1055
1463
  type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
1056
1464
  /**
1057
- * Component = {@link Plugin} with a bundle of registered functions. The
1058
- * extension + middleware + functions are independent: a component can ship
1059
- * functions without a schema (e.g. a stateless utility), or a schema
1060
- * without functions (e.g. shared table definitions), and any combination.
1061
- */
1465
+ * Component = {@link Plugin} with a bundle of registered functions. The
1466
+ * extension + middleware + functions are independent: a component can ship
1467
+ * functions without a schema (e.g. a stateless utility), or a schema
1468
+ * without functions (e.g. shared table definitions), and any combination.
1469
+ */
1062
1470
  interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
1063
1471
  readonly functions: F;
1064
1472
  }
@@ -1067,81 +1475,91 @@ interface DefineComponentOptions<TExtension extends Record<string, TableDefiniti
1067
1475
  functions?: F;
1068
1476
  }
1069
1477
  /**
1070
- * Convenience wrapper around {@link definePlugin} that also bundles a set
1071
- * of registered functions. The resulting `component.functions` object is a
1072
- * record of `name → registered query/mutation/action`; consumers
1073
- * re-export entries so codegen discovers them as user functions:
1074
- *
1075
- * ```ts
1076
- * export const ratelimit = defineComponent("ratelimit", {
1077
- * // Bare `buckets` merges in as `ratelimit_buckets`.
1078
- * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1079
- * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1080
- * functions: {
1081
- * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1082
- * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1083
- * },
1084
- * });
1085
- * ```
1086
- *
1087
- * Re-exporting an entry (by property access or destructuring) is enough for
1088
- * codegen to discover it in the host app's namespace — the discovery resolver
1089
- * chases the re-export back to the bundled registration call.
1090
- */
1478
+ * Convenience wrapper around {@link definePlugin} that also bundles a set
1479
+ * of registered functions. The resulting `component.functions` object is a
1480
+ * record of `name → registered query/mutation/action`; consumers
1481
+ * re-export entries so codegen discovers them as user functions:
1482
+ *
1483
+ * ```ts
1484
+ * export const ratelimit = defineComponent("ratelimit", {
1485
+ * // Bare `buckets` merges in as `ratelimit_buckets`.
1486
+ * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1487
+ * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1488
+ * functions: {
1489
+ * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1490
+ * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1491
+ * },
1492
+ * });
1493
+ * ```
1494
+ *
1495
+ * Re-exporting an entry (by property access or destructuring) is enough for
1496
+ * codegen to discover it in the host app's namespace — the discovery resolver
1497
+ * chases the re-export back to the bundled registration call.
1498
+ */
1091
1499
  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>;
1092
1500
  /**
1093
- * Map every key `K` of an extension's table map `X` to its auto-prefixed name
1094
- * `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
1095
- * so the typed `.extend(...)` chain reflects the real merged table names.
1096
- */
1097
- type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K] };
1098
- /**
1099
- * Merge a {@link SchemaExtension} into an existing schema. Returns a new
1100
- * schema object — never mutates the input.
1101
- *
1102
- * Extension tables are auto-namespaced: each bare table name is prefixed with
1103
- * the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
1104
- * style, and every intra-extension reference (relation targets, aggregate /
1105
- * rank index `on`, standalone vector index `table`) is rewritten to match.
1106
- * References to base/app tables are left untouched.
1107
- *
1108
- * Because each extension lives in its own `key` namespace, app↔component
1109
- * collisions are impossible. The only remaining hard error is two extensions
1110
- * sharing the same `key` and producing the same prefixed table (or vector
1111
- * index) name — silent shadow would let one plugin hijack another's data.
1112
- */
1501
+ * Map every key `K` of an extension's table map `X` to its auto-prefixed name
1502
+ * `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
1503
+ * so the typed `.extend(...)` chain reflects the real merged table names.
1504
+ */
1505
+ type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K]; };
1506
+ /**
1507
+ * Merge a {@link SchemaExtension} into an existing schema. Returns a new
1508
+ * schema object — never mutates the input.
1509
+ *
1510
+ * Extension tables are auto-namespaced: each bare table name is prefixed with
1511
+ * the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
1512
+ * style, and every intra-extension reference (relation targets, aggregate /
1513
+ * rank index `on`, standalone vector index `table`) is rewritten to match.
1514
+ * References to base/app tables are left untouched.
1515
+ *
1516
+ * Because each extension lives in its own `key` namespace, app↔component
1517
+ * collisions are impossible. The only remaining hard error is two extensions
1518
+ * sharing the same `key` and producing the same prefixed table (or vector
1519
+ * index) name — silent shadow would let one plugin hijack another's data.
1520
+ *
1521
+ * Re-runs {@link validateIndexFields} against the merged table set before
1522
+ * returning: `defineSchema` only validates the tables it was called with, so
1523
+ * without this an extension-contributed index with a typo'd/out-of-shape
1524
+ * field (or a duplicate name within one kind) would never be checked at all.
1525
+ * Re-validating the whole merged set (base + prefixed extension tables) is
1526
+ * cheap and idempotent for the base tables, which already passed this same
1527
+ * check when the base schema was built. Both callers of this function —
1528
+ * `withExtend.extend()` (`./schema`) and `installPlugins` (below) — get the
1529
+ * re-validation for free from this single call site (plan 258 §4/§9 Q3).
1530
+ */
1113
1531
  declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
1114
1532
  readonly key: Key;
1115
1533
  }) => Schema<PrefixedTables<X, Key> & T>;
1116
1534
  /**
1117
- * Install several plugins' schema extensions in one call — the one-shot
1118
- * counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
1119
- * without an `extension` (middleware-only) are skipped; tables from those that
1120
- * do are auto-prefixed and reference-rewritten exactly as
1121
- * {@link mergeSchemaExtension} does for a single `.extend(...)`.
1122
- *
1123
- * ```ts
1124
- * const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
1125
- * // → todos + ratelimit_* + audit_*
1126
- * ```
1127
- *
1128
- * Pair it with {@link composePluginMiddleware} to attach every plugin's
1129
- * middleware in a single `.use(...)`, so installing N plugins is two calls
1130
- * rather than N `.extend(...)` + N `.use(...)`.
1131
- */
1535
+ * Install several plugins' schema extensions in one call — the one-shot
1536
+ * counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
1537
+ * without an `extension` (middleware-only) are skipped; tables from those that
1538
+ * do are auto-prefixed and reference-rewritten exactly as
1539
+ * {@link mergeSchemaExtension} does for a single `.extend(...)`.
1540
+ *
1541
+ * ```ts
1542
+ * const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
1543
+ * // → todos + ratelimit_* + audit_*
1544
+ * ```
1545
+ *
1546
+ * Pair it with {@link composePluginMiddleware} to attach every plugin's
1547
+ * middleware in a single `.use(...)`, so installing N plugins is two calls
1548
+ * rather than N `.extend(...)` + N `.use(...)`.
1549
+ */
1132
1550
  declare const installPlugins: <T extends Record<string, TableDefinition>, const Plugins extends ReadonlyArray<Plugin<any, any, any>>>(base: Schema<T>, plugins: Plugins) => Schema<InstalledTables<T, Plugins>>;
1133
1551
  /**
1134
- * Compose every plugin's middleware into a single middleware you attach with one
1135
- * `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
1136
- * in array order, each seeing the context the previous one widened, so the final
1137
- * `next({ ctx })` the builder receives carries every plugin's `ctx.api.&lt;key>`
1138
- * additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
1139
- * value, the middleware sibling of {@link installPlugins}.
1140
- *
1141
- * `ContextIn` is left free so the builder infers it from the context at the
1142
- * `.use(...)` site; the result type widens it by the union of the plugins'
1143
- * outputs.
1144
- */
1552
+ * Compose every plugin's middleware into a single middleware you attach with one
1553
+ * `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
1554
+ * in array order, each seeing the context the previous one widened, so the final
1555
+ * `next({ ctx })` the builder receives carries every plugin's `ctx.api.<key>`
1556
+ * additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
1557
+ * value, the middleware sibling of {@link installPlugins}.
1558
+ *
1559
+ * `ContextIn` is left free so the builder infers it from the context at the
1560
+ * `.use(...)` site; the result type widens it by the union of the plugins'
1561
+ * outputs.
1562
+ */
1145
1563
  declare const composePluginMiddleware: <ContextIn = unknown, const Plugins extends ReadonlyArray<Plugin<any, any, any>> = ReadonlyArray<Plugin<any, any, any>>>(plugins: Plugins) => Middleware<ContextIn, ComposedOut<Plugins> & ContextIn>;
1146
1564
  /** Options for `.vectorize(field, opts)` (DSL Shape A). */
1147
1565
  interface VectorizeOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
@@ -1178,10 +1596,10 @@ interface RelationBuilder {
1178
1596
  }) => OneRelation<Target>;
1179
1597
  }
1180
1598
  /**
1181
- * Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
1182
- * `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
1183
- * `COUNT(*) GROUP BY userId` accelerator.
1184
- */
1599
+ * Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
1600
+ * `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
1601
+ * `COUNT(*) GROUP BY userId` accelerator.
1602
+ */
1185
1603
  interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
1186
1604
  /** Group keys; counter rows are one per distinct tuple. Omitted = single-row aggregate over the whole table. */
1187
1605
  by?: ReadonlyArray<keyof Shape & string>;
@@ -1193,11 +1611,11 @@ interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> =
1193
1611
  where?: Record<string, unknown>;
1194
1612
  }
1195
1613
  /**
1196
- * Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
1197
- * accepts either an array of `{ field, direction }` keys, or the shorthand
1198
- * `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
1199
- * rank — omitted ⇒ one global rank over the whole table.
1200
- */
1614
+ * Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
1615
+ * accepts either an array of `{ field, direction }` keys, or the shorthand
1616
+ * `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
1617
+ * rank — omitted ⇒ one global rank over the whole table.
1618
+ */
1201
1619
  interface InlineRankIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
1202
1620
  /** Columns that scope each ranking; omitted ⇒ one global rank. */
1203
1621
  partitionBy?: ReadonlyArray<keyof Shape & string>;
@@ -1213,79 +1631,187 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1213
1631
  /** Declare an aggregate (counter/sum/…) maintained by triggers for O(1) reads. */
1214
1632
  aggregateIndex: (name: string, options?: InlineAggregateIndexOptions<Shape>) => TableBuilder<Shape>;
1215
1633
  /**
1216
- * Mark this table as written outside Lunora's discoverable insert path
1217
- * by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
1218
- * better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
1219
- * (`table_without_insert`) then skip it instead of flagging the absent
1220
- * `ctx.db.insert(...)`.
1221
- */
1634
+ * Stamp every row with `_commitSeq` a per-shard integer, allocated once
1635
+ * per mutation and strictly increasing in **commit order**, refreshed on
1636
+ * every write to the row (insert, patch, replace, and the marker flip a
1637
+ * `.softDelete()` performs).
1638
+ *
1639
+ * `_creationTime` is wall-clock and therefore cannot order commits: the
1640
+ * clock is read when the handler runs, the write lands when the transaction
1641
+ * commits, and nothing ties those instants together. A changefeed paging on
1642
+ * `_creationTime` can skip a row permanently. Paging on `_commitSeq`
1643
+ * (`where: { _commitSeq: { gt: cursor } }, orderBy: ["_commitSeq"]`) cannot.
1644
+ *
1645
+ * It orders COMMITS, not rows: one mutation's rows share a value. A bounded
1646
+ * page can therefore end mid-group, so a consumer must checkpoint at a
1647
+ * sequence it has seen the whole of, never at the last row of a full page.
1648
+ * An action's writes are the exception to the grouping — they commit
1649
+ * independently, so each gets its own sequence.
1650
+ *
1651
+ * Ordered, not contiguous — read a gap as "nothing to see", never as loss.
1652
+ * Per-shard, not global: two shards allocate independently, so a cursor is
1653
+ * only meaningful against the shard it came from. Rejected on `.global()`
1654
+ * tables, which have no shard-local transaction to allocate inside.
1655
+ *
1656
+ * **A hard delete is invisible to the feed.** The sequence lives on the row,
1657
+ * so a physically removed row takes it along: the row stops appearing, but
1658
+ * no event says it went away. Pair `.commitOrdered()` with `.softDelete()`
1659
+ * when the feed must observe deletes — the tombstone flip is an UPDATE, so
1660
+ * it advances the sequence and pages through like any other change.
1661
+ */
1662
+ commitOrdered: () => TableBuilder<Shape>;
1663
+ /**
1664
+ * Mark this table as written outside Lunora's discoverable insert path —
1665
+ * by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
1666
+ * better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
1667
+ * (`table_without_insert`) then skip it instead of flagging the absent
1668
+ * `ctx.db.insert(...)`.
1669
+ */
1222
1670
  externallyManaged: () => TableBuilder<Shape>;
1223
1671
  /**
1224
- * Mark this table as global (cross-shard). Backed by **D1** by default;
1225
- * pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
1226
- * via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
1227
- * table stays reactive live queries re-run on write.
1228
- */
1672
+ * Declare a geospatial index over a `v.geoPoint()` column. The runtime keeps
1673
+ * a geohash companion so `withGeoIndex(name, q => q.near(point, radius))` and
1674
+ * `.within(bbox)` resolve as a geohash-prefix range scan + Haversine
1675
+ * refine/sort. `options.precision` tunes the geohash length (default 9).
1676
+ */
1677
+ geoIndex: (name: string, options: {
1678
+ field: keyof Shape & string;
1679
+ precision?: number;
1680
+ }) => TableBuilder<Shape>;
1681
+ /**
1682
+ * Mark this table as global (cross-shard). Backed by **D1** by default;
1683
+ * pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
1684
+ * via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
1685
+ * table stays reactive — live queries re-run on write.
1686
+ */
1229
1687
  global: (options?: {
1230
1688
  backend?: GlobalBackend;
1231
1689
  }) => TableBuilder<Shape>;
1232
1690
  /** Add a secondary index. */
1233
- index: (name: string, fields: ReadonlyArray<string>, options?: {
1691
+ index: (name: string, fields: ReadonlyArray<(keyof Shape & string) | (typeof SYSTEM_INDEX_FIELDS)[number]>, options?: {
1234
1692
  unique?: boolean;
1235
1693
  }) => TableBuilder<Shape>;
1236
1694
  /**
1237
- * Opt this table OUT of secure-by-default RLS. Under a schema marked
1238
- * `.rls("required")`, every table is protected (the write path denies raw,
1239
- * non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
1240
- * plain `query`/`mutation` may read/write it without an RLS policy. No effect
1241
- * when the schema does not require RLS.
1242
- */
1695
+ * Declare this table EPHEMERAL state the shard rebuilds rather than
1696
+ * remembers.
1697
+ *
1698
+ * A memory table is a full `ctx.db` table: indexes, `where`, `orderBy`,
1699
+ * pagination, relations, live queries. What it is not is durable. Its rows
1700
+ * are wiped the moment the Durable Object is reconstructed — which happens
1701
+ * on every eviction, and a WebSocket-hibernating shard is evicted often — so
1702
+ * a memory table holds only what can be derived again: presence and cursors,
1703
+ * a live participant list, a rate-limit window, an actor's scratch state.
1704
+ *
1705
+ * Pair it with `onShardInit` to rebuild whatever the app needs present.
1706
+ * The framework guarantees the ordering: every memory table is cleared, and
1707
+ * every init hook has run, before any handler can read one. Without a hook a
1708
+ * memory table simply comes back empty, which is a correct state for
1709
+ * presence and a wrong one for a cache someone is treating as authoritative.
1710
+ *
1711
+ * **On Cloudflare the rows still transit the DO's SQLite.** workerd exposes
1712
+ * exactly one SQL handle and no memory-backed database, so `.memory()` buys
1713
+ * the LIFETIME (and skips the CDC changelog, so an append-heavy presence
1714
+ * table does not grow the op-log), not the write. Treat it as "state I am
1715
+ * happy to lose", not as "state that is free to write" — see
1716
+ * `PlatformCapabilities.memoryTables`, rated `emulated` for exactly this
1717
+ * reason.
1718
+ *
1719
+ * Rejected alongside `.global()` (a D1 table is not this shard's to clear),
1720
+ * `.commitOrdered()` (a sequence that resets is not a sequence), and
1721
+ * `.source()` (an externally-materialized table is not ours to wipe).
1722
+ */
1723
+ memory: () => TableBuilder<Shape>;
1724
+ /**
1725
+ * Name the column holding the owning user's id, so "only the owner sees these
1726
+ * rows" is declared once here rather than restated in every shape.
1727
+ *
1728
+ * A `defineShape({ table, owner: true })` over this table derives its predicate
1729
+ * from the field: the subscriber's verified `ctx.auth.userId` must match, and an
1730
+ * anonymous subscriber is denied. Pairs naturally with `.shardBy(field)` on the
1731
+ * same column — the shard key routes the storage, `ownedBy` states who the rows
1732
+ * belong to — but the two are independent and either can be used alone.
1733
+ *
1734
+ * This is a *shape* declaration, not an RLS policy: it narrows what a shape
1735
+ * replicates. Guarding procedure reads/writes is still `rls(...)`'s job.
1736
+ */
1737
+ ownedBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1738
+ /**
1739
+ * Opt this table OUT of secure-by-default RLS. Under a schema marked
1740
+ * `.rls("required")`, every table is protected (the write path denies raw,
1741
+ * non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
1742
+ * plain `query`/`mutation` may read/write it without an RLS policy. No effect
1743
+ * when the schema does not require RLS.
1744
+ */
1243
1745
  public: () => TableBuilder<Shape>;
1244
1746
  /**
1245
- * Declare a rank index (sorted companion table, btree-backed) for
1246
- * `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
1247
- */
1747
+ * Declare a rank index (sorted companion table, btree-backed) for
1748
+ * `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
1749
+ */
1248
1750
  rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
1249
1751
  /** Declare relations to other tables, loaded via `findMany({ with })`. */
1250
1752
  relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
1251
- /** Add a search index over a field with optional filter fields. */
1753
+ /**
1754
+ * Add a full-text search index over `field`, queried with
1755
+ * `.withSearchIndex(name, q => q.search(field, term))`. `field` may be a
1756
+ * dot-separated path into a nested object (`"properties.name"`).
1757
+ * `filterFields` (at most 16) lists the columns `.eq()` may narrow by inside
1758
+ * the search. `language` selects the text analysis (accent folding always,
1759
+ * plus that language's stopwords). `staged: true` skips the migration-time
1760
+ * backfill on a large existing table. `strategy: "native"` uses the engine's
1761
+ * own full-text index where it has one (Postgres) — faster on large corpora,
1762
+ * at the cost of the engine ranking rather than the shared scorer.
1763
+ */
1252
1764
  searchIndex: (name: string, options: {
1253
1765
  field: string;
1254
1766
  filterFields?: ReadonlyArray<string>;
1767
+ language?: SearchLanguage;
1768
+ staged?: boolean;
1769
+ strategy?: SearchStrategy;
1255
1770
  }) => TableBuilder<Shape>;
1256
1771
  /** Route storage by the named field — one DO per distinct value. */
1257
1772
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1258
1773
  /**
1259
- * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1260
- * default `deletedAt`) and changes `ctx.db.&lt;table>.delete()` to **set** it
1261
- * instead of removing the row; `onDelete: "cascade"` children are recursively
1262
- * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1263
- * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1264
- * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1265
- * `restore()` still address the row directly. `hardDelete()` physically
1266
- * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1267
- * scope, not access control — anyone who can run the read can set it; a unique
1268
- * index still rejects a new row that collides with a soft-deleted one (the row
1269
- * physically persists).
1270
- */
1774
+ * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1775
+ * default `deletedAt`) and changes `ctx.db.<table>.delete()` to **set** it
1776
+ * instead of removing the row; `onDelete: "cascade"` children are recursively
1777
+ * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1778
+ * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1779
+ * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1780
+ * `restore()` still address the row directly. `hardDelete()` physically
1781
+ * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1782
+ * scope, not access control — anyone who can run the read can set it; a unique
1783
+ * index still rejects a new row that collides with a soft-deleted one (the row
1784
+ * physically persists).
1785
+ */
1271
1786
  softDelete: (options?: {
1272
1787
  field?: string;
1273
1788
  }) => TableBuilder<Shape>;
1274
1789
  /**
1275
- * Materialize this table from an external Postgres/MySQL behind Cloudflare
1276
- * Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
1277
- * (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
1278
- * after which `defineShape` carries it to clients unchanged. Implies
1279
- * `.externallyManaged()` (rows come from the ingest loop, not user mutations).
1280
- *
1281
- * Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
1282
- * `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
1283
- * `external_source_unscoped` advisor lint fails the build when it is absent, and
1284
- * `external_source_on_global` rejects combining `.source()` with `.global()`.
1285
- */
1790
+ * Materialize this table from an external Postgres/MySQL behind Cloudflare
1791
+ * Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
1792
+ * (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
1793
+ * after which `defineShape` carries it to clients unchanged. Implies
1794
+ * `.externallyManaged()` (rows come from the ingest loop, not user mutations).
1795
+ *
1796
+ * Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
1797
+ * `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
1798
+ * `external_source_unscoped` advisor lint fails the build when it is absent, and
1799
+ * `external_source_on_global` rejects combining `.source()` with `.global()`.
1800
+ */
1286
1801
  source: (definition: ExternalSourceDefinition) => TableBuilder<Shape>;
1287
1802
  /** Declare named lifecycle triggers fired inline within the write path. */
1288
1803
  triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
1804
+ /**
1805
+ * Declare a table-level TTL: a DO alarm-driven sweep auto-deletes rows whose
1806
+ * expiry has passed (or soft-deletes them when the table also
1807
+ * `.softDelete()`s). `field` is an epoch-millisecond column; without
1808
+ * `options.after` its value is the absolute expiry instant, with `after` the
1809
+ * row expires `after` ms past `field` (`field + after`). Coarse, cheap,
1810
+ * table-level — for per-row schedules use `@lunora/scheduler`.
1811
+ */
1812
+ ttl: (field: keyof Shape & string, options?: {
1813
+ after?: number;
1814
+ }) => TableBuilder<Shape>;
1289
1815
  /** Declare a vector index over a single text field on this table. */
1290
1816
  vectorize: (field: keyof Shape & string, options: VectorizeOptions<Shape>) => TableBuilder<Shape>;
1291
1817
  }
@@ -1303,22 +1829,22 @@ interface VectorIndexOptions {
1303
1829
  };
1304
1830
  }
1305
1831
  /**
1306
- * Build a table definition. Returned object is both the table definition (for
1307
- * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1308
- */
1832
+ * Build a table definition. Returned object is both the table definition (for
1833
+ * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1834
+ */
1309
1835
  declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
1310
1836
  /**
1311
- * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1312
- * the `vectorIndexes` map of {@link defineSchema} when the source is derived
1313
- * from multiple fields or a computation rather than a single column.
1314
- */
1837
+ * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1838
+ * the `vectorIndexes` map of {@link defineSchema} when the source is derived
1839
+ * from multiple fields or a computation rather than a single column.
1840
+ */
1315
1841
  declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
1316
1842
  /**
1317
- * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1318
- * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1319
- * the owning table explicitly via `on` — handy when a single counter wants to
1320
- * live next to the schema map rather than inside a table chain.
1321
- */
1843
+ * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1844
+ * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1845
+ * the owning table explicitly via `on` — handy when a single counter wants to
1846
+ * live next to the schema map rather than inside a table chain.
1847
+ */
1322
1848
  interface AggregateIndexOptions {
1323
1849
  by?: ReadonlyArray<string>;
1324
1850
  field?: string;
@@ -1327,17 +1853,17 @@ interface AggregateIndexOptions {
1327
1853
  where?: Record<string, unknown>;
1328
1854
  }
1329
1855
  /**
1330
- * Declare a standalone aggregate index. Pass the returned value to
1331
- * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
1332
- * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1333
- * (DO + D1) read every index uniformly off the table definition.
1334
- */
1856
+ * Declare a standalone aggregate index. Pass the returned value to
1857
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
1858
+ * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1859
+ * (DO + D1) read every index uniformly off the table definition.
1860
+ */
1335
1861
  declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
1336
1862
  /**
1337
- * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1338
- * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1339
- * `table` so it can sit next to the schema map.
1340
- */
1863
+ * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1864
+ * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1865
+ * `table` so it can sit next to the schema map.
1866
+ */
1341
1867
  interface RankIndexOptions {
1342
1868
  partitionBy?: ReadonlyArray<string>;
1343
1869
  sortBy: ReadonlyArray<{
@@ -1348,375 +1874,276 @@ interface RankIndexOptions {
1348
1874
  where?: Record<string, unknown>;
1349
1875
  }
1350
1876
  /**
1351
- * Declare a standalone rank index. Pass the returned value to
1352
- * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1353
- * by index name — the schema attaches it to `tables[on].rankIndexes`.
1354
- */
1877
+ * Declare a standalone rank index. Pass the returned value to
1878
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1879
+ * by index name — the schema attaches it to `tables[on].rankIndexes`.
1880
+ */
1355
1881
  declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
1356
1882
  /**
1357
- * Build the application schema. The first argument is the table map; the
1358
- * optional second argument registers standalone `defineVectorIndex(...)`
1359
- * declarations (DSL Shape B) keyed by index name. The optional third argument
1360
- * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1361
- * the optional fourth argument registers standalone `defineRankIndex(...)`
1362
- * declarations. Both are folded into the matching `tables[on].*Indexes` array
1363
- * so runtime backends read every index uniformly off the table definition.
1364
- */
1365
- /**
1366
- * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1367
- * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1368
- *
1369
- * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1370
- * the merged tables. Extension tables are auto-namespaced by the extension
1371
- * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1372
- * prefixed names via {@link PrefixedTables}. Chains:
1373
- * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1374
- * `a`'s prefixed tables then `b`'s.
1375
- */
1883
+ * Build the application schema. The first argument is the table map; the
1884
+ * optional second argument registers standalone `defineVectorIndex(...)`
1885
+ * declarations (DSL Shape B) keyed by index name. The optional third argument
1886
+ * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1887
+ * the optional fourth argument registers standalone `defineRankIndex(...)`
1888
+ * declarations. Both are folded into the matching `tables[on].*Indexes` array
1889
+ * so runtime backends read every index uniformly off the table definition.
1890
+ */
1891
+ /**
1892
+ * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1893
+ * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1894
+ *
1895
+ * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1896
+ * the merged tables. Extension tables are auto-namespaced by the extension
1897
+ * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1898
+ * prefixed names via {@link PrefixedTables}. Chains:
1899
+ * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1900
+ * `a`'s prefixed tables then `b`'s.
1901
+ */
1376
1902
  type ExtendableSchema<T extends Record<string, TableDefinition>> = {
1377
1903
  extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
1378
1904
  readonly key: Key;
1379
1905
  }) => ExtendableSchema<PrefixedTables<X, Key> & T>;
1380
1906
  /**
1381
- * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1382
- * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1383
- * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1384
- * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1385
- * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1386
- * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1387
- *
1388
- * ⚠️ **Set this once, before your first deploy — changing or removing it
1389
- * strands data.** A Durable Object name maps to a *different* ID in each
1390
- * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1391
- * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1392
- * old jurisdiction's DOs and is no longer reachable. There is no in-place
1393
- * migration — you would have to export from the old jurisdiction and import
1394
- * into the new one.
1395
- *
1396
- * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1397
- * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1398
- * settings, not this option.
1399
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1400
- */
1907
+ * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1908
+ * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1909
+ * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1910
+ * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1911
+ * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1912
+ * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1913
+ *
1914
+ * ⚠️ **Set this once, before your first deploy — changing or removing it
1915
+ * strands data.** A Durable Object name maps to a *different* ID in each
1916
+ * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1917
+ * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1918
+ * old jurisdiction's DOs and is no longer reachable. There is no in-place
1919
+ * migration — you would have to export from the old jurisdiction and import
1920
+ * into the new one.
1921
+ *
1922
+ * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1923
+ * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1924
+ * settings, not this option.
1925
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1926
+ */
1401
1927
  jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
1402
1928
  /**
1403
- * Turn on secure-by-default RLS for the whole schema. Every table is then
1404
- * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1405
- * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1406
- * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1407
- * mode, so `.rls("required")` composes with `.extend(...)` either order.
1408
- */
1929
+ * Turn on secure-by-default RLS for the whole schema. Every table is then
1930
+ * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1931
+ * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1932
+ * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1933
+ * mode, so `.rls("required")` composes with `.extend(...)` either order.
1934
+ */
1409
1935
  rls: (mode: "required") => ExtendableSchema<T>;
1410
1936
  } & Schema<T>;
1411
- declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
1412
- /** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
1413
- declare const DEFAULT_TTL_MS = 3e4;
1414
- declare const PRESENCE_BARE_TABLE = "present";
1415
1937
  /**
1416
- * The prefixed table name the extension produces at merge time. The handlers
1417
- * read/write this name directly so they always agree with the merged schema.
1418
- */
1419
- declare const PRESENCE_TABLE: "presence_present";
1938
+ * Columns every row carries implicitly (never part of a table's declared
1939
+ * `shape`), so `.index()` may legitimately name them. The single source for
1940
+ * both the compile-time allow-list (`TableBuilder["index"]`'s `fields` type,
1941
+ * via `(typeof SYSTEM_INDEX_FIELDS)[number]`) and the runtime cross-check
1942
+ * below (via `SYSTEM_INDEX_FIELDS_SET`) — declared once so the two can't
1943
+ * drift apart.
1944
+ */
1945
+ declare const SYSTEM_INDEX_FIELDS: readonly ["_commitSeq", "_creationTime", "_id"];
1946
+ /**
1947
+ * Per-table, per-KIND index→declared-fields map: for each table, each index
1948
+ * KIND (`index` | `rank` | `geo`) that has at least one declared index maps
1949
+ * to a name→fields record for that kind only. Distilled by
1950
+ * {@link indexFieldsFromSchema}; this is the shape `mask()`'s
1951
+ * `MaskOptions.indexFields` expects (see `./mask/types`), so a table not
1952
+ * present here (no declared indexes of any kind) is simply absent from the
1953
+ * map rather than mapped to `{}`, and a kind with no declared indexes on a
1954
+ * table that HAS other kinds is simply absent from that table's entry.
1955
+ *
1956
+ * Kept per kind (rather than one flat name→fields record) because the engine
1957
+ * resolves `withIndex`/`withGeoIndex`/rank reads in THREE separate
1958
+ * namespaces (`tableDefinition.indexes` / `.geoIndexes` / `.rankIndexes` —
1959
+ * see `@lunora/shard-engine`'s `ctx-db.ts`), so the same name can legally and
1960
+ * unambiguously denote a different index per kind. A flat map would let one
1961
+ * kind's fields silently shadow another's for a colliding name, producing a
1962
+ * wrong-namespace answer from the mask guard (checking the wrong index's
1963
+ * fields) instead of the documented fail-open (missing lookup) — see plan 258.
1964
+ */
1965
+ type IndexFieldsByTable = Readonly<Record<string, {
1966
+ readonly geo?: Readonly<Record<string, ReadonlyArray<string>>>;
1967
+ readonly index?: Readonly<Record<string, ReadonlyArray<string>>>;
1968
+ readonly rank?: Readonly<Record<string, ReadonlyArray<string>>>;
1969
+ }>>;
1970
+ declare const indexFieldsFromSchema: (schema: Schema) => IndexFieldsByTable;
1971
+ declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
1420
1972
  /**
1421
- * A single present member as returned by `listPresent`.
1422
- *
1423
- * Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
1424
- * connection secret — disclosing every member's `sessionId` would let any
1425
- * subscriber enumerate them and target the heartbeat / disconnect write paths.
1426
- * A "who's here" UI needs only `userId` + awareness `data`; the caller already
1427
- * knows its own session id locally (the `usePresence` hook returns it).
1428
- */
1429
- interface PresenceMember {
1430
- /** Opaque awareness blob (selection, cursor, name, color…). */
1431
- data?: Record<string, unknown>;
1432
- /** Last heartbeat time (epoch ms). */
1433
- lastSeen: number;
1434
- /** The room / channel / document this presence is scoped to. */
1435
- roomId: string;
1436
- /** Authenticated user id, when known. */
1437
- userId?: string;
1438
- }
1439
- /** Options for {@link definePresence}. */
1440
- interface DefinePresenceOptions {
1441
- /**
1442
- * Grace window (ms) before a gracefully-closed session is dropped from the
1443
- * present list. When `0` (the default), `onDisconnect` hard-deletes the
1444
- * session's row the instant its socket closes. When `> 0`, the row is
1445
- * instead aged so the read-time TTL filter hides it `disconnectGraceMs`
1446
- * from now — a reconnect with the same `sessionId` within the window
1447
- * re-heartbeats and restores full presence with no visible flicker (the
1448
- * AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
1449
- */
1450
- disconnectGraceMs?: number;
1451
- /**
1452
- * How long (ms) a heartbeat keeps a member present. `listPresent` excludes
1453
- * rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
1454
- */
1455
- ttlMs?: number;
1456
- }
1457
- /** The registered functions a presence component ships. */
1458
- interface PresenceFunctions {
1459
- /**
1460
- * Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
1461
- * delete its presence row so it disappears from `listPresent` with no TTL
1462
- * lag. Targets the row by the `{ roomId, sessionId }` the client passed as
1463
- * the connection `context`, and only deletes it when the disconnecting
1464
- * VERIFIED identity owns the row (so a forged context can't evict another
1465
- * member). The TTL filter + `sweep` remain the fallback for ungraceful drops
1466
- * where no `context` was recorded.
1467
- */
1468
- disconnect: RegisteredLifecycleHook;
1469
- /**
1470
- * Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
1471
- * Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
1472
- * subscribers receive a single-row delta, not a churn of insert/delete. A
1473
- * heartbeat may only patch a row owned by the same identity (an existing row
1474
- * held by a different `userId` is refused with `FORBIDDEN`), so a client
1475
- * can't overwrite another member's awareness data via a guessed `sessionId`.
1476
- */
1477
- heartbeat: RegisteredMutation<{
1478
- data: ReturnType<typeof v.optional>;
1479
- roomId: ReturnType<typeof v.string>;
1480
- sessionId: ReturnType<typeof v.string>;
1481
- }, {
1482
- lastSeen: number;
1483
- }>;
1484
- /**
1485
- * Live query returning the non-expired members of `roomId`, newest heartbeat
1486
- * first. Subscribe to it for a reactive "who's here" list.
1487
- */
1488
- listPresent: RegisteredQuery<{
1489
- roomId: ReturnType<typeof v.string>;
1490
- }, PresenceMember[]>;
1491
- /**
1492
- * Internal mutation that hard-deletes every expired row for `roomId`. Stale
1493
- * rows already vanish from `listPresent` via the read-time TTL filter; this
1494
- * only reclaims storage. Schedule it (cron / `runAfter`) if you care.
1495
- */
1496
- sweep: RegisteredMutation<{
1497
- roomId: ReturnType<typeof v.string>;
1498
- }, {
1499
- deleted: number;
1500
- }>;
1501
- }
1502
- /** The component shape `definePresence` returns: the presence extension + typed functions. */
1503
- type PresenceComponent = Component<{
1504
- [PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
1505
- }> & {
1506
- functions: PresenceFunctions;
1507
- };
1508
- /**
1509
- * The presence schema extension: a single `present` table, auto-namespaced to
1510
- * `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
1511
- * heartbeat upsert and by `roomId` for `listPresent`.
1512
- */
1513
- declare const presenceExtension: SchemaExtension<{
1514
- [PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
1515
- }>;
1516
- declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
1517
- /**
1518
- * The middlewares `protectPublic` chains, in the order they run. Every field is
1519
- * optional, so a bundle can be just a rate limit, just a captcha, or any mix —
1520
- * pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
1521
- * from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
1522
- * `@lunora/auth`). They are accepted as values rather than imported here so
1523
- * `@lunora/server` keeps no dependency on those packages (which depend on it).
1524
- */
1525
- interface ProtectPublicOptions<Context> {
1526
- /**
1527
- * A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
1528
- * an obvious flood is cheaper to reject with the in-memory limiter than with
1529
- * a Turnstile siteverify round-trip.
1530
- */
1531
- captcha?: Middleware<Context, Context>;
1532
- /**
1533
- * A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
1534
- * any network-bound check below it runs.
1535
- */
1536
- rateLimit?: Middleware<Context, Context>;
1537
- /** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
1538
- use?: ReadonlyArray<Middleware<Context, Context>>;
1973
+ * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
1974
+ * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
1975
+ * resolver, same `can(...)` permission check so an author can branch a mask
1976
+ * on the caller's role/permission. `row` is the full pre-mask row the column
1977
+ * belongs to; `column` is the column currently being masked. Both are absent
1978
+ * when the context is used for the procedure-wide `bypass` check (no specific
1979
+ * cell is in play yet).
1980
+ */
1981
+ interface MaskContext<Context = unknown> {
1982
+ readonly auth: {
1983
+ /** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
1984
+ readonly can: (permission: Permission | string) => boolean;
1985
+ readonly identity?: Record<string, unknown> | null;
1986
+ readonly roles: ReadonlyArray<string>;
1987
+ readonly userId: null | string;
1988
+ };
1989
+ /** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
1990
+ readonly column?: string;
1991
+ readonly ctx: Context;
1992
+ /** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
1993
+ readonly row?: Record<string, unknown>;
1539
1994
  }
1540
1995
  /**
1541
- * Compose the recommended public-procedure protections into a single
1542
- * `.use()`-able middleware. It is thin sugar over middleware composition no
1543
- * new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
1544
- * and any extra middlewares so a public mutation that creates users, sends
1545
- * mail, or consumes credits is guarded in one attachment:
1546
- *
1547
- * ```ts
1548
- * export const signUp = mutation
1549
- * .use(protectPublic({
1550
- * rateLimit: rateLimit(limiter, "signup"),
1551
- * captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
1552
- * }))
1553
- * .handler(async (ctx, args) => { ... });
1554
- * ```
1555
- *
1556
- * The bundle is context-preserving — each inner middleware leaves the context
1557
- * unchanged — so it slots into any `.use()` chain without reshaping the
1558
- * procedure context. Omitted fields are skipped; an empty bundle is a
1559
- * transparent pass-through.
1560
- */
1561
- declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
1562
- declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
1996
+ * A custom masking function. Receives the raw cell value and the
1997
+ * {@link MaskContext}, returns the value to surface. Use it for partial masks
1998
+ * (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
1999
+ * or format-preserving tokens. A function that **throws** fails closed the
2000
+ * cell is redacted to `null`, never leaked raw.
2001
+ */
2002
+ type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
1563
2003
  /**
1564
- * Build a project-bound, relation-aware `definePolicy` typed against the
1565
- * generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
1566
- * `createPolicyDsl&lt;DataModel, Relations>()` binding into `_generated/server.ts`,
1567
- * so importing `definePolicy` from the generated module constrains `table` to a
1568
- * real table name and type-checks the `when` predicate including Prisma-style
1569
- * relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
1570
- * on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
1571
- * the compile-time surface narrows, so a policy authored either way is
1572
- * discovered identically by the `rls()` chain.
1573
- */
1574
- declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context>) => Policy<Context>;
1575
- /**
1576
- * Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
1577
- * it to a role through `defineRole`'s `permissions`, register those roles with
1578
- * the middleware via `rls(policies, { roles })`, then check it in a policy with
1579
- * `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
1580
- * worked example.
1581
- */
1582
- declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
2004
+ * How a column is masked:
2005
+ *
2006
+ * - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
2007
+ * the right choice for any value that must actually be kept secret.
2008
+ * - `"hash"` replace with a stable token (unsalted 32-bit FNV-1a hex) so the
2009
+ * same input always yields the same token (joinable/groupable client-side).
2010
+ * **This is NOT a confidentiality control.** It is a non-cryptographic,
2011
+ * unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
2012
+ * phone numbers, SSNs) are brute-force-recoverable by the very caller you are
2013
+ * masking from, and identical values always produce identical tokens across
2014
+ * rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
2015
+ * stable pseudonym for grouping/joining and leaking the value is acceptable —
2016
+ * never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
2017
+ * - a {@link MaskFn} author-defined transform (partial mask, role-aware reveal).
2018
+ */
2019
+ type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
2020
+ /** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
2021
+ type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
1583
2022
  /**
1584
- * Collect a list of policies into the structure the `rls()` middleware
1585
- * consumes. Multiple read policies on the same table OR together (any one
1586
- * matching reveals the row); multiple write policies for the same `(table,
1587
- * op)` AND together (every one must allow). The middleware keeps them in order
1588
- * and decides — see `./middleware`.
1589
- *
1590
- * Validates against an **accidentally duplicated policy** — the same
1591
- * `(table, on)` registered with the *same* decision function (a copy-paste, or
1592
- * the same policy object spread in twice). Because multiple DISTINCT policies
1593
- * per `(table, on)` are intentional, the check keys on the `when` reference too:
1594
- * only a reference-identical `when` for the same `(table, on)` is a real
1595
- * duplicate. Throws at module load so the misconfiguration surfaces immediately
1596
- * rather than as a silently double-evaluated predicate at request time.
1597
- */
1598
- declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
1599
- declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
2023
+ * The mask declaration passed to `mask(...)`: a table column → strategy map.
2024
+ * Deliberately a plain object literal so the codegen feeder can statically read
2025
+ * which columns a procedure masks (powering the `mask_uncovered_pii_column`
2026
+ * advisor lint), exactly as the RLS feeder reads policy tables.
2027
+ */
2028
+ type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
1600
2029
  /**
1601
- * Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
1602
- * runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
1603
- * `restrictsCounts` straight off these option objects, so as long as the
1604
- * fields here stay name-compatible the wrapper is portable across the two
1605
- * dialects without an inter-package dependency.
1606
- */
2030
+ * Options for `mask(policies, options)`.
2031
+ *
2032
+ * - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
2033
+ * inside a {@link MaskFn} identical to `rls(policies, { roles })`. A role
2034
+ * not listed grants no permissions (fails closed for unknown roles).
2035
+ * - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
2036
+ * mask is skipped (the caller sees raw values). Use it for a privileged
2037
+ * viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
2038
+ * branching every column when an entire class of caller should see clear data.
2039
+ * - `indexFields` closes the bare-index-scan / rank / geo position oracle (see
2040
+ * the `mask/middleware` module docblock's "Residual read-position oracles" section).
2041
+ */
2042
+ interface MaskOptions<Context = unknown> {
2043
+ readonly bypass?: (context: MaskContext<Context>) => boolean;
2044
+ /**
2045
+ * Per-table, per-KIND index→declared-fields map (regular index `fields`
2046
+ * under `index`; rank index `sortBy` ∪ `partitionBy` under `rank`; geo
2047
+ * index `field` under `geo`). Supplied to close the bare-index-scan /
2048
+ * rank / geo position oracle: a `withIndex(name)` with no range callback,
2049
+ * a `rank`/`rankPage`/`rankBefore` read, or a `withGeoIndex` read, over an
2050
+ * index whose DECLARED fields (for that read's own kind) intersect a
2051
+ * masked column, is rejected. Kept per kind — rather than one flat
2052
+ * name→fields map — because the engine resolves `withIndex` /
2053
+ * `withGeoIndex` / rank reads in three separate namespaces, so the same
2054
+ * index name can legally denote a different index per kind; a flat map
2055
+ * would let one kind's fields shadow another's for a colliding name.
2056
+ * OPTIONAL and additive — omit it and behaviour is unchanged (the oracle
2057
+ * stays open, exactly as before this option existed). Build it with
2058
+ * `indexFieldsFromSchema` (exported from `@lunora/server`):
2059
+ * `mask(policies, { indexFields: indexFieldsFromSchema(schema) })`.
2060
+ */
2061
+ readonly indexFields?: IndexFieldsByTable;
2062
+ readonly roles?: ReadonlyArray<Role>;
2063
+ }
2064
+ interface QueryPage {
2065
+ continueCursor: null | string;
2066
+ isDone: boolean;
2067
+ page: Record<string, unknown>[];
2068
+ }
1607
2069
  interface QueryArgs {
1608
- baseWhere?: WhereInput;
2070
+ baseWhere?: unknown;
1609
2071
  cursor?: null | string;
1610
2072
  limit?: number;
1611
- orderBy?: ReadonlyArray<unknown>;
1612
- /**
1613
- * Per-target-table read filter the RLS wrapper attaches so a `with` relation
1614
- * is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
1615
- * the top-level read: `(table) => readBase(table).baseWhere`.
1616
- */
1617
- relationBaseWhere?: (table: string) => undefined | WhereInput;
1618
- restrictsCounts?: boolean;
1619
- where?: WhereInput;
2073
+ orderBy?: ReadonlyArray<Record<string, unknown>>;
2074
+ /** Per-target-table mask applied to `with`-hydrated children — mirrors `@lunora/shard-engine`'s `RelationMask`. */
2075
+ relationMask?: (table: string, rows: Record<string, unknown>[]) => Record<string, unknown>[];
2076
+ where?: unknown;
1620
2077
  with?: Record<string, unknown>;
1621
2078
  }
1622
- interface CountArgs {
1623
- baseWhere?: WhereInput;
1624
- relationBaseWhere?: (table: string) => undefined | WhereInput;
1625
- restrictsCounts?: boolean;
1626
- where?: WhereInput;
1627
- }
1628
- /** Structural mirror of `@lunora/do`'s `AggregateOptions` — only the fields the wrapper touches. */
1629
2079
  interface AggregateArgs {
1630
- baseWhere?: WhereInput;
1631
2080
  field?: string;
1632
2081
  op: string;
1633
- relationBaseWhere?: (table: string) => undefined | WhereInput;
1634
- restrictsCounts?: boolean;
1635
- where?: WhereInput;
2082
+ where?: unknown;
1636
2083
  }
1637
- /** Structural mirror of `@lunora/do`'s `GroupByOptions`. */
1638
2084
  interface GroupByArgs {
1639
2085
  agg?: {
1640
2086
  field?: string;
1641
2087
  op: string;
1642
2088
  };
1643
- baseWhere?: WhereInput;
1644
2089
  by: ReadonlyArray<string>;
1645
- relationBaseWhere?: (table: string) => undefined | WhereInput;
1646
- restrictsCounts?: boolean;
1647
- where?: WhereInput;
1648
- }
1649
- /** Structural mirror of `@lunora/do`'s `RankOptions`. */
1650
- interface RankArgs {
1651
- baseWhere?: WhereInput;
1652
- restrictsCounts?: boolean;
1653
- row: Record<string, unknown> | string;
1654
- where?: WhereInput;
2090
+ where?: unknown;
1655
2091
  }
1656
- /** Structural mirror of `@lunora/do`'s `RankBeforeOptions`. */
1657
- interface RankBeforeArgs {
1658
- partitionKey: string;
1659
- restrictsCounts?: boolean;
1660
- rowId: string;
1661
- sortValues: ReadonlyArray<unknown>;
2092
+ /** One row of a `.collectWithScores()` result — mirrors `@lunora/shard-engine`'s `ScoredDocument`. */
2093
+ type ScoredDocument = GeoScoredDocument | SearchScoredDocument;
2094
+ /** A `.withGeoIndex()` row — mirrors `@lunora/shard-engine`'s `GeoScoredDocument`. */
2095
+ interface GeoScoredDocument {
2096
+ distanceMeters: null | number;
2097
+ document: Record<string, unknown>;
2098
+ score?: never;
1662
2099
  }
1663
- /** Structural mirror of `@lunora/do`'s `RankPageOptions`. */
1664
- interface RankPageArgs {
1665
- baseWhere?: WhereInput;
1666
- cursor?: null | string;
1667
- restrictsCounts?: boolean;
1668
- take?: number;
1669
- where?: WhereInput;
1670
- }
1671
- interface QueryPage {
1672
- continueCursor: null | string;
1673
- isDone: boolean;
1674
- page: Record<string, unknown>[];
2100
+ /** A `.withSearchIndex()` row — mirrors `@lunora/shard-engine`'s `SearchScoredDocument`. */
2101
+ interface SearchScoredDocument {
2102
+ distanceMeters?: never;
2103
+ document: Record<string, unknown>;
2104
+ score: number;
1675
2105
  }
1676
2106
  interface TableReaderLike {
2107
+ [Symbol.asyncIterator]: () => AsyncIterator<Record<string, unknown>>;
1677
2108
  collect: () => Promise<Record<string, unknown>[]>;
2109
+ collectWithScores: () => Promise<ScoredDocument[]>;
1678
2110
  filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike;
1679
2111
  first: () => Promise<Record<string, unknown> | null>;
2112
+ order: (direction: "asc" | "desc") => TableReaderLike;
1680
2113
  paginate: (options: {
1681
2114
  cursor?: null | string;
1682
2115
  numItems: number;
1683
2116
  }) => Promise<QueryPage>;
1684
2117
  take: (limit: number) => Promise<Record<string, unknown>[]>;
2118
+ unique: () => Promise<Record<string, unknown> | null>;
2119
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike;
1685
2120
  withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike;
1686
2121
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike;
1687
2122
  }
1688
2123
  /**
1689
- * Structural projection of the runtime ORM writer. The wrapper relies only
1690
- * on these fields, so it's interchangeable between `@lunora/do`'s
1691
- * `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
1692
- */
1693
- interface DatabaseWriterLike {
1694
- /**
1695
- * Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
1696
- * `baseWhere` into `options` so the reduction only sees policy-visible rows
1697
- * (safe: an aggregate scoped to `where` never reveals a hidden row — see
1698
- * `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
1699
- * wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
1700
- */
2124
+ * Structural projection of the runtime ORM writer the same subset
2125
+ * `../rls/middleware` mirrors, so the wrapper is interchangeable between
2126
+ * `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
2127
+ * inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
2128
+ */
2129
+ interface MaskDatabase {
1701
2130
  aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
1702
- count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
1703
- delete: (id: string, expectedTable?: string, options?: {
1704
- hard?: boolean;
1705
- }) => Promise<void>;
2131
+ count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
2132
+ delete: (id: string, expectedTable?: string) => Promise<void>;
1706
2133
  deleteMany: (ids: ReadonlyArray<string>, options?: {
1707
2134
  limit?: number;
1708
- }, expectedTable?: string) => Promise<{
2135
+ }) => Promise<{
2136
+ deleted: number;
2137
+ }>;
2138
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
2139
+ limit?: number;
2140
+ }) => Promise<{
1709
2141
  deleted: number;
1710
2142
  }>;
1711
2143
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1712
2144
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1713
2145
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
1714
2146
  get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
1715
- /**
1716
- * Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
1717
- * reduction is scoped to policy-visible rows, so a group count tallies only
1718
- * rows the caller may read. Required for the same reason as `aggregate`.
1719
- */
1720
2147
  groupBy: (tableName: string, options: GroupByArgs) => Promise<ReadonlyArray<{
1721
2148
  key: Record<string, unknown>;
1722
2149
  value: null | number;
@@ -1724,19 +2151,8 @@ interface DatabaseWriterLike {
1724
2151
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1725
2152
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1726
2153
  limit?: number;
1727
- }) => Promise<string[]>;
1728
- insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1729
- allowExplicitId?: boolean;
1730
- limit?: number;
1731
- }) => Promise<string[]>;
1732
- /**
1733
- * Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
1734
- * already knows the owning table of an id internally, so it can return
1735
- * `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
1736
- * uses it to collapse the per-call membership-probe fan-out (1 `get` + N
1737
- * `findFirst` across every policy table) down to one lookup. Writers that
1738
- * don't implement it fall back to the probe path.
1739
- */
2154
+ skipDuplicates?: boolean;
2155
+ }) => Promise<(string | null)[]>;
1740
2156
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
1741
2157
  row: Record<string, unknown>;
1742
2158
  tableName: string;
@@ -1747,61 +2163,470 @@ interface DatabaseWriterLike {
1747
2163
  patch: Record<string, unknown>;
1748
2164
  }>, options?: {
1749
2165
  limit?: number;
1750
- }, expectedTable?: string) => Promise<void>;
2166
+ }) => Promise<{
2167
+ patched: number;
2168
+ }>;
2169
+ patchWhere?: (tableName: string, args: {
2170
+ patch: Record<string, unknown>;
2171
+ where: Record<string, unknown>;
2172
+ }, options?: {
2173
+ limit?: number;
2174
+ }) => Promise<{
2175
+ patched: number;
2176
+ }>;
1751
2177
  query: (tableName: string) => TableReaderLike;
1752
- /**
1753
- * Rank a row within its partition. A position is a count-of-rows-before, so
1754
- * — exactly like `count()` — it can't be trusted in an RLS-restricted
1755
- * reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
1756
- * for the same reason as `aggregate`.
1757
- */
1758
- rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
2178
+ rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
1759
2179
  position: number;
1760
2180
  total: number;
1761
2181
  }>;
1762
- /** Cross-shard rank primitive same count-of-before RLS hazard as `rank`; failed closed under a read policy. */
1763
- rankBefore?: (tableName: string, indexName: string, options: RankBeforeArgs) => Promise<{
2182
+ rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
1764
2183
  before: number;
1765
2184
  total: number;
1766
2185
  }>;
1767
- /**
1768
- * Sorted pagination over a rank companion. The companion stores only the
1769
- * partition + sort keys + id, so an arbitrary read `baseWhere` can't be
1770
- * enforced against it (and re-filtering the fetched rows would break page
1771
- * sizing). RLS therefore fails it closed rather than leak hidden rows.
1772
- * Required for the same reason as `aggregate`.
1773
- */
1774
- rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
2186
+ rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage>;
2187
+ /** Cross-shard companion to `rankPage`, gated the same way `rankPage` is masked below. */
2188
+ rankPageRows?: (tableName: string, indexName: string, options?: unknown) => Promise<ShardRankPageResultLike>;
1775
2189
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1776
- restore?: (id: string, expectedTable?: string) => Promise<void>;
2190
+ }
2191
+ interface MaskContextIn {
2192
+ auth?: AuthLike;
2193
+ db: MaskDatabase;
1777
2194
  }
1778
2195
  /**
1779
- * What a procedure's `ctx.db` must structurally satisfy for the middleware
1780
- * to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
1781
- * rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
1782
- * the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
1783
- * and structural matching keeps this module free of an `@lunora/do`-typed
1784
- * `ctx`.
1785
- */
1786
- type RlsDatabase = DatabaseWriterLike;
1787
- /** Roles list source on the context. Tolerant of older auth states. */
1788
- type AuthLike = {
1789
- getIdentity?: () => Promise<Record<string, unknown> | null>;
1790
- roles?: ReadonlyArray<string>;
1791
- userId?: null | string;
2196
+ * Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
2197
+ * Closes over the policy map at builder-construction time; resolves identity +
2198
+ * the `bypass` decision per call against the live ctx.
2199
+ *
2200
+ * IMPORTANT: a mask is in scope only for procedures whose builder chain
2201
+ * includes this middleware — opt-in, never global (the same invariant as RLS).
2202
+ */
2203
+ declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
2204
+ /** Project-wide masked-column registry: every table any registered function masks, unioned. */
2205
+ type MaskRegistry = ReadonlyMap<string, ReadonlySet<string>>;
2206
+ /**
2207
+ * Build the project-wide masked-column registry from the registered functions
2208
+ * (pass `Object.values(LUNORA_FUNCTIONS)`) — the mask-column twin of
2209
+ * `buildRlsReadRegistry`. Unions every function's `.use(mask(...))` columns
2210
+ * per table: a column masked by ANY registered function counts as masked in
2211
+ * the registry — there is no "which procedure would this shape have gone
2212
+ * through" question to narrow by, so the union is the only safe answer.
2213
+ */
2214
+ declare const buildMaskRegistry: (functions: Iterable<unknown>) => MaskRegistry;
2215
+ /** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
2216
+ type MigrationDocument = Record<string, unknown>;
2217
+ /**
2218
+ * The read surface a transform reaches through its `ctx`.
2219
+ *
2220
+ * Read-only by design: the runner accounts for exactly one rewrite per row read,
2221
+ * and a transform writing directly would make that count describe something
2222
+ * other than what happened. Scoped to the shard the runner is walking.
2223
+ */
2224
+ interface MigrationReader {
2225
+ count: (table: string, where?: Record<string, unknown>) => Promise<number>;
2226
+ findFirst: (table: string, args?: Record<string, unknown>) => Promise<MigrationDocument | null>;
2227
+ findMany: (table: string, args?: Record<string, unknown>) => Promise<{
2228
+ isDone: boolean;
2229
+ page: MigrationDocument[];
2230
+ }>;
2231
+ get: (id: string, expectedTable?: string) => Promise<MigrationDocument | null>;
2232
+ }
2233
+ /** The context handed to a transform alongside the row. */
2234
+ interface MigrationCtx {
2235
+ db: MigrationReader;
2236
+ }
2237
+ /**
2238
+ * Transform applied to one document. Return a new document to rewrite the row,
2239
+ * or `undefined` to leave it untouched (skipped, not counted as changed). The
2240
+ * runner always preserves the original `_id` and `_creationTime`, so the
2241
+ * returned document neither needs to nor should change row identity.
2242
+ *
2243
+ * The second parameter carries a shard-scoped reader. Without it a transform
2244
+ * could only rewrite the row it was handed — enough for a backfill whose new
2245
+ * value is a pure function of the old row (`displayName = name ?? "Anonymous"`),
2246
+ * but not for the shape people actually write: read the parent, copy a field
2247
+ * down onto its children.
2248
+ *
2249
+ * May return a promise, since a cross-table read is asynchronous.
2250
+ *
2251
+ * **A shard key cannot be backfilled this way, even with a reader.** A row whose
2252
+ * shard-key field is unset does not belong to any shard, so a shard-scoped query
2253
+ * will not enumerate it; and writing the key would have to MOVE the row to a
2254
+ * different Durable Object, which a per-shard runner cannot do. Re-keying is an
2255
+ * export → transform → import, not a migration.
2256
+ */
2257
+ type MigrationTransform = (document: MigrationDocument, ctx: MigrationCtx) => MigrationDocument | Promise<MigrationDocument | undefined | void> | undefined | void;
2258
+ interface MigrationDefinition {
2259
+ /** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
2260
+ readonly batchSize?: number;
2261
+ /** Optional reverse transform, applied by `migrate down`. */
2262
+ readonly down?: MigrationTransform;
2263
+ /** Stable, unique identifier — the key per-shard run-state is tracked under. */
2264
+ readonly id: string;
2265
+ /** Table whose documents this migration iterates. */
2266
+ readonly table: string;
2267
+ /** Forward transform, applied to every row by `migrate up`. */
2268
+ readonly up: MigrationTransform;
2269
+ }
2270
+ /** A {@link MigrationDefinition} plus the codegen discovery marker. */
2271
+ interface RegisteredMigration extends MigrationDefinition {
2272
+ readonly __lunoraMigration: true;
2273
+ }
2274
+ /** Declare an online data migration. See the module docs for runtime semantics. */
2275
+ declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
2276
+ /**
2277
+ * A mutator declaration. `server` is authoritative; `client` is the optimistic
2278
+ * twin (optional — omit it to let the optimistic write fall through to the
2279
+ * server round-trip with no local preview). Both receive the same validated
2280
+ * `args`.
2281
+ */
2282
+ interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
2283
+ /**
2284
+ * Validator for the mutator's arguments. Validated on the DO before `server`
2285
+ * runs and (when present) on the client before `client` runs, so both impls
2286
+ * see the same parsed shape. Omit for a parameterless mutator.
2287
+ */
2288
+ readonly args?: Args;
2289
+ /**
2290
+ * Optimistic client implementation. Runs in a TanStack DB transaction
2291
+ * against the local collections; its writes are applied immediately and
2292
+ * automatically rolled back / rebased as the authoritative result syncs
2293
+ * back. Pure and side-effect-free beyond the local store. Omit to skip the
2294
+ * local preview.
2295
+ */
2296
+ readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
2297
+ /**
2298
+ * Owner-scope the write: names the column carrying the row owner (e.g.
2299
+ * `owner: "userId"`). Before `server` runs, the mutator requires a verified
2300
+ * identity, rejects a client-supplied owner that disagrees with it, and sets
2301
+ * the column to the verified value — so the impl reads `args[owner]` without
2302
+ * trusting the client and never repeats the check by hand.
2303
+ *
2304
+ * This replaces the "every mutator opens with `assertOwner(ctx, args.userId)`"
2305
+ * pattern, and is the write-side counterpart to an `owner`-scoped
2306
+ * {@link import("./shapes").ShapeDefinition}. Unlike a shape it takes the column
2307
+ * NAME rather than `true`: a shape is bound to one `table`, so the table's
2308
+ * `.ownedBy(field)` resolves unambiguously, whereas one mutator may write
2309
+ * several tables and has no single owning table to read it from.
2310
+ *
2311
+ * Declare the column `v.optional(...)` to leave it off the wire entirely; it is
2312
+ * injected either way.
2313
+ */
2314
+ readonly owner?: string;
2315
+ /**
2316
+ * Authoritative server implementation. Runs inside the shard DO with a full
2317
+ * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
2318
+ * and poke back to subscribers. This is the source of truth — the client
2319
+ * impl is only a prediction of it.
2320
+ */
2321
+ readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
2322
+ }
2323
+ /**
2324
+ * A {@link MutatorDefinition} plus the codegen discovery marker and a
2325
+ * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
2326
+ * invokes a mutator exactly like a registered procedure.
2327
+ */
2328
+ interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
2329
+ readonly __lunoraMutator: true;
2330
+ /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
2331
+ readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
2332
+ /**
2333
+ * Marks the dispatch kind so codegen can register the mutator in the same
2334
+ * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
2335
+ * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
2336
+ * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
2337
+ * ordinary `mutation`.
2338
+ */
2339
+ readonly kind: "mutation";
2340
+ }
2341
+ /** Declare a custom mutator. See the module docs for runtime semantics. */
2342
+ declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
2343
+ /** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
2344
+ declare const DEFAULT_TTL_MS = 3e4;
2345
+ declare const PRESENCE_BARE_TABLE = "present";
2346
+ /**
2347
+ * The prefixed table name the extension produces at merge time. The handlers
2348
+ * read/write this name directly so they always agree with the merged schema.
2349
+ */
2350
+ declare const PRESENCE_TABLE: "presence_present";
2351
+ /**
2352
+ * A single present member as returned by `listPresent`.
2353
+ *
2354
+ * Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
2355
+ * connection secret — disclosing every member's `sessionId` would let any
2356
+ * subscriber enumerate them and target the heartbeat / disconnect write paths.
2357
+ * A "who's here" UI needs only `userId` + awareness `data`; the caller already
2358
+ * knows its own session id locally (the `usePresence` hook returns it).
2359
+ */
2360
+ interface PresenceMember {
2361
+ /** Opaque awareness blob (selection, cursor, name, color…). */
2362
+ data?: Record<string, unknown>;
2363
+ /** Last heartbeat time (epoch ms). */
2364
+ lastSeen: number;
2365
+ /** The room / channel / document this presence is scoped to. */
2366
+ roomId: string;
2367
+ /** Authenticated user id, when known. */
2368
+ userId?: string;
2369
+ }
2370
+ /** Options for {@link definePresence}. */
2371
+ interface DefinePresenceOptions {
2372
+ /**
2373
+ * Grace window (ms) before a gracefully-closed session is dropped from the
2374
+ * present list. When `0` (the default), `onDisconnect` hard-deletes the
2375
+ * session's row the instant its socket closes. When `> 0`, the row is
2376
+ * instead aged so the read-time TTL filter hides it `disconnectGraceMs`
2377
+ * from now — a reconnect with the same `sessionId` within the window
2378
+ * re-heartbeats and restores full presence with no visible flicker (the
2379
+ * AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
2380
+ */
2381
+ disconnectGraceMs?: number;
2382
+ /**
2383
+ * Upper bound on SESSION ROWS `listPresent` reads per call — one row per
2384
+ * `(roomId, sessionId)`, so one per open tab, NOT one per person. The read
2385
+ * is newest-first over the `(roomId, lastSeen)` index, so the rows kept are
2386
+ * always the freshest heartbeats and a room with more sessions than this
2387
+ * has its stalest ones truncated.
2388
+ *
2389
+ * Because the multi-tab dedup collapses rows only AFTER the read, a cap
2390
+ * below the room's live session count drops real, currently-heartbeating
2391
+ * members off the bottom of the list. Size it against expected tabs (a
2392
+ * 300-person room at two tabs each is 600 rows), not expected people.
2393
+ * Defaults to 1024. A non-finite value falls back to the default.
2394
+ */
2395
+ maxSessions?: number;
2396
+ /**
2397
+ * How long (ms) a heartbeat keeps a member present. `listPresent` excludes
2398
+ * rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
2399
+ */
2400
+ ttlMs?: number;
2401
+ }
2402
+ /** The registered functions a presence component ships. */
2403
+ interface PresenceFunctions {
2404
+ /**
2405
+ * Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
2406
+ * delete its presence row so it disappears from `listPresent` with no TTL
2407
+ * lag. Targets the row by the `{ roomId, sessionId }` the client passed as
2408
+ * the connection `context`, and only deletes it when the disconnecting
2409
+ * VERIFIED identity owns the row (so a forged context can't evict another
2410
+ * member). The TTL filter + `sweep` remain the fallback for ungraceful drops
2411
+ * where no `context` was recorded.
2412
+ */
2413
+ disconnect: RegisteredLifecycleHook;
2414
+ /**
2415
+ * Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
2416
+ * Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
2417
+ * subscribers receive a single-row delta, not a churn of insert/delete. A
2418
+ * heartbeat may only patch a row owned by the same identity (an existing row
2419
+ * held by a different `userId` is refused with `FORBIDDEN`), so a client
2420
+ * can't overwrite another member's awareness data via a guessed `sessionId`.
2421
+ */
2422
+ heartbeat: RegisteredMutation<{
2423
+ data: ReturnType<typeof v.optional>;
2424
+ roomId: ReturnType<typeof v.string>;
2425
+ sessionId: ReturnType<typeof v.string>;
2426
+ }, {
2427
+ lastSeen: number;
2428
+ }>;
2429
+ /**
2430
+ * Live query returning the non-expired members of `roomId`, newest heartbeat
2431
+ * first. Subscribe to it for a reactive "who's here" list.
2432
+ */
2433
+ listPresent: RegisteredQuery<{
2434
+ roomId: ReturnType<typeof v.string>;
2435
+ }, PresenceMember[]>;
2436
+ /**
2437
+ * Internal mutation that hard-deletes every expired row for `roomId`. Stale
2438
+ * rows already vanish from `listPresent` via the read-time TTL filter, and
2439
+ * active rooms self-clean via the heartbeat's opportunistic reap; this is
2440
+ * optional hardening for bulk cleanup of rooms that went quiet with stale
2441
+ * rows left behind (schedule it on a cron / `runAfter` if you care).
2442
+ */
2443
+ sweep: RegisteredMutation<{
2444
+ roomId: ReturnType<typeof v.string>;
2445
+ }, {
2446
+ deleted: number;
2447
+ }>;
2448
+ }
2449
+ /** The component shape `definePresence` returns: the presence extension + typed functions. */
2450
+ type PresenceComponent = Component<{
2451
+ [PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
2452
+ }> & {
2453
+ functions: PresenceFunctions;
1792
2454
  };
1793
- /** Minimal shape the middleware needs on the incoming ctx. */
1794
- interface RlsContextIn {
1795
- auth?: AuthLike;
1796
- db: RlsDatabase;
2455
+ /**
2456
+ * The presence schema extension: a single `present` table, auto-namespaced to
2457
+ * `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
2458
+ * heartbeat upsert and by `roomId` for `listPresent`.
2459
+ */
2460
+ declare const presenceExtension: SchemaExtension<{
2461
+ [PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
2462
+ }>;
2463
+ declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
2464
+ /**
2465
+ * The middlewares `protectPublic` chains, in the order they run. Every field is
2466
+ * optional, so a bundle can be just a rate limit, just a captcha, or any mix —
2467
+ * pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
2468
+ * from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
2469
+ * `@lunora/auth`). They are accepted as values rather than imported here so
2470
+ * `@lunora/server` keeps no dependency on those packages (which depend on it).
2471
+ */
2472
+ interface ProtectPublicOptions<Context> {
2473
+ /**
2474
+ * A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
2475
+ * an obvious flood is cheaper to reject with the in-memory limiter than with
2476
+ * a Turnstile siteverify round-trip.
2477
+ */
2478
+ captcha?: Middleware<Context, Context>;
2479
+ /**
2480
+ * A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
2481
+ * any network-bound check below it runs.
2482
+ */
2483
+ rateLimit?: Middleware<Context, Context>;
2484
+ /** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
2485
+ use?: ReadonlyArray<Middleware<Context, Context>>;
1797
2486
  }
1798
- declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
1799
2487
  /**
1800
- * One `rls()` tag's read policies for a table, paired with the role→permission
1801
- * grants of that SAME middleware. Keeping the role map per-group is what lets a
1802
- * policy's `auth.can(...)` resolve against its own middleware's roles never a
1803
- * permission registered on a different `rls()` step.
1804
- */
2488
+ * Compose the recommended public-procedure protections into a single
2489
+ * `.use()`-able middleware. It is thin sugar over middleware composition no
2490
+ * new enforcement engine chaining (in order) a rate limit, a CAPTCHA check,
2491
+ * and any extra middlewares so a public mutation that creates users, sends
2492
+ * mail, or consumes credits is guarded in one attachment:
2493
+ *
2494
+ * ```ts
2495
+ * export const signUp = mutation
2496
+ * .use(protectPublic({
2497
+ * rateLimit: rateLimit(limiter, "signup"),
2498
+ * captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
2499
+ * }))
2500
+ * .handler(async (ctx, args) => { ... });
2501
+ * ```
2502
+ *
2503
+ * The bundle is context-preserving — each inner middleware leaves the context
2504
+ * unchanged — so it slots into any `.use()` chain without reshaping the
2505
+ * procedure context. Omitted fields are skipped; an empty bundle is a
2506
+ * transparent pass-through.
2507
+ */
2508
+ declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
2509
+ /**
2510
+ * The read a reactor watches. Runs on every flush that touched a table it read
2511
+ * last time, so keep it indexed and bounded — this is the reactor's steady-state
2512
+ * cost, paid whether or not the handler ends up running.
2513
+ */
2514
+ type ReactorSelect<T> = (context: QueryCtx) => Promise<T> | T;
2515
+ /** What runs when {@link ReactorSelect}'s result changes. Receives the current result. */
2516
+ type ReactorHandler<T> = (context: MutationCtx, result: T) => Promise<void> | void;
2517
+ /**
2518
+ * A registered reactor — an internal mutation tagged `lifecycle: "reactor"`.
2519
+ *
2520
+ * Typed separately from `RegisteredLifecycleHook` (which returns `void`) because
2521
+ * a reactor dispatch reports a {@link ReactorOutcome} back to the shard: the
2522
+ * digest becomes the next baseline, and the shard cannot compute it itself
2523
+ * without re-running the read.
2524
+ */
2525
+ type RegisteredReactor = RegisteredFunction<Record<string, never>, ReactorOutcome, "mutation"> & {
2526
+ readonly lifecycle: "reactor";
2527
+ };
2528
+ /** What a reactor dispatch reports back so the shard can update the baseline. */
2529
+ interface ReactorOutcome {
2530
+ /** Digest of the result the handler was given (or would have been). */
2531
+ digest: string;
2532
+ /** Whether the handler actually ran — `false` when the result was unchanged. */
2533
+ ran: boolean;
2534
+ }
2535
+ /**
2536
+ * Register a reactor: run `handler` after a write flush, whenever `select`'s
2537
+ * result differs from the last result the handler saw. See the module docblock
2538
+ * for the trigger comparison and the convergence contract.
2539
+ *
2540
+ * The read is an inline callback rather than a reference to a registered query
2541
+ * on purpose. It keeps the declaration statically discoverable with no import
2542
+ * resolution and no dependency on `_generated/api`, and it lets the read and the
2543
+ * handler share one transaction and one read footprint.
2544
+ *
2545
+ * That shared footprint is deliberately conservative: it is the union of what
2546
+ * `select` AND `handler` read, so a table the handler merely consults can wake
2547
+ * the reactor later. The cost of that is one extra `select` — whose digest then
2548
+ * matches and suppresses the handler — never a missed reaction.
2549
+ */
2550
+ declare const onQueryChange: <T>(select: ReactorSelect<T>, handler: ReactorHandler<T>) => RegisteredReactor;
2551
+ declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
2552
+ /**
2553
+ * Build a project-bound, relation-aware `definePolicy` typed against the
2554
+ * generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
2555
+ * `createPolicyDsl<DataModel, Relations>()` binding into `_generated/server.ts`,
2556
+ * so importing `definePolicy` from the generated module constrains `table` to a
2557
+ * real table name and type-checks the `when` predicate — including Prisma-style
2558
+ * relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
2559
+ * on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
2560
+ * the compile-time surface narrows, so a policy authored either way is
2561
+ * discovered identically by the `rls()` chain.
2562
+ */
2563
+ declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>, Identity = Record<string, unknown>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context, Identity>) => Policy<Context>;
2564
+ /**
2565
+ * Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
2566
+ * it to a role through `defineRole`'s `permissions`, register those roles with
2567
+ * the middleware via `rls(policies, { roles })`, then check it in a policy with
2568
+ * `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
2569
+ * worked example.
2570
+ */
2571
+ declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
2572
+ /**
2573
+ * Collect a list of policies into the structure the `rls()` middleware
2574
+ * consumes. Multiple read policies on the same table OR together (any one
2575
+ * matching reveals the row); multiple write policies for the same `(table,
2576
+ * op)` AND together (every one must allow). The middleware keeps them in order
2577
+ * and decides — see `./middleware`.
2578
+ *
2579
+ * Validates against an **accidentally duplicated policy** — the same
2580
+ * `(table, on)` registered with the *same* decision function (a copy-paste, or
2581
+ * the same policy object spread in twice). Because multiple DISTINCT policies
2582
+ * per `(table, on)` are intentional, the check keys on the `when` reference too:
2583
+ * only a reference-identical `when` for the same `(table, on)` is a real
2584
+ * duplicate. Throws at module load so the misconfiguration surfaces immediately
2585
+ * rather than as a silently double-evaluated predicate at request time.
2586
+ */
2587
+ declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
2588
+ declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
2589
+ /**
2590
+ * A predicate matching **no** rows — the deny decision.
2591
+ *
2592
+ * `{ OR: [] }` is a disjunction over zero branches, which the where-compiler
2593
+ * folds to a constant false. Use it (never a bare `{}`) for the unauthorized
2594
+ * branch of a shape or policy predicate:
2595
+ *
2596
+ * ```ts
2597
+ * where: (ctx, { userId }) => (ctx.auth.userId === userId ? { userId } : deny())
2598
+ * ```
2599
+ *
2600
+ * A fresh object per call, so a caller can safely spread or extend the result.
2601
+ */
2602
+ declare const deny: () => WhereInput;
2603
+ /**
2604
+ * A predicate matching **every** row — no restriction beyond whatever it is
2605
+ * AND-composed with. The honest spelling of "this branch adds no filter", where a
2606
+ * bare `{}` reads like an oversight.
2607
+ */
2608
+ declare const allowAll: () => WhereInput;
2609
+ /**
2610
+ * `true` when `where` is the vacuously-false deny predicate — i.e. an `OR` over
2611
+ * zero branches, carrying no other constraint. Used by the runtime to short-circuit
2612
+ * a denied read instead of compiling and running SQL that provably matches nothing.
2613
+ */
2614
+ declare const isDeny: (where: WhereInput) => boolean;
2615
+ /**
2616
+ * Normalize a boolean-or-predicate decision to a {@link WhereInput}.
2617
+ *
2618
+ * The authoring surfaces accept `true`/`false` as sugar — they read far better
2619
+ * than the sentinel objects in an ownership check — and every consumer downstream
2620
+ * wants a plain predicate. `undefined` means "no opinion" and maps to
2621
+ * {@link allowAll}, matching the RLS `PolicyDecision` contract.
2622
+ */
2623
+ declare const toWhereInput: (decision: WhereInput | boolean | undefined) => WhereInput;
2624
+ /**
2625
+ * One `rls()` tag's read policies for a table, paired with the role→permission
2626
+ * grants of that SAME middleware. Keeping the role map per-group is what lets a
2627
+ * policy's `auth.can(...)` resolve against its own middleware's roles — never a
2628
+ * permission registered on a different `rls()` step.
2629
+ */
1805
2630
  interface ScopedReadPolicies {
1806
2631
  readonly policies: ReadonlyArray<Policy>;
1807
2632
  readonly rolePermissions: ReadonlyMap<string, ReadonlySet<string>>;
@@ -1830,86 +2655,115 @@ interface ShapeReadWhereRequest {
1830
2655
  readonly userId: null | string;
1831
2656
  }
1832
2657
  /**
1833
- * Build the read-policy registry from the registered functions (pass
1834
- * `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
1835
- * grouped per `rls()` middleware so each group keeps its own role→permission map
1836
- * (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
1837
- * several procedures (a shared `const guard = rls(...)`) is folded once. This
1838
- * mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
1839
- * resolves against the roles of the middleware that declared it, never a union.
1840
- */
2658
+ * Build the read-policy registry from the registered functions (pass
2659
+ * `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
2660
+ * grouped per `rls()` middleware so each group keeps its own role→permission map
2661
+ * (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
2662
+ * several procedures (a shared `const guard = rls(...)`) is folded once. This
2663
+ * mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
2664
+ * resolves against the roles of the middleware that declared it, never a union.
2665
+ */
1841
2666
  declare const buildRlsReadRegistry: (functions: Iterable<unknown>) => RlsReadRegistry;
1842
2667
  /**
1843
- * Compute the effective `where` a shape replicates: the table's RLS read
1844
- * base-where AND the shape's own predicate. Returns the shape predicate
1845
- * unchanged for a table with no read policy (a `.public()` or non-RLS table),
1846
- * and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
1847
- * exposes a protected, policy-less table.
1848
- */
2668
+ * Compute the effective `where` a shape replicates: the table's RLS read
2669
+ * base-where AND the shape's own predicate. Returns the shape predicate
2670
+ * unchanged for a table with no read policy (a `.public()` or non-RLS table),
2671
+ * and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
2672
+ * exposes a protected, policy-less table.
2673
+ */
1849
2674
  declare const composeShapeReadWhere: (registry: RlsReadRegistry, request: ShapeReadWhereRequest) => WhereInput;
1850
2675
  /**
1851
- * A shape declaration. `where` receives the trusted procedure context and the
1852
- * validated client args and returns the same {@link WhereInput} shape the RLS
1853
- * DSL uses, so the DO can AND-merge it with the table's read base-where via the
1854
- * existing where-compiler (zero second predicate implementation).
1855
- */
2676
+ * A shape declaration. `where` receives the trusted procedure context and the
2677
+ * validated client args and returns the same {@link WhereInput} shape the RLS
2678
+ * DSL uses, so the DO can AND-merge it with the table's read base-where via the
2679
+ * existing where-compiler (zero second predicate implementation).
2680
+ */
1856
2681
  interface ShapeDefinition<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> {
1857
2682
  /**
1858
- * Validator for the client-supplied shape parameters. Validated on the DO
1859
- * before `where` runs, so a malformed `args` envelope is rejected at the
1860
- * subscription boundary rather than silently widening the partition. Omit
1861
- * for a parameterless shape.
1862
- */
2683
+ * Validator for the client-supplied shape parameters. Validated on the DO
2684
+ * before `where` runs, so a malformed `args` envelope is rejected at the
2685
+ * subscription boundary rather than silently widening the partition. Omit
2686
+ * for a parameterless shape.
2687
+ */
1863
2688
  readonly args?: Args;
1864
2689
  /**
1865
- * Project the replicated rows to these columns (the system columns `_id` and
1866
- * `_creationTime` are always included). Omit to replicate every column. An
1867
- * empty array is rejected — it would replicate no data, which is never the
1868
- * intent.
1869
- */
2690
+ * Project the replicated rows to these columns (the system columns `_id` and
2691
+ * `_creationTime` are always included). Omit to replicate every column. An
2692
+ * empty array is rejected — it would replicate no data, which is never the
2693
+ * intent.
2694
+ */
1870
2695
  readonly columns?: ReadonlyArray<string>;
2696
+ /**
2697
+ * Restrict this shape to rows the **subscriber owns**, without writing the
2698
+ * predicate by hand.
2699
+ *
2700
+ * `true` uses the owning column declared by `.ownedBy(field)` on the table;
2701
+ * `"field"` names the owning column here instead (for a table that has no
2702
+ * `.ownedBy()`, or a shape that scopes by a different column).
2703
+ *
2704
+ * The derived predicate is `{ [field]: ctx.auth.userId }`, AND-composed with
2705
+ * the shape's own `where` when both are present, and an anonymous subscriber is denied
2706
+ * outright. Because the value comes from the socket's verified identity rather
2707
+ * than from `args`, a client cannot ask for another user's partition at all —
2708
+ * strictly safer than accepting a `userId` arg and comparing it.
2709
+ *
2710
+ * This is the primitive for the "every shape repeats the same ownership check"
2711
+ * pattern: with it, an owner-scoped shape needs no `where` at all.
2712
+ */
2713
+ readonly owner?: string | true;
1871
2714
  /** Logical table this shape replicates a partition of. */
1872
2715
  readonly table: string;
1873
2716
  /**
1874
- * Predicate selecting the rows this shape replicates. AND-composed with the
1875
- * table's RLS read base-where on the DO. Runs server-side with a trusted
1876
- * `ctx` (identity/auth the client can't forge) and the validated client
1877
- * `args`; returns a {@link WhereInput} using the same operator set as the
1878
- * SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
1879
- */
1880
- readonly where: (context: Context, args: InferValidatorMap<Args>) => WhereInput;
2717
+ * Predicate selecting the rows this shape replicates. AND-composed with the
2718
+ * table's RLS read base-where on the DO. Runs server-side with a trusted
2719
+ * `ctx` (identity/auth the client can't forge) and the validated client
2720
+ * `args`; returns a {@link WhereInput} using the same operator set as the
2721
+ * SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
2722
+ *
2723
+ * `true`/`false` are accepted as sugar for "no further restriction" / "deny" —
2724
+ * `false` compiles to the vacuously-false predicate, so a denial branch can
2725
+ * never be mistyped as the everything-matches `{}`. Optional when `owner` is
2726
+ * set; otherwise required.
2727
+ */
2728
+ readonly where?: (context: Context, args: InferValidatorMap<Args>) => WhereInput | boolean;
1881
2729
  }
1882
2730
  /** A {@link ShapeDefinition} plus the codegen discovery marker and a dispatch-shaped `compileWhere`. */
1883
2731
  interface RegisteredShape<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> extends ShapeDefinition<Args, Context> {
1884
2732
  readonly __lunoraShape: true;
1885
2733
  /**
1886
- * Validate `rawArgs`, then evaluate `where` under the trusted `ctx` and
1887
- * return its {@link WhereInput}. Used by the generated DO's `resolveShape`
1888
- * override: `ctx` is erased to `unknown` at this dispatch boundary (the DO
1889
- * builds it from the socket's verified identity and hands it back as the
1890
- * concrete {@link QueryContext} the predicate expects), exactly like
1891
- * `RegisteredLunoraFunction.handler` erases its context.
1892
- */
1893
- readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>) => WhereInput;
2734
+ * Validate `rawArgs`, then evaluate the shape's predicate under the trusted
2735
+ * `ctx` and return its {@link WhereInput}. Used by the generated DO's
2736
+ * `resolveShape` override: `ctx` is erased to `unknown` at this dispatch
2737
+ * boundary (the DO builds it from the socket's verified identity and hands it
2738
+ * back as the concrete {@link QueryContext} the predicate expects), exactly like
2739
+ * `RegisteredLunoraFunction.handler` erases its context.
2740
+ *
2741
+ * `options.ownerField` carries the table's `.ownedBy(field)` column, which only
2742
+ * the caller (the DO, holding the schema) can look up — it is what resolves an
2743
+ * `owner: true` shape. Omitted for an `owner: "field"` or plain `where` shape.
2744
+ */
2745
+ readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>, options?: {
2746
+ ownerField?: string;
2747
+ }) => WhereInput;
1894
2748
  }
1895
2749
  /** Declare a replication shape. See the module docs for runtime semantics. */
1896
2750
  declare const defineShape: <Args extends ValidatorMap = ValidatorMap, Context = QueryCtx>(definition: ShapeDefinition<Args, Context>) => RegisteredShape<Args, Context>;
1897
2751
  /**
1898
- * Operations a storage rule can gate. `read` covers `download` / `getMetadata`
1899
- * / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
1900
- * `delete` is `delete`; `list` is a prefix listing (governed via the file
1901
- * browser / admin path, not `ctx.storage` which has no `list`).
1902
- */
2752
+ * Operations a storage rule can gate. `read` covers `download` / `getMetadata`
2753
+ * / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
2754
+ * `delete` is `delete`; `list` is a prefix listing (governed via the file
2755
+ * browser / admin path, not `ctx.storage` which has no `list`).
2756
+ */
1903
2757
  type StorageOperation = "delete" | "list" | "read" | "write";
1904
2758
  /** A rule's decision. `true` allows, `false` denies, `undefined` opts this rule out. */
1905
2759
  type StorageRuleDecision = boolean | undefined;
1906
2760
  /**
1907
- * Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
1908
- * (the per-request userId / roles / identity and the `can(permission)` helper),
1909
- * so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
1910
- * `key` is the object key the operation targets (for `list`, the listing
1911
- * prefix). `ctx` is the full procedure context the middleware closed over.
1912
- */
2761
+ * Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
2762
+ * (the per-request userId / roles / identity and the `can(permission)` helper),
2763
+ * so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
2764
+ * `key` is the object key the operation targets (for `list`, the listing
2765
+ * prefix). `ctx` is the full procedure context the middleware closed over.
2766
+ */
1913
2767
  interface StorageRuleContext<Context = unknown> {
1914
2768
  readonly auth: {
1915
2769
  readonly can: (permission: Permission | string) => boolean;
@@ -1924,11 +2778,11 @@ interface StorageRuleContext<Context = unknown> {
1924
2778
  /** A registered storage rule as stored in the rule table. */
1925
2779
  interface StorageRule<Context = unknown> {
1926
2780
  /**
1927
- * Logical bucket the rule governs — matched against the accessor's bucket
1928
- * (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
1929
- * A rule only applies to operations on its own bucket. The unnamed bucket is
1930
- * `"default"`. Also surfaced in the studio's access-rules view.
1931
- */
2781
+ * Logical bucket the rule governs — matched against the accessor's bucket
2782
+ * (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
2783
+ * A rule only applies to operations on its own bucket. The unnamed bucket is
2784
+ * `"default"`. Also surfaced in the studio's access-rules view.
2785
+ */
1932
2786
  readonly bucket: string;
1933
2787
  readonly on: StorageOperation;
1934
2788
  /** Optional key-prefix scope; the rule only governs keys under it. Absent ⇒ the whole bucket. */
@@ -1943,38 +2797,32 @@ interface DefineStorageRuleInput<Context = unknown> {
1943
2797
  when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
1944
2798
  }
1945
2799
  /**
1946
- * Options for the `storageRules(rules, options)` middleware. `roles` registers
1947
- * the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
1948
- * `RlsOptions.roles` does — fail-closed for unlisted roles.
1949
- */
2800
+ * Options for the `storageRules(rules, options)` middleware. `roles` registers
2801
+ * the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
2802
+ * `RlsOptions.roles` does — fail-closed for unlisted roles.
2803
+ */
1950
2804
  interface StorageRulesOptions {
1951
2805
  readonly roles?: ReadonlyArray<Role>;
1952
2806
  }
1953
2807
  declare const defineStorageRule: <Context = unknown>(input: DefineStorageRuleInput<Context>) => StorageRule<Context>;
1954
2808
  /**
1955
- * Collect a list of storage rules into the structure the `storageRules()`
1956
- * middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
1957
- * any one allowing grants the operation (each rule grants a slice of the
1958
- * keyspace).
1959
- *
1960
- * Validates against an **accidentally duplicated rule** — the same
1961
- * `(bucket, on, prefix)` registered with the *same* decision function (a
1962
- * copy-paste, or the same rule object spread in twice). Because multiple
1963
- * DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
1964
- * `when` reference too. Throws at module load so the misconfiguration surfaces
1965
- * immediately rather than as a silently double-evaluated predicate.
1966
- */
2809
+ * Collect a list of storage rules into the structure the `storageRules()`
2810
+ * middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
2811
+ * any one allowing grants the operation (each rule grants a slice of the
2812
+ * keyspace).
2813
+ *
2814
+ * Validates against an **accidentally duplicated rule** — the same
2815
+ * `(bucket, on, prefix)` registered with the *same* decision function (a
2816
+ * copy-paste, or the same rule object spread in twice). Because multiple
2817
+ * DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
2818
+ * `when` reference too. Throws at module load so the misconfiguration surfaces
2819
+ * immediately rather than as a silently double-evaluated predicate.
2820
+ */
1967
2821
  declare const defineStorageRules: <Context = unknown>(rules: ReadonlyArray<StorageRule<Context>>) => ReadonlyArray<StorageRule<Context>>;
1968
- /** The minimal `ctx.auth` shape the middleware reads — a structural subset that the full AuthState satisfies. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
1969
- type StorageAuthLike = {
1970
- getIdentity?: () => Promise<Record<string, unknown> | null>;
1971
- roles?: ReadonlyArray<string>;
1972
- userId?: null | string;
1973
- };
1974
2822
  interface StorageContextIn {
1975
- auth?: StorageAuthLike;
2823
+ auth?: AuthLike;
1976
2824
  storage?: unknown;
1977
2825
  }
1978
2826
  declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
1979
2827
  declare const VERSION = "0.0.0";
1980
- export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, type DataModelInit, type DefineComponentOptions, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, 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 InferArgs, type InferEnv, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraErrorCode, 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, asBucketStorage, bindOrm, bindTableFacade, buildRlsReadRegistry, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
2828
+ 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 DurableStreamOptions, 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 ReactorHandler, type ReactorOutcome, type ReactorSelect, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredReactor, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type ShardInitEvent, type ShardInitHandler, 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, onQueryChange, onShardInit, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules, toWhereInput };