@lunora/server 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (41) hide show
  1. package/LICENSE.md +6 -0
  2. package/README.md +50 -0
  3. package/dist/data-model.d.mts +231 -156
  4. package/dist/data-model.d.ts +231 -156
  5. package/dist/index.d.mts +1191 -811
  6. package/dist/index.d.ts +1191 -811
  7. package/dist/index.mjs +20 -15
  8. package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
  9. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
  10. package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs → PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs} +5 -5
  11. package/dist/packem_shared/{bindOrm-Ce57S3N9.mjs → bindOrm-CaY7Wq9Z.mjs} +42 -23
  12. package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +107 -0
  13. package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-z62dttBo.mjs} +11 -8
  14. package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
  15. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
  16. package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs} +108 -8
  17. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
  18. package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
  19. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
  20. package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
  21. package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
  22. package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
  23. package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
  24. package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-D0Wuki7S.mjs} +36 -7
  25. package/dist/packem_shared/{mask-eCUYOwhd.mjs → mask-BepaW7YN.mjs} +132 -9
  26. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
  27. package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
  28. package/dist/packem_shared/{rls-Bi9HiyDC.mjs → rls-BaDQf7MG.mjs} +34 -6
  29. package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
  30. package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
  31. package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
  32. package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
  33. package/dist/rls/testing.d.mts +31 -31
  34. package/dist/rls/testing.d.ts +31 -31
  35. package/dist/rls/testing.mjs +1 -1
  36. package/dist/types.d.mts +957 -433
  37. package/dist/types.d.ts +957 -433
  38. package/package.json +6 -5
  39. package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
  40. package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
  41. package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/dist/types.d.mts CHANGED
@@ -5,6 +5,14 @@ type ArgsValidator = ValidatorMap;
5
5
  type InferArgs<A extends ArgsValidator> = InferValidatorMap<A>;
6
6
  /** Storage backend for a `.global()` table: D1 (default) or a Postgres/MySQL database via Cloudflare Hyperdrive (PlanetScale, Neon, …). */
7
7
  type GlobalBackend = "d1" | "hyperdrive";
8
+ /**
9
+ * Cloudflare Durable Object data-residency jurisdiction declared via
10
+ * `defineSchema(...).jurisdiction("…")`. Restricts where every DO the app
11
+ * reaches runs and persists data (GDPR, FedRAMP, US data residency). Widening
12
+ * union — Cloudflare adds values over time.
13
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
14
+ */
15
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
8
16
  /** How a table is routed at runtime. */
9
17
  type ShardMode = {
10
18
  backend?: GlobalBackend;
@@ -15,6 +23,90 @@ type ShardMode = {
15
23
  } | {
16
24
  kind: "root";
17
25
  };
26
+ /** Poll cadence for a sourced table — `"manual"` (pull only on an explicit trigger) or a fixed interval. */
27
+ type ExternalSourceRefresh = "manual" | {
28
+ everyMs: number;
29
+ };
30
+ /**
31
+ * Delete-detection mode for external-source ingest (plan 077 / 136).
32
+ *
33
+ * `"full-pull"` (the default) reads the **whole** tenant membership each tick and
34
+ * diffs it, so it observes upstream deletes for free — but costs a full read per tick
35
+ * (the Phase-0 bench put the ceiling at ~10k rows).
36
+ *
37
+ * `"incremental"` pulls **only rows past a durable watermark** (`cursor`), cheap for
38
+ * large low-churn tables above the full-pull cap. Because an absent row then means
39
+ * "unchanged", not "deleted", incremental requires a delete-visibility path: either a
40
+ * `reconcileEveryMs` periodic full-pull sweep, or a `softDeleteColumn` whose
41
+ * tombstones the pull returns. `defineSchema` throws (and the
42
+ * `external_source_incremental_no_delete_path` advisor lint fails the build) when an
43
+ * incremental source declares neither.
44
+ */
45
+ type ExternalSourceMode = "full-pull" | "incremental";
46
+ /**
47
+ * Incremental-ingest cursor (plan 136): the monotonic watermark column plus the
48
+ * watermark-parameterized pull query. `column` names the field in the pulled rows
49
+ * whose max becomes the next watermark (e.g. `"updated_at"`). `query` is a second
50
+ * SQL that returns only rows changed since the watermark — the watermark binds as
51
+ * the parameter AFTER `tenantBy`'s params (e.g. Postgres
52
+ * `... WHERE tenant_id = $1 AND updated_at >= $2 ORDER BY updated_at`). Prefer `>=`
53
+ * with the idempotent upsert apply so rows sharing the boundary timestamp are never
54
+ * skipped (re-pulling them is a no-op).
55
+ */
56
+ interface ExternalSourceCursor {
57
+ /** The monotonic watermark column in the pulled rows; its max advances the stored watermark. */
58
+ column: string;
59
+ /** The incremental pull SQL. `tenantBy`'s params bind first, then the watermark as the trailing param. */
60
+ query: string;
61
+ }
62
+ /**
63
+ * Config for `.source(...)` (plan 077): declares a table as **materialized from an
64
+ * external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
65
+ * mutations. A system-driven poll loop reads the tenant slice and lands it in the
66
+ * DO's SQLite (via the validated CDC writer), after which `defineShape` carries it
67
+ * to clients unchanged. Orthogonal to `shardMode` — a sourced table almost always
68
+ * also `.shardBy()`s, in which case `tenantBy` is the mandatory tenant-isolation
69
+ * boundary (enforced by the `external_source_unscoped` advisor lint).
70
+ */
71
+ interface ExternalSourceDefinition {
72
+ /** The wrangler Hyperdrive binding name the poll loop reads from. */
73
+ binding: string;
74
+ /** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
75
+ columns?: ReadonlyArray<string>;
76
+ /** **Required for `mode: "incremental"`**: the watermark column + watermark-parameterized pull query (plan 136). Rejected on a `"full-pull"` source. */
77
+ cursor?: ExternalSourceCursor;
78
+ /** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
79
+ idColumn?: string;
80
+ /** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
81
+ map?: (row: Record<string, unknown>) => Record<string, unknown>;
82
+ /** Delete-detection mode. `"full-pull"` (the default) diffs the whole membership; `"incremental"` pulls past a `cursor` watermark. */
83
+ mode?: ExternalSourceMode;
84
+ /** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
85
+ query: string;
86
+ /**
87
+ * **Incremental delete-visibility (plan 136)**: run a full-pull sweep at most
88
+ * this often (millis) to GC upstream deletes an incremental slice can't see.
89
+ * One of `reconcileEveryMs` / `softDeleteColumn` is required for incremental;
90
+ * rejected on a `"full-pull"` source.
91
+ */
92
+ reconcileEveryMs?: number;
93
+ /** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
94
+ refresh?: ExternalSourceRefresh;
95
+ /**
96
+ * **Incremental delete-visibility (plan 136)**: the upstream soft-delete
97
+ * tombstone column (e.g. `"deleted_at"`). When set, the incremental pull must
98
+ * return tombstoned rows and the ingest turns each into a local delete — an
99
+ * alternative to `reconcileEveryMs`. Rejected on a `"full-pull"` source.
100
+ */
101
+ softDeleteColumn?: string;
102
+ /**
103
+ * **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
104
+ * params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
105
+ * sharded table replicates the whole multitenant table into every shard — the
106
+ * `external_source_unscoped` advisor lint fails the build when this is absent.
107
+ */
108
+ tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
109
+ }
18
110
  interface IndexDefinition {
19
111
  fields: ReadonlyArray<string>;
20
112
  name: string;
@@ -25,21 +117,48 @@ interface SearchIndexDefinition {
25
117
  filterFields?: ReadonlyArray<string>;
26
118
  name: string;
27
119
  }
120
+ /**
121
+ * A geospatial index declared via `.geoIndex(name, { field })`. The runtime
122
+ * maintains a geohash companion table over the `v.geoPoint()` column `field` so
123
+ * `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves a
124
+ * proximity / bounding-box read as a geohash-prefix range scan plus a Haversine
125
+ * refine/sort on the candidate rows.
126
+ *
127
+ * - `field` — the `v.geoPoint()` column whose lat/lng feed the geohash.
128
+ * - `precision` — geohash character length on the companion (default 9, ~4.8 m cells); higher precision narrows each cell.
129
+ */
130
+ interface GeoIndexDefinition {
131
+ field: string;
132
+ name: string;
133
+ precision?: number;
134
+ }
135
+ /**
136
+ * Declarative table-level TTL declared via `.ttl(field, { after? })`. A DO
137
+ * alarm-driven sweep deletes (or, when the table also `.softDelete()`s,
138
+ * soft-deletes) rows whose expiry timestamp has passed.
139
+ *
140
+ * - `field` — an epoch-millisecond column. Without `after`, its value is the absolute expiry instant; with `after`, `field` is a base timestamp and the row expires `after` milliseconds later (`field + after`).
141
+ * - `after` — optional millisecond offset added to `field` to derive the expiry.
142
+ */
143
+ interface TtlDefinition {
144
+ after?: number;
145
+ field: string;
146
+ }
28
147
  /** Reducer applied by an aggregate index. */
29
148
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
30
149
  /**
31
- * Declared aggregate index — the schema-level seam that lets the runtime keep
32
- * O(1) counters/sums in step with row writes (via the trigger runner) and
33
- * route matching reads through them.
34
- *
35
- * - `on` — the table whose rows feed the aggregate.
36
- * - `op` — the reducer. `count` is field-less; the others take `field`.
37
- * - `field` — the column the reducer applies to (required for non-count ops).
38
- * - `by` — group keys. When all `where` keys in a read participate in `by`, the
39
- * reader can answer from the counter table without scanning rows.
40
- * - `where` — optional static predicate baked into the counter (only the rows
41
- * matching it ever land in the counter).
42
- */
150
+ * Declared aggregate index — the schema-level seam that lets the runtime keep
151
+ * O(1) counters/sums in step with row writes (via the trigger runner) and
152
+ * route matching reads through them.
153
+ *
154
+ * - `on` — the table whose rows feed the aggregate.
155
+ * - `op` — the reducer. `count` is field-less; the others take `field`.
156
+ * - `field` — the column the reducer applies to (required for non-count ops).
157
+ * - `by` — group keys. When all `where` keys in a read participate in `by`, the
158
+ * reader can answer from the counter table without scanning rows.
159
+ * - `where` — optional static predicate baked into the counter (only the rows
160
+ * matching it ever land in the counter).
161
+ */
43
162
  interface AggregateIndexDefinition {
44
163
  by?: ReadonlyArray<string>;
45
164
  field?: string;
@@ -49,32 +168,32 @@ interface AggregateIndexDefinition {
49
168
  where?: Record<string, unknown>;
50
169
  }
51
170
  /**
52
- * One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
53
- * direction. The runtime breaks ties on the row's `_id` ASC so the order is
54
- * total and `rank()` always returns a deterministic 1-based position.
55
- */
171
+ * One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
172
+ * direction. The runtime breaks ties on the row's `_id` ASC so the order is
173
+ * total and `rank()` always returns a deterministic 1-based position.
174
+ */
56
175
  interface RankSortKey {
57
176
  direction: "asc" | "desc";
58
177
  field: string;
59
178
  }
60
179
  /**
61
- * Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
62
- * maintained by triggers, so:
63
- *
64
- * - `rank(row)` returns the row's 1-based position within its partition under
65
- * the declared `sortBy` order, plus the partition's total row count, in
66
- * O(log n) lookups against the SQLite btree on the companion table.
67
- * - `rankPage({ where, take, from })` walks the same companion table to return
68
- * rows in the declared order — a sorted-pagination accelerator.
69
- *
70
- * Fields mirror `AggregateIndexDefinition`:
71
- *
72
- * - `on` — the source table whose rows feed the rank.
73
- * - `sortBy` — ordered keys driving the rank. Required.
74
- * - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
75
- * to rank within a channel). Omitted ⇒ one global rank across the table.
76
- * - `where` — static predicate baked into the index; only matching rows enter.
77
- */
180
+ * Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
181
+ * maintained by triggers, so:
182
+ *
183
+ * - `rank(row)` returns the row's 1-based position within its partition under
184
+ * the declared `sortBy` order, plus the partition's total row count, in
185
+ * O(log n) lookups against the SQLite btree on the companion table.
186
+ * - `rankPage({ where, take, from })` walks the same companion table to return
187
+ * rows in the declared order — a sorted-pagination accelerator.
188
+ *
189
+ * Fields mirror `AggregateIndexDefinition`:
190
+ *
191
+ * - `on` — the source table whose rows feed the rank.
192
+ * - `sortBy` — ordered keys driving the rank. Required.
193
+ * - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
194
+ * to rank within a channel). Omitted ⇒ one global rank across the table.
195
+ * - `where` — static predicate baked into the index; only matching rows enter.
196
+ */
78
197
  interface RankIndexDefinition {
79
198
  name: string;
80
199
  on: string;
@@ -85,16 +204,16 @@ interface RankIndexDefinition {
85
204
  /** FK behavior when a referenced parent row is deleted (mirrors SQL `ON DELETE`). */
86
205
  type OnDeleteAction = "cascade" | "restrict" | "set null";
87
206
  /**
88
- * A declared relation between two tables, recorded by `.relations((r) => …)`.
89
- *
90
- * - `one` (many-to-one): the FK column `field` lives on **this** table and
91
- * points at `table`.`references` (default `_id`). Loads a single doc.
92
- * - `many` (one-to-many): the FK column `field` lives on the **target** table
93
- * and points back at this table's `references` (default `_id`). Loads an array.
94
- *
95
- * `onDelete` is meaningful only on `one`: it is the action applied to the
96
- * holder rows when the referenced parent row is deleted.
97
- */
207
+ * A declared relation between two tables, recorded by `.relations((r) => …)`.
208
+ *
209
+ * - `one` (many-to-one): the FK column `field` lives on **this** table and
210
+ * points at `table`.`references` (default `_id`). Loads a single doc.
211
+ * - `many` (one-to-many): the FK column `field` lives on the **target** table
212
+ * and points back at this table's `references` (default `_id`). Loads an array.
213
+ *
214
+ * `onDelete` is meaningful only on `one`: it is the action applied to the
215
+ * holder rows when the referenced parent row is deleted.
216
+ */
98
217
  interface RelationDefinition {
99
218
  field: string;
100
219
  kind: "many" | "one";
@@ -105,15 +224,15 @@ interface RelationDefinition {
105
224
  /** Distance metric used by a Vectorize index. */
106
225
  type VectorMetric = "cosine" | "dot-product" | "euclidean";
107
226
  /**
108
- * Bring-your-own-embedder: a user-supplied fn turning a source string into a
109
- * numeric vector. The runtime calls it at upsert/query time so the framework
110
- * never couples to a single embedding provider.
111
- */
227
+ * Bring-your-own-embedder: a user-supplied fn turning a source string into a
228
+ * numeric vector. The runtime calls it at upsert/query time so the framework
229
+ * never couples to a single embedding provider.
230
+ */
112
231
  type VectorEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
113
232
  /**
114
- * Vector index declared inline on a table via `.vectorize(field, opts)`
115
- * (DSL Shape A). The source is always a single column on the owning table.
116
- */
233
+ * Vector index declared inline on a table via `.vectorize(field, opts)`
234
+ * (DSL Shape A). The source is always a single column on the owning table.
235
+ */
117
236
  interface TableVectorIndex {
118
237
  dimensions: number;
119
238
  embed: VectorEmbedder;
@@ -124,74 +243,99 @@ interface TableVectorIndex {
124
243
  }
125
244
  interface TableDefinition<Shape extends Record<string, Validator> = Record<string, Validator>> {
126
245
  /**
127
- * Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
128
- * maintains a counter row per `by` group via the trigger seam, so reads
129
- * whose `where` keys all participate in the index's `by` set are answered
130
- * without scanning the underlying table.
131
- */
246
+ * Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
247
+ * maintains a counter row per `by` group via the trigger seam, so reads
248
+ * whose `where` keys all participate in the index's `by` set are answered
249
+ * without scanning the underlying table.
250
+ */
132
251
  aggregateIndexes: ReadonlyArray<AggregateIndexDefinition>;
252
+ /**
253
+ * Set by `.source(...)` (named `externalSource`, not `source`, so the data
254
+ * field doesn't collide with the fluent `.source()` builder method — same
255
+ * convention as `shardBy()`/`shardMode`). When present, the table is
256
+ * materialized from an external Hyperdrive-backed database by a system poll
257
+ * loop rather than user mutations. Implies `isExternallyManaged`.
258
+ */
259
+ externalSource?: ExternalSourceDefinition;
260
+ /**
261
+ * Geospatial indexes declared via `.geoIndex(name, { field })`. The runtime
262
+ * maintains a geohash companion over the named `v.geoPoint()` column so
263
+ * `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves
264
+ * a proximity/bounding-box read as a geohash-prefix range scan plus a
265
+ * Haversine refine/sort. Empty unless `.geoIndex()` was called.
266
+ */
267
+ geoIndexes: ReadonlyArray<GeoIndexDefinition>;
133
268
  indexes: ReadonlyArray<IndexDefinition>;
134
269
  /**
135
- * `true` when `.externallyManaged()` was called — the table's rows are
136
- * written outside Lunora's discoverable insert path (an adapter, a
137
- * migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
138
- * tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
139
- * (`table_without_insert`) skip such tables instead of flagging the absent
140
- * `ctx.db.insert(...)`.
141
- */
270
+ * `true` when `.externallyManaged()` was called — the table's rows are
271
+ * written outside Lunora's discoverable insert path (an adapter, a
272
+ * migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
273
+ * tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
274
+ * (`table_without_insert`) skip such tables instead of flagging the absent
275
+ * `ctx.db.insert(...)`.
276
+ */
142
277
  isExternallyManaged?: boolean;
143
278
  /**
144
- * `true` when `.public()` was called — the table opts OUT of secure-by-default
145
- * RLS. Under a schema marked `.rls("required")`, every table is protected (the
146
- * DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
147
- * Has no effect when the schema does not require RLS.
148
- */
279
+ * `true` when `.public()` was called — the table opts OUT of secure-by-default
280
+ * RLS. Under a schema marked `.rls("required")`, every table is protected (the
281
+ * DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
282
+ * Has no effect when the schema does not require RLS.
283
+ */
149
284
  isPublic?: boolean;
150
285
  /**
151
- * Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
152
- * a sorted companion table per declared rank with a btree on
153
- * `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
154
- * within its partition in O(log n), and `rankPage()` walks the index for
155
- * sorted pagination.
156
- */
286
+ * Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
287
+ * a sorted companion table per declared rank with a btree on
288
+ * `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
289
+ * within its partition in O(log n), and `rankPage()` walks the index for
290
+ * sorted pagination.
291
+ */
157
292
  rankIndexes: ReadonlyArray<RankIndexDefinition>;
158
293
  /**
159
- * Declared relations keyed by accessor name; empty unless `.relations()`
160
- * was called. Named `relationMap` (not `relations`) so the fluent
161
- * `.relations((r) => …)` builder method doesn't collide with this field.
162
- */
294
+ * Declared relations keyed by accessor name; empty unless `.relations()`
295
+ * was called. Named `relationMap` (not `relations`) so the fluent
296
+ * `.relations((r) => …)` builder method doesn't collide with this field.
297
+ */
163
298
  relationMap: Record<string, RelationDefinition>;
164
299
  searchIndexes: ReadonlyArray<SearchIndexDefinition>;
165
300
  shape: Shape;
166
301
  shardMode: ShardMode;
167
302
  /**
168
- * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
169
- * data field doesn't collide with the fluent `.softDelete()` builder method —
170
- * same convention as `shardBy()`/`shardMode`). When present, the table carries
171
- * a nullable timestamp column (`field`, default `deletedAt`):
172
- * `ctx.db.&lt;table>.delete()` flips it instead of physically removing the row,
173
- * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
174
- * relation loads) hide rows whose `field` is set unless
175
- * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
176
- * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
177
- */
303
+ * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
304
+ * data field doesn't collide with the fluent `.softDelete()` builder method —
305
+ * same convention as `shardBy()`/`shardMode`). When present, the table carries
306
+ * a nullable timestamp column (`field`, default `deletedAt`):
307
+ * `ctx.db.&lt;table>.delete()` flips it instead of physically removing the row,
308
+ * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
309
+ * relation loads) hide rows whose `field` is set unless
310
+ * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
311
+ * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
312
+ */
178
313
  softDeleteMode?: {
179
314
  field: string;
180
315
  };
181
316
  /**
182
- * Declared lifecycle triggers keyed by accessor name; empty unless
183
- * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
184
- * fluent `.triggers((t) => …)` builder method doesn't collide with this
185
- * field — same reasoning as {@link TableDefinition.relationMap}.
186
- */
317
+ * Declared lifecycle triggers keyed by accessor name; empty unless
318
+ * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
319
+ * fluent `.triggers((t) => …)` builder method doesn't collide with this
320
+ * field — same reasoning as {@link TableDefinition.relationMap}.
321
+ */
187
322
  triggerMap: Record<string, TriggerDefinition>;
323
+ /**
324
+ * Set by `.ttl(field, { after })` — the declarative auto-expiry policy. A DO
325
+ * alarm-driven sweep deletes rows whose expiry timestamp has passed (or
326
+ * soft-deletes them when the table also `.softDelete()`s). Named `ttlPolicy`
327
+ * (a data field) rather than colliding with the fluent `.ttl()` builder
328
+ * method — same convention as `shardBy()`/`shardMode`. Absent ⇒ rows never
329
+ * auto-expire.
330
+ */
331
+ ttlPolicy?: TtlDefinition;
188
332
  vectorIndexes: ReadonlyArray<TableVectorIndex>;
189
333
  }
190
334
  /**
191
- * Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
192
- * Unlike {@link TableVectorIndex}, the source is a `select` function so it can
193
- * derive the embedded text from any computation (e.g. `title + body`).
194
- */
335
+ * Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
336
+ * Unlike {@link TableVectorIndex}, the source is a `select` function so it can
337
+ * derive the embedded text from any computation (e.g. `title + body`).
338
+ */
195
339
  interface VectorIndexDefinition {
196
340
  readonly dimensions: number;
197
341
  readonly embed: VectorEmbedder;
@@ -203,38 +347,78 @@ interface VectorIndexDefinition {
203
347
  }
204
348
  interface Schema<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
205
349
  /**
206
- * Secure-by-default RLS mode declared via `.rls("required")`. When
207
- * `"required"`, every table is protected: the DO/D1 write path denies raw
208
- * (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
209
- * `.use(rls(...))` fails closed instead of silently exposing the table. A
210
- * table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
211
- * Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
212
- */
350
+ * Secure-by-default RLS mode declared via `.rls("required")`. When
351
+ * `"required"`, every table is protected: the DO/D1 write path denies raw
352
+ * (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
353
+ * `.use(rls(...))` fails closed instead of silently exposing the table. A
354
+ * table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
355
+ * Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
356
+ */
213
357
  readonly rlsMode?: "required";
214
358
  readonly tables: T;
215
359
  readonly vectorIndexes: Record<string, VectorIndexDefinition>;
216
360
  }
217
361
  type FunctionKind = "action" | "mutation" | "query" | "stream";
218
362
  /**
219
- * Call surface a function is exposed on. `public` functions are reachable from
220
- * clients via the generated `api`; `internal` functions are reachable only
221
- * server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
222
- * by the DO's external RPC path. Absence is treated as `public` for
223
- * back-compat with functions registered before visibility existed.
224
- */
363
+ * Call surface a function is exposed on. `public` functions are reachable from
364
+ * clients via the generated `api`; `internal` functions are reachable only
365
+ * server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
366
+ * by the DO's external RPC path. Absence is treated as `public` for
367
+ * back-compat with functions registered before visibility existed.
368
+ */
225
369
  type FunctionVisibility = "internal" | "public";
370
+ /**
371
+ * x402 payment tag attached by the `.x402({ price })` builder modifier. Marks a
372
+ * public procedure as paid: the origin worker answers an unpaid client RPC with
373
+ * HTTP 402, verifies + settles the payment, and only then dispatches to the
374
+ * shard. The runtime reads only `price` from here — the network, recipient, and
375
+ * facilitator live in the worker-level x402 charge config, so `@lunora/runtime`
376
+ * never has to import `@lunora/x402` (and its viem/solana deps).
377
+ */
378
+ interface X402ProcedureConfig {
379
+ /**
380
+ * USD-denominated price: a number of dollars (`0.01`) or a decimal string
381
+ * (`"0.01"`, or the `"$0.01"` shorthand). Resolved to the network
382
+ * stablecoin's base units (USDC has 6 decimals) at challenge time.
383
+ */
384
+ readonly price: number | string;
385
+ }
386
+ /**
387
+ * Opt-in public-surface tag attached by the `.expose({ rest: true })` builder
388
+ * modifier (plan 167). Marks a procedure as deliberately published over the
389
+ * public REST surface: the runtime mints a `/_lunora/rest/&lt;namespace>/&lt;fn>` route
390
+ * that dispatches THROUGH the procedure (so `ctx.auth` / RLS / validators are
391
+ * enforced), and the generated OpenAPI describes it. Everything is default-closed
392
+ * — a procedure without this tag is unreachable over REST.
393
+ */
394
+ interface ExposeConfig {
395
+ /** Publish this procedure over the public REST surface. */
396
+ readonly rest?: boolean;
397
+ }
226
398
  interface RegisteredFunction<A extends ArgsValidator, R, Kind extends FunctionKind> {
227
399
  readonly args: A;
400
+ /**
401
+ * Set by the `.expose({ rest: true })` builder modifier. Marks the procedure
402
+ * as published on the public REST surface (plan 167). Absent on procedures that
403
+ * are reachable only via typed RPC (the default).
404
+ */
405
+ readonly expose?: ExposeConfig;
228
406
  readonly handler: (context: unknown, args: InferArgs<A>) => Promise<R> | R;
229
407
  readonly kind: Kind;
230
408
  /**
231
- * Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`).
232
- * Marks the function for the generated `LUNORA_LIFECYCLE_HOOKS` manifest so the
233
- * DO dispatches it on socket connect/disconnect rather than via a client RPC.
234
- * Absent on ordinary registrations.
235
- */
409
+ * Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`).
410
+ * Marks the function for the generated `LUNORA_LIFECYCLE_HOOKS` manifest so the
411
+ * DO dispatches it on socket connect/disconnect rather than via a client RPC.
412
+ * Absent on ordinary registrations.
413
+ */
236
414
  readonly lifecycle?: LifecycleEventKind;
237
415
  readonly visibility?: FunctionVisibility;
416
+ /**
417
+ * Set by the `.x402({ price })` builder modifier. Marks the procedure as paid
418
+ * so the origin worker gates it behind an x402 402-challenge before dispatch.
419
+ * Absent on unpaid functions.
420
+ */
421
+ readonly x402?: X402ProcedureConfig;
238
422
  }
239
423
  type RegisteredQuery<A extends ArgsValidator, R> = RegisteredFunction<A, R, "query">;
240
424
  type RegisteredMutation<A extends ArgsValidator, R> = RegisteredFunction<A, R, "mutation">;
@@ -242,11 +426,11 @@ type RegisteredAction<A extends ArgsValidator, R> = RegisteredFunction<A, R, "ac
242
426
  /** Which side of the WebSocket lifecycle a hook fires on. */
243
427
  type LifecycleEventKind = "connect" | "disconnect";
244
428
  /**
245
- * The event a connection-lifecycle hook receives as its second argument. It is
246
- * the JSON-serializable payload the DO forwards on socket connect/disconnect;
247
- * the verified caller identity is also reflected on `ctx.auth` (the hook runs
248
- * under the connecting user via `resolveIdentity`).
249
- */
429
+ * The event a connection-lifecycle hook receives as its second argument. It is
430
+ * the JSON-serializable payload the DO forwards on socket connect/disconnect;
431
+ * the verified caller identity is also reflected on `ctx.auth` (the hook runs
432
+ * under the connecting user via `resolveIdentity`).
433
+ */
250
434
  interface LifecycleEvent {
251
435
  /** Stable per-socket id, minted at upgrade and replayed verbatim on disconnect. */
252
436
  readonly connectionId: string;
@@ -258,20 +442,20 @@ interface LifecycleEvent {
258
442
  readonly userId: string | null;
259
443
  }
260
444
  /**
261
- * A registered connection-lifecycle hook — an internal mutation tagged with the
262
- * lifecycle side it fires on. Produced by `onConnect` / `onDisconnect`.
263
- */
445
+ * A registered connection-lifecycle hook — an internal mutation tagged with the
446
+ * lifecycle side it fires on. Produced by `onConnect` / `onDisconnect`.
447
+ */
264
448
  type RegisteredLifecycleHook = RegisteredFunction<Record<string, never>, void, "mutation"> & {
265
449
  readonly lifecycle: LifecycleEventKind;
266
450
  };
267
451
  /**
268
- * A streaming query registration. Unlike {@link RegisteredFunction} the handler
269
- * returns an `AsyncIterable&lt;R>` synchronously (it does NOT `Promise&lt;R>`); the
270
- * runtime drives it frame by frame and forwards each chunk to the caller. The
271
- * third `signal` argument is wired to the caller's cancel signal so the handler
272
- * can stop early — break out of the loop or check `signal.aborted` between
273
- * yields.
274
- */
452
+ * A streaming query registration. Unlike {@link RegisteredFunction} the handler
453
+ * returns an `AsyncIterable&lt;R>` synchronously (it does NOT `Promise&lt;R>`); the
454
+ * runtime drives it frame by frame and forwards each chunk to the caller. The
455
+ * third `signal` argument is wired to the caller's cancel signal so the handler
456
+ * can stop early — break out of the loop or check `signal.aborted` between
457
+ * yields.
458
+ */
275
459
  interface RegisteredStream<A extends ArgsValidator, R> {
276
460
  readonly args: A;
277
461
  readonly handler: (context: unknown, args: InferArgs<A>, signal: AbortSignal) => AsyncIterable<R>;
@@ -281,10 +465,10 @@ interface RegisteredStream<A extends ArgsValidator, R> {
281
465
  /** The system tables `ctx.db.system` can read. */
282
466
  type SystemTableName = "_scheduled_functions" | "_storage";
283
467
  /**
284
- * A pending scheduled invocation as surfaced by the `_scheduled_functions`
285
- * system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
286
- * separate name keeps the system-table read surface self-describing.
287
- */
468
+ * A pending scheduled invocation as surfaced by the `_scheduled_functions`
469
+ * system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
470
+ * separate name keeps the system-table read surface self-describing.
471
+ */
288
472
  interface ScheduledFunctionDoc {
289
473
  /** Function arguments the job will be dispatched with. */
290
474
  args: Record<string, unknown>;
@@ -314,55 +498,55 @@ interface SystemQuery<T extends SystemTableName> {
314
498
  collect: () => Promise<SystemDoc<T>[]>;
315
499
  }
316
500
  /**
317
- * Read-only reader over Lunora's system tables (`_scheduled_functions`,
318
- * `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
319
- *
320
- * **Best-effort and eventually consistent.** Unlike `ctx.db.&lt;table>` — which
321
- * reads the shard's transactional SQLite snapshot — the data behind these tables
322
- * lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
323
- * objects in R2). Every `collect()` / `get()` reaches across to that source.
324
- *
325
- * It is **not part of the mutation transaction snapshot** (no OCC guard, no
326
- * subscription dependency recorded — reading it inside a mutation does not pin
327
- * it), and results are **eventually consistent** with writes a mutation just
328
- * made (e.g. a freshly scheduled job may not appear yet).
329
- *
330
- * Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
331
- * objects via `ctx.storage`.
332
- */
501
+ * Read-only reader over Lunora's system tables (`_scheduled_functions`,
502
+ * `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
503
+ *
504
+ * **Best-effort and eventually consistent.** Unlike `ctx.db.&lt;table>` — which
505
+ * reads the shard's transactional SQLite snapshot — the data behind these tables
506
+ * lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
507
+ * objects in R2). Every `collect()` / `get()` reaches across to that source.
508
+ *
509
+ * It is **not part of the mutation transaction snapshot** (no OCC guard, no
510
+ * subscription dependency recorded — reading it inside a mutation does not pin
511
+ * it), and results are **eventually consistent** with writes a mutation just
512
+ * made (e.g. a freshly scheduled job may not appear yet).
513
+ *
514
+ * Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
515
+ * objects via `ctx.storage`.
516
+ */
333
517
  interface SystemDatabaseReader {
334
518
  /**
335
- * Resolve a single system-table row by id, or `null` when absent.
336
- * (`_scheduled_functions` → job id; `_storage` → object key.)
337
- */
519
+ * Resolve a single system-table row by id, or `null` when absent.
520
+ * (`_scheduled_functions` → job id; `_storage` → object key.)
521
+ */
338
522
  get: <T extends SystemTableName>(table: T, id: string) => Promise<SystemDoc<T> | null>;
339
523
  /**
340
- * Begin a read over a system table; call `.collect()` to resolve the full
341
- * list. No filtering, indexing, or pagination — the backing source is remote
342
- * and the surface stays deliberately minimal.
343
- */
524
+ * Begin a read over a system table; call `.collect()` to resolve the full
525
+ * list. No filtering, indexing, or pagination — the backing source is remote
526
+ * and the surface stays deliberately minimal.
527
+ */
344
528
  query: <T extends SystemTableName>(table: T) => SystemQuery<T>;
345
529
  }
346
530
  /**
347
- * Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
348
- * actual SQL implementation lives in `@lunora/do`; these are signatures only.
349
- */
531
+ * Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
532
+ * actual SQL implementation lives in `@lunora/do`; these are signatures only.
533
+ */
350
534
  interface DatabaseReader {
351
535
  get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
352
536
  /**
353
- * Validate an untrusted `id` string against the structural shape of an id
354
- * for `tableName`, returning the branded {@link Id} when it is well-formed
355
- * and `null` otherwise. Pure structural validation — it never reads the
356
- * database, so a structurally valid id for a row that doesn't exist still
357
- * returns the branded id (mirrors Convex's `db.normalizeId`).
358
- */
537
+ * Validate an untrusted `id` string against the structural shape of an id
538
+ * for `tableName`, returning the branded {@link Id} when it is well-formed
539
+ * and `null` otherwise. Pure structural validation — it never reads the
540
+ * database, so a structurally valid id for a row that doesn't exist still
541
+ * returns the branded id (mirrors Convex's `db.normalizeId`).
542
+ */
359
543
  normalizeId: <T extends string>(tableName: T, id: string) => Id<T> | null;
360
544
  query: (tableName: string) => TableReader;
361
545
  /**
362
- * Best-effort, read-only reader over Lunora's system tables
363
- * (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
364
- * part of the transaction snapshot — see {@link SystemDatabaseReader}.
365
- */
546
+ * Best-effort, read-only reader over Lunora's system tables
547
+ * (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
548
+ * part of the transaction snapshot — see {@link SystemDatabaseReader}.
549
+ */
366
550
  readonly system: SystemDatabaseReader;
367
551
  }
368
552
  /** Options for {@link TableReader.paginate} — Convex-compatible page request. */
@@ -370,14 +554,14 @@ interface PaginationOptions {
370
554
  /** Opaque cursor from the prior page's `continueCursor`; `null`/omitted starts at the first page. */
371
555
  cursor?: null | string;
372
556
  /**
373
- * Optional inclusive upper bound for reactive pagination. When supplied the
374
- * page covers the fixed half-open range `(cursor, endCursor]` (ignoring
375
- * `numItems`): every row strictly after `cursor` up to and including the
376
- * boundary row `endCursor` encodes. The page's `isDone` is `true` and its
377
- * `continueCursor` echoes `endCursor`, so the next page keeps starting where
378
- * this one ends even as rows are inserted/deleted inside the range. Omit (or
379
- * pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
380
- */
557
+ * Optional inclusive upper bound for reactive pagination. When supplied the
558
+ * page covers the fixed half-open range `(cursor, endCursor]` (ignoring
559
+ * `numItems`): every row strictly after `cursor` up to and including the
560
+ * boundary row `endCursor` encodes. The page's `isDone` is `true` and its
561
+ * `continueCursor` echoes `endCursor`, so the next page keeps starting where
562
+ * this one ends even as rows are inserted/deleted inside the range. Omit (or
563
+ * pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
564
+ */
381
565
  endCursor?: null | string;
382
566
  /** Maximum rows to return for this page. */
383
567
  numItems: number;
@@ -390,46 +574,55 @@ interface PaginationResult<T = Record<string, unknown>> {
390
574
  isDone: boolean;
391
575
  page: T[];
392
576
  /**
393
- * Reactive-pagination only: the midpoint cursor of a bounded
394
- * `(cursor, endCursor]` page, used by the client to split an over-grown page
395
- * into two adjacent ranges. Absent on legacy (open-ended) pages.
396
- */
577
+ * Reactive-pagination only: the midpoint cursor of a bounded
578
+ * `(cursor, endCursor]` page, used by the client to split an over-grown page
579
+ * into two adjacent ranges. Absent on legacy (open-ended) pages.
580
+ */
397
581
  splitCursor?: null | string;
398
582
  }
399
583
  /**
400
- * The fluent `ctx.db.query(table)` reader. Generic over the document type
401
- * `Row` so the generated `ctx.db` can bind it to `Doc&lt;table>` (the chain and
402
- * every terminal then resolve typed rows — no `as unknown as Doc&lt;...>` casts).
403
- * Defaults to the untyped `Record&lt;string, unknown>` shape for the base
404
- * (schema-agnostic) `@lunora/server` reader.
405
- */
584
+ * The fluent `ctx.db.query(table)` reader. Generic over the document type
585
+ * `Row` so the generated `ctx.db` can bind it to `Doc&lt;table>` (the chain and
586
+ * every terminal then resolve typed rows — no `as unknown as Doc&lt;...>` casts).
587
+ * Defaults to the untyped `Record&lt;string, unknown>` shape for the base
588
+ * (schema-agnostic) `@lunora/server` reader.
589
+ */
406
590
  interface TableReader<Row = Record<string, unknown>> {
407
591
  collect: () => Promise<Row[]>;
408
592
  filter: (predicate: (document: Row) => boolean) => TableReader<Row>;
409
593
  first: () => Promise<Row | null>;
410
594
  /**
411
- * Set the result order. Orders by the active `.withIndex()` (or by
412
- * `_creationTime` when none is staged), `"asc"` by default; `"desc"`
413
- * reverses it. Composes with `.withIndex()`, `.filter()`, and every
414
- * terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
415
- * `.order("asc" | "desc")`.
416
- */
595
+ * Set the result order. Orders by the active `.withIndex()` (or by
596
+ * `_creationTime` when none is staged), `"asc"` by default; `"desc"`
597
+ * reverses it. Composes with `.withIndex()`, `.filter()`, and every
598
+ * terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
599
+ * `.order("asc" | "desc")`.
600
+ */
417
601
  order: (direction: "asc" | "desc") => TableReader<Row>;
418
602
  paginate: (options: PaginationOptions) => Promise<PaginationResult<Row>>;
419
603
  take: (limit: number) => Promise<Row[]>;
420
604
  /**
421
- * Return the single matching document. Returns `null` when nothing matches
422
- * and throws when more than one row matches. Mirrors Convex's `.unique()`.
423
- */
605
+ * Return the single matching document. Returns `null` when nothing matches
606
+ * and throws when more than one row matches. Mirrors Convex's `.unique()`.
607
+ */
424
608
  unique: () => Promise<Row | null>;
609
+ /**
610
+ * Restrict the query to a declared `.geoIndex()`. The builder's
611
+ * `.near(point, radiusMeters)` returns rows within `radiusMeters` of `point`,
612
+ * ordered nearest-first; `.within(bbox)` returns rows inside the
613
+ * latitude/longitude bounding box. Both resolve as a geohash-prefix range
614
+ * scan over the index's companion followed by a Haversine refine. Pair with
615
+ * `.take(n)` to cap results (`.paginate()` is not supported on a geo query).
616
+ */
617
+ withGeoIndex: (indexName: string, build: (q: GeoFilterBuilder) => GeoFilterBuilder) => TableReader<Row>;
425
618
  withIndex: (indexName: string, range?: (q: IndexRangeBuilder) => IndexRangeBuilder) => TableReader<Row>;
426
619
  /**
427
- * Restrict the query to a declared `.searchIndex()`. The builder's
428
- * `.search(field, query)` runs a full-text match against the index's
429
- * searchable field; `.eq(field, value)` narrows by a declared filter
430
- * field. Results come back ordered by relevance — pair with `.take(n)`
431
- * (`.paginate()` is not supported on a search query).
432
- */
620
+ * Restrict the query to a declared `.searchIndex()`. The builder's
621
+ * `.search(field, query)` runs a full-text match against the index's
622
+ * searchable field; `.eq(field, value)` narrows by a declared filter
623
+ * field. Results come back ordered by relevance — pair with `.take(n)`
624
+ * (`.paginate()` is not supported on a search query).
625
+ */
433
626
  withSearchIndex: (indexName: string, search: (q: SearchFilterBuilder) => SearchFilterBuilder) => TableReader<Row>;
434
627
  }
435
628
  interface IndexRangeBuilder {
@@ -446,86 +639,159 @@ interface SearchFilterBuilder {
446
639
  /** Full-text match `query` against the index's searchable `field`. Call exactly once. */
447
640
  search: (field: string, query: string) => SearchFilterBuilder;
448
641
  }
642
+ /** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
643
+ interface GeoPointInput {
644
+ lat: number;
645
+ lng: number;
646
+ }
647
+ /**
648
+ * An axis-aligned latitude/longitude bounding box: `sw` is the south-west
649
+ * (min lat, min lng) corner, `ne` the north-east (max lat, max lng) corner.
650
+ */
651
+ interface GeoBoundingBox {
652
+ ne: GeoPointInput;
653
+ sw: GeoPointInput;
654
+ }
449
655
  /**
450
- * Options shared by the batch-write methods (`insertMany`/`deleteMany`/
451
- * `patchMany`)a per-call payload cap. The default cap (500) rejects an
452
- * oversized call up front so an accidental O(n²) or a payload past the Durable
453
- * Object request limit fails loudly instead of degrading the mutation. Callers
454
- * with larger sets should chunk their own loop or raise `limit`.
455
- */
656
+ * Builder passed to {@link TableReader.withGeoIndex}. Call exactly one of
657
+ * `.near(...)` / `.within(...)` — the two are mutually exclusive proximity vs
658
+ * bounding-box modes.
659
+ */
660
+ interface GeoFilterBuilder {
661
+ /** Rows within `radiusMeters` of `point`, resolved nearest-first. Call exactly once. */
662
+ near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
663
+ /** Rows whose point falls inside the bounding `box`. Call exactly once. */
664
+ within: (box: GeoBoundingBox) => GeoFilterBuilder;
665
+ }
666
+ /**
667
+ * Options shared by the batch-write methods (`insertMany`/`deleteMany`/
668
+ * `patchMany`) — a per-call payload cap. The default cap (500) rejects an
669
+ * oversized call up front so an accidental O(n²) or a payload past the Durable
670
+ * Object request limit fails loudly instead of degrading the mutation. Callers
671
+ * with larger sets should chunk their own loop or raise `limit`.
672
+ */
456
673
  interface BatchWriteOptions {
457
674
  /** Reject the call when the batch size exceeds this value (default 500). */
458
675
  limit?: number;
459
676
  }
677
+ /** Options accepted by {@link DatabaseWriter.insertMany} and the per-table facade. */
678
+ interface InsertManyOptions extends BatchWriteOptions {
679
+ /**
680
+ * When `true`, a UNIQUE-constraint breach for a row resolves to `null`
681
+ * instead of throwing — the rest of the batch is still inserted. Skipped rows
682
+ * keep their input-order slot with `null` in the returned array. Mirrors
683
+ * better-drizzle's `createMany({ skipDuplicates: true })`.
684
+ */
685
+ skipDuplicates?: boolean;
686
+ }
460
687
  interface DatabaseWriter extends DatabaseReader {
461
688
  delete: <T extends string>(id: Id<T>) => Promise<void>;
462
689
  /**
463
- * Delete many rows by id in one call. Each id is deleted through the full
464
- * single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
465
- * number of ids **requested**, not the rows actually removed — an unknown or
466
- * duplicated id is a silent no-op.
467
- *
468
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
469
- * BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
470
- * error) rolls back the whole mutation. (In an action there is no transaction
471
- * span, so the prior deletes persist; the in-memory test harness mirrors the span.)
472
- */
690
+ * Delete many rows by id in one call. Each id is deleted through the full
691
+ * single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
692
+ * number of ids **requested**, not the rows actually removed — an unknown or
693
+ * duplicated id is a silent no-op.
694
+ *
695
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
696
+ * BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
697
+ * error) rolls back the whole mutation. (In an action there is no transaction
698
+ * span, so the prior deletes persist; the in-memory test harness mirrors the span.)
699
+ */
473
700
  deleteMany: <T extends string>(ids: ReadonlyArray<Id<T>>, options?: BatchWriteOptions) => Promise<{
474
701
  deleted: number;
475
702
  }>;
476
703
  /**
477
- * Insert a document, returning its server id.
478
- *
479
- * Pass `options.clientId` (a UUID) to key the row yourself for an
480
- * optimistic client that needs the persisted row to match the key it
481
- * already rendered. It's validated for shape and still subject to the
482
- * primary-key uniqueness constraint; omit it and the server mints the id.
483
- */
704
+ * Delete every row matching `where` in one call. Matching rows are resolved
705
+ * first, then each row is deleted through the single-row delete pipeline
706
+ * (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
707
+ * search/aggregate companions stay correct.
708
+ *
709
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
710
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
711
+ */
712
+ deleteWhere: (tableName: string, where: Record<string, unknown>, options?: BatchWriteOptions) => Promise<{
713
+ deleted: number;
714
+ }>;
715
+ /**
716
+ * Insert a document, returning its server id.
717
+ *
718
+ * Pass `options.clientId` (a UUID) to key the row yourself — for an
719
+ * optimistic client that needs the persisted row to match the key it
720
+ * already rendered. It's validated for shape and still subject to the
721
+ * primary-key uniqueness constraint; omit it and the server mints the id.
722
+ */
484
723
  insert: <T extends string>(tableName: T, document: Record<string, unknown>, options?: {
485
724
  clientId?: string;
486
725
  }) => Promise<Id<T>>;
487
726
  /**
488
- * Insert many documents into one table in a single call, returning the
489
- * minted ids in input order. Equivalent to a per-row `insert()` loop — each
490
- * row gets defaults, validators, triggers, and a per-row RLS check — but the
491
- * caller pays one round-trip instead of N.
492
- *
493
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
494
- * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
495
- * rolls back the whole mutation. (In an action there is no transaction span,
496
- * so the prior inserts persist; the in-memory test harness mirrors the span.)
497
- */
498
- insertMany: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions) => Promise<Id<T>[]>;
727
+ * Insert many documents into one table in a single call, returning the
728
+ * minted ids in input order. Equivalent to a per-row `insert()` loop — each
729
+ * row gets defaults, validators, triggers, and a per-row RLS check — but the
730
+ * caller pays one round-trip instead of N.
731
+ *
732
+ * Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
733
+ * `null` results for that row instead of failing the whole batch; the rest of
734
+ * the batch is still inserted and order is preserved.
735
+ *
736
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
737
+ * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
738
+ * rolls back the whole mutation. (In an action there is no transaction span,
739
+ * so the prior inserts persist; the in-memory test harness mirrors the span.)
740
+ */
741
+ insertMany: {
742
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options: BatchWriteOptions & {
743
+ skipDuplicates: true;
744
+ }): Promise<(Id<T> | null)[]>;
745
+ <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: InsertManyOptions): Promise<Id<T>[]>;
746
+ };
499
747
  /**
500
- * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
501
- * `.check()` validators and before/after triggers** for throughput on data you
502
- * control (seed, migration, admin import). Defaults, ids, and every companion
503
- * (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
504
- * stay correct.
505
- *
506
- * It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
507
- * RLS is **not** bypassed: secure-by-default and the table's insert policy still
508
- * apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
509
- * preserve a supplied `_id` (import). Use only for data you trust; prefer
510
- * `insertMany` for anything user-supplied.
511
- */
748
+ * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
749
+ * `.check()` validators and before/after triggers** for throughput on data you
750
+ * control (seed, migration, admin import). Defaults, ids, and every companion
751
+ * (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
752
+ * stay correct.
753
+ *
754
+ * It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
755
+ * RLS is **not** bypassed: secure-by-default and the table's insert policy still
756
+ * apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
757
+ * preserve a supplied `_id` (import). Use only for data you trust; prefer
758
+ * `insertMany` for anything user-supplied.
759
+ */
512
760
  insertManyUnsafe: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions & {
513
761
  allowExplicitId?: boolean;
514
762
  }) => Promise<Id<T>[]>;
515
763
  patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
516
764
  /**
517
- * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
518
- * single `patch()` (per-row triggers + RLS).
519
- *
520
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
521
- * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
522
- * (In an action there is no transaction span, so the prior patches persist;
523
- * the in-memory test harness mirrors the span.)
524
- */
765
+ * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
766
+ * single `patch()` (per-row triggers + RLS). Returns the number of rows
767
+ * actually patched.
768
+ *
769
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
770
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
771
+ * (In an action there is no transaction span, so the prior patches persist;
772
+ * the in-memory test harness mirrors the span.)
773
+ */
525
774
  patchMany: <T extends string>(patches: ReadonlyArray<{
526
775
  id: Id<T>;
527
776
  patch: Record<string, unknown>;
528
- }>, options?: BatchWriteOptions) => Promise<void>;
777
+ }>, options?: BatchWriteOptions) => Promise<{
778
+ patched: number;
779
+ }>;
780
+ /**
781
+ * Patch every row matching `where` with the same `patch` in one call. The
782
+ * matching rows are resolved first, then each row is updated through the
783
+ * single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
784
+ * so reactive subscriptions and search/aggregate companions stay correct.
785
+ *
786
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
787
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
788
+ */
789
+ patchWhere: (tableName: string, args: {
790
+ patch: Record<string, unknown>;
791
+ where: Record<string, unknown>;
792
+ }, options?: BatchWriteOptions) => Promise<{
793
+ patched: number;
794
+ }>;
529
795
  replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
530
796
  }
531
797
  /** Authenticated identity surfaced into every context. */
@@ -534,11 +800,11 @@ interface AuthState {
534
800
  readonly userId: string | null;
535
801
  }
536
802
  /**
537
- * A pending scheduled invocation as surfaced by {@link Scheduler.list} /
538
- * {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s internal
539
- * `ScheduleRecord` — re-declared here so the public ctx surface carries no
540
- * dependency on the scheduler package's internal types.
541
- */
803
+ * A pending scheduled invocation as surfaced by {@link Scheduler.list} /
804
+ * {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s internal
805
+ * `ScheduleRecord` — re-declared here so the public ctx surface carries no
806
+ * dependency on the scheduler package's internal types.
807
+ */
542
808
  interface ScheduledJob {
543
809
  args: Record<string, unknown>;
544
810
  /** Number of dispatch attempts already made (absent until the first retry). */
@@ -552,6 +818,21 @@ interface ScheduledJob {
552
818
  /** Routing hint forwarded so dispatch lands on the right shard. */
553
819
  shardKey?: string;
554
820
  }
821
+ /**
822
+ * A schedulable durable-workflow reference — the generated `workflows.&lt;name>` /
823
+ * `agents.&lt;name>` object, which carries its `WORKFLOW_*`/`AGENT_*` binding and
824
+ * stable name. Structural mirror of `@lunora/scheduler`'s `WorkflowReference` so
825
+ * `ctx.scheduler` can target a workflow/agent without a dependency on
826
+ * `@lunora/scheduler` / `@lunora/workflow`. A scheduled workflow target starts a
827
+ * fresh instance on fire (the args become its `params`).
828
+ */
829
+ interface SchedulableWorkflowReference {
830
+ /** The `WORKFLOW_*`/`AGENT_*` binding name (present on a generated ref). */
831
+ readonly binding?: string;
832
+ readonly isLunoraWorkflow: true;
833
+ /** The workflow/agent export/stable name (present on a generated ref). */
834
+ readonly name?: string;
835
+ }
555
836
  interface Scheduler {
556
837
  /** Cancel a pending job by id. `cancelled` is `false` when no such job exists. */
557
838
  cancel: (id: string) => Promise<{
@@ -561,16 +842,23 @@ interface Scheduler {
561
842
  get: (id: string) => Promise<ScheduledJob | null>;
562
843
  /** List all pending scheduled jobs. */
563
844
  list: () => Promise<ScheduledJob[]>;
564
- runAfter: (delayMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
565
- runAt: (timestampMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
845
+ /**
846
+ * Schedule a one-shot run `delayMs` from now. `target` is a function path
847
+ * (`"ns:fn"`) dispatched as a one-shot, or a generated `workflows.&lt;name>` /
848
+ * `agents.&lt;name>` reference which starts a fresh durable instance on fire
849
+ * (the args become its `params`).
850
+ */
851
+ runAfter: (delayMs: number, target: SchedulableWorkflowReference | string, args?: Record<string, unknown>) => Promise<string>;
852
+ /** Like {@link Scheduler.runAfter} but fires at an absolute epoch-ms timestamp. */
853
+ runAt: (timestampMs: number, target: SchedulableWorkflowReference | string, args?: Record<string, unknown>) => Promise<string>;
566
854
  }
567
855
  /**
568
- * A workflow instance's lifecycle status. Clean public mirror of
569
- * `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
570
- * `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
571
- * dependency on the workflow package, exactly as {@link Scheduler} avoids a
572
- * dependency on `@lunora/scheduler`.
573
- */
856
+ * A workflow instance's lifecycle status. Clean public mirror of
857
+ * `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
858
+ * `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
859
+ * dependency on the workflow package, exactly as {@link Scheduler} avoids a
860
+ * dependency on `@lunora/scheduler`.
861
+ */
574
862
  type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
575
863
  /** Result of {@link WorkflowInstance.status}. Mirrors `@lunora/workflow`'s `WorkflowStatusResult`. */
576
864
  interface WorkflowStatusResult {
@@ -607,9 +895,9 @@ interface WorkflowInstance {
607
895
  terminate: () => Promise<void>;
608
896
  }
609
897
  /**
610
- * A typed handle to one declared workflow, addressable from `ctx.workflows`.
611
- * Mirrors `@lunora/workflow`'s `WorkflowHandle`.
612
- */
898
+ * A typed handle to one declared workflow, addressable from `ctx.workflows`.
899
+ * Mirrors `@lunora/workflow`'s `WorkflowHandle`.
900
+ */
613
901
  interface WorkflowHandle<Params = Record<string, unknown>> {
614
902
  /** Start a new instance (optionally with an id + params). */
615
903
  create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstance>;
@@ -619,25 +907,65 @@ interface WorkflowHandle<Params = Record<string, unknown>> {
619
907
  get: (id: string) => Promise<WorkflowInstance>;
620
908
  }
621
909
  /**
622
- * The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
623
- * workflow declared in `lunora/workflows.ts` is reachable by its export name;
624
- * codegen narrows the `get(name)` overloads to the known workflow names + their
625
- * inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
626
- */
910
+ * The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
911
+ * workflow declared in `lunora/workflows.ts` is reachable by its export name;
912
+ * codegen narrows the `get(name)` overloads to the known workflow names + their
913
+ * inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
914
+ */
627
915
  interface Workflows {
628
916
  /** Resolve the handle for a declared workflow by export name. */
629
917
  get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
630
918
  }
919
+ /**
920
+ * Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
921
+ * in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
922
+ * Queries and mutations do not expose this — they run inside the Durable Object.
923
+ */
924
+ interface CachePurge {
925
+ /**
926
+ * Purge cached responses matching the given tags, or everything when
927
+ * `purgeEverything` is true. Only available in action handlers.
928
+ */
929
+ purge: (options: {
930
+ purgeEverything?: boolean;
931
+ tags?: string[];
932
+ }) => Promise<unknown>;
933
+ }
934
+ /**
935
+ * Structural projection of workers-types' `SecretsStoreSecret` binding — the
936
+ * per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
937
+ * secret value (or throws if it does not exist). Mirrored structurally so the
938
+ * runtime resolves it without a workerd type dependency.
939
+ */
940
+ interface SecretsStoreSecretLike {
941
+ get: () => Promise<string>;
942
+ }
943
+ /**
944
+ * `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
945
+ * A core built-in (always present on every context, like `ctx.log`): a binding
946
+ * named in wrangler's `secrets_store_secrets[]` is read by its binding name.
947
+ *
948
+ * ```ts
949
+ * const apiKey = await ctx.secrets.get("STRIPE_KEY");
950
+ * ```
951
+ *
952
+ * The lookup is async (the platform fetches and decrypts on first read);
953
+ * reading an undeclared name throws a directed error naming the bound secrets.
954
+ */
955
+ interface Secrets {
956
+ /** Resolve a Secrets Store secret by its wrangler binding name. */
957
+ get: (name: string) => Promise<string>;
958
+ }
631
959
  /** Lifecycle phase relative to the SQL write. */
632
960
  type TriggerTiming = "after" | "before";
633
961
  /** The CRUD operation a trigger reacts to. `patch` and `replace` both map to `update`. */
634
962
  type TriggerOp = "delete" | "insert" | "update";
635
963
  /**
636
- * A row as observed by a trigger handler: the table's `Shape` (with the same
637
- * optionality rules as {@link InferArgs}) plus the system columns every stored
638
- * doc carries.
639
- */
640
- type TriggerRow<Shape extends Record<string, Validator>> = { [K in keyof Shape as undefined extends Infer<Shape[K]> ? K : never]?: Infer<Shape[K]> } & { [K in keyof Shape as undefined extends Infer<Shape[K]> ? never : K]: Infer<Shape[K]> } & {
964
+ * A row as observed by a trigger handler: the table's `Shape` (with the same
965
+ * optionality rules as {@link InferArgs}) plus the system columns every stored
966
+ * doc carries.
967
+ */
968
+ type TriggerRow<Shape extends Record<string, Validator>> = { [K in keyof Shape as undefined extends Infer<Shape[K]> ? K : never]?: Infer<Shape[K]>; } & { [K in keyof Shape as undefined extends Infer<Shape[K]> ? never : K]: Infer<Shape[K]>; } & {
641
969
  readonly _creationTime: number;
642
970
  readonly _id: string;
643
971
  };
@@ -649,11 +977,11 @@ interface TriggerInsertEvent<Shape extends Record<string, Validator> = Record<st
649
977
  readonly table: string;
650
978
  }
651
979
  /**
652
- * What an `update` trigger observes: the merged row plus the pre-write row.
653
- * `previous` is typed as always present (the row must exist to be updated); the
654
- * runtime supplies it best-effort and only omits it in the unreachable
655
- * row-vanished-mid-write case.
656
- */
980
+ * What an `update` trigger observes: the merged row plus the pre-write row.
981
+ * `previous` is typed as always present (the row must exist to be updated); the
982
+ * runtime supplies it best-effort and only omits it in the unreachable
983
+ * row-vanished-mid-write case.
984
+ */
657
985
  interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
658
986
  readonly doc: TriggerRow<Shape>;
659
987
  readonly id: string;
@@ -662,10 +990,10 @@ interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<st
662
990
  readonly table: string;
663
991
  }
664
992
  /**
665
- * What a `delete` trigger observes: the row about to be (or just) removed.
666
- * `previous` is typed as always present; the runtime supplies it best-effort
667
- * and only omits it in the unreachable row-vanished-mid-write case.
668
- */
993
+ * What a `delete` trigger observes: the row about to be (or just) removed.
994
+ * `previous` is typed as always present; the runtime supplies it best-effort
995
+ * and only omits it in the unreachable row-vanished-mid-write case.
996
+ */
669
997
  interface TriggerDeleteEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
670
998
  readonly id: string;
671
999
  readonly op: "delete";
@@ -689,10 +1017,10 @@ interface TriggerQueryArgs {
689
1017
  with?: Record<string, unknown>;
690
1018
  }
691
1019
  /**
692
- * Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
693
- * `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
694
- * `@lunora/server` don't take a hard dep on the DO runtime.
695
- */
1020
+ * Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
1021
+ * `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
1022
+ * `@lunora/server` don't take a hard dep on the DO runtime.
1023
+ */
696
1024
  interface TriggerAggregateOptions {
697
1025
  baseWhere?: Record<string, unknown>;
698
1026
  field?: string;
@@ -737,17 +1065,17 @@ interface TriggerRankPageOptions {
737
1065
  where?: Record<string, unknown>;
738
1066
  }
739
1067
  /**
740
- * Portable, table/id-addressed ORM writer handed to trigger handlers via
741
- * `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
742
- * **not** the generated per-table `ctx.db.&lt;table>` facade (which can't be typed
743
- * from inside `defineTable`, where the full schema isn't known).
744
- *
745
- * `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
746
- * trigger-maintained counter and rank tables the user-facing reader uses, so
747
- * a handler's `ctx.db.&lt;table>.aggregate(...)` observes the just-staged write
748
- * within the same DO transaction (the counter step happens before the trigger
749
- * fires).
750
- */
1068
+ * Portable, table/id-addressed ORM writer handed to trigger handlers via
1069
+ * `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
1070
+ * **not** the generated per-table `ctx.db.&lt;table>` facade (which can't be typed
1071
+ * from inside `defineTable`, where the full schema isn't known).
1072
+ *
1073
+ * `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
1074
+ * trigger-maintained counter and rank tables the user-facing reader uses, so
1075
+ * a handler's `ctx.db.&lt;table>.aggregate(...)` observes the just-staged write
1076
+ * within the same DO transaction (the counter step happens before the trigger
1077
+ * fires).
1078
+ */
751
1079
  interface TriggerDatabase {
752
1080
  aggregate: (tableName: string, options: TriggerAggregateOptions) => Promise<null | number>;
753
1081
  count: (tableName: string, where?: Record<string, unknown>) => Promise<number>;
@@ -763,10 +1091,10 @@ interface TriggerDatabase {
763
1091
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
764
1092
  }
765
1093
  /**
766
- * Handle injected into every trigger handler. `db` is the portable ORM writer;
767
- * `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
768
- * **not** transactional with the firing write).
769
- */
1094
+ * Handle injected into every trigger handler. `db` is the portable ORM writer;
1095
+ * `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
1096
+ * **not** transactional with the firing write).
1097
+ */
770
1098
  interface TriggerCtx {
771
1099
  readonly db: TriggerDatabase;
772
1100
  readonly scheduler: Scheduler;
@@ -774,20 +1102,20 @@ interface TriggerCtx {
774
1102
  /** A user-declared trigger handler. Throwing from a `before*` handler aborts the write. */
775
1103
  type TriggerHandler<Event> = (context: TriggerCtx, event: Event) => Promise<void> | void;
776
1104
  /**
777
- * A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
778
- * The handler's event type is erased to the {@link TriggerEvent} union here; the
779
- * per-op {@link TriggerBuilder} methods recover the precise event type for
780
- * authors.
781
- */
1105
+ * A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
1106
+ * The handler's event type is erased to the {@link TriggerEvent} union here; the
1107
+ * per-op {@link TriggerBuilder} methods recover the precise event type for
1108
+ * authors.
1109
+ */
782
1110
  interface TriggerDefinition {
783
1111
  readonly handler: TriggerHandler<TriggerEvent>;
784
1112
  readonly op: TriggerOp;
785
1113
  readonly timing: TriggerTiming;
786
1114
  }
787
1115
  /**
788
- * The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
789
- * to one `timing`+`op` pair, typing the event against the table's `Shape`.
790
- */
1116
+ * The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
1117
+ * to one `timing`+`op` pair, typing the event against the table's `Shape`.
1118
+ */
791
1119
  interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string, Validator>> {
792
1120
  afterDelete: (handler: TriggerHandler<TriggerDeleteEvent<Shape>>) => TriggerDefinition;
793
1121
  afterInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
@@ -797,11 +1125,11 @@ interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string
797
1125
  beforeUpdate: (handler: TriggerHandler<TriggerUpdateEvent<Shape>>) => TriggerDefinition;
798
1126
  }
799
1127
  /**
800
- * Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
801
- * public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
802
- * the ctx surface carries no dependency on the storage package's types. Matches
803
- * the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
804
- */
1128
+ * Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
1129
+ * public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
1130
+ * the ctx surface carries no dependency on the storage package's types. Matches
1131
+ * the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
1132
+ */
805
1133
  interface StorageMetadata {
806
1134
  /** The object's `Content-Type`, when recorded. */
807
1135
  contentType?: string;
@@ -817,31 +1145,31 @@ interface StorageMetadata {
817
1145
  uploaded?: number;
818
1146
  }
819
1147
  /**
820
- * Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
821
- *
822
- * Queries are pure reads, and mutations run inside a transactional scope —
823
- * neither is allowed to perform side-effectful R2 writes (`upload`) or
824
- * deletes (`delete`). They can, however, **read** existing objects and
825
- * resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
826
- * trip), so the read-only surface keeps `download` and `getSignedUrl`. The
827
- * full {@link Storage} surface stays on `ActionCtx`.
828
- */
1148
+ * Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
1149
+ *
1150
+ * Queries are pure reads, and mutations run inside a transactional scope —
1151
+ * neither is allowed to perform side-effectful R2 writes (`upload`) or
1152
+ * deletes (`delete`). They can, however, **read** existing objects and
1153
+ * resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
1154
+ * trip), so the read-only surface keeps `download` and `getSignedUrl`. The
1155
+ * full {@link Storage} surface stays on `ActionCtx`.
1156
+ */
829
1157
  interface ReadOnlyStorage<Buckets extends string = string> {
830
1158
  /**
831
- * Select a named bucket (declared via `v.storage("name")`). The returned
832
- * accessor's operations target that bucket — `ctx.storage.bucket("avatars")
833
- * .download(key)`. The bare `ctx.storage` targets the default bucket.
834
- */
1159
+ * Select a named bucket (declared via `v.storage("name")`). The returned
1160
+ * accessor's operations target that bucket — `ctx.storage.bucket("avatars")
1161
+ * .download(key)`. The bare `ctx.storage` targets the default bucket.
1162
+ */
835
1163
  bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
836
1164
  /** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
837
1165
  readonly bucketName: string;
838
1166
  /** Fetch the body of an existing object. Returns `null` when absent. */
839
1167
  download: (key: string) => Promise<ReadableStream | null>;
840
1168
  /**
841
- * Read a file's metadata (size, content-type, sha256, upload time, custom
842
- * metadata) without fetching its body. Returns `null` when the object is
843
- * absent. Mirrors Convex's `ctx.storage.getMetadata`.
844
- */
1169
+ * Read a file's metadata (size, content-type, sha256, upload time, custom
1170
+ * metadata) without fetching its body. Returns `null` when the object is
1171
+ * absent. Mirrors Convex's `ctx.storage.getMetadata`.
1172
+ */
845
1173
  getMetadata: (key: string) => Promise<StorageMetadata | null>;
846
1174
  /** Resolve a short-lived signed URL for an existing object. */
847
1175
  getSignedUrl: (key: string, options?: {
@@ -855,20 +1183,20 @@ interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Bucke
855
1183
  bucket: (name: Buckets) => Storage<Buckets>;
856
1184
  delete: (key: string) => Promise<void>;
857
1185
  /**
858
- * Mint a short-lived signed `PUT` URL a client can upload directly to,
859
- * optionally pinning the `Content-Type` the uploader must send. Mirrors
860
- * Convex's `storage.generateUploadUrl`.
861
- */
1186
+ * Mint a short-lived signed `PUT` URL a client can upload directly to,
1187
+ * optionally pinning the `Content-Type` the uploader must send. Mirrors
1188
+ * Convex's `storage.generateUploadUrl`.
1189
+ */
862
1190
  generateUploadUrl: (key: string, options?: {
863
1191
  contentType?: string;
864
1192
  expiresInSeconds?: number;
865
1193
  }) => Promise<string>;
866
1194
  /**
867
- * Upload `body` to `key` from the server, returning the stored object's key
868
- * and etag. Mirrors Convex's `storage.store`. Accepts the same guard fields
869
- * as `@lunora/storage`'s `UploadOptions` so `maxSize` /
870
- * `allowedContentTypes` enforcement isn't lost behind the Convex-style alias.
871
- */
1195
+ * Upload `body` to `key` from the server, returning the stored object's key
1196
+ * and etag. Mirrors Convex's `storage.store`. Accepts the same guard fields
1197
+ * as `@lunora/storage`'s `UploadOptions` so `maxSize` /
1198
+ * `allowedContentTypes` enforcement isn't lost behind the Convex-style alias.
1199
+ */
872
1200
  store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: {
873
1201
  allowedContentTypes?: ReadonlyArray<string>;
874
1202
  contentType?: string;
@@ -912,154 +1240,350 @@ interface VectorRecord {
912
1240
  values: ReadonlyArray<number>;
913
1241
  }
914
1242
  /**
915
- * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
916
- * of `@lunora/vectors`' `LunoraVectors` so the live adapter is assignable.
917
- */
1243
+ * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
1244
+ * of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
1245
+ */
918
1246
  interface VectorSearchReader {
919
1247
  getByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
920
1248
  query: (indexName: string, input: VectorQueryInput) => Promise<VectorMatches>;
921
1249
  }
922
1250
  /**
923
- * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
924
- * is queued post-commit by default; `upsertNow` forces a synchronous write.
925
- * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
926
- */
1251
+ * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
1252
+ * is queued post-commit by default; `upsertNow` forces a synchronous write.
1253
+ * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
1254
+ */
927
1255
  interface VectorSearch extends VectorSearchReader {
928
1256
  deleteByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<void>;
929
1257
  upsert: (indexName: string, input: VectorUpsertInput) => Promise<void>;
930
1258
  upsertNow: (indexName: string, input: VectorUpsertInput) => Promise<void>;
931
1259
  }
932
1260
  /**
933
- * Structured logger on every function `ctx`. Each call emits one attributed log
934
- * line tagged with the function path on the server that flows to an
935
- * `ObservabilitySink`'s `onLog` (where you route it in production) and, in
936
- * development, to the dev server terminal via the CLI / Vite plugin formatter.
937
- * Mirrors the `console` method names so it's a drop-in for `console.log` inside a
938
- * handler, but with attribution and a routable transport.
939
- *
940
- * Accepts any number of values per call, exactly like `console`; objects are
941
- * rendered into the human-readable message. The raw, un-rendered arguments are
942
- * preserved ONLY on the in-process `onLog` sink (which you opt into and control);
943
- * the rendered message — not the structured args — is what reaches the dev
944
- * terminal and the platform's Workers Logs.
945
- *
946
- * Attribution follows the dispatched function: a log emitted inside an internal
947
- * function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
948
- * the outer request entrypoint, since the composed call reuses its context.
949
- */
1261
+ * Structured, filterable key/value fields attached to a log line the second
1262
+ * argument of a `ctx.log.&lt;level>(message, fields)` call, or the fields bound by
1263
+ * `ctx.log.with(fields)`. They travel to an `ObservabilitySink`'s `onLog` and,
1264
+ * for a network sink, become OTLP log-record attributes a log pipeline (or the
1265
+ * Cloud log viewer) can filter and index on. Primitive values pass through;
1266
+ * objects/arrays are JSON-encoded at the sink boundary.
1267
+ */
1268
+ type LogFields = Record<string, unknown>;
1269
+ /**
1270
+ * One `ctx.log` severity method. Two call forms:
1271
+ *
1272
+ * - **Structured** `ctx.log.info("order placed", { orderId, total })`: a message string plus a `fields` object. The fields are indexed as attributes.
1273
+ * - **Console-style** — `ctx.log.info("state", value, other)`: any number of values, joined into the display message exactly like `console.log`.
1274
+ *
1275
+ * The structured form is matched when the second argument is a plain object;
1276
+ * otherwise the call is treated as console-style, so existing `console`-shaped
1277
+ * calls keep working unchanged.
1278
+ */
1279
+ interface LunoraLogMethod {
1280
+ (message: string, fields?: LogFields): void;
1281
+ (...args: unknown[]): void;
1282
+ }
1283
+ /**
1284
+ * Structured logger on every function `ctx`. Each call emits one attributed log
1285
+ * line — tagged with the function path on the server — that flows to an
1286
+ * `ObservabilitySink`'s `onLog` (where you route it in production) and, in
1287
+ * development, to the dev server terminal via the CLI / Vite plugin formatter.
1288
+ * Mirrors the `console` method names so it's a drop-in for `console.log` inside a
1289
+ * handler, but with attribution, structured fields, and a routable transport.
1290
+ *
1291
+ * Six severities spanning the OpenTelemetry ramp: `trace`, `debug`, `info` (and
1292
+ * its `log` alias), `warn`, `error`, `fatal`.
1293
+ *
1294
+ * Two ways to attach structured {@link LogFields}: pass them per call
1295
+ * (`ctx.log.info(message, fields)`) or bind them once with {@link with} for a
1296
+ * child logger that stamps every line. The rendered `message` and the structured
1297
+ * `fields` reach the dev terminal and the platform's Workers Logs; the raw,
1298
+ * un-rendered console-style arguments are preserved ONLY on the in-process
1299
+ * `onLog` sink (which you opt into and control).
1300
+ *
1301
+ * Attribution follows the dispatched function: a log emitted inside an internal
1302
+ * function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
1303
+ * the outer request entrypoint, since the composed call reuses its context.
1304
+ */
950
1305
  interface LunoraLogger {
951
- readonly debug: (...args: unknown[]) => void;
952
- readonly error: (...args: unknown[]) => void;
953
- readonly info: (...args: unknown[]) => void;
954
- readonly log: (...args: unknown[]) => void;
955
- readonly warn: (...args: unknown[]) => void;
1306
+ readonly debug: LunoraLogMethod;
1307
+ readonly error: LunoraLogMethod;
1308
+ readonly fatal: LunoraLogMethod;
1309
+ readonly info: LunoraLogMethod;
1310
+ readonly log: LunoraLogMethod;
1311
+ readonly trace: LunoraLogMethod;
1312
+ readonly warn: LunoraLogMethod;
1313
+ /**
1314
+ * Return a child logger that stamps `fields` onto every line it emits,
1315
+ * merged under any per-call fields (per-call wins on a key clash). Chainable
1316
+ * — `ctx.log.with({ requestId }).with({ step })` accumulates both. Use it to
1317
+ * bind request-scoped context once instead of repeating it per call.
1318
+ */
1319
+ readonly with: (fields: LogFields) => LunoraLogger;
1320
+ }
1321
+ /**
1322
+ * Handle the enclosing `ctx.trace` span hands its body, so the body can attach
1323
+ * attributes only known *after* it resolves (an AI call's token usage / dollar
1324
+ * cost, a downstream status, a computed count). Declared structurally here to
1325
+ * mirror `shared/span-event.ts`'s `SpanHandle` and `@lunora/do`'s implementation;
1326
+ * a cross-package assignability guard in `@lunora/testing` fails the build if the
1327
+ * three drift apart. Start attributes are snapshotted before the body runs;
1328
+ * handle writes are merged over them at record time, post-hoc winning on a clash.
1329
+ */
1330
+ interface SpanHandle {
1331
+ /** Set one attribute on the enclosing span (merged at record time; post-hoc wins on key clash). */
1332
+ setAttribute: (key: string, value: LogFields[string]) => void;
1333
+ /** Merge attributes onto the enclosing span (post-hoc wins on key clash). */
1334
+ setAttributes: (fields: LogFields) => void;
1335
+ }
1336
+ /**
1337
+ * Span factory on every function `ctx`. Wraps a sub-operation so it becomes its
1338
+ * own **span** nested under the dispatch's RPC span, giving a trace real shape:
1339
+ * without it a slow request is one opaque bar, with it you see which part was
1340
+ * slow.
1341
+ *
1342
+ * ```ts
1343
+ * const charge = await ctx.trace("stripe.charge", () => stripe.charges.create(…), { orderId });
1344
+ * ```
1345
+ *
1346
+ * **Nesting is explicit.** The body receives a tracer bound to its own span;
1347
+ * calling *that* is what makes a child:
1348
+ *
1349
+ * ```ts
1350
+ * await ctx.trace("fulfil", async (trace) => {
1351
+ * // Children of "fulfil" — including under Promise.all, where an ambient
1352
+ * // "currently open span" would mis-record these as nested inside each other.
1353
+ * await Promise.all([trace("reserve.stock", …), trace("email.receipt", …)]);
1354
+ * });
1355
+ * ```
1356
+ *
1357
+ * Calling `ctx.trace` again inside a body (rather than the passed tracer) is not
1358
+ * an error — that span is simply parented to the dispatch instead of to the
1359
+ * enclosing span, which is flatter but never wrong.
1360
+ *
1361
+ * Spans share the dispatch's trace id with its `ctx.log` lines and any container
1362
+ * the handler calls (the same `traceparent` is propagated), so one trace spans
1363
+ * worker, shard, and container.
1364
+ *
1365
+ * The span is recorded when the body settles, and the body's value is returned
1366
+ * unchanged. A throw is recorded as an error span and then **re-thrown** — this
1367
+ * is instrumentation, never flow control. Recording is best-effort: a failing
1368
+ * sink can't turn a working handler into a broken one.
1369
+ *
1370
+ * **Post-hoc attributes.** The body also receives a {@link SpanHandle} as its
1371
+ * second argument. The `attributes` passed here are stamped at span start (and
1372
+ * snapshotted, so a later mutation can't rewrite them); anything the body sets
1373
+ * through the handle — `span.setAttribute(k, v)` / `span.setAttributes({…})` — is
1374
+ * merged over that snapshot when the span is recorded, so a value known only once
1375
+ * the body has resolved (an AI call's token usage / dollar cost, a computed
1376
+ * count) still lands on the span. Post-hoc wins on a key clash. The handle is a
1377
+ * trailing parameter, so every existing `(trace) => …` body keeps working
1378
+ * unchanged.
1379
+ * @param name Span name, e.g. `"stripe.charge"`. Prefer a low-cardinality name
1380
+ * and put the varying part in `attributes` — a name built from an id makes every
1381
+ * span its own group in a collector.
1382
+ * @param fn The body to time, receiving a tracer bound to this span for any
1383
+ * nested spans and the enclosing span's {@link SpanHandle} for post-hoc
1384
+ * attributes. May be sync or async; the result is awaited.
1385
+ * @param attributes Structured attributes to stamp on the span at start,
1386
+ * normalized like a log line's `fields`.
1387
+ */
1388
+ type LunoraTracer = <T>(name: string, function_: (trace: LunoraTracer, span: SpanHandle) => Promise<T> | T, attributes?: LogFields) => Promise<T>;
1389
+ /**
1390
+ * Application metrics on every function `ctx` — the third signal alongside
1391
+ * `ctx.log` and `ctx.trace`. Each call records one measurement that flows to an
1392
+ * `ObservabilitySink`'s `onMetric`, and from `otlpSink` to a collector's
1393
+ * `/v1/metrics`.
1394
+ *
1395
+ * ```ts
1396
+ * ctx.metrics.count("orders.placed", 1, { plan: user.plan });
1397
+ * ctx.metrics.record("checkout.latency_ms", Date.now() - started);
1398
+ * ctx.metrics.gauge("cart.items", cart.items.length);
1399
+ * ```
1400
+ *
1401
+ * Pick the instrument by the question you want to answer: `count` for "how many"
1402
+ * (summed over time), `gauge` for "how many right now" (replaces the last
1403
+ * reading), `record` for "what's the distribution" (percentiles, not just a
1404
+ * mean).
1405
+ *
1406
+ * `attributes` are the metric's dimensions. Keep them **low-cardinality** — an
1407
+ * attribute valued by user id or order id creates a distinct time series per id,
1408
+ * which is how a metrics backend gets expensive. Put identifiers on a log line or
1409
+ * a span instead.
1410
+ *
1411
+ * No pre-aggregation happens: one call is one exported measurement, with counter
1412
+ * deltas for the collector to sum. In a hot loop, sum locally and record once
1413
+ * rather than calling per iteration.
1414
+ */
1415
+ interface LunoraMetrics {
1416
+ /**
1417
+ * Add to a monotonic counter (default `1`) — requests served, retries,
1418
+ * bytes sent. The collector sums successive deltas.
1419
+ */
1420
+ readonly count: (name: string, value?: number, attributes?: LogFields) => void;
1421
+ /**
1422
+ * Report a point-in-time reading that replaces the previous one — queue
1423
+ * depth, cache size, connections open.
1424
+ */
1425
+ readonly gauge: (name: string, value: number, attributes?: LogFields) => void;
1426
+ /**
1427
+ * Observe one sample of a distribution — latency, payload size. Use this,
1428
+ * not a counter, when percentiles matter.
1429
+ */
1430
+ readonly record: (name: string, value: number, attributes?: LogFields) => void;
956
1431
  }
957
1432
  interface QueryCtx {
958
1433
  readonly auth: AuthState;
959
1434
  readonly db: DatabaseReader;
960
1435
  /**
961
- * The caller's IP for this request Cloudflare's trusted `CF-Connecting-IP`,
962
- * forwarded server-side (never read from a client header). `undefined` when
963
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
964
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
965
- */
1436
+ * The validated, typed environment. Populated only when the project declares
1437
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1438
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1439
+ * coercion-aware. Absent (optional) without a contract declare
1440
+ * `lunora/env.ts` to populate and type it.
1441
+ */
1442
+ readonly env?: Record<string, unknown>;
1443
+ /**
1444
+ * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1445
+ * forwarded server-side (never read from a client header). `undefined` when
1446
+ * unknown: a live-subscription re-run, a server-initiated dispatch, or
1447
+ * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1448
+ */
966
1449
  readonly ip?: string;
967
1450
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
968
1451
  readonly log: LunoraLogger;
1452
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
1453
+ readonly metrics: LunoraMetrics;
969
1454
  /**
970
- * Wall-clock time (epoch ms) the function began, captured once so the whole
971
- * handler sees a single stable value. Query/mutation handlers must be
972
- * deterministic — they may be re-run on OCC retry / subscription re-eval — so
973
- * read time through `ctx.now` instead of `Date.now()` (the latter is flagged
974
- * by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
975
- */
1455
+ * Wall-clock time (epoch ms) the function began, captured once so the whole
1456
+ * handler sees a single stable value. Query/mutation handlers must be
1457
+ * deterministic — they may be re-run on OCC retry / subscription re-eval — so
1458
+ * read time through `ctx.now` instead of `Date.now()` (the latter is flagged
1459
+ * by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
1460
+ */
976
1461
  readonly now: number;
977
1462
  /**
978
- * Compose a read-only subquery in-process, reusing this query's read
979
- * context (same transaction, same `db`). Executes the referenced query's
980
- * handler directly — no fresh DO RPC round-trip — so it observes the exact
981
- * same snapshot. A query may only call other queries; there is no
982
- * `runMutation` on a `QueryCtx` (writes are not allowed from a query).
983
- * Mirrors Convex's `ctx.runQuery`.
984
- */
1463
+ * Compose a read-only subquery in-process, reusing this query's read
1464
+ * context (same transaction, same `db`). Executes the referenced query's
1465
+ * handler directly — no fresh DO RPC round-trip — so it observes the exact
1466
+ * same snapshot. A query may only call other queries; there is no
1467
+ * `runMutation` on a `QueryCtx` (writes are not allowed from a query).
1468
+ * Mirrors Convex's `ctx.runQuery`.
1469
+ */
985
1470
  readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
1471
+ /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1472
+ readonly secrets: Secrets;
986
1473
  readonly storage: ReadOnlyStorage;
1474
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
1475
+ readonly trace: LunoraTracer;
987
1476
  readonly vectors: VectorSearchReader;
988
1477
  }
989
1478
  interface MutationCtx {
990
1479
  readonly auth: AuthState;
991
1480
  readonly db: DatabaseWriter;
992
1481
  /**
993
- * The caller's IP for this request Cloudflare's trusted `CF-Connecting-IP`,
994
- * forwarded server-side (never read from a client header). `undefined` when
995
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
996
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
997
- */
1482
+ * The validated, typed environment. Populated only when the project declares
1483
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1484
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1485
+ * coercion-aware. Absent (optional) without a contract declare
1486
+ * `lunora/env.ts` to populate and type it.
1487
+ */
1488
+ readonly env?: Record<string, unknown>;
1489
+ /**
1490
+ * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1491
+ * forwarded server-side (never read from a client header). `undefined` when
1492
+ * unknown: a live-subscription re-run, a server-initiated dispatch, or
1493
+ * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1494
+ */
998
1495
  readonly ip?: string;
999
1496
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
1000
1497
  readonly log: LunoraLogger;
1498
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
1499
+ readonly metrics: LunoraMetrics;
1001
1500
  /**
1002
- * Wall-clock time (epoch ms) the function began, captured once so the whole
1003
- * handler sees a single stable value. Mutation handlers must be deterministic
1004
- * — they may be re-run on OCC retry — so read time through `ctx.now` instead
1005
- * of `Date.now()` (the latter is flagged by the `nondeterministic_query_mutation`
1006
- * advisor). Actions may use `Date.now()`.
1007
- */
1501
+ * Wall-clock time (epoch ms) the function began, captured once so the whole
1502
+ * handler sees a single stable value. Mutation handlers must be deterministic
1503
+ * — they may be re-run on OCC retry — so read time through `ctx.now` instead
1504
+ * of `Date.now()` (the latter is flagged by the `nondeterministic_query_mutation`
1505
+ * advisor). Actions may use `Date.now()`.
1506
+ */
1008
1507
  readonly now: number;
1009
1508
  /**
1010
- * Compose a submutation in-process, reusing this mutation's `db` writer.
1011
- * Executes the referenced mutation's handler directly — no fresh DO RPC —
1012
- * so its writes apply through the same shard invocation as the enclosing
1013
- * mutation. Note: writes are not wrapped in a SQL transaction, so a partial
1014
- * failure does not roll back earlier writes (the same as a top-level
1015
- * mutation). Mirrors Convex's `ctx.runMutation`.
1016
- */
1509
+ * Compose a submutation in-process, reusing this mutation's `db` writer.
1510
+ * Executes the referenced mutation's handler directly — no fresh DO RPC —
1511
+ * so its writes apply through the same shard invocation as the enclosing
1512
+ * mutation. Note: writes are not wrapped in a SQL transaction, so a partial
1513
+ * failure does not roll back earlier writes (the same as a top-level
1514
+ * mutation). Mirrors Convex's `ctx.runMutation`.
1515
+ */
1017
1516
  readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
1018
1517
  /**
1019
- * Compose a read-only subquery in-process, reusing this mutation's `db`.
1020
- * Executes the referenced query's handler directly — no fresh DO RPC — so
1021
- * it observes this mutation's in-flight writes. Mirrors Convex's
1022
- * `ctx.runQuery`.
1023
- */
1518
+ * Compose a read-only subquery in-process, reusing this mutation's `db`.
1519
+ * Executes the referenced query's handler directly — no fresh DO RPC — so
1520
+ * it observes this mutation's in-flight writes. Mirrors Convex's
1521
+ * `ctx.runQuery`.
1522
+ */
1024
1523
  readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
1025
1524
  readonly scheduler: Scheduler;
1525
+ /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1526
+ readonly secrets: Secrets;
1026
1527
  readonly storage: ReadOnlyStorage;
1528
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
1529
+ readonly trace: LunoraTracer;
1027
1530
  readonly vectors: VectorSearch;
1028
1531
  /** Start / resume / inspect durable workflows; see {@link Workflows}. */
1029
1532
  readonly workflows: Workflows;
1030
1533
  }
1031
1534
  interface ActionCtx {
1032
1535
  readonly auth: AuthState;
1536
+ /**
1537
+ * Programmatic Workers Cache purge; see {@link CachePurge}.
1538
+ * **Action-only** — actions run in the Worker, which has a `cache` binding.
1539
+ * Queries and mutations run inside the Durable Object and do not expose this.
1540
+ * Optional at runtime because Workers Cache is only present when enabled.
1541
+ */
1542
+ readonly cache?: CachePurge;
1033
1543
  readonly db: DatabaseWriter;
1544
+ /**
1545
+ * The validated, typed environment. Populated only when the project declares
1546
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1547
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1548
+ * coercion-aware. Absent (optional) without a contract — declare
1549
+ * `lunora/env.ts` to populate and type it.
1550
+ */
1551
+ readonly env?: Record<string, unknown>;
1034
1552
  readonly fetch: typeof globalThis.fetch;
1035
1553
  /**
1036
- * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1037
- * forwarded server-side (never read from a client header). `undefined` when
1038
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
1039
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1040
- */
1554
+ * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1555
+ * forwarded server-side (never read from a client header). `undefined` when
1556
+ * unknown: a live-subscription re-run, a server-initiated dispatch, or
1557
+ * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1558
+ */
1041
1559
  readonly ip?: string;
1042
1560
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
1043
1561
  readonly log: LunoraLogger;
1562
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
1563
+ readonly metrics: LunoraMetrics;
1044
1564
  /**
1045
- * Wall-clock time (epoch ms) the action began, captured once for convenience
1046
- * and parity with query/mutation `ctx.now`. Actions run exactly once, so they
1047
- * may also use ambient `Date.now()` freely.
1048
- */
1565
+ * Wall-clock time (epoch ms) the action began, captured once for convenience
1566
+ * and parity with query/mutation `ctx.now`. Actions run exactly once, so they
1567
+ * may also use ambient `Date.now()` freely.
1568
+ */
1049
1569
  readonly now: number;
1050
1570
  readonly runAction: <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>) => Promise<R>;
1051
1571
  readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
1052
1572
  readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
1053
1573
  readonly scheduler: Scheduler;
1574
+ /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1575
+ readonly secrets: Secrets;
1054
1576
  readonly storage: Storage;
1577
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
1578
+ readonly trace: LunoraTracer;
1055
1579
  readonly vectors: VectorSearch;
1056
1580
  /** Start / resume / inspect durable workflows; see {@link Workflows}. */
1057
1581
  readonly workflows: Workflows;
1058
1582
  }
1059
1583
  /**
1060
- * Stand-in returned by codegen so projects can `import { api } from "./_generated/api"`.
1061
- * The runtime value is opaque; the types are filled in by generated declarations.
1062
- */
1584
+ * Stand-in returned by codegen so projects can `import { api } from "./_generated/api"`.
1585
+ * The runtime value is opaque; the types are filled in by generated declarations.
1586
+ */
1063
1587
  type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1064
1588
  declare const anyApi: AnyApi;
1065
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionKind, type FunctionVisibility, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LunoraLogger, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, 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 VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi };
1589
+ export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExposeConfig, type ExternalSourceCursor, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GeoBoundingBox, type GeoFilterBuilder, type GeoIndexDefinition, type GeoPointInput, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LogFields, type LunoraLogMethod, type LunoraLogger, type LunoraMetrics, type LunoraTracer, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type SpanHandle, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, 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 VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };