@lunora/server 1.0.0-alpha.5 → 1.0.0-alpha.51

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 (70) hide show
  1. package/LICENSE.md +219 -0
  2. package/README.md +50 -0
  3. package/dist/data-model.d.mts +231 -156
  4. package/dist/data-model.d.ts +231 -156
  5. package/dist/data-model.mjs +0 -1
  6. package/dist/drizzle.mjs +1 -1
  7. package/dist/index.d.mts +1405 -840
  8. package/dist/index.d.ts +1405 -840
  9. package/dist/index.mjs +1 -25
  10. package/dist/otel.d.mts +544 -0
  11. package/dist/otel.d.ts +544 -0
  12. package/dist/otel.mjs +1 -0
  13. package/dist/packem_shared/DEFAULT_LIMIT-yHJ5O96W.mjs +1 -0
  14. package/dist/packem_shared/LunoraEnvError-CgpI2Mm_.mjs +3 -0
  15. package/dist/packem_shared/LunoraError-LVhdU0Lo.mjs +1 -0
  16. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-DA0unh6n.mjs +1 -0
  17. package/dist/packem_shared/allowAll-BnyNbJZT.mjs +1 -0
  18. package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +1 -0
  19. package/dist/packem_shared/bindOrm-Bp9hsM2q.mjs +1 -0
  20. package/dist/packem_shared/buildRlsReadRegistry-WdiqSj87.mjs +1 -0
  21. package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +1 -0
  22. package/dist/packem_shared/context-identity-GunnA6La.mjs +1 -0
  23. package/dist/packem_shared/createPolicyDsl-sV1swpkD.mjs +1 -0
  24. package/dist/packem_shared/createSecrets-CgVPiW2C.mjs +1 -0
  25. package/dist/packem_shared/defineAggregateIndex-B-401CrP.mjs +1 -0
  26. package/dist/packem_shared/defineIdentity-B7gfAgxx.mjs +1 -0
  27. package/dist/packem_shared/defineMigration-Bfpwxv2f.mjs +1 -0
  28. package/dist/packem_shared/defineMutator-BgpQ-xUo.mjs +1 -0
  29. package/dist/packem_shared/defineShape-Ds8uNqzX.mjs +1 -0
  30. package/dist/packem_shared/defineStorageRule-BDu01PUn.mjs +1 -0
  31. package/dist/packem_shared/functions-CDC08CWY.mjs +1 -0
  32. package/dist/packem_shared/httpAction-C14NuF3V.mjs +4 -0
  33. package/dist/packem_shared/initLunora-CHh1EmR4.mjs +1 -0
  34. package/dist/packem_shared/mask-C6Bi78qj.mjs +1 -0
  35. package/dist/packem_shared/onConnect-CEtRmUpJ.mjs +1 -0
  36. package/dist/packem_shared/policy-tag-Dprt9JWo.mjs +1 -0
  37. package/dist/packem_shared/protectPublic-BhKewPqm.mjs +1 -0
  38. package/dist/packem_shared/rls-_iVsPvhX.mjs +1 -0
  39. package/dist/packem_shared/run-middleware-BeEEqmdE.mjs +1 -0
  40. package/dist/packem_shared/storageRules-BptPZbi8.mjs +1 -0
  41. package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
  42. package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
  43. package/dist/rls/testing.d.mts +31 -31
  44. package/dist/rls/testing.d.ts +31 -31
  45. package/dist/rls/testing.mjs +1 -49
  46. package/dist/types.d.mts +1268 -463
  47. package/dist/types.d.ts +1268 -463
  48. package/dist/types.mjs +1 -31
  49. package/package.json +18 -4
  50. package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +0 -187
  51. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  52. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BgBQsqQ-.mjs +0 -114
  53. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  54. package/dist/packem_shared/bindOrm-Ce57S3N9.mjs +0 -128
  55. package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +0 -100
  56. package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +0 -29
  57. package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +0 -55
  58. package/dist/packem_shared/defineAggregateIndex-C2gT1GzM.mjs +0 -252
  59. package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
  60. package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
  61. package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
  62. package/dist/packem_shared/initLunora-CATvPsVt.mjs +0 -86
  63. package/dist/packem_shared/mask-eCUYOwhd.mjs +0 -211
  64. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  65. package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
  66. package/dist/packem_shared/rls-Bi9HiyDC.mjs +0 -567
  67. package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
  68. package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
  69. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  70. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/dist/index.d.mts CHANGED
@@ -1,37 +1,40 @@
1
- import { Validator, Infer, 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, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.mjs";
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.mjs";
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, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.mjs";
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 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 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 WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.mjs";
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-DmvyEMD6.mjs";
7
- export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-DmvyEMD6.mjs";
8
+ import { WhereOperators, QueryArgs as QueryArgs$2 } from "./data-model.mjs";
9
+ 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-C4CMJK8x.mjs";
10
+ export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-C4CMJK8x.mjs";
11
+ import { SearchLanguage, SearchStrategy } from '@lunora/search-core';
8
12
  export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
9
- import "./data-model.mjs";
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
- */
13
+ /**
14
+ * Make any `config.storage` result bucket-aware so `ctx.storage.bucket(name)`
15
+ * always resolves. A `createBucketStorage(...)` result already carries
16
+ * `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
17
+ * (or the no-storage stub) is tagged as the `"default"` bucket, where
18
+ * `.bucket(name)` is the identity single-bucket apps address one binding under
19
+ * every name.
20
+ *
21
+ * This is the runtime counterpart the generated `_generated/shard.ts` imports to
22
+ * wrap `ctx.storage`; it lives here (the single source) rather than being stamped
23
+ * inline into every generated file, so the bucket-tagging behaviour has one home
24
+ * alongside the storage ctx types. The input is genuinely heterogeneous (a thunk
25
+ * result cast through `unknown`), so the signature is `unknown unknown`; the
26
+ * generated caller casts the result to its storage type.
27
+ */
25
28
  declare const asBucketStorage: (raw: unknown) => unknown;
26
29
  /** Builder discriminator. Codegen reads this kind. */
27
30
  type TerminalKind = FunctionKind;
28
31
  /** Initial (empty) accumulated args for a fresh builder. */
29
32
  type EmptyArgs = Record<never, never>;
30
33
  /**
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
- */
34
+ * `next()` advances the middleware chain. Called with no argument it forwards
35
+ * the current context unchanged; called with `{ ctx }` it shallow-merges the
36
+ * extension, and the result type reflects the widened context.
37
+ */
35
38
  interface MiddlewareNext<ContextIn> {
36
39
  (): Promise<ContextIn>;
37
40
  <Extension extends Record<string, unknown>>(options: {
@@ -39,10 +42,10 @@ interface MiddlewareNext<ContextIn> {
39
42
  }): Promise<ContextIn & Extension>;
40
43
  }
41
44
  /**
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
- */
45
+ * A middleware receives the current context and a `next` continuation. Its
46
+ * return type becomes the builder's new context, so `return next({ ctx })`
47
+ * propagates the extension into every downstream `.use()` and the handler.
48
+ */
46
49
  type Middleware<ContextIn, ContextOut> = (options: {
47
50
  ctx: ContextIn;
48
51
  next: MiddlewareNext<ContextIn>;
@@ -50,16 +53,23 @@ type Middleware<ContextIn, ContextOut> = (options: {
50
53
  /** Options accepted by `initLunora.dataModel&lt;DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
51
54
  type CreateOptions = Record<never, never>;
52
55
  /**
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
- */
56
+ * `Output` carries the type declared by `.output(validator)`. It defaults to
57
+ * the `undefined` sentinel meaning "not declared": in that state the terminal
58
+ * stays generic over the handler's own return type. Once `.output()` sets it to
59
+ * a concrete type, the terminal requires the handler to return that type and
60
+ * the registration is typed to it (the runtime parses the result through the
61
+ * validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
62
+ * `Output` doesn't distribute and so the test is for the exact sentinel.
63
+ */
61
64
  interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
62
65
  readonly __lunoraProcedure: "query";
66
+ /**
67
+ * Publish this query on the opt-in public REST surface (plan 167) — the
68
+ * runtime mints `GET /_lunora/rest/&lt;namespace>/&lt;fn>` (and `POST`), dispatching
69
+ * THROUGH the procedure so `ctx.auth` / RLS / validators are enforced, and the
70
+ * generated OpenAPI describes it. Default-closed: omit to keep it RPC-only.
71
+ */
72
+ expose: (config: ExposeConfig) => QueryBuilder<Context, Args, Output>;
63
73
  input: <A extends ArgsValidator>(validators: A) => QueryBuilder<Context, A & Args, Output>;
64
74
  output: <V extends Validator>(validator: V) => QueryBuilder<Context, Args, Infer<V>>;
65
75
  query: [Output] extends [undefined] ? <R>(handler: (options: {
@@ -70,22 +80,36 @@ interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined>
70
80
  ctx: Context;
71
81
  }) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
72
82
  /**
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
- */
83
+ * Terminal: declare this procedure as a streaming query. The handler is an
84
+ * async generator (or any function returning an `AsyncIterable&lt;R>`) that
85
+ * yields one chunk per server-pushed frame. The third `signal` argument is
86
+ * tripped when the client cancels — break out of the loop or check
87
+ * `signal.aborted` between yields. `.output()` does not apply: per-chunk
88
+ * validation is opt-in via the handler itself.
89
+ */
80
90
  stream: <R>(handler: (options: {
81
91
  args: InferArgs<Args>;
82
92
  ctx: Context;
83
93
  signal: AbortSignal;
84
94
  }) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
85
95
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => QueryBuilder<ContextOut, Args, Output>;
96
+ /**
97
+ * Mark this query as paid. The origin worker answers an unpaid client RPC
98
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
99
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
100
+ * recipient, and facilitator come from the worker-level x402 charge config.
101
+ */
102
+ x402: (config: X402ProcedureConfig) => QueryBuilder<Context, Args, Output>;
86
103
  }
87
104
  interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
88
105
  readonly __lunoraProcedure: "mutation";
106
+ /**
107
+ * Publish this mutation on the opt-in public REST surface (plan 167) — the
108
+ * runtime mints `POST /_lunora/rest/&lt;namespace>/&lt;fn>`, dispatching THROUGH the
109
+ * procedure so `ctx.auth` / RLS / validators are enforced, and the generated
110
+ * OpenAPI describes it. Default-closed: omit to keep it RPC-only.
111
+ */
112
+ expose: (config: ExposeConfig) => MutationBuilder<Context, Args, Output>;
89
113
  input: <A extends ArgsValidator>(validators: A) => MutationBuilder<Context, A & Args, Output>;
90
114
  mutation: [Output] extends [undefined] ? <R>(handler: (options: {
91
115
  args: InferArgs<Args>;
@@ -96,6 +120,13 @@ interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefine
96
120
  }) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
97
121
  output: <V extends Validator>(validator: V) => MutationBuilder<Context, Args, Infer<V>>;
98
122
  use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => MutationBuilder<ContextOut, Args, Output>;
123
+ /**
124
+ * Mark this mutation as paid. The origin worker answers an unpaid client RPC
125
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
126
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
127
+ * recipient, and facilitator come from the worker-level x402 charge config.
128
+ */
129
+ x402: (config: X402ProcedureConfig) => MutationBuilder<Context, Args, Output>;
99
130
  }
100
131
  interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
101
132
  readonly __lunoraProcedure: "action";
@@ -106,16 +137,30 @@ interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined>
106
137
  args: InferArgs<Args>;
107
138
  ctx: Context;
108
139
  }) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
140
+ /**
141
+ * Publish this action on the opt-in public REST surface (plan 167) — the
142
+ * runtime mints `POST /_lunora/rest/&lt;namespace>/&lt;fn>`, dispatching THROUGH the
143
+ * procedure so `ctx.auth` / RLS / validators are enforced, and the generated
144
+ * OpenAPI describes it. Default-closed: omit to keep it RPC-only.
145
+ */
146
+ expose: (config: ExposeConfig) => ActionBuilder<Context, Args, Output>;
109
147
  input: <A extends ArgsValidator>(validators: A) => ActionBuilder<Context, A & Args, Output>;
110
148
  output: <V extends Validator>(validator: V) => ActionBuilder<Context, Args, Infer<V>>;
111
149
  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
- */
150
+ /**
151
+ * Mark this action as paid. The origin worker answers an unpaid client RPC
152
+ * with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
153
+ * is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
154
+ * recipient, and facilitator come from the worker-level x402 charge config.
155
+ */
156
+ x402: (config: X402ProcedureConfig) => ActionBuilder<Context, Args, Output>;
157
+ }
158
+ /**
159
+ * Internal builder variants. Identical to their public counterparts but carry
160
+ * the `__lunoraVisibility: "internal"` brand codegen keys off to route the
161
+ * registration into the `internal` object (and keep it off `api`). `input`/`use`
162
+ * return the internal builder type so the brand survives the whole chain.
163
+ */
119
164
  interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
120
165
  readonly __lunoraProcedure: "query";
121
166
  readonly __lunoraVisibility: "internal";
@@ -179,38 +224,38 @@ interface DataModelInit<DataModel> {
179
224
  create: (options?: CreateOptions) => LunoraBuilders;
180
225
  }
181
226
  /**
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
- */
227
+ * Entry point for the procedure builder. `dataModel&lt;DM>()` binds the generated
228
+ * `DataModel` (phantom for now), and `.create()` yields the public root builders
229
+ * plus their `internal*` counterparts.
230
+ */
186
231
  declare const initLunora: {
187
232
  dataModel: <DataModel>() => DataModelInit<DataModel>;
188
233
  };
189
234
  /**
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
- */
235
+ * Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
236
+ * `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
237
+ * absent or non-Secrets-Store binding throws a directed error pointing at the
238
+ * wrangler config; the lookup is lazy, so an unused secret never resolves.
239
+ */
195
240
  declare const createSecrets: (env: Record<string, unknown>) => Secrets;
196
241
  /**
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
- */
242
+ * Redact secrets from a free-form message. Masks, in order: any quoted value
243
+ * whose contents look like a credential (so a value surfaced as `received string
244
+ * "sk_live_…"` is masked even though the surrounding text is not a token); a
245
+ * `scheme://user:password@host` URL credential (the password segment); any
246
+ * known-prefix credential token wherever it appears, at any length; any value
247
+ * following a secret-named key in `KEY=value` / `KEY: value` form; and any
248
+ * remaining bare high-entropy ≥24-char token run anywhere in the message.
249
+ *
250
+ * This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
251
+ * secret under a non-secret-named key (and embedded credentials in shapes not
252
+ * enumerated here) can still slip through. Treat it as a backstop — prefer
253
+ * structured logging that never serializes raw env/secret fields in the first
254
+ * place over relying on post-hoc scrubbing of untrusted data.
255
+ *
256
+ * Exported because it is independently useful — call it before logging anything
257
+ * derived from `env`, request bodies, or thrown errors.
258
+ */
214
259
  declare const redactSecrets: (message: string) => string;
215
260
  /** One key's validation failure, secrets already redacted out of `message`. */
216
261
  interface EnvKeyFailure {
@@ -220,32 +265,31 @@ interface EnvKeyFailure {
220
265
  message: string;
221
266
  }
222
267
  /**
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";
268
+ * Thrown when one or more env keys are missing or fail validation. Carries the
269
+ * structured list of `failures` (each with the offending `key`) so callers can
270
+ * react programmatically; `message` is the joined, secret-redacted summary.
271
+ *
272
+ * Named export only (no default) per the repo export convention.
273
+ */
274
+ declare class LunoraEnvError extends LunoraError$1 {
231
275
  readonly failures: ReadonlyArray<EnvKeyFailure>;
232
276
  constructor(failures: ReadonlyArray<EnvKeyFailure>);
233
277
  }
234
278
  /** A record of `v.*` validators describing the expected env shape. */
235
279
  type EnvShape = Record<string, Validator>;
236
280
  /**
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
- */
281
+ * The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
282
+ * become optional keys; everything else is required. Mirrors how `InferArgs`
283
+ * derives an args object from a validator map.
284
+ */
285
+ 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]>; };
286
+ /**
287
+ * The accessor returned by {@link defineEnv}. A typed view over an `env` object
288
+ * plus a `.parse(env)` escape hatch that validates every key eagerly.
289
+ *
290
+ * Call the accessor with the worker's `env` to get the typed, lazily-validated
291
+ * proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
292
+ */
249
293
  interface EnvAccessor<S extends EnvShape> {
250
294
  /** Validate every key eagerly and return the typed, plain (non-proxy) object. Use for fail-fast-at-boot. */
251
295
  parse: (env: unknown) => InferEnv<S>;
@@ -253,91 +297,40 @@ interface EnvAccessor<S extends EnvShape> {
253
297
  (env: unknown): InferEnv<S>;
254
298
  }
255
299
  /**
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
- */
300
+ * Define a typed, validated accessor over a Worker's `env`. Pass a record of
301
+ * `v.*` validators; receive an accessor that validates lazily per key (cached
302
+ * per `env` identity) and infers its output type from the validators.
303
+ *
304
+ * ```ts
305
+ * import { defineEnv, v } from "@lunora/server";
306
+ *
307
+ * const config = defineEnv({
308
+ * STRIPE_KEY: v.string(),
309
+ * PORT: v.optional(v.number()),
310
+ * });
311
+ *
312
+ * export default {
313
+ * fetch(request, env) {
314
+ * const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
315
+ * // …
316
+ * },
317
+ * };
318
+ * ```
319
+ *
320
+ * Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
321
+ * invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
322
+ */
279
323
  declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
324
+ declare class LunoraError extends LunoraError$1 {
325
+ constructor(code: LunoraErrorCode, message?: string, data?: unknown);
326
+ }
280
327
  /**
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
- */
328
+ * Minimal structural writer the facade binds over. Declared with **method**
329
+ * syntax (not arrow properties) so a more-specifically-typed writer — both
330
+ * `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer
331
+ * stays assignable under bivariant parameter checking. That is the whole reason
332
+ * the shared helper can serve both callers, hence the rule exemption.
333
+ */
341
334
  interface FacadeWriterLike {
342
335
  aggregate(tableName: string, options: unknown): Promise<unknown>;
343
336
  count(tableName: string, where?: unknown): Promise<number>;
@@ -349,6 +342,11 @@ interface FacadeWriterLike {
349
342
  }, expectedTable?: string): Promise<{
350
343
  deleted: number;
351
344
  }>;
345
+ deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
346
+ limit?: number;
347
+ }): Promise<{
348
+ deleted: number;
349
+ }>;
352
350
  findFirst(tableName: string, args?: unknown): Promise<unknown>;
353
351
  findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
354
352
  findMany(tableName: string, args?: unknown): Promise<unknown>;
@@ -357,15 +355,27 @@ interface FacadeWriterLike {
357
355
  insert(tableName: string, document: Record<string, unknown>): Promise<string>;
358
356
  insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
359
357
  limit?: number;
360
- }): Promise<string[]>;
358
+ skipDuplicates?: boolean;
359
+ }): Promise<(string | null)[]>;
361
360
  patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
362
361
  patchMany?(patches: ReadonlyArray<{
363
362
  id: string;
364
363
  patch: Record<string, unknown>;
365
364
  }>, options?: {
366
365
  limit?: number;
367
- }, expectedTable?: string): Promise<void>;
366
+ }, expectedTable?: string): Promise<{
367
+ patched: number;
368
+ }>;
369
+ patchWhere?(tableName: string, args: {
370
+ patch: Record<string, unknown>;
371
+ where: Record<string, unknown>;
372
+ }, options?: {
373
+ limit?: number;
374
+ }): Promise<{
375
+ patched: number;
376
+ }>;
368
377
  query(tableName: string): {
378
+ withGeoIndex(indexName: string, build: (q: unknown) => unknown): unknown;
369
379
  withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
370
380
  };
371
381
  rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
@@ -378,11 +388,19 @@ interface FacadeEntry {
378
388
  aggregate: (options: unknown) => Promise<unknown>;
379
389
  count: (where?: unknown) => Promise<number>;
380
390
  delete: (id: string) => Promise<void>;
381
- deleteMany: (ids: ReadonlyArray<string>, options?: {
382
- limit?: number;
383
- }) => Promise<{
384
- deleted: number;
385
- }>;
391
+ deleteMany: {
392
+ (ids: ReadonlyArray<string>, options?: {
393
+ limit?: number;
394
+ }): Promise<{
395
+ deleted: number;
396
+ }>;
397
+ (args: {
398
+ limit?: number;
399
+ where: Record<string, unknown>;
400
+ }): Promise<{
401
+ deleted: number;
402
+ }>;
403
+ };
386
404
  /** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
387
405
  exists: (where?: unknown) => Promise<boolean>;
388
406
  findFirst: (args?: unknown) => Promise<unknown>;
@@ -393,16 +411,34 @@ interface FacadeEntry {
393
411
  /** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
394
412
  hardDelete: (id: string) => Promise<void>;
395
413
  insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
414
+ /**
415
+ * Insert many documents into this table in one call. With
416
+ * `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
417
+ * instead of failing the batch. The typed facade narrows the return to
418
+ * `Id&lt;T>[]` when skipDuplicates is not requested.
419
+ */
396
420
  insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
397
421
  limit?: number;
398
- }) => Promise<string[]>;
422
+ skipDuplicates?: boolean;
423
+ }) => Promise<(string | null)[]>;
399
424
  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>;
425
+ patchMany: {
426
+ (patches: ReadonlyArray<{
427
+ id: string;
428
+ values: Record<string, unknown>;
429
+ }>, options?: {
430
+ limit?: number;
431
+ }): Promise<{
432
+ patched: number;
433
+ }>;
434
+ (args: {
435
+ limit?: number;
436
+ values: Record<string, unknown>;
437
+ where: Record<string, unknown>;
438
+ }): Promise<{
439
+ patched: number;
440
+ }>;
441
+ };
406
442
  rank: (indexName: string, options: unknown) => Promise<unknown>;
407
443
  rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
408
444
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
@@ -412,15 +448,16 @@ interface FacadeEntry {
412
448
  upsert: (args: UpsertArgs) => Promise<UpsertResult>;
413
449
  /** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
414
450
  upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
451
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => unknown;
415
452
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
416
453
  }
417
454
  /** Options accepted by the per-table `insert` accessor. */
418
455
  interface FacadeInsertOptions {
419
456
  /**
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
- */
457
+ * When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
458
+ * silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
459
+ * other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
460
+ */
424
461
  skipDuplicates?: boolean;
425
462
  }
426
463
  /** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
@@ -448,17 +485,17 @@ interface UpsertManyArgs {
448
485
  target: UpsertTarget;
449
486
  }
450
487
  /**
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
- */
488
+ * Bind a structural writer to one table, producing its `ctx.db` table accessor.
489
+ *
490
+ * The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
491
+ * `tableName` as `expectedTable` so the underlying writer scopes its id lookup
492
+ * to this table. Without it, a branded `Id&lt;"posts">` carrying another table's
493
+ * id would resolve cross-table (the writer probes every table by id), letting
494
+ * `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
495
+ * mutate — a row in an unrelated table (IDOR). Writers that ignore the second
496
+ * argument keep their previous global behaviour; the scoping is opt-in via this
497
+ * forwarded name.
498
+ */
462
499
  declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) => FacadeEntry;
463
500
  /** The kitcn-style `ctx.orm` namespace over a per-table facade map. */
464
501
  interface OrmLike {
@@ -479,21 +516,21 @@ declare const bindOrm: (facade: Record<string, FacadeEntry>) => OrmLike;
479
516
  /** HTTP verbs the typed {@link httpRoute} builder can bind to. */
480
517
  type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
481
518
  /**
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">;
519
+ * Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
520
+ * HTTP actions run in the worker (the "action runtime"), separate from the
521
+ * transactional store, so there is no direct `db` / `vectors` / `scheduler` /
522
+ * `storage` surface — reach the data layer through `runQuery` / `runMutation` /
523
+ * `runAction`, which forward to the owning shard.
524
+ */
525
+ type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery">;
489
526
  /** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
490
527
  type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
491
528
  /**
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
- */
529
+ * The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
530
+ * The runtime injects the per-request {@link HttpActionCtx} on the private
531
+ * `__lunoraCtx` binding; the router's lifting middleware promotes it to
532
+ * `c.var.lunora` so handlers can read it as a typed variable.
533
+ */
497
534
  interface LunoraHttpEnv {
498
535
  Bindings: Record<string, unknown> & {
499
536
  __lunoraCtx?: HttpActionCtx;
@@ -507,31 +544,31 @@ type LunoraHttpApp = Hono<LunoraHttpEnv>;
507
544
  /** A compiled route handler: a hono handler that resolves to a raw {@link Response}. */
508
545
  type LunoraRouteHandler = (c: Context<LunoraHttpEnv>) => Promise<Response>;
509
546
  /**
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
- */
547
+ * Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
548
+ * hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
549
+ * runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
550
+ * {@link httpRouter}; `request` is the underlying `c.req.raw`.
551
+ */
515
552
  declare const httpAction: (handler: HttpActionHandler) => LunoraRouteHandler;
516
553
  /**
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
- */
554
+ * Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
555
+ * the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
556
+ * {@link httpAction} and the typed {@link httpRoute} builder can read the action
557
+ * context. The full hono surface is available — plugins, path params, `.route`:
558
+ *
559
+ * ```ts
560
+ * const app = httpRouter();
561
+ * app.use("*", cors());
562
+ * app.post("/webhook", httpAction(onWebhook));
563
+ * app.get("/users/:id", getUser);
564
+ * export default createWorker({ httpRouter: app, ... });
565
+ * ```
566
+ *
567
+ * The lifting middleware throws if the context is absent. `createWorker` injects
568
+ * it on every request the router sees, so this only trips when the app is run
569
+ * outside the runtime — a misconfiguration we surface loudly rather than let
570
+ * `c.var.lunora` be silently `undefined` despite its non-optional type.
571
+ */
535
572
  declare const httpRouter: () => LunoraHttpApp;
536
573
  /** The `{ ctx, searchParams, body, params }` a typed route handler receives. */
537
574
  interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator> {
@@ -541,11 +578,12 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
541
578
  searchParams: InferArgs<SearchParams>;
542
579
  }
543
580
  /**
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
- */
581
+ * The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
582
+ * handler receives. There is no parsed `body` — streams are typically GET, and
583
+ * the raw `request` is exposed if a handler needs to read the body itself.
584
+ * `signal` is tripped when the client disconnects.
585
+ * @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
586
+ */
549
587
  interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
550
588
  ctx: HttpActionCtx;
551
589
  params: InferArgs<Params>;
@@ -554,35 +592,51 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
554
592
  signal: AbortSignal;
555
593
  }
556
594
  /**
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
- */
595
+ * A typed REST route under construction. `.searchParams()` / `.body()` /
596
+ * `.params()` accumulate validator maps (later calls merge, a colliding key
597
+ * wins) that decode the URL query, JSON body, and hono path params into the
598
+ * handler's typed `searchParams` / `body` / `params`. Like the procedure
599
+ * builder, `.output(validator)` defaults to the `undefined` sentinel — while
600
+ * unset the handler is generic over its own return; once set the handler must
601
+ * return that type and the result is parsed through the validator before
602
+ * serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
603
+ * `Output` doesn't distribute and the test is for the exact sentinel.
604
+ *
605
+ * The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
606
+ * directly with `app.get(path, route)`.
607
+ */
570
608
  interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
571
609
  body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
610
+ /**
611
+ * Attach a `Cache-Control` header to the response. Only meaningful when
612
+ * Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
613
+ */
614
+ cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
615
+ /**
616
+ * Attach a `Cache-Tag` header to the response for tag-based purging via
617
+ * `ctx.cache.purge({ tags: [...] })`.
618
+ */
619
+ cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
572
620
  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
621
  output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
574
622
  params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
575
623
  searchParams: <S extends ArgsValidator>(validators: S) => HttpRouteBuilder<S & SearchParams, Body, Params, Output>;
576
624
  /**
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
- */
625
+ * Terminal: declare this route as a streaming Server-Sent Events endpoint.
626
+ * The handler is an async generator (or any function returning an
627
+ * `AsyncIterable&lt;R>`) that yields one chunk per SSE `data:` frame; on
628
+ * iterator completion the route writes a final `event: complete` frame; on
629
+ * throw, an `event: error` frame is written with `{code, message}` before
630
+ * the stream closes. The chunks are JSON-encoded; `R` is inferred from the
631
+ * handler's yielded type.
632
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
633
+ */
585
634
  stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
635
+ /**
636
+ * Attach a `Vary` header to the response so Cloudflare stores separate
637
+ * cached variants per distinct value of the listed request headers.
638
+ */
639
+ vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
586
640
  }
587
641
  /** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
588
642
  type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
@@ -597,33 +651,33 @@ interface HttpRoute {
597
651
  put: HttpRouteFactory;
598
652
  }
599
653
  /**
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
- */
654
+ * Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
655
+ * typed route and a hand-written {@link httpAction} are interchangeable when
656
+ * mounted on {@link httpRouter}:
657
+ *
658
+ * ```ts
659
+ * export const listTodos = httpRoute
660
+ * .get("/api/todos")
661
+ * .searchParams({ limit: v.number(), q: v.optional(v.string()) })
662
+ * .output(v.array(v.object({ id: v.string(), text: v.string() })))
663
+ * .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
664
+ *
665
+ * export const getTodo = httpRoute
666
+ * .get("/api/todos/:id")
667
+ * .params({ id: v.string() })
668
+ * .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
669
+ *
670
+ * const app = httpRouter();
671
+ * app.get("/api/todos", listTodos);
672
+ * app.get("/api/todos/:id", getTodo);
673
+ * ```
674
+ */
621
675
  declare const httpRoute: HttpRoute;
622
676
  /**
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
- */
677
+ * Structural view of an R2 object body, as returned by `@lunora/storage`'s
678
+ * `download()`. Re-declared here (not imported) so `@lunora/server` takes no
679
+ * runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
680
+ */
627
681
  interface StorageObjectBody {
628
682
  /** The object body stream (`null` for a zero-byte object). */
629
683
  body: ReadableStream | null;
@@ -654,36 +708,200 @@ interface ContextWithStorage {
654
708
  storage: StorageDownloader;
655
709
  }
656
710
  /**
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
- */
711
+ * True when `value` is safe to use as an HTTP header field-value: no CR, LF, or
712
+ * NUL. Guards against response-header injection / `Headers`-construction throws
713
+ * when reflecting attacker-influenced object metadata (e.g. a stored
714
+ * `Content-Type`). Exported (see the `export {}` at the file end) so an `httpAction`
715
+ * handler can guard a request-derived header value before writing it the fix the
716
+ * `http_action_response_header_injection` advisor lint points to.
717
+ */
718
+ declare const isSafeHeaderValue: (value: string) => boolean;
719
+ /**
720
+ * Stream a stored object as an HTTP {@link Response} from an `httpAction`
721
+ * handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
722
+ * Honors a single-range `Range` request **206 Partial Content** with
723
+ * `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
724
+ * **404**; an out-of-bounds range is a **416** with a `Content-Range` of
725
+ * `bytes` star-slash-size.
726
+ *
727
+ * A range request re-issues the `download()` with the resolved `{ offset, length }`
728
+ * window so R2 streams only those bytes back to the Worker — the slice is never
729
+ * buffered in the isolate. The first `download()` is used only for the object's
730
+ * size + metadata (its body is left unread and cancelled). For very large
731
+ * objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since the
732
+ * client then ranges against R2/CDN directly with no Worker hop.
733
+ */
671
734
  declare const serveStorageObject: (context: ContextWithStorage, key: string, request: Request) => Promise<Response>;
735
+ /**
736
+ * What the worker does with a resolver's identity when it fails contract
737
+ * validation (a forged / malformed claim set arriving from an untrusted token).
738
+ * `"anonymous"` (default, safe) treats the request as anonymous, so the bad
739
+ * identity never reaches a policy as a valid identity (`ctx.auth.userId`
740
+ * becomes `undefined`). `"reject"` fails the request closed (a `401`) — use
741
+ * when a malformed credential should be a hard error, not a silent downgrade.
742
+ */
743
+ type IdentityRejectMode = "anonymous" | "reject";
744
+ /** Options for {@link defineIdentity}. */
745
+ interface DefineIdentityOptions {
746
+ /**
747
+ * How to handle a resolver identity that violates the contract at the trust
748
+ * boundary. Defaults to `"anonymous"` (a forged claim set is downgraded to
749
+ * anonymous rather than flowing in as an unchecked cast).
750
+ */
751
+ readonly onInvalid?: IdentityRejectMode;
752
+ }
753
+ /** Result of validating a candidate identity against the contract. */
754
+ type IdentityValidation = {
755
+ ok: true;
756
+ } | {
757
+ error: string;
758
+ ok: false;
759
+ };
760
+ /**
761
+ * A declared identity claim contract. Carries the codegen discovery brand, the
762
+ * declared claim validators, the reject policy, and a runtime `validate`. The
763
+ * `TClaims` type parameter is the inferred claim shape (always extending
764
+ * `{ userId: string }`); it is phantom (no runtime field) and exists so
765
+ * `@lunora/codegen` and {@link InferIdentity} can recover the type.
766
+ */
767
+ interface IdentityContract<TClaims extends {
768
+ userId: string;
769
+ } = {
770
+ userId: string;
771
+ }> {
772
+ /**
773
+ * Phantom carrier for the inferred claim type. Never populated at runtime
774
+ * (`undefined`); present only so the type flows to codegen / {@link InferIdentity}.
775
+ */
776
+ readonly __claimType?: TClaims;
777
+ readonly __lunoraIdentity: true;
778
+ /** The declared claim validators (a `@lunora/values` validator map). */
779
+ readonly claims: ValidatorMap;
780
+ /** Reject policy applied at the trust boundary. See {@link IdentityRejectMode}. */
781
+ readonly onInvalid: IdentityRejectMode;
782
+ /**
783
+ * Validate a resolver's returned identity against the declared claims. On
784
+ * success the caller keeps the original identity untouched (so undeclared
785
+ * claims are forwarded verbatim, preserving today's behaviour); on failure
786
+ * the worker applies the `onInvalid` policy.
787
+ */
788
+ validate: (identity: Record<string, unknown>) => IdentityValidation;
789
+ }
790
+ /** Recover the declared claim type from a {@link defineIdentity} contract. */
791
+ type InferIdentity<T> = T extends IdentityContract<infer TClaims> ? TClaims : never;
792
+ /**
793
+ * Declare the identity claim contract. `claims` is a `@lunora/values` validator
794
+ * map whose inferred type must extend `{ userId: string }` — if it does not
795
+ * (e.g. `userId` is missing or not a required string), the argument type
796
+ * collapses to `never` and the call fails to typecheck.
797
+ * @example
798
+ * export const identity = defineIdentity({ userId: v.string(), tenantId: v.optional(v.string()), scopes: v.optional(v.array(v.string())) });
799
+ */
800
+ declare const defineIdentity: <A extends ValidatorMap>(claims: InferValidatorMap<A> extends {
801
+ userId: string;
802
+ } ? A : never, options?: DefineIdentityOptions) => IdentityContract<InferValidatorMap<A> & {
803
+ userId: string;
804
+ }>;
672
805
  /** Handler for a connection-lifecycle hook. */
673
806
  type LifecycleHandler = (context: MutationCtx, event: LifecycleEvent) => Promise<void> | void;
674
807
  /** Register a hook that fires once when a client's WebSocket connects. */
675
808
  declare const onConnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
676
809
  /** Register a hook that fires once when a client's WebSocket disconnects. */
677
810
  declare const onDisconnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
678
- /**
679
- * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
680
- * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one same 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
- */
811
+ /** Default `limit` when the caller doesn't ask for one. */
812
+ declare const DEFAULT_LIMIT = 25;
813
+ /** Default ceiling on `limit`, so one request can't ask for an unbounded page. */
814
+ declare const DEFAULT_MAX_LIMIT = 100;
815
+ /**
816
+ * Per-field predicate accepted for a declared filter column. An alias of the
817
+ * `ctx.db` `where` DSL's own operator type rather than a copy, so the two cannot
818
+ * drift as operators are added.
819
+ */
820
+ type ListFilterOperators<T> = WhereOperators<T>;
821
+ /**
822
+ * The filter allow-list a caller may declare: a subset of the document's own
823
+ * columns, each with a validator for that column's type. Constraining the KEYS to
824
+ * `keyof Doc` is what turns a typo'd or renamed column into a compile error
825
+ * instead of a predicate that silently never matches.
826
+ */
827
+ type ListFilterShape<TDocument> = { [K in keyof TDocument & string]?: Validator<TDocument[K]>; };
828
+ /** The `where` argument: each declared filter column, optionally, as a bare value or an operator object. */
829
+ type ListWhere<F> = { [K in keyof F]?: Infer<NonNullable<F[K]>> | ListFilterOperators<Infer<NonNullable<F[K]>>>; };
830
+ /** One `orderBy` entry. `direction` defaults to `"asc"`. */
831
+ interface ListOrderByEntry<O extends string> {
832
+ direction?: "asc" | "desc";
833
+ field: O;
834
+ }
835
+ /** The decoded arguments a {@link defineListArgs} endpoint receives. */
836
+ interface ListArgsValue<F, O extends string> {
837
+ cursor?: null | number | string;
838
+ limit?: number;
839
+ orderBy?: ListOrderByEntry<O>[];
840
+ where?: ListWhere<F>;
841
+ }
842
+ interface DefineListArgsConfig<F, O extends string> {
843
+ /** `limit` applied when the caller omits one. Defaults to 25. */
844
+ readonly defaultLimit?: number;
845
+ /**
846
+ * Allow-list of filterable columns. Publish only columns an index can serve;
847
+ * anything absent here is unreachable from the client.
848
+ */
849
+ readonly filter: F;
850
+ /** Ceiling on `in` / `notIn` array length — one bound parameter each. Defaults to 100. */
851
+ readonly maxInValues?: number;
852
+ /** Ceiling on `limit`; a larger request is clamped down, not rejected. Defaults to 100. */
853
+ readonly maxLimit?: number;
854
+ /** Ceiling on how many `orderBy` entries a request may ask for. Defaults to 8. */
855
+ readonly maxOrderBy?: number;
856
+ /** Allow-list of sortable columns. Pass `[]` to fix the order server-side. */
857
+ readonly orderBy: ReadonlyArray<O>;
858
+ }
859
+ /** The validator map handed to `.input()`. Typed precisely so `args` infers end-to-end. */
860
+ interface ListArgsValidators<F, O extends string> {
861
+ cursor: ColumnValidator<null | number | string | undefined, null | number | string | undefined>;
862
+ limit: ColumnValidator<number | undefined, number | undefined>;
863
+ orderBy: ColumnValidator<ListOrderByEntry<O>[] | undefined, ListOrderByEntry<O>[] | undefined>;
864
+ where: ColumnValidator<ListWhere<F> | undefined, ListWhere<F> | undefined>;
865
+ }
866
+ interface ListArgsSpec<TDocument, F, O extends string> {
867
+ /** Spread into `.input(...)` — `{ cursor, limit, orderBy, where }`. */
868
+ readonly args: ListArgsValidators<F, O>;
869
+ /**
870
+ * Translate the decoded arguments into the `findMany` options object:
871
+ * `limit` clamped into `[1, maxLimit]`, `orderBy` reshaped from
872
+ * `{ field, direction }[]` into `ctx.db`'s `{ column: direction }[]`.
873
+ *
874
+ * Returns `QueryArgs&lt;Doc>` — bound to the table, not free — so a mismatch
875
+ * between what this helper declares and what the table actually holds is a
876
+ * compile error at the `findMany` call site.
877
+ */
878
+ readonly toQueryArgs: (args: ListArgsValue<F, O>) => QueryArgs$2<TDocument>;
879
+ }
880
+ /** Clamp a caller-supplied `limit` into `[1, maxLimit]`; a non-finite value falls back to `fallback`. */
881
+ declare const clampLimit: (limit: number | undefined, fallback: number, maxLimit: number) => number;
882
+ /**
883
+ * Declare the filter / sort / page arguments for a list endpoint, plus the
884
+ * translation into `ctx.db.&lt;table>.findMany(...)` options. See the module docs
885
+ * for the shape and the reasoning behind it.
886
+ *
887
+ * Curried on the document type: `defineListArgs&lt;Doc&lt;"messages">>()({ … })`. The
888
+ * extra `()` buys the thing that matters — with `Doc` bound, `filter` keys and
889
+ * `orderBy` entries are checked against the table's real columns, so a typo or a
890
+ * column renamed out from under the endpoint is a COMPILE error instead of a
891
+ * predicate that silently matches nothing. TypeScript has no partial type-argument
892
+ * inference, so binding `Doc` explicitly while still inferring `F` and `O` from
893
+ * the config requires the second call.
894
+ */
895
+ declare const defineListArgs: <TDocument>() => <F extends ListFilterShape<TDocument>, O extends keyof TDocument & string>(config: DefineListArgsConfig<F, O>) => ListArgsSpec<TDocument, F, O>;
896
+ /**
897
+ * Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
898
+ * `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
899
+ * resolver, same `can(...)` permission check — so an author can branch a mask
900
+ * on the caller's role/permission. `row` is the full pre-mask row the column
901
+ * belongs to; `column` is the column currently being masked. Both are absent
902
+ * when the context is used for the procedure-wide `bypass` check (no specific
903
+ * cell is in play yet).
904
+ */
687
905
  interface MaskContext<Context = unknown> {
688
906
  readonly auth: {
689
907
  /** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
@@ -699,50 +917,50 @@ interface MaskContext<Context = unknown> {
699
917
  readonly row?: Record<string, unknown>;
700
918
  }
701
919
  /**
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
- */
920
+ * A custom masking function. Receives the raw cell value and the
921
+ * {@link MaskContext}, returns the value to surface. Use it for partial masks
922
+ * (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
923
+ * or format-preserving tokens. A function that **throws** fails closed — the
924
+ * cell is redacted to `null`, never leaked raw.
925
+ */
708
926
  type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
709
927
  /**
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
- */
928
+ * How a column is masked:
929
+ *
930
+ * - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
931
+ * the right choice for any value that must actually be kept secret.
932
+ * - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
933
+ * same input always yields the same token (joinable/groupable client-side).
934
+ * **This is NOT a confidentiality control.** It is a non-cryptographic,
935
+ * unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
936
+ * phone numbers, SSNs) are brute-force-recoverable by the very caller you are
937
+ * masking from, and identical values always produce identical tokens across
938
+ * rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
939
+ * stable pseudonym for grouping/joining and leaking the value is acceptable —
940
+ * never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
941
+ * - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
942
+ */
725
943
  type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
726
944
  /** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
727
945
  type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
728
946
  /**
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
- */
947
+ * The mask declaration passed to `mask(...)`: a table → column → strategy map.
948
+ * Deliberately a plain object literal so the codegen feeder can statically read
949
+ * which columns a procedure masks (powering the `mask_uncovered_pii_column`
950
+ * advisor lint), exactly as the RLS feeder reads policy tables.
951
+ */
734
952
  type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
735
953
  /**
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
- */
954
+ * Options for `mask(policies, options)`.
955
+ *
956
+ * - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
957
+ * inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
958
+ * not listed grants no permissions (fails closed for unknown roles).
959
+ * - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
960
+ * mask is skipped (the caller sees raw values). Use it for a privileged
961
+ * viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
962
+ * branching every column when an entire class of caller should see clear data.
963
+ */
746
964
  interface MaskOptions<Context = unknown> {
747
965
  readonly bypass?: (context: MaskContext<Context>) => boolean;
748
966
  readonly roles?: ReadonlyArray<Role>;
@@ -756,12 +974,14 @@ interface QueryArgs$1 {
756
974
  baseWhere?: unknown;
757
975
  cursor?: null | string;
758
976
  limit?: number;
977
+ orderBy?: ReadonlyArray<Record<string, unknown>>;
759
978
  where?: unknown;
760
979
  with?: Record<string, unknown>;
761
980
  }
762
981
  interface AggregateArgs$1 {
763
982
  field?: string;
764
983
  op: string;
984
+ where?: unknown;
765
985
  }
766
986
  interface GroupByArgs$1 {
767
987
  agg?: {
@@ -769,6 +989,7 @@ interface GroupByArgs$1 {
769
989
  op: string;
770
990
  };
771
991
  by: ReadonlyArray<string>;
992
+ where?: unknown;
772
993
  }
773
994
  interface TableReaderLike$1 {
774
995
  collect: () => Promise<Record<string, unknown>[]>;
@@ -781,15 +1002,16 @@ interface TableReaderLike$1 {
781
1002
  }) => Promise<QueryPage$1>;
782
1003
  take: (limit: number) => Promise<Record<string, unknown>[]>;
783
1004
  unique: () => Promise<Record<string, unknown> | null>;
1005
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
784
1006
  withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
785
1007
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
786
1008
  }
787
1009
  /**
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
- */
1010
+ * Structural projection of the runtime ORM writer — the same subset
1011
+ * `../rls/middleware` mirrors, so the wrapper is interchangeable between
1012
+ * `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
1013
+ * inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
1014
+ */
793
1015
  interface MaskDatabase {
794
1016
  aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
795
1017
  count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
@@ -799,6 +1021,11 @@ interface MaskDatabase {
799
1021
  }) => Promise<{
800
1022
  deleted: number;
801
1023
  }>;
1024
+ deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
1025
+ limit?: number;
1026
+ }) => Promise<{
1027
+ deleted: number;
1028
+ }>;
802
1029
  findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
803
1030
  findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
804
1031
  findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
@@ -810,7 +1037,8 @@ interface MaskDatabase {
810
1037
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
811
1038
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
812
1039
  limit?: number;
813
- }) => Promise<string[]>;
1040
+ skipDuplicates?: boolean;
1041
+ }) => Promise<(string | null)[]>;
814
1042
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
815
1043
  row: Record<string, unknown>;
816
1044
  tableName: string;
@@ -821,7 +1049,17 @@ interface MaskDatabase {
821
1049
  patch: Record<string, unknown>;
822
1050
  }>, options?: {
823
1051
  limit?: number;
824
- }) => Promise<void>;
1052
+ }) => Promise<{
1053
+ patched: number;
1054
+ }>;
1055
+ patchWhere?: (tableName: string, args: {
1056
+ patch: Record<string, unknown>;
1057
+ where: Record<string, unknown>;
1058
+ }, options?: {
1059
+ limit?: number;
1060
+ }) => Promise<{
1061
+ patched: number;
1062
+ }>;
825
1063
  query: (tableName: string) => TableReaderLike$1;
826
1064
  rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
827
1065
  position: number;
@@ -845,37 +1083,22 @@ interface MaskContextIn {
845
1083
  db: MaskDatabase;
846
1084
  }
847
1085
  /**
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
- */
1086
+ * Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
1087
+ * Closes over the policy map at builder-construction time; resolves identity +
1088
+ * the `bypass` decision per call against the live ctx.
1089
+ *
1090
+ * IMPORTANT: a mask is in scope only for procedures whose builder chain
1091
+ * includes this middleware — opt-in, never global (the same invariant as RLS).
1092
+ */
855
1093
  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
1094
  /** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
872
1095
  type MigrationDocument = Record<string, unknown>;
873
1096
  /**
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
- */
1097
+ * Transform applied to one document. Return a new document to rewrite the row,
1098
+ * or `undefined` to leave it untouched (skipped, not counted as changed). The
1099
+ * runner always preserves the original `_id` and `_creationTime`, so the
1100
+ * returned document neither needs to nor should change row identity.
1101
+ */
879
1102
  type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
880
1103
  interface MigrationDefinition {
881
1104
  /** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
@@ -896,83 +1119,150 @@ interface RegisteredMigration extends MigrationDefinition {
896
1119
  /** Declare an online data migration. See the module docs for runtime semantics. */
897
1120
  declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
898
1121
  /**
899
- * The prefixed tables a single plugin `P` contributes, or an empty map when it
900
- * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
901
- * so {@link InstalledTables} can fold a tuple of plugins.
902
- */
1122
+ * A mutator declaration. `server` is authoritative; `client` is the optimistic
1123
+ * twin (optional omit it to let the optimistic write fall through to the
1124
+ * server round-trip with no local preview). Both receive the same validated
1125
+ * `args`.
1126
+ */
1127
+ interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
1128
+ /**
1129
+ * Validator for the mutator's arguments. Validated on the DO before `server`
1130
+ * runs and (when present) on the client before `client` runs, so both impls
1131
+ * see the same parsed shape. Omit for a parameterless mutator.
1132
+ */
1133
+ readonly args?: Args;
1134
+ /**
1135
+ * Optimistic client implementation. Runs in a TanStack DB transaction
1136
+ * against the local collections; its writes are applied immediately and
1137
+ * automatically rolled back / rebased as the authoritative result syncs
1138
+ * back. Pure and side-effect-free beyond the local store. Omit to skip the
1139
+ * local preview.
1140
+ */
1141
+ readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
1142
+ /**
1143
+ * Owner-scope the write: names the column carrying the row owner (e.g.
1144
+ * `owner: "userId"`). Before `server` runs, the mutator requires a verified
1145
+ * identity, rejects a client-supplied owner that disagrees with it, and sets
1146
+ * the column to the verified value — so the impl reads `args[owner]` without
1147
+ * trusting the client and never repeats the check by hand.
1148
+ *
1149
+ * This replaces the "every mutator opens with `assertOwner(ctx, args.userId)`"
1150
+ * pattern, and is the write-side counterpart to an `owner`-scoped
1151
+ * {@link import("./shapes").ShapeDefinition}. Unlike a shape it takes the column
1152
+ * NAME rather than `true`: a shape is bound to one `table`, so the table's
1153
+ * `.ownedBy(field)` resolves unambiguously, whereas one mutator may write
1154
+ * several tables and has no single owning table to read it from.
1155
+ *
1156
+ * Declare the column `v.optional(...)` to leave it off the wire entirely; it is
1157
+ * injected either way.
1158
+ */
1159
+ readonly owner?: string;
1160
+ /**
1161
+ * Authoritative server implementation. Runs inside the shard DO with a full
1162
+ * {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
1163
+ * and poke back to subscribers. This is the source of truth — the client
1164
+ * impl is only a prediction of it.
1165
+ */
1166
+ readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
1167
+ }
1168
+ /**
1169
+ * A {@link MutatorDefinition} plus the codegen discovery marker and a
1170
+ * dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
1171
+ * invokes a mutator exactly like a registered procedure.
1172
+ */
1173
+ interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
1174
+ readonly __lunoraMutator: true;
1175
+ /** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
1176
+ readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
1177
+ /**
1178
+ * Marks the dispatch kind so codegen can register the mutator in the same
1179
+ * `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
1180
+ * reads `kind === "mutation"` to wrap the authoritative `server` impl in the
1181
+ * shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
1182
+ * ordinary `mutation`.
1183
+ */
1184
+ readonly kind: "mutation";
1185
+ }
1186
+ /** Declare a custom mutator. See the module docs for runtime semantics. */
1187
+ declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
1188
+ /**
1189
+ * The prefixed tables a single plugin `P` contributes, or an empty map when it
1190
+ * ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
1191
+ * so {@link InstalledTables} can fold a tuple of plugins.
1192
+ */
903
1193
  type ExtensionTablesOf<P> = P extends {
904
1194
  readonly extension: SchemaExtension<infer X> & {
905
1195
  readonly key: infer K;
906
1196
  };
907
1197
  } ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
908
1198
  /**
909
- * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
910
- * auto-prefixed extension tables left-to-right — the type-level mirror of
911
- * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
912
- */
1199
+ * Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
1200
+ * auto-prefixed extension tables left-to-right — the type-level mirror of
1201
+ * {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
1202
+ */
913
1203
  type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
914
1204
  /**
915
- * Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
916
- * `ctx.api.&lt;key>` additions {@link composePluginMiddleware} accumulates as each
917
- * plugin middleware runs. Independent of the incoming context, which the builder
918
- * infers at the `.use(...)` site.
919
- */
1205
+ * Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
1206
+ * `ctx.api.&lt;key>` additions {@link composePluginMiddleware} accumulates as each
1207
+ * plugin middleware runs. Independent of the incoming context, which the builder
1208
+ * infers at the `.use(...)` site.
1209
+ */
920
1210
  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;
921
1211
  /**
922
- * Schema fragment a plugin contributes. Same shape as the `tables` map
923
- * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
924
- * `defineSchema` argument so a plugin can ship vector decls alongside its
925
- * tables.
926
- */
1212
+ * Schema fragment a plugin contributes. Same shape as the `tables` map
1213
+ * passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
1214
+ * `defineSchema` argument so a plugin can ship vector decls alongside its
1215
+ * tables.
1216
+ */
927
1217
  interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
928
1218
  /** Stable key identifying the plugin that owns this extension. */
929
1219
  readonly key: string;
930
1220
  /**
931
- * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
932
- * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
933
- * collide with an app table; do **not** namespace manually.
934
- */
1221
+ * Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
1222
+ * each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
1223
+ * collide with an app table; do **not** namespace manually.
1224
+ */
935
1225
  readonly tables: T;
936
1226
  /**
937
- * Optional standalone vector indexes the plugin ships, keyed by index
938
- * name. Merged into the host schema's `vectorIndexes`; a key collision
939
- * with the base schema is a hard error (same policy as tables).
940
- */
1227
+ * Optional standalone vector indexes the plugin ships, keyed by index
1228
+ * name. Merged into the host schema's `vectorIndexes`; a key collision
1229
+ * with the base schema is a hard error (same policy as tables).
1230
+ */
941
1231
  readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
942
1232
  }
943
1233
  /**
944
- * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
945
- * error messages on collision) and a type-level brand.
946
- */
1234
+ * Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
1235
+ * error messages on collision) and a type-level brand.
1236
+ */
947
1237
  declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
948
1238
  tables: T;
949
1239
  vectorIndexes?: Record<string, VectorIndexDefinition>;
950
1240
  }) => SchemaExtension<T>;
951
1241
  /**
952
- * A plugin packages an optional schema extension and optional middleware.
953
- * Both are independently usable: an app can install only the schema (e.g.
954
- * for plugins that ship background workers but no per-request behavior)
955
- * or only the middleware (plugins that augment ctx without persistent
956
- * state).
957
- */
1242
+ * A plugin packages an optional schema extension and optional middleware.
1243
+ * Both are independently usable: an app can install only the schema (e.g.
1244
+ * for plugins that ship background workers but no per-request behavior)
1245
+ * or only the middleware (plugins that augment ctx without persistent
1246
+ * state).
1247
+ */
958
1248
  interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
959
1249
  /**
960
- * Optional schema extension. Apps install via
961
- * `defineSchema(...).extend(plugin.extension)`.
962
- */
1250
+ * Optional schema extension. Apps install via
1251
+ * `defineSchema(...).extend(plugin.extension)`.
1252
+ */
963
1253
  readonly extension?: SchemaExtension<TExtension>;
964
1254
  /** Stable key identifying the plugin. Matches `extension.key` when set. */
965
1255
  readonly key: string;
966
1256
  /**
967
- * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
968
- * The middleware can extend `ctx`; convention is to attach helpers under
969
- * `ctx.api.&lt;key>`, e.g.
970
- *
971
- * ```ts
972
- * middleware: ({ ctx, next }) =>
973
- * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
974
- * ```
975
- */
1257
+ * Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
1258
+ * The middleware can extend `ctx`; convention is to attach helpers under
1259
+ * `ctx.api.&lt;key>`, e.g.
1260
+ *
1261
+ * ```ts
1262
+ * middleware: ({ ctx, next }) =>
1263
+ * next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
1264
+ * ```
1265
+ */
976
1266
  readonly middleware?: Middleware<TContextIn, TContextOut>;
977
1267
  }
978
1268
  /** Options to {@link definePlugin}. */
@@ -981,35 +1271,51 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
981
1271
  middleware?: Middleware<TContextIn, TContextOut>;
982
1272
  }
983
1273
  /**
984
- * Package a schema extension + middleware as a reusable plugin. Either
985
- * field is optional `definePlugin("foo", {})` is valid but degenerate.
986
- */
987
- declare const definePlugin: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>) => Plugin<TExtension, TContextIn, TContextOut>;
988
- /**
989
- * Bundle of registered functions a {@link Component} ships. Keys are the
990
- * function's local name (e.g. `check`, `reset`); the registered function
991
- * value carries its own kind / args / handler.
992
- *
993
- * Users re-export from their own lunora module so codegen picks them up:
994
- *
995
- * ```ts
996
- * // lunora/ratelimit.ts
997
- * import { ratelimit } from "@vendor/ratelimit-component";
998
- * export const { check, reset } = ratelimit.functions;
999
- * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1000
- * ```
1001
- *
1002
- * Codegen follows the re-export back to the bundled `query/mutation/action`
1003
- * call (property access or destructuring both work), so the functions land in
1004
- * the generated `api` under the re-exporting file's namespace.
1005
- */
1274
+ * Call signatures for {@link definePlugin}. When `extension` is supplied the
1275
+ * returned plugin's `extension` is typed as PRESENT (not `?`), so the
1276
+ * canonical install pattern `defineSchema(...).extend(plugin.extension)`
1277
+ * typechecks without a non-null assertion the shape every scaffold template
1278
+ * ships. The bare-options signature keeps `extension` optional for plugins
1279
+ * that carry only middleware.
1280
+ */
1281
+ interface DefinePluginFunction {
1282
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
1283
+ extension: SchemaExtension<TExtension>;
1284
+ }): Plugin<TExtension, TContextIn, TContextOut> & {
1285
+ readonly extension: SchemaExtension<TExtension>;
1286
+ };
1287
+ <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
1288
+ }
1289
+ /**
1290
+ * Package a schema extension + middleware as a reusable plugin. Either
1291
+ * field is optional — `definePlugin("foo", {})` is valid but degenerate.
1292
+ */
1293
+ declare const definePlugin: DefinePluginFunction;
1294
+ /**
1295
+ * Bundle of registered functions a {@link Component} ships. Keys are the
1296
+ * function's local name (e.g. `check`, `reset`); the registered function
1297
+ * value carries its own kind / args / handler.
1298
+ *
1299
+ * Users re-export from their own lunora module so codegen picks them up:
1300
+ *
1301
+ * ```ts
1302
+ * // lunora/ratelimit.ts
1303
+ * import { ratelimit } from "@vendor/ratelimit-component";
1304
+ * export const { check, reset } = ratelimit.functions;
1305
+ * // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
1306
+ * ```
1307
+ *
1308
+ * Codegen follows the re-export back to the bundled `query/mutation/action`
1309
+ * call (property access or destructuring both work), so the functions land in
1310
+ * the generated `api` under the re-exporting file's namespace.
1311
+ */
1006
1312
  type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
1007
1313
  /**
1008
- * Component = {@link Plugin} with a bundle of registered functions. The
1009
- * extension + middleware + functions are independent: a component can ship
1010
- * functions without a schema (e.g. a stateless utility), or a schema
1011
- * without functions (e.g. shared table definitions), and any combination.
1012
- */
1314
+ * Component = {@link Plugin} with a bundle of registered functions. The
1315
+ * extension + middleware + functions are independent: a component can ship
1316
+ * functions without a schema (e.g. a stateless utility), or a schema
1317
+ * without functions (e.g. shared table definitions), and any combination.
1318
+ */
1013
1319
  interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
1014
1320
  readonly functions: F;
1015
1321
  }
@@ -1018,81 +1324,81 @@ interface DefineComponentOptions<TExtension extends Record<string, TableDefiniti
1018
1324
  functions?: F;
1019
1325
  }
1020
1326
  /**
1021
- * Convenience wrapper around {@link definePlugin} that also bundles a set
1022
- * of registered functions. The resulting `component.functions` object is a
1023
- * record of `name → registered query/mutation/action`; consumers
1024
- * re-export entries so codegen discovers them as user functions:
1025
- *
1026
- * ```ts
1027
- * export const ratelimit = defineComponent("ratelimit", {
1028
- * // Bare `buckets` merges in as `ratelimit_buckets`.
1029
- * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1030
- * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1031
- * functions: {
1032
- * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1033
- * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1034
- * },
1035
- * });
1036
- * ```
1037
- *
1038
- * Re-exporting an entry (by property access or destructuring) is enough for
1039
- * codegen to discover it in the host app's namespace — the discovery resolver
1040
- * chases the re-export back to the bundled registration call.
1041
- */
1327
+ * Convenience wrapper around {@link definePlugin} that also bundles a set
1328
+ * of registered functions. The resulting `component.functions` object is a
1329
+ * record of `name → registered query/mutation/action`; consumers
1330
+ * re-export entries so codegen discovers them as user functions:
1331
+ *
1332
+ * ```ts
1333
+ * export const ratelimit = defineComponent("ratelimit", {
1334
+ * // Bare `buckets` merges in as `ratelimit_buckets`.
1335
+ * extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
1336
+ * middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
1337
+ * functions: {
1338
+ * check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
1339
+ * reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
1340
+ * },
1341
+ * });
1342
+ * ```
1343
+ *
1344
+ * Re-exporting an entry (by property access or destructuring) is enough for
1345
+ * codegen to discover it in the host app's namespace — the discovery resolver
1346
+ * chases the re-export back to the bundled registration call.
1347
+ */
1042
1348
  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>;
1043
1349
  /**
1044
- * Map every key `K` of an extension's table map `X` to its auto-prefixed name
1045
- * `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
1046
- * so the typed `.extend(...)` chain reflects the real merged table names.
1047
- */
1048
- type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K] };
1049
- /**
1050
- * Merge a {@link SchemaExtension} into an existing schema. Returns a new
1051
- * schema object — never mutates the input.
1052
- *
1053
- * Extension tables are auto-namespaced: each bare table name is prefixed with
1054
- * the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
1055
- * style, and every intra-extension reference (relation targets, aggregate /
1056
- * rank index `on`, standalone vector index `table`) is rewritten to match.
1057
- * References to base/app tables are left untouched.
1058
- *
1059
- * Because each extension lives in its own `key` namespace, app↔component
1060
- * collisions are impossible. The only remaining hard error is two extensions
1061
- * sharing the same `key` and producing the same prefixed table (or vector
1062
- * index) name — silent shadow would let one plugin hijack another's data.
1063
- */
1350
+ * Map every key `K` of an extension's table map `X` to its auto-prefixed name
1351
+ * `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
1352
+ * so the typed `.extend(...)` chain reflects the real merged table names.
1353
+ */
1354
+ type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K]; };
1355
+ /**
1356
+ * Merge a {@link SchemaExtension} into an existing schema. Returns a new
1357
+ * schema object — never mutates the input.
1358
+ *
1359
+ * Extension tables are auto-namespaced: each bare table name is prefixed with
1360
+ * the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
1361
+ * style, and every intra-extension reference (relation targets, aggregate /
1362
+ * rank index `on`, standalone vector index `table`) is rewritten to match.
1363
+ * References to base/app tables are left untouched.
1364
+ *
1365
+ * Because each extension lives in its own `key` namespace, app↔component
1366
+ * collisions are impossible. The only remaining hard error is two extensions
1367
+ * sharing the same `key` and producing the same prefixed table (or vector
1368
+ * index) name — silent shadow would let one plugin hijack another's data.
1369
+ */
1064
1370
  declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
1065
1371
  readonly key: Key;
1066
1372
  }) => Schema<PrefixedTables<X, Key> & T>;
1067
1373
  /**
1068
- * Install several plugins' schema extensions in one call — the one-shot
1069
- * counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
1070
- * without an `extension` (middleware-only) are skipped; tables from those that
1071
- * do are auto-prefixed and reference-rewritten exactly as
1072
- * {@link mergeSchemaExtension} does for a single `.extend(...)`.
1073
- *
1074
- * ```ts
1075
- * const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
1076
- * // → todos + ratelimit_* + audit_*
1077
- * ```
1078
- *
1079
- * Pair it with {@link composePluginMiddleware} to attach every plugin's
1080
- * middleware in a single `.use(...)`, so installing N plugins is two calls
1081
- * rather than N `.extend(...)` + N `.use(...)`.
1082
- */
1374
+ * Install several plugins' schema extensions in one call — the one-shot
1375
+ * counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
1376
+ * without an `extension` (middleware-only) are skipped; tables from those that
1377
+ * do are auto-prefixed and reference-rewritten exactly as
1378
+ * {@link mergeSchemaExtension} does for a single `.extend(...)`.
1379
+ *
1380
+ * ```ts
1381
+ * const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
1382
+ * // → todos + ratelimit_* + audit_*
1383
+ * ```
1384
+ *
1385
+ * Pair it with {@link composePluginMiddleware} to attach every plugin's
1386
+ * middleware in a single `.use(...)`, so installing N plugins is two calls
1387
+ * rather than N `.extend(...)` + N `.use(...)`.
1388
+ */
1083
1389
  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>>;
1084
1390
  /**
1085
- * Compose every plugin's middleware into a single middleware you attach with one
1086
- * `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
1087
- * in array order, each seeing the context the previous one widened, so the final
1088
- * `next({ ctx })` the builder receives carries every plugin's `ctx.api.&lt;key>`
1089
- * additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
1090
- * value, the middleware sibling of {@link installPlugins}.
1091
- *
1092
- * `ContextIn` is left free so the builder infers it from the context at the
1093
- * `.use(...)` site; the result type widens it by the union of the plugins'
1094
- * outputs.
1095
- */
1391
+ * Compose every plugin's middleware into a single middleware you attach with one
1392
+ * `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
1393
+ * in array order, each seeing the context the previous one widened, so the final
1394
+ * `next({ ctx })` the builder receives carries every plugin's `ctx.api.&lt;key>`
1395
+ * additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
1396
+ * value, the middleware sibling of {@link installPlugins}.
1397
+ *
1398
+ * `ContextIn` is left free so the builder infers it from the context at the
1399
+ * `.use(...)` site; the result type widens it by the union of the plugins'
1400
+ * outputs.
1401
+ */
1096
1402
  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>;
1097
1403
  /** Options for `.vectorize(field, opts)` (DSL Shape A). */
1098
1404
  interface VectorizeOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
@@ -1129,10 +1435,10 @@ interface RelationBuilder {
1129
1435
  }) => OneRelation<Target>;
1130
1436
  }
1131
1437
  /**
1132
- * Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
1133
- * `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
1134
- * `COUNT(*) GROUP BY userId` accelerator.
1135
- */
1438
+ * Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
1439
+ * `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
1440
+ * `COUNT(*) GROUP BY userId` accelerator.
1441
+ */
1136
1442
  interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
1137
1443
  /** Group keys; counter rows are one per distinct tuple. Omitted = single-row aggregate over the whole table. */
1138
1444
  by?: ReadonlyArray<keyof Shape & string>;
@@ -1144,11 +1450,11 @@ interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> =
1144
1450
  where?: Record<string, unknown>;
1145
1451
  }
1146
1452
  /**
1147
- * Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
1148
- * accepts either an array of `{ field, direction }` keys, or the shorthand
1149
- * `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
1150
- * rank — omitted ⇒ one global rank over the whole table.
1151
- */
1453
+ * Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
1454
+ * accepts either an array of `{ field, direction }` keys, or the shorthand
1455
+ * `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
1456
+ * rank — omitted ⇒ one global rank over the whole table.
1457
+ */
1152
1458
  interface InlineRankIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
1153
1459
  /** Columns that scope each ranking; omitted ⇒ one global rank. */
1154
1460
  partitionBy?: ReadonlyArray<keyof Shape & string>;
@@ -1164,19 +1470,29 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1164
1470
  /** Declare an aggregate (counter/sum/…) maintained by triggers for O(1) reads. */
1165
1471
  aggregateIndex: (name: string, options?: InlineAggregateIndexOptions<Shape>) => TableBuilder<Shape>;
1166
1472
  /**
1167
- * Mark this table as written outside Lunora's discoverable insert path —
1168
- * by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
1169
- * better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
1170
- * (`table_without_insert`) then skip it instead of flagging the absent
1171
- * `ctx.db.insert(...)`.
1172
- */
1473
+ * Mark this table as written outside Lunora's discoverable insert path —
1474
+ * by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
1475
+ * better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
1476
+ * (`table_without_insert`) then skip it instead of flagging the absent
1477
+ * `ctx.db.insert(...)`.
1478
+ */
1173
1479
  externallyManaged: () => TableBuilder<Shape>;
1174
1480
  /**
1175
- * Mark this table as global (cross-shard). Backed by **D1** by default;
1176
- * pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
1177
- * via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
1178
- * table stays reactive live queries re-run on write.
1179
- */
1481
+ * Declare a geospatial index over a `v.geoPoint()` column. The runtime keeps
1482
+ * a geohash companion so `withGeoIndex(name, q => q.near(point, radius))` and
1483
+ * `.within(bbox)` resolve as a geohash-prefix range scan + Haversine
1484
+ * refine/sort. `options.precision` tunes the geohash length (default 9).
1485
+ */
1486
+ geoIndex: (name: string, options: {
1487
+ field: keyof Shape & string;
1488
+ precision?: number;
1489
+ }) => TableBuilder<Shape>;
1490
+ /**
1491
+ * Mark this table as global (cross-shard). Backed by **D1** by default;
1492
+ * pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
1493
+ * via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
1494
+ * table stays reactive — live queries re-run on write.
1495
+ */
1180
1496
  global: (options?: {
1181
1497
  backend?: GlobalBackend;
1182
1498
  }) => TableBuilder<Shape>;
@@ -1185,45 +1501,96 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
1185
1501
  unique?: boolean;
1186
1502
  }) => TableBuilder<Shape>;
1187
1503
  /**
1188
- * Opt this table OUT of secure-by-default RLS. Under a schema marked
1189
- * `.rls("required")`, every table is protected (the write path denies raw,
1190
- * non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
1191
- * plain `query`/`mutation` may read/write it without an RLS policy. No effect
1192
- * when the schema does not require RLS.
1193
- */
1504
+ * Name the column holding the owning user's id, so "only the owner sees these
1505
+ * rows" is declared once here rather than restated in every shape.
1506
+ *
1507
+ * A `defineShape({ table, owner: true })` over this table derives its predicate
1508
+ * from the field: the subscriber's verified `ctx.auth.userId` must match, and an
1509
+ * anonymous subscriber is denied. Pairs naturally with `.shardBy(field)` on the
1510
+ * same column — the shard key routes the storage, `ownedBy` states who the rows
1511
+ * belong to — but the two are independent and either can be used alone.
1512
+ *
1513
+ * This is a *shape* declaration, not an RLS policy: it narrows what a shape
1514
+ * replicates. Guarding procedure reads/writes is still `rls(...)`'s job.
1515
+ */
1516
+ ownedBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1517
+ /**
1518
+ * Opt this table OUT of secure-by-default RLS. Under a schema marked
1519
+ * `.rls("required")`, every table is protected (the write path denies raw,
1520
+ * non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
1521
+ * plain `query`/`mutation` may read/write it without an RLS policy. No effect
1522
+ * when the schema does not require RLS.
1523
+ */
1194
1524
  public: () => TableBuilder<Shape>;
1195
1525
  /**
1196
- * Declare a rank index (sorted companion table, btree-backed) for
1197
- * `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
1198
- */
1526
+ * Declare a rank index (sorted companion table, btree-backed) for
1527
+ * `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
1528
+ */
1199
1529
  rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
1200
1530
  /** Declare relations to other tables, loaded via `findMany({ with })`. */
1201
1531
  relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
1202
- /** Add a search index over a field with optional filter fields. */
1532
+ /**
1533
+ * Add a full-text search index over `field`, queried with
1534
+ * `.withSearchIndex(name, q => q.search(field, term))`. `field` may be a
1535
+ * dot-separated path into a nested object (`"properties.name"`).
1536
+ * `filterFields` (at most 16) lists the columns `.eq()` may narrow by inside
1537
+ * the search. `language` selects the text analysis (accent folding always,
1538
+ * plus that language's stopwords). `staged: true` skips the migration-time
1539
+ * backfill on a large existing table. `strategy: "native"` uses the engine's
1540
+ * own full-text index where it has one (Postgres) — faster on large corpora,
1541
+ * at the cost of the engine ranking rather than the shared scorer.
1542
+ */
1203
1543
  searchIndex: (name: string, options: {
1204
1544
  field: string;
1205
1545
  filterFields?: ReadonlyArray<string>;
1546
+ language?: SearchLanguage;
1547
+ staged?: boolean;
1548
+ strategy?: SearchStrategy;
1206
1549
  }) => TableBuilder<Shape>;
1207
1550
  /** Route storage by the named field — one DO per distinct value. */
1208
1551
  shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
1209
1552
  /**
1210
- * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1211
- * default `deletedAt`) and changes `ctx.db.&lt;table>.delete()` to **set** it
1212
- * instead of removing the row; `onDelete: "cascade"` children are recursively
1213
- * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1214
- * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1215
- * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1216
- * `restore()` still address the row directly. `hardDelete()` physically
1217
- * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1218
- * scope, not access control — anyone who can run the read can set it; a unique
1219
- * index still rejects a new row that collides with a soft-deleted one (the row
1220
- * physically persists).
1221
- */
1553
+ * Turn on soft delete. Adds a nullable timestamp column (`options.field`,
1554
+ * default `deletedAt`) and changes `ctx.db.&lt;table>.delete()` to **set** it
1555
+ * instead of removing the row; `onDelete: "cascade"` children are recursively
1556
+ * soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
1557
+ * `aggregate`/relation loads) then hide soft-deleted rows unless they pass
1558
+ * `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
1559
+ * `restore()` still address the row directly. `hardDelete()` physically
1560
+ * removes it (cascading as a real delete). Note: `includeDeleted` is a read
1561
+ * scope, not access control — anyone who can run the read can set it; a unique
1562
+ * index still rejects a new row that collides with a soft-deleted one (the row
1563
+ * physically persists).
1564
+ */
1222
1565
  softDelete: (options?: {
1223
1566
  field?: string;
1224
1567
  }) => TableBuilder<Shape>;
1568
+ /**
1569
+ * Materialize this table from an external Postgres/MySQL behind Cloudflare
1570
+ * Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
1571
+ * (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
1572
+ * after which `defineShape` carries it to clients unchanged. Implies
1573
+ * `.externallyManaged()` (rows come from the ingest loop, not user mutations).
1574
+ *
1575
+ * Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
1576
+ * `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
1577
+ * `external_source_unscoped` advisor lint fails the build when it is absent, and
1578
+ * `external_source_on_global` rejects combining `.source()` with `.global()`.
1579
+ */
1580
+ source: (definition: ExternalSourceDefinition) => TableBuilder<Shape>;
1225
1581
  /** Declare named lifecycle triggers fired inline within the write path. */
1226
1582
  triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
1583
+ /**
1584
+ * Declare a table-level TTL: a DO alarm-driven sweep auto-deletes rows whose
1585
+ * expiry has passed (or soft-deletes them when the table also
1586
+ * `.softDelete()`s). `field` is an epoch-millisecond column; without
1587
+ * `options.after` its value is the absolute expiry instant, with `after` the
1588
+ * row expires `after` ms past `field` (`field + after`). Coarse, cheap,
1589
+ * table-level — for per-row schedules use `@lunora/scheduler`.
1590
+ */
1591
+ ttl: (field: keyof Shape & string, options?: {
1592
+ after?: number;
1593
+ }) => TableBuilder<Shape>;
1227
1594
  /** Declare a vector index over a single text field on this table. */
1228
1595
  vectorize: (field: keyof Shape & string, options: VectorizeOptions<Shape>) => TableBuilder<Shape>;
1229
1596
  }
@@ -1241,22 +1608,22 @@ interface VectorIndexOptions {
1241
1608
  };
1242
1609
  }
1243
1610
  /**
1244
- * Build a table definition. Returned object is both the table definition (for
1245
- * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1246
- */
1611
+ * Build a table definition. Returned object is both the table definition (for
1612
+ * `defineSchema`) and a fluent builder for indexes + sharding metadata.
1613
+ */
1247
1614
  declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
1248
1615
  /**
1249
- * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1250
- * the `vectorIndexes` map of {@link defineSchema} when the source is derived
1251
- * from multiple fields or a computation rather than a single column.
1252
- */
1616
+ * Declare a standalone vector index (DSL Shape B). Pass the returned value in
1617
+ * the `vectorIndexes` map of {@link defineSchema} when the source is derived
1618
+ * from multiple fields or a computation rather than a single column.
1619
+ */
1253
1620
  declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
1254
1621
  /**
1255
- * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1256
- * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1257
- * the owning table explicitly via `on` — handy when a single counter wants to
1258
- * live next to the schema map rather than inside a table chain.
1259
- */
1622
+ * Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
1623
+ * Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
1624
+ * the owning table explicitly via `on` — handy when a single counter wants to
1625
+ * live next to the schema map rather than inside a table chain.
1626
+ */
1260
1627
  interface AggregateIndexOptions {
1261
1628
  by?: ReadonlyArray<string>;
1262
1629
  field?: string;
@@ -1265,17 +1632,17 @@ interface AggregateIndexOptions {
1265
1632
  where?: Record<string, unknown>;
1266
1633
  }
1267
1634
  /**
1268
- * Declare a standalone aggregate index. Pass the returned value to
1269
- * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
1270
- * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1271
- * (DO + D1) read every index uniformly off the table definition.
1272
- */
1635
+ * Declare a standalone aggregate index. Pass the returned value to
1636
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
1637
+ * the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
1638
+ * (DO + D1) read every index uniformly off the table definition.
1639
+ */
1273
1640
  declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
1274
1641
  /**
1275
- * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1276
- * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1277
- * `table` so it can sit next to the schema map.
1278
- */
1642
+ * Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
1643
+ * Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
1644
+ * `table` so it can sit next to the schema map.
1645
+ */
1279
1646
  interface RankIndexOptions {
1280
1647
  partitionBy?: ReadonlyArray<string>;
1281
1648
  sortBy: ReadonlyArray<{
@@ -1286,64 +1653,64 @@ interface RankIndexOptions {
1286
1653
  where?: Record<string, unknown>;
1287
1654
  }
1288
1655
  /**
1289
- * Declare a standalone rank index. Pass the returned value to
1290
- * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1291
- * by index name — the schema attaches it to `tables[on].rankIndexes`.
1292
- */
1656
+ * Declare a standalone rank index. Pass the returned value to
1657
+ * `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
1658
+ * by index name — the schema attaches it to `tables[on].rankIndexes`.
1659
+ */
1293
1660
  declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
1294
1661
  /**
1295
- * Build the application schema. The first argument is the table map; the
1296
- * optional second argument registers standalone `defineVectorIndex(...)`
1297
- * declarations (DSL Shape B) keyed by index name. The optional third argument
1298
- * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1299
- * the optional fourth argument registers standalone `defineRankIndex(...)`
1300
- * declarations. Both are folded into the matching `tables[on].*Indexes` array
1301
- * so runtime backends read every index uniformly off the table definition.
1302
- */
1303
- /**
1304
- * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1305
- * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1306
- *
1307
- * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1308
- * the merged tables. Extension tables are auto-namespaced by the extension
1309
- * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1310
- * prefixed names via {@link PrefixedTables}. Chains:
1311
- * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1312
- * `a`'s prefixed tables then `b`'s.
1313
- */
1662
+ * Build the application schema. The first argument is the table map; the
1663
+ * optional second argument registers standalone `defineVectorIndex(...)`
1664
+ * declarations (DSL Shape B) keyed by index name. The optional third argument
1665
+ * registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
1666
+ * the optional fourth argument registers standalone `defineRankIndex(...)`
1667
+ * declarations. Both are folded into the matching `tables[on].*Indexes` array
1668
+ * so runtime backends read every index uniformly off the table definition.
1669
+ */
1670
+ /**
1671
+ * Schema with an in-place `.extend(plugin.extension)` method. Used so apps
1672
+ * can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
1673
+ *
1674
+ * `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
1675
+ * the merged tables. Extension tables are auto-namespaced by the extension
1676
+ * `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
1677
+ * prefixed names via {@link PrefixedTables}. Chains:
1678
+ * `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
1679
+ * `a`'s prefixed tables then `b`'s.
1680
+ */
1314
1681
  type ExtendableSchema<T extends Record<string, TableDefinition>> = {
1315
1682
  extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
1316
1683
  readonly key: Key;
1317
1684
  }) => ExtendableSchema<PrefixedTables<X, Key> & T>;
1318
1685
  /**
1319
- * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1320
- * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1321
- * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1322
- * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1323
- * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1324
- * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1325
- *
1326
- * ⚠️ **Set this once, before your first deploy — changing or removing it
1327
- * strands data.** A Durable Object name maps to a *different* ID in each
1328
- * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1329
- * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1330
- * old jurisdiction's DOs and is no longer reachable. There is no in-place
1331
- * migration — you would have to export from the old jurisdiction and import
1332
- * into the new one.
1333
- *
1334
- * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1335
- * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1336
- * settings, not this option.
1337
- * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1338
- */
1686
+ * Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
1687
+ * the scheduler, and `ctx.containers` — to a Cloudflare data-residency
1688
+ * jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
1689
+ * schema and emits it into the generated worker's `createWorker({ jurisdiction })`
1690
+ * (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
1691
+ * `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
1692
+ *
1693
+ * ⚠️ **Set this once, before your first deploy — changing or removing it
1694
+ * strands data.** A Durable Object name maps to a *different* ID in each
1695
+ * jurisdiction, so toggling this on an existing app makes every shard, scheduler
1696
+ * job, and session DO resolve to a NEW, empty DO; the previous data stays in the
1697
+ * old jurisdiction's DOs and is no longer reachable. There is no in-place
1698
+ * migration — you would have to export from the old jurisdiction and import
1699
+ * into the new one.
1700
+ *
1701
+ * Note: this pins **DO-backed** state only. D1-backed state — `.global()`
1702
+ * tables and `@lunora/auth` sessions alike — is governed by D1's own location
1703
+ * settings, not this option.
1704
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1705
+ */
1339
1706
  jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
1340
1707
  /**
1341
- * Turn on secure-by-default RLS for the whole schema. Every table is then
1342
- * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1343
- * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1344
- * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1345
- * mode, so `.rls("required")` composes with `.extend(...)` either order.
1346
- */
1708
+ * Turn on secure-by-default RLS for the whole schema. Every table is then
1709
+ * protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
1710
+ * procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
1711
+ * `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
1712
+ * mode, so `.rls("required")` composes with `.extend(...)` either order.
1713
+ */
1347
1714
  rls: (mode: "required") => ExtendableSchema<T>;
1348
1715
  } & Schema<T>;
1349
1716
  declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
@@ -1351,19 +1718,19 @@ declare const defineSchema: <T extends Record<string, TableDefinition>>(tables:
1351
1718
  declare const DEFAULT_TTL_MS = 3e4;
1352
1719
  declare const PRESENCE_BARE_TABLE = "present";
1353
1720
  /**
1354
- * The prefixed table name the extension produces at merge time. The handlers
1355
- * read/write this name directly so they always agree with the merged schema.
1356
- */
1721
+ * The prefixed table name the extension produces at merge time. The handlers
1722
+ * read/write this name directly so they always agree with the merged schema.
1723
+ */
1357
1724
  declare const PRESENCE_TABLE: "presence_present";
1358
1725
  /**
1359
- * A single present member as returned by `listPresent`.
1360
- *
1361
- * Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
1362
- * connection secret — disclosing every member's `sessionId` would let any
1363
- * subscriber enumerate them and target the heartbeat / disconnect write paths.
1364
- * A "who's here" UI needs only `userId` + awareness `data`; the caller already
1365
- * knows its own session id locally (the `usePresence` hook returns it).
1366
- */
1726
+ * A single present member as returned by `listPresent`.
1727
+ *
1728
+ * Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
1729
+ * connection secret — disclosing every member's `sessionId` would let any
1730
+ * subscriber enumerate them and target the heartbeat / disconnect write paths.
1731
+ * A "who's here" UI needs only `userId` + awareness `data`; the caller already
1732
+ * knows its own session id locally (the `usePresence` hook returns it).
1733
+ */
1367
1734
  interface PresenceMember {
1368
1735
  /** Opaque awareness blob (selection, cursor, name, color…). */
1369
1736
  data?: Record<string, unknown>;
@@ -1377,41 +1744,41 @@ interface PresenceMember {
1377
1744
  /** Options for {@link definePresence}. */
1378
1745
  interface DefinePresenceOptions {
1379
1746
  /**
1380
- * Grace window (ms) before a gracefully-closed session is dropped from the
1381
- * present list. When `0` (the default), `onDisconnect` hard-deletes the
1382
- * session's row the instant its socket closes. When `> 0`, the row is
1383
- * instead aged so the read-time TTL filter hides it `disconnectGraceMs`
1384
- * from now — a reconnect with the same `sessionId` within the window
1385
- * re-heartbeats and restores full presence with no visible flicker (the
1386
- * AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
1387
- */
1747
+ * Grace window (ms) before a gracefully-closed session is dropped from the
1748
+ * present list. When `0` (the default), `onDisconnect` hard-deletes the
1749
+ * session's row the instant its socket closes. When `> 0`, the row is
1750
+ * instead aged so the read-time TTL filter hides it `disconnectGraceMs`
1751
+ * from now — a reconnect with the same `sessionId` within the window
1752
+ * re-heartbeats and restores full presence with no visible flicker (the
1753
+ * AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
1754
+ */
1388
1755
  disconnectGraceMs?: number;
1389
1756
  /**
1390
- * How long (ms) a heartbeat keeps a member present. `listPresent` excludes
1391
- * rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
1392
- */
1757
+ * How long (ms) a heartbeat keeps a member present. `listPresent` excludes
1758
+ * rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
1759
+ */
1393
1760
  ttlMs?: number;
1394
1761
  }
1395
1762
  /** The registered functions a presence component ships. */
1396
1763
  interface PresenceFunctions {
1397
1764
  /**
1398
- * Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
1399
- * delete its presence row so it disappears from `listPresent` with no TTL
1400
- * lag. Targets the row by the `{ roomId, sessionId }` the client passed as
1401
- * the connection `context`, and only deletes it when the disconnecting
1402
- * VERIFIED identity owns the row (so a forged context can't evict another
1403
- * member). The TTL filter + `sweep` remain the fallback for ungraceful drops
1404
- * where no `context` was recorded.
1405
- */
1765
+ * Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
1766
+ * delete its presence row so it disappears from `listPresent` with no TTL
1767
+ * lag. Targets the row by the `{ roomId, sessionId }` the client passed as
1768
+ * the connection `context`, and only deletes it when the disconnecting
1769
+ * VERIFIED identity owns the row (so a forged context can't evict another
1770
+ * member). The TTL filter + `sweep` remain the fallback for ungraceful drops
1771
+ * where no `context` was recorded.
1772
+ */
1406
1773
  disconnect: RegisteredLifecycleHook;
1407
1774
  /**
1408
- * Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
1409
- * Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
1410
- * subscribers receive a single-row delta, not a churn of insert/delete. A
1411
- * heartbeat may only patch a row owned by the same identity (an existing row
1412
- * held by a different `userId` is refused with `FORBIDDEN`), so a client
1413
- * can't overwrite another member's awareness data via a guessed `sessionId`.
1414
- */
1775
+ * Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
1776
+ * Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
1777
+ * subscribers receive a single-row delta, not a churn of insert/delete. A
1778
+ * heartbeat may only patch a row owned by the same identity (an existing row
1779
+ * held by a different `userId` is refused with `FORBIDDEN`), so a client
1780
+ * can't overwrite another member's awareness data via a guessed `sessionId`.
1781
+ */
1415
1782
  heartbeat: RegisteredMutation<{
1416
1783
  data: ReturnType<typeof v.optional>;
1417
1784
  roomId: ReturnType<typeof v.string>;
@@ -1420,17 +1787,17 @@ interface PresenceFunctions {
1420
1787
  lastSeen: number;
1421
1788
  }>;
1422
1789
  /**
1423
- * Live query returning the non-expired members of `roomId`, newest heartbeat
1424
- * first. Subscribe to it for a reactive "who's here" list.
1425
- */
1790
+ * Live query returning the non-expired members of `roomId`, newest heartbeat
1791
+ * first. Subscribe to it for a reactive "who's here" list.
1792
+ */
1426
1793
  listPresent: RegisteredQuery<{
1427
1794
  roomId: ReturnType<typeof v.string>;
1428
1795
  }, PresenceMember[]>;
1429
1796
  /**
1430
- * Internal mutation that hard-deletes every expired row for `roomId`. Stale
1431
- * rows already vanish from `listPresent` via the read-time TTL filter; this
1432
- * only reclaims storage. Schedule it (cron / `runAfter`) if you care.
1433
- */
1797
+ * Internal mutation that hard-deletes every expired row for `roomId`. Stale
1798
+ * rows already vanish from `listPresent` via the read-time TTL filter; this
1799
+ * only reclaims storage. Schedule it (cron / `runAfter`) if you care.
1800
+ */
1434
1801
  sweep: RegisteredMutation<{
1435
1802
  roomId: ReturnType<typeof v.string>;
1436
1803
  }, {
@@ -1444,114 +1811,114 @@ type PresenceComponent = Component<{
1444
1811
  functions: PresenceFunctions;
1445
1812
  };
1446
1813
  /**
1447
- * The presence schema extension: a single `present` table, auto-namespaced to
1448
- * `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
1449
- * heartbeat upsert and by `roomId` for `listPresent`.
1450
- */
1814
+ * The presence schema extension: a single `present` table, auto-namespaced to
1815
+ * `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
1816
+ * heartbeat upsert and by `roomId` for `listPresent`.
1817
+ */
1451
1818
  declare const presenceExtension: SchemaExtension<{
1452
1819
  [PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
1453
1820
  }>;
1454
1821
  declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
1455
1822
  /**
1456
- * The middlewares `protectPublic` chains, in the order they run. Every field is
1457
- * optional, so a bundle can be just a rate limit, just a captcha, or any mix —
1458
- * pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
1459
- * from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
1460
- * `@lunora/auth`). They are accepted as values rather than imported here so
1461
- * `@lunora/server` keeps no dependency on those packages (which depend on it).
1462
- */
1823
+ * The middlewares `protectPublic` chains, in the order they run. Every field is
1824
+ * optional, so a bundle can be just a rate limit, just a captcha, or any mix —
1825
+ * pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
1826
+ * from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
1827
+ * `@lunora/auth`). They are accepted as values rather than imported here so
1828
+ * `@lunora/server` keeps no dependency on those packages (which depend on it).
1829
+ */
1463
1830
  interface ProtectPublicOptions<Context> {
1464
1831
  /**
1465
- * A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
1466
- * an obvious flood is cheaper to reject with the in-memory limiter than with
1467
- * a Turnstile siteverify round-trip.
1468
- */
1832
+ * A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
1833
+ * an obvious flood is cheaper to reject with the in-memory limiter than with
1834
+ * a Turnstile siteverify round-trip.
1835
+ */
1469
1836
  captcha?: Middleware<Context, Context>;
1470
1837
  /**
1471
- * A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
1472
- * any network-bound check below it runs.
1473
- */
1838
+ * A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
1839
+ * any network-bound check below it runs.
1840
+ */
1474
1841
  rateLimit?: Middleware<Context, Context>;
1475
1842
  /** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
1476
1843
  use?: ReadonlyArray<Middleware<Context, Context>>;
1477
1844
  }
1478
1845
  /**
1479
- * Compose the recommended public-procedure protections into a single
1480
- * `.use()`-able middleware. It is thin sugar over middleware composition — no
1481
- * new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
1482
- * and any extra middlewares so a public mutation that creates users, sends
1483
- * mail, or consumes credits is guarded in one attachment:
1484
- *
1485
- * ```ts
1486
- * export const signUp = mutation
1487
- * .use(protectPublic({
1488
- * rateLimit: rateLimit(limiter, "signup"),
1489
- * captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
1490
- * }))
1491
- * .handler(async (ctx, args) => { ... });
1492
- * ```
1493
- *
1494
- * The bundle is context-preserving — each inner middleware leaves the context
1495
- * unchanged — so it slots into any `.use()` chain without reshaping the
1496
- * procedure context. Omitted fields are skipped; an empty bundle is a
1497
- * transparent pass-through.
1498
- */
1846
+ * Compose the recommended public-procedure protections into a single
1847
+ * `.use()`-able middleware. It is thin sugar over middleware composition — no
1848
+ * new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
1849
+ * and any extra middlewares so a public mutation that creates users, sends
1850
+ * mail, or consumes credits is guarded in one attachment:
1851
+ *
1852
+ * ```ts
1853
+ * export const signUp = mutation
1854
+ * .use(protectPublic({
1855
+ * rateLimit: rateLimit(limiter, "signup"),
1856
+ * captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
1857
+ * }))
1858
+ * .handler(async (ctx, args) => { ... });
1859
+ * ```
1860
+ *
1861
+ * The bundle is context-preserving — each inner middleware leaves the context
1862
+ * unchanged — so it slots into any `.use()` chain without reshaping the
1863
+ * procedure context. Omitted fields are skipped; an empty bundle is a
1864
+ * transparent pass-through.
1865
+ */
1499
1866
  declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
1500
1867
  declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
1501
1868
  /**
1502
- * Build a project-bound, relation-aware `definePolicy` typed against the
1503
- * generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
1504
- * `createPolicyDsl&lt;DataModel, Relations>()` binding into `_generated/server.ts`,
1505
- * so importing `definePolicy` from the generated module constrains `table` to a
1506
- * real table name and type-checks the `when` predicate — including Prisma-style
1507
- * relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
1508
- * on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
1509
- * the compile-time surface narrows, so a policy authored either way is
1510
- * discovered identically by the `rls()` chain.
1511
- */
1512
- declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context>) => Policy<Context>;
1513
- /**
1514
- * Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
1515
- * it to a role through `defineRole`'s `permissions`, register those roles with
1516
- * the middleware via `rls(policies, { roles })`, then check it in a policy with
1517
- * `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
1518
- * worked example.
1519
- */
1869
+ * Build a project-bound, relation-aware `definePolicy` typed against the
1870
+ * generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
1871
+ * `createPolicyDsl&lt;DataModel, Relations>()` binding into `_generated/server.ts`,
1872
+ * so importing `definePolicy` from the generated module constrains `table` to a
1873
+ * real table name and type-checks the `when` predicate — including Prisma-style
1874
+ * relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
1875
+ * on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
1876
+ * the compile-time surface narrows, so a policy authored either way is
1877
+ * discovered identically by the `rls()` chain.
1878
+ */
1879
+ 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>;
1880
+ /**
1881
+ * Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
1882
+ * it to a role through `defineRole`'s `permissions`, register those roles with
1883
+ * the middleware via `rls(policies, { roles })`, then check it in a policy with
1884
+ * `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
1885
+ * worked example.
1886
+ */
1520
1887
  declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
1521
1888
  /**
1522
- * Collect a list of policies into the structure the `rls()` middleware
1523
- * consumes. Multiple read policies on the same table OR together (any one
1524
- * matching reveals the row); multiple write policies for the same `(table,
1525
- * op)` AND together (every one must allow). The middleware keeps them in order
1526
- * and decides — see `./middleware`.
1527
- *
1528
- * Validates against an **accidentally duplicated policy** — the same
1529
- * `(table, on)` registered with the *same* decision function (a copy-paste, or
1530
- * the same policy object spread in twice). Because multiple DISTINCT policies
1531
- * per `(table, on)` are intentional, the check keys on the `when` reference too:
1532
- * only a reference-identical `when` for the same `(table, on)` is a real
1533
- * duplicate. Throws at module load so the misconfiguration surfaces immediately
1534
- * rather than as a silently double-evaluated predicate at request time.
1535
- */
1889
+ * Collect a list of policies into the structure the `rls()` middleware
1890
+ * consumes. Multiple read policies on the same table OR together (any one
1891
+ * matching reveals the row); multiple write policies for the same `(table,
1892
+ * op)` AND together (every one must allow). The middleware keeps them in order
1893
+ * and decides — see `./middleware`.
1894
+ *
1895
+ * Validates against an **accidentally duplicated policy** — the same
1896
+ * `(table, on)` registered with the *same* decision function (a copy-paste, or
1897
+ * the same policy object spread in twice). Because multiple DISTINCT policies
1898
+ * per `(table, on)` are intentional, the check keys on the `when` reference too:
1899
+ * only a reference-identical `when` for the same `(table, on)` is a real
1900
+ * duplicate. Throws at module load so the misconfiguration surfaces immediately
1901
+ * rather than as a silently double-evaluated predicate at request time.
1902
+ */
1536
1903
  declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
1537
1904
  declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
1538
1905
  /**
1539
- * Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
1540
- * runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
1541
- * `restrictsCounts` straight off these option objects, so as long as the
1542
- * fields here stay name-compatible the wrapper is portable across the two
1543
- * dialects without an inter-package dependency.
1544
- */
1906
+ * Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
1907
+ * runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
1908
+ * `restrictsCounts` straight off these option objects, so as long as the
1909
+ * fields here stay name-compatible the wrapper is portable across the two
1910
+ * dialects without an inter-package dependency.
1911
+ */
1545
1912
  interface QueryArgs {
1546
1913
  baseWhere?: WhereInput;
1547
1914
  cursor?: null | string;
1548
1915
  limit?: number;
1549
1916
  orderBy?: ReadonlyArray<unknown>;
1550
1917
  /**
1551
- * Per-target-table read filter the RLS wrapper attaches so a `with` relation
1552
- * is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
1553
- * the top-level read: `(table) => readBase(table).baseWhere`.
1554
- */
1918
+ * Per-target-table read filter the RLS wrapper attaches so a `with` relation
1919
+ * is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
1920
+ * the top-level read: `(table) => readBase(table).baseWhere`.
1921
+ */
1555
1922
  relationBaseWhere?: (table: string) => undefined | WhereInput;
1556
1923
  restrictsCounts?: boolean;
1557
1924
  where?: WhereInput;
@@ -1620,41 +1987,54 @@ interface TableReaderLike {
1620
1987
  numItems: number;
1621
1988
  }) => Promise<QueryPage>;
1622
1989
  take: (limit: number) => Promise<Record<string, unknown>[]>;
1990
+ withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike;
1623
1991
  withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike;
1624
1992
  withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike;
1625
1993
  }
1626
1994
  /**
1627
- * Structural projection of the runtime ORM writer. The wrapper relies only
1628
- * on these fields, so it's interchangeable between `@lunora/do`'s
1629
- * `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
1630
- */
1995
+ * Structural projection of the runtime ORM writer. The wrapper relies only
1996
+ * on these fields, so it's interchangeable between `@lunora/do`'s
1997
+ * `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
1998
+ */
1631
1999
  interface DatabaseWriterLike {
1632
2000
  /**
1633
- * Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
1634
- * `baseWhere` into `options` so the reduction only sees policy-visible rows
1635
- * (safe: an aggregate scoped to `where` never reveals a hidden row — see
1636
- * `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
1637
- * wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
1638
- */
2001
+ * Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
2002
+ * `baseWhere` into `options` so the reduction only sees policy-visible rows
2003
+ * (safe: an aggregate scoped to `where` never reveals a hidden row — see
2004
+ * `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
2005
+ * wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
2006
+ */
1639
2007
  aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
1640
2008
  count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
1641
2009
  delete: (id: string, expectedTable?: string, options?: {
1642
2010
  hard?: boolean;
1643
2011
  }) => Promise<void>;
2012
+ /** Uncapped, chunked erase of a whole table. The RLS wrapper gates each row like a single delete. */
2013
+ deleteAll?: (tableName: string, options?: {
2014
+ chunkSize?: number;
2015
+ hard?: boolean;
2016
+ }) => Promise<{
2017
+ deleted: number;
2018
+ }>;
1644
2019
  deleteMany: (ids: ReadonlyArray<string>, options?: {
1645
2020
  limit?: number;
1646
2021
  }, expectedTable?: string) => Promise<{
1647
2022
  deleted: number;
1648
2023
  }>;
2024
+ deleteWhere?: (tableName: string, where: WhereInput, options?: {
2025
+ limit?: number;
2026
+ }) => Promise<{
2027
+ deleted: number;
2028
+ }>;
1649
2029
  findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
1650
2030
  findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
1651
2031
  findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
1652
2032
  get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
1653
2033
  /**
1654
- * Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
1655
- * reduction is scoped to policy-visible rows, so a group count tallies only
1656
- * rows the caller may read. Required for the same reason as `aggregate`.
1657
- */
2034
+ * Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
2035
+ * reduction is scoped to policy-visible rows, so a group count tallies only
2036
+ * rows the caller may read. Required for the same reason as `aggregate`.
2037
+ */
1658
2038
  groupBy: (tableName: string, options: GroupByArgs) => Promise<ReadonlyArray<{
1659
2039
  key: Record<string, unknown>;
1660
2040
  value: null | number;
@@ -1662,19 +2042,20 @@ interface DatabaseWriterLike {
1662
2042
  insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
1663
2043
  insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1664
2044
  limit?: number;
1665
- }) => Promise<string[]>;
2045
+ skipDuplicates?: boolean;
2046
+ }) => Promise<(string | null)[]>;
1666
2047
  insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
1667
2048
  allowExplicitId?: boolean;
1668
2049
  limit?: number;
1669
2050
  }) => Promise<string[]>;
1670
2051
  /**
1671
- * Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
1672
- * already knows the owning table of an id internally, so it can return
1673
- * `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
1674
- * uses it to collapse the per-call membership-probe fan-out (1 `get` + N
1675
- * `findFirst` across every policy table) down to one lookup. Writers that
1676
- * don't implement it fall back to the probe path.
1677
- */
2052
+ * Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
2053
+ * already knows the owning table of an id internally, so it can return
2054
+ * `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
2055
+ * uses it to collapse the per-call membership-probe fan-out (1 `get` + N
2056
+ * `findFirst` across every policy table) down to one lookup. Writers that
2057
+ * don't implement it fall back to the probe path.
2058
+ */
1678
2059
  lookupById?: (id: string, expectedTable?: string) => Promise<null | {
1679
2060
  row: Record<string, unknown>;
1680
2061
  tableName: string;
@@ -1685,14 +2066,24 @@ interface DatabaseWriterLike {
1685
2066
  patch: Record<string, unknown>;
1686
2067
  }>, options?: {
1687
2068
  limit?: number;
1688
- }, expectedTable?: string) => Promise<void>;
2069
+ }, expectedTable?: string) => Promise<{
2070
+ patched: number;
2071
+ }>;
2072
+ patchWhere?: (tableName: string, args: {
2073
+ patch: Record<string, unknown>;
2074
+ where: WhereInput;
2075
+ }, options?: {
2076
+ limit?: number;
2077
+ }) => Promise<{
2078
+ patched: number;
2079
+ }>;
1689
2080
  query: (tableName: string) => TableReaderLike;
1690
2081
  /**
1691
- * Rank a row within its partition. A position is a count-of-rows-before, so
1692
- * — exactly like `count()` — it can't be trusted in an RLS-restricted
1693
- * reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
1694
- * for the same reason as `aggregate`.
1695
- */
2082
+ * Rank a row within its partition. A position is a count-of-rows-before, so
2083
+ * — exactly like `count()` — it can't be trusted in an RLS-restricted
2084
+ * reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
2085
+ * for the same reason as `aggregate`.
2086
+ */
1696
2087
  rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
1697
2088
  position: number;
1698
2089
  total: number;
@@ -1703,24 +2094,36 @@ interface DatabaseWriterLike {
1703
2094
  total: number;
1704
2095
  }>;
1705
2096
  /**
1706
- * Sorted pagination over a rank companion. The companion stores only the
1707
- * partition + sort keys + id, so an arbitrary read `baseWhere` can't be
1708
- * enforced against it (and re-filtering the fetched rows would break page
1709
- * sizing). RLS therefore fails it closed rather than leak hidden rows.
1710
- * Required for the same reason as `aggregate`.
1711
- */
2097
+ * Sorted pagination over a rank companion. The companion stores only the
2098
+ * partition + sort keys + id, so an arbitrary read `baseWhere` can't be
2099
+ * enforced against it (and re-filtering the fetched rows would break page
2100
+ * sizing). RLS therefore fails it closed rather than leak hidden rows.
2101
+ * Required for the same reason as `aggregate`.
2102
+ */
1712
2103
  rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
1713
2104
  replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
1714
2105
  restore?: (id: string, expectedTable?: string) => Promise<void>;
2106
+ /**
2107
+ * Whole-shard erase. The RLS wrapper deliberately **fails this closed** rather
2108
+ * than wrapping it — see the wrapper's `wipeShard`.
2109
+ */
2110
+ wipeShard?: (options?: {
2111
+ chunkSize?: number;
2112
+ exclude?: ReadonlyArray<string>;
2113
+ tables?: ReadonlyArray<string>;
2114
+ }) => Promise<{
2115
+ deleted: number;
2116
+ tables: Record<string, number>;
2117
+ }>;
1715
2118
  }
1716
2119
  /**
1717
- * What a procedure's `ctx.db` must structurally satisfy for the middleware
1718
- * to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
1719
- * rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
1720
- * the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
1721
- * and structural matching keeps this module free of an `@lunora/do`-typed
1722
- * `ctx`.
1723
- */
2120
+ * What a procedure's `ctx.db` must structurally satisfy for the middleware
2121
+ * to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
2122
+ * rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
2123
+ * the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
2124
+ * and structural matching keeps this module free of an `@lunora/do`-typed
2125
+ * `ctx`.
2126
+ */
1724
2127
  type RlsDatabase = DatabaseWriterLike;
1725
2128
  /** Roles list source on the context. Tolerant of older auth states. */
1726
2129
  type AuthLike = {
@@ -1735,21 +2138,183 @@ interface RlsContextIn {
1735
2138
  }
1736
2139
  declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
1737
2140
  /**
1738
- * Operations a storage rule can gate. `read` covers `download` / `getMetadata`
1739
- * / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
1740
- * `delete` is `delete`; `list` is a prefix listing (governed via the file
1741
- * browser / admin path, not `ctx.storage` which has no `list`).
1742
- */
2141
+ * A predicate matching **no** rows the deny decision.
2142
+ *
2143
+ * `{ OR: [] }` is a disjunction over zero branches, which the where-compiler
2144
+ * folds to a constant false. Use it (never a bare `{}`) for the unauthorized
2145
+ * branch of a shape or policy predicate:
2146
+ *
2147
+ * ```ts
2148
+ * where: (ctx, { userId }) => (ctx.auth.userId === userId ? { userId } : deny())
2149
+ * ```
2150
+ *
2151
+ * A fresh object per call, so a caller can safely spread or extend the result.
2152
+ */
2153
+ declare const deny: () => WhereInput;
2154
+ /**
2155
+ * A predicate matching **every** row — no restriction beyond whatever it is
2156
+ * AND-composed with. The honest spelling of "this branch adds no filter", where a
2157
+ * bare `{}` reads like an oversight.
2158
+ */
2159
+ declare const allowAll: () => WhereInput;
2160
+ /**
2161
+ * `true` when `where` is the vacuously-false deny predicate — i.e. an `OR` over
2162
+ * zero branches, carrying no other constraint. Used by the runtime to short-circuit
2163
+ * a denied read instead of compiling and running SQL that provably matches nothing.
2164
+ */
2165
+ declare const isDeny: (where: WhereInput) => boolean;
2166
+ /**
2167
+ * Normalize a boolean-or-predicate decision to a {@link WhereInput}.
2168
+ *
2169
+ * The authoring surfaces accept `true`/`false` as sugar — they read far better
2170
+ * than the sentinel objects in an ownership check — and every consumer downstream
2171
+ * wants a plain predicate. `undefined` means "no opinion" and maps to
2172
+ * {@link allowAll}, matching the RLS `PolicyDecision` contract.
2173
+ */
2174
+ declare const toWhereInput: (decision: WhereInput | boolean | undefined) => WhereInput;
2175
+ /**
2176
+ * One `rls()` tag's read policies for a table, paired with the role→permission
2177
+ * grants of that SAME middleware. Keeping the role map per-group is what lets a
2178
+ * policy's `auth.can(...)` resolve against its own middleware's roles — never a
2179
+ * permission registered on a different `rls()` step.
2180
+ */
2181
+ interface ScopedReadPolicies {
2182
+ readonly policies: ReadonlyArray<Policy>;
2183
+ readonly rolePermissions: ReadonlyMap<string, ReadonlySet<string>>;
2184
+ }
2185
+ /** Table-indexed read-policy groups, each scoped to the roles of the rls() middleware that declared it. */
2186
+ interface RlsReadRegistry {
2187
+ readonly byTable: ReadonlyMap<string, ReadonlyArray<ScopedReadPolicies>>;
2188
+ }
2189
+ /** The trusted, server-resolved facts a shape's RLS evaluation runs under. */
2190
+ interface ShapeReadWhereRequest {
2191
+ /** The shape ctx (the procedure context a policy `when` reads as `ctx`). */
2192
+ readonly ctx: unknown;
2193
+ /** Resolved identity claims (the socket's verified identity), or `null` when anonymous. */
2194
+ readonly identity: Record<string, unknown> | null;
2195
+ /** `true` when the schema is `.rls("required")` — gates the fail-closed branch. */
2196
+ readonly rlsRequired: boolean;
2197
+ /** Role labels the request carries (drives `auth.can(...)`). */
2198
+ readonly roles: ReadonlyArray<string>;
2199
+ /** The shape's own predicate (`where(ctx, args)`). */
2200
+ readonly shapeWhere: WhereInput;
2201
+ /** Logical table the shape replicates. */
2202
+ readonly table: string;
2203
+ /** `true` when the table is `.public()` (exempt from `.rls("required")` denial). */
2204
+ readonly tablePublic: boolean;
2205
+ /** Verified user id, or `null` when anonymous. */
2206
+ readonly userId: null | string;
2207
+ }
2208
+ /**
2209
+ * Build the read-policy registry from the registered functions (pass
2210
+ * `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
2211
+ * grouped per `rls()` middleware so each group keeps its own role→permission map
2212
+ * (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
2213
+ * several procedures (a shared `const guard = rls(...)`) is folded once. This
2214
+ * mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
2215
+ * resolves against the roles of the middleware that declared it, never a union.
2216
+ */
2217
+ declare const buildRlsReadRegistry: (functions: Iterable<unknown>) => RlsReadRegistry;
2218
+ /**
2219
+ * Compute the effective `where` a shape replicates: the table's RLS read
2220
+ * base-where AND the shape's own predicate. Returns the shape predicate
2221
+ * unchanged for a table with no read policy (a `.public()` or non-RLS table),
2222
+ * and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
2223
+ * exposes a protected, policy-less table.
2224
+ */
2225
+ declare const composeShapeReadWhere: (registry: RlsReadRegistry, request: ShapeReadWhereRequest) => WhereInput;
2226
+ /**
2227
+ * A shape declaration. `where` receives the trusted procedure context and the
2228
+ * validated client args and returns the same {@link WhereInput} shape the RLS
2229
+ * DSL uses, so the DO can AND-merge it with the table's read base-where via the
2230
+ * existing where-compiler (zero second predicate implementation).
2231
+ */
2232
+ interface ShapeDefinition<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> {
2233
+ /**
2234
+ * Validator for the client-supplied shape parameters. Validated on the DO
2235
+ * before `where` runs, so a malformed `args` envelope is rejected at the
2236
+ * subscription boundary rather than silently widening the partition. Omit
2237
+ * for a parameterless shape.
2238
+ */
2239
+ readonly args?: Args;
2240
+ /**
2241
+ * Project the replicated rows to these columns (the system columns `_id` and
2242
+ * `_creationTime` are always included). Omit to replicate every column. An
2243
+ * empty array is rejected — it would replicate no data, which is never the
2244
+ * intent.
2245
+ */
2246
+ readonly columns?: ReadonlyArray<string>;
2247
+ /**
2248
+ * Restrict this shape to rows the **subscriber owns**, without writing the
2249
+ * predicate by hand.
2250
+ *
2251
+ * `true` uses the owning column declared by `.ownedBy(field)` on the table;
2252
+ * `"field"` names the owning column here instead (for a table that has no
2253
+ * `.ownedBy()`, or a shape that scopes by a different column).
2254
+ *
2255
+ * The derived predicate is `{ [field]: ctx.auth.userId }`, AND-composed with
2256
+ * the shape's own `where` when both are present, and an anonymous subscriber is denied
2257
+ * outright. Because the value comes from the socket's verified identity rather
2258
+ * than from `args`, a client cannot ask for another user's partition at all —
2259
+ * strictly safer than accepting a `userId` arg and comparing it.
2260
+ *
2261
+ * This is the primitive for the "every shape repeats the same ownership check"
2262
+ * pattern: with it, an owner-scoped shape needs no `where` at all.
2263
+ */
2264
+ readonly owner?: string | true;
2265
+ /** Logical table this shape replicates a partition of. */
2266
+ readonly table: string;
2267
+ /**
2268
+ * Predicate selecting the rows this shape replicates. AND-composed with the
2269
+ * table's RLS read base-where on the DO. Runs server-side with a trusted
2270
+ * `ctx` (identity/auth the client can't forge) and the validated client
2271
+ * `args`; returns a {@link WhereInput} using the same operator set as the
2272
+ * SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
2273
+ *
2274
+ * `true`/`false` are accepted as sugar for "no further restriction" / "deny" —
2275
+ * `false` compiles to the vacuously-false predicate, so a denial branch can
2276
+ * never be mistyped as the everything-matches `{}`. Optional when `owner` is
2277
+ * set; otherwise required.
2278
+ */
2279
+ readonly where?: (context: Context, args: InferValidatorMap<Args>) => WhereInput | boolean;
2280
+ }
2281
+ /** A {@link ShapeDefinition} plus the codegen discovery marker and a dispatch-shaped `compileWhere`. */
2282
+ interface RegisteredShape<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> extends ShapeDefinition<Args, Context> {
2283
+ readonly __lunoraShape: true;
2284
+ /**
2285
+ * Validate `rawArgs`, then evaluate the shape's predicate under the trusted
2286
+ * `ctx` and return its {@link WhereInput}. Used by the generated DO's
2287
+ * `resolveShape` override: `ctx` is erased to `unknown` at this dispatch
2288
+ * boundary (the DO builds it from the socket's verified identity and hands it
2289
+ * back as the concrete {@link QueryContext} the predicate expects), exactly like
2290
+ * `RegisteredLunoraFunction.handler` erases its context.
2291
+ *
2292
+ * `options.ownerField` carries the table's `.ownedBy(field)` column, which only
2293
+ * the caller (the DO, holding the schema) can look up — it is what resolves an
2294
+ * `owner: true` shape. Omitted for an `owner: "field"` or plain `where` shape.
2295
+ */
2296
+ readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>, options?: {
2297
+ ownerField?: string;
2298
+ }) => WhereInput;
2299
+ }
2300
+ /** Declare a replication shape. See the module docs for runtime semantics. */
2301
+ declare const defineShape: <Args extends ValidatorMap = ValidatorMap, Context = QueryCtx>(definition: ShapeDefinition<Args, Context>) => RegisteredShape<Args, Context>;
2302
+ /**
2303
+ * Operations a storage rule can gate. `read` covers `download` / `getMetadata`
2304
+ * / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
2305
+ * `delete` is `delete`; `list` is a prefix listing (governed via the file
2306
+ * browser / admin path, not `ctx.storage` which has no `list`).
2307
+ */
1743
2308
  type StorageOperation = "delete" | "list" | "read" | "write";
1744
2309
  /** A rule's decision. `true` allows, `false` denies, `undefined` opts this rule out. */
1745
2310
  type StorageRuleDecision = boolean | undefined;
1746
2311
  /**
1747
- * Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
1748
- * (the per-request userId / roles / identity and the `can(permission)` helper),
1749
- * so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
1750
- * `key` is the object key the operation targets (for `list`, the listing
1751
- * prefix). `ctx` is the full procedure context the middleware closed over.
1752
- */
2312
+ * Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
2313
+ * (the per-request userId / roles / identity and the `can(permission)` helper),
2314
+ * so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
2315
+ * `key` is the object key the operation targets (for `list`, the listing
2316
+ * prefix). `ctx` is the full procedure context the middleware closed over.
2317
+ */
1753
2318
  interface StorageRuleContext<Context = unknown> {
1754
2319
  readonly auth: {
1755
2320
  readonly can: (permission: Permission | string) => boolean;
@@ -1764,11 +2329,11 @@ interface StorageRuleContext<Context = unknown> {
1764
2329
  /** A registered storage rule as stored in the rule table. */
1765
2330
  interface StorageRule<Context = unknown> {
1766
2331
  /**
1767
- * Logical bucket the rule governs — matched against the accessor's bucket
1768
- * (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
1769
- * A rule only applies to operations on its own bucket. The unnamed bucket is
1770
- * `"default"`. Also surfaced in the studio's access-rules view.
1771
- */
2332
+ * Logical bucket the rule governs — matched against the accessor's bucket
2333
+ * (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
2334
+ * A rule only applies to operations on its own bucket. The unnamed bucket is
2335
+ * `"default"`. Also surfaced in the studio's access-rules view.
2336
+ */
1772
2337
  readonly bucket: string;
1773
2338
  readonly on: StorageOperation;
1774
2339
  /** Optional key-prefix scope; the rule only governs keys under it. Absent ⇒ the whole bucket. */
@@ -1783,27 +2348,27 @@ interface DefineStorageRuleInput<Context = unknown> {
1783
2348
  when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
1784
2349
  }
1785
2350
  /**
1786
- * Options for the `storageRules(rules, options)` middleware. `roles` registers
1787
- * the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
1788
- * `RlsOptions.roles` does — fail-closed for unlisted roles.
1789
- */
2351
+ * Options for the `storageRules(rules, options)` middleware. `roles` registers
2352
+ * the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
2353
+ * `RlsOptions.roles` does — fail-closed for unlisted roles.
2354
+ */
1790
2355
  interface StorageRulesOptions {
1791
2356
  readonly roles?: ReadonlyArray<Role>;
1792
2357
  }
1793
2358
  declare const defineStorageRule: <Context = unknown>(input: DefineStorageRuleInput<Context>) => StorageRule<Context>;
1794
2359
  /**
1795
- * Collect a list of storage rules into the structure the `storageRules()`
1796
- * middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
1797
- * any one allowing grants the operation (each rule grants a slice of the
1798
- * keyspace).
1799
- *
1800
- * Validates against an **accidentally duplicated rule** — the same
1801
- * `(bucket, on, prefix)` registered with the *same* decision function (a
1802
- * copy-paste, or the same rule object spread in twice). Because multiple
1803
- * DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
1804
- * `when` reference too. Throws at module load so the misconfiguration surfaces
1805
- * immediately rather than as a silently double-evaluated predicate.
1806
- */
2360
+ * Collect a list of storage rules into the structure the `storageRules()`
2361
+ * middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
2362
+ * any one allowing grants the operation (each rule grants a slice of the
2363
+ * keyspace).
2364
+ *
2365
+ * Validates against an **accidentally duplicated rule** — the same
2366
+ * `(bucket, on, prefix)` registered with the *same* decision function (a
2367
+ * copy-paste, or the same rule object spread in twice). Because multiple
2368
+ * DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
2369
+ * `when` reference too. Throws at module load so the misconfiguration surfaces
2370
+ * immediately rather than as a silently double-evaluated predicate.
2371
+ */
1807
2372
  declare const defineStorageRules: <Context = unknown>(rules: ReadonlyArray<StorageRule<Context>>) => ReadonlyArray<StorageRule<Context>>;
1808
2373
  /** 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`). */
1809
2374
  type StorageAuthLike = {
@@ -1817,4 +2382,4 @@ interface StorageContextIn {
1817
2382
  }
1818
2383
  declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
1819
2384
  declare const VERSION = "0.0.0";
1820
- 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 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 RegisteredQuery, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type Role, type Schema, type SchemaExtension, 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, composePluginMiddleware, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineMigration, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
2385
+ export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, type DataModelInit, type DefineComponentOptions, type DefineIdentityOptions, type DefineListArgsConfig, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExposeConfig, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type IdentityContract, type IdentityRejectMode, type IdentityValidation, type InferArgs, type InferEnv, type InferIdentity, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type ListArgsSpec, type ListArgsValidators, type ListArgsValue, type ListFilterOperators, type ListOrderByEntry, type ListWhere, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationDefinition, type MigrationDocument, type MigrationTransform, type MutationBuilder, type MutationCtx, type MutatorDefinition, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, allowAll, asBucketStorage, bindOrm, bindTableFacade, buildRlsReadRegistry, clampLimit, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineIdentity, defineListArgs, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, deny, httpAction, httpRoute, httpRouter, initLunora, installPlugins, isDeny, isSafeHeaderValue, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules, toWhereInput };