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