@lunora/codegen 1.0.0-alpha.5 → 1.0.0-alpha.50

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 (42) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/index.d.mts +1950 -734
  4. package/dist/index.d.ts +1950 -734
  5. package/dist/index.mjs +32 -24
  6. package/dist/packem_shared/AGENTS_FILENAME-Cxx7wiSs.mjs +158 -0
  7. package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DjpXMqhp.mjs} +1 -1
  8. package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DyQ5FwkM.mjs} +7 -5
  9. package/dist/packem_shared/FLAGS_FILENAME-Dg4mKUuz.mjs +139 -0
  10. package/dist/packem_shared/GENERATED_HEADER-C5paWNYO.mjs +4 -0
  11. package/dist/packem_shared/{LUNORA_ERROR_CODES-CySpQPD3.mjs → LUNORA_ERROR_CODES-DvTLozCu.mjs} +8 -1
  12. package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +81 -0
  13. package/dist/packem_shared/{OPENRPC_VERSION-C7zKVeKi.mjs → OPENRPC_VERSION-7A2weq2u.mjs} +2 -2
  14. package/dist/packem_shared/QUEUES_FILENAME-Xv6Vy-Ku.mjs +145 -0
  15. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-G5dkcxPR.mjs +3502 -0
  16. package/dist/packem_shared/{SCHEMA_SNAPSHOT_VERSION-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs} +18 -2
  17. package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +94 -0
  18. package/dist/packem_shared/WORKFLOWS_FILENAME-Dtuzvluc.mjs +158 -0
  19. package/dist/packem_shared/{buildOpenApiDocument-Cns1EkCQ.mjs → buildOpenApiDocument-B7mEdzA2.mjs} +2 -2
  20. package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-Dx3K42rk.mjs} +1 -1
  21. package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-DvqkEWdx.mjs} +68 -36
  22. package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BJ-qR7Rg.mjs} +72 -14
  23. package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-daCzuTe8.mjs} +10 -2
  24. package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-DI4q5NxE.mjs} +24 -2
  25. package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-BcTOEKNU.mjs} +58 -2
  26. package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-VNUFvCwr.mjs} +10 -4
  27. package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-S0N2xLCq.mjs} +1 -1
  28. package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-CJnnnLpd.mjs} +1 -1
  29. package/dist/packem_shared/{discoverR2sqlCalls-BpDqvcUn.mjs → discoverR2sqlCalls-pnpicWfz.mjs} +1 -1
  30. package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-DppniPUH.mjs} +1 -1
  31. package/dist/packem_shared/discoverSandboxUsage-BUM2r90k.mjs +38 -0
  32. package/dist/packem_shared/{discoverSchema-DB_QrIQe.mjs → discoverSchema-BaSAvooG.mjs} +326 -14
  33. package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-CnHl2rXD.mjs} +1 -1
  34. package/dist/packem_shared/{GENERATED_HEADER-DF1hQcix.mjs → emit-ClTbCLy4.mjs} +1458 -154
  35. package/dist/packem_shared/{emitApp-Ci_hcJNO.mjs → emitApp-CzzrjVqH.mjs} +129 -31
  36. package/dist/packem_shared/formatAdvisories-BmF7Mplc.mjs +115 -0
  37. package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-BSJo1HGP.mjs} +18 -6
  38. package/dist/packem_shared/redact-oTmsol5A.mjs +33 -0
  39. package/package.json +10 -7
  40. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-BqOtU2lC.mjs +0 -922
  41. package/dist/packem_shared/WORKFLOWS_FILENAME-DRDQdhfq.mjs +0 -84
  42. package/dist/packem_shared/formatAdvisories-8NIv1k0I.mjs +0 -69
@@ -1,27 +1,379 @@
1
+ import { agentComponent } from '@lunora/agent/component';
2
+ import { LunoraError } from '@lunora/errors';
1
3
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
2
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
+
3
352
  const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
4
353
  const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
5
354
  client: "lunorash/client",
6
355
  do: "lunorash/do",
356
+ flags: "lunorash/flags",
7
357
  server: "lunorash/server",
8
358
  serverDataModel: "lunorash/server/data-model",
9
359
  serverDrizzle: "lunorash/server/drizzle",
10
- serverTypes: "lunorash/server/types"
360
+ serverTypes: "lunorash/server/types",
361
+ values: "lunorash/values"
11
362
  } : {
12
363
  client: "@lunora/client",
13
364
  do: "@lunora/do",
365
+ flags: "@lunora/flags",
14
366
  server: "@lunora/server",
15
367
  serverDataModel: "@lunora/server/data-model",
16
368
  serverDrizzle: "@lunora/server/drizzle",
17
- serverTypes: "@lunora/server/types"
369
+ serverTypes: "@lunora/server/types",
370
+ values: "@lunora/values"
18
371
  };
19
372
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
20
- const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
21
373
  const IMPORT_PATH_RE = /^[\w./-]+$/u;
22
374
  const assertIdentifier = (value, context) => {
23
375
  if (!IDENTIFIER_RE.test(value)) {
24
- throw new Error(`@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
376
+ throw new LunoraError("INTERNAL", `@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
25
377
  }
26
378
  };
27
379
  const renderPropertyKey = (fieldName) => IDENTIFIER_RE.test(fieldName) ? fieldName : JSON.stringify(fieldName);
@@ -48,7 +400,10 @@ const literalToType = (value) => {
48
400
  return "unknown";
49
401
  }
50
402
  if (!LITERAL_VALUE_RE.test(value)) {
51
- throw new Error(`@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`);
403
+ throw new LunoraError(
404
+ "INTERNAL",
405
+ `@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`
406
+ );
52
407
  }
53
408
  return value;
54
409
  };
@@ -342,19 +697,19 @@ ${members}
342
697
  };
343
698
  return [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b)).map((entry) => renderNamespace(entry)).join("\n");
344
699
  };
345
- const renderWorkflowsRef = (workflows) => {
346
- if (workflows.length === 0) {
700
+ const renderSchedulerReferences = (workflows, agents) => {
701
+ if (workflows.length === 0 && agents.length === 0) {
347
702
  return { block: "", importLine: "" };
348
703
  }
349
- const sorted = [...workflows].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
350
- const refMembers = sorted.map((workflow) => ` ${workflow.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n");
351
- const objectMembers = sorted.map(
352
- (workflow) => ` ${workflow.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(workflow.bindingName)}, name: ${JSON.stringify(workflow.exportName)} },`
353
- ).join("\n");
354
- const block = `
704
+ const importLines = [];
705
+ let block = "";
706
+ if (workflows.length > 0) {
707
+ block += `
355
708
  /** Params type carried by a \`defineWorkflow\` definition (its phantom \`__params\`). */
356
709
  type WorkflowParamsOf<Definition> = Definition extends { __params?: infer Params } ? (unknown extends Params ? Record<string, unknown> : Params) : Record<string, unknown>;
357
-
710
+ `;
711
+ }
712
+ block += `
358
713
  /** A typed reference to a durable workflow, addressable for \`cronJobs()\` targets. Mirrors \`@lunora/scheduler\`'s \`WorkflowReference\` structurally. */
359
714
  export interface WorkflowReference<Params = Record<string, unknown>> {
360
715
  readonly isLunoraWorkflow: true;
@@ -362,7 +717,14 @@ export interface WorkflowReference<Params = Record<string, unknown>> {
362
717
  readonly binding: string;
363
718
  readonly name: string;
364
719
  }
365
-
720
+ `;
721
+ if (workflows.length > 0) {
722
+ const sorted = [...workflows].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
723
+ const refMembers = sorted.map((workflow) => ` ${workflow.exportName}: WorkflowReference<WorkflowParamsOf<typeof lunoraWorkflowDefinitions.${workflow.exportName}>>;`).join("\n");
724
+ const objectMembers = sorted.map(
725
+ (workflow) => ` ${workflow.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(workflow.bindingName)}, name: ${JSON.stringify(workflow.exportName)} },`
726
+ ).join("\n");
727
+ block += `
366
728
  /** This project's durable workflows, addressable as typed references (e.g. \`crons.daily("digest", …, workflows.digestPipeline, params)\`). */
367
729
  export interface WorkflowsRef {
368
730
  ${refMembers}
@@ -372,17 +734,200 @@ export const workflows: WorkflowsRef = {
372
734
  ${objectMembers}
373
735
  };
374
736
  `;
375
- return { block, importLine: `import type * as lunoraWorkflowDefinitions from "../workflows.js";
376
- ` };
737
+ importLines.push(`import type * as lunoraWorkflowDefinitions from "../workflows.js";
738
+ `);
739
+ }
740
+ if (agents.length > 0) {
741
+ const sorted = [...agents].toSorted((a, b) => a.exportName.localeCompare(b.exportName));
742
+ const refMembers = sorted.map((agent) => ` ${agent.exportName}: WorkflowReference<AgentRunInput>;`).join("\n");
743
+ const objectMembers = sorted.map(
744
+ (agent) => ` ${agent.exportName}: { isLunoraWorkflow: true, binding: ${JSON.stringify(agent.bindingName)}, name: ${JSON.stringify(agent.name)} },`
745
+ ).join("\n");
746
+ block += `
747
+ /** 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 })\`). */
748
+ export interface AgentsRef {
749
+ ${refMembers}
750
+ }
751
+
752
+ export const agents: AgentsRef = {
753
+ ${objectMembers}
754
+ };
755
+ `;
756
+ importLines.push(`import type { AgentRunInput } from "@lunora/agent";
757
+ `);
758
+ }
759
+ return { block, importLine: importLines.join("") };
377
760
  };
378
- const emitApi = (functions, workflows = [], useUmbrella = false) => {
761
+ const agentRuntimeFunctionNames = () => Object.keys(agentComponent().functions).toSorted((a, b) => a.localeCompare(b));
762
+ const takenAgentFunctionNames = (functions) => new Set(functions.filter((definition) => sanitizeNamespace(definition.filePath) === "agents").map((definition) => definition.exportName));
763
+ const internalAgentRuntimeFunctionNames = () => new Set(
764
+ Object.entries(agentComponent().functions).filter(([, definition]) => definition.visibility === "internal").map(([name]) => name)
765
+ );
766
+ const renderAgentFunctionRegistry = (agents, functions) => {
767
+ const empty = { importLine: "", lines: "", prelude: "" };
768
+ if (agents.length === 0) {
769
+ return empty;
770
+ }
771
+ const taken = takenAgentFunctionNames(functions);
772
+ const internal = internalAgentRuntimeFunctionNames();
773
+ const shadowedInternal = functions.find((definition) => sanitizeNamespace(definition.filePath) === "agents" && internal.has(definition.exportName));
774
+ if (shadowedInternal) {
775
+ throw new LunoraError(
776
+ "INTERNAL",
777
+ `@lunora/codegen: "agents:${shadowedInternal.exportName}" is reserved for the durable agent loop's internal dispatch — rename the "${shadowedInternal.exportName}" export in ${shadowedInternal.filePath}`
778
+ );
779
+ }
780
+ const names = agentRuntimeFunctionNames().filter((name) => !taken.has(name));
781
+ if (names.length === 0) {
782
+ return empty;
783
+ }
784
+ return {
785
+ importLine: 'import { agentComponent } from "@lunora/agent/component";\n',
786
+ lines: names.map((name) => ` "agents:${name}": lunoraAgentRuntimeFunctions.${name} as unknown as RegisteredLunoraFunction,`).join("\n"),
787
+ 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"
788
+ };
789
+ };
790
+ const renderSandboxFunctionRegistry = (usesSandbox, functions) => {
791
+ if (!usesSandbox) {
792
+ return { importLine: "", lines: "", prelude: "" };
793
+ }
794
+ const collision = functions.find((definition) => sanitizeNamespace(definition.filePath) === "sandbox" && definition.exportName === "invoke");
795
+ if (collision) {
796
+ throw new LunoraError(
797
+ "INTERNAL",
798
+ `@lunora/codegen: "sandbox:invoke" is reserved for the batteries-included sandbox tool dispatcher (browserTool/containerTool) — rename the "invoke" export in ${collision.filePath}`
799
+ );
800
+ }
801
+ return {
802
+ importLine: 'import { sandboxComponent } from "@lunora/agent/component";\n',
803
+ lines: ' "sandbox:invoke": lunoraSandbox.invoke as unknown as RegisteredLunoraFunction,',
804
+ 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"
805
+ };
806
+ };
807
+ const syntheticAgentApiFunctions = (agents, functions) => {
808
+ if (agents.length === 0) {
809
+ return [];
810
+ }
811
+ const taken = takenAgentFunctionNames(functions);
812
+ const definitions = [
813
+ {
814
+ args: { key: { kind: "string" }, limit: { inner: { kind: "number" }, kind: "optional" } },
815
+ exportName: "agentMessages",
816
+ filePath: "agents",
817
+ kind: "query",
818
+ returnType: "Record<string, unknown>[]"
819
+ },
820
+ {
821
+ args: {
822
+ decision: {
823
+ kind: "union",
824
+ members: [
825
+ { kind: "literal", literalValue: '"approve"' },
826
+ { kind: "literal", literalValue: '"reject"' }
827
+ ]
828
+ },
829
+ instanceId: { kind: "string" },
830
+ note: { inner: { kind: "string" }, kind: "optional" },
831
+ threadKey: { kind: "string" },
832
+ toolCallId: { kind: "string" }
833
+ },
834
+ exportName: "agentResolveApproval",
835
+ filePath: "agents",
836
+ kind: "mutation",
837
+ returnType: "{ resolved: boolean }"
838
+ },
839
+ {
840
+ args: {
841
+ agent: { kind: "string" },
842
+ input: { kind: "string" },
843
+ threadKey: { kind: "string" },
844
+ title: { inner: { kind: "string" }, kind: "optional" }
845
+ },
846
+ exportName: "agentRun",
847
+ filePath: "agents",
848
+ kind: "mutation",
849
+ returnType: "{ id: string; threadKey: string }"
850
+ },
851
+ {
852
+ args: { key: { kind: "string" } },
853
+ exportName: "agentState",
854
+ filePath: "agents",
855
+ kind: "query",
856
+ returnType: "Record<string, unknown> | undefined"
857
+ },
858
+ {
859
+ args: { key: { kind: "string" } },
860
+ exportName: "agentThread",
861
+ filePath: "agents",
862
+ kind: "query",
863
+ returnType: "Record<string, unknown> | undefined"
864
+ }
865
+ ];
866
+ for (const agent of agents) {
867
+ if (agent.voice) {
868
+ definitions.push({
869
+ args: { threadKey: { kind: "string" } },
870
+ exportName: `${agent.exportName}Voice`,
871
+ filePath: "agents",
872
+ kind: "stream",
873
+ returnType: "Record<string, unknown>"
874
+ });
875
+ }
876
+ }
877
+ return definitions.filter((definition) => !taken.has(definition.exportName));
878
+ };
879
+ const renderHttpStreamsRef = (httpRoutes) => {
880
+ const streams = httpRoutes.filter((route) => route.stream);
881
+ if (streams.length === 0) {
882
+ return { block: "", body: "" };
883
+ }
884
+ const namespaces = /* @__PURE__ */ new Map();
885
+ for (const route of streams) {
886
+ const list = namespaces.get(route.filePath) ?? [];
887
+ list.push(route);
888
+ namespaces.set(route.filePath, list);
889
+ }
890
+ const sortedNamespaces = [...namespaces.entries()].toSorted(([a], [b]) => a.localeCompare(b));
891
+ const typeBody = sortedNamespaces.map(([file, list]) => {
892
+ const members = list.map((route) => {
893
+ const chunkType = relocateGeneratedImports(route.chunkType ?? "unknown");
894
+ return ` ${renderPropertyKey(route.exportName)}: HttpStreamRef<${chunkType}, ${renderArgsType(route.searchParams)}, ${renderArgsType(route.params)}>;`;
895
+ }).join("\n");
896
+ return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
897
+ ${members}
898
+ };`;
899
+ }).join("\n");
900
+ const valueBody = sortedNamespaces.map(([file, list]) => {
901
+ const members = list.map(
902
+ (route) => ` ${renderPropertyKey(route.exportName)}: { method: ${JSON.stringify(route.method)}, path: ${JSON.stringify(route.path)} },`
903
+ ).join("\n");
904
+ return ` ${renderPropertyKey(sanitizeNamespace(file))}: {
905
+ ${members}
906
+ },`;
907
+ }).join("\n");
908
+ const block = `
909
+ /** This project's HTTP-SSE stream routes (\`httpRoute.<verb>(path).stream()\`), addressable as typed references for \`client.httpStream\` / \`useHttpStream\`. */
910
+ export interface HttpStreamsRef {
911
+ ${typeBody}
912
+ }
913
+
914
+ export const httpStreams: HttpStreamsRef = {
915
+ ${valueBody}
916
+ };
917
+ `;
918
+ return { block, body: typeBody };
919
+ };
920
+ const emitApi = (options) => {
921
+ const { agents = [], functions, httpRoutes = [], useUmbrella = false, workflows = [] } = options;
379
922
  const base = baseSpecifiers(useUmbrella);
380
- const publicFunctions = functions.filter((definition) => definition.visibility !== "internal");
923
+ const publicFunctions = [...functions.filter((definition) => definition.visibility !== "internal"), ...syntheticAgentApiFunctions(agents, functions)];
381
924
  const internalFunctions = functions.filter((definition) => definition.visibility === "internal");
382
925
  const publicBody = renderApiBody(publicFunctions);
383
926
  const internalBody = renderApiBody(internalFunctions);
927
+ const httpStreamsRef = renderHttpStreamsRef(httpRoutes);
384
928
  const combinedBody = `${publicBody}
385
- ${internalBody}`;
929
+ ${internalBody}
930
+ ${httpStreamsRef.body}`;
386
931
  const dataModelImports = referencedDataModelImports(combinedBody);
387
932
  const dataModelImportLine = dataModelImports.length > 0 ? `
388
933
  import type { ${dataModelImports.join(", ")} } from "./dataModel.js";
@@ -393,10 +938,11 @@ ${publicBody}
393
938
  const internalBlock = internalBody ? `
394
939
  ${internalBody}
395
940
  ` : "";
396
- const workflowsRef = renderWorkflowsRef(workflows);
941
+ const schedulerReferences = renderSchedulerReferences(workflows, agents);
942
+ const clientImports = httpStreamsRef.block === "" ? "FunctionReference" : "FunctionReference, HttpStreamRef";
397
943
  return `${GENERATED_HEADER}import { anyApi } from "${base.serverTypes}";
398
- import type { FunctionReference } from "${base.client}";
399
- ${workflowsRef.importLine}${dataModelImportLine}
944
+ import type { ${clientImports} } from "${base.client}";
945
+ ${schedulerReferences.importLine}${dataModelImportLine}
400
946
  export interface ApiTypes {${apiBlock}}
401
947
 
402
948
  export const api = anyApi as unknown as ApiTypes;
@@ -405,7 +951,7 @@ export const api = anyApi as unknown as ApiTypes;
405
951
  export interface InternalApiTypes {${internalBlock}}
406
952
 
407
953
  export const internal = anyApi as unknown as InternalApiTypes;
408
- ${workflowsRef.block}`;
954
+ ${schedulerReferences.block}${httpStreamsRef.block}`;
409
955
  };
410
956
  const emitSeed = (enabled) => {
411
957
  if (!enabled) {
@@ -430,21 +976,37 @@ import type { InsertModel } from "./dataModel.js";
430
976
  export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
431
977
  `;
432
978
  };
979
+ const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
980
+ if (shapes.length === 0 || !hasDatabase) {
981
+ return "";
982
+ }
983
+ const base = baseSpecifiers(useUmbrella);
984
+ const factories = shapes.map((shape) => {
985
+ assertIdentifier(shape.exportName, "shape export name");
986
+ return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
987
+ export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
988
+ createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
989
+ }).join("\n\n");
990
+ return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
991
+ import { lunoraCollectionOptions } from "@lunora/db/collections";
992
+ import type { Row } from "@lunora/db";
993
+ import type { Collection } from "@tanstack/db";
994
+ import { createCollection } from "@tanstack/db";
995
+
996
+ ${factories}
997
+ `;
998
+ };
433
999
  const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
434
1000
  const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
435
1001
  const registered = LUNORA_FUNCTIONS[functionPath];
436
1002
 
437
1003
  if (!registered) {
438
- throw Object.assign(new Error(\`function not registered: \${functionPath}\`), {
439
- name: "LunoraError",
440
- code: "FUNCTION_NOT_FOUND",
441
- status: 404,
442
- });
1004
+ throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
443
1005
  }
444
1006
 
445
1007
  return (await registered.handler(context, args ?? {})) as R;
446
1008
  };`;
447
- const renderFunctionRegistry = (functions, migrations) => {
1009
+ const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
448
1010
  const aliasByPath = /* @__PURE__ */ new Map();
449
1011
  const registerPath = (filePath) => {
450
1012
  if (!aliasByPath.has(filePath)) {
@@ -457,9 +1019,15 @@ const renderFunctionRegistry = (functions, migrations) => {
457
1019
  for (const migration of migrations) {
458
1020
  registerPath(migration.filePath);
459
1021
  }
1022
+ for (const mutator of mutators) {
1023
+ registerPath(mutator.filePath);
1024
+ }
1025
+ for (const shape of shapes) {
1026
+ registerPath(shape.filePath);
1027
+ }
460
1028
  const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
461
1029
  if (!IMPORT_PATH_RE.test(filePath)) {
462
- throw new Error(`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
1030
+ throw new LunoraError("INTERNAL", `@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
463
1031
  }
464
1032
  return `import * as ${alias} from "../${filePath}.js";`;
465
1033
  }).join("\n");
@@ -469,15 +1037,37 @@ const renderFunctionRegistry = (functions, migrations) => {
469
1037
  const migrationEntries = migrations.map(
470
1038
  (migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
471
1039
  ).join("\n");
1040
+ const mutatorEntries = mutators.map(
1041
+ (mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
1042
+ ).join("\n");
1043
+ const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
1044
+ const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
1045
+ const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
1046
+ const installLines = functions.map((definition) => {
1047
+ if (definition.lifecycle || Object.keys(definition.args).length === 0) {
1048
+ return void 0;
1049
+ }
1050
+ const compiled = compileArgsValidator(definition.args);
1051
+ if (compiled === void 0) {
1052
+ return void 0;
1053
+ }
1054
+ const alias = aliasByPath.get(definition.filePath) ?? "";
1055
+ return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
1056
+ }).filter((line) => line !== void 0).join("\n");
472
1057
  return {
473
- dispatchBody: dispatchEntries.length > 0 ? `
474
- ${dispatchEntries}
1058
+ dispatchBody: combinedDispatch.length > 0 ? `
1059
+ ${combinedDispatch}
475
1060
  ` : "",
476
1061
  importBlock: importLines.length > 0 ? `${importLines}
477
1062
 
478
1063
  ` : "",
1064
+ installBlock: installLines,
479
1065
  migrationBody: migrationEntries.length > 0 ? `
480
1066
  ${migrationEntries}
1067
+ ` : "",
1068
+ mutatorPaths,
1069
+ shapeBody: shapeEntries.length > 0 ? `
1070
+ ${shapeEntries}
481
1071
  ` : ""
482
1072
  };
483
1073
  };
@@ -530,16 +1120,23 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
530
1120
  return ["default", ...[...named].toSorted((a, b) => a.localeCompare(b))];
531
1121
  };
532
1122
  const emitServer = ({
1123
+ agents = [],
533
1124
  containers = [],
1125
+ env,
1126
+ hasAccessFacade = false,
534
1127
  hasAi = false,
535
1128
  hasAnalytics = false,
536
1129
  hasBrowser = false,
1130
+ hasFlags = false,
537
1131
  hasHyperdrive = false,
538
1132
  hasImages = false,
539
1133
  hasKv = false,
540
1134
  hasPayments = false,
541
1135
  hasPipelines = false,
542
1136
  hasR2sql = false,
1137
+ hasX402 = false,
1138
+ identity,
1139
+ queues = [],
543
1140
  schema,
544
1141
  storageRuleBuckets = [],
545
1142
  useUmbrella = false,
@@ -555,6 +1152,10 @@ const emitServer = ({
555
1152
  ` : "";
556
1153
  const paymentsActionField = hasPayments ? `
557
1154
  readonly payments: LunoraPayment;` : "";
1155
+ const x402TypeImport = hasX402 ? `import type { X402Pay } from "@lunora/x402/pay";
1156
+ ` : "";
1157
+ const x402ActionField = hasX402 ? `
1158
+ readonly x402: X402Pay;` : "";
558
1159
  const containersTypeImport = containers.length > 0 ? `import type { ContainerAccessor } from "@lunora/container";
559
1160
  ` : "";
560
1161
  const containersActionField = containers.length > 0 ? `
@@ -573,6 +1174,16 @@ const emitServer = ({
573
1174
  assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
574
1175
  return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
575
1176
  readonly ${workflow.bindingName}?: unknown;`;
1177
+ }),
1178
+ ...queues.map((queue) => {
1179
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
1180
+ return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
1181
+ readonly ${queue.bindingName}?: unknown;`;
1182
+ }),
1183
+ ...agents.map((agent) => {
1184
+ assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
1185
+ return ` /** Workflow binding for the \`${agent.exportName}\` agent. */
1186
+ readonly ${agent.bindingName}?: unknown;`;
576
1187
  })
577
1188
  ].join("\n");
578
1189
  const envBlock = `
@@ -594,30 +1205,20 @@ ${envBindingFields}` : ""}
594
1205
 
595
1206
  /** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
596
1207
  export type Env = CloudflareBindings;`;
597
- const kvContextField = hasKv ? `
598
- readonly kv: import("@lunora/kv").Kv;` : "";
599
- const hyperdriveActionField = hasHyperdrive ? `
600
- /**
601
- * 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.
602
- */
603
- readonly sql: import("@lunora/hyperdrive").SqlClient;` : "";
604
- const browserActionField = hasBrowser ? `
605
- /** Browser Rendering (screenshots/PDF/scrape). Non-deterministic — available only in actions. */
606
- readonly browser: import("@lunora/browser").Browser;` : "";
607
- const imagesActionField = hasImages ? `
608
- /** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
609
- readonly images: import("@lunora/images").Images;` : "";
610
- const analyticsContextField = hasAnalytics ? `
611
- /** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
612
- readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
613
- const pipelinesActionField = hasPipelines ? `
614
- /** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
615
- readonly pipelines: import("@lunora/pipelines").PipelineClient;` : "";
616
- const r2sqlActionField = hasR2sql ? `
617
- /**
618
- * 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.
619
- */
620
- readonly r2sql: import("@lunora/r2sql").R2SqlClient;` : "";
1208
+ const serverCapabilityField = (key, enabled) => enabled ? SERVER_CTX_FIELDS.get(key)?.field ?? "" : "";
1209
+ const kvContextField = serverCapabilityField("kv", hasKv);
1210
+ const analyticsContextField = serverCapabilityField("analytics", hasAnalytics);
1211
+ const hyperdriveActionField = serverCapabilityField("hyperdrive", hasHyperdrive);
1212
+ const browserActionField = serverCapabilityField("browser", hasBrowser);
1213
+ const imagesActionField = serverCapabilityField("images", hasImages);
1214
+ const pipelinesActionField = serverCapabilityField("pipelines", hasPipelines);
1215
+ const r2sqlActionField = serverCapabilityField("r2sql", hasR2sql);
1216
+ const accessContextField = hasAccessFacade ? `
1217
+ /** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
1218
+ readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
1219
+ const flagsContextField = hasFlags ? `
1220
+ /** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
1221
+ readonly flags: import("${base.flags}").LunoraFlags;` : "";
621
1222
  const hasWorkflows = workflows.length > 0;
622
1223
  const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
623
1224
  import type * as lunoraWorkflowDefinitions from "../workflows.js";
@@ -638,6 +1239,59 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
638
1239
  const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
639
1240
  const workflowsContextField = hasWorkflows ? `
640
1241
  readonly workflows: LunoraWorkflows;` : "";
1242
+ const hasQueues = queues.length > 0;
1243
+ const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
1244
+ import type * as lunoraQueueDefinitions from "../queues.js";
1245
+ ` : "";
1246
+ const queuesTypeBlock = hasQueues ? `
1247
+
1248
+ /** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
1249
+ type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
1250
+
1251
+ /** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
1252
+ export interface LunoraQueues {
1253
+ ${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
1254
+ }` : "";
1255
+ const queuesContextField = hasQueues ? `
1256
+ readonly queues: LunoraQueues;` : "";
1257
+ const hasAgents = agents.length > 0;
1258
+ const agentsTypeImport = hasAgents ? `import type { AgentHandle } from "@lunora/agent";
1259
+ ` : "";
1260
+ const agentsTypeBlock = hasAgents ? `
1261
+
1262
+ /** This project's declared agents, addressable from \`ctx.agents\` by their \`lunora/agents.ts\` export name. */
1263
+ export interface LunoraAgents {
1264
+ ${agents.map((agent) => {
1265
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
1266
+ return ` readonly ${agent.exportName}: AgentHandle;`;
1267
+ }).join("\n")}
1268
+ }` : "";
1269
+ const agentsContextField = hasAgents ? `
1270
+ readonly agents: LunoraAgents;` : "";
1271
+ const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
1272
+ import type * as lunoraIdentityContract from "../identity.js";
1273
+ ` : "";
1274
+ const identityTypeBlock = identity ? `
1275
+
1276
+ /** 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. */
1277
+ export type Identity = InferIdentity<typeof lunoraIdentityContract.${identity.exportName}>;
1278
+
1279
+ /** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
1280
+ type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };` : "";
1281
+ const authOmit = identity ? ` | "auth"` : "";
1282
+ const authContextField = identity ? `
1283
+ readonly auth: NarrowedAuth;` : "";
1284
+ const policyIdentityArgument = identity ? ", Identity" : "";
1285
+ const envTypeImport = env ? `import type * as lunoraEnvContract from "../env.js";
1286
+ ` : "";
1287
+ const envTypeBlock = env ? `
1288
+
1289
+ /** This app's declared env contract (\`defineEnv\` in \`lunora/env.ts\`) — the validated, coercion-aware shape of \`ctx.env\`. */
1290
+ export type LunoraEnv = ReturnType<typeof lunoraEnvContract.${env.exportName}>;` : "";
1291
+ const envOmit = env ? ` | "env"` : "";
1292
+ const envContextField = env ? `
1293
+ /** 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. */
1294
+ readonly env: LunoraEnv;` : "";
641
1295
  const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
642
1296
  import type {
643
1297
  ActionBuilder,
@@ -659,11 +1313,11 @@ import type {
659
1313
  } from "${base.server}";
660
1314
 
661
1315
  import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
662
- ${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
1316
+ ${aiTypeImport}${paymentsTypeImport}${x402TypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${agentsTypeImport}${identityTypeImport}${envTypeImport}
663
1317
  export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
664
1318
 
665
1319
  /** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
666
- export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
1320
+ export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${agentsTypeBlock}${identityTypeBlock}${envTypeBlock}
667
1321
 
668
1322
  /**
669
1323
  * Project-typed contexts. The base contexts from \`@lunora/server\` are
@@ -692,22 +1346,22 @@ type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>)
692
1346
  */
693
1347
  type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
694
1348
 
695
- export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
1349
+ export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}${envOmit}> {
696
1350
  readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
697
1351
  readonly orm: OrmReader;
698
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
1352
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${authContextField}
699
1353
  }
700
1354
 
701
- export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
1355
+ export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
702
1356
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
703
1357
  readonly orm: OrmWriter;
704
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
1358
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
705
1359
  }
706
1360
 
707
- export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
1361
+ export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}${envOmit}> {
708
1362
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
709
1363
  readonly orm: OrmWriter;
710
- readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}
1364
+ readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${x402ActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${envContextField}${workflowsContextField}${queuesContextField}${agentsContextField}${authContextField}
711
1365
  }
712
1366
 
713
1367
  /**
@@ -747,7 +1401,7 @@ export const internalAction = lunoraBuilders.internalAction as unknown as Intern
747
1401
  * Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
748
1402
  * so the \`rls()\` chain discovers a policy authored either way the same.
749
1403
  */
750
- export const definePolicy = createPolicyDsl<DataModel, Relations>();
1404
+ export const definePolicy = createPolicyDsl<DataModel, Relations${policyIdentityArgument}>();
751
1405
 
752
1406
  /**
753
1407
  * The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
@@ -777,10 +1431,52 @@ const renderLifecycleManifest = (functions) => {
777
1431
  }
778
1432
  return manifest;
779
1433
  };
780
- const emitFunctions = (functions, migrations = []) => {
1434
+ const emitFunctions = (options) => {
1435
+ const { agents = [], functions, migrations = [], mutators = [], shapes = [], useUmbrella = false, usesSandbox = false } = options;
781
1436
  const hasFunctions = functions.length > 0;
782
- const { dispatchBody, importBlock, migrationBody } = renderFunctionRegistry(functions, migrations);
1437
+ const base = baseSpecifiers(useUmbrella);
1438
+ const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
1439
+ const agentRegistry = renderAgentFunctionRegistry(agents, functions);
1440
+ const sandboxRegistry = renderSandboxFunctionRegistry(usesSandbox, functions);
1441
+ const autoLines = [agentRegistry.lines, sandboxRegistry.lines].filter((block) => block.length > 0).join("\n");
1442
+ let dispatchBodyWithAgents = dispatchBody;
1443
+ if (autoLines.length > 0) {
1444
+ dispatchBodyWithAgents = dispatchBody.length > 0 ? `${dispatchBody.trimEnd()}
1445
+ ${autoLines}
1446
+ ` : `
1447
+ ${autoLines}
1448
+ `;
1449
+ }
783
1450
  const lifecycleHooks = renderLifecycleManifest(functions);
1451
+ const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
1452
+ ` : "";
1453
+ const shapeRegistry = shapes.length > 0 ? `
1454
+ /**
1455
+ * Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
1456
+ * The generated ShardDO's \`resolveShape\` override looks a shape up by name and
1457
+ * evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
1458
+ * \`shape_subscribe\` (reads-as-permissions).
1459
+ */
1460
+ export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
1461
+ ` : "";
1462
+ const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
1463
+ /**
1464
+ * Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
1465
+ * ShardDO's \`isCustomMutator\` override routes through the client-watermark push
1466
+ * protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
1467
+ */
1468
+ export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
1469
+ ` : "";
1470
+ const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
1471
+ ` : "";
1472
+ const compiledArgsInstall = installBlock.length > 0 ? `
1473
+ /**
1474
+ * AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
1475
+ * onto its function's live \`.args\` object and consulted by the interpreted
1476
+ * parser as a zero-allocation fast path; anything it can't model is deferred.
1477
+ */
1478
+ ${installBlock}
1479
+ ` : "";
784
1480
  const caller = renderCaller(functions);
785
1481
  const callerTypes = caller.types ? `
786
1482
  ${caller.types}
@@ -795,7 +1491,8 @@ ${caller.implementation}
795
1491
  const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
796
1492
 
797
1493
  ` : "";
798
- return `${GENERATED_HEADER}${importBlock}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
1494
+ return `${GENERATED_HEADER}${importBlock}${agentRegistry.importLine}${sandboxRegistry.importLine}${compiledArgsImport}${shapeTypeImport}import { LunoraError } from "${base.server}";
1495
+ import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
799
1496
  ${dataModelImport}
800
1497
  /**
801
1498
  * Single registered function, narrowed to the shape \`handleRpc\` needs.
@@ -815,13 +1512,13 @@ export interface RegisteredLunoraFunction {
815
1512
  /** \`"internal"\` functions are rejected on the external RPC path; absence === public. */
816
1513
  visibility?: "internal" | "public";
817
1514
  }
818
-
1515
+ ${agentRegistry.prelude}${sandboxRegistry.prelude}
819
1516
  /**
820
1517
  * Static dispatch table. The key matches the \`__lunoraRef\` the client
821
1518
  * emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
822
1519
  */
823
- export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
824
-
1520
+ export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBodyWithAgents}};
1521
+ ${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
825
1522
  /**
826
1523
  * Connection-lifecycle manifest: the function paths the generated ShardDO
827
1524
  * dispatches when a client's WebSocket connects (\`connect\`) or disconnects
@@ -843,11 +1540,7 @@ export const dispatchLunoraFunction = async (functionPath: string, context: unkn
843
1540
  const registered = LUNORA_FUNCTIONS[functionPath];
844
1541
 
845
1542
  if (!registered || registered.visibility === "internal") {
846
- throw Object.assign(new Error(\`function not registered: \${functionPath}\`), {
847
- name: "LunoraError",
848
- code: "FUNCTION_NOT_FOUND",
849
- status: 404,
850
- });
1543
+ throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
851
1544
  }
852
1545
 
853
1546
  return registered.handler(context, args);
@@ -1042,7 +1735,7 @@ const emitKvFragments = (hasKv) => {
1042
1735
  kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
1043
1736
  contextField: `
1044
1737
  kv,`,
1045
- importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
1738
+ importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
1046
1739
  stub: `
1047
1740
  const kvStub: Kv = {
1048
1741
  delete: async () => {
@@ -1067,6 +1760,131 @@ const kvStub: Kv = {
1067
1760
  `
1068
1761
  };
1069
1762
  };
1763
+ const emitFlagsFragments = (hasFlags, flagsSpecifier) => {
1764
+ if (!hasFlags) {
1765
+ return EMPTY_HELPER_FRAGMENTS;
1766
+ }
1767
+ return {
1768
+ build: `
1769
+ const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1770
+ hooks: flagsConfig.hooks,
1771
+ logger: flagsConfig.logger,
1772
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1773
+ targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
1774
+ });
1775
+ `,
1776
+ configField: `
1777
+ flags?: (env: Record<string, unknown>) => import("${flagsSpecifier}").Provider;`,
1778
+ contextField: `
1779
+ flags,`,
1780
+ importLines: [`import { createFlags } from "${flagsSpecifier}";`, `import flagsConfig from "../flags.js";`],
1781
+ stub: ""
1782
+ };
1783
+ };
1784
+ const emitEnvFragments = (env) => {
1785
+ if (!env) {
1786
+ return EMPTY_HELPER_FRAGMENTS;
1787
+ }
1788
+ return {
1789
+ build: `
1790
+ const envConfig = lunoraEnvContract.${env.exportName}(env);
1791
+ `,
1792
+ configField: "",
1793
+ contextField: `
1794
+ env: envConfig,`,
1795
+ importLines: [`import * as lunoraEnvContract from "../env.js";`],
1796
+ stub: ""
1797
+ };
1798
+ };
1799
+ const emitAccessFragments = (hasAccessFacade) => {
1800
+ if (!hasAccessFacade) {
1801
+ return EMPTY_HELPER_FRAGMENTS;
1802
+ }
1803
+ return {
1804
+ build: `
1805
+ const access = accessFacade(identity, userId);
1806
+ `,
1807
+ configField: "",
1808
+ contextField: `
1809
+ access,`,
1810
+ importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
1811
+ stub: ""
1812
+ };
1813
+ };
1814
+ const emitFlagsOverrides = (flagKeys, hasFlags, flagsSpecifier) => {
1815
+ if (!hasFlags) {
1816
+ return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
1817
+ }
1818
+ const clientBuild = (targetingKey) => `
1819
+ const env = (this.env ?? {}) as Record<string, unknown>;
1820
+ const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1821
+ hooks: flagsConfig.hooks,
1822
+ logger: flagsConfig.logger,
1823
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1824
+ targetingKey: ${targetingKey},
1825
+ });`;
1826
+ const constant = `
1827
+ /** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
1828
+ const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
1829
+ `;
1830
+ const evaluateOverride = `
1831
+ protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
1832
+ const evalContext = context as import("${flagsSpecifier}").EvaluationContext | undefined;
1833
+ const evaluations: FlagsResult["flags"] = [];
1834
+
1835
+ for (const entry of LUNORA_FLAG_KEYS) {
1836
+ // eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
1837
+ const details =
1838
+ entry.type === "boolean"
1839
+ ? await flags.details.boolean(entry.key, false, evalContext)
1840
+ : entry.type === "number"
1841
+ ? await flags.details.number(entry.key, 0, evalContext)
1842
+ : entry.type === "string"
1843
+ ? await flags.details.string(entry.key, "", evalContext)
1844
+ : await flags.details.object(entry.key, {}, evalContext);
1845
+
1846
+ evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
1847
+ }
1848
+
1849
+ return { configured: true, flags: evaluations };
1850
+ }
1851
+ `;
1852
+ const subscriptionOverride = `
1853
+ 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 })")}
1854
+ const key = typeof args.key === "string" ? args.key : "";
1855
+
1856
+ // SECURITY: the reactive channel is public (any socket, no auth). Serve
1857
+ // ONLY statically-discovered flag keys — an arbitrary client-supplied key
1858
+ // would let a subscriber probe the value of internal/unreleased flags the
1859
+ // app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
1860
+ // eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
1861
+ if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
1862
+ return Promise.resolve(null);
1863
+ }
1864
+
1865
+ // SECURITY: evaluate under the socket's server-verified identity ONLY
1866
+ // (the targetingKey resolved above). Client-supplied targeting context is
1867
+ // NOT honored on this public channel — otherwise a subscriber could spoof
1868
+ // targeting attributes (e.g. plan/role) to unlock a flag gated on them.
1869
+ const context = undefined;
1870
+
1871
+ if (args.type === "number") {
1872
+ return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
1873
+ }
1874
+
1875
+ if (args.type === "string") {
1876
+ return flags.string(key, typeof args.default === "string" ? args.default : "", context);
1877
+ }
1878
+
1879
+ if (args.type === "object") {
1880
+ return flags.object(key, (args.default ?? {}) as import("${flagsSpecifier}").JsonValue, context);
1881
+ }
1882
+
1883
+ return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
1884
+ }
1885
+ `;
1886
+ return { constant, evaluateOverride, subscriptionOverride };
1887
+ };
1070
1888
  const emitAnalyticsFragments = (hasAnalytics) => {
1071
1889
  if (!hasAnalytics) {
1072
1890
  return EMPTY_HELPER_FRAGMENTS;
@@ -1082,8 +1900,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
1082
1900
  contextField: `
1083
1901
  analytics,`,
1084
1902
  importLines: [
1085
- `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
1086
- `import { createAnalytics } from "@lunora/analytics";`
1903
+ `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
1904
+ `import { createAnalytics } from "@lunora/bindings/analytics";`
1087
1905
  ],
1088
1906
  stub: `
1089
1907
  const analyticsStub: AnalyticsClient = {
@@ -1112,7 +1930,7 @@ const emitImagesFragments = (hasImages) => {
1112
1930
  // ActionCtx-only: woven onto the action ctx object, never query/mutation.
1113
1931
  contextField: `
1114
1932
  images,`,
1115
- importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
1933
+ importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
1116
1934
  stub: `
1117
1935
  const imagesStub: Images = {
1118
1936
  info: async () => {
@@ -1211,10 +2029,10 @@ const emitR2sqlFragments = (hasR2sql) => {
1211
2029
  // ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
1212
2030
  // \`isAction\` block, never the every-ctx object literal.
1213
2031
  contextField: "",
1214
- importLines: [`import type { R2SqlClient } from "@lunora/r2sql";`, `import { createR2Sql } from "@lunora/r2sql";`],
2032
+ importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
1215
2033
  // The stub is typed `R2SqlClient`, so TS flags a missing method at build
1216
2034
  // time — but it must stay structurally in sync with that interface
1217
- // (`@lunora/r2sql` client.ts) when a method is added there.
2035
+ // (`@lunora/bindings/r2sql` client.ts) when a method is added there.
1218
2036
  stub: `
1219
2037
  const r2sqlStub: R2SqlClient = {
1220
2038
  describe: async () => {
@@ -1239,17 +2057,46 @@ const r2sqlStub: R2SqlClient = {
1239
2057
  `
1240
2058
  };
1241
2059
  };
1242
- const emitContainers = (containers) => {
2060
+ const emitPipelinesFragments = (hasPipelines) => {
2061
+ if (!hasPipelines) {
2062
+ return EMPTY_HELPER_FRAGMENTS;
2063
+ }
2064
+ 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().");`;
2065
+ return {
2066
+ build: `
2067
+ const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
2068
+ const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
2069
+ `,
2070
+ configField: `
2071
+ pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
2072
+ // ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
2073
+ // in the \`isAction\` block, never the every-ctx object literal.
2074
+ contextField: "",
2075
+ importLines: [
2076
+ `import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
2077
+ `import { createPipelines } from "@lunora/bindings/pipelines";`
2078
+ ],
2079
+ stub: `
2080
+ const pipelinesStub: PipelineClient = {
2081
+ send: async () => {
2082
+ ${pipelinesMissing}
2083
+ },
2084
+ };
2085
+ `
2086
+ };
2087
+ };
2088
+ const emitContainers = (containers, jurisdiction) => {
1243
2089
  if (containers.length === 0) {
1244
2090
  return "";
1245
2091
  }
2092
+ const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
1246
2093
  const classes = containers.map((container) => {
1247
2094
  assertIdentifier(container.exportName, `container export "${container.exportName}"`);
1248
2095
  assertIdentifier(container.className, `container class "${container.className}"`);
1249
2096
  return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
1250
2097
  export class ${container.className} extends LunoraContainer {
1251
2098
  public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
1252
- super(ctx, env, ${container.exportName}, "${container.exportName}");
2099
+ super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
1253
2100
  }
1254
2101
  }
1255
2102
  `;
@@ -1261,14 +2108,21 @@ export class ${container.className} extends LunoraContainer {
1261
2108
  * requires each \`containers[].class_name\` to be exported by the worker:
1262
2109
  *
1263
2110
  * \`export * from "./lunora/_generated/containers.js";\`
2111
+ *
2112
+ * \`ContainerProxy\` is re-exported alongside them: the egress-interception path
2113
+ * (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
2114
+ * \`handle.egress\` controls) routes container outbound traffic through this
2115
+ * WorkerEntrypoint, so it too must be exported by the deployed worker.
1264
2116
  */
1265
- import LunoraContainer from "@lunora/container/do";
2117
+ import { LunoraContainer } from "@lunora/container/do";
1266
2118
 
1267
2119
  import { ${imports} } from "../containers.js";
1268
2120
 
2121
+ export { ContainerProxy } from "@lunora/container/do";
2122
+
1269
2123
  ${classes}`;
1270
2124
  };
1271
- const emitContainerFragments = (containers) => {
2125
+ const emitContainerFragments = (containers, jurisdiction) => {
1272
2126
  if (containers.length === 0) {
1273
2127
  return { build: "", contextField: "", importLines: [], specs: "" };
1274
2128
  }
@@ -1281,8 +2135,12 @@ const emitContainerFragments = (containers) => {
1281
2135
  return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
1282
2136
  }).join("\n");
1283
2137
  return {
2138
+ // Schema `.jurisdiction("…")` pins every container DO this shard reaches
2139
+ // to the data-residency region (`undefined` when undeclared). The trailing
2140
+ // `this.getCurrentTraceparent()` forwards the inbound RPC's W3C trace
2141
+ // context onto outbound container fetches so their spans join the trace.
1284
2142
  build: `
1285
- const containers = createContainerContext(env, LUNORA_CONTAINERS);
2143
+ const containers = createContainerContext(env, LUNORA_CONTAINERS, ${jurisdiction ? JSON.stringify(jurisdiction) : "undefined"}, this.getCurrentTraceparent());
1286
2144
  `,
1287
2145
  contextField: `
1288
2146
  containers,`,
@@ -1335,6 +2193,88 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
1335
2193
 
1336
2194
  ${classes}`;
1337
2195
  };
2196
+ const emitAgents = (agents) => {
2197
+ if (agents.length === 0) {
2198
+ return "";
2199
+ }
2200
+ const voiceAgents = agents.filter((agent) => agent.voice);
2201
+ const hasVoice = voiceAgents.length > 0;
2202
+ const classes = agents.map((agent) => {
2203
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2204
+ assertIdentifier(agent.className, `agent class "${agent.className}"`);
2205
+ const workflowClass = `/** WorkflowEntrypoint for the \`${agent.exportName}\` agent (binding \`${agent.bindingName}\`). */
2206
+ export class ${agent.className} extends LunoraWorkflow<AgentRunInput, AgentRunResult> {
2207
+ public constructor(ctx: ConstructorParameters<typeof LunoraWorkflow>[0], env: Record<string, unknown>) {
2208
+ super(ctx, env, compileAgentWorkflow(${agent.exportName}, "${agent.exportName}"), "${agent.exportName}");
2209
+ }
2210
+ }
2211
+ `;
2212
+ if (!agent.voice) {
2213
+ return workflowClass;
2214
+ }
2215
+ const voiceClassName = agent.voiceClassName ?? "";
2216
+ const voiceBindingName = agent.voiceBindingName ?? "";
2217
+ assertIdentifier(voiceClassName, `agent voice class "${voiceClassName}"`);
2218
+ const voiceClass = `
2219
+ /** Voice-session Durable Object for the \`${agent.exportName}\` agent (binding \`${voiceBindingName}\`). */
2220
+ export class ${voiceClassName} extends VoiceSessionDO {
2221
+ public constructor(ctx: ConstructorParameters<typeof VoiceSessionDO>[0], env: Record<string, unknown>) {
2222
+ super(ctx, env, ${agent.exportName}, "${agent.exportName}");
2223
+ }
2224
+ }
2225
+ `;
2226
+ return `${workflowClass}${voiceClass}`;
2227
+ }).join("\n");
2228
+ const imports = agents.map((agent) => agent.exportName).join(", ");
2229
+ const runtimeImport = hasVoice ? `import { compileAgentWorkflow, VoiceSessionDO } from "@lunora/agent";` : `import { compileAgentWorkflow } from "@lunora/agent";`;
2230
+ const voiceHeaderNote = hasVoice ? `
2231
+ *
2232
+ * Voice-enabled agents ALSO get a \`VoiceSessionDO\` subclass here — a real
2233
+ * Durable Object (unlike the Workflow), so wrangler needs both its
2234
+ * \`durable_objects\` binding and its \`class_name\` export (the same re-export
2235
+ * line covers it).` : "";
2236
+ return `${GENERATED_HEADER}/**
2237
+ * WorkflowEntrypoint classes for the agents declared in \`lunora/agents.ts\`.
2238
+ * Each \`defineAgent\` compiles a replay-safe tool-loop onto a Cloudflare
2239
+ * Workflow. Re-export them from your worker entry — wrangler requires each
2240
+ * \`workflows[].class_name\` to be exported by the worker:
2241
+ *
2242
+ * \`export * from "./lunora/_generated/agents.js";\`${voiceHeaderNote}
2243
+ */
2244
+ import LunoraWorkflow from "@lunora/workflow/do";
2245
+ ${runtimeImport}
2246
+ import type { AgentRunInput, AgentRunResult } from "@lunora/agent";
2247
+
2248
+ import { ${imports} } from "../agents.js";
2249
+
2250
+ ${classes}`;
2251
+ };
2252
+ const emitQueues = (queues) => {
2253
+ const pushQueues = queues.filter((queue) => queue.mode === "push");
2254
+ if (pushQueues.length === 0) {
2255
+ return "";
2256
+ }
2257
+ for (const queue of pushQueues) {
2258
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
2259
+ }
2260
+ const imports = pushQueues.map((queue) => queue.exportName).join(", ");
2261
+ const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
2262
+ return `${GENERATED_HEADER}/**
2263
+ * Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
2264
+ * composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
2265
+ * by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
2266
+ * by \`defineApp\` — you don't import this directly.
2267
+ */
2268
+ import type { QueueRegistry } from "@lunora/queue";
2269
+
2270
+ import { ${imports} } from "../queues.js";
2271
+
2272
+ /** Stable wrangler queue name → { definition, exportName } for batch routing. */
2273
+ export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
2274
+ ${entries}
2275
+ };
2276
+ `;
2277
+ };
1338
2278
  const emitWorkflowFragments = (workflows) => {
1339
2279
  if (workflows.length === 0) {
1340
2280
  return { build: "", contextField: "", importLines: [], specs: "" };
@@ -1360,6 +2300,56 @@ ${specEntries}
1360
2300
  `
1361
2301
  };
1362
2302
  };
2303
+ const emitQueueFragments = (queues) => {
2304
+ if (queues.length === 0) {
2305
+ return { build: "", contextField: "", importLines: [], specs: "" };
2306
+ }
2307
+ for (const queue of queues) {
2308
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
2309
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
2310
+ }
2311
+ const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
2312
+ return {
2313
+ build: `
2314
+ const queues = createQueueContext(env, LUNORA_QUEUES);
2315
+ `,
2316
+ contextField: `
2317
+ queues,`,
2318
+ importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
2319
+ // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2320
+ specs: `
2321
+ /** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
2322
+ const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
2323
+ ${specEntries}
2324
+ ];
2325
+ `
2326
+ };
2327
+ };
2328
+ const emitAgentFragments = (agents) => {
2329
+ if (agents.length === 0) {
2330
+ return { build: "", contextField: "", importLines: [], specs: "" };
2331
+ }
2332
+ for (const agent of agents) {
2333
+ assertIdentifier(agent.exportName, `agent export "${agent.exportName}"`);
2334
+ assertIdentifier(agent.bindingName, `agent binding "${agent.bindingName}"`);
2335
+ }
2336
+ const specEntries = agents.map((agent) => ` { binding: "${agent.bindingName}", exportName: "${agent.exportName}"${agent.publicRun === true ? ", publicRun: true" : ""} },`).join("\n");
2337
+ return {
2338
+ build: `
2339
+ const agents = createAgentContext(env, LUNORA_AGENTS);
2340
+ `,
2341
+ contextField: `
2342
+ agents,`,
2343
+ importLines: [`import { createAgentContext } from "@lunora/agent";`, `import type { AgentBindingSpec } from "@lunora/agent";`],
2344
+ // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
2345
+ specs: `
2346
+ /** Wiring specs for \`ctx.agents\` (codegen-derived from \`lunora/agents.ts\`). */
2347
+ const LUNORA_AGENTS: ReadonlyArray<AgentBindingSpec> = [
2348
+ ${specEntries}
2349
+ ];
2350
+ `
2351
+ };
2352
+ };
1363
2353
  const emitWorkflowsMetadataFragments = (workflows) => {
1364
2354
  if (workflows.length === 0) {
1365
2355
  return { constant: "", override: "" };
@@ -1386,6 +2376,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
1386
2376
  `
1387
2377
  };
1388
2378
  };
2379
+ const emitQueuesMetadataFragments = (queues) => {
2380
+ if (queues.length === 0) {
2381
+ return { constant: "", override: "" };
2382
+ }
2383
+ const metadata = {
2384
+ queues: queues.map((queue) => {
2385
+ return {
2386
+ binding: queue.bindingName,
2387
+ ...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
2388
+ exportName: queue.exportName,
2389
+ mode: queue.mode,
2390
+ name: queue.name
2391
+ };
2392
+ })
2393
+ };
2394
+ return {
2395
+ constant: `
2396
+ /** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
2397
+ const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
2398
+ `,
2399
+ override: `
2400
+ protected override queuesMetadata(): QueuesResult {
2401
+ return LUNORA_QUEUES_INFO;
2402
+ }
2403
+ `
2404
+ };
2405
+ };
1389
2406
  const emitPaymentFragments = (hasPayments) => {
1390
2407
  if (!hasPayments) {
1391
2408
  return { build: "", configField: "", contextField: "", imports: [], stub: "" };
@@ -1441,13 +2458,41 @@ const paymentStub: LunoraPayment = {
1441
2458
  `
1442
2459
  };
1443
2460
  };
1444
- const buildDoTypeImports = (hasVectors, hasWorkflows) => [
2461
+ const emitX402Fragments = (hasX402) => {
2462
+ if (!hasX402) {
2463
+ return { build: "", configField: "", contextField: "", imports: [], stub: "" };
2464
+ }
2465
+ return {
2466
+ imports: [`import type { X402Pay, X402PayConfig } from "@lunora/x402/pay";`, `import { lazyX402Pay } from "@lunora/x402/pay";`],
2467
+ // Built lazily off `secrets` (the Secrets Store facade already in scope) and
2468
+ // the `config.x402` thunk over env; falls back to `x402Stub`.
2469
+ build: `
2470
+ const x402: X402Pay = config.x402
2471
+ ? lazyX402Pay(config.x402(env), { getSecret: (name: string) => secrets.get(name) })
2472
+ : x402Stub;
2473
+ `,
2474
+ configField: `
2475
+ x402?: (env: Record<string, unknown>) => X402PayConfig;`,
2476
+ contextField: `
2477
+ x402,`,
2478
+ stub: `
2479
+ const x402Stub: X402Pay = {
2480
+ fetch: () => {
2481
+ throw new Error("ctx.x402: no pay rail configured. Pass \\\`x402\\\` to createShardDO().");
2482
+ },
2483
+ } as unknown as X402Pay;
2484
+ `
2485
+ };
2486
+ };
2487
+ const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
1445
2488
  "AdvisoryFinding",
1446
2489
  "DatabaseWriterLike",
1447
2490
  "DataMigrationLike",
2491
+ ...hasFlags ? ["FlagsResult"] : [],
1448
2492
  "LogSink",
1449
2493
  "MaskPoliciesResult",
1450
2494
  "MigrationRunResult",
2495
+ ...hasQueues ? ["QueuesResult"] : [],
1451
2496
  "RunShardApplyCdcArgs",
1452
2497
  "RunShardMigrationArgs",
1453
2498
  "RlsPoliciesResult",
@@ -1468,43 +2513,62 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
1468
2513
  ];
1469
2514
  const emitShard = ({
1470
2515
  advisories = [],
2516
+ agents = [],
1471
2517
  containers = [],
2518
+ env,
2519
+ flagKeys = [],
2520
+ hasAccessFacade = false,
1472
2521
  hasAi = false,
1473
2522
  hasAnalytics = false,
1474
2523
  hasBrowser = false,
2524
+ hasFlags = false,
1475
2525
  hasHyperdrive = false,
1476
2526
  hasImages = false,
1477
2527
  hasKv = false,
1478
2528
  hasPayments = false,
2529
+ hasPipelines = false,
1479
2530
  hasR2sql = false,
2531
+ hasX402 = false,
1480
2532
  maskMetadata,
2533
+ mutators = [],
2534
+ queues = [],
1481
2535
  rlsMetadata,
1482
2536
  schema,
2537
+ shapes = [],
1483
2538
  storageRules,
1484
2539
  studioFeatures,
1485
2540
  useUmbrella = false,
1486
2541
  workflows = []
1487
2542
  }) => {
1488
2543
  const base = baseSpecifiers(useUmbrella);
2544
+ const hasMutators = mutators.length > 0;
2545
+ const hasShapes = shapes.length > 0;
1489
2546
  const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
2547
+ const accessFragments = emitAccessFragments(hasAccessFacade);
1490
2548
  const kvFragments = emitKvFragments(hasKv);
2549
+ const flagsFragments = emitFlagsFragments(hasFlags, base.flags);
2550
+ const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags, base.flags);
2551
+ const envFragments = emitEnvFragments(env);
1491
2552
  const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
1492
2553
  const imagesFragments = emitImagesFragments(hasImages);
1493
2554
  const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
1494
2555
  const browserFragments = emitBrowserFragments(hasBrowser);
1495
2556
  const r2sqlFragments = emitR2sqlFragments(hasR2sql);
2557
+ const pipelinesFragments = emitPipelinesFragments(hasPipelines);
2558
+ const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
1496
2559
  const {
1497
2560
  build: containersBuild,
1498
2561
  contextField: containersContextField,
1499
2562
  importLines: containerImportLines,
1500
2563
  specs: containerSpecs
1501
- } = emitContainerFragments(containers);
2564
+ } = emitContainerFragments(containers, schema.jurisdiction);
1502
2565
  const {
1503
2566
  build: workflowsBuild,
1504
2567
  contextField: workflowsContextField,
1505
2568
  importLines: workflowImportLines,
1506
2569
  specs: workflowSpecs
1507
2570
  } = emitWorkflowFragments(workflows);
2571
+ const { build: agentsBuild, contextField: agentsContextField, importLines: agentImportLines, specs: agentSpecs } = emitAgentFragments(agents);
1508
2572
  const {
1509
2573
  build: paymentsBuild,
1510
2574
  configField: paymentsConfigField,
@@ -1512,25 +2576,35 @@ const emitShard = ({
1512
2576
  imports: paymentsImports,
1513
2577
  stub: paymentStub
1514
2578
  } = emitPaymentFragments(hasPayments);
2579
+ const { build: x402Build, configField: x402ConfigField, imports: x402Imports, stub: x402Stub } = emitX402Fragments(hasX402);
1515
2580
  const advisoryData = advisories;
1516
2581
  const rlsData = rlsMetadata ?? { policies: [], roles: [] };
1517
2582
  const maskData = maskMetadata ?? { columns: [] };
1518
2583
  const storageRulesData = storageRules ?? { rules: [] };
1519
2584
  const studioFeaturesData = studioFeatures ?? {
2585
+ analytics: false,
2586
+ auth: false,
2587
+ containers: false,
2588
+ flags: false,
2589
+ kv: false,
1520
2590
  mail: false,
1521
2591
  payments: false,
2592
+ queues: false,
1522
2593
  scheduler: false,
1523
2594
  storage: false,
1524
2595
  vectors: false,
1525
2596
  workflows: false
1526
2597
  };
1527
2598
  const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
2599
+ const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
1528
2600
  const hasVectors = schema.vectorIndexes.length > 0;
1529
2601
  const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
1530
2602
  const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
1531
2603
  const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
2604
+ const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
1532
2605
  if (hasD1Global && hasHyperdriveGlobal) {
1533
- throw new Error(
2606
+ throw new LunoraError(
2607
+ "INTERNAL",
1534
2608
  'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
1535
2609
  );
1536
2610
  }
@@ -1539,41 +2613,116 @@ const emitShard = ({
1539
2613
  const tableIndexes = buildTableIndexes(schema);
1540
2614
  const tableColumns = buildTableColumns(schema);
1541
2615
  const storageColumns = buildStorageColumns(schema);
1542
- const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
2616
+ const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
2617
+ if (hasShapes) {
2618
+ doTypeImports.push("WhereInput");
2619
+ }
1543
2620
  const relationFanout = emitRelationFanout(hasGlobalTables);
2621
+ const shapeResolveOverride = hasShapes ? `
2622
+ 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 {
2623
+ const shape = LUNORA_SHAPES[name];
2624
+
2625
+ if (!shape) {
2626
+ return undefined;
2627
+ }
2628
+
2629
+ this.ensureMigrated();
2630
+
2631
+ // Trusted ctx from the socket's OWN verified identity — the client
2632
+ // supplies only the shape name + args; \`compileWhere\` validates the
2633
+ // args then runs the shape's \`where(ctx, args)\` under that identity,
2634
+ // so which rows replicate is a server decision (reads-as-permissions).
2635
+ const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
2636
+ const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
2637
+
2638
+ // AND-compose with the table's RLS read base-where. A shape runs no
2639
+ // procedure, so the \`.use(rls(...))\` middleware never fires; without
2640
+ // this merge its reads would bypass every read policy on the table
2641
+ // (rows the caller can't see would replicate). \`composeShapeReadWhere\`
2642
+ // evaluates the table's read policies under this same trusted ctx —
2643
+ // exactly the request-time path — and fails closed under a
2644
+ // \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
2645
+ const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
2646
+ ctx,
2647
+ identity: identity?.identity ?? null,
2648
+ rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
2649
+ roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
2650
+ shapeWhere,
2651
+ table: shape.table,
2652
+ tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
2653
+ userId: identity?.userId ?? null,
2654
+ });
2655
+
2656
+ // A live shape pokes only from its OWN shard's op-log, so a \`where()\`
2657
+ // that joins to a \`.shardBy()\` table (rows in another DO) is rejected
2658
+ // here — the first point the compiled predicate + shard modes are both
2659
+ // known. Remedy: denormalize, or move the joined table to \`.global()\`.
2660
+ assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
2661
+
2662
+ // A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
2663
+ // base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
2664
+ // instead of the CDC poke path.
2665
+ const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
2666
+
2667
+ return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
2668
+ }
2669
+ ` : "";
2670
+ const customMutatorOverride = hasMutators ? `
2671
+ protected override isCustomMutator(functionPath: string): boolean {
2672
+ return LUNORA_MUTATOR_PATHS.has(functionPath);
2673
+ }
2674
+ ` : "";
2675
+ const shapeReadRegistryConst = hasShapes ? `
2676
+ /** 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. */
2677
+ const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
2678
+ ` : "";
2679
+ const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
1544
2680
  const importLines = [
1545
2681
  `import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
1546
- `import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
1547
- // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) lives in
2682
+ `import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceIncrementalTick, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
2683
+ ...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
2684
+ // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
2685
+ // `createSecrets` (the `ctx.secrets` core built-in) live in
1548
2686
  // `@lunora/server`, the single source — imported here rather than stamped
1549
- // inline into every generated shard.
1550
- `import { asBucketStorage } from "${base.server}";`
2687
+ // inline into every generated shard. With shapes, also pull the RLS
2688
+ // read-registry builder + `composeShapeReadWhere` so `resolveShape` can
2689
+ // AND-merge a shape's predicate with the table's read base-where.
2690
+ hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets, LunoraError } from "${base.server}";` : `import { asBucketStorage, createSecrets, LunoraError } from "${base.server}";`
1551
2691
  ];
1552
2692
  if (hasTables) {
1553
2693
  importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
1554
2694
  }
1555
2695
  if (hasVectors) {
1556
2696
  importLines.push(
1557
- `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
1558
- `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
2697
+ `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
2698
+ `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
1559
2699
  );
1560
2700
  }
1561
2701
  if (hasAi) {
1562
2702
  importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
1563
2703
  }
1564
2704
  importLines.push(
2705
+ ...accessFragments.importLines,
1565
2706
  ...kvFragments.importLines,
2707
+ ...flagsFragments.importLines,
2708
+ ...envFragments.importLines,
1566
2709
  ...analyticsFragments.importLines,
1567
2710
  ...imagesFragments.importLines,
1568
2711
  ...hyperdriveFragments.importLines,
1569
2712
  ...browserFragments.importLines,
1570
2713
  ...r2sqlFragments.importLines,
2714
+ ...pipelinesFragments.importLines,
1571
2715
  ...containerImportLines,
1572
2716
  ...workflowImportLines,
2717
+ ...queueImportLines,
2718
+ ...agentImportLines,
1573
2719
  ...paymentsImports,
2720
+ ...x402Imports,
1574
2721
  ``,
1575
2722
  `import schema from "../schema.js";`,
1576
- `import { LUNORA_FUNCTIONS, LUNORA_LIFECYCLE_HOOKS, LUNORA_MIGRATIONS } from "./functions.js";`
2723
+ // Local-first sync registries are pulled in alongside the function table
2724
+ // only when the project declares them, so the import list stays minimal.
2725
+ `import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
1577
2726
  );
1578
2727
  const vectorsConfigField = hasVectors ? `
1579
2728
  vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
@@ -1581,6 +2730,8 @@ const emitShard = ({
1581
2730
  d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
1582
2731
  const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
1583
2732
  hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
2733
+ const sourceClientConfigField = hasSourcedTables ? `
2734
+ sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
1584
2735
  const globalDatabaseStub = hasGlobalTables ? `
1585
2736
  const globalDbStub: DatabaseWriterLike = {
1586
2737
  aggregate: async () => {
@@ -1698,6 +2849,158 @@ const vectorsStub: VectorSearchLike = {
1698
2849
  const bindTableHelper = "";
1699
2850
  const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
1700
2851
  const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
2852
+ ` : "";
2853
+ const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
2854
+ 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 }>> {
2855
+ const env = this.env as Record<string, unknown>;
2856
+ const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
2857
+ const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
2858
+
2859
+ let cursor: null | string = null;
2860
+
2861
+ // Drain every page of the global membership so the seed/diff sees the
2862
+ // full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
2863
+ // a broad \`.global()\` shape would otherwise materialize an unbounded
2864
+ // array before the caller's \`withinGlobalShapeBound\` check rejects it,
2865
+ // so bail early and let the caller fail it closed.
2866
+ do {
2867
+ // eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
2868
+ const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
2869
+
2870
+ for (const doc of page.page) {
2871
+ rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
2872
+
2873
+ if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
2874
+ return rows;
2875
+ }
2876
+ }
2877
+
2878
+ cursor = page.isDone ? null : page.continueCursor;
2879
+ } while (cursor !== null);
2880
+
2881
+ return rows;
2882
+ }
2883
+ ` : "";
2884
+ const sourceClientCacheConst = hasSourcedTables ? `
2885
+ const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
2886
+ const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
2887
+ ` : "";
2888
+ const externalSourceOverride = hasSourcedTables ? `
2889
+ protected override async pollExternalSources(): Promise<number | undefined> {
2890
+ const env = (this.env ?? {}) as Record<string, unknown>;
2891
+ const sourced = Object.entries((schema as unknown as SchemaLike).tables)
2892
+ .map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
2893
+ .filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
2894
+
2895
+ if (sourced.length === 0) {
2896
+ return undefined;
2897
+ }
2898
+
2899
+ const shardKey = this.currentShardKey();
2900
+ const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2901
+ const writer = createShardCtxDb({
2902
+ broadcast: (delta) => {
2903
+ this.recordChangedTable(delta.table);
2904
+ },
2905
+ cdc: config.cdc ?? false,
2906
+ scheduler,
2907
+ schema: schema as unknown as SchemaLike,
2908
+ sql: this.sql as SqlExec,
2909
+ });
2910
+
2911
+ let clients = sourceClientCache.get(this);
2912
+
2913
+ if (clients === undefined) {
2914
+ clients = new Map();
2915
+ sourceClientCache.set(this, clients);
2916
+ }
2917
+
2918
+ let polledAt = sourcePollAtCache.get(this);
2919
+
2920
+ if (polledAt === undefined) {
2921
+ polledAt = new Map();
2922
+ sourcePollAtCache.set(this, polledAt);
2923
+ }
2924
+
2925
+ const now = Date.now();
2926
+ // \`nextDueAt\` tracks the EARLIEST next-due timestamp across every
2927
+ // non-manual source; the shared alarm re-arms there instead of the
2928
+ // fixed 2 s global-shape floor, so a large \`refresh.everyMs\` actually
2929
+ // sleeps until it's due. Stays \`undefined\` when every source is
2930
+ // \`refresh: "manual"\`, so the shared alarm goes idle for this tier.
2931
+ let nextDueAt: number | undefined;
2932
+
2933
+ for (const [table, source] of sourced) {
2934
+ if (source.refresh === "manual") {
2935
+ continue;
2936
+ }
2937
+
2938
+ if (isSourceDue(source.refresh, polledAt.get(table), now)) {
2939
+ try {
2940
+ let client = clients.get(source.binding);
2941
+
2942
+ if (client === undefined) {
2943
+ client = config.sourceClient?.(env, source.binding);
2944
+
2945
+ if (client !== undefined) {
2946
+ clients.set(source.binding, client);
2947
+ }
2948
+ }
2949
+
2950
+ if (client === undefined) {
2951
+ // No SqlClient resolved for this binding (host never wired
2952
+ // \`config.sourceClient\`, or wired it wrong). Surface it in the
2953
+ // Logs panel and stamp \`polledAt\` so a persistent misconfig backs
2954
+ // off to \`refresh.everyMs\` instead of retrying every alarm tick.
2955
+ this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
2956
+ } else if (source.mode === "incremental") {
2957
+ // Incremental (plan 136): pull only rows past the durable
2958
+ // watermark (or a full-pull seed/reconcile), upsert-only.
2959
+ // eslint-disable-next-line no-await-in-loop -- one sourced table at a time; sequential keeps the writer transaction simple
2960
+ await pullExternalSourceIncrementalTick(this.sql as SqlExec, writer, client, table, source, shardKey, now);
2961
+ } else {
2962
+ // 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
2963
+ await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
2964
+ }
2965
+
2966
+ // Timestamp AFTER the poll finishes, not the batch-start \`now\` — a
2967
+ // poll that outruns \`everyMs\` must not make \`nextDueAt\` (below)
2968
+ // stale-immediate and re-arm the alarm in a hammering loop.
2969
+ polledAt.set(table, Date.now());
2970
+ } catch (error) {
2971
+ this.recordExternalSourceError(table, error);
2972
+ // Stamp on failure too, so a persistently failing source throttles
2973
+ // to \`refresh.everyMs\` rather than being hammered every tick.
2974
+ polledAt.set(table, Date.now());
2975
+ }
2976
+ }
2977
+
2978
+ // This source's own next-due time, read AFTER the poll-or-skip above
2979
+ // so a just-polled source reports \`now + everyMs\` (its FRESH due
2980
+ // time), not the stale pre-poll one. An omitted \`refresh\` (poll
2981
+ // every tick) is due again immediately.
2982
+ const sourceNextDueAt = source.refresh === undefined ? now : (polledAt.get(table) ?? now) + source.refresh.everyMs;
2983
+
2984
+ nextDueAt = nextDueAt === undefined ? sourceNextDueAt : Math.min(nextDueAt, sourceNextDueAt);
2985
+ }
2986
+
2987
+ return nextDueAt;
2988
+ }
2989
+ ` : "";
2990
+ const sourceConstructorOverride = hasSourcedTables ? `
2991
+ public constructor(state: ShardDOState, env: unknown) {
2992
+ super(state, env);
2993
+
2994
+ const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
2995
+ const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
2996
+
2997
+ return source !== undefined && source.refresh !== "manual";
2998
+ });
2999
+
3000
+ if (autoSourced) {
3001
+ void this.scheduleSourcePoll();
3002
+ }
3003
+ }
1701
3004
  ` : "";
1702
3005
  const facadeBlock = hasTables ? `
1703
3006
  const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
@@ -1711,18 +3014,24 @@ ${schema.tables.map(
1711
3014
  (table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
1712
3015
  ).join("\n")}
1713
3016
  ` : "";
1714
- const everyContextBuild = `${kvFragments.build}${analyticsFragments.build}`;
1715
- const everyContextField = `${kvFragments.contextField}${analyticsFragments.contextField}`;
1716
- const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql;
3017
+ const secretsBuild = `
3018
+ const secrets = createSecrets(env);
3019
+ `;
3020
+ const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${envFragments.build}${secretsBuild}`;
3021
+ const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}${envFragments.contextField}
3022
+ secrets,`;
3023
+ const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines || hasX402;
1717
3024
  const actionOnlyBlock = actionOnlyHasAny ? `
1718
3025
  // ActionCtx-only helpers (external, non-deterministic I/O): constructed
1719
3026
  // and attached only for an \`action\` so query/mutation ctx never carry them.
1720
3027
  if (isAction) {
1721
- ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${[
3028
+ ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${x402Build}${[
1722
3029
  ...hasImages ? [" ctx.images = images;"] : [],
1723
3030
  ...hasHyperdrive ? [" ctx.sql = sql;"] : [],
1724
3031
  ...hasBrowser ? [" ctx.browser = browser;"] : [],
1725
- ...hasR2sql ? [" ctx.r2sql = r2sql;"] : []
3032
+ ...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
3033
+ ...hasPipelines ? [" ctx.pipelines = pipelines;"] : [],
3034
+ ...hasX402 ? [" ctx.x402 = x402;"] : []
1726
3035
  ].join("\n")}
1727
3036
  }
1728
3037
  ` : "";
@@ -1762,14 +3071,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
1762
3071
 
1763
3072
  /** 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. */
1764
3073
  const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
1765
- ${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
3074
+ ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}${agentSpecs}
1766
3075
  export interface ShardDOConfig {
1767
3076
  /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
1768
3077
  cdc?: boolean;
1769
3078
  /** 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. */
1770
3079
  observability?: (env: Record<string, unknown>) => LogSink | undefined;
1771
3080
  scheduler?: (env: Record<string, unknown>) => unknown;
1772
- storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${r2sqlFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}
3081
+ 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}
1773
3082
  }
1774
3083
 
1775
3084
  const schedulerStub = {
@@ -1807,7 +3116,7 @@ const storageStub = {
1807
3116
  throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
1808
3117
  },
1809
3118
  };
1810
- ${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${paymentStub}${bindTableHelper}
3119
+ ${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${x402Stub}${bindTableHelper}
1811
3120
  // Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
1812
3121
  // referencing call fails loudly with a clear error instead of overflowing the
1813
3122
  // stack. Tracked across the awaited handler chain (one DO invocation is
@@ -1827,11 +3136,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
1827
3136
  }
1828
3137
 
1829
3138
  if (runDepth >= MAX_RUN_DEPTH) {
1830
- throw Object.assign(new Error(\`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`), {
1831
- name: "LunoraError",
1832
- code: "RUN_DEPTH_EXCEEDED",
1833
- status: 500,
1834
- });
3139
+ throw new LunoraError("RUN_DEPTH_EXCEEDED", \`ctx.run*: composition depth limit (\${MAX_RUN_DEPTH}) exceeded — likely a cyclic runQuery/runMutation\`);
1835
3140
  }
1836
3141
 
1837
3142
  runDepth += 1;
@@ -1848,7 +3153,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
1848
3153
  * from the worker entry so wrangler binds it by name.
1849
3154
  */
1850
3155
  export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
1851
- class extends ShardDOBase {
3156
+ class extends ShardDOBase {${sourceConstructorOverride}
1852
3157
  private migrated = false;
1853
3158
 
1854
3159
  public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
@@ -1859,11 +3164,7 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
1859
3164
  // \`isSystemDispatch()\`). A client RPC never carries that flag, so its
1860
3165
  // internals stay not-found and never leak across the external boundary.
1861
3166
  if (!registered || (registered.visibility === "internal" && !this.isSystemDispatch())) {
1862
- throw Object.assign(new Error(\`function not registered: \${functionPath}\`), {
1863
- name: "LunoraError",
1864
- code: "FUNCTION_NOT_FOUND",
1865
- status: 404,
1866
- });
3167
+ throw new LunoraError("FUNCTION_NOT_FOUND", \`function not registered: \${functionPath}\`);
1867
3168
  }
1868
3169
 
1869
3170
  this.ensureMigrated();
@@ -1877,8 +3178,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
1877
3178
  // do external I/O that can't be rolled back, so both dispatch directly.
1878
3179
  // \`ctx.run*\` composition runs inside this span (it never re-enters
1879
3180
  // handleRpc); runInTransaction's own guard rejects accidental nesting.
3181
+ //
3182
+ // The replay bookkeeping (idempotency dedup row + custom-mutator
3183
+ // watermark advance) commits INSIDE this span via
3184
+ // \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
3185
+ // watermark are atomic — a crash can't leave the writes durable without
3186
+ // the replay guard.
1880
3187
  if (registered.kind === "mutation") {
1881
- return this.runInTransaction(() => registered.handler(ctx, args));
3188
+ return this.runInTransaction(async () => {
3189
+ const result = await registered.handler(ctx, args);
3190
+
3191
+ this.commitMutationBookkeeping(result);
3192
+
3193
+ return result;
3194
+ });
1882
3195
  }
1883
3196
 
1884
3197
  return registered.handler(ctx, args);
@@ -1916,7 +3229,7 @@ ${relationFanout.override}
1916
3229
  iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
1917
3230
  };
1918
3231
  }
1919
-
3232
+ ${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
1920
3233
  protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
1921
3234
  return LUNORA_LIFECYCLE_HOOKS[event];
1922
3235
  }
@@ -1952,7 +3265,7 @@ ${relationFanout.override}
1952
3265
  protected override studioFeatures(): StudioFeaturesResult {
1953
3266
  return LUNORA_STUDIO_FEATURES;
1954
3267
  }
1955
- ${workflowsMetadataOverride}
3268
+ ${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
1956
3269
  protected override advisories(): AdvisoryFinding[] {
1957
3270
  return LUNORA_ADVISORIES;
1958
3271
  }
@@ -1961,11 +3274,7 @@ ${workflowsMetadataOverride}
1961
3274
  const migration = LUNORA_MIGRATIONS[args.id];
1962
3275
 
1963
3276
  if (!migration) {
1964
- throw Object.assign(new Error(\`data migration "\${args.id}" is not registered\`), {
1965
- name: "LunoraError",
1966
- code: "MIGRATION_NOT_FOUND",
1967
- status: 404,
1968
- });
3277
+ throw new LunoraError("MIGRATION_NOT_FOUND", \`data migration "\${args.id}" is not registered\`, { status: 404 });
1969
3278
  }
1970
3279
 
1971
3280
  this.ensureMigrated();
@@ -2001,18 +3310,14 @@ ${workflowsMetadataOverride}
2001
3310
  const definition = (schema as unknown as SchemaLike).tables[args.table];
2002
3311
 
2003
3312
  if (!definition) {
2004
- throw Object.assign(new Error(\`unknown table: \${args.table}\`), { name: "LunoraError", code: "UNKNOWN_TABLE", status: 404 });
3313
+ throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${args.table}\`, { status: 404 });
2005
3314
  }
2006
3315
 
2007
3316
  // \`.global()\` tables live in D1, not this DO's SQLite — editing them
2008
3317
  // here would corrupt nothing but would fail confusingly, so reject up
2009
3318
  // front with a clear code the studio can surface.
2010
3319
  if (definition.shardMode?.kind === "global") {
2011
- throw Object.assign(new Error(\`table "\${args.table}" is global; edit it through D1, not the shard\`), {
2012
- name: "LunoraError",
2013
- code: "GLOBAL_TABLE_NOT_EDITABLE",
2014
- status: 400,
2015
- });
3320
+ throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${args.table}" is global; edit it through D1, not the shard\`, { status: 400 });
2016
3321
  }
2017
3322
 
2018
3323
  this.ensureMigrated();
@@ -2056,17 +3361,13 @@ ${workflowsMetadataOverride}
2056
3361
  const definition = (schema as unknown as SchemaLike).tables[table];
2057
3362
 
2058
3363
  if (!definition) {
2059
- throw Object.assign(new Error(\`unknown table: \${table}\`), { name: "LunoraError", code: "UNKNOWN_TABLE", status: 404 });
3364
+ throw new LunoraError("UNKNOWN_TABLE", \`unknown table: \${table}\`, { status: 404 });
2060
3365
  }
2061
3366
 
2062
3367
  // \`.global()\` tables live in D1, not this DO's SQLite — the same
2063
3368
  // guard \`runShardWrite\` applies to single-row edits.
2064
3369
  if (definition.shardMode?.kind === "global") {
2065
- throw Object.assign(new Error(\`table "\${table}" is global; edit it through D1, not the shard\`), {
2066
- name: "LunoraError",
2067
- code: "GLOBAL_TABLE_NOT_EDITABLE",
2068
- status: 400,
2069
- });
3370
+ throw new LunoraError("GLOBAL_TABLE_NOT_EDITABLE", \`table "\${table}" is global; edit it through D1, not the shard\`, { status: 400 });
2070
3371
  }
2071
3372
 
2072
3373
  this.ensureMigrated();
@@ -2107,7 +3408,7 @@ ${workflowsMetadataOverride}
2107
3408
  // \`rankBefore\` is optional on \`DatabaseWriterLike\` (the D1 twin omits it),
2108
3409
  // but the shard writer from \`createShardCtxDb\` always defines it.
2109
3410
  if (!writer.rankBefore) {
2110
- throw Object.assign(new Error("rankBefore is unavailable on the shard writer"), { name: "LunoraError", code: "NOT_IMPLEMENTED", status: 500 });
3411
+ throw new LunoraError("NOT_IMPLEMENTED", "rankBefore is unavailable on the shard writer", { status: 500 });
2111
3412
  }
2112
3413
 
2113
3414
  return writer.rankBefore(args.table, args.index, {
@@ -2137,7 +3438,7 @@ ${workflowsMetadataOverride}
2137
3438
  // directions live in the schema's rankIndex, so the shard reads them itself;
2138
3439
  // \`args.directions\` is only the coordinator's comparator hint and isn't forwarded.
2139
3440
  if (!writer.rankPageRows) {
2140
- throw Object.assign(new Error("rankPage is unavailable on the shard writer"), { name: "LunoraError", code: "NOT_IMPLEMENTED", status: 500 });
3441
+ throw new LunoraError("NOT_IMPLEMENTED", "rankPage is unavailable on the shard writer", { status: 500 });
2141
3442
  }
2142
3443
 
2143
3444
  return writer.rankPageRows(args.table, args.index, {
@@ -2186,7 +3487,7 @@ ${workflowsMetadataOverride}
2186
3487
  // dispatch path) fall back to the per-request fields as before.
2187
3488
  const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
2188
3489
  const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
2189
- ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
3490
+ ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}${agentsBuild}
2190
3491
  const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2191
3492
  // Build the storage adapter once and share it between \`ctx.storage\`
2192
3493
  // and \`ctx.db.system._storage\` so both read the same R2 binding. The
@@ -2195,19 +3496,20 @@ ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
2195
3496
  const storage = asBucketStorage(config.storage?.(env) ?? storageStub) as unknown as SystemReaderStorageLike;
2196
3497
  ${globalDatabaseLine} const db: DatabaseWriterLike = createShardCtxDb(${databaseOptions});
2197
3498
  ${facadeBlock}${paymentsBuild}
2198
- // \`ctx.log\`: each call is captured + attributed to the executing
2199
- // function and routed to the optional \`observability\` sink. The DO base
2200
- // also buffers it (studio Logs panel) and emits a structured console
2201
- // event the dev-server formats in the terminal.
3499
+ // \`ctx.log\`: the DO base builds the attributed logger (structured
3500
+ // fields + \`.with(...)\` child + trace correlation) and routes each call
3501
+ // to the optional \`observability\` sink. It also buffers the line (studio
3502
+ // Logs panel) and emits a structured console event the dev-server formats.
2202
3503
  const observability = config.observability?.(env);
2203
3504
  const logFunctionPath = options.functionPath ?? "";
2204
- const log = {
2205
- debug: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "debug", args, observability); },
2206
- error: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "error", args, observability); },
2207
- info: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "info", args, observability); },
2208
- log: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "log", args, observability); },
2209
- warn: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "warn", args, observability); },
2210
- };
3505
+ const log = this.makeLogger(logFunctionPath, observability);
3506
+
3507
+ // \`ctx.trace\` / \`ctx.metrics\`: spans and measurements to the same sink.
3508
+ // The trace anchor is threaded explicitly for the same reason \`identity\`
3509
+ // is a deferred caller (a subscription re-run) must not inherit the
3510
+ // writing mutation's trace from the shared per-request field.
3511
+ const trace = this.makeTracer(logFunctionPath, observability, options.identity ? undefined : this.getCurrentTrace());
3512
+ const metrics = this.makeMetrics(logFunctionPath, observability);
2211
3513
 
2212
3514
  // \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
2213
3515
  // captured ONCE so the whole handler body sees a single stable value.
@@ -2226,9 +3528,11 @@ ${facadeBlock}${paymentsBuild}
2226
3528
  fetch: globalThis.fetch.bind(globalThis),
2227
3529
  ip: this.getCurrentIp(),
2228
3530
  log,
3531
+ metrics,
2229
3532
  now,${ormContextField}
2230
3533
  scheduler,
2231
- storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
3534
+ storage,
3535
+ trace,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}${agentsContextField}
2232
3536
  };
2233
3537
  ${isActionLine}${actionOnlyBlock}
2234
3538
  ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
@@ -2492,4 +3796,4 @@ const emitWranglerCronTriggers = (crons) => {
2492
3796
  return triggers;
2493
3797
  };
2494
3798
 
2495
- export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
3799
+ 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 };