@lunora/server 1.0.0-alpha.23 → 1.0.0-alpha.230

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