@lunora/server 1.0.0-alpha.19 → 1.0.0-alpha.191

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 (98) hide show
  1. package/LICENSE.md +213 -0
  2. package/README.md +18 -12
  3. package/dist/data-model.d.mts +230 -159
  4. package/dist/data-model.d.ts +230 -159
  5. package/dist/data-model.mjs +0 -1
  6. package/dist/drizzle.mjs +1 -1
  7. package/dist/in-process.d.mts +101 -0
  8. package/dist/in-process.d.ts +101 -0
  9. package/dist/in-process.mjs +1 -0
  10. package/dist/index.d.mts +3424 -1670
  11. package/dist/index.d.ts +3424 -1670
  12. package/dist/index.mjs +1 -29
  13. package/dist/otel.d.mts +554 -0
  14. package/dist/otel.d.ts +554 -0
  15. package/dist/otel.mjs +1 -0
  16. package/dist/packem_shared/ACTION_CACHE_DEFAULT_TTL_MS-DITNUhX1.mjs +1 -0
  17. package/dist/packem_shared/DEFAULT_LIMIT-pT_tV8R0.mjs +1 -0
  18. package/dist/packem_shared/DOCUMENT_HISTORY_REDACTED_FIELDS-BYeHnULU.mjs +1 -0
  19. package/dist/packem_shared/LunoraEnvError-bkdMu_-N.mjs +4 -0
  20. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-Bvno49rr.mjs +1 -0
  21. package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
  22. package/dist/packem_shared/apply-output-C5wZ5EAL.mjs +1 -0
  23. package/dist/packem_shared/asBucketStorage-DoGYOjq3.mjs +1 -0
  24. package/dist/packem_shared/assertShapesDeclareReadPolicies-mKiSAI2N.mjs +1 -0
  25. package/dist/packem_shared/beginDeferredDeletes-DhwFBbvs.mjs +1 -0
  26. package/dist/packem_shared/beginDeferredSchedules-CmmdpWRS.mjs +1 -0
  27. package/dist/packem_shared/bindOrm-D_hFUjmr.mjs +1 -0
  28. package/dist/packem_shared/buildMaskRegistry-DpWMtalG.mjs +1 -0
  29. package/dist/packem_shared/compose-middleware-BK9FlblN.mjs +1 -0
  30. package/dist/packem_shared/composePluginMiddleware-bf3t7KZi.mjs +1 -0
  31. package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
  32. package/dist/packem_shared/createPolicyDsl-BZa6SqLJ.mjs +1 -0
  33. package/dist/packem_shared/createSecrets-D6rLB42U.mjs +1 -0
  34. package/dist/packem_shared/createServices-IJvoqi0e.mjs +1 -0
  35. package/dist/packem_shared/defineAggregateIndex--ttNAY7M.mjs +1 -0
  36. package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
  37. package/dist/packem_shared/defineIdentityGuard-Bt8N65Zs.mjs +1 -0
  38. package/dist/packem_shared/defineMigration-CXOS0Bvq.mjs +1 -0
  39. package/dist/packem_shared/defineModule-OecXWM3U.mjs +1 -0
  40. package/dist/packem_shared/defineMutator-B4ipDtDR.mjs +1 -0
  41. package/dist/packem_shared/defineShape-B8U-uTeQ.mjs +1 -0
  42. package/dist/packem_shared/defineStorageRule-Dv4nJE0H.mjs +1 -0
  43. package/dist/packem_shared/fnv1a-D3ueNTWB.mjs +1 -0
  44. package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
  45. package/dist/packem_shared/httpAction-CsKNC6op.mjs +5 -0
  46. package/dist/packem_shared/initLunora-B-7Xs3Qq.mjs +1 -0
  47. package/dist/packem_shared/isPerDispatchMiddleware-DDQPatSm.mjs +1 -0
  48. package/dist/packem_shared/mask-BwK_5ndJ.mjs +1 -0
  49. package/dist/packem_shared/middleware-C1CfQMYn.mjs +1 -0
  50. package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
  51. package/dist/packem_shared/onQueryChange-CatdYnH5.mjs +1 -0
  52. package/dist/packem_shared/onWhisper-CfDbFLQh.mjs +1 -0
  53. package/dist/packem_shared/platformAdmin-kczXQme8.mjs +1 -0
  54. package/dist/packem_shared/plugin-xbEqjwqP.mjs +1 -0
  55. package/dist/packem_shared/policy-tag-V0lqFzgS.mjs +1 -0
  56. package/dist/packem_shared/protectPublic-Bi5_95aP.mjs +1 -0
  57. package/dist/packem_shared/rls-B5WKMHyP.mjs +1 -0
  58. package/dist/packem_shared/run-middleware-BefYq7Xd.mjs +1 -0
  59. package/dist/packem_shared/serveStorageObject-SPHQyLa4.mjs +1 -0
  60. package/dist/packem_shared/stable-key-B_BlboiY.mjs +1 -0
  61. package/dist/packem_shared/storageRules-nxlI5c15.mjs +1 -0
  62. package/dist/packem_shared/types.d-B7FLdYUD.d.mts +148 -0
  63. package/dist/packem_shared/types.d-BCtjbIzl.d.ts +148 -0
  64. package/dist/packem_shared/wire-codec-CZ4obM6q.mjs +1 -0
  65. package/dist/rls/testing.d.mts +42 -34
  66. package/dist/rls/testing.d.ts +42 -34
  67. package/dist/rls/testing.mjs +1 -49
  68. package/dist/types.d.mts +2138 -622
  69. package/dist/types.d.ts +2138 -622
  70. package/dist/types.mjs +0 -31
  71. package/package.json +23 -6
  72. package/dist/packem_shared/LunoraEnvError-BGmd1Qs0.mjs +0 -187
  73. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +0 -9
  74. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BtPCiNLW.mjs +0 -114
  75. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  76. package/dist/packem_shared/bindOrm-Bfgl7f-Q.mjs +0 -146
  77. package/dist/packem_shared/buildRlsReadRegistry-D54vUQe4.mjs +0 -107
  78. package/dist/packem_shared/composePluginMiddleware-CcTj1_v2.mjs +0 -103
  79. package/dist/packem_shared/createPolicyDsl-By3QB4he.mjs +0 -32
  80. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +0 -58
  81. package/dist/packem_shared/defineAggregateIndex-znq4ygKQ.mjs +0 -295
  82. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +0 -35
  83. package/dist/packem_shared/defineMigration-Hx01yIht.mjs +0 -10
  84. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
  85. package/dist/packem_shared/defineShape-C5scNOrf.mjs +0 -18
  86. package/dist/packem_shared/defineStorageRule-B5nL4Z1P.mjs +0 -23
  87. package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
  88. package/dist/packem_shared/httpAction-DCXoYPIk.mjs +0 -355
  89. package/dist/packem_shared/initLunora-sQUqaejx.mjs +0 -100
  90. package/dist/packem_shared/mask-BT1YgRbF.mjs +0 -291
  91. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  92. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
  93. package/dist/packem_shared/protectPublic-BlcGpiRc.mjs +0 -15
  94. package/dist/packem_shared/rls-B_ZWgslr.mjs +0 -595
  95. package/dist/packem_shared/run-middleware-I6EiQfxL.mjs +0 -20
  96. package/dist/packem_shared/storageRules-6QxzDOcx.mjs +0 -88
  97. package/dist/packem_shared/types.d-BB3pjV0m.d.mts +0 -141
  98. package/dist/packem_shared/types.d-Cxl6ndhm.d.ts +0 -141
package/dist/types.d.mts CHANGED
@@ -1,4 +1,406 @@
1
- import { ValidatorMap, InferValidatorMap, Id, Validator, Infer } from '@lunora/values';
1
+ import { Id, Validator, Infer, ValidatorMap, InferValidatorMap } from '@lunora/values';
2
+ /**
3
+ * The set of languages a `.searchIndex()` can declare, in one place.
4
+ *
5
+ * Three layers need it and none of them can import the others: `@lunora/server`
6
+ * types the option and validates it at schema-build time, and `@lunora/do` maps
7
+ * it onto a stopword list at analysis time — with no dependency edge between
8
+ * them (nor should there be: the schema builder must stay usable without the DO
9
+ * runtime). So it lived in three hand-maintained copies, where adding a
10
+ * language silently produced a schema the builder accepts and the analyzer
11
+ * treats as `"none"` — an index that quietly stops dropping stopwords.
12
+ *
13
+ * The list is the single source: the type is derived from it, so the two cannot
14
+ * drift. `@lunora/do` still owns the stopword *lists*; it keys them by these
15
+ * names, and its own tests assert the two agree.
16
+ *
17
+ * Inlined by the bundler into each `dist` rather than published, so this stays
18
+ * a shared *constant*, not a dependency edge. See the `shared/` section of
19
+ * AGENTS.md.
20
+ */
21
+ /**
22
+ * Every accepted value, sorted, so an error message can list what *is*
23
+ * accepted without re-sorting. `"none"` is explicit rather than absent: it
24
+ * means "fold, but drop nothing", which is also what an index with no declared
25
+ * language gets.
26
+ */
27
+ declare const SEARCH_LANGUAGES: readonly ["de", "en", "es", "fr", "it", "nl", "none", "pt"];
28
+ /** A declared analysis language, derived from the list above. */
29
+ type SearchLanguage = (typeof SEARCH_LANGUAGES)[number];
30
+ /**
31
+ * How a search index is stored. `"portable"` promises identical behaviour on
32
+ * every backend; `"native"` opts into the engine's own full-text index where it
33
+ * has one, trading that promise for its speed.
34
+ *
35
+ * Lives beside the languages for the same reason they do: it is declared in
36
+ * `@lunora/server`, acted on in the storage layer, and there is no dependency
37
+ * edge between them — so a third hand-maintained copy is how a typo turns into
38
+ * a silently different *physical layout*.
39
+ */
40
+ declare const SEARCH_STRATEGIES: readonly ["native", "portable"];
41
+ /** A declared storage strategy, derived from the list above. */
42
+ type SearchStrategy = (typeof SEARCH_STRATEGIES)[number];
43
+ /**
44
+ * Declared HTTP caching for an exposed endpoint (`.expose({ cache })`). Lives
45
+ * here, alongside the path/method contract, for the same reason: the runtime
46
+ * WRITES these headers and the OpenAPI emitter DESCRIBES them, and the two must
47
+ * not be able to disagree. Deriving both from {@link cacheControlValue} /
48
+ * {@link cacheVaryValue} makes "the published spec matches what the runtime
49
+ * actually sends" structural rather than a hand-kept invariant.
50
+ */
51
+ interface RestCachePolicy {
52
+ /**
53
+ * Extra request headers this app authenticates on, beyond
54
+ * {@link CREDENTIAL_HEADERS}. Declare these whenever `resolveIdentity` reads
55
+ * something else (`x-api-key`, a tenant header, …): they join both the
56
+ * credential check and the emitted `Vary`. Without them, a caller
57
+ * authenticating that way is treated as anonymous.
58
+ */
59
+ readonly credentialHeaders?: ReadonlyArray<string>;
60
+ readonly maxAge: number;
61
+ readonly scope: "private" | "public";
62
+ readonly staleWhileRevalidate?: number;
63
+ readonly tag?: string;
64
+ readonly vary?: string;
65
+ }
66
+ /**
67
+ * The `ctx.storage` surface, per function tier: {@link ReadOnlyStorage} for a
68
+ * query, {@link MutationStorage} for a mutation, {@link Storage} for an action.
69
+ *
70
+ * A structural mirror of `@lunora/storage`'s `Storage` — `@lunora/server` takes
71
+ * no dependency on that package. Re-exported through `../types`, so the public
72
+ * names are unchanged.
73
+ */
74
+ /**
75
+ * Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
76
+ * public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
77
+ * the ctx surface carries no dependency on the storage package's types. Matches
78
+ * the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
79
+ */
80
+ interface StorageMetadata {
81
+ /** The object's `Content-Type`, when recorded. */
82
+ contentType?: string;
83
+ /** Custom metadata set at upload time, if any. */
84
+ customMetadata?: Record<string, string>;
85
+ /** The object's key. */
86
+ key: string;
87
+ /** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
88
+ sha256?: string;
89
+ /** Body length in bytes. */
90
+ size: number;
91
+ /** When the object was last written (epoch ms), when reported. */
92
+ uploaded?: number;
93
+ }
94
+ /**
95
+ * The body-free object shape returned by {@link ReadOnlyStorage.head} — a clean
96
+ * public mirror of `@lunora/storage`'s head projection, re-declared here for the
97
+ * same reason as {@link StorageMetadata}: the ctx surface carries no dependency
98
+ * on the storage package's types.
99
+ *
100
+ * Richer than {@link StorageMetadata} on purpose. `getMetadata` is the tidy
101
+ * Convex-shaped summary; `head` is what an HTTP layer needs, so it keeps the
102
+ * validator (`etag`) and the base64 digest RFC 9530 `Repr-Digest` requires, and
103
+ * leaves `uploaded` as the `Date` the binding reports rather than epoch ms.
104
+ */
105
+ interface StorageObjectHead {
106
+ /** Custom metadata set at upload time, if any. */
107
+ customMetadata?: Record<string, string>;
108
+ /**
109
+ * R2's unquoted etag (the MD5 hex for a single-part upload). Required: R2
110
+ * reports one on every object, and an HTTP layer built on `head()` (the
111
+ * `serveStorageObject` helper) needs it to emit a validator.
112
+ */
113
+ etag: string;
114
+ /** The already-quoted form of {@link StorageObjectHead.etag}, when the binding reports one. */
115
+ httpEtag?: string;
116
+ /** Recorded HTTP metadata, notably the `Content-Type`. */
117
+ httpMetadata?: {
118
+ contentType?: string;
119
+ };
120
+ /** The object's key. */
121
+ key: string;
122
+ /** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
123
+ sha256?: string;
124
+ /** Base64-encoded SHA-256 of the same checksum — the encoding RFC 9530 digest headers require. */
125
+ sha256Base64?: string;
126
+ /** The FULL object size in bytes: R2 reports the object's size, not a returned window's, which is what makes a head enough to resolve a `Range` against. */
127
+ size: number;
128
+ /** When the object was last written. */
129
+ uploaded?: Date;
130
+ }
131
+ /**
132
+ * Byte window forwarded to {@link ReadOnlyStorage.download} so R2 resolves the
133
+ * slice server-side and streams only those bytes back. Mirrors R2's own `range`
134
+ * option (`@lunora/platform`'s `R2RangeLike`), restated structurally so
135
+ * `@lunora/server` takes no dependency on the bindings package.
136
+ */
137
+ type StorageRange = {
138
+ length?: number;
139
+ offset: number;
140
+ } | {
141
+ length: number;
142
+ offset?: number;
143
+ } | {
144
+ suffix: number;
145
+ };
146
+ /**
147
+ * A downloaded object: the same metadata {@link StorageObjectHead} carries, plus
148
+ * the body stream. This is what `download()` resolves to — R2's object, NOT a
149
+ * bare stream.
150
+ */
151
+ interface StorageObjectBody extends StorageObjectHead {
152
+ /** The object body stream. `null` for a zero-byte object. */
153
+ body: ReadableStream | null;
154
+ }
155
+ /**
156
+ * Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
157
+ *
158
+ * Queries are pure reads, and mutations run inside a transactional scope —
159
+ * neither is allowed to perform side-effectful R2 writes (`upload`) or
160
+ * deletes (`delete`). They can, however, **read** existing objects and
161
+ * resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
162
+ * trip), so the read-only surface keeps `download` and `getSignedUrl`. The
163
+ * full {@link Storage} surface stays on `ActionCtx`.
164
+ */
165
+ interface ReadOnlyStorage<Buckets extends string = string> {
166
+ /**
167
+ * Select a named bucket (declared via `v.storage("name")`). The returned
168
+ * accessor's operations target that bucket — `ctx.storage.bucket("avatars")
169
+ * .download(key)`. The bare `ctx.storage` targets the default bucket.
170
+ */
171
+ bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
172
+ /** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
173
+ readonly bucketName: string;
174
+ /**
175
+ * Fetch an existing object. Returns the R2 object — metadata plus a `body`
176
+ * stream — or `null` when absent.
177
+ *
178
+ * NOT a bare stream: `new Response(await ctx.storage.download(key))` would
179
+ * stringify the object and serve the literal text `[object Object]`. Reach
180
+ * for the body explicitly:
181
+ *
182
+ * ```ts
183
+ * const object = await ctx.storage.download(key);
184
+ *
185
+ * return object ? new Response(object.body) : new Response("Not found", { status: 404 });
186
+ * ```
187
+ *
188
+ * (`serveStorageObject` from `@lunora/server` does this, plus range/ETag
189
+ * handling, for the common "serve a stored file over HTTP" case.)
190
+ *
191
+ * Pass `range` to have R2 resolve the byte window server-side, so the
192
+ * unwanted bytes never reach the worker.
193
+ */
194
+ download: (key: string, options?: {
195
+ range?: StorageRange;
196
+ }) => Promise<StorageObjectBody | null>;
197
+ /**
198
+ * Read a file's metadata (size, content-type, sha256, upload time, custom
199
+ * metadata) without fetching its body. Returns `null` when the object is
200
+ * absent. Mirrors Convex's `ctx.storage.getMetadata`.
201
+ */
202
+ getMetadata: (key: string) => Promise<StorageMetadata | null>;
203
+ /** Resolve a short-lived signed URL for an existing object. */
204
+ getSignedUrl: (key: string, options?: {
205
+ expiresInSeconds?: number;
206
+ }) => Promise<string>;
207
+ /** Public URL pointing at the configured base for `key`. */
208
+ getUrl: (key: string) => string;
209
+ /**
210
+ * Read an object's metadata with NO body transfer, as the raw object shape —
211
+ * `etag` and the base64 digest included, which is what an HTTP layer needs to
212
+ * answer a `Range` request. Returns `null` when the object is absent.
213
+ *
214
+ * {@link ReadOnlyStorage.getMetadata} is the tidier summary over the same
215
+ * read; reach for this one when building a response.
216
+ */
217
+ head: (key: string) => Promise<StorageObjectHead | null>;
218
+ }
219
+ /**
220
+ * `ctx.storage` inside a **mutation**: the read surface, plus the one write a
221
+ * transactional context can safely express. See `@lunora/server`'s
222
+ * `deferred-deletes.ts` for why `delete` itself stays action-only.
223
+ */
224
+ interface MutationStorage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
225
+ /** Select a named bucket; deletes queued on it are flushed against that bucket. */
226
+ bucket: (name: Buckets) => MutationStorage<Buckets>;
227
+ /**
228
+ * Queue `key` for deletion once this mutation commits.
229
+ *
230
+ * Returns `void`, not a promise: nothing has been attempted yet, so the object
231
+ * is still there on the next line. A rolled-back mutation never flushes, so
232
+ * the row deletion and the object cleanup cannot disagree. A failed delete
233
+ * leaks the object rather than failing a mutation that already succeeded, and
234
+ * is logged with its key.
235
+ */
236
+ deleteAfterCommit: (key: string) => void;
237
+ }
238
+ /**
239
+ * The body shapes `store` / `upload` accept — what R2's `put` stores bytes from.
240
+ * Mirrors `@lunora/storage`'s `Storage.upload` parameter.
241
+ */
242
+ type StorageUploadBody = ArrayBuffer | ArrayBufferView | Blob | ReadableStream | string;
243
+ /**
244
+ * Options for `store` / `upload` — a structural mirror of `@lunora/storage`'s
245
+ * `UploadOptions`, so the `maxSize` / `allowedContentTypes` guards are reachable
246
+ * from `ctx.storage` without a type dependency on the storage package.
247
+ */
248
+ interface StorageUploadOptions {
249
+ /** Content-type allowlist; the supplied `contentType` must match. */
250
+ allowedContentTypes?: ReadonlyArray<string>;
251
+ contentType?: string;
252
+ customMetadata?: Record<string, string>;
253
+ /**
254
+ * Maximum body size in bytes. A `ReadableStream` is buffered under the cap,
255
+ * so on the stream path `maxSize` is itself capped at 16 MiB — above that use
256
+ * {@link Storage.createMultipartUpload}.
257
+ */
258
+ maxSize?: number;
259
+ /** SHA-256 of the body (hex or a 32-byte buffer); R2 records and verifies it. */
260
+ sha256?: ArrayBuffer | string;
261
+ }
262
+ /** What `store` / `upload` resolve to: the stored key plus its etag (unquoted and quoted). */
263
+ interface StorageUploadResult {
264
+ etag: string;
265
+ httpEtag: string;
266
+ key: string;
267
+ }
268
+ /** One uploaded multipart part — returned by `uploadPart`, required by `complete`. Mirrors R2's `R2UploadedPart`. */
269
+ interface StorageUploadedPart {
270
+ etag: string;
271
+ partNumber: number;
272
+ }
273
+ /**
274
+ * An in-progress R2 multipart upload, as {@link Storage.createMultipartUpload} /
275
+ * {@link Storage.resumeMultipartUpload} return it. A structural mirror of
276
+ * R2's own multipart upload handle, as `@lunora/platform` declares it.
277
+ *
278
+ * Every part except the last must be the same size, and at least 5 MiB. The
279
+ * object does not guarantee the upload still exists — a parallel `complete` /
280
+ * `abort` invalidates it — so handle errors on each call.
281
+ */
282
+ interface StorageMultipartUpload {
283
+ /** Abort the upload, discarding every uploaded part. */
284
+ abort: () => Promise<void>;
285
+ /** Finish the upload from the collected parts; resolves to the stored object's metadata. */
286
+ complete: (uploadedParts: StorageUploadedPart[]) => Promise<StorageObjectHead>;
287
+ /** The object key being assembled. */
288
+ readonly key: string;
289
+ /** The R2 upload id — persist it to resume the upload in a later request. */
290
+ readonly uploadId: string;
291
+ /** Upload one part (1-indexed); returns the `{ partNumber, etag }` to pass to `complete`. */
292
+ uploadPart: (partNumber: number, value: StorageUploadBody) => Promise<StorageUploadedPart>;
293
+ }
294
+ /** Options for {@link Storage.createMultipartUpload}: the metadata recorded on the assembled object. */
295
+ interface StorageMultipartUploadOptions {
296
+ contentType?: string;
297
+ customMetadata?: Record<string, string>;
298
+ }
299
+ /** Options for {@link Storage.list}. Mirrors `@lunora/storage`'s `ListOptions`. */
300
+ interface StorageListOptions {
301
+ /** The `cursor` of the previous page. */
302
+ cursor?: string;
303
+ /** R2 list delimiter — keys sharing a segment roll up into `delimitedPrefixes`. */
304
+ delimiter?: string;
305
+ /** Page-size ceiling: defaults to 100, capped at 1000. A positive integer; anything else throws. */
306
+ limit?: number;
307
+ }
308
+ /** One page of {@link Storage.list}. */
309
+ interface StorageListResult {
310
+ /** Pass back as `options.cursor` for the next page, while `truncated` is `true`. */
311
+ cursor?: string;
312
+ /** The rolled-up "folders" when `options.delimiter` is set — these are NOT in `objects`. */
313
+ delimitedPrefixes?: string[];
314
+ /** The objects on this page, body-free and JSON-serializable. */
315
+ objects: StorageObjectHead[];
316
+ /** `true` while more pages remain. Paginate on this, never on `objects.length`. */
317
+ truncated?: boolean;
318
+ }
319
+ /**
320
+ * `ctx.storage` inside an **action**: the full `@lunora/storage` `Storage`
321
+ * surface, plus `bucket(name)`.
322
+ *
323
+ * Declared structurally rather than imported — `@lunora/server` takes no
324
+ * runtime or published-type dependency on `@lunora/storage` — so it tracks that
325
+ * surface by hand. A type test (`__tests__/storage-surface.test-d.ts`) fails
326
+ * `tsc` when the two drift —
327
+ * the member sets differ (a runtime member left untyped here, or a member typed
328
+ * here the runtime lacks — `bucket` aside, which the ctx wiring adds); the
329
+ * runtime object no longer satisfies a signature here (a return type widened,
330
+ * or a parameter narrowed, upstream); or a parameter list here differs from the
331
+ * runtime's (an option or body shape the runtime takes but this interface
332
+ * refuses). `getSignedUrl` is the one deliberate parameter exception: its PUT
333
+ * options would mint an upload URL from a query.
334
+ *
335
+ * Return types here may be NARROWER than the runtime's on purpose — `head`
336
+ * resolves to the {@link StorageObjectHead} mirror rather than R2's object.
337
+ *
338
+ * Every member here is action-only. Under a procedure guarded by
339
+ * `storageRules(...)`, the write paths (`store` / `upload` /
340
+ * `createMultipartUpload` / `resumeMultipartUpload`) are gated as `write`, and
341
+ * `getPresignedUrl` / `list`, which no rule can gate, reject with `FORBIDDEN`.
342
+ */
343
+ interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
344
+ /** Select a named bucket; the returned accessor exposes the full read/write surface. */
345
+ bucket: (name: Buckets) => Storage<Buckets>;
346
+ /**
347
+ * Begin a native R2 **multipart upload** — for objects above `store()`'s
348
+ * 16 MiB stream cap, streamed through the Worker without buffering. Upload
349
+ * the parts (each at least 5 MiB and uniform in size, except the last), then
350
+ * `complete` with the returned parts, or `abort`. Persist `uploadId` to
351
+ * continue in a later request via {@link Storage.resumeMultipartUpload}.
352
+ */
353
+ createMultipartUpload: (key: string, options?: StorageMultipartUploadOptions) => Promise<StorageMultipartUpload>;
354
+ delete: (key: string) => Promise<void>;
355
+ /**
356
+ * Mint a short-lived signed `PUT` URL a client can upload directly to,
357
+ * optionally pinning the `Content-Type` the uploader must send. Mirrors
358
+ * Convex's `storage.generateUploadUrl`.
359
+ */
360
+ generateUploadUrl: (key: string, options?: {
361
+ contentType?: string;
362
+ expiresInSeconds?: number;
363
+ }) => Promise<string>;
364
+ /**
365
+ * Mint a native S3 SigV4 URL that hits R2 **directly**, so the bytes never
366
+ * pass through the Worker — unlike {@link Storage.generateUploadUrl}, whose
367
+ * signed URL points back at this app so its storage rules still apply.
368
+ *
369
+ * Requires `s3` credentials on the `.storage({ s3 })` declaration; without
370
+ * them the call throws. That is the trade-off: no Worker in the path also
371
+ * means no rule enforcement in the path.
372
+ */
373
+ getPresignedUrl: (key: string, options?: {
374
+ expiresInSeconds?: number;
375
+ method?: "GET" | "PUT";
376
+ }) => Promise<string>;
377
+ /**
378
+ * List objects under `prefix` — e.g. to sweep abandoned staging uploads. With
379
+ * `options.delimiter` set, keys sharing a segment roll up into
380
+ * `delimitedPrefixes` and are NOT in `objects`, so an empty `objects` is not
381
+ * an empty directory. A page may hold fewer objects than `options.limit`:
382
+ * paginate on `truncated` / `cursor`.
383
+ *
384
+ * Not gated by storage access rules (and dropped under `storageRules(...)`);
385
+ * a rule-scoped enumeration goes through `ctx.db.system.query("_storage")`.
386
+ */
387
+ list: (prefix?: string, options?: StorageListOptions) => Promise<StorageListResult>;
388
+ /**
389
+ * Resume an in-progress multipart upload by its `uploadId` (e.g. across
390
+ * requests). Synchronous: R2 does not validate the id, so a stale one
391
+ * surfaces as an error on the first `uploadPart` / `complete`.
392
+ */
393
+ resumeMultipartUpload: (key: string, uploadId: string) => StorageMultipartUpload;
394
+ /**
395
+ * Upload `body` to `key` from the server, returning the stored object's key
396
+ * and etag. Mirrors Convex's `storage.store`; the same function as
397
+ * {@link Storage.upload}, so the `maxSize` / `allowedContentTypes` guards
398
+ * apply through the alias.
399
+ */
400
+ store: (key: string, body: StorageUploadBody, options?: StorageUploadOptions) => Promise<StorageUploadResult>;
401
+ /** Upload `body` to `key` — {@link Storage.store} under `@lunora/storage`'s own name, with one shared signature. */
402
+ upload: Storage<Buckets>["store"];
403
+ }
2
404
  /** Map of validators describing a function's args record. Alias of `@lunora/values`' shared {@link ValidatorMap}. */
3
405
  type ArgsValidator = ValidatorMap;
4
406
  /** Infer the args object type from an {@link ArgsValidator}. Alias of `@lunora/values`' shared {@link InferValidatorMap}. */
@@ -6,12 +408,12 @@ type InferArgs<A extends ArgsValidator> = InferValidatorMap<A>;
6
408
  /** Storage backend for a `.global()` table: D1 (default) or a Postgres/MySQL database via Cloudflare Hyperdrive (PlanetScale, Neon, …). */
7
409
  type GlobalBackend = "d1" | "hyperdrive";
8
410
  /**
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
- */
411
+ * Cloudflare Durable Object data-residency jurisdiction declared via
412
+ * `defineSchema(...).jurisdiction("…")`. Restricts where every DO the app
413
+ * reaches runs and persists data (GDPR, FedRAMP, US data residency). Widening
414
+ * union — Cloudflare adds values over time.
415
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
416
+ */
15
417
  type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
16
418
  /** How a table is routed at runtime. */
17
419
  type ShardMode = {
@@ -28,44 +430,83 @@ type ExternalSourceRefresh = "manual" | {
28
430
  everyMs: number;
29
431
  };
30
432
  /**
31
- * Delete-detection mode for external-source ingest. `"full-pull"` (default) reads
32
- * the whole tenant membership each tick and diffs it, so it observes upstream
33
- * deletes; `"incremental"` pulls only changed rows (cheap) and is blind to deletes
34
- * unless paired with a soft-delete column or a `reconcileEveryMs` full-pull sweep.
35
- */
433
+ * Delete-detection mode for external-source ingest (plan 077 / 136).
434
+ *
435
+ * `"full-pull"` (the default) reads the **whole** tenant membership each tick and
436
+ * diffs it, so it observes upstream deletes for free — but costs a full read per tick
437
+ * (the Phase-0 bench put the ceiling at ~10k rows).
438
+ *
439
+ * `"incremental"` pulls **only rows past a durable watermark** (`cursor`), cheap for
440
+ * large low-churn tables above the full-pull cap. Because an absent row then means
441
+ * "unchanged", not "deleted", incremental requires a delete-visibility path: either a
442
+ * `reconcileEveryMs` periodic full-pull sweep, or a `softDeleteColumn` whose
443
+ * tombstones the pull returns. `defineSchema` throws (and the
444
+ * `external_source_incremental_no_delete_path` advisor lint fails the build) when an
445
+ * incremental source declares neither.
446
+ */
36
447
  type ExternalSourceMode = "full-pull" | "incremental";
37
448
  /**
38
- * Config for `.source(...)` (plan 077): declares a table as **materialized from an
39
- * external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
40
- * mutations. A system-driven poll loop reads the tenant slice and lands it in the
41
- * DO's SQLite (via the validated CDC writer), after which `defineShape` carries it
42
- * to clients unchanged. Orthogonal to `shardMode` — a sourced table almost always
43
- * also `.shardBy()`s, in which case `tenantBy` is the mandatory tenant-isolation
44
- * boundary (enforced by the `external_source_unscoped` advisor lint).
45
- */
449
+ * Incremental-ingest cursor (plan 136): the monotonic watermark column plus the
450
+ * watermark-parameterized pull query. `column` names the field in the pulled rows
451
+ * whose max becomes the next watermark (e.g. `"updated_at"`). `query` is a second
452
+ * SQL that returns only rows changed since the watermark — the watermark binds as
453
+ * the parameter AFTER `tenantBy`'s params (e.g. Postgres
454
+ * `... WHERE tenant_id = $1 AND updated_at >= $2 ORDER BY updated_at`). Prefer `>=`
455
+ * with the idempotent upsert apply so rows sharing the boundary timestamp are never
456
+ * skipped (re-pulling them is a no-op).
457
+ */
458
+ interface ExternalSourceCursor {
459
+ /** The monotonic watermark column in the pulled rows; its max advances the stored watermark. */
460
+ column: string;
461
+ /** The incremental pull SQL. `tenantBy`'s params bind first, then the watermark as the trailing param. */
462
+ query: string;
463
+ }
464
+ /**
465
+ * Config for `.source(...)` (plan 077): declares a table as **materialized from an
466
+ * external Postgres/MySQL behind Cloudflare Hyperdrive**, not written by user
467
+ * mutations. A system-driven poll loop reads the tenant slice and lands it in the
468
+ * DO's SQLite (via the validated CDC writer), after which `defineShape` carries it
469
+ * to clients unchanged. Orthogonal to `shardMode` — a sourced table almost always
470
+ * also `.shardBy()`s, in which case `tenantBy` is the mandatory tenant-isolation
471
+ * boundary (enforced by the `external_source_unscoped` advisor lint).
472
+ */
46
473
  interface ExternalSourceDefinition {
47
474
  /** The wrangler Hyperdrive binding name the poll loop reads from. */
48
475
  binding: string;
49
476
  /** Project the materialized rows to these columns (passed to the membership diff). Omit ⇒ the full mapped document. */
50
477
  columns?: ReadonlyArray<string>;
478
+ /** **Required for `mode: "incremental"`**: the watermark column + watermark-parameterized pull query (plan 136). Rejected on a `"full-pull"` source. */
479
+ cursor?: ExternalSourceCursor;
51
480
  /** Column whose value becomes the Lunora `_id`. Defaults to `"id"`. */
52
481
  idColumn?: string;
53
482
  /** Transform an external row into the stored document body. Omit ⇒ every selected column except `idColumn` is copied. */
54
483
  map?: (row: Record<string, unknown>) => Record<string, unknown>;
55
- /** Delete-detection mode. Defaults to `"full-pull"`. */
484
+ /** Delete-detection mode. `"full-pull"` (the default) diffs the whole membership; `"incremental"` pulls past a `cursor` watermark. */
56
485
  mode?: ExternalSourceMode;
57
486
  /** The full tenant-membership query, with driver-native placeholders (`$1` / `?`). `tenantBy` binds its params. */
58
487
  query: string;
59
- /** `"incremental"` only: run a full-pull reconcile this often to garbage-collect tombstones the incremental path can't see. */
488
+ /**
489
+ * **Incremental delete-visibility (plan 136)**: run a full-pull sweep at most
490
+ * this often (millis) to GC upstream deletes an incremental slice can't see.
491
+ * One of `reconcileEveryMs` / `softDeleteColumn` is required for incremental;
492
+ * rejected on a `"full-pull"` source.
493
+ */
60
494
  reconcileEveryMs?: number;
61
495
  /** Poll cadence, or `"manual"`. Omit ⇒ the runtime's size-scaled default. */
62
496
  refresh?: ExternalSourceRefresh;
63
497
  /**
64
- * **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
65
- * params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
66
- * sharded table replicates the whole multitenant table into every shard — the
67
- * `external_source_unscoped` advisor lint fails the build when this is absent.
68
- */
498
+ * **Incremental delete-visibility (plan 136)**: the upstream soft-delete
499
+ * tombstone column (e.g. `"deleted_at"`). When set, the incremental pull must
500
+ * return tombstoned rows and the ingest turns each into a local delete — an
501
+ * alternative to `reconcileEveryMs`. Rejected on a `"full-pull"` source.
502
+ */
503
+ softDeleteColumn?: string;
504
+ /**
505
+ * **Mandatory under `.shardBy()`**: map this DO's shard key → the query's bound
506
+ * params, so a tenant DO can only ever pull its own rows. An unscoped sourced +
507
+ * sharded table replicates the whole multitenant table into every shard — the
508
+ * `external_source_unscoped` advisor lint fails the build when this is absent.
509
+ */
69
510
  tenantBy?: (shardKey: string) => ReadonlyArray<unknown>;
70
511
  }
71
512
  interface IndexDefinition {
@@ -74,25 +515,97 @@ interface IndexDefinition {
74
515
  unique?: boolean;
75
516
  }
76
517
  interface SearchIndexDefinition {
518
+ /** Indexed text column; a dot-separated path (`"properties.name"`) reads a nested field. */
77
519
  field: string;
520
+ /** Columns `.eq()` may narrow by inside the search builder. At most 16. */
78
521
  filterFields?: ReadonlyArray<string>;
522
+ /**
523
+ * Text analysis for this index. Accent folding is always applied — it is
524
+ * what makes `café` and `cafe` the same token on every backend, which they
525
+ * otherwise are not. Naming a language additionally drops that language's
526
+ * stopwords from both documents and queries.
527
+ *
528
+ * Analysis is baked into the stored index, so changing this rebuilds it:
529
+ * the runtime records which profile a companion was built with and
530
+ * re-indexes when it no longer matches.
531
+ */
532
+ language?: SearchLanguage;
533
+ name: string;
534
+ /**
535
+ * Skip the migration-time backfill. By default, creating the index's
536
+ * companion also indexes the rows already in the table, so a search index
537
+ * added to a populated table works immediately. On a very large table that
538
+ * scan is expensive to run inside a deploy: `staged: true` maintains the
539
+ * index on write only and leaves the initial population to an out-of-band
540
+ * run.
541
+ *
542
+ * That run is mandatory, not optional — until it happens, every row written
543
+ * BEFORE the deploy is unsearchable. Drive it against a deployment with
544
+ * `lunora run '__lunora_admin__:backfillSearch' --args '{"maxPages":20}'`,
545
+ * repeating until the response reports `done: true`; progress is durable, so
546
+ * each call resumes where the last stopped.
547
+ */
548
+ staged?: boolean;
549
+ /**
550
+ * How the index is stored and matched.
551
+ *
552
+ * `"portable"` (default) keeps one implementation on every backend, with
553
+ * identical matching *and* ranking — the invariant the rest of the search
554
+ * docs rest on.
555
+ *
556
+ * `"native"` hands matching to the engine's own full-text index where it has
557
+ * one (Postgres `tsvector` + GIN today; ignored elsewhere). It scales far
558
+ * better on large corpora and common terms, because the portable path
559
+ * aggregates every matching token row before ranking. The trade: the engine
560
+ * ranks, so results are ordered by its formula rather than the shared
561
+ * scorer. Matching still agrees — the vector is built from the same analyzed
562
+ * tokens — but a `.global()` table using it will not return rows in the same
563
+ * order as the sharded twin.
564
+ */
565
+ strategy?: SearchStrategy;
566
+ }
567
+ /**
568
+ * A geospatial index declared via `.geoIndex(name, { field })`. The runtime
569
+ * maintains a geohash companion table over the `v.geoPoint()` column `field` so
570
+ * `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves a
571
+ * proximity / bounding-box read as a geohash-prefix range scan plus a Haversine
572
+ * refine/sort on the candidate rows.
573
+ *
574
+ * - `field` — the `v.geoPoint()` column whose lat/lng feed the geohash.
575
+ * - `precision` — geohash character length on the companion (default 9, ~4.8 m cells); higher precision narrows each cell.
576
+ */
577
+ interface GeoIndexDefinition {
578
+ field: string;
79
579
  name: string;
580
+ precision?: number;
581
+ }
582
+ /**
583
+ * Declarative table-level TTL declared via `.ttl(field, { after? })`. A DO
584
+ * alarm-driven sweep deletes (or, when the table also `.softDelete()`s,
585
+ * soft-deletes) rows whose expiry timestamp has passed.
586
+ *
587
+ * - `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`).
588
+ * - `after` — optional millisecond offset added to `field` to derive the expiry.
589
+ */
590
+ interface TtlDefinition {
591
+ after?: number;
592
+ field: string;
80
593
  }
81
594
  /** Reducer applied by an aggregate index. */
82
595
  type AggregateOp = "avg" | "count" | "max" | "min" | "sum";
83
596
  /**
84
- * Declared aggregate index — the schema-level seam that lets the runtime keep
85
- * O(1) counters/sums in step with row writes (via the trigger runner) and
86
- * route matching reads through them.
87
- *
88
- * - `on` — the table whose rows feed the aggregate.
89
- * - `op` — the reducer. `count` is field-less; the others take `field`.
90
- * - `field` — the column the reducer applies to (required for non-count ops).
91
- * - `by` — group keys. When all `where` keys in a read participate in `by`, the
92
- * reader can answer from the counter table without scanning rows.
93
- * - `where` — optional static predicate baked into the counter (only the rows
94
- * matching it ever land in the counter).
95
- */
597
+ * Declared aggregate index — the schema-level seam that lets the runtime keep
598
+ * O(1) counters/sums in step with row writes (via the trigger runner) and
599
+ * route matching reads through them.
600
+ *
601
+ * - `on` — the table whose rows feed the aggregate.
602
+ * - `op` — the reducer. `count` is field-less; the others take `field`.
603
+ * - `field` — the column the reducer applies to (required for non-count ops).
604
+ * - `by` — group keys. When all `where` keys in a read participate in `by`, the
605
+ * reader can answer from the counter table without scanning rows.
606
+ * - `where` — optional static predicate baked into the counter (only the rows
607
+ * matching it ever land in the counter).
608
+ */
96
609
  interface AggregateIndexDefinition {
97
610
  by?: ReadonlyArray<string>;
98
611
  field?: string;
@@ -102,32 +615,32 @@ interface AggregateIndexDefinition {
102
615
  where?: Record<string, unknown>;
103
616
  }
104
617
  /**
105
- * One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
106
- * direction. The runtime breaks ties on the row's `_id` ASC so the order is
107
- * total and `rank()` always returns a deterministic 1-based position.
108
- */
618
+ * One ordering key on a `rankIndex.sortBy`: which column to sort by, and the
619
+ * direction. The runtime breaks ties on the row's `_id` ASC so the order is
620
+ * total and `rank()` always returns a deterministic 1-based position.
621
+ */
109
622
  interface RankSortKey {
110
623
  direction: "asc" | "desc";
111
624
  field: string;
112
625
  }
113
626
  /**
114
- * Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
115
- * maintained by triggers, so:
116
- *
117
- * - `rank(row)` returns the row's 1-based position within its partition under
118
- * the declared `sortBy` order, plus the partition's total row count, in
119
- * O(log n) lookups against the SQLite btree on the companion table.
120
- * - `rankPage({ where, take, from })` walks the same companion table to return
121
- * rows in the declared order — a sorted-pagination accelerator.
122
- *
123
- * Fields mirror `AggregateIndexDefinition`:
124
- *
125
- * - `on` — the source table whose rows feed the rank.
126
- * - `sortBy` — ordered keys driving the rank. Required.
127
- * - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
128
- * to rank within a channel). Omitted ⇒ one global rank across the table.
129
- * - `where` — static predicate baked into the index; only matching rows enter.
130
- */
627
+ * Declared rank index — a sorted companion table per `(partition tuple, sortBy)`
628
+ * maintained by triggers, so:
629
+ *
630
+ * - `rank(row)` returns the row's 1-based position within its partition under
631
+ * the declared `sortBy` order, plus the partition's total row count, in
632
+ * O(log n) lookups against the SQLite btree on the companion table.
633
+ * - `rankPage({ where, take, from })` walks the same companion table to return
634
+ * rows in the declared order — a sorted-pagination accelerator.
635
+ *
636
+ * Fields mirror `AggregateIndexDefinition`:
637
+ *
638
+ * - `on` — the source table whose rows feed the rank.
639
+ * - `sortBy` — ordered keys driving the rank. Required.
640
+ * - `partitionBy` — columns that scope each rank context (e.g. `["channelId"]`
641
+ * to rank within a channel). Omitted ⇒ one global rank across the table.
642
+ * - `where` — static predicate baked into the index; only matching rows enter.
643
+ */
131
644
  interface RankIndexDefinition {
132
645
  name: string;
133
646
  on: string;
@@ -138,16 +651,16 @@ interface RankIndexDefinition {
138
651
  /** FK behavior when a referenced parent row is deleted (mirrors SQL `ON DELETE`). */
139
652
  type OnDeleteAction = "cascade" | "restrict" | "set null";
140
653
  /**
141
- * A declared relation between two tables, recorded by `.relations((r) => …)`.
142
- *
143
- * - `one` (many-to-one): the FK column `field` lives on **this** table and
144
- * points at `table`.`references` (default `_id`). Loads a single doc.
145
- * - `many` (one-to-many): the FK column `field` lives on the **target** table
146
- * and points back at this table's `references` (default `_id`). Loads an array.
147
- *
148
- * `onDelete` is meaningful only on `one`: it is the action applied to the
149
- * holder rows when the referenced parent row is deleted.
150
- */
654
+ * A declared relation between two tables, recorded by `.relations((r) => …)`.
655
+ *
656
+ * - `one` (many-to-one): the FK column `field` lives on **this** table and
657
+ * points at `table`.`references` (default `_id`). Loads a single doc.
658
+ * - `many` (one-to-many): the FK column `field` lives on the **target** table
659
+ * and points back at this table's `references` (default `_id`). Loads an array.
660
+ *
661
+ * `onDelete` is meaningful only on `one`: it is the action applied to the
662
+ * holder rows when the referenced parent row is deleted.
663
+ */
151
664
  interface RelationDefinition {
152
665
  field: string;
153
666
  kind: "many" | "one";
@@ -158,156 +671,402 @@ interface RelationDefinition {
158
671
  /** Distance metric used by a Vectorize index. */
159
672
  type VectorMetric = "cosine" | "dot-product" | "euclidean";
160
673
  /**
161
- * Bring-your-own-embedder: a user-supplied fn turning a source string into a
162
- * numeric vector. The runtime calls it at upsert/query time so the framework
163
- * never couples to a single embedding provider.
164
- */
674
+ * Bring-your-own-embedder: a user-supplied fn turning a source string into a
675
+ * numeric vector. The runtime calls it at upsert/query time so the framework
676
+ * never couples to a single embedding provider.
677
+ */
165
678
  type VectorEmbedder = (input: string) => Promise<ReadonlyArray<number>> | ReadonlyArray<number>;
166
679
  /**
167
- * Vector index declared inline on a table via `.vectorize(field, opts)`
168
- * (DSL Shape A). The source is always a single column on the owning table.
169
- */
680
+ * Vector index declared inline on a table via `.vectorize(field, opts)`
681
+ * (DSL Shape A). The source is always a single column on the owning table.
682
+ */
170
683
  interface TableVectorIndex {
171
684
  dimensions: number;
172
685
  embed: VectorEmbedder;
173
686
  field: string;
174
687
  metadata?: ReadonlyArray<string>;
175
688
  metric: VectorMetric;
689
+ /** Declared embedding model; see `VectorizeOptions.model`. */
690
+ model?: string;
176
691
  name: string;
177
692
  }
178
693
  interface TableDefinition<Shape extends Record<string, Validator> = Record<string, Validator>> {
179
694
  /**
180
- * Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
181
- * maintains a counter row per `by` group via the trigger seam, so reads
182
- * whose `where` keys all participate in the index's `by` set are answered
183
- * without scanning the underlying table.
184
- */
695
+ * Aggregate indexes declared via `.aggregateIndex(name, opts)`. The runtime
696
+ * maintains a counter row per `by` group via the trigger seam, so reads
697
+ * whose `where` keys all participate in the index's `by` set are answered
698
+ * without scanning the underlying table.
699
+ */
185
700
  aggregateIndexes: ReadonlyArray<AggregateIndexDefinition>;
186
701
  /**
187
- * Set by `.source(...)` (named `externalSource`, not `source`, so the data
188
- * field doesn't collide with the fluent `.source()` builder method — same
189
- * convention as `shardBy()`/`shardMode`). When present, the table is
190
- * materialized from an external Hyperdrive-backed database by a system poll
191
- * loop rather than user mutations. Implies `isExternallyManaged`.
192
- */
702
+ * Set by `.commitOrdered()` (named `commitOrderedMode`, not `commitOrdered`,
703
+ * so the data field doesn't collide with the fluent `.commitOrdered()`
704
+ * builder method — same convention as `shardBy()`/`shardMode`). When `true`,
705
+ * every write to a row stamps `_commitSeq`: a per-shard integer allocated
706
+ * once per mutation and strictly increasing in commit order. Absent/`false`
707
+ * ⇒ rows carry no `_commitSeq`, as before.
708
+ */
709
+ commitOrderedMode?: boolean;
710
+ /**
711
+ * `.dropStalePatches()` — drop a `patch` whose fields moved since the caller's
712
+ * CDC baseline rather than clobbering the newer value. See the builder method
713
+ * for the rule, the whole-patch granularity, and why it fails open.
714
+ */
715
+ dropStalePatchesMode?: boolean;
716
+ /**
717
+ * Set by `.source(...)` (named `externalSource`, not `source`, so the data
718
+ * field doesn't collide with the fluent `.source()` builder method — same
719
+ * convention as `shardBy()`/`shardMode`). When present, the table is
720
+ * materialized from an external Hyperdrive-backed database by a system poll
721
+ * loop rather than user mutations. Implies `isExternallyManaged`.
722
+ */
193
723
  externalSource?: ExternalSourceDefinition;
724
+ /**
725
+ * Geospatial indexes declared via `.geoIndex(name, { field })`. The runtime
726
+ * maintains a geohash companion over the named `v.geoPoint()` column so
727
+ * `withGeoIndex(name, q => q.near(point, radius) | q.within(bbox))` resolves
728
+ * a proximity/bounding-box read as a geohash-prefix range scan plus a
729
+ * Haversine refine/sort. Empty unless `.geoIndex()` was called.
730
+ */
731
+ geoIndexes: ReadonlyArray<GeoIndexDefinition>;
194
732
  indexes: ReadonlyArray<IndexDefinition>;
195
733
  /**
196
- * `true` when `.externallyManaged()` was called — the table's rows are
197
- * written outside Lunora's discoverable insert path (an adapter, a
198
- * migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
199
- * tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
200
- * (`table_without_insert`) skip such tables instead of flagging the absent
201
- * `ctx.db.insert(...)`.
202
- */
734
+ * `true` when `.externallyManaged()` was called — the table's rows are
735
+ * written outside Lunora's discoverable insert path (an adapter, a
736
+ * migration, or framework middleware), e.g. `@lunora/auth`'s better-auth
737
+ * tables or `@lunora/ratelimit`'s store. Advisor insert-path lints
738
+ * (`table_without_insert`) skip such tables instead of flagging the absent
739
+ * `ctx.db.insert(...)`.
740
+ */
203
741
  isExternallyManaged?: boolean;
204
742
  /**
205
- * `true` when `.public()` was called — the table opts OUT of secure-by-default
206
- * RLS. Under a schema marked `.rls("required")`, every table is protected (the
207
- * DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
208
- * Has no effect when the schema does not require RLS.
209
- */
743
+ * `true` when `.public()` was called — the table opts OUT of secure-by-default
744
+ * RLS. Under a schema marked `.rls("required")`, every table is protected (the
745
+ * DO/D1 write path denies raw, non-RLS `ctx.db` access) UNLESS it is `isPublic`.
746
+ * Has no effect when the schema does not require RLS.
747
+ */
210
748
  isPublic?: boolean;
211
749
  /**
212
- * Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
213
- * a sorted companion table per declared rank with a btree on
214
- * `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
215
- * within its partition in O(log n), and `rankPage()` walks the index for
216
- * sorted pagination.
217
- */
750
+ * Set by `.memory()` (named `memoryMode`, not `memory`, so the data field
751
+ * doesn't collide with the fluent `.memory()` builder method — same
752
+ * convention as `shardBy()`/`shardMode`). When `true`, the table's rows are
753
+ * cleared every time the Durable Object is reconstructed, and its writes
754
+ * never reach the CDC changelog. Absent/`false` ⇒ an ordinary durable table.
755
+ */
756
+ memoryMode?: boolean;
757
+ /**
758
+ * Set by `.ownedBy(field)` — the column holding the owning user's id (named
759
+ * `ownerField`, a data field, rather than colliding with the fluent
760
+ * `.ownedBy()` builder method — same convention as `shardBy()`/`shardMode`).
761
+ *
762
+ * A shape over this table with `owner: true` derives its predicate from this
763
+ * field, so "only the owner may replicate these rows" is declared once on the
764
+ * table instead of being restated in every shape's `where`. Absent ⇒ the table
765
+ * has no single owning column and a shape must spell its predicate out.
766
+ */
767
+ ownerField?: string;
768
+ /**
769
+ * Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
770
+ * a sorted companion table per declared rank with a btree on
771
+ * `(partition, sortBy)` so `rank(row)` returns the row's 1-based position
772
+ * within its partition in O(log n), and `rankPage()` walks the index for
773
+ * sorted pagination.
774
+ */
218
775
  rankIndexes: ReadonlyArray<RankIndexDefinition>;
219
776
  /**
220
- * Declared relations keyed by accessor name; empty unless `.relations()`
221
- * was called. Named `relationMap` (not `relations`) so the fluent
222
- * `.relations((r) => …)` builder method doesn't collide with this field.
223
- */
777
+ * Declared relations keyed by accessor name; empty unless `.relations()`
778
+ * was called. Named `relationMap` (not `relations`) so the fluent
779
+ * `.relations((r) => …)` builder method doesn't collide with this field.
780
+ */
224
781
  relationMap: Record<string, RelationDefinition>;
225
782
  searchIndexes: ReadonlyArray<SearchIndexDefinition>;
226
783
  shape: Shape;
227
784
  shardMode: ShardMode;
228
785
  /**
229
- * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
230
- * data field doesn't collide with the fluent `.softDelete()` builder method —
231
- * same convention as `shardBy()`/`shardMode`). When present, the table carries
232
- * a nullable timestamp column (`field`, default `deletedAt`):
233
- * `ctx.db.&lt;table>.delete()` flips it instead of physically removing the row,
234
- * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
235
- * relation loads) hide rows whose `field` is set unless
236
- * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
237
- * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
238
- */
786
+ * Set by `.softDelete()` (named `softDeleteMode`, not `softDelete`, so the
787
+ * data field doesn't collide with the fluent `.softDelete()` builder method —
788
+ * same convention as `shardBy()`/`shardMode`). When present, the table carries
789
+ * a nullable timestamp column (`field`, default `deletedAt`):
790
+ * `ctx.db.<table>.delete()` flips it instead of physically removing the row,
791
+ * and **list reads** (`findMany`/`findFirst`/`query()`/`count`/`aggregate`/
792
+ * relation loads) hide rows whose `field` is set unless
793
+ * `includeDeleted: true` is passed. By-id `get`/`patch`/`replace` and
794
+ * `restore` are unaffected. Absent ⇒ deletes are physical, as before.
795
+ */
239
796
  softDeleteMode?: {
240
797
  field: string;
241
798
  };
242
799
  /**
243
- * Declared lifecycle triggers keyed by accessor name; empty unless
244
- * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
245
- * fluent `.triggers((t) => …)` builder method doesn't collide with this
246
- * field — same reasoning as {@link TableDefinition.relationMap}.
247
- */
800
+ * Declared lifecycle triggers keyed by accessor name; empty unless
801
+ * `.triggers()` was called. Named `triggerMap` (not `triggers`) so the
802
+ * fluent `.triggers((t) => …)` builder method doesn't collide with this
803
+ * field — same reasoning as {@link TableDefinition.relationMap}.
804
+ */
248
805
  triggerMap: Record<string, TriggerDefinition>;
806
+ /**
807
+ * Set by `.ttl(field, { after })` — the declarative auto-expiry policy. A DO
808
+ * alarm-driven sweep deletes rows whose expiry timestamp has passed (or
809
+ * soft-deletes them when the table also `.softDelete()`s). Named `ttlPolicy`
810
+ * (a data field) rather than colliding with the fluent `.ttl()` builder
811
+ * method — same convention as `shardBy()`/`shardMode`. Absent ⇒ rows never
812
+ * auto-expire.
813
+ */
814
+ ttlPolicy?: TtlDefinition;
249
815
  vectorIndexes: ReadonlyArray<TableVectorIndex>;
250
816
  }
251
817
  /**
252
- * Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
253
- * Unlike {@link TableVectorIndex}, the source is a `select` function so it can
254
- * derive the embedded text from any computation (e.g. `title + body`).
255
- */
818
+ * Standalone vector index declared via `defineVectorIndex(...)` (DSL Shape B).
819
+ * Unlike {@link TableVectorIndex}, the source is a `select` function so it can
820
+ * derive the embedded text from any computation (e.g. `title + body`).
821
+ */
256
822
  interface VectorIndexDefinition {
257
823
  readonly dimensions: number;
258
824
  readonly embed: VectorEmbedder;
259
825
  readonly kind: "vectorIndex";
260
826
  readonly metadata?: (row: Record<string, unknown>) => Record<string, unknown>;
261
827
  readonly metric: VectorMetric;
828
+ /** Declared embedding model; see `VectorIndexOptions.model`. */
829
+ readonly model?: string;
262
830
  readonly select: (row: Record<string, unknown>) => string;
263
831
  readonly table: string;
264
832
  }
265
833
  interface Schema<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
266
834
  /**
267
- * Secure-by-default RLS mode declared via `.rls("required")`. When
268
- * `"required"`, every table is protected: the DO/D1 write path denies raw
269
- * (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
270
- * `.use(rls(...))` fails closed instead of silently exposing the table. A
271
- * table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
272
- * Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
273
- */
835
+ * Secure-by-default RLS mode declared via `.rls("required")`. When
836
+ * `"required"`, every table is protected: the DO/D1 write path denies raw
837
+ * (non-RLS-wrapped) `ctx.db` access at runtime, so a procedure that forgets
838
+ * `.use(rls(...))` fails closed instead of silently exposing the table. A
839
+ * table opts out with `.public()` (→ {@link TableDefinition.isPublic}).
840
+ * Absent ⇒ legacy opt-in behavior (RLS only where a policy is applied).
841
+ */
274
842
  readonly rlsMode?: "required";
275
843
  readonly tables: T;
276
844
  readonly vectorIndexes: Record<string, VectorIndexDefinition>;
277
845
  }
278
846
  type FunctionKind = "action" | "mutation" | "query" | "stream";
279
847
  /**
280
- * Call surface a function is exposed on. `public` functions are reachable from
281
- * clients via the generated `api`; `internal` functions are reachable only
282
- * server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
283
- * by the DO's external RPC path. Absence is treated as `public` for
284
- * back-compat with functions registered before visibility existed.
285
- */
848
+ * Call surface a function is exposed on. `public` functions are reachable from
849
+ * clients via the generated `api`; `internal` functions are reachable only
850
+ * server-to-server (`ctx.runQuery`/`runMutation`/`runAction`) and are rejected
851
+ * by the DO's external RPC path. Absence is treated as `public` for
852
+ * back-compat with functions registered before visibility existed.
853
+ */
286
854
  type FunctionVisibility = "internal" | "public";
855
+ /**
856
+ * x402 payment tag attached by the `.x402({ price })` builder modifier. Marks a
857
+ * public procedure as paid: the origin worker answers an unpaid client RPC with
858
+ * HTTP 402, verifies + settles the payment, and only then dispatches to the
859
+ * shard. The runtime reads only `price` from here — the network, recipient, and
860
+ * facilitator live in the worker-level x402 charge config, so `@lunora/runtime`
861
+ * never has to import `@lunora/x402` (and its viem/solana deps).
862
+ */
863
+ interface X402ProcedureConfig {
864
+ /**
865
+ * USD-denominated price: a number of dollars (`0.01`) or a decimal string
866
+ * (`"0.01"`, or the `"$0.01"` shorthand). Resolved to the network
867
+ * stablecoin's base units (USDC has 6 decimals) at challenge time.
868
+ */
869
+ readonly price: number | string;
870
+ }
871
+ /**
872
+ * HTTP caching for an exposed REST endpoint, declared as
873
+ * `.expose({ rest: true, cache: { scope: "public", maxAge: 60 } })`. The runtime
874
+ * turns this into `Cache-Control` / `Cache-Tag` / `Vary` response headers, and a
875
+ * `cache.tag` is purgeable through `ctx.cache.purge({ tags: [...] })`.
876
+ *
877
+ * This is NOT equivalent to `httpRoute(...).cacheControl()`, which writes whatever
878
+ * value the author passes with no credential downgrade. That is the unguarded
879
+ * escape hatch; this is the guarded surface.
880
+ *
881
+ * Caching a procedure whose result depends on the caller is how a REST cache
882
+ * turns into a data leak, so `scope` is enforced at runtime rather than trusted:
883
+ * a request that carries credentials (an `Authorization` header or any `Cookie`)
884
+ * is ALWAYS answered `private`, even under `scope: "public"` — see
885
+ * {@link https://developer.mozilla.org/docs/Web/HTTP/Headers/Cache-Control MDN}
886
+ * for what `private` forbids. So a per-user response can never be stored in a
887
+ * shared/edge cache, and the worst a mis-declared `scope` can cost is a missed
888
+ * cache hit. `scope: "public"` additionally emits `Vary: authorization, cookie`
889
+ * so an intermediary can't hand a stored anonymous variant to a signed-in caller.
890
+ *
891
+ * Only ever applied to a cacheable exchange: a `GET` (so `query` procedures — a
892
+ * `mutation` / `action` is `POST`-only) that produced a 2xx.
893
+ */
894
+ type RestCacheConfig = RestCachePolicy;
895
+ /**
896
+ * Opt-in public-surface tag attached by the `.expose({ rest: true })` builder
897
+ * modifier (plan 167). Marks a procedure as deliberately published over the
898
+ * public REST surface: the runtime mints a `/_lunora/rest/<namespace>/<fn>` route
899
+ * that dispatches THROUGH the procedure (so `ctx.auth` / RLS / validators are
900
+ * enforced), and the generated OpenAPI describes it. Everything is default-closed
901
+ * — a procedure without this tag is unreachable over REST.
902
+ */
903
+ interface ExposeConfig {
904
+ /** Opt this endpoint's responses into HTTP caching. Omit to leave them uncached. */
905
+ readonly cache?: RestCacheConfig;
906
+ /** Publish this procedure over the public REST surface. */
907
+ readonly rest?: boolean;
908
+ }
287
909
  interface RegisteredFunction<A extends ArgsValidator, R, Kind extends FunctionKind> {
288
910
  readonly args: A;
911
+ /**
912
+ * Set by the `.expose({ rest: true })` builder modifier. Marks the procedure
913
+ * as published on the public REST surface (plan 167). Absent on procedures that
914
+ * are reachable only via typed RPC (the default).
915
+ */
916
+ readonly expose?: ExposeConfig;
289
917
  readonly handler: (context: unknown, args: InferArgs<A>) => Promise<R> | R;
290
918
  readonly kind: Kind;
291
919
  /**
292
- * Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`).
293
- * Marks the function for the generated `LUNORA_LIFECYCLE_HOOKS` manifest so the
294
- * DO dispatches it on socket connect/disconnect rather than via a client RPC.
295
- * Absent on ordinary registrations.
296
- */
920
+ * Set on connection-lifecycle hooks (`onConnect` / `onDisconnect`).
921
+ * Marks the function for the generated `LUNORA_LIFECYCLE_HOOKS` manifest so the
922
+ * DO dispatches it on socket connect/disconnect rather than via a client RPC.
923
+ * Absent on ordinary registrations.
924
+ */
297
925
  readonly lifecycle?: LifecycleEventKind;
298
926
  readonly visibility?: FunctionVisibility;
927
+ /**
928
+ * Set by the `.x402({ price })` builder modifier. Marks the procedure as paid
929
+ * so the origin worker gates it behind an x402 402-challenge before dispatch.
930
+ * Absent on unpaid functions.
931
+ */
932
+ readonly x402?: X402ProcedureConfig;
299
933
  }
300
934
  type RegisteredQuery<A extends ArgsValidator, R> = RegisteredFunction<A, R, "query">;
301
935
  type RegisteredMutation<A extends ArgsValidator, R> = RegisteredFunction<A, R, "mutation">;
302
936
  type RegisteredAction<A extends ArgsValidator, R> = RegisteredFunction<A, R, "action">;
937
+ /**
938
+ * Structural mirror of `@lunora/client`'s `FunctionReference` — the handle the
939
+ * generated `api` / `internal` objects hand you, carrying `<file>:<function>`
940
+ * in `__lunoraRef`. Redeclared here so `@lunora/server` needs no dependency on
941
+ * the client package, exactly as {@link Scheduler} avoids one on
942
+ * `@lunora/scheduler`. `RegisteredFunction` has no `__lunoraRef`, so the two
943
+ * shapes never overlap.
944
+ */
945
+ interface FunctionHandle<Kind extends "action" | "mutation" | "query" | "stream", Args, Return> {
946
+ /** Phantom marker carrying the type parameters; never present at runtime. */
947
+ readonly __lunoraPhantom?: {
948
+ args: Args;
949
+ kind: Kind;
950
+ returns: Return;
951
+ };
952
+ readonly __lunoraRef: string;
953
+ }
954
+ /**
955
+ * `ctx.runQuery` — overloaded, see the note above.
956
+ *
957
+ * A single generic signature over the reference would be nicer (TS will not
958
+ * contextually type a parameter against a multi-signature type, so a hand-built
959
+ * ctx object must annotate its `(reference, args)` explicitly — see
960
+ * `@lunora/testing`'s harness). It does not work: a concrete
961
+ * `RegisteredQuery<{…}, number>` is not assignable to a
962
+ * `RegisteredFunction<ArgsValidator, …>` constraint, because `handler`'s args
963
+ * are in a contravariant position. Two inference sites it is.
964
+ */
965
+ /**
966
+ * Options for `ctx.runQuery`.
967
+ *
968
+ * Lunora's answer to Convex's `useStaleSnapshot`, and deliberately NOT a port of
969
+ * it. Convex needs that flag because a mutation there validates its whole read
970
+ * set at commit, so merely *reading* a hot append-only table manufactures OCC
971
+ * conflicts. Lunora has no such conflict class: its OCC is a compare-and-swap on
972
+ * the `__doc__` of each row a mutation actually WRITES (see `runGuardedWrite`),
973
+ * so a read costs a mutation nothing and there is nothing for a stale snapshot
974
+ * to relieve.
975
+ *
976
+ * What a read does cost here is REACTIVITY. Every table a live query touches
977
+ * enters its read footprint, and every write to any of those tables re-runs the
978
+ * subscription. A query that reads one hot table — an append-only audit log, a
979
+ * counter, a feed — therefore re-runs on every append even when the append
980
+ * cannot change its result. That is Lunora's version of the pressure Convex is
981
+ * relieving, and {@link RunQueryOptions.untracked} is the release valve.
982
+ */
983
+ interface RunQueryOptions {
984
+ /**
985
+ * Run the query without recording its reads in the CALLER's read footprint.
986
+ * The subscription therefore does not re-run when the tables that query
987
+ * touched change.
988
+ *
989
+ * The result is exactly as fresh as a tracked call — same SQLite, same
990
+ * instant, no snapshot involved. The only thing given up is the invalidation
991
+ * edge, so use it when the read genuinely should not wake the subscriber
992
+ * (a monotonic counter rendered once, a config row, an audit tail whose
993
+ * growth is irrelevant to the result) — and never for data whose change
994
+ * should reach the client, which is what makes the subscription stale
995
+ * indefinitely rather than merely late.
996
+ *
997
+ * Scoped to the sub-query and nothing else: the untracked call runs on its
998
+ * own context, so a read interleaved on the caller's `ctx.db` while it is in
999
+ * flight still tracks normally. That is why this is an option on
1000
+ * `ctx.runQuery` rather than a `ctx.db.untracked(...)` scope — a
1001
+ * writer-wide flag would silently swallow a concurrent read's dependency
1002
+ * (compare `meterExempt`'s documented interleaving caveat, which is
1003
+ * tolerable for a resource meter and would not be here).
1004
+ *
1005
+ * No effect outside a subscription: a one-shot query records no footprint,
1006
+ * so there is nothing to opt out of.
1007
+ */
1008
+ untracked?: boolean;
1009
+ }
1010
+ interface RunQuery {
1011
+ <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>, options?: RunQueryOptions): Promise<R>;
1012
+ <Args, R>(reference: FunctionHandle<"query", Args, R>, args: Args, options?: RunQueryOptions): Promise<R>;
1013
+ }
1014
+ /** `ctx.runMutation` — overloaded for the same reason as {@link RunQuery}. */
1015
+ interface RunMutation {
1016
+ <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>): Promise<R>;
1017
+ <Args, R>(reference: FunctionHandle<"mutation", Args, R>, args: Args): Promise<R>;
1018
+ }
1019
+ /** `ctx.runAction` — overloaded for the same reason as {@link RunQuery}. */
1020
+ interface RunAction {
1021
+ <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>): Promise<R>;
1022
+ <Args, R>(reference: FunctionHandle<"action", Args, R>, args: Args): Promise<R>;
1023
+ }
303
1024
  /** Which side of the WebSocket lifecycle a hook fires on. */
304
- type LifecycleEventKind = "connect" | "disconnect";
305
1025
  /**
306
- * The event a connection-lifecycle hook receives as its second argument. It is
307
- * the JSON-serializable payload the DO forwards on socket connect/disconnect;
308
- * the verified caller identity is also reflected on `ctx.auth` (the hook runs
309
- * under the connecting user via `resolveIdentity`).
310
- */
1026
+ * Which lifecycle moment a hook fires on.
1027
+ *
1028
+ * `connect`/`disconnect` are per-SOCKET and fire many times over a shard's life.
1029
+ * `init` is per-INSTANCE and fires once per cold start, before any handler runs
1030
+ * — see {@link ShardInitEvent}. `reactor` is per-WRITE-FLUSH and fires only when
1031
+ * a watched read's result changed — see `onQueryChange`. `whisper` is per-TOPIC
1032
+ * and decides whether a socket may join or broadcast to one — see `onWhisper`
1033
+ * and {@link WhisperEvent}; unlike the other four it is a query, and its return
1034
+ * value is the verdict.
1035
+ */
1036
+ type LifecycleEventKind = "connect" | "disconnect" | "init" | "reactor" | "whisper";
1037
+ /**
1038
+ * The event a whisper authorizer (`onWhisper`) receives as its second argument.
1039
+ *
1040
+ * Everything on it is either server-stamped from the socket's attachment
1041
+ * (`connectionId`, `shardKey`, `userId`, `context`) or the client-supplied
1042
+ * `topic` the verdict is about. The verified caller identity is also on
1043
+ * `ctx.auth` — the authorizer runs under the socket's own identity.
1044
+ */
1045
+ interface WhisperEvent {
1046
+ /**
1047
+ * Which side of the channel is being authorized: `"subscribe"` when the
1048
+ * socket asks to JOIN the topic (and so to receive every message on it),
1049
+ * `"send"` when it asks to BROADCAST to it. Checked separately because they
1050
+ * are separate powers — a read-only observer is a coherent thing to allow.
1051
+ */
1052
+ readonly action: "send" | "subscribe";
1053
+ /** Stable per-socket id, the same one `onConnect` saw. */
1054
+ readonly connectionId: string;
1055
+ /** App-supplied connection context from the client `connect` envelope (e.g. `{ roomId }`). */
1056
+ readonly context?: Record<string, unknown>;
1057
+ /** The shard this socket is bound to — the outer boundary the topic lives inside. */
1058
+ readonly shardKey: string;
1059
+ /** The topic name the client asked for. Client-supplied: validate it, never trust its shape. */
1060
+ readonly topic: string;
1061
+ /** Verified user id resolved at upgrade, or `null` for an anonymous socket. */
1062
+ readonly userId: string | null;
1063
+ }
1064
+ /**
1065
+ * The event a connection-lifecycle hook receives as its second argument. It is
1066
+ * the JSON-serializable payload the DO forwards on socket connect/disconnect;
1067
+ * the verified caller identity is also reflected on `ctx.auth` (the hook runs
1068
+ * under the connecting user via `resolveIdentity`).
1069
+ */
311
1070
  interface LifecycleEvent {
312
1071
  /** Stable per-socket id, minted at upgrade and replayed verbatim on disconnect. */
313
1072
  readonly connectionId: string;
@@ -319,33 +1078,85 @@ interface LifecycleEvent {
319
1078
  readonly userId: string | null;
320
1079
  }
321
1080
  /**
322
- * A registered connection-lifecycle hook — an internal mutation tagged with the
323
- * lifecycle side it fires on. Produced by `onConnect` / `onDisconnect`.
324
- */
1081
+ * A registered connection-lifecycle hook — an internal mutation tagged with the
1082
+ * lifecycle side it fires on. Produced by `onConnect` / `onDisconnect` /
1083
+ * `onShardInit`.
1084
+ */
325
1085
  type RegisteredLifecycleHook = RegisteredFunction<Record<string, never>, void, "mutation"> & {
326
1086
  readonly lifecycle: LifecycleEventKind;
327
1087
  };
328
1088
  /**
329
- * A streaming query registration. Unlike {@link RegisteredFunction} the handler
330
- * returns an `AsyncIterable&lt;R>` synchronously (it does NOT `Promise&lt;R>`); the
331
- * runtime drives it frame by frame and forwards each chunk to the caller. The
332
- * third `signal` argument is wired to the caller's cancel signal so the handler
333
- * can stop early — break out of the loop or check `signal.aborted` between
334
- * yields.
335
- */
1089
+ * The event an `onShardInit` hook receives.
1090
+ *
1091
+ * Deliberately thin, and deliberately NOT a {@link LifecycleEvent}: there is no
1092
+ * socket here. An init hook fires because the Durable Object was constructed,
1093
+ * not because anyone connected — so there is no `connectionId` to report and no
1094
+ * user to attribute it to. The hook runs as a trusted system dispatch with no
1095
+ * request identity, exactly like a cron tick; `ctx.auth` is anonymous and RLS
1096
+ * does not apply. Derive whatever you need from `shardKey` and the shard's own
1097
+ * durable tables, never from an ambient caller — there isn't one.
1098
+ */
1099
+ interface ShardInitEvent {
1100
+ /** The shard this Durable Object instance serves. */
1101
+ readonly shardKey: string;
1102
+ }
1103
+ /**
1104
+ * A streaming query registration. Unlike {@link RegisteredFunction} the handler
1105
+ * returns an `AsyncIterable<R>` synchronously (it does NOT `Promise<R>`); the
1106
+ * runtime drives it frame by frame and forwards each chunk to the caller. The
1107
+ * third `signal` argument is wired to the caller's cancel signal so the handler
1108
+ * can stop early — break out of the loop or check `signal.aborted` between
1109
+ * yields.
1110
+ */
336
1111
  interface RegisteredStream<A extends ArgsValidator, R> {
337
1112
  readonly args: A;
1113
+ /**
1114
+ * Present when the stream was declared `durable`. Chunks are persisted per
1115
+ * run before they reach a socket, the producer outlives the socket that
1116
+ * opened it, and a reconnecting (or second) client attaches to the same run
1117
+ * and replays what it missed. See {@link DurableStreamOptions}.
1118
+ */
1119
+ readonly durable?: DurableStreamOptions;
338
1120
  readonly handler: (context: unknown, args: InferArgs<A>, signal: AbortSignal) => AsyncIterable<R>;
339
1121
  readonly kind: "stream";
340
1122
  readonly visibility?: FunctionVisibility;
341
1123
  }
1124
+ /**
1125
+ * Durability settings for a `.stream()` procedure.
1126
+ *
1127
+ * A run is identified by the socket's verified identity plus the function path
1128
+ * and arguments, so two clients of the SAME user calling the same stream with
1129
+ * the same arguments observe one producer and one transcript. That identity is
1130
+ * the feature: it is what makes a reload resume rather than re-generate, and
1131
+ * what lets a second tab watch the same answer. A different identity always gets
1132
+ * its own run.
1133
+ *
1134
+ * Sharing applies to a run still in flight. Once a run finishes, a later caller
1135
+ * asking the same question gets a fresh one — a transcript is the record of one
1136
+ * execution, not a cached response.
1137
+ *
1138
+ * **What an attach does not do:** it replays a transcript, it does not re-run the
1139
+ * handler — so the procedure's middleware chain (`.use(rls(...))`, rate limits,
1140
+ * any custom guard) runs once, for the caller that started the run. That is why
1141
+ * the run key is identity-scoped: a guard that has already passed for one user
1142
+ * must never be skipped for another.
1143
+ */
1144
+ interface DurableStreamOptions {
1145
+ /**
1146
+ * How long a finished transcript is kept before it is trimmed, in
1147
+ * milliseconds. Defaults to 24 hours — long enough to survive a reload and
1148
+ * a commute, short enough that a chatty shard doesn't accumulate
1149
+ * transcripts forever.
1150
+ */
1151
+ readonly ttlMs?: number;
1152
+ }
342
1153
  /** The system tables `ctx.db.system` can read. */
343
1154
  type SystemTableName = "_scheduled_functions" | "_storage";
344
1155
  /**
345
- * A pending scheduled invocation as surfaced by the `_scheduled_functions`
346
- * system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
347
- * separate name keeps the system-table read surface self-describing.
348
- */
1156
+ * A pending scheduled invocation as surfaced by the `_scheduled_functions`
1157
+ * system table. Mirrors {@link ScheduledJob} (the `ctx.scheduler` view); the
1158
+ * separate name keeps the system-table read surface self-describing.
1159
+ */
349
1160
  interface ScheduledFunctionDoc {
350
1161
  /** Function arguments the job will be dispatched with. */
351
1162
  args: Record<string, unknown>;
@@ -353,14 +1164,26 @@ interface ScheduledFunctionDoc {
353
1164
  attempts?: number;
354
1165
  /** When the job was enqueued (epoch ms). */
355
1166
  enqueuedAt: number;
356
- /** Fully-qualified path of the function to invoke. */
357
- functionPath: string;
1167
+ /**
1168
+ * Fully-qualified `ns:fn` path of the function to invoke. Absent when the job
1169
+ * targets a durable workflow/agent instead — exactly one of `functionPath` /
1170
+ * {@link ScheduledFunctionDoc.workflow} is set on any given row.
1171
+ */
1172
+ functionPath?: string;
358
1173
  /** The job's id (the `_scheduled_functions` row id). */
359
1174
  id: string;
1175
+ /** Logical workpool the job is concurrency-gated by, when any. */
1176
+ pool?: string;
360
1177
  /** When the job is scheduled to fire (epoch ms). */
361
1178
  scheduledFor: number;
362
1179
  /** Routing hint forwarded so dispatch lands on the right shard. */
363
1180
  shardKey?: string;
1181
+ /**
1182
+ * The workflow/agent export key (its generated class name) a fresh durable instance is started from
1183
+ * on fire (the {@link ScheduledFunctionDoc.args} become its `params`). Set
1184
+ * instead of {@link ScheduledFunctionDoc.functionPath}.
1185
+ */
1186
+ workflow?: string;
364
1187
  }
365
1188
  /** Maps each system table name to the document shape its reads return. */
366
1189
  interface SystemDocMap {
@@ -375,55 +1198,102 @@ interface SystemQuery<T extends SystemTableName> {
375
1198
  collect: () => Promise<SystemDoc<T>[]>;
376
1199
  }
377
1200
  /**
378
- * Read-only reader over Lunora's system tables (`_scheduled_functions`,
379
- * `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
380
- *
381
- * **Best-effort and eventually consistent.** Unlike `ctx.db.&lt;table>` — which
382
- * reads the shard's transactional SQLite snapshot — the data behind these tables
383
- * lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
384
- * objects in R2). Every `collect()` / `get()` reaches across to that source.
385
- *
386
- * It is **not part of the mutation transaction snapshot** (no OCC guard, no
387
- * subscription dependency recorded — reading it inside a mutation does not pin
388
- * it), and results are **eventually consistent** with writes a mutation just
389
- * made (e.g. a freshly scheduled job may not appear yet).
390
- *
391
- * Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
392
- * objects via `ctx.storage`.
393
- */
1201
+ * Read-only reader over Lunora's system tables (`_scheduled_functions`,
1202
+ * `_storage`), exposed as `ctx.db.system`. Mirrors Convex's `ctx.db.system`.
1203
+ *
1204
+ * **Best-effort and eventually consistent.** Unlike `ctx.db.<table>` — which
1205
+ * reads the shard's transactional SQLite snapshot — the data behind these tables
1206
+ * lives OUTSIDE the shard (scheduled functions in the `SchedulerDO`, storage
1207
+ * objects in R2). Every `collect()` / `get()` reaches across to that source.
1208
+ *
1209
+ * It is **not part of the mutation transaction snapshot** (no OCC guard, no
1210
+ * subscription dependency recorded — reading it inside a mutation does not pin
1211
+ * it), and results are **eventually consistent** with writes a mutation just
1212
+ * made (e.g. a freshly scheduled job may not appear yet).
1213
+ *
1214
+ * Read-only by design: mutate scheduled jobs via `ctx.scheduler`, storage
1215
+ * objects via `ctx.storage`.
1216
+ */
394
1217
  interface SystemDatabaseReader {
395
1218
  /**
396
- * Resolve a single system-table row by id, or `null` when absent.
397
- * (`_scheduled_functions` → job id; `_storage` → object key.)
398
- */
1219
+ * Resolve a single system-table row by id, or `null` when absent.
1220
+ * (`_scheduled_functions` → job id; `_storage` → object key.)
1221
+ */
399
1222
  get: <T extends SystemTableName>(table: T, id: string) => Promise<SystemDoc<T> | null>;
400
1223
  /**
401
- * Begin a read over a system table; call `.collect()` to resolve the full
402
- * list. No filtering, indexing, or pagination — the backing source is remote
403
- * and the surface stays deliberately minimal.
404
- */
1224
+ * Begin a read over a system table; call `.collect()` to resolve the full
1225
+ * list. No filtering, indexing, or pagination — the backing source is remote
1226
+ * and the surface stays deliberately minimal.
1227
+ */
405
1228
  query: <T extends SystemTableName>(table: T) => SystemQuery<T>;
406
1229
  }
407
1230
  /**
408
- * Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
409
- * actual SQL implementation lives in `@lunora/do`; these are signatures only.
410
- */
1231
+ * Read-only handle bound to a table. Used by `query`/`mutation`/`action`. The
1232
+ * actual SQL implementation lives in `@lunora/do`; these are signatures only.
1233
+ */
411
1234
  interface DatabaseReader {
1235
+ /**
1236
+ * The throwing sibling of {@link DatabaseReader.normalizeId}: brand `id` as an
1237
+ * {@link Id} for `tableName`, or throw `BAD_REQUEST` when it is not structurally
1238
+ * an id. Pure — it never reads the database, so a valid id for a row that
1239
+ * doesn't exist still returns.
1240
+ *
1241
+ * This is the **parse boundary** for an id that arrived as a plain `string`: a
1242
+ * wire payload, a mutator's args, a change plan computed on the client. The
1243
+ * alternative is `value as Id<"table">` at every such call site — an assertion,
1244
+ * not a check, and one that has to be repeated for every table a helper is
1245
+ * generic over:
1246
+ *
1247
+ * ```ts
1248
+ * for (const patch of plan.patches) {
1249
+ * await ctx.db.patch(ctx.db.asId("nodes", patch.id), patch.fields);
1250
+ * }
1251
+ * ```
1252
+ *
1253
+ * Ids are opaque strings, so the check is exactly `normalizeId`'s: it rejects
1254
+ * empty, whitespace-bearing, and NUL-bearing values, not "an id that isn't in
1255
+ * this table". Use it to get the brand honestly, and `get()` to learn whether
1256
+ * the row exists.
1257
+ */
1258
+ asId: <T extends string>(tableName: T, id: string) => Id<T>;
412
1259
  get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
413
1260
  /**
414
- * Validate an untrusted `id` string against the structural shape of an id
415
- * for `tableName`, returning the branded {@link Id} when it is well-formed
416
- * and `null` otherwise. Pure structural validation — it never reads the
417
- * database, so a structurally valid id for a row that doesn't exist still
418
- * returns the branded id (mirrors Convex's `db.normalizeId`).
419
- */
1261
+ * Validate an untrusted `id` string against the structural shape of an id
1262
+ * for `tableName`, returning the branded {@link Id} when it is well-formed
1263
+ * and `null` otherwise. Pure structural validation — it never reads the
1264
+ * database, so a structurally valid id for a row that doesn't exist still
1265
+ * returns the branded id (mirrors Convex's `db.normalizeId`).
1266
+ */
420
1267
  normalizeId: <T extends string>(tableName: T, id: string) => Id<T> | null;
421
1268
  query: (tableName: string) => TableReader;
422
1269
  /**
423
- * Best-effort, read-only reader over Lunora's system tables
424
- * (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
425
- * part of the transaction snapshot — see {@link SystemDatabaseReader}.
426
- */
1270
+ * Walk the relation graph out of one row: follow the foreign keys the
1271
+ * schema's `v.id("target")` columns already declare, breadth-first, and
1272
+ * return each reached row with its `depth`, the edge names walked to reach
1273
+ * it (`path`), the ids along the way (`pathIds`), and a depth-decaying
1274
+ * `score`.
1275
+ *
1276
+ * This is the read a keyword or vector search cannot do: "everything
1277
+ * connected to this customer" is a property of the edges, not of the text.
1278
+ * Depth 1 (the default) is the direct neighbourhood; a multi-hop expansion
1279
+ * is the same call with a higher `depth`.
1280
+ *
1281
+ * ```ts
1282
+ * const { nodes } = await ctx.db.related({ table: "customers", id }, { depth: 2 });
1283
+ * ```
1284
+ *
1285
+ * Every hop is a normal `ctx.db` read, so row-level security, column
1286
+ * masking, soft-delete scoping and subscription dependency tracking all
1287
+ * apply per hop. `depth` is capped at 4 and `limit` at 200; a
1288
+ * `v.array(v.id(...))` column is followed outward only (there is no
1289
+ * array-containment filter to find its holders with).
1290
+ */
1291
+ related: (start: RelatedStart, options?: RelatedOptions) => Promise<RelatedPage>;
1292
+ /**
1293
+ * Best-effort, read-only reader over Lunora's system tables
1294
+ * (`_scheduled_functions`, `_storage`). Eventually consistent and **not**
1295
+ * part of the transaction snapshot — see {@link SystemDatabaseReader}.
1296
+ */
427
1297
  readonly system: SystemDatabaseReader;
428
1298
  }
429
1299
  /** Options for {@link TableReader.paginate} — Convex-compatible page request. */
@@ -431,14 +1301,14 @@ interface PaginationOptions {
431
1301
  /** Opaque cursor from the prior page's `continueCursor`; `null`/omitted starts at the first page. */
432
1302
  cursor?: null | string;
433
1303
  /**
434
- * Optional inclusive upper bound for reactive pagination. When supplied the
435
- * page covers the fixed half-open range `(cursor, endCursor]` (ignoring
436
- * `numItems`): every row strictly after `cursor` up to and including the
437
- * boundary row `endCursor` encodes. The page's `isDone` is `true` and its
438
- * `continueCursor` echoes `endCursor`, so the next page keeps starting where
439
- * this one ends even as rows are inserted/deleted inside the range. Omit (or
440
- * pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
441
- */
1304
+ * Optional inclusive upper bound for reactive pagination. When supplied the
1305
+ * page covers the fixed half-open range `(cursor, endCursor]` (ignoring
1306
+ * `numItems`): every row strictly after `cursor` up to and including the
1307
+ * boundary row `endCursor` encodes. The page's `isDone` is `true` and its
1308
+ * `continueCursor` echoes `endCursor`, so the next page keeps starting where
1309
+ * this one ends even as rows are inserted/deleted inside the range. Omit (or
1310
+ * pass `null`) for the legacy "first `numItems` after `cursor`" behaviour.
1311
+ */
442
1312
  endCursor?: null | string;
443
1313
  /** Maximum rows to return for this page. */
444
1314
  numItems: number;
@@ -451,47 +1321,176 @@ interface PaginationResult<T = Record<string, unknown>> {
451
1321
  isDone: boolean;
452
1322
  page: T[];
453
1323
  /**
454
- * Reactive-pagination only: the midpoint cursor of a bounded
455
- * `(cursor, endCursor]` page, used by the client to split an over-grown page
456
- * into two adjacent ranges. Absent on legacy (open-ended) pages.
457
- */
1324
+ * Reactive-pagination only: the midpoint cursor of a bounded
1325
+ * `(cursor, endCursor]` page, used by the client to split an over-grown page
1326
+ * into two adjacent ranges. Absent on legacy (open-ended) pages.
1327
+ */
458
1328
  splitCursor?: null | string;
459
1329
  }
1330
+ /** Which way foreign-key edges are followed by {@link DatabaseReader.related}. */
1331
+ type RelatedDirection = "both" | "in" | "out";
1332
+ /** An explicit `{ table, id }` start node for {@link DatabaseReader.related}. */
1333
+ interface RelatedStartReference {
1334
+ id: string;
1335
+ table: string;
1336
+ }
460
1337
  /**
461
- * The fluent `ctx.db.query(table)` reader. Generic over the document type
462
- * `Row` so the generated `ctx.db` can bind it to `Doc&lt;table>` (the chain and
463
- * every terminal then resolve typed rows — no `as unknown as Doc&lt;...>` casts).
464
- * Defaults to the untyped `Record&lt;string, unknown>` shape for the base
465
- * (schema-agnostic) `@lunora/server` reader.
466
- */
467
- interface TableReader<Row = Record<string, unknown>> {
1338
+ * Where a traversal starts: a loaded document (recognised by its `_id`, whose
1339
+ * table the reader resolves), or an explicit `{ table, id }`.
1340
+ *
1341
+ * The document arm requires `_id`, which is what makes the two arms actually
1342
+ * distinguishable: against a bare `Record<string, unknown>` the union collapses
1343
+ * — `{ table, id }` is assignable to it — so a misspelled `{ tabel, id }` used
1344
+ * to type-check and fail only at runtime.
1345
+ */
1346
+ type RelatedStart = (Record<string, unknown> & {
1347
+ _id: string;
1348
+ }) | RelatedStartReference;
1349
+ /** Options for {@link DatabaseReader.related}. */
1350
+ interface RelatedOptions {
1351
+ /** Opaque cursor from a prior page's `continueCursor`; `null`/omitted starts at the first page. */
1352
+ cursor?: null | string;
1353
+ /**
1354
+ * How many hops to expand. `1` (the default) is the direct neighbourhood.
1355
+ * Must be an integer in `1 … 4`; anything else is refused rather than
1356
+ * clamped, because a silently-shortened walk looks like a graph with fewer
1357
+ * edges than it has.
1358
+ */
1359
+ depth?: number;
1360
+ /**
1361
+ * Which way foreign keys are followed. `"out"` follows the ids the start
1362
+ * row HOLDS (ticket → customer), `"in"` the rows that point AT it (customer
1363
+ * → tickets), `"both"` (the default) does both.
1364
+ */
1365
+ direction?: RelatedDirection;
1366
+ /**
1367
+ * Restrict the walk to these edge-type names — `"<table>.<column>"`, e.g.
1368
+ * `"tickets.customerId"`. Omitted ⇒ every `v.id(...)` column the schema
1369
+ * declares. An unknown name is refused, so a typo cannot silently widen the
1370
+ * traversal.
1371
+ */
1372
+ edges?: ReadonlyArray<string>;
1373
+ /** Maximum nodes per page. Default 50, capped at 200. */
1374
+ limit?: number;
1375
+ }
1376
+ /** One row reached by {@link DatabaseReader.related}, with how it was reached. */
1377
+ interface RelatedNode<T = Record<string, unknown>> {
1378
+ /** Hops from the start node; always `>= 1`. */
1379
+ depth: number;
1380
+ /** The reached row. */
1381
+ document: T;
1382
+ /** Edge-type names walked from the start node to this one, in order. Length equals `depth`. */
1383
+ path: ReadonlyArray<string>;
1384
+ /** Document ids from the start node to this one inclusive. Length equals `depth + 1`. */
1385
+ pathIds: ReadonlyArray<string>;
1386
+ /** Depth-decaying relevance: `1` at depth 1, halving each hop (`0.5 ** (depth - 1)`). */
1387
+ score: number;
1388
+ /** The table {@link RelatedNode.document} lives in. */
1389
+ table: string;
1390
+ }
1391
+ /** One page of a {@link DatabaseReader.related} traversal — the {@link PaginationResult} envelope, with `nodes` instead of `page`. */
1392
+ interface RelatedPage<T = Record<string, unknown>> {
1393
+ /** Cursor to pass back for the next page, or `null` once `isDone`. */
1394
+ continueCursor: null | string;
1395
+ /** `true` when this page is the last one. */
1396
+ isDone: boolean;
1397
+ /** The reached nodes, nearest first. */
1398
+ nodes: RelatedNode<T>[];
1399
+ }
1400
+ /**
1401
+ * The fluent `ctx.db.query(table)` reader. Generic over the document type
1402
+ * `Row` so the generated `ctx.db` can bind it to `Doc<table>` (the chain and
1403
+ * every terminal then resolve typed rows — no `as unknown as Doc<...>` casts),
1404
+ * and over the table's declared index names so `.withIndex()` / `.withSearchIndex()`
1405
+ * / `.withGeoIndex()` reject a name the table does not declare.
1406
+ *
1407
+ * All four default to the untyped shape for the base (schema-agnostic)
1408
+ * `@lunora/server` reader, which is also the wide `(table: string) => TableReader`
1409
+ * overload the generated `ctx.db` intersects in — so a caller holding a runtime
1410
+ * string (e.g. `@lunora/ratelimit`'s `createDbStore`) is unaffected.
1411
+ *
1412
+ * The index-name parameters are what make a stale index name a compile error.
1413
+ * They resolve to `never` for a table that declares none of that kind, so the
1414
+ * only way to satisfy the call is to declare the index. Before this, a renamed
1415
+ * or dropped index left its call sites typechecking, and the query either threw
1416
+ * at runtime or silently degraded to a full table scan — the second being the
1417
+ * worse outcome, since it stays green in tests and surfaces months later as a
1418
+ * latency regression.
1419
+ *
1420
+ * **Why `with*` are method signatures and everything else is a property.**
1421
+ * Narrowing a parameter makes the enclosing type contravariant in it, so as
1422
+ * function properties these would make a BOUND reader
1423
+ * (`TableReader<Doc, "by_x">`, what `ctx.db.query(t)` returns) unassignable to
1424
+ * the unbound `TableReader<Doc>` — quietly breaking every helper factored as
1425
+ * `(reader: TableReader<Doc<"users">>) => …`, which is the obvious way to share
1426
+ * query logic. A method signature is bivariant in its parameters, which keeps
1427
+ * that direction working while the narrow parameter still rejects an undeclared
1428
+ * name at the call site. Both directions are pinned in `types.test-d.ts`.
1429
+ */
1430
+ interface TableReader<Row = Record<string, unknown>, Indexes extends string = string, SearchIndexes extends string = string, GeoIndexes extends string = string> {
1431
+ /**
1432
+ * Iterate rows lazily: `for await (const row of ctx.db.query("t").withIndex(…))`.
1433
+ *
1434
+ * Pages through the result set behind the scenes and yields row by row, so
1435
+ * a consumer that stops early stops the reads too. `.collect()` is still the
1436
+ * right terminal when you want the whole set; this exists for the cases
1437
+ * where you cannot know up front how far you need to read.
1438
+ *
1439
+ * That is what merged/ordered index streams need. Reimplementing Convex's
1440
+ * `convex-helpers/server/stream` in userland previously meant materialising
1441
+ * each branch with a bounded `.take(1024)` before merging, so asking a
1442
+ * merged stream for ONE row read up to 1,024 rows per branch. The k-way merge itself is application code and stays there — only
1443
+ * the laziness had to come from the database layer.
1444
+ *
1445
+ * Iteration pages through `.paginate()`, so it follows the same order —
1446
+ * which is `.collect()`'s order whenever the sort key is unique. Under a
1447
+ * TIED sort key (an unindexed read whose rows share `_creationTime`) the
1448
+ * two can disagree, because the tie-break is left to SQLite. Read through
1449
+ * an index when order matters, exactly as you would for `.paginate()`.
1450
+ */
1451
+ [Symbol.asyncIterator]: () => AsyncIterator<Row>;
468
1452
  collect: () => Promise<Row[]>;
469
- filter: (predicate: (document: Row) => boolean) => TableReader<Row>;
1453
+ filter: (predicate: (document: Row) => boolean) => TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
470
1454
  first: () => Promise<Row | null>;
471
1455
  /**
472
- * Set the result order. Orders by the active `.withIndex()` (or by
473
- * `_creationTime` when none is staged), `"asc"` by default; `"desc"`
474
- * reverses it. Composes with `.withIndex()`, `.filter()`, and every
475
- * terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
476
- * `.order("asc" | "desc")`.
477
- */
478
- order: (direction: "asc" | "desc") => TableReader<Row>;
1456
+ * Set the result order. Orders by the active `.withIndex()` (or by
1457
+ * `_creationTime` when none is staged), `"asc"` by default; `"desc"`
1458
+ * reverses it. Composes with `.withIndex()`, `.filter()`, and every
1459
+ * terminal (`collect`/`first`/`take`/`paginate`/`unique`). Mirrors Convex's
1460
+ * `.order("asc" | "desc")`.
1461
+ */
1462
+ order: (direction: "asc" | "desc") => TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
479
1463
  paginate: (options: PaginationOptions) => Promise<PaginationResult<Row>>;
480
1464
  take: (limit: number) => Promise<Row[]>;
481
1465
  /**
482
- * Return the single matching document. Returns `null` when nothing matches
483
- * and throws when more than one row matches. Mirrors Convex's `.unique()`.
484
- */
1466
+ * Return the single matching document. Returns `null` when nothing matches
1467
+ * and throws when more than one row matches. Mirrors Convex's `.unique()`.
1468
+ */
485
1469
  unique: () => Promise<Row | null>;
486
- withIndex: (indexName: string, range?: (q: IndexRangeBuilder) => IndexRangeBuilder) => TableReader<Row>;
487
1470
  /**
488
- * Restrict the query to a declared `.searchIndex()`. The builder's
489
- * `.search(field, query)` runs a full-text match against the index's
490
- * searchable field; `.eq(field, value)` narrows by a declared filter
491
- * field. Results come back ordered by relevance — pair with `.take(n)`
492
- * (`.paginate()` is not supported on a search query).
493
- */
494
- withSearchIndex: (indexName: string, search: (q: SearchFilterBuilder) => SearchFilterBuilder) => TableReader<Row>;
1471
+ * Restrict the query to a declared `.geoIndex()`. The builder's
1472
+ * `.near(point, radiusMeters)` returns rows within `radiusMeters` of `point`,
1473
+ * ordered nearest-first; `.within(bbox)` returns rows inside the
1474
+ * latitude/longitude bounding box. Both resolve as a geohash-prefix range
1475
+ * scan over the index's companion followed by a Haversine refine. Pair with
1476
+ * `.take(n)` to cap results (`.paginate()` is not supported on a geo query).
1477
+ */
1478
+ withGeoIndex(indexName: GeoIndexes, build: (q: GeoFilterBuilder) => GeoFilterBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
1479
+ /**
1480
+ * Restrict the query to a declared `.index()`. `indexName` is constrained to
1481
+ * this table's declared index names (`never` when it declares none), so a
1482
+ * renamed, dropped, or mistyped index is a compile error rather than a
1483
+ * runtime throw or a silent full-table scan.
1484
+ */
1485
+ withIndex(indexName: Indexes, range?: (q: IndexRangeBuilder) => IndexRangeBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
1486
+ /**
1487
+ * Restrict the query to a declared `.searchIndex()`. The builder's
1488
+ * `.search(field, query)` runs a full-text match against the index's
1489
+ * searchable field; `.eq(field, value)` narrows by a declared filter
1490
+ * field. Results come back ordered by relevance — pair with `.take(n)`
1491
+ * (`.paginate()` is not supported on a search query).
1492
+ */
1493
+ withSearchIndex(indexName: SearchIndexes, search: (q: SearchFilterBuilder) => SearchFilterBuilder): TableReader<Row, Indexes, SearchIndexes, GeoIndexes>;
495
1494
  }
496
1495
  interface IndexRangeBuilder {
497
1496
  eq: (field: string, value: unknown) => IndexRangeBuilder;
@@ -507,13 +1506,37 @@ interface SearchFilterBuilder {
507
1506
  /** Full-text match `query` against the index's searchable `field`. Call exactly once. */
508
1507
  search: (field: string, query: string) => SearchFilterBuilder;
509
1508
  }
1509
+ /** A latitude/longitude point (WGS84 decimal degrees) accepted by geo queries. */
1510
+ interface GeoPointInput {
1511
+ lat: number;
1512
+ lng: number;
1513
+ }
1514
+ /**
1515
+ * An axis-aligned latitude/longitude bounding box: `sw` is the south-west
1516
+ * (min lat, min lng) corner, `ne` the north-east (max lat, max lng) corner.
1517
+ */
1518
+ interface GeoBoundingBox {
1519
+ ne: GeoPointInput;
1520
+ sw: GeoPointInput;
1521
+ }
1522
+ /**
1523
+ * Builder passed to {@link TableReader.withGeoIndex}. Call exactly one of
1524
+ * `.near(...)` / `.within(...)` — the two are mutually exclusive proximity vs
1525
+ * bounding-box modes.
1526
+ */
1527
+ interface GeoFilterBuilder {
1528
+ /** Rows within `radiusMeters` of `point`, resolved nearest-first. Call exactly once. */
1529
+ near: (point: GeoPointInput, radiusMeters: number) => GeoFilterBuilder;
1530
+ /** Rows whose point falls inside the bounding `box`. Call exactly once. */
1531
+ within: (box: GeoBoundingBox) => GeoFilterBuilder;
1532
+ }
510
1533
  /**
511
- * Options shared by the batch-write methods (`insertMany`/`deleteMany`/
512
- * `patchMany`) — a per-call payload cap. The default cap (500) rejects an
513
- * oversized call up front so an accidental O(n²) or a payload past the Durable
514
- * Object request limit fails loudly instead of degrading the mutation. Callers
515
- * with larger sets should chunk their own loop or raise `limit`.
516
- */
1534
+ * Options shared by the batch-write methods (`insertMany`/`deleteMany`/
1535
+ * `patchMany`) — a per-call payload cap. The default cap (500) rejects an
1536
+ * oversized call up front so an accidental O(n²) or a payload past the Durable
1537
+ * Object request limit fails loudly instead of degrading the mutation. Callers
1538
+ * with larger sets should chunk their own loop or raise `limit`.
1539
+ */
517
1540
  interface BatchWriteOptions {
518
1541
  /** Reject the call when the batch size exceeds this value (default 500). */
519
1542
  limit?: number;
@@ -521,67 +1544,85 @@ interface BatchWriteOptions {
521
1544
  /** Options accepted by {@link DatabaseWriter.insertMany} and the per-table facade. */
522
1545
  interface InsertManyOptions extends BatchWriteOptions {
523
1546
  /**
524
- * When `true`, a UNIQUE-constraint breach for a row resolves to `null`
525
- * instead of throwing — the rest of the batch is still inserted. Skipped rows
526
- * keep their input-order slot with `null` in the returned array. Mirrors
527
- * better-drizzle's `createMany({ skipDuplicates: true })`.
528
- */
1547
+ * When `true`, a UNIQUE-constraint breach for a row resolves to `null`
1548
+ * instead of throwing — the rest of the batch is still inserted. Skipped rows
1549
+ * keep their input-order slot with `null` in the returned array. Mirrors
1550
+ * better-drizzle's `createMany({ skipDuplicates: true })`.
1551
+ */
529
1552
  skipDuplicates?: boolean;
530
1553
  }
531
1554
  interface DatabaseWriter extends DatabaseReader {
532
1555
  delete: <T extends string>(id: Id<T>) => Promise<void>;
533
1556
  /**
534
- * Delete many rows by id in one call. Each id is deleted through the full
535
- * single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
536
- * number of ids **requested**, not the rows actually removed — an unknown or
537
- * duplicated id is a silent no-op.
538
- *
539
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
540
- * BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
541
- * error) rolls back the whole mutation. (In an action there is no transaction
542
- * span, so the prior deletes persist; the in-memory test harness mirrors the span.)
543
- */
1557
+ * Delete EVERY row in `tableName`, chunking internally until the table is
1558
+ * empty — the erasure primitive.
1559
+ *
1560
+ * Unlike `deleteWhere(tableName, {})` there is **no batch cap**: a
1561
+ * `BATCH_LIMIT_EXCEEDED` at row 501 of an account deletion is a bug, not a
1562
+ * safety rail. Rows still go through the single-row delete pipeline, so
1563
+ * triggers, cascades, companions, CDC, and live subscriptions stay correct.
1564
+ *
1565
+ * On a `.softDelete()` table the default flips the marker column; pass
1566
+ * `{ hard: true }` to remove the rows physically (what GDPR erasure means).
1567
+ */
1568
+ deleteAll: (tableName: string, options?: {
1569
+ chunkSize?: number;
1570
+ hard?: boolean;
1571
+ }) => Promise<{
1572
+ deleted: number;
1573
+ }>;
1574
+ /**
1575
+ * Delete many rows by id in one call. Each id is deleted through the full
1576
+ * single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
1577
+ * number of ids **requested**, not the rows actually removed — an unknown or
1578
+ * duplicated id is a silent no-op.
1579
+ *
1580
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
1581
+ * BEGIN/COMMIT span, so a mid-batch failure (a later RLS denial or handler
1582
+ * error) rolls back the whole mutation. (In an action there is no transaction
1583
+ * span, so the prior deletes persist; the in-memory test harness mirrors the span.)
1584
+ */
544
1585
  deleteMany: <T extends string>(ids: ReadonlyArray<Id<T>>, options?: BatchWriteOptions) => Promise<{
545
1586
  deleted: number;
546
1587
  }>;
547
1588
  /**
548
- * Delete every row matching `where` in one call. Matching rows are resolved
549
- * first, then each row is deleted through the single-row delete pipeline
550
- * (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
551
- * search/aggregate companions stay correct.
552
- *
553
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
554
- * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
555
- */
1589
+ * Delete every row matching `where` in one call. Matching rows are resolved
1590
+ * first, then each row is deleted through the single-row delete pipeline
1591
+ * (triggers, companion sync, CDC, broadcast) so reactive subscriptions and
1592
+ * search/aggregate companions stay correct.
1593
+ *
1594
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
1595
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
1596
+ */
556
1597
  deleteWhere: (tableName: string, where: Record<string, unknown>, options?: BatchWriteOptions) => Promise<{
557
1598
  deleted: number;
558
1599
  }>;
559
1600
  /**
560
- * Insert a document, returning its server id.
561
- *
562
- * Pass `options.clientId` (a UUID) to key the row yourself — for an
563
- * optimistic client that needs the persisted row to match the key it
564
- * already rendered. It's validated for shape and still subject to the
565
- * primary-key uniqueness constraint; omit it and the server mints the id.
566
- */
1601
+ * Insert a document, returning its server id.
1602
+ *
1603
+ * Pass `options.clientId` (a UUID) to key the row yourself — for an
1604
+ * optimistic client that needs the persisted row to match the key it
1605
+ * already rendered. It's validated for shape and still subject to the
1606
+ * primary-key uniqueness constraint; omit it and the server mints the id.
1607
+ */
567
1608
  insert: <T extends string>(tableName: T, document: Record<string, unknown>, options?: {
568
1609
  clientId?: string;
569
1610
  }) => Promise<Id<T>>;
570
1611
  /**
571
- * Insert many documents into one table in a single call, returning the
572
- * minted ids in input order. Equivalent to a per-row `insert()` loop — each
573
- * row gets defaults, validators, triggers, and a per-row RLS check — but the
574
- * caller pays one round-trip instead of N.
575
- *
576
- * Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
577
- * `null` results for that row instead of failing the whole batch; the rest of
578
- * the batch is still inserted and order is preserved.
579
- *
580
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
581
- * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
582
- * rolls back the whole mutation. (In an action there is no transaction span,
583
- * so the prior inserts persist; the in-memory test harness mirrors the span.)
584
- */
1612
+ * Insert many documents into one table in a single call, returning the
1613
+ * minted ids in input order. Equivalent to a per-row `insert()` loop — each
1614
+ * row gets defaults, validators, triggers, and a per-row RLS check — but the
1615
+ * caller pays one round-trip instead of N.
1616
+ *
1617
+ * Pass `{ skipDuplicates: true }` to turn UNIQUE-constraint breaches into
1618
+ * `null` results for that row instead of failing the whole batch; the rest of
1619
+ * the batch is still inserted and order is preserved.
1620
+ *
1621
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
1622
+ * BEGIN/COMMIT span, so a mid-batch failure (an invalid or RLS-denied row)
1623
+ * rolls back the whole mutation. (In an action there is no transaction span,
1624
+ * so the prior inserts persist; the in-memory test harness mirrors the span.)
1625
+ */
585
1626
  insertMany: {
586
1627
  <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options: BatchWriteOptions & {
587
1628
  skipDuplicates: true;
@@ -589,32 +1630,32 @@ interface DatabaseWriter extends DatabaseReader {
589
1630
  <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: InsertManyOptions): Promise<Id<T>[]>;
590
1631
  };
591
1632
  /**
592
- * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
593
- * `.check()` validators and before/after triggers** for throughput on data you
594
- * control (seed, migration, admin import). Defaults, ids, and every companion
595
- * (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
596
- * stay correct.
597
- *
598
- * It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
599
- * RLS is **not** bypassed: secure-by-default and the table's insert policy still
600
- * apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
601
- * preserve a supplied `_id` (import). Use only for data you trust; prefer
602
- * `insertMany` for anything user-supplied.
603
- */
1633
+ * **Trusted** bulk insert: one multi-row `INSERT` that **skips per-row
1634
+ * `.check()` validators and before/after triggers** for throughput on data you
1635
+ * control (seed, migration, admin import). Defaults, ids, and every companion
1636
+ * (search/aggregate/rank/CDC + live subscriptions) are still applied, so reads
1637
+ * stay correct.
1638
+ *
1639
+ * It is **"unsafe" only in that it bypasses the validation/trigger pipeline** —
1640
+ * RLS is **not** bypassed: secure-by-default and the table's insert policy still
1641
+ * apply (the framework ships no RLS-bypassing writer). Pass `allowExplicitId` to
1642
+ * preserve a supplied `_id` (import). Use only for data you trust; prefer
1643
+ * `insertMany` for anything user-supplied.
1644
+ */
604
1645
  insertManyUnsafe: <T extends string>(tableName: T, documents: ReadonlyArray<Record<string, unknown>>, options?: BatchWriteOptions & {
605
1646
  allowExplicitId?: boolean;
606
1647
  }) => Promise<Id<T>[]>;
607
1648
  patch: <T extends string>(id: Id<T>, patch: Record<string, unknown>) => Promise<void>;
608
1649
  /**
609
- * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
610
- * single `patch()` (per-row triggers + RLS). Returns the number of rows
611
- * actually patched.
612
- *
613
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
614
- * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
615
- * (In an action there is no transaction span, so the prior patches persist;
616
- * the in-memory test harness mirrors the span.)
617
- */
1650
+ * Patch many rows by id in one call. Each `{ id, patch }` is applied like a
1651
+ * single `patch()` (per-row triggers + RLS). Returns the number of rows
1652
+ * actually patched.
1653
+ *
1654
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
1655
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
1656
+ * (In an action there is no transaction span, so the prior patches persist;
1657
+ * the in-memory test harness mirrors the span.)
1658
+ */
618
1659
  patchMany: <T extends string>(patches: ReadonlyArray<{
619
1660
  id: Id<T>;
620
1661
  patch: Record<string, unknown>;
@@ -622,14 +1663,14 @@ interface DatabaseWriter extends DatabaseReader {
622
1663
  patched: number;
623
1664
  }>;
624
1665
  /**
625
- * Patch every row matching `where` with the same `patch` in one call. The
626
- * matching rows are resolved first, then each row is updated through the
627
- * single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
628
- * so reactive subscriptions and search/aggregate companions stay correct.
629
- *
630
- * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
631
- * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
632
- */
1666
+ * Patch every row matching `where` with the same `patch` in one call. The
1667
+ * matching rows are resolved first, then each row is updated through the
1668
+ * single-row patch pipeline (OCC, triggers, companion sync, CDC, broadcast)
1669
+ * so reactive subscriptions and search/aggregate companions stay correct.
1670
+ *
1671
+ * **Atomic within a mutation:** the DO wraps a mutation's dispatch in a
1672
+ * BEGIN/COMMIT span, so a mid-batch failure rolls back the whole mutation.
1673
+ */
633
1674
  patchWhere: (tableName: string, args: {
634
1675
  patch: Record<string, unknown>;
635
1676
  where: Record<string, unknown>;
@@ -637,6 +1678,29 @@ interface DatabaseWriter extends DatabaseReader {
637
1678
  patched: number;
638
1679
  }>;
639
1680
  replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
1681
+ /**
1682
+ * Erase every shard-local table — the account-deletion / tenant-teardown
1683
+ * primitive. Sweeps the schema's non-`.global()` tables with
1684
+ * {@link DatabaseWriter.deleteAll}`({ hard: true })` and returns the per-table
1685
+ * counts.
1686
+ *
1687
+ * `.global()` tables are skipped by design: their rows live in D1 and are shared
1688
+ * across shards, so "wipe this shard" must not reach them. Restrict the sweep
1689
+ * with `options.tables`, or spare one with `options.exclude` (e.g. an audit log
1690
+ * that must outlive the data).
1691
+ *
1692
+ * ```ts
1693
+ * export const deleteAccount = internalMutation({ handler: async ({ ctx }) => ctx.db.wipeShard() });
1694
+ * ```
1695
+ */
1696
+ wipeShard: (options?: {
1697
+ chunkSize?: number;
1698
+ exclude?: ReadonlyArray<string>;
1699
+ tables?: ReadonlyArray<string>;
1700
+ }) => Promise<{
1701
+ deleted: number;
1702
+ tables: Record<string, number>;
1703
+ }>;
640
1704
  }
641
1705
  /** Authenticated identity surfaced into every context. */
642
1706
  interface AuthState {
@@ -644,24 +1708,81 @@ interface AuthState {
644
1708
  readonly userId: string | null;
645
1709
  }
646
1710
  /**
647
- * A pending scheduled invocation as surfaced by {@link Scheduler.list} /
648
- * {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s internal
649
- * `ScheduleRecord` — re-declared here so the public ctx surface carries no
650
- * dependency on the scheduler package's internal types.
651
- */
1711
+ * A pending scheduled invocation as surfaced by {@link Scheduler.list} /
1712
+ * {@link Scheduler.get}. A clean public mirror of `@lunora/scheduler`'s
1713
+ * `ScheduleRecord`, re-declared so the public ctx surface names its own type
1714
+ * rather than re-exporting the scheduler's. It must stay field-for-field
1715
+ * identical: `__tests__/scheduler-mirror.test.ts` asserts mutual assignability
1716
+ * against the real `ScheduleRecord` and fails `lint:types` if either side moves.
1717
+ */
1718
+ interface RetryPolicy {
1719
+ /** Backoff growth across attempts. Default `"exponential"`. */
1720
+ backoff?: "exponential" | "linear";
1721
+ /** Base delay in milliseconds for the first retry. Default `30_000`. */
1722
+ baseMs?: number;
1723
+ /** Maximum number of dispatch attempts before dead-lettering. Default `5`. */
1724
+ maxAttempts?: number;
1725
+ /** Optional ceiling clamping the computed backoff delay. */
1726
+ maxMs?: number;
1727
+ }
652
1728
  interface ScheduledJob {
653
1729
  args: Record<string, unknown>;
654
1730
  /** Number of dispatch attempts already made (absent until the first retry). */
655
1731
  attempts?: number;
656
1732
  /** When the job was enqueued (epoch ms). */
657
1733
  enqueuedAt: number;
658
- functionPath: string;
1734
+ /**
1735
+ * The `ns:fn` path of the function to dispatch on fire. Absent when the job
1736
+ * targets a durable workflow/agent instead — see {@link ScheduledJob.workflow}.
1737
+ * Exactly one of `functionPath` / `workflow` is set.
1738
+ */
1739
+ functionPath?: string;
659
1740
  id: string;
1741
+ /** Scheduler/workpool instance the job was enqueued through. Absent for the default instance. */
1742
+ instanceName?: string;
1743
+ /** Logical workpool the job is concurrency-gated by. Absent for plain `runAfter`/`runAt` jobs. */
1744
+ pool?: string;
1745
+ /** Per-job retry policy; absent means the scheduler's built-in defaults. */
1746
+ retry?: RetryPolicy;
660
1747
  /** When the job is scheduled to fire (epoch ms). */
661
1748
  scheduledFor: number;
662
1749
  /** Routing hint forwarded so dispatch lands on the right shard. */
663
1750
  shardKey?: string;
1751
+ /**
1752
+ * The workflow/agent export key a fresh durable instance is started
1753
+ * from on fire (the {@link ScheduledJob.args} become its `params`). Set
1754
+ * instead of {@link ScheduledJob.functionPath}.
1755
+ */
1756
+ workflow?: string;
1757
+ }
1758
+ /**
1759
+ * A schedulable durable-workflow reference — the generated `workflows.<name>` /
1760
+ * `agents.<name>` object, which carries its export key (the generated class name) and
1761
+ * stable name. Structural mirror of `@lunora/scheduler`'s `WorkflowReference` so
1762
+ * `ctx.scheduler` can target a workflow/agent without a dependency on
1763
+ * `@lunora/scheduler` / `@lunora/workflow`. A scheduled workflow target starts a
1764
+ * fresh instance on fire (the args become its `params`).
1765
+ */
1766
+ interface SchedulableWorkflowReference {
1767
+ /** The workflow/agent export key — its generated class name (present on a generated ref). */
1768
+ readonly className?: string;
1769
+ readonly isLunoraWorkflow: true;
1770
+ /** The workflow/agent export/stable name (present on a generated ref). */
1771
+ readonly name?: string;
664
1772
  }
1773
+ /**
1774
+ * What `ctx.scheduler.runAfter` / `runAt` accept as a target:
1775
+ *
1776
+ * - a generated `internal.<file>.<fn>` / `api.<file>.<fn>` reference to a
1777
+ * mutation or action — the form the docs and the setup skills use, and the one
1778
+ * `@lunora/scheduler` has always resolved at runtime (it reads `__lunoraRef`);
1779
+ * - the equivalent `"file:fn"` path string;
1780
+ * - a generated `workflows.<name>` / `agents.<name>` reference, which starts a
1781
+ * fresh durable instance on fire.
1782
+ *
1783
+ * A `query` is not schedulable — a deferred job exists to have an effect.
1784
+ */
1785
+ type SchedulableTarget = FunctionHandle<"action" | "mutation", unknown, unknown> | SchedulableWorkflowReference | string;
665
1786
  interface Scheduler {
666
1787
  /** Cancel a pending job by id. `cancelled` is `false` when no such job exists. */
667
1788
  cancel: (id: string) => Promise<{
@@ -671,16 +1792,22 @@ interface Scheduler {
671
1792
  get: (id: string) => Promise<ScheduledJob | null>;
672
1793
  /** List all pending scheduled jobs. */
673
1794
  list: () => Promise<ScheduledJob[]>;
674
- runAfter: (delayMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
675
- runAt: (timestampMs: number, functionPath: string, args?: Record<string, unknown>) => Promise<string>;
1795
+ /**
1796
+ * Schedule a one-shot run `delayMs` from now; see {@link SchedulableTarget}
1797
+ * for the accepted targets. A workflow/agent reference starts a fresh
1798
+ * durable instance on fire (the args become its `params`).
1799
+ */
1800
+ runAfter: (delayMs: number, target: SchedulableTarget, args?: Record<string, unknown>) => Promise<string>;
1801
+ /** Like {@link Scheduler.runAfter} but fires at an absolute epoch-ms timestamp. */
1802
+ runAt: (timestampMs: number, target: SchedulableTarget, args?: Record<string, unknown>) => Promise<string>;
676
1803
  }
677
1804
  /**
678
- * A workflow instance's lifecycle status. Clean public mirror of
679
- * `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
680
- * `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
681
- * dependency on the workflow package, exactly as {@link Scheduler} avoids a
682
- * dependency on `@lunora/scheduler`.
683
- */
1805
+ * A workflow instance's lifecycle status. Clean public mirror of
1806
+ * `@lunora/workflow`'s `WorkflowInstanceStatus` (itself a mirror of Cloudflare's
1807
+ * `WorkflowInstanceStatus`) — re-declared here so the ctx surface carries no
1808
+ * dependency on the workflow package, exactly as {@link Scheduler} avoids a
1809
+ * dependency on `@lunora/scheduler`.
1810
+ */
684
1811
  type WorkflowInstanceStatus = "complete" | "errored" | "paused" | "queued" | "running" | "terminated" | "unknown" | "waiting" | "waitingForPause";
685
1812
  /** Result of {@link WorkflowInstance.status}. Mirrors `@lunora/workflow`'s `WorkflowStatusResult`. */
686
1813
  interface WorkflowStatusResult {
@@ -691,20 +1818,62 @@ interface WorkflowStatusResult {
691
1818
  output?: unknown;
692
1819
  status: WorkflowInstanceStatus;
693
1820
  }
1821
+ /** Options accepted by {@link WorkflowHandle.createBatch}: explicit `instances`, or `count` sharing one `params`/`retention`. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchOptions`. */
1822
+ type WorkflowCreateBatchOptions<Params = Record<string, unknown>> = {
1823
+ instances: ReadonlyArray<WorkflowCreateOptions<Params>>;
1824
+ } | {
1825
+ count: number;
1826
+ params?: Params;
1827
+ retention?: {
1828
+ errorRetention?: string;
1829
+ successRetention?: string;
1830
+ };
1831
+ };
1832
+ /** One instance a batch did not create; `index` is its input position. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchError`. */
1833
+ interface WorkflowCreateBatchError {
1834
+ code: number;
1835
+ id?: string;
1836
+ index: number;
1837
+ message: string;
1838
+ }
1839
+ /** Result of {@link WorkflowHandle.createBatch}. Mirrors `@lunora/workflow`'s `WorkflowCreateBatchResult`. */
1840
+ interface WorkflowCreateBatchResult {
1841
+ created: WorkflowInstance[];
1842
+ errors: WorkflowCreateBatchError[];
1843
+ }
694
1844
  /** Options accepted by {@link WorkflowHandle.create}. Mirrors `@lunora/workflow`'s `WorkflowCreateOptions`. */
695
1845
  interface WorkflowCreateOptions<Params = Record<string, unknown>> {
696
1846
  /** Unique-within-the-workflow instance id. Generated by Cloudflare when omitted. */
697
1847
  id?: string;
698
1848
  /** The event payload the instance is triggered with — surfaced as `event.payload`. */
699
1849
  params?: Params;
700
- /** Instance retention policy (defaults to the account maximum). */
1850
+ /** Instance retention policy (defaults to the workflow's `defaultRetention`, else the plan default). */
701
1851
  retention?: {
702
1852
  errorRetention?: string;
703
1853
  successRetention?: string;
704
1854
  };
705
1855
  }
1856
+ /**
1857
+ * One declared external event a workflow waits on. Mirrors `@lunora/workflow`'s
1858
+ * `WorkflowEventDefinition` — the value `defineWorkflowEvent` returns, taken by
1859
+ * both {@link WorkflowInstance.sendEvent} and the workflow's `ctx.waitForEvent`.
1860
+ */
1861
+ interface WorkflowEventDefinition<Payload = unknown> {
1862
+ readonly isLunoraWorkflowEvent: true;
1863
+ readonly payload: Validator<Payload>;
1864
+ readonly type: string;
1865
+ }
1866
+ /** One event `WorkflowInstance.subscribe()` streams. Mirrors `@lunora/workflow`'s `WorkflowInstanceEventLike`. */
1867
+ interface WorkflowInstanceEvent {
1868
+ [field: string]: unknown;
1869
+ eventId: number;
1870
+ instanceId: string;
1871
+ timestamp: number;
1872
+ type: string;
1873
+ }
706
1874
  /** A live handle to a single workflow instance. Mirrors `@lunora/workflow`'s `WorkflowInstanceLike`. */
707
1875
  interface WorkflowInstance {
1876
+ delete: () => Promise<void>;
708
1877
  readonly id: string;
709
1878
  pause: () => Promise<void>;
710
1879
  restart: () => Promise<void>;
@@ -714,66 +1883,86 @@ interface WorkflowInstance {
714
1883
  type: string;
715
1884
  }) => Promise<void>;
716
1885
  status: () => Promise<WorkflowStatusResult>;
1886
+ /** Stream the instance's events — its full history first, then new ones as it runs. */
1887
+ subscribe: (options?: {
1888
+ cursor?: number;
1889
+ filter?: string[];
1890
+ }) => Promise<{
1891
+ next: () => Promise<IteratorResult<WorkflowInstanceEvent, void>>;
1892
+ }>;
717
1893
  terminate: () => Promise<void>;
718
1894
  }
719
1895
  /**
720
- * A typed handle to one declared workflow, addressable from `ctx.workflows`.
721
- * Mirrors `@lunora/workflow`'s `WorkflowHandle`.
722
- */
1896
+ * A typed handle to one declared workflow, addressable from `ctx.workflows`.
1897
+ * Mirrors `@lunora/workflow`'s `WorkflowHandle`.
1898
+ */
723
1899
  interface WorkflowHandle<Params = Record<string, unknown>> {
724
1900
  /** Start a new instance (optionally with an id + params). */
725
1901
  create: (options?: WorkflowCreateOptions<Params>) => Promise<WorkflowInstance>;
726
- /** Start many instances in one batched RPC. */
727
- createBatch: (batch: ReadonlyArray<WorkflowCreateOptions<Params>>) => Promise<WorkflowInstance[]>;
1902
+ /** Start many instances in one batched RPC; per-instance failures come back in `errors`, not dropped. */
1903
+ createBatch: (options: WorkflowCreateBatchOptions<Params>) => Promise<WorkflowCreateBatchResult>;
1904
+ /** Delete up to 100 instances and their stored state; ids that do not exist come back as per-instance errors. */
1905
+ deleteBatch: (instanceIds: ReadonlyArray<string>) => Promise<{
1906
+ deleted: {
1907
+ id: string;
1908
+ }[];
1909
+ errors: {
1910
+ code: number;
1911
+ id: string;
1912
+ message: string;
1913
+ }[];
1914
+ }>;
728
1915
  /** Get a handle to an existing instance by id. */
729
1916
  get: (id: string) => Promise<WorkflowInstance>;
1917
+ /** Deliver a declared event (`defineWorkflowEvent`) to one instance; the payload is validated before the send. */
1918
+ sendEvent: <Payload>(instanceId: string, event: WorkflowEventDefinition<Payload>, payload: Payload) => Promise<void>;
730
1919
  }
731
1920
  /**
732
- * The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
733
- * workflow declared in `lunora/workflows.ts` is reachable by its export name;
734
- * codegen narrows the `get(name)` overloads to the known workflow names + their
735
- * inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
736
- */
1921
+ * The `ctx.workflows` surface on {@link MutationCtx} / {@link ActionCtx}. Each
1922
+ * workflow declared in `lunora/workflows.ts` is reachable by its export name;
1923
+ * codegen narrows the `get(name)` overloads to the known workflow names + their
1924
+ * inferred param types. Mirrors `@lunora/workflow`'s `Workflows`.
1925
+ */
737
1926
  interface Workflows {
738
1927
  /** Resolve the handle for a declared workflow by export name. */
739
1928
  get: <Params = Record<string, unknown>>(name: string) => WorkflowHandle<Params>;
740
1929
  }
741
1930
  /**
742
- * Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
743
- * in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
744
- * Queries and mutations do not expose this — they run inside the Durable Object.
745
- */
1931
+ * Programmatic cache purge surface exposed on {@link ActionCtx}. Actions run
1932
+ * in the Worker (not the DO), so they can reach the Worker's `ctx.cache.purge`.
1933
+ * Queries and mutations do not expose this — they run inside the Durable Object.
1934
+ */
746
1935
  interface CachePurge {
747
1936
  /**
748
- * Purge cached responses matching the given tags, or everything when
749
- * `purgeEverything` is true. Only available in action handlers.
750
- */
1937
+ * Purge cached responses matching the given tags, or everything when
1938
+ * `purgeEverything` is true. Only available in action handlers.
1939
+ */
751
1940
  purge: (options: {
752
1941
  purgeEverything?: boolean;
753
1942
  tags?: string[];
754
1943
  }) => Promise<unknown>;
755
1944
  }
756
1945
  /**
757
- * Structural projection of workers-types' `SecretsStoreSecret` binding — the
758
- * per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
759
- * secret value (or throws if it does not exist). Mirrored structurally so the
760
- * runtime resolves it without a workerd type dependency.
761
- */
1946
+ * Structural projection of workers-types' `SecretsStoreSecret` binding — the
1947
+ * per-secret `secrets_store_secrets[]` binding whose `.get()` resolves the
1948
+ * secret value (or throws if it does not exist). Mirrored structurally so the
1949
+ * runtime resolves it without a workerd type dependency.
1950
+ */
762
1951
  interface SecretsStoreSecretLike {
763
1952
  get: () => Promise<string>;
764
1953
  }
765
1954
  /**
766
- * `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
767
- * A core built-in (always present on every context, like `ctx.log`): a binding
768
- * named in wrangler's `secrets_store_secrets[]` is read by its binding name.
769
- *
770
- * ```ts
771
- * const apiKey = await ctx.secrets.get("STRIPE_KEY");
772
- * ```
773
- *
774
- * The lookup is async (the platform fetches and decrypts on first read);
775
- * reading an undeclared name throws a directed error naming the bound secrets.
776
- */
1955
+ * `ctx.secrets` — read account-level secrets bound via Cloudflare Secrets Store.
1956
+ * A core built-in (always present on every context, like `ctx.log`): a binding
1957
+ * named in wrangler's `secrets_store_secrets[]` is read by its binding name.
1958
+ *
1959
+ * ```ts
1960
+ * const apiKey = await ctx.secrets.get("STRIPE_KEY");
1961
+ * ```
1962
+ *
1963
+ * The lookup is async (the platform fetches and decrypts on first read);
1964
+ * reading an undeclared name throws a directed error naming the bound secrets.
1965
+ */
777
1966
  interface Secrets {
778
1967
  /** Resolve a Secrets Store secret by its wrangler binding name. */
779
1968
  get: (name: string) => Promise<string>;
@@ -783,11 +1972,11 @@ type TriggerTiming = "after" | "before";
783
1972
  /** The CRUD operation a trigger reacts to. `patch` and `replace` both map to `update`. */
784
1973
  type TriggerOp = "delete" | "insert" | "update";
785
1974
  /**
786
- * A row as observed by a trigger handler: the table's `Shape` (with the same
787
- * optionality rules as {@link InferArgs}) plus the system columns every stored
788
- * doc carries.
789
- */
790
- 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]> } & {
1975
+ * A row as observed by a trigger handler: the table's `Shape` (with the same
1976
+ * optionality rules as {@link InferArgs}) plus the system columns every stored
1977
+ * doc carries.
1978
+ */
1979
+ 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]>; } & {
791
1980
  readonly _creationTime: number;
792
1981
  readonly _id: string;
793
1982
  };
@@ -799,11 +1988,11 @@ interface TriggerInsertEvent<Shape extends Record<string, Validator> = Record<st
799
1988
  readonly table: string;
800
1989
  }
801
1990
  /**
802
- * What an `update` trigger observes: the merged row plus the pre-write row.
803
- * `previous` is typed as always present (the row must exist to be updated); the
804
- * runtime supplies it best-effort and only omits it in the unreachable
805
- * row-vanished-mid-write case.
806
- */
1991
+ * What an `update` trigger observes: the merged row plus the pre-write row.
1992
+ * `previous` is typed as always present (the row must exist to be updated); the
1993
+ * runtime supplies it best-effort and only omits it in the unreachable
1994
+ * row-vanished-mid-write case.
1995
+ */
807
1996
  interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
808
1997
  readonly doc: TriggerRow<Shape>;
809
1998
  readonly id: string;
@@ -812,10 +2001,10 @@ interface TriggerUpdateEvent<Shape extends Record<string, Validator> = Record<st
812
2001
  readonly table: string;
813
2002
  }
814
2003
  /**
815
- * What a `delete` trigger observes: the row about to be (or just) removed.
816
- * `previous` is typed as always present; the runtime supplies it best-effort
817
- * and only omits it in the unreachable row-vanished-mid-write case.
818
- */
2004
+ * What a `delete` trigger observes: the row about to be (or just) removed.
2005
+ * `previous` is typed as always present; the runtime supplies it best-effort
2006
+ * and only omits it in the unreachable row-vanished-mid-write case.
2007
+ */
819
2008
  interface TriggerDeleteEvent<Shape extends Record<string, Validator> = Record<string, Validator>> {
820
2009
  readonly id: string;
821
2010
  readonly op: "delete";
@@ -839,10 +2028,10 @@ interface TriggerQueryArgs {
839
2028
  with?: Record<string, unknown>;
840
2029
  }
841
2030
  /**
842
- * Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
843
- * `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
844
- * `@lunora/server` don't take a hard dep on the DO runtime.
845
- */
2031
+ * Args accepted by {@link TriggerDatabase.aggregate} — structural mirror of
2032
+ * `@lunora/do`'s `AggregateOptions`, kept local so trigger handlers in
2033
+ * `@lunora/server` don't take a hard dep on the DO runtime.
2034
+ */
846
2035
  interface TriggerAggregateOptions {
847
2036
  baseWhere?: Record<string, unknown>;
848
2037
  field?: string;
@@ -887,17 +2076,17 @@ interface TriggerRankPageOptions {
887
2076
  where?: Record<string, unknown>;
888
2077
  }
889
2078
  /**
890
- * Portable, table/id-addressed ORM writer handed to trigger handlers via
891
- * `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
892
- * **not** the generated per-table `ctx.db.&lt;table>` facade (which can't be typed
893
- * from inside `defineTable`, where the full schema isn't known).
894
- *
895
- * `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
896
- * trigger-maintained counter and rank tables the user-facing reader uses, so
897
- * a handler's `ctx.db.&lt;table>.aggregate(...)` observes the just-staged write
898
- * within the same DO transaction (the counter step happens before the trigger
899
- * fires).
900
- */
2079
+ * Portable, table/id-addressed ORM writer handed to trigger handlers via
2080
+ * `ctx.db`. Mirrors `@lunora/do`'s runtime `DatabaseWriterLike` surface — it is
2081
+ * **not** the generated per-table `ctx.db.<table>` facade (which can't be typed
2082
+ * from inside `defineTable`, where the full schema isn't known).
2083
+ *
2084
+ * `aggregate`/`groupBy`/`count`/`rank`/`rankPage` route through the same
2085
+ * trigger-maintained counter and rank tables the user-facing reader uses, so
2086
+ * a handler's `ctx.db.<table>.aggregate(...)` observes the just-staged write
2087
+ * within the same DO transaction (the counter step happens before the trigger
2088
+ * fires).
2089
+ */
901
2090
  interface TriggerDatabase {
902
2091
  aggregate: (tableName: string, options: TriggerAggregateOptions) => Promise<null | number>;
903
2092
  count: (tableName: string, where?: Record<string, unknown>) => Promise<number>;
@@ -913,10 +2102,10 @@ interface TriggerDatabase {
913
2102
  replace: (id: string, document: Record<string, unknown>) => Promise<void>;
914
2103
  }
915
2104
  /**
916
- * Handle injected into every trigger handler. `db` is the portable ORM writer;
917
- * `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
918
- * **not** transactional with the firing write).
919
- */
2105
+ * Handle injected into every trigger handler. `db` is the portable ORM writer;
2106
+ * `scheduler` enqueues async / cross-shard follow-up work (cross-shard work is
2107
+ * **not** transactional with the firing write).
2108
+ */
920
2109
  interface TriggerCtx {
921
2110
  readonly db: TriggerDatabase;
922
2111
  readonly scheduler: Scheduler;
@@ -924,20 +2113,20 @@ interface TriggerCtx {
924
2113
  /** A user-declared trigger handler. Throwing from a `before*` handler aborts the write. */
925
2114
  type TriggerHandler<Event> = (context: TriggerCtx, event: Event) => Promise<void> | void;
926
2115
  /**
927
- * A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
928
- * The handler's event type is erased to the {@link TriggerEvent} union here; the
929
- * per-op {@link TriggerBuilder} methods recover the precise event type for
930
- * authors.
931
- */
2116
+ * A single declared trigger, as stored in {@link TableDefinition.triggerMap}.
2117
+ * The handler's event type is erased to the {@link TriggerEvent} union here; the
2118
+ * per-op {@link TriggerBuilder} methods recover the precise event type for
2119
+ * authors.
2120
+ */
932
2121
  interface TriggerDefinition {
933
2122
  readonly handler: TriggerHandler<TriggerEvent>;
934
2123
  readonly op: TriggerOp;
935
2124
  readonly timing: TriggerTiming;
936
2125
  }
937
2126
  /**
938
- * The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
939
- * to one `timing`+`op` pair, typing the event against the table's `Shape`.
940
- */
2127
+ * The `t` argument passed to `.triggers((t) => …)`. Each method binds a handler
2128
+ * to one `timing`+`op` pair, typing the event against the table's `Shape`.
2129
+ */
941
2130
  interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string, Validator>> {
942
2131
  afterDelete: (handler: TriggerHandler<TriggerDeleteEvent<Shape>>) => TriggerDefinition;
943
2132
  afterInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
@@ -946,89 +2135,6 @@ interface TriggerBuilder<Shape extends Record<string, Validator> = Record<string
946
2135
  beforeInsert: (handler: TriggerHandler<TriggerInsertEvent<Shape>>) => TriggerDefinition;
947
2136
  beforeUpdate: (handler: TriggerHandler<TriggerUpdateEvent<Shape>>) => TriggerDefinition;
948
2137
  }
949
- /**
950
- * Per-file metadata returned by {@link ReadOnlyStorage.getMetadata}. A clean
951
- * public mirror of `@lunora/storage`'s `ObjectMetadata` — re-declared here so
952
- * the ctx surface carries no dependency on the storage package's types. Matches
953
- * the columns Convex surfaces for `ctx.storage.getMetadata` / `_storage`.
954
- */
955
- interface StorageMetadata {
956
- /** The object's `Content-Type`, when recorded. */
957
- contentType?: string;
958
- /** Custom metadata set at upload time, if any. */
959
- customMetadata?: Record<string, string>;
960
- /** The object's key. */
961
- key: string;
962
- /** Hex-encoded SHA-256 of the body, when R2 carries a checksum. */
963
- sha256?: string;
964
- /** Body length in bytes. */
965
- size: number;
966
- /** When the object was last written (epoch ms), when reported. */
967
- uploaded?: number;
968
- }
969
- /**
970
- * Read-only projection of `Storage` exposed on `QueryCtx` / `MutationCtx`.
971
- *
972
- * Queries are pure reads, and mutations run inside a transactional scope —
973
- * neither is allowed to perform side-effectful R2 writes (`upload`) or
974
- * deletes (`delete`). They can, however, **read** existing objects and
975
- * resolve signed URLs (the URL signing itself is HMAC-only — no R2 round
976
- * trip), so the read-only surface keeps `download` and `getSignedUrl`. The
977
- * full {@link Storage} surface stays on `ActionCtx`.
978
- */
979
- interface ReadOnlyStorage<Buckets extends string = string> {
980
- /**
981
- * Select a named bucket (declared via `v.storage("name")`). The returned
982
- * accessor's operations target that bucket — `ctx.storage.bucket("avatars")
983
- * .download(key)`. The bare `ctx.storage` targets the default bucket.
984
- */
985
- bucket: (name: Buckets) => ReadOnlyStorage<Buckets>;
986
- /** The bucket this accessor's operations target (the default for the bare `ctx.storage`). */
987
- readonly bucketName: string;
988
- /** Fetch the body of an existing object. Returns `null` when absent. */
989
- download: (key: string) => Promise<ReadableStream | null>;
990
- /**
991
- * Read a file's metadata (size, content-type, sha256, upload time, custom
992
- * metadata) without fetching its body. Returns `null` when the object is
993
- * absent. Mirrors Convex's `ctx.storage.getMetadata`.
994
- */
995
- getMetadata: (key: string) => Promise<StorageMetadata | null>;
996
- /** Resolve a short-lived signed URL for an existing object. */
997
- getSignedUrl: (key: string, options?: {
998
- expiresInSeconds?: number;
999
- }) => Promise<string>;
1000
- /** Public URL pointing at the configured base for `key`. */
1001
- getUrl: (key: string) => string;
1002
- }
1003
- interface Storage<Buckets extends string = string> extends ReadOnlyStorage<Buckets> {
1004
- /** Select a named bucket; the returned accessor exposes the full read/write surface. */
1005
- bucket: (name: Buckets) => Storage<Buckets>;
1006
- delete: (key: string) => Promise<void>;
1007
- /**
1008
- * Mint a short-lived signed `PUT` URL a client can upload directly to,
1009
- * optionally pinning the `Content-Type` the uploader must send. Mirrors
1010
- * Convex's `storage.generateUploadUrl`.
1011
- */
1012
- generateUploadUrl: (key: string, options?: {
1013
- contentType?: string;
1014
- expiresInSeconds?: number;
1015
- }) => Promise<string>;
1016
- /**
1017
- * Upload `body` to `key` from the server, returning the stored object's key
1018
- * and etag. Mirrors Convex's `storage.store`. Accepts the same guard fields
1019
- * as `@lunora/storage`'s `UploadOptions` so `maxSize` /
1020
- * `allowedContentTypes` enforcement isn't lost behind the Convex-style alias.
1021
- */
1022
- store: (key: string, body: ReadableStream | ArrayBuffer | Blob, options?: {
1023
- allowedContentTypes?: ReadonlyArray<string>;
1024
- contentType?: string;
1025
- customMetadata?: Record<string, string>;
1026
- maxSize?: number;
1027
- }) => Promise<{
1028
- etag: string;
1029
- key: string;
1030
- }>;
1031
- }
1032
2138
  interface VectorMatch {
1033
2139
  id: string;
1034
2140
  metadata?: Record<string, unknown>;
@@ -1062,138 +2168,529 @@ interface VectorRecord {
1062
2168
  values: ReadonlyArray<number>;
1063
2169
  }
1064
2170
  /**
1065
- * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
1066
- * of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
1067
- */
1068
- interface VectorSearchReader {
1069
- getByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
1070
- query: (indexName: string, input: VectorQueryInput) => Promise<VectorMatches>;
1071
- }
1072
- /**
1073
- * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
1074
- * is queued post-commit by default; `upsertNow` forces a synchronous write.
1075
- * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
1076
- */
1077
- interface VectorSearch extends VectorSearchReader {
1078
- deleteByIds: (indexName: string, ids: ReadonlyArray<string>) => Promise<void>;
1079
- upsert: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1080
- upsertNow: (indexName: string, input: VectorUpsertInput) => Promise<void>;
1081
- }
1082
- /**
1083
- * Structured logger on every function `ctx`. Each call emits one attributed log
1084
- * line — tagged with the function path on the server — that flows to an
1085
- * `ObservabilitySink`'s `onLog` (where you route it in production) and, in
1086
- * development, to the dev server terminal via the CLI / Vite plugin formatter.
1087
- * Mirrors the `console` method names so it's a drop-in for `console.log` inside a
1088
- * handler, but with attribution and a routable transport.
1089
- *
1090
- * Accepts any number of values per call, exactly like `console`; objects are
1091
- * rendered into the human-readable message. The raw, un-rendered arguments are
1092
- * preserved ONLY on the in-process `onLog` sink (which you opt into and control);
1093
- * the rendered message — not the structured args — is what reaches the dev
1094
- * terminal and the platform's Workers Logs.
1095
- *
1096
- * Attribution follows the dispatched function: a log emitted inside an internal
1097
- * function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
1098
- * the outer request entrypoint, since the composed call reuses its context.
1099
- */
2171
+ * Read-only vector surface exposed on {@link QueryCtx}. Mirrors the read half
2172
+ * of `@lunora/bindings/vectors`' `LunoraVectors` so the live adapter is assignable.
2173
+ */
2174
+ interface VectorSearchReader<IndexName extends string = string> {
2175
+ getByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<ReadonlyArray<VectorRecord>>;
2176
+ query: (indexName: IndexName, input: VectorQueryInput) => Promise<VectorMatches>;
2177
+ }
2178
+ /**
2179
+ * Mutating vector surface on {@link MutationCtx} / {@link ActionCtx}. `upsert`
2180
+ * is queued post-commit by default; `upsertNow` forces a synchronous write.
2181
+ * `db.delete` on a vectorized table auto-propagates the matching `deleteByIds`.
2182
+ */
2183
+ interface VectorSearch<IndexName extends string = string> extends VectorSearchReader<IndexName> {
2184
+ deleteByIds: (indexName: IndexName, ids: ReadonlyArray<string>) => Promise<void>;
2185
+ upsert: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
2186
+ upsertNow: (indexName: IndexName, input: VectorUpsertInput) => Promise<void>;
2187
+ }
2188
+ /**
2189
+ * Structured, filterable key/value fields attached to a log line — the second
2190
+ * argument of a `ctx.log.<level>(message, fields)` call, or the fields bound by
2191
+ * `ctx.log.with(fields)`. They travel to an `ObservabilitySink`'s `onLog` and,
2192
+ * for a network sink, become OTLP log-record attributes a log pipeline (or the
2193
+ * Cloud log viewer) can filter and index on. Primitive values pass through;
2194
+ * objects/arrays are JSON-encoded at the sink boundary.
2195
+ */
2196
+ type LogFields = Record<string, unknown>;
2197
+ /**
2198
+ * One `ctx.log` severity method. Two call forms:
2199
+ *
2200
+ * - **Structured** — `ctx.log.info("order placed", { orderId, total })`: a message string plus a `fields` object. The fields are indexed as attributes.
2201
+ * - **Console-style** — `ctx.log.info("state", value, other)`: any number of values, joined into the display message exactly like `console.log`.
2202
+ *
2203
+ * The structured form is matched when the second argument is a plain object;
2204
+ * otherwise the call is treated as console-style, so existing `console`-shaped
2205
+ * calls keep working unchanged.
2206
+ */
2207
+ interface LunoraLogMethod {
2208
+ (message: string, fields?: LogFields): void;
2209
+ (...args: unknown[]): void;
2210
+ }
2211
+ /**
2212
+ * Structured logger on every function `ctx`. Each call emits one attributed log
2213
+ * line — tagged with the function path on the server — that flows to an
2214
+ * `ObservabilitySink`'s `onLog` (where you route it in production) and, in
2215
+ * development, to the dev server terminal via the CLI / Vite plugin formatter.
2216
+ * Mirrors the `console` method names so it's a drop-in for `console.log` inside a
2217
+ * handler, but with attribution, structured fields, and a routable transport.
2218
+ *
2219
+ * Six severities spanning the OpenTelemetry ramp: `trace`, `debug`, `info` (and
2220
+ * its `log` alias), `warn`, `error`, `fatal`.
2221
+ *
2222
+ * Two ways to attach structured {@link LogFields}: pass them per call
2223
+ * (`ctx.log.info(message, fields)`) or bind them once with {@link with} for a
2224
+ * child logger that stamps every line. The rendered `message` and the structured
2225
+ * `fields` reach the dev terminal and the platform's Workers Logs; the raw,
2226
+ * un-rendered console-style arguments are preserved ONLY on the in-process
2227
+ * `onLog` sink (which you opt into and control).
2228
+ *
2229
+ * Attribution follows the dispatched function: a log emitted inside an internal
2230
+ * function invoked via `ctx.runQuery`/`runMutation`/`runAction` is attributed to
2231
+ * the outer request entrypoint, since the composed call reuses its context.
2232
+ */
1100
2233
  interface LunoraLogger {
1101
- readonly debug: (...args: unknown[]) => void;
1102
- readonly error: (...args: unknown[]) => void;
1103
- readonly info: (...args: unknown[]) => void;
1104
- readonly log: (...args: unknown[]) => void;
1105
- readonly warn: (...args: unknown[]) => void;
2234
+ readonly debug: LunoraLogMethod;
2235
+ readonly error: LunoraLogMethod;
2236
+ /**
2237
+ * Emit a **structured event** instead of a log line — OpenTelemetry's Events
2238
+ * API, on the wire as `LogRecord.eventName` (plus an `event.name` attribute
2239
+ * for collectors predating that field).
2240
+ *
2241
+ * ```ts
2242
+ * ctx.log.event("checkout.completed", { plan: user.plan, total, currency });
2243
+ * ```
2244
+ *
2245
+ * The difference from `ctx.log.info("checkout completed", { … })` is what a
2246
+ * backend can do with it. A log line's payload is its message: prose, written
2247
+ * for a human, free to be reworded next sprint — so "how many checkouts
2248
+ * completed, by plan, this hour" degrades into a substring search over
2249
+ * English. An event's payload is its `fields` under a **stable name**, which a
2250
+ * collector can index, group, and alert on directly.
2251
+ *
2252
+ * Rule of thumb: `log.*` for narration you'd read while debugging, `event` for
2253
+ * anything you'd ever put on a dashboard. And for facts about the request as a
2254
+ * whole, prefer `ctx.span` — one wide event beats a dozen
2255
+ * events, however well named.
2256
+ */
2257
+ readonly event: (name: string, fields?: LogFields) => void;
2258
+ readonly fatal: LunoraLogMethod;
2259
+ readonly info: LunoraLogMethod;
2260
+ readonly log: LunoraLogMethod;
2261
+ readonly trace: LunoraLogMethod;
2262
+ readonly warn: LunoraLogMethod;
2263
+ /**
2264
+ * Return a child logger that stamps `fields` onto every line it emits,
2265
+ * merged under any per-call fields (per-call wins on a key clash). Chainable
2266
+ * — `ctx.log.with({ requestId }).with({ step })` accumulates both. Use it to
2267
+ * bind request-scoped context once instead of repeating it per call.
2268
+ */
2269
+ readonly with: (fields: LogFields) => LunoraLogger;
2270
+ }
2271
+ /**
2272
+ * Handle the enclosing `ctx.trace` span hands its body, so the body can attach
2273
+ * attributes only known *after* it resolves (an AI call's token usage / dollar
2274
+ * cost, a downstream status, a computed count). Declared structurally here to
2275
+ * mirror `shared/span-event.ts`'s `SpanHandle` and `@lunora/do`'s implementation;
2276
+ * a cross-package assignability guard in `@lunora/testing` fails the build if the
2277
+ * three drift apart. Start attributes are snapshotted before the body runs;
2278
+ * handle writes are merged over them at record time, post-hoc winning on a clash.
2279
+ */
2280
+ /**
2281
+ * One AI **evaluation** verdict — a scorer's `{name, score, label?}` — to attach
2282
+ * to a generation span via {@link SpanHandle.recordEvaluation}. Declared
2283
+ * structurally to mirror `shared/evaluation-attributes.ts`'s `EvaluationInput`
2284
+ * without a dependency edge.
2285
+ */
2286
+ interface SpanEvaluation {
2287
+ /** Optional categorical label (e.g. `"pass"` / `"fail"`), emitted as `.label`. */
2288
+ label?: string;
2289
+ /** Scorer name — the key's name segment; non-`[A-Za-z0-9._-]` chars become `_`. */
2290
+ name: string;
2291
+ /** Numeric score (typically `[0, 1]`), emitted as `.score`. */
2292
+ score: number;
2293
+ }
2294
+ interface SpanHandle {
2295
+ /**
2296
+ * Record a timestamped event on the enclosing span — a retry, a cache miss, a
2297
+ * state transition. Prefer this over an extra `ctx.log` line for anything that
2298
+ * only makes sense *relative to this span*: it rides the span's own export, so
2299
+ * it costs no additional record and can never be separated from its context.
2300
+ */
2301
+ addEvent: (name: string, attributes?: LogFields) => void;
2302
+ /**
2303
+ * Link this span to one in another trace — how a queue consumer points back at
2304
+ * the request that enqueued its message without collapsing every producer into
2305
+ * one giant trace.
2306
+ */
2307
+ addLink: (link: SpanLink) => void;
2308
+ /**
2309
+ * Attach an AI **evaluation** verdict to this (generation) span as the
2310
+ * `gen_ai.evaluation.<name>.score` / `.label` OpenTelemetry attributes, so a
2311
+ * scorer's grade rides the same trace as the generation it graded. Convenience
2312
+ * over {@link SpanHandle.setAttributes} that owns the key format; privacy-safe —
2313
+ * only the name, score, and optional label are emitted. Throws on an empty name
2314
+ * or a non-finite score.
2315
+ */
2316
+ recordEvaluation: (evaluation: SpanEvaluation) => void;
2317
+ /**
2318
+ * Record a **handled** exception as the OTel-conventional `exception` span
2319
+ * event (`exception.type` / `exception.message` / `exception.stacktrace`),
2320
+ * without marking the span failed.
2321
+ *
2322
+ * For an error you swallowed — a retried request, a fallback that worked. An
2323
+ * error that escapes the span body is recorded automatically and *does* set
2324
+ * the error status, so don't call this for one you're re-throwing.
2325
+ */
2326
+ recordException: (error: unknown) => void;
2327
+ /** Set one attribute on the enclosing span (merged at record time; post-hoc wins on key clash). */
2328
+ setAttribute: (key: string, value: LogFields[string]) => void;
2329
+ /** Merge attributes onto the enclosing span (post-hoc wins on key clash). */
2330
+ setAttributes: (fields: LogFields) => void;
2331
+ /**
2332
+ * The W3C ids of the span this handle refers to (32-hex trace, 16-hex span).
2333
+ *
2334
+ * On `ctx.span` these are the DISPATCH's ids — the trace the whole request
2335
+ * belongs to. Use it to echo a trace id back to a caller so a user can quote
2336
+ * it in a bug report, to build a `traceparent` for a hand-rolled outbound
2337
+ * call, or to parent a third-party library's spans onto this request.
2338
+ */
2339
+ spanContext: () => SpanContextIds;
2340
+ }
2341
+ /**
2342
+ * A span's W3C ids plus the trace's settled sampling verdict.
2343
+ *
2344
+ * `sampled` is the propagated head decision — absent means none reached this
2345
+ * tier, which every consumer reads as keep. It rides with the ids because
2346
+ * everything that announces this span downstream from them (a hand-built
2347
+ * `traceparent`, an `@opentelemetry/api` `SpanContext`) needs the flag in the
2348
+ * same breath: claiming SAMPLED on a trace that was sampled out leaves a
2349
+ * collector holding the middle of a trace nobody kept.
2350
+ */
2351
+ interface SpanContextIds {
2352
+ /** The trace's settled W3C `sampled` verdict; absent when none was propagated. */
2353
+ sampled?: boolean;
2354
+ /** This span's id (16-hex). */
2355
+ spanId: string;
2356
+ /** The trace this span belongs to (32-hex). */
2357
+ traceId: string;
2358
+ }
2359
+ /**
2360
+ * A causal reference to a span in another trace (OTel `Span.links`). Ids are
2361
+ * lowercase hex — 32 chars for `traceId`, 16 for `spanId`.
2362
+ */
2363
+ interface SpanLink {
2364
+ /** Attributes describing the relationship, e.g. `{ "link.kind": "enqueued_by" }`. */
2365
+ attributes?: LogFields;
2366
+ spanId: string;
2367
+ traceId: string;
2368
+ }
2369
+ /** OTel `SpanKind`. Drives a collector's service map — see {@link SpanOptions.kind}. */
2370
+ type SpanKind = "client" | "consumer" | "internal" | "producer" | "server";
2371
+ /** Options accepted by `ctx.trace(name, fn, options)` beyond a plain attribute bag. */
2372
+ interface SpanOptions {
2373
+ /** Start attributes, snapshotted before the body runs. */
2374
+ attributes?: LogFields;
2375
+ /**
2376
+ * OTel `SpanKind`, default `"internal"`. Set `"client"` for a call OUT to
2377
+ * another service and `"producer"`/`"consumer"` for queue hops: a collector
2378
+ * builds its service map from this, so leaving everything `"internal"` yields
2379
+ * a trace with no topology.
2380
+ */
2381
+ kind?: SpanKind;
2382
+ /** Links to spans in other traces, known at span start. */
2383
+ links?: SpanLink[];
2384
+ }
2385
+ /**
2386
+ * Span factory on every function `ctx`. Wraps a sub-operation so it becomes its
2387
+ * own **span** nested under the dispatch's RPC span, giving a trace real shape:
2388
+ * without it a slow request is one opaque bar, with it you see which part was
2389
+ * slow.
2390
+ *
2391
+ * ```ts
2392
+ * const charge = await ctx.trace("stripe.charge", () => stripe.charges.create(…), { orderId });
2393
+ * ```
2394
+ *
2395
+ * **Nesting is explicit.** The body receives a tracer bound to its own span;
2396
+ * calling *that* is what makes a child:
2397
+ *
2398
+ * ```ts
2399
+ * await ctx.trace("fulfil", async (trace) => {
2400
+ * // Children of "fulfil" — including under Promise.all, where an ambient
2401
+ * // "currently open span" would mis-record these as nested inside each other.
2402
+ * await Promise.all([trace("reserve.stock", …), trace("email.receipt", …)]);
2403
+ * });
2404
+ * ```
2405
+ *
2406
+ * Calling `ctx.trace` again inside a body (rather than the passed tracer) is not
2407
+ * an error — that span is simply parented to the dispatch instead of to the
2408
+ * enclosing span, which is flatter but never wrong.
2409
+ *
2410
+ * Spans share the dispatch's trace id with its `ctx.log` lines and any container
2411
+ * the handler calls (the same `traceparent` is propagated), so one trace spans
2412
+ * worker, shard, and container.
2413
+ *
2414
+ * The span is recorded when the body settles, and the body's value is returned
2415
+ * unchanged. A throw is recorded as an error span and then **re-thrown** — this
2416
+ * is instrumentation, never flow control. Recording is best-effort: a failing
2417
+ * sink can't turn a working handler into a broken one.
2418
+ *
2419
+ * **Post-hoc attributes.** The body also receives a {@link SpanHandle} as its
2420
+ * second argument. The `attributes` passed here are stamped at span start (and
2421
+ * snapshotted, so a later mutation can't rewrite them); anything the body sets
2422
+ * through the handle — `span.setAttribute(k, v)` / `span.setAttributes({…})` — is
2423
+ * merged over that snapshot when the span is recorded, so a value known only once
2424
+ * the body has resolved (an AI call's token usage / dollar cost, a computed
2425
+ * count) still lands on the span. Post-hoc wins on a key clash. The handle is a
2426
+ * trailing parameter, so every existing `(trace) => …` body keeps working
2427
+ * unchanged.
2428
+ * @param name Span name, e.g. `"stripe.charge"`. Prefer a low-cardinality name
2429
+ * and put the varying part in `attributes` — a name built from an id makes every
2430
+ * span its own group in a collector.
2431
+ * @param function_ The body to time, receiving a tracer bound to this span for any
2432
+ * nested spans and the enclosing span's {@link SpanHandle} for post-hoc
2433
+ * attributes. May be sync or async; the result is awaited.
2434
+ * @param attributes Either a plain attribute bag to stamp on the span at start
2435
+ * (normalized like a log line's `fields`), or a {@link SpanOptions} object when
2436
+ * you need `kind` or `links`. It is read as options only when *every* key is one
2437
+ * of `attributes`/`kind`/`links` AND a `kind`, if present, actually names a
2438
+ * {@link SpanKind}; `{ attributes: { kind: "premium" } }` is the explicit form if
2439
+ * your own attributes happen to be named that.
2440
+ * @param identity Adapter-only: record the span under ids the caller has ALREADY
2441
+ * published (see {@link SpanIdentity}). A handler never passes this — it exists
2442
+ * so the `@opentelemetry/api` bridge, which must hand a library a `SpanContext`
2443
+ * synchronously, is recorded under the id it handed out rather than a phantom.
2444
+ */
2445
+ type LunoraTracer = <T>(name: string, function_: (trace: LunoraTracer, span: SpanHandle) => Promise<T> | T, attributes?: LogFields | SpanOptions, identity?: SpanIdentity) => Promise<T>;
2446
+ /**
2447
+ * Caller-supplied ids for one `ctx.trace` span — the tracer's fourth argument.
2448
+ *
2449
+ * For adapters that must publish a span's identity BEFORE the body runs: the
2450
+ * `@opentelemetry/api` bridge returns a `SpanContext` synchronously from
2451
+ * `startSpan` and a library builds a `traceparent` from it, so the span has to be
2452
+ * recorded under the id already announced or every downstream span parents to an
2453
+ * id that never reaches the collector. `parentSpanId` lets such an adapter
2454
+ * express its own parent/child structure without an ambient span stack.
2455
+ *
2456
+ * Both ids are required: an adapter that has published one has published the
2457
+ * other, and `identity` is itself optional — omitting it, not passing a partial
2458
+ * object, is how a caller says "no adapter involved".
2459
+ */
2460
+ interface SpanIdentity {
2461
+ /** Parent to this span id instead of the enclosing `ctx.trace` / dispatch span. */
2462
+ parentSpanId: string;
2463
+ /** Record the span under this id (16-hex) instead of a freshly minted one. */
2464
+ spanId: string;
2465
+ }
2466
+ /**
2467
+ * `ctx.span` — a handle onto **this request's own span**, and with it the
2468
+ * wide-event API.
2469
+ *
2470
+ * ```ts
2471
+ * export const checkout = mutation({ handler: async (ctx, args) => {
2472
+ * ctx.span.setAttributes({ "user.plan": user.plan, "cart.items": cart.length });
2473
+ * const payment = await charge(cart);
2474
+ * ctx.span.setAttributes({ "payment.provider": payment.provider, "payment.total": payment.total });
2475
+ * if (payment.retried) ctx.span.addEvent("payment.retried", { attempts: payment.attempts });
2476
+ * return payment;
2477
+ * }});
2478
+ * ```
2479
+ *
2480
+ * **Why this instead of more log lines.** The usual way to make a handler
2481
+ * observable is to sprinkle `ctx.log.info` through it, which costs one record per
2482
+ * call, scatters one request's facts across a dozen rows, and forces every
2483
+ * question to be answered by correlating them back together. The wide-event
2484
+ * pattern inverts that: accumulate the facts as you learn them, and emit **one**
2485
+ * richly-attributed record per unit of work. Cost is flat — one span per request
2486
+ * no matter how much you attach — and every question ("p99 checkout latency for
2487
+ * pro-plan users with >10 items") becomes a single filter over one table instead
2488
+ * of a join across log lines.
2489
+ *
2490
+ * **This is plain OpenTelemetry, not a Lunora convention.** The attributes land
2491
+ * on the span the dispatch already emits, and are additionally exported as an
2492
+ * OTel Event record named `lunora.dispatch`, correlated by `trace_id`/`span_id`.
2493
+ * Any OTLP backend groups and aggregates them with no special configuration.
2494
+ *
2495
+ * **`span` vs `trace`.** `ctx.trace(name, fn)` creates a NEW child span to time a
2496
+ * sub-operation; `ctx.span` attaches to the one that already exists for the
2497
+ * request. Use `trace` for "how long did this part take", `span` for "what was
2498
+ * true about this request". Inside a `ctx.trace` body, the handle passed as the
2499
+ * body's second argument is that child span's equivalent of this.
2500
+ *
2501
+ * Attributes are normalized exactly like `ctx.log` fields, and recording is
2502
+ * best-effort — a telemetry failure never breaks the handler.
2503
+ */
2504
+ type LunoraWideEvent = SpanHandle;
2505
+ /**
2506
+ * Application metrics on every function `ctx` — the third signal alongside
2507
+ * `ctx.log` and `ctx.trace`. Each call records one measurement that flows to an
2508
+ * `ObservabilitySink`'s `onMetric`, and from `otlpSink` to a collector's
2509
+ * `/v1/metrics`.
2510
+ *
2511
+ * ```ts
2512
+ * ctx.metrics.count("orders.placed", 1, { plan: user.plan });
2513
+ * ctx.metrics.record("checkout.latency_ms", Date.now() - started);
2514
+ * ctx.metrics.gauge("cart.items", cart.items.length);
2515
+ * ```
2516
+ *
2517
+ * Pick the instrument by the question you want to answer: `count` for "how many"
2518
+ * (summed over time), `gauge` for "how many right now" (replaces the last
2519
+ * reading), `record` for "what's the distribution" (percentiles, not just a
2520
+ * mean).
2521
+ *
2522
+ * `attributes` are the metric's dimensions. Keep them **low-cardinality** — an
2523
+ * attribute valued by user id or order id creates a distinct time series per id,
2524
+ * which is how a metrics backend gets expensive. Put identifiers on a log line or
2525
+ * a span instead.
2526
+ *
2527
+ * No pre-aggregation happens: one call is one exported measurement, with counter
2528
+ * deltas for the collector to sum. In a hot loop, sum locally and record once
2529
+ * rather than calling per iteration.
2530
+ */
2531
+ interface LunoraMetrics {
2532
+ /**
2533
+ * Add to a monotonic counter (default `1`) — requests served, retries,
2534
+ * bytes sent. The collector sums successive deltas.
2535
+ */
2536
+ readonly count: (name: string, value?: number, attributes?: LogFields) => void;
2537
+ /**
2538
+ * Report a point-in-time reading that replaces the previous one — queue
2539
+ * depth, cache size, connections open.
2540
+ */
2541
+ readonly gauge: (name: string, value: number, attributes?: LogFields) => void;
2542
+ /**
2543
+ * Observe one sample of a distribution — latency, payload size. Use this,
2544
+ * not a counter, when percentiles matter.
2545
+ */
2546
+ readonly record: (name: string, value: number, attributes?: LogFields) => void;
1106
2547
  }
1107
2548
  interface QueryCtx {
1108
2549
  readonly auth: AuthState;
1109
2550
  readonly db: DatabaseReader;
1110
2551
  /**
1111
- * The validated, typed environment. Populated only when the project declares
1112
- * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1113
- * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1114
- * coercion-aware. Absent (optional) without a contract — declare
1115
- * `lunora/env.ts` to populate and type it.
1116
- */
2552
+ * The validated, typed environment. Populated only when the project declares
2553
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
2554
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
2555
+ * coercion-aware. Absent (optional) without a contract — declare
2556
+ * `lunora/env.ts` to populate and type it.
2557
+ */
1117
2558
  readonly env?: Record<string, unknown>;
1118
2559
  /**
1119
- * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1120
- * forwarded server-side (never read from a client header). `undefined` when
1121
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
1122
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1123
- */
2560
+ * The caller's IP for this request, or `undefined` when nothing trustworthy
2561
+ * says who called.
2562
+ *
2563
+ * Populated only from Cloudflare's `CF-Connecting-IP`, forwarded server-side,
2564
+ * and only while running ON Cloudflare — that is the one place the edge stamps
2565
+ * the header over anything the client sent. On any other host it is a header
2566
+ * the caller typed, so the runtime resolves nothing rather than hand a handler
2567
+ * an attacker-chosen address; a rate limit keyed on a forgeable `ip` is worse
2568
+ * than no limit, because it reads as enforced. `undefined` therefore covers: a
2569
+ * live-subscription re-run, a server-initiated dispatch, and ANY non-Cloudflare
2570
+ * host. A convenient rate-limit key for anonymous traffic on Cloudflare;
2571
+ * elsewhere, key on something the caller cannot choose.
2572
+ */
1124
2573
  readonly ip?: string;
1125
2574
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
1126
2575
  readonly log: LunoraLogger;
1127
2576
  /**
1128
- * Wall-clock time (epoch ms) the function began, captured once so the whole
1129
- * handler sees a single stable value. Query/mutation handlers must be
1130
- * deterministic — they may be re-run on OCC retry / subscription re-eval — so
1131
- * read time through `ctx.now` instead of `Date.now()` (the latter is flagged
1132
- * by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
1133
- */
2577
+ * Static metadata declared on this procedure with `.meta(...)`, merged
2578
+ * across calls and deep-frozen. Present so middleware can read the policy it
2579
+ * is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
2580
+ * hard-wired at each `.use()` site; absent when the procedure never called
2581
+ * `.meta()`.
2582
+ */
2583
+ readonly meta?: Readonly<Record<string, unknown>>;
2584
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
2585
+ readonly metrics: LunoraMetrics;
2586
+ /** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
2587
+ readonly newId: () => string;
2588
+ /**
2589
+ * Wall-clock time (epoch ms) the function began, captured once so the whole
2590
+ * handler sees a single stable value. Query/mutation handlers must be
2591
+ * deterministic — they may be re-run on subscription re-evaluation (an OCC
2592
+ * conflict surfaces as a `409` to the caller, not an internal retry) — so
2593
+ * read time through `ctx.now` instead of `Date.now()` (the latter is flagged
2594
+ * by the `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
2595
+ */
1134
2596
  readonly now: number;
1135
2597
  /**
1136
- * Compose a read-only subquery in-process, reusing this query's read
1137
- * context (same transaction, same `db`). Executes the referenced query's
1138
- * handler directly — no fresh DO RPC round-trip — so it observes the exact
1139
- * same snapshot. A query may only call other queries; there is no
1140
- * `runMutation` on a `QueryCtx` (writes are not allowed from a query).
1141
- * Mirrors Convex's `ctx.runQuery`.
1142
- */
1143
- readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
2598
+ * Compose a read-only subquery in-process, reusing this query's read
2599
+ * context (same transaction, same `db`). Executes the referenced query's
2600
+ * handler directly — no fresh DO RPC round-trip — so it observes the exact
2601
+ * same snapshot. A query may only call other queries; there is no
2602
+ * `runMutation` on a `QueryCtx` (writes are not allowed from a query).
2603
+ * Mirrors Convex's `ctx.runQuery`.
2604
+ */
2605
+ readonly runQuery: RunQuery;
1144
2606
  /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1145
2607
  readonly secrets: Secrets;
2608
+ /** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
2609
+ readonly span: LunoraWideEvent;
1146
2610
  readonly storage: ReadOnlyStorage;
2611
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
2612
+ readonly trace: LunoraTracer;
1147
2613
  readonly vectors: VectorSearchReader;
1148
2614
  }
1149
2615
  interface MutationCtx {
1150
2616
  readonly auth: AuthState;
1151
2617
  readonly db: DatabaseWriter;
1152
2618
  /**
1153
- * The validated, typed environment. Populated only when the project declares
1154
- * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1155
- * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1156
- * coercion-aware. Absent (optional) without a contract — declare
1157
- * `lunora/env.ts` to populate and type it.
1158
- */
2619
+ * The validated, typed environment. Populated only when the project declares
2620
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
2621
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
2622
+ * coercion-aware. Absent (optional) without a contract — declare
2623
+ * `lunora/env.ts` to populate and type it.
2624
+ */
1159
2625
  readonly env?: Record<string, unknown>;
1160
2626
  /**
1161
- * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1162
- * forwarded server-side (never read from a client header). `undefined` when
1163
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
1164
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1165
- */
2627
+ * The caller's IP for this request, or `undefined` when nothing trustworthy
2628
+ * says who called. Identical to {@link QueryCtx.ip} — see there for which
2629
+ * host populates it and why every other one deliberately does not.
2630
+ */
1166
2631
  readonly ip?: string;
1167
2632
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
1168
2633
  readonly log: LunoraLogger;
1169
2634
  /**
1170
- * Wall-clock time (epoch ms) the function began, captured once so the whole
1171
- * handler sees a single stable value. Mutation handlers must be deterministic
1172
- * — they may be re-run on OCC retry — so read time through `ctx.now` instead
1173
- * of `Date.now()` (the latter is flagged by the `nondeterministic_query_mutation`
1174
- * advisor). Actions may use `Date.now()`.
1175
- */
2635
+ * Static metadata declared on this procedure with `.meta(...)`, merged
2636
+ * across calls and deep-frozen. Present so middleware can read the policy it
2637
+ * is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
2638
+ * hard-wired at each `.use()` site; absent when the procedure never called
2639
+ * `.meta()`.
2640
+ */
2641
+ readonly meta?: Readonly<Record<string, unknown>>;
2642
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
2643
+ readonly metrics: LunoraMetrics;
2644
+ /** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
2645
+ readonly newId: () => string;
2646
+ /**
2647
+ * Wall-clock time (epoch ms) the function began, captured once so the whole
2648
+ * handler sees a single stable value. Mutation handlers must be deterministic
2649
+ * — an OCC conflict surfaces as a `409` to the caller rather than an internal
2650
+ * retry, but a caller's own retry is a fresh dispatch — so read time through
2651
+ * `ctx.now` instead of `Date.now()` (the latter is flagged by the
2652
+ * `nondeterministic_query_mutation` advisor). Actions may use `Date.now()`.
2653
+ */
1176
2654
  readonly now: number;
1177
2655
  /**
1178
- * Compose a submutation in-process, reusing this mutation's `db` writer.
1179
- * Executes the referenced mutation's handler directly — no fresh DO RPC —
1180
- * so its writes apply through the same shard invocation as the enclosing
1181
- * mutation. Note: writes are not wrapped in a SQL transaction, so a partial
1182
- * failure does not roll back earlier writes (the same as a top-level
1183
- * mutation). Mirrors Convex's `ctx.runMutation`.
1184
- */
1185
- readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
1186
- /**
1187
- * Compose a read-only subquery in-process, reusing this mutation's `db`.
1188
- * Executes the referenced query's handler directly — no fresh DO RPC — so
1189
- * it observes this mutation's in-flight writes. Mirrors Convex's
1190
- * `ctx.runQuery`.
1191
- */
1192
- readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
2656
+ * The origin (`scheme://host[:port]`) the request behind this dispatch reached
2657
+ * the worker on: the base for an absolute URL back to this app, such as a
2658
+ * signed storage URL, with no per-environment value to configure.
2659
+ *
2660
+ * The runtime reads it off the request URL, never off a client header, and
2661
+ * forwards it to the shard with the dispatch. `undefined` wherever no request
2662
+ * is behind the call: a scheduled or workflow run, a queue consumer, a
2663
+ * server-initiated dispatch. A query has no `origin` at all: a live query
2664
+ * re-runs with no request behind it, and its cached result is shared across
2665
+ * callers on every host, so a query that builds such a URL needs a configured
2666
+ * base.
2667
+ */
2668
+ readonly origin?: string;
2669
+ /**
2670
+ * Compose a submutation in-process, reusing this mutation's `db` writer.
2671
+ * Executes the referenced mutation's handler directly — no fresh DO RPC —
2672
+ * so its writes apply through the same shard invocation as the enclosing
2673
+ * mutation. The submutation joins the enclosing mutation's transaction
2674
+ * (SQLite in a Durable Object has no savepoints, so it rides the open span):
2675
+ * its writes commit or roll back together with the caller's, and a throw that
2676
+ * escapes the outer handler rolls back both. Mirrors Convex's `ctx.runMutation`.
2677
+ */
2678
+ readonly runMutation: RunMutation;
2679
+ /**
2680
+ * Compose a read-only subquery in-process, reusing this mutation's `db`.
2681
+ * Executes the referenced query's handler directly — no fresh DO RPC — so
2682
+ * it observes this mutation's in-flight writes. Mirrors Convex's
2683
+ * `ctx.runQuery`.
2684
+ */
2685
+ readonly runQuery: RunQuery;
1193
2686
  readonly scheduler: Scheduler;
1194
2687
  /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1195
2688
  readonly secrets: Secrets;
1196
- readonly storage: ReadOnlyStorage;
2689
+ /** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
2690
+ readonly span: LunoraWideEvent;
2691
+ readonly storage: MutationStorage;
2692
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
2693
+ readonly trace: LunoraTracer;
1197
2694
  readonly vectors: VectorSearch;
1198
2695
  /** Start / resume / inspect durable workflows; see {@link Workflows}. */
1199
2696
  readonly workflows: Workflows;
@@ -1201,52 +2698,71 @@ interface MutationCtx {
1201
2698
  interface ActionCtx {
1202
2699
  readonly auth: AuthState;
1203
2700
  /**
1204
- * Programmatic Workers Cache purge; see {@link CachePurge}.
1205
- * **Action-only** — actions run in the Worker, which has a `cache` binding.
1206
- * Queries and mutations run inside the Durable Object and do not expose this.
1207
- * Optional at runtime because Workers Cache is only present when enabled.
1208
- */
2701
+ * Programmatic Workers Cache purge; see {@link CachePurge}.
2702
+ *
2703
+ * **HTTP actions only.** It is the Worker that holds the `cache` binding, and
2704
+ * only `HttpActionCtx` is built there — an `action` reached over RPC runs
2705
+ * inside the Durable Object like a query or a mutation, so `ctx.cache` is
2706
+ * `undefined` for it. Declared here because `HttpActionCtx` is a `Pick` of
2707
+ * this interface. Optional because Workers Cache is only present when
2708
+ * enabled in `wrangler.jsonc`; always branch on it.
2709
+ */
1209
2710
  readonly cache?: CachePurge;
1210
2711
  readonly db: DatabaseWriter;
1211
2712
  /**
1212
- * The validated, typed environment. Populated only when the project declares
1213
- * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
1214
- * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
1215
- * coercion-aware. Absent (optional) without a contract — declare
1216
- * `lunora/env.ts` to populate and type it.
1217
- */
2713
+ * The validated, typed environment. Populated only when the project declares
2714
+ * a `defineEnv(...)` contract in `lunora/env.ts`; codegen then narrows this to
2715
+ * the validated `InferEnv` shape so `ctx.env.STRIPE_KEY` is parsed and
2716
+ * coercion-aware. Absent (optional) without a contract — declare
2717
+ * `lunora/env.ts` to populate and type it.
2718
+ */
1218
2719
  readonly env?: Record<string, unknown>;
1219
2720
  readonly fetch: typeof globalThis.fetch;
1220
2721
  /**
1221
- * The caller's IP for this request — Cloudflare's trusted `CF-Connecting-IP`,
1222
- * forwarded server-side (never read from a client header). `undefined` when
1223
- * unknown: a live-subscription re-run, a server-initiated dispatch, or
1224
- * non-Cloudflare hosting. A convenient rate-limit key for anonymous traffic.
1225
- */
2722
+ * The caller's IP for this request, or `undefined` when nothing trustworthy
2723
+ * says who called. Identical to {@link QueryCtx.ip} — see there for which
2724
+ * host populates it and why every other one deliberately does not.
2725
+ */
1226
2726
  readonly ip?: string;
1227
2727
  /** Structured, function-attributed logger; see {@link LunoraLogger}. */
1228
2728
  readonly log: LunoraLogger;
1229
2729
  /**
1230
- * Wall-clock time (epoch ms) the action began, captured once for convenience
1231
- * and parity with query/mutation `ctx.now`. Actions run exactly once, so they
1232
- * may also use ambient `Date.now()` freely.
1233
- */
2730
+ * Static metadata declared on this procedure with `.meta(...)`, merged
2731
+ * across calls and deep-frozen. Present so middleware can read the policy it
2732
+ * is meant to enforce (`ctx.meta.rateLimit`, …) instead of having it
2733
+ * hard-wired at each `.use()` site; absent when the procedure never called
2734
+ * `.meta()`.
2735
+ */
2736
+ readonly meta?: Readonly<Record<string, unknown>>;
2737
+ /** Application counters, gauges, and histograms; see {@link LunoraMetrics}. */
2738
+ readonly metrics: LunoraMetrics;
2739
+ /** `ctx.newId()`: see `createStableIdFactory`. Derived, not random, when the call carries a replay key. */
2740
+ readonly newId: () => string;
2741
+ /**
2742
+ * Wall-clock time (epoch ms) the action began, captured once for convenience
2743
+ * and parity with query/mutation `ctx.now`. Actions run exactly once, so they
2744
+ * may also use ambient `Date.now()` freely.
2745
+ */
1234
2746
  readonly now: number;
1235
- readonly runAction: <A extends ArgsValidator, R>(reference: RegisteredAction<A, R>, args: InferArgs<A>) => Promise<R>;
1236
- readonly runMutation: <A extends ArgsValidator, R>(reference: RegisteredMutation<A, R>, args: InferArgs<A>) => Promise<R>;
1237
- readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
2747
+ /**
2748
+ * The origin the request behind this dispatch reached the worker on, or
2749
+ * `undefined` when no request is behind it. Identical to
2750
+ * {@link MutationCtx.origin}.
2751
+ */
2752
+ readonly origin?: string;
2753
+ readonly runAction: RunAction;
2754
+ readonly runMutation: RunMutation;
2755
+ readonly runQuery: RunQuery;
1238
2756
  readonly scheduler: Scheduler;
1239
2757
  /** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
1240
2758
  readonly secrets: Secrets;
2759
+ /** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
2760
+ readonly span: LunoraWideEvent;
1241
2761
  readonly storage: Storage;
2762
+ /** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
2763
+ readonly trace: LunoraTracer;
1242
2764
  readonly vectors: VectorSearch;
1243
2765
  /** Start / resume / inspect durable workflows; see {@link Workflows}. */
1244
2766
  readonly workflows: Workflows;
1245
2767
  }
1246
- /**
1247
- * Stand-in returned by codegen so projects can `import { api } from "./_generated/api"`.
1248
- * The runtime value is opaque; the types are filled in by generated declarations.
1249
- */
1250
- type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
1251
- declare const anyApi: AnyApi;
1252
- export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, 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 Secrets, type SecretsStoreSecretLike, 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 };
2768
+ export type { ActionCtx, AggregateIndexDefinition, AggregateOp, ArgsValidator, AuthState, CachePurge, DatabaseReader, DatabaseWriter, DurableObjectJurisdiction, DurableStreamOptions, ExposeConfig, ExternalSourceCursor, ExternalSourceDefinition, ExternalSourceMode, ExternalSourceRefresh, FunctionKind, FunctionVisibility, GeoBoundingBox, GeoFilterBuilder, GeoIndexDefinition, GeoPointInput, GlobalBackend, IndexDefinition, IndexRangeBuilder, InferArgs, LifecycleEvent, LifecycleEventKind, LogFields, LunoraLogMethod, LunoraLogger, LunoraMetrics, LunoraTracer, LunoraWideEvent, MutationCtx, MutationStorage, OnDeleteAction, PaginationOptions, PaginationResult, QueryCtx, RankIndexDefinition, RankSortKey, ReadOnlyStorage, RegisteredAction, RegisteredFunction, RegisteredLifecycleHook, RegisteredMutation, RegisteredQuery, RegisteredStream, RelatedDirection, RelatedNode, RelatedOptions, RelatedPage, RelatedStart, RelatedStartReference, RelationDefinition, RestCacheConfig, RetryPolicy, RunQueryOptions, ScheduledFunctionDoc, ScheduledJob, Scheduler, Schema, SearchFilterBuilder, SearchIndexDefinition, SearchLanguage, SearchStrategy, Secrets, SecretsStoreSecretLike, ShardInitEvent, ShardMode, SpanContextIds, SpanEvaluation, SpanHandle, SpanIdentity, SpanKind, SpanLink, SpanOptions, Storage, StorageListOptions, StorageListResult, StorageMetadata, StorageMultipartUpload, StorageMultipartUploadOptions, StorageObjectBody, StorageObjectHead, StorageRange, StorageUploadBody, StorageUploadOptions, StorageUploadResult, StorageUploadedPart, SystemDatabaseReader, SystemDoc, SystemQuery, SystemTableName, TableDefinition, TableReader, TableVectorIndex, TriggerAggregateOptions, TriggerBuilder, TriggerCtx, TriggerDatabase, TriggerDefinition, TriggerDeleteEvent, TriggerEvent, TriggerGroupByEntry, TriggerGroupByOptions, TriggerHandler, TriggerInsertEvent, TriggerOp, TriggerQueryArgs, TriggerQueryPage, TriggerRankOptions, TriggerRankPageOptions, TriggerRankResult, TriggerRow, TriggerTiming, TriggerUpdateEvent, TtlDefinition, VectorEmbedder, VectorIndexDefinition, VectorMatch, VectorMatches, VectorMetric, VectorQueryInput, VectorRecord, VectorSearch, VectorSearchReader, VectorUpsertInput, WhisperEvent, WorkflowCreateOptions, WorkflowEventDefinition, WorkflowHandle, WorkflowInstance, WorkflowInstanceEvent, WorkflowInstanceStatus, WorkflowStatusResult, Workflows, X402ProcedureConfig };