@lunora/codegen 1.0.0-alpha.2 → 1.0.0-alpha.21

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 (34) hide show
  1. package/__assets__/package-og.svg +1 -1
  2. package/dist/index.d.mts +444 -16
  3. package/dist/index.d.ts +444 -16
  4. package/dist/index.mjs +28 -22
  5. package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DlP6YM_Q.mjs} +1 -1
  6. package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DeblMkzO.mjs} +3 -2
  7. package/dist/packem_shared/FLAGS_FILENAME-B1vE0LIo.mjs +139 -0
  8. package/dist/packem_shared/{emitApi-hRVC-kE7.mjs → GENERATED_HEADER-CI272wRm.mjs} +873 -54
  9. package/dist/packem_shared/LUNORA_SOLUTION_RULES-BTejmZp-.mjs +131 -0
  10. package/dist/packem_shared/MUTATORS_FILENAME-BhqdPtKp.mjs +81 -0
  11. package/dist/packem_shared/{buildOpenRpcDocument-BZGY1-jT.mjs → OPENRPC_VERSION-t5pV2NIN.mjs} +1 -1
  12. package/dist/packem_shared/QUEUES_FILENAME-B5_eWCRe.mjs +119 -0
  13. package/dist/packem_shared/{createCodegenProject-DGJm0_Pk.mjs → SCHEMA_SNAPSHOT_FILENAME-BMez-jgr.mjs} +187 -43
  14. package/dist/packem_shared/{buildSchemaSnapshot-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-wWGP2_5E.mjs} +12 -0
  15. package/dist/packem_shared/SHAPES_FILENAME-ChV7MqgE.mjs +94 -0
  16. package/dist/packem_shared/{discoverWorkflows-DRDQdhfq.mjs → WORKFLOWS_FILENAME-CCisG0Vy.mjs} +42 -2
  17. package/dist/packem_shared/{buildOpenApiDocument-yHVN66Xd.mjs → buildOpenApiDocument-Dx5QftUn.mjs} +1 -1
  18. package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-CoirYbg6.mjs} +1 -1
  19. package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-Cev7RRAf.mjs} +20 -17
  20. package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BWMczzBx.mjs} +7 -2
  21. package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-CfP6cMzt.mjs} +2 -2
  22. package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-C7zxXkUf.mjs} +24 -2
  23. package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-Cm81kwrb.mjs} +1 -1
  24. package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-Bi5nJ0mJ.mjs} +10 -4
  25. package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-C4M8AXmQ.mjs} +1 -1
  26. package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-B0wGT-xe.mjs} +1 -1
  27. package/dist/packem_shared/discoverR2sqlCalls-BVNMd428.mjs +80 -0
  28. package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-BS9GOGC5.mjs} +1 -1
  29. package/dist/packem_shared/{discoverSchema-BBulgGbH.mjs → discoverSchema-KYq55qu3.mjs} +338 -16
  30. package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-Da8BKXcI.mjs} +1 -1
  31. package/dist/packem_shared/{emitApp-CzZ6GbrD.mjs → emitApp-cOSypPuO.mjs} +74 -8
  32. package/dist/packem_shared/{lintSchema-DicbOHvH.mjs → formatAdvisories-DdjK7sgh.mjs} +15 -2
  33. package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-Cabb60UV.mjs} +2 -1
  34. package/package.json +8 -7
@@ -1,5 +1,174 @@
1
1
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
2
2
 
3
+ const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
4
+
5
+ const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
6
+ const hasColumnModifier = (node) => node.column !== void 0;
7
+ const emitScalarGuard = (kind, inExpr) => {
8
+ switch (kind) {
9
+ case "bigint": {
10
+ return `if (typeof ${inExpr} !== "bigint") return DEFER;
11
+ `;
12
+ }
13
+ case "boolean": {
14
+ return `if (typeof ${inExpr} !== "boolean") return DEFER;
15
+ `;
16
+ }
17
+ case "date":
18
+ case "number":
19
+ case "timestamp": {
20
+ return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
21
+ `;
22
+ }
23
+ case "null": {
24
+ return `if (${inExpr} !== null) return DEFER;
25
+ `;
26
+ }
27
+ // string / id / storage all parse as a bare string at runtime.
28
+ default: {
29
+ return `if (typeof ${inExpr} !== "string") return DEFER;
30
+ `;
31
+ }
32
+ }
33
+ };
34
+ const compileLiteral = (node, inExpr) => {
35
+ const literal = node.literalValue?.trim();
36
+ if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
37
+ return void 0;
38
+ }
39
+ return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
40
+ ` };
41
+ };
42
+ const compileNode = (node, inExpr, context) => {
43
+ if (hasColumnModifier(node)) {
44
+ return void 0;
45
+ }
46
+ if (node.hasRefinement || node.sourceText !== void 0) {
47
+ return void 0;
48
+ }
49
+ if (PASS_THROUGH_KINDS.has(node.kind)) {
50
+ if (node.kind === "any") {
51
+ return { out: inExpr, pre: "" };
52
+ }
53
+ return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
54
+ }
55
+ switch (node.kind) {
56
+ case "array": {
57
+ return compileArray(node, inExpr, context);
58
+ }
59
+ case "literal": {
60
+ return compileLiteral(node, inExpr);
61
+ }
62
+ case "object": {
63
+ return compileObject(node, inExpr, context);
64
+ }
65
+ default: {
66
+ return void 0;
67
+ }
68
+ }
69
+ };
70
+ const compileArray = (node, inExpr, context) => {
71
+ const { inner } = node;
72
+ if (!inner) {
73
+ return void 0;
74
+ }
75
+ const id = context.next();
76
+ const array = `__arr${String(id)}`;
77
+ const index = `__i${String(id)}`;
78
+ const element = `__e${String(id)}`;
79
+ const innerEmit = compileNode(inner, element, context);
80
+ if (!innerEmit) {
81
+ return void 0;
82
+ }
83
+ const pre = `if (!Array.isArray(${inExpr})) return DEFER;
84
+ const ${array} = new Array(${inExpr}.length);
85
+ for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
86
+ const ${element} = ${inExpr}[${index}];
87
+ ${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
88
+ }
89
+ `;
90
+ return { out: array, pre };
91
+ };
92
+ const compileField = (key, node, access, context) => {
93
+ const keyLiteral = JSON.stringify(key);
94
+ if (node.kind === "optional") {
95
+ if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
96
+ return void 0;
97
+ }
98
+ const { inner } = node;
99
+ if (!inner) {
100
+ return void 0;
101
+ }
102
+ const innerEmit = compileNode(inner, access, context);
103
+ if (!innerEmit) {
104
+ return void 0;
105
+ }
106
+ const id = context.next();
107
+ const has = `__has${String(id)}`;
108
+ const value = `__val${String(id)}`;
109
+ const pre = `let ${has} = false;
110
+ let ${value};
111
+ if (${access} !== undefined) {
112
+ ${innerEmit.pre}${value} = ${innerEmit.out};
113
+ ${has} = true;
114
+ }
115
+ `;
116
+ return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
117
+ }
118
+ const emit = compileNode(node, access, context);
119
+ if (!emit) {
120
+ return void 0;
121
+ }
122
+ return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
123
+ };
124
+ const compileFields = (shape, accessFor, context) => {
125
+ let pre = "";
126
+ const entries = [];
127
+ for (const key of Object.keys(shape)) {
128
+ const node = shape[key];
129
+ if (!node) {
130
+ return void 0;
131
+ }
132
+ const field = compileField(key, node, accessFor(key), context);
133
+ if (!field) {
134
+ return void 0;
135
+ }
136
+ pre += field.pre;
137
+ entries.push(field.entry);
138
+ }
139
+ return { entries: entries.join(", "), pre };
140
+ };
141
+ const compileObject = (node, inExpr, context) => {
142
+ const shape = node.shape ?? {};
143
+ const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
144
+ if (!fields) {
145
+ return void 0;
146
+ }
147
+ const id = context.next();
148
+ const object = `__obj${String(id)}`;
149
+ const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
150
+ ${fields.pre}const ${object} = { ${fields.entries} };
151
+ `;
152
+ return { out: object, pre };
153
+ };
154
+ const compileArgsValidator = (args) => {
155
+ let counter = 0;
156
+ const context = {
157
+ next: () => {
158
+ counter += 1;
159
+ return counter;
160
+ }
161
+ };
162
+ const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
163
+ if (!fields) {
164
+ return void 0;
165
+ }
166
+ return `(source) => {
167
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
168
+ ${fields.pre}return { ${fields.entries} };
169
+ }`;
170
+ };
171
+
3
172
  const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
4
173
  const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
5
174
  client: "lunorash/client",
@@ -7,17 +176,18 @@ const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
7
176
  server: "lunorash/server",
8
177
  serverDataModel: "lunorash/server/data-model",
9
178
  serverDrizzle: "lunorash/server/drizzle",
10
- serverTypes: "lunorash/server/types"
179
+ serverTypes: "lunorash/server/types",
180
+ values: "lunorash/values"
11
181
  } : {
12
182
  client: "@lunora/client",
13
183
  do: "@lunora/do",
14
184
  server: "@lunora/server",
15
185
  serverDataModel: "@lunora/server/data-model",
16
186
  serverDrizzle: "@lunora/server/drizzle",
17
- serverTypes: "@lunora/server/types"
187
+ serverTypes: "@lunora/server/types",
188
+ values: "@lunora/values"
18
189
  };
19
190
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
20
- const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
21
191
  const IMPORT_PATH_RE = /^[\w./-]+$/u;
22
192
  const assertIdentifier = (value, context) => {
23
193
  if (!IDENTIFIER_RE.test(value)) {
@@ -430,6 +600,26 @@ import type { InsertModel } from "./dataModel.js";
430
600
  export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
431
601
  `;
432
602
  };
603
+ const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
604
+ if (shapes.length === 0 || !hasDatabase) {
605
+ return "";
606
+ }
607
+ const base = baseSpecifiers(useUmbrella);
608
+ const factories = shapes.map((shape) => {
609
+ assertIdentifier(shape.exportName, "shape export name");
610
+ return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
611
+ export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
612
+ createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
613
+ }).join("\n\n");
614
+ return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
615
+ import { lunoraCollectionOptions } from "@lunora/db/collections";
616
+ import type { Row } from "@lunora/db";
617
+ import type { Collection } from "@tanstack/db";
618
+ import { createCollection } from "@tanstack/db";
619
+
620
+ ${factories}
621
+ `;
622
+ };
433
623
  const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
434
624
  const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
435
625
  const registered = LUNORA_FUNCTIONS[functionPath];
@@ -444,7 +634,7 @@ const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: Caller
444
634
 
445
635
  return (await registered.handler(context, args ?? {})) as R;
446
636
  };`;
447
- const renderFunctionRegistry = (functions, migrations) => {
637
+ const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
448
638
  const aliasByPath = /* @__PURE__ */ new Map();
449
639
  const registerPath = (filePath) => {
450
640
  if (!aliasByPath.has(filePath)) {
@@ -457,6 +647,12 @@ const renderFunctionRegistry = (functions, migrations) => {
457
647
  for (const migration of migrations) {
458
648
  registerPath(migration.filePath);
459
649
  }
650
+ for (const mutator of mutators) {
651
+ registerPath(mutator.filePath);
652
+ }
653
+ for (const shape of shapes) {
654
+ registerPath(shape.filePath);
655
+ }
460
656
  const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
461
657
  if (!IMPORT_PATH_RE.test(filePath)) {
462
658
  throw new Error(`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
@@ -469,15 +665,37 @@ const renderFunctionRegistry = (functions, migrations) => {
469
665
  const migrationEntries = migrations.map(
470
666
  (migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
471
667
  ).join("\n");
668
+ const mutatorEntries = mutators.map(
669
+ (mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
670
+ ).join("\n");
671
+ const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
672
+ const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
673
+ const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
674
+ const installLines = functions.map((definition) => {
675
+ if (definition.lifecycle || Object.keys(definition.args).length === 0) {
676
+ return void 0;
677
+ }
678
+ const compiled = compileArgsValidator(definition.args);
679
+ if (compiled === void 0) {
680
+ return void 0;
681
+ }
682
+ const alias = aliasByPath.get(definition.filePath) ?? "";
683
+ return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
684
+ }).filter((line) => line !== void 0).join("\n");
472
685
  return {
473
- dispatchBody: dispatchEntries.length > 0 ? `
474
- ${dispatchEntries}
686
+ dispatchBody: combinedDispatch.length > 0 ? `
687
+ ${combinedDispatch}
475
688
  ` : "",
476
689
  importBlock: importLines.length > 0 ? `${importLines}
477
690
 
478
691
  ` : "",
692
+ installBlock: installLines,
479
693
  migrationBody: migrationEntries.length > 0 ? `
480
694
  ${migrationEntries}
695
+ ` : "",
696
+ mutatorPaths,
697
+ shapeBody: shapeEntries.length > 0 ? `
698
+ ${shapeEntries}
481
699
  ` : ""
482
700
  };
483
701
  };
@@ -531,14 +749,18 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
531
749
  };
532
750
  const emitServer = ({
533
751
  containers = [],
752
+ hasAccessFacade = false,
534
753
  hasAi = false,
535
754
  hasAnalytics = false,
536
755
  hasBrowser = false,
756
+ hasFlags = false,
537
757
  hasHyperdrive = false,
538
758
  hasImages = false,
539
759
  hasKv = false,
540
760
  hasPayments = false,
541
761
  hasPipelines = false,
762
+ hasR2sql = false,
763
+ queues = [],
542
764
  schema,
543
765
  storageRuleBuckets = [],
544
766
  useUmbrella = false,
@@ -572,6 +794,11 @@ const emitServer = ({
572
794
  assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
573
795
  return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
574
796
  readonly ${workflow.bindingName}?: unknown;`;
797
+ }),
798
+ ...queues.map((queue) => {
799
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
800
+ return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
801
+ readonly ${queue.bindingName}?: unknown;`;
575
802
  })
576
803
  ].join("\n");
577
804
  const envBlock = `
@@ -594,7 +821,13 @@ ${envBindingFields}` : ""}
594
821
  /** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
595
822
  export type Env = CloudflareBindings;`;
596
823
  const kvContextField = hasKv ? `
597
- readonly kv: import("@lunora/kv").Kv;` : "";
824
+ readonly kv: import("@lunora/bindings/kv").Kv;` : "";
825
+ const accessContextField = hasAccessFacade ? `
826
+ /** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
827
+ readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
828
+ const flagsContextField = hasFlags ? `
829
+ /** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
830
+ readonly flags: import("@lunora/flags").LunoraFlags;` : "";
598
831
  const hyperdriveActionField = hasHyperdrive ? `
599
832
  /**
600
833
  * 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.
@@ -605,13 +838,18 @@ export type Env = CloudflareBindings;`;
605
838
  readonly browser: import("@lunora/browser").Browser;` : "";
606
839
  const imagesActionField = hasImages ? `
607
840
  /** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
608
- readonly images: import("@lunora/images").Images;` : "";
841
+ readonly images: import("@lunora/bindings/images").Images;` : "";
609
842
  const analyticsContextField = hasAnalytics ? `
610
843
  /** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
611
- readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
844
+ readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;` : "";
612
845
  const pipelinesActionField = hasPipelines ? `
613
846
  /** Pipelines ingestion sink (durable, R2-backed). Fire-and-forget and batched; do not read it back in-handler. */
614
- readonly pipelines: import("@lunora/pipelines").PipelineClient;` : "";
847
+ readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;` : "";
848
+ const r2sqlActionField = hasR2sql ? `
849
+ /**
850
+ * 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.
851
+ */
852
+ readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;` : "";
615
853
  const hasWorkflows = workflows.length > 0;
616
854
  const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
617
855
  import type * as lunoraWorkflowDefinitions from "../workflows.js";
@@ -632,6 +870,21 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
632
870
  const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
633
871
  const workflowsContextField = hasWorkflows ? `
634
872
  readonly workflows: LunoraWorkflows;` : "";
873
+ const hasQueues = queues.length > 0;
874
+ const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
875
+ import type * as lunoraQueueDefinitions from "../queues.js";
876
+ ` : "";
877
+ const queuesTypeBlock = hasQueues ? `
878
+
879
+ /** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
880
+ type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
881
+
882
+ /** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
883
+ export interface LunoraQueues {
884
+ ${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
885
+ }` : "";
886
+ const queuesContextField = hasQueues ? `
887
+ readonly queues: LunoraQueues;` : "";
635
888
  const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
636
889
  import type {
637
890
  ActionBuilder,
@@ -653,11 +906,11 @@ import type {
653
906
  } from "${base.server}";
654
907
 
655
908
  import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
656
- ${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
909
+ ${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}
657
910
  export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
658
911
 
659
912
  /** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
660
- export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
913
+ export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}
661
914
 
662
915
  /**
663
916
  * Project-typed contexts. The base contexts from \`@lunora/server\` are
@@ -689,19 +942,19 @@ type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> |
689
942
  export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
690
943
  readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
691
944
  readonly orm: OrmReader;
692
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
945
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}
693
946
  }
694
947
 
695
948
  export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
696
949
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
697
950
  readonly orm: OrmWriter;
698
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
951
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}
699
952
  }
700
953
 
701
954
  export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
702
955
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
703
956
  readonly orm: OrmWriter;
704
- readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${workflowsContextField}
957
+ readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}
705
958
  }
706
959
 
707
960
  /**
@@ -771,10 +1024,40 @@ const renderLifecycleManifest = (functions) => {
771
1024
  }
772
1025
  return manifest;
773
1026
  };
774
- const emitFunctions = (functions, migrations = []) => {
1027
+ const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
775
1028
  const hasFunctions = functions.length > 0;
776
- const { dispatchBody, importBlock, migrationBody } = renderFunctionRegistry(functions, migrations);
1029
+ const base = baseSpecifiers(useUmbrella);
1030
+ const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
777
1031
  const lifecycleHooks = renderLifecycleManifest(functions);
1032
+ const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
1033
+ ` : "";
1034
+ const shapeRegistry = shapes.length > 0 ? `
1035
+ /**
1036
+ * Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
1037
+ * The generated ShardDO's \`resolveShape\` override looks a shape up by name and
1038
+ * evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
1039
+ * \`shape_subscribe\` (reads-as-permissions).
1040
+ */
1041
+ export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
1042
+ ` : "";
1043
+ const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
1044
+ /**
1045
+ * Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
1046
+ * ShardDO's \`isCustomMutator\` override routes through the client-watermark push
1047
+ * protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
1048
+ */
1049
+ export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
1050
+ ` : "";
1051
+ const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
1052
+ ` : "";
1053
+ const compiledArgsInstall = installBlock.length > 0 ? `
1054
+ /**
1055
+ * AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
1056
+ * onto its function's live \`.args\` object and consulted by the interpreted
1057
+ * parser as a zero-allocation fast path; anything it can't model is deferred.
1058
+ */
1059
+ ${installBlock}
1060
+ ` : "";
778
1061
  const caller = renderCaller(functions);
779
1062
  const callerTypes = caller.types ? `
780
1063
  ${caller.types}
@@ -789,7 +1072,7 @@ ${caller.implementation}
789
1072
  const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
790
1073
 
791
1074
  ` : "";
792
- return `${GENERATED_HEADER}${importBlock}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
1075
+ return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
793
1076
  ${dataModelImport}
794
1077
  /**
795
1078
  * Single registered function, narrowed to the shape \`handleRpc\` needs.
@@ -815,7 +1098,7 @@ export interface RegisteredLunoraFunction {
815
1098
  * emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
816
1099
  */
817
1100
  export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
818
-
1101
+ ${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
819
1102
  /**
820
1103
  * Connection-lifecycle manifest: the function paths the generated ShardDO
821
1104
  * dispatches when a client's WebSocket connects (\`connect\`) or disconnects
@@ -1036,7 +1319,7 @@ const emitKvFragments = (hasKv) => {
1036
1319
  kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
1037
1320
  contextField: `
1038
1321
  kv,`,
1039
- importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
1322
+ importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
1040
1323
  stub: `
1041
1324
  const kvStub: Kv = {
1042
1325
  delete: async () => {
@@ -1061,6 +1344,113 @@ const kvStub: Kv = {
1061
1344
  `
1062
1345
  };
1063
1346
  };
1347
+ const emitFlagsFragments = (hasFlags) => {
1348
+ if (!hasFlags) {
1349
+ return EMPTY_HELPER_FRAGMENTS;
1350
+ }
1351
+ return {
1352
+ build: `
1353
+ const flags: import("@lunora/flags").LunoraFlags = createFlags({
1354
+ hooks: flagsConfig.hooks,
1355
+ logger: flagsConfig.logger,
1356
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1357
+ targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
1358
+ });
1359
+ `,
1360
+ configField: `
1361
+ flags?: (env: Record<string, unknown>) => import("@lunora/flags").Provider;`,
1362
+ contextField: `
1363
+ flags,`,
1364
+ importLines: [`import { createFlags } from "@lunora/flags";`, `import flagsConfig from "../flags.js";`],
1365
+ stub: ""
1366
+ };
1367
+ };
1368
+ const emitAccessFragments = (hasAccessFacade) => {
1369
+ if (!hasAccessFacade) {
1370
+ return EMPTY_HELPER_FRAGMENTS;
1371
+ }
1372
+ return {
1373
+ build: `
1374
+ const access = accessFacade(identity, userId);
1375
+ `,
1376
+ configField: "",
1377
+ contextField: `
1378
+ access,`,
1379
+ importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
1380
+ stub: ""
1381
+ };
1382
+ };
1383
+ const emitFlagsOverrides = (flagKeys, hasFlags) => {
1384
+ if (!hasFlags) {
1385
+ return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
1386
+ }
1387
+ const clientBuild = (targetingKey) => `
1388
+ const env = (this.env ?? {}) as Record<string, unknown>;
1389
+ const flags: import("@lunora/flags").LunoraFlags = createFlags({
1390
+ hooks: flagsConfig.hooks,
1391
+ logger: flagsConfig.logger,
1392
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1393
+ targetingKey: ${targetingKey},
1394
+ });`;
1395
+ const constant = `
1396
+ /** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
1397
+ const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
1398
+ `;
1399
+ const evaluateOverride = `
1400
+ protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
1401
+ const evalContext = context as import("@lunora/flags").EvaluationContext | undefined;
1402
+ const evaluations: FlagsResult["flags"] = [];
1403
+
1404
+ for (const entry of LUNORA_FLAG_KEYS) {
1405
+ // eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
1406
+ const details =
1407
+ entry.type === "boolean"
1408
+ ? await flags.details.boolean(entry.key, false, evalContext)
1409
+ : entry.type === "number"
1410
+ ? await flags.details.number(entry.key, 0, evalContext)
1411
+ : entry.type === "string"
1412
+ ? await flags.details.string(entry.key, "", evalContext)
1413
+ : await flags.details.object(entry.key, {}, evalContext);
1414
+
1415
+ evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
1416
+ }
1417
+
1418
+ return { configured: true, flags: evaluations };
1419
+ }
1420
+ `;
1421
+ const subscriptionOverride = `
1422
+ 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 })")}
1423
+ const key = typeof args.key === "string" ? args.key : "";
1424
+ const rawContext = typeof args.context === "object" && args.context !== null ? (args.context as Record<string, unknown>) : undefined;
1425
+ // The reactive channel is public (any socket, no auth required): never
1426
+ // let a subscriber-supplied targetingKey override the verified identity
1427
+ // resolved above, or a client could read another user's flags. Other
1428
+ // (non-identity) targeting attributes pass through unchanged.
1429
+ const { targetingKey: _clientTargetingKey, ...safeContext } = rawContext ?? {};
1430
+ const context = (rawContext === undefined ? undefined : safeContext) as import("@lunora/flags").EvaluationContext | undefined;
1431
+
1432
+ // eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
1433
+ if (key.length === 0) {
1434
+ return Promise.resolve(null);
1435
+ }
1436
+
1437
+ if (args.type === "number") {
1438
+ return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
1439
+ }
1440
+
1441
+ if (args.type === "string") {
1442
+ return flags.string(key, typeof args.default === "string" ? args.default : "", context);
1443
+ }
1444
+
1445
+ if (args.type === "object") {
1446
+ return flags.object(key, (args.default ?? {}) as import("@lunora/flags").JsonValue, context);
1447
+ }
1448
+
1449
+ return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
1450
+ }
1451
+ `;
1452
+ return { constant, evaluateOverride, subscriptionOverride };
1453
+ };
1064
1454
  const emitAnalyticsFragments = (hasAnalytics) => {
1065
1455
  if (!hasAnalytics) {
1066
1456
  return EMPTY_HELPER_FRAGMENTS;
@@ -1076,8 +1466,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
1076
1466
  contextField: `
1077
1467
  analytics,`,
1078
1468
  importLines: [
1079
- `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
1080
- `import { createAnalytics } from "@lunora/analytics";`
1469
+ `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
1470
+ `import { createAnalytics } from "@lunora/bindings/analytics";`
1081
1471
  ],
1082
1472
  stub: `
1083
1473
  const analyticsStub: AnalyticsClient = {
@@ -1106,7 +1496,7 @@ const emitImagesFragments = (hasImages) => {
1106
1496
  // ActionCtx-only: woven onto the action ctx object, never query/mutation.
1107
1497
  contextField: `
1108
1498
  images,`,
1109
- importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
1499
+ importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
1110
1500
  stub: `
1111
1501
  const imagesStub: Images = {
1112
1502
  info: async () => {
@@ -1183,17 +1573,96 @@ const browserStub: Browser = {
1183
1573
  `
1184
1574
  };
1185
1575
  };
1186
- const emitContainers = (containers) => {
1576
+ const emitR2sqlFragments = (hasR2sql) => {
1577
+ if (!hasR2sql) {
1578
+ return EMPTY_HELPER_FRAGMENTS;
1579
+ }
1580
+ const r2sqlMissing = `throw new Error("ctx.r2sql: no R2 SQL credentials found. Set \\\`R2_SQL_TOKEN\\\`, \\\`R2_SQL_ACCOUNT_ID\\\` (or \\\`CLOUDFLARE_ACCOUNT_ID\\\`), and \\\`R2_SQL_BUCKET\\\` in your env/.dev.vars, or pass an \\\`r2sql\\\` config thunk to createShardDO().");`;
1581
+ return {
1582
+ build: `
1583
+ const r2sqlEnv = env as Record<string, unknown>;
1584
+ const r2sqlAccountId = (r2sqlEnv.R2_SQL_ACCOUNT_ID ?? r2sqlEnv.CLOUDFLARE_ACCOUNT_ID) as string | undefined;
1585
+ const r2sqlToken = r2sqlEnv.R2_SQL_TOKEN as string | undefined;
1586
+ const r2sqlBucket = r2sqlEnv.R2_SQL_BUCKET as string | undefined;
1587
+ const r2sql: R2SqlClient = config.r2sql
1588
+ ? config.r2sql(env)
1589
+ : r2sqlAccountId && r2sqlToken && r2sqlBucket
1590
+ ? createR2Sql({ accountId: r2sqlAccountId, apiToken: r2sqlToken, bucket: r2sqlBucket })
1591
+ : r2sqlStub;
1592
+ `,
1593
+ configField: `
1594
+ r2sql?: (env: Record<string, unknown>) => R2SqlClient;`,
1595
+ // ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
1596
+ // \`isAction\` block, never the every-ctx object literal.
1597
+ contextField: "",
1598
+ importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
1599
+ // The stub is typed `R2SqlClient`, so TS flags a missing method at build
1600
+ // time — but it must stay structurally in sync with that interface
1601
+ // (`@lunora/bindings/r2sql` client.ts) when a method is added there.
1602
+ stub: `
1603
+ const r2sqlStub: R2SqlClient = {
1604
+ describe: async () => {
1605
+ ${r2sqlMissing}
1606
+ },
1607
+ explain: async () => {
1608
+ ${r2sqlMissing}
1609
+ },
1610
+ from: () => {
1611
+ ${r2sqlMissing}
1612
+ },
1613
+ query: async () => {
1614
+ ${r2sqlMissing}
1615
+ },
1616
+ showDatabases: async () => {
1617
+ ${r2sqlMissing}
1618
+ },
1619
+ showTables: async () => {
1620
+ ${r2sqlMissing}
1621
+ },
1622
+ };
1623
+ `
1624
+ };
1625
+ };
1626
+ const emitPipelinesFragments = (hasPipelines) => {
1627
+ if (!hasPipelines) {
1628
+ return EMPTY_HELPER_FRAGMENTS;
1629
+ }
1630
+ 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().");`;
1631
+ return {
1632
+ build: `
1633
+ const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
1634
+ const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
1635
+ `,
1636
+ configField: `
1637
+ pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
1638
+ // ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
1639
+ // in the \`isAction\` block, never the every-ctx object literal.
1640
+ contextField: "",
1641
+ importLines: [
1642
+ `import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
1643
+ `import { createPipelines } from "@lunora/bindings/pipelines";`
1644
+ ],
1645
+ stub: `
1646
+ const pipelinesStub: PipelineClient = {
1647
+ send: async () => {
1648
+ ${pipelinesMissing}
1649
+ },
1650
+ };
1651
+ `
1652
+ };
1653
+ };
1654
+ const emitContainers = (containers, jurisdiction) => {
1187
1655
  if (containers.length === 0) {
1188
1656
  return "";
1189
1657
  }
1658
+ const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
1190
1659
  const classes = containers.map((container) => {
1191
1660
  assertIdentifier(container.exportName, `container export "${container.exportName}"`);
1192
1661
  assertIdentifier(container.className, `container class "${container.className}"`);
1193
1662
  return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
1194
1663
  export class ${container.className} extends LunoraContainer {
1195
1664
  public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
1196
- super(ctx, env, ${container.exportName}, "${container.exportName}");
1665
+ super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
1197
1666
  }
1198
1667
  }
1199
1668
  `;
@@ -1205,14 +1674,21 @@ export class ${container.className} extends LunoraContainer {
1205
1674
  * requires each \`containers[].class_name\` to be exported by the worker:
1206
1675
  *
1207
1676
  * \`export * from "./lunora/_generated/containers.js";\`
1677
+ *
1678
+ * \`ContainerProxy\` is re-exported alongside them: the egress-interception path
1679
+ * (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
1680
+ * \`handle.egress\` controls) routes container outbound traffic through this
1681
+ * WorkerEntrypoint, so it too must be exported by the deployed worker.
1208
1682
  */
1209
- import LunoraContainer from "@lunora/container/do";
1683
+ import { LunoraContainer } from "@lunora/container/do";
1210
1684
 
1211
1685
  import { ${imports} } from "../containers.js";
1212
1686
 
1687
+ export { ContainerProxy } from "@lunora/container/do";
1688
+
1213
1689
  ${classes}`;
1214
1690
  };
1215
- const emitContainerFragments = (containers) => {
1691
+ const emitContainerFragments = (containers, jurisdiction) => {
1216
1692
  if (containers.length === 0) {
1217
1693
  return { build: "", contextField: "", importLines: [], specs: "" };
1218
1694
  }
@@ -1225,8 +1701,11 @@ const emitContainerFragments = (containers) => {
1225
1701
  return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
1226
1702
  }).join("\n");
1227
1703
  return {
1704
+ // Schema `.jurisdiction("…")` pins every container DO this shard reaches
1705
+ // to the data-residency region; the arg is omitted when undeclared so
1706
+ // existing generated output is unchanged.
1228
1707
  build: `
1229
- const containers = createContainerContext(env, LUNORA_CONTAINERS);
1708
+ const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
1230
1709
  `,
1231
1710
  contextField: `
1232
1711
  containers,`,
@@ -1279,6 +1758,32 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
1279
1758
 
1280
1759
  ${classes}`;
1281
1760
  };
1761
+ const emitQueues = (queues) => {
1762
+ const pushQueues = queues.filter((queue) => queue.mode === "push");
1763
+ if (pushQueues.length === 0) {
1764
+ return "";
1765
+ }
1766
+ for (const queue of pushQueues) {
1767
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
1768
+ }
1769
+ const imports = pushQueues.map((queue) => queue.exportName).join(", ");
1770
+ const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
1771
+ return `${GENERATED_HEADER}/**
1772
+ * Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
1773
+ * composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
1774
+ * by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
1775
+ * by \`defineApp\` — you don't import this directly.
1776
+ */
1777
+ import type { QueueRegistry } from "@lunora/queue";
1778
+
1779
+ import { ${imports} } from "../queues.js";
1780
+
1781
+ /** Stable wrangler queue name → { definition, exportName } for batch routing. */
1782
+ export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
1783
+ ${entries}
1784
+ };
1785
+ `;
1786
+ };
1282
1787
  const emitWorkflowFragments = (workflows) => {
1283
1788
  if (workflows.length === 0) {
1284
1789
  return { build: "", contextField: "", importLines: [], specs: "" };
@@ -1304,6 +1809,31 @@ ${specEntries}
1304
1809
  `
1305
1810
  };
1306
1811
  };
1812
+ const emitQueueFragments = (queues) => {
1813
+ if (queues.length === 0) {
1814
+ return { build: "", contextField: "", importLines: [], specs: "" };
1815
+ }
1816
+ for (const queue of queues) {
1817
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
1818
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
1819
+ }
1820
+ const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
1821
+ return {
1822
+ build: `
1823
+ const queues = createQueueContext(env, LUNORA_QUEUES);
1824
+ `,
1825
+ contextField: `
1826
+ queues,`,
1827
+ importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
1828
+ // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
1829
+ specs: `
1830
+ /** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
1831
+ const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
1832
+ ${specEntries}
1833
+ ];
1834
+ `
1835
+ };
1836
+ };
1307
1837
  const emitWorkflowsMetadataFragments = (workflows) => {
1308
1838
  if (workflows.length === 0) {
1309
1839
  return { constant: "", override: "" };
@@ -1330,6 +1860,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
1330
1860
  `
1331
1861
  };
1332
1862
  };
1863
+ const emitQueuesMetadataFragments = (queues) => {
1864
+ if (queues.length === 0) {
1865
+ return { constant: "", override: "" };
1866
+ }
1867
+ const metadata = {
1868
+ queues: queues.map((queue) => {
1869
+ return {
1870
+ binding: queue.bindingName,
1871
+ ...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
1872
+ exportName: queue.exportName,
1873
+ mode: queue.mode,
1874
+ name: queue.name
1875
+ };
1876
+ })
1877
+ };
1878
+ return {
1879
+ constant: `
1880
+ /** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
1881
+ const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
1882
+ `,
1883
+ override: `
1884
+ protected override queuesMetadata(): QueuesResult {
1885
+ return LUNORA_QUEUES_INFO;
1886
+ }
1887
+ `
1888
+ };
1889
+ };
1333
1890
  const emitPaymentFragments = (hasPayments) => {
1334
1891
  if (!hasPayments) {
1335
1892
  return { build: "", configField: "", contextField: "", imports: [], stub: "" };
@@ -1385,13 +1942,15 @@ const paymentStub: LunoraPayment = {
1385
1942
  `
1386
1943
  };
1387
1944
  };
1388
- const buildDoTypeImports = (hasVectors, hasWorkflows) => [
1945
+ const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
1389
1946
  "AdvisoryFinding",
1390
1947
  "DatabaseWriterLike",
1391
1948
  "DataMigrationLike",
1949
+ ...hasFlags ? ["FlagsResult"] : [],
1392
1950
  "LogSink",
1393
1951
  "MaskPoliciesResult",
1394
1952
  "MigrationRunResult",
1953
+ ...hasQueues ? ["QueuesResult"] : [],
1395
1954
  "RunShardApplyCdcArgs",
1396
1955
  "RunShardMigrationArgs",
1397
1956
  "RlsPoliciesResult",
@@ -1413,34 +1972,50 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
1413
1972
  const emitShard = ({
1414
1973
  advisories = [],
1415
1974
  containers = [],
1975
+ flagKeys = [],
1976
+ hasAccessFacade = false,
1416
1977
  hasAi = false,
1417
1978
  hasAnalytics = false,
1418
1979
  hasBrowser = false,
1980
+ hasFlags = false,
1419
1981
  hasHyperdrive = false,
1420
1982
  hasImages = false,
1421
1983
  hasKv = false,
1422
1984
  hasPayments = false,
1985
+ hasPipelines = false,
1986
+ hasR2sql = false,
1423
1987
  maskMetadata,
1988
+ mutators = [],
1989
+ queues = [],
1424
1990
  rlsMetadata,
1425
1991
  schema,
1992
+ shapes = [],
1426
1993
  storageRules,
1427
1994
  studioFeatures,
1428
1995
  useUmbrella = false,
1429
1996
  workflows = []
1430
1997
  }) => {
1431
1998
  const base = baseSpecifiers(useUmbrella);
1999
+ const hasMutators = mutators.length > 0;
2000
+ const hasShapes = shapes.length > 0;
1432
2001
  const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
2002
+ const accessFragments = emitAccessFragments(hasAccessFacade);
1433
2003
  const kvFragments = emitKvFragments(hasKv);
2004
+ const flagsFragments = emitFlagsFragments(hasFlags);
2005
+ const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags);
1434
2006
  const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
1435
2007
  const imagesFragments = emitImagesFragments(hasImages);
1436
2008
  const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
1437
2009
  const browserFragments = emitBrowserFragments(hasBrowser);
2010
+ const r2sqlFragments = emitR2sqlFragments(hasR2sql);
2011
+ const pipelinesFragments = emitPipelinesFragments(hasPipelines);
2012
+ const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
1438
2013
  const {
1439
2014
  build: containersBuild,
1440
2015
  contextField: containersContextField,
1441
2016
  importLines: containerImportLines,
1442
2017
  specs: containerSpecs
1443
- } = emitContainerFragments(containers);
2018
+ } = emitContainerFragments(containers, schema.jurisdiction);
1444
2019
  const {
1445
2020
  build: workflowsBuild,
1446
2021
  contextField: workflowsContextField,
@@ -1459,18 +2034,22 @@ const emitShard = ({
1459
2034
  const maskData = maskMetadata ?? { columns: [] };
1460
2035
  const storageRulesData = storageRules ?? { rules: [] };
1461
2036
  const studioFeaturesData = studioFeatures ?? {
2037
+ flags: false,
1462
2038
  mail: false,
1463
2039
  payments: false,
2040
+ queues: false,
1464
2041
  scheduler: false,
1465
2042
  storage: false,
1466
2043
  vectors: false,
1467
2044
  workflows: false
1468
2045
  };
1469
2046
  const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
2047
+ const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
1470
2048
  const hasVectors = schema.vectorIndexes.length > 0;
1471
2049
  const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
1472
2050
  const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
1473
2051
  const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
2052
+ const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
1474
2053
  if (hasD1Global && hasHyperdriveGlobal) {
1475
2054
  throw new Error(
1476
2055
  'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
@@ -1481,40 +2060,113 @@ const emitShard = ({
1481
2060
  const tableIndexes = buildTableIndexes(schema);
1482
2061
  const tableColumns = buildTableColumns(schema);
1483
2062
  const storageColumns = buildStorageColumns(schema);
1484
- const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
2063
+ const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
2064
+ if (hasShapes) {
2065
+ doTypeImports.push("WhereInput");
2066
+ }
1485
2067
  const relationFanout = emitRelationFanout(hasGlobalTables);
2068
+ const shapeResolveOverride = hasShapes ? `
2069
+ 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 {
2070
+ const shape = LUNORA_SHAPES[name];
2071
+
2072
+ if (!shape) {
2073
+ return undefined;
2074
+ }
2075
+
2076
+ this.ensureMigrated();
2077
+
2078
+ // Trusted ctx from the socket's OWN verified identity — the client
2079
+ // supplies only the shape name + args; \`compileWhere\` validates the
2080
+ // args then runs the shape's \`where(ctx, args)\` under that identity,
2081
+ // so which rows replicate is a server decision (reads-as-permissions).
2082
+ const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
2083
+ const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
2084
+
2085
+ // AND-compose with the table's RLS read base-where. A shape runs no
2086
+ // procedure, so the \`.use(rls(...))\` middleware never fires; without
2087
+ // this merge its reads would bypass every read policy on the table
2088
+ // (rows the caller can't see would replicate). \`composeShapeReadWhere\`
2089
+ // evaluates the table's read policies under this same trusted ctx —
2090
+ // exactly the request-time path — and fails closed under a
2091
+ // \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
2092
+ const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
2093
+ ctx,
2094
+ identity: identity?.identity ?? null,
2095
+ rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
2096
+ roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
2097
+ shapeWhere,
2098
+ table: shape.table,
2099
+ tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
2100
+ userId: identity?.userId ?? null,
2101
+ });
2102
+
2103
+ // A live shape pokes only from its OWN shard's op-log, so a \`where()\`
2104
+ // that joins to a \`.shardBy()\` table (rows in another DO) is rejected
2105
+ // here — the first point the compiled predicate + shard modes are both
2106
+ // known. Remedy: denormalize, or move the joined table to \`.global()\`.
2107
+ assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
2108
+
2109
+ // A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
2110
+ // base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
2111
+ // instead of the CDC poke path.
2112
+ const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
2113
+
2114
+ return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
2115
+ }
2116
+ ` : "";
2117
+ const customMutatorOverride = hasMutators ? `
2118
+ protected override isCustomMutator(functionPath: string): boolean {
2119
+ return LUNORA_MUTATOR_PATHS.has(functionPath);
2120
+ }
2121
+ ` : "";
2122
+ const shapeReadRegistryConst = hasShapes ? `
2123
+ /** 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. */
2124
+ const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
2125
+ ` : "";
2126
+ const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
1486
2127
  const importLines = [
1487
2128
  `import type { ${doTypeImports.join(", ")} } from "${base.do}";`,
1488
- `import { applyCdcChanges, createShardCtxDb, runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
1489
- // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) lives in
2129
+ `import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
2130
+ ...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
2131
+ // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
2132
+ // `createSecrets` (the `ctx.secrets` core built-in) live in
1490
2133
  // `@lunora/server`, the single source — imported here rather than stamped
1491
- // inline into every generated shard.
1492
- `import { asBucketStorage } from "${base.server}";`
2134
+ // inline into every generated shard. With shapes, also pull the RLS
2135
+ // read-registry builder + `composeShapeReadWhere` so `resolveShape` can
2136
+ // AND-merge a shape's predicate with the table's read base-where.
2137
+ hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets } from "${base.server}";` : `import { asBucketStorage, createSecrets } from "${base.server}";`
1493
2138
  ];
1494
2139
  if (hasTables) {
1495
2140
  importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
1496
2141
  }
1497
2142
  if (hasVectors) {
1498
2143
  importLines.push(
1499
- `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
1500
- `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
2144
+ `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
2145
+ `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
1501
2146
  );
1502
2147
  }
1503
2148
  if (hasAi) {
1504
2149
  importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
1505
2150
  }
1506
2151
  importLines.push(
2152
+ ...accessFragments.importLines,
1507
2153
  ...kvFragments.importLines,
2154
+ ...flagsFragments.importLines,
1508
2155
  ...analyticsFragments.importLines,
1509
2156
  ...imagesFragments.importLines,
1510
2157
  ...hyperdriveFragments.importLines,
1511
2158
  ...browserFragments.importLines,
2159
+ ...r2sqlFragments.importLines,
2160
+ ...pipelinesFragments.importLines,
1512
2161
  ...containerImportLines,
1513
2162
  ...workflowImportLines,
2163
+ ...queueImportLines,
1514
2164
  ...paymentsImports,
1515
2165
  ``,
1516
2166
  `import schema from "../schema.js";`,
1517
- `import { LUNORA_FUNCTIONS, LUNORA_LIFECYCLE_HOOKS, LUNORA_MIGRATIONS } from "./functions.js";`
2167
+ // Local-first sync registries are pulled in alongside the function table
2168
+ // only when the project declares them, so the import list stays minimal.
2169
+ `import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
1518
2170
  );
1519
2171
  const vectorsConfigField = hasVectors ? `
1520
2172
  vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
@@ -1522,6 +2174,8 @@ const emitShard = ({
1522
2174
  d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
1523
2175
  const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
1524
2176
  hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
2177
+ const sourceClientConfigField = hasSourcedTables ? `
2178
+ sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
1525
2179
  const globalDatabaseStub = hasGlobalTables ? `
1526
2180
  const globalDbStub: DatabaseWriterLike = {
1527
2181
  aggregate: async () => {
@@ -1639,6 +2293,144 @@ const vectorsStub: VectorSearchLike = {
1639
2293
  const bindTableHelper = "";
1640
2294
  const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
1641
2295
  const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
2296
+ ` : "";
2297
+ const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
2298
+ 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 }>> {
2299
+ const env = this.env as Record<string, unknown>;
2300
+ const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
2301
+ const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
2302
+
2303
+ let cursor: null | string = null;
2304
+
2305
+ // Drain every page of the global membership so the seed/diff sees the
2306
+ // full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
2307
+ // a broad \`.global()\` shape would otherwise materialize an unbounded
2308
+ // array before the caller's \`withinGlobalShapeBound\` check rejects it,
2309
+ // so bail early and let the caller fail it closed.
2310
+ do {
2311
+ // eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
2312
+ const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
2313
+
2314
+ for (const doc of page.page) {
2315
+ rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
2316
+
2317
+ if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
2318
+ return rows;
2319
+ }
2320
+ }
2321
+
2322
+ cursor = page.isDone ? null : page.continueCursor;
2323
+ } while (cursor !== null);
2324
+
2325
+ return rows;
2326
+ }
2327
+ ` : "";
2328
+ const sourceClientCacheConst = hasSourcedTables ? `
2329
+ const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
2330
+ const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
2331
+ ` : "";
2332
+ const externalSourceOverride = hasSourcedTables ? `
2333
+ protected override async pollExternalSources(): Promise<number> {
2334
+ const env = (this.env ?? {}) as Record<string, unknown>;
2335
+ const sourced = Object.entries((schema as unknown as SchemaLike).tables)
2336
+ .map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
2337
+ .filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
2338
+
2339
+ if (sourced.length === 0) {
2340
+ return 0;
2341
+ }
2342
+
2343
+ const shardKey = this.currentShardKey();
2344
+ const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2345
+ const writer = createShardCtxDb({
2346
+ broadcast: (delta) => {
2347
+ this.recordChangedTable(delta.table);
2348
+ },
2349
+ cdc: config.cdc ?? false,
2350
+ scheduler,
2351
+ schema: schema as unknown as SchemaLike,
2352
+ sql: this.sql as SqlExec,
2353
+ });
2354
+
2355
+ let clients = sourceClientCache.get(this);
2356
+
2357
+ if (clients === undefined) {
2358
+ clients = new Map();
2359
+ sourceClientCache.set(this, clients);
2360
+ }
2361
+
2362
+ let polledAt = sourcePollAtCache.get(this);
2363
+
2364
+ if (polledAt === undefined) {
2365
+ polledAt = new Map();
2366
+ sourcePollAtCache.set(this, polledAt);
2367
+ }
2368
+
2369
+ const now = Date.now();
2370
+ // \`active\` counts non-manual sources: while > 0 the alarm re-arms; a
2371
+ // manual-only schema returns 0 so the shared alarm goes idle.
2372
+ let active = 0;
2373
+
2374
+ for (const [table, source] of sourced) {
2375
+ if (source.refresh === "manual") {
2376
+ continue;
2377
+ }
2378
+
2379
+ active += 1;
2380
+
2381
+ if (!isSourceDue(source.refresh, polledAt.get(table), now)) {
2382
+ continue;
2383
+ }
2384
+
2385
+ try {
2386
+ let client = clients.get(source.binding);
2387
+
2388
+ if (client === undefined) {
2389
+ client = config.sourceClient?.(env, source.binding);
2390
+
2391
+ if (client !== undefined) {
2392
+ clients.set(source.binding, client);
2393
+ }
2394
+ }
2395
+
2396
+ if (client === undefined) {
2397
+ // No SqlClient resolved for this binding (host never wired
2398
+ // \`config.sourceClient\`, or wired it wrong). Surface it in the
2399
+ // Logs panel and stamp \`polledAt\` so a persistent misconfig backs
2400
+ // off to \`refresh.everyMs\` instead of retrying every alarm tick.
2401
+ this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
2402
+ polledAt.set(table, now);
2403
+ continue;
2404
+ }
2405
+
2406
+ // 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
2407
+ await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
2408
+ polledAt.set(table, now);
2409
+ } catch (error) {
2410
+ this.recordExternalSourceError(table, error);
2411
+ // Stamp on failure too, so a persistently failing source throttles
2412
+ // to \`refresh.everyMs\` rather than being hammered every tick.
2413
+ polledAt.set(table, now);
2414
+ }
2415
+ }
2416
+
2417
+ return active;
2418
+ }
2419
+ ` : "";
2420
+ const sourceConstructorOverride = hasSourcedTables ? `
2421
+ public constructor(state: ShardDOState, env: unknown) {
2422
+ super(state, env);
2423
+
2424
+ const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
2425
+ const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
2426
+
2427
+ return source !== undefined && source.refresh !== "manual";
2428
+ });
2429
+
2430
+ if (autoSourced) {
2431
+ void this.scheduleSourcePoll();
2432
+ }
2433
+ }
1642
2434
  ` : "";
1643
2435
  const facadeBlock = hasTables ? `
1644
2436
  const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
@@ -1652,17 +2444,23 @@ ${schema.tables.map(
1652
2444
  (table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
1653
2445
  ).join("\n")}
1654
2446
  ` : "";
1655
- const everyContextBuild = `${kvFragments.build}${analyticsFragments.build}`;
1656
- const everyContextField = `${kvFragments.contextField}${analyticsFragments.contextField}`;
1657
- const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser;
2447
+ const secretsBuild = `
2448
+ const secrets = createSecrets(env);
2449
+ `;
2450
+ const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
2451
+ const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
2452
+ secrets,`;
2453
+ const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines;
1658
2454
  const actionOnlyBlock = actionOnlyHasAny ? `
1659
2455
  // ActionCtx-only helpers (external, non-deterministic I/O): constructed
1660
2456
  // and attached only for an \`action\` so query/mutation ctx never carry them.
1661
2457
  if (isAction) {
1662
- ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${[
2458
+ ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${[
1663
2459
  ...hasImages ? [" ctx.images = images;"] : [],
1664
2460
  ...hasHyperdrive ? [" ctx.sql = sql;"] : [],
1665
- ...hasBrowser ? [" ctx.browser = browser;"] : []
2461
+ ...hasBrowser ? [" ctx.browser = browser;"] : [],
2462
+ ...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
2463
+ ...hasPipelines ? [" ctx.pipelines = pipelines;"] : []
1666
2464
  ].join("\n")}
1667
2465
  }
1668
2466
  ` : "";
@@ -1702,14 +2500,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
1702
2500
 
1703
2501
  /** 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. */
1704
2502
  const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
1705
- ${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
2503
+ ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
1706
2504
  export interface ShardDOConfig {
1707
2505
  /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
1708
2506
  cdc?: boolean;
1709
2507
  /** 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. */
1710
2508
  observability?: (env: Record<string, unknown>) => LogSink | undefined;
1711
2509
  scheduler?: (env: Record<string, unknown>) => unknown;
1712
- storage?: (env: Record<string, unknown>) => unknown;${vectorsConfigField}${aiConfigField}${kvFragments.configField}${analyticsFragments.configField}${imagesFragments.configField}${hyperdriveFragments.configField}${browserFragments.configField}${paymentsConfigField}${d1ConfigField}${hyperdriveGlobalConfigField}
2510
+ 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}${d1ConfigField}${hyperdriveGlobalConfigField}${sourceClientConfigField}
1713
2511
  }
1714
2512
 
1715
2513
  const schedulerStub = {
@@ -1747,7 +2545,7 @@ const storageStub = {
1747
2545
  throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
1748
2546
  },
1749
2547
  };
1750
- ${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${paymentStub}${bindTableHelper}
2548
+ ${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${bindTableHelper}
1751
2549
  // Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
1752
2550
  // referencing call fails loudly with a clear error instead of overflowing the
1753
2551
  // stack. Tracked across the awaited handler chain (one DO invocation is
@@ -1788,7 +2586,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
1788
2586
  * from the worker entry so wrangler binds it by name.
1789
2587
  */
1790
2588
  export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
1791
- class extends ShardDOBase {
2589
+ class extends ShardDOBase {${sourceConstructorOverride}
1792
2590
  private migrated = false;
1793
2591
 
1794
2592
  public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
@@ -1817,8 +2615,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
1817
2615
  // do external I/O that can't be rolled back, so both dispatch directly.
1818
2616
  // \`ctx.run*\` composition runs inside this span (it never re-enters
1819
2617
  // handleRpc); runInTransaction's own guard rejects accidental nesting.
2618
+ //
2619
+ // The replay bookkeeping (idempotency dedup row + custom-mutator
2620
+ // watermark advance) commits INSIDE this span via
2621
+ // \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
2622
+ // watermark are atomic — a crash can't leave the writes durable without
2623
+ // the replay guard.
1820
2624
  if (registered.kind === "mutation") {
1821
- return this.runInTransaction(() => registered.handler(ctx, args));
2625
+ return this.runInTransaction(async () => {
2626
+ const result = await registered.handler(ctx, args);
2627
+
2628
+ this.commitMutationBookkeeping(result);
2629
+
2630
+ return result;
2631
+ });
1822
2632
  }
1823
2633
 
1824
2634
  return registered.handler(ctx, args);
@@ -1856,7 +2666,7 @@ ${relationFanout.override}
1856
2666
  iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
1857
2667
  };
1858
2668
  }
1859
-
2669
+ ${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
1860
2670
  protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
1861
2671
  return LUNORA_LIFECYCLE_HOOKS[event];
1862
2672
  }
@@ -1892,7 +2702,7 @@ ${relationFanout.override}
1892
2702
  protected override studioFeatures(): StudioFeaturesResult {
1893
2703
  return LUNORA_STUDIO_FEATURES;
1894
2704
  }
1895
- ${workflowsMetadataOverride}
2705
+ ${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
1896
2706
  protected override advisories(): AdvisoryFinding[] {
1897
2707
  return LUNORA_ADVISORIES;
1898
2708
  }
@@ -2126,7 +2936,7 @@ ${workflowsMetadataOverride}
2126
2936
  // dispatch path) fall back to the per-request fields as before.
2127
2937
  const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
2128
2938
  const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
2129
- ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
2939
+ ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
2130
2940
  const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2131
2941
  // Build the storage adapter once and share it between \`ctx.storage\`
2132
2942
  // and \`ctx.db.system._storage\` so both read the same R2 binding. The
@@ -2149,6 +2959,14 @@ ${facadeBlock}${paymentsBuild}
2149
2959
  warn: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "warn", args, observability); },
2150
2960
  };
2151
2961
 
2962
+ // \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
2963
+ // captured ONCE so the whole handler body sees a single stable value.
2964
+ // Query/mutation handlers must be deterministic (they may be re-run on
2965
+ // OCC retry / subscription re-eval), so they must read time through
2966
+ // \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
2967
+ // advisor flags the latter. Actions may still use ambient \`Date.now()\`.
2968
+ const now = Date.now();
2969
+
2152
2970
  const ctx: Record<string, unknown> = {
2153
2971
  auth: {
2154
2972
  getIdentity: async () => identity ?? null,
@@ -2157,9 +2975,10 @@ ${facadeBlock}${paymentsBuild}
2157
2975
  db,
2158
2976
  fetch: globalThis.fetch.bind(globalThis),
2159
2977
  ip: this.getCurrentIp(),
2160
- log,${ormContextField}
2978
+ log,
2979
+ now,${ormContextField}
2161
2980
  scheduler,
2162
- storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
2981
+ storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
2163
2982
  };
2164
2983
  ${isActionLine}${actionOnlyBlock}
2165
2984
  ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
@@ -2423,4 +3242,4 @@ const emitWranglerCronTriggers = (crons) => {
2423
3242
  return triggers;
2424
3243
  };
2425
3244
 
2426
- export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
3245
+ export { GENERATED_HEADER, buildStorageColumns, emitApi, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };