@lunora/runtime 0.0.0 → 1.0.0-alpha.10

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.
@@ -0,0 +1,2426 @@
1
+ import { RankDirection, RankPageRow, DatabaseWriterLike } from '@lunora/do';
2
+ export type { RankDirection as RankPageDirection, RankPageRowKey as RankPageKey, RankPageRow, ShardRankPageResult } from '@lunora/do';
3
+ import { WorkflowsRestClient } from '@lunora/workflow';
4
+ /**
5
+ * Turn-key incremental-sync source helpers for warehouse connectors
6
+ * (Fivetran custom functions, Airbyte incremental sources).
7
+ *
8
+ * The runtime's admin `/_lunora/admin/connector/sync` endpoint returns a
9
+ * {@link ConnectorSyncPage}: a flat list of change records since an opaque
10
+ * cursor, a `nextCursor` to resume from, and a `hasMore` flag. These helpers
11
+ * reshape that page into the response envelopes the two ecosystems expect, so a
12
+ * connector wrapper stays a few lines.
13
+ *
14
+ * {@link toFivetranResponse} produces the `{ state, insert, update, delete,
15
+ * hasMore, schema }` object a Fivetran connector function returns from its
16
+ * handler. {@link toAirbyteMessages} produces an ordered array of Airbyte
17
+ * protocol messages (a `RECORD` per row, a trailing `STATE` carrying the cursor),
18
+ * the line-delimited stream an Airbyte incremental source emits.
19
+ *
20
+ * Both consume the SAME page, so a single endpoint feeds either ecosystem.
21
+ */
22
+ /**
23
+ * One change record in a {@link ConnectorSyncPage}. Mirrors a row of the CDC log
24
+ * the shard / D1 change feed produces: an `op` (insert / update / delete), the
25
+ * owning `table`, and the document. `op` is normalised to the three warehouse
26
+ * verbs; an unknown / absent op is treated as `"upsert"` (insert-or-update),
27
+ * which is the safe default for change feeds that don't distinguish the two.
28
+ */
29
+ interface ConnectorChange {
30
+ /** The full document. For a delete, may carry only the primary key. */
31
+ doc: Record<string, unknown>;
32
+ /** Change verb. `upsert` collapses insert+update for feeds that don't separate them. */
33
+ op: "delete" | "insert" | "update" | "upsert";
34
+ /** Source table this change belongs to. */
35
+ table: string;
36
+ }
37
+ /**
38
+ * A page of changes the connector endpoint returns. `nextCursor` is an opaque
39
+ * token the consumer stores and re-posts verbatim to resume; never parse it.
40
+ * `hasMore` is `true` while the source has further pages past this one — keep
41
+ * paging until it is `false` (caught up).
42
+ */
43
+ interface ConnectorSyncPage {
44
+ changes: ReadonlyArray<ConnectorChange>;
45
+ hasMore: boolean;
46
+ /** Opaque resume token. Treat as a black box; store and re-send unchanged. */
47
+ nextCursor: string;
48
+ }
49
+ /**
50
+ * Fivetran connector-function response envelope. A Fivetran custom function
51
+ * returns this object: `state` is persisted by Fivetran and handed back on the
52
+ * next sync (map it straight to {@link ConnectorSyncPage.nextCursor}), the
53
+ * `insert` / `update` / `delete` maps bucket records per table, `hasMore` drives
54
+ * Fivetran's "call me again immediately" loop, and `schema` declares each table's
55
+ * primary key.
56
+ *
57
+ * See https://fivetran.com/docs/connectors/functions#responseformat.
58
+ */
59
+ interface FivetranResponse {
60
+ delete: Record<string, Record<string, unknown>[]>;
61
+ hasMore: boolean;
62
+ insert: Record<string, Record<string, unknown>[]>;
63
+ schema: Record<string, {
64
+ primary_key: string[];
65
+ }>;
66
+ state: {
67
+ cursor: string;
68
+ };
69
+ update: Record<string, Record<string, unknown>[]>;
70
+ }
71
+ /** One Airbyte protocol message (a `RECORD` row or a `STATE` checkpoint). */
72
+ type AirbyteMessage = {
73
+ record: {
74
+ data: Record<string, unknown>;
75
+ emitted_at: number;
76
+ stream: string;
77
+ };
78
+ type: "RECORD";
79
+ } | {
80
+ state: {
81
+ data: {
82
+ cursor: string;
83
+ };
84
+ };
85
+ type: "STATE";
86
+ };
87
+ /**
88
+ * Format a {@link ConnectorSyncPage} as a Fivetran connector-function response.
89
+ *
90
+ * Inserts and upserts both land in `insert` (Fivetran upserts on primary key, so
91
+ * an insert and an update of an existing row are wire-identical); explicit
92
+ * updates land in `update`; deletes in `delete`. `state.cursor` carries the
93
+ * opaque resume token Fivetran will echo back on the next invocation.
94
+ * @param page the page returned by the connector sync endpoint.
95
+ * @param primaryKey the primary-key column per table (default `"_id"`); pass a
96
+ * map to override per table, used to fill the `schema` block.
97
+ */
98
+ declare const toFivetranResponse: (page: ConnectorSyncPage, primaryKey?: Record<string, string> | string) => FivetranResponse;
99
+ /**
100
+ * Format a {@link ConnectorSyncPage} as an ordered array of Airbyte protocol
101
+ * messages: one `RECORD` per change (stream = table name), followed by a single
102
+ * trailing `STATE` message carrying the opaque cursor. An Airbyte source serializes
103
+ * these as line-delimited JSON to stdout.
104
+ *
105
+ * Airbyte's protocol has no native delete verb in `RECORD`; a delete is emitted
106
+ * as a `RECORD` with a `_lunora_deleted: true` marker on the row so a downstream
107
+ * normalization / dbt step can tombstone it. Callers needing true CDC deletes
108
+ * should run Airbyte's CDC-deletion handling on that marker.
109
+ * @param page the page returned by the connector sync endpoint.
110
+ * @param emittedAt epoch-ms stamped on each `RECORD` (default `Date.now()`).
111
+ */
112
+ declare const toAirbyteMessages: (page: ConnectorSyncPage, emittedAt?: number) => AirbyteMessage[];
113
+ /**
114
+ * The subset of the Cloudflare `ExecutionContext` the Lunora worker entry and
115
+ * the framework mount seams rely on — `waitUntil` for fire-and-forget work that
116
+ * must outlive the response, and `passThroughOnException` for the top-level
117
+ * error posture.
118
+ *
119
+ * It is deliberately **not** a package. `@lunora/runtime` is the leaf server
120
+ * runtime and `@lunora/nuxt` is a framework integration that intentionally does
121
+ * not depend on `@lunora/runtime`'s worker types, yet both need this exact
122
+ * shape: the runtime to build/forward the worker `fetch`, Nuxt to forward an
123
+ * inbound request to the user's composed worker. Each imports this file by
124
+ * relative path and the bundler (packem/rollup) inlines it: no runtime
125
+ * dependency edge is created, the helper is duplicated only in emitted output,
126
+ * never in source. One source of truth, zero deps. See AGENTS.md → "Top-level
127
+ * `shared/` — bundler-inlined source".
128
+ *
129
+ * Both methods are **optional**: a real Cloudflare `ExecutionContext` always
130
+ * supplies them, but a host that mounts Lunora as a sub-handler (Nitro/H3, a
131
+ * non-Cloudflare preview, a unit test) may hand over a partial context or none
132
+ * at all. Callers therefore invoke them defensively (`ctx.waitUntil?.(…)`) or
133
+ * fall back to {@link NOOP_EXECUTION_CONTEXT}.
134
+ */
135
+ interface ExecutionContextLike {
136
+ passThroughOnException?: () => void;
137
+ waitUntil?: (promise: Promise<unknown>) => void;
138
+ }
139
+ /**
140
+ * No-op `ExecutionContext` used when the host runtime didn't supply one (a
141
+ * non-Cloudflare preview, or a unit test), so the worker's `fetch` always
142
+ * receives a valid third argument.
143
+ */
144
+ declare const NOOP_EXECUTION_CONTEXT: ExecutionContextLike;
145
+ /** A timestamp as better-auth stores it: epoch-ms, an ISO string, or absent. */
146
+ type AuthTimestamp = null | number | string;
147
+ /**
148
+ * One authenticated user, as the auth browser surfaces it. Mirrors better-auth's
149
+ * `user` row plus the `admin()` plugin columns (`role`/`banned`/…); the index
150
+ * signature additionally carries any app-defined `user.additionalFields`.
151
+ */
152
+ interface AuthUser {
153
+ [key: string]: unknown;
154
+ banExpires?: AuthTimestamp;
155
+ banned?: boolean | null;
156
+ banReason?: null | string;
157
+ createdAt?: AuthTimestamp;
158
+ email?: null | string;
159
+ emailVerified?: boolean | null;
160
+ id: string;
161
+ image?: null | string;
162
+ name?: null | string;
163
+ role?: null | string;
164
+ }
165
+ /** One auth session, as the auth browser surfaces it. Mirrors better-auth's `session` row. */
166
+ interface AuthSession {
167
+ [key: string]: unknown;
168
+ createdAt?: AuthTimestamp;
169
+ expiresAt?: AuthTimestamp;
170
+ id: string;
171
+ impersonatedBy?: null | string;
172
+ ipAddress?: null | string;
173
+ userAgent?: null | string;
174
+ userId: string;
175
+ }
176
+ /** A page of users or sessions plus the total count, for paginated browsing. */
177
+ interface AuthPage<T> {
178
+ rows: T[];
179
+ total: number;
180
+ }
181
+ /** The result of {@link AuthAdmin.impersonateUser}: a session token to act as the target user. */
182
+ interface AuthImpersonation {
183
+ expiresAt?: AuthTimestamp;
184
+ token: string;
185
+ user: AuthUser;
186
+ }
187
+ /**
188
+ * Which admin surfaces the configured auth plane supports, derived from the
189
+ * enabled better-auth plugins. The studio renders only the panels whose
190
+ * capability is `true`.
191
+ */
192
+ interface AuthCapabilities {
193
+ accounts: boolean;
194
+ admin: boolean;
195
+ organization: boolean;
196
+ passkey: boolean;
197
+ twoFactor: boolean;
198
+ }
199
+ /** Filtering / paging options forwarded to {@link AuthAdmin.listUsers} from the users endpoint's query string. */
200
+ interface ListAuthUsersOptions {
201
+ filterField?: string;
202
+ filterValue?: string;
203
+ limit?: number;
204
+ offset?: number;
205
+ search?: string;
206
+ searchField?: string;
207
+ sortBy?: string;
208
+ sortDirection?: "asc" | "desc";
209
+ }
210
+ /**
211
+ * The auth user-management plane backing the studio's auth dashboard. The host
212
+ * wires this to better-auth (typically via `@lunora/auth`'s `createAuthAdmin`);
213
+ * the runtime stays free of a hard dependency on `@lunora/auth`. The read
214
+ * methods back the GET browse endpoints; the optional mutations back the
215
+ * admin-gated POST endpoints — a host that only needs read-only browsing can
216
+ * omit them (the POST routes then respond `AUTH_OP_NOT_SUPPORTED`). Omit the
217
+ * whole option and every `/auth/*` endpoint responds `AUTH_NOT_CONFIGURED`.
218
+ *
219
+ * Every method here runs behind the worker's `LUNORA_ADMIN_TOKEN` gate — the
220
+ * implementation is a trusted server-side operator, not an end-user API.
221
+ */
222
+ interface AuthAdmin {
223
+ banUser?: (input: {
224
+ expiresInSeconds?: number;
225
+ reason?: string;
226
+ userId: string;
227
+ }) => Promise<AuthUser>;
228
+ cancelInvitation?: (input: {
229
+ invitationId: string;
230
+ }) => Promise<void>;
231
+ capabilities?: () => Promise<AuthCapabilities>;
232
+ createUser?: (input: {
233
+ data?: Record<string, unknown>;
234
+ email: string;
235
+ name: string;
236
+ password?: string;
237
+ role?: string | string[];
238
+ }) => Promise<AuthUser>;
239
+ deletePasskey?: (input: {
240
+ passkeyId: string;
241
+ }) => Promise<void>;
242
+ disableTwoFactor?: (input: {
243
+ userId: string;
244
+ }) => Promise<void>;
245
+ impersonateUser?: (input: {
246
+ userId: string;
247
+ }) => Promise<AuthImpersonation>;
248
+ listAccounts?: (input: {
249
+ userId: string;
250
+ }) => Promise<Record<string, unknown>[]>;
251
+ listInvitations?: (options: {
252
+ limit?: number;
253
+ offset?: number;
254
+ organizationId: string;
255
+ }) => Promise<AuthPage<Record<string, unknown>>>;
256
+ listMembers?: (options: {
257
+ limit?: number;
258
+ offset?: number;
259
+ organizationId: string;
260
+ }) => Promise<AuthPage<Record<string, unknown>>>;
261
+ listOrganizations?: (options: {
262
+ limit?: number;
263
+ offset?: number;
264
+ }) => Promise<AuthPage<Record<string, unknown>>>;
265
+ listPasskeys?: (input: {
266
+ userId: string;
267
+ }) => Promise<Record<string, unknown>[]>;
268
+ listSessions: (options: {
269
+ limit?: number;
270
+ offset?: number;
271
+ userId?: string;
272
+ }) => Promise<AuthPage<AuthSession>>;
273
+ listUsers: (options: ListAuthUsersOptions) => Promise<AuthPage<AuthUser>>;
274
+ removeMember?: (input: {
275
+ memberId: string;
276
+ }) => Promise<void>;
277
+ removeUser?: (input: {
278
+ userId: string;
279
+ }) => Promise<void>;
280
+ revokeUserSession?: (input: {
281
+ sessionId: string;
282
+ }) => Promise<void>;
283
+ revokeUserSessions?: (input: {
284
+ userId: string;
285
+ }) => Promise<void>;
286
+ setRole?: (input: {
287
+ role: string | string[];
288
+ userId: string;
289
+ }) => Promise<AuthUser>;
290
+ setUserPassword?: (input: {
291
+ newPassword: string;
292
+ userId: string;
293
+ }) => Promise<void>;
294
+ unbanUser?: (input: {
295
+ userId: string;
296
+ }) => Promise<AuthUser>;
297
+ unlinkAccount?: (input: {
298
+ accountId: string;
299
+ userId: string;
300
+ }) => Promise<void>;
301
+ updateUser?: (input: {
302
+ data: Record<string, unknown>;
303
+ userId: string;
304
+ }) => Promise<AuthUser>;
305
+ }
306
+ /**
307
+ * Read-only subset of {@link AuthAdmin}, kept as an alias for the former
308
+ * `authIntrospector` option (which the worker still honours as a browse-only
309
+ * fallback). Prefer wiring `authAdmin` with `@lunora/auth`'s `createAuthAdmin`
310
+ * so the mutation endpoints light up too.
311
+ */
312
+ type AuthIntrospector = Pick<AuthAdmin, "listSessions" | "listUsers">;
313
+ /** Closure-scoped worker helpers the auth routes borrow (so this module stays out of the worker's god-closure). */
314
+ /**
315
+ * A compact, transport-safe description of one function argument — the runtime
316
+ * read of a `v.*` validator's reflection tags (`kind` + `_meta`). The runtime
317
+ * deliberately avoids a hard dependency on `@lunora/values`, so this reads the
318
+ * validator structurally rather than importing its types.
319
+ */
320
+ interface FunctionArgumentDescriptor {
321
+ /** Element validator kind for an `array` arg (one level), e.g. `string`. */
322
+ element?: string;
323
+ /** The (optional-unwrapped) validator kind, e.g. `string`, `id`, `object`. */
324
+ kind: string;
325
+ /** The argument name. */
326
+ name: string;
327
+ /** True when the arg is wrapped in `v.optional(...)`. */
328
+ optional: boolean;
329
+ /** Target table for an `id` arg (`v.id("table")`). */
330
+ table?: string;
331
+ }
332
+ /**
333
+ * Describe one named argument from its validator. Unwraps a single `v.optional`
334
+ * layer (marking the arg optional and reporting the inner kind), and surfaces
335
+ * the two most useful per-kind details: an `id` arg's target table and an
336
+ * `array` arg's element kind. Nested object/union shapes report their top-level
337
+ * kind only — enough for a signature view without a deep recursive walk.
338
+ */
339
+ /**
340
+ * Observability hooks for the Lunora runtime.
341
+ *
342
+ * A user-supplied {@link ObservabilitySink} receives one event per dispatched
343
+ * RPC (single-shard forward or fan-out). The runtime is otherwise oblivious
344
+ * to where the telemetry goes — adapters that forward to Cloudflare Analytics
345
+ * Engine, OTLP-over-HTTP, Sentry, or stdout all implement the same shape.
346
+ *
347
+ * Failure model: the sink callback is wrapped in a try/catch so a faulty
348
+ * adapter never breaks user-facing RPC dispatch. Errors thrown from inside
349
+ * the sink are swallowed (they would otherwise replace a useful user-visible
350
+ * error with a telemetry-pipeline failure).
351
+ */
352
+ /**
353
+ * Per-RPC dispatch event. Single-shard calls set `shardKey`; cross-shard
354
+ * fan-outs set `fanOut` with the table being aggregated, shard count, and
355
+ * per-shard failure count.
356
+ */
357
+ interface ObservabilityEvent {
358
+ /** Wall-clock duration of the dispatch, in milliseconds. */
359
+ durationMs: number;
360
+ /**
361
+ * Populated on `ok === false`. `code`/`status` mirror the LunoraError
362
+ * taxonomy; `message` is the human-readable string (may include user
363
+ * input — sinks that ship to third parties should scrub it).
364
+ */
365
+ error?: {
366
+ code: string;
367
+ message: string;
368
+ status: number;
369
+ };
370
+ /**
371
+ * Populated for fan-out dispatches.
372
+ * `shards` is the total fan-out cardinality; `failed` counts shards that
373
+ * timed out or returned an error (the same `errors[]` the response body
374
+ * carries to the caller).
375
+ */
376
+ fanOut?: {
377
+ failed: number;
378
+ shards: number;
379
+ table: string;
380
+ };
381
+ /** Function path being invoked, e.g. `"messages:list"`. */
382
+ functionPath: string;
383
+ /** True when the dispatch completed without throwing. */
384
+ ok: boolean;
385
+ /** Shard key for single-shard calls; absent for fan-outs. */
386
+ shardKey?: string;
387
+ }
388
+ /** Severity of a {@link LogEvent}, mirroring the usual console levels. */
389
+ type LogLevel = "debug" | "error" | "info" | "log" | "warn";
390
+ /**
391
+ * One application log line emitted from a function handler via `ctx.log`.
392
+ *
393
+ * Unlike {@link ObservabilityEvent} (one summary per dispatch), a `LogEvent`
394
+ * is produced for each `ctx.log.*` call, carrying the human-readable `message`
395
+ * (the args joined for display) plus the structured `args` array for sinks that
396
+ * want the raw values. `functionPath` attributes the line to the handler that
397
+ * emitted it; `shardKey`/`userId` mirror the dispatch context.
398
+ *
399
+ * This is how `ctx.log` reaches a destination in production: wire a sink's
400
+ * {@link ObservabilitySink.onLog} and route it wherever you ship logs. In dev
401
+ * the runtime also emits these to `console` so the CLI / Vite plugin can format
402
+ * them in the terminal.
403
+ */
404
+ interface LogEvent {
405
+ /** Raw arguments passed to the `ctx.log.*` call, in order. */
406
+ args: unknown[];
407
+ /** Function path that emitted the line, e.g. `"messages:list"`. */
408
+ functionPath: string;
409
+ /** Severity the line was logged at. */
410
+ level: LogLevel;
411
+ /** Display string — the args rendered and space-joined. */
412
+ message: string;
413
+ /** Shard key for single-shard calls; absent for the unnamed root DO. */
414
+ shardKey?: string;
415
+ /** Wall-clock millis when the line was emitted. */
416
+ ts: number;
417
+ /** Acting userId, or absent when anonymous. */
418
+ userId?: string;
419
+ }
420
+ /**
421
+ * Per-event context handed to a sink alongside the event. Lets a sink register
422
+ * background work (e.g. a telemetry POST) with the request's `ctx.waitUntil` so
423
+ * it survives isolate teardown after the response returns. Absent (`undefined`
424
+ * `waitUntil`) on paths with no request context (e.g. the in-process
425
+ * `serverQuery` fast-path), where the sink falls back to fire-and-forget.
426
+ */
427
+ interface ObservabilitySinkContext {
428
+ /** Keep a background promise alive past the response (the request's `ctx.waitUntil`). */
429
+ waitUntil?: (promise: Promise<unknown>) => void;
430
+ }
431
+ /**
432
+ * The hook contract. Methods are optional so a sink can opt into only the
433
+ * events it cares about; the runtime no-ops the others.
434
+ */
435
+ interface ObservabilitySink {
436
+ /** Invoked once per `ctx.log.*` call from a function handler. */
437
+ onLog?: (event: LogEvent, context?: ObservabilitySinkContext) => void;
438
+ /** Invoked once per dispatched RPC (single-shard or fan-out). */
439
+ onRpc?: (event: ObservabilityEvent, context?: ObservabilitySinkContext) => void;
440
+ }
441
+ /**
442
+ * Invoke `sink.onRpc` with the given event, swallowing any error the sink
443
+ * throws. Use at the dispatch boundary; the runtime should never see a
444
+ * sink-originating throw bubble up past this point. `context.waitUntil`, when
445
+ * supplied, lets a network sink keep its send alive past the response.
446
+ */
447
+ declare const emitRpcEvent: (sink: ObservabilitySink | undefined, event: ObservabilityEvent, context?: ObservabilitySinkContext) => void;
448
+ /**
449
+ * Invoke `sink.onLog` with the given log event, swallowing any error the sink
450
+ * throws. The same failure model as {@link emitRpcEvent}: a buggy log sink must
451
+ * never break the handler that emitted the line.
452
+ */
453
+ declare const emitLogEvent: (sink: ObservabilitySink | undefined, event: LogEvent, context?: ObservabilitySinkContext) => void;
454
+ /**
455
+ * Cloudflare Durable Object jurisdictions restrict where a DO runs and persists
456
+ * data, for data-residency / compliance regimes (GDPR, FedRAMP, US data
457
+ * residency). The set is open — Cloudflare adds values over time — so this is a
458
+ * widening union rather than a closed enum.
459
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
460
+ */
461
+ type DurableObjectJurisdiction = "eu" | "fedramp" | "us";
462
+ /**
463
+ * Structural projection of the bits of `DurableObjectNamespace` the runtime
464
+ * needs. Real workers-types defines a much wider surface; this lets us pass
465
+ * unit-test doubles without coupling to `@cloudflare/workers-types`.
466
+ */
467
+ interface ShardNamespaceLike {
468
+ get: (id: unknown) => {
469
+ fetch: (request: Request) => Promise<Response>;
470
+ };
471
+ /**
472
+ * `getByName` is the friendlier API but isn't on every workers-types
473
+ * release yet. We prefer it when available and fall back to
474
+ * `idFromName` + `get` for compatibility.
475
+ */
476
+ getByName?: (name: string) => {
477
+ fetch: (request: Request) => Promise<Response>;
478
+ };
479
+ idFromName: (name: string) => unknown;
480
+ /**
481
+ * Derive a jurisdiction-restricted subnamespace. Every ID and stub created
482
+ * from the returned namespace is pinned to `jurisdiction`. Optional because
483
+ * older workers-types releases (and unit-test doubles) may not expose it;
484
+ * {@link applyJurisdiction} fails closed when a jurisdiction is requested
485
+ * but this method is absent.
486
+ */
487
+ jurisdiction?: (jurisdiction: DurableObjectJurisdiction) => ShardNamespaceLike;
488
+ }
489
+ interface ResolvedShard {
490
+ fetch: (request: Request) => Promise<Response>;
491
+ }
492
+ /**
493
+ * Return a jurisdiction-restricted view of `namespace`, or `namespace`
494
+ * unchanged when no jurisdiction is configured.
495
+ *
496
+ * Fail-closed: if a jurisdiction is requested but the binding does not expose
497
+ * `.jurisdiction()` (an older workers-types, or a misconfigured test double),
498
+ * this throws rather than silently routing to the un-pinned global namespace —
499
+ * silently dropping a residency constraint would let data land outside the
500
+ * compliance boundary the caller asked for.
501
+ */
502
+ declare const applyJurisdiction: (namespace: ShardNamespaceLike, jurisdiction?: DurableObjectJurisdiction) => ShardNamespaceLike;
503
+ /** Look up a shard stub by name, preferring `getByName` when present. */
504
+ declare const resolveShard: (namespace: ShardNamespaceLike, shardKey: string) => ResolvedShard;
505
+ /**
506
+ * Source of "which shard keys exist for a given table right now". Returning
507
+ * an empty array is valid — the coordinator will respond with the merge
508
+ * strategy's identity (empty array for `concat`, `0` for `sum`, etc.).
509
+ */
510
+ interface ShardRegistry {
511
+ listShardKeys: (table: string) => Promise<ReadonlyArray<string>> | ReadonlyArray<string>;
512
+ }
513
+ /**
514
+ * Static-map implementation. Useful for tests and for small deployments
515
+ * where shard keys are known up front (e.g. a fixed set of channel IDs).
516
+ */
517
+ declare const createStaticShardRegistry: (table_to_keys: Readonly<Record<string, ReadonlyArray<string>>>) => ShardRegistry;
518
+ /**
519
+ * Wire-serializable merge strategy. `topK.by` is a field name on the row
520
+ * (the runtime looks it up with a string key), not a closure.
521
+ *
522
+ * Aggregate-friendly variants for cross-shard `count` / `aggregate` /
523
+ * `groupBy` fan-outs:
524
+ *
525
+ * - `sum` — `count(*)`, `aggregate({ op: "sum" })` (sums numeric per-shard payloads).
526
+ * - `max` — `aggregate({ op: "max" })`.
527
+ * - `min` — `aggregate({ op: "min" })`.
528
+ * - `groupBy` — per-shard `GroupByEntry[]` payloads, reduced into one
529
+ * entry per distinct key tuple. `op` controls how values combine across
530
+ * shards: `sum` (default — works for `COUNT(*)` and `SUM`), `max`, `min`.
531
+ *
532
+ * `avg` is intentionally absent in v1 — a correct cross-shard average
533
+ * requires shipping `(sum, count)` per shard, not the post-shard mean.
534
+ * Use two separate fan-outs (`sum` + `count`) and divide in the caller.
535
+ *
536
+ * `rank` — cross-shard `rank()` over a partition that spans shards (e.g. a
537
+ * global leaderboard `.shardBy("userId")` with `rankIndex(partitionBy: [])`).
538
+ * Each shard's `__lunora_admin__:rankBefore` returns `{before, total}` (its
539
+ * local rows strictly-before the explicit key, plus its local partition
540
+ * total); the merge sums them into `{position: Σbefore + 1, total: Σtotal}` —
541
+ * the 1-based global position and global partition size.
542
+ */
543
+ type MergeStrategy = {
544
+ kind: "concat";
545
+ } | {
546
+ by: string;
547
+ direction?: "asc" | "desc";
548
+ k: number;
549
+ kind: "topK";
550
+ } | {
551
+ kind: "first";
552
+ } | {
553
+ kind: "max";
554
+ } | {
555
+ kind: "min";
556
+ } | {
557
+ kind: "rank";
558
+ } | {
559
+ kind: "sum";
560
+ } | {
561
+ kind: "groupBy";
562
+ op?: "max" | "min" | "sum";
563
+ };
564
+ /**
565
+ * Convenience: build the right wire-serializable {@link MergeStrategy} for a
566
+ * given aggregate read. The reader doesn't know which op the caller chose, so
567
+ * a fan-out wrapper passes the user's op + by-keys through this to derive the
568
+ * merge.
569
+ *
570
+ * - `count` → `sum`.
571
+ * - `aggregate({ op })` → `sum`/`max`/`min` (or throws for `avg`).
572
+ * - `groupBy({ by, agg })` → `groupBy({ op })` (defaults to `sum` since
573
+ * `groupBy`'s default reducer is `count`).
574
+ * @returns the derived {@link MergeStrategy}.
575
+ */
576
+ declare const mergeStrategyForAggregate: (input: {
577
+ agg?: {
578
+ op?: "avg" | "count" | "max" | "min" | "sum";
579
+ };
580
+ kind: "groupBy";
581
+ } | {
582
+ kind: "count";
583
+ } | {
584
+ kind: "scalar";
585
+ op: "avg" | "count" | "max" | "min" | "sum";
586
+ }) => MergeStrategy;
587
+ interface FanOutSpec {
588
+ merge: MergeStrategy;
589
+ /** Table whose shard keys drive the fan-out. */
590
+ table: string;
591
+ }
592
+ /**
593
+ * Per-shard failure surfaced in the aggregate response's `errors` field. We
594
+ * never throw out of `fanOut` — slow/failed shards are *data*, not an
595
+ * exception, so callers can decide whether to retry or surface a partial
596
+ * UI.
597
+ */
598
+ interface ShardError {
599
+ /** Human-readable; tests assert on `.includes("timeout")` and similar. */
600
+ message: string;
601
+ shardKey: string;
602
+ /** Set when the per-shard timeout fired. */
603
+ timedOut: boolean;
604
+ }
605
+ interface FanOutResult<T = unknown> {
606
+ /** Merged value — type depends on the merge strategy. */
607
+ data: T;
608
+ errors: ReadonlyArray<ShardError>;
609
+ /** Shards that failed or timed out. */
610
+ failed: number;
611
+ /** Shards that returned successfully. */
612
+ ok: number;
613
+ }
614
+ interface QueryCoordinatorOptions {
615
+ /**
616
+ * Maximum number of shard RPCs to issue in parallel. Defaults to 16 —
617
+ * keeps the 30-second Worker CPU budget healthy when fanning out to
618
+ * dozens of shards and avoids stampeding the DO namespace.
619
+ */
620
+ maxConcurrency?: number;
621
+ /**
622
+ * Hard per-shard timeout in milliseconds. Defaults to 5000; a slow
623
+ * shard surfaces in `errors[]` rather than stalling the aggregate.
624
+ */
625
+ perShardTimeoutMs?: number;
626
+ /** Required — drives which shards to fan out to. */
627
+ registry: ShardRegistry;
628
+ }
629
+ interface FanOutRequest {
630
+ args?: Record<string, unknown>;
631
+ fanOut: FanOutSpec;
632
+ functionPath: string;
633
+ /** Forwarded to each shard fetch (auth, cookies, bookmark). */
634
+ headers?: Record<string, string>;
635
+ }
636
+ /**
637
+ * Cross-shard migration request. Unlike {@link FanOutRequest} there is no merge
638
+ * strategy — per-shard payloads are `MigrationRunResult`-shaped objects, not
639
+ * rows, so {@link QueryCoordinator.orchestrateMigration} rolls them up with the
640
+ * fixed semantics documented on {@link MigrationFanOutResult}.
641
+ *
642
+ * `functionPath` is the admin RPC to invoke on each shard
643
+ * (`__lunora_admin__:runMigration` or `:migrationStatus`); `headers` must carry
644
+ * the `Authorization` bearer header the shard's admin gate requires (the
645
+ * configured admin token), or every shard comes back as a 403 error.
646
+ */
647
+ interface MigrationFanOutRequest {
648
+ args?: Record<string, unknown>;
649
+ functionPath: string;
650
+ headers?: Record<string, string>;
651
+ /** Table whose live shard keys the migration runs across. */
652
+ table: string;
653
+ }
654
+ /** One shard's outcome: either the unwrapped admin `result` payload, or an error. */
655
+ interface ShardMigrationOutcome {
656
+ error?: {
657
+ message: string;
658
+ timedOut: boolean;
659
+ };
660
+ /** The shard's admin `result`, peeled out of the `{ result }` envelope. */
661
+ result?: unknown;
662
+ shardKey: string;
663
+ }
664
+ interface MigrationFanOutResult {
665
+ /** Summed `changed` across shards whose result carried a numeric count. */
666
+ changed: number;
667
+ /** Shards that errored or timed out. */
668
+ failed: number;
669
+ /** Shards that returned a 2xx result. */
670
+ ok: number;
671
+ /** Summed `processed` across shards whose result carried a numeric count. */
672
+ processed: number;
673
+ /** Per-shard outcomes, in registry order. */
674
+ shards: ReadonlyArray<ShardMigrationOutcome>;
675
+ /**
676
+ * Rolled-up status. `"failed"` if any shard's runner reported failure;
677
+ * `"in_progress"` if any shard is incomplete or unreachable (the run stays
678
+ * resumable); `"completed"` only when every shard finished cleanly.
679
+ */
680
+ status: "completed" | "failed" | "in_progress";
681
+ }
682
+ /**
683
+ * Cross-shard rank request. Like {@link MigrationFanOutRequest} there is no
684
+ * caller-supplied merge — per-shard payloads are `{before, total}` objects, so
685
+ * {@link QueryCoordinator.orchestrateRank} rolls them up with the fixed
686
+ * `{position: Σbefore + 1, total: Σtotal}` semantics {@link mergeRank} defines.
687
+ *
688
+ * The key tuple (`partitionKey`/`sortValues`/`rowId`) is built off the row doc
689
+ * via `@lunora/do`'s `rankKeyFromDoc(index, doc)` and forwarded verbatim to
690
+ * each shard's `__lunora_admin__:rankBefore` admin RPC; `headers` must carry
691
+ * the admin bearer the shard's admin gate requires.
692
+ */
693
+ interface RankFanOutRequest {
694
+ headers?: Record<string, string>;
695
+ /** Rank index name on `table`. */
696
+ index: string;
697
+ /** Canonical-JSON partition tuple — `encodePartitionKey(index.partitionBy, doc)`. */
698
+ partitionKey: string;
699
+ /** The `__id__` tiebreak value — `doc._id`. */
700
+ rowId: string;
701
+ /** Serialized sort-key values in `index.sortBy` order, as produced by `rankKeyFromDoc` (wire-safe + byte-matching the stored columns). */
702
+ sortValues: ReadonlyArray<unknown>;
703
+ /** Table whose live shard keys the rank fans out across. */
704
+ table: string;
705
+ }
706
+ interface RankFanOutResult {
707
+ /** Shards that errored or timed out. */
708
+ failed: number;
709
+ /** Shards that returned a 2xx `{before, total}`. */
710
+ ok: number;
711
+ /** `true` when at least one shard failed/timed out, so `position`/`total` are under-counts (failed shards' rows missing). A caller needing an exact global rank should treat this as an error, not trust the numbers. */
712
+ partial: boolean;
713
+ /** 1-based global position within the partition (`Σbefore + 1`). */
714
+ position: number;
715
+ /** Per-shard outcomes, in registry order. */
716
+ shards: ReadonlyArray<ShardRankOutcome>;
717
+ /** Global partition total (`Σtotal`). */
718
+ total: number;
719
+ }
720
+ /** One shard's rank outcome: its `{before, total}` payload, or an error. */
721
+ interface ShardRankOutcome {
722
+ error?: {
723
+ message: string;
724
+ timedOut: boolean;
725
+ };
726
+ result?: {
727
+ before: number;
728
+ total: number;
729
+ };
730
+ shardKey: string;
731
+ }
732
+ /**
733
+ * Cross-shard ranked-pagination request. Like {@link RankFanOutRequest} there's
734
+ * no caller-supplied merge — the merge is the fixed k-way merge by the rank-key
735
+ * tuple. `take` is the global page size; `cursor` is the opaque composite cursor
736
+ * from the prior page's `continueCursor` (absent → first page). `partitionKey`,
737
+ * when set, pins a single partition (`encodePartitionKey(index.partitionBy, where)`),
738
+ * forwarded so each shard scopes its local slice to that partition.
739
+ *
740
+ * `directions` is the per-sort-key direction list (`index.sortBy[i].direction`)
741
+ * the coordinator's comparator needs to break ties the same way each shard's
742
+ * `ORDER BY` does. `partitionKey` and the `__id__` tiebreak are always ascending
743
+ * (matching the shard companion's btree), so only the sort columns vary.
744
+ */
745
+ interface RankPageFanOutRequest {
746
+ /** Opaque composite cursor from the prior page's `continueCursor`. */
747
+ cursor?: null | string;
748
+ /** Per-sort-key directions, in `index.sortBy` order. Missing/short → ascending. */
749
+ directions?: ReadonlyArray<RankDirection>;
750
+ headers?: Record<string, string>;
751
+ /** Rank index name on `table`. */
752
+ index: string;
753
+ /** Optional partition pin forwarded to each shard's local `rankPage`. */
754
+ partitionKey?: string;
755
+ /** Table whose live shard keys the page fans out across. */
756
+ table: string;
757
+ /** Global page size; defaults to 100, capped at 1000 (matching the shard-local `rankPage`). */
758
+ take?: number;
759
+ }
760
+ /** One shard's `rankPage` outcome: its local ranked slice, or an error. */
761
+ interface ShardRankPageOutcome {
762
+ /** The directions the shard ordered by (`index.sortBy[i].direction`); authoritative for the merge. */
763
+ directions?: ReadonlyArray<RankDirection>;
764
+ error?: {
765
+ message: string;
766
+ timedOut: boolean;
767
+ };
768
+ hasMore?: boolean;
769
+ rows?: ReadonlyArray<RankPageRow>;
770
+ shardKey: string;
771
+ }
772
+ interface RankPageFanOutResult {
773
+ /** Opaque composite cursor for the next page, or `null` when the merge is exhausted. */
774
+ continueCursor: null | string;
775
+ /** Shards that errored or timed out. */
776
+ failed: number;
777
+ /** `true` when the global merge has no further rows. */
778
+ isDone: boolean;
779
+ /** Shards that returned a 2xx slice. */
780
+ ok: number;
781
+ /** The globally-ranked page of hydrated docs, in cross-shard rank order. */
782
+ page: ReadonlyArray<Record<string, unknown>>;
783
+ /** `true` when at least one shard failed/timed out, so the page may be missing that shard's rows. */
784
+ partial: boolean;
785
+ /** Per-shard outcomes, in registry order. */
786
+ shards: ReadonlyArray<ShardRankPageOutcome>;
787
+ }
788
+ interface QueryCoordinator {
789
+ fanOut: <T = unknown>(namespace: ShardNamespaceLike, request: FanOutRequest) => Promise<FanOutResult<T>>;
790
+ /**
791
+ * Fan the `__lunora_admin__:applyCdc` admin RPC out by forwarding each
792
+ * pre-bucketed per-shard batch of CDC changes, rolling up the applied/failed
793
+ * counts. The replay half of point-in-time recovery.
794
+ */
795
+ orchestrateApplyCdc: (namespace: ShardNamespaceLike, request: ApplyCdcFanOutRequest) => Promise<ApplyCdcFanOutResult>;
796
+ /**
797
+ * Fan the `__lunora_admin__:cdcSync` admin RPC out to every live shard,
798
+ * each resumed from its own cursor in `request.cursors` (shardKey → seq).
799
+ * Returns the per-shard change pages plus their new cursors so the caller
800
+ * can checkpoint each shard independently — the streaming-export feed.
801
+ */
802
+ orchestrateCdcSync: (namespace: ShardNamespaceLike, request: CdcSyncFanOutRequest) => Promise<CdcSyncFanOutResult>;
803
+ /**
804
+ * Fan an export admin RPC out to every live shard, returning the
805
+ * per-shard `{rows}` payloads alongside any per-shard errors. Each shard
806
+ * returns a JSON envelope (not a streaming body) so this method is the
807
+ * collector — the worker assembles the NDJSON stream.
808
+ */
809
+ orchestrateExport: (namespace: ShardNamespaceLike, request: ExportFanOutRequest) => Promise<ExportFanOutResult>;
810
+ /**
811
+ * Fan an import admin RPC out by routing each row to its owning shard. The
812
+ * shard registry resolves which shards exist; rows whose table has a
813
+ * `shardBy(field)` are bucketed using that field's value as the shard key,
814
+ * other tables fall back to the runtime's default `__root__` shard.
815
+ */
816
+ orchestrateImport: (namespace: ShardNamespaceLike, request: ImportFanOutRequest) => Promise<ImportFanOutResult>;
817
+ /** Fan a migration admin RPC out to every live shard of a table and roll up the per-shard outcomes. */
818
+ orchestrateMigration: (namespace: ShardNamespaceLike, request: MigrationFanOutRequest) => Promise<MigrationFanOutResult>;
819
+ /**
820
+ * Fan the `__lunora_admin__:rankBefore` admin RPC out to every live shard of
821
+ * a table and roll up the per-shard `{before, total}` payloads into the
822
+ * global rank (`{position: Σbefore + 1, total: Σtotal}`). The cross-shard
823
+ * `rank()` path for a partition that spans shards.
824
+ */
825
+ orchestrateRank: (namespace: ShardNamespaceLike, request: RankFanOutRequest) => Promise<RankFanOutResult>;
826
+ /**
827
+ * Page a ranked query across every live shard of a `.shardBy(...)` table.
828
+ * Fans `__lunora_admin__:rankPage` out to each shard, gathers each shard's
829
+ * local ranked slice (rows tagged with their rank-key tuple), and k-way
830
+ * merges them by that tuple into one globally-ranked page of `take` rows.
831
+ * The opaque `continueCursor` is a composite of per-shard cursors so the
832
+ * next page resumes each shard strictly-after the last row the global page
833
+ * consumed from it — pages never drop or duplicate a row at a shard
834
+ * boundary. The cross-shard `rankPage()` path (PLAN5 §7.1 / PLAN2 #3).
835
+ */
836
+ orchestrateRankPage: (namespace: ShardNamespaceLike, request: RankPageFanOutRequest) => Promise<RankPageFanOutResult>;
837
+ /**
838
+ * Fan the `__lunora_admin__:getMetrics` admin RPC out to every live shard of
839
+ * a table and collect each shard's lifetime `requests` total into a per-shard
840
+ * `{ shardKey, requests }` distribution. The feed the studio's `hot_shard`
841
+ * advisor lint needs: a single shard's snapshot can't reveal cross-shard
842
+ * skew, so this fans the cheap metrics read out and returns the whole shard
843
+ * set's request volumes (a failed shard surfaces as `requests: 0`).
844
+ */
845
+ orchestrateShardTraffic: (namespace: ShardNamespaceLike, request: ShardTrafficFanOutRequest) => Promise<ShardTrafficFanOutResult>;
846
+ readonly registry: ShardRegistry;
847
+ }
848
+ /**
849
+ * Cross-shard export request. `tables` is the union of every table the caller
850
+ * wants exported (shard-local **or** global); `headers` carries the admin
851
+ * bearer the per-shard gate expects. Shard registries are queried for the
852
+ * complete set of live shards across all listed shard-local tables.
853
+ */
854
+ interface ExportFanOutRequest {
855
+ args?: Record<string, unknown>;
856
+ headers?: Record<string, string>;
857
+ /**
858
+ * Tables driving the fan-out. Shards are derived from the union of each
859
+ * table's live shard keys — so an export of `["users","messages"]` reaches
860
+ * every shard that holds either table. Globals are skipped here; the
861
+ * worker reads them from D1 directly.
862
+ */
863
+ tables: ReadonlyArray<string>;
864
+ }
865
+ /** Per-shard export outcome. */
866
+ interface ShardExportOutcome {
867
+ error?: {
868
+ message: string;
869
+ timedOut: boolean;
870
+ };
871
+ /** Rows from this shard, or undefined when an error occurred. */
872
+ rows?: ReadonlyArray<{
873
+ doc: Record<string, unknown>;
874
+ table: string;
875
+ }>;
876
+ shardKey: string;
877
+ }
878
+ interface ExportFanOutResult {
879
+ failed: number;
880
+ ok: number;
881
+ shards: ReadonlyArray<ShardExportOutcome>;
882
+ }
883
+ /**
884
+ * Cross-shard change-data-capture request. `tables` drives shard discovery (the
885
+ * union of their live shard keys, like export); `cursors` maps each shard key
886
+ * to the `seq` it was last read through (absent → from the beginning). `limit`
887
+ * caps each shard's page.
888
+ */
889
+ interface CdcSyncFanOutRequest {
890
+ cursors?: Record<string, number>;
891
+ headers?: Record<string, string>;
892
+ limit?: number;
893
+ tables: ReadonlyArray<string>;
894
+ }
895
+ /** Per-shard CDC page: the changes plus the new cursor to resume this shard from. */
896
+ interface ShardCdcOutcome {
897
+ changes?: ReadonlyArray<Record<string, unknown>>;
898
+ /** New per-shard cursor; on error it echoes the shard's prior cursor so a retry resumes cleanly. */
899
+ cursor: number;
900
+ error?: {
901
+ message: string;
902
+ timedOut: boolean;
903
+ };
904
+ shardKey: string;
905
+ }
906
+ interface CdcSyncFanOutResult {
907
+ failed: number;
908
+ ok: number;
909
+ shards: ReadonlyArray<ShardCdcOutcome>;
910
+ }
911
+ /**
912
+ * Cross-shard import request. Rows have already been bucketed by the runtime
913
+ * into one batch per shard key — the coordinator's job is to forward each
914
+ * batch and roll up the per-shard insert counts + errors.
915
+ */
916
+ interface ImportFanOutRequest {
917
+ /**
918
+ * Per-shard batches keyed by shard key. Each entry will be POSTed as the
919
+ * `rows` arg of `__lunora_admin__:importShard`. The shard's
920
+ * starting-line-number for error attribution is carried in `startLine`.
921
+ */
922
+ batches: ReadonlyArray<{
923
+ rows: ReadonlyArray<{
924
+ doc: Record<string, unknown>;
925
+ table: string;
926
+ }>;
927
+ shardKey: string;
928
+ startLine?: number;
929
+ }>;
930
+ headers?: Record<string, string>;
931
+ }
932
+ interface ShardImportOutcome {
933
+ error?: {
934
+ message: string;
935
+ timedOut: boolean;
936
+ };
937
+ result?: {
938
+ conflicts: number;
939
+ errors: ReadonlyArray<{
940
+ code: string;
941
+ line: number;
942
+ message: string;
943
+ table: string;
944
+ }>;
945
+ inserted: Record<string, number>;
946
+ };
947
+ shardKey: string;
948
+ }
949
+ interface ImportFanOutResult {
950
+ /** Total conflicts (skipped `_id`s) across shards. */
951
+ conflicts: number;
952
+ /** Errors merged across all per-shard outcomes. */
953
+ errors: ReadonlyArray<{
954
+ code: string;
955
+ line: number;
956
+ message: string;
957
+ table: string;
958
+ }>;
959
+ failed: number;
960
+ /** Per-table summed insert counts. */
961
+ inserted: Record<string, number>;
962
+ ok: number;
963
+ shards: ReadonlyArray<ShardImportOutcome>;
964
+ }
965
+ /**
966
+ * Cross-shard CDC replay request (point-in-time recovery). Changes are
967
+ * pre-bucketed by the runtime into one batch per shard key — the coordinator
968
+ * forwards each batch to `__lunora_admin__:applyCdc` and rolls up the counts.
969
+ */
970
+ interface ApplyCdcFanOutRequest {
971
+ batches: ReadonlyArray<{
972
+ changes: ReadonlyArray<Record<string, unknown>>;
973
+ shardKey: string;
974
+ }>;
975
+ headers?: Record<string, string>;
976
+ }
977
+ interface ApplyCdcFanOutResult {
978
+ /** Total changes applied across shards. */
979
+ applied: number;
980
+ failed: number;
981
+ ok: number;
982
+ }
983
+ /**
984
+ * Cross-shard traffic request. Like {@link MigrationFanOutRequest} there is no
985
+ * caller-supplied merge — each shard's `__lunora_admin__:getMetrics` payload
986
+ * carries its own lifetime `requests` total, and {@link rollUpShardTraffic}
987
+ * collects them into one `{ shardKey, requests }` entry per shard. `headers`
988
+ * must carry the admin bearer the per-shard `getMetrics` gate requires.
989
+ *
990
+ * `table` drives shard discovery: the registry's live shard keys for the table
991
+ * are the shards fanned out to. This is the feed the studio's `hot_shard`
992
+ * runtime advisor consumes to compute cross-shard skew — a single shard's
993
+ * snapshot can't, so the panel fans this out on demand.
994
+ */
995
+ interface ShardTrafficFanOutRequest {
996
+ headers?: Record<string, string>;
997
+ /** Table whose live shard keys the traffic fan-out runs across. */
998
+ table: string;
999
+ }
1000
+ /** One shard's traffic total, mirroring the advisor's `AdvisorShardTraffic` (sans the optional `group`). */
1001
+ interface ShardTrafficEntry {
1002
+ /** Lifetime request count read off the shard's `getMetrics` snapshot; `0` for a shard that failed/timed out. */
1003
+ requests: number;
1004
+ /** The shard key (the DO id name); `""` for the unnamed root shard. */
1005
+ shardKey: string;
1006
+ }
1007
+ interface ShardTrafficFanOutResult {
1008
+ /** Shards that errored or timed out (their `requests` are reported as `0`). */
1009
+ failed: number;
1010
+ /** Shards that returned a 2xx `getMetrics` snapshot. */
1011
+ ok: number;
1012
+ /**
1013
+ * Per-shard request totals, in registry order. Shaped to plug straight into
1014
+ * the advisor's `LintContext.shardTraffic` so the `hot_shard` lint can
1015
+ * compute the cross-shard share. A failed shard still appears (with
1016
+ * `requests: 0`) so callers see the full shard set.
1017
+ */
1018
+ shards: ReadonlyArray<ShardTrafficEntry>;
1019
+ }
1020
+ declare const createQueryCoordinator: (options: QueryCoordinatorOptions) => QueryCoordinator;
1021
+ /**
1022
+ * Secure-by-default HTTP edge for the Lunora worker.
1023
+ *
1024
+ * The worker's top-level `fetch` (see `./create-worker`) is the single choke
1025
+ * point every response passes through — RPC, auth, admin, `httpRoute` handlers,
1026
+ * and the SSR fallback alike. This module supplies what is applied there:
1027
+ * `decorateResponse` adds baseline security headers plus, for allowed
1028
+ * cross-origin requests, the matching `Access-Control-Allow-*` headers (never
1029
+ * overwriting a header the inner handler set); `handleCorsPreflight` answers
1030
+ * `OPTIONS` preflights for allowlisted origins; `enforceOrigin` is a CSRF guard
1031
+ * that rejects state-changing, cookie-authenticated requests from untrusted
1032
+ * origins.
1033
+ *
1034
+ * Every layer is on by default and individually disable-able through the
1035
+ * `SecurityOptions` passed to `createWorker`. Resolution (`resolveSecurity`) is
1036
+ * pure and platform-agnostic — it touches only the global `Request`/`Response`/
1037
+ * `Headers`/`URL`, so it unit-tests under plain Node without workerd.
1038
+ */
1039
+ /** Per-header overrides for {@link SecurityHeadersOptions}. `false` omits the header. */
1040
+ interface SecurityHeadersOptions {
1041
+ /**
1042
+ * `Content-Security-Policy`. Omitted (`undefined`) applies a restrictive
1043
+ * default to **non-HTML** responses only, so an SSR page is never broken by
1044
+ * a policy it didn't opt into. Pass a string to apply that policy to every
1045
+ * response (HTML included); `false` to never send one.
1046
+ */
1047
+ csp?: string | false;
1048
+ /** `X-Frame-Options` (clickjacking). Defaults to `SAMEORIGIN`. `false` omits it. */
1049
+ frameOptions?: "DENY" | "SAMEORIGIN" | false;
1050
+ /** `Strict-Transport-Security`. Only ever sent over HTTPS. `false` omits it. */
1051
+ hsts?: boolean | {
1052
+ includeSubDomains?: boolean;
1053
+ maxAge?: number;
1054
+ preload?: boolean;
1055
+ };
1056
+ /** `Permissions-Policy`. Defaults to a minimal deny list. `false` omits it. */
1057
+ permissionsPolicy?: string | false;
1058
+ /** `Referrer-Policy`. Defaults to `strict-origin-when-cross-origin`. `false` omits it. */
1059
+ referrerPolicy?: string | false;
1060
+ }
1061
+ /** CORS allowlist. Cross-origin is denied unless an origin matches. */
1062
+ interface CorsOptions {
1063
+ /** Echo `Access-Control-Allow-Credentials: true`. Incompatible with a `*` allowlist. */
1064
+ allowCredentials?: boolean;
1065
+ /** Request headers permitted on the actual request (preflight `Allow-Headers`). */
1066
+ allowedHeaders?: string[];
1067
+ /** Methods permitted cross-origin (preflight `Allow-Methods`). */
1068
+ allowedMethods?: string[];
1069
+ /** Allowed origins — an explicit list (`"*"` permitted only without credentials) or a predicate. */
1070
+ allowedOrigins: string[] | ((origin: string) => boolean);
1071
+ /** Preflight cache lifetime in seconds (`Access-Control-Max-Age`). */
1072
+ maxAge?: number;
1073
+ }
1074
+ /** Origin/CSRF guard configuration. */
1075
+ interface CsrfOptions {
1076
+ /** Extra origins (beyond same-origin and the CORS allowlist) accepted on unsafe cookie requests. */
1077
+ trustedOrigins?: string[];
1078
+ }
1079
+ /**
1080
+ * The `security` option on `createWorker`. Every field is optional and defaults
1081
+ * to a secure posture; set a field to `false` to opt out of that layer.
1082
+ */
1083
+ interface SecurityOptions {
1084
+ /** CORS. Defaults to **deny cross-origin**; supply an allowlist to permit specific origins. `false` disables CORS handling. */
1085
+ cors?: CorsOptions | false;
1086
+ /** CSRF/origin guard for unsafe, cookie-authenticated requests. `true`/object = on (default), `false` = off. */
1087
+ csrf?: boolean | CsrfOptions;
1088
+ /** Baseline security response headers. `true`/object = on (default), `false` = off. */
1089
+ headers?: boolean | SecurityHeadersOptions;
1090
+ }
1091
+ interface ResolvedHeaders {
1092
+ coop: string | undefined;
1093
+ csp: {
1094
+ htmlToo: boolean;
1095
+ value: string;
1096
+ } | undefined;
1097
+ enabled: boolean;
1098
+ frameOptions: string | undefined;
1099
+ hsts: string | undefined;
1100
+ permissionsPolicy: string | undefined;
1101
+ referrerPolicy: string | undefined;
1102
+ }
1103
+ interface ResolvedCors {
1104
+ allowCredentials: boolean;
1105
+ allowedHeaders: string[];
1106
+ allowedMethods: string[];
1107
+ enabled: boolean;
1108
+ isAllowed: (origin: string) => boolean;
1109
+ /**
1110
+ * Like {@link ResolvedCors.isAllowed} but NEVER satisfied by a wildcard `*`
1111
+ * allowlist — an origin counts only when matched by an explicit, non-wildcard
1112
+ * rule (a named origin in the list, or a custom predicate the developer
1113
+ * wrote). Used by the CSRF guard: a wildcard CORS allowlist means "any origin
1114
+ * may read my non-credentialed responses", which must NOT be conflated with
1115
+ * "I trust any origin to make authenticated state changes".
1116
+ */
1117
+ isExplicitlyAllowed: (origin: string) => boolean;
1118
+ maxAge: number;
1119
+ }
1120
+ interface ResolvedCsrf {
1121
+ enabled: boolean;
1122
+ trustedOrigins: string[];
1123
+ }
1124
+ /** Normalized, ready-to-apply security configuration. */
1125
+ interface ResolvedSecurity {
1126
+ cors: ResolvedCors;
1127
+ csrf: ResolvedCsrf;
1128
+ headers: ResolvedHeaders;
1129
+ }
1130
+ /**
1131
+ * Normalize the public {@link SecurityOptions} into the resolved form the
1132
+ * request path applies. Pure — throws only on an invalid combination (wildcard
1133
+ * CORS + credentials) so the misconfiguration surfaces at worker construction
1134
+ * rather than silently shipping an unenforceable policy.
1135
+ *
1136
+ * `env` supplies the deployment-level security vars: `LUNORA_SECURITY_HEADERS` /
1137
+ * `LUNORA_SECURITY_CSRF` opt out of those layers (set either to `off`/`false`/`0`),
1138
+ * and `LUNORA_ALLOWED_ORIGINS` / `LUNORA_CORS_ALLOW_CREDENTIALS` configure CORS
1139
+ * when it isn't set in code. **Code config wins** — an explicit `security.*` in
1140
+ * {@link SecurityOptions} overrides the matching env knob — so the env var only
1141
+ * relaxes or fills the secure default, and the DO security audit (which reads the
1142
+ * same vars) and the running worker stay in agreement.
1143
+ */
1144
+ declare const resolveSecurity: (security: SecurityOptions | undefined, env?: Record<string, unknown>) => ResolvedSecurity;
1145
+ /**
1146
+ * CSRF defense: reject an unsafe (state-changing), **cookie-authenticated**
1147
+ * request whose `Origin`/`Referer` is neither same-origin nor allowlisted.
1148
+ *
1149
+ * Scoped deliberately to cookie-bearing browser requests — the only vector a
1150
+ * cross-site forgery can ride, since a browser auto-attaches cookies but never a
1151
+ * bearer token or custom header. Bearer/server-to-server traffic (no `Cookie`)
1152
+ * is exempt, as are safe methods. Returns a `403` `Response` to short-circuit,
1153
+ * or `undefined` when the request may proceed.
1154
+ * @returns a `403` Response when the origin is untrusted, or `undefined` when the request is allowed.
1155
+ */
1156
+ declare const enforceOrigin: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
1157
+ /**
1158
+ * Answer a CORS preflight (`OPTIONS` carrying `Access-Control-Request-Method`)
1159
+ * for an allowlisted origin with a `204`. Returns `undefined` for non-preflight
1160
+ * requests, a disabled CORS layer, or a disallowed origin — letting the request
1161
+ * fall through to normal routing.
1162
+ * @returns a `204` Response for valid preflights, or `undefined` to fall through.
1163
+ */
1164
+ declare const handleCorsPreflight: (request: Request, resolved: ResolvedSecurity) => Response | undefined;
1165
+ /**
1166
+ * Apply baseline security headers and (for allowed cross-origin requests) CORS
1167
+ * headers to an outgoing response, without overwriting anything the inner
1168
+ * handler already set.
1169
+ *
1170
+ * WebSocket upgrade responses (`status 101` / a `webSocket` field) are returned
1171
+ * untouched: re-wrapping them in a new `Response` would drop the socket and the
1172
+ * hibernation handshake.
1173
+ */
1174
+ declare const decorateResponse: (response: Response, request: Request, resolved: ResolvedSecurity) => Response;
1175
+ /**
1176
+ * Wire-format RPC envelope. Posted to `POST /_lunora/rpc`.
1177
+ *
1178
+ * `functionPath` is the `&lt;file>:&lt;function>` identifier emitted by codegen,
1179
+ * e.g. `"messages:list"`. `shardKey` is optional — when omitted the runtime
1180
+ * routes to {@link WorkerOptions.defaultShardKey} (default `"__root__"`).
1181
+ *
1182
+ * `fanOut` opts the envelope into cross-shard routing via the
1183
+ * {@link WorkerOptions.queryCoordinator}; mutually exclusive with
1184
+ * `shardKey` (specifying both is a 400 — fan-out *is* the shard choice).
1185
+ */
1186
+ interface RpcEnvelope {
1187
+ args?: Record<string, unknown>;
1188
+ fanOut?: FanOutSpec;
1189
+ functionPath: string;
1190
+ shardKey?: string;
1191
+ }
1192
+ type Route = (request: Request, env: unknown, context: ExecutionContextLike) => Promise<Response> | Response;
1193
+ /**
1194
+ * Context handed to HTTP-action handlers. Built per request by the worker; its
1195
+ * `run*` methods forward an RPC envelope to the shard, so handlers reach
1196
+ * queries/mutations/actions without a direct DB binding.
1197
+ *
1198
+ * `reference` is typed `unknown` so this structural contract stays free of a
1199
+ * `@lunora/server` dependency while remaining assignable from the fully-typed
1200
+ * `HttpActionCtx` on the server side (`{ __lunoraRef }` is read at runtime).
1201
+ */
1202
+ interface HttpActionContext {
1203
+ auth: {
1204
+ getIdentity: () => Promise<Record<string, unknown> | null>;
1205
+ userId: null | string;
1206
+ };
1207
+ fetch: typeof globalThis.fetch;
1208
+ runAction: <R>(reference: unknown, args?: Record<string, unknown>) => Promise<R>;
1209
+ runMutation: <R>(reference: unknown, args?: Record<string, unknown>) => Promise<R>;
1210
+ runQuery: <R>(reference: unknown, args?: Record<string, unknown>) => Promise<R>;
1211
+ }
1212
+ interface HttpActionLike {
1213
+ handler: (context: HttpActionContext, request: Request) => Promise<Response> | Response;
1214
+ }
1215
+ /**
1216
+ * Structural view of `@lunora/server`'s `httpRouter()`. The worker dispatches by
1217
+ * calling `fetch` — the same shape as a hono app's `app.fetch` — so the runtime
1218
+ * stays free of a hard dependency on the server package (and on hono). The
1219
+ * per-request {@link HttpActionContext} is injected on the `__lunoraCtx` env
1220
+ * binding; the router lifts it into the handler's context.
1221
+ */
1222
+ interface HttpRouterLike {
1223
+ fetch(request: Request, env?: unknown, context?: ExecutionContextLike): Promise<Response> | Response;
1224
+ }
1225
+ /**
1226
+ * Identity resolved from the inbound request by {@link WorkerOptions.resolveIdentity}.
1227
+ *
1228
+ * The `userId` field is special — it becomes `ctx.auth.userId` inside the
1229
+ * Durable Object. Any other keys (`email`, `name`, custom roles, etc.) are
1230
+ * forwarded verbatim as `ctx.auth.getIdentity()`'s return value.
1231
+ *
1232
+ * Return `null` to signal that the request is anonymous; the runtime will
1233
+ * skip both `x-lunora-userid` and `x-lunora-identity` headers, and
1234
+ * `ctx.auth.userId` will be `undefined` on the shard side.
1235
+ */
1236
+ interface ResolvedIdentity {
1237
+ /** Arbitrary additional claims. Must be JSON-serialisable. */
1238
+ [key: string]: unknown;
1239
+ /**
1240
+ * JWT-standard expiry in epoch SECONDS. When present (and `expiresAtMs` is
1241
+ * absent), the runtime forwards it as the socket's credential expiry — the
1242
+ * DO drops the socket once it lapses. Used only on the WebSocket path.
1243
+ */
1244
+ exp?: number;
1245
+ /**
1246
+ * Credential expiry in epoch MILLISECONDS. Preferred over `exp` when
1247
+ * both are present. Forwarded as the socket's expiry on the WebSocket path
1248
+ * so the DO drops the socket once it lapses; omit for non-expiring sessions.
1249
+ */
1250
+ expiresAtMs?: number;
1251
+ /** Stable user identifier (e.g. `"user_2k3..."` or `"u_42"`). */
1252
+ userId: string;
1253
+ }
1254
+ /**
1255
+ * Per-table sharding metadata the admin import endpoint needs to route rows.
1256
+ * Structural so this package stays free of `@lunora/server`. The codegen-
1257
+ * generated worker entry passes a thin projection of the user's schema.
1258
+ */
1259
+ interface ShardingInfo {
1260
+ /** `global` when the table lives in D1; `shardBy` when keyed by a field; `root` (or absent) otherwise. */
1261
+ mode: {
1262
+ field?: string;
1263
+ kind: "global" | "root" | "shardBy";
1264
+ };
1265
+ }
1266
+ /**
1267
+ * Lookup the runtime uses to bucket an import row to its owning shard. Returns
1268
+ * `undefined` for unknown tables — the row is reported as a hard error.
1269
+ */
1270
+ type AdminTableResolver = (table: string) => ShardingInfo | undefined;
1271
+ /**
1272
+ * Streamed bulk export of `.global()` tables, materialised as an async iterable
1273
+ * of `{table, doc}` rows. The runtime concatenates this stream after the
1274
+ * shard-local stream so the receiver sees a single NDJSON body.
1275
+ */
1276
+ type GlobalExportFunction = (request: {
1277
+ tables: ReadonlyArray<string>;
1278
+ }) => AsyncIterable<{
1279
+ doc: Record<string, unknown>;
1280
+ table: string;
1281
+ }>;
1282
+ /**
1283
+ * Read a page of the `.global()` (D1) change-data-capture log past `sinceSeq`
1284
+ * for the admin sync endpoint. Wire it to `@lunora/d1`'s `readD1CdcChanges`.
1285
+ * When omitted, the sync endpoint returns only shard-local changes.
1286
+ */
1287
+ type GlobalCdcSyncFunction = (request: {
1288
+ limit?: number;
1289
+ sinceSeq: number;
1290
+ }) => Promise<{
1291
+ changes: ReadonlyArray<Record<string, unknown>>;
1292
+ cursor: number;
1293
+ }>;
1294
+ /**
1295
+ * Replay a batch of `.global()` (D1) CDC changes for the admin apply endpoint
1296
+ * (point-in-time recovery). Wire it to `applyCdcChanges` on a D1 writer;
1297
+ * returns the number applied. When omitted, the apply endpoint replays only
1298
+ * shard-local changes.
1299
+ */
1300
+ type GlobalCdcApplyFunction = (request: {
1301
+ changes: ReadonlyArray<Record<string, unknown>>;
1302
+ }) => Promise<number>;
1303
+ /**
1304
+ * Bulk import of `.global()` rows. Returns insert counts + errors merged across
1305
+ * tables.
1306
+ *
1307
+ * Each row carries its true physical source `line` so error attribution stays
1308
+ * accurate even when global rows are interspersed with shard rows or blank lines
1309
+ * in the NDJSON (a single `startLine` can't describe non-contiguous rows). The
1310
+ * `startLine` field is the line of the FIRST global row, retained only as a
1311
+ * backward-compatible fallback for importers that haven't adopted per-row lines.
1312
+ */
1313
+ type GlobalImportFunction = (request: {
1314
+ rows: ReadonlyArray<{
1315
+ doc: Record<string, unknown>;
1316
+ line: number;
1317
+ table: string;
1318
+ }>;
1319
+ startLine?: number;
1320
+ }) => Promise<{
1321
+ conflicts: number;
1322
+ errors: ReadonlyArray<{
1323
+ code: string;
1324
+ line: number;
1325
+ message: string;
1326
+ table: string;
1327
+ }>;
1328
+ inserted: Record<string, number>;
1329
+ }>;
1330
+ /** One R2 object as the storage browser surfaces it. Mirrors `@lunora/storage`'s `R2ObjectLike`. */
1331
+ interface StorageObject {
1332
+ customMetadata?: Record<string, string>;
1333
+ etag: string;
1334
+ httpMetadata?: {
1335
+ contentType?: string;
1336
+ };
1337
+ key: string;
1338
+ size: number;
1339
+ }
1340
+ /**
1341
+ * One registered function, as the discovery endpoint surfaces it. Structurally
1342
+ * a subset of codegen's `RegisteredLunoraFunction` — only `kind` and
1343
+ * `visibility` matter here, so the generated `LUNORA_FUNCTIONS` map satisfies
1344
+ * the {@link FunctionRegistryLike} value shape.
1345
+ */
1346
+ interface FunctionDescriptor {
1347
+ /** The function's declared argument schema, derived from its `v.*` validators. */
1348
+ args: FunctionArgumentDescriptor[];
1349
+ kind: "action" | "mutation" | "query";
1350
+ /** The `&lt;file>:&lt;function>` identifier, e.g. `messages:list`. */
1351
+ path: string;
1352
+ /** `"internal"` functions are never exposed by the discovery endpoint; absence === public. */
1353
+ visibility?: "internal" | "public";
1354
+ }
1355
+ /** One value in {@link FunctionRegistryLike} — the bits of a registered function the discovery endpoint reads. */
1356
+ interface FunctionRegistryEntry {
1357
+ /** The function's `v.*` args validator map; read structurally for the signature view. */
1358
+ args?: unknown;
1359
+ /**
1360
+ * The generated registry carries `"stream"` alongside query/mutation/action;
1361
+ * the discovery endpoint surfaces the latter three only (a `stream` function
1362
+ * isn't runnable from the function runner), but accepting the kind here lets
1363
+ * callers pass the generated `LUNORA_FUNCTIONS` map without a cast.
1364
+ */
1365
+ kind: "action" | "mutation" | "query" | "stream";
1366
+ visibility?: "internal" | "public";
1367
+ }
1368
+ /**
1369
+ * The generated `LUNORA_FUNCTIONS` dispatch table, narrowed to what the
1370
+ * discovery endpoint reads. Pass the map straight from `_generated/functions.ts`.
1371
+ */
1372
+ type FunctionRegistryLike = Record<string, FunctionRegistryEntry>;
1373
+ /**
1374
+ * Lists objects in the storage bucket for the admin file browser. Structurally
1375
+ * compatible with `@lunora/storage`'s `Storage["list"]` — the runtime stays free
1376
+ * of a hard dependency on the storage package.
1377
+ */
1378
+ type StorageListFunction = (prefix?: string, options?: {
1379
+ bucket?: string;
1380
+ cursor?: string;
1381
+ limit?: number;
1382
+ }) => Promise<{
1383
+ cursor?: string;
1384
+ objects: StorageObject[];
1385
+ }>;
1386
+ /**
1387
+ * Deletes one object from a storage bucket for the admin file browser.
1388
+ * Structurally compatible with `@lunora/storage`'s `Storage["delete"]`, so
1389
+ * passing `createStorage(...).delete` satisfies it. The optional `bucket` selects
1390
+ * a named bucket for a multi-bucket deployment (ignored by single-bucket hosts).
1391
+ */
1392
+ type StorageDeleteFunction = (key: string, options?: {
1393
+ bucket?: string;
1394
+ }) => Promise<void> | void;
1395
+ /**
1396
+ * Uploads one object to a storage bucket for the admin file browser. Mirrors
1397
+ * `@lunora/storage`'s `Storage["upload"]` (only the bits the admin endpoint
1398
+ * needs): the key, the raw bytes, an optional content-type, and an optional
1399
+ * target `bucket` for multi-bucket deployments.
1400
+ */
1401
+ type StorageUploadFunction = (key: string, body: ArrayBuffer, options?: {
1402
+ bucket?: string;
1403
+ contentType?: string;
1404
+ }) => Promise<{
1405
+ etag?: string;
1406
+ key: string;
1407
+ }> | {
1408
+ etag?: string;
1409
+ key: string;
1410
+ };
1411
+ /**
1412
+ * Mints a (signed or public) URL for one object so the admin file browser can
1413
+ * offer a "copy URL" action. The optional `expiresInSeconds` lets the caller pick
1414
+ * a share-link lifetime (the host clamps it); `bucket` selects a named bucket.
1415
+ * Structurally compatible with `@lunora/storage`'s `Storage["getSignedUrl"]`.
1416
+ */
1417
+ type StorageSignedUrlFunction = (key: string, options?: {
1418
+ bucket?: string;
1419
+ expiresInSeconds?: number;
1420
+ }) => Promise<string> | string;
1421
+ /** One `.global()` table plus its row count. Mirrors `@lunora/d1`'s `GlobalTableInfo`. */
1422
+ interface GlobalTableInfo {
1423
+ name: string;
1424
+ rowCount: number;
1425
+ }
1426
+ /** A window of rows from one global table. Mirrors `@lunora/d1`'s `GlobalTablePage`. */
1427
+ interface GlobalTablePage {
1428
+ columns: string[];
1429
+ /** FK columns (local column → referenced table) for external tables with real `REFERENCES` constraints. */
1430
+ refs?: Record<string, string>;
1431
+ rows: Record<string, unknown>[];
1432
+ total: number;
1433
+ }
1434
+ /** One eq constraint a facet-value click adds to the global browser's view. Mirrors `@lunora/d1`'s `GlobalFilterClause`. */
1435
+ interface GlobalFilterClause {
1436
+ column: string;
1437
+ value: unknown;
1438
+ }
1439
+ /** Per-column distinct-value summary for the global browser. Mirrors `@lunora/d1`'s `GlobalFacetResult`. */
1440
+ interface GlobalFacetResult {
1441
+ truncated: boolean;
1442
+ values: {
1443
+ count: number;
1444
+ value: unknown;
1445
+ }[];
1446
+ }
1447
+ /**
1448
+ * Introspect `.global()` (D1-backed) tables for the data browser. Structurally
1449
+ * compatible with `@lunora/d1`'s `listGlobalTables` / `readGlobalTablePage` /
1450
+ * `facetGlobalColumn` (curried with the D1 exec + schema) — the runtime stays
1451
+ * free of a hard dependency on the D1 package.
1452
+ */
1453
+ interface GlobalIntrospector {
1454
+ facetColumn: (options: {
1455
+ column: string;
1456
+ filters?: GlobalFilterClause[];
1457
+ limit?: number;
1458
+ table: string;
1459
+ }) => Promise<GlobalFacetResult>;
1460
+ listTables: () => Promise<GlobalTableInfo[]>;
1461
+ readTablePage: (options: {
1462
+ filters?: GlobalFilterClause[];
1463
+ limit?: number;
1464
+ offset?: number;
1465
+ table: string;
1466
+ }) => Promise<GlobalTablePage>;
1467
+ }
1468
+ /**
1469
+ * One vector index as the studio's vector browser lists it: the static schema
1470
+ * metadata (name/table/field/dimensions/metric/metadata) merged with the live
1471
+ * Vectorize `describe()` stats (`vectorsCount`, processing watermark) when the
1472
+ * binding is reachable. The live fields are optional so a never-bound index
1473
+ * still lists with its declared shape.
1474
+ */
1475
+ interface VectorIndexSummary {
1476
+ dimensions?: number;
1477
+ field?: string;
1478
+ metadata?: ReadonlyArray<string>;
1479
+ metric?: "cosine" | "dot-product" | "euclidean";
1480
+ name: string;
1481
+ /** Most recent mutation Vectorize has finished indexing, from `describe()`. */
1482
+ processedUpToMutation?: string;
1483
+ table: string;
1484
+ /** Live vector count from `describe()`; absent when the binding is unreachable. */
1485
+ vectorsCount?: number;
1486
+ }
1487
+ /** One nearest-neighbour hit from a vector-index similarity query. */
1488
+ interface VectorQueryMatch {
1489
+ id: string;
1490
+ metadata?: Record<string, unknown>;
1491
+ score: number;
1492
+ }
1493
+ /**
1494
+ * Introspect Vectorize indexes for the studio's vector browser. Built in the
1495
+ * worker entry from the generated `LUNORA_VECTOR_INDEXES` registry (Vectorize
1496
+ * cannot enumerate indexes at runtime) paired with the env bindings + the
1497
+ * schema's per-index embedders. `queryIndex` is optional: an index with no
1498
+ * embedder (a `select`-derived Shape B index, or a deployment that withholds the
1499
+ * embedder) lists but cannot be similarity-queried from the studio.
1500
+ */
1501
+ interface VectorIntrospector {
1502
+ listIndexes: () => Promise<VectorIndexSummary[]>;
1503
+ queryIndex?: (options: {
1504
+ name: string;
1505
+ text: string;
1506
+ topK?: number;
1507
+ }) => Promise<{
1508
+ matches: VectorQueryMatch[];
1509
+ }>;
1510
+ }
1511
+ /**
1512
+ * Cron controller handed to the worker's `scheduled()` entry by the Workers
1513
+ * runtime. `cron` is the exact trigger expression that fired (matched against
1514
+ * {@link WorkerOptions.crons} keys and {@link WorkerOptions.backupCron});
1515
+ * `scheduledTime` is the firing time in epoch-ms, used as the backup id so the
1516
+ * snapshot is named after the moment it represents rather than wall-clock skew.
1517
+ */
1518
+ interface ScheduledControllerLike {
1519
+ cron: string;
1520
+ noRetry?: () => void;
1521
+ scheduledTime: number;
1522
+ }
1523
+ /**
1524
+ * A cron-trigger handler registered on {@link WorkerOptions.crons}. The worker's
1525
+ * `scheduled()` entry invokes the handler whose map key equals the firing
1526
+ * trigger's `cron` expression. Runs server-side with no end-user identity.
1527
+ */
1528
+ type CronHandler = (controller: ScheduledControllerLike, env: unknown, context: ExecutionContextLike) => Promise<void> | void;
1529
+ /**
1530
+ * A Cloudflare Queues push-consumer handler — the worker's `queue()` entry
1531
+ * forwards each delivered `MessageBatch` (typed `unknown` here to keep the
1532
+ * runtime decoupled from `@lunora/queue`'s structural batch type).
1533
+ */
1534
+ type QueueConsumerHandler = (batch: unknown, env: unknown, context: ExecutionContextLike) => Promise<void>;
1535
+ /**
1536
+ * A single code-defined cron job, shaped like an entry of the generated
1537
+ * `LUNORA_CRONS` map. `functionPath` is the `"namespace:fn"` to run, `args` its
1538
+ * bound arguments, and `name` the human label from the `cronJobs()` builder.
1539
+ * Pass the whole `LUNORA_CRONS` map as {@link WorkerOptions.cronJobs}; the worker
1540
+ * dispatches each job on its firing trigger via the same authorized shard path
1541
+ * as the scheduler.
1542
+ */
1543
+ interface CronJobDispatch {
1544
+ args?: Record<string, unknown>;
1545
+ functionPath?: string;
1546
+ name: string;
1547
+ shardKey?: string;
1548
+ /**
1549
+ * Set when the job targets a durable workflow instead of a function: the
1550
+ * `WORKFLOW_*` binding name on `env`. On a firing trigger the worker starts a
1551
+ * NEW workflow instance (the {@link CronJobDispatch.args} become its
1552
+ * `params`) rather than dispatching {@link CronJobDispatch.functionPath} to a
1553
+ * shard. Mutually exclusive with `functionPath`.
1554
+ */
1555
+ workflow?: string;
1556
+ }
1557
+ /**
1558
+ * One scheduled cron invocation as the discovery endpoint surfaces it: a
1559
+ * {@link CronJobDispatch} flattened together with the `cron` expression that
1560
+ * fires it. Cloudflare exposes no runtime cron introspection, so the injected
1561
+ * `cronJobs` map is the only source of truth; the studio renders these read-only.
1562
+ */
1563
+ interface CronJobInfo {
1564
+ args?: Record<string, unknown>;
1565
+ /** The compiled cron expression, e.g. `"0 9 * * *"`. */
1566
+ cron: string;
1567
+ functionPath?: string;
1568
+ name: string;
1569
+ shardKey?: string;
1570
+ /** The `WORKFLOW_*` binding name when the job starts a durable workflow instead of a function. */
1571
+ workflow?: string;
1572
+ }
1573
+ /**
1574
+ * R2-like sink for scheduled backups. Structurally a subset of `@lunora/storage`'s
1575
+ * `R2BucketLike` (and of the raw R2 binding), so passing `env.BACKUPS` straight
1576
+ * through satisfies it. `put` writes the NDJSON snapshot and its manifest
1577
+ * sidecar; `list`/`delete` drive retention pruning when
1578
+ * {@link WorkerOptions.backupRetain} is set.
1579
+ */
1580
+ interface BackupStore {
1581
+ delete: (key: string) => Promise<unknown>;
1582
+ list: (options?: {
1583
+ cursor?: string;
1584
+ limit?: number;
1585
+ prefix?: string;
1586
+ }) => Promise<{
1587
+ cursor?: string;
1588
+ objects: ReadonlyArray<{
1589
+ key: string;
1590
+ }>;
1591
+ truncated?: boolean;
1592
+ }>;
1593
+ put: (key: string, body: ArrayBuffer | Blob | null | ReadableStream | string, options?: {
1594
+ customMetadata?: Record<string, string>;
1595
+ httpMetadata?: {
1596
+ contentType?: string;
1597
+ };
1598
+ }) => Promise<unknown>;
1599
+ }
1600
+ /**
1601
+ * Manifest sidecar written next to each scheduled backup's NDJSON object (at
1602
+ * `&lt;file>.manifest.json`). Mirrors the manifest entry the CLI records for local
1603
+ * backups so both backup planes describe a snapshot the same way;
1604
+ * `cron`/`scheduledTime` additionally record which trigger produced it.
1605
+ */
1606
+ interface BackupManifest {
1607
+ bytes: number;
1608
+ createdAt: string;
1609
+ cron: string;
1610
+ file: string;
1611
+ id: string;
1612
+ rows: number;
1613
+ scheduledTime: number;
1614
+ tables?: string;
1615
+ }
1616
+ interface WorkerOptions {
1617
+ /**
1618
+ * An additional, async authorization gate for the `/_lunora/admin/*` plane
1619
+ * (the Studio's HTTP + WS endpoints), OR-ed with the static {@link WorkerOptions.adminToken}
1620
+ * bearer. When it resolves `true` for a request, that request is treated as
1621
+ * admin-authorized even without the bearer; when it resolves `false` (or is
1622
+ * unset) the bearer remains the only path. Evaluated once per admin request
1623
+ * and never on the RPC/WebSocket data hot path.
1624
+ *
1625
+ * The intended producer is `@lunora/cloudflare-access`'s `accessAdminGate(...)`,
1626
+ * which verifies the request's `Cf-Access-Jwt-Assertion` JWT and applies an
1627
+ * `isAdmin(claims)` predicate — so the Studio can sit behind Cloudflare Access
1628
+ * instead of (or alongside) a shared admin token. It takes only the request
1629
+ * (verification needs static team-domain/aud config + the remote JWKS, no env
1630
+ * binding), so it composes without threading async through every admin route.
1631
+ */
1632
+ adminGate?: (request: Request) => boolean | Promise<boolean>;
1633
+ /**
1634
+ * Admin bearer token expected by the export/import endpoints. When unset,
1635
+ * the endpoints respond with `ADMIN_FORBIDDEN` — the same posture the
1636
+ * per-shard admin gate uses.
1637
+ */
1638
+ adminToken?: string;
1639
+ /**
1640
+ * Acknowledge — explicitly — that sharded and fan-out access may be
1641
+ * exercised by any caller (including unauthenticated ones) because no
1642
+ * authorization callback is configured. When neither {@link WorkerOptions.authorizeShard}
1643
+ * nor {@link WorkerOptions.authorizeFanOut} is set, naming a non-default shard or sending
1644
+ * a fan-out envelope is authorization-open: this is the historical posture,
1645
+ * preserved for backward compatibility. The runtime emits a single loud
1646
+ * `console.warn` the first time such a request is seen so the gap is
1647
+ * visible in logs. Set this to `true` to assert the posture is intentional
1648
+ * and silence that warning. It does NOT change behaviour — it is purely an
1649
+ * acknowledgement flag — and has no effect once an `authorize*` callback is
1650
+ * configured.
1651
+ */
1652
+ allowUnauthenticatedShardAccess?: boolean;
1653
+ /**
1654
+ * Replay `.global()` (D1) CDC changes for the admin apply endpoint
1655
+ * (point-in-time recovery). When omitted, apply covers only shard-local tables.
1656
+ */
1657
+ applyGlobals?: GlobalCdcApplyFunction;
1658
+ /**
1659
+ * The auth user-management plane backing the studio's users dashboard:
1660
+ * browse via `GET /_lunora/admin/auth/users` + `/sessions`, and (when the
1661
+ * implementation provides the optional mutations) create/ban/role/revoke/
1662
+ * delete/impersonate via the matching admin-gated `POST /_lunora/admin/auth/*`
1663
+ * routes. Wire it with `@lunora/auth`'s `createAuthAdmin(auth)`. Omit it and
1664
+ * every `/auth/*` endpoint responds `AUTH_NOT_CONFIGURED`.
1665
+ */
1666
+ authAdmin?: AuthAdmin;
1667
+ /**
1668
+ * Base path the auth routes are mounted under (default `/api/auth`). Used
1669
+ * to classify which inbound paths are auth ATTEMPTS for the app-level
1670
+ * auth-failure SLO signal (PLAN3 §2.3) — see {@link WorkerOptions.authHandler}.
1671
+ * Only meaningful alongside `authHandler`.
1672
+ */
1673
+ authBasePath?: string;
1674
+ /**
1675
+ * Optional prebound `@lunora/auth` handler (`handleAuthRequest(auth, …)`
1676
+ * with its `auth` argument already bound) the worker dispatches BEFORE its
1677
+ * own routing — auth runs as a top-level `/api/auth/*` route, not through
1678
+ * lunora functions. It returns a `Response` for an auth route and
1679
+ * `undefined` to let the request fall through to the worker.
1680
+ *
1681
+ * Wiring it here (rather than in the host entry) lets the runtime instrument
1682
+ * it for the app-level auth-failure SLO (PLAN3 §2.3): after the handler
1683
+ * answers a genuine auth ATTEMPT route (sign-in / sign-up / callback under
1684
+ * {@link WorkerOptions.authBasePath}), the worker fires a fire-and-forget
1685
+ * `recordAuthEvent` against the root shard via `ctx.waitUntil` — classifying
1686
+ * the outcome by status (`≥ 400` ⇒ `fail`). The recording never blocks or
1687
+ * fails the auth response, and is skipped silently when no admin token or
1688
+ * shard namespace is configured (the SLO signal is simply absent).
1689
+ *
1690
+ * Omit it and the host keeps calling `handleAuthRequest` itself; the SLO
1691
+ * signal is then absent but auth behaves identically.
1692
+ */
1693
+ authHandler?: (request: Request) => Promise<Response | undefined>;
1694
+ /**
1695
+ * @deprecated Use {@link WorkerOptions.authAdmin} (an {@link AuthAdmin}),
1696
+ * which also lights up the user-management mutation endpoints. Still honored
1697
+ * as a read-only fallback for the browse endpoints.
1698
+ */
1699
+ authIntrospector?: AuthIntrospector;
1700
+ /**
1701
+ * Optional table-level authorization callback for fan-out RPC envelopes.
1702
+ * Called after `resolveIdentity` and before `coordinator.fanOut` walks
1703
+ * the registry. Returning `false` rejects the request with 403
1704
+ * `FORBIDDEN_FANOUT`. When unset, fan-out is denied by default
1705
+ * whenever {@link WorkerOptions.authorizeShard} is configured — fan-out is a
1706
+ * privileged operation (it dispatches the caller's function across
1707
+ * every live shard for the table) and a per-shard gate is not
1708
+ * sufficient to authorize it. Apps that need client-driven fan-out
1709
+ * must opt in explicitly via this callback.
1710
+ */
1711
+ authorizeFanOut?: (identity: ResolvedIdentity | null, table: string, functionPath: string) => boolean | Promise<boolean>;
1712
+ /**
1713
+ * Optional per-shard authorization callback. Called from both the RPC
1714
+ * dispatch path and the WebSocket upgrade path after `resolveIdentity`
1715
+ * has produced an identity but before the request is forwarded to the
1716
+ * named shard. Returning `false` (or a promise resolving to `false`)
1717
+ * causes the runtime to reject the request with a 403
1718
+ * `FORBIDDEN_SHARD` error. When unset, the runtime allows the
1719
+ * request — preserving the historical "any client may name any
1720
+ * shard" posture.
1721
+ *
1722
+ * Note: this callback does NOT gate fan-out envelopes — fan-out
1723
+ * targets every live shard for a table and must be authorized at the
1724
+ * table level via {@link WorkerOptions.authorizeFanOut}. Configuring this callback
1725
+ * without `authorizeFanOut` causes fan-out envelopes to be denied by
1726
+ * default.
1727
+ */
1728
+ authorizeShard?: (identity: ResolvedIdentity | null, shardKey: string) => boolean | Promise<boolean>;
1729
+ /**
1730
+ * Cron expression that triggers the built-in backup. When set alongside
1731
+ * {@link WorkerOptions.backupStore} and {@link WorkerOptions.adminToken}, the
1732
+ * worker's `scheduled()` entry runs a full export and writes an NDJSON
1733
+ * snapshot + manifest sidecar to the backup store whenever a cron trigger
1734
+ * with this exact expression fires. Must match an entry in the worker's
1735
+ * wrangler `triggers.crons` (and the string is compared verbatim). Omit it
1736
+ * and no automatic backup runs.
1737
+ */
1738
+ backupCron?: string;
1739
+ /**
1740
+ * Key prefix the scheduled backup writes under (default `"backups/"`). The
1741
+ * NDJSON object lands at `&lt;prefix>lunora-backup-&lt;id>.ndjson` and its manifest
1742
+ * at the same key plus `.manifest.json`.
1743
+ */
1744
+ backupPrefix?: string;
1745
+ /**
1746
+ * Retention bound for scheduled backups: keep only the newest N snapshots
1747
+ * under {@link WorkerOptions.backupPrefix}, pruning older NDJSON objects and
1748
+ * their manifests after each run. Omit (or `0`) to keep every backup.
1749
+ */
1750
+ backupRetain?: number;
1751
+ /**
1752
+ * R2-like store the scheduled backup writes snapshots to. Pass the bound R2
1753
+ * bucket (`env.BACKUPS`) directly — its shape satisfies {@link BackupStore}.
1754
+ * Without it (or without {@link WorkerOptions.backupCron}) no automatic
1755
+ * backup runs.
1756
+ */
1757
+ backupStore?: BackupStore;
1758
+ /**
1759
+ * Table allowlist for the scheduled backup. Omit to back up every table
1760
+ * (shard-local + `.global()`). Mirrors the export endpoint's `tables`.
1761
+ */
1762
+ backupTables?: ReadonlyArray<string>;
1763
+ /**
1764
+ * Code-defined cron jobs keyed by cron expression — pass the generated
1765
+ * `LUNORA_CRONS` map directly. On a firing trigger the worker runs every job
1766
+ * listed under the matching expression by dispatching its `functionPath`/`args`
1767
+ * to the shard, server-side, through the same authorization as the scheduler.
1768
+ * Runs alongside any {@link WorkerOptions.crons} handler and the backup.
1769
+ */
1770
+ cronJobs?: Record<string, ReadonlyArray<CronJobDispatch>>;
1771
+ /**
1772
+ * Cron-trigger handlers keyed by their exact cron expression. The worker's
1773
+ * `scheduled()` entry dispatches the handler whose key equals the firing
1774
+ * trigger's `cron`. Independent of the built-in backup — a handler keyed on
1775
+ * the same expression as {@link WorkerOptions.backupCron} runs alongside it.
1776
+ */
1777
+ crons?: Record<string, CronHandler>;
1778
+ /**
1779
+ * D1 binding for `.global()` tables. Currently unused by the routing
1780
+ * layer; downstream packages will read it from `env.DB` directly.
1781
+ */
1782
+ d1?: unknown;
1783
+ /** Default shard key used when an envelope omits one. */
1784
+ defaultShardKey?: string;
1785
+ /**
1786
+ * Stream `.global()` rows for the admin export endpoint. When omitted,
1787
+ * the export endpoint covers only shard-local tables.
1788
+ */
1789
+ exportGlobals?: GlobalExportFunction;
1790
+ /**
1791
+ * The generated `LUNORA_FUNCTIONS` map (from `_generated/functions.ts`). When
1792
+ * set, the worker exposes the admin-gated `GET /_lunora/admin/functions`
1793
+ * endpoint the studio uses to auto-discover queries/mutations/actions
1794
+ * (internal functions are filtered out). Omit it and the endpoint responds
1795
+ * `FUNCTIONS_NOT_CONFIGURED`.
1796
+ */
1797
+ functions?: FunctionRegistryLike;
1798
+ /**
1799
+ * Read-only introspector for `.global()` (D1) tables, backing the data
1800
+ * browser's global mode via `GET /_lunora/admin/global/tables` and
1801
+ * `/_lunora/admin/global/table`. Build it from `@lunora/d1`'s
1802
+ * `listGlobalTables` / `readGlobalTablePage`. Omit it and those endpoints
1803
+ * respond `GLOBALS_NOT_CONFIGURED`.
1804
+ */
1805
+ globalIntrospector?: GlobalIntrospector;
1806
+ /**
1807
+ * Router for HTTP actions (`httpRouter()` from `@lunora/server`, a hono app).
1808
+ * Consulted for requests that miss the explicit {@link WorkerOptions.routes}
1809
+ * map and the internal `/_lunora/*` endpoints. The runtime builds the action
1810
+ * context, injects it on the `__lunoraCtx` env binding, and dispatches via
1811
+ * `httpRouter.fetch`; matched handlers reach the data layer through
1812
+ * `ctx.run*`, which forward to the shard. An unmatched request returns hono's
1813
+ * own 404 (a path-match with the wrong verb is a 404, not a 405).
1814
+ */
1815
+ httpRouter?: HttpRouterLike;
1816
+ /**
1817
+ * Insert `.global()` rows for the admin import endpoint. When omitted,
1818
+ * rows targeting global tables are reported as hard errors.
1819
+ */
1820
+ importGlobals?: GlobalImportFunction;
1821
+ /**
1822
+ * Restrict every Durable Object this worker reaches — shard DOs, the
1823
+ * scheduler DO, the fan-out coordinator, subscriptions — to a Cloudflare
1824
+ * data-residency jurisdiction (`"eu"`, `"us"`, `"fedramp"`). The runtime
1825
+ * derives a jurisdiction-pinned subnamespace from {@link WorkerOptions.shardDO}
1826
+ * and {@link WorkerOptions.schedulerDO} once, so all routing inherits it.
1827
+ *
1828
+ * Fail-closed: if the bound namespace does not expose `.jurisdiction()`
1829
+ * (an older `@cloudflare/workers-types`), the worker throws rather than
1830
+ * silently routing to the un-pinned global namespace. Omit it for the
1831
+ * default, un-pinned behaviour.
1832
+ *
1833
+ * ⚠️ Set once, before the first deploy — changing it strands data. A DO name
1834
+ * maps to a *different* ID per jurisdiction, so toggling this on an existing
1835
+ * deployment makes every shard/scheduler call resolve to a new, empty DO; the
1836
+ * prior data stays in the old jurisdiction and is unreachable (no in-place
1837
+ * migration). Usually set via the schema's `.jurisdiction(...)`, which codegen
1838
+ * threads here.
1839
+ * @see https://developers.cloudflare.com/durable-objects/reference/data-location/
1840
+ */
1841
+ jurisdiction?: DurableObjectJurisdiction;
1842
+ /**
1843
+ * Optional telemetry sink. When supplied, the worker emits one
1844
+ * `onRpc` event per dispatched RPC (single-shard forward or fan-out)
1845
+ * with duration / ok / error / shardKey or fanOut metadata. Sink
1846
+ * throws are swallowed so a faulty adapter cannot break user-facing
1847
+ * dispatch. See {@link ObservabilitySink}.
1848
+ */
1849
+ observability?: ObservabilitySink;
1850
+ /**
1851
+ * The generated OpenAPI 3.1 document. Import it from the codegen-emitted
1852
+ * module and pass it through:
1853
+ * `import { openApiSpec } from "./lunora/_generated/openapi"`. A Worker can't
1854
+ * read the `_generated/openapi.json` file at runtime, so codegen also emits
1855
+ * `openapi.ts` (the same document inlined as `export const openApiSpec`) for
1856
+ * exactly this wiring — it regenerates on every `lunora/` change so the spec
1857
+ * stays live.
1858
+ *
1859
+ * When set, the worker exposes the admin-gated `GET /_lunora/admin/openapi`
1860
+ * endpoint the studio's API-reference view renders. The runtime does
1861
+ * NOT assemble or validate the spec — it serves what the host injects verbatim.
1862
+ * Omit it and the endpoint returns an empty-but-valid OpenAPI 3.1 document
1863
+ * (no paths), so the studio shows a "not configured" state rather than erroring.
1864
+ */
1865
+ openApiSpec?: unknown;
1866
+ /**
1867
+ * The generated OpenRPC 1.x document. Import it from the codegen-emitted
1868
+ * module and pass it through:
1869
+ * `import { openRpcSpec } from "./lunora/_generated/openrpc"` (only emitted
1870
+ * when the project opts into `apiSpec: "openrpc"` or `"both"`). Like
1871
+ * `openApiSpec`, codegen inlines the document into `openrpc.ts` because a
1872
+ * Worker can't read the `.json` at runtime; both regenerate together.
1873
+ *
1874
+ * When set, the worker exposes the admin-gated `GET /_lunora/admin/openrpc`
1875
+ * endpoint the studio's API-reference view can render. OpenRPC is the
1876
+ * RPC-native spec (a `methods` array over the JSON-RPC-shaped
1877
+ * `POST /_lunora/rpc` transport); it covers only the RPC functions, not
1878
+ * `httpRouter()` REST routes. The runtime does NOT assemble or validate the
1879
+ * spec — it serves what the host injects verbatim. Omit it and the endpoint
1880
+ * returns an empty-but-valid OpenRPC 1.x document (no methods), so the studio
1881
+ * shows a "not configured" state rather than erroring.
1882
+ */
1883
+ openRpcSpec?: unknown;
1884
+ /**
1885
+ * When true, the runtime calls `ctx.passThroughOnException()` at the top
1886
+ * of the fetch handler. Forwards uncaught exceptions to the origin
1887
+ * instead of returning a synthetic 500.
1888
+ */
1889
+ passThroughOnException?: boolean;
1890
+ /**
1891
+ * Coordinator for cross-shard RPCs. When absent, envelopes with
1892
+ * `fanOut` set are rejected with a 400. Construct via
1893
+ * `createQueryCoordinator({ registry })`.
1894
+ */
1895
+ queryCoordinator?: QueryCoordinator;
1896
+ /**
1897
+ * Cloudflare Queues push-consumer handler — the worker's `queue(batch, …)`
1898
+ * entry forwards every delivered `MessageBatch` here. Built by codegen from
1899
+ * `lunora/queues.ts` (via `@lunora/queue`'s `dispatchQueueBatch`, which routes
1900
+ * by `batch.queue` to the matching `defineQueue` handler), so the runtime
1901
+ * stays decoupled from the queue package. Omitted when no push queues exist.
1902
+ */
1903
+ queue?: QueueConsumerHandler;
1904
+ /**
1905
+ * Resolve the calling identity from the inbound RPC request. Called once
1906
+ * per RPC (and per fan-out) before the request is forwarded to the
1907
+ * shard. The returned `userId` becomes `ctx.auth.userId` on the shard
1908
+ * side; remaining keys (`email`, role flags, etc.) are JSON-encoded and
1909
+ * forwarded as `x-lunora-identity` so `ctx.auth.getIdentity()` can
1910
+ * return them. Returning `null` (or omitting this option) means
1911
+ * anonymous — no identity headers are injected.
1912
+ */
1913
+ resolveIdentity?: (request: Request, env: unknown) => Promise<ResolvedIdentity | null> | ResolvedIdentity | null;
1914
+ /**
1915
+ * Resolve a table's sharding metadata. Required by the import endpoint to
1916
+ * bucket rows; when omitted, every row routes to the default shard.
1917
+ */
1918
+ resolveTableSharding?: AdminTableResolver;
1919
+ /**
1920
+ * Map of routes for custom HTTP handlers (auth callbacks etc.). Keys can
1921
+ * be either `"METHOD path"` (e.g. `"GET /healthz"`) or just `"path"`
1922
+ * (e.g. `"/healthz"`) — the runtime will match the more specific form
1923
+ * first.
1924
+ */
1925
+ routes?: Record<string, Route>;
1926
+ /**
1927
+ * Namespace binding for the `SchedulerDO` (typically `env.SCHEDULER`). When
1928
+ * set, the worker exposes the admin-gated `/_lunora/admin/scheduled`
1929
+ * endpoints used by the studio to list and cancel `runAfter` / `runAt`
1930
+ * jobs. Omit it and those endpoints respond `SCHEDULER_NOT_CONFIGURED`.
1931
+ */
1932
+ schedulerDO?: ShardNamespaceLike;
1933
+ /**
1934
+ * Named `SchedulerDO` instance the admin endpoints target. Must match the
1935
+ * `instanceName` passed to `createScheduler` (both default to `default`).
1936
+ */
1937
+ schedulerInstanceName?: string;
1938
+ /**
1939
+ * Secure-by-default HTTP edge applied to every response the worker emits
1940
+ * (RPC, auth, admin, `httpRoute` handlers, SSR fallback): baseline security
1941
+ * headers, deny-by-default CORS, and a CSRF/origin guard. Every layer is on
1942
+ * by default and individually opt-out — see {@link SecurityOptions}. Omit it
1943
+ * to take the hardened defaults; set a field to `false` to relax that layer
1944
+ * (e.g. `security: { cors: { allowedOrigins: ["https://app.example.com"] } }`).
1945
+ */
1946
+ security?: SecurityOptions;
1947
+ /** Namespace binding for the shard Durable Object (typically `env.SHARD`). */
1948
+ shardDO: ShardNamespaceLike;
1949
+ /**
1950
+ * Names of the storage buckets the studio's file browser offers in its bucket
1951
+ * picker, backing `GET /_lunora/admin/storage/buckets`. Supply the keys of a
1952
+ * multi-bucket `createBucketStorage({...})` so the operator can switch buckets;
1953
+ * the selected name is forwarded to the storage ops as `options.bucket`. Omit
1954
+ * it (single-bucket deployments) and the picker is hidden — the ops target the
1955
+ * default bucket.
1956
+ */
1957
+ storageBuckets?: string[];
1958
+ /**
1959
+ * Deletes one object, backing the admin-gated `DELETE /_lunora/admin/storage`
1960
+ * endpoint the studio's file browser calls. Passing
1961
+ * `createStorage(...).delete` satisfies it. Omit it and the endpoint responds
1962
+ * `STORAGE_DELETE_NOT_CONFIGURED` — the studio surfaces a clear inline error.
1963
+ */
1964
+ storageDelete?: StorageDeleteFunction;
1965
+ /**
1966
+ * Storage lister backing the admin-gated `GET /_lunora/admin/storage`
1967
+ * endpoint the studio's file browser calls. The structural shape matches
1968
+ * `@lunora/storage`'s `Storage["list"]`, so passing `createStorage(...).list`
1969
+ * (or the raw R2 bucket's `list`) satisfies it. Omit it and the endpoint
1970
+ * responds `STORAGE_NOT_CONFIGURED`.
1971
+ */
1972
+ storageList?: StorageListFunction;
1973
+ /**
1974
+ * Mints a (signed or public) URL for one object, backing the admin-gated
1975
+ * `GET /_lunora/admin/storage/url` endpoint the studio's "copy URL" action
1976
+ * calls. Passing `createStorage(...).getSignedUrl` (or `.getUrl`) satisfies
1977
+ * it. Omit it and the endpoint responds `STORAGE_URL_NOT_CONFIGURED` — the
1978
+ * studio surfaces a clear inline error.
1979
+ */
1980
+ storageSignedUrl?: StorageSignedUrlFunction;
1981
+ /**
1982
+ * Uploads one object, backing the admin-gated `PUT /_lunora/admin/storage`
1983
+ * endpoint the studio's file browser calls. Passing `createStorage(...).upload`
1984
+ * satisfies it. Omit it and the endpoint responds
1985
+ * `STORAGE_UPLOAD_NOT_CONFIGURED` — the studio surfaces a clear inline error.
1986
+ */
1987
+ storageUpload?: StorageUploadFunction;
1988
+ /**
1989
+ * Page the `.global()` (D1) change-data-capture log for the admin sync
1990
+ * endpoint. When omitted, the sync feed covers only shard-local tables.
1991
+ */
1992
+ syncGlobals?: GlobalCdcSyncFunction;
1993
+ /**
1994
+ * Read-only introspector for Vectorize indexes, backing the studio's vector
1995
+ * browser via `GET /_lunora/admin/vector/indexes` and
1996
+ * `POST /_lunora/admin/vector/query`. Build it from the generated
1997
+ * `LUNORA_VECTOR_INDEXES` registry plus the env Vectorize bindings (and the
1998
+ * schema's embedders, to enable similarity queries). Omit it and those
1999
+ * endpoints respond `VECTORS_NOT_CONFIGURED`.
2000
+ */
2001
+ vectorIntrospector?: VectorIntrospector;
2002
+ /**
2003
+ * Resolver for the Cloudflare Workflows REST client, built from the
2004
+ * deployment `env` (its `CLOUDFLARE_ACCOUNT_ID` / `CLOUDFLARE_API_TOKEN`).
2005
+ * Set by the codegen-emitted worker entry (which depends on
2006
+ * `@lunora/workflow`); when omitted, the `/_lunora/admin/workflows*` proxy
2007
+ * reports "not configured" and the studio shows the credentials empty state.
2008
+ */
2009
+ workflowsClient?: (env: unknown) => undefined | WorkflowsRestClient;
2010
+ }
2011
+ interface RpcContext {
2012
+ ctx: ExecutionContextLike;
2013
+ env: unknown;
2014
+ request: Request;
2015
+ shardKey: string;
2016
+ }
2017
+ /**
2018
+ * The composed Lunora worker. `fetch` / `scheduled` are the standard Cloudflare
2019
+ * module-worker entrypoints (so the object can be re-exported directly as
2020
+ * `export default createWorker(...)`). `serverQuery` is the in-process fast-path
2021
+ * (PLAN4 §2.2) an SSR loader running inside the same worker calls to reach a
2022
+ * Lunora query without a self-`fetch` to `/_lunora/rpc`, with identity / RLS /
2023
+ * auth semantics identical to the HTTP path.
2024
+ */
2025
+ interface LunoraWorker {
2026
+ fetch: (request: Request, env: unknown, context: ExecutionContextLike) => Promise<Response>;
2027
+ /**
2028
+ * Cloudflare Queues consumer entry — present only when the app declares push
2029
+ * queues. Forwards each delivered `MessageBatch` to the configured
2030
+ * {@link WorkerOptions.queue} handler; a no-op when none is set.
2031
+ */
2032
+ queue?: (batch: unknown, env: unknown, context: ExecutionContextLike) => Promise<void>;
2033
+ scheduled: (controller: ScheduledControllerLike, env: unknown, context: ExecutionContextLike) => Promise<void>;
2034
+ /**
2035
+ * In-process query/mutation dispatch for SSR loaders co-located in this
2036
+ * worker. Resolves identity off `request` (cookies / bearer / bookmark) and
2037
+ * runs the per-shard authorization gate exactly like `POST /_lunora/rpc`,
2038
+ * then dispatches to the owning shard — no network self-fetch. Returns the
2039
+ * raw shard {@link Response}, byte-identical to the HTTP path's, so callers
2040
+ * can `.json()` it (`{ result }` / `{ error }`) or forward it verbatim. Like
2041
+ * the worker's `fetch`, it never throws on a request fault: a denied auth
2042
+ * gate, a bad reference, or a downstream error comes back as the SAME JSON
2043
+ * error `Response` (`toErrorResponse`) the HTTP path returns.
2044
+ * @param request The inbound SSR request — its `cookie` / `authorization`
2045
+ * / `x-d1-bookmark` headers drive identity, exactly as the
2046
+ * HTTP RPC path reads them.
2047
+ * @param env The worker `env`, forwarded to `resolveIdentity`.
2048
+ * @param reference A generated function reference (`api.foo.bar`); its
2049
+ * `__lunoraRef` is the `"namespace:fn"` dispatched.
2050
+ * @param args The function arguments.
2051
+ * @param options Call options mirroring the RPC envelope.
2052
+ * @param options.shardKey Routes to a specific shard (omitted → the worker's
2053
+ * `defaultShardKey`).
2054
+ */
2055
+ serverQuery: (request: Request, env: unknown, reference: unknown, args?: Record<string, unknown>, options?: {
2056
+ shardKey?: string;
2057
+ }) => Promise<Response>;
2058
+ }
2059
+ /**
2060
+ * Build a Cloudflare Worker entry. Returns an object with `fetch` so it can
2061
+ * be re-exported directly as `export default createWorker(...)`.
2062
+ */
2063
+ declare const createWorker: (options: WorkerOptions) => LunoraWorker;
2064
+ /**
2065
+ * Compose a meta-framework SSR handler and Lunora into a single Cloudflare
2066
+ * Worker (PLAN4 §1, §2.2). Thin sugar over {@link createWorker} — a
2067
+ * near-pass-through whose value is naming and a documented, framework-neutral
2068
+ * entrypoint, so a template reads cleanly:
2069
+ *
2070
+ * ```ts
2071
+ * import { composeWorker } from "@lunora/runtime";
2072
+ *
2073
+ * export default composeWorker({
2074
+ * httpRouter: ssrHandler, // TanStack Start / React Router / SolidStart / …
2075
+ * shardDO: env.SHARD,
2076
+ * auth,
2077
+ * });
2078
+ * ```
2079
+ *
2080
+ * `httpRouter` is *any* meta-framework SSR handler — structurally an
2081
+ * {@link HttpRouterLike} (`{ fetch(request, env?, ctx?) }`). It is the
2082
+ * lowest-priority matcher: the worker dispatches auth (`/api/auth/*`), explicit
2083
+ * {@link WorkerOptions.routes}, and the reserved realtime endpoints
2084
+ * (`/_lunora/rpc`, `/_lunora/ws`, `/_lunora/admin/*`) first, then falls through
2085
+ * to `httpRouter.fetch` for everything else. An SSR render that throws is
2086
+ * contained at that seam and surfaced as a plain 500 — it can never take down
2087
+ * the realtime plane (see `dispatchHttpRoute`). The two flows share one worker
2088
+ * but never collide.
2089
+ *
2090
+ * The signature is identical to {@link createWorker}; pass exactly the same
2091
+ * options. Prefer this name in framework templates to make the composition
2092
+ * intent explicit.
2093
+ */
2094
+ declare const composeWorker: (options: WorkerOptions) => LunoraWorker;
2095
+ /**
2096
+ * A meta-framework's emitted Cloudflare handler: either a bare `fetch` function
2097
+ * or a `{ fetch }` module object (optionally carrying its own `scheduled`). Every
2098
+ * class-B adapter output (`@sveltejs/adapter-cloudflare`, Nitro's
2099
+ * `cloudflare-module`, `@astrojs/cloudflare`) is structurally one of these.
2100
+ */
2101
+ type FrameworkHostHandler = ((request: Request, env?: unknown, context?: ExecutionContextLike) => Promise<Response> | Response) | (HttpRouterLike & {
2102
+ scheduled?: (controller: ScheduledControllerLike, env: unknown, context: ExecutionContextLike) => Promise<void> | void;
2103
+ });
2104
+ /** Lunora worker options for {@link withFrameworkWorker} — everything except `httpRouter` (supplied from the framework host). */
2105
+ type FrameworkWorkerOptions = Omit<WorkerOptions, "httpRouter">;
2106
+ /**
2107
+ * Either fixed {@link FrameworkWorkerOptions}, or a factory deriving them from the
2108
+ * per-request `env` — for bindings (like `env.SHARD` → `shardDO`) that only exist
2109
+ * at request time.
2110
+ */
2111
+ type FrameworkWorkerOptionsInput = ((env: unknown) => FrameworkWorkerOptions) | FrameworkWorkerOptions;
2112
+ /**
2113
+ * Compose a meta-framework's Cloudflare Worker handler with Lunora's realtime
2114
+ * plane into one `{ fetch, scheduled }` Worker — the **single, shared** class-B
2115
+ * (own-CF-adapter, hook-injection) composer behind `@lunora/svelte/worker`,
2116
+ * `@lunora/vue/worker`, and `@lunora/astro`'s `withLunora` (PLAN4 §3). It wraps
2117
+ * the framework handler as {@link composeWorker}'s `httpRouter`, so the reserved
2118
+ * realtime endpoints (`/_lunora/rpc`, `/_lunora/ws`, `/_lunora/admin/*`) plus
2119
+ * auth/explicit `routes` go to Lunora and **everything else** delegates to the
2120
+ * framework. A framework render that throws is contained at the seam and
2121
+ * surfaced as a plain 500 — it can never take down the realtime plane.
2122
+ *
2123
+ * Owns the three behaviors the adapters otherwise each re-implemented (and
2124
+ * diverged on): (1) the host may be a bare `fetch` fn or a `{ fetch }` object;
2125
+ * (2) options may be a fixed object or an `(env) => options` factory, rebuilt per
2126
+ * request so per-request bindings wire in; (3) **`scheduled` preservation** — when
2127
+ * Lunora configures no cron surface, the framework host's own `scheduled` (if any)
2128
+ * is preserved rather than silently dropped; otherwise Lunora owns it (crons /
2129
+ * backup).
2130
+ * @param host The framework's emitted Cloudflare handler.
2131
+ * @param optionsInput Lunora options minus `httpRouter`, or an `(env) => options` factory.
2132
+ */
2133
+ declare const withFrameworkWorker: (host: FrameworkHostHandler, optionsInput: FrameworkWorkerOptionsInput) => LunoraWorker;
2134
+ /**
2135
+ * Options for {@link createLunoraHandler}. Either an `(env) => options` factory
2136
+ * (full control — for bindings that only exist at request time), or a partial
2137
+ * {@link FrameworkWorkerOptions} object whose `shardDO` defaults to the
2138
+ * conventional `env.SHARD` binding. Pass nothing for the common case.
2139
+ */
2140
+ type LunoraHandlerOptions = ((env: unknown) => FrameworkWorkerOptions) | Partial<FrameworkWorkerOptions>;
2141
+ /**
2142
+ * Resolve per-request Lunora worker options. A factory is called with the
2143
+ * request `env`; a partial object has its `shardDO` defaulted to `env.SHARD` so
2144
+ * the common case needs no configuration. Throws a clear error when no shard
2145
+ * namespace can be found — a wiring mistake, not a runtime condition to swallow.
2146
+ */
2147
+ declare const resolveLunoraOptions: (options: LunoraHandlerOptions, env: unknown) => FrameworkWorkerOptions;
2148
+ /**
2149
+ * Build a framework-neutral request handler for Lunora's realtime plane
2150
+ * (`/_lunora/rpc`, `/_lunora/ws`, `/_lunora/admin/*`). This is the **one shared
2151
+ * seam** every web-standard framework integration mounts — Hono, Nitro/h3,
2152
+ * Elysia, or any WinterCG host running on Cloudflare Workers — so each is a
2153
+ * 1–2 line bridge (`(request, env, ctx) => Response`) rather than a bespoke
2154
+ * adapter package.
2155
+ *
2156
+ * Mount it under `/_lunora/*` (or whatever path you reserve) inside your app's
2157
+ * router; everything else stays your framework's. The host supplies, per
2158
+ * request: a Web `Request`, the Cloudflare `env` (carrying the `SHARD` Durable
2159
+ * Object namespace), and — when available — the `ExecutionContext`. The
2160
+ * `101 Switching Protocols` WebSocket-upgrade `Response` (with its `webSocket`)
2161
+ * is returned verbatim, so the framework streams the socket through unchanged.
2162
+ *
2163
+ * ```ts
2164
+ * // Hono
2165
+ * const lunora = createLunoraHandler();
2166
+ * app.use("/_lunora/*", (c) => lunora(c.req.raw, c.env, c.executionCtx));
2167
+ *
2168
+ * // Nitro / h3
2169
+ * const lunora = createLunoraHandler();
2170
+ * export default defineEventHandler((event) => {
2171
+ * const { ctx, env } = event.context.cloudflare;
2172
+ * return lunora(toWebRequest(event), env, ctx);
2173
+ * });
2174
+ * ```
2175
+ *
2176
+ * `shardDO` defaults to `env.SHARD`; pass `options` (or an `(env) => options`
2177
+ * factory) to add `auth`, `crons`, a `security` posture, or a custom namespace.
2178
+ * A new worker is composed per request because the options (and the `SHARD`
2179
+ * binding they default from) are only known once `env` arrives.
2180
+ * @param options Partial worker options (default `shardDO: env.SHARD`), or an `(env) => options` factory.
2181
+ */
2182
+ declare const createLunoraHandler: (options?: LunoraHandlerOptions) => ((request: Request, env: unknown, context?: ExecutionContextLike) => Promise<Response>);
2183
+ /** Re-exported helper so callers can roundtrip envelopes in tests. */
2184
+ declare const defineRpcEnvelope: (envelope: RpcEnvelope) => RpcEnvelope;
2185
+ /**
2186
+ * Reader / counter capabilities, typed against the SAME canonical
2187
+ * `DatabaseWriterLike` the `@lunora/d1` ctx-db derives its `crossShardReader` /
2188
+ * `crossShardCounter` options from (`DatabaseWriterLike["findMany"]` /
2189
+ * `["count"]`) — so the pair drops straight into `createD1CtxDb` with no cast and
2190
+ * no structural drift. The import is type-only: `@lunora/runtime` keeps no hard
2191
+ * (value) dependency on `@lunora/do`.
2192
+ */
2193
+ type CrossShardCounter = DatabaseWriterLike["count"];
2194
+ type CrossShardReader = DatabaseWriterLike["findMany"];
2195
+ interface CrossShardRelationOptions {
2196
+ /**
2197
+ * `fetch` used for the worker subrequest. Defaults to `globalThis.fetch`.
2198
+ * Injectable so the in-DO loopback (or a test) can supply its own.
2199
+ */
2200
+ fetch?: typeof globalThis.fetch;
2201
+ /** Forwarded identity claims (the `x-lunora-identity` envelope), when present. */
2202
+ identity?: Record<string, unknown>;
2203
+ /**
2204
+ * Origin the worker is reachable at (`LUNORA_WORKER_ORIGIN`). The DO issues a
2205
+ * loopback subrequest to `${origin}/_lunora/rpc`.
2206
+ */
2207
+ origin: string;
2208
+ /** Forwarded user id (the `x-lunora-userid` header), when authenticated. */
2209
+ userId?: string;
2210
+ }
2211
+ interface CrossShardRelationCapabilities {
2212
+ crossShardCounter: CrossShardCounter;
2213
+ crossShardReader: CrossShardReader;
2214
+ }
2215
+ /**
2216
+ * Build the `crossShardReader` / `crossShardCounter` pair for a single request,
2217
+ * wired to fan reverse-relation reads out across every shard via the worker's
2218
+ * coordinator. Pass the result straight into `createD1CtxDb`.
2219
+ */
2220
+ declare const createCrossShardRelationCapabilities: (options: CrossShardRelationOptions) => CrossShardRelationCapabilities;
2221
+ /**
2222
+ * Conventional DO instance name. Kept in sync with `SHARD_REGISTRY_DO_NAME`
2223
+ * in `@lunora/do` (not imported to avoid the runtime → do dependency edge —
2224
+ * `@lunora/runtime` MUST stay free of a hard `@lunora/do` dep).
2225
+ */
2226
+ declare const SHARD_REGISTRY_DO_NAME: string;
2227
+ /**
2228
+ * Default per-table cache TTL in milliseconds. 30s is a balance between
2229
+ * read amplification (a wide fan-out costs N registry round-trips at
2230
+ * minimum every 30s) and registration latency (newly registered shards
2231
+ * take up to 30s to participate in fan-outs).
2232
+ */
2233
+ declare const DEFAULT_REGISTRY_CACHE_TTL_MS: number;
2234
+ interface DynamicShardRegistryOptions {
2235
+ /**
2236
+ * Override the in-process per-table cache TTL. Set to `0` to disable
2237
+ * caching (every `listShardKeys` call hits the DO — useful only for
2238
+ * tests).
2239
+ */
2240
+ cacheTtlMs?: number;
2241
+ /**
2242
+ * DO instance name. Defaults to {@link SHARD_REGISTRY_DO_NAME}. Override
2243
+ * only if you run multiple isolated registries in one environment.
2244
+ */
2245
+ instanceName?: string;
2246
+ /**
2247
+ * Pin the registry DO to a Cloudflare data-residency jurisdiction. Pass the
2248
+ * same value as the worker's `jurisdiction` so the registry co-locates with
2249
+ * the shards it tracks. Omit for the un-pinned global namespace.
2250
+ */
2251
+ jurisdiction?: DurableObjectJurisdiction;
2252
+ /** DO namespace binding (`env.SHARD_REGISTRY`). */
2253
+ namespace: ShardNamespaceLike;
2254
+ }
2255
+ /**
2256
+ * Extension of {@link ShardRegistry} with the mutator surface a worker
2257
+ * needs to register / unregister shard keys.
2258
+ */
2259
+ interface DynamicShardRegistry extends ShardRegistry {
2260
+ /** Drop the local cache. Pass a table to invalidate one entry; omit for everything. */
2261
+ invalidate: (table?: string) => void;
2262
+ /** Register a shard key as live for `table`. Idempotent. */
2263
+ register: (table: string, shardKey: string) => Promise<void>;
2264
+ /**
2265
+ * Read the full `table → shardKeys` map. Useful for admin / debug UIs;
2266
+ * not on the fan-out hot path.
2267
+ */
2268
+ snapshot: () => Promise<Record<string, ReadonlyArray<string>>>;
2269
+ /** Remove a shard key from `table`'s live set. Idempotent. */
2270
+ unregister: (table: string, shardKey: string) => Promise<void>;
2271
+ }
2272
+ declare const createDynamicShardRegistry: (options: DynamicShardRegistryOptions) => DynamicShardRegistry;
2273
+ interface LunoraErrorBody {
2274
+ error: {
2275
+ code: string;
2276
+ message: string;
2277
+ };
2278
+ }
2279
+ /**
2280
+ * Error type recognised by the runtime's error middleware. Anything thrown
2281
+ * that isn't a `LunoraError` is mapped to a generic 500 with code `INTERNAL`.
2282
+ */
2283
+ declare class LunoraError extends Error {
2284
+ readonly code: string;
2285
+ readonly status: number;
2286
+ constructor(message: string, options?: {
2287
+ cause?: unknown;
2288
+ code?: string;
2289
+ status?: number;
2290
+ });
2291
+ toResponse(): Response;
2292
+ }
2293
+ /** Shape recognised by the runtime's structural error checks. */
2294
+
2295
+ /** Convert any thrown value into a JSON error response. */
2296
+ declare const toErrorResponse: (error: unknown) => Response;
2297
+ /** Shared shape for sinks that can be limited to error events only. */
2298
+ interface OnlyErrorsOption {
2299
+ /** When true, only events with `ok === false` are forwarded. */
2300
+ onlyErrors?: boolean;
2301
+ }
2302
+ /**
2303
+ * A sink that logs each event via `console`.
2304
+ *
2305
+ * Useful as a zero-config default during development, or wired behind
2306
+ * {@link combineSinks} alongside a network sink. Successful events are logged
2307
+ * with `console.log`; error events (`ok === false`) with `console.error`.
2308
+ * @param options Sink options; set `onlyErrors` to log error events only.
2309
+ */
2310
+ declare const consoleSink: (options?: OnlyErrorsOption) => ObservabilitySink;
2311
+ /** Options for {@link webhookSink}. */
2312
+ interface WebhookSinkOptions extends OnlyErrorsOption {
2313
+ /**
2314
+ * Extra headers merged onto the POST. `Content-Type: application/json` is
2315
+ * set by default and may be overridden here (e.g. to add an
2316
+ * `Authorization` / API-key header for Axiom, Datadog, etc.).
2317
+ */
2318
+ headers?: Record<string, string>;
2319
+ /**
2320
+ * Optional redaction hook applied to each event immediately before it is
2321
+ * serialized and shipped. Use it to scrub or drop PII (e.g. strip
2322
+ * `error.message`) before it leaves the worker. Return the (possibly
2323
+ * modified) event to send, or `null`/`undefined` to drop the event
2324
+ * entirely. A throwing `transform` drops the event (fail-closed) so a buggy
2325
+ * redactor can never leak the un-scrubbed payload.
2326
+ */
2327
+ transform?: (event: ObservabilityEvent) => null | ObservabilityEvent | undefined;
2328
+ /** The ingestion endpoint to POST each event to. */
2329
+ url: string;
2330
+ }
2331
+ /**
2332
+ * A fire-and-forget sink that POSTs each event as JSON to an HTTP endpoint.
2333
+ *
2334
+ * This covers Axiom, Datadog, and any generic webhook/log-ingestion service —
2335
+ * point `url` at the ingestion endpoint and supply auth via `headers`. Each
2336
+ * event is sent as its own `fetch`. When the runtime supplies a per-event
2337
+ * `context.waitUntil` (the request's `ctx.waitUntil`), the send is registered
2338
+ * with it so it survives isolate teardown after the response returns; otherwise
2339
+ * it degrades to fire-and-forget. Either way its rejection is swallowed so a
2340
+ * flaky endpoint never surfaces to the caller.
2341
+ *
2342
+ * Privacy: the full event is serialized, including `error.message`, which may
2343
+ * contain user input. See the module-level note. Pass a `transform` callback to
2344
+ * scrub or drop fields before they leave the worker.
2345
+ * @param options Sink options: `url` is the POST target, `headers` are merged
2346
+ * request headers (e.g. an API key), `onlyErrors` ships error events only, and
2347
+ * `transform` redacts/drops each event before send.
2348
+ */
2349
+ declare const webhookSink: (options: WebhookSinkOptions) => ObservabilitySink;
2350
+ /** Options for {@link sentrySink}. */
2351
+ interface SentrySinkOptions extends OnlyErrorsOption {
2352
+ /**
2353
+ * User-supplied capture callback. Wire this to your Sentry client, e.g.
2354
+ * `(event) => Sentry.captureMessage(...)` or `captureException`. Kept as an
2355
+ * injected callback so the runtime takes no dependency on `@sentry/*`.
2356
+ */
2357
+ capture: (event: ObservabilityEvent) => void;
2358
+ }
2359
+ /**
2360
+ * A thin adapter that forwards events to an injected `capture` callback.
2361
+ *
2362
+ * Intentionally does NOT bundle `@sentry/*`: the user wires their own Sentry
2363
+ * client (`captureException` / `captureMessage`) into `capture`, giving Sentry
2364
+ * parity without a hard dependency. The callback is invoked inside a try/catch
2365
+ * so a throwing client can't break dispatch.
2366
+ * @param options Sink options: `capture` is invoked per forwarded event;
2367
+ * `onlyErrors` defaults to true (error events only) — pass `false` for all.
2368
+ */
2369
+ declare const sentrySink: (options: SentrySinkOptions) => ObservabilitySink;
2370
+ /** One Analytics Engine data point — the structural subset {@link analyticsEngineSink} writes. */
2371
+ interface AnalyticsEngineDataPointLike {
2372
+ /** Free-form string dimensions (≤20, ≤5120 bytes total). */
2373
+ blobs?: (null | string)[];
2374
+ /** Numeric metrics (≤20). */
2375
+ doubles?: number[];
2376
+ /** Sampling key(s) — Analytics Engine accepts a single index (≤96 bytes). */
2377
+ indexes?: (null | string)[];
2378
+ }
2379
+ /**
2380
+ * The Cloudflare Analytics Engine dataset binding surface this sink needs — the
2381
+ * `env` binding declared in `wrangler.jsonc` under `analytics_engine_datasets`.
2382
+ * Typed structurally so the runtime takes no dependency on
2383
+ * `@cloudflare/workers-types`.
2384
+ */
2385
+ interface AnalyticsEngineDatasetLike {
2386
+ writeDataPoint: (point: AnalyticsEngineDataPointLike) => void;
2387
+ }
2388
+ /** Options for {@link analyticsEngineSink}. */
2389
+ interface AnalyticsEngineSinkOptions extends OnlyErrorsOption {
2390
+ /** The Analytics Engine dataset binding to write each event to. */
2391
+ dataset: AnalyticsEngineDatasetLike;
2392
+ }
2393
+ /**
2394
+ * A sink that writes each event to a Cloudflare Analytics Engine dataset.
2395
+ *
2396
+ * Analytics Engine is the platform's unbounded-cardinality, sampled time-series
2397
+ * store — the natural backing for high-volume RPC observability metrics, queried
2398
+ * later over SQL. Prefer it over rolling your own counters table for anything
2399
+ * that doesn't need to be exact. Each event maps to one data point.
2400
+ *
2401
+ * indexes: `[functionPath]` — the sampling key, so Analytics Engine samples per
2402
+ * function rather than globally.
2403
+ *
2404
+ * blobs (string dimensions): `[functionPath, ok-or-error, shardKey, error.code,
2405
+ * fanOut.table]` — group/filter dimensions; absent fields are the empty string.
2406
+ *
2407
+ * doubles (numeric metrics): `[durationMs, errorCount, fanOut.shards,
2408
+ * fanOut.failed]` where errorCount is 0 or 1 — so `SUM(double2)` is the error
2409
+ * count and `AVG(double1)` the latency.
2410
+ *
2411
+ * `writeDataPoint` is fire-and-forget on the platform; the call is still wrapped
2412
+ * in a try/catch so a missing/throwing binding can never break dispatch.
2413
+ * @param options Sink options: `dataset` is the AE binding; `onlyErrors` writes
2414
+ * only error events (defaults to all events).
2415
+ */
2416
+ declare const analyticsEngineSink: (options: AnalyticsEngineSinkOptions) => ObservabilitySink;
2417
+ /**
2418
+ * Combine several sinks into one that fans each event out to all of them.
2419
+ *
2420
+ * Each child sink is invoked in order; a throw from one does not prevent the
2421
+ * others from running (each call is individually guarded).
2422
+ * @param sinks The sinks to fan out to.
2423
+ */
2424
+ declare const combineSinks: (...sinks: ObservabilitySink[]) => ObservabilitySink;
2425
+ declare const VERSION: string;
2426
+ export { type AdminTableResolver, type AirbyteMessage, type AnalyticsEngineDataPointLike, type AnalyticsEngineDatasetLike, type AnalyticsEngineSinkOptions, type AuthAdmin, type AuthCapabilities, type AuthImpersonation, type AuthIntrospector, type AuthPage, type AuthSession, type AuthUser, type BackupManifest, type BackupStore, type ConnectorChange, type ConnectorSyncPage, type CorsOptions, type CronHandler, type CronJobDispatch, type CronJobInfo, type CrossShardCounter, type CrossShardReader, type CrossShardRelationCapabilities, type CrossShardRelationOptions, type CsrfOptions, DEFAULT_REGISTRY_CACHE_TTL_MS, type DurableObjectJurisdiction, type DynamicShardRegistry, type DynamicShardRegistryOptions, type ExecutionContextLike, type ExportFanOutRequest, type ExportFanOutResult, type FanOutRequest, type FanOutResult, type FanOutSpec, type FivetranResponse, type FrameworkHostHandler, type FrameworkWorkerOptions, type FrameworkWorkerOptionsInput, type FunctionDescriptor, type FunctionRegistryEntry, type FunctionRegistryLike, type GlobalExportFunction as GlobalExportFn, type GlobalImportFunction as GlobalImportFn, type GlobalIntrospector, type GlobalTableInfo as GlobalTableInfoMeta, type GlobalTablePage as GlobalTablePageMeta, type HttpActionContext, type HttpActionLike, type HttpRouterLike, type ImportFanOutRequest, type ImportFanOutResult, type ListAuthUsersOptions, type LogEvent, type LogLevel, LunoraError, type LunoraErrorBody, type LunoraHandlerOptions, type LunoraWorker, type MergeStrategy, type MigrationFanOutRequest, type MigrationFanOutResult, NOOP_EXECUTION_CONTEXT, type ObservabilityEvent, type ObservabilitySink, type ObservabilitySinkContext, type QueryCoordinator, type QueryCoordinatorOptions, type RankFanOutRequest, type RankFanOutResult, type RankPageFanOutRequest, type RankPageFanOutResult, type ResolvedSecurity, type ResolvedShard, type Route, type RpcContext, type RpcEnvelope, SHARD_REGISTRY_DO_NAME, type ScheduledControllerLike, type SecurityHeadersOptions, type SecurityOptions, type SentrySinkOptions, type ShardError, type ShardExportOutcome, type ShardImportOutcome, type ShardMigrationOutcome, type ShardNamespaceLike, type ShardRankOutcome, type ShardRankPageOutcome, type ShardRegistry, type ShardTrafficEntry, type ShardTrafficFanOutRequest, type ShardTrafficFanOutResult, type ShardingInfo, type StorageListFunction as StorageListFn, type StorageObject, VERSION, type VectorIndexSummary, type VectorIntrospector, type VectorQueryMatch, type WebhookSinkOptions, type WorkerOptions, analyticsEngineSink, applyJurisdiction, combineSinks, composeWorker, consoleSink, createCrossShardRelationCapabilities, createDynamicShardRegistry, createLunoraHandler, createQueryCoordinator, createStaticShardRegistry, createWorker, decorateResponse, defineRpcEnvelope, emitLogEvent, emitRpcEvent, enforceOrigin, handleCorsPreflight, mergeStrategyForAggregate, resolveLunoraOptions, resolveSecurity, resolveShard, sentrySink, toAirbyteMessages, toErrorResponse, toFivetranResponse, webhookSink, withFrameworkWorker };