@lunora/server 1.0.0-alpha.9 → 1.0.0-alpha.91

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