@lunora/codegen 1.0.0-alpha.54 → 1.0.0-alpha.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/index.d.mts +65 -6
  2. package/dist/index.d.ts +65 -6
  3. package/dist/index.mjs +1 -38
  4. package/dist/packem_shared/AGENTS_FILENAME-BesaDYkL.mjs +1 -0
  5. package/dist/packem_shared/CONTAINERS_FILENAME-CFwdyaCI.mjs +1 -0
  6. package/dist/packem_shared/CodegenDiagnosticError-DPezpZTz.mjs +1 -0
  7. package/dist/packem_shared/FLAGS_FILENAME-DW5oGq1q.mjs +1 -0
  8. package/dist/packem_shared/GENERATED_HEADER-C94PuLpQ.mjs +1 -0
  9. package/dist/packem_shared/LUNORA_ERROR_CODES-Um9hC1gr.mjs +1 -0
  10. package/dist/packem_shared/MUTATORS_FILENAME-BKKPROzu.mjs +1 -0
  11. package/dist/packem_shared/NOTIFY_FILENAME-C-c7Lg-U.mjs +1 -0
  12. package/dist/packem_shared/OPENRPC_VERSION-DpV3v2W8.mjs +3 -0
  13. package/dist/packem_shared/QUEUES_FILENAME-CSE6i-ld.mjs +1 -0
  14. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-DJsxljOQ.mjs +3 -0
  15. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-Bpf1FGMG.mjs +15 -0
  16. package/dist/packem_shared/SHAPES_FILENAME-DTkSJSsg.mjs +1 -0
  17. package/dist/packem_shared/WORKFLOWS_FILENAME-Bk5upkGM.mjs +1 -0
  18. package/dist/packem_shared/buildOpenApiDocument-DXX1G3GZ.mjs +3 -0
  19. package/dist/packem_shared/discover-ast-CezKqEhE.mjs +1 -0
  20. package/dist/packem_shared/discoverAuthApiCalls-DOfSQqVK.mjs +1 -0
  21. package/dist/packem_shared/discoverCrons-B09_uEuL.mjs +1 -0
  22. package/dist/packem_shared/discoverFunctions-BdibLQmc.mjs +1 -0
  23. package/dist/packem_shared/discoverHttpRoutes-BOWz6y_n.mjs +1 -0
  24. package/dist/packem_shared/discoverInserts-BtteTyNy.mjs +1 -0
  25. package/dist/packem_shared/discoverMaskProcedures-COPmwMBm.mjs +1 -0
  26. package/dist/packem_shared/discoverMigrations-iGMlp9YJ.mjs +1 -0
  27. package/dist/packem_shared/discoverNondeterministicCalls-Dmgc4QBx.mjs +1 -0
  28. package/dist/packem_shared/discoverQueries-CcfuGKHh.mjs +1 -0
  29. package/dist/packem_shared/discoverR2sqlCalls-CjZKvg-N.mjs +1 -0
  30. package/dist/packem_shared/discoverRlsMetadata-BXmNOgry.mjs +1 -0
  31. package/dist/packem_shared/discoverSandboxUsage-CDdBqWcM.mjs +1 -0
  32. package/dist/packem_shared/discoverSchema-m2voTncu.mjs +1 -0
  33. package/dist/packem_shared/discoverStorageRulesMetadata-b71cJJHz.mjs +1 -0
  34. package/dist/packem_shared/emit-Dr6XSLfW.mjs +2104 -0
  35. package/dist/packem_shared/emitApp-B671iU7r.mjs +413 -0
  36. package/dist/packem_shared/formatAdvisories-DZvv-0MG.mjs +2 -0
  37. package/dist/packem_shared/parse-validator-CSDlMEvE.mjs +1 -0
  38. package/dist/packem_shared/paths-BmX5O1sG.mjs +1 -0
  39. package/dist/packem_shared/redact-CQ8-to6l.mjs +1 -0
  40. package/dist/packem_shared/schemaFromIr-R1ZFzVyy.mjs +1 -0
  41. package/package.json +3 -3
  42. package/dist/packem_shared/AGENTS_FILENAME-Cxx7wiSs.mjs +0 -158
  43. package/dist/packem_shared/CONTAINERS_FILENAME-DjpXMqhp.mjs +0 -224
  44. package/dist/packem_shared/CodegenDiagnosticError-DyQ5FwkM.mjs +0 -24
  45. package/dist/packem_shared/FLAGS_FILENAME-D5igzqwR.mjs +0 -139
  46. package/dist/packem_shared/GENERATED_HEADER-C8tAvxqP.mjs +0 -4
  47. package/dist/packem_shared/LUNORA_ERROR_CODES-DvTLozCu.mjs +0 -68
  48. package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +0 -81
  49. package/dist/packem_shared/NOTIFY_FILENAME-CK2IrsrP.mjs +0 -152
  50. package/dist/packem_shared/OPENRPC_VERSION-DvDChMhC.mjs +0 -60
  51. package/dist/packem_shared/QUEUES_FILENAME-Xv6Vy-Ku.mjs +0 -145
  52. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME--TKFIHmr.mjs +0 -3597
  53. package/dist/packem_shared/SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs +0 -249
  54. package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +0 -94
  55. package/dist/packem_shared/WORKFLOWS_FILENAME-Dtuzvluc.mjs +0 -158
  56. package/dist/packem_shared/buildOpenApiDocument-DCQSLuFc.mjs +0 -262
  57. package/dist/packem_shared/discover-ast-CT6BgBr4.mjs +0 -13
  58. package/dist/packem_shared/discoverAuthApiCalls-Dhwgf4J4.mjs +0 -62
  59. package/dist/packem_shared/discoverCrons-UbQLLmMS.mjs +0 -286
  60. package/dist/packem_shared/discoverFunctions-COG_Xkyz.mjs +0 -544
  61. package/dist/packem_shared/discoverHttpRoutes-CVXWZkzo.mjs +0 -139
  62. package/dist/packem_shared/discoverInserts-DdWnXmLS.mjs +0 -61
  63. package/dist/packem_shared/discoverMaskProcedures-BG58_fw3.mjs +0 -273
  64. package/dist/packem_shared/discoverMigrations-Bw1U01zr.mjs +0 -97
  65. package/dist/packem_shared/discoverNondeterministicCalls-BbXEFZUT.mjs +0 -122
  66. package/dist/packem_shared/discoverQueries-CDmxu2ET.mjs +0 -62
  67. package/dist/packem_shared/discoverR2sqlCalls-Bn7QVv7J.mjs +0 -80
  68. package/dist/packem_shared/discoverRlsMetadata-D9uN35no.mjs +0 -280
  69. package/dist/packem_shared/discoverSandboxUsage-B5jDU6Bt.mjs +0 -38
  70. package/dist/packem_shared/discoverSchema-BJp5_T4o.mjs +0 -906
  71. package/dist/packem_shared/discoverStorageRulesMetadata-BqMhuEjo.mjs +0 -97
  72. package/dist/packem_shared/emit-DIbr7U31.mjs +0 -3884
  73. package/dist/packem_shared/emitApp-DbMM8iHe.mjs +0 -706
  74. package/dist/packem_shared/formatAdvisories-Cans5TWS.mjs +0 -131
  75. package/dist/packem_shared/parse-validator-BDGV8iue.mjs +0 -144
  76. package/dist/packem_shared/paths-BRd6JHuF.mjs +0 -11
  77. package/dist/packem_shared/redact-oTmsol5A.mjs +0 -33
  78. package/dist/packem_shared/schemaFromIr-DTYsLBaA.mjs +0 -57
@@ -1,3884 +0,0 @@
1
- import { agentComponent } from '@lunora/agent/component';
2
- import { LunoraError } from '@lunora/errors';
3
- import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
4
-
5
- const CAPABILITY_ROWS = [
6
- // The middleware (`accessContext()` / `accessRoles()`) imports the `/context`
7
- // and `/roles` subpaths, NOT the bare `@lunora/cloudflare-access` specifier —
8
- // so the per-procedure middleware never trips the global `ctx.access` wiring.
9
- // A handler reading `ctx.access` is the signal that wires it onto every ctx.
10
- // `access` has a synchronous facade type, so its ctx field stays bespoke in
11
- // `emit.ts` (no `serverCtxField` here).
12
- { contextProperty: "access", key: "access", moduleSpecifier: "@lunora/cloudflare-access" },
13
- {
14
- appMethod: { configKey: "ai", doc: "Override the Workers AI binding backing `ctx.ai` (defaults to `env.AI`).", method: "ai" },
15
- contextProperty: "ai",
16
- key: "ai",
17
- moduleSpecifier: "@lunora/ai"
18
- },
19
- {
20
- appMethod: {
21
- configKey: "analytics",
22
- doc: "Override the Analytics Engine dataset backing `ctx.analytics` (defaults to `env.ANALYTICS`).",
23
- method: "analytics"
24
- },
25
- contextProperty: "analytics",
26
- key: "analytics",
27
- moduleSpecifier: "@lunora/bindings/analytics",
28
- // `ctx.analytics` — Analytics Engine write helper. EVERY ctx: a write-only,
29
- // fire-and-forget side effect, not a determinism hazard for reads.
30
- serverCtxField: {
31
- field: `
32
- /** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
33
- readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;`,
34
- tier: "every"
35
- }
36
- },
37
- {
38
- appMethod: {
39
- configKey: "browser",
40
- doc: "Override the Browser Rendering binding backing `ctx.browser` (defaults to `env.BROWSER`).",
41
- method: "browser"
42
- },
43
- contextProperty: "browser",
44
- key: "browser",
45
- moduleSpecifier: "@lunora/browser",
46
- // `ctx.browser` — Browser Rendering. ActionCtx ONLY: non-deterministic network I/O.
47
- serverCtxField: {
48
- field: `
49
- /** Browser Rendering (screenshots/PDF/scrape). Non-deterministic — available only in actions. */
50
- readonly browser: import("@lunora/browser").Browser;`,
51
- tier: "action"
52
- }
53
- },
54
- // `lunora/containers.ts` imports `defineContainer` from `@lunora/container`,
55
- // and handlers reach live instances via `ctx.containers` — either signals the
56
- // app wires containers, so the studio should show the Containers page. The ctx
57
- // field is a per-declaration emitter (kept bespoke), so no `serverCtxField`.
58
- { contextProperty: "containers", key: "container", moduleSpecifier: "@lunora/container" },
59
- // `ctx.flags` — OpenFeature. Umbrella-aware specifier + its own provider
60
- // client, so both the ctx field and the shard fragment stay bespoke.
61
- { contextProperty: "flags", key: "flags", moduleSpecifier: "@lunora/flags" },
62
- {
63
- appMethod: {
64
- configKey: "sql",
65
- doc: "Wire the Hyperdrive SQL client backing `ctx.sql` — build it with `createHyperdrive` + `fromPostgresJs`/`fromNodePg`/`fromMysql2`.",
66
- method: "hyperdrive"
67
- },
68
- contextProperty: "sql",
69
- key: "hyperdrive",
70
- moduleSpecifier: "@lunora/hyperdrive",
71
- // `ctx.sql` — Hyperdrive (external Postgres/MySQL). ActionCtx ONLY: external,
72
- // non-deterministic I/O whose writes are invisible to Lunora live queries.
73
- serverCtxField: {
74
- field: `
75
- /**
76
- * External database access via Hyperdrive. Non-deterministic — available only in actions. Writes here are NOT tracked by Lunora live queries; subscriptions will not re-run on external DB changes.
77
- */
78
- readonly sql: import("@lunora/hyperdrive").SqlClient;`,
79
- tier: "action"
80
- }
81
- },
82
- {
83
- appMethod: { configKey: "images", doc: "Override the Images binding backing `ctx.images` (defaults to `env.IMAGES`).", method: "images" },
84
- contextProperty: "images",
85
- key: "images",
86
- moduleSpecifier: "@lunora/bindings/images",
87
- // `ctx.images` — Cloudflare Images binding transforms. ActionCtx ONLY: non-deterministic compute/network I/O.
88
- serverCtxField: {
89
- field: `
90
- /** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
91
- readonly images: import("@lunora/bindings/images").Images;`,
92
- tier: "action"
93
- }
94
- },
95
- {
96
- appMethod: { configKey: "kv", doc: "Override the Workers KV binding backing `ctx.kv` (defaults to `env.KV`).", method: "kv" },
97
- contextProperty: "kv",
98
- key: "kv",
99
- moduleSpecifier: "@lunora/bindings/kv",
100
- // `ctx.kv` — Workers KV. Typed on EVERY ctx (a KV read is allowed in a
101
- // deterministic read path the way `ctx.db` is; the binding is user-named).
102
- serverCtxField: { field: `
103
- readonly kv: import("@lunora/bindings/kv").Kv;`, tier: "every" }
104
- },
105
- // `mail` is import-only — no `ctx.mail` helper (mail is reached through its own
106
- // client), so only a `@lunora/mail` import flips it.
107
- { key: "mail", moduleSpecifier: "@lunora/mail" },
108
- {
109
- appMethod: { configKey: "payment", doc: "Wire the payment options backing `ctx.payments`.", method: "payment" },
110
- contextProperty: "payments",
111
- key: "payments",
112
- moduleSpecifier: "@lunora/payment"
113
- },
114
- // `ctx.x402` — the x402 agent-wallet pay rail. ActionCtx ONLY: it signs and
115
- // settles real USDC over the network per request. Like `payments`, its ctx
116
- // field is bespoke (a lazily-built, per-run-metered rail), so no
117
- // `serverCtxField` — `emit.ts` hand-wires it.
118
- {
119
- appMethod: {
120
- configKey: "x402",
121
- doc: "Wire the x402 agent-wallet pay rail backing `ctx.x402` — a payment-enabled `fetch` that answers `402` challenges under a mandatory spend policy (ActionCtx-only; spends real funds).",
122
- method: "x402"
123
- },
124
- contextProperty: "x402",
125
- key: "x402",
126
- moduleSpecifier: "@lunora/x402/pay"
127
- },
128
- // Pipelines is its own `@lunora/bindings/pipelines` subpath (distinct from
129
- // `/analytics`), so a real import is a clean signal that won't be flipped by a
130
- // plain analytics import; `ctx.pipelines` reads flip it too.
131
- {
132
- contextProperty: "pipelines",
133
- key: "pipelines",
134
- moduleSpecifier: "@lunora/bindings/pipelines",
135
- // `ctx.pipelines` — Pipelines (R2-backed) ingestion sink. ActionCtx ONLY
136
- // (write-only fire-and-forget, but external I/O — kept off query/mutation).
137
- serverCtxField: {
138
- field: `
139
- /** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
140
- readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;`,
141
- tier: "action"
142
- }
143
- },
144
- {
145
- appMethod: {
146
- configKey: "r2sql",
147
- doc: "Wire the R2 SQL client backing `ctx.r2sql` — build it with `createR2Sql({ accountId, apiToken, bucket })` (defaults to env `R2_SQL_TOKEN` / `R2_SQL_ACCOUNT_ID` / `R2_SQL_BUCKET`).",
148
- method: "r2sql"
149
- },
150
- contextProperty: "r2sql",
151
- key: "r2sql",
152
- moduleSpecifier: "@lunora/bindings/r2sql",
153
- // `ctx.r2sql` — R2 SQL (serverless query engine over Apache Iceberg tables).
154
- // ActionCtx ONLY: external REST I/O, non-deterministic, and non-reactive
155
- // (reads are not tracked by Lunora live queries).
156
- serverCtxField: {
157
- field: `
158
- /**
159
- * R2 SQL over Apache Iceberg tables (window functions, DISTINCT, set operations). Non-deterministic — available only in actions. Reads here are NOT tracked by Lunora live queries.
160
- */
161
- readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;`,
162
- tier: "action"
163
- }
164
- },
165
- { contextProperty: "scheduler", key: "scheduler", moduleSpecifier: "@lunora/scheduler" },
166
- { contextProperty: "storage", key: "storage", moduleSpecifier: "@lunora/storage" },
167
- {
168
- appMethod: { configKey: "vectors", doc: "Wire the Vectorize index map backing `ctx.vectors`.", method: "vectors" },
169
- contextProperty: "vectors",
170
- key: "vectors",
171
- moduleSpecifier: "@lunora/bindings/vectors"
172
- },
173
- { contextProperty: "workflows", key: "workflows", moduleSpecifier: "@lunora/workflow" }
174
- ];
175
- const CAPABILITIES = CAPABILITY_ROWS;
176
- const SERVER_CTX_FIELDS = new Map(
177
- CAPABILITIES.flatMap((capability) => capability.serverCtxField ? [[capability.key, capability.serverCtxField]] : [])
178
- );
179
- const APP_METHOD_CAPABILITIES = CAPABILITY_ROWS.flatMap(
180
- (capability) => "appMethod" in capability ? [{ appMethod: capability.appMethod, key: capability.key }] : []
181
- );
182
-
183
- const LITERAL_VALUE_RE = /^(?:"(?:[^"\\]|\\(?:["\\/bfnrt]|u[0-9A-Fa-f]{4}))*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
184
-
185
- const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
186
- const hasColumnModifier = (node) => node.column !== void 0;
187
- const emitScalarGuard = (kind, inExpr) => {
188
- switch (kind) {
189
- case "bigint": {
190
- return `if (typeof ${inExpr} !== "bigint") return DEFER;
191
- `;
192
- }
193
- case "boolean": {
194
- return `if (typeof ${inExpr} !== "boolean") return DEFER;
195
- `;
196
- }
197
- case "date":
198
- case "number":
199
- case "timestamp": {
200
- return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
201
- `;
202
- }
203
- case "null": {
204
- return `if (${inExpr} !== null) return DEFER;
205
- `;
206
- }
207
- // string / id / storage all parse as a bare string at runtime.
208
- default: {
209
- return `if (typeof ${inExpr} !== "string") return DEFER;
210
- `;
211
- }
212
- }
213
- };
214
- const compileLiteral = (node, inExpr) => {
215
- const literal = node.literalValue?.trim();
216
- if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
217
- return void 0;
218
- }
219
- return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
220
- ` };
221
- };
222
- const compileNode = (node, inExpr, context) => {
223
- if (hasColumnModifier(node)) {
224
- return void 0;
225
- }
226
- if (node.hasRefinement || node.sourceText !== void 0) {
227
- return void 0;
228
- }
229
- if (PASS_THROUGH_KINDS.has(node.kind)) {
230
- if (node.kind === "any") {
231
- return { out: inExpr, pre: "" };
232
- }
233
- return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
234
- }
235
- switch (node.kind) {
236
- case "array": {
237
- return compileArray(node, inExpr, context);
238
- }
239
- case "literal": {
240
- return compileLiteral(node, inExpr);
241
- }
242
- case "object": {
243
- return compileObject(node, inExpr, context);
244
- }
245
- default: {
246
- return void 0;
247
- }
248
- }
249
- };
250
- const compileArray = (node, inExpr, context) => {
251
- const { inner } = node;
252
- if (!inner) {
253
- return void 0;
254
- }
255
- const id = context.next();
256
- const array = `__arr${String(id)}`;
257
- const index = `__i${String(id)}`;
258
- const element = `__e${String(id)}`;
259
- const innerEmit = compileNode(inner, element, context);
260
- if (!innerEmit) {
261
- return void 0;
262
- }
263
- const pre = `if (!Array.isArray(${inExpr})) return DEFER;
264
- const ${array} = new Array(${inExpr}.length);
265
- for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
266
- const ${element} = ${inExpr}[${index}];
267
- ${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
268
- }
269
- `;
270
- return { out: array, pre };
271
- };
272
- const compileField = (key, node, access, context) => {
273
- const keyLiteral = JSON.stringify(key);
274
- if (node.kind === "optional") {
275
- if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
276
- return void 0;
277
- }
278
- const { inner } = node;
279
- if (!inner) {
280
- return void 0;
281
- }
282
- const innerEmit = compileNode(inner, access, context);
283
- if (!innerEmit) {
284
- return void 0;
285
- }
286
- const id = context.next();
287
- const has = `__has${String(id)}`;
288
- const value = `__val${String(id)}`;
289
- const pre = `let ${has} = false;
290
- let ${value};
291
- if (${access} !== undefined) {
292
- ${innerEmit.pre}${value} = ${innerEmit.out};
293
- ${has} = true;
294
- }
295
- `;
296
- return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
297
- }
298
- const emit = compileNode(node, access, context);
299
- if (!emit) {
300
- return void 0;
301
- }
302
- return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
303
- };
304
- const compileFields = (shape, accessFor, context) => {
305
- let pre = "";
306
- const entries = [];
307
- for (const key of Object.keys(shape)) {
308
- const node = shape[key];
309
- if (!node) {
310
- return void 0;
311
- }
312
- const field = compileField(key, node, accessFor(key), context);
313
- if (!field) {
314
- return void 0;
315
- }
316
- pre += field.pre;
317
- entries.push(field.entry);
318
- }
319
- return { entries: entries.join(", "), pre };
320
- };
321
- const compileObject = (node, inExpr, context) => {
322
- const shape = node.shape ?? {};
323
- const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
324
- if (!fields) {
325
- return void 0;
326
- }
327
- const id = context.next();
328
- const object = `__obj${String(id)}`;
329
- const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
330
- ${fields.pre}const ${object} = { ${fields.entries} };
331
- `;
332
- return { out: object, pre };
333
- };
334
- const compileArgsValidator = (args) => {
335
- let counter = 0;
336
- const context = {
337
- next: () => {
338
- counter += 1;
339
- return counter;
340
- }
341
- };
342
- const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
343
- if (!fields) {
344
- return void 0;
345
- }
346
- return `(source) => {
347
- if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
348
- ${fields.pre}return { ${fields.entries} };
349
- }`;
350
- };
351
-
352
- const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
353
- const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
354
- client: "lunorash/client",
355
- do: "lunorash/do",
356
- flags: "lunorash/flags",
357
- server: "lunorash/server",
358
- serverDataModel: "lunorash/server/data-model",
359
- serverDrizzle: "lunorash/server/drizzle",
360
- serverTypes: "lunorash/server/types",
361
- values: "lunorash/values"
362
- } : {
363
- client: "@lunora/client",
364
- do: "@lunora/do",
365
- flags: "@lunora/flags",
366
- server: "@lunora/server",
367
- serverDataModel: "@lunora/server/data-model",
368
- serverDrizzle: "@lunora/server/drizzle",
369
- serverTypes: "@lunora/server/types",
370
- values: "@lunora/values"
371
- };
372
- const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
373
- const IMPORT_PATH_RE = /^[\w./-]+$/u;
374
- const assertIdentifier = (value, context) => {
375
- if (!IDENTIFIER_RE.test(value)) {
376
- throw new LunoraError("INTERNAL", `@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
377
- }
378
- };
379
- const renderPropertyKey = (fieldName) => IDENTIFIER_RE.test(fieldName) ? fieldName : JSON.stringify(fieldName);
380
- const SCALAR_TYPE_BY_KIND = {
381
- any: "unknown",
382
- bigint: "bigint",
383
- boolean: "boolean",
384
- bytes: "ArrayBuffer",
385
- // Epoch-millisecond numbers; the distinction is semantic only.
386
- date: "number",
387
- // v.from() wraps an external Standard Schema validator whose output type is
388
- // not statically recoverable at codegen time — emit `unknown` so the
389
- // generated api types still compile without depending on the library.
390
- from: "unknown",
391
- // A geographic point stored as a `{ lat, lng }` JSON object.
392
- geoPoint: "{ lat: number; lng: number }",
393
- null: "null",
394
- number: "number",
395
- // The key of a stored R2 object — a string; the distinction is semantic only.
396
- storage: "string",
397
- string: "string",
398
- timestamp: "number"
399
- };
400
- const literalToType = (value) => {
401
- if (value === void 0) {
402
- return "unknown";
403
- }
404
- if (!LITERAL_VALUE_RE.test(value)) {
405
- throw new LunoraError(
406
- "INTERNAL",
407
- `@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`
408
- );
409
- }
410
- return value;
411
- };
412
- const validatorToType = (validator) => {
413
- const scalar = SCALAR_TYPE_BY_KIND[validator.kind];
414
- if (scalar !== void 0) {
415
- return scalar;
416
- }
417
- switch (validator.kind) {
418
- case "array": {
419
- return `Array<${validator.inner ? validatorToType(validator.inner) : "unknown"}>`;
420
- }
421
- case "id": {
422
- const tableName = validator.tableName ?? "_unknown_";
423
- assertIdentifier(tableName, "v.id() table name");
424
- return `Id<"${tableName}">`;
425
- }
426
- case "literal": {
427
- return literalToType(validator.literalValue);
428
- }
429
- case "object": {
430
- if (!validator.shape) {
431
- return "Record<string, unknown>";
432
- }
433
- const fields = Object.entries(validator.shape).map(([key, child]) => `${renderPropertyKey(key)}: ${validatorToType(child)}`).join("; ");
434
- return `{ ${fields} }`;
435
- }
436
- case "optional": {
437
- return `${validator.inner ? validatorToType(validator.inner) : "unknown"} | undefined`;
438
- }
439
- case "record": {
440
- const key = validator.keyType ? validatorToType(validator.keyType) : "string";
441
- const value = validator.valueType ? validatorToType(validator.valueType) : "unknown";
442
- return `Record<${key}, ${value}>`;
443
- }
444
- case "union": {
445
- return validator.members && validator.members.length > 0 ? validator.members.map((member) => validatorToType(member)).join(" | ") : "never";
446
- }
447
- default: {
448
- return "unknown";
449
- }
450
- }
451
- };
452
- const renderArgsType = (args) => {
453
- const entries = Object.entries(args);
454
- if (entries.length === 0) {
455
- return "{}";
456
- }
457
- return `{ ${entries.map(([key, validator]) => {
458
- const propertyKey = renderPropertyKey(key);
459
- if (validator.kind === "optional") {
460
- const inner = validator.inner ? validatorToType(validator.inner) : "unknown";
461
- return `${propertyKey}?: ${inner}`;
462
- }
463
- return `${propertyKey}: ${validatorToType(validator)}`;
464
- }).join("; ")} }`;
465
- };
466
- const isOptionalOnInsert = (validator) => validator.kind === "optional" || Boolean(validator.column?.hasDefault);
467
- const renderInsertInterface = (table) => {
468
- assertIdentifier(table.name, "table name");
469
- const fields = Object.entries(table.shape).map(([fieldName, validator]) => {
470
- const inner = validator.kind === "optional" && validator.inner ? validator.inner : validator;
471
- const type = validatorToType(inner);
472
- const propertyKey = renderPropertyKey(fieldName);
473
- return isOptionalOnInsert(validator) ? ` ${propertyKey}?: ${type};` : ` ${propertyKey}: ${type};`;
474
- }).join("\n");
475
- const body = fields ? `
476
- ${fields}
477
- ` : "";
478
- return `export interface Insert_${table.name} {
479
- _id?: Id<"${table.name}">;
480
- _creationTime?: number;${body}}`;
481
- };
482
- const emitDataModel = (schema, useUmbrella = false) => {
483
- const base = baseSpecifiers(useUmbrella);
484
- for (const table of schema.tables) {
485
- assertIdentifier(table.name, "table name");
486
- }
487
- const tableNames = schema.tables.map((table) => `"${table.name}"`).join(" | ") || "never";
488
- const documents = schema.tables.map((table) => {
489
- const fields = Object.entries(table.shape).map(([fieldName, validator]) => {
490
- const propertyKey = renderPropertyKey(fieldName);
491
- if (validator.kind === "optional") {
492
- const inner = validator.inner ? validatorToType(validator.inner) : "unknown";
493
- return ` ${propertyKey}?: ${inner};`;
494
- }
495
- return ` ${propertyKey}: ${validatorToType(validator)};`;
496
- }).join("\n");
497
- const body = fields ? `
498
- ${fields}
499
- ` : "";
500
- return `export interface Doc_${table.name} {
501
- _id: Id<"${table.name}">;
502
- _creationTime: number;${body}}`;
503
- }).join("\n\n");
504
- const documentMap = schema.tables.map((table) => ` ${table.name}: Doc_${table.name};`).join("\n");
505
- const indexNamesByTable = schema.tables.map((table) => {
506
- const names = table.indexes.map((index) => JSON.stringify(index.name)).join(" | ");
507
- return ` ${table.name}: ${names || "never"};`;
508
- }).join("\n");
509
- const searchIndexNamesByTable = schema.tables.map((table) => {
510
- const names = table.shardMode === "global" ? "" : table.searchIndexes.map((index) => JSON.stringify(index.name)).join(" | ");
511
- return ` ${table.name}: ${names || "never"};`;
512
- }).join("\n");
513
- const rankIndexNamesByTable = schema.tables.map((table) => {
514
- const names = table.rankIndexes.map((index) => JSON.stringify(index.name)).join(" | ");
515
- return ` ${table.name}: ${names || "never"};`;
516
- }).join("\n");
517
- const geoIndexNamesByTable = schema.tables.map((table) => {
518
- const names = table.shardMode === "global" ? "" : (table.geoIndexes ?? []).map((index) => JSON.stringify(index.name)).join(" | ");
519
- return ` ${table.name}: ${names || "never"};`;
520
- }).join("\n");
521
- const vectorIndexNames = schema.vectorIndexes.map((index) => JSON.stringify(index.name)).join(" | ") || "never";
522
- const insertInterfaces = schema.tables.map((table) => renderInsertInterface(table)).join("\n\n");
523
- const insertMap = schema.tables.map((table) => ` ${table.name}: Insert_${table.name};`).join("\n");
524
- const relationsMap = schema.tables.map((table) => {
525
- const entries = table.relations.map((relation) => {
526
- assertIdentifier(relation.table, "relation target table");
527
- return ` ${renderPropertyKey(relation.name)}: ${relation.kind === "one" ? "OneRelation" : "ManyRelation"}<"${relation.table}">;`;
528
- }).join("\n");
529
- return entries ? ` ${table.name}: {
530
- ${entries}
531
- };` : ` ${table.name}: {};`;
532
- }).join("\n");
533
- return `${GENERATED_HEADER}import type {
534
- DatabaseReaderFacade as DatabaseReaderFacadeOf,
535
- DatabaseWriterFacade as DatabaseWriterFacadeOf,
536
- LoadWith as LoadWithOf,
537
- TableReaderFacade as TableReaderFacadeOf,
538
- TableWriterFacade as TableWriterFacadeOf,
539
- WithArg as WithArgOf,
540
- } from "${base.serverDataModel}";
541
-
542
- export type {
543
- AggregateOp,
544
- GroupByEntry,
545
- OrderBy,
546
- QueryArgs,
547
- QueryPage,
548
- RankPage,
549
- RankResult,
550
- RestrictableQueryOptions,
551
- RestrictableQueryOptionsOf,
552
- SearchFilterBuilder,
553
- SearchReader,
554
- TableAggregateOptions,
555
- TableAggregateOptionsOf,
556
- TableGroupByOptions,
557
- TableGroupByOptionsOf,
558
- TableRankOptions,
559
- TableRankPageOptions,
560
- Where,
561
- WhereOf,
562
- WhereOperators,
563
- } from "${base.serverDataModel}";
564
-
565
- export type TableName = ${tableNames};
566
-
567
- export type Id<TName extends string> = string & { readonly __table: TName };
568
-
569
- ${documents}
570
-
571
- export interface DataModel {
572
- ${documentMap}
573
- }
574
-
575
- export type Doc<T extends keyof DataModel> = DataModel[T];
576
-
577
- /**
578
- * Per-table index name union. \`never\` for tables without secondary indexes.
579
- * Used by \`TableReader.withIndex()\` to constrain callers to declared names.
580
- */
581
- export interface IndexNamesByTable {
582
- ${indexNamesByTable}
583
- }
584
-
585
- export type IndexName<T extends keyof DataModel> = IndexNamesByTable[T];
586
-
587
- /** Per-table search-index name union. \`never\` for tables without searchIndex. */
588
- export interface SearchIndexNamesByTable {
589
- ${searchIndexNamesByTable}
590
- }
591
-
592
- export type SearchIndexName<T extends keyof DataModel> = SearchIndexNamesByTable[T];
593
-
594
- /** Per-table rank-index name union. \`never\` for tables without a rankIndex. */
595
- export interface RankIndexNamesByTable {
596
- ${rankIndexNamesByTable}
597
- }
598
-
599
- export type RankIndexName<T extends keyof DataModel> = RankIndexNamesByTable[T];
600
-
601
- /** Per-table geo-index name union. \`never\` for tables without a geoIndex. */
602
- export interface GeoIndexNamesByTable {
603
- ${geoIndexNamesByTable}
604
- }
605
-
606
- export type GeoIndexName<T extends keyof DataModel> = GeoIndexNamesByTable[T];
607
-
608
- /** Union of declared vector index names. \`never\` when none are declared. */
609
- export type VectorIndexName = ${vectorIndexNames};
610
-
611
- ${insertInterfaces}
612
-
613
- /** Per-table insert shape, accepted by \`ctx.db.<table>.insert(...)\`. */
614
- export interface InsertModel {
615
- ${insertMap}
616
- }
617
-
618
- export type Insert<T extends keyof DataModel> = InsertModel[T];
619
-
620
- /**
621
- * Phantom relation descriptors. They carry the relation kind and target table
622
- * as type parameters only — there is no runtime value — so the \`with\` argument
623
- * and its return type can be inferred from {@link Relations}.
624
- */
625
- export interface OneRelation<Target extends keyof DataModel> {
626
- readonly __relationKind: "one";
627
- readonly __target: Target;
628
- }
629
-
630
- export interface ManyRelation<Target extends keyof DataModel> {
631
- readonly __relationKind: "many";
632
- readonly __target: Target;
633
- }
634
-
635
- /** Per-table relation map keyed by accessor name. \`{}\` for tables with none. */
636
- export interface Relations {
637
- ${relationsMap}
638
- }
639
-
640
- /** The \`with\` argument for table \`T\` — see \`@lunora/server/data-model\`. */
641
- export type WithArg<T extends keyof DataModel> = WithArgOf<DataModel, Relations, T>;
642
-
643
- /** \`Doc<T>\` narrowed to exactly the relations requested in the with-arg \`W\`. */
644
- export type LoadWith<T extends keyof DataModel, W> = LoadWithOf<DataModel, Relations, T, W>;
645
-
646
- /** Read-only typed table accessor exposed on \`QueryCtx.db.<table>\`. */
647
- export type TableReaderFacade<T extends keyof DataModel> = TableReaderFacadeOf<DataModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, T, GeoIndexNamesByTable>;
648
-
649
- /** Read-write typed table accessor exposed on \`MutationCtx.db.<table>\` / \`ActionCtx.db.<table>\`. */
650
- export type TableWriterFacade<T extends keyof DataModel> = TableWriterFacadeOf<DataModel, InsertModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, T, GeoIndexNamesByTable>;
651
-
652
- /** Per-table read facade — \`ctx.db.<table>\` on a \`QueryCtx\`. */
653
- export type DatabaseReaderFacade = DatabaseReaderFacadeOf<DataModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, GeoIndexNamesByTable>;
654
-
655
- /** Per-table read-write facade — \`ctx.db.<table>\` on a \`MutationCtx\` / \`ActionCtx\`. */
656
- export type DatabaseWriterFacade = DatabaseWriterFacadeOf<DataModel, InsertModel, Relations, RankIndexNamesByTable, SearchIndexNamesByTable, GeoIndexNamesByTable>;
657
-
658
- /** Insert builder returned by \`ctx.orm.insert(table)\`. */
659
- export interface OrmInsertBuilder<T extends keyof DataModel> {
660
- values: (values: Insert<T>) => Promise<Id<T>>;
661
- }
662
-
663
- /** Replace builder returned by \`ctx.orm.replace(table, id)\` — swaps the whole document. */
664
- export interface OrmReplaceBuilder<T extends keyof DataModel> {
665
- with: (values: Insert<T>) => Promise<void>;
666
- }
667
-
668
- /** Update builder returned by \`ctx.orm.update(table, id)\` — patches the named fields. */
669
- export interface OrmUpdateBuilder<T extends keyof DataModel> {
670
- set: (values: Partial<Insert<T>>) => Promise<void>;
671
- }
672
-
673
- /** Read-only ORM surface — \`ctx.orm\` on a \`QueryCtx\`. Mirrors \`ctx.db\` reads under a kitcn-style \`query\` namespace. */
674
- export interface OrmReader {
675
- query: DatabaseReaderFacade;
676
- }
677
-
678
- /** Read-write ORM surface — \`ctx.orm\` on a \`MutationCtx\` / \`ActionCtx\`. Writes are addressed by id, like \`ctx.db\`. */
679
- export interface OrmWriter extends OrmReader {
680
- delete: <T extends keyof DataModel>(table: T, id: Id<T>) => Promise<void>;
681
- insert: <T extends keyof DataModel>(table: T) => OrmInsertBuilder<T>;
682
- replace: <T extends keyof DataModel>(table: T, id: Id<T>) => OrmReplaceBuilder<T>;
683
- update: <T extends keyof DataModel>(table: T, id: Id<T>) => OrmUpdateBuilder<T>;
684
- }
685
- `;
686
- };
687
- const GENERATED_IMPORT_RE = /import\("(?<spec>(?:\.\.?\/)*_generated\/[^"]+)"\)/gu;
688
- const GENERATED_PREFIX_RE = /^(?:\.\.?\/)*_generated\//u;
689
- const relocateGeneratedImports = (returnType) => returnType.replaceAll(GENERATED_IMPORT_RE, (_match, spec) => {
690
- const stripped = spec.replace(GENERATED_PREFIX_RE, "./");
691
- return `import("${stripped}")`;
692
- });
693
- const referencedDataModelImports = (body) => ["Doc", "Id"].filter((name) => new RegExp(String.raw`\b${name}<`, "u").test(body));
694
- const renderApiBody = (functions) => {
695
- const namespaces = /* @__PURE__ */ new Map();
696
- for (const definition of functions) {
697
- const list = namespaces.get(definition.filePath) ?? [];
698
- list.push(definition);
699
- namespaces.set(definition.filePath, list);
700
- }
701
- const renderNamespace = ([file, list]) => {
702
- const members = list.map((definition) => {
703
- const argsType = renderArgsType(definition.args);
704
- const returnType = relocateGeneratedImports(definition.returnType);
705
- return ` ${definition.exportName}: FunctionReference<"${definition.kind}", ${argsType}, ${returnType}>;`;
706
- }).join("\n");
707
- return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
708
- ${members}
709
- };`;
710
- };
711
- return [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b)).map((entry) => renderNamespace(entry)).join("\n");
712
- };
713
- const renderSchedulerReferences = (workflows, agents) => {
714
- if (workflows.length === 0 && agents.length === 0) {
715
- return { block: "", importLine: "" };
716
- }
717
- const importLines = [];
718
- let block = "";
719
- if (workflows.length > 0) {
720
- block += `
721
- /** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`). */
722
- type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params } ? (unknown extends Params ? Record<string, unknown> : Params) : Record<string, unknown>;
723
- `;
724
- }
725
- block += `
726
- /** A typed reference to a durable workflow, addressable for \`cronJobs()\` targets. Mirrors \`@lunora/scheduler\`'s \`WorkflowReference\` structurally. */
727
- export interface WorkflowReference<Params = Record<string, unknown>> {
728
- readonly isLunoraWorkflow: true;
729
- readonly __params?: Params;
730
- readonly binding: string;
731
- readonly name: string;
732
- }
733
- `;
734
- if (workflows.length > 0) {
735
- const sorted = [...workflows].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
736
- const refMembers = sorted.map((workflow) => ` ${workflow.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n");
737
- const objectMembers = sorted.map(
738
- (workflow) => ` ${workflow.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(workflow.bindingName)}, name: ${JSON.stringify(workflow.exportName)} },`
739
- ).join("\n");
740
- block += `
741
- /** This project's durable workflows, addressable as typed references (e.g. \`crons.daily("digest", …, workflows.digestPipeline, params)\`). */
742
- export interface WorkflowsRef {
743
- ${refMembers}
744
- }
745
-
746
- export const workflows: WorkflowsRef = {
747
- ${objectMembers}
748
- };
749
- `;
750
- importLines.push(`import type * as lunoraWorkflowDefinitions from "../workflows.js";
751
- `);
752
- }
753
- if (agents.length > 0) {
754
- const sorted = [...agents].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
755
- const refMembers = sorted.map((agent) => ` ${agent.exportName}: WorkflowReference<AgentRunInput>;`).join("\n");
756
- const objectMembers = sorted.map(
757
- (agent) => ` ${agent.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(agent.bindingName)}, name: ${JSON.stringify(agent.name)} },`
758
- ).join("\n");
759
- block += `
760
- /** This project's durable agents, addressable as typed references for \`cronJobs()\` — each fire starts a fresh agent run (e.g. \`crons.daily("sweep", …, agents.support, { input, threadKey })\`). */
761
- export interface AgentsRef {
762
- ${refMembers}
763
- }
764
-
765
- export const agents: AgentsRef = {
766
- ${objectMembers}
767
- };
768
- `;
769
- importLines.push(`import type { AgentRunInput } from "@lunora/agent";
770
- `);
771
- }
772
- return { block, importLine: importLines.join("") };
773
- };
774
- const agentRuntimeFunctionNames = () => Object.keys(agentComponent().functions).toSorted((a, b) => a.localeCompare(b));
775
- const takenAgentFunctionNames = (functions) => new Set(functions.filter((definition) => sanitizeNamespace(definition.filePath) === "agents").map((definition) => definition.exportName));
776
- const internalAgentRuntimeFunctionNames = () => new Set(
777
- Object.entries(agentComponent().functions).filter(([, definition]) => definition.visibility === "internal").map(([name]) => name)
778
- );
779
- const renderAgentFunctionRegistry = (agents, functions) => {
780
- const empty = { importLine: "", lines: "", prelude: "" };
781
- if (agents.length === 0) {
782
- return empty;
783
- }
784
- const taken = takenAgentFunctionNames(functions);
785
- const internal = internalAgentRuntimeFunctionNames();
786
- const shadowedInternal = functions.find((definition) => sanitizeNamespace(definition.filePath) === "agents" && internal.has(definition.exportName));
787
- if (shadowedInternal) {
788
- throw new LunoraError(
789
- "INTERNAL",
790
- `@lunora/codegen: "agents:${shadowedInternal.exportName}" is reserved for the durable agent loop's internal dispatch — rename the "${shadowedInternal.exportName}" export in ${shadowedInternal.filePath}`
791
- );
792
- }
793
- const names = agentRuntimeFunctionNames().filter((name) => !taken.has(name));
794
- if (names.length === 0) {
795
- return empty;
796
- }
797
- return {
798
- importLine: 'import { agentComponent } from "@lunora/agent/component";\n',
799
- lines: names.map((name) => ` "agents:${name}": lunoraAgentRuntimeFunctions.${name} as unknown as RegisteredLunoraFunction,`).join("\n"),
800
- prelude: "\n/**\n * The `@lunora/agent` runtime component — auto-registered because `lunora/agents.ts`\n * declares agents. The durable loop dispatches the internal mutations; clients\n * subscribe to the public queries (`agents:agentMessages` is the live thread view).\n */\nconst lunoraAgentRuntimeFunctions = agentComponent().functions;\n"
801
- };
802
- };
803
- const renderSandboxFunctionRegistry = (usesSandbox, functions) => {
804
- if (!usesSandbox) {
805
- return { importLine: "", lines: "", prelude: "" };
806
- }
807
- const collision = functions.find((definition) => sanitizeNamespace(definition.filePath) === "sandbox" && definition.exportName === "invoke");
808
- if (collision) {
809
- throw new LunoraError(
810
- "INTERNAL",
811
- `@lunora/codegen: "sandbox:invoke" is reserved for the batteries-included sandbox tool dispatcher (browserTool/containerTool) — rename the "invoke" export in ${collision.filePath}`
812
- );
813
- }
814
- return {
815
- importLine: 'import { sandboxComponent } from "@lunora/agent/component";\n',
816
- lines: ' "sandbox:invoke": lunoraSandbox.invoke as unknown as RegisteredLunoraFunction,',
817
- prelude: "\n/**\n * The `@lunora/agent` sandbox dispatcher — auto-registered because `lunora/`\n * imports a sandbox tool (`browserTool`/`containerTool`). The batteries-included\n * tools dispatch to this internal action, which runs on an action ctx carrying\n * `ctx.browser` + `ctx.containers` (the durable tool step itself has neither).\n */\nconst lunoraSandbox = sandboxComponent();\n"
818
- };
819
- };
820
- const syntheticAgentApiFunctions = (agents, functions) => {
821
- if (agents.length === 0) {
822
- return [];
823
- }
824
- const taken = takenAgentFunctionNames(functions);
825
- const definitions = [
826
- {
827
- args: { key: { kind: "string" }, limit: { inner: { kind: "number" }, kind: "optional" } },
828
- exportName: "agentMessages",
829
- filePath: "agents",
830
- kind: "query",
831
- returnType: "Record<string, unknown>[]"
832
- },
833
- {
834
- args: {
835
- decision: {
836
- kind: "union",
837
- members: [
838
- { kind: "literal", literalValue: '"approve"' },
839
- { kind: "literal", literalValue: '"reject"' }
840
- ]
841
- },
842
- instanceId: { kind: "string" },
843
- note: { inner: { kind: "string" }, kind: "optional" },
844
- threadKey: { kind: "string" },
845
- toolCallId: { kind: "string" }
846
- },
847
- exportName: "agentResolveApproval",
848
- filePath: "agents",
849
- kind: "mutation",
850
- returnType: "{ resolved: boolean }"
851
- },
852
- {
853
- args: {
854
- agent: { kind: "string" },
855
- input: { kind: "string" },
856
- threadKey: { kind: "string" },
857
- title: { inner: { kind: "string" }, kind: "optional" }
858
- },
859
- exportName: "agentRun",
860
- filePath: "agents",
861
- kind: "mutation",
862
- returnType: "{ id: string; threadKey: string }"
863
- },
864
- {
865
- args: { key: { kind: "string" } },
866
- exportName: "agentState",
867
- filePath: "agents",
868
- kind: "query",
869
- returnType: "Record<string, unknown> | undefined"
870
- },
871
- {
872
- args: { key: { kind: "string" } },
873
- exportName: "agentThread",
874
- filePath: "agents",
875
- kind: "query",
876
- returnType: "Record<string, unknown> | undefined"
877
- }
878
- ];
879
- for (const agent of agents) {
880
- if (agent.voice) {
881
- definitions.push({
882
- args: { threadKey: { kind: "string" } },
883
- exportName: `${agent.exportName}Voice`,
884
- filePath: "agents",
885
- kind: "stream",
886
- returnType: "Record<string, unknown>"
887
- });
888
- }
889
- }
890
- return definitions.filter((definition) => !taken.has(definition.exportName));
891
- };
892
- const renderHttpStreamsRef = (httpRoutes) => {
893
- const streams = httpRoutes.filter((route) => route.stream);
894
- if (streams.length === 0) {
895
- return { block: "", body: "" };
896
- }
897
- const namespaces = /* @__PURE__ */ new Map();
898
- for (const route of streams) {
899
- const list = namespaces.get(route.filePath) ?? [];
900
- list.push(route);
901
- namespaces.set(route.filePath, list);
902
- }
903
- const sortedNamespaces = [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b));
904
- const typeBody = sortedNamespaces.map(([file, list]) => {
905
- const members = list.map((route) => {
906
- const chunkType = relocateGeneratedImports(route.chunkType ?? "unknown");
907
- return ` ${renderPropertyKey(route.exportName)}: HttpStreamRef<${chunkType}, ${renderArgsType(route.searchParams)}, ${renderArgsType(route.params)}>;`;
908
- }).join("\n");
909
- return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
910
- ${members}
911
- };`;
912
- }).join("\n");
913
- const valueBody = sortedNamespaces.map(([file, list]) => {
914
- const members = list.map(
915
- (route) => ` ${renderPropertyKey(route.exportName)}: { method: ${JSON.stringify(route.method)}, path: ${JSON.stringify(route.path)} },`
916
- ).join("\n");
917
- return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
918
- ${members}
919
- },`;
920
- }).join("\n");
921
- const block = `
922
- /** This project's HTTP-SSE stream routes (\`httpRoute.<verb>(path).stream()\`), addressable as typed references for \`client.httpStream\` / \`useHttpStream\`. */
923
- export interface HttpStreamsRef {
924
- ${typeBody}
925
- }
926
-
927
- export const httpStreams: HttpStreamsRef = {
928
- ${valueBody}
929
- };
930
- `;
931
- return { block, body: typeBody };
932
- };
933
- const emitApi = (options) => {
934
- const { agents = [], functions, httpRoutes = [], useUmbrella = false, workflows = [] } = options;
935
- const base = baseSpecifiers(useUmbrella);
936
- const publicFunctions = [...functions.filter((definition) => definition.visibility !== "internal"), ...syntheticAgentApiFunctions(agents, functions)];
937
- const internalFunctions = functions.filter((definition) => definition.visibility === "internal");
938
- const publicBody = renderApiBody(publicFunctions);
939
- const internalBody = renderApiBody(internalFunctions);
940
- const httpStreamsRef = renderHttpStreamsRef(httpRoutes);
941
- const combinedBody = `${publicBody}
942
- ${internalBody}
943
- ${httpStreamsRef.body}`;
944
- const dataModelImports = referencedDataModelImports(combinedBody);
945
- const dataModelImportLine = dataModelImports.length > 0 ? `
946
- import type { ${dataModelImports.join(", ")} } from "./dataModel.js";
947
- ` : "";
948
- const apiBlock = publicBody ? `
949
- ${publicBody}
950
- ` : "";
951
- const internalBlock = internalBody ? `
952
- ${internalBody}
953
- ` : "";
954
- const schedulerReferences = renderSchedulerReferences(workflows, agents);
955
- const clientImports = httpStreamsRef.block === "" ? "FunctionReference" : "FunctionReference, HttpStreamRef";
956
- return `${GENERATED_HEADER}import { anyApi } from "${base.serverTypes}";
957
- import type { ${clientImports} } from "${base.client}";
958
- ${schedulerReferences.importLine}${dataModelImportLine}
959
- export interface ApiTypes {${apiBlock}}
960
-
961
- export const api = anyApi as unknown as ApiTypes;
962
-
963
- /** Internal functions — callable only server-side via \`ctx.run*\`, never from a client. */
964
- export interface InternalApiTypes {${internalBlock}}
965
-
966
- export const internal = anyApi as unknown as InternalApiTypes;
967
- ${schedulerReferences.block}${httpStreamsRef.block}`;
968
- };
969
- const emitSeed = (enabled) => {
970
- if (!enabled) {
971
- return "";
972
- }
973
- return `${GENERATED_HEADER}import { createSeedClient as createSeedClientBase } from "@lunora/seed";
974
- import type { SeedClient, SeedClientOptions } from "@lunora/seed";
975
-
976
- import schema from "../schema.js";
977
- import type { InsertModel } from "./dataModel.js";
978
-
979
- /**
980
- * Schema-aware seed client with this project's \`InsertModel\` and runtime schema
981
- * pre-bound. Each table is a method; call it with a count, a range, explicit
982
- * partial rows, or per-field overrides. Foreign keys connect to rows seeded
983
- * earlier in the run, and FK-parent tables are seeded automatically.
984
- * @example
985
- * const seed = createSeedClient({ seed: 1 });
986
- * const { users } = await seed.users(5);
987
- * const { posts } = await seed.posts((x) => x([10, 20]));
988
- */
989
- export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
990
- `;
991
- };
992
- const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
993
- if (shapes.length === 0 || !hasDatabase) {
994
- return "";
995
- }
996
- const base = baseSpecifiers(useUmbrella);
997
- const factories = shapes.map((shape) => {
998
- assertIdentifier(shape.exportName, "shape export name");
999
- return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
1000
- export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
1001
- createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
1002
- }).join("\n\n");
1003
- return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
1004
- import { lunoraCollectionOptions } from "@lunora/db/collections";
1005
- import type { Row } from "@lunora/db";
1006
- import type { Collection } from "@tanstack/db";
1007
- import { createCollection } from "@tanstack/db";
1008
-
1009
- ${factories}
1010
- `;
1011
- };
1012
- const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
1013
- const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
1014
- const registered = LUNORA_FUNCTIONS[functionPath];
1015
-
1016
- if (!registered) {
1017
- throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
1018
- }
1019
-
1020
- return (await registered.handler(context, args ?? {})) as R;
1021
- };`;
1022
- const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
1023
- const aliasByPath = /* @__PURE__ */ new Map();
1024
- const registerPath = (filePath) => {
1025
- if (!aliasByPath.has(filePath)) {
1026
- aliasByPath.set(filePath, moduleAlias(filePath, aliasByPath.size));
1027
- }
1028
- };
1029
- for (const definition of functions) {
1030
- registerPath(definition.filePath);
1031
- }
1032
- for (const migration of migrations) {
1033
- registerPath(migration.filePath);
1034
- }
1035
- for (const mutator of mutators) {
1036
- registerPath(mutator.filePath);
1037
- }
1038
- for (const shape of shapes) {
1039
- registerPath(shape.filePath);
1040
- }
1041
- const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
1042
- if (!IMPORT_PATH_RE.test(filePath)) {
1043
- throw new LunoraError("INTERNAL", `@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
1044
- }
1045
- return `import * as ${alias} from "../${filePath}.js";`;
1046
- }).join("\n");
1047
- const dispatchEntries = functions.map(
1048
- (definition) => ` "${sanitizeNamespace(definition.filePath)}:${definition.exportName}": ${aliasByPath.get(definition.filePath) ?? ""}.${definition.exportName} as unknown as RegisteredLunoraFunction,`
1049
- ).join("\n");
1050
- const migrationEntries = migrations.map(
1051
- (migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
1052
- ).join("\n");
1053
- const mutatorEntries = mutators.map(
1054
- (mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
1055
- ).join("\n");
1056
- const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
1057
- const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
1058
- const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
1059
- const installLines = functions.map((definition) => {
1060
- if (definition.lifecycle || Object.keys(definition.args).length === 0) {
1061
- return void 0;
1062
- }
1063
- const compiled = compileArgsValidator(definition.args);
1064
- if (compiled === void 0) {
1065
- return void 0;
1066
- }
1067
- const alias = aliasByPath.get(definition.filePath) ?? "";
1068
- return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
1069
- }).filter((line) => line !== void 0).join("\n");
1070
- return {
1071
- dispatchBody: combinedDispatch.length > 0 ? `
1072
- ${combinedDispatch}
1073
- ` : "",
1074
- importBlock: importLines.length > 0 ? `${importLines}
1075
-
1076
- ` : "",
1077
- installBlock: installLines,
1078
- migrationBody: migrationEntries.length > 0 ? `
1079
- ${migrationEntries}
1080
- ` : "",
1081
- mutatorPaths,
1082
- shapeBody: shapeEntries.length > 0 ? `
1083
- ${shapeEntries}
1084
- ` : ""
1085
- };
1086
- };
1087
- const renderCaller = (functions) => {
1088
- const namespaces = /* @__PURE__ */ new Map();
1089
- for (const definition of functions) {
1090
- const list = namespaces.get(definition.filePath) ?? [];
1091
- list.push(definition);
1092
- namespaces.set(definition.filePath, list);
1093
- }
1094
- const ordered = [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b));
1095
- const types = ordered.map(([file, list]) => {
1096
- const members = list.map((definition) => {
1097
- const argsType = renderArgsType(definition.args);
1098
- const optional = argsType === "{}" ? "?" : "";
1099
- const returnType = relocateGeneratedImports(definition.returnType);
1100
- if (definition.kind === "stream") {
1101
- return ` ${definition.exportName}: (args${optional}: ${argsType}) => Promise<AsyncIterable<${returnType}>>;`;
1102
- }
1103
- return ` ${definition.exportName}: (args${optional}: ${argsType}) => Promise<${returnType}>;`;
1104
- }).join("\n");
1105
- return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
1106
- ${members}
1107
- };`;
1108
- }).join("\n");
1109
- const implementation = ordered.map(([file, list]) => {
1110
- const namespace = sanitizeNamespace(file);
1111
- const leaves = list.map((definition) => ` ${definition.exportName}: (args) => callRegistered(context, "${namespace}:${definition.exportName}", args),`).join("\n");
1112
- return ` ${renderPropertyKey(namespace)}: {
1113
- ${leaves}
1114
- },`;
1115
- }).join("\n");
1116
- return { implementation, types };
1117
- };
1118
- const buildStorageBucketNames = (schema, ruleBuckets = []) => {
1119
- const named = /* @__PURE__ */ new Set();
1120
- for (const table of schema.tables) {
1121
- for (const validator of Object.values(table.shape)) {
1122
- const resolved = validator.kind === "optional" && validator.inner ? validator.inner : validator;
1123
- if (resolved.kind === "storage" && typeof resolved.bucket === "string" && resolved.bucket !== "") {
1124
- named.add(resolved.bucket);
1125
- }
1126
- }
1127
- }
1128
- for (const bucket of ruleBuckets) {
1129
- if (bucket !== "" && bucket !== "default") {
1130
- named.add(bucket);
1131
- }
1132
- }
1133
- return ["default", ...[...named].toSorted((a, b) => a.localeCompare(b))];
1134
- };
1135
- const emitServer = ({
1136
- agents = [],
1137
- containers = [],
1138
- env,
1139
- hasAccessFacade = false,
1140
- hasAi = false,
1141
- hasAnalytics = false,
1142
- hasBrowser = false,
1143
- hasFlags = false,
1144
- hasHyperdrive = false,
1145
- hasImages = false,
1146
- hasKv = false,
1147
- hasNotify = false,
1148
- hasPayments = false,
1149
- hasPipelines = false,
1150
- hasR2sql = false,
1151
- hasX402 = false,
1152
- identity,
1153
- queues = [],
1154
- schema,
1155
- storageRuleBuckets = [],
1156
- useUmbrella = false,
1157
- workflows = []
1158
- } = {}) => {
1159
- const base = baseSpecifiers(useUmbrella);
1160
- const storageBucketUnion = buildStorageBucketNames(schema ?? { tables: []}, storageRuleBuckets).map((name) => JSON.stringify(name)).join(" | ");
1161
- const aiTypeImport = hasAi ? `import type { LunoraAi } from "@lunora/ai";
1162
- ` : "";
1163
- const aiActionField = hasAi ? `
1164
- readonly ai: LunoraAi;` : "";
1165
- const paymentsTypeImport = hasPayments ? `import type { LunoraPayment } from "@lunora/payment";
1166
- ` : "";
1167
- const paymentsActionField = hasPayments ? `
1168
- readonly payments: LunoraPayment;` : "";
1169
- const x402TypeImport = hasX402 ? `import type { X402Pay } from "@lunora/x402/pay";
1170
- ` : "";
1171
- const x402ActionField = hasX402 ? `
1172
- readonly x402: X402Pay;` : "";
1173
- const containersTypeImport = containers.length > 0 ? `import type { ContainerAccessor } from "@lunora/container";
1174
- ` : "";
1175
- const containersActionField = containers.length > 0 ? `
1176
- readonly containers: {${containers.map((container) => `
1177
- readonly ${container.exportName}: ContainerAccessor;`).join("")}
1178
- };` : "";
1179
- const envBindingFields = [
1180
- ...hasAi ? [` /** Workers AI binding (the conventional \`env.AI\`), narrowing \`ctx.ai\`. */
1181
- readonly AI?: unknown;`] : [],
1182
- ...containers.map((container) => {
1183
- assertIdentifier(container.bindingName, `container binding "${container.bindingName}"`);
1184
- return ` /** Durable Object namespace for the \`${container.exportName}\` container. */
1185
- readonly ${container.bindingName}?: unknown;`;
1186
- }),
1187
- ...workflows.map((workflow) => {
1188
- assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
1189
- return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
1190
- readonly ${workflow.bindingName}?: unknown;`;
1191
- }),
1192
- ...queues.map((queue) => {
1193
- assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
1194
- return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
1195
- readonly ${queue.bindingName}?: unknown;`;
1196
- }),
1197
- ...agents.map((agent) => {
1198
- assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
1199
- return ` /** Workflow binding for the \`${agent.exportName}\` agent. */
1200
- readonly ${agent.bindingName}?: unknown;`;
1201
- })
1202
- ].join("\n");
1203
- const envBlock = `
1204
-
1205
- /**
1206
- * This project's Cloudflare bindings, as far as codegen can discover them from
1207
- * \`lunora/\` source — the container/workflow Durable Object namespaces and the
1208
- * conventional Workers AI binding. Bindings the config layer reconciles from
1209
- * user-named wrangler config (R2/KV/D1/\`services\`/queues/…) aren't statically
1210
- * visible here, so the open index signature keeps them reachable (cast at the
1211
- * use site). Service bindings (\`env.<SERVICE>.fetch(...)\` / RPC stubs) live
1212
- * under this signature too — Lunora can't type a third-party worker's RPC
1213
- * surface, so treat them as \`Fetcher\`/\`Service<unknown>\` and cast.
1214
- */
1215
- export interface CloudflareBindings {
1216
- readonly [binding: string]: unknown;${envBindingFields ? `
1217
- ${envBindingFields}` : ""}
1218
- }
1219
-
1220
- /** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
1221
- export type Env = CloudflareBindings;`;
1222
- const serverCapabilityField = (key, enabled) => enabled ? SERVER_CTX_FIELDS.get(key)?.field ?? "" : "";
1223
- const kvContextField = serverCapabilityField("kv", hasKv);
1224
- const analyticsContextField = serverCapabilityField("analytics", hasAnalytics);
1225
- const hyperdriveActionField = serverCapabilityField("hyperdrive", hasHyperdrive);
1226
- const browserActionField = serverCapabilityField("browser", hasBrowser);
1227
- const imagesActionField = serverCapabilityField("images", hasImages);
1228
- const pipelinesActionField = serverCapabilityField("pipelines", hasPipelines);
1229
- const r2sqlActionField = serverCapabilityField("r2sql", hasR2sql);
1230
- const accessContextField = hasAccessFacade ? `
1231
- /** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
1232
- readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
1233
- const flagsContextField = hasFlags ? `
1234
- /** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
1235
- readonly flags: import("${base.flags}").LunoraFlags;` : "";
1236
- const notifyContextField = hasNotify ? `
1237
- /** Multi-channel notifications (@lunora/notify): send / chat / inApp / webhook plus the push device sub-facade. Sends are external I/O — confine them to action handlers. */
1238
- readonly notify: import("@lunora/notify").LunoraNotify;
1239
- /** Device push sub-facade — the same object as ctx.notify.push (register / send / broadcast). Sends belong in action handlers. */
1240
- readonly push: import("@lunora/notify").LunoraPush;` : "";
1241
- const hasWorkflows = workflows.length > 0;
1242
- const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
1243
- import type * as lunoraWorkflowDefinitions from "../workflows.js";
1244
- ` : "";
1245
- const workflowsTypeBlock = hasWorkflows ? `
1246
-
1247
- /** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`). */
1248
- type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params }
1249
- ? unknown extends Params
1250
- ? Record<string, unknown>
1251
- : NonNullable<Params>
1252
- : Record<string, unknown>;
1253
-
1254
- /** This project's declared workflows, addressable from \`ctx.workflows\` by their \`lunora/workflows.ts\` export name. */
1255
- export interface LunoraWorkflows {
1256
- ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportName)}): WorkflowHandle<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n")}
1257
- }` : "";
1258
- const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
1259
- const workflowsContextField = hasWorkflows ? `
1260
- readonly workflows: LunoraWorkflows;` : "";
1261
- const hasQueues = queues.length > 0;
1262
- const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
1263
- import type * as lunoraQueueDefinitions from "../queues.js";
1264
- ` : "";
1265
- const queuesTypeBlock = hasQueues ? `
1266
-
1267
- /** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
1268
- type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
1269
-
1270
- /** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
1271
- export interface LunoraQueues {
1272
- ${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
1273
- }` : "";
1274
- const queuesContextField = hasQueues ? `
1275
- readonly queues: LunoraQueues;` : "";
1276
- const hasAgents = agents.length > 0;
1277
- const agentsTypeImport = hasAgents ? `import type { AgentHandle } from "@lunora/agent";
1278
- ` : "";
1279
- const agentsTypeBlock = hasAgents ? `
1280
-
1281
- /** This project's declared agents, addressable from \`ctx.agents\` by their \`lunora/agents.ts\` export name. */
1282
- export interface LunoraAgents {
1283
- ${agents.map((agent) => {
1284
- assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
1285
- return ` readonly ${agent.exportName}: AgentHandle;`;
1286
- }).join("\n")}
1287
- }` : "";
1288
- const agentsContextField = hasAgents ? `
1289
- readonly agents: LunoraAgents;` : "";
1290
- const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
1291
- import type * as lunoraIdentityContract from "../identity.js";
1292
- ` : "";
1293
- const identityTypeBlock = identity ? `
1294
-
1295
- /** This app's declared identity claim contract (\`defineIdentity\` in \`lunora/identity.ts\`) — the typed shape of \`ctx.auth.getIdentity()\`, the RLS policy \`ctx.auth.identity\`, and the \`authorizeShard\`/\`authorizeFanOut\` identity argument. */
1296
- export type Identity = InferIdentity<typeof lunoraIdentityContract.${identity.exportName}>;
1297
-
1298
- /** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
1299
- type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };` : "";
1300
- const authOmit = identity ? ` | "auth"` : "";
1301
- const authContextField = identity ? `
1302
- readonly auth: NarrowedAuth;` : "";
1303
- const policyIdentityArgument = identity ? ", Identity" : "";
1304
- const envTypeImport = env ? `import type * as lunoraEnvContract from "../env.js";
1305
- ` : "";
1306
- const envTypeBlock = env ? `
1307
-
1308
- /** This app's declared env contract (\`defineEnv\` in \`lunora/env.ts\`) — the validated, coercion-aware shape of \`ctx.env\`. */
1309
- export type LunoraEnv = ReturnType<typeof lunoraEnvContract.${env.exportName}>;` : "";
1310
- const envOmit = env ? ` | "env"` : "";
1311
- const envContextField = env ? `
1312
- /** Validated, typed environment declared by \`defineEnv\` in \`lunora/env.ts\` — parsed & coercion-aware config values (\`ctx.env.STRIPE_KEY\`); a missing or invalid value throws at read time. */
1313
- readonly env: LunoraEnv;` : "";
1314
- const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
1315
- import type {
1316
- ActionBuilder,
1317
- ActionCtx as ActionCtxBase,
1318
- ColumnValidator,
1319
- DatabaseReader,
1320
- DatabaseWriter,
1321
- EmptyArgs,
1322
- InternalActionBuilder,
1323
- InternalMutationBuilder,
1324
- InternalQueryBuilder,
1325
- MutationBuilder,
1326
- MutationCtx as MutationCtxBase,
1327
- QueryBuilder,
1328
- QueryCtx as QueryCtxBase,
1329
- ReadOnlyStorage,
1330
- Storage as StorageBase,
1331
- TableReader,
1332
- } from "${base.server}";
1333
-
1334
- import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
1335
- ${aiTypeImport}${paymentsTypeImport}${x402TypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${agentsTypeImport}${identityTypeImport}${envTypeImport}
1336
- export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
1337
-
1338
- /** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
1339
- export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${agentsTypeBlock}${identityTypeBlock}${envTypeBlock}
1340
-
1341
- /**
1342
- * Project-typed contexts. The base contexts from \`@lunora/server\` are
1343
- * untyped against the schema; here \`db\` is widened to the generated per-table
1344
- * facade (\`ctx.db.<table>.findMany(...)\`) while keeping the legacy structural
1345
- * \`db.get\`/\`db.query\` surface for back-compat, and \`storage\` is narrowed so
1346
- * \`ctx.storage.bucket(name)\` autocompletes the declared buckets.
1347
- */
1348
- /**
1349
- * The Convex-style fluent reader \`ctx.db.query(table)\`, bound to this schema:
1350
- * a table-name literal narrows the row type, so \`.collect()\` / \`.first()\` /
1351
- * \`.take()\` (and the \`.withIndex()\` / \`.filter()\` / \`.order()\` chain) resolve
1352
- * as \`Doc<table>\` with no \`as unknown as Doc<...>\` casts. The per-table accessor
1353
- * \`ctx.db.<table>\` stays available alongside it via the facade.
1354
- *
1355
- * Intersected with the wide \`(string) => TableReader\` signature so the bound
1356
- * \`ctx.db\` is still structurally assignable to schema-agnostic consumers that
1357
- * call \`db.query(someString)\` (e.g. \`@lunora/ratelimit\`'s \`createDbStore\`).
1358
- */
1359
- type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>) & ((table: string) => TableReader);
1360
-
1361
- /**
1362
- * The point read \`ctx.db.get(id)\`, bound to this schema: an \`Id<"table">\`
1363
- * carries its table name, so the resolved document is typed \`Doc<table>\` (or
1364
- * \`null\` when absent) with no \`as Doc<...>\` cast. Mirrors {@link TypedTableQuery}.
1365
- */
1366
- type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
1367
-
1368
- export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}${envOmit}> {
1369
- readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
1370
- readonly orm: OrmReader;
1371
- readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${notifyContextField}${analyticsContextField}${envContextField}${authContextField}
1372
- }
1373
-
1374
- export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
1375
- readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
1376
- readonly orm: OrmWriter;
1377
- readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${notifyContextField}${analyticsContextField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
1378
- }
1379
-
1380
- export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
1381
- readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
1382
- readonly orm: OrmWriter;
1383
- readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${x402ActionField}${containersActionField}${kvContextField}${flagsContextField}${notifyContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
1384
- }
1385
-
1386
- /**
1387
- * Procedure builders bound to this project's typed contexts. Chain
1388
- * \`.input({...})\` to declare args, \`.use(middleware)\` for rate limiting / RLS /
1389
- * masking (each middleware may refine \`ctx\`), then the terminal
1390
- * \`.query()\` / \`.mutation()\` / \`.action()\` with a \`({ ctx, args }) => ...\`
1391
- * handler. \`.output(validator)\` and (queries) \`.stream()\` are also available.
1392
- */
1393
- const lunoraBuilders = initLunora.dataModel<DataModel>().create();
1394
-
1395
- /** \`query\` builder bound to this project's typed {@link QueryCtx}. */
1396
- export const query = lunoraBuilders.query as unknown as QueryBuilder<QueryCtx, EmptyArgs>;
1397
-
1398
- /** \`mutation\` builder bound to this project's typed {@link MutationCtx}. */
1399
- export const mutation = lunoraBuilders.mutation as unknown as MutationBuilder<MutationCtx, EmptyArgs>;
1400
-
1401
- /** \`action\` builder bound to this project's typed {@link ActionCtx}. */
1402
- export const action = lunoraBuilders.action as unknown as ActionBuilder<ActionCtx, EmptyArgs>;
1403
-
1404
- /** \`internalQuery\` builder bound to this project's typed {@link QueryCtx} — never exposed on \`api\`. */
1405
- export const internalQuery = lunoraBuilders.internalQuery as unknown as InternalQueryBuilder<QueryCtx, EmptyArgs>;
1406
-
1407
- /** \`internalMutation\` builder bound to this project's typed {@link MutationCtx} — never exposed on \`api\`. */
1408
- export const internalMutation = lunoraBuilders.internalMutation as unknown as InternalMutationBuilder<MutationCtx, EmptyArgs>;
1409
-
1410
- /** \`internalAction\` builder bound to this project's typed {@link ActionCtx} — never exposed on \`api\`. */
1411
- export const internalAction = lunoraBuilders.internalAction as unknown as InternalActionBuilder<ActionCtx, EmptyArgs>;
1412
-
1413
- /**
1414
- * \`definePolicy\` bound to THIS schema's {@link DataModel} + {@link Relations}.
1415
- * Import it from here (\`./_generated/server\`) instead of \`@lunora/server\` to get
1416
- * a relation-aware RLS authoring surface: \`table\` autocompletes to a real table
1417
- * name and the \`when\` predicate type-checks against the table's columns **and**
1418
- * its declared relations — \`{ author: { is: {...} } }\`, \`{ posts: { some: {...} } }\`,
1419
- * etc. (the \`@lunora/do\` pre-resolver resolves relation predicates on reads).
1420
- * Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
1421
- * so the \`rls()\` chain discovers a policy authored either way the same.
1422
- */
1423
- export const definePolicy = createPolicyDsl<DataModel, Relations${policyIdentityArgument}>();
1424
-
1425
- /**
1426
- * The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
1427
- * table names — so the argument autocompletes and returns a precise
1428
- * \`Id<"table">\`. Identical to \`@lunora/server\`'s \`v\` at runtime; the only
1429
- * difference is the tightened \`id\` type.
1430
- *
1431
- * Import \`v\` from here (\`./_generated/server\`) in your queries/mutations/actions
1432
- * to get table-name autocomplete. \`lunora/schema.ts\` can't use it — it is the
1433
- * file that defines the table names, so they aren't known yet there; pass the
1434
- * names as string literals and the generated \`Id<"table">\` types still catch a
1435
- * typo wherever the id is used.
1436
- */
1437
- export const v = vBase as unknown as Omit<typeof vBase, "id"> & {
1438
- id: <T extends TableName>(table: T) => ColumnValidator<IdOfTable<T>, IdOfTable<T>>;
1439
- };
1440
- `;
1441
- return server;
1442
- };
1443
- const renderLifecycleManifest = (functions) => {
1444
- const manifest = { connect: [], disconnect: [] };
1445
- for (const definition of functions) {
1446
- if (!definition.lifecycle) {
1447
- continue;
1448
- }
1449
- manifest[definition.lifecycle].push(`${sanitizeNamespace(definition.filePath)}:${definition.exportName}`);
1450
- }
1451
- return manifest;
1452
- };
1453
- const emitFunctions = (options) => {
1454
- const { agents = [], functions, migrations = [], mutators = [], shapes = [], useUmbrella = false, usesSandbox = false } = options;
1455
- const hasFunctions = functions.length > 0;
1456
- const base = baseSpecifiers(useUmbrella);
1457
- const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
1458
- const agentRegistry = renderAgentFunctionRegistry(agents, functions);
1459
- const sandboxRegistry = renderSandboxFunctionRegistry(usesSandbox, functions);
1460
- const autoLines = [agentRegistry.lines, sandboxRegistry.lines].filter((block) => block.length > 0).join("\n");
1461
- let dispatchBodyWithAgents = dispatchBody;
1462
- if (autoLines.length > 0) {
1463
- dispatchBodyWithAgents = dispatchBody.length > 0 ? `${dispatchBody.trimEnd()}
1464
- ${autoLines}
1465
- ` : `
1466
- ${autoLines}
1467
- `;
1468
- }
1469
- const lifecycleHooks = renderLifecycleManifest(functions);
1470
- const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
1471
- ` : "";
1472
- const shapeRegistry = shapes.length > 0 ? `
1473
- /**
1474
- * Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
1475
- * The generated ShardDO's \`resolveShape\` override looks a shape up by name and
1476
- * evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
1477
- * \`shape_subscribe\` (reads-as-permissions).
1478
- */
1479
- export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
1480
- ` : "";
1481
- const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
1482
- /**
1483
- * Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
1484
- * ShardDO's \`isCustomMutator\` override routes through the client-watermark push
1485
- * protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
1486
- */
1487
- export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
1488
- ` : "";
1489
- const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
1490
- ` : "";
1491
- const compiledArgsInstall = installBlock.length > 0 ? `
1492
- /**
1493
- * AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
1494
- * onto its function's live \`.args\` object and consulted by the interpreted
1495
- * parser as a zero-allocation fast path; anything it can't model is deferred.
1496
- */
1497
- ${installBlock}
1498
- ` : "";
1499
- const caller = renderCaller(functions);
1500
- const callerTypes = caller.types ? `
1501
- ${caller.types}
1502
- ` : "";
1503
- const callerImpl = caller.implementation ? `
1504
- ${caller.implementation}
1505
- ` : "";
1506
- const callerDataModelImports = referencedDataModelImports(caller.types);
1507
- const dataModelImport = callerDataModelImports.length > 0 ? `import type { ${callerDataModelImports.join(", ")} } from "./dataModel.js";
1508
- ` : "";
1509
- const callerParameter = hasFunctions ? "context" : "_context";
1510
- const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
1511
-
1512
- ` : "";
1513
- return `${GENERATED_HEADER}${importBlock}${agentRegistry.importLine}${sandboxRegistry.importLine}${compiledArgsImport}${shapeTypeImport}import { LunoraError } from "${base.server}";
1514
- import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
1515
- ${dataModelImport}
1516
- /**
1517
- * Single registered function, narrowed to the shape \`handleRpc\` needs.
1518
- * The real argument validators / return types are checked elsewhere — at
1519
- * the dispatch site we erase to \`unknown\` so a single map can host every
1520
- * registered function.
1521
- */
1522
- export interface RegisteredLunoraFunction {
1523
- kind: "action" | "mutation" | "query" | "stream";
1524
- args: Record<string, unknown>;
1525
- /**
1526
- * For \`"action" | "mutation" | "query"\` the handler is awaited and its result returned.
1527
- * For \`"stream"\` the handler returns an \`AsyncIterable\` synchronously and takes an
1528
- * \`AbortSignal\` as a third argument — the runtime drives it frame-by-frame.
1529
- */
1530
- handler: ((context: unknown, args: Record<string, unknown>) => Promise<unknown> | unknown) | ((context: unknown, args: Record<string, unknown>, signal?: AbortSignal) => AsyncIterable<unknown>);
1531
- /** \`"internal"\` functions are rejected on the external RPC path; absence === public. */
1532
- visibility?: "internal" | "public";
1533
- }
1534
- ${agentRegistry.prelude}${sandboxRegistry.prelude}
1535
- /**
1536
- * Static dispatch table. The key matches the \`__lunoraRef\` the client
1537
- * emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
1538
- */
1539
- export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBodyWithAgents}};
1540
- ${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
1541
- /**
1542
- * Connection-lifecycle manifest: the function paths the generated ShardDO
1543
- * dispatches when a client's WebSocket connects (\`connect\`) or disconnects
1544
- * (\`disconnect\`). Each path also resolves through {@link LUNORA_FUNCTIONS}; the
1545
- * DO runs every hook under the socket's verified identity via system dispatch.
1546
- */
1547
- export const LUNORA_LIFECYCLE_HOOKS: { connect: readonly string[]; disconnect: readonly string[] } = {
1548
- connect: [${lifecycleHooks.connect.map((path) => JSON.stringify(path)).join(", ")}],
1549
- disconnect: [${lifecycleHooks.disconnect.map((path) => JSON.stringify(path)).join(", ")}],
1550
- };
1551
-
1552
- /**
1553
- * Resolve and invoke a registered function from an external caller. Throws a
1554
- * LunoraError-shaped object (404) when the path is unknown — the runtime's
1555
- * structural error mapper turns that into the right HTTP status. Internal
1556
- * functions are treated as not-found so their existence never leaks to clients.
1557
- */
1558
- export const dispatchLunoraFunction = async (functionPath: string, context: unknown, args: Record<string, unknown>): Promise<unknown> => {
1559
- const registered = LUNORA_FUNCTIONS[functionPath];
1560
-
1561
- if (!registered || registered.visibility === "internal") {
1562
- throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
1563
- }
1564
-
1565
- return registered.handler(context, args);
1566
- };
1567
-
1568
- /**
1569
- * A handler context accepted by {@link createCaller}. The runtime context the
1570
- * shard DO builds is uniform across kinds, so any handler's \`ctx\` works here.
1571
- */
1572
- export type CallerCtx = ActionCtx | MutationCtx | QueryCtx;
1573
-
1574
- /**
1575
- * Typed, in-process server-to-server caller. Every registered function — public
1576
- * *and* internal — is reachable; each call dispatches against the same shard
1577
- * with the supplied \`context\`, exactly like \`ctx.runQuery\`/\`runMutation\`/
1578
- * \`runAction\` but without re-stating the function path.
1579
- */
1580
- export interface Caller {${callerTypes}}
1581
-
1582
- ${callRegisteredHelper}/** Build a {@link Caller} bound to \`context\` (typically a handler's \`ctx\`). */
1583
- export const createCaller = (${callerParameter}: CallerCtx): Caller => ({${callerImpl}});
1584
-
1585
- /**
1586
- * Single registered data migration, narrowed to the shape the per-shard runner
1587
- * consumes. \`up\`/\`down\` are erased to a structural transform; the authoring
1588
- * validation lives in \`defineMigration\`.
1589
- */
1590
- export interface RegisteredDataMigration {
1591
- batchSize?: number;
1592
- down?: (document: Record<string, unknown>) => Record<string, unknown> | undefined;
1593
- id: string;
1594
- table: string;
1595
- up: (document: Record<string, unknown>) => Record<string, unknown> | undefined;
1596
- }
1597
-
1598
- /**
1599
- * Registry of online data migrations keyed by \`defineMigration\`'s \`id\`. The
1600
- * shard DO's admin RPC and the CLI resolve migrations to run through this map.
1601
- */
1602
- export const LUNORA_MIGRATIONS: Record<string, RegisteredDataMigration> = {${migrationBody}};
1603
- `;
1604
- };
1605
- const buildTableReferences = (schema) => {
1606
- const references = {};
1607
- for (const table of schema.tables) {
1608
- const fields = {};
1609
- for (const [field, validator] of Object.entries(table.shape)) {
1610
- const resolved = validator.kind === "optional" && validator.inner ? validator.inner : validator;
1611
- if (resolved.kind === "id" && resolved.tableName !== void 0) {
1612
- fields[field] = resolved.tableName;
1613
- }
1614
- }
1615
- if (Object.keys(fields).length > 0) {
1616
- references[table.name] = fields;
1617
- }
1618
- }
1619
- return references;
1620
- };
1621
- const buildStorageColumns = (schema) => {
1622
- const byTable = {};
1623
- for (const table of schema.tables) {
1624
- const fields = [];
1625
- for (const [field, validator] of Object.entries(table.shape)) {
1626
- const resolved = validator.kind === "optional" && validator.inner ? validator.inner : validator;
1627
- if (resolved.kind === "storage") {
1628
- fields.push(field);
1629
- }
1630
- }
1631
- if (fields.length > 0) {
1632
- byTable[table.name] = fields;
1633
- }
1634
- }
1635
- return byTable;
1636
- };
1637
- const buildTableIndexes = (schema) => {
1638
- const byTable = {};
1639
- for (const table of schema.tables) {
1640
- const entries = [
1641
- ...table.indexes.map((index) => {
1642
- return { fields: [...index.fields], name: index.name, type: "index", ...index.unique === true ? { unique: true } : {} };
1643
- }),
1644
- ...table.searchIndexes.map((index) => {
1645
- return { fields: [index.field, ...index.filterFields ?? []], name: index.name, type: "search" };
1646
- }),
1647
- ...(table.geoIndexes ?? []).map((index) => {
1648
- return { fields: [index.field], name: index.name, type: "geo" };
1649
- }),
1650
- ...table.rankIndexes.map((index) => {
1651
- return { fields: index.sortBy.map((key) => key.field), name: index.name, type: "rank" };
1652
- }),
1653
- ...table.vectorIndexes.map((index) => {
1654
- return { fields: index.field === void 0 ? [] : [index.field], name: index.name, type: "vector" };
1655
- })
1656
- ];
1657
- if (entries.length > 0) {
1658
- byTable[table.name] = entries;
1659
- }
1660
- }
1661
- return byTable;
1662
- };
1663
- const buildTtlSweeps = (schema) => {
1664
- const sweeps = [];
1665
- for (const table of schema.tables) {
1666
- if (!table.ttl || table.shardMode === "global") {
1667
- continue;
1668
- }
1669
- sweeps.push({
1670
- ...table.ttl.after === void 0 ? {} : { after: table.ttl.after },
1671
- field: table.ttl.field,
1672
- ...table.softDelete ? { softDeleteField: table.softDelete.field } : {},
1673
- table: table.name
1674
- });
1675
- }
1676
- return sweeps;
1677
- };
1678
- const buildTableColumns = (schema) => {
1679
- const byTable = {};
1680
- for (const table of schema.tables) {
1681
- const columns = [
1682
- { name: "_id", optional: false, pk: true, type: "id" },
1683
- { name: "_creationTime", optional: false, type: "number" }
1684
- ];
1685
- for (const [field, validator] of Object.entries(table.shape)) {
1686
- const optional = validator.kind === "optional" || Boolean(validator.column?.hasDefault);
1687
- const resolved = validator.kind === "optional" && validator.inner ? validator.inner : validator;
1688
- const column = { name: field, optional, type: resolved.kind };
1689
- if (resolved.kind === "id" && resolved.tableName !== void 0) {
1690
- column.ref = resolved.tableName;
1691
- }
1692
- if (resolved.kind === "storage") {
1693
- column.isStorage = true;
1694
- }
1695
- columns.push(column);
1696
- }
1697
- byTable[table.name] = columns;
1698
- }
1699
- return byTable;
1700
- };
1701
- const emitRelationFanout = (hasGlobalTables) => {
1702
- if (!hasGlobalTables) {
1703
- return { importFragment: "", override: "" };
1704
- }
1705
- return {
1706
- importFragment: "serveRelationFanout, ",
1707
- override: `
1708
- protected override async runRelationFanoutRead(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
1709
- this.ensureMigrated();
1710
-
1711
- const { db } = this.buildCtx({ functionPath }) as { db: DatabaseWriterLike };
1712
-
1713
- return serveRelationFanout(schema as unknown as SchemaLike, db, functionPath, args);
1714
- }
1715
- `
1716
- };
1717
- };
1718
- const emitAiFragments = (hasAi) => {
1719
- if (!hasAi) {
1720
- return { build: "", configField: "", contextField: "", stub: "" };
1721
- }
1722
- const aiMissing = `throw new Error("ctx.ai: no AI binding found. Add an \\\`ai\\\` binding (env.AI) to wrangler.jsonc, or pass \\\`ai\\\` to createShardDO().");`;
1723
- return {
1724
- // Build ctx.ai from the resolved Workers AI binding (a `config.ai` thunk
1725
- // override, else `env.AI`). createAi is provider-agnostic — a model-id
1726
- // string resolves Workers AI, any AI SDK model object passes through — so
1727
- // a handler is never locked to Workers AI. Falls back to `aiStub`.
1728
- build: `
1729
- const aiBinding = config.ai?.(env) ?? (env as Record<string, unknown>).AI;
1730
- // Correlate AI-Gateway-routed calls with the Lunora trace: thread the
1731
- // function path + trace id into createAi, which folds them into the
1732
- // gateway's native \`metadata\` only when a gateway is configured (absent
1733
- // otherwise). Mirror the tracer's anchor guard — a deferred subscription
1734
- // re-run must not borrow a concurrent dispatch's trace, so read
1735
- // \`getCurrentTrace()\` only on the synchronous (non-threaded-identity) path.
1736
- const aiTrace = options.identity ? undefined : this.getCurrentTrace();
1737
- const ai: LunoraAi = aiBinding
1738
- ? createAi({ binding: aiBinding as AiBindingLike, env: env as Record<string, unknown>, metadata: { functionPath: options.functionPath, traceId: aiTrace?.traceId } })
1739
- : aiStub;
1740
- `,
1741
- // Optional override for the Workers AI binding. When omitted, ctx.ai is
1742
- // built from `env.AI` (the conventional binding the config layer
1743
- // auto-reconciles); the thunk lets a caller point it elsewhere or inject
1744
- // a double in tests.
1745
- configField: `
1746
- ai?: (env: Record<string, unknown>) => AiBindingLike;`,
1747
- contextField: `
1748
- ai,`,
1749
- stub: `
1750
- const aiStub: LunoraAi = {
1751
- embeddingModel: () => {
1752
- ${aiMissing}
1753
- },
1754
- model: () => {
1755
- ${aiMissing}
1756
- },
1757
- run: async () => {
1758
- ${aiMissing}
1759
- },
1760
- // workersai is a callable-with-properties; a bare throwing arrow isn't
1761
- // structurally assignable, so cast it. Never invoked (the stub throws first).
1762
- workersai: (() => {
1763
- ${aiMissing}
1764
- }) as unknown as LunoraAi["workersai"],
1765
- };
1766
- `
1767
- };
1768
- };
1769
- const EMPTY_HELPER_FRAGMENTS = { build: "", configField: "", contextField: "", importLines: [], stub: "" };
1770
- const emitKvFragments = (hasKv) => {
1771
- if (!hasKv) {
1772
- return EMPTY_HELPER_FRAGMENTS;
1773
- }
1774
- const kvMissing = `throw new Error("ctx.kv: no KV binding found. Add a \\\`kv_namespaces\\\` binding (env.KV) to wrangler.jsonc, or pass \\\`kv\\\` to createShardDO().");`;
1775
- return {
1776
- build: `
1777
- const kvBinding = config.kv?.(env) ?? (env as Record<string, unknown>).KV;
1778
- const kv: Kv = kvBinding ? createKv({ namespace: kvBinding as KVNamespaceLike }) : kvStub;
1779
- `,
1780
- configField: `
1781
- kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
1782
- contextField: `
1783
- kv,`,
1784
- importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
1785
- stub: `
1786
- const kvStub: Kv = {
1787
- delete: async () => {
1788
- ${kvMissing}
1789
- },
1790
- get: async () => {
1791
- ${kvMissing}
1792
- },
1793
- getRaw: async () => {
1794
- ${kvMissing}
1795
- },
1796
- getWithMetadata: async () => {
1797
- ${kvMissing}
1798
- },
1799
- list: async () => {
1800
- ${kvMissing}
1801
- },
1802
- put: async () => {
1803
- ${kvMissing}
1804
- },
1805
- };
1806
- `
1807
- };
1808
- };
1809
- const emitFlagsFragments = (hasFlags, flagsSpecifier) => {
1810
- if (!hasFlags) {
1811
- return EMPTY_HELPER_FRAGMENTS;
1812
- }
1813
- return {
1814
- build: `
1815
- const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1816
- hooks: flagsConfig.hooks,
1817
- logger: flagsConfig.logger,
1818
- provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1819
- targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
1820
- });
1821
- `,
1822
- configField: `
1823
- flags?: (env: Record<string, unknown>) => import("${flagsSpecifier}").Provider;`,
1824
- contextField: `
1825
- flags,`,
1826
- importLines: [`import { createFlags } from "${flagsSpecifier}";`, `import flagsConfig from "../flags.js";`],
1827
- stub: ""
1828
- };
1829
- };
1830
- const emitNotifyFragments = (hasNotify) => {
1831
- if (!hasNotify) {
1832
- return EMPTY_HELPER_FRAGMENTS;
1833
- }
1834
- return {
1835
- // Threads the request's `ctx.log` / `ctx.metrics` into the facade so a send
1836
- // emits the `notify.send` / `notify.skipped` observability signals. Because
1837
- // `log`/`metrics` are built LATER in the context builder, this fragment is
1838
- // injected after them (see `notifyBuild` below), NOT inside `everyContextBuild`.
1839
- build: `
1840
- const { notify, push } = createNotify(notifyConfig, env, { log, metrics });
1841
- `,
1842
- configField: "",
1843
- contextField: `
1844
- notify,
1845
- push,`,
1846
- importLines: [`import { createNotify } from "@lunora/notify";`, `import notifyConfig from "../notify.js";`],
1847
- stub: ""
1848
- };
1849
- };
1850
- const emitEnvFragments = (env) => {
1851
- if (!env) {
1852
- return EMPTY_HELPER_FRAGMENTS;
1853
- }
1854
- return {
1855
- build: `
1856
- const envConfig = lunoraEnvContract.${env.exportName}(env);
1857
- `,
1858
- configField: "",
1859
- contextField: `
1860
- env: envConfig,`,
1861
- importLines: [`import * as lunoraEnvContract from "../env.js";`],
1862
- stub: ""
1863
- };
1864
- };
1865
- const emitAccessFragments = (hasAccessFacade) => {
1866
- if (!hasAccessFacade) {
1867
- return EMPTY_HELPER_FRAGMENTS;
1868
- }
1869
- return {
1870
- build: `
1871
- const access = accessFacade(identity, userId);
1872
- `,
1873
- configField: "",
1874
- contextField: `
1875
- access,`,
1876
- importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
1877
- stub: ""
1878
- };
1879
- };
1880
- const emitFlagsOverrides = (flagKeys, hasFlags, flagsSpecifier) => {
1881
- if (!hasFlags) {
1882
- return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
1883
- }
1884
- const clientBuild = (targetingKey) => `
1885
- const env = (this.env ?? {}) as Record<string, unknown>;
1886
- const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1887
- hooks: flagsConfig.hooks,
1888
- logger: flagsConfig.logger,
1889
- provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1890
- targetingKey: ${targetingKey},
1891
- });`;
1892
- const constant = `
1893
- /** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
1894
- const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
1895
- `;
1896
- const evaluateOverride = `
1897
- protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
1898
- const evalContext = context as import("${flagsSpecifier}").EvaluationContext | undefined;
1899
- const evaluations: FlagsResult["flags"] = [];
1900
-
1901
- for (const entry of LUNORA_FLAG_KEYS) {
1902
- // eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
1903
- const details =
1904
- entry.type === "boolean"
1905
- ? await flags.details.boolean(entry.key, false, evalContext)
1906
- : entry.type === "number"
1907
- ? await flags.details.number(entry.key, 0, evalContext)
1908
- : entry.type === "string"
1909
- ? await flags.details.string(entry.key, "", evalContext)
1910
- : await flags.details.object(entry.key, {}, evalContext);
1911
-
1912
- evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
1913
- }
1914
-
1915
- return { configured: true, flags: evaluations };
1916
- }
1917
- `;
1918
- const subscriptionOverride = `
1919
- protected override runFlagSubscriptionRead(_functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<unknown> {${clientBuild("() => flagsConfig.identify?.({ identity: identity?.identity ?? null, userId: identity?.userId ?? null })")}
1920
- const key = typeof args.key === "string" ? args.key : "";
1921
-
1922
- // SECURITY: the reactive channel is public (any socket, no auth). Serve
1923
- // ONLY statically-discovered flag keys — an arbitrary client-supplied key
1924
- // would let a subscriber probe the value of internal/unreleased flags the
1925
- // app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
1926
- // eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
1927
- if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
1928
- return Promise.resolve(null);
1929
- }
1930
-
1931
- // SECURITY: evaluate under the socket's server-verified identity ONLY
1932
- // (the targetingKey resolved above). Client-supplied targeting context is
1933
- // NOT honored on this public channel — otherwise a subscriber could spoof
1934
- // targeting attributes (e.g. plan/role) to unlock a flag gated on them.
1935
- const context = undefined;
1936
-
1937
- if (args.type === "number") {
1938
- return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
1939
- }
1940
-
1941
- if (args.type === "string") {
1942
- return flags.string(key, typeof args.default === "string" ? args.default : "", context);
1943
- }
1944
-
1945
- if (args.type === "object") {
1946
- return flags.object(key, (args.default ?? {}) as import("${flagsSpecifier}").JsonValue, context);
1947
- }
1948
-
1949
- return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
1950
- }
1951
- `;
1952
- return { constant, evaluateOverride, subscriptionOverride };
1953
- };
1954
- const emitAnalyticsFragments = (hasAnalytics) => {
1955
- if (!hasAnalytics) {
1956
- return EMPTY_HELPER_FRAGMENTS;
1957
- }
1958
- const analyticsMissing = `throw new Error("ctx.analytics: no Analytics Engine binding found. Add an \\\`analytics_engine_datasets\\\` binding (env.ANALYTICS) to wrangler.jsonc, or pass \\\`analytics\\\` to createShardDO().");`;
1959
- return {
1960
- build: `
1961
- const analyticsBinding = config.analytics?.(env) ?? (env as Record<string, unknown>).ANALYTICS;
1962
- const analytics: AnalyticsClient = analyticsBinding ? createAnalytics(analyticsBinding as AnalyticsEngineDatasetLike) : analyticsStub;
1963
- `,
1964
- configField: `
1965
- analytics?: (env: Record<string, unknown>) => AnalyticsEngineDatasetLike;`,
1966
- contextField: `
1967
- analytics,`,
1968
- importLines: [
1969
- `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
1970
- `import { createAnalytics } from "@lunora/bindings/analytics";`
1971
- ],
1972
- stub: `
1973
- const analyticsStub: AnalyticsClient = {
1974
- track: () => {
1975
- ${analyticsMissing}
1976
- },
1977
- writeDataPoint: () => {
1978
- ${analyticsMissing}
1979
- },
1980
- };
1981
- `
1982
- };
1983
- };
1984
- const emitImagesFragments = (hasImages) => {
1985
- if (!hasImages) {
1986
- return EMPTY_HELPER_FRAGMENTS;
1987
- }
1988
- const imagesMissing = `throw new Error("ctx.images: no Images binding found. Add an \\\`images\\\` binding (env.IMAGES) to wrangler.jsonc, or pass \\\`images\\\` to createShardDO().");`;
1989
- return {
1990
- build: `
1991
- const imagesBinding = config.images?.(env) ?? (env as Record<string, unknown>).IMAGES;
1992
- const images: Images = imagesBinding ? createImages({ binding: imagesBinding as ImagesBindingLike }) : imagesStub;
1993
- `,
1994
- configField: `
1995
- images?: (env: Record<string, unknown>) => ImagesBindingLike;`,
1996
- // ActionCtx-only: woven onto the action ctx object, never query/mutation.
1997
- contextField: `
1998
- images,`,
1999
- importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
2000
- stub: `
2001
- const imagesStub: Images = {
2002
- info: async () => {
2003
- ${imagesMissing}
2004
- },
2005
- transform: async () => {
2006
- ${imagesMissing}
2007
- },
2008
- };
2009
- `
2010
- };
2011
- };
2012
- const emitHyperdriveFragments = (hasHyperdrive) => {
2013
- if (!hasHyperdrive) {
2014
- return EMPTY_HELPER_FRAGMENTS;
2015
- }
2016
- const sqlMissing = `throw new Error("ctx.sql: provide a \\\`sql\\\` config thunk that builds a SqlClient from your driver, e.g. \\\`sql: (env) => fromPostgresJs(postgres(env.HYPERDRIVE.connectionString))\\\`.");`;
2017
- return {
2018
- build: `
2019
- const sql: SqlClient = config.sql ? config.sql(env) : sqlStub;
2020
- `,
2021
- configField: `
2022
- sql?: (env: Record<string, unknown>) => SqlClient;`,
2023
- // ActionCtx-only: woven onto the action ctx object, never query/mutation.
2024
- contextField: `
2025
- sql,`,
2026
- importLines: [`import type { SqlClient } from "@lunora/hyperdrive";`],
2027
- stub: `
2028
- const sqlStub: SqlClient = {
2029
- query: async () => {
2030
- ${sqlMissing}
2031
- },
2032
- };
2033
- `
2034
- };
2035
- };
2036
- const emitBrowserFragments = (hasBrowser) => {
2037
- if (!hasBrowser) {
2038
- return EMPTY_HELPER_FRAGMENTS;
2039
- }
2040
- const browserMissing = `throw new Error("ctx.browser: provide a \\\`browser\\\` config thunk, e.g. \\\`browser: (env) => createBrowser({ binding: env.BROWSER, launch })\\\` with \\\`import { launch } from '@cloudflare/playwright'\\\`.");`;
2041
- return {
2042
- build: `
2043
- const browser: Browser = config.browser ? config.browser(env) : browserStub;
2044
- `,
2045
- configField: `
2046
- browser?: (env: Record<string, unknown>) => Browser;`,
2047
- // ActionCtx-only: woven onto the action ctx object, never query/mutation.
2048
- contextField: `
2049
- browser,`,
2050
- // Type-only import: the generated build never calls `createBrowser`
2051
- // (the `config.browser` thunk owns construction, injecting the optional
2052
- // `@cloudflare/playwright` peer the worker stays free of), so only the
2053
- // `Browser` type is referenced here.
2054
- importLines: [`import type { Browser } from "@lunora/browser";`],
2055
- stub: `
2056
- const browserStub: Browser = {
2057
- content: async () => {
2058
- ${browserMissing}
2059
- },
2060
- launch: async () => {
2061
- ${browserMissing}
2062
- },
2063
- pdf: async () => {
2064
- ${browserMissing}
2065
- },
2066
- scrape: async () => {
2067
- ${browserMissing}
2068
- },
2069
- screenshot: async () => {
2070
- ${browserMissing}
2071
- },
2072
- };
2073
- `
2074
- };
2075
- };
2076
- const emitR2sqlFragments = (hasR2sql) => {
2077
- if (!hasR2sql) {
2078
- return EMPTY_HELPER_FRAGMENTS;
2079
- }
2080
- const r2sqlMissing = `throw new Error("ctx.r2sql: no R2 SQL credentials found. Set \\\`R2_SQL_TOKEN\\\`, \\\`R2_SQL_ACCOUNT_ID\\\` (or \\\`CLOUDFLARE_ACCOUNT_ID\\\`), and \\\`R2_SQL_BUCKET\\\` in your env/.dev.vars, or pass an \\\`r2sql\\\` config thunk to createShardDO().");`;
2081
- return {
2082
- build: `
2083
- const r2sqlEnv = env as Record<string, unknown>;
2084
- const r2sqlAccountId = (r2sqlEnv.R2_SQL_ACCOUNT_ID ?? r2sqlEnv.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
2085
- const r2sqlToken = r2sqlEnv.R2_SQL_TOKEN as string | undefined;
2086
- const r2sqlBucket = r2sqlEnv.R2_SQL_BUCKET as string | undefined;
2087
- const r2sql: R2SqlClient = config.r2sql
2088
- ? config.r2sql(env)
2089
- : r2sqlAccountId && r2sqlToken && r2sqlBucket
2090
- ? createR2Sql({ accountId: r2sqlAccountId, apiToken: r2sqlToken, bucket: r2sqlBucket })
2091
- : r2sqlStub;
2092
- `,
2093
- configField: `
2094
- r2sql?: (env: Record<string, unknown>) => R2SqlClient;`,
2095
- // ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
2096
- // \`isAction\` block, never the every-ctx object literal.
2097
- contextField: "",
2098
- importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
2099
- // The stub is typed `R2SqlClient`, so TS flags a missing method at build
2100
- // time — but it must stay structurally in sync with that interface
2101
- // (`@lunora/bindings/r2sql` client.ts) when a method is added there.
2102
- stub: `
2103
- const r2sqlStub: R2SqlClient = {
2104
- describe: async () => {
2105
- ${r2sqlMissing}
2106
- },
2107
- explain: async () => {
2108
- ${r2sqlMissing}
2109
- },
2110
- from: () => {
2111
- ${r2sqlMissing}
2112
- },
2113
- query: async () => {
2114
- ${r2sqlMissing}
2115
- },
2116
- showDatabases: async () => {
2117
- ${r2sqlMissing}
2118
- },
2119
- showTables: async () => {
2120
- ${r2sqlMissing}
2121
- },
2122
- };
2123
- `
2124
- };
2125
- };
2126
- const emitPipelinesFragments = (hasPipelines) => {
2127
- if (!hasPipelines) {
2128
- return EMPTY_HELPER_FRAGMENTS;
2129
- }
2130
- const pipelinesMissing = `throw new Error("ctx.pipelines: no Pipelines binding found. Add a \\\`pipelines\\\` binding (env.PIPELINES) to wrangler.jsonc, or pass \\\`pipelines\\\` to createShardDO().");`;
2131
- return {
2132
- build: `
2133
- const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
2134
- const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
2135
- `,
2136
- configField: `
2137
- pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
2138
- // ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
2139
- // in the \`isAction\` block, never the every-ctx object literal.
2140
- contextField: "",
2141
- importLines: [
2142
- `import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
2143
- `import { createPipelines } from "@lunora/bindings/pipelines";`
2144
- ],
2145
- stub: `
2146
- const pipelinesStub: PipelineClient = {
2147
- send: async () => {
2148
- ${pipelinesMissing}
2149
- },
2150
- };
2151
- `
2152
- };
2153
- };
2154
- const emitContainers = (containers, jurisdiction) => {
2155
- if (containers.length === 0) {
2156
- return "";
2157
- }
2158
- const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
2159
- const classes = containers.map((container) => {
2160
- assertIdentifier(container.exportName, `container export "${container.exportName}"`);
2161
- assertIdentifier(container.className, `container class "${container.className}"`);
2162
- return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
2163
- export class ${container.className} extends LunoraContainer {
2164
- public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
2165
- super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
2166
- }
2167
- }
2168
- `;
2169
- }).join("\n");
2170
- const imports = containers.map((container) => container.exportName).join(", ");
2171
- return `${GENERATED_HEADER}/**
2172
- * Container-enabled Durable Object classes for the containers declared in
2173
- * \`lunora/containers.ts\`. Re-export them from your worker entry — wrangler
2174
- * requires each \`containers[].class_name\` to be exported by the worker:
2175
- *
2176
- * \`export * from "./lunora/_generated/containers.js";\`
2177
- *
2178
- * \`ContainerProxy\` is re-exported alongside them: the egress-interception path
2179
- * (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
2180
- * \`handle.egress\` controls) routes container outbound traffic through this
2181
- * WorkerEntrypoint, so it too must be exported by the deployed worker.
2182
- */
2183
- import { LunoraContainer } from "@lunora/container/do";
2184
-
2185
- import { ${imports} } from "../containers.js";
2186
-
2187
- export { ContainerProxy } from "@lunora/container/do";
2188
-
2189
- ${classes}`;
2190
- };
2191
- const emitContainerFragments = (containers, jurisdiction) => {
2192
- if (containers.length === 0) {
2193
- return { build: "", contextField: "", importLines: [], specs: "" };
2194
- }
2195
- for (const container of containers) {
2196
- assertIdentifier(container.exportName, `container export "${container.exportName}"`);
2197
- assertIdentifier(container.bindingName, `container binding "${container.bindingName}"`);
2198
- }
2199
- const specEntries = containers.map((container) => {
2200
- const maxInstances = container.maxInstances === void 0 ? "" : `, maxInstances: ${String(container.maxInstances)}`;
2201
- return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
2202
- }).join("\n");
2203
- return {
2204
- // Schema `.jurisdiction("…")` pins every container DO this shard reaches
2205
- // to the data-residency region (`undefined` when undeclared). The trailing
2206
- // `this.getCurrentTraceparent()` forwards the inbound RPC's W3C trace
2207
- // context onto outbound container fetches so their spans join the trace.
2208
- build: `
2209
- const containers = createContainerContext(env, LUNORA_CONTAINERS, ${jurisdiction ? JSON.stringify(jurisdiction) : "undefined"}, this.getCurrentTraceparent());
2210
- `,
2211
- contextField: `
2212
- containers,`,
2213
- importLines: [`import type { ContainerBindingSpec } from "@lunora/container";`, `import { createContainerContext } from "@lunora/container";`],
2214
- // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2215
- specs: `
2216
- /** Wiring specs for \`ctx.containers\` (codegen-derived from \`lunora/containers.ts\`). */
2217
- const LUNORA_CONTAINERS: ReadonlyArray<ContainerBindingSpec> = [
2218
- ${specEntries}
2219
- ];
2220
- `
2221
- };
2222
- };
2223
- const emitWorkflows = (workflows) => {
2224
- if (workflows.length === 0) {
2225
- return "";
2226
- }
2227
- const classes = workflows.map((workflow) => {
2228
- assertIdentifier(workflow.exportName, `workflow export "${workflow.exportName}"`);
2229
- assertIdentifier(workflow.className, `workflow class "${workflow.className}"`);
2230
- return `/** WorkflowEntrypoint for the \`${workflow.exportName}\` definition (binding \`${workflow.bindingName}\`). */
2231
- export class ${workflow.className} extends LunoraWorkflow<WorkflowParamsOf<typeof ${workflow.exportName}>, WorkflowOutputOf<typeof ${workflow.exportName}>> {
2232
- public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
2233
- super(ctx, env, ${workflow.exportName}, "${workflow.exportName}");
2234
- }
2235
- }
2236
- `;
2237
- }).join("\n");
2238
- const imports = workflows.map((workflow) => workflow.exportName).join(", ");
2239
- return `${GENERATED_HEADER}/**
2240
- * WorkflowEntrypoint classes for the workflows declared in
2241
- * \`lunora/workflows.ts\`. Re-export them from your worker entry — wrangler
2242
- * requires each \`workflows[].class_name\` to be exported by the worker:
2243
- *
2244
- * \`export * from "./lunora/_generated/workflows.js";\`
2245
- */
2246
- import LunoraWorkflow from "@lunora/workflow/do";
2247
-
2248
- import { ${imports} } from "../workflows.js";
2249
-
2250
- /** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`), so each entrypoint keeps the authored \`ctx.params\` type. */
2251
- type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params }
2252
- ? unknown extends Params
2253
- ? Record<string, unknown>
2254
- : NonNullable<Params>
2255
- : Record<string, unknown>;
2256
-
2257
- /** Output type carried by a \`defineWorkflow\` definition (its phantom \`__output\`), so each entrypoint keeps the authored return type. */
2258
- type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output } ? Output : unknown;
2259
-
2260
- ${classes}`;
2261
- };
2262
- const emitAgents = (agents) => {
2263
- if (agents.length === 0) {
2264
- return "";
2265
- }
2266
- const voiceAgents = agents.filter((agent) => agent.voice);
2267
- const hasVoice = voiceAgents.length > 0;
2268
- const classes = agents.map((agent) => {
2269
- assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2270
- assertIdentifier(agent.className, `agent class "${agent.className}"`);
2271
- const workflowClass = `/** WorkflowEntrypoint for the \`${agent.exportName}\` agent (binding \`${agent.bindingName}\`). */
2272
- export class ${agent.className} extends LunoraWorkflow<AgentRunInput, AgentRunResult> {
2273
- public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
2274
- super(ctx, env, compileAgentWorkflow(${agent.exportName}, "${agent.exportName}"), "${agent.exportName}");
2275
- }
2276
- }
2277
- `;
2278
- if (!agent.voice) {
2279
- return workflowClass;
2280
- }
2281
- const voiceClassName = agent.voiceClassName ?? "";
2282
- const voiceBindingName = agent.voiceBindingName ?? "";
2283
- assertIdentifier(voiceClassName, `agent voice class "${voiceClassName}"`);
2284
- const voiceClass = `
2285
- /** Voice-session Durable Object for the \`${agent.exportName}\` agent (binding \`${voiceBindingName}\`). */
2286
- export class ${voiceClassName} extends VoiceSessionDO {
2287
- public constructor(ctx: ConstructorParameters<typeof VoiceSessionDO>[0], env: Record<string, unknown>) {
2288
- super(ctx, env, ${agent.exportName}, "${agent.exportName}");
2289
- }
2290
- }
2291
- `;
2292
- return `${workflowClass}${voiceClass}`;
2293
- }).join("\n");
2294
- const imports = agents.map((agent) => agent.exportName).join(", ");
2295
- const runtimeImport = hasVoice ? `import { compileAgentWorkflow, VoiceSessionDO } from "@lunora/agent";` : `import { compileAgentWorkflow } from "@lunora/agent";`;
2296
- const voiceHeaderNote = hasVoice ? `
2297
- *
2298
- * Voice-enabled agents ALSO get a \`VoiceSessionDO\` subclass here — a real
2299
- * Durable Object (unlike the Workflow), so wrangler needs both its
2300
- * \`durable_objects\` binding and its \`class_name\` export (the same re-export
2301
- * line covers it).` : "";
2302
- return `${GENERATED_HEADER}/**
2303
- * WorkflowEntrypoint classes for the agents declared in \`lunora/agents.ts\`.
2304
- * Each \`defineAgent\` compiles a replay-safe tool-loop onto a Cloudflare
2305
- * Workflow. Re-export them from your worker entry — wrangler requires each
2306
- * \`workflows[].class_name\` to be exported by the worker:
2307
- *
2308
- * \`export * from "./lunora/_generated/agents.js";\`${voiceHeaderNote}
2309
- */
2310
- import LunoraWorkflow from "@lunora/workflow/do";
2311
- ${runtimeImport}
2312
- import type { AgentRunInput, AgentRunResult } from "@lunora/agent";
2313
-
2314
- import { ${imports} } from "../agents.js";
2315
-
2316
- ${classes}`;
2317
- };
2318
- const emitQueues = (queues) => {
2319
- const pushQueues = queues.filter((queue) => queue.mode === "push");
2320
- if (pushQueues.length === 0) {
2321
- return "";
2322
- }
2323
- for (const queue of pushQueues) {
2324
- assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
2325
- }
2326
- const imports = pushQueues.map((queue) => queue.exportName).join(", ");
2327
- const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
2328
- return `${GENERATED_HEADER}/**
2329
- * Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
2330
- * composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
2331
- * by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
2332
- * by \`defineApp\` — you don't import this directly.
2333
- */
2334
- import type { QueueRegistry } from "@lunora/queue";
2335
-
2336
- import { ${imports} } from "../queues.js";
2337
-
2338
- /** Stable wrangler queue name → { definition, exportName } for batch routing. */
2339
- export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
2340
- ${entries}
2341
- };
2342
- `;
2343
- };
2344
- const emitWorkflowFragments = (workflows) => {
2345
- if (workflows.length === 0) {
2346
- return { build: "", contextField: "", importLines: [], specs: "" };
2347
- }
2348
- for (const workflow of workflows) {
2349
- assertIdentifier(workflow.exportName, `workflow export "${workflow.exportName}"`);
2350
- assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
2351
- }
2352
- const specEntries = workflows.map((workflow) => ` { binding: "${workflow.bindingName}", exportName: "${workflow.exportName}" },`).join("\n");
2353
- return {
2354
- build: `
2355
- const workflows = createWorkflowContext(env, LUNORA_WORKFLOWS);
2356
- `,
2357
- contextField: `
2358
- workflows,`,
2359
- importLines: [`import type { WorkflowBindingSpec } from "@lunora/workflow";`, `import { createWorkflowContext } from "@lunora/workflow";`],
2360
- // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2361
- specs: `
2362
- /** Wiring specs for \`ctx.workflows\` (codegen-derived from \`lunora/workflows.ts\`). */
2363
- const LUNORA_WORKFLOWS: ReadonlyArray<WorkflowBindingSpec> = [
2364
- ${specEntries}
2365
- ];
2366
- `
2367
- };
2368
- };
2369
- const emitQueueFragments = (queues) => {
2370
- if (queues.length === 0) {
2371
- return { build: "", contextField: "", importLines: [], specs: "" };
2372
- }
2373
- for (const queue of queues) {
2374
- assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
2375
- assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
2376
- }
2377
- const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
2378
- return {
2379
- build: `
2380
- const queues = createQueueContext(env, LUNORA_QUEUES);
2381
- `,
2382
- contextField: `
2383
- queues,`,
2384
- importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
2385
- // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2386
- specs: `
2387
- /** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
2388
- const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
2389
- ${specEntries}
2390
- ];
2391
- `
2392
- };
2393
- };
2394
- const emitAgentFragments = (agents) => {
2395
- if (agents.length === 0) {
2396
- return { build: "", contextField: "", importLines: [], specs: "" };
2397
- }
2398
- for (const agent of agents) {
2399
- assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2400
- assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
2401
- }
2402
- const specEntries = agents.map((agent) => ` { binding: "${agent.bindingName}", exportName: "${agent.exportName}"${agent.publicRun === true ? ", publicRun: true" : ""} },`).join("\n");
2403
- return {
2404
- build: `
2405
- const agents = createAgentContext(env, LUNORA_AGENTS);
2406
- `,
2407
- contextField: `
2408
- agents,`,
2409
- importLines: [`import { createAgentContext } from "@lunora/agent";`, `import type { AgentBindingSpec } from "@lunora/agent";`],
2410
- // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2411
- specs: `
2412
- /** Wiring specs for \`ctx.agents\` (codegen-derived from \`lunora/agents.ts\`). */
2413
- const LUNORA_AGENTS: ReadonlyArray<AgentBindingSpec> = [
2414
- ${specEntries}
2415
- ];
2416
- `
2417
- };
2418
- };
2419
- const emitWorkflowsMetadataFragments = (workflows) => {
2420
- if (workflows.length === 0) {
2421
- return { constant: "", override: "" };
2422
- }
2423
- const metadata = {
2424
- workflows: workflows.map((workflow) => {
2425
- return {
2426
- binding: workflow.bindingName,
2427
- className: workflow.className,
2428
- exportName: workflow.exportName,
2429
- name: workflow.name
2430
- };
2431
- })
2432
- };
2433
- return {
2434
- constant: `
2435
- /** Read-only declared-workflow metadata (discovered from \`lunora/workflows.ts\`) served via \`__lunora_admin__:listWorkflows\` for the studio's workflows view. */
2436
- const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0, 4)};
2437
- `,
2438
- override: `
2439
- protected override workflowsMetadata(): WorkflowsResult {
2440
- return LUNORA_WORKFLOWS_INFO;
2441
- }
2442
- `
2443
- };
2444
- };
2445
- const emitQueuesMetadataFragments = (queues) => {
2446
- if (queues.length === 0) {
2447
- return { constant: "", override: "" };
2448
- }
2449
- const metadata = {
2450
- queues: queues.map((queue) => {
2451
- return {
2452
- binding: queue.bindingName,
2453
- ...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
2454
- exportName: queue.exportName,
2455
- mode: queue.mode,
2456
- name: queue.name
2457
- };
2458
- })
2459
- };
2460
- return {
2461
- constant: `
2462
- /** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
2463
- const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
2464
- `,
2465
- override: `
2466
- protected override queuesMetadata(): QueuesResult {
2467
- return LUNORA_QUEUES_INFO;
2468
- }
2469
- `
2470
- };
2471
- };
2472
- const emitPaymentFragments = (hasPayments) => {
2473
- if (!hasPayments) {
2474
- return { build: "", configField: "", contextField: "", imports: [], stub: "" };
2475
- }
2476
- const missing = `throw new Error("ctx.payments: no payment configured. Pass \\\`payment\\\` to createShardDO().");`;
2477
- return {
2478
- imports: [
2479
- `import type { LunoraDatabaseLike as LunoraPaymentDbLike, LunoraPayment, PaymentsFromContextOptions } from "@lunora/payment";`,
2480
- `import { paymentsFromContext } from "@lunora/payment";`
2481
- ],
2482
- // Built after `db` (the store rides ctx.db) and `userId` (the default authorizer ties a
2483
- // referenceId to the caller). The adapter — which carries provider secrets — comes from
2484
- // the `config.payment` thunk over env. Falls back to `paymentStub`.
2485
- build: `
2486
- const payments: LunoraPayment = config.payment
2487
- ? paymentsFromContext({ auth: { userId: userId ?? null }, db: db as unknown as LunoraPaymentDbLike }, config.payment(env))
2488
- : paymentStub;
2489
- `,
2490
- configField: `
2491
- payment?: (env: Record<string, unknown>) => PaymentsFromContextOptions;`,
2492
- contextField: `
2493
- payments,`,
2494
- stub: `
2495
- const paymentStub: LunoraPayment = {
2496
- attach: () => {
2497
- ${missing}
2498
- },
2499
- cancelSubscription: () => {
2500
- ${missing}
2501
- },
2502
- check: () => {
2503
- ${missing}
2504
- },
2505
- createCheckout: () => {
2506
- ${missing}
2507
- },
2508
- createPortalSession: () => {
2509
- ${missing}
2510
- },
2511
- handleWebhook: () => {
2512
- ${missing}
2513
- },
2514
- listBalances: () => {
2515
- ${missing}
2516
- },
2517
- listSubscriptions: () => {
2518
- ${missing}
2519
- },
2520
- track: () => {
2521
- ${missing}
2522
- },
2523
- } as unknown as LunoraPayment;
2524
- `
2525
- };
2526
- };
2527
- const emitX402Fragments = (hasX402) => {
2528
- if (!hasX402) {
2529
- return { build: "", configField: "", contextField: "", imports: [], stub: "" };
2530
- }
2531
- return {
2532
- imports: [`import type { X402Pay, X402PayConfig } from "@lunora/x402/pay";`, `import { lazyX402Pay } from "@lunora/x402/pay";`],
2533
- // Built lazily off `secrets` (the Secrets Store facade already in scope) and
2534
- // the `config.x402` thunk over env; falls back to `x402Stub`.
2535
- build: `
2536
- const x402: X402Pay = config.x402
2537
- ? lazyX402Pay(config.x402(env), { getSecret: (name: string) => secrets.get(name) })
2538
- : x402Stub;
2539
- `,
2540
- configField: `
2541
- x402?: (env: Record<string, unknown>) => X402PayConfig;`,
2542
- contextField: `
2543
- x402,`,
2544
- stub: `
2545
- const x402Stub: X402Pay = {
2546
- fetch: () => {
2547
- throw new Error("ctx.x402: no pay rail configured. Pass \\\`x402\\\` to createShardDO().");
2548
- },
2549
- } as unknown as X402Pay;
2550
- `
2551
- };
2552
- };
2553
- const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
2554
- "AdvisoryFinding",
2555
- "DatabaseWriterLike",
2556
- "DataMigrationLike",
2557
- ...hasFlags ? ["FlagsResult"] : [],
2558
- "LogSink",
2559
- "MaskPoliciesResult",
2560
- "MigrationRunResult",
2561
- ...hasQueues ? ["QueuesResult"] : [],
2562
- "RunShardApplyCdcArgs",
2563
- "RunShardMigrationArgs",
2564
- "RlsPoliciesResult",
2565
- "RunShardRankBeforeArgs",
2566
- "RunShardRankPageArgs",
2567
- "RunShardWriteArgs",
2568
- "RunShardWriteResult",
2569
- "SchedulerLike",
2570
- "SchemaLike",
2571
- "ShardDOState",
2572
- "ShardRankPageResult",
2573
- "SqlExec",
2574
- "StorageRulesResult",
2575
- "StudioFeaturesResult",
2576
- "SystemReaderStorageLike",
2577
- ...hasWorkflows ? ["WorkflowsResult"] : [],
2578
- ...hasVectors ? ["WriteHook"] : []
2579
- ];
2580
- const emitShard = ({
2581
- advisories = [],
2582
- agents = [],
2583
- containers = [],
2584
- env,
2585
- flagKeys = [],
2586
- hasAccessFacade = false,
2587
- hasAi = false,
2588
- hasAnalytics = false,
2589
- hasBrowser = false,
2590
- hasFlags = false,
2591
- hasHyperdrive = false,
2592
- hasImages = false,
2593
- hasKv = false,
2594
- hasNotify = false,
2595
- hasPayments = false,
2596
- hasPipelines = false,
2597
- hasR2sql = false,
2598
- hasX402 = false,
2599
- maskMetadata,
2600
- mutators = [],
2601
- queues = [],
2602
- rlsMetadata,
2603
- schema,
2604
- shapes = [],
2605
- storageRules,
2606
- studioFeatures,
2607
- useUmbrella = false,
2608
- workflows = []
2609
- }) => {
2610
- const base = baseSpecifiers(useUmbrella);
2611
- const hasMutators = mutators.length > 0;
2612
- const hasShapes = shapes.length > 0;
2613
- const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
2614
- const accessFragments = emitAccessFragments(hasAccessFacade);
2615
- const kvFragments = emitKvFragments(hasKv);
2616
- const flagsFragments = emitFlagsFragments(hasFlags, base.flags);
2617
- const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags, base.flags);
2618
- const notifyFragments = emitNotifyFragments(hasNotify);
2619
- const envFragments = emitEnvFragments(env);
2620
- const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
2621
- const imagesFragments = emitImagesFragments(hasImages);
2622
- const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
2623
- const browserFragments = emitBrowserFragments(hasBrowser);
2624
- const r2sqlFragments = emitR2sqlFragments(hasR2sql);
2625
- const pipelinesFragments = emitPipelinesFragments(hasPipelines);
2626
- const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
2627
- const {
2628
- build: containersBuild,
2629
- contextField: containersContextField,
2630
- importLines: containerImportLines,
2631
- specs: containerSpecs
2632
- } = emitContainerFragments(containers, schema.jurisdiction);
2633
- const {
2634
- build: workflowsBuild,
2635
- contextField: workflowsContextField,
2636
- importLines: workflowImportLines,
2637
- specs: workflowSpecs
2638
- } = emitWorkflowFragments(workflows);
2639
- const { build: agentsBuild, contextField: agentsContextField, importLines: agentImportLines, specs: agentSpecs } = emitAgentFragments(agents);
2640
- const {
2641
- build: paymentsBuild,
2642
- configField: paymentsConfigField,
2643
- contextField: paymentsContextField,
2644
- imports: paymentsImports,
2645
- stub: paymentStub
2646
- } = emitPaymentFragments(hasPayments);
2647
- const { build: x402Build, configField: x402ConfigField, imports: x402Imports, stub: x402Stub } = emitX402Fragments(hasX402);
2648
- const advisoryData = advisories;
2649
- const rlsData = rlsMetadata ?? { policies: [], roles: [] };
2650
- const maskData = maskMetadata ?? { columns: [] };
2651
- const storageRulesData = storageRules ?? { rules: [] };
2652
- const studioFeaturesData = studioFeatures ?? {
2653
- analytics: false,
2654
- auth: false,
2655
- containers: false,
2656
- flags: false,
2657
- kv: false,
2658
- mail: false,
2659
- payments: false,
2660
- queues: false,
2661
- scheduler: false,
2662
- storage: false,
2663
- vectors: false,
2664
- workflows: false
2665
- };
2666
- const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
2667
- const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
2668
- const hasVectors = schema.vectorIndexes.length > 0;
2669
- const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
2670
- const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
2671
- const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
2672
- const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
2673
- if (hasD1Global && hasHyperdriveGlobal) {
2674
- throw new LunoraError(
2675
- "INTERNAL",
2676
- 'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
2677
- );
2678
- }
2679
- const hasTables = schema.tables.length > 0;
2680
- const tableReferences = buildTableReferences(schema);
2681
- const tableIndexes = buildTableIndexes(schema);
2682
- const tableColumns = buildTableColumns(schema);
2683
- const storageColumns = buildStorageColumns(schema);
2684
- const ttlSweeps = buildTtlSweeps(schema);
2685
- const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
2686
- if (hasShapes) {
2687
- doTypeImports.push("WhereInput");
2688
- }
2689
- const relationFanout = emitRelationFanout(hasGlobalTables);
2690
- const shapeResolveOverride = hasShapes ? `
2691
- protected override resolveShape(name: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): { columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string } | undefined {
2692
- const shape = LUNORA_SHAPES[name];
2693
-
2694
- if (!shape) {
2695
- return undefined;
2696
- }
2697
-
2698
- this.ensureMigrated();
2699
-
2700
- // Trusted ctx from the socket's OWN verified identity — the client
2701
- // supplies only the shape name + args; \`compileWhere\` validates the
2702
- // args then runs the shape's \`where(ctx, args)\` under that identity,
2703
- // so which rows replicate is a server decision (reads-as-permissions).
2704
- const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
2705
- const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
2706
-
2707
- // AND-compose with the table's RLS read base-where. A shape runs no
2708
- // procedure, so the \`.use(rls(...))\` middleware never fires; without
2709
- // this merge its reads would bypass every read policy on the table
2710
- // (rows the caller can't see would replicate). \`composeShapeReadWhere\`
2711
- // evaluates the table's read policies under this same trusted ctx —
2712
- // exactly the request-time path — and fails closed under a
2713
- // \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
2714
- const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
2715
- ctx,
2716
- identity: identity?.identity ?? null,
2717
- rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
2718
- roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
2719
- shapeWhere,
2720
- table: shape.table,
2721
- tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
2722
- userId: identity?.userId ?? null,
2723
- });
2724
-
2725
- // A live shape pokes only from its OWN shard's op-log, so a \`where()\`
2726
- // that joins to a \`.shardBy()\` table (rows in another DO) is rejected
2727
- // here — the first point the compiled predicate + shard modes are both
2728
- // known. Remedy: denormalize, or move the joined table to \`.global()\`.
2729
- assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
2730
-
2731
- // A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
2732
- // base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
2733
- // instead of the CDC poke path.
2734
- const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
2735
-
2736
- return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
2737
- }
2738
- ` : "";
2739
- const customMutatorOverride = hasMutators ? `
2740
- protected override isCustomMutator(functionPath: string): boolean {
2741
- return LUNORA_MUTATOR_PATHS.has(functionPath);
2742
- }
2743
- ` : "";
2744
- const shapeReadRegistryConst = hasShapes ? `
2745
- /** Per-table RLS read policies (hoisted from \`.use(rls(...))\` chains) the shape resolver AND-merges into each \`defineShape\` predicate so partial replication honours read policies. */
2746
- const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
2747
- ` : "";
2748
- const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
2749
- const importLines = [
2750
- `import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
2751
- `import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceIncrementalTick, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
2752
- ...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
2753
- // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
2754
- // `createSecrets` (the `ctx.secrets` core built-in) live in
2755
- // `@lunora/server`, the single source — imported here rather than stamped
2756
- // inline into every generated shard. With shapes, also pull the RLS
2757
- // read-registry builder + `composeShapeReadWhere` so `resolveShape` can
2758
- // AND-merge a shape's predicate with the table's read base-where.
2759
- hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets, LunoraError } from "${base.server}";` : `import { asBucketStorage, createSecrets, LunoraError } from "${base.server}";`
2760
- ];
2761
- if (hasTables) {
2762
- importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
2763
- }
2764
- if (hasVectors) {
2765
- importLines.push(
2766
- `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
2767
- `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
2768
- );
2769
- }
2770
- if (hasAi) {
2771
- importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
2772
- }
2773
- importLines.push(
2774
- ...accessFragments.importLines,
2775
- ...kvFragments.importLines,
2776
- ...flagsFragments.importLines,
2777
- ...notifyFragments.importLines,
2778
- ...envFragments.importLines,
2779
- ...analyticsFragments.importLines,
2780
- ...imagesFragments.importLines,
2781
- ...hyperdriveFragments.importLines,
2782
- ...browserFragments.importLines,
2783
- ...r2sqlFragments.importLines,
2784
- ...pipelinesFragments.importLines,
2785
- ...containerImportLines,
2786
- ...workflowImportLines,
2787
- ...queueImportLines,
2788
- ...agentImportLines,
2789
- ...paymentsImports,
2790
- ...x402Imports,
2791
- ``,
2792
- `import schema from "../schema.js";`,
2793
- // Local-first sync registries are pulled in alongside the function table
2794
- // only when the project declares them, so the import list stays minimal.
2795
- `import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
2796
- );
2797
- const vectorsConfigField = hasVectors ? `
2798
- vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
2799
- const d1ConfigField = hasD1Global ? `
2800
- d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
2801
- const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
2802
- hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
2803
- const sourceClientConfigField = hasSourcedTables ? `
2804
- sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
2805
- const globalDatabaseStub = hasGlobalTables ? `
2806
- const globalDbStub: DatabaseWriterLike = {
2807
- aggregate: async () => {
2808
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2809
- },
2810
- count: async () => {
2811
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2812
- },
2813
- delete: async () => {
2814
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2815
- },
2816
- findFirst: async () => {
2817
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2818
- },
2819
- findFirstOrThrow: async () => {
2820
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2821
- },
2822
- findMany: async () => {
2823
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2824
- },
2825
- get: async () => {
2826
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2827
- },
2828
- groupBy: async () => {
2829
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2830
- },
2831
- insert: async () => {
2832
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2833
- },
2834
- normalizeId: () => {
2835
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2836
- },
2837
- patch: async () => {
2838
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2839
- },
2840
- query: () => {
2841
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2842
- },
2843
- rank: async () => {
2844
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2845
- },
2846
- rankPage: async () => {
2847
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2848
- },
2849
- replace: async () => {
2850
- throw new Error("ctx.db.<globalTable>: no global backend configured. Pass \`d1\` or \`hyperdriveGlobal\` to createShardDO().");
2851
- },
2852
- };
2853
- ` : "";
2854
- const vectorsStub = hasVectors ? `
2855
- const vectorsStub: VectorSearchLike = {
2856
- deleteByIds: async () => {
2857
- throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
2858
- },
2859
- getByIds: async () => {
2860
- throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
2861
- },
2862
- query: async () => {
2863
- throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
2864
- },
2865
- upsert: async () => {
2866
- throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
2867
- },
2868
- upsertNow: async () => {
2869
- throw new Error("ctx.vectors: no vectors configured. Pass \`vectors\` to createShardDO().");
2870
- },
2871
- };
2872
- ` : "";
2873
- const vectorsBuild = hasVectors ? `
2874
- let vectors: VectorSearchLike;
2875
- let onWrite: WriteHook | undefined;
2876
-
2877
- if (config.vectors) {
2878
- const lunora = createVectors({ indexes: config.vectors(env) });
2879
-
2880
- vectors = createContextVectors(lunora);
2881
- onWrite = createVectorSyncHook({ schema: schema as unknown as VectorSchemaLike, vectors });
2882
- } else {
2883
- vectors = vectorsStub;
2884
- onWrite = undefined;
2885
- }
2886
- ` : "";
2887
- const globalDatabaseField = hasGlobalTables ? "\n globalDb," : "";
2888
- const authField = "\n auth: { identity: identity ?? null, userId: userId ?? null },";
2889
- const databaseOptions = hasVectors ? `{${authField}
2890
- broadcast: (delta) => {
2891
- this.recordChangedTable(delta.table);
2892
- },
2893
- cdc: config.cdc ?? false,
2894
- enforceRls: true,
2895
- onIndexUse: this.getCtxDbIndexUseHook(),
2896
- onRead: options.onRead ?? this.getCtxDbReadHook(),
2897
- onWrite,
2898
- scheduler,
2899
- schema: schema as unknown as SchemaLike,
2900
- sql: this.sql as SqlExec,
2901
- storage,${globalDatabaseField}
2902
- }` : `{${authField}
2903
- broadcast: (delta) => {
2904
- this.recordChangedTable(delta.table);
2905
- },
2906
- cdc: config.cdc ?? false,
2907
- enforceRls: true,
2908
- onIndexUse: this.getCtxDbIndexUseHook(),
2909
- onRead: options.onRead ?? this.getCtxDbReadHook(),
2910
- scheduler,
2911
- schema: schema as unknown as SchemaLike,
2912
- sql: this.sql as SqlExec,
2913
- storage,${globalDatabaseField}
2914
- }`;
2915
- const vectorsContextField = hasVectors ? `
2916
- vectors,` : "";
2917
- const ormContextField = hasTables ? `
2918
- orm: bindOrm(facade),` : "";
2919
- const bindTableHelper = "";
2920
- const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
2921
- const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
2922
- ` : "";
2923
- const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
2924
- protected override async readGlobalShapeRows(resolved: { columns?: readonly string[]; effectiveWhere?: WhereInput; global?: boolean; table: string }, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<Array<{ doc: Record<string, unknown>; id: string }>> {
2925
- const env = this.env as Record<string, unknown>;
2926
- const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
2927
- const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
2928
-
2929
- let cursor: null | string = null;
2930
-
2931
- // Drain every page of the global membership so the seed/diff sees the
2932
- // full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
2933
- // a broad \`.global()\` shape would otherwise materialize an unbounded
2934
- // array before the caller's \`withinGlobalShapeBound\` check rejects it,
2935
- // so bail early and let the caller fail it closed.
2936
- do {
2937
- // eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
2938
- const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
2939
-
2940
- for (const doc of page.page) {
2941
- rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
2942
-
2943
- if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
2944
- return rows;
2945
- }
2946
- }
2947
-
2948
- cursor = page.isDone ? null : page.continueCursor;
2949
- } while (cursor !== null);
2950
-
2951
- return rows;
2952
- }
2953
- ` : "";
2954
- const sourceClientCacheConst = hasSourcedTables ? `
2955
- const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
2956
- const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
2957
- ` : "";
2958
- const externalSourceOverride = hasSourcedTables ? `
2959
- protected override async pollExternalSources(): Promise<number | undefined> {
2960
- const env = (this.env ?? {}) as Record<string, unknown>;
2961
- const sourced = Object.entries((schema as unknown as SchemaLike).tables)
2962
- .map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
2963
- .filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
2964
-
2965
- if (sourced.length === 0) {
2966
- return undefined;
2967
- }
2968
-
2969
- const shardKey = this.currentShardKey();
2970
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2971
- const writer = createShardCtxDb({
2972
- broadcast: (delta) => {
2973
- this.recordChangedTable(delta.table);
2974
- },
2975
- cdc: config.cdc ?? false,
2976
- scheduler,
2977
- schema: schema as unknown as SchemaLike,
2978
- sql: this.sql as SqlExec,
2979
- });
2980
-
2981
- let clients = sourceClientCache.get(this);
2982
-
2983
- if (clients === undefined) {
2984
- clients = new Map();
2985
- sourceClientCache.set(this, clients);
2986
- }
2987
-
2988
- let polledAt = sourcePollAtCache.get(this);
2989
-
2990
- if (polledAt === undefined) {
2991
- polledAt = new Map();
2992
- sourcePollAtCache.set(this, polledAt);
2993
- }
2994
-
2995
- const now = Date.now();
2996
- // \`nextDueAt\` tracks the EARLIEST next-due timestamp across every
2997
- // non-manual source; the shared alarm re-arms there instead of the
2998
- // fixed 2 s global-shape floor, so a large \`refresh.everyMs\` actually
2999
- // sleeps until it's due. Stays \`undefined\` when every source is
3000
- // \`refresh: "manual"\`, so the shared alarm goes idle for this tier.
3001
- let nextDueAt: number | undefined;
3002
-
3003
- for (const [table, source] of sourced) {
3004
- if (source.refresh === "manual") {
3005
- continue;
3006
- }
3007
-
3008
- if (isSourceDue(source.refresh, polledAt.get(table), now)) {
3009
- try {
3010
- let client = clients.get(source.binding);
3011
-
3012
- if (client === undefined) {
3013
- client = config.sourceClient?.(env, source.binding);
3014
-
3015
- if (client !== undefined) {
3016
- clients.set(source.binding, client);
3017
- }
3018
- }
3019
-
3020
- if (client === undefined) {
3021
- // No SqlClient resolved for this binding (host never wired
3022
- // \`config.sourceClient\`, or wired it wrong). Surface it in the
3023
- // Logs panel and stamp \`polledAt\` so a persistent misconfig backs
3024
- // off to \`refresh.everyMs\` instead of retrying every alarm tick.
3025
- this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
3026
- } else if (source.mode === "incremental") {
3027
- // Incremental (plan 136): pull only rows past the durable
3028
- // watermark (or a full-pull seed/reconcile), upsert-only.
3029
- // eslint-disable-next-line no-await-in-loop -- one sourced table at a time; sequential keeps the writer transaction simple
3030
- await pullExternalSourceIncrementalTick(this.sql as SqlExec, writer, client, table, source, shardKey, now);
3031
- } else {
3032
- // eslint-disable-next-line no-await-in-loop -- one sourced table at a time; slices are independent but small and sequential keeps the writer transaction simple
3033
- await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
3034
- }
3035
-
3036
- // Timestamp AFTER the poll finishes, not the batch-start \`now\` — a
3037
- // poll that outruns \`everyMs\` must not make \`nextDueAt\` (below)
3038
- // stale-immediate and re-arm the alarm in a hammering loop.
3039
- polledAt.set(table, Date.now());
3040
- } catch (error) {
3041
- this.recordExternalSourceError(table, error);
3042
- // Stamp on failure too, so a persistently failing source throttles
3043
- // to \`refresh.everyMs\` rather than being hammered every tick.
3044
- polledAt.set(table, Date.now());
3045
- }
3046
- }
3047
-
3048
- // This source's own next-due time, read AFTER the poll-or-skip above
3049
- // so a just-polled source reports \`now + everyMs\` (its FRESH due
3050
- // time), not the stale pre-poll one. An omitted \`refresh\` (poll
3051
- // every tick) is due again immediately.
3052
- const sourceNextDueAt = source.refresh === undefined ? now : (polledAt.get(table) ?? now) + source.refresh.everyMs;
3053
-
3054
- nextDueAt = nextDueAt === undefined ? sourceNextDueAt : Math.min(nextDueAt, sourceNextDueAt);
3055
- }
3056
-
3057
- return nextDueAt;
3058
- }
3059
- ` : "";
3060
- const hasTtlTables = ttlSweeps.length > 0;
3061
- const sourceBootstrap = hasSourcedTables ? `
3062
- const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
3063
- const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
3064
-
3065
- return source !== undefined && source.refresh !== "manual";
3066
- });
3067
-
3068
- if (autoSourced) {
3069
- void this.scheduleSourcePoll();
3070
- }
3071
- ` : "";
3072
- const ttlBootstrap = hasTtlTables ? `
3073
- if (LUNORA_TTL_SWEEPS.length > 0) {
3074
- void this.scheduleTtlSweep();
3075
- }
3076
- ` : "";
3077
- const sourceConstructorOverride = hasSourcedTables || hasTtlTables ? `
3078
- public constructor(state: ShardDOState, env: unknown) {
3079
- super(state, env);
3080
- ${sourceBootstrap}${ttlBootstrap} }
3081
- ` : "";
3082
- const facadeBlock = hasTables ? `
3083
- const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
3084
- ${schema.tables.map(
3085
- // Always bind through `db` (the shard ctx-db) — even for `.global()`
3086
- // tables. `createShardCtxDb` routes global ops to the D1 `globalDb`
3087
- // internally AND stamps the read-dependency / change-broadcast hooks that
3088
- // drive live subscriptions; binding a global facade straight to `globalDb`
3089
- // would skip those, so a `ctx.db.<global>` read/write would never refresh
3090
- // a subscriber of that table.
3091
- (table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
3092
- ).join("\n")}
3093
- ` : "";
3094
- const secretsBuild = `
3095
- const secrets = createSecrets(env);
3096
- `;
3097
- const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${envFragments.build}${secretsBuild}`;
3098
- const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${notifyFragments.contextField}${analyticsFragments.contextField}${envFragments.contextField}
3099
- secrets,`;
3100
- const notifyBuild = notifyFragments.build;
3101
- const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines || hasX402;
3102
- const actionOnlyBlock = actionOnlyHasAny ? `
3103
- // ActionCtx-only helpers (external, non-deterministic I/O): constructed
3104
- // and attached only for an \`action\` so query/mutation ctx never carry them.
3105
- if (isAction) {
3106
- ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${x402Build}${[
3107
- ...hasImages ? [" ctx.images = images;"] : [],
3108
- ...hasHyperdrive ? [" ctx.sql = sql;"] : [],
3109
- ...hasBrowser ? [" ctx.browser = browser;"] : [],
3110
- ...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
3111
- ...hasPipelines ? [" ctx.pipelines = pipelines;"] : [],
3112
- ...hasX402 ? [" ctx.x402 = x402;"] : []
3113
- ].join("\n")}
3114
- }
3115
- ` : "";
3116
- const isActionLine = actionOnlyHasAny ? ` const isAction = LUNORA_FUNCTIONS[options.functionPath ?? ""]?.kind === "action";
3117
- ` : "";
3118
- return `${GENERATED_HEADER}${importLines.join("\n")}
3119
-
3120
- type FunctionKind = "action" | "mutation" | "query";
3121
-
3122
- interface FunctionReference {
3123
- __lunoraRef: string;
3124
- }
3125
-
3126
- /** Foreign-key columns per table (\`v.id("target")\` fields) for the data browser. */
3127
- const LUNORA_TABLE_REFS: Record<string, Record<string, string>> = ${JSON.stringify(tableReferences, void 0, 4)};
3128
-
3129
- /** Declared indexes per table (secondary, search, geo, rank, vector) for the schema viewer. */
3130
- const LUNORA_TABLE_INDEXES: Record<string, Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }>> = ${JSON.stringify(tableIndexes, void 0, 4)};
3131
-
3132
- /** Columns per table (typed, with PK/FK markers) for the studio's schema diagram, served via \`__lunora_admin__:describeTable\`. */
3133
- const LUNORA_TABLE_COLUMNS: Record<string, Array<{ isStorage?: boolean; name: string; optional: boolean; pk?: boolean; ref?: string; type: string }>> = ${JSON.stringify(tableColumns, void 0, 4)};
3134
-
3135
- /** Storage-key columns per table (\`v.storage(...)\` fields) for the file browser's records↔files join. */
3136
- const LUNORA_STORAGE_COLUMNS: Record<string, string[]> = ${JSON.stringify(storageColumns, void 0, 4)};
3137
-
3138
- /** Declarative TTL policies (\`.ttl(field, { after? })\`) the DO alarm sweep auto-expires rows for. */
3139
- const LUNORA_TTL_SWEEPS: Array<{ after?: number; field: string; softDeleteField?: string; table: string }> = ${JSON.stringify(ttlSweeps, void 0, 4)};
3140
-
3141
- /** Static schema advisories (computed by @lunora/advisor at codegen time) served via \`__lunora_admin__:getAdvisories\`. */
3142
- const LUNORA_ADVISORIES: AdvisoryFinding[] = ${JSON.stringify(advisoryData, void 0, 4)};
3143
-
3144
- /** Read-only RLS metadata (policies + roles discovered from \`.use(rls(...))\` chains) served via \`__lunora_admin__:rlsPolicies\` for the studio's RLS inspector. */
3145
- const LUNORA_RLS_METADATA: RlsPoliciesResult = ${JSON.stringify(rlsData, void 0, 4)};
3146
-
3147
- /** Read-only masking metadata (table + column + strategy discovered from \`.use(mask(...))\` chains) served via \`__lunora_admin__:maskPolicies\` for the studio's data-browser mask preview. */
3148
- const LUNORA_MASK_METADATA: MaskPoliciesResult = ${JSON.stringify(maskData, void 0, 4)};
3149
-
3150
- /** Read-only storage access-rule metadata (discovered from \`.use(storageRules(...))\` chains) served via \`__lunora_admin__:storageRules\` for the studio's access-rules view. */
3151
- const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesData, void 0, 4)};
3152
-
3153
- /** Which optional package-backed features this app wires up (discovered from imports / \`ctx.*\` reads / schema signals) served via \`__lunora_admin__:studioFeatures\` so the studio hides nav pages whose package isn't enabled. */
3154
- const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
3155
- ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}${agentSpecs}
3156
- export interface ShardDOConfig {
3157
- /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
3158
- cdc?: boolean;
3159
- /** Optional telemetry sink. When supplied, each \`ctx.log.*\` call is forwarded to \`sink.onLog\`. Pass the SAME sink you give \`createWorker({ observability })\` (which drives \`onRpc\`) to route both RPC and log events. */
3160
- observability?: (env: Record<string, unknown>) => LogSink | undefined;
3161
- scheduler?: (env: Record<string, unknown>) => unknown;
3162
- storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${flagsFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${pipelinesFragments.configField}${paymentsConfigField}${x402ConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}${sourceClientConfigField}
3163
- }
3164
-
3165
- const schedulerStub = {
3166
- cancel: async () => {
3167
- throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
3168
- },
3169
- runAfter: async () => {
3170
- throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
3171
- },
3172
- runAt: async () => {
3173
- throw new Error("ctx.scheduler: no scheduler configured. Pass \`scheduler\` to createShardDO().");
3174
- },
3175
- };
3176
-
3177
- const storageStub = {
3178
- delete: async () => {
3179
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3180
- },
3181
- download: async () => {
3182
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3183
- },
3184
- getMetadata: async () => {
3185
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3186
- },
3187
- getSignedUrl: async () => {
3188
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3189
- },
3190
- getUrl: () => {
3191
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3192
- },
3193
- list: async () => {
3194
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3195
- },
3196
- upload: async () => {
3197
- throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
3198
- },
3199
- };
3200
- ${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${x402Stub}${bindTableHelper}
3201
- // Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
3202
- // referencing call fails loudly with a clear error instead of overflowing the
3203
- // stack. Tracked across the awaited handler chain (one DO invocation is
3204
- // single-threaded), so the counter reflects true nesting depth.
3205
- const MAX_RUN_DEPTH = 32;
3206
- let runDepth = 0;
3207
-
3208
- const dispatchRun = async (expected: FunctionKind, functionPath: string, args: Record<string, unknown>, ctx: unknown): Promise<unknown> => {
3209
- const registered = LUNORA_FUNCTIONS[functionPath];
3210
-
3211
- if (!registered) {
3212
- throw new Error(\`unknown function: \${functionPath}\`);
3213
- }
3214
-
3215
- if (registered.kind !== expected) {
3216
- throw new Error(\`ctx.run\${expected[0]!.toUpperCase()}\${expected.slice(1)}: "\${functionPath}" is registered as a \${registered.kind}, not a \${expected}\`);
3217
- }
3218
-
3219
- if (runDepth >= MAX_RUN_DEPTH) {
3220
- throw new LunoraError("RUN_DEPTH_EXCEEDED", \`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`);
3221
- }
3222
-
3223
- runDepth += 1;
3224
-
3225
- try {
3226
- return await registered.handler(ctx, args);
3227
- } finally {
3228
- runDepth -= 1;
3229
- }
3230
- };
3231
-
3232
- /**
3233
- * Build the project's shard Durable Object. Export the result as \`ShardDO\`
3234
- * from the worker entry so wrangler binds it by name.
3235
- */
3236
- export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
3237
- class extends ShardDOBase {${sourceConstructorOverride}
3238
- private migrated = false;
3239
-
3240
- public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
3241
- const registered = LUNORA_FUNCTIONS[functionPath];
3242
-
3243
- // Internal functions are reachable server-side only: via \`ctx.run*\`
3244
- // composition, or a trusted system dispatch (scheduler/cron, marked by
3245
- // \`isSystemDispatch()\`). A client RPC never carries that flag, so its
3246
- // internals stay not-found and never leak across the external boundary.
3247
- if (!registered || (registered.visibility === "internal" && !this.isSystemDispatch())) {
3248
- throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
3249
- }
3250
-
3251
- this.ensureMigrated();
3252
-
3253
- const ctx = this.buildCtx({ functionPath });
3254
-
3255
- // A mutation's writes must commit all-or-nothing: wrap its dispatch in
3256
- // the DO's BEGIN/COMMIT span so any throw (a validator, an RLS denial,
3257
- // an OCC conflict, a failed row mid-\`insertMany\`/\`patchMany\`) rolls
3258
- // back every write the mutation made. Queries are read-only and actions
3259
- // do external I/O that can't be rolled back, so both dispatch directly.
3260
- // \`ctx.run*\` composition runs inside this span (it never re-enters
3261
- // handleRpc); runInTransaction's own guard rejects accidental nesting.
3262
- //
3263
- // The replay bookkeeping (idempotency dedup row + custom-mutator
3264
- // watermark advance) commits INSIDE this span via
3265
- // \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
3266
- // watermark are atomic — a crash can't leave the writes durable without
3267
- // the replay guard.
3268
- if (registered.kind === "mutation") {
3269
- return this.runInTransaction(async () => {
3270
- const result = await registered.handler(ctx, args);
3271
-
3272
- this.commitMutationBookkeeping(result);
3273
-
3274
- return result;
3275
- });
3276
- }
3277
-
3278
- return registered.handler(ctx, args);
3279
- }
3280
- ${relationFanout.override}
3281
- protected override async executeSubscription(functionPath: string, args: Record<string, unknown>, identity?: { identity?: Record<string, unknown>; userId?: string }): Promise<{ result: unknown; tables: Set<string> } | null> {
3282
- const registered = LUNORA_FUNCTIONS[functionPath];
3283
-
3284
- if (!registered || registered.kind !== "query" || registered.visibility === "internal") {
3285
- return null;
3286
- }
3287
-
3288
- this.ensureMigrated();
3289
-
3290
- const tables = new Set<string>();
3291
- // Identity is threaded EXPLICITLY from the (deferred/interleaved)
3292
- // subscription caller — never read from the shared per-request field
3293
- // here — so a concurrent RPC can't leak its identity into this re-run.
3294
- const ctx = this.buildCtx({ functionPath, identity, onRead: (table) => tables.add(table) });
3295
- const result = await registered.handler(ctx, args);
3296
-
3297
- return { result, tables };
3298
- }
3299
-
3300
- protected override executeStream(functionPath: string, args: Record<string, unknown>): null | { iterator: (signal: AbortSignal) => AsyncIterable<unknown> } {
3301
- const registered = LUNORA_FUNCTIONS[functionPath];
3302
-
3303
- if (!registered || registered.kind !== "stream" || registered.visibility === "internal") {
3304
- return null;
3305
- }
3306
-
3307
- this.ensureMigrated();
3308
-
3309
- return {
3310
- iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
3311
- };
3312
- }
3313
- ${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
3314
- protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
3315
- return LUNORA_LIFECYCLE_HOOKS[event];
3316
- }
3317
-
3318
- protected override tableRefs(table: string): Record<string, string> | undefined {
3319
- return LUNORA_TABLE_REFS[table];
3320
- }
3321
-
3322
- protected override tableIndexes(table: string): Array<{ fields: string[]; name: string; type: "geo" | "index" | "rank" | "search" | "vector"; unique?: boolean }> {
3323
- return LUNORA_TABLE_INDEXES[table] ?? [];
3324
- }
3325
-
3326
- protected override ttlSweeps(): ReadonlyArray<{ after?: number; field: string; softDeleteField?: string; table: string }> {
3327
- return LUNORA_TTL_SWEEPS;
3328
- }
3329
-
3330
- protected override tableColumns(table: string): Array<{ isStorage?: boolean; name: string; optional: boolean; pk?: boolean; ref?: string; type: string }> {
3331
- return LUNORA_TABLE_COLUMNS[table] ?? [];
3332
- }
3333
-
3334
- protected override storageColumns(): Record<string, string[]> {
3335
- return LUNORA_STORAGE_COLUMNS;
3336
- }
3337
-
3338
- protected override rlsMetadata(): RlsPoliciesResult {
3339
- return LUNORA_RLS_METADATA;
3340
- }
3341
-
3342
- protected override maskMetadata(): MaskPoliciesResult {
3343
- return LUNORA_MASK_METADATA;
3344
- }
3345
-
3346
- protected override storageRulesMetadata(): StorageRulesResult {
3347
- return LUNORA_STORAGE_RULES;
3348
- }
3349
-
3350
- protected override studioFeatures(): StudioFeaturesResult {
3351
- return LUNORA_STUDIO_FEATURES;
3352
- }
3353
- ${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
3354
- protected override advisories(): AdvisoryFinding[] {
3355
- return LUNORA_ADVISORIES;
3356
- }
3357
-
3358
- protected override async runShardDataMigration(args: RunShardMigrationArgs): Promise<MigrationRunResult> {
3359
- const migration = LUNORA_MIGRATIONS[args.id];
3360
-
3361
- if (!migration) {
3362
- throw new LunoraError("MIGRATION_NOT_FOUND", \`data migration "\${args.id}" is not registered\`, { status: 404 });
3363
- }
3364
-
3365
- this.ensureMigrated();
3366
-
3367
- const env = (this.env ?? {}) as Record<string, unknown>;
3368
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3369
- const writer = createShardCtxDb({
3370
- broadcast: (delta) => {
3371
- this.recordChangedTable(delta.table);
3372
- },
3373
- cdc: config.cdc ?? false,
3374
- scheduler,
3375
- schema: schema as unknown as SchemaLike,
3376
- sql: this.sql as SqlExec,
3377
- });
3378
-
3379
- return runDataMigration({
3380
- batchSize: args.batchSize,
3381
- direction: args.direction,
3382
- dryRun: args.dryRun,
3383
- maxBatches: args.maxBatches,
3384
- migration: migration as unknown as DataMigrationLike,
3385
- // Flush after each batch so live migrationStatus subscribers see
3386
- // progress mid-run (the base method records the reserved state
3387
- // table the raw-SQL progress write is invisible to the tracker).
3388
- onBatch: () => this.flushMigrationProgress(),
3389
- sql: this.sql as SqlExec,
3390
- writer,
3391
- });
3392
- }
3393
-
3394
- protected override async runShardWrite(args: RunShardWriteArgs): Promise<RunShardWriteResult> {
3395
- const definition = (schema as unknown as SchemaLike).tables[args.table];
3396
-
3397
- if (!definition) {
3398
- throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${args.table}\`, { status: 404 });
3399
- }
3400
-
3401
- // \`.global()\` tables live in D1, not this DO's SQLite — editing them
3402
- // here would corrupt nothing but would fail confusingly, so reject up
3403
- // front with a clear code the studio can surface.
3404
- if (definition.shardMode?.kind === "global") {
3405
- throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${args.table}" is global; edit it through D1, not the shard\`, { status: 400 });
3406
- }
3407
-
3408
- this.ensureMigrated();
3409
-
3410
- const env = (this.env ?? {}) as Record<string, unknown>;
3411
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3412
- const writer = createShardCtxDb({
3413
- broadcast: (delta) => {
3414
- this.recordChangedTable(delta.table);
3415
- },
3416
- cdc: config.cdc ?? false,
3417
- scheduler,
3418
- schema: schema as unknown as SchemaLike,
3419
- sql: this.sql as SqlExec,
3420
- });
3421
-
3422
- if (args.op === "insert") {
3423
- const id = await writer.insert(args.table, args.doc ?? {});
3424
-
3425
- return { id, op: "insert" };
3426
- }
3427
-
3428
- if (args.op === "delete") {
3429
- await writer.delete(args.id ?? "");
3430
-
3431
- return { id: args.id ?? null, op: "delete" };
3432
- }
3433
-
3434
- if (args.op === "replace") {
3435
- await writer.replace(args.id ?? "", args.doc ?? {});
3436
-
3437
- return { id: args.id ?? null, op: "replace" };
3438
- }
3439
-
3440
- await writer.patch(args.id ?? "", args.doc ?? {});
3441
-
3442
- return { id: args.id ?? null, op: "patch" };
3443
- }
3444
-
3445
- protected override async deleteRowThroughWriter(table: string, id: string): Promise<void> {
3446
- const definition = (schema as unknown as SchemaLike).tables[table];
3447
-
3448
- if (!definition) {
3449
- throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${table}\`, { status: 404 });
3450
- }
3451
-
3452
- // \`.global()\` tables live in D1, not this DO's SQLite — the same
3453
- // guard \`runShardWrite\` applies to single-row edits.
3454
- if (definition.shardMode?.kind === "global") {
3455
- throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${table}" is global; edit it through D1, not the shard\`, { status: 400 });
3456
- }
3457
-
3458
- this.ensureMigrated();
3459
-
3460
- const env = (this.env ?? {}) as Record<string, unknown>;
3461
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3462
- const writer = createShardCtxDb({
3463
- broadcast: (delta) => {
3464
- this.recordChangedTable(delta.table);
3465
- },
3466
- cdc: config.cdc ?? false,
3467
- scheduler,
3468
- schema: schema as unknown as SchemaLike,
3469
- sql: this.sql as SqlExec,
3470
- });
3471
-
3472
- // Routes through the writer (not raw SQL) so FTS / aggregate / rank
3473
- // shadow tables stay in sync and \`onDelete\` cascades fire — the
3474
- // bounded loop lives in the base \`runShardBulkDelete\`.
3475
- await writer.delete(id);
3476
- }
3477
-
3478
- protected override async runShardRankBefore(args: RunShardRankBeforeArgs): Promise<{ before: number; total: number }> {
3479
- this.ensureMigrated();
3480
-
3481
- const env = (this.env ?? {}) as Record<string, unknown>;
3482
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3483
- const writer = createShardCtxDb({
3484
- broadcast: (delta) => {
3485
- this.recordChangedTable(delta.table);
3486
- },
3487
- cdc: config.cdc ?? false,
3488
- scheduler,
3489
- schema: schema as unknown as SchemaLike,
3490
- sql: this.sql as SqlExec,
3491
- });
3492
-
3493
- // \`rankBefore\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
3494
- // but the shard writer from \`createShardCtxDb\` always defines it.
3495
- if (!writer.rankBefore) {
3496
- throw new LunoraError("NOT_IMPLEMENTED", "rankBefore is unavailable on the shard writer", { status: 500 });
3497
- }
3498
-
3499
- return writer.rankBefore(args.table, args.index, {
3500
- partitionKey: args.partitionKey,
3501
- rowId: args.rowId,
3502
- sortValues: args.sortValues,
3503
- });
3504
- }
3505
-
3506
- protected override async runShardRankPage(args: RunShardRankPageArgs): Promise<ShardRankPageResult> {
3507
- this.ensureMigrated();
3508
-
3509
- const env = (this.env ?? {}) as Record<string, unknown>;
3510
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3511
- const writer = createShardCtxDb({
3512
- broadcast: (delta) => {
3513
- this.recordChangedTable(delta.table);
3514
- },
3515
- cdc: config.cdc ?? false,
3516
- scheduler,
3517
- schema: schema as unknown as SchemaLike,
3518
- sql: this.sql as SqlExec,
3519
- });
3520
-
3521
- // \`rankPageRows\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
3522
- // but the shard writer from \`createShardCtxDb\` always defines it. The sort
3523
- // directions live in the schema's rankIndex, so the shard reads them itself;
3524
- // \`args.directions\` is only the coordinator's comparator hint and isn't forwarded.
3525
- if (!writer.rankPageRows) {
3526
- throw new LunoraError("NOT_IMPLEMENTED", "rankPage is unavailable on the shard writer", { status: 500 });
3527
- }
3528
-
3529
- return writer.rankPageRows(args.table, args.index, {
3530
- after: args.after,
3531
- cursor: args.cursor,
3532
- partitionKey: args.partitionKey,
3533
- take: args.take,
3534
- });
3535
- }
3536
-
3537
- protected override async runShardApplyCdc(args: RunShardApplyCdcArgs): Promise<{ applied: number }> {
3538
- this.ensureMigrated();
3539
-
3540
- const env = (this.env ?? {}) as Record<string, unknown>;
3541
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3542
- const writer = createShardCtxDb({
3543
- broadcast: (delta) => {
3544
- this.recordChangedTable(delta.table);
3545
- },
3546
- cdc: config.cdc ?? false,
3547
- scheduler,
3548
- schema: schema as unknown as SchemaLike,
3549
- sql: this.sql as SqlExec,
3550
- });
3551
-
3552
- await applyCdcChanges(writer, args.changes);
3553
-
3554
- return { applied: args.changes.length };
3555
- }
3556
-
3557
- protected override ensureMigrated(): void {
3558
- if (this.migrated) {
3559
- return;
3560
- }
3561
-
3562
- runShardMigrations(this.sql as SqlExec, schema as unknown as SchemaLike, { cdc: config.cdc ?? false });
3563
- this.migrated = true;
3564
- }
3565
-
3566
- private buildCtx(options: { functionPath?: string; identity?: { identity?: Record<string, unknown>; userId?: string }; onRead?: (table: string) => void } = {}): unknown {
3567
- const env = (this.env ?? {}) as Record<string, unknown>;
3568
- // When the caller threads an explicit identity (subscription seed /
3569
- // refresh — both run in deferred/interleaved contexts), use it by
3570
- // value and NEVER read the shared per-request identity field, which a
3571
- // concurrent RPC may have re-set. Otherwise (the synchronous RPC
3572
- // dispatch path) fall back to the per-request fields as before.
3573
- const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
3574
- const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
3575
- ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}${agentsBuild}
3576
- const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
3577
- // Build the storage adapter once and share it between \`ctx.storage\`
3578
- // and \`ctx.db.system._storage\` so both read the same R2 binding. The
3579
- // \`storageStub\` fallback satisfies SystemReaderStorageLike structurally
3580
- // (its \`list\`/\`getMetadata\` throw the "no storage configured" error).
3581
- const storage = asBucketStorage(config.storage?.(env) ?? storageStub) as unknown as SystemReaderStorageLike;
3582
- ${globalDatabaseLine} const db: DatabaseWriterLike = createShardCtxDb(${databaseOptions});
3583
- ${facadeBlock}${paymentsBuild}
3584
- // \`ctx.log\`: the DO base builds the attributed logger (structured
3585
- // fields + \`.with(...)\` child + trace correlation) and routes each call
3586
- // to the optional \`observability\` sink. It also buffers the line (studio
3587
- // Logs panel) and emits a structured console event the dev-server formats.
3588
- const observability = config.observability?.(env);
3589
- const logFunctionPath = options.functionPath ?? "";
3590
- const log = this.makeLogger(logFunctionPath, observability);
3591
-
3592
- // \`ctx.trace\` / \`ctx.metrics\`: spans and measurements to the same sink.
3593
- // The trace anchor is threaded explicitly for the same reason \`identity\`
3594
- // is — a deferred caller (a subscription re-run) must not inherit the
3595
- // writing mutation's trace from the shared per-request field.
3596
- const trace = this.makeTracer(logFunctionPath, observability, options.identity ? undefined : this.getCurrentTrace());
3597
- const metrics = this.makeMetrics(logFunctionPath, observability);
3598
- ${notifyBuild}
3599
- // \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
3600
- // captured ONCE so the whole handler body sees a single stable value.
3601
- // Query/mutation handlers must be deterministic (they may be re-run on
3602
- // OCC retry / subscription re-eval), so they must read time through
3603
- // \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
3604
- // advisor flags the latter. Actions may still use ambient \`Date.now()\`.
3605
- const now = Date.now();
3606
-
3607
- const ctx: Record<string, unknown> = {
3608
- auth: {
3609
- getIdentity: async () => identity ?? null,
3610
- userId: userId ?? null,
3611
- },
3612
- db,
3613
- fetch: globalThis.fetch.bind(globalThis),
3614
- ip: this.getCurrentIp(),
3615
- log,
3616
- metrics,
3617
- now,${ormContextField}
3618
- scheduler,
3619
- storage,
3620
- trace,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}${agentsContextField}
3621
- };
3622
- ${isActionLine}${actionOnlyBlock}
3623
- ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
3624
- ctx.runMutation = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("mutation", reference.__lunoraRef, fnArgs, ctx);
3625
- ctx.runQuery = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("query", reference.__lunoraRef, fnArgs, ctx);
3626
-
3627
- return ctx;
3628
- }
3629
- };
3630
- `;
3631
- };
3632
- const validatorToDrizzleColumn = (validator) => {
3633
- switch (validator.kind) {
3634
- case "array":
3635
- case "object":
3636
- case "record":
3637
- case "union": {
3638
- return {
3639
- builder: "text",
3640
- mode: "json",
3641
- notNull: true,
3642
- typeAnnotation: validatorToType(validator)
3643
- };
3644
- }
3645
- case "bigint": {
3646
- return { builder: "blob", mode: "bigint", notNull: true };
3647
- }
3648
- case "boolean": {
3649
- return { builder: "integer", mode: "boolean", notNull: true };
3650
- }
3651
- case "bytes": {
3652
- return { builder: "blob", mode: "buffer", notNull: true };
3653
- }
3654
- case "date":
3655
- case "timestamp": {
3656
- return { builder: "integer", notNull: true };
3657
- }
3658
- case "id": {
3659
- return { builder: "text", notNull: true };
3660
- }
3661
- case "literal": {
3662
- const value = validator.literalValue ?? "";
3663
- if (value.startsWith('"') || value.startsWith("'")) {
3664
- return { builder: "text", notNull: true };
3665
- }
3666
- if (value === "true" || value === "false") {
3667
- return { builder: "integer", mode: "boolean", notNull: true };
3668
- }
3669
- if (value === "null") {
3670
- return { builder: "text", notNull: true };
3671
- }
3672
- return { builder: "real", notNull: true };
3673
- }
3674
- case "null": {
3675
- return { builder: "text", notNull: true };
3676
- }
3677
- case "number": {
3678
- return { builder: "real", notNull: true };
3679
- }
3680
- case "optional": {
3681
- const inner = validator.inner ? validatorToDrizzleColumn(validator.inner) : { builder: "text", notNull: false };
3682
- return { ...inner, notNull: false };
3683
- }
3684
- case "storage":
3685
- case "string": {
3686
- return { builder: "text", notNull: true };
3687
- }
3688
- default: {
3689
- return { builder: "text", notNull: true };
3690
- }
3691
- }
3692
- };
3693
- const renderDrizzleColumn = (name, validator, knownTables) => {
3694
- assertIdentifier(name, "drizzle column name");
3695
- const column = validatorToDrizzleColumn(validator);
3696
- const isOptional = validator.kind === "optional";
3697
- const innerValidator = isOptional && validator.inner ? validator.inner : validator;
3698
- const modeArgument = column.mode ? `, { mode: "${column.mode}" }` : "";
3699
- let expression = `${column.builder}("${name}"${modeArgument})`;
3700
- if (column.typeAnnotation) {
3701
- expression += `.$type<${column.typeAnnotation}>()`;
3702
- }
3703
- if (innerValidator.kind === "id" && innerValidator.tableName && knownTables.has(innerValidator.tableName)) {
3704
- expression += `.references(() => ${innerValidator.tableName}._id)`;
3705
- }
3706
- if (column.notNull) {
3707
- expression += ".notNull()";
3708
- }
3709
- return expression;
3710
- };
3711
- const renderIndexEntry = (index) => {
3712
- assertIdentifier(index.name, "drizzle index name");
3713
- const constructor = index.unique ? "uniqueIndex" : "index";
3714
- const fields = index.fields.map((field) => {
3715
- assertIdentifier(field, "drizzle index field");
3716
- return `t.${field}`;
3717
- }).join(", ");
3718
- return ` ${index.name}: ${constructor}("${index.name}").on(${fields}),`;
3719
- };
3720
- const renderDrizzleTable = (table, knownTables) => {
3721
- assertIdentifier(table.name, "table name");
3722
- const columns = [
3723
- ` _id: text("_id").primaryKey(),`,
3724
- ` _creationTime: integer("_creationTime").notNull(),`,
3725
- ...Object.entries(table.shape).map(([fieldName, validator]) => ` ${fieldName}: ${renderDrizzleColumn(fieldName, validator, knownTables)},`)
3726
- ].join("\n");
3727
- const indexBody = table.indexes.length > 0 ? `, (t) => ({
3728
- ${table.indexes.map((index) => renderIndexEntry(index)).join("\n")}
3729
- })` : "";
3730
- return `export const ${table.name} = sqliteTable("${table.name}", {
3731
- ${columns}
3732
- }${indexBody});`;
3733
- };
3734
- const usedImports = (tables) => {
3735
- const columns = /* @__PURE__ */ new Set();
3736
- const indexes = /* @__PURE__ */ new Set();
3737
- columns.add("text");
3738
- columns.add("integer");
3739
- columns.add("sqliteTable");
3740
- for (const table of tables) {
3741
- for (const validator of Object.values(table.shape)) {
3742
- const column = validatorToDrizzleColumn(validator);
3743
- columns.add(column.builder);
3744
- }
3745
- for (const index of table.indexes) {
3746
- indexes.add(index.unique ? "uniqueIndex" : "index");
3747
- }
3748
- }
3749
- return { columns: [...columns].toSorted((a, b) => a.localeCompare(b)), indexes: [...indexes].toSorted((a, b) => a.localeCompare(b)) };
3750
- };
3751
- const renderDrizzleFile = (tables, useUmbrella = false) => {
3752
- if (tables.length === 0) {
3753
- return `${GENERATED_HEADER}export {};
3754
- `;
3755
- }
3756
- const base = baseSpecifiers(useUmbrella);
3757
- const { columns, indexes } = usedImports(tables);
3758
- const importParts = [...indexes, ...columns].toSorted((a, b) => a.localeCompare(b));
3759
- const knownTables = new Set(tables.map((table) => table.name));
3760
- const tableBlocks = tables.map((table) => renderDrizzleTable(table, knownTables)).join("\n\n");
3761
- return `${GENERATED_HEADER}import { ${importParts.join(", ")} } from "${base.serverDrizzle}";
3762
-
3763
- ${tableBlocks}
3764
- `;
3765
- };
3766
- const emitDrizzleSchema = (schema, useUmbrella = false) => {
3767
- const globalTables = schema.tables.filter((table) => table.shardMode === "global");
3768
- const shardTables = schema.tables.filter((table) => table.shardMode !== "global");
3769
- return {
3770
- global: renderDrizzleFile(globalTables, useUmbrella),
3771
- shard: renderDrizzleFile(shardTables, useUmbrella)
3772
- };
3773
- };
3774
- const emitCrons = (crons) => {
3775
- const triggers = [];
3776
- const byExpression = /* @__PURE__ */ new Map();
3777
- for (const cron of crons) {
3778
- const existing = byExpression.get(cron.cron);
3779
- if (existing) {
3780
- existing.push(cron);
3781
- } else {
3782
- byExpression.set(cron.cron, [cron]);
3783
- triggers.push(cron.cron);
3784
- }
3785
- }
3786
- const triggerEntries = triggers.map((expression) => ` ${JSON.stringify(expression)},`).join("\n");
3787
- const triggerBody = triggerEntries.length > 0 ? `
3788
- ${triggerEntries}
3789
- ` : "";
3790
- const mapEntries = [...byExpression.entries()].map(([expression, jobs]) => {
3791
- const jobEntries = jobs.map((job) => {
3792
- const targetField = job.workflow ? `workflow: ${JSON.stringify(job.workflow.binding)}` : `functionPath: ${JSON.stringify(job.functionPath)}`;
3793
- return ` { name: ${JSON.stringify(job.name)}, ${targetField}, args: ${JSON.stringify(job.args)} },`;
3794
- }).join("\n");
3795
- return ` ${JSON.stringify(expression)}: [
3796
- ${jobEntries}
3797
- ],`;
3798
- }).join("\n");
3799
- const mapBody = mapEntries.length > 0 ? `
3800
- ${mapEntries}
3801
- ` : "";
3802
- return `${GENERATED_HEADER}/**
3803
- * One scheduled cron invocation. Exactly one target is set: \`functionPath\` is
3804
- * the \`namespace:fn\` dispatch ref (matches \`__lunoraRef\`), invoked on the
3805
- * shard; \`workflow\` is a \`WORKFLOW_*\` binding name whose durable workflow is
3806
- * started fresh per fire. \`args\` are forwarded verbatim (a workflow's become
3807
- * its \`params\`).
3808
- */
3809
- export interface LunoraCronJob {
3810
- args: Record<string, unknown>;
3811
- functionPath?: string;
3812
- name: string;
3813
- workflow?: string;
3814
- }
3815
-
3816
- /**
3817
- * Deduplicated cron schedules — mirror of wrangler's \`triggers.crons\`. The
3818
- * Lunora vite plugin keeps \`wrangler.jsonc\` in sync with this array.
3819
- */
3820
- export const LUNORA_CRON_TRIGGERS: ReadonlyArray<string> = [${triggerBody}];
3821
-
3822
- /**
3823
- * Dispatcher map keyed by cron expression. The Worker's \`scheduled()\` handler
3824
- * looks up \`event.cron\` here and dispatches every job in the list.
3825
- */
3826
- export const LUNORA_CRONS: Record<string, ReadonlyArray<LunoraCronJob>> = {${mapBody}};
3827
- `;
3828
- };
3829
- const emitVectors = (vectorIndexes) => {
3830
- const entries = [...vectorIndexes].toSorted((a, b) => a.name.localeCompare(b.name)).map((index) => {
3831
- const parts = [`name: ${JSON.stringify(index.name)}`, `table: ${JSON.stringify(index.table)}`];
3832
- if (index.field !== void 0) {
3833
- parts.push(`field: ${JSON.stringify(index.field)}`);
3834
- }
3835
- if (index.dimensions !== void 0) {
3836
- parts.push(`dimensions: ${JSON.stringify(index.dimensions)}`);
3837
- }
3838
- if (index.metric !== void 0) {
3839
- parts.push(`metric: ${JSON.stringify(index.metric)}`);
3840
- }
3841
- if (index.metadata !== void 0) {
3842
- parts.push(`metadata: ${JSON.stringify(index.metadata)}`);
3843
- }
3844
- return ` { ${parts.join(", ")} },`;
3845
- }).join("\n");
3846
- const body = entries.length > 0 ? `
3847
- ${entries}
3848
- ` : "";
3849
- return `${GENERATED_HEADER}/**
3850
- * One vector index declared in \`schema.ts\` — an inline \`.vectorize()\` column
3851
- * or a standalone \`defineVectorIndex()\`. \`field\` is the source column (absent
3852
- * for a \`select\`-derived Shape B index); \`metadata\` lists the row fields
3853
- * mirrored into Vectorize for filtering.
3854
- */
3855
- export interface LunoraVectorIndex {
3856
- dimensions?: number;
3857
- field?: string;
3858
- metadata?: ReadonlyArray<string>;
3859
- metric?: "cosine" | "dot-product" | "euclidean";
3860
- name: string;
3861
- table: string;
3862
- }
3863
-
3864
- /**
3865
- * Static registry of every vector index in the schema, sorted by name.
3866
- * Vectorize cannot enumerate indexes at runtime, so the worker passes this to
3867
- * \`createWorker({ vectorIntrospector })\` to back the studio's vector browser.
3868
- */
3869
- export const LUNORA_VECTOR_INDEXES: ReadonlyArray<LunoraVectorIndex> = [${body}];
3870
- `;
3871
- };
3872
- const emitWranglerCronTriggers = (crons) => {
3873
- const seen = /* @__PURE__ */ new Set();
3874
- const triggers = [];
3875
- for (const cron of crons) {
3876
- if (!seen.has(cron.cron)) {
3877
- seen.add(cron.cron);
3878
- triggers.push(cron.cron);
3879
- }
3880
- }
3881
- return triggers;
3882
- };
3883
-
3884
- export { APP_METHOD_CAPABILITIES as A, CAPABILITIES as C, GENERATED_HEADER as G, emitApi as a, emitCollections as b, emitContainers as c, emitCrons as d, emitAgents as e, emitDataModel as f, emitDrizzleSchema as g, emitFunctions as h, emitServer as i, emitShard as j, emitVectors as k, emitWorkflows as l, emitWranglerCronTriggers as m, buildStorageColumns as n, emitQueues as o, emitSeed as p };