@lunora/codegen 1.0.0-alpha.3 → 1.0.0-alpha.30

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 (35) hide show
  1. package/__assets__/package-og.svg +1 -1
  2. package/dist/index.d.mts +1084 -25
  3. package/dist/index.d.ts +1084 -25
  4. package/dist/index.mjs +29 -23
  5. package/dist/packem_shared/{CONTAINERS_FILENAME-0K-pjNb8.mjs → CONTAINERS_FILENAME-DjpXMqhp.mjs} +1 -1
  6. package/dist/packem_shared/{CodegenDiagnosticError-54jWDxA9.mjs → CodegenDiagnosticError-DyQ5FwkM.mjs} +7 -5
  7. package/dist/packem_shared/FLAGS_FILENAME-BjpS5w08.mjs +139 -0
  8. package/dist/packem_shared/{GENERATED_HEADER-CuLyj0WQ.mjs → GENERATED_HEADER-f6nMvllu.mjs} +853 -65
  9. package/dist/packem_shared/MUTATORS_FILENAME-BZOfUhlY.mjs +81 -0
  10. package/dist/packem_shared/{OPENRPC_VERSION-BGUrsrt_.mjs → OPENRPC_VERSION-u5SqDDGk.mjs} +1 -1
  11. package/dist/packem_shared/QUEUES_FILENAME-BF0iUmx7.mjs +119 -0
  12. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-Cpfn80SH.mjs +3481 -0
  13. package/dist/packem_shared/{SCHEMA_SNAPSHOT_VERSION-DzLDbWk3.mjs → SCHEMA_SNAPSHOT_VERSION-D0ARY6rL.mjs} +18 -2
  14. package/dist/packem_shared/SHAPES_FILENAME-DOhPGi-6.mjs +94 -0
  15. package/dist/packem_shared/{WORKFLOWS_FILENAME-DRDQdhfq.mjs → WORKFLOWS_FILENAME-CCC_42o3.mjs} +42 -2
  16. package/dist/packem_shared/{buildOpenApiDocument-CXwnbp4b.mjs → buildOpenApiDocument-AgKWmlry.mjs} +1 -1
  17. package/dist/packem_shared/{discoverAuthApiCalls-C35R6z0T.mjs → discoverAuthApiCalls-URDoTADN.mjs} +1 -1
  18. package/dist/packem_shared/{discoverCrons-BL6iGuJ3.mjs → discoverCrons-DdZOeroL.mjs} +20 -17
  19. package/dist/packem_shared/{discoverFunctions-DEgAcRuD.mjs → discoverFunctions-BJdF7lRs.mjs} +63 -9
  20. package/dist/packem_shared/{discoverHttpRoutes-C978pBiG.mjs → discoverHttpRoutes-CcwP-Rkr.mjs} +2 -2
  21. package/dist/packem_shared/{discoverInserts-CRQdXvHO.mjs → discoverInserts-BpFhM32L.mjs} +24 -2
  22. package/dist/packem_shared/{discoverMaskProcedures-B64zA740.mjs → discoverMaskProcedures-DT-v8wvj.mjs} +58 -2
  23. package/dist/packem_shared/{discoverMigrations-Doj_-BAA.mjs → discoverMigrations-cG1idBVM.mjs} +10 -4
  24. package/dist/packem_shared/{discoverNondeterministicCalls-4KiPQxQU.mjs → discoverNondeterministicCalls-CEOjmfk4.mjs} +1 -1
  25. package/dist/packem_shared/{discoverQueries-BkIi0dBD.mjs → discoverQueries-DeVBQVAG.mjs} +1 -1
  26. package/dist/packem_shared/{discoverR2sqlCalls-BpDqvcUn.mjs → discoverR2sqlCalls-CWCAU-xJ.mjs} +1 -1
  27. package/dist/packem_shared/{discoverRlsMetadata-DpRB1HMe.mjs → discoverRlsMetadata-BL0oukaK.mjs} +1 -1
  28. package/dist/packem_shared/{discoverSchema-BBulgGbH.mjs → discoverSchema-CeXJWVKV.mjs} +349 -17
  29. package/dist/packem_shared/{discoverStorageRulesMetadata-DAqJUxUv.mjs → discoverStorageRulesMetadata-CFHByjIl.mjs} +1 -1
  30. package/dist/packem_shared/{emitApp-CzSxVDaG.mjs → emitApp-DKt3rPtr.mjs} +84 -8
  31. package/dist/packem_shared/{formatAdvisories-8NIv1k0I.mjs → formatAdvisories-C9wNBNvL.mjs} +47 -3
  32. package/dist/packem_shared/{parse-validator-tuQtHrsr.mjs → parse-validator-D6zI2i85.mjs} +5 -3
  33. package/dist/packem_shared/redact-oTmsol5A.mjs +33 -0
  34. package/package.json +9 -7
  35. package/dist/packem_shared/SCHEMA_SNAPSHOT_FILENAME-BQ_kCZ81.mjs +0 -903
@@ -1,27 +1,200 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { s as sanitizeNamespace } from './paths-BRd6JHuF.mjs';
2
3
 
4
+ const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
5
+
6
+ const PASS_THROUGH_KINDS = /* @__PURE__ */ new Set(["any", "bigint", "boolean", "date", "id", "null", "number", "storage", "string", "timestamp"]);
7
+ const hasColumnModifier = (node) => node.column !== void 0;
8
+ const emitScalarGuard = (kind, inExpr) => {
9
+ switch (kind) {
10
+ case "bigint": {
11
+ return `if (typeof ${inExpr} !== "bigint") return DEFER;
12
+ `;
13
+ }
14
+ case "boolean": {
15
+ return `if (typeof ${inExpr} !== "boolean") return DEFER;
16
+ `;
17
+ }
18
+ case "date":
19
+ case "number":
20
+ case "timestamp": {
21
+ return `if (typeof ${inExpr} !== "number" || !Number.isFinite(${inExpr})) return DEFER;
22
+ `;
23
+ }
24
+ case "null": {
25
+ return `if (${inExpr} !== null) return DEFER;
26
+ `;
27
+ }
28
+ // string / id / storage all parse as a bare string at runtime.
29
+ default: {
30
+ return `if (typeof ${inExpr} !== "string") return DEFER;
31
+ `;
32
+ }
33
+ }
34
+ };
35
+ const compileLiteral = (node, inExpr) => {
36
+ const literal = node.literalValue?.trim();
37
+ if (literal === void 0 || !LITERAL_VALUE_RE.test(literal)) {
38
+ return void 0;
39
+ }
40
+ return { out: inExpr, pre: `if (${inExpr} !== ${literal}) return DEFER;
41
+ ` };
42
+ };
43
+ const compileNode = (node, inExpr, context) => {
44
+ if (hasColumnModifier(node)) {
45
+ return void 0;
46
+ }
47
+ if (node.hasRefinement || node.sourceText !== void 0) {
48
+ return void 0;
49
+ }
50
+ if (PASS_THROUGH_KINDS.has(node.kind)) {
51
+ if (node.kind === "any") {
52
+ return { out: inExpr, pre: "" };
53
+ }
54
+ return { out: inExpr, pre: emitScalarGuard(node.kind, inExpr) };
55
+ }
56
+ switch (node.kind) {
57
+ case "array": {
58
+ return compileArray(node, inExpr, context);
59
+ }
60
+ case "literal": {
61
+ return compileLiteral(node, inExpr);
62
+ }
63
+ case "object": {
64
+ return compileObject(node, inExpr, context);
65
+ }
66
+ default: {
67
+ return void 0;
68
+ }
69
+ }
70
+ };
71
+ const compileArray = (node, inExpr, context) => {
72
+ const { inner } = node;
73
+ if (!inner) {
74
+ return void 0;
75
+ }
76
+ const id = context.next();
77
+ const array = `__arr${String(id)}`;
78
+ const index = `__i${String(id)}`;
79
+ const element = `__e${String(id)}`;
80
+ const innerEmit = compileNode(inner, element, context);
81
+ if (!innerEmit) {
82
+ return void 0;
83
+ }
84
+ const pre = `if (!Array.isArray(${inExpr})) return DEFER;
85
+ const ${array} = new Array(${inExpr}.length);
86
+ for (let ${index} = 0; ${index} < ${inExpr}.length; ${index}++) {
87
+ const ${element} = ${inExpr}[${index}];
88
+ ${innerEmit.pre}${array}[${index}] = ${innerEmit.out};
89
+ }
90
+ `;
91
+ return { out: array, pre };
92
+ };
93
+ const compileField = (key, node, access, context) => {
94
+ const keyLiteral = JSON.stringify(key);
95
+ if (node.kind === "optional") {
96
+ if (node.hasRefinement || node.sourceText !== void 0 || hasColumnModifier(node)) {
97
+ return void 0;
98
+ }
99
+ const { inner } = node;
100
+ if (!inner) {
101
+ return void 0;
102
+ }
103
+ const innerEmit = compileNode(inner, access, context);
104
+ if (!innerEmit) {
105
+ return void 0;
106
+ }
107
+ const id = context.next();
108
+ const has = `__has${String(id)}`;
109
+ const value = `__val${String(id)}`;
110
+ const pre = `let ${has} = false;
111
+ let ${value};
112
+ if (${access} !== undefined) {
113
+ ${innerEmit.pre}${value} = ${innerEmit.out};
114
+ ${has} = true;
115
+ }
116
+ `;
117
+ return { entry: `...(${has} ? { ${keyLiteral}: ${value} } : {})`, pre };
118
+ }
119
+ const emit = compileNode(node, access, context);
120
+ if (!emit) {
121
+ return void 0;
122
+ }
123
+ return { entry: `${keyLiteral}: ${emit.out}`, pre: emit.pre };
124
+ };
125
+ const compileFields = (shape, accessFor, context) => {
126
+ let pre = "";
127
+ const entries = [];
128
+ for (const key of Object.keys(shape)) {
129
+ const node = shape[key];
130
+ if (!node) {
131
+ return void 0;
132
+ }
133
+ const field = compileField(key, node, accessFor(key), context);
134
+ if (!field) {
135
+ return void 0;
136
+ }
137
+ pre += field.pre;
138
+ entries.push(field.entry);
139
+ }
140
+ return { entries: entries.join(", "), pre };
141
+ };
142
+ const compileObject = (node, inExpr, context) => {
143
+ const shape = node.shape ?? {};
144
+ const fields = compileFields(shape, (key) => `${inExpr}[${JSON.stringify(key)}]`, context);
145
+ if (!fields) {
146
+ return void 0;
147
+ }
148
+ const id = context.next();
149
+ const object = `__obj${String(id)}`;
150
+ const pre = `if (typeof ${inExpr} !== "object" || ${inExpr} === null || Array.isArray(${inExpr})) return DEFER;
151
+ ${fields.pre}const ${object} = { ${fields.entries} };
152
+ `;
153
+ return { out: object, pre };
154
+ };
155
+ const compileArgsValidator = (args) => {
156
+ let counter = 0;
157
+ const context = {
158
+ next: () => {
159
+ counter += 1;
160
+ return counter;
161
+ }
162
+ };
163
+ const fields = compileFields(args, (key) => `source[${JSON.stringify(key)}]`, context);
164
+ if (!fields) {
165
+ return void 0;
166
+ }
167
+ return `(source) => {
168
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return DEFER;
169
+ ${fields.pre}return { ${fields.entries} };
170
+ }`;
171
+ };
172
+
3
173
  const GENERATED_HEADER = "// GENERATED by @lunora/codegen — do not edit.\n// Run `lunora codegen` to regenerate.\n\n";
4
174
  const baseSpecifiers = (useUmbrella = false) => useUmbrella ? {
5
175
  client: "lunorash/client",
6
176
  do: "lunorash/do",
177
+ flags: "lunorash/flags",
7
178
  server: "lunorash/server",
8
179
  serverDataModel: "lunorash/server/data-model",
9
180
  serverDrizzle: "lunorash/server/drizzle",
10
- serverTypes: "lunorash/server/types"
181
+ serverTypes: "lunorash/server/types",
182
+ values: "lunorash/values"
11
183
  } : {
12
184
  client: "@lunora/client",
13
185
  do: "@lunora/do",
186
+ flags: "@lunora/flags",
14
187
  server: "@lunora/server",
15
188
  serverDataModel: "@lunora/server/data-model",
16
189
  serverDrizzle: "@lunora/server/drizzle",
17
- serverTypes: "@lunora/server/types"
190
+ serverTypes: "@lunora/server/types",
191
+ values: "@lunora/values"
18
192
  };
19
193
  const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/u;
20
- const LITERAL_VALUE_RE = /^(?:"[^"\\]*"|'[^'\\]*'|-?\d+(?:\.\d+)?|true|false|null)$/u;
21
194
  const IMPORT_PATH_RE = /^[\w./-]+$/u;
22
195
  const assertIdentifier = (value, context) => {
23
196
  if (!IDENTIFIER_RE.test(value)) {
24
- throw new Error(`@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
197
+ throw new LunoraError("INTERNAL", `@lunora/codegen: ${context} is not a valid JS identifier: ${JSON.stringify(value)}`);
25
198
  }
26
199
  };
27
200
  const renderPropertyKey = (fieldName) => IDENTIFIER_RE.test(fieldName) ? fieldName : JSON.stringify(fieldName);
@@ -48,7 +221,10 @@ const literalToType = (value) => {
48
221
  return "unknown";
49
222
  }
50
223
  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)}`);
224
+ throw new LunoraError(
225
+ "INTERNAL",
226
+ `@lunora/codegen: v.literal() argument is not a parseable string/number/boolean/null literal: ${JSON.stringify(value)}`
227
+ );
52
228
  }
53
229
  return value;
54
230
  };
@@ -430,6 +606,26 @@ import type { InsertModel } from "./dataModel.js";
430
606
  export const createSeedClient = (options?: SeedClientOptions): SeedClient<InsertModel> => createSeedClientBase<InsertModel>(schema, options);
431
607
  `;
432
608
  };
609
+ const emitCollections = (shapes, hasDatabase, useUmbrella = false) => {
610
+ if (shapes.length === 0 || !hasDatabase) {
611
+ return "";
612
+ }
613
+ const base = baseSpecifiers(useUmbrella);
614
+ const factories = shapes.map((shape) => {
615
+ assertIdentifier(shape.exportName, "shape export name");
616
+ return `/** Live collection for the \`${shape.exportName}\` replication shape — pass the shape's validated \`args\` (its partition selector). */
617
+ export const ${shape.exportName}Collection = (client: LunoraClient, args?: Record<string, unknown>): Collection<Row, string> =>
618
+ createCollection(lunoraCollectionOptions({ client, shape: { args, name: "${shape.exportName}" } }).config);`;
619
+ }).join("\n\n");
620
+ return `${GENERATED_HEADER}import type { LunoraClient } from "${base.client}";
621
+ import { lunoraCollectionOptions } from "@lunora/db/collections";
622
+ import type { Row } from "@lunora/db";
623
+ import type { Collection } from "@tanstack/db";
624
+ import { createCollection } from "@tanstack/db";
625
+
626
+ ${factories}
627
+ `;
628
+ };
433
629
  const moduleAlias = (filePath, index) => `lunora_${sanitizeNamespace(filePath)}_${String(index)}`;
434
630
  const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: CallerCtx, functionPath: string, args: Record<string, unknown> | undefined): Promise<R> => {
435
631
  const registered = LUNORA_FUNCTIONS[functionPath];
@@ -444,7 +640,7 @@ const CALL_REGISTERED_HELPER = `const callRegistered = async <R>(context: Caller
444
640
 
445
641
  return (await registered.handler(context, args ?? {})) as R;
446
642
  };`;
447
- const renderFunctionRegistry = (functions, migrations) => {
643
+ const renderFunctionRegistry = (functions, migrations, mutators = [], shapes = []) => {
448
644
  const aliasByPath = /* @__PURE__ */ new Map();
449
645
  const registerPath = (filePath) => {
450
646
  if (!aliasByPath.has(filePath)) {
@@ -457,9 +653,15 @@ const renderFunctionRegistry = (functions, migrations) => {
457
653
  for (const migration of migrations) {
458
654
  registerPath(migration.filePath);
459
655
  }
656
+ for (const mutator of mutators) {
657
+ registerPath(mutator.filePath);
658
+ }
659
+ for (const shape of shapes) {
660
+ registerPath(shape.filePath);
661
+ }
460
662
  const importLines = [...aliasByPath.entries()].map(([filePath, alias]) => {
461
663
  if (!IMPORT_PATH_RE.test(filePath)) {
462
- throw new Error(`@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
664
+ throw new LunoraError("INTERNAL", `@lunora/codegen: refusing to emit import for unsafe file path: ${JSON.stringify(filePath)}`);
463
665
  }
464
666
  return `import * as ${alias} from "../${filePath}.js";`;
465
667
  }).join("\n");
@@ -469,15 +671,37 @@ const renderFunctionRegistry = (functions, migrations) => {
469
671
  const migrationEntries = migrations.map(
470
672
  (migration) => ` ${JSON.stringify(migration.id)}: ${aliasByPath.get(migration.filePath) ?? ""}.${migration.exportName} as unknown as RegisteredDataMigration,`
471
673
  ).join("\n");
674
+ const mutatorEntries = mutators.map(
675
+ (mutator) => ` "${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}": ${aliasByPath.get(mutator.filePath) ?? ""}.${mutator.exportName} as unknown as RegisteredLunoraFunction,`
676
+ ).join("\n");
677
+ const shapeEntries = shapes.map((shape) => ` "${shape.exportName}": ${aliasByPath.get(shape.filePath) ?? ""}.${shape.exportName} as unknown as RegisteredShape,`).join("\n");
678
+ const mutatorPaths = mutators.map((mutator) => `${sanitizeNamespace(mutator.filePath)}:${mutator.exportName}`);
679
+ const combinedDispatch = [dispatchEntries, mutatorEntries].filter((entries) => entries.length > 0).join("\n");
680
+ const installLines = functions.map((definition) => {
681
+ if (definition.lifecycle || Object.keys(definition.args).length === 0) {
682
+ return void 0;
683
+ }
684
+ const compiled = compileArgsValidator(definition.args);
685
+ if (compiled === void 0) {
686
+ return void 0;
687
+ }
688
+ const alias = aliasByPath.get(definition.filePath) ?? "";
689
+ return `installCompiledValidatorMap(${alias}.${definition.exportName}.args, ${compiled});`;
690
+ }).filter((line) => line !== void 0).join("\n");
472
691
  return {
473
- dispatchBody: dispatchEntries.length > 0 ? `
474
- ${dispatchEntries}
692
+ dispatchBody: combinedDispatch.length > 0 ? `
693
+ ${combinedDispatch}
475
694
  ` : "",
476
695
  importBlock: importLines.length > 0 ? `${importLines}
477
696
 
478
697
  ` : "",
698
+ installBlock: installLines,
479
699
  migrationBody: migrationEntries.length > 0 ? `
480
700
  ${migrationEntries}
701
+ ` : "",
702
+ mutatorPaths,
703
+ shapeBody: shapeEntries.length > 0 ? `
704
+ ${shapeEntries}
481
705
  ` : ""
482
706
  };
483
707
  };
@@ -531,15 +755,19 @@ const buildStorageBucketNames = (schema, ruleBuckets = []) => {
531
755
  };
532
756
  const emitServer = ({
533
757
  containers = [],
758
+ hasAccessFacade = false,
534
759
  hasAi = false,
535
760
  hasAnalytics = false,
536
761
  hasBrowser = false,
762
+ hasFlags = false,
537
763
  hasHyperdrive = false,
538
764
  hasImages = false,
539
765
  hasKv = false,
540
766
  hasPayments = false,
541
767
  hasPipelines = false,
542
768
  hasR2sql = false,
769
+ identity,
770
+ queues = [],
543
771
  schema,
544
772
  storageRuleBuckets = [],
545
773
  useUmbrella = false,
@@ -573,6 +801,11 @@ const emitServer = ({
573
801
  assertIdentifier(workflow.bindingName, `workflow binding "${workflow.bindingName}"`);
574
802
  return ` /** Workflow binding for the \`${workflow.exportName}\` workflow. */
575
803
  readonly ${workflow.bindingName}?: unknown;`;
804
+ }),
805
+ ...queues.map((queue) => {
806
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
807
+ return ` /** Queue producer binding for the \`${queue.exportName}\` queue. */
808
+ readonly ${queue.bindingName}?: unknown;`;
576
809
  })
577
810
  ].join("\n");
578
811
  const envBlock = `
@@ -595,7 +828,13 @@ ${envBindingFields}` : ""}
595
828
  /** Alias for {@link CloudflareBindings} — the typed shape of \`env\`. */
596
829
  export type Env = CloudflareBindings;`;
597
830
  const kvContextField = hasKv ? `
598
- readonly kv: import("@lunora/kv").Kv;` : "";
831
+ readonly kv: import("@lunora/bindings/kv").Kv;` : "";
832
+ const accessContextField = hasAccessFacade ? `
833
+ /** Verified Cloudflare Access identity — a synchronous facade over the resolved claims (email / groups / hasGroup / claims). Anonymous when no Access token is present. */
834
+ readonly access: import("@lunora/cloudflare-access/context").AccessFacade;` : "";
835
+ const flagsContextField = hasFlags ? `
836
+ /** Feature-flag evaluation (OpenFeature). Reads are memoized per request; evaluations never throw — a provider error resolves to the supplied default. */
837
+ readonly flags: import("${base.flags}").LunoraFlags;` : "";
599
838
  const hyperdriveActionField = hasHyperdrive ? `
600
839
  /**
601
840
  * 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.
@@ -606,18 +845,18 @@ export type Env = CloudflareBindings;`;
606
845
  readonly browser: import("@lunora/browser").Browser;` : "";
607
846
  const imagesActionField = hasImages ? `
608
847
  /** Cloudflare Images transforms (resize/format/optimize). Non-deterministic — available only in actions. */
609
- readonly images: import("@lunora/images").Images;` : "";
848
+ readonly images: import("@lunora/bindings/images").Images;` : "";
610
849
  const analyticsContextField = hasAnalytics ? `
611
850
  /** Analytics Engine telemetry sink. Fire-and-forget and sampled; do not read it back in-handler. */
612
- readonly analytics: import("@lunora/analytics").AnalyticsClient;` : "";
851
+ readonly analytics: import("@lunora/bindings/analytics").AnalyticsClient;` : "";
613
852
  const pipelinesActionField = hasPipelines ? `
614
853
  /** 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;` : "";
854
+ readonly pipelines: import("@lunora/bindings/pipelines").PipelineClient;` : "";
616
855
  const r2sqlActionField = hasR2sql ? `
617
856
  /**
618
857
  * 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
858
  */
620
- readonly r2sql: import("@lunora/r2sql").R2SqlClient;` : "";
859
+ readonly r2sql: import("@lunora/bindings/r2sql").R2SqlClient;` : "";
621
860
  const hasWorkflows = workflows.length > 0;
622
861
  const workflowsTypeImport = hasWorkflows ? `import type { WorkflowHandle } from "@lunora/workflow";
623
862
  import type * as lunoraWorkflowDefinitions from "../workflows.js";
@@ -638,6 +877,35 @@ ${workflows.map((workflow) => ` get(name: ${JSON.stringify(workflow.exportNam
638
877
  const workflowsOmit = hasWorkflows ? ` | "workflows"` : "";
639
878
  const workflowsContextField = hasWorkflows ? `
640
879
  readonly workflows: LunoraWorkflows;` : "";
880
+ const hasQueues = queues.length > 0;
881
+ const queuesTypeImport = hasQueues ? `import type { QueueProducer } from "@lunora/queue";
882
+ import type * as lunoraQueueDefinitions from "../queues.js";
883
+ ` : "";
884
+ const queuesTypeBlock = hasQueues ? `
885
+
886
+ /** Message body type carried by a \`defineQueue\` definition (its phantom \`__lunoraBody\`). */
887
+ type QueueBodyOf<Definition> = Definition extends { __lunoraBody?: infer Body } ? (unknown extends Body ? unknown : NonNullable<Body>) : unknown;
888
+
889
+ /** This project's declared queues, addressable from \`ctx.queues\` by their \`lunora/queues.ts\` export name. */
890
+ export interface LunoraQueues {
891
+ ${queues.map((queue) => ` readonly ${queue.exportName}: QueueProducer<QueueBodyOf<typeof lunoraQueueDefinitions.${queue.exportName}>>;`).join("\n")}
892
+ }` : "";
893
+ const queuesContextField = hasQueues ? `
894
+ readonly queues: LunoraQueues;` : "";
895
+ const identityTypeImport = identity ? `import type { InferIdentity } from "${base.server}";
896
+ import type * as lunoraIdentityContract from "../identity.js";
897
+ ` : "";
898
+ const identityTypeBlock = identity ? `
899
+
900
+ /** 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. */
901
+ export type Identity = InferIdentity<typeof lunoraIdentityContract.${identity.exportName}>;
902
+
903
+ /** \`ctx.auth\` narrowed so \`getIdentity()\` resolves the declared {@link Identity} contract instead of the untyped claim bag. */
904
+ type NarrowedAuth = Omit<QueryCtxBase["auth"], "getIdentity"> & { getIdentity: () => Promise<Identity | null> };` : "";
905
+ const authOmit = identity ? ` | "auth"` : "";
906
+ const authContextField = identity ? `
907
+ readonly auth: NarrowedAuth;` : "";
908
+ const policyIdentityArgument = identity ? ", Identity" : "";
641
909
  const server = `${GENERATED_HEADER}import { createPolicyDsl, initLunora, v as vBase } from "${base.server}";
642
910
  import type {
643
911
  ActionBuilder,
@@ -659,11 +927,11 @@ import type {
659
927
  } from "${base.server}";
660
928
 
661
929
  import type { DataModel, DatabaseReaderFacade, DatabaseWriterFacade, Doc, Id as IdOfTable, OrmReader, OrmWriter, Relations, TableName } from "./dataModel.js";
662
- ${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}
930
+ ${aiTypeImport}${paymentsTypeImport}${containersTypeImport}${workflowsTypeImport}${queuesTypeImport}${identityTypeImport}
663
931
  export type { DataModel, Doc, Id, TableName } from "./dataModel.js";
664
932
 
665
933
  /** Storage buckets this schema declares (\`v.storage("name")\`), narrowing \`ctx.storage.bucket(name)\`. */
666
- export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}
934
+ export type StorageBucketName = ${storageBucketUnion};${envBlock}${workflowsTypeBlock}${queuesTypeBlock}${identityTypeBlock}
667
935
 
668
936
  /**
669
937
  * Project-typed contexts. The base contexts from \`@lunora/server\` are
@@ -692,22 +960,22 @@ type TypedTableQuery = (<T extends TableName>(table: T) => TableReader<Doc<T>>)
692
960
  */
693
961
  type TypedTableGet = <T extends TableName>(id: IdOfTable<T>) => Promise<Doc<T> | null>;
694
962
 
695
- export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"> {
963
+ export interface QueryCtx extends Omit<QueryCtxBase, "db" | "storage"${authOmit}> {
696
964
  readonly db: Omit<DatabaseReader, "query" | "get"> & DatabaseReaderFacade & { query: TypedTableQuery; get: TypedTableGet };
697
965
  readonly orm: OrmReader;
698
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}
966
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${authContextField}
699
967
  }
700
968
 
701
- export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}> {
969
+ export interface MutationCtx extends Omit<MutationCtxBase, "db" | "storage"${workflowsOmit}${authOmit}> {
702
970
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
703
971
  readonly orm: OrmWriter;
704
- readonly storage: ReadOnlyStorage<StorageBucketName>;${kvContextField}${analyticsContextField}${workflowsContextField}
972
+ readonly storage: ReadOnlyStorage<StorageBucketName>;${accessContextField}${kvContextField}${flagsContextField}${analyticsContextField}${workflowsContextField}${queuesContextField}${authContextField}
705
973
  }
706
974
 
707
- export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}> {
975
+ export interface ActionCtx extends Omit<ActionCtxBase, "db" | "storage"${workflowsOmit}${authOmit}> {
708
976
  readonly db: Omit<DatabaseWriter, "query" | "get"> & DatabaseWriterFacade & { query: TypedTableQuery; get: TypedTableGet };
709
977
  readonly orm: OrmWriter;
710
- readonly storage: StorageBase<StorageBucketName>;${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}
978
+ readonly storage: StorageBase<StorageBucketName>;${accessContextField}${aiActionField}${paymentsActionField}${containersActionField}${kvContextField}${flagsContextField}${hyperdriveActionField}${browserActionField}${imagesActionField}${analyticsContextField}${pipelinesActionField}${r2sqlActionField}${workflowsContextField}${queuesContextField}${authContextField}
711
979
  }
712
980
 
713
981
  /**
@@ -747,7 +1015,7 @@ export const internalAction = lunoraBuilders.internalAction as unknown as Intern
747
1015
  * Runtime-identical to \`@lunora/server\`'s \`definePolicy\`; only the types narrow,
748
1016
  * so the \`rls()\` chain discovers a policy authored either way the same.
749
1017
  */
750
- export const definePolicy = createPolicyDsl<DataModel, Relations>();
1018
+ export const definePolicy = createPolicyDsl<DataModel, Relations${policyIdentityArgument}>();
751
1019
 
752
1020
  /**
753
1021
  * The validator builder \`v\`, with \`v.id(...)\` constrained to THIS schema's
@@ -777,10 +1045,40 @@ const renderLifecycleManifest = (functions) => {
777
1045
  }
778
1046
  return manifest;
779
1047
  };
780
- const emitFunctions = (functions, migrations = []) => {
1048
+ const emitFunctions = (functions, migrations = [], useUmbrella = false, mutators = [], shapes = []) => {
781
1049
  const hasFunctions = functions.length > 0;
782
- const { dispatchBody, importBlock, migrationBody } = renderFunctionRegistry(functions, migrations);
1050
+ const base = baseSpecifiers(useUmbrella);
1051
+ const { dispatchBody, importBlock, installBlock, migrationBody, mutatorPaths, shapeBody } = renderFunctionRegistry(functions, migrations, mutators, shapes);
783
1052
  const lifecycleHooks = renderLifecycleManifest(functions);
1053
+ const shapeTypeImport = shapes.length > 0 ? `import type { RegisteredShape } from "${base.server}";
1054
+ ` : "";
1055
+ const shapeRegistry = shapes.length > 0 ? `
1056
+ /**
1057
+ * Replication-shape registry — one entry per \`defineShape\` in \`lunora/shapes.ts\`.
1058
+ * The generated ShardDO's \`resolveShape\` override looks a shape up by name and
1059
+ * evaluates its trusted \`compileWhere(ctx, args)\` to authorize + scope a
1060
+ * \`shape_subscribe\` (reads-as-permissions).
1061
+ */
1062
+ export const LUNORA_SHAPES: Record<string, RegisteredShape> = {${shapeBody}};
1063
+ ` : "";
1064
+ const mutatorPathsRegistry = mutatorPaths.length > 0 ? `
1065
+ /**
1066
+ * Custom-mutator function paths — the \`LUNORA_FUNCTIONS\` keys the generated
1067
+ * ShardDO's \`isCustomMutator\` override routes through the client-watermark push
1068
+ * protocol (\`x-lunora-client-id\`/\`x-lunora-client-seq\` ordering).
1069
+ */
1070
+ export const LUNORA_MUTATOR_PATHS: ReadonlySet<string> = new Set([${mutatorPaths.map((path) => JSON.stringify(path)).join(", ")}]);
1071
+ ` : "";
1072
+ const compiledArgsImport = installBlock.length > 0 ? `import { DEFER_VALIDATION as DEFER, installCompiledValidatorMap } from "${base.values}";
1073
+ ` : "";
1074
+ const compiledArgsInstall = installBlock.length > 0 ? `
1075
+ /**
1076
+ * AOT-compiled argument validators (Worker-safe, no \`eval\`). Each is installed
1077
+ * onto its function's live \`.args\` object and consulted by the interpreted
1078
+ * parser as a zero-allocation fast path; anything it can't model is deferred.
1079
+ */
1080
+ ${installBlock}
1081
+ ` : "";
784
1082
  const caller = renderCaller(functions);
785
1083
  const callerTypes = caller.types ? `
786
1084
  ${caller.types}
@@ -795,7 +1093,7 @@ ${caller.implementation}
795
1093
  const callRegisteredHelper = hasFunctions ? `${CALL_REGISTERED_HELPER}
796
1094
 
797
1095
  ` : "";
798
- return `${GENERATED_HEADER}${importBlock}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
1096
+ return `${GENERATED_HEADER}${importBlock}${compiledArgsImport}${shapeTypeImport}import type { ActionCtx, MutationCtx, QueryCtx } from "./server.js";
799
1097
  ${dataModelImport}
800
1098
  /**
801
1099
  * Single registered function, narrowed to the shape \`handleRpc\` needs.
@@ -821,7 +1119,7 @@ export interface RegisteredLunoraFunction {
821
1119
  * emits (\`api[namespace][fn].__lunoraRef === "namespace:fn"\`).
822
1120
  */
823
1121
  export const LUNORA_FUNCTIONS: Record<string, RegisteredLunoraFunction> = {${dispatchBody}};
824
-
1122
+ ${compiledArgsInstall}${shapeRegistry}${mutatorPathsRegistry}
825
1123
  /**
826
1124
  * Connection-lifecycle manifest: the function paths the generated ShardDO
827
1125
  * dispatches when a client's WebSocket connects (\`connect\`) or disconnects
@@ -1042,7 +1340,7 @@ const emitKvFragments = (hasKv) => {
1042
1340
  kv?: (env: Record<string, unknown>) => KVNamespaceLike;`,
1043
1341
  contextField: `
1044
1342
  kv,`,
1045
- importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/kv";`, `import { createKv } from "@lunora/kv";`],
1343
+ importLines: [`import type { Kv, KVNamespaceLike } from "@lunora/bindings/kv";`, `import { createKv } from "@lunora/bindings/kv";`],
1046
1344
  stub: `
1047
1345
  const kvStub: Kv = {
1048
1346
  delete: async () => {
@@ -1067,6 +1365,116 @@ const kvStub: Kv = {
1067
1365
  `
1068
1366
  };
1069
1367
  };
1368
+ const emitFlagsFragments = (hasFlags, flagsSpecifier) => {
1369
+ if (!hasFlags) {
1370
+ return EMPTY_HELPER_FRAGMENTS;
1371
+ }
1372
+ return {
1373
+ build: `
1374
+ const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1375
+ hooks: flagsConfig.hooks,
1376
+ logger: flagsConfig.logger,
1377
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1378
+ targetingKey: () => flagsConfig.identify?.({ identity: identity ?? null, userId: userId ?? null }),
1379
+ });
1380
+ `,
1381
+ configField: `
1382
+ flags?: (env: Record<string, unknown>) => import("${flagsSpecifier}").Provider;`,
1383
+ contextField: `
1384
+ flags,`,
1385
+ importLines: [`import { createFlags } from "${flagsSpecifier}";`, `import flagsConfig from "../flags.js";`],
1386
+ stub: ""
1387
+ };
1388
+ };
1389
+ const emitAccessFragments = (hasAccessFacade) => {
1390
+ if (!hasAccessFacade) {
1391
+ return EMPTY_HELPER_FRAGMENTS;
1392
+ }
1393
+ return {
1394
+ build: `
1395
+ const access = accessFacade(identity, userId);
1396
+ `,
1397
+ configField: "",
1398
+ contextField: `
1399
+ access,`,
1400
+ importLines: [`import { accessFacade } from "@lunora/cloudflare-access/context";`],
1401
+ stub: ""
1402
+ };
1403
+ };
1404
+ const emitFlagsOverrides = (flagKeys, hasFlags, flagsSpecifier) => {
1405
+ if (!hasFlags) {
1406
+ return { constant: "", evaluateOverride: "", subscriptionOverride: "" };
1407
+ }
1408
+ const clientBuild = (targetingKey) => `
1409
+ const env = (this.env ?? {}) as Record<string, unknown>;
1410
+ const flags: import("${flagsSpecifier}").LunoraFlags = createFlags({
1411
+ hooks: flagsConfig.hooks,
1412
+ logger: flagsConfig.logger,
1413
+ provider: () => config.flags?.(env) ?? flagsConfig.provider(env),
1414
+ targetingKey: ${targetingKey},
1415
+ });`;
1416
+ const constant = `
1417
+ /** Statically-discovered feature flags (\`ctx.flags.<type>("key")\` reads) served via \`__lunora_admin__:listFlags\` + the reactive \`__lunora_flags__:\` channel. */
1418
+ const LUNORA_FLAG_KEYS: ReadonlyArray<{ key: string; type: "boolean" | "number" | "object" | "string" }> = ${JSON.stringify(flagKeys, void 0, 4)};
1419
+ `;
1420
+ const evaluateOverride = `
1421
+ protected override async evaluateFlags(context?: Record<string, unknown>): Promise<FlagsResult> {${clientBuild("undefined")}
1422
+ const evalContext = context as import("${flagsSpecifier}").EvaluationContext | undefined;
1423
+ const evaluations: FlagsResult["flags"] = [];
1424
+
1425
+ for (const entry of LUNORA_FLAG_KEYS) {
1426
+ // eslint-disable-next-line no-await-in-loop -- flags evaluate sequentially; each shares the single memoized provider client
1427
+ const details =
1428
+ entry.type === "boolean"
1429
+ ? await flags.details.boolean(entry.key, false, evalContext)
1430
+ : entry.type === "number"
1431
+ ? await flags.details.number(entry.key, 0, evalContext)
1432
+ : entry.type === "string"
1433
+ ? await flags.details.string(entry.key, "", evalContext)
1434
+ : await flags.details.object(entry.key, {}, evalContext);
1435
+
1436
+ evaluations.push({ errorCode: details.errorCode, key: entry.key, reason: details.reason, type: entry.type, value: details.value, variant: details.variant });
1437
+ }
1438
+
1439
+ return { configured: true, flags: evaluations };
1440
+ }
1441
+ `;
1442
+ const subscriptionOverride = `
1443
+ 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 })")}
1444
+ const key = typeof args.key === "string" ? args.key : "";
1445
+
1446
+ // SECURITY: the reactive channel is public (any socket, no auth). Serve
1447
+ // ONLY statically-discovered flag keys — an arbitrary client-supplied key
1448
+ // would let a subscriber probe the value of internal/unreleased flags the
1449
+ // app never exposes. Unknown key ⇒ the "nothing to deliver" sentinel.
1450
+ // eslint-disable-next-line unicorn/no-null -- the base hook's "nothing to deliver" sentinel
1451
+ if (key.length === 0 || !LUNORA_FLAG_KEYS.some((entry) => entry.key === key)) {
1452
+ return Promise.resolve(null);
1453
+ }
1454
+
1455
+ // SECURITY: evaluate under the socket's server-verified identity ONLY
1456
+ // (the targetingKey resolved above). Client-supplied targeting context is
1457
+ // NOT honored on this public channel — otherwise a subscriber could spoof
1458
+ // targeting attributes (e.g. plan/role) to unlock a flag gated on them.
1459
+ const context = undefined;
1460
+
1461
+ if (args.type === "number") {
1462
+ return flags.number(key, typeof args.default === "number" ? args.default : 0, context);
1463
+ }
1464
+
1465
+ if (args.type === "string") {
1466
+ return flags.string(key, typeof args.default === "string" ? args.default : "", context);
1467
+ }
1468
+
1469
+ if (args.type === "object") {
1470
+ return flags.object(key, (args.default ?? {}) as import("${flagsSpecifier}").JsonValue, context);
1471
+ }
1472
+
1473
+ return flags.boolean(key, typeof args.default === "boolean" ? args.default : false, context);
1474
+ }
1475
+ `;
1476
+ return { constant, evaluateOverride, subscriptionOverride };
1477
+ };
1070
1478
  const emitAnalyticsFragments = (hasAnalytics) => {
1071
1479
  if (!hasAnalytics) {
1072
1480
  return EMPTY_HELPER_FRAGMENTS;
@@ -1082,8 +1490,8 @@ const emitAnalyticsFragments = (hasAnalytics) => {
1082
1490
  contextField: `
1083
1491
  analytics,`,
1084
1492
  importLines: [
1085
- `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/analytics";`,
1086
- `import { createAnalytics } from "@lunora/analytics";`
1493
+ `import type { AnalyticsClient, AnalyticsEngineDatasetLike } from "@lunora/bindings/analytics";`,
1494
+ `import { createAnalytics } from "@lunora/bindings/analytics";`
1087
1495
  ],
1088
1496
  stub: `
1089
1497
  const analyticsStub: AnalyticsClient = {
@@ -1112,7 +1520,7 @@ const emitImagesFragments = (hasImages) => {
1112
1520
  // ActionCtx-only: woven onto the action ctx object, never query/mutation.
1113
1521
  contextField: `
1114
1522
  images,`,
1115
- importLines: [`import type { Images, ImagesBindingLike } from "@lunora/images";`, `import { createImages } from "@lunora/images";`],
1523
+ importLines: [`import type { Images, ImagesBindingLike } from "@lunora/bindings/images";`, `import { createImages } from "@lunora/bindings/images";`],
1116
1524
  stub: `
1117
1525
  const imagesStub: Images = {
1118
1526
  info: async () => {
@@ -1211,10 +1619,10 @@ const emitR2sqlFragments = (hasR2sql) => {
1211
1619
  // ActionCtx-only: attached via the \`ctx.r2sql = r2sql\` assignment in the
1212
1620
  // \`isAction\` block, never the every-ctx object literal.
1213
1621
  contextField: "",
1214
- importLines: [`import type { R2SqlClient } from "@lunora/r2sql";`, `import { createR2Sql } from "@lunora/r2sql";`],
1622
+ importLines: [`import type { R2SqlClient } from "@lunora/bindings/r2sql";`, `import { createR2Sql } from "@lunora/bindings/r2sql";`],
1215
1623
  // The stub is typed `R2SqlClient`, so TS flags a missing method at build
1216
1624
  // time — but it must stay structurally in sync with that interface
1217
- // (`@lunora/r2sql` client.ts) when a method is added there.
1625
+ // (`@lunora/bindings/r2sql` client.ts) when a method is added there.
1218
1626
  stub: `
1219
1627
  const r2sqlStub: R2SqlClient = {
1220
1628
  describe: async () => {
@@ -1239,17 +1647,46 @@ const r2sqlStub: R2SqlClient = {
1239
1647
  `
1240
1648
  };
1241
1649
  };
1242
- const emitContainers = (containers) => {
1650
+ const emitPipelinesFragments = (hasPipelines) => {
1651
+ if (!hasPipelines) {
1652
+ return EMPTY_HELPER_FRAGMENTS;
1653
+ }
1654
+ 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().");`;
1655
+ return {
1656
+ build: `
1657
+ const pipelinesBinding = config.pipelines?.(env) ?? (env as Record<string, unknown>).PIPELINES;
1658
+ const pipelines: PipelineClient = pipelinesBinding ? createPipelines({ binding: pipelinesBinding as PipelineBindingLike }) : pipelinesStub;
1659
+ `,
1660
+ configField: `
1661
+ pipelines?: (env: Record<string, unknown>) => PipelineBindingLike;`,
1662
+ // ActionCtx-only: attached via the \`ctx.pipelines = pipelines\` assignment
1663
+ // in the \`isAction\` block, never the every-ctx object literal.
1664
+ contextField: "",
1665
+ importLines: [
1666
+ `import type { PipelineBindingLike, PipelineClient } from "@lunora/bindings/pipelines";`,
1667
+ `import { createPipelines } from "@lunora/bindings/pipelines";`
1668
+ ],
1669
+ stub: `
1670
+ const pipelinesStub: PipelineClient = {
1671
+ send: async () => {
1672
+ ${pipelinesMissing}
1673
+ },
1674
+ };
1675
+ `
1676
+ };
1677
+ };
1678
+ const emitContainers = (containers, jurisdiction) => {
1243
1679
  if (containers.length === 0) {
1244
1680
  return "";
1245
1681
  }
1682
+ const jurisdictionArgument = jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : "";
1246
1683
  const classes = containers.map((container) => {
1247
1684
  assertIdentifier(container.exportName, `container export "${container.exportName}"`);
1248
1685
  assertIdentifier(container.className, `container class "${container.className}"`);
1249
1686
  return `/** Container DO for the \`${container.exportName}\` definition (binding \`${container.bindingName}\`). */
1250
1687
  export class ${container.className} extends LunoraContainer {
1251
1688
  public constructor(ctx: ConstructorParameters<typeof LunoraContainer>[0], env: Record<string, unknown>) {
1252
- super(ctx, env, ${container.exportName}, "${container.exportName}");
1689
+ super(ctx, env, ${container.exportName}, "${container.exportName}"${jurisdictionArgument});
1253
1690
  }
1254
1691
  }
1255
1692
  `;
@@ -1261,14 +1698,21 @@ export class ${container.className} extends LunoraContainer {
1261
1698
  * requires each \`containers[].class_name\` to be exported by the worker:
1262
1699
  *
1263
1700
  * \`export * from "./lunora/_generated/containers.js";\`
1701
+ *
1702
+ * \`ContainerProxy\` is re-exported alongside them: the egress-interception path
1703
+ * (\`allowedHosts\`/\`deniedHosts\`/\`interceptHttps\` and the runtime
1704
+ * \`handle.egress\` controls) routes container outbound traffic through this
1705
+ * WorkerEntrypoint, so it too must be exported by the deployed worker.
1264
1706
  */
1265
- import LunoraContainer from "@lunora/container/do";
1707
+ import { LunoraContainer } from "@lunora/container/do";
1266
1708
 
1267
1709
  import { ${imports} } from "../containers.js";
1268
1710
 
1711
+ export { ContainerProxy } from "@lunora/container/do";
1712
+
1269
1713
  ${classes}`;
1270
1714
  };
1271
- const emitContainerFragments = (containers) => {
1715
+ const emitContainerFragments = (containers, jurisdiction) => {
1272
1716
  if (containers.length === 0) {
1273
1717
  return { build: "", contextField: "", importLines: [], specs: "" };
1274
1718
  }
@@ -1281,8 +1725,11 @@ const emitContainerFragments = (containers) => {
1281
1725
  return ` { binding: "${container.bindingName}", exportName: "${container.exportName}"${maxInstances} },`;
1282
1726
  }).join("\n");
1283
1727
  return {
1728
+ // Schema `.jurisdiction("…")` pins every container DO this shard reaches
1729
+ // to the data-residency region; the arg is omitted when undeclared so
1730
+ // existing generated output is unchanged.
1284
1731
  build: `
1285
- const containers = createContainerContext(env, LUNORA_CONTAINERS);
1732
+ const containers = createContainerContext(env, LUNORA_CONTAINERS${jurisdiction ? `, ${JSON.stringify(jurisdiction)}` : ""});
1286
1733
  `,
1287
1734
  contextField: `
1288
1735
  containers,`,
@@ -1335,6 +1782,32 @@ type WorkflowOutputOf<Definition> = Definition extends { __output?: infer Output
1335
1782
 
1336
1783
  ${classes}`;
1337
1784
  };
1785
+ const emitQueues = (queues) => {
1786
+ const pushQueues = queues.filter((queue) => queue.mode === "push");
1787
+ if (pushQueues.length === 0) {
1788
+ return "";
1789
+ }
1790
+ for (const queue of pushQueues) {
1791
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
1792
+ }
1793
+ const imports = pushQueues.map((queue) => queue.exportName).join(", ");
1794
+ const entries = pushQueues.map((queue) => ` ${JSON.stringify(queue.name)}: { definition: ${queue.exportName}, exportName: ${JSON.stringify(queue.exportName)} },`).join("\n");
1795
+ return `${GENERATED_HEADER}/**
1796
+ * Push-consumer registry for the queues declared in \`lunora/queues.ts\`. The
1797
+ * composed worker's \`queue(batch, env, ctx)\` entry routes each delivered batch
1798
+ * by \`batch.queue\` to the matching \`defineQueue\` handler. Wired automatically
1799
+ * by \`defineApp\` — you don't import this directly.
1800
+ */
1801
+ import type { QueueRegistry } from "@lunora/queue";
1802
+
1803
+ import { ${imports} } from "../queues.js";
1804
+
1805
+ /** Stable wrangler queue name → { definition, exportName } for batch routing. */
1806
+ export const LUNORA_QUEUE_REGISTRY: QueueRegistry = {
1807
+ ${entries}
1808
+ };
1809
+ `;
1810
+ };
1338
1811
  const emitWorkflowFragments = (workflows) => {
1339
1812
  if (workflows.length === 0) {
1340
1813
  return { build: "", contextField: "", importLines: [], specs: "" };
@@ -1360,6 +1833,31 @@ ${specEntries}
1360
1833
  `
1361
1834
  };
1362
1835
  };
1836
+ const emitQueueFragments = (queues) => {
1837
+ if (queues.length === 0) {
1838
+ return { build: "", contextField: "", importLines: [], specs: "" };
1839
+ }
1840
+ for (const queue of queues) {
1841
+ assertIdentifier(queue.exportName, `queue export "${queue.exportName}"`);
1842
+ assertIdentifier(queue.bindingName, `queue binding "${queue.bindingName}"`);
1843
+ }
1844
+ const specEntries = queues.map((queue) => ` { binding: "${queue.bindingName}", exportName: "${queue.exportName}", name: ${JSON.stringify(queue.name)} },`).join("\n");
1845
+ return {
1846
+ build: `
1847
+ const queues = createQueueContext(env, LUNORA_QUEUES);
1848
+ `,
1849
+ contextField: `
1850
+ queues,`,
1851
+ importLines: [`import type { QueueBindingSpec } from "@lunora/queue";`, `import { createQueueContext } from "@lunora/queue";`],
1852
+ // eslint-disable-next-line no-secrets/no-secrets -- the emitted readonly-array type annotation is dense generated TS, not a credential
1853
+ specs: `
1854
+ /** Wiring specs for \`ctx.queues\` (codegen-derived from \`lunora/queues.ts\`). */
1855
+ const LUNORA_QUEUES: ReadonlyArray<QueueBindingSpec> = [
1856
+ ${specEntries}
1857
+ ];
1858
+ `
1859
+ };
1860
+ };
1363
1861
  const emitWorkflowsMetadataFragments = (workflows) => {
1364
1862
  if (workflows.length === 0) {
1365
1863
  return { constant: "", override: "" };
@@ -1386,6 +1884,33 @@ const LUNORA_WORKFLOWS_INFO: WorkflowsResult = ${JSON.stringify(metadata, void 0
1386
1884
  `
1387
1885
  };
1388
1886
  };
1887
+ const emitQueuesMetadataFragments = (queues) => {
1888
+ if (queues.length === 0) {
1889
+ return { constant: "", override: "" };
1890
+ }
1891
+ const metadata = {
1892
+ queues: queues.map((queue) => {
1893
+ return {
1894
+ binding: queue.bindingName,
1895
+ ...queue.tuning.deadLetterQueue === void 0 ? {} : { deadLetterQueue: queue.tuning.deadLetterQueue },
1896
+ exportName: queue.exportName,
1897
+ mode: queue.mode,
1898
+ name: queue.name
1899
+ };
1900
+ })
1901
+ };
1902
+ return {
1903
+ constant: `
1904
+ /** Read-only declared-queue metadata (discovered from \`lunora/queues.ts\`) served via \`__lunora_admin__:listQueues\` for the studio's queues view. */
1905
+ const LUNORA_QUEUES_INFO: QueuesResult = ${JSON.stringify(metadata, void 0, 4)};
1906
+ `,
1907
+ override: `
1908
+ protected override queuesMetadata(): QueuesResult {
1909
+ return LUNORA_QUEUES_INFO;
1910
+ }
1911
+ `
1912
+ };
1913
+ };
1389
1914
  const emitPaymentFragments = (hasPayments) => {
1390
1915
  if (!hasPayments) {
1391
1916
  return { build: "", configField: "", contextField: "", imports: [], stub: "" };
@@ -1441,13 +1966,15 @@ const paymentStub: LunoraPayment = {
1441
1966
  `
1442
1967
  };
1443
1968
  };
1444
- const buildDoTypeImports = (hasVectors, hasWorkflows) => [
1969
+ const buildDoTypeImports = (hasVectors, hasWorkflows, hasQueues, hasFlags) => [
1445
1970
  "AdvisoryFinding",
1446
1971
  "DatabaseWriterLike",
1447
1972
  "DataMigrationLike",
1973
+ ...hasFlags ? ["FlagsResult"] : [],
1448
1974
  "LogSink",
1449
1975
  "MaskPoliciesResult",
1450
1976
  "MigrationRunResult",
1977
+ ...hasQueues ? ["QueuesResult"] : [],
1451
1978
  "RunShardApplyCdcArgs",
1452
1979
  "RunShardMigrationArgs",
1453
1980
  "RlsPoliciesResult",
@@ -1469,36 +1996,50 @@ const buildDoTypeImports = (hasVectors, hasWorkflows) => [
1469
1996
  const emitShard = ({
1470
1997
  advisories = [],
1471
1998
  containers = [],
1999
+ flagKeys = [],
2000
+ hasAccessFacade = false,
1472
2001
  hasAi = false,
1473
2002
  hasAnalytics = false,
1474
2003
  hasBrowser = false,
2004
+ hasFlags = false,
1475
2005
  hasHyperdrive = false,
1476
2006
  hasImages = false,
1477
2007
  hasKv = false,
1478
2008
  hasPayments = false,
2009
+ hasPipelines = false,
1479
2010
  hasR2sql = false,
1480
2011
  maskMetadata,
2012
+ mutators = [],
2013
+ queues = [],
1481
2014
  rlsMetadata,
1482
2015
  schema,
2016
+ shapes = [],
1483
2017
  storageRules,
1484
2018
  studioFeatures,
1485
2019
  useUmbrella = false,
1486
2020
  workflows = []
1487
2021
  }) => {
1488
2022
  const base = baseSpecifiers(useUmbrella);
2023
+ const hasMutators = mutators.length > 0;
2024
+ const hasShapes = shapes.length > 0;
1489
2025
  const { build: aiBuild, configField: aiConfigField, contextField: aiContextField, stub: aiStub } = emitAiFragments(hasAi);
2026
+ const accessFragments = emitAccessFragments(hasAccessFacade);
1490
2027
  const kvFragments = emitKvFragments(hasKv);
2028
+ const flagsFragments = emitFlagsFragments(hasFlags, base.flags);
2029
+ const flagsOverrides = emitFlagsOverrides(flagKeys, hasFlags, base.flags);
1491
2030
  const analyticsFragments = emitAnalyticsFragments(hasAnalytics);
1492
2031
  const imagesFragments = emitImagesFragments(hasImages);
1493
2032
  const hyperdriveFragments = emitHyperdriveFragments(hasHyperdrive);
1494
2033
  const browserFragments = emitBrowserFragments(hasBrowser);
1495
2034
  const r2sqlFragments = emitR2sqlFragments(hasR2sql);
2035
+ const pipelinesFragments = emitPipelinesFragments(hasPipelines);
2036
+ const { build: queuesBuild, contextField: queuesContextField, importLines: queueImportLines, specs: queueSpecs } = emitQueueFragments(queues);
1496
2037
  const {
1497
2038
  build: containersBuild,
1498
2039
  contextField: containersContextField,
1499
2040
  importLines: containerImportLines,
1500
2041
  specs: containerSpecs
1501
- } = emitContainerFragments(containers);
2042
+ } = emitContainerFragments(containers, schema.jurisdiction);
1502
2043
  const {
1503
2044
  build: workflowsBuild,
1504
2045
  contextField: workflowsContextField,
@@ -1517,20 +2058,29 @@ const emitShard = ({
1517
2058
  const maskData = maskMetadata ?? { columns: [] };
1518
2059
  const storageRulesData = storageRules ?? { rules: [] };
1519
2060
  const studioFeaturesData = studioFeatures ?? {
2061
+ analytics: false,
2062
+ auth: false,
2063
+ containers: false,
2064
+ flags: false,
2065
+ kv: false,
1520
2066
  mail: false,
1521
2067
  payments: false,
2068
+ queues: false,
1522
2069
  scheduler: false,
1523
2070
  storage: false,
1524
2071
  vectors: false,
1525
2072
  workflows: false
1526
2073
  };
1527
2074
  const { constant: workflowsMetadataConst, override: workflowsMetadataOverride } = emitWorkflowsMetadataFragments(workflows);
2075
+ const { constant: queuesMetadataConst, override: queuesMetadataOverride } = emitQueuesMetadataFragments(queues);
1528
2076
  const hasVectors = schema.vectorIndexes.length > 0;
1529
2077
  const hasGlobalTables = schema.tables.some((table) => table.shardMode === "global");
1530
2078
  const hasHyperdriveGlobal = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend === "hyperdrive");
1531
2079
  const hasD1Global = schema.tables.some((table) => table.shardMode === "global" && table.globalBackend !== "hyperdrive");
2080
+ const hasSourcedTables = schema.tables.some((table) => table.externalSource !== void 0);
1532
2081
  if (hasD1Global && hasHyperdriveGlobal) {
1533
- throw new Error(
2082
+ throw new LunoraError(
2083
+ "INTERNAL",
1534
2084
  'lunora codegen: mixing `.global()` (D1) and `.global({ backend: "hyperdrive" })` tables in one app is not supported yet — use a single global backend.'
1535
2085
  );
1536
2086
  }
@@ -1539,41 +2089,113 @@ const emitShard = ({
1539
2089
  const tableIndexes = buildTableIndexes(schema);
1540
2090
  const tableColumns = buildTableColumns(schema);
1541
2091
  const storageColumns = buildStorageColumns(schema);
1542
- const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0);
2092
+ const doTypeImports = buildDoTypeImports(hasVectors, workflows.length > 0, queues.length > 0, hasFlags);
2093
+ if (hasShapes) {
2094
+ doTypeImports.push("WhereInput");
2095
+ }
1543
2096
  const relationFanout = emitRelationFanout(hasGlobalTables);
2097
+ const shapeResolveOverride = hasShapes ? `
2098
+ 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 {
2099
+ const shape = LUNORA_SHAPES[name];
2100
+
2101
+ if (!shape) {
2102
+ return undefined;
2103
+ }
2104
+
2105
+ this.ensureMigrated();
2106
+
2107
+ // Trusted ctx from the socket's OWN verified identity — the client
2108
+ // supplies only the shape name + args; \`compileWhere\` validates the
2109
+ // args then runs the shape's \`where(ctx, args)\` under that identity,
2110
+ // so which rows replicate is a server decision (reads-as-permissions).
2111
+ const ctx = this.buildCtx({ functionPath: \`__shape__:\${name}\`, identity });
2112
+ const shapeWhere = shape.compileWhere(ctx, args) as unknown as WhereInput;
2113
+
2114
+ // AND-compose with the table's RLS read base-where. A shape runs no
2115
+ // procedure, so the \`.use(rls(...))\` middleware never fires; without
2116
+ // this merge its reads would bypass every read policy on the table
2117
+ // (rows the caller can't see would replicate). \`composeShapeReadWhere\`
2118
+ // evaluates the table's read policies under this same trusted ctx —
2119
+ // exactly the request-time path — and fails closed under a
2120
+ // \`.rls("required")\` schema for a non-\`.public()\`, policy-less table.
2121
+ const effectiveWhere = composeShapeReadWhere(LUNORA_RLS_READ_REGISTRY, {
2122
+ ctx,
2123
+ identity: identity?.identity ?? null,
2124
+ rlsRequired: (schema as unknown as { rlsMode?: string }).rlsMode === "required",
2125
+ roles: (ctx as { auth?: { roles?: readonly string[] } }).auth?.roles ?? [],
2126
+ shapeWhere,
2127
+ table: shape.table,
2128
+ tablePublic: (schema as unknown as { tables: Record<string, { isPublic?: boolean }> }).tables[shape.table]?.isPublic === true,
2129
+ userId: identity?.userId ?? null,
2130
+ });
2131
+
2132
+ // A live shape pokes only from its OWN shard's op-log, so a \`where()\`
2133
+ // that joins to a \`.shardBy()\` table (rows in another DO) is rejected
2134
+ // here — the first point the compiled predicate + shard modes are both
2135
+ // known. Remedy: denormalize, or move the joined table to \`.global()\`.
2136
+ assertShapeShardable(effectiveWhere, schema as unknown as SchemaLike, shape.table);
2137
+
2138
+ // A \`.global()\` table lives in D1 (no per-DO op-log): flag it so the
2139
+ // base serves it through the latency-tiered poll path (\`readGlobalShapeRows\`)
2140
+ // instead of the CDC poke path.
2141
+ const isGlobal = (schema as unknown as SchemaLike).tables[shape.table]?.shardMode?.kind === "global";
2142
+
2143
+ return { columns: shape.columns, effectiveWhere, global: isGlobal, table: shape.table };
2144
+ }
2145
+ ` : "";
2146
+ const customMutatorOverride = hasMutators ? `
2147
+ protected override isCustomMutator(functionPath: string): boolean {
2148
+ return LUNORA_MUTATOR_PATHS.has(functionPath);
2149
+ }
2150
+ ` : "";
2151
+ const shapeReadRegistryConst = hasShapes ? `
2152
+ /** 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. */
2153
+ const LUNORA_RLS_READ_REGISTRY = buildRlsReadRegistry(Object.values(LUNORA_FUNCTIONS));
2154
+ ` : "";
2155
+ const shapeGuardImport = hasShapes ? "assertShapeShardable, " : "";
1544
2156
  const importLines = [
1545
2157
  `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
2158
+ `import { applyCdcChanges, ${shapeGuardImport}createShardCtxDb, ${hasSourcedTables ? "isSourceDue, pullExternalSourceTick, " : ""}runDataMigration, runShardMigrations, ${relationFanout.importFragment}ShardDO as ShardDOBase } from "${base.do}";`,
2159
+ ...hasSourcedTables ? [`import type { ExternalSourceLike, SourceClientLike } from "${base.do}";`] : [],
2160
+ // `asBucketStorage` (the bucket-aware `ctx.storage` wrapper) and
2161
+ // `createSecrets` (the `ctx.secrets` core built-in) live in
1548
2162
  // `@lunora/server`, the single source — imported here rather than stamped
1549
- // inline into every generated shard.
1550
- `import { asBucketStorage } from "${base.server}";`
2163
+ // inline into every generated shard. With shapes, also pull the RLS
2164
+ // read-registry builder + `composeShapeReadWhere` so `resolveShape` can
2165
+ // AND-merge a shape's predicate with the table's read base-where.
2166
+ hasShapes ? `import { asBucketStorage, buildRlsReadRegistry, composeShapeReadWhere, createSecrets } from "${base.server}";` : `import { asBucketStorage, createSecrets } from "${base.server}";`
1551
2167
  ];
1552
2168
  if (hasTables) {
1553
2169
  importLines.push(`import { bindOrm, bindTableFacade } from "${base.server}";`);
1554
2170
  }
1555
2171
  if (hasVectors) {
1556
2172
  importLines.push(
1557
- `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/vectors";`,
1558
- `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/vectors";`
2173
+ `import type { SchemaLike as VectorSchemaLike, VectorizeIndexLike, VectorSearchLike } from "@lunora/bindings/vectors";`,
2174
+ `import { createContextVectors, createVectors, createVectorSyncHook } from "@lunora/bindings/vectors";`
1559
2175
  );
1560
2176
  }
1561
2177
  if (hasAi) {
1562
2178
  importLines.push(`import type { AiBindingLike, LunoraAi } from "@lunora/ai";`, `import { createAi } from "@lunora/ai";`);
1563
2179
  }
1564
2180
  importLines.push(
2181
+ ...accessFragments.importLines,
1565
2182
  ...kvFragments.importLines,
2183
+ ...flagsFragments.importLines,
1566
2184
  ...analyticsFragments.importLines,
1567
2185
  ...imagesFragments.importLines,
1568
2186
  ...hyperdriveFragments.importLines,
1569
2187
  ...browserFragments.importLines,
1570
2188
  ...r2sqlFragments.importLines,
2189
+ ...pipelinesFragments.importLines,
1571
2190
  ...containerImportLines,
1572
2191
  ...workflowImportLines,
2192
+ ...queueImportLines,
1573
2193
  ...paymentsImports,
1574
2194
  ``,
1575
2195
  `import schema from "../schema.js";`,
1576
- `import { LUNORA_FUNCTIONS, LUNORA_LIFECYCLE_HOOKS, LUNORA_MIGRATIONS } from "./functions.js";`
2196
+ // Local-first sync registries are pulled in alongside the function table
2197
+ // only when the project declares them, so the import list stays minimal.
2198
+ `import { ${["LUNORA_FUNCTIONS", "LUNORA_LIFECYCLE_HOOKS", "LUNORA_MIGRATIONS", ...hasMutators ? ["LUNORA_MUTATOR_PATHS"] : [], ...hasShapes ? ["LUNORA_SHAPES"] : []].join(", ")} } from "./functions.js";`
1577
2199
  );
1578
2200
  const vectorsConfigField = hasVectors ? `
1579
2201
  vectors?: (env: Record<string, unknown>) => Record<string, VectorizeIndexLike>;` : "";
@@ -1581,6 +2203,8 @@ const emitShard = ({
1581
2203
  d1?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
1582
2204
  const hyperdriveGlobalConfigField = hasHyperdriveGlobal ? `
1583
2205
  hyperdriveGlobal?: (env: Record<string, unknown>, request?: { identity?: Record<string, unknown>; userId?: string }) => DatabaseWriterLike | undefined;` : "";
2206
+ const sourceClientConfigField = hasSourcedTables ? `
2207
+ sourceClient?: (env: Record<string, unknown>, binding: string) => { query: <Row = Record<string, unknown>>(text: string, params?: readonly unknown[]) => Promise<Row[]> } | undefined;` : "";
1584
2208
  const globalDatabaseStub = hasGlobalTables ? `
1585
2209
  const globalDbStub: DatabaseWriterLike = {
1586
2210
  aggregate: async () => {
@@ -1698,6 +2322,144 @@ const vectorsStub: VectorSearchLike = {
1698
2322
  const bindTableHelper = "";
1699
2323
  const globalDatabaseThunk = hasHyperdriveGlobal ? "config.hyperdriveGlobal" : "config.d1";
1700
2324
  const globalDatabaseLine = hasGlobalTables ? ` const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, { identity, userId }) ?? globalDbStub;
2325
+ ` : "";
2326
+ const globalShapeReaderOverride = hasShapes && hasGlobalTables ? `
2327
+ 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 }>> {
2328
+ const env = this.env as Record<string, unknown>;
2329
+ const globalDb: DatabaseWriterLike = ${globalDatabaseThunk}?.(env, identity) ?? globalDbStub;
2330
+ const rows: Array<{ doc: Record<string, unknown>; id: string }> = [];
2331
+
2332
+ let cursor: null | string = null;
2333
+
2334
+ // Drain every page of the global membership so the seed/diff sees the
2335
+ // full rowset (D1 \`findMany\` is paginated). Stop one row past the cap:
2336
+ // a broad \`.global()\` shape would otherwise materialize an unbounded
2337
+ // array before the caller's \`withinGlobalShapeBound\` check rejects it,
2338
+ // so bail early and let the caller fail it closed.
2339
+ do {
2340
+ // eslint-disable-next-line no-await-in-loop -- sequential page drain to assemble the full membership
2341
+ const page = await globalDb.findMany(resolved.table, { cursor, where: resolved.effectiveWhere });
2342
+
2343
+ for (const doc of page.page) {
2344
+ rows.push({ doc, id: String((doc as { _id?: unknown })._id) });
2345
+
2346
+ if (rows.length > ShardDOBase.GLOBAL_SHAPE_MAX_ROWS) {
2347
+ return rows;
2348
+ }
2349
+ }
2350
+
2351
+ cursor = page.isDone ? null : page.continueCursor;
2352
+ } while (cursor !== null);
2353
+
2354
+ return rows;
2355
+ }
2356
+ ` : "";
2357
+ const sourceClientCacheConst = hasSourcedTables ? `
2358
+ const sourceClientCache = new WeakMap<object, Map<string, SourceClientLike>>();
2359
+ const sourcePollAtCache = new WeakMap<object, Map<string, number>>();
2360
+ ` : "";
2361
+ const externalSourceOverride = hasSourcedTables ? `
2362
+ protected override async pollExternalSources(): Promise<number> {
2363
+ const env = (this.env ?? {}) as Record<string, unknown>;
2364
+ const sourced = Object.entries((schema as unknown as SchemaLike).tables)
2365
+ .map(([table, definition]) => [table, (definition as { externalSource?: ExternalSourceLike }).externalSource] as const)
2366
+ .filter((entry): entry is [string, ExternalSourceLike] => entry[1] !== undefined);
2367
+
2368
+ if (sourced.length === 0) {
2369
+ return 0;
2370
+ }
2371
+
2372
+ const shardKey = this.currentShardKey();
2373
+ const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2374
+ const writer = createShardCtxDb({
2375
+ broadcast: (delta) => {
2376
+ this.recordChangedTable(delta.table);
2377
+ },
2378
+ cdc: config.cdc ?? false,
2379
+ scheduler,
2380
+ schema: schema as unknown as SchemaLike,
2381
+ sql: this.sql as SqlExec,
2382
+ });
2383
+
2384
+ let clients = sourceClientCache.get(this);
2385
+
2386
+ if (clients === undefined) {
2387
+ clients = new Map();
2388
+ sourceClientCache.set(this, clients);
2389
+ }
2390
+
2391
+ let polledAt = sourcePollAtCache.get(this);
2392
+
2393
+ if (polledAt === undefined) {
2394
+ polledAt = new Map();
2395
+ sourcePollAtCache.set(this, polledAt);
2396
+ }
2397
+
2398
+ const now = Date.now();
2399
+ // \`active\` counts non-manual sources: while > 0 the alarm re-arms; a
2400
+ // manual-only schema returns 0 so the shared alarm goes idle.
2401
+ let active = 0;
2402
+
2403
+ for (const [table, source] of sourced) {
2404
+ if (source.refresh === "manual") {
2405
+ continue;
2406
+ }
2407
+
2408
+ active += 1;
2409
+
2410
+ if (!isSourceDue(source.refresh, polledAt.get(table), now)) {
2411
+ continue;
2412
+ }
2413
+
2414
+ try {
2415
+ let client = clients.get(source.binding);
2416
+
2417
+ if (client === undefined) {
2418
+ client = config.sourceClient?.(env, source.binding);
2419
+
2420
+ if (client !== undefined) {
2421
+ clients.set(source.binding, client);
2422
+ }
2423
+ }
2424
+
2425
+ if (client === undefined) {
2426
+ // No SqlClient resolved for this binding (host never wired
2427
+ // \`config.sourceClient\`, or wired it wrong). Surface it in the
2428
+ // Logs panel and stamp \`polledAt\` so a persistent misconfig backs
2429
+ // off to \`refresh.everyMs\` instead of retrying every alarm tick.
2430
+ this.recordExternalSourceError(table, new Error(\`external-source: no sourceClient resolved for binding "\${source.binding}"\`));
2431
+ polledAt.set(table, now);
2432
+ continue;
2433
+ }
2434
+
2435
+ // 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
2436
+ await pullExternalSourceTick(this.sql as SqlExec, writer, client, table, source, shardKey);
2437
+ polledAt.set(table, now);
2438
+ } catch (error) {
2439
+ this.recordExternalSourceError(table, error);
2440
+ // Stamp on failure too, so a persistently failing source throttles
2441
+ // to \`refresh.everyMs\` rather than being hammered every tick.
2442
+ polledAt.set(table, now);
2443
+ }
2444
+ }
2445
+
2446
+ return active;
2447
+ }
2448
+ ` : "";
2449
+ const sourceConstructorOverride = hasSourcedTables ? `
2450
+ public constructor(state: ShardDOState, env: unknown) {
2451
+ super(state, env);
2452
+
2453
+ const autoSourced = Object.values((schema as unknown as SchemaLike).tables).some((definition) => {
2454
+ const source = (definition as { externalSource?: ExternalSourceLike }).externalSource;
2455
+
2456
+ return source !== undefined && source.refresh !== "manual";
2457
+ });
2458
+
2459
+ if (autoSourced) {
2460
+ void this.scheduleSourcePoll();
2461
+ }
2462
+ }
1701
2463
  ` : "";
1702
2464
  const facadeBlock = hasTables ? `
1703
2465
  const facade = db as unknown as Record<string, ReturnType<typeof bindTableFacade>>;
@@ -1711,18 +2473,23 @@ ${schema.tables.map(
1711
2473
  (table) => ` facade[${JSON.stringify(table.name)}] = bindTableFacade(db, ${JSON.stringify(table.name)});`
1712
2474
  ).join("\n")}
1713
2475
  ` : "";
1714
- const everyContextBuild = `${kvFragments.build}${analyticsFragments.build}`;
1715
- const everyContextField = `${kvFragments.contextField}${analyticsFragments.contextField}`;
1716
- const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql;
2476
+ const secretsBuild = `
2477
+ const secrets = createSecrets(env);
2478
+ `;
2479
+ const everyContextBuild = `${accessFragments.build}${kvFragments.build}${flagsFragments.build}${analyticsFragments.build}${secretsBuild}`;
2480
+ const everyContextField = `${accessFragments.contextField}${kvFragments.contextField}${flagsFragments.contextField}${analyticsFragments.contextField}
2481
+ secrets,`;
2482
+ const actionOnlyHasAny = hasImages || hasHyperdrive || hasBrowser || hasR2sql || hasPipelines;
1717
2483
  const actionOnlyBlock = actionOnlyHasAny ? `
1718
2484
  // ActionCtx-only helpers (external, non-deterministic I/O): constructed
1719
2485
  // and attached only for an \`action\` so query/mutation ctx never carry them.
1720
2486
  if (isAction) {
1721
- ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${[
2487
+ ${imagesFragments.build}${hyperdriveFragments.build}${browserFragments.build}${r2sqlFragments.build}${pipelinesFragments.build}${[
1722
2488
  ...hasImages ? [" ctx.images = images;"] : [],
1723
2489
  ...hasHyperdrive ? [" ctx.sql = sql;"] : [],
1724
2490
  ...hasBrowser ? [" ctx.browser = browser;"] : [],
1725
- ...hasR2sql ? [" ctx.r2sql = r2sql;"] : []
2491
+ ...hasR2sql ? [" ctx.r2sql = r2sql;"] : [],
2492
+ ...hasPipelines ? [" ctx.pipelines = pipelines;"] : []
1726
2493
  ].join("\n")}
1727
2494
  }
1728
2495
  ` : "";
@@ -1762,14 +2529,14 @@ const LUNORA_STORAGE_RULES: StorageRulesResult = ${JSON.stringify(storageRulesDa
1762
2529
 
1763
2530
  /** 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
2531
  const LUNORA_STUDIO_FEATURES: StudioFeaturesResult = ${JSON.stringify(studioFeaturesData, void 0, 4)};
1765
- ${workflowsMetadataConst}${containerSpecs}${workflowSpecs}
2532
+ ${flagsOverrides.constant}${shapeReadRegistryConst}${workflowsMetadataConst}${queuesMetadataConst}${containerSpecs}${workflowSpecs}${queueSpecs}
1766
2533
  export interface ShardDOConfig {
1767
2534
  /** Opt into change-data-capture: records a post-image to \`__cdc_log\` on every write (backs streaming export + replay-PITR). */
1768
2535
  cdc?: boolean;
1769
2536
  /** 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
2537
  observability?: (env: Record<string, unknown>) => LogSink | undefined;
1771
2538
  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}
2539
+ 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}
1773
2540
  }
1774
2541
 
1775
2542
  const schedulerStub = {
@@ -1807,7 +2574,7 @@ const storageStub = {
1807
2574
  throw new Error("ctx.storage: no storage configured. Pass \`storage\` to createShardDO().");
1808
2575
  },
1809
2576
  };
1810
- ${globalDatabaseStub}${vectorsStub}${aiStub}${kvFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${paymentStub}${bindTableHelper}
2577
+ ${globalDatabaseStub}${sourceClientCacheConst}${vectorsStub}${aiStub}${kvFragments.stub}${flagsFragments.stub}${analyticsFragments.stub}${imagesFragments.stub}${hyperdriveFragments.stub}${browserFragments.stub}${r2sqlFragments.stub}${pipelinesFragments.stub}${paymentStub}${bindTableHelper}
1811
2578
  // Bound in-process \`ctx.run*\` composition depth so a self- or cyclically-
1812
2579
  // referencing call fails loudly with a clear error instead of overflowing the
1813
2580
  // stack. Tracked across the awaited handler chain (one DO invocation is
@@ -1848,7 +2615,7 @@ const dispatchRun = async (expected: FunctionKind, functionPath: string, args: R
1848
2615
  * from the worker entry so wrangler binds it by name.
1849
2616
  */
1850
2617
  export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOState, env: unknown) => ShardDOBase =>
1851
- class extends ShardDOBase {
2618
+ class extends ShardDOBase {${sourceConstructorOverride}
1852
2619
  private migrated = false;
1853
2620
 
1854
2621
  public override async handleRpc(functionPath: string, args: Record<string, unknown>): Promise<unknown> {
@@ -1877,8 +2644,20 @@ export const createShardDO = (config: ShardDOConfig = {}): new (state: ShardDOSt
1877
2644
  // do external I/O that can't be rolled back, so both dispatch directly.
1878
2645
  // \`ctx.run*\` composition runs inside this span (it never re-enters
1879
2646
  // handleRpc); runInTransaction's own guard rejects accidental nesting.
2647
+ //
2648
+ // The replay bookkeeping (idempotency dedup row + custom-mutator
2649
+ // watermark advance) commits INSIDE this span via
2650
+ // \`commitMutationBookkeeping\`, so the writes, the dedup row, and the
2651
+ // watermark are atomic — a crash can't leave the writes durable without
2652
+ // the replay guard.
1880
2653
  if (registered.kind === "mutation") {
1881
- return this.runInTransaction(() => registered.handler(ctx, args));
2654
+ return this.runInTransaction(async () => {
2655
+ const result = await registered.handler(ctx, args);
2656
+
2657
+ this.commitMutationBookkeeping(result);
2658
+
2659
+ return result;
2660
+ });
1882
2661
  }
1883
2662
 
1884
2663
  return registered.handler(ctx, args);
@@ -1916,7 +2695,7 @@ ${relationFanout.override}
1916
2695
  iterator: (signal) => (registered.handler as (context: unknown, args: Record<string, unknown>, signal: AbortSignal) => AsyncIterable<unknown>)(this.buildCtx({ functionPath }), args, signal),
1917
2696
  };
1918
2697
  }
1919
-
2698
+ ${customMutatorOverride}${shapeResolveOverride}${globalShapeReaderOverride}${externalSourceOverride}
1920
2699
  protected override lifecycleHookPaths(event: "connect" | "disconnect"): readonly string[] {
1921
2700
  return LUNORA_LIFECYCLE_HOOKS[event];
1922
2701
  }
@@ -1952,7 +2731,7 @@ ${relationFanout.override}
1952
2731
  protected override studioFeatures(): StudioFeaturesResult {
1953
2732
  return LUNORA_STUDIO_FEATURES;
1954
2733
  }
1955
- ${workflowsMetadataOverride}
2734
+ ${flagsOverrides.evaluateOverride}${flagsOverrides.subscriptionOverride}${workflowsMetadataOverride}${queuesMetadataOverride}
1956
2735
  protected override advisories(): AdvisoryFinding[] {
1957
2736
  return LUNORA_ADVISORIES;
1958
2737
  }
@@ -2186,7 +2965,7 @@ ${workflowsMetadataOverride}
2186
2965
  // dispatch path) fall back to the per-request fields as before.
2187
2966
  const userId = options.identity ? options.identity.userId : this.getCurrentUserId();
2188
2967
  const identity = options.identity ? options.identity.identity : this.getCurrentIdentity();
2189
- ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}
2968
+ ${vectorsBuild}${aiBuild}${everyContextBuild}${containersBuild}${workflowsBuild}${queuesBuild}
2190
2969
  const scheduler = (config.scheduler?.(env) ?? schedulerStub) as SchedulerLike;
2191
2970
  // Build the storage adapter once and share it between \`ctx.storage\`
2192
2971
  // and \`ctx.db.system._storage\` so both read the same R2 binding. The
@@ -2209,6 +2988,14 @@ ${facadeBlock}${paymentsBuild}
2209
2988
  warn: (...args: unknown[]) => { this.recordUserLog(logFunctionPath, "warn", args, observability); },
2210
2989
  };
2211
2990
 
2991
+ // \`ctx.now\`: the wall-clock instant (epoch ms) this function began,
2992
+ // captured ONCE so the whole handler body sees a single stable value.
2993
+ // Query/mutation handlers must be deterministic (they may be re-run on
2994
+ // OCC retry / subscription re-eval), so they must read time through
2995
+ // \`ctx.now\` instead of \`Date.now()\` — the \`nondeterministic_query_mutation\`
2996
+ // advisor flags the latter. Actions may still use ambient \`Date.now()\`.
2997
+ const now = Date.now();
2998
+
2212
2999
  const ctx: Record<string, unknown> = {
2213
3000
  auth: {
2214
3001
  getIdentity: async () => identity ?? null,
@@ -2217,9 +3004,10 @@ ${facadeBlock}${paymentsBuild}
2217
3004
  db,
2218
3005
  fetch: globalThis.fetch.bind(globalThis),
2219
3006
  ip: this.getCurrentIp(),
2220
- log,${ormContextField}
3007
+ log,
3008
+ now,${ormContextField}
2221
3009
  scheduler,
2222
- storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}
3010
+ storage,${vectorsContextField}${aiContextField}${everyContextField}${paymentsContextField}${containersContextField}${workflowsContextField}${queuesContextField}
2223
3011
  };
2224
3012
  ${isActionLine}${actionOnlyBlock}
2225
3013
  ctx.runAction = (reference: FunctionReference, fnArgs: Record<string, unknown>) => dispatchRun("action", reference.__lunoraRef, fnArgs, ctx);
@@ -2483,4 +3271,4 @@ const emitWranglerCronTriggers = (crons) => {
2483
3271
  return triggers;
2484
3272
  };
2485
3273
 
2486
- export { GENERATED_HEADER, buildStorageColumns, emitApi, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };
3274
+ export { GENERATED_HEADER, buildStorageColumns, emitApi, emitCollections, emitContainers, emitCrons, emitDataModel, emitDrizzleSchema, emitFunctions, emitQueues, emitSeed, emitServer, emitShard, emitVectors, emitWorkflows, emitWranglerCronTriggers };