@dragonmastery/tamer 0.30.0 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +2 -1
  2. package/dist/{apply-CWU3HY0P.mjs → apply-BjrYbyHn.mjs} +14 -16
  3. package/dist/{apply-CWU3HY0P.mjs.map → apply-BjrYbyHn.mjs.map} +1 -1
  4. package/dist/{applyTarget-D15T_q7G.mjs → applyTarget-Ce_mtRQX.mjs} +3 -3
  5. package/dist/{applyTarget-D15T_q7G.mjs.map → applyTarget-Ce_mtRQX.mjs.map} +1 -1
  6. package/dist/{bootstrap-BicPW44a.mjs → bootstrap-D__dHw1w.mjs} +6 -6
  7. package/dist/bootstrap-D__dHw1w.mjs.map +1 -0
  8. package/dist/{buildDispatchUploadForm-BoUB93b3.mjs → buildDispatchUploadForm-CVnPmHg4.mjs} +1 -1
  9. package/dist/{buildDispatchUploadForm-BoUB93b3.mjs.map → buildDispatchUploadForm-CVnPmHg4.mjs.map} +1 -1
  10. package/dist/{cloudflareSnapshot-GBUHeg2m.mjs → cloudflareSnapshot-C6cF8GG8.mjs} +5 -7
  11. package/dist/{cloudflareSnapshot-GBUHeg2m.mjs.map → cloudflareSnapshot-C6cF8GG8.mjs.map} +1 -1
  12. package/dist/{deploy-DAEjDjOm.mjs → deploy-C6fX9td0.mjs} +23 -11
  13. package/dist/deploy-C6fX9td0.mjs.map +1 -0
  14. package/dist/{destroy-tenant-B-VLKfc6.mjs → destroy-tenant-T_94ed9x.mjs} +2 -4
  15. package/dist/{destroy-tenant-B-VLKfc6.mjs.map → destroy-tenant-T_94ed9x.mjs.map} +1 -1
  16. package/dist/{destroy-DtgPD_bD.mjs → destroy-vfk2Zbfj.mjs} +11 -13
  17. package/dist/{destroy-DtgPD_bD.mjs.map → destroy-vfk2Zbfj.mjs.map} +1 -1
  18. package/dist/{dev-BYItpt9U.mjs → dev-BLthyLml.mjs} +8 -10
  19. package/dist/{dev-BYItpt9U.mjs.map → dev-BLthyLml.mjs.map} +1 -1
  20. package/dist/{dns-records.resolve-C2T0m4NG.mjs → dns-records.resolve-8a_eHfVI.mjs} +1 -1
  21. package/dist/{dns-records.resolve-DwBR_1WI.mjs → dns-records.resolve-BB2agPAb.mjs} +1 -1
  22. package/dist/{dns-records.resolve-DwBR_1WI.mjs.map → dns-records.resolve-BB2agPAb.mjs.map} +1 -1
  23. package/dist/{dns-records.sync-CfI1mqXv.mjs → dns-records.sync-DqYROe07.mjs} +3 -3
  24. package/dist/{dns-records.sync-CfI1mqXv.mjs.map → dns-records.sync-DqYROe07.mjs.map} +1 -1
  25. package/dist/{doctor-C_hs7k2D.mjs → doctor-32YLAXXl.mjs} +2 -2
  26. package/dist/{doctor-C_hs7k2D.mjs.map → doctor-32YLAXXl.mjs.map} +1 -1
  27. package/dist/drift-BCxWdYHG.mjs +8 -0
  28. package/dist/{drift-DncpkI2R.mjs → drift-CeemyFqL.mjs} +37 -9
  29. package/dist/drift-CeemyFqL.mjs.map +1 -0
  30. package/dist/{events-B6oCdvSt.mjs → events-otk0l3aJ.mjs} +2 -4
  31. package/dist/{events-B6oCdvSt.mjs.map → events-otk0l3aJ.mjs.map} +1 -1
  32. package/dist/{generator-h_VG0Q5f.mjs → generator-gvCy7ouY.mjs} +2 -2
  33. package/dist/{generator-h_VG0Q5f.mjs.map → generator-gvCy7ouY.mjs.map} +1 -1
  34. package/dist/{import-D8zaVvwK.mjs → import-OvohE-H2.mjs} +6 -8
  35. package/dist/{import-D8zaVvwK.mjs.map → import-OvohE-H2.mjs.map} +1 -1
  36. package/dist/index.d.mts +264 -26
  37. package/dist/index.d.mts.map +1 -1
  38. package/dist/{logpush-job-DsRkOORJ.mjs → logpush-job-DJPlpnRu.mjs} +2 -2
  39. package/dist/{logpush-job-DsRkOORJ.mjs.map → logpush-job-DJPlpnRu.mjs.map} +1 -1
  40. package/dist/{migrate-Bwl0w6XN.mjs → migrate-CroDjbJz.mjs} +6 -8
  41. package/dist/{migrate-Bwl0w6XN.mjs.map → migrate-CroDjbJz.mjs.map} +1 -1
  42. package/dist/normalize-DVSTRZhO.mjs.map +1 -1
  43. package/dist/{plan-BNIAD--f.mjs → plan-C2urqJOz.mjs} +39 -14
  44. package/dist/plan-C2urqJOz.mjs.map +1 -0
  45. package/dist/{planFormat-CJw8Kq2s.mjs → planFormat-5XMJK879.mjs} +1 -1
  46. package/dist/{planFormat-CJw8Kq2s.mjs.map → planFormat-5XMJK879.mjs.map} +1 -1
  47. package/dist/{provision-tenant-BcZocyyn.mjs → provision-tenant-BJ1KugON.mjs} +6 -8
  48. package/dist/{provision-tenant-BcZocyyn.mjs.map → provision-tenant-BJ1KugON.mjs.map} +1 -1
  49. package/dist/{r2S3EmptyBucket-DD81ZWQ7.mjs → r2S3EmptyBucket-B9_pHfvB.mjs} +1 -1
  50. package/dist/{r2S3EmptyBucket-DD81ZWQ7.mjs.map → r2S3EmptyBucket-B9_pHfvB.mjs.map} +1 -1
  51. package/dist/{fetchStackImports-ClUYZy_U.mjs → registry-EWWdkLf7.mjs} +5 -982
  52. package/dist/registry-EWWdkLf7.mjs.map +1 -0
  53. package/dist/secrets-CnzjvndT.mjs +3 -0
  54. package/dist/{stackOutputs-D33EmyfT.mjs → stackOutputs-Cltzl2g0.mjs} +2 -2
  55. package/dist/{stackOutputs-D33EmyfT.mjs.map → stackOutputs-Cltzl2g0.mjs.map} +1 -1
  56. package/dist/{status-BAPpi2Zt.mjs → status-DkkS5lc9.mjs} +7 -9
  57. package/dist/{status-BAPpi2Zt.mjs.map → status-DkkS5lc9.mjs.map} +1 -1
  58. package/dist/{sync-BdJ43vO7.mjs → sync-CpfxqlOx.mjs} +7 -9
  59. package/dist/{sync-BdJ43vO7.mjs.map → sync-CpfxqlOx.mjs.map} +1 -1
  60. package/dist/tamer.mjs +4423 -221
  61. package/dist/tamer.mjs.map +1 -1
  62. package/dist/{tamerArtifactsR2-Ccgplu2Q.mjs → tamerArtifactsR2-DnUJmxnO.mjs} +2 -2
  63. package/dist/{tamerArtifactsR2-Ccgplu2Q.mjs.map → tamerArtifactsR2-DnUJmxnO.mjs.map} +1 -1
  64. package/dist/{types-CN1BOr0U.mjs → types-BzzHwIdw.mjs} +6 -8
  65. package/dist/{types-CN1BOr0U.mjs.map → types-BzzHwIdw.mjs.map} +1 -1
  66. package/dist/{verifyPlanFile-BQ7GCDC2.mjs → verifyPlanFile-BmEadIqm.mjs} +2 -2
  67. package/dist/{verifyPlanFile-BQ7GCDC2.mjs.map → verifyPlanFile-BmEadIqm.mjs.map} +1 -1
  68. package/dist/{wfp-delete-BG9WBd7F.mjs → wfp-delete-CDBFqmrM.mjs} +2 -3
  69. package/dist/{wfp-delete-BG9WBd7F.mjs.map → wfp-delete-CDBFqmrM.mjs.map} +1 -1
  70. package/dist/{wfp-put-DjErqxFa.mjs → wfp-put-BrwICc9i.mjs} +3 -4
  71. package/dist/{wfp-put-DjErqxFa.mjs.map → wfp-put-BrwICc9i.mjs.map} +1 -1
  72. package/dist/{worker-route-DY1onr-h.mjs → worker-route-x8q3K4-z.mjs} +3 -4
  73. package/dist/{worker-route-DY1onr-h.mjs.map → worker-route-x8q3K4-z.mjs.map} +1 -1
  74. package/dist/{workers-DNKsZOq4.mjs → workers-D3Ekf3mF.mjs} +3 -4
  75. package/dist/{workers-DNKsZOq4.mjs.map → workers-D3Ekf3mF.mjs.map} +1 -1
  76. package/dist/{wranglerSpawn-DmEz0ldT.mjs → wranglerSpawn-CUlo2qOJ.mjs} +1 -1
  77. package/dist/{wranglerSpawn-DmEz0ldT.mjs.map → wranglerSpawn-CUlo2qOJ.mjs.map} +1 -1
  78. package/dist/{zoneResolver-VoxLHM4N.mjs → zoneResolver-DNNNmO_w.mjs} +1 -1
  79. package/dist/{zoneResolver-VoxLHM4N.mjs.map → zoneResolver-DNNNmO_w.mjs.map} +1 -1
  80. package/package.json +1 -1
  81. package/dist/CFApiClient-DhbyyV71.mjs +0 -868
  82. package/dist/CFApiClient-DhbyyV71.mjs.map +0 -1
  83. package/dist/StateManager-JLBtz9V-.mjs +0 -760
  84. package/dist/StateManager-JLBtz9V-.mjs.map +0 -1
  85. package/dist/bootstrap-BicPW44a.mjs.map +0 -1
  86. package/dist/deploy-DAEjDjOm.mjs.map +0 -1
  87. package/dist/drift-DRnwTyZD.mjs +0 -10
  88. package/dist/drift-DncpkI2R.mjs.map +0 -1
  89. package/dist/fetchStackImports-ClUYZy_U.mjs.map +0 -1
  90. package/dist/loader-DnT9iqz9.mjs +0 -531
  91. package/dist/loader-DnT9iqz9.mjs.map +0 -1
  92. package/dist/plan-BNIAD--f.mjs.map +0 -1
package/dist/tamer.mjs CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { c as TAMER_OVERLAY_ENV_KEY, f as getDispatchNamespaces, n as materializeTamerResolvable, r as materializeVars, t as materializeCloudflareBindings } from "./normalize-DVSTRZhO.mjs";
3
+ import { basename, dirname, resolve } from "path";
4
+ import { existsSync, readFileSync } from "fs";
5
+ import * as readline from "readline/promises";
6
+
2
7
  //#region node_modules/zod/v4/core/core.js
3
8
  /** A special constant with type `never` */
4
9
  const NEVER = Object.freeze({ status: "aborted" });
@@ -150,7 +155,7 @@ const allowsEval = cached(() => {
150
155
  return false;
151
156
  }
152
157
  });
153
- function isPlainObject(o) {
158
+ function isPlainObject$1(o) {
154
159
  if (isObject(o) === false) return false;
155
160
  const ctor = o.constructor;
156
161
  if (ctor === void 0) return true;
@@ -161,7 +166,7 @@ function isPlainObject(o) {
161
166
  return true;
162
167
  }
163
168
  function shallowClone(o) {
164
- if (isPlainObject(o)) return { ...o };
169
+ if (isPlainObject$1(o)) return { ...o };
165
170
  if (Array.isArray(o)) return [...o];
166
171
  return o;
167
172
  }
@@ -242,7 +247,7 @@ function omit(schema, mask) {
242
247
  }));
243
248
  }
244
249
  function extend(schema, shape) {
245
- if (!isPlainObject(shape)) throw new Error("Invalid input to extend: expected a plain object");
250
+ if (!isPlainObject$1(shape)) throw new Error("Invalid input to extend: expected a plain object");
246
251
  const checks = schema._zod.def.checks;
247
252
  if (checks && checks.length > 0) {
248
253
  const existingShape = schema._zod.def.shape;
@@ -258,7 +263,7 @@ function extend(schema, shape) {
258
263
  } }));
259
264
  }
260
265
  function safeExtend(schema, shape) {
261
- if (!isPlainObject(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
266
+ if (!isPlainObject$1(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
262
267
  return clone(schema, mergeDefs(schema._zod.def, { get shape() {
263
268
  const _shape = {
264
269
  ...schema._zod.def.shape,
@@ -1777,7 +1782,7 @@ function mergeValues(a, b) {
1777
1782
  valid: true,
1778
1783
  data: a
1779
1784
  };
1780
- if (isPlainObject(a) && isPlainObject(b)) {
1785
+ if (isPlainObject$1(a) && isPlainObject$1(b)) {
1781
1786
  const bKeys = Object.keys(b);
1782
1787
  const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
1783
1788
  const newObj = {
@@ -1853,7 +1858,7 @@ const $ZodRecord = /* @__PURE__ */ $constructor("$ZodRecord", (inst, def) => {
1853
1858
  $ZodType.init(inst, def);
1854
1859
  inst._zod.parse = (payload, ctx) => {
1855
1860
  const input = payload.value;
1856
- if (!isPlainObject(input)) {
1861
+ if (!isPlainObject$1(input)) {
1857
1862
  payload.issues.push({
1858
1863
  expected: "record",
1859
1864
  code: "invalid_type",
@@ -4050,207 +4055,4401 @@ function number(params) {
4050
4055
  }
4051
4056
 
4052
4057
  //#endregion
4053
- //#region src/cli/args.ts
4054
- const BaseArgsSchema = object({
4055
- env: string().optional(),
4056
- config: string().optional(),
4057
- worker: string().optional()
4058
- });
4059
- const ApplyArgsSchema = BaseArgsSchema.extend({
4060
- add_shard: string().optional(),
4061
- plan: string().optional(),
4062
- allow_stale: boolean().optional(),
4063
- rollback_on_failure: boolean().optional(),
4064
- target: string().optional()
4065
- });
4066
- const DestroyArgsSchema = BaseArgsSchema.extend({
4067
- env: string().min(1, { error: "env is required for destroy" }),
4068
- force: boolean().optional(),
4069
- skip_workers: boolean().optional(),
4070
- confirm_env: string().optional(),
4071
- wipe_metadata: boolean().optional(),
4072
- plan: string().optional(),
4073
- allow_stale: boolean().optional()
4074
- });
4075
- const BootstrapArgsSchema = object({
4076
- env: string().min(1, { error: "env is required for bootstrap" }),
4077
- config: string().optional()
4078
- });
4079
- const DriftArgsSchema = BaseArgsSchema.extend({ json: boolean().optional() });
4080
- const PlanArgsSchema = BaseArgsSchema.extend({
4081
- json: boolean().optional(),
4082
- detailed_exitcode: boolean().optional(),
4083
- out: string().optional(),
4084
- destroy: boolean().optional(),
4085
- target: string().optional()
4086
- });
4087
- const ImportArgsSchema = object({
4088
- env: string().min(1, { error: "env is required for import" }),
4089
- config: string().optional(),
4090
- kind: _enum([
4091
- "d1",
4092
- "r2",
4093
- "kv",
4094
- "queue",
4095
- "hyperdrive",
4096
- "vectorize",
4097
- "ai_gateway",
4098
- "pipeline",
4099
- "workflow",
4100
- "secret_store",
4101
- "dns_record",
4102
- "dispatch_namespace",
4103
- "worker_route"
4104
- ], { error: "kind must be one of d1 | r2 | kv | queue | hyperdrive | vectorize | ai_gateway | pipeline | workflow | secret_store | dns_record | dispatch_namespace | worker_route" }),
4105
- logical: string().min(1, { error: "logical is required" }),
4106
- cf_id: string().optional(),
4107
- shard_date: string().optional(),
4108
- created_date: string().optional(),
4109
- route_id: string().optional(),
4110
- zone_name: string().optional()
4111
- });
4112
- const StatusArgsSchema = BaseArgsSchema.extend({ tenant: string().optional() });
4113
- const EventsArgsSchema = BaseArgsSchema.extend({
4114
- json: boolean().optional(),
4115
- limit: number().int().min(1).max(100).optional()
4116
- });
4117
- const DoctorArgsSchema = object({ json: boolean().optional() });
4118
- const ProvisionTenantArgsSchema = object({
4119
- env: string().min(1, { error: "env is required" }),
4120
- product: string().min(1, { error: "product is required" }),
4121
- workspace: string().min(1, { error: "workspace is required" }),
4122
- main: string().optional(),
4123
- artifact_key: string().optional(),
4124
- module_name: string().optional(),
4125
- config: string().optional(),
4126
- compatibility_date: string().optional(),
4127
- compat_flags: string().optional(),
4128
- shards: string().optional(),
4129
- json: boolean().optional()
4130
- }).refine((d) => !!(d.main || d.artifact_key), { message: "Provide --main <file> or --artifact-key <r2-key> (under tamer-artifacts-{env})" });
4131
- const DestroyTenantArgsSchema = object({
4132
- env: string().min(1, { error: "env is required" }),
4133
- product: string().min(1, { error: "product is required" }),
4134
- workspace: string().min(1, { error: "workspace is required" }),
4135
- force: boolean().optional(),
4136
- confirm_tenant: string().optional(),
4137
- config: string().optional(),
4138
- json: boolean().optional()
4139
- });
4140
- const DeployArgsSchema = BaseArgsSchema.extend({ dispatch_namespace: string().optional() });
4141
- const DevArgsSchema = BaseArgsSchema.extend({ all: boolean().optional() });
4142
- function parseArgs(argv) {
4143
- const opts = {};
4144
- for (let i = 0; i < argv.length; i++) {
4145
- const arg = argv[i];
4146
- if (arg.startsWith("--")) {
4147
- const key = arg.slice(2).replace(/-/g, "_");
4148
- const next = argv[i + 1];
4149
- if (next && !next.startsWith("--")) {
4150
- opts[key] = next;
4151
- i++;
4152
- } else opts[key] = true;
4058
+ //#region src/core/config/merge-project-overlay.ts
4059
+ /**
4060
+ * Deep-merge `tamer/env/<env>.config.ts` onto `tamer/project.config.ts`.
4061
+ * See docs/design-tamer-project-config.md.
4062
+ */
4063
+ function isPlainObject(v) {
4064
+ return v !== null && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
4065
+ }
4066
+ function validateOverlayWorkers(projectWorkers, overlayWorkers) {
4067
+ for (const k of Object.keys(overlayWorkers)) if (!(k in projectWorkers)) throw new Error(`Tamer env overlay: unknown worker key "${k}". Declare "${k}" in tamer/project.config.ts first — overlays may only patch existing workers.`);
4068
+ }
4069
+ function mergeWorkerResources(base, patch) {
4070
+ const out = { ...base };
4071
+ for (const k of Object.keys(patch)) {
4072
+ const pk = patch[k];
4073
+ if (pk !== void 0) out[k] = pk;
4074
+ }
4075
+ return out;
4076
+ }
4077
+ function mergeEnvMap(base, patch) {
4078
+ const out = { ...base };
4079
+ for (const k of Object.keys(patch)) {
4080
+ const b = base[k];
4081
+ const p = patch[k];
4082
+ if (isPlainObject(b) && isPlainObject(p)) out[k] = mergeWorkerConfig(b, p);
4083
+ else out[k] = p;
4084
+ }
4085
+ return out;
4086
+ }
4087
+ function mergeWorkerConfig(base, patch) {
4088
+ const out = { ...base };
4089
+ for (const key of Object.keys(patch)) {
4090
+ const pv = patch[key];
4091
+ if (pv === void 0) continue;
4092
+ if (key === "vars") {
4093
+ const bv = isPlainObject(base.vars) ? base.vars : {};
4094
+ const ov = isPlainObject(patch.vars) ? patch.vars : {};
4095
+ out.vars = {
4096
+ ...bv,
4097
+ ...ov
4098
+ };
4099
+ continue;
4100
+ }
4101
+ if (key === "tamerRoutes") {
4102
+ out.tamerRoutes = pv;
4103
+ continue;
4104
+ }
4105
+ if (key === "tamerStaleRouteSweepZones") {
4106
+ out.tamerStaleRouteSweepZones = pv;
4107
+ continue;
4108
+ }
4109
+ if (key === "resources") {
4110
+ if (isPlainObject(base.resources) && isPlainObject(pv)) out.resources = mergeWorkerResources(base.resources, pv);
4111
+ else out.resources = pv;
4112
+ continue;
4113
+ }
4114
+ if (key === "env") {
4115
+ if (isPlainObject(base.env) && isPlainObject(pv)) out.env = mergeEnvMap(base.env, pv);
4116
+ else out.env = pv;
4117
+ continue;
4118
+ }
4119
+ if (key === "local") {
4120
+ if (isPlainObject(base.local) && isPlainObject(pv)) out.local = mergeWorkerConfig(base.local, pv);
4121
+ else out.local = pv;
4122
+ continue;
4153
4123
  }
4124
+ out[key] = pv;
4154
4125
  }
4155
- return opts;
4126
+ return out;
4127
+ }
4128
+ function mergeWorkersRecord(base, patch) {
4129
+ const out = { ...base };
4130
+ for (const k of Object.keys(patch)) {
4131
+ const b = base[k];
4132
+ const p = patch[k];
4133
+ if (isPlainObject(b) && isPlainObject(p)) out[k] = mergeWorkerConfig(b, p);
4134
+ else out[k] = p;
4135
+ }
4136
+ return out;
4156
4137
  }
4157
- function toOpts(raw) {
4158
- const opts = {};
4159
- for (const [k, v] of Object.entries(raw)) opts[k] = v;
4160
- return opts;
4138
+ /**
4139
+ * Merge env overlay onto project config (both plain objects from `default` exports).
4140
+ * Does not run {@link materializeCloudflareBindings} caller does that on the result.
4141
+ */
4142
+ function mergeProjectOverlay(project, overlay) {
4143
+ const out = { ...project };
4144
+ for (const key of Object.keys(overlay)) {
4145
+ const ov = overlay[key];
4146
+ if (ov === void 0) continue;
4147
+ const pv = project[key];
4148
+ switch (key) {
4149
+ case "tenant":
4150
+ if (isPlainObject(pv) && isPlainObject(ov)) out[key] = {
4151
+ ...pv,
4152
+ ...ov
4153
+ };
4154
+ else out[key] = ov;
4155
+ break;
4156
+ case "naming":
4157
+ out[key] = ov;
4158
+ break;
4159
+ case "workers":
4160
+ if (!isPlainObject(pv) || !isPlainObject(ov)) {
4161
+ out[key] = ov;
4162
+ break;
4163
+ }
4164
+ validateOverlayWorkers(pv, ov);
4165
+ out[key] = mergeWorkersRecord(pv, ov);
4166
+ break;
4167
+ case "worker":
4168
+ if (isPlainObject(pv) && isPlainObject(ov)) out[key] = mergeWorkerConfig(pv, ov);
4169
+ else out[key] = ov;
4170
+ break;
4171
+ case "outputs":
4172
+ if (isPlainObject(pv) && isPlainObject(ov)) out[key] = {
4173
+ ...pv,
4174
+ ...ov
4175
+ };
4176
+ else out[key] = ov;
4177
+ break;
4178
+ case "logpushJobs":
4179
+ case "dispatchNamespaces":
4180
+ case "dnsRecords":
4181
+ out[key] = ov;
4182
+ break;
4183
+ default: out[key] = ov;
4184
+ }
4185
+ }
4186
+ return out;
4161
4187
  }
4162
- function formatZodError(error) {
4163
- return error.issues.map((e) => e.path.length ? `${e.path.join(".")}: ${e.message}` : e.message).join("; ");
4188
+
4189
+ //#endregion
4190
+ //#region src/core/config/resolve-config-sources.ts
4191
+ function isProjectFileName(file) {
4192
+ return file === "project.config.ts" || file === "tamer.project.config.ts";
4164
4193
  }
4165
- function parseSyncArgs(argv) {
4166
- const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
4167
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4168
- return {
4169
- env: parsed.data.env,
4170
- configPath: parsed.data.config
4171
- };
4194
+ function projectDirForMergedEntry(absProject) {
4195
+ const dir = dirname(absProject);
4196
+ if (basename(absProject) === "project.config.ts") return dir;
4197
+ return dir;
4172
4198
  }
4173
- function parseApplyArgs(argv) {
4174
- const parsed = ApplyArgsSchema.safeParse(toOpts(parseArgs(argv)));
4175
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4176
- return {
4177
- env: parsed.data.env,
4178
- addShard: parsed.data.add_shard,
4179
- configPath: parsed.data.config,
4180
- planFile: parsed.data.plan,
4181
- allowStale: parsed.data.allow_stale,
4182
- rollbackOnFailure: parsed.data.rollback_on_failure,
4183
- target: parsed.data.target
4184
- };
4199
+ function overlayPathForNestedLayout(projectDir, env) {
4200
+ return resolve(projectDir, "env", `${env}.config.ts`);
4185
4201
  }
4186
- function parseDevArgs(argv) {
4187
- const parsed = DevArgsSchema.safeParse(toOpts(parseArgs(argv)));
4188
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4189
- return {
4190
- worker: parsed.data.worker,
4191
- env: parsed.data.env,
4192
- configPath: parsed.data.config,
4193
- all: parsed.data.all
4194
- };
4202
+ function overlayPathForFlatLayout(projectRoot, env) {
4203
+ return resolve(projectRoot, `tamer.env.${env}.ts`);
4195
4204
  }
4196
- function parseDeployArgs(argv) {
4197
- const parsed = DeployArgsSchema.safeParse(toOpts(parseArgs(argv)));
4198
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4199
- return {
4200
- worker: parsed.data.worker,
4201
- env: parsed.data.env,
4202
- configPath: parsed.data.config,
4203
- dispatchNamespace: parsed.data.dispatch_namespace
4204
- };
4205
+ function rejectLegacyTamerConfigFile(context) {
4206
+ throw new Error(`${context}: tamer.config.ts is not supported. Move the default export to tamer/project.config.ts and optional per-env overlays to tamer/env/<env>.config.ts (or use tamer.project.config.ts at the repo root with optional tamer.env.<env>.ts).`);
4205
4207
  }
4206
- function parseMigrateArgs(argv) {
4207
- const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
4208
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4209
- return {
4210
- worker: parsed.data.worker,
4211
- env: parsed.data.env,
4212
- configPath: parsed.data.config
4213
- };
4208
+ /**
4209
+ * Resolve which config file(s) to load. See docs/design-tamer-project-config.md.
4210
+ *
4211
+ * - **Merged:** `tamer/project.config.ts` or `tamer.project.config.ts`, plus optional env overlay.
4212
+ * - **Single:** explicit `--config` path to any `.ts` file **except** `tamer.config.ts` (e.g. alternate snapshots in tests).
4213
+ */
4214
+ function resolveConfigSources(cwd, explicitConfigPath, env) {
4215
+ if (explicitConfigPath) {
4216
+ const abs = resolve(cwd, explicitConfigPath);
4217
+ const file = basename(abs);
4218
+ if (file === "tamer.config.ts") rejectLegacyTamerConfigFile("--config");
4219
+ if (isProjectFileName(file)) {
4220
+ const projectDir = projectDirForMergedEntry(abs);
4221
+ let overlay = null;
4222
+ if (env) if (file === "project.config.ts") {
4223
+ const candidate = overlayPathForNestedLayout(projectDir, env);
4224
+ if (existsSync(candidate)) overlay = candidate;
4225
+ } else {
4226
+ const candidate = overlayPathForFlatLayout(dirname(abs), env);
4227
+ if (existsSync(candidate)) overlay = candidate;
4228
+ }
4229
+ return {
4230
+ mode: "merged",
4231
+ projectPath: abs,
4232
+ overlayPath: overlay
4233
+ };
4234
+ }
4235
+ return {
4236
+ mode: "single",
4237
+ path: abs
4238
+ };
4239
+ }
4240
+ const nestedProject = resolve(cwd, "tamer", "project.config.ts");
4241
+ if (existsSync(nestedProject)) {
4242
+ let overlay = null;
4243
+ if (env) {
4244
+ const candidate = overlayPathForNestedLayout(dirname(nestedProject), env);
4245
+ if (existsSync(candidate)) overlay = candidate;
4246
+ }
4247
+ return {
4248
+ mode: "merged",
4249
+ projectPath: nestedProject,
4250
+ overlayPath: overlay
4251
+ };
4252
+ }
4253
+ const flatProject = resolve(cwd, "tamer.project.config.ts");
4254
+ if (existsSync(flatProject)) {
4255
+ let overlay = null;
4256
+ if (env) {
4257
+ const candidate = overlayPathForFlatLayout(cwd, env);
4258
+ if (existsSync(candidate)) overlay = candidate;
4259
+ }
4260
+ return {
4261
+ mode: "merged",
4262
+ projectPath: flatProject,
4263
+ overlayPath: overlay
4264
+ };
4265
+ }
4266
+ if (existsSync(resolve(cwd, "tamer.config.ts"))) rejectLegacyTamerConfigFile("Config discovery");
4267
+ throw new Error(`No Tamer project config under ${cwd}. Create tamer/project.config.ts (or tamer.project.config.ts at the repo root).`);
4214
4268
  }
4215
- function parseTypesArgs(argv) {
4216
- const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
4217
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4218
- return {
4219
- worker: parsed.data.worker,
4220
- env: parsed.data.env,
4221
- configPath: parsed.data.config
4222
- };
4269
+
4270
+ //#endregion
4271
+ //#region src/core/config/loader.ts
4272
+ const TENANT_SHARD_ROLE_RE = /^[a-z][a-z0-9_-]*$/;
4273
+ const TenantMetaSchema = object({
4274
+ id: string().min(1),
4275
+ name: string().min(1),
4276
+ slug: string().min(1),
4277
+ d1Shards: array(string().min(1).refine((s) => TENANT_SHARD_ROLE_RE.test(s), { error: "tenant.d1Shards entries must be lowercase ASCII (letters, digits, `_`, `-`) and start with a letter" })).optional().refine((arr) => !arr || new Set(arr).size === arr.length, "tenant.d1Shards must not contain duplicate roles"),
4278
+ protectedEnvs: array(string().min(1)).optional(),
4279
+ ephemeralEnvPattern: string().min(1).optional().refine((s) => {
4280
+ if (s == null) return true;
4281
+ try {
4282
+ new RegExp(s);
4283
+ return true;
4284
+ } catch {
4285
+ return false;
4286
+ }
4287
+ }, "tenant.ephemeralEnvPattern must be a valid JavaScript RegExp source string")
4288
+ });
4289
+ const CloudflareNameFnSchema = custom((v) => v === void 0 || typeof v === "function").optional();
4290
+ const D1ResourceConfigSchema = object({
4291
+ logicalName: string().min(1),
4292
+ type: _enum(["single", "sharded"]),
4293
+ cloudflareName: CloudflareNameFnSchema,
4294
+ ownership: _enum(["managed", "external"]).optional(),
4295
+ databaseName: string().optional(),
4296
+ binding: string().optional(),
4297
+ migrationsDir: string().optional(),
4298
+ migrationsTable: string().optional(),
4299
+ preserveOnDestroy: boolean().optional()
4300
+ }).refine((d) => d.ownership !== "external" || typeof d.databaseName === "string" && d.databaseName.length > 0, { error: "resources.d1: ownership 'external' requires non-empty databaseName" }).refine((d) => d.ownership !== "external" || d.type === "single", { error: "resources.d1: ownership 'external' only supports type 'single'" });
4301
+ const R2ResourceConfigSchema = object({
4302
+ logicalName: string().min(1),
4303
+ cloudflareName: CloudflareNameFnSchema,
4304
+ binding: string().optional()
4305
+ });
4306
+ const KVResourceConfigSchema = object({
4307
+ logicalName: string().min(1),
4308
+ cloudflareName: CloudflareNameFnSchema,
4309
+ binding: string().optional()
4310
+ });
4311
+ const QueueResourceConfigSchema = object({
4312
+ logicalName: string().min(1),
4313
+ cloudflareName: CloudflareNameFnSchema,
4314
+ binding: string().optional(),
4315
+ consumerOnly: boolean().optional()
4316
+ });
4317
+ const HyperdriveSecretSchema = union([string(), object({ fromEnv: string().min(1) })]);
4318
+ const HyperdriveOriginSchema = object({
4319
+ scheme: _enum([
4320
+ "postgres",
4321
+ "postgresql",
4322
+ "mysql"
4323
+ ]),
4324
+ host: string().min(1),
4325
+ port: number$1().optional(),
4326
+ database: string().min(1),
4327
+ user: string().min(1),
4328
+ password: HyperdriveSecretSchema,
4329
+ access_client_id: string().optional(),
4330
+ access_client_secret: HyperdriveSecretSchema.optional()
4331
+ });
4332
+ const HyperdriveResourceConfigSchema = object({
4333
+ logicalName: string().min(1),
4334
+ cloudflareName: CloudflareNameFnSchema,
4335
+ binding: string().optional(),
4336
+ origin: HyperdriveOriginSchema,
4337
+ caching: object({
4338
+ disabled: boolean().optional(),
4339
+ max_age: number$1().optional(),
4340
+ stale_while_revalidate: number$1().optional()
4341
+ }).optional(),
4342
+ mtls: object({
4343
+ ca_certificate_id: string().optional(),
4344
+ mtls_certificate_id: string().optional()
4345
+ }).optional(),
4346
+ localConnectionString: string().optional()
4347
+ });
4348
+ const VectorizeResourceConfigSchema = object({
4349
+ logicalName: string().min(1),
4350
+ cloudflareName: CloudflareNameFnSchema,
4351
+ binding: string().optional(),
4352
+ dimensions: number$1().int().positive(),
4353
+ metric: _enum([
4354
+ "cosine",
4355
+ "euclidean",
4356
+ "dot-product"
4357
+ ]),
4358
+ description: string().optional()
4359
+ });
4360
+ const AIGatewayResourceConfigSchema = object({
4361
+ logicalName: string().min(1),
4362
+ cloudflareName: CloudflareNameFnSchema,
4363
+ cacheTtl: number$1().int().nonnegative().optional(),
4364
+ cacheInvalidateOnUpdate: boolean().optional(),
4365
+ collectLogs: boolean().optional(),
4366
+ authentication: boolean().optional(),
4367
+ rateLimitingInterval: number$1().int().nonnegative().optional(),
4368
+ rateLimitingLimit: number$1().int().nonnegative().optional(),
4369
+ rateLimitingTechnique: _enum(["fixed", "sliding"]).optional()
4370
+ });
4371
+ const DispatchNamespaceResourceSchema = object({
4372
+ logicalName: string().min(1),
4373
+ namespace: string().min(1),
4374
+ envSuffix: boolean().optional()
4375
+ });
4376
+ const LogpushJobR2DestinationSchema = object({
4377
+ bucketLogicalName: string().min(1),
4378
+ pathPrefix: string().optional(),
4379
+ accessKeyIdEnv: string().min(1),
4380
+ secretAccessKeyEnv: string().min(1)
4381
+ });
4382
+ const LogpushJobPipelinesIngestSchema = object({
4383
+ streamId: string().min(1),
4384
+ pipelineId: string().min(1),
4385
+ bearerTokenEnv: string().min(1)
4386
+ });
4387
+ const LogpushJobPipelinesAutoSchema = object({
4388
+ catalogBucketLogicalName: string().min(1),
4389
+ tableName: string().min(1),
4390
+ namespace: string().min(1).optional(),
4391
+ tableNameAppendTimestamp: boolean().optional(),
4392
+ sinkRowGroupBytes: number$1().int().positive().optional(),
4393
+ sinkRollingFileSizeBytes: number$1().int().positive().optional(),
4394
+ sinkRollingIntervalSeconds: number$1().int().positive().optional()
4395
+ });
4396
+ const LogpushJobResourceConfigSchema = object({
4397
+ logicalName: string().min(1),
4398
+ dataset: literal("workers_trace_events"),
4399
+ jobName: string().optional(),
4400
+ r2: LogpushJobR2DestinationSchema.optional(),
4401
+ pipelinesIngest: LogpushJobPipelinesIngestSchema.optional(),
4402
+ pipelinesAuto: LogpushJobPipelinesAutoSchema.optional(),
4403
+ destinationConfEnv: string().min(1).optional(),
4404
+ destinationConfFromJobId: number$1().int().positive().optional(),
4405
+ destinationConfFromJobIdEnv: string().min(1).optional(),
4406
+ filter: string().min(1).optional(),
4407
+ fieldNames: array(string()).optional(),
4408
+ enabled: boolean().optional()
4409
+ }).refine((d) => {
4410
+ const hasR2 = Boolean(d.r2);
4411
+ const n = (d.destinationConfEnv ? 1 : 0) + (d.destinationConfFromJobId != null ? 1 : 0) + (d.destinationConfFromJobIdEnv ? 1 : 0) + (d.pipelinesIngest ? 1 : 0) + (d.pipelinesAuto ? 1 : 0);
4412
+ if (hasR2) return n === 0;
4413
+ return n === 1;
4414
+ }, { error: "logpushJobs: set exactly one of r2 | pipelinesIngest | pipelinesAuto | destinationConfEnv | destinationConfFromJobId | destinationConfFromJobIdEnv" }).refine((d) => !(d.destinationConfFromJobId != null && d.destinationConfFromJobIdEnv), { error: "logpushJobs: use only one of destinationConfFromJobId or destinationConfFromJobIdEnv" }).refine((d) => !(d.pipelinesIngest && d.pipelinesAuto), { error: "logpushJobs: use only one of pipelinesIngest or pipelinesAuto" });
4415
+ const DnsRecordResourceConfigSchema = object({
4416
+ logicalName: string().min(1),
4417
+ zoneId: string().min(1),
4418
+ type: _enum([
4419
+ "A",
4420
+ "AAAA",
4421
+ "CNAME",
4422
+ "TXT",
4423
+ "MX",
4424
+ "NS",
4425
+ "CAA",
4426
+ "SRV",
4427
+ "PTR",
4428
+ "HTTPS",
4429
+ "SVCB"
4430
+ ]),
4431
+ name: string().min(1),
4432
+ content: string().min(1),
4433
+ ttl: number$1().int().positive().optional(),
4434
+ proxied: boolean().optional(),
4435
+ priority: number$1().int().nonnegative().optional(),
4436
+ comment: string().optional(),
4437
+ skipEnvs: array(string()).optional(),
4438
+ preserveOnDestroy: boolean().optional()
4439
+ });
4440
+ const PipelineResourceConfigSchema = object({
4441
+ logicalName: string().min(1),
4442
+ cloudflareName: CloudflareNameFnSchema,
4443
+ sql: string().min(1),
4444
+ binding: string().min(1).optional()
4445
+ });
4446
+ const WorkflowResourceConfigSchema = object({
4447
+ logicalName: string().min(1),
4448
+ cloudflareName: CloudflareNameFnSchema,
4449
+ className: string().min(1),
4450
+ scriptName: string().min(1).optional(),
4451
+ binding: string().min(1).optional(),
4452
+ limits: object({ steps: number$1().int().positive().optional() }).optional()
4453
+ });
4454
+ const SecretsStoreResourceConfigSchema = object({
4455
+ logicalName: string().min(1),
4456
+ cloudflareName: CloudflareNameFnSchema
4457
+ });
4458
+ const SecretsStoreSecretBindingSchema = object({
4459
+ binding: string().min(1),
4460
+ store: string().min(1),
4461
+ secretName: string().min(1)
4462
+ });
4463
+ const WorkerSecretsConfigSchema = object({ required: array(string().min(1)) });
4464
+ const WorkerResourcesSchema = object({
4465
+ d1: array(D1ResourceConfigSchema).optional(),
4466
+ r2: array(R2ResourceConfigSchema).optional(),
4467
+ kv: array(KVResourceConfigSchema).optional(),
4468
+ queues: array(QueueResourceConfigSchema).optional(),
4469
+ hyperdrive: array(HyperdriveResourceConfigSchema).optional(),
4470
+ vectorize: array(VectorizeResourceConfigSchema).optional(),
4471
+ aiGateway: array(AIGatewayResourceConfigSchema).optional(),
4472
+ pipelines: array(PipelineResourceConfigSchema).optional(),
4473
+ workflows: array(WorkflowResourceConfigSchema).optional(),
4474
+ secretsStores: array(SecretsStoreResourceConfigSchema).optional(),
4475
+ secretsStoreSecrets: array(SecretsStoreSecretBindingSchema).optional()
4476
+ });
4477
+ const WranglerRouteSchema = object({
4478
+ pattern: string(),
4479
+ custom_domain: boolean().optional(),
4480
+ zone_name: string().optional(),
4481
+ zone_id: string().optional()
4482
+ });
4483
+ const WranglerAssetsSchema = object({
4484
+ directory: string(),
4485
+ binding: string().optional(),
4486
+ not_found_handling: _enum(["single-page-application", "return-404"]).optional()
4487
+ });
4488
+ const EnvOverrideSchema = object({
4489
+ vars: record(string(), string()).optional(),
4490
+ scriptName: string().optional(),
4491
+ wranglerOutFile: string().optional(),
4492
+ route: union([WranglerRouteSchema, array(WranglerRouteSchema)]).optional(),
4493
+ routes: array(WranglerRouteSchema).optional()
4494
+ }).passthrough();
4495
+ const WorkerConfigSchema = object({
4496
+ path: string().optional(),
4497
+ config: string().optional(),
4498
+ scriptName: string().optional(),
4499
+ wranglerOutFile: string().optional(),
4500
+ dispatchNamespace: string().optional(),
4501
+ main: string().optional(),
4502
+ compatibility_date: string().optional(),
4503
+ compatibility_flags: array(string()).optional(),
4504
+ limits: record(string(), union([number$1(), string()])).optional(),
4505
+ workers_dev: boolean().optional(),
4506
+ preview_urls: boolean().optional(),
4507
+ resources: WorkerResourcesSchema.optional(),
4508
+ secrets: WorkerSecretsConfigSchema.optional(),
4509
+ alias: record(string(), string()).optional(),
4510
+ vars: record(string(), string()).optional(),
4511
+ local: EnvOverrideSchema.optional(),
4512
+ env: record(string(), EnvOverrideSchema).optional(),
4513
+ assets: WranglerAssetsSchema.optional(),
4514
+ route: union([WranglerRouteSchema, array(WranglerRouteSchema)]).optional(),
4515
+ routes: array(WranglerRouteSchema).optional()
4516
+ }).passthrough();
4517
+ const CfiConfigSchema = object({
4518
+ tenant: TenantMetaSchema,
4519
+ account_id: string().optional(),
4520
+ compatibility_date: string().optional(),
4521
+ naming: any().optional(),
4522
+ dispatchNamespaces: array(DispatchNamespaceResourceSchema).optional(),
4523
+ dnsRecords: array(DnsRecordResourceConfigSchema).optional(),
4524
+ logpushJobs: array(LogpushJobResourceConfigSchema).optional(),
4525
+ stack: object({
4526
+ name: string().regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, { error: "stack.name must match /^[a-zA-Z][a-zA-Z0-9_-]*$/ (CloudFormation-style identifier)" }).optional(),
4527
+ description: string().min(1).optional()
4528
+ }).optional(),
4529
+ outputs: record(string().regex(/^[a-zA-Z][a-zA-Z0-9_-]*$/, { error: "outputs key must match /^[a-zA-Z][a-zA-Z0-9_-]*$/ (CloudFormation-style identifier)" }), string().min(1)).optional(),
4530
+ worker: WorkerConfigSchema.optional(),
4531
+ workers: record(string(), WorkerConfigSchema).optional()
4532
+ }).refine((data) => (data.worker ?? data.workers) && !(data.worker && data.workers), { error: "Must have either worker or workers, not both" });
4533
+ async function loadConfig(configPath, options = {}) {
4534
+ const sources = resolveConfigSources(options.cwd ?? process.cwd(), configPath, options.env);
4535
+ let raw;
4536
+ if (sources.mode === "single") {
4537
+ raw = (await import(sources.path)).default;
4538
+ if (!raw) throw new Error(`No default export in ${sources.path}`);
4539
+ } else {
4540
+ const projectRaw = (await import(sources.projectPath)).default;
4541
+ if (!projectRaw || typeof projectRaw !== "object") throw new Error(`No default export in ${sources.projectPath}`);
4542
+ let merged = projectRaw;
4543
+ if (sources.overlayPath) {
4544
+ const overlayRaw = (await import(sources.overlayPath)).default;
4545
+ if (!overlayRaw || typeof overlayRaw !== "object") throw new Error(`Env overlay must default-export an object: ${sources.overlayPath}`);
4546
+ const overlayObj = overlayRaw;
4547
+ const declaredEnv = overlayObj[TAMER_OVERLAY_ENV_KEY];
4548
+ if (declaredEnv !== void 0) {
4549
+ if (typeof declaredEnv !== "string") throw new Error(`Env overlay ${sources.overlayPath}: ${TAMER_OVERLAY_ENV_KEY} must be a string`);
4550
+ if (options.env !== declaredEnv) throw new Error(`Env overlay ${sources.overlayPath} sets ${TAMER_OVERLAY_ENV_KEY} "${declaredEnv}" but this load uses --env "${options.env ?? "(none)"}".`);
4551
+ }
4552
+ const { [TAMER_OVERLAY_ENV_KEY]: _strip, ...overlayRest } = overlayObj;
4553
+ merged = mergeProjectOverlay(merged, overlayRest);
4554
+ }
4555
+ raw = merged;
4556
+ }
4557
+ const parsed = CfiConfigSchema.safeParse(materializeCloudflareBindings(raw));
4558
+ if (!parsed.success) throw new Error(`Invalid Tamer project config: ${parsed.error.message}`);
4559
+ return parsed.data;
4560
+ }
4561
+ async function getWorkers(config$1, baseDir) {
4562
+ const cwd = baseDir ?? process.cwd();
4563
+ if ("worker" in config$1 && config$1.worker) return [["default", await resolveWorkerConfigRef(config$1.worker, cwd)]];
4564
+ if ("workers" in config$1 && config$1.workers) {
4565
+ const entries = [];
4566
+ for (const [key, wc] of Object.entries(config$1.workers)) entries.push([key, await resolveWorkerConfigRef(wc, cwd)]);
4567
+ return entries;
4568
+ }
4569
+ return [];
4223
4570
  }
4224
- function parseStatusArgs(argv) {
4225
- const parsed = StatusArgsSchema.safeParse(toOpts(parseArgs(argv)));
4226
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4571
+ async function resolveWorkerConfigRef(wc, baseDir) {
4572
+ if (!wc.config) return wc;
4573
+ const configPath = resolve(baseDir, wc.config);
4574
+ const loaded = (await import(configPath)).default;
4575
+ if (!loaded) throw new Error(`No default export in ${configPath}`);
4227
4576
  return {
4228
- env: parsed.data.env,
4229
- configPath: parsed.data.config,
4230
- tenant: parsed.data.tenant
4577
+ ...loaded,
4578
+ path: wc.path,
4579
+ config: wc.config
4231
4580
  };
4232
4581
  }
4233
- function parseEventsArgs(argv) {
4234
- const parsed = EventsArgsSchema.safeParse(toOpts(parseArgs(argv)));
4235
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4236
- return {
4237
- env: parsed.data.env,
4238
- configPath: parsed.data.config,
4239
- limit: parsed.data.limit,
4240
- json: parsed.data.json
4241
- };
4582
+
4583
+ //#endregion
4584
+ //#region src/core/cloudflareEnv.ts
4585
+ /**
4586
+ * Wrangler-aligned Cloudflare credentials (same names as `wrangler` CLI).
4587
+ * @see https://developers.cloudflare.com/workers/wrangler/system-environment-variables/
4588
+ */
4589
+ function cloudflareAccountIdFromEnv() {
4590
+ return process.env.CLOUDFLARE_ACCOUNT_ID;
4242
4591
  }
4243
- function parseDoctorArgs(argv) {
4244
- const parsed = DoctorArgsSchema.safeParse(toOpts(parseArgs(argv)));
4245
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4246
- return { json: parsed.data.json };
4592
+ function cloudflareApiTokenFromEnv() {
4593
+ return process.env.CLOUDFLARE_API_TOKEN ?? "";
4247
4594
  }
4248
- function parseProvisionTenantArgs(argv) {
4249
- const parsed = ProvisionTenantArgsSchema.safeParse(toOpts(parseArgs(argv)));
4250
- if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
4251
- const d = parsed.data;
4252
- return {
4253
- env: d.env,
4595
+
4596
+ //#endregion
4597
+ //#region src/core/api/pipelinesV1PathId.ts
4598
+ /**
4599
+ * Pipelines v1 path segments (streams, sinks, SQL pipelines) expect **stream_id**
4600
+ * / **sink_id** as **exactly 32 lower-case hex characters** (no hyphens). The API
4601
+ * returns 400 if the URL contains a hyphenated UUID (36 chars) — see
4602
+ * `stream_id: String must contain exactly 32 character(s)`.
4603
+ *
4604
+ * State and list APIs may return ids with or without hyphens; we normalize to 32-hex
4605
+ * for GET/DELETE paths.
4606
+ */
4607
+ function pipelinesV1IdForPath(id) {
4608
+ const c = id.trim().toLowerCase().replace(/-/g, "");
4609
+ if (!/^[0-9a-f]{32}$/.test(c)) return id.trim();
4610
+ return c;
4611
+ }
4612
+
4613
+ //#endregion
4614
+ //#region src/core/api/CFApiClient.ts
4615
+ const CF_API_BASE = "https://api.cloudflare.com/client/v4";
4616
+ /**
4617
+ * Renders the Cloudflare API `errors` / `messages` array into a single string
4618
+ * (message, error code, documentation URL, JSON pointer) for operator-friendly logs.
4619
+ */
4620
+ function describeCloudflareErrorItem(e) {
4621
+ if (e == null) return "null";
4622
+ if (typeof e === "string") return e;
4623
+ if (typeof e !== "object") return String(e);
4624
+ const o = e;
4625
+ const parts = [];
4626
+ if (typeof o.message === "string" && o.message) parts.push(o.message);
4627
+ if (typeof o.code === "number") parts.push(`[code: ${o.code}]`);
4628
+ if (typeof o.code === "string" && o.code) parts.push(`[code: ${o.code}]`);
4629
+ if (typeof o.documentation_url === "string" && o.documentation_url) parts.push(o.documentation_url);
4630
+ const src = o.source;
4631
+ if (src && typeof src === "object" && "pointer" in src) {
4632
+ const p = src.pointer;
4633
+ if (typeof p === "string" && p) parts.push(`(field: ${p})`);
4634
+ }
4635
+ if (Array.isArray(o.error_chain) && o.error_chain.length > 0) parts.push(`[chain: ${o.error_chain.map(describeCloudflareErrorItem).join(" → ")}]`);
4636
+ if (parts.length) return parts.join(" ");
4637
+ try {
4638
+ return JSON.stringify(e);
4639
+ } catch {
4640
+ return String(e);
4641
+ }
4642
+ }
4643
+ function formatCloudflareErrorBody(errors, messages, fallback) {
4644
+ const errList = formatCloudflareList(errors) || fallback;
4645
+ const msgList = formatCloudflareList(messages);
4646
+ if (msgList) return `${errList} — messages: ${msgList}`;
4647
+ return errList;
4648
+ }
4649
+ function formatCloudflareList(v) {
4650
+ if (!Array.isArray(v) || v.length === 0) return "";
4651
+ return v.map(describeCloudflareErrorItem).join(" | ");
4652
+ }
4653
+ var CFApiClient = class {
4654
+ accountId;
4655
+ apiToken;
4656
+ constructor(accountId, apiToken) {
4657
+ this.accountId = accountId;
4658
+ this.apiToken = apiToken ?? cloudflareApiTokenFromEnv();
4659
+ if (!this.apiToken) throw new Error("CLOUDFLARE_API_TOKEN is required");
4660
+ }
4661
+ getAccountId() {
4662
+ return this.accountId;
4663
+ }
4664
+ /** Lightweight account read — validates token + account scope. */
4665
+ async accountRead() {
4666
+ return (await this.request(`/accounts/${this.accountId}`)).result;
4667
+ }
4668
+ /**
4669
+ * Get a single Worker script by name (account-scoped, **not** dispatch).
4670
+ * Returns `undefined` when Cloudflare responds 404 so callers can treat it
4671
+ * as "not deployed" without try/catch on the message.
4672
+ *
4673
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/get/
4674
+ */
4675
+ async workersScriptGet(scriptName) {
4676
+ const url = `${CF_API_BASE}/accounts/${this.accountId}/workers/scripts/${encodeURIComponent(scriptName)}`;
4677
+ const res = await fetch(url, { headers: {
4678
+ Authorization: `Bearer ${this.apiToken}`,
4679
+ "Content-Type": "application/json"
4680
+ } });
4681
+ if (res.status === 404) return void 0;
4682
+ const data = await res.json();
4683
+ if (!res.ok) {
4684
+ const msg = data?.errors?.map((e) => e.message).join("; ") ?? res.statusText;
4685
+ throw new Error(`CF API error (workers script get): ${msg}`);
4686
+ }
4687
+ return data.result;
4688
+ }
4689
+ /**
4690
+ * Upsert a plain-text secret binding on a Worker script.
4691
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/secrets/methods/update/
4692
+ */
4693
+ async workersSecretPut(scriptName, secret) {
4694
+ return (await this.request(`/accounts/${this.accountId}/workers/scripts/${encodeURIComponent(scriptName)}/secrets`, {
4695
+ method: "PUT",
4696
+ body: JSON.stringify({
4697
+ name: secret.name,
4698
+ text: secret.text,
4699
+ type: "secret_text"
4700
+ })
4701
+ })).result;
4702
+ }
4703
+ /**
4704
+ * List secret binding names on a Worker script. Values are write-only and
4705
+ * are not returned by the Cloudflare API.
4706
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/secrets/methods/list/
4707
+ */
4708
+ async workersSecretsList(scriptName) {
4709
+ return ((await this.request(`/accounts/${this.accountId}/workers/scripts/${encodeURIComponent(scriptName)}/secrets`)).result ?? []).map((s) => s.name);
4710
+ }
4711
+ /**
4712
+ * Remove a secret binding from a Worker script.
4713
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/scripts/subresources/secrets/methods/delete/
4714
+ */
4715
+ async workersSecretDelete(scriptName, name) {
4716
+ await this.request(`/accounts/${this.accountId}/workers/scripts/${encodeURIComponent(scriptName)}/secrets/${encodeURIComponent(name)}`, { method: "DELETE" });
4717
+ }
4718
+ async request(path, options = {}) {
4719
+ const method = (options.method ?? "GET").toUpperCase();
4720
+ const url = `${CF_API_BASE}${path}`;
4721
+ const res = await fetch(url, {
4722
+ ...options,
4723
+ headers: {
4724
+ Authorization: `Bearer ${this.apiToken}`,
4725
+ "Content-Type": "application/json",
4726
+ ...options.headers
4727
+ }
4728
+ });
4729
+ let parsed;
4730
+ try {
4731
+ parsed = await res.json();
4732
+ } catch {
4733
+ throw new Error(`CF API error: ${method} \`${path}\` → HTTP ${res.status} (response was not valid JSON)`);
4734
+ }
4735
+ /** Some endpoints (e.g. R2 catalog disable) respond `200` with literal JSON `null`. */
4736
+ const data = parsed !== null && typeof parsed === "object" ? parsed : {};
4737
+ const httpError = !res.ok;
4738
+ const bodySuccessFalse = data.success === false;
4739
+ if (httpError || bodySuccessFalse) {
4740
+ const detail = formatCloudflareErrorBody(data.errors, data.messages, res.statusText || "no error array in body");
4741
+ throw new Error(`CF API error: ${method} \`${path}\` → HTTP ${res.status}. ${detail}`);
4742
+ }
4743
+ return data;
4744
+ }
4745
+ async d1ListAll() {
4746
+ const all = [];
4747
+ let page = 1;
4748
+ const perPage = 100;
4749
+ while (true) {
4750
+ const data = await this.request(`/accounts/${this.accountId}/d1/database?per_page=${perPage}&page=${page}`);
4751
+ all.push(...data.result);
4752
+ const info = data.result_info;
4753
+ if (!info || info.count < perPage) break;
4754
+ page++;
4755
+ }
4756
+ return all;
4757
+ }
4758
+ async r2ListAll() {
4759
+ const all = [];
4760
+ const perPage = 100;
4761
+ let cursor;
4762
+ while (true) {
4763
+ const qs = new URLSearchParams({ per_page: String(perPage) });
4764
+ if (cursor) qs.set("cursor", cursor);
4765
+ const data = await this.request(`/accounts/${this.accountId}/r2/buckets?${qs.toString()}`);
4766
+ const buckets = data.result?.buckets ?? [];
4767
+ all.push(...buckets.map((b) => ({
4768
+ name: b.name ?? "",
4769
+ creation_date: b.creation_date ?? ""
4770
+ })));
4771
+ const next = data.result_info?.cursor;
4772
+ if (!next) break;
4773
+ cursor = next;
4774
+ }
4775
+ return all;
4776
+ }
4777
+ async kvListAll() {
4778
+ const all = [];
4779
+ let page = 1;
4780
+ const perPage = 100;
4781
+ while (true) {
4782
+ const data = await this.request(`/accounts/${this.accountId}/storage/kv/namespaces?per_page=${perPage}&page=${page}`);
4783
+ all.push(...data.result);
4784
+ const info = data.result_info;
4785
+ if (!info || info.count < perPage) break;
4786
+ page++;
4787
+ }
4788
+ return all;
4789
+ }
4790
+ async d1Create(name) {
4791
+ return (await this.request(`/accounts/${this.accountId}/d1/database`, {
4792
+ method: "POST",
4793
+ body: JSON.stringify({ name })
4794
+ })).result;
4795
+ }
4796
+ async r2Create(name, location = "auto") {
4797
+ await this.request(`/accounts/${this.accountId}/r2/buckets`, {
4798
+ method: "POST",
4799
+ body: JSON.stringify({
4800
+ name,
4801
+ location
4802
+ })
4803
+ });
4804
+ }
4805
+ async kvCreate(title) {
4806
+ return (await this.request(`/accounts/${this.accountId}/storage/kv/namespaces`, {
4807
+ method: "POST",
4808
+ body: JSON.stringify({ title })
4809
+ })).result;
4810
+ }
4811
+ async d1Delete(databaseId) {
4812
+ await this.request(`/accounts/${this.accountId}/d1/database/${databaseId}`, { method: "DELETE" });
4813
+ }
4814
+ /**
4815
+ * Run SQL against a D1 database (HTTP API).
4816
+ * @see https://developers.cloudflare.com/d1/build-with-d1/rest-api/
4817
+ */
4818
+ async d1Query(databaseId, sql, params = []) {
4819
+ return { rows: (await this.request(`/accounts/${this.accountId}/d1/database/${databaseId}/query`, {
4820
+ method: "POST",
4821
+ body: JSON.stringify({
4822
+ sql,
4823
+ params
4824
+ })
4825
+ })).result?.[0]?.results ?? [] };
4826
+ }
4827
+ async r2Delete(bucketName) {
4828
+ await this.request(`/accounts/${this.accountId}/r2/buckets/${bucketName}`, { method: "DELETE" });
4829
+ }
4830
+ /**
4831
+ * Download an object from R2 (HTTP API).
4832
+ * @see https://developers.cloudflare.com/api/resources/r2/subresources/buckets/subresources/objects/methods/get/
4833
+ */
4834
+ async r2GetObject(bucketName, objectKey) {
4835
+ const pathEncoded = objectKey.split("/").map((seg) => encodeURIComponent(seg)).join("/");
4836
+ const url = `${CF_API_BASE}/accounts/${this.accountId}/r2/buckets/${encodeURIComponent(bucketName)}/objects/${pathEncoded}`;
4837
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${this.apiToken}` } });
4838
+ if (!res.ok) {
4839
+ let errMsg = res.statusText;
4840
+ try {
4841
+ errMsg = (await res.json())?.errors?.map((e) => e.message).join("; ") ?? errMsg;
4842
+ } catch {}
4843
+ throw new Error(`CF API error (R2 get): ${errMsg}`);
4844
+ }
4845
+ return res.arrayBuffer();
4846
+ }
4847
+ async kvDelete(namespaceId) {
4848
+ await this.request(`/accounts/${this.accountId}/storage/kv/namespaces/${namespaceId}`, { method: "DELETE" });
4849
+ }
4850
+ /**
4851
+ * List every Cloudflare Queue in the account.
4852
+ * @see https://developers.cloudflare.com/api/resources/queues/methods/list/
4853
+ */
4854
+ async queuesListAll() {
4855
+ const all = [];
4856
+ let page = 1;
4857
+ const perPage = 100;
4858
+ while (true) {
4859
+ const batch = (await this.request(`/accounts/${this.accountId}/queues?per_page=${perPage}&page=${page}`)).result ?? [];
4860
+ all.push(...batch);
4861
+ if (batch.length < perPage) break;
4862
+ page++;
4863
+ }
4864
+ return all;
4865
+ }
4866
+ /**
4867
+ * Create a new Cloudflare Queue.
4868
+ * @see https://developers.cloudflare.com/api/resources/queues/methods/create/
4869
+ */
4870
+ async queueCreate(queueName) {
4871
+ return (await this.request(`/accounts/${this.accountId}/queues`, {
4872
+ method: "POST",
4873
+ body: JSON.stringify({ queue_name: queueName })
4874
+ })).result;
4875
+ }
4876
+ /**
4877
+ * Delete a Cloudflare Queue by id.
4878
+ * @see https://developers.cloudflare.com/api/resources/queues/methods/delete/
4879
+ */
4880
+ async queueDelete(queueId) {
4881
+ await this.request(`/accounts/${this.accountId}/queues/${encodeURIComponent(queueId)}`, { method: "DELETE" });
4882
+ }
4883
+ /**
4884
+ * List every Hyperdrive config in the account.
4885
+ * @see https://developers.cloudflare.com/api/resources/hyperdrive/subresources/configs/methods/list/
4886
+ */
4887
+ async hyperdriveListAll() {
4888
+ return (await this.request(`/accounts/${this.accountId}/hyperdrive/configs`)).result ?? [];
4889
+ }
4890
+ /**
4891
+ * Create a Hyperdrive config.
4892
+ * @see https://developers.cloudflare.com/api/resources/hyperdrive/subresources/configs/methods/create/
4893
+ */
4894
+ async hyperdriveCreate(body) {
4895
+ return (await this.request(`/accounts/${this.accountId}/hyperdrive/configs`, {
4896
+ method: "POST",
4897
+ body: JSON.stringify(body)
4898
+ })).result;
4899
+ }
4900
+ /**
4901
+ * Patch an existing Hyperdrive configuration in place. Cloudflare accepts
4902
+ * partial updates here (true `PATCH`), so callers may send only the
4903
+ * fields that drifted. Origin patches must include the full origin
4904
+ * payload (the password is write-only and never returned, so Tamer
4905
+ * reads it back from the resource config every apply).
4906
+ * @see https://developers.cloudflare.com/api/resources/hyperdrive/subresources/configs/methods/edit/
4907
+ */
4908
+ async hyperdrivePatch(configId, body) {
4909
+ await this.request(`/accounts/${this.accountId}/hyperdrive/configs/${encodeURIComponent(configId)}`, {
4910
+ method: "PATCH",
4911
+ body: JSON.stringify(body)
4912
+ });
4913
+ }
4914
+ /**
4915
+ * Delete a Hyperdrive config by id.
4916
+ * @see https://developers.cloudflare.com/api/resources/hyperdrive/subresources/configs/methods/delete/
4917
+ */
4918
+ async hyperdriveDelete(configId) {
4919
+ await this.request(`/accounts/${this.accountId}/hyperdrive/configs/${encodeURIComponent(configId)}`, { method: "DELETE" });
4920
+ }
4921
+ /**
4922
+ * List every Vectorize index in the account (v2 storage subsystem).
4923
+ * @see https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/list/
4924
+ */
4925
+ async vectorizeListAll() {
4926
+ return (await this.request(`/accounts/${this.accountId}/vectorize/v2/indexes`)).result ?? [];
4927
+ }
4928
+ /**
4929
+ * Create a Vectorize v2 index. `dimensions` and `metric` are immutable.
4930
+ * @see https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/create/
4931
+ */
4932
+ async vectorizeCreate(body) {
4933
+ return (await this.request(`/accounts/${this.accountId}/vectorize/v2/indexes`, {
4934
+ method: "POST",
4935
+ body: JSON.stringify(body)
4936
+ })).result;
4937
+ }
4938
+ /**
4939
+ * Delete a Vectorize index by name.
4940
+ * @see https://developers.cloudflare.com/api/resources/vectorize/subresources/indexes/methods/delete/
4941
+ */
4942
+ async vectorizeDelete(indexName) {
4943
+ await this.request(`/accounts/${this.accountId}/vectorize/v2/indexes/${encodeURIComponent(indexName)}`, { method: "DELETE" });
4944
+ }
4945
+ /**
4946
+ * List every AI Gateway in the account.
4947
+ * @see https://developers.cloudflare.com/api/resources/ai_gateway/methods/list/
4948
+ */
4949
+ async aiGatewayListAll() {
4950
+ return (await this.request(`/accounts/${this.accountId}/ai-gateway/gateways?per_page=100`)).result ?? [];
4951
+ }
4952
+ /**
4953
+ * Create an AI Gateway. The gateway `id` is user-supplied and acts as both
4954
+ * primary key and routing slug under `https://gateway.ai.cloudflare.com/`.
4955
+ * @see https://developers.cloudflare.com/api/resources/ai_gateway/methods/create/
4956
+ */
4957
+ async aiGatewayCreate(body) {
4958
+ return (await this.request(`/accounts/${this.accountId}/ai-gateway/gateways`, {
4959
+ method: "POST",
4960
+ body: JSON.stringify(body)
4961
+ })).result;
4962
+ }
4963
+ /**
4964
+ * Update an existing AI Gateway in place. The Cloudflare API uses `PUT`
4965
+ * (not PATCH) and is full-replace on the listed fields, so callers must
4966
+ * supply the complete desired state — Tamer's `apply` reads the recorded
4967
+ * state row and merges declared overrides before calling this.
4968
+ * @see https://developers.cloudflare.com/api/resources/ai_gateway/methods/update/
4969
+ */
4970
+ async aiGatewayUpdate(gatewayId, body) {
4971
+ await this.request(`/accounts/${this.accountId}/ai-gateway/gateways/${encodeURIComponent(gatewayId)}`, {
4972
+ method: "PUT",
4973
+ body: JSON.stringify(body)
4974
+ });
4975
+ }
4976
+ /**
4977
+ * Delete an AI Gateway by id.
4978
+ * @see https://developers.cloudflare.com/api/resources/ai_gateway/methods/delete/
4979
+ */
4980
+ async aiGatewayDelete(gatewayId) {
4981
+ await this.request(`/accounts/${this.accountId}/ai-gateway/gateways/${encodeURIComponent(gatewayId)}`, { method: "DELETE" });
4982
+ }
4983
+ /**
4984
+ * List every Pipeline in the account (paginated, 100/page). Uses the V1
4985
+ * SQL pipelines endpoint; the deprecated `/accounts/{id}/pipelines`
4986
+ * (HTTP/binding sources) is not used.
4987
+ * @see https://developers.cloudflare.com/api/resources/pipelines/methods/list_v1/
4988
+ */
4989
+ async pipelineListAll() {
4990
+ const all = [];
4991
+ let page = 1;
4992
+ const perPage = 100;
4993
+ while (true) {
4994
+ const batch = (await this.request(`/accounts/${this.accountId}/pipelines/v1/pipelines?per_page=${perPage}&page=${page}`)).result ?? [];
4995
+ all.push(...batch);
4996
+ if (batch.length < perPage) break;
4997
+ page += 1;
4998
+ }
4999
+ return all;
5000
+ }
5001
+ /**
5002
+ * Create a Pipeline. Server assigns the `id`; `name` is user-supplied
5003
+ * and uniquely identifies the pipeline within the account.
5004
+ * @see https://developers.cloudflare.com/api/resources/pipelines/methods/create_v1/
5005
+ */
5006
+ async pipelineCreate(body) {
5007
+ return (await this.request(`/accounts/${this.accountId}/pipelines/v1/pipelines`, {
5008
+ method: "POST",
5009
+ body: JSON.stringify(body)
5010
+ })).result;
5011
+ }
5012
+ /**
5013
+ * Delete a Pipeline by server-assigned id.
5014
+ * @see https://developers.cloudflare.com/api/resources/pipelines/methods/delete_v1/
5015
+ */
5016
+ async pipelineDelete(pipelineId) {
5017
+ const pathId = pipelinesV1IdForPath(pipelineId);
5018
+ await this.request(`/accounts/${this.accountId}/pipelines/v1/pipelines/${encodeURIComponent(pathId)}`, { method: "DELETE" });
5019
+ }
5020
+ /**
5021
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/streams/methods/list/
5022
+ */
5023
+ async pipelinesV1StreamListAll() {
5024
+ const all = [];
5025
+ let page = 1;
5026
+ const perPage = 100;
5027
+ while (true) {
5028
+ const batch = (await this.request(`/accounts/${this.accountId}/pipelines/v1/streams?per_page=${perPage}&page=${page}`)).result ?? [];
5029
+ all.push(...batch);
5030
+ if (batch.length < perPage) break;
5031
+ page += 1;
5032
+ }
5033
+ return all;
5034
+ }
5035
+ /**
5036
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/streams/methods/create/
5037
+ */
5038
+ async pipelinesV1StreamCreate(body) {
5039
+ return (await this.request(`/accounts/${this.accountId}/pipelines/v1/streams`, {
5040
+ method: "POST",
5041
+ body: JSON.stringify(body)
5042
+ })).result;
5043
+ }
5044
+ /**
5045
+ * Path id: 32 lower-case hex chars (no hyphens). The dashboard issues a
5046
+ * plain `DELETE` with no query string; pass `{ force: true }` only if the API
5047
+ * docs require it for your case.
5048
+ *
5049
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/streams/methods/delete/
5050
+ */
5051
+ async pipelinesV1StreamDelete(streamId, query) {
5052
+ const pathId = pipelinesV1IdForPath(streamId);
5053
+ const qs = new URLSearchParams();
5054
+ if (query?.force) qs.set("force", "true");
5055
+ const tail = qs.toString() ? `?${qs.toString()}` : "";
5056
+ await this.request(`/accounts/${this.accountId}/pipelines/v1/streams/${encodeURIComponent(pathId)}${tail}`, { method: "DELETE" });
5057
+ }
5058
+ /**
5059
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/sinks/methods/list/
5060
+ */
5061
+ async pipelinesV1SinkListAll() {
5062
+ const all = [];
5063
+ let page = 1;
5064
+ const perPage = 100;
5065
+ while (true) {
5066
+ const batch = (await this.request(`/accounts/${this.accountId}/pipelines/v1/sinks?per_page=${perPage}&page=${page}`)).result ?? [];
5067
+ all.push(...batch);
5068
+ if (batch.length < perPage) break;
5069
+ page += 1;
5070
+ }
5071
+ return all;
5072
+ }
5073
+ /**
5074
+ * R2 Data Catalog sink `config` includes `table_name` and `namespace` as
5075
+ * Cloudflare has registered them (may differ from the name sent at create).
5076
+ *
5077
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/sinks/methods/get/
5078
+ */
5079
+ async pipelinesV1SinkGet(sinkId) {
5080
+ const pathId = pipelinesV1IdForPath(sinkId);
5081
+ return (await this.request(`/accounts/${this.accountId}/pipelines/v1/sinks/${encodeURIComponent(pathId)}`)).result;
5082
+ }
5083
+ /**
5084
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/sinks/methods/create/
5085
+ */
5086
+ async pipelinesV1SinkCreate(body) {
5087
+ return (await this.request(`/accounts/${this.accountId}/pipelines/v1/sinks`, {
5088
+ method: "POST",
5089
+ body: JSON.stringify(body)
5090
+ })).result;
5091
+ }
5092
+ /**
5093
+ * Path id: 32 lower-case hex chars (no hyphens). Optional `{ force: true }`
5094
+ * matches `?force=true` when the product requires it (e.g. some sink types).
5095
+ *
5096
+ * @see https://developers.cloudflare.com/api/resources/pipelines/subresources/sinks/methods/delete/
5097
+ */
5098
+ async pipelinesV1SinkDelete(sinkId, query) {
5099
+ const pathId = pipelinesV1IdForPath(sinkId);
5100
+ const qs = new URLSearchParams();
5101
+ if (query?.force) qs.set("force", "true");
5102
+ const tail = qs.toString() ? `?${qs.toString()}` : "";
5103
+ await this.request(`/accounts/${this.accountId}/pipelines/v1/sinks/${encodeURIComponent(pathId)}${tail}`, { method: "DELETE" });
5104
+ }
5105
+ /**
5106
+ * @see https://developers.cloudflare.com/api/resources/r2_data_catalog/methods/list/
5107
+ */
5108
+ async r2DataCatalogList() {
5109
+ return { warehouses: (await this.request(`/accounts/${this.accountId}/r2-catalog`)).result?.warehouses ?? [] };
5110
+ }
5111
+ /**
5112
+ * @see https://developers.cloudflare.com/api/resources/r2_data_catalog/methods/enable/
5113
+ */
5114
+ async r2DataCatalogEnable(bucketName) {
5115
+ await this.request(`/accounts/${this.accountId}/r2-catalog/${encodeURIComponent(bucketName)}/enable`, { method: "POST" });
5116
+ }
5117
+ /**
5118
+ * @see https://developers.cloudflare.com/api/resources/r2_data_catalog/methods/update/
5119
+ * (Store catalog credentials)
5120
+ */
5121
+ async r2DataCatalogStoreCredential(bucketName, token) {
5122
+ await this.request(`/accounts/${this.accountId}/r2-catalog/${encodeURIComponent(bucketName)}/credential`, {
5123
+ method: "POST",
5124
+ body: JSON.stringify({ token })
5125
+ });
5126
+ }
5127
+ /**
5128
+ * Deactivates the R2 Data Catalog for a bucket (metadata files remain in R2
5129
+ * until the bucket is emptied or objects are removed). Next `apply` will
5130
+ * re-enable the catalog as part of `pipelinesAuto` when needed.
5131
+ *
5132
+ * @see https://developers.cloudflare.com/api/resources/r2_data_catalog/methods/disable/
5133
+ */
5134
+ async r2DataCatalogDisable(bucketName) {
5135
+ await this.request(`/accounts/${this.accountId}/r2-catalog/${encodeURIComponent(bucketName)}/disable`, { method: "POST" });
5136
+ }
5137
+ /**
5138
+ * List every Workflow registered on the account (paginated, 100/page).
5139
+ * Cloudflare returns the configured class + script binding plus
5140
+ * aggregate instance counts; we only consume the registration metadata.
5141
+ * @see https://developers.cloudflare.com/api/resources/workflows/methods/list/
5142
+ */
5143
+ async workflowListAll() {
5144
+ const all = [];
5145
+ let page = 1;
5146
+ while (true) {
5147
+ const data = await this.request(`/accounts/${this.accountId}/workflows?page=${page}&per_page=100`);
5148
+ const batch = data.result ?? [];
5149
+ all.push(...batch);
5150
+ const totalPages = data.result_info?.total_pages ?? 1;
5151
+ if (page >= totalPages || batch.length === 0) break;
5152
+ page += 1;
5153
+ }
5154
+ return all;
5155
+ }
5156
+ /**
5157
+ * Create or update a Workflow registration. Cloudflare's `PUT` is upsert:
5158
+ * if `workflow_name` exists it patches `class_name` / `script_name` /
5159
+ * `limits`, otherwise it creates a new one. The bound class must already
5160
+ * be deployed in `script_name`'s code (PUT against an unknown class
5161
+ * succeeds at registration time but fails at first instance create).
5162
+ * @see https://developers.cloudflare.com/api/resources/workflows/methods/update/
5163
+ */
5164
+ async workflowUpsert(name, body) {
5165
+ return (await this.request(`/accounts/${this.accountId}/workflows/${encodeURIComponent(name)}`, {
5166
+ method: "PUT",
5167
+ body: JSON.stringify(body)
5168
+ })).result;
5169
+ }
5170
+ /**
5171
+ * Delete a Workflow registration by name. Per Cloudflare's docs this only
5172
+ * removes the workflow itself — the worker that hosts the class is
5173
+ * untouched and existing in-flight instances continue to run.
5174
+ * @see https://developers.cloudflare.com/api/resources/workflows/methods/delete/
5175
+ */
5176
+ async workflowDelete(name) {
5177
+ await this.request(`/accounts/${this.accountId}/workflows/${encodeURIComponent(name)}`, { method: "DELETE" });
5178
+ }
5179
+ /**
5180
+ * List every Secrets Store store on the account (paginated, 100/page).
5181
+ * Stores are account-scoped containers; their internal secret entries
5182
+ * are listed via {@link secretsStoreSecretListAll}.
5183
+ * @see https://developers.cloudflare.com/api/resources/secrets_store/subresources/stores/methods/list/
5184
+ */
5185
+ async secretsStoreListAll() {
5186
+ const all = [];
5187
+ let page = 1;
5188
+ while (true) {
5189
+ const data = await this.request(`/accounts/${this.accountId}/secrets_store/stores?page=${page}&per_page=100`);
5190
+ const batch = data.result ?? [];
5191
+ all.push(...batch);
5192
+ const totalPages = data.result_info?.total_pages ?? 1;
5193
+ if (page >= totalPages || batch.length === 0) break;
5194
+ page += 1;
5195
+ }
5196
+ return all;
5197
+ }
5198
+ /**
5199
+ * Create a new Secrets Store. Per the API a store name uniquely identifies
5200
+ * the container within an account but is **not** itself a primary key —
5201
+ * the assigned `id` is. Tamer enforces uniqueness via the derived
5202
+ * `sec-{logical}-t-{tenantId}-{env}` name and short-circuits to the
5203
+ * existing id when it finds a match in `secretsStoreListAll`.
5204
+ * @see https://developers.cloudflare.com/api/resources/secrets_store/subresources/stores/methods/create/
5205
+ */
5206
+ async secretsStoreCreate(name) {
5207
+ return (await this.request(`/accounts/${this.accountId}/secrets_store/stores`, {
5208
+ method: "POST",
5209
+ body: JSON.stringify({ name })
5210
+ })).result;
5211
+ }
5212
+ /**
5213
+ * Delete a Secrets Store by id. Cloudflare cascades the deletion to all
5214
+ * secrets inside the store, so callers should ensure no live worker is
5215
+ * still binding into this store before invoking.
5216
+ * @see https://developers.cloudflare.com/api/resources/secrets_store/subresources/stores/methods/delete/
5217
+ */
5218
+ async secretsStoreDelete(storeId) {
5219
+ await this.request(`/accounts/${this.accountId}/secrets_store/stores/${encodeURIComponent(storeId)}`, { method: "DELETE" });
5220
+ }
5221
+ async dispatchNamespaceListAll() {
5222
+ return (await this.request(`/accounts/${this.accountId}/workers/dispatch/namespaces`)).result ?? [];
5223
+ }
5224
+ async dispatchNamespaceCreate(namespaceName) {
5225
+ await this.request(`/accounts/${this.accountId}/workers/dispatch/namespaces`, {
5226
+ method: "POST",
5227
+ body: JSON.stringify({ name: namespaceName })
5228
+ });
5229
+ }
5230
+ async dispatchNamespaceDelete(namespaceName) {
5231
+ await this.request(`/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(namespaceName)}`, { method: "DELETE" });
5232
+ }
5233
+ /**
5234
+ * Upload a user Worker module to a Workers for Platforms dispatch namespace (multipart).
5235
+ * @see https://developers.cloudflare.com/api/operations/namespace-worker-script-upload-worker-module
5236
+ */
5237
+ async dispatchNamespaceScriptPut(dispatchNamespace, scriptName, formData) {
5238
+ const url = `${CF_API_BASE}/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(dispatchNamespace)}/scripts/${encodeURIComponent(scriptName)}`;
5239
+ const res = await fetch(url, {
5240
+ method: "PUT",
5241
+ headers: { Authorization: `Bearer ${this.apiToken}` },
5242
+ body: formData
5243
+ });
5244
+ const data = await res.json();
5245
+ if (!res.ok) {
5246
+ const errMsg = data?.errors?.map((e) => e.message).join("; ") ?? res.statusText;
5247
+ throw new Error(`CF API error: ${errMsg}`);
5248
+ }
5249
+ }
5250
+ /**
5251
+ * List every script inside a dispatch namespace.
5252
+ * @see https://developers.cloudflare.com/api/resources/workers_for_platforms/subresources/dispatch/subresources/namespaces/subresources/scripts/methods/list/
5253
+ */
5254
+ async dispatchNamespaceScriptList(dispatchNamespace) {
5255
+ return (await this.request(`/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(dispatchNamespace)}/scripts`)).result ?? [];
5256
+ }
5257
+ /**
5258
+ * Remove a script from a Workers for Platforms dispatch namespace.
5259
+ * @see https://developers.cloudflare.com/api/resources/workers_for_platforms/subresources/dispatch/subresources/namespaces/subresources/scripts/methods/delete/
5260
+ */
5261
+ async dispatchNamespaceScriptDelete(dispatchNamespace, scriptName, options) {
5262
+ const q = options?.force ? "?force=true" : "";
5263
+ const url = `${CF_API_BASE}/accounts/${this.accountId}/workers/dispatch/namespaces/${encodeURIComponent(dispatchNamespace)}/scripts/${encodeURIComponent(scriptName)}${q}`;
5264
+ const res = await fetch(url, {
5265
+ method: "DELETE",
5266
+ headers: { Authorization: `Bearer ${this.apiToken}` }
5267
+ });
5268
+ if (!res.ok) {
5269
+ let errMsg = res.statusText;
5270
+ try {
5271
+ errMsg = (await res.json())?.errors?.map((e) => e.message).join("; ") ?? errMsg;
5272
+ } catch {}
5273
+ throw new Error(`CF API error: ${errMsg}`);
5274
+ }
5275
+ }
5276
+ /**
5277
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/list/
5278
+ */
5279
+ async zoneWorkerRoutesList(zoneId) {
5280
+ return (await this.request(`/zones/${zoneId}/workers/routes`)).result ?? [];
5281
+ }
5282
+ /**
5283
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/create/
5284
+ */
5285
+ async zoneWorkerRouteCreate(zoneId, body) {
5286
+ return (await this.request(`/zones/${zoneId}/workers/routes`, {
5287
+ method: "POST",
5288
+ body: JSON.stringify(body)
5289
+ })).result;
5290
+ }
5291
+ /**
5292
+ * @see https://developers.cloudflare.com/api/resources/workers/subresources/routes/methods/delete/
5293
+ */
5294
+ async zoneWorkerRouteDelete(zoneId, routeId) {
5295
+ await this.request(`/zones/${zoneId}/workers/routes/${encodeURIComponent(routeId)}`, { method: "DELETE" });
5296
+ }
5297
+ /**
5298
+ * List every DNS record on a zone (paginated, 100/page). Returns the
5299
+ * subset of fields Tamer cares about — the full Cloudflare object also
5300
+ * carries `meta`, `proxiable`, `created_on`, `modified_on`, etc., none of
5301
+ * which we persist in state.
5302
+ *
5303
+ * @see https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/list/
5304
+ */
5305
+ async zoneDnsRecordListAll(zoneId) {
5306
+ const all = [];
5307
+ let page = 1;
5308
+ while (true) {
5309
+ const data = await this.request(`/zones/${zoneId}/dns_records?page=${page}&per_page=100`);
5310
+ const batch = data.result ?? [];
5311
+ all.push(...batch);
5312
+ const totalPages = data.result_info?.total_pages ?? 1;
5313
+ if (page >= totalPages || batch.length === 0) break;
5314
+ page += 1;
5315
+ }
5316
+ return all;
5317
+ }
5318
+ /**
5319
+ * Create a DNS record on a zone. The Cloudflare API rejects duplicate
5320
+ * `(type, name, content)` triples for record types where that combination
5321
+ * is meaningful (CNAME, A, AAAA), so callers should pre-check via
5322
+ * {@link zoneDnsRecordListAll}.
5323
+ *
5324
+ * @see https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/
5325
+ */
5326
+ async zoneDnsRecordCreate(zoneId, body) {
5327
+ return (await this.request(`/zones/${zoneId}/dns_records`, {
5328
+ method: "POST",
5329
+ body: JSON.stringify(body)
5330
+ })).result;
5331
+ }
5332
+ /**
5333
+ * Patch a DNS record in place. Cloudflare's PATCH endpoint accepts any
5334
+ * subset of mutable fields (`content`, `ttl`, `proxied`, `priority`,
5335
+ * `comment`) but rejects `type` changes — Tamer falls back to
5336
+ * delete-and-recreate when the type drifts.
5337
+ *
5338
+ * @see https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/
5339
+ */
5340
+ async zoneDnsRecordPatch(zoneId, recordId, body) {
5341
+ await this.request(`/zones/${zoneId}/dns_records/${encodeURIComponent(recordId)}`, {
5342
+ method: "PATCH",
5343
+ body: JSON.stringify(body)
5344
+ });
5345
+ }
5346
+ /**
5347
+ * @see https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/delete/
5348
+ */
5349
+ async zoneDnsRecordDelete(zoneId, recordId) {
5350
+ await this.request(`/zones/${zoneId}/dns_records/${encodeURIComponent(recordId)}`, { method: "DELETE" });
5351
+ }
5352
+ /**
5353
+ * Look up zones by exact name.
5354
+ *
5355
+ * Account-scoped tokens with `Zone Read` see every zone under the account.
5356
+ * Returns an array because Cloudflare allows the same name across accounts;
5357
+ * the caller normally takes `[0]`.
5358
+ *
5359
+ * @see https://developers.cloudflare.com/api/operations/zones-get
5360
+ */
5361
+ async zonesListByName(name) {
5362
+ const qs = new URLSearchParams({
5363
+ name,
5364
+ "account.id": this.accountId,
5365
+ per_page: "50"
5366
+ });
5367
+ return (await this.request(`/zones?${qs.toString()}`)).result ?? [];
5368
+ }
5369
+ /**
5370
+ * List account-scoped Logpush jobs (includes `workers_trace_events`).
5371
+ * @see https://developers.cloudflare.com/api/resources/logpush/subresources/jobs/methods/list/
5372
+ */
5373
+ async logpushAccountJobsList() {
5374
+ return (await this.request(`/accounts/${this.accountId}/logpush/jobs`)).result ?? [];
5375
+ }
5376
+ /**
5377
+ * Fetch one account-scoped Logpush job (includes `destination_conf`).
5378
+ * @see https://developers.cloudflare.com/api/resources/logpush/subresources/jobs/methods/get/
5379
+ */
5380
+ async logpushAccountJobGet(jobId) {
5381
+ const data = await this.request(`/accounts/${this.accountId}/logpush/jobs/${jobId}`);
5382
+ if (!data.result?.id) throw new Error(`Logpush job GET ${jobId}: missing result (job may not exist on this account)`);
5383
+ return data.result;
5384
+ }
5385
+ /**
5386
+ * Create an account Logpush job. Caller supplies `destination_conf` and dataset.
5387
+ * @see https://developers.cloudflare.com/api/resources/logpush/subresources/jobs/methods/create/
5388
+ */
5389
+ async logpushAccountJobCreate(body) {
5390
+ return (await this.request(`/accounts/${this.accountId}/logpush/jobs`, {
5391
+ method: "POST",
5392
+ body: JSON.stringify(body)
5393
+ })).result;
5394
+ }
5395
+ /**
5396
+ * Update mutable Logpush job fields in place.
5397
+ * @see https://developers.cloudflare.com/api/resources/logpush/subresources/jobs/methods/update/
5398
+ */
5399
+ async logpushAccountJobUpdate(jobId, body) {
5400
+ await this.request(`/accounts/${this.accountId}/logpush/jobs/${jobId}`, {
5401
+ method: "PUT",
5402
+ body: JSON.stringify(body)
5403
+ });
5404
+ }
5405
+ /**
5406
+ * Delete a Logpush job by numeric id.
5407
+ * @see https://developers.cloudflare.com/api/resources/logpush/subresources/jobs/methods/delete/
5408
+ */
5409
+ async logpushAccountJobDelete(jobId) {
5410
+ await this.request(`/accounts/${this.accountId}/logpush/jobs/${jobId}`, { method: "DELETE" });
5411
+ }
5412
+ /**
5413
+ * List all permission groups for account-owned API tokens (paginated).
5414
+ * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/subresources/permission_groups/methods/list/
5415
+ */
5416
+ async accountTokenPermissionGroupsListAll() {
5417
+ const all = [];
5418
+ const seen = /* @__PURE__ */ new Set();
5419
+ let page = 1;
5420
+ const perPage = 100;
5421
+ /** Failsafe: pagination must always terminate even if the API omits `total_count` or repeats pages. */
5422
+ const maxPage = 500;
5423
+ while (true) {
5424
+ if (page > maxPage) throw new Error(`Account token permission groups: exceeded ${maxPage} pages (partial count ${all.length}) — refusing to paginate further (check Cloudflare API or file a bug)`);
5425
+ const data = await this.request(`/accounts/${this.accountId}/tokens/permission_groups?per_page=${perPage}&page=${page}`);
5426
+ const batch = data.result ?? [];
5427
+ if (batch.length === 0) break;
5428
+ const newRows = batch.filter((g) => g.id && !seen.has(g.id));
5429
+ for (const g of newRows) seen.add(g.id);
5430
+ if (newRows.length === 0) break;
5431
+ all.push(...newRows);
5432
+ if (batch.length < perPage) break;
5433
+ const total = data.result_info?.total_count;
5434
+ if (typeof total === "number" && all.length >= total) break;
5435
+ page++;
5436
+ }
5437
+ return all;
5438
+ }
5439
+ /**
5440
+ * List all account-owned API tokens (paginated).
5441
+ * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/list/
5442
+ */
5443
+ async accountTokenListAll() {
5444
+ const all = [];
5445
+ let page = 1;
5446
+ const perPage = 50;
5447
+ const maxPage = 500;
5448
+ while (page <= maxPage) {
5449
+ const data = await this.request(`/accounts/${this.accountId}/tokens?per_page=${perPage}&page=${page}`);
5450
+ const batch = data.result ?? [];
5451
+ all.push(...batch);
5452
+ const total = data.result_info?.total_count;
5453
+ if (typeof total === "number" && all.length >= total) break;
5454
+ if (batch.length < perPage) break;
5455
+ page += 1;
5456
+ }
5457
+ return all;
5458
+ }
5459
+ /**
5460
+ * Create an account-owned API token. `result.value` is only returned on create.
5461
+ * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
5462
+ */
5463
+ async accountTokenCreate(body) {
5464
+ return (await this.request(`/accounts/${this.accountId}/tokens`, {
5465
+ method: "POST",
5466
+ body: JSON.stringify(body)
5467
+ })).result;
5468
+ }
5469
+ /**
5470
+ * Delete (revoke) an account-owned API token.
5471
+ * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/delete/
5472
+ */
5473
+ async accountTokenDelete(tokenId) {
5474
+ await this.request(`/accounts/${this.accountId}/tokens/${encodeURIComponent(tokenId)}`, { method: "DELETE" });
5475
+ }
5476
+ };
5477
+
5478
+ //#endregion
5479
+ //#region src/core/secrets/secretsDb.ts
5480
+ /** Max audit rows retained per secret name (oldest dropped on insert). */
5481
+ const SECRET_HISTORY_CAP = 50;
5482
+ /** Cloudflare D1 database for encrypted secrets (`tamer-secrets-dev`, …). */
5483
+ function tamerSecretsDatabaseName(env) {
5484
+ return `tamer-secrets-${env}`;
5485
+ }
5486
+ async function findTamerSecretsDatabaseUuid(api, env) {
5487
+ const name = tamerSecretsDatabaseName(env);
5488
+ return (await api.d1ListAll()).find((d) => d.name === name)?.uuid;
5489
+ }
5490
+ const SECRETS_TABLE_DDL = `CREATE TABLE IF NOT EXISTS secrets (
5491
+ name TEXT PRIMARY KEY,
5492
+ ciphertext BLOB NOT NULL,
5493
+ iv BLOB NOT NULL,
5494
+ wrapped_dek BLOB NOT NULL,
5495
+ dek_iv BLOB NOT NULL,
5496
+ value_hash TEXT NOT NULL,
5497
+ updated_at TEXT NOT NULL,
5498
+ updated_by TEXT
5499
+ )`;
5500
+ const SECRET_HISTORY_TABLE_DDL = `CREATE TABLE IF NOT EXISTS secret_history (
5501
+ name TEXT NOT NULL,
5502
+ value_hash TEXT NOT NULL,
5503
+ updated_at TEXT NOT NULL,
5504
+ updated_by TEXT
5505
+ )`;
5506
+ /**
5507
+ * Create `tamer-secrets-{env}` if missing and ensure `secrets` / `secret_history`
5508
+ * tables. Idempotent — safe to call on every bootstrap or first vault touch.
5509
+ */
5510
+ async function ensureTamerSecretsDatabase(api, env) {
5511
+ let uuid$1 = await findTamerSecretsDatabaseUuid(api, env);
5512
+ if (!uuid$1) uuid$1 = (await api.d1Create(tamerSecretsDatabaseName(env))).uuid;
5513
+ await api.d1Query(uuid$1, SECRETS_TABLE_DDL);
5514
+ await api.d1Query(uuid$1, SECRET_HISTORY_TABLE_DDL);
5515
+ return uuid$1;
5516
+ }
5517
+
5518
+ //#endregion
5519
+ //#region src/core/secrets/masterKey.ts
5520
+ const MASTER_KEY_BYTE_LENGTH = 32;
5521
+ const MASTER_KEY_ENV_PREFIX = "TAMER_SECRETS_KEY_";
5522
+ /** Env var name for the per-env master key (e.g. `TAMER_SECRETS_KEY_dev`). */
5523
+ function masterKeyEnvVarName(env) {
5524
+ return `${MASTER_KEY_ENV_PREFIX}${env}`;
5525
+ }
5526
+ /** Generate a fresh 256-bit master key as base64 (print once, store externally). */
5527
+ function generateMasterKey() {
5528
+ return bytesToBase64(crypto.getRandomValues(new Uint8Array(MASTER_KEY_BYTE_LENGTH)));
5529
+ }
5530
+ /** Parse and validate a base64 master key string into raw key material. */
5531
+ function parseMasterKey(base64$1) {
5532
+ const trimmed = base64$1.trim();
5533
+ if (!trimmed) throw new Error("secrets: master key must be a non-empty base64 string");
5534
+ const bytes = base64ToBytes(trimmed);
5535
+ if (bytes.length !== MASTER_KEY_BYTE_LENGTH) throw new Error(`secrets: master key must decode to ${MASTER_KEY_BYTE_LENGTH} bytes (got ${bytes.length})`);
5536
+ return bytes;
5537
+ }
5538
+ /**
5539
+ * Read the master key for `env` from `process.env`.
5540
+ * Never logs or returns the env var name together with the key value.
5541
+ */
5542
+ function readMasterKeyFromEnv(env) {
5543
+ const varName = masterKeyEnvVarName(env);
5544
+ const raw = process.env[varName];
5545
+ if (!raw) throw new Error(`secrets: master key requires env var ${varName} (set it before running secrets commands)`);
5546
+ return parseMasterKey(raw);
5547
+ }
5548
+ function bytesToBase64(bytes) {
5549
+ return Buffer.from(bytes).toString("base64");
5550
+ }
5551
+ function base64ToBytes(base64$1) {
5552
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64$1)) throw new Error("secrets: master key is not valid base64");
5553
+ return new Uint8Array(Buffer.from(base64$1, "base64"));
5554
+ }
5555
+
5556
+ //#endregion
5557
+ //#region src/core/naming/NamingEngine.ts
5558
+ function reLiteral(s) {
5559
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5560
+ }
5561
+ var NamingEngine = class {
5562
+ constructor(tenant, conventions) {
5563
+ this.tenant = tenant;
5564
+ this.conventions = conventions;
5565
+ }
5566
+ /** Tenant id for per-resource {@link CloudflareNameFn} overrides. */
5567
+ get tenantId() {
5568
+ return this.tenant.id;
5569
+ }
5570
+ d1SingleName(logicalName, env) {
5571
+ if (this.conventions?.d1Single) return this.conventions.d1Single(logicalName, this.tenant.id, env);
5572
+ return `db_${logicalName}_t_${this.tenant.id}_${env}`;
5573
+ }
5574
+ d1ShardName(logicalName, shardDate, env) {
5575
+ if (this.conventions?.d1Shard) return this.conventions.d1Shard(logicalName, shardDate, this.tenant.id, env);
5576
+ const dateNoDashes = shardDate.replace(/-/g, "");
5577
+ if (logicalName === "default" || logicalName === "") return `db_${dateNoDashes}_t_${this.tenant.id}_${env}`;
5578
+ return `db_${logicalName}_${dateNoDashes}_t_${this.tenant.id}_${env}`;
5579
+ }
5580
+ d1SingleBindingKey(logicalName) {
5581
+ return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5582
+ }
5583
+ d1ShardBindingKey(logicalName, shardDate) {
5584
+ const dateNoDashes = shardDate.replace(/-/g, "");
5585
+ return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_${dateNoDashes}_T_${this.tenant.id.toUpperCase()}`;
5586
+ }
5587
+ r2BucketName(logicalName, env) {
5588
+ if (this.conventions?.r2Bucket) {
5589
+ const dateNoDashes = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
5590
+ return this.conventions.r2Bucket(logicalName, dateNoDashes, this.tenant.id, env);
5591
+ }
5592
+ return `r2-${logicalName}-t-${this.tenant.id}-${env}`;
5593
+ }
5594
+ /**
5595
+ * Wrangler `r2_buckets[].binding`: logical + tenant only (same idea as {@link kvBindingKey}).
5596
+ * Stable across envs; default {@link r2BucketName} is `r2-{logical}-t-{tenant}-{env}`.
5597
+ */
5598
+ r2BindingKey(logicalName) {
5599
+ return `R2_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5600
+ }
5601
+ kvNamespaceName(logicalName, env) {
5602
+ return `kv_${logicalName}_t_${this.tenant.id}_${env}`;
5603
+ }
5604
+ kvBindingKey(logicalName) {
5605
+ return `KV_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5606
+ }
5607
+ queueName(logicalName, env) {
5608
+ return `q-${logicalName}-t-${this.tenant.id}-${env}`;
5609
+ }
5610
+ queueBindingKey(logicalName) {
5611
+ return `Q_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5612
+ }
5613
+ hyperdriveName(logicalName, env) {
5614
+ return `hd-${logicalName}-t-${this.tenant.id}-${env}`;
5615
+ }
5616
+ hyperdriveBindingKey(logicalName) {
5617
+ return `HD_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5618
+ }
5619
+ vectorizeName(logicalName, env) {
5620
+ return `vec-${logicalName}-t-${this.tenant.id}-${env}`;
5621
+ }
5622
+ vectorizeBindingKey(logicalName) {
5623
+ return `VEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5624
+ }
5625
+ /**
5626
+ * AI Gateway slug (== Cloudflare gateway id). Per the Cloudflare API,
5627
+ * gateway ids are case-sensitive lowercase ascii with `-`/`_`. Format:
5628
+ * `aigw-{logical}-t-{tenantId}-{env}`.
5629
+ */
5630
+ aiGatewayId(logicalName, env) {
5631
+ return `aigw-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
5632
+ }
5633
+ /**
5634
+ * Stable cross-reference binding key for AI Gateway. AI Gateway has no
5635
+ * Wrangler binding kind today, so this is only consumed by
5636
+ * `${tamer:ai_gateway:<logical>.binding}` interpolations.
5637
+ */
5638
+ aiGatewayBindingKey(logicalName) {
5639
+ return `AI_GW_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5640
+ }
5641
+ /**
5642
+ * Cloudflare-side pipeline name. Pipelines V1 names must be lowercase
5643
+ * alphanumerics + hyphens (no underscores), so we mirror the R2 scheme.
5644
+ * Pattern: `pipe-{logical}-t-{tenantId}-{env}`.
5645
+ */
5646
+ pipelineName(logicalName, env) {
5647
+ return `pipe-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
5648
+ }
5649
+ /**
5650
+ * Wrangler binding key emitted in `pipelines[]`. Uppercased logical with
5651
+ * the tenant id appended so two tenants can share a worker namespace
5652
+ * without colliding bindings.
5653
+ */
5654
+ pipelineBindingKey(logicalName) {
5655
+ return `PIPE_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5656
+ }
5657
+ pipelineMatchPattern(logicalName, env) {
5658
+ const exact = this.pipelineName(logicalName, env);
5659
+ return (name) => name === exact;
5660
+ }
5661
+ /**
5662
+ * Cloudflare-side workflow name. Workflow names accept lowercase
5663
+ * alphanumerics + hyphens; we mirror the pipelines/AI-Gateway hyphen
5664
+ * scheme. Pattern: `wf-{logical}-t-{tenantId}-{env}`.
5665
+ */
5666
+ workflowName(logicalName, env) {
5667
+ if (this.conventions?.workflow) return this.conventions.workflow(logicalName, this.tenant.id, env);
5668
+ return `wf-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
5669
+ }
5670
+ /**
5671
+ * Wrangler binding key emitted in `workflows[]`. Uppercased logical with
5672
+ * the tenant id appended so two tenants sharing a script can't collide.
5673
+ */
5674
+ workflowBindingKey(logicalName) {
5675
+ return `WF_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5676
+ }
5677
+ workflowMatchPattern(logicalName, env) {
5678
+ const exact = this.workflowName(logicalName, env);
5679
+ return (name) => name === exact;
5680
+ }
5681
+ /**
5682
+ * Cloudflare Secrets Store name. Account-scoped — the API allows free-form
5683
+ * names (the dashboard examples use both `service_x_keys` and dashed
5684
+ * variants), so we mirror our hyphen convention for consistency with
5685
+ * pipelines / workflows / AI Gateway. Pattern:
5686
+ * `sec-{logical}-t-{tenantId}-{env}`.
5687
+ */
5688
+ secretsStoreName(logicalName, env) {
5689
+ return `sec-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
5690
+ }
5691
+ /**
5692
+ * Stable cross-reference key for the store. Secrets Store has no Wrangler
5693
+ * binding kind directly — `secrets_store_secrets[]` references the
5694
+ * resolved `store_id`, not the store name — so this only powers
5695
+ * `${tamer:secret_store:<n>.binding}` interpolations.
5696
+ */
5697
+ secretsStoreBindingKey(logicalName) {
5698
+ return `SEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
5699
+ }
5700
+ secretsStoreMatchPattern(logicalName, env) {
5701
+ const exact = this.secretsStoreName(logicalName, env);
5702
+ return (name) => name === exact;
5703
+ }
5704
+ workerName(workerKey, env) {
5705
+ if (this.conventions?.workerName) return this.conventions.workerName(this.tenant.slug, workerKey, env, this.tenant.id);
5706
+ if (env === "local") return `${this.tenant.slug}-${workerKey}-${this.tenant.id}`;
5707
+ return `${this.tenant.slug}-${workerKey}-${env}-${this.tenant.id}`;
5708
+ }
5709
+ /** Whether stack {@link NamingConventions.d1Shard} is configured. */
5710
+ hasD1ShardConvention() {
5711
+ return Boolean(this.conventions?.d1Shard);
5712
+ }
5713
+ d1MatchPattern(logicalName, env) {
5714
+ if (this.conventions?.d1Shard) return (name) => {
5715
+ const shardDate = this.extractD1ShardDate(name);
5716
+ if (!shardDate) return false;
5717
+ return this.d1ShardName(logicalName, shardDate, env) === name;
5718
+ };
5719
+ const suffix = `_t_${this.tenant.id}_${env}`;
5720
+ if (logicalName === "default" || logicalName === "") return (name) => /^db_\d{8}_t_/.test(name) && name.endsWith(suffix);
5721
+ const prefix = `db_${logicalName}_`;
5722
+ return (name) => name.startsWith(prefix) && name.endsWith(suffix);
5723
+ }
5724
+ /**
5725
+ * Default: exact {@link r2BucketName}, or legacy dated buckets
5726
+ * `r2-{logical}-{YYYYMMDD}-t-{tenant}-{env}` from older Tamer versions.
5727
+ * Custom {@link NamingConventions.r2Bucket}: exact name only (uses today's date
5728
+ * when calling the hook, same as apply).
5729
+ */
5730
+ r2MatchPattern(logicalName, env) {
5731
+ if (this.conventions?.r2Bucket) {
5732
+ const expected = this.r2BucketName(logicalName, env);
5733
+ return (name) => name === expected;
5734
+ }
5735
+ const exactNew = `r2-${logicalName}-t-${this.tenant.id}-${env}`;
5736
+ const legacyDated = /* @__PURE__ */ new RegExp(`^r2-${reLiteral(logicalName)}-\\d{8}-t-${reLiteral(this.tenant.id)}-${reLiteral(env)}$`);
5737
+ return (name) => name === exactNew || legacyDated.test(name);
5738
+ }
5739
+ extractD1ShardDate(name) {
5740
+ const compact = name.match(/_(\d{8})_t_/);
5741
+ if (compact) {
5742
+ const d = compact[1];
5743
+ return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
5744
+ }
5745
+ const underscored = name.match(/_(\d{4})_(\d{2})_(\d{2})_t_/);
5746
+ if (underscored) return `${underscored[1]}-${underscored[2]}-${underscored[3]}`;
5747
+ return null;
5748
+ }
5749
+ extractR2Date(name) {
5750
+ const match = name.match(/r2-\w+-(\d{8})-t-/);
5751
+ if (match) {
5752
+ const d = match[1];
5753
+ return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
5754
+ }
5755
+ return null;
5756
+ }
5757
+ };
5758
+
5759
+ //#endregion
5760
+ //#region src/core/config/namingFromConfig.ts
5761
+ function namingFromConfig(config$1) {
5762
+ const conventions = "naming" in config$1 && config$1.naming ? config$1.naming : void 0;
5763
+ return new NamingEngine(config$1.tenant, conventions);
5764
+ }
5765
+
5766
+ //#endregion
5767
+ //#region src/features/dispatch-namespace/dispatch-namespace.resolve.ts
5768
+ const ephemeralPredicateCache = /* @__PURE__ */ new WeakMap();
5769
+ function ephemeralPredicateFor(tenant) {
5770
+ if (ephemeralPredicateCache.has(tenant)) return ephemeralPredicateCache.get(tenant) ?? null;
5771
+ const pat = tenant.ephemeralEnvPattern;
5772
+ const compiled = pat ? new RegExp(pat) : null;
5773
+ ephemeralPredicateCache.set(tenant, compiled);
5774
+ return compiled;
5775
+ }
5776
+ /**
5777
+ * `true` when `env` matches `tenant.ephemeralEnvPattern` (e.g.
5778
+ * `"^pr-"` for PR previews, `"^(pr|feature)-"` for branch previews).
5779
+ *
5780
+ * Ephemeral envs share **one** dispatch namespace
5781
+ * (`{ns}-ephemeral`) so we don't churn dispatch-namespace creates per
5782
+ * preview, and dispatch-script names carry the env suffix
5783
+ * (`{product}-{workspace}-{env}`) so multiple previews can coexist
5784
+ * inside that shared namespace. When the config doesn't pin a
5785
+ * pattern, no env is ephemeral — every env gets its own dispatch
5786
+ * namespace `{ns}-{env}`.
5787
+ */
5788
+ function isEphemeralEnv(env, tenant) {
5789
+ const re = ephemeralPredicateFor(tenant);
5790
+ if (!re) return false;
5791
+ return re.test(env);
5792
+ }
5793
+ /** Resolved Cloudflare dispatch namespace name for the given env. */
5794
+ function effectiveDispatchNamespaceName(config$1, env, tenant) {
5795
+ if (config$1.envSuffix) {
5796
+ if (env === "local") return config$1.namespace;
5797
+ if (isEphemeralEnv(env, tenant)) return `${config$1.namespace}-ephemeral`;
5798
+ return `${config$1.namespace}-${env}`;
5799
+ }
5800
+ return config$1.namespace;
5801
+ }
5802
+
5803
+ //#endregion
5804
+ //#region src/core/routes/routes.resolve.ts
5805
+ const DEFAULT_PROD_ENVS = ["prod", "production"];
5806
+ const DEFAULT_SKIP_ENVS = ["local"];
5807
+ /**
5808
+ * Per `docs/handoff.md` §6:
5809
+ * - prod / production → bare apex (`todo.com`)
5810
+ * - any other env (including ephemeral `pr-*`) → `{env}.{apex}`
5811
+ * - `local` (or anything in `skipEnvs`) → no route
5812
+ *
5813
+ * The resource-name `-{env}` suffix on workers/D1/R2/KV is decoupled from
5814
+ * URLs; this function only computes the URL.
5815
+ */
5816
+ function effectiveHostForEnv(route, env) {
5817
+ if ((route.skipEnvs ?? DEFAULT_SKIP_ENVS).includes(env)) return void 0;
5818
+ if ((route.prodEnvs ?? DEFAULT_PROD_ENVS).includes(env)) return route.host;
5819
+ return `${env}.${route.host}`;
5820
+ }
5821
+ /**
5822
+ * Expand one Tamer route into a wrangler `Route` (or `undefined` if the env
5823
+ * should not receive the route at all).
5824
+ */
5825
+ function expandRouteForEnv(route, env) {
5826
+ const host = effectiveHostForEnv(route, env);
5827
+ if (!host) return void 0;
5828
+ const zone = route.zone ?? route.host;
5829
+ if (route.customDomain) return {
5830
+ pattern: host,
5831
+ custom_domain: true
5832
+ };
5833
+ return {
5834
+ pattern: `${host}${route.path ?? "/*"}`,
5835
+ zone_name: zone
5836
+ };
5837
+ }
5838
+ /**
5839
+ * Expand `tamerRoutes` for the given env, dropping any that resolve to
5840
+ * `undefined` (`local`, `skipEnvs`).
5841
+ *
5842
+ * Note: ephemeral envs (matching `tenant.ephemeralEnvPattern`) follow the
5843
+ * same `{env}.{apex}` prefix rule as any non-prod env — e.g. an env named
5844
+ * `pr-1234` resolves to `pr-1234.todo.com`. Callers that want a different
5845
+ * URL scheme for ephemeral envs should special-case before calling.
5846
+ */
5847
+ function effectiveRoutesForEnv(tamerRoutes, env) {
5848
+ if (!tamerRoutes || tamerRoutes.length === 0) return [];
5849
+ const out = [];
5850
+ for (const r of tamerRoutes) {
5851
+ const expanded = expandRouteForEnv(r, env);
5852
+ if (expanded) out.push(expanded);
5853
+ }
5854
+ return out;
5855
+ }
5856
+ /**
5857
+ * Zone-name routes are attached via the Cloudflare Workers Routes HTTP API
5858
+ * (`/zones/{id}/workers/routes`), not via `wrangler.json`, so deploys stay
5859
+ * consistent with `tamer drift` / destroy.
5860
+ */
5861
+ function isApiManagedZoneRoute(r) {
5862
+ return typeof r === "object" && r !== null && "zone_name" in r && typeof r.zone_name === "string" && "pattern" in r && typeof r.pattern === "string" && !("custom_domain" in r && r.custom_domain === true);
5863
+ }
5864
+ /** Custom-domain `tamerRoutes` stay in wrangler until a dedicated API path exists. */
5865
+ function isWranglerOnlyTamerRoute(r) {
5866
+ return typeof r === "object" && r !== null && "custom_domain" in r && r.custom_domain === true;
5867
+ }
5868
+
5869
+ //#endregion
5870
+ //#region src/core/wrangler/wranglerOutFile.ts
5871
+ /** Reject path segments in generated Wrangler config filename. */
5872
+ function assertSafeWranglerOutFile(name) {
5873
+ const trimmed = name.trim();
5874
+ if (!trimmed) throw new Error("wranglerOutFile cannot be empty");
5875
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("..")) throw new Error(`Invalid wranglerOutFile "${name}": use a basename only (e.g. wrangler.json)`);
5876
+ return trimmed;
5877
+ }
5878
+ /** Extra CLI args so Wrangler uses a non-default config file. */
5879
+ function wranglerConfigCliArgs(outFile) {
5880
+ if (outFile === "wrangler.json") return [];
5881
+ return ["--config", outFile];
5882
+ }
5883
+
5884
+ //#endregion
5885
+ //#region src/core/references/references.ts
5886
+ var TamerReferenceError = class extends Error {
5887
+ constructor(message, fieldPath) {
5888
+ super(`${message} (at ${fieldPath})`);
5889
+ this.fieldPath = fieldPath;
5890
+ this.name = "TamerReferenceError";
5891
+ }
5892
+ };
5893
+ const REF_RE = /\$\{tamer:([a-z0-9_]+):([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
5894
+ /**
5895
+ * Scan a string for any `${tamer:...}` references. Returns true when at
5896
+ * least one reference is present (used for cheap pre-checks).
5897
+ */
5898
+ function stringHasReference(s) {
5899
+ REF_RE.lastIndex = 0;
5900
+ return REF_RE.test(s);
5901
+ }
5902
+ /**
5903
+ * Resolve every `${tamer:...}` reference in `value`. Replacement preserves
5904
+ * surrounding text for partial-string interpolation. `fieldPath` is included
5905
+ * in any thrown {@link TamerReferenceError} for actionable diagnostics.
5906
+ */
5907
+ function resolveReferencesInString(value, ctx, fieldPath) {
5908
+ if (!stringHasReference(value)) return value;
5909
+ return value.replace(REF_RE, (match, kind, logicalName, field) => {
5910
+ try {
5911
+ return lookupReference(kind, logicalName, field, ctx, fieldPath);
5912
+ } catch (err) {
5913
+ if (ctx.tolerant && err instanceof TamerReferenceError) return match;
5914
+ throw err;
5915
+ }
5916
+ });
5917
+ }
5918
+ /**
5919
+ * Walk a `vars` record (or any flat string→string map) replacing references
5920
+ * in every value. Returns a new object; the input is not mutated.
5921
+ */
5922
+ function resolveReferencesInVars(vars, ctx, fieldPathPrefix) {
5923
+ if (!vars) return vars;
5924
+ const out = {};
5925
+ for (const [key, value] of Object.entries(vars)) {
5926
+ if (typeof value !== "string") {
5927
+ out[key] = value;
5928
+ continue;
5929
+ }
5930
+ out[key] = resolveReferencesInString(value, ctx, `${fieldPathPrefix}.${key}`);
5931
+ }
5932
+ return out;
5933
+ }
5934
+ function lookupReference(kind, logicalName, field, ctx, fieldPath) {
5935
+ switch (kind) {
5936
+ case "d1": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "d1_database" && entry.logicalName === logicalName);
5937
+ case "r2": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "r2_bucket" && entry.logicalName === logicalName);
5938
+ case "kv": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "kv_namespace" && entry.logicalName === logicalName);
5939
+ case "queue": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "queue" && entry.logicalName === logicalName);
5940
+ case "hyperdrive": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "hyperdrive" && entry.logicalName === logicalName);
5941
+ case "vectorize": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "vectorize" && entry.logicalName === logicalName);
5942
+ case "ai_gateway": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "ai_gateway" && entry.logicalName === logicalName);
5943
+ case "pipeline": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "pipeline" && entry.logicalName === logicalName);
5944
+ case "workflow": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "workflow" && entry.logicalName === logicalName);
5945
+ case "secret_store": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "secrets_store" && entry.logicalName === logicalName);
5946
+ case "dispatch_namespace": return lookupDispatchNamespace(ctx, logicalName, field, fieldPath);
5947
+ case "worker": return lookupWorker(ctx, logicalName, field, fieldPath);
5948
+ case "import": return lookupImport(ctx, logicalName, field, fieldPath);
5949
+ case "logpush_pipelines": return lookupLogpushPipelines(ctx, logicalName, field, fieldPath);
5950
+ case "config": return lookupConfigField(ctx, logicalName, field, fieldPath);
5951
+ default: throw new TamerReferenceError(`Unknown reference kind "${kind}" — expected one of d1 | r2 | kv | queue | hyperdrive | vectorize | ai_gateway | pipeline | workflow | secret_store | dispatch_namespace | worker | import | logpush_pipelines | config`, fieldPath);
5952
+ }
5953
+ }
5954
+ function lookupConfigField(ctx, _logicalName, field, fieldPath) {
5955
+ if (field === "account_id") {
5956
+ const id = (ctx.accountId ?? ctx.config.account_id ?? "").trim();
5957
+ if (!id) throw new TamerReferenceError(`Reference \${tamer:config:stack.account_id} needs the stack Cloudflare account id — set top-level account_id in tamer/project.config.ts or pass CLOUDFLARE_ACCOUNT_ID when running tamer.`, fieldPath);
5958
+ return id;
5959
+ }
5960
+ throw new TamerReferenceError(`Unknown field "${field}" on config reference — expected account_id`, fieldPath);
5961
+ }
5962
+ function getLogpushPipelinesEntry(ctx, logicalName, fieldPath) {
5963
+ const all = ctx.state.getAll();
5964
+ const entry = Object.values(all).find((e) => e.type === "logpush_pipelines" && e.logicalName === logicalName);
5965
+ if (!entry) throw new TamerReferenceError(`Reference \${tamer:logpush_pipelines:${logicalName}.…} cannot be resolved — no pipelines graph in state for logpush job "${logicalName}". Run a full \`tamer apply\` (including the Logpush / pipelinesAuto step) for env "${ctx.env}" first.`, fieldPath);
5966
+ return entry;
5967
+ }
5968
+ /**
5969
+ * Exposes fields from the `logpush_pipelines:*` D1 state row (after
5970
+ * `ensurePipelinesLogpushProvision` has run) for `outputs` and `vars`, e.g.
5971
+ * `${tamer:logpush_pipelines:workers-trace.r2_data_catalog_table_name}`.
5972
+ */
5973
+ function lookupLogpushPipelines(ctx, logicalName, field, fieldPath) {
5974
+ const entry = getLogpushPipelinesEntry(ctx, logicalName, fieldPath);
5975
+ switch (field) {
5976
+ case "r2_data_catalog_table_name":
5977
+ case "iceberg_table": {
5978
+ const v = entry.r2DataCatalogTableName?.trim();
5979
+ if (!v) throw new TamerReferenceError(`logpush_pipelines state for "${logicalName}" has no r2DataCatalogTableName (sink not yet created, or pre-upgrade state). Re-run a full \`tamer apply\` for env "${ctx.env}" after pipelinesAuto provisions the sink, or set table manually in consuming stack.`, fieldPath);
5980
+ return v;
5981
+ }
5982
+ case "r2_data_catalog_table_name_pipelines":
5983
+ case "iceberg_table_pipelines": {
5984
+ const v = entry.r2DataCatalogTableNamePipelines?.trim();
5985
+ if (v) return v;
5986
+ const fallback = entry.r2DataCatalogTableName?.trim();
5987
+ if (!fallback) throw new TamerReferenceError(`logpush_pipelines state for "${logicalName}" has no table name (sink not yet created). Re-run \`tamer apply\` for env "${ctx.env}".`, fieldPath);
5988
+ return fallback;
5989
+ }
5990
+ case "r2_data_catalog_namespace":
5991
+ case "iceberg_namespace": return (entry.r2DataCatalogNamespace ?? "default").trim() || "default";
5992
+ case "name": return entry.pipelineName;
5993
+ case "id": return entry.pipelineId;
5994
+ default: throw new TamerReferenceError(`Unknown field "${field}" on logpush_pipelines reference — expected r2_data_catalog_table_name | r2_data_catalog_table_name_pipelines | r2_data_catalog_namespace | name | id | iceberg_table | iceberg_table_pipelines | iceberg_namespace`, fieldPath);
5995
+ }
5996
+ }
5997
+ /**
5998
+ * Resolve a `${tamer:import:<stack>.<output>}` against pre-fetched sibling
5999
+ * stack outputs. The pre-fetch (`fetchStackImports`) loads every imported
6000
+ * stack's `cfi_state:{stack}` row before resolution begins; this lookup is
6001
+ * pure map access. Throws if the stack isn't in `ctx.imports` (config
6002
+ * never declared it / pre-fetch wasn't wired in for this command) or if
6003
+ * the named output hasn't been published yet (sibling stack hasn't run
6004
+ * `apply` since the output was declared, or has been destroyed).
6005
+ */
6006
+ function lookupImport(ctx, stackName, outputName, fieldPath) {
6007
+ const imports = ctx.imports;
6008
+ if (!imports || !(stackName in imports)) throw new TamerReferenceError(`Reference \${tamer:import:${stackName}.${outputName}} cannot be resolved — no imported stack "${stackName}" available. Ensure stack "${stackName}" exists in env "${ctx.env}" (run 'tamer apply' there first) and that the current command pre-fetches sibling stacks.`, fieldPath);
6009
+ const stackOutputs = imports[stackName];
6010
+ if (!(outputName in stackOutputs)) {
6011
+ const available = Object.keys(stackOutputs).sort();
6012
+ throw new TamerReferenceError(`Reference \${tamer:import:${stackName}.${outputName}} cannot be resolved — output "${outputName}" not found on imported stack "${stackName}".${available.length > 0 ? ` Available outputs on stack "${stackName}": ${available.join(", ")}.` : ` Stack "${stackName}" has no published outputs — run 'tamer apply' there with an \`outputs:\` block.`}`, fieldPath);
6013
+ }
6014
+ return stackOutputs[outputName];
6015
+ }
6016
+ function lookupResource(ctx, kind, logicalName, field, fieldPath, predicate) {
6017
+ const all = ctx.state.getAll();
6018
+ const entry = Object.values(all).find(predicate);
6019
+ if (!entry) throw new TamerReferenceError(`Reference \${tamer:${kind}:${logicalName}.${field}} cannot be resolved — no ${kind} resource named "${logicalName}" in state. Run 'tamer apply --env ${ctx.env}' first.`, fieldPath);
6020
+ switch (field) {
6021
+ case "name": return resourceName(entry);
6022
+ case "id": return resourceId(entry, kind, logicalName, fieldPath);
6023
+ case "binding": return resourceBinding(entry);
6024
+ default: throw new TamerReferenceError(`Unknown field "${field}" on ${kind} reference — expected name | id | binding`, fieldPath);
6025
+ }
6026
+ }
6027
+ function resourceName(entry) {
6028
+ switch (entry.type) {
6029
+ case "d1_database":
6030
+ case "kv_namespace":
6031
+ case "queue":
6032
+ case "hyperdrive":
6033
+ case "vectorize":
6034
+ case "ai_gateway":
6035
+ case "pipeline":
6036
+ case "workflow":
6037
+ case "secrets_store":
6038
+ case "dispatch_namespace":
6039
+ case "r2_bucket": return entry.derivedName;
6040
+ case "dns_record": return entry.name;
6041
+ case "logpush_job": return entry.derivedName;
6042
+ case "logpush_pipelines": return entry.pipelineName;
6043
+ case "worker_route": return entry.pattern;
6044
+ case "secret": throw new Error("internal: secret state entries have no .name reference — secrets are not cross-referenced via ${tamer:…}");
6045
+ }
6046
+ }
6047
+ function resourceId(entry, kind, logicalName, fieldPath) {
6048
+ switch (entry.type) {
6049
+ case "d1_database":
6050
+ case "kv_namespace":
6051
+ case "queue":
6052
+ case "hyperdrive":
6053
+ case "vectorize":
6054
+ case "ai_gateway":
6055
+ case "pipeline":
6056
+ case "workflow":
6057
+ case "secrets_store": return entry.cfId;
6058
+ case "r2_bucket": throw new TamerReferenceError(`R2 bucket "${logicalName}" has no .id (R2 buckets are addressed by name); use \${tamer:${kind}:${logicalName}.name}`, fieldPath);
6059
+ case "dispatch_namespace": return entry.derivedName;
6060
+ case "dns_record": return entry.recordId;
6061
+ case "logpush_job": return String(entry.cfJobId);
6062
+ case "logpush_pipelines": return entry.pipelineId;
6063
+ case "worker_route": return entry.routeId;
6064
+ case "secret": throw new Error("internal: secret state entries have no .id — secrets are not cross-referenced via ${tamer:…}");
6065
+ }
6066
+ }
6067
+ function resourceBinding(entry) {
6068
+ switch (entry.type) {
6069
+ case "d1_database":
6070
+ case "r2_bucket":
6071
+ case "kv_namespace":
6072
+ case "queue":
6073
+ case "hyperdrive":
6074
+ case "vectorize":
6075
+ case "ai_gateway":
6076
+ case "pipeline":
6077
+ case "workflow":
6078
+ case "secrets_store": return entry.bindingKey;
6079
+ case "dispatch_namespace": return entry.derivedName;
6080
+ case "dns_record": throw new Error("internal: dns_record has no .binding — use .name or .id in config references");
6081
+ case "logpush_job": throw new Error("internal: logpush_job has no .binding — use .name or .id in config references");
6082
+ case "logpush_pipelines": throw new Error("internal: logpush_pipelines has no .binding — use .name or .id in config references");
6083
+ case "worker_route": return entry.routeId;
6084
+ case "secret": throw new Error("internal: secret state entries have no .binding — secrets are not cross-referenced via ${tamer:…}");
6085
+ }
6086
+ }
6087
+ function lookupDispatchNamespace(ctx, logicalName, field, fieldPath) {
6088
+ if (!getDispatchNamespaces(ctx.config).find((d) => d.logicalName === logicalName)) throw new TamerReferenceError(`Reference \${tamer:dispatch_namespace:${logicalName}.${field}} cannot be resolved — no dispatchNamespaces entry named "${logicalName}" in the Tamer project config`, fieldPath);
6089
+ if (field !== "name" && field !== "id") throw new TamerReferenceError(`Unknown field "${field}" on dispatch_namespace reference — expected name | id`, fieldPath);
6090
+ const all = ctx.state.getAll();
6091
+ const stateEntry = Object.values(all).find((e) => e.type === "dispatch_namespace" && e.logicalName === logicalName);
6092
+ if (!stateEntry || stateEntry.type !== "dispatch_namespace") throw new TamerReferenceError(`Reference \${tamer:dispatch_namespace:${logicalName}.${field}} unresolved — no state entry. Run 'tamer apply --env ${ctx.env}' first.`, fieldPath);
6093
+ return stateEntry.derivedName;
6094
+ }
6095
+ function lookupWorker(ctx, workerKey, field, fieldPath) {
6096
+ let target;
6097
+ if (ctx.config.workers && ctx.config.workers[workerKey]) target = ctx.config.workers[workerKey];
6098
+ else if (ctx.config.worker && workerKey === "default") target = ctx.config.worker;
6099
+ if (!target) throw new TamerReferenceError(`Reference \${tamer:worker:${workerKey}.${field}} cannot be resolved — no worker key "${workerKey}" in the Tamer project config`, fieldPath);
6100
+ if (field !== "name") throw new TamerReferenceError(`Unknown field "${field}" on worker reference — only "name" is supported (the env-suffixed deployed script name)`, fieldPath);
6101
+ return resolveDeployedWorkerName(ctx.config, workerKey, target, ctx.env, ctx.naming);
6102
+ }
6103
+
6104
+ //#endregion
6105
+ //#region src/core/config/resolver.ts
6106
+ /** Wrangler script name after env suffix rules (matches `tamer deploy`). */
6107
+ function resolveDeployedWorkerName(_config, workerKey, workerConfig, env, naming) {
6108
+ const sn = workerConfig.scriptName?.trim();
6109
+ if (sn) {
6110
+ if (env === "local") return sn;
6111
+ return `${sn}-${env}`;
6112
+ }
6113
+ return naming.workerName(workerKey, env);
6114
+ }
6115
+ /**
6116
+ * Map from base service-binding target name (`scriptName` for envs other than
6117
+ * `local`, or the local-deployed name) → env-suffixed deployed name for every
6118
+ * worker in `config`. Used to auto-rewrite intra-stack `services[].service`
6119
+ * fields so fixtures don't have to repeat env overrides for every env.
6120
+ */
6121
+ function buildIntraStackScriptNameMap(config$1, env, naming) {
6122
+ const map = /* @__PURE__ */ new Map();
6123
+ if (config$1.workers) for (const [key, wc] of Object.entries(config$1.workers)) {
6124
+ const baseName = wc.scriptName?.trim() ?? naming.workerName(key, "local");
6125
+ const deployed = resolveDeployedWorkerName(config$1, key, wc, env, naming);
6126
+ map.set(baseName, deployed);
6127
+ }
6128
+ if (config$1.worker) {
6129
+ const w = config$1.worker;
6130
+ const baseName = w.scriptName?.trim() ?? naming.workerName("default", "local");
6131
+ const deployed = resolveDeployedWorkerName(config$1, "default", w, env, naming);
6132
+ map.set(baseName, deployed);
6133
+ }
6134
+ return map;
6135
+ }
6136
+ /** Returns a worker config copy whose `services[].service` is rewritten to env-suffixed deployed names. */
6137
+ function rewriteIntraStackServiceTargets(workerConfig, baseToDeployed) {
6138
+ const services = workerConfig.services;
6139
+ if (!services || services.length === 0) return workerConfig;
6140
+ let rewroteAny = false;
6141
+ const rewritten = services.map((s) => {
6142
+ const target = baseToDeployed.get(s.service);
6143
+ if (!target || target === s.service) return s;
6144
+ rewroteAny = true;
6145
+ return {
6146
+ ...s,
6147
+ service: target
6148
+ };
6149
+ });
6150
+ if (!rewroteAny) return workerConfig;
6151
+ return {
6152
+ ...workerConfig,
6153
+ services: rewritten
6154
+ };
6155
+ }
6156
+ function mergeVars(base = {}, override = {}) {
6157
+ return {
6158
+ ...base,
6159
+ ...override
6160
+ };
6161
+ }
6162
+ /** Same env merge as `resolveWorkerConfig` (for deploy topo-sort service edges). */
6163
+ function mergedWorkerConfigForEnv(workerConfig, env, tenant) {
6164
+ let merged = { ...workerConfig };
6165
+ if (env === "local" && workerConfig.local) merged = {
6166
+ ...merged,
6167
+ ...workerConfig.local,
6168
+ vars: mergeVars(workerConfig.vars, workerConfig.local.vars)
6169
+ };
6170
+ else if (workerConfig.env?.[env]) {
6171
+ const envOverride = workerConfig.env[env];
6172
+ merged = {
6173
+ ...merged,
6174
+ ...envOverride,
6175
+ vars: mergeVars(workerConfig.vars, envOverride.vars)
6176
+ };
6177
+ }
6178
+ const mv = merged.vars;
6179
+ if (mv && Object.prototype.hasOwnProperty.call(mv, "BRANCH_SUFFIX")) merged = {
6180
+ ...merged,
6181
+ vars: {
6182
+ ...mv,
6183
+ BRANCH_SUFFIX: isEphemeralEnv(env, tenant) ? env : ""
6184
+ }
6185
+ };
6186
+ return merged;
6187
+ }
6188
+ /**
6189
+ * Align `dispatch_namespaces[].namespace` and `vars.WFP_NAMESPACE` with
6190
+ * {@link effectiveDispatchNamespaceName} for envs that have no explicit
6191
+ * `worker.env[env]` block (e.g. `pr-*` shared namespace).
6192
+ */
6193
+ function applyDispatchNamespaceEnvOverrides(config$1, merged, env) {
6194
+ const dns = getDispatchNamespaces(config$1);
6195
+ if (dns.length === 0) return merged;
6196
+ const resolved = effectiveDispatchNamespaceName(dns[0], env, config$1.tenant);
6197
+ const m = merged;
6198
+ let next = merged;
6199
+ if (m.dispatch_namespaces?.length) next = {
6200
+ ...next,
6201
+ dispatch_namespaces: m.dispatch_namespaces.map((d) => ({
6202
+ ...d,
6203
+ namespace: resolved
6204
+ }))
6205
+ };
6206
+ if (m.vars && typeof m.vars.WFP_NAMESPACE === "string" && m.vars.WFP_NAMESPACE === dns[0].namespace) {
6207
+ const v = next.vars;
6208
+ next = {
6209
+ ...next,
6210
+ vars: {
6211
+ ...v,
6212
+ WFP_NAMESPACE: resolved
6213
+ }
6214
+ };
6215
+ }
6216
+ return next;
6217
+ }
6218
+ /**
6219
+ * Walk the merged worker config and replace `${tamer:<kind>:<logical>.<field>}`
6220
+ * references in `vars` and `tamerRoutes[].host` / `.zone` against the current
6221
+ * state snapshot. Also resolves `r2_buckets[].bucket_name`,
6222
+ * **`services[].service`**, **`dispatch_namespaces[].namespace`**, and
6223
+ * `resources.d1[].databaseName` when `ownership` is `external`. Throws
6224
+ * `TamerReferenceError` (with field path) if any
6225
+ * reference is unresolved — bubbles up to the caller as a fatal.
6226
+ */
6227
+ function resolveCrossResourceReferences(merged, ctx) {
6228
+ const refCtx = {
6229
+ config: ctx.config,
6230
+ env: ctx.env,
6231
+ state: ctx.state,
6232
+ naming: ctx.naming,
6233
+ tolerant: ctx.tolerant,
6234
+ imports: ctx.imports,
6235
+ accountId: ctx.accountId
6236
+ };
6237
+ const m = merged;
6238
+ let next = merged;
6239
+ if (m.vars) {
6240
+ const resolvedVars = resolveReferencesInVars(materializeVars(m.vars), refCtx, `worker[${ctx.workerKey}].vars`);
6241
+ if (resolvedVars && resolvedVars !== m.vars) next = {
6242
+ ...next,
6243
+ vars: resolvedVars
6244
+ };
6245
+ }
6246
+ const tamerRoutes = next.tamerRoutes;
6247
+ if (tamerRoutes && tamerRoutes.length > 0) {
6248
+ let mutated = false;
6249
+ const resolvedRoutes = tamerRoutes.map((r, idx) => {
6250
+ const fieldBase = `worker[${ctx.workerKey}].tamerRoutes[${idx}]`;
6251
+ const host = resolveReferencesInString(r.host, refCtx, `${fieldBase}.host`);
6252
+ const zone = r.zone ? resolveReferencesInString(r.zone, refCtx, `${fieldBase}.zone`) : r.zone;
6253
+ if (host !== r.host || zone !== r.zone) mutated = true;
6254
+ return {
6255
+ ...r,
6256
+ host,
6257
+ ...zone !== void 0 ? { zone } : {}
6258
+ };
6259
+ });
6260
+ if (mutated) next = {
6261
+ ...next,
6262
+ tamerRoutes: resolvedRoutes
6263
+ };
6264
+ }
6265
+ const r2Buckets = next.r2_buckets;
6266
+ if (r2Buckets && r2Buckets.length > 0) {
6267
+ let mutated = false;
6268
+ const resolvedBuckets = r2Buckets.map((b, idx) => {
6269
+ const raw = b.bucket_name;
6270
+ if (raw === void 0) return b;
6271
+ const fieldBase = `worker[${ctx.workerKey}].r2_buckets[${idx}].bucket_name`;
6272
+ const bucket_name = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
6273
+ if (bucket_name !== raw) mutated = true;
6274
+ return {
6275
+ ...b,
6276
+ bucket_name
6277
+ };
6278
+ });
6279
+ if (mutated) next = {
6280
+ ...next,
6281
+ r2_buckets: resolvedBuckets
6282
+ };
6283
+ }
6284
+ const svc = next.services;
6285
+ if (svc && svc.length > 0) {
6286
+ let mutated = false;
6287
+ const resolvedSvc = svc.map((s, idx) => {
6288
+ const raw = s.service;
6289
+ const fieldBase = `worker[${ctx.workerKey}].services[${idx}].service`;
6290
+ const service = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
6291
+ if (service !== raw) mutated = true;
6292
+ return {
6293
+ ...s,
6294
+ service
6295
+ };
6296
+ });
6297
+ if (mutated) next = {
6298
+ ...next,
6299
+ services: resolvedSvc
6300
+ };
6301
+ }
6302
+ const dispatchNsMerged = next.dispatch_namespaces;
6303
+ if (dispatchNsMerged && dispatchNsMerged.length > 0) {
6304
+ let mutated = false;
6305
+ const resolvedDn = dispatchNsMerged.map((d, idx) => {
6306
+ const raw = d.namespace;
6307
+ const fieldBase = `worker[${ctx.workerKey}].dispatch_namespaces[${idx}].namespace`;
6308
+ const namespace = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
6309
+ if (namespace !== raw) mutated = true;
6310
+ return {
6311
+ ...d,
6312
+ namespace
6313
+ };
6314
+ });
6315
+ if (mutated) next = {
6316
+ ...next,
6317
+ dispatch_namespaces: resolvedDn
6318
+ };
6319
+ }
6320
+ const resBlock = next.resources;
6321
+ if (resBlock?.d1 && resBlock.d1.length > 0) {
6322
+ let mutated = false;
6323
+ const d1Resolved = resBlock.d1.map((d1c, idx) => {
6324
+ if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
6325
+ const fieldBase = `worker[${ctx.workerKey}].resources.d1[${idx}].databaseName`;
6326
+ const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
6327
+ if (databaseName !== d1c.databaseName) mutated = true;
6328
+ return {
6329
+ ...d1c,
6330
+ databaseName
6331
+ };
6332
+ });
6333
+ if (mutated) next = {
6334
+ ...next,
6335
+ resources: {
6336
+ ...resBlock,
6337
+ d1: d1Resolved
6338
+ }
6339
+ };
6340
+ }
6341
+ return next;
6342
+ }
6343
+ function stripTamerFields(config$1) {
6344
+ const { path, config: configPath, resources, local, env, scriptName: _scriptName, wranglerOutFile: _out, dispatchNamespace: _dispatchNs, tamerRoutes: _tamerRoutes, tamerStaleRouteSweepZones: _tamerStaleRouteSweepZones, ...rest$1 } = config$1;
6345
+ return rest$1;
6346
+ }
6347
+ /**
6348
+ * Env merge + intra-stack `services` rewrite + sibling-stack resolution for
6349
+ * **`resources.d1[].databaseName` only** when `ownership: "external"`.
6350
+ *
6351
+ * Use before {@link ResourceModule.pickResources} during `apply` / `sync` /
6352
+ * `drift` so imported D1 names are known **without** resolving worker `vars`
6353
+ * (those references often need resources created earlier in the same `apply`).
6354
+ */
6355
+ function mergeWorkerConfigForResourcePick(config$1, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
6356
+ let merged = mergedWorkerConfigForEnv(workerConfig, env, config$1.tenant);
6357
+ merged = applyDispatchNamespaceEnvOverrides(config$1, merged, env);
6358
+ const intraMap = buildIntraStackScriptNameMap(config$1, env, naming);
6359
+ merged = rewriteIntraStackServiceTargets(merged, intraMap);
6360
+ const refCtx = {
6361
+ config: config$1,
6362
+ env,
6363
+ state,
6364
+ naming,
6365
+ tolerant: opts.referencesMode === "tolerant",
6366
+ imports: opts.imports,
6367
+ accountId
6368
+ };
6369
+ const resBlock = merged.resources;
6370
+ if (!resBlock?.d1?.length) return merged;
6371
+ const d1Resolved = resBlock.d1.map((d1c, idx) => {
6372
+ if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
6373
+ const fieldBase = `worker[${workerKey}].resources.d1[${idx}].databaseName`;
6374
+ const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
6375
+ return {
6376
+ ...d1c,
6377
+ databaseName
6378
+ };
6379
+ });
6380
+ return {
6381
+ ...merged,
6382
+ resources: {
6383
+ ...resBlock,
6384
+ d1: d1Resolved
6385
+ }
6386
+ };
6387
+ }
6388
+ /**
6389
+ * Env-merged worker config with every `${tamer:…}` site resolved for wrangler
6390
+ * (`vars`, `tamerRoutes`, `r2_buckets[].bucket_name`, external D1 names).
6391
+ * Used by {@link resolveWorkerConfig} after resource state is up to date.
6392
+ */
6393
+ function mergeWorkerConfigWithResolvedRefs(config$1, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
6394
+ let merged = mergedWorkerConfigForEnv(workerConfig, env, config$1.tenant);
6395
+ merged = applyDispatchNamespaceEnvOverrides(config$1, merged, env);
6396
+ const intraMap = buildIntraStackScriptNameMap(config$1, env, naming);
6397
+ merged = rewriteIntraStackServiceTargets(merged, intraMap);
6398
+ return resolveCrossResourceReferences(merged, {
6399
+ config: config$1,
6400
+ env,
6401
+ state,
6402
+ naming,
6403
+ workerKey,
6404
+ tolerant: opts.referencesMode === "tolerant",
6405
+ imports: opts.imports,
6406
+ accountId
6407
+ });
6408
+ }
6409
+ async function resolveWorkerConfig(config$1, workerKey, workerConfig, env, baseDir, accountId, naming, state, opts = {}) {
6410
+ const merged = mergeWorkerConfigWithResolvedRefs(config$1, workerKey, workerConfig, env, accountId, naming, state, opts);
6411
+ const workerDir = workerConfig.path ? resolve(baseDir, workerConfig.path) : baseDir;
6412
+ const m = merged;
6413
+ const workerName = resolveDeployedWorkerName(config$1, workerKey, merged, env, naming);
6414
+ const wranglerOutFile = assertSafeWranglerOutFile(m.wranglerOutFile?.trim() || "wrangler.json");
6415
+ const dispatchNamespace = m.dispatchNamespace?.trim() || void 0;
6416
+ const stripped = stripTamerFields(merged);
6417
+ const tamerRoutes = merged.tamerRoutes;
6418
+ const expandedRoutes = effectiveRoutesForEnv(tamerRoutes, env);
6419
+ const apiManagedRoutes = expandedRoutes.filter(isApiManagedZoneRoute);
6420
+ const wranglerTamerRoutes = expandedRoutes.filter(isWranglerOnlyTamerRoute);
6421
+ const mergedRoutes = [...stripped.routes ?? [], ...wranglerTamerRoutes];
6422
+ /** Non-local deploys emit `routes: []` when there are none — omitting `routes` can leave stale Wrangler-published custom domains attached from a prior config. */
6423
+ const wranglerRoutes = mergedRoutes.length > 0 ? mergedRoutes : env === "local" ? void 0 : [];
6424
+ return {
6425
+ workerKey,
6426
+ workerName,
6427
+ workerDir,
6428
+ env,
6429
+ wranglerOutFile,
6430
+ dispatchNamespace,
6431
+ wranglerConfig: {
6432
+ ...stripped,
6433
+ ...wranglerRoutes !== void 0 ? { routes: wranglerRoutes } : {},
6434
+ name: workerName,
6435
+ account_id: accountId,
6436
+ compatibility_date: stripped.compatibility_date ?? config$1.compatibility_date
6437
+ },
6438
+ resources: merged.resources ?? workerConfig.resources ?? {},
6439
+ apiManagedRoutes
6440
+ };
6441
+ }
6442
+
6443
+ //#endregion
6444
+ //#region src/core/state/stateSchema.ts
6445
+ const D1StateEntrySchema = object({
6446
+ type: literal("d1_database"),
6447
+ logicalName: string(),
6448
+ shardDate: string().optional(),
6449
+ derivedName: string(),
6450
+ bindingKey: string(),
6451
+ cfId: string(),
6452
+ migrationsDir: string().optional(),
6453
+ preserveOnDestroy: boolean().optional(),
6454
+ createdAt: string(),
6455
+ updatedAt: string()
6456
+ });
6457
+ const R2StateEntrySchema = object({
6458
+ type: literal("r2_bucket"),
6459
+ logicalName: string(),
6460
+ createdDate: string(),
6461
+ derivedName: string(),
6462
+ bindingKey: string(),
6463
+ createdAt: string(),
6464
+ updatedAt: string()
6465
+ });
6466
+ const KVStateEntrySchema = object({
6467
+ type: literal("kv_namespace"),
6468
+ logicalName: string(),
6469
+ derivedName: string(),
6470
+ bindingKey: string(),
6471
+ cfId: string(),
6472
+ createdAt: string(),
6473
+ updatedAt: string()
6474
+ });
6475
+ const QueueStateEntrySchema = object({
6476
+ type: literal("queue"),
6477
+ logicalName: string(),
6478
+ derivedName: string(),
6479
+ bindingKey: string(),
6480
+ cfId: string(),
6481
+ producerBinding: boolean(),
6482
+ createdAt: string(),
6483
+ updatedAt: string()
6484
+ });
6485
+ const VectorizeStateEntrySchema = object({
6486
+ type: literal("vectorize"),
6487
+ logicalName: string(),
6488
+ derivedName: string(),
6489
+ bindingKey: string(),
6490
+ cfId: string(),
6491
+ dimensions: number$1(),
6492
+ metric: _enum([
6493
+ "cosine",
6494
+ "euclidean",
6495
+ "dot-product"
6496
+ ]),
6497
+ createdAt: string(),
6498
+ updatedAt: string()
6499
+ });
6500
+ const AIGatewayStateEntrySchema = object({
6501
+ type: literal("ai_gateway"),
6502
+ logicalName: string(),
6503
+ derivedName: string(),
6504
+ bindingKey: string(),
6505
+ cfId: string(),
6506
+ cacheTtl: number$1(),
6507
+ cacheInvalidateOnUpdate: boolean(),
6508
+ collectLogs: boolean(),
6509
+ authentication: boolean(),
6510
+ rateLimitingInterval: number$1(),
6511
+ rateLimitingLimit: number$1(),
6512
+ rateLimitingTechnique: _enum(["fixed", "sliding"]),
6513
+ createdAt: string(),
6514
+ updatedAt: string()
6515
+ });
6516
+ const PipelineStateEntrySchema = object({
6517
+ type: literal("pipeline"),
6518
+ logicalName: string(),
6519
+ derivedName: string(),
6520
+ bindingKey: string(),
6521
+ cfId: string(),
6522
+ sql: string(),
6523
+ status: string().optional(),
6524
+ createdAt: string(),
6525
+ updatedAt: string()
6526
+ });
6527
+ const WorkflowStateEntrySchema = object({
6528
+ type: literal("workflow"),
6529
+ logicalName: string(),
6530
+ derivedName: string(),
6531
+ bindingKey: string(),
6532
+ cfId: string(),
6533
+ className: string(),
6534
+ scriptName: string(),
6535
+ limits: object({ steps: number$1().int().positive().optional() }).optional(),
6536
+ createdAt: string(),
6537
+ updatedAt: string()
6538
+ });
6539
+ const SecretsStoreStateEntrySchema = object({
6540
+ type: literal("secrets_store"),
6541
+ logicalName: string(),
6542
+ derivedName: string(),
6543
+ bindingKey: string(),
6544
+ cfId: string(),
6545
+ createdAt: string(),
6546
+ updatedAt: string()
6547
+ });
6548
+ const HyperdriveStateEntrySchema = object({
6549
+ type: literal("hyperdrive"),
6550
+ logicalName: string(),
6551
+ derivedName: string(),
6552
+ bindingKey: string(),
6553
+ cfId: string(),
6554
+ scheme: _enum([
6555
+ "postgres",
6556
+ "postgresql",
6557
+ "mysql"
6558
+ ]),
6559
+ originHost: string(),
6560
+ originDatabase: string(),
6561
+ createdAt: string(),
6562
+ updatedAt: string()
6563
+ });
6564
+ const DnsRecordTypeSchema = _enum([
6565
+ "A",
6566
+ "AAAA",
6567
+ "CNAME",
6568
+ "TXT",
6569
+ "MX",
6570
+ "NS",
6571
+ "CAA",
6572
+ "SRV",
6573
+ "PTR",
6574
+ "HTTPS",
6575
+ "SVCB"
6576
+ ]);
6577
+ const DnsRecordStateEntrySchema = object({
6578
+ type: literal("dns_record"),
6579
+ logicalName: string(),
6580
+ zoneId: string(),
6581
+ recordType: DnsRecordTypeSchema,
6582
+ name: string(),
6583
+ content: string(),
6584
+ ttl: number$1(),
6585
+ proxied: boolean(),
6586
+ priority: number$1().optional(),
6587
+ comment: string(),
6588
+ recordId: string(),
6589
+ createdAt: string(),
6590
+ updatedAt: string()
6591
+ });
6592
+ const DispatchNamespaceStateEntrySchema = object({
6593
+ type: literal("dispatch_namespace"),
6594
+ logicalName: string(),
6595
+ derivedName: string(),
6596
+ createdAt: string(),
6597
+ updatedAt: string()
6598
+ });
6599
+ const LogpushJobStateEntrySchema = object({
6600
+ type: literal("logpush_job"),
6601
+ logicalName: string(),
6602
+ derivedName: string(),
6603
+ cfJobId: number$1(),
6604
+ dataset: string(),
6605
+ createdAt: string(),
6606
+ updatedAt: string()
6607
+ });
6608
+ const LogpushPipelinesStateEntrySchema = object({
6609
+ type: literal("logpush_pipelines"),
6610
+ logicalName: string(),
6611
+ streamId: string(),
6612
+ streamIngestBaseUrl: string().optional(),
6613
+ sinkId: string(),
6614
+ pipelineId: string(),
6615
+ streamName: string(),
6616
+ sinkName: string(),
6617
+ pipelineName: string(),
6618
+ r2DataCatalogTableName: string().optional(),
6619
+ r2DataCatalogTableNamePipelines: string().optional(),
6620
+ r2DataCatalogNamespace: string().optional(),
6621
+ catalogBucketDerivedName: string(),
6622
+ mintedR2CatalogTokenId: string().optional(),
6623
+ mintedR2CatalogTokenValue: string().optional(),
6624
+ mintedPipelinesSendTokenId: string().optional(),
6625
+ mintedPipelinesSendTokenValue: string().optional(),
6626
+ createdAt: string(),
6627
+ updatedAt: string()
6628
+ });
6629
+ const WorkerRouteStateEntrySchema = object({
6630
+ type: literal("worker_route"),
6631
+ workerKey: string(),
6632
+ workerName: string(),
6633
+ zoneId: string(),
6634
+ zoneName: string(),
6635
+ routeId: string(),
6636
+ pattern: string(),
6637
+ createdAt: string(),
6638
+ updatedAt: string()
6639
+ });
6640
+ const SecretStateEntrySchema = object({
6641
+ type: literal("secret"),
6642
+ worker: string(),
6643
+ name: string(),
6644
+ lastPushedHash: string(),
6645
+ lastPushedAt: string()
6646
+ });
6647
+ const StateEntrySchema = discriminatedUnion("type", [
6648
+ D1StateEntrySchema,
6649
+ R2StateEntrySchema,
6650
+ KVStateEntrySchema,
6651
+ QueueStateEntrySchema,
6652
+ HyperdriveStateEntrySchema,
6653
+ VectorizeStateEntrySchema,
6654
+ AIGatewayStateEntrySchema,
6655
+ PipelineStateEntrySchema,
6656
+ WorkflowStateEntrySchema,
6657
+ SecretsStoreStateEntrySchema,
6658
+ DnsRecordStateEntrySchema,
6659
+ DispatchNamespaceStateEntrySchema,
6660
+ LogpushJobStateEntrySchema,
6661
+ LogpushPipelinesStateEntrySchema,
6662
+ WorkerRouteStateEntrySchema,
6663
+ SecretStateEntrySchema
6664
+ ]);
6665
+ const ProvisioningStatusSchema = _enum([
6666
+ "pending",
6667
+ "d1_created",
6668
+ "migrations_applied",
6669
+ "script_uploaded",
6670
+ "ready",
6671
+ "tombstoned"
6672
+ ]);
6673
+ const TenantD1ShardRefSchema = object({
6674
+ role: string(),
6675
+ derivedName: string(),
6676
+ cfId: string()
6677
+ });
6678
+ const TenantStateEntrySchema = object({
6679
+ product: string(),
6680
+ workspace: string(),
6681
+ provisioningStatus: ProvisioningStatusSchema,
6682
+ dispatchNamespaceName: string(),
6683
+ scriptName: string(),
6684
+ d1Shards: array(TenantD1ShardRefSchema).optional(),
6685
+ createdAt: string(),
6686
+ updatedAt: string()
6687
+ });
6688
+ const CfiStackMetaSchema = object({
6689
+ name: string().optional(),
6690
+ owner: string().optional()
6691
+ });
6692
+ const CfiOperationNameSchema = _enum([
6693
+ "bootstrap",
6694
+ "apply",
6695
+ "deploy",
6696
+ "destroy",
6697
+ "provision-tenant",
6698
+ "destroy-tenant",
6699
+ "import",
6700
+ "sync"
6701
+ ]);
6702
+ const CfiOperationStatusSchema = _enum([
6703
+ "in_progress",
6704
+ "succeeded",
6705
+ "failed"
6706
+ ]);
6707
+ const CfiStackOutputValueSchema = object({
6708
+ value: string(),
6709
+ source: string(),
6710
+ resolvedAt: string()
6711
+ });
6712
+ const CfiOperationRecordSchema = object({
6713
+ command: CfiOperationNameSchema,
6714
+ status: CfiOperationStatusSchema,
6715
+ startedAt: string(),
6716
+ completedAt: string().optional(),
6717
+ errorMessage: string().optional(),
6718
+ detail: string().optional()
6719
+ });
6720
+ const CfiStateSchema = object({
6721
+ tenantId: string(),
6722
+ env: string(),
6723
+ schemaVersion: number$1(),
6724
+ syncedAt: string(),
6725
+ resources: record(string(), StateEntrySchema),
6726
+ revision: number$1().optional(),
6727
+ tenants: record(string(), TenantStateEntrySchema).optional(),
6728
+ stack: CfiStackMetaSchema.optional(),
6729
+ stackOutputs: record(string(), CfiStackOutputValueSchema).optional(),
6730
+ lastOperation: CfiOperationRecordSchema.optional(),
6731
+ operationHistory: array(CfiOperationRecordSchema).optional()
6732
+ });
6733
+
6734
+ //#endregion
6735
+ //#region src/core/state/stackName.ts
6736
+ const DEFAULT_STACK_NAME = "default";
6737
+ function stackNameForConfig(config$1) {
6738
+ return config$1.stack?.name ?? config$1.tenant.slug ?? DEFAULT_STACK_NAME;
6739
+ }
6740
+
6741
+ //#endregion
6742
+ //#region src/core/state/tamerStateDb.ts
6743
+ /**
6744
+ * Schema versions:
6745
+ * 2: original (resources only).
6746
+ * 3: + `tenants` map, + `revision` for optimistic concurrency.
6747
+ * 4: + `stack` metadata, + `lastOperation` (CloudFormation-style stack info).
6748
+ * All v4 additions are optional; existing v3 documents are upgraded
6749
+ * in-place during parse with no data loss.
6750
+ * 5: + `secret` state entries (fingerprints only; additive resource rows).
6751
+ */
6752
+ const STATE_SCHEMA_VERSION = 5;
6753
+ /** Cloudflare D1 database that holds JSON state for an env (`tamer-state-dev`, …). */
6754
+ function tamerStateDatabaseName(env) {
6755
+ return `tamer-state-${env}`;
6756
+ }
6757
+ function createEmptyCfiState(tenantId, env) {
6758
+ return {
6759
+ tenantId,
6760
+ env,
6761
+ schemaVersion: STATE_SCHEMA_VERSION,
6762
+ syncedAt: (/* @__PURE__ */ new Date()).toISOString(),
6763
+ resources: {},
6764
+ revision: 0,
6765
+ tenants: {}
6766
+ };
6767
+ }
6768
+ /** In-place upgrade for parsed JSON before Zod validation. */
6769
+ function migrateRawCfiStateInPlace(raw) {
6770
+ const v = raw.schemaVersion;
6771
+ if (typeof v !== "number") throw new Error("tamer state: schemaVersion must be a number");
6772
+ if (v < 2) throw new Error(`tamer state: unsupported schemaVersion ${v}`);
6773
+ if (v > STATE_SCHEMA_VERSION) throw new Error(`tamer state: unknown schemaVersion ${v} (engine supports up to ${STATE_SCHEMA_VERSION})`);
6774
+ if (v === 2) {
6775
+ raw.tenants = {};
6776
+ raw.revision = 0;
6777
+ raw.schemaVersion = 3;
6778
+ }
6779
+ if (!raw.tenants || typeof raw.tenants !== "object") raw.tenants = {};
6780
+ if (typeof raw.revision !== "number") raw.revision = 0;
6781
+ if (raw.schemaVersion === 3) raw.schemaVersion = 4;
6782
+ if (raw.schemaVersion === 4) raw.schemaVersion = STATE_SCHEMA_VERSION;
6783
+ }
6784
+ async function findTamerStateDatabaseUuid(api, env) {
6785
+ const name = tamerStateDatabaseName(env);
6786
+ return (await api.d1ListAll()).find((d) => d.name === name)?.uuid;
6787
+ }
6788
+ /**
6789
+ * Create `tamer-state-{env}` if missing, ensure `tamer_kv` table, and seed an
6790
+ * initial empty `cfi_state:{stackName}` row when this stack has no row yet.
6791
+ * Idempotent — re-running for the same stack is a no-op; re-running for a
6792
+ * different stack against the same env D1 just adds another row.
6793
+ */
6794
+ async function ensureTamerStateDatabase(api, tenantId, env, stackName = DEFAULT_STACK_NAME) {
6795
+ let uuid$1 = await findTamerStateDatabaseUuid(api, env);
6796
+ if (!uuid$1) uuid$1 = (await api.d1Create(tamerStateDatabaseName(env))).uuid;
6797
+ await api.d1Query(uuid$1, `CREATE TABLE IF NOT EXISTS tamer_kv (
6798
+ k TEXT PRIMARY KEY,
6799
+ v TEXT NOT NULL
6800
+ )`);
6801
+ const rowKey = `cfi_state:${stackName}`;
6802
+ const { rows } = await api.d1Query(uuid$1, `SELECT v FROM tamer_kv WHERE k = ?`, [rowKey]);
6803
+ if (rows.length === 0) {
6804
+ const initial = createEmptyCfiState(tenantId, env);
6805
+ await api.d1Query(uuid$1, `INSERT INTO tamer_kv (k, v) VALUES (?, ?)`, [rowKey, JSON.stringify(initial)]);
6806
+ }
6807
+ return uuid$1;
6808
+ }
6809
+ function parseCfiStateJson(json) {
6810
+ const raw = JSON.parse(json);
6811
+ migrateRawCfiStateInPlace(raw);
6812
+ const result = CfiStateSchema.safeParse(raw);
6813
+ if (!result.success) throw new Error(`Invalid tamer state JSON: ${result.error.message}`);
6814
+ return result.data;
6815
+ }
6816
+ async function destroyTamerStateDatabase(api, env) {
6817
+ const uuid$1 = await findTamerStateDatabaseUuid(api, env);
6818
+ if (!uuid$1) return false;
6819
+ await api.d1Delete(uuid$1);
6820
+ return true;
6821
+ }
6822
+
6823
+ //#endregion
6824
+ //#region src/core/tenant/tenantKeys.ts
6825
+ /** Stable map key for `CfiState.tenants` (workspace-scoped product tenant). */
6826
+ function tenantStateKey(product, workspace) {
6827
+ return `${product}:${workspace}`;
6828
+ }
6829
+ /**
6830
+ * Dispatch-namespace script name per `docs/handoff.md` §6: non-ephemeral
6831
+ * envs collapse to `{product}-{workspace}` (one script per workspace);
6832
+ * ephemeral envs (matching `tenant.ephemeralEnvPattern`) carry the env
6833
+ * in the script name (`{product}-{workspace}-{env}`) so multiple
6834
+ * previews can coexist in the shared `{ns}-ephemeral` namespace.
6835
+ */
6836
+ function tenantDispatchScriptName(product, workspace, env, tenant) {
6837
+ if (isEphemeralEnv(env, tenant)) return `${product}-${workspace}-${env}`;
6838
+ return `${product}-${workspace}`;
6839
+ }
6840
+ const SAFE = /[^a-z0-9_-]/gi;
6841
+ /**
6842
+ * Per-shard D1 database name for a tenant. Stable across `provision-tenant`
6843
+ * runs and across env so re-provisioning + drift detection can match by
6844
+ * name. Format: `db_{role}_{w}_{p}_t_{tid}_{env}`.
6845
+ *
6846
+ * db_system_acme_todo_t_platform_prod
6847
+ * db_app_acme_todo_t_platform_prod
6848
+ *
6849
+ * `role` is whatever the operator declared in `tenant.d1Shards` in
6850
+ * `tamer.config.ts`. Tamer is opinion-free about the shard layout — a
6851
+ * Dragoncore-style product picks `["system", "app", "history"]`, a
6852
+ * single-DB tenant picks `["main"]`, an audit-only tenant picks
6853
+ * `["audit"]`, etc. The role itself is validated by the loader (lowercase
6854
+ * ASCII subset) so it slots cleanly into the D1 naming scheme.
6855
+ *
6856
+ * D1 names are length-bounded (Cloudflare currently allows up to 64
6857
+ * chars), and this scheme keeps every shard well under that limit even
6858
+ * for long workspace + product slugs.
6859
+ */
6860
+ function tenantShardDatabaseName(role, workspace, product, platformTenantId, env) {
6861
+ return `db_${role}_${workspace.replace(SAFE, "_").toLowerCase()}_${product.replace(SAFE, "_").toLowerCase()}_t_${platformTenantId}_${env}`;
6862
+ }
6863
+ /**
6864
+ * Parse + validate a `--shards a,b,c` CLI argument against the configured
6865
+ * shard set in `tamer.config.ts`. The CLI flag may only **trim** the
6866
+ * configured layout (e.g. `--shards system` on a stack whose config
6867
+ * declares `["system","app","history"]` provisions just the system
6868
+ * shard for an ephemeral preview); it cannot extend it, because the
6869
+ * config is the source of truth that `apply` / `drift` / `destroy`
6870
+ * other operators read.
6871
+ *
6872
+ * Returns the requested roles in canonical order (matches `allowed`
6873
+ * order, regardless of input order) so plan/apply output is
6874
+ * deterministic and partial-failure resumes pick up at the next
6875
+ * canonical role.
6876
+ */
6877
+ function parseTenantShardRoles(raw, allowed) {
6878
+ const allowedSet = new Set(allowed);
6879
+ const requested = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
6880
+ const unknown$1 = requested.filter((r) => !allowedSet.has(r));
6881
+ if (unknown$1.length > 0) {
6882
+ const list = allowed.length > 0 ? allowed.join(", ") : "(none configured)";
6883
+ throw new Error(`unknown tenant shard role(s) "${unknown$1.join(", ")}"; must be a subset of tenant.d1Shards in the Tamer project config: ${list}`);
6884
+ }
6885
+ const seen = new Set(requested);
6886
+ return allowed.filter((r) => seen.has(r));
6887
+ }
6888
+
6889
+ //#endregion
6890
+ //#region src/core/state/StateConflictError.ts
6891
+ /** Thrown when D1 `cfi_state` was updated by another writer since {@link StateManager.hydrate}. */
6892
+ var StateConflictError = class extends Error {
6893
+ code = "STATE_CONFLICT";
6894
+ constructor(message) {
6895
+ super(message);
6896
+ this.name = "StateConflictError";
6897
+ }
6898
+ };
6899
+
6900
+ //#endregion
6901
+ //#region src/core/state/StateManager.ts
6902
+ /** D1 `tamer_kv.k` value for a given stack's state row. */
6903
+ function stateRowKey(stackName) {
6904
+ return `cfi_state:${stackName}`;
6905
+ }
6906
+ const OPERATION_HISTORY_CAP = 50;
6907
+ /**
6908
+ * Authoritative deployment state for an env.
6909
+ *
6910
+ * - **Non-local:** stored as JSON in Cloudflare D1 (`tamer-state-{env}`).
6911
+ * Call {@link hydrate} before {@link load}, then {@link persist} after mutations.
6912
+ * - **local:** in-memory only (no persistence).
6913
+ */
6914
+ var StateManager = class {
6915
+ state = null;
6916
+ dirty = false;
6917
+ /** Set when {@link hydrate} loads remote state. */
6918
+ tamerStateDbUuid = null;
6919
+ /**
6920
+ * Remote `revision` at last hydrate (or last successful persist). Used for
6921
+ * optimistic concurrency on D1 persist.
6922
+ */
6923
+ baselineRevision = 0;
6924
+ /**
6925
+ * @param tenantId `config.tenant.id` — recorded on the state row for
6926
+ * diagnostics; not part of the row key.
6927
+ * @param env Cloudflare environment name; selects the
6928
+ * `tamer-state-{env}` D1 database.
6929
+ * @param stackName Stack identity (`config.stack.name ?? tenant.slug`).
6930
+ * The state row in D1 is keyed `cfi_state:{stackName}`,
6931
+ * so multiple stacks coexist in one env D1 without
6932
+ * clobbering each other. Defaults to `"default"` —
6933
+ * unit tests that synthesize a StateManager without
6934
+ * a config get a stable key without extra plumbing.
6935
+ */
6936
+ constructor(tenantId, env, stackName = DEFAULT_STACK_NAME) {
6937
+ this.tenantId = tenantId;
6938
+ this.env = env;
6939
+ this.stackName = stackName;
6940
+ }
6941
+ /**
6942
+ * Load state from D1 (remote) or allocate empty state (local).
6943
+ * Required before {@link load} for every command.
6944
+ */
6945
+ async hydrate(api) {
6946
+ if (this.state) return;
6947
+ if (this.env === "local") {
6948
+ this.state = createEmptyCfiState(this.tenantId, this.env);
6949
+ this.baselineRevision = this.state.revision ?? 0;
6950
+ return;
6951
+ }
6952
+ const name = tamerStateDatabaseName(this.env);
6953
+ const uuid$1 = await findTamerStateDatabaseUuid(api, this.env);
6954
+ if (!uuid$1) throw new Error(`Tamer state database "${name}" not found. Run: tamer bootstrap --env ${this.env}`);
6955
+ this.tamerStateDbUuid = uuid$1;
6956
+ const rowKey = stateRowKey(this.stackName);
6957
+ const { rows } = await api.d1Query(uuid$1, `SELECT v FROM tamer_kv WHERE k = ?`, [rowKey]);
6958
+ if (rows.length === 0) {
6959
+ this.state = createEmptyCfiState(this.tenantId, this.env);
6960
+ this.baselineRevision = this.state.revision ?? 0;
6961
+ this.dirty = true;
6962
+ return;
6963
+ }
6964
+ const v = rows[0]["v"];
6965
+ if (typeof v !== "string") throw new Error(`tamer_kv.${rowKey} must be a string column`);
6966
+ this.state = parseCfiStateJson(v);
6967
+ this.baselineRevision = this.state.revision ?? 0;
6968
+ }
6969
+ /**
6970
+ * Stack identifier this manager is bound to (the `cfi_state:{name}` row
6971
+ * key suffix). Exposed so `fetchStackImports` and diagnostics can show
6972
+ * the operator which row this manager owns.
6973
+ */
6974
+ getStackName() {
6975
+ return this.stackName;
6976
+ }
6977
+ /**
6978
+ * Allocate empty in-memory state without touching D1. Use for read-only
6979
+ * "what-would-state-look-like" snapshots (e.g. drift-aware plan refresh)
6980
+ * where we want to drive the module `sync` hooks against a fresh slate
6981
+ * and then discard the result. {@link persist} is unsafe afterwards
6982
+ * because there is no D1 baseline to compare against.
6983
+ */
6984
+ hydrateInMemory() {
6985
+ if (this.state) return;
6986
+ this.state = createEmptyCfiState(this.tenantId, this.env);
6987
+ this.baselineRevision = this.state.revision ?? 0;
6988
+ }
6989
+ /** Clear cached state so the next {@link hydrate} reloads from D1. */
6990
+ reset() {
6991
+ this.state = null;
6992
+ this.tamerStateDbUuid = null;
6993
+ this.dirty = false;
6994
+ this.baselineRevision = 0;
6995
+ }
6996
+ load() {
6997
+ if (!this.state) throw new Error("StateManager: call await hydrate(api) before load()");
6998
+ return this.state;
6999
+ }
7000
+ get(derivedName) {
7001
+ return this.load().resources[derivedName];
7002
+ }
7003
+ set(derivedName, entry) {
7004
+ const s = this.load();
7005
+ s.resources[derivedName] = entry;
7006
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7007
+ this.dirty = true;
7008
+ }
7009
+ delete(derivedName) {
7010
+ const s = this.load();
7011
+ delete s.resources[derivedName];
7012
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7013
+ this.dirty = true;
7014
+ }
7015
+ getAll() {
7016
+ return this.load().resources;
7017
+ }
7018
+ getTenant(product, workspace) {
7019
+ return this.load().tenants?.[tenantStateKey(product, workspace)];
7020
+ }
7021
+ setTenant(entry) {
7022
+ const s = this.load();
7023
+ if (!s.tenants) s.tenants = {};
7024
+ s.tenants[tenantStateKey(entry.product, entry.workspace)] = entry;
7025
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7026
+ this.dirty = true;
7027
+ }
7028
+ deleteTenant(product, workspace) {
7029
+ const s = this.load();
7030
+ if (!s.tenants) return;
7031
+ delete s.tenants[tenantStateKey(product, workspace)];
7032
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7033
+ this.dirty = true;
7034
+ }
7035
+ listTenants() {
7036
+ return Object.values(this.load().tenants ?? {});
7037
+ }
7038
+ /** CloudFormation-style stack metadata (name, owner). Returns a copy. */
7039
+ getStackMeta() {
7040
+ const s = this.load().stack;
7041
+ return s ? { ...s } : void 0;
7042
+ }
7043
+ /**
7044
+ * Set or merge stack metadata. Pass `undefined` fields to clear them; only
7045
+ * provided keys are written, so callers can update one field at a time.
7046
+ */
7047
+ setStackMeta(meta$2) {
7048
+ const s = this.load();
7049
+ s.stack = {
7050
+ ...s.stack ?? {},
7051
+ ...meta$2
7052
+ };
7053
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7054
+ this.dirty = true;
7055
+ }
7056
+ /**
7057
+ * Resolved + persisted `outputs:` for this stack. Returns `{}` when none
7058
+ * have been recorded yet (e.g. before the first successful `apply`). The
7059
+ * returned object is a shallow copy — mutate via {@link replaceStackOutputs}.
7060
+ */
7061
+ getStackOutputs() {
7062
+ return { ...this.load().stackOutputs ?? {} };
7063
+ }
7064
+ /**
7065
+ * Replace this stack's `stackOutputs` map wholesale. Pass `{}` to clear
7066
+ * (e.g. when `outputs` is removed from `tamer.config.ts`); pass a fresh
7067
+ * map keyed by output name to commit a successful apply's resolved values.
7068
+ * No-op when the new map is structurally identical to the existing one
7069
+ * (avoids gratuitous `revision` bumps on no-op applies).
7070
+ */
7071
+ replaceStackOutputs(next) {
7072
+ const s = this.load();
7073
+ if (stackOutputsEqual(s.stackOutputs ?? {}, next)) return;
7074
+ if (Object.keys(next).length === 0) delete s.stackOutputs;
7075
+ else s.stackOutputs = { ...next };
7076
+ s.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7077
+ this.dirty = true;
7078
+ }
7079
+ getLastOperation() {
7080
+ return this.load().lastOperation;
7081
+ }
7082
+ /** Completed operations (`succeeded` / `failed` only), newest first. */
7083
+ getOperationHistory() {
7084
+ const h = this.load().operationHistory;
7085
+ return h ? h.map((e) => ({ ...e })) : [];
7086
+ }
7087
+ /**
7088
+ * Begin recording a CloudFormation-style operation marker. Sets `status:
7089
+ * "in_progress"` and `startedAt`; pair with {@link finishOperation} on
7090
+ * success or {@link failOperation} on error. Persist between calls if the
7091
+ * operation may take a long time and you want concurrent operators to see
7092
+ * the in-progress marker.
7093
+ */
7094
+ beginOperation(command$1, detail) {
7095
+ const s = this.load();
7096
+ s.lastOperation = {
7097
+ command: command$1,
7098
+ status: "in_progress",
7099
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7100
+ detail
7101
+ };
7102
+ s.syncedAt = s.lastOperation.startedAt;
7103
+ this.dirty = true;
7104
+ }
7105
+ finishOperation(detail) {
7106
+ const s = this.load();
7107
+ if (!s.lastOperation) return;
7108
+ s.lastOperation.status = "succeeded";
7109
+ s.lastOperation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
7110
+ if (detail !== void 0) s.lastOperation.detail = detail;
7111
+ s.syncedAt = s.lastOperation.completedAt;
7112
+ this.appendTerminalOperationToHistory(s.lastOperation);
7113
+ this.dirty = true;
7114
+ }
7115
+ failOperation(errorMessage) {
7116
+ const s = this.load();
7117
+ if (!s.lastOperation) return;
7118
+ s.lastOperation.status = "failed";
7119
+ s.lastOperation.completedAt = (/* @__PURE__ */ new Date()).toISOString();
7120
+ s.lastOperation.errorMessage = errorMessage;
7121
+ s.syncedAt = s.lastOperation.completedAt;
7122
+ this.appendTerminalOperationToHistory(s.lastOperation);
7123
+ this.dirty = true;
7124
+ }
7125
+ appendTerminalOperationToHistory(op) {
7126
+ if (op.status !== "succeeded" && op.status !== "failed") return;
7127
+ const s = this.load();
7128
+ s.operationHistory = [{
7129
+ command: op.command,
7130
+ status: op.status,
7131
+ startedAt: op.startedAt,
7132
+ completedAt: op.completedAt,
7133
+ errorMessage: op.errorMessage,
7134
+ detail: op.detail
7135
+ }, ...s.operationHistory ?? []].slice(0, OPERATION_HISTORY_CAP);
7136
+ }
7137
+ /**
7138
+ * Persist to D1 (no-op for local). Uses optimistic concurrency: re-reads
7139
+ * `revision` before write; throws {@link StateConflictError} if another
7140
+ * writer advanced the row since {@link hydrate}.
7141
+ */
7142
+ async persist(api) {
7143
+ if (this.env === "local") {
7144
+ this.dirty = false;
7145
+ return;
7146
+ }
7147
+ if (!this.dirty || !this.state || !this.tamerStateDbUuid) return;
7148
+ const rowKey = stateRowKey(this.stackName);
7149
+ const { rows } = await api.d1Query(this.tamerStateDbUuid, `SELECT v FROM tamer_kv WHERE k = ?`, [rowKey]);
7150
+ let remoteRev = 0;
7151
+ if (rows.length > 0) {
7152
+ const v = rows[0]["v"];
7153
+ if (typeof v === "string") remoteRev = parseCfiStateJson(v).revision ?? 0;
7154
+ }
7155
+ if (remoteRev !== this.baselineRevision) throw new StateConflictError(`Tamer state conflict (stack=${this.stackName}): remote revision ${remoteRev} !== expected ${this.baselineRevision}. Re-run after refresh.`);
7156
+ this.state.revision = remoteRev + 1;
7157
+ this.state.syncedAt = (/* @__PURE__ */ new Date()).toISOString();
7158
+ const json = JSON.stringify(this.state);
7159
+ await api.d1Query(this.tamerStateDbUuid, `INSERT INTO tamer_kv (k, v) VALUES (?, ?)
7160
+ ON CONFLICT(k) DO UPDATE SET v = excluded.v`, [rowKey, json]);
7161
+ this.baselineRevision = this.state.revision;
7162
+ this.dirty = false;
7163
+ }
7164
+ /** Mark clean without writing (e.g. before deleting the state database). */
7165
+ clearDirty() {
7166
+ this.dirty = false;
7167
+ }
7168
+ };
7169
+ function stackOutputsEqual(a, b) {
7170
+ const ak = Object.keys(a).sort();
7171
+ const bk = Object.keys(b).sort();
7172
+ if (ak.length !== bk.length) return false;
7173
+ for (let i = 0; i < ak.length; i++) {
7174
+ if (ak[i] !== bk[i]) return false;
7175
+ const k = ak[i];
7176
+ const av = a[k];
7177
+ const bv = b[k];
7178
+ if (av.value !== bv.value || av.source !== bv.source) return false;
7179
+ }
7180
+ return true;
7181
+ }
7182
+
7183
+ //#endregion
7184
+ //#region src/core/imports/fetchStackImports.ts
7185
+ const IMPORT_RE = /\$\{tamer:import:([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
7186
+ /**
7187
+ * Walk the merged `CfiConfig` and collect every `${tamer:import:…}` ref
7188
+ * site along with where it was found. Used both to drive the pre-fetch
7189
+ * (which sibling stacks to load) and by `tamer status` to render an
7190
+ * "inbound imports" panel even before any `apply` has run.
7191
+ *
7192
+ * Self-imports (current stack importing from its own name) are filtered
7193
+ * out — they are almost always a config typo and would otherwise
7194
+ * silently resolve via the same row this command is about to write.
7195
+ */
7196
+ function scanConfigForImports(config$1) {
7197
+ const selfStack = stackNameForConfig(config$1);
7198
+ const refs = [];
7199
+ const seen = /* @__PURE__ */ new Set();
7200
+ const push = (raw, fieldPath) => {
7201
+ if (!raw) return;
7202
+ IMPORT_RE.lastIndex = 0;
7203
+ let m;
7204
+ while ((m = IMPORT_RE.exec(raw)) !== null) {
7205
+ const stack = m[1];
7206
+ const output = m[2];
7207
+ if (stack === selfStack) continue;
7208
+ const key = `${fieldPath}::${stack}.${output}`;
7209
+ if (seen.has(key)) continue;
7210
+ seen.add(key);
7211
+ refs.push({
7212
+ stack,
7213
+ output,
7214
+ fieldPath
7215
+ });
7216
+ }
7217
+ };
7218
+ const walkVars = (vars, pathPrefix) => {
7219
+ if (!vars) return;
7220
+ for (const [k, v] of Object.entries(vars)) push(materializeTamerResolvable(v), `${pathPrefix}.${k}`);
7221
+ };
7222
+ const walkR2BucketNames = (w, pathPrefix) => {
7223
+ if (!w.r2_buckets) return;
7224
+ w.r2_buckets.forEach((b, i) => {
7225
+ if (b.bucket_name === void 0) return;
7226
+ push(materializeTamerResolvable(b.bucket_name), `${pathPrefix}.r2_buckets[${i}].bucket_name`);
7227
+ });
7228
+ };
7229
+ const walkD1DatabaseNames = (w, pathPrefix) => {
7230
+ const d1 = w.resources?.d1;
7231
+ if (!d1) return;
7232
+ d1.forEach((d, i) => {
7233
+ if (d.databaseName === void 0) return;
7234
+ push(materializeTamerResolvable(d.databaseName), `${pathPrefix}.resources.d1[${i}].databaseName`);
7235
+ });
7236
+ };
7237
+ /** Service bindings / WfP namespace strings may carry `${tamer:import:…}`. */
7238
+ const walkBindingsWithRefs = (w, pathPrefix) => {
7239
+ w.services?.forEach((s, i) => {
7240
+ if (s.service === void 0) return;
7241
+ push(materializeTamerResolvable(s.service), `${pathPrefix}.services[${i}].service`);
7242
+ });
7243
+ w.dispatch_namespaces?.forEach((d, i) => {
7244
+ if (d.namespace === void 0) return;
7245
+ push(materializeTamerResolvable(d.namespace), `${pathPrefix}.dispatch_namespaces[${i}].namespace`);
7246
+ });
7247
+ };
7248
+ if (config$1.worker) {
7249
+ walkVars(config$1.worker.vars, "worker.vars");
7250
+ walkR2BucketNames(config$1.worker, "worker");
7251
+ walkD1DatabaseNames(config$1.worker, "worker");
7252
+ walkBindingsWithRefs(config$1.worker, "worker");
7253
+ if (config$1.worker.tamerRoutes) config$1.worker.tamerRoutes.forEach((r, i) => {
7254
+ push(r.host, `worker.tamerRoutes[${i}].host`);
7255
+ push(r.zone, `worker.tamerRoutes[${i}].zone`);
7256
+ });
7257
+ }
7258
+ if (config$1.workers) for (const [key, w] of Object.entries(config$1.workers)) {
7259
+ walkVars(w.vars, `worker[${key}].vars`);
7260
+ walkR2BucketNames(w, `worker[${key}]`);
7261
+ walkD1DatabaseNames(w, `worker[${key}]`);
7262
+ walkBindingsWithRefs(w, `worker[${key}]`);
7263
+ if (w.tamerRoutes) w.tamerRoutes.forEach((r, i) => {
7264
+ push(r.host, `worker[${key}].tamerRoutes[${i}].host`);
7265
+ push(r.zone, `worker[${key}].tamerRoutes[${i}].zone`);
7266
+ });
7267
+ }
7268
+ if (config$1.outputs) for (const [name, source] of Object.entries(config$1.outputs)) push(materializeTamerResolvable(source), `outputs.${name}`);
7269
+ return refs;
7270
+ }
7271
+ /** Distinct sibling stack names referenced anywhere in `config`. */
7272
+ function importedStackNames(config$1) {
7273
+ const refs = scanConfigForImports(config$1);
7274
+ return [...new Set(refs.map((r) => r.stack))].sort();
7275
+ }
7276
+ /**
7277
+ * Hydrate every imported sibling stack's persisted outputs and return
7278
+ * them shaped for {@link ReferenceContext.imports}.
7279
+ *
7280
+ * - `local` env: returns `{}` immediately. Local mode never persists
7281
+ * state, so cross-stack imports are inherently unresolvable. Callers
7282
+ * running in tolerant mode (`plan`, `status`) will see the placeholder
7283
+ * verbatim; strict callers (`apply`, `deploy`) will fail at resolution
7284
+ * with a clear "no imported stack" message.
7285
+ * - Missing sibling state row: recorded as an empty outputs map, so
7286
+ * `lookupImport` can produce a "no published outputs" diagnostic
7287
+ * (vs. the generic "stack not pre-fetched" error).
7288
+ *
7289
+ * The {@link CFApiClient} is shared with the caller for socket reuse.
7290
+ */
7291
+ async function fetchStackImports(api, config$1, env) {
7292
+ const stacks = importedStackNames(config$1);
7293
+ if (stacks.length === 0 || env === "local") return {};
7294
+ const out = {};
7295
+ for (const stack of stacks) {
7296
+ const sibling = new StateManager(config$1.tenant.id, env, stack);
7297
+ try {
7298
+ await sibling.hydrate(api);
7299
+ } catch (err) {
7300
+ throw new Error(`Failed to hydrate imported stack "${stack}" from env "${env}": ${err instanceof Error ? err.message : String(err)}`);
7301
+ }
7302
+ const persisted = sibling.getStackOutputs();
7303
+ const flat = {};
7304
+ for (const [name, v] of Object.entries(persisted)) flat[name] = v.value;
7305
+ out[stack] = flat;
7306
+ }
7307
+ return out;
7308
+ }
7309
+
7310
+ //#endregion
7311
+ //#region src/core/secrets/declared.ts
7312
+ /** Declared secret names for a worker; empty when `secrets` is absent. */
7313
+ function requiredSecretsForWorker(workerConfig) {
7314
+ return workerConfig.secrets?.required ?? [];
7315
+ }
7316
+
7317
+ //#endregion
7318
+ //#region src/core/secrets/SecretsVault.ts
7319
+ /**
7320
+ * Encrypted secrets vault backed by `tamer-secrets-{env}` D1.
7321
+ * Stores ciphertext only — never plaintext.
7322
+ */
7323
+ var SecretsVault = class {
7324
+ databaseId;
7325
+ constructor(api, env, databaseId) {
7326
+ this.api = api;
7327
+ this.env = env;
7328
+ this.databaseId = databaseId;
7329
+ }
7330
+ async dbId() {
7331
+ if (this.databaseId) return this.databaseId;
7332
+ const uuid$1 = await findTamerSecretsDatabaseUuid(this.api, this.env);
7333
+ if (!uuid$1) throw new Error(`secrets vault not provisioned for env "${this.env}" (expected D1 ${tamerSecretsDatabaseName(this.env)}); run tamer bootstrap`);
7334
+ this.databaseId = uuid$1;
7335
+ return uuid$1;
7336
+ }
7337
+ async upsert(name, encrypted, valueHash, options) {
7338
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7339
+ const updatedBy = options?.updatedBy;
7340
+ const db = await this.dbId();
7341
+ await this.api.d1Query(db, `INSERT INTO secrets (
7342
+ name, ciphertext, iv, wrapped_dek, dek_iv, value_hash, updated_at, updated_by
7343
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
7344
+ ON CONFLICT(name) DO UPDATE SET
7345
+ ciphertext = excluded.ciphertext,
7346
+ iv = excluded.iv,
7347
+ wrapped_dek = excluded.wrapped_dek,
7348
+ dek_iv = excluded.dek_iv,
7349
+ value_hash = excluded.value_hash,
7350
+ updated_at = excluded.updated_at,
7351
+ updated_by = excluded.updated_by`, [
7352
+ name,
7353
+ blobParam(encrypted.ciphertext),
7354
+ blobParam(encrypted.iv),
7355
+ blobParam(encrypted.wrappedDek),
7356
+ blobParam(encrypted.dekIv),
7357
+ valueHash,
7358
+ updatedAt,
7359
+ updatedBy ?? null
7360
+ ]);
7361
+ await this.api.d1Query(db, `INSERT INTO secret_history (name, value_hash, updated_at, updated_by)
7362
+ VALUES (?, ?, ?, ?)`, [
7363
+ name,
7364
+ valueHash,
7365
+ updatedAt,
7366
+ updatedBy ?? null
7367
+ ]);
7368
+ await this.api.d1Query(db, `DELETE FROM secret_history
7369
+ WHERE name = ?
7370
+ AND rowid IN (
7371
+ SELECT rowid FROM (
7372
+ SELECT rowid FROM secret_history
7373
+ WHERE name = ?
7374
+ ORDER BY updated_at DESC
7375
+ LIMIT -1 OFFSET ?
7376
+ )
7377
+ )`, [
7378
+ name,
7379
+ name,
7380
+ SECRET_HISTORY_CAP
7381
+ ]);
7382
+ }
7383
+ async get(name) {
7384
+ const db = await this.dbId();
7385
+ const { rows } = await this.api.d1Query(db, `SELECT name, ciphertext, iv, wrapped_dek, dek_iv, value_hash, updated_at, updated_by
7386
+ FROM secrets WHERE name = ?`, [name]);
7387
+ if (rows.length === 0) return void 0;
7388
+ return rowToVaultSecret(rows[0]);
7389
+ }
7390
+ async list() {
7391
+ const db = await this.dbId();
7392
+ const { rows } = await this.api.d1Query(db, `SELECT name, value_hash, updated_at, updated_by
7393
+ FROM secrets ORDER BY name`);
7394
+ return rows.map((row) => ({
7395
+ name: String(row.name),
7396
+ valueHash: row.value_hash,
7397
+ updatedAt: String(row.updated_at),
7398
+ updatedBy: row.updated_by != null ? String(row.updated_by) : void 0
7399
+ }));
7400
+ }
7401
+ async delete(name) {
7402
+ const db = await this.dbId();
7403
+ const { rows } = await this.api.d1Query(db, `DELETE FROM secrets WHERE name = ? RETURNING name`, [name]);
7404
+ return rows.length > 0;
7405
+ }
7406
+ /** Append an audit row (e.g. after `secrets get`) without changing the secret. */
7407
+ async appendAudit(name, options) {
7408
+ const record$1 = await this.get(name);
7409
+ if (!record$1) throw new Error(`secret "${name}" not found in vault`);
7410
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
7411
+ const updatedBy = options?.updatedBy;
7412
+ const db = await this.dbId();
7413
+ await this.api.d1Query(db, `INSERT INTO secret_history (name, value_hash, updated_at, updated_by)
7414
+ VALUES (?, ?, ?, ?)`, [
7415
+ name,
7416
+ record$1.valueHash,
7417
+ updatedAt,
7418
+ updatedBy ?? null
7419
+ ]);
7420
+ await this.api.d1Query(db, `DELETE FROM secret_history
7421
+ WHERE name = ?
7422
+ AND rowid IN (
7423
+ SELECT rowid FROM (
7424
+ SELECT rowid FROM secret_history
7425
+ WHERE name = ?
7426
+ ORDER BY updated_at DESC
7427
+ LIMIT -1 OFFSET ?
7428
+ )
7429
+ )`, [
7430
+ name,
7431
+ name,
7432
+ SECRET_HISTORY_CAP
7433
+ ]);
7434
+ }
7435
+ async history(name) {
7436
+ const db = await this.dbId();
7437
+ const { rows } = await this.api.d1Query(db, `SELECT name, value_hash, updated_at, updated_by
7438
+ FROM secret_history
7439
+ WHERE name = ?
7440
+ ORDER BY updated_at DESC`, [name]);
7441
+ return rows.map((row) => ({
7442
+ name: String(row.name),
7443
+ valueHash: row.value_hash,
7444
+ updatedAt: String(row.updated_at),
7445
+ updatedBy: row.updated_by != null ? String(row.updated_by) : void 0
7446
+ }));
7447
+ }
7448
+ };
7449
+ function rowToVaultSecret(row) {
7450
+ return {
7451
+ name: String(row.name),
7452
+ ciphertext: blobFromRow(row.ciphertext),
7453
+ iv: blobFromRow(row.iv),
7454
+ wrappedDek: blobFromRow(row.wrapped_dek),
7455
+ dekIv: blobFromRow(row.dek_iv),
7456
+ valueHash: row.value_hash,
7457
+ updatedAt: String(row.updated_at),
7458
+ updatedBy: row.updated_by != null ? String(row.updated_by) : void 0
7459
+ };
7460
+ }
7461
+ /** D1 HTTP API accepts base64 for BLOB bind parameters. */
7462
+ function blobParam(bytes) {
7463
+ return Buffer.from(bytes).toString("base64");
7464
+ }
7465
+ function blobFromRow(value) {
7466
+ if (value instanceof Uint8Array) return value;
7467
+ if (typeof value === "string") return new Uint8Array(Buffer.from(value, "base64"));
7468
+ if (Array.isArray(value)) return new Uint8Array(value);
7469
+ throw new Error("secrets vault: invalid BLOB column value");
7470
+ }
7471
+
7472
+ //#endregion
7473
+ //#region src/cli/commands/secrets/context.ts
7474
+ function resolveSecretsEnv(env) {
7475
+ const resolved = env ?? "dev";
7476
+ if (resolved === "local") throw new Error("secrets commands require a remote env (e.g. --env dev); \"local\" has no vault");
7477
+ return resolved;
7478
+ }
7479
+ /** Operator identity for vault audit rows (`USER` / `USERNAME` / `cli`). */
7480
+ function cliUpdatedBy() {
7481
+ return process.env.USER ?? process.env.USERNAME ?? "cli";
7482
+ }
7483
+ function vaultReaderFromVault(vault) {
7484
+ return { async get(name) {
7485
+ const row = await vault.get(name);
7486
+ if (!row) return void 0;
7487
+ return {
7488
+ name: row.name,
7489
+ valueHash: row.valueHash
7490
+ };
7491
+ } };
7492
+ }
7493
+ async function createSecretsContext(options) {
7494
+ const env = resolveSecretsEnv(options.env);
7495
+ const config$1 = await loadConfig(options.configPath, { env });
7496
+ const accountId = config$1.account_id ?? cloudflareAccountIdFromEnv();
7497
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
7498
+ const api = new CFApiClient(accountId);
7499
+ const vault = new SecretsVault(api, env, await ensureTamerSecretsDatabase(api, env));
7500
+ const state = new StateManager(config$1.tenant.id, env, stackNameForConfig(config$1));
7501
+ await state.hydrate(api);
7502
+ const masterKey = readMasterKeyFromEnv(env);
7503
+ return {
7504
+ env,
7505
+ config: config$1,
7506
+ api,
7507
+ vault,
7508
+ state,
7509
+ naming: namingFromConfig(config$1),
7510
+ accountId,
7511
+ masterKey
7512
+ };
7513
+ }
7514
+ async function collectSecretWorkers(ctx, workerFilter) {
7515
+ const workers = await getWorkers(ctx.config);
7516
+ const imports = await fetchStackImports(ctx.api, ctx.config, ctx.env).catch(() => ({}));
7517
+ const out = [];
7518
+ for (const [workerKey, workerConfig] of workers) {
7519
+ if (workerFilter && workerKey !== workerFilter) continue;
7520
+ const required$1 = requiredSecretsForWorker(mergeWorkerConfigForResourcePick(ctx.config, workerKey, workerConfig, ctx.env, ctx.accountId, ctx.naming, ctx.state, {
7521
+ referencesMode: "tolerant",
7522
+ imports
7523
+ }));
7524
+ if (required$1.length === 0) continue;
7525
+ const deployedName = resolveDeployedWorkerName(ctx.config, workerKey, workerConfig, ctx.env, ctx.naming);
7526
+ let workerSecretNames = [];
7527
+ try {
7528
+ workerSecretNames = await ctx.api.workersSecretsList(deployedName);
7529
+ } catch {
7530
+ workerSecretNames = [];
7531
+ }
7532
+ out.push({
7533
+ workerKey,
7534
+ required: required$1,
7535
+ workerSecretNames,
7536
+ deployedName
7537
+ });
7538
+ }
7539
+ return out;
7540
+ }
7541
+ function assertSecretName(name) {
7542
+ const trimmed = name?.trim();
7543
+ if (!trimmed) throw new Error("secret name is required");
7544
+ return trimmed;
7545
+ }
7546
+ /** Vault + master key for deploy auto-push (remote envs only). */
7547
+ async function createDeploySecretsResources(api, env) {
7548
+ const resolvedEnv = resolveSecretsEnv(env);
7549
+ return {
7550
+ vault: new SecretsVault(api, resolvedEnv, await ensureTamerSecretsDatabase(api, resolvedEnv)),
7551
+ masterKey: readMasterKeyFromEnv(resolvedEnv)
7552
+ };
7553
+ }
7554
+
7555
+ //#endregion
7556
+ //#region src/cli/commands/secrets/init.ts
7557
+ async function runSecretsInit(options) {
7558
+ const env = resolveSecretsEnv(options.env);
7559
+ const accountId = (await loadConfig(options.configPath, { env })).account_id ?? cloudflareAccountIdFromEnv();
7560
+ if (!accountId) throw new Error("account_id required in config or CLOUDFLARE_ACCOUNT_ID env var");
7561
+ const masterKey = generateMasterKey();
7562
+ const varName = masterKeyEnvVarName(env);
7563
+ const uuid$1 = await ensureTamerSecretsDatabase(new CFApiClient(accountId), env);
7564
+ console.log(`Secrets vault ready: D1 uuid=${uuid$1} name=${tamerSecretsDatabaseName(env)}`);
7565
+ console.log("");
7566
+ console.log(`Generated master key for env "${env}" (store in two durable places — CI + password manager):`);
7567
+ console.log("");
7568
+ console.log(`${varName}=${masterKey}`);
7569
+ console.log("");
7570
+ console.log("This key is shown once. Tamer never persists it.");
7571
+ }
7572
+
7573
+ //#endregion
7574
+ //#region src/core/secrets/crypto.ts
7575
+ const AES_GCM = "AES-GCM";
7576
+ const AES_KEY_LENGTH = 256;
7577
+ const IV_BYTE_LENGTH = 12;
7578
+ function toArrayBuffer(bytes) {
7579
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
7580
+ }
7581
+ /** Thrown when decrypt or re-wrap fails (wrong key, tampered ciphertext, etc.). */
7582
+ var SecretsCryptoError = class extends Error {
7583
+ code = "SECRETS_CRYPTO_ERROR";
7584
+ constructor(message) {
7585
+ super(message);
7586
+ this.name = "SecretsCryptoError";
7587
+ }
7588
+ };
7589
+ /** Envelope-encrypt a secret value under the master key. */
7590
+ async function encryptSecretValue(plaintext, masterKey) {
7591
+ const kek = await importAesKey(masterKey, ["wrapKey"]);
7592
+ const dek = await crypto.subtle.generateKey({
7593
+ name: AES_GCM,
7594
+ length: AES_KEY_LENGTH
7595
+ }, true, ["encrypt", "decrypt"]);
7596
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTE_LENGTH));
7597
+ const dekIv = crypto.getRandomValues(new Uint8Array(IV_BYTE_LENGTH));
7598
+ const plaintextBytes = new TextEncoder().encode(plaintext);
7599
+ const ciphertextBuffer = await crypto.subtle.encrypt({
7600
+ name: AES_GCM,
7601
+ iv
7602
+ }, dek, plaintextBytes);
7603
+ const wrappedDekBuffer = await crypto.subtle.wrapKey("raw", dek, kek, {
7604
+ name: AES_GCM,
7605
+ iv: dekIv
7606
+ });
7607
+ return {
7608
+ ciphertext: new Uint8Array(ciphertextBuffer),
7609
+ iv,
7610
+ wrappedDek: new Uint8Array(wrappedDekBuffer),
7611
+ dekIv
7612
+ };
7613
+ }
7614
+ /** Decrypt an envelope-encrypted secret value. */
7615
+ async function decryptSecretValue(encrypted, masterKey) {
7616
+ try {
7617
+ const kek = await importAesKey(masterKey, ["unwrapKey"]);
7618
+ const dek = await crypto.subtle.unwrapKey("raw", toArrayBuffer(encrypted.wrappedDek), kek, {
7619
+ name: AES_GCM,
7620
+ iv: toArrayBuffer(encrypted.dekIv)
7621
+ }, {
7622
+ name: AES_GCM,
7623
+ length: AES_KEY_LENGTH
7624
+ }, false, ["decrypt"]);
7625
+ const plaintextBuffer = await crypto.subtle.decrypt({
7626
+ name: AES_GCM,
7627
+ iv: toArrayBuffer(encrypted.iv)
7628
+ }, dek, toArrayBuffer(encrypted.ciphertext));
7629
+ return new TextDecoder().decode(plaintextBuffer);
7630
+ } catch {
7631
+ throw new SecretsCryptoError("Failed to decrypt secret (wrong master key or corrupted ciphertext)");
7632
+ }
7633
+ }
7634
+ async function importAesKey(rawKey, usages) {
7635
+ return crypto.subtle.importKey("raw", toArrayBuffer(rawKey), {
7636
+ name: AES_GCM,
7637
+ length: AES_KEY_LENGTH
7638
+ }, false, usages);
7639
+ }
7640
+
7641
+ //#endregion
7642
+ //#region src/core/secrets/fingerprint.ts
7643
+ /** SHA-256 fingerprint of secret plaintext (non-reversible). */
7644
+ async function secretValueFingerprint(plaintext) {
7645
+ const bytes = new TextEncoder().encode(plaintext);
7646
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
7647
+ return `sha256:${bytesToHex(new Uint8Array(hashBuffer))}`;
7648
+ }
7649
+ function bytesToHex(bytes) {
7650
+ return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
7651
+ }
7652
+
7653
+ //#endregion
7654
+ //#region src/cli/commands/secrets/stdin.ts
7655
+ /**
7656
+ * Read a secret value from stdin (pipe only — avoids shell history / argv).
7657
+ * Interactive prompt is intentionally not supported in v1.
7658
+ */
7659
+ async function readSecretValueFromStdin() {
7660
+ if (process.stdin.isTTY) throw new Error("secrets set: pipe the value on stdin (e.g. echo -n 'value' | tamer secrets set NAME --env dev); interactive entry is not supported yet");
7661
+ const chunks = [];
7662
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
7663
+ let value = Buffer.concat(chunks).toString("utf8");
7664
+ if (value.endsWith("\n")) value = value.slice(0, -1);
7665
+ if (value.endsWith("\r")) value = value.slice(0, -1);
7666
+ if (!value) throw new Error("secrets set: empty value on stdin");
7667
+ return value;
7668
+ }
7669
+
7670
+ //#endregion
7671
+ //#region src/cli/commands/secrets/set.ts
7672
+ async function runSecretsSet(options) {
7673
+ const name = assertSecretName(options.name);
7674
+ const ctx = await createSecretsContext({
7675
+ env: options.env,
7676
+ configPath: options.configPath
7677
+ });
7678
+ const plaintext = options.readValue ? await options.readValue() : await readSecretValueFromStdin();
7679
+ const encrypted = await encryptSecretValue(plaintext, ctx.masterKey);
7680
+ const valueHash = await secretValueFingerprint(plaintext);
7681
+ await ctx.vault.upsert(name, encrypted, valueHash, { updatedBy: cliUpdatedBy() });
7682
+ console.log(`secrets: stored ${name} (${valueHash}) in ${ctx.env} vault`);
7683
+ }
7684
+
7685
+ //#endregion
7686
+ //#region src/cli/commands/secrets/dotenv.ts
7687
+ /** Parse a dotenv-style file into key/value pairs (no variable expansion). */
7688
+ function parseDotenvContent(content) {
7689
+ const result = {};
7690
+ for (const rawLine of content.split(/\r?\n/)) {
7691
+ const line = rawLine.trim();
7692
+ if (!line || line.startsWith("#")) continue;
7693
+ const eq = line.indexOf("=");
7694
+ if (eq <= 0) continue;
7695
+ const key = line.slice(0, eq).trim();
7696
+ let value = line.slice(eq + 1).trim();
7697
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
7698
+ if (key) result[key] = value;
7699
+ }
7700
+ return result;
7701
+ }
7702
+ function readDotenvFile(filePath) {
7703
+ return parseDotenvContent(readFileSync(resolve(process.cwd(), filePath), "utf8"));
7704
+ }
7705
+ /** Default bulk-load file for a remote env. Plain `.dev.vars` is wrangler dev / local only. */
7706
+ function defaultSecretsLoadFile(env) {
7707
+ return `.dev.vars.${env}`;
7708
+ }
7709
+ /**
7710
+ * Reject plain `.dev.vars` — reserved for local `wrangler dev`, not vault seeding.
7711
+ */
7712
+ function assertRemoteSecretsLoadFile(filePath, env) {
7713
+ if ((filePath.replace(/\\/g, "/").split("/").pop() ?? filePath) === ".dev.vars") throw new Error(`\`.dev.vars\` is for local wrangler dev only. Seed the vault with \`.dev.vars.${env}\` or omit --file (default).`);
7714
+ }
7715
+
7716
+ //#endregion
7717
+ //#region src/cli/commands/secrets/load.ts
7718
+ async function runSecretsLoad(options) {
7719
+ const env = resolveSecretsEnv(options.env);
7720
+ const file = options.file?.trim() || defaultSecretsLoadFile(env);
7721
+ assertRemoteSecretsLoadFile(file, env);
7722
+ const ctx = await createSecretsContext({
7723
+ env,
7724
+ configPath: options.configPath
7725
+ });
7726
+ const fileEntries = readDotenvFile(file);
7727
+ const names = Object.keys(fileEntries).sort();
7728
+ if (names.length === 0) {
7729
+ console.log("secrets load: no entries to import");
7730
+ return;
7731
+ }
7732
+ let count = 0;
7733
+ for (const name of names) {
7734
+ const plaintext = fileEntries[name];
7735
+ const encrypted = await encryptSecretValue(plaintext, ctx.masterKey);
7736
+ const valueHash = await secretValueFingerprint(plaintext);
7737
+ await ctx.vault.upsert(name, encrypted, valueHash, { updatedBy: cliUpdatedBy() });
7738
+ count += 1;
7739
+ }
7740
+ console.log(`secrets: loaded ${count} secret(s) into ${ctx.env} vault from ${file}`);
7741
+ }
7742
+
7743
+ //#endregion
7744
+ //#region src/cli/commands/secrets/get.ts
7745
+ async function defaultConfirm(prompt) {
7746
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("secrets get: confirmation required; run in an interactive terminal or use a test hook");
7747
+ const rl = readline.createInterface({
7748
+ input: process.stdin,
7749
+ output: process.stdout
7750
+ });
7751
+ try {
7752
+ const normalized = (await rl.question(`${prompt} [y/N] `)).trim().toLowerCase();
7753
+ return normalized === "y" || normalized === "yes";
7754
+ } finally {
7755
+ rl.close();
7756
+ }
7757
+ }
7758
+ async function runSecretsGet(options) {
7759
+ const name = assertSecretName(options.name);
7760
+ const ctx = await createSecretsContext({
7761
+ env: options.env,
7762
+ configPath: options.configPath
7763
+ });
7764
+ const record$1 = await ctx.vault.get(name);
7765
+ if (!record$1) throw new Error(`secret "${name}" not found in vault`);
7766
+ const confirm = options.confirm ?? defaultConfirm;
7767
+ if (!options.yes) {
7768
+ if (!await confirm(`Reveal secret "${name}" from ${ctx.env} vault? This action is audit-logged.`)) {
7769
+ console.log("secrets get: cancelled");
7770
+ return;
7771
+ }
7772
+ }
7773
+ await ctx.vault.appendAudit(name, { updatedBy: cliUpdatedBy() });
7774
+ const plaintext = await decryptSecretValue({
7775
+ ciphertext: record$1.ciphertext,
7776
+ iv: record$1.iv,
7777
+ wrappedDek: record$1.wrappedDek,
7778
+ dekIv: record$1.dekIv
7779
+ }, ctx.masterKey);
7780
+ process.stdout.write(plaintext);
7781
+ if (!plaintext.endsWith("\n")) process.stdout.write("\n");
7782
+ }
7783
+
7784
+ //#endregion
7785
+ //#region src/cli/commands/secrets/list.ts
7786
+ async function runSecretsList(options) {
7787
+ const ctx = await createSecretsContext({
7788
+ env: options.env,
7789
+ configPath: options.configPath
7790
+ });
7791
+ const items = await ctx.vault.list();
7792
+ if (items.length === 0) {
7793
+ console.log(`secrets (${ctx.env}): (empty)`);
7794
+ return;
7795
+ }
7796
+ console.log(`secrets (${ctx.env}):`);
7797
+ for (const item of items) {
7798
+ const by = item.updatedBy ? ` by ${item.updatedBy}` : "";
7799
+ console.log(` ${item.name} ${item.valueHash} last-set ${item.updatedAt}${by}`);
7800
+ }
7801
+ }
7802
+
7803
+ //#endregion
7804
+ //#region src/cli/commands/secrets/rm.ts
7805
+ async function runSecretsRm(options) {
7806
+ const name = assertSecretName(options.name);
7807
+ const ctx = await createSecretsContext({
7808
+ env: options.env,
7809
+ configPath: options.configPath
7810
+ });
7811
+ if (!await ctx.vault.delete(name)) throw new Error(`secret "${name}" not found in vault`);
7812
+ console.log(`secrets: removed ${name} from ${ctx.env} vault`);
7813
+ }
7814
+
7815
+ //#endregion
7816
+ //#region src/core/secrets/reconcile.ts
7817
+ /** State row key: `secret:{worker}:{name}`. */
7818
+ function secretStateKey(worker, name) {
7819
+ return `secret:${worker}:${name}`;
7820
+ }
7821
+ /** Plan/drift derived identifier: `{worker}:{name}`. */
7822
+ function secretDerivedName(worker, name) {
7823
+ return `${worker}:${name}`;
7824
+ }
7825
+ /**
7826
+ * Compare declared secrets × vault fingerprints × state last-pushed hashes ×
7827
+ * worker presence. Returns one entry per relevant secret×worker pair.
7828
+ */
7829
+ async function reconcileSecrets(input) {
7830
+ const { workers, vault, state } = input;
7831
+ const allState = state.getAll();
7832
+ const secretStateByKey = /* @__PURE__ */ new Map();
7833
+ for (const [key, entry] of Object.entries(allState)) if (entry.type === "secret") secretStateByKey.set(key, entry);
7834
+ const entries = [];
7835
+ const seen = /* @__PURE__ */ new Set();
7836
+ function push(entry) {
7837
+ const key = secretStateKey(entry.worker, entry.name);
7838
+ if (seen.has(key)) return;
7839
+ seen.add(key);
7840
+ entries.push(entry);
7841
+ }
7842
+ for (const { workerKey, required: required$1, workerSecretNames } of workers) {
7843
+ const onWorkerSet = new Set(workerSecretNames);
7844
+ for (const name of required$1) {
7845
+ const vaultEntry = await vault.get(name);
7846
+ const stateEntry = secretStateByKey.get(secretStateKey(workerKey, name));
7847
+ push(classifyDeclaredSecret({
7848
+ worker: workerKey,
7849
+ name,
7850
+ vaultHash: vaultEntry?.valueHash,
7851
+ lastPushedHash: stateEntry?.lastPushedHash,
7852
+ onWorker: onWorkerSet.has(name)
7853
+ }));
7854
+ }
7855
+ for (const name of workerSecretNames) {
7856
+ if (required$1.includes(name)) continue;
7857
+ const stateKey = secretStateKey(workerKey, name);
7858
+ if (seen.has(stateKey)) continue;
7859
+ const stateEntry = secretStateByKey.get(stateKey);
7860
+ const vaultEntry = await vault.get(name);
7861
+ if (stateEntry?.lastPushedHash && !vaultEntry) {
7862
+ push({
7863
+ worker: workerKey,
7864
+ name,
7865
+ status: "removed_from_vault",
7866
+ lastPushedHash: stateEntry.lastPushedHash,
7867
+ onWorker: true
7868
+ });
7869
+ continue;
7870
+ }
7871
+ push({
7872
+ worker: workerKey,
7873
+ name,
7874
+ status: "undeclared_on_worker",
7875
+ onWorker: true
7876
+ });
7877
+ }
7878
+ }
7879
+ for (const [key, stateEntry] of secretStateByKey) {
7880
+ if (seen.has(key)) continue;
7881
+ if (await vault.get(stateEntry.name)) continue;
7882
+ push({
7883
+ worker: stateEntry.worker,
7884
+ name: stateEntry.name,
7885
+ status: "removed_from_vault",
7886
+ lastPushedHash: stateEntry.lastPushedHash,
7887
+ onWorker: workers.some((w) => w.workerKey === stateEntry.worker && w.workerSecretNames.includes(stateEntry.name))
7888
+ });
7889
+ }
7890
+ return entries;
7891
+ }
7892
+ function classifyDeclaredSecret(args$1) {
7893
+ const { worker, name, vaultHash, lastPushedHash, onWorker } = args$1;
7894
+ if (!vaultHash) return {
7895
+ worker,
7896
+ name,
7897
+ status: "declared_no_value",
7898
+ lastPushedHash,
7899
+ onWorker
7900
+ };
7901
+ if (!lastPushedHash) return {
7902
+ worker,
7903
+ name,
7904
+ status: "never_deployed",
7905
+ vaultHash,
7906
+ onWorker
7907
+ };
7908
+ if (vaultHash !== lastPushedHash) return {
7909
+ worker,
7910
+ name,
7911
+ status: "rotated_not_deployed",
7912
+ vaultHash,
7913
+ lastPushedHash,
7914
+ onWorker
7915
+ };
7916
+ if (!onWorker) return {
7917
+ worker,
7918
+ name,
7919
+ status: "never_deployed",
7920
+ vaultHash,
7921
+ lastPushedHash,
7922
+ onWorker: false
7923
+ };
7924
+ return {
7925
+ worker,
7926
+ name,
7927
+ status: "in_sync",
7928
+ vaultHash,
7929
+ lastPushedHash,
7930
+ onWorker: true
7931
+ };
7932
+ }
7933
+ function secretsDrift(entries) {
7934
+ const drift = {
7935
+ kind: "secret",
7936
+ missingFromCloudflare: [],
7937
+ unrecordedInState: [],
7938
+ undeployed: []
7939
+ };
7940
+ for (const e of entries) {
7941
+ if (e.status === "in_sync") continue;
7942
+ const derivedName = secretDerivedName(e.worker, e.name);
7943
+ switch (e.status) {
7944
+ case "declared_no_value":
7945
+ drift.undeployed.push({
7946
+ logicalName: e.name,
7947
+ derivedName,
7948
+ detail: "declared, no vault value"
7949
+ });
7950
+ break;
7951
+ case "never_deployed":
7952
+ drift.undeployed.push({
7953
+ logicalName: e.name,
7954
+ derivedName,
7955
+ detail: "never deployed"
7956
+ });
7957
+ break;
7958
+ case "rotated_not_deployed":
7959
+ drift.undeployed.push({
7960
+ logicalName: e.name,
7961
+ derivedName,
7962
+ detail: "rotated, not deployed"
7963
+ });
7964
+ break;
7965
+ case "removed_from_vault":
7966
+ drift.missingFromCloudflare.push({
7967
+ logicalName: e.name,
7968
+ derivedName,
7969
+ detail: "removed from vault"
7970
+ });
7971
+ break;
7972
+ case "undeclared_on_worker":
7973
+ drift.unrecordedInState.push({
7974
+ logicalName: e.name,
7975
+ derivedName,
7976
+ detail: "undeclared on worker"
7977
+ });
7978
+ break;
7979
+ }
7980
+ }
7981
+ return drift;
7982
+ }
7983
+ function secretsPlanItems(entries) {
7984
+ const items = [];
7985
+ for (const e of entries) {
7986
+ const derivedName = secretDerivedName(e.worker, e.name);
7987
+ switch (e.status) {
7988
+ case "never_deployed":
7989
+ items.push({
7990
+ kind: "secret",
7991
+ action: "create",
7992
+ logicalName: e.name,
7993
+ derivedName,
7994
+ detail: "never deployed"
7995
+ });
7996
+ break;
7997
+ case "rotated_not_deployed":
7998
+ items.push({
7999
+ kind: "secret",
8000
+ action: "update",
8001
+ logicalName: e.name,
8002
+ derivedName,
8003
+ detail: "rotated, not deployed",
8004
+ changes: [{
8005
+ field: "lastPushedHash",
8006
+ from: e.lastPushedHash,
8007
+ to: e.vaultHash,
8008
+ kind: "mutable"
8009
+ }]
8010
+ });
8011
+ break;
8012
+ default: break;
8013
+ }
8014
+ }
8015
+ return items;
8016
+ }
8017
+ /** In-memory vault stub for tests and pre-WS1 integration. */
8018
+ function vaultReaderFromMap(secrets) {
8019
+ const map = secrets instanceof Map ? secrets : new Map(Object.entries(secrets).map(([name, meta$2]) => [name, meta$2]));
8020
+ return { async get(name) {
8021
+ return map.get(name);
8022
+ } };
8023
+ }
8024
+
8025
+ //#endregion
8026
+ //#region src/cli/commands/secrets/verify.ts
8027
+ const STATUS_LABEL = {
8028
+ in_sync: "in sync",
8029
+ declared_no_value: "declared, no vault value",
8030
+ never_deployed: "never deployed",
8031
+ rotated_not_deployed: "rotated, not deployed",
8032
+ removed_from_vault: "removed from vault",
8033
+ undeclared_on_worker: "undeclared on worker"
8034
+ };
8035
+ async function runSecretsVerify(options) {
8036
+ const ctx = await createSecretsContext({
8037
+ env: options.env,
8038
+ configPath: options.configPath
8039
+ });
8040
+ const workers = await collectSecretWorkers(ctx);
8041
+ if (workers.length === 0) {
8042
+ console.log(`secrets verify (${ctx.env}): no workers declare secrets.required`);
8043
+ return 0;
8044
+ }
8045
+ const entries = await reconcileSecrets({
8046
+ workers: workers.map((w) => ({
8047
+ workerKey: w.workerKey,
8048
+ required: w.required,
8049
+ workerSecretNames: w.workerSecretNames
8050
+ })),
8051
+ vault: vaultReaderFromVault(ctx.vault),
8052
+ state: ctx.state
8053
+ });
8054
+ console.log(`\nSecrets verify — env ${ctx.env}\n`);
8055
+ if (entries.length === 0) {
8056
+ console.log(" (no declared secrets)\n");
8057
+ return 0;
8058
+ }
8059
+ let issues = 0;
8060
+ for (const entry of entries.sort((a, b) => secretDerivedName(a.worker, a.name).localeCompare(secretDerivedName(b.worker, b.name)))) {
8061
+ const label = STATUS_LABEL[entry.status];
8062
+ const id = secretDerivedName(entry.worker, entry.name);
8063
+ const workerFlag = entry.onWorker ? "on worker" : "not on worker";
8064
+ console.log(` ${id} ${label} (${workerFlag})`);
8065
+ if (entry.status !== "in_sync") issues += 1;
8066
+ }
8067
+ console.log(issues === 0 ? "\nAll declared secrets in sync.\n" : `\n${issues} secret(s) need attention.\n`);
8068
+ return issues === 0 ? 0 : 1;
8069
+ }
8070
+
8071
+ //#endregion
8072
+ //#region src/cli/commands/secrets/push.ts
8073
+ /**
8074
+
8075
+ * Push stale/new required secrets for one worker via the CF API.
8076
+
8077
+ * Skips secrets whose vault hash matches `lastPushedHash` in state.
8078
+
8079
+ */
8080
+ async function pushSecretsForDeploy(options) {
8081
+ const { workerKey, deployedName, required: required$1, vault, state, api, masterKey, logPushes = false } = options;
8082
+ if (required$1.length === 0) return {
8083
+ pushed: 0,
8084
+ skipped: 0
8085
+ };
8086
+ let pushed = 0;
8087
+ let skipped = 0;
8088
+ for (const secretName of required$1) {
8089
+ const vaultRow = await vault.get(secretName);
8090
+ if (!vaultRow) throw new Error(`secret "${secretName}" required by worker "${workerKey}" is missing from vault; run tamer secrets set ${secretName}`);
8091
+ const stateKey = secretStateKey(workerKey, secretName);
8092
+ const stateEntry = state.get(stateKey);
8093
+ if ((stateEntry?.type === "secret" ? stateEntry.lastPushedHash : void 0) === vaultRow.valueHash) {
8094
+ skipped += 1;
8095
+ continue;
8096
+ }
8097
+ const plaintext = await decryptSecretValue({
8098
+ ciphertext: vaultRow.ciphertext,
8099
+ iv: vaultRow.iv,
8100
+ wrappedDek: vaultRow.wrappedDek,
8101
+ dekIv: vaultRow.dekIv
8102
+ }, masterKey);
8103
+ await api.workersSecretPut(deployedName, {
8104
+ name: secretName,
8105
+ text: plaintext
8106
+ });
8107
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8108
+ state.set(stateKey, {
8109
+ type: "secret",
8110
+ worker: workerKey,
8111
+ name: secretName,
8112
+ lastPushedHash: vaultRow.valueHash,
8113
+ lastPushedAt: now
8114
+ });
8115
+ pushed += 1;
8116
+ if (logPushes) console.log(`secrets push: ${workerKey}:${secretName} → ${deployedName}`);
8117
+ }
8118
+ return {
8119
+ pushed,
8120
+ skipped
8121
+ };
8122
+ }
8123
+ async function runSecretsPush(options) {
8124
+ const ctx = await createSecretsContext({
8125
+ env: options.env,
8126
+ configPath: options.configPath
8127
+ });
8128
+ const workers = await collectSecretWorkers(ctx, options.worker);
8129
+ if (workers.length === 0) {
8130
+ const hint = options.worker ? `worker "${options.worker}" has no secrets.required` : "no workers declare secrets.required";
8131
+ console.log(`secrets push (${ctx.env}): ${hint}`);
8132
+ return;
8133
+ }
8134
+ let pushed = 0;
8135
+ let skipped = 0;
8136
+ for (const worker of workers) {
8137
+ const result = await pushSecretsForDeploy({
8138
+ workerKey: worker.workerKey,
8139
+ deployedName: worker.deployedName,
8140
+ required: worker.required,
8141
+ vault: ctx.vault,
8142
+ state: ctx.state,
8143
+ api: ctx.api,
8144
+ masterKey: ctx.masterKey,
8145
+ logPushes: true
8146
+ });
8147
+ pushed += result.pushed;
8148
+ skipped += result.skipped;
8149
+ }
8150
+ if (pushed > 0) await ctx.state.persist(ctx.api);
8151
+ console.log(`secrets push (${ctx.env}): ${pushed} pushed, ${skipped} already current`);
8152
+ }
8153
+
8154
+ //#endregion
8155
+ //#region src/cli/commands/secrets/index.ts
8156
+ const SECRETS_USAGE = `usage:
8157
+ tamer secrets init --env <env> [--config <path>]
8158
+ tamer secrets set <NAME> --env <env> [--config <path>] # value on stdin (pipe only)
8159
+ tamer secrets load --env <env> [--file <path>] [--config <path>] # default file: .dev.vars.{env}
8160
+ tamer secrets get <NAME> --env <env> [--config <path>] # confirmation + audit log
8161
+ tamer secrets list --env <env> [--config <path>]
8162
+ tamer secrets rm <NAME> --env <env> [--config <path>]
8163
+ tamer secrets verify --env <env> [--config <path>]
8164
+ tamer secrets push --env <env> [--worker <name>] [--config <path>]`;
8165
+ function parseSecretsArgs(argv) {
8166
+ const positional = [];
8167
+ const opts = {};
8168
+ for (let i = 0; i < argv.length; i++) {
8169
+ const arg = argv[i];
8170
+ if (arg.startsWith("--")) {
8171
+ const key = arg.slice(2).replace(/-/g, "_");
8172
+ const next = argv[i + 1];
8173
+ if (next && !next.startsWith("--")) {
8174
+ opts[key] = next;
8175
+ i++;
8176
+ } else opts[key] = true;
8177
+ } else positional.push(arg);
8178
+ }
8179
+ return {
8180
+ subcommand: positional[0],
8181
+ name: positional[1],
8182
+ env: opts.env,
8183
+ configPath: opts.config,
8184
+ file: opts.file,
8185
+ worker: opts.worker,
8186
+ yes: opts.yes === true
8187
+ };
8188
+ }
8189
+ async function runSecrets(argv) {
8190
+ const parsed = parseSecretsArgs(argv);
8191
+ const { subcommand } = parsed;
8192
+ switch (subcommand) {
8193
+ case "init":
8194
+ await runSecretsInit({
8195
+ env: parsed.env,
8196
+ configPath: parsed.configPath
8197
+ });
8198
+ return 0;
8199
+ case "set":
8200
+ await runSecretsSet({
8201
+ name: parsed.name,
8202
+ env: parsed.env,
8203
+ configPath: parsed.configPath
8204
+ });
8205
+ return 0;
8206
+ case "load":
8207
+ await runSecretsLoad({
8208
+ file: parsed.file,
8209
+ env: parsed.env,
8210
+ configPath: parsed.configPath
8211
+ });
8212
+ return 0;
8213
+ case "get":
8214
+ await runSecretsGet({
8215
+ name: parsed.name,
8216
+ env: parsed.env,
8217
+ configPath: parsed.configPath,
8218
+ yes: parsed.yes
8219
+ });
8220
+ return 0;
8221
+ case "list":
8222
+ await runSecretsList({
8223
+ env: parsed.env,
8224
+ configPath: parsed.configPath
8225
+ });
8226
+ return 0;
8227
+ case "rm":
8228
+ await runSecretsRm({
8229
+ name: parsed.name,
8230
+ env: parsed.env,
8231
+ configPath: parsed.configPath
8232
+ });
8233
+ return 0;
8234
+ case "verify": return runSecretsVerify({
8235
+ env: parsed.env,
8236
+ configPath: parsed.configPath
8237
+ });
8238
+ case "push":
8239
+ await runSecretsPush({
8240
+ env: parsed.env,
8241
+ configPath: parsed.configPath,
8242
+ worker: parsed.worker
8243
+ });
8244
+ return 0;
8245
+ default:
8246
+ console.error(SECRETS_USAGE);
8247
+ return 1;
8248
+ }
8249
+ }
8250
+
8251
+ //#endregion
8252
+ //#region src/cli/args.ts
8253
+ const BaseArgsSchema = object({
8254
+ env: string().optional(),
8255
+ config: string().optional(),
8256
+ worker: string().optional()
8257
+ });
8258
+ const ApplyArgsSchema = BaseArgsSchema.extend({
8259
+ add_shard: string().optional(),
8260
+ plan: string().optional(),
8261
+ allow_stale: boolean().optional(),
8262
+ rollback_on_failure: boolean().optional(),
8263
+ target: string().optional()
8264
+ });
8265
+ const DestroyArgsSchema = BaseArgsSchema.extend({
8266
+ env: string().min(1, { error: "env is required for destroy" }),
8267
+ force: boolean().optional(),
8268
+ skip_workers: boolean().optional(),
8269
+ confirm_env: string().optional(),
8270
+ wipe_metadata: boolean().optional(),
8271
+ plan: string().optional(),
8272
+ allow_stale: boolean().optional()
8273
+ });
8274
+ const BootstrapArgsSchema = object({
8275
+ env: string().min(1, { error: "env is required for bootstrap" }),
8276
+ config: string().optional()
8277
+ });
8278
+ const DriftArgsSchema = BaseArgsSchema.extend({ json: boolean().optional() });
8279
+ const PlanArgsSchema = BaseArgsSchema.extend({
8280
+ json: boolean().optional(),
8281
+ detailed_exitcode: boolean().optional(),
8282
+ out: string().optional(),
8283
+ destroy: boolean().optional(),
8284
+ target: string().optional()
8285
+ });
8286
+ const ImportArgsSchema = object({
8287
+ env: string().min(1, { error: "env is required for import" }),
8288
+ config: string().optional(),
8289
+ kind: _enum([
8290
+ "d1",
8291
+ "r2",
8292
+ "kv",
8293
+ "queue",
8294
+ "hyperdrive",
8295
+ "vectorize",
8296
+ "ai_gateway",
8297
+ "pipeline",
8298
+ "workflow",
8299
+ "secret_store",
8300
+ "dns_record",
8301
+ "dispatch_namespace",
8302
+ "worker_route"
8303
+ ], { error: "kind must be one of d1 | r2 | kv | queue | hyperdrive | vectorize | ai_gateway | pipeline | workflow | secret_store | dns_record | dispatch_namespace | worker_route" }),
8304
+ logical: string().min(1, { error: "logical is required" }),
8305
+ cf_id: string().optional(),
8306
+ shard_date: string().optional(),
8307
+ created_date: string().optional(),
8308
+ route_id: string().optional(),
8309
+ zone_name: string().optional()
8310
+ });
8311
+ const StatusArgsSchema = BaseArgsSchema.extend({ tenant: string().optional() });
8312
+ const EventsArgsSchema = BaseArgsSchema.extend({
8313
+ json: boolean().optional(),
8314
+ limit: number().int().min(1).max(100).optional()
8315
+ });
8316
+ const DoctorArgsSchema = object({ json: boolean().optional() });
8317
+ const ProvisionTenantArgsSchema = object({
8318
+ env: string().min(1, { error: "env is required" }),
8319
+ product: string().min(1, { error: "product is required" }),
8320
+ workspace: string().min(1, { error: "workspace is required" }),
8321
+ main: string().optional(),
8322
+ artifact_key: string().optional(),
8323
+ module_name: string().optional(),
8324
+ config: string().optional(),
8325
+ compatibility_date: string().optional(),
8326
+ compat_flags: string().optional(),
8327
+ shards: string().optional(),
8328
+ json: boolean().optional()
8329
+ }).refine((d) => !!(d.main || d.artifact_key), { message: "Provide --main <file> or --artifact-key <r2-key> (under tamer-artifacts-{env})" });
8330
+ const DestroyTenantArgsSchema = object({
8331
+ env: string().min(1, { error: "env is required" }),
8332
+ product: string().min(1, { error: "product is required" }),
8333
+ workspace: string().min(1, { error: "workspace is required" }),
8334
+ force: boolean().optional(),
8335
+ confirm_tenant: string().optional(),
8336
+ config: string().optional(),
8337
+ json: boolean().optional()
8338
+ });
8339
+ const DeployArgsSchema = BaseArgsSchema.extend({ dispatch_namespace: string().optional() });
8340
+ const DevArgsSchema = BaseArgsSchema.extend({ all: boolean().optional() });
8341
+ function parseArgs(argv) {
8342
+ const opts = {};
8343
+ for (let i = 0; i < argv.length; i++) {
8344
+ const arg = argv[i];
8345
+ if (arg.startsWith("--")) {
8346
+ const key = arg.slice(2).replace(/-/g, "_");
8347
+ const next = argv[i + 1];
8348
+ if (next && !next.startsWith("--")) {
8349
+ opts[key] = next;
8350
+ i++;
8351
+ } else opts[key] = true;
8352
+ }
8353
+ }
8354
+ return opts;
8355
+ }
8356
+ function toOpts(raw) {
8357
+ const opts = {};
8358
+ for (const [k, v] of Object.entries(raw)) opts[k] = v;
8359
+ return opts;
8360
+ }
8361
+ function formatZodError(error) {
8362
+ return error.issues.map((e) => e.path.length ? `${e.path.join(".")}: ${e.message}` : e.message).join("; ");
8363
+ }
8364
+ function parseSyncArgs(argv) {
8365
+ const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
8366
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8367
+ return {
8368
+ env: parsed.data.env,
8369
+ configPath: parsed.data.config
8370
+ };
8371
+ }
8372
+ function parseApplyArgs(argv) {
8373
+ const parsed = ApplyArgsSchema.safeParse(toOpts(parseArgs(argv)));
8374
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8375
+ return {
8376
+ env: parsed.data.env,
8377
+ addShard: parsed.data.add_shard,
8378
+ configPath: parsed.data.config,
8379
+ planFile: parsed.data.plan,
8380
+ allowStale: parsed.data.allow_stale,
8381
+ rollbackOnFailure: parsed.data.rollback_on_failure,
8382
+ target: parsed.data.target
8383
+ };
8384
+ }
8385
+ function parseDevArgs(argv) {
8386
+ const parsed = DevArgsSchema.safeParse(toOpts(parseArgs(argv)));
8387
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8388
+ return {
8389
+ worker: parsed.data.worker,
8390
+ env: parsed.data.env,
8391
+ configPath: parsed.data.config,
8392
+ all: parsed.data.all
8393
+ };
8394
+ }
8395
+ function parseDeployArgs(argv) {
8396
+ const parsed = DeployArgsSchema.safeParse(toOpts(parseArgs(argv)));
8397
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8398
+ return {
8399
+ worker: parsed.data.worker,
8400
+ env: parsed.data.env,
8401
+ configPath: parsed.data.config,
8402
+ dispatchNamespace: parsed.data.dispatch_namespace
8403
+ };
8404
+ }
8405
+ function parseMigrateArgs(argv) {
8406
+ const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
8407
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8408
+ return {
8409
+ worker: parsed.data.worker,
8410
+ env: parsed.data.env,
8411
+ configPath: parsed.data.config
8412
+ };
8413
+ }
8414
+ function parseTypesArgs(argv) {
8415
+ const parsed = BaseArgsSchema.safeParse(toOpts(parseArgs(argv)));
8416
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8417
+ return {
8418
+ worker: parsed.data.worker,
8419
+ env: parsed.data.env,
8420
+ configPath: parsed.data.config
8421
+ };
8422
+ }
8423
+ function parseStatusArgs(argv) {
8424
+ const parsed = StatusArgsSchema.safeParse(toOpts(parseArgs(argv)));
8425
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8426
+ return {
8427
+ env: parsed.data.env,
8428
+ configPath: parsed.data.config,
8429
+ tenant: parsed.data.tenant
8430
+ };
8431
+ }
8432
+ function parseEventsArgs(argv) {
8433
+ const parsed = EventsArgsSchema.safeParse(toOpts(parseArgs(argv)));
8434
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8435
+ return {
8436
+ env: parsed.data.env,
8437
+ configPath: parsed.data.config,
8438
+ limit: parsed.data.limit,
8439
+ json: parsed.data.json
8440
+ };
8441
+ }
8442
+ function parseDoctorArgs(argv) {
8443
+ const parsed = DoctorArgsSchema.safeParse(toOpts(parseArgs(argv)));
8444
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8445
+ return { json: parsed.data.json };
8446
+ }
8447
+ function parseProvisionTenantArgs(argv) {
8448
+ const parsed = ProvisionTenantArgsSchema.safeParse(toOpts(parseArgs(argv)));
8449
+ if (!parsed.success) throw new Error(`Invalid arguments: ${formatZodError(parsed.error)}`);
8450
+ const d = parsed.data;
8451
+ return {
8452
+ env: d.env,
4254
8453
  product: d.product,
4255
8454
  workspace: d.workspace,
4256
8455
  main: d.main,
@@ -4339,17 +8538,6 @@ function parseBootstrapArgs(argv) {
4339
8538
  };
4340
8539
  }
4341
8540
 
4342
- //#endregion
4343
- //#region src/core/state/StateConflictError.ts
4344
- /** Thrown when D1 `cfi_state` was updated by another writer since {@link StateManager.hydrate}. */
4345
- var StateConflictError = class extends Error {
4346
- code = "STATE_CONFLICT";
4347
- constructor(message) {
4348
- super(message);
4349
- this.name = "StateConflictError";
4350
- }
4351
- };
4352
-
4353
8541
  //#endregion
4354
8542
  //#region src/cli/index.ts
4355
8543
  const args = process.argv.slice(2);
@@ -4360,14 +8548,14 @@ async function main() {
4360
8548
  try {
4361
8549
  switch (command) {
4362
8550
  case "bootstrap":
4363
- await import("./bootstrap-BicPW44a.mjs").then((m) => m.runBootstrap(parseBootstrapArgs(rest)));
8551
+ await import("./bootstrap-D__dHw1w.mjs").then((m) => m.runBootstrap(parseBootstrapArgs(rest)));
4364
8552
  break;
4365
8553
  case "sync":
4366
- await import("./sync-BdJ43vO7.mjs").then((m) => m.runSync(parseSyncArgs(rest)));
8554
+ await import("./sync-CpfxqlOx.mjs").then((m) => m.runSync(parseSyncArgs(rest)));
4367
8555
  break;
4368
8556
  case "apply": {
4369
8557
  const a = parseApplyArgs(rest);
4370
- await import("./apply-CWU3HY0P.mjs").then((m) => m.runApply({
8558
+ await import("./apply-BjrYbyHn.mjs").then((m) => m.runApply({
4371
8559
  env: a.env,
4372
8560
  addShard: a.addShard,
4373
8561
  configPath: a.configPath,
@@ -4379,11 +8567,11 @@ async function main() {
4379
8567
  break;
4380
8568
  }
4381
8569
  case "dev":
4382
- await import("./dev-BYItpt9U.mjs").then((m) => m.runDev(parseDevArgs(rest)));
8570
+ await import("./dev-BLthyLml.mjs").then((m) => m.runDev(parseDevArgs(rest)));
4383
8571
  break;
4384
8572
  case "deploy": {
4385
8573
  const d = parseDeployArgs(rest);
4386
- await import("./deploy-DAEjDjOm.mjs").then((m) => m.runDeploy({
8574
+ await import("./deploy-C6fX9td0.mjs").then((m) => m.runDeploy({
4387
8575
  worker: d.worker,
4388
8576
  env: d.env,
4389
8577
  configPath: d.configPath,
@@ -4392,23 +8580,23 @@ async function main() {
4392
8580
  break;
4393
8581
  }
4394
8582
  case "migrate":
4395
- await import("./migrate-Bwl0w6XN.mjs").then((m) => m.runMigrate(parseMigrateArgs(rest)));
8583
+ await import("./migrate-CroDjbJz.mjs").then((m) => m.runMigrate(parseMigrateArgs(rest)));
4396
8584
  break;
4397
8585
  case "types":
4398
- await import("./types-CN1BOr0U.mjs").then((m) => m.runTypes(parseTypesArgs(rest)));
8586
+ await import("./types-BzzHwIdw.mjs").then((m) => m.runTypes(parseTypesArgs(rest)));
4399
8587
  break;
4400
8588
  case "status":
4401
- await import("./status-BAPpi2Zt.mjs").then((m) => m.runStatus(parseStatusArgs(rest)));
8589
+ await import("./status-DkkS5lc9.mjs").then((m) => m.runStatus(parseStatusArgs(rest)));
4402
8590
  break;
4403
8591
  case "events":
4404
- await import("./events-B6oCdvSt.mjs").then((m) => m.runEvents(parseEventsArgs(rest)));
8592
+ await import("./events-otk0l3aJ.mjs").then((m) => m.runEvents(parseEventsArgs(rest)));
4405
8593
  break;
4406
8594
  case "drift":
4407
- exitStatus = await import("./drift-DRnwTyZD.mjs").then((m) => m.runDrift(parseDriftArgs(rest)));
8595
+ exitStatus = await import("./drift-BCxWdYHG.mjs").then((m) => m.runDrift(parseDriftArgs(rest)));
4408
8596
  break;
4409
8597
  case "plan": {
4410
8598
  const p = parsePlanArgs(rest);
4411
- exitStatus = await import("./plan-BNIAD--f.mjs").then((m) => m.runPlan({
8599
+ exitStatus = await import("./plan-C2urqJOz.mjs").then((m) => m.runPlan({
4412
8600
  env: p.env,
4413
8601
  configPath: p.configPath,
4414
8602
  json: p.json,
@@ -4420,14 +8608,14 @@ async function main() {
4420
8608
  break;
4421
8609
  }
4422
8610
  case "import":
4423
- await import("./import-D8zaVvwK.mjs").then((m) => m.runImport(parseImportArgs(rest)));
8611
+ await import("./import-OvohE-H2.mjs").then((m) => m.runImport(parseImportArgs(rest)));
4424
8612
  break;
4425
8613
  case "doctor":
4426
- exitStatus = await import("./doctor-C_hs7k2D.mjs").then((m) => m.runDoctor(parseDoctorArgs(rest)));
8614
+ exitStatus = await import("./doctor-32YLAXXl.mjs").then((m) => m.runDoctor(parseDoctorArgs(rest)));
4427
8615
  break;
4428
8616
  case "provision-tenant": {
4429
8617
  const p = parseProvisionTenantArgs(rest);
4430
- await import("./provision-tenant-BcZocyyn.mjs").then((m) => m.runProvisionTenant({
8618
+ await import("./provision-tenant-BJ1KugON.mjs").then((m) => m.runProvisionTenant({
4431
8619
  env: p.env,
4432
8620
  product: p.product,
4433
8621
  workspace: p.workspace,
@@ -4444,7 +8632,7 @@ async function main() {
4444
8632
  }
4445
8633
  case "destroy-tenant": {
4446
8634
  const t = parseDestroyTenantArgs(rest);
4447
- await import("./destroy-tenant-B-VLKfc6.mjs").then((m) => m.runDestroyTenant({
8635
+ await import("./destroy-tenant-T_94ed9x.mjs").then((m) => m.runDestroyTenant({
4448
8636
  env: t.env,
4449
8637
  product: t.product,
4450
8638
  workspace: t.workspace,
@@ -4457,7 +8645,7 @@ async function main() {
4457
8645
  }
4458
8646
  case "destroy": {
4459
8647
  const d = parseDestroyArgs(rest);
4460
- await import("./destroy-DtgPD_bD.mjs").then((m) => m.runDestroy({
8648
+ await import("./destroy-vfk2Zbfj.mjs").then((m) => m.runDestroy({
4461
8649
  env: d.env,
4462
8650
  force: d.force,
4463
8651
  skipWorkers: d.skipWorkers,
@@ -4472,18 +8660,21 @@ async function main() {
4472
8660
  case "wfp": {
4473
8661
  const [sub, ...wfpRest] = rest;
4474
8662
  if (sub === "put") {
4475
- const { parseWfpPutArgs, runWfpPut } = await import("./wfp-put-DjErqxFa.mjs");
8663
+ const { parseWfpPutArgs, runWfpPut } = await import("./wfp-put-BrwICc9i.mjs");
4476
8664
  await runWfpPut(parseWfpPutArgs(wfpRest));
4477
8665
  break;
4478
8666
  }
4479
8667
  if (sub === "delete") {
4480
- const { parseWfpDeleteArgs, runWfpDelete } = await import("./wfp-delete-BG9WBd7F.mjs");
8668
+ const { parseWfpDeleteArgs, runWfpDelete } = await import("./wfp-delete-CDBFqmrM.mjs");
4481
8669
  await runWfpDelete(parseWfpDeleteArgs(wfpRest));
4482
8670
  break;
4483
8671
  }
4484
8672
  console.error("usage:\n tamer wfp put --namespace <n> --script-name <s> --main <file> [--compatibility-date <d>] [--compat-flags a,b] [--config <path>]\n tamer wfp delete --namespace <n> --script-name <s> [--force] [--config <path>]");
4485
8673
  process.exit(1);
4486
8674
  }
8675
+ case "secrets":
8676
+ exitStatus = await import("./secrets-CnzjvndT.mjs").then((m) => m.runSecrets(rest));
8677
+ break;
4487
8678
  case "help":
4488
8679
  case "-h":
4489
8680
  case "--help":
@@ -4510,6 +8701,7 @@ Commands:
4510
8701
  provision-tenant Runtime: create per-tenant D1 shards declared in tenant.d1Shards + upload dispatch script (--main or --artifact-key); --shards a,b trims to a subset of the configured layout (omit for all); --json emits machine-readable result for Cloudflare Container callers
4511
8702
  destroy-tenant Runtime: remove tenant script + D1 + state (shared envs need --confirm-tenant); --json emits machine-readable result
4512
8703
  destroy Delete all resources for an env (use with caution)
8704
+ secrets Encrypted secrets vault (init, set, load, get, list, rm, verify, push)
4513
8705
  wfp put Upload a single-module user Worker to a dispatch namespace (multipart API)
4514
8706
  wfp delete Delete a user Worker from a dispatch namespace
4515
8707
 
@@ -4536,6 +8728,16 @@ Options:
4536
8728
  --route-id <id> import worker_route: route id from /zones/{id}/workers/routes
4537
8729
  --zone-name <z> import worker_route: optional zone hint when a worker has multiple routes
4538
8730
 
8731
+ Secrets (requires TAMER_SECRETS_KEY_{env} master key env var):
8732
+ tamer secrets init --env <env> Generate master key (print once) + provision vault
8733
+ tamer secrets set <NAME> --env <env> Encrypt value from stdin into vault (pipe only)
8734
+ tamer secrets load --env <env> [--file .dev.vars.<env>] Bulk import from env file; not plain .dev.vars
8735
+ tamer secrets get <NAME> --env <env> Decrypt + print (confirmation + audit log)
8736
+ tamer secrets list --env <env> Names + fingerprints + last-set (never values)
8737
+ tamer secrets rm <NAME> --env <env> Remove from vault
8738
+ tamer secrets verify --env <env> Reconcile declared vs vault vs pushed vs worker
8739
+ tamer secrets push --env <env> Push stale/new secrets to workers via CF API
8740
+
4539
8741
  Environment variables (same as Wrangler):
4540
8742
  CLOUDFLARE_ACCOUNT_ID Cloudflare account ID
4541
8743
  CLOUDFLARE_API_TOKEN Cloudflare API token
@@ -4563,5 +8765,5 @@ Environment variables (same as Wrangler):
4563
8765
  main();
4564
8766
 
4565
8767
  //#endregion
4566
- export { boolean as a, literal as c, record as d, string as f, array as i, number$1 as l, _enum as n, custom as o, union as p, any as r, discriminatedUnion as s, StateConflictError as t, object as u };
8768
+ export { resolveReferencesInString as A, getWorkers as B, stackNameForConfig as C, resolveDeployedWorkerName as D, mergedWorkerConfigForEnv as E, ensureTamerSecretsDatabase as F, tamerSecretsDatabaseName as I, CFApiClient as L, effectiveDispatchNamespaceName as M, isEphemeralEnv as N, resolveWorkerConfig as O, namingFromConfig as P, cloudflareAccountIdFromEnv as R, tamerStateDatabaseName as S, mergeWorkerConfigForResourcePick as T, loadConfig as V, tenantShardDatabaseName as _, reconcileSecrets as a, destroyTamerStateDatabase as b, vaultReaderFromMap as c, fetchStackImports as d, importedStackNames as f, tenantDispatchScriptName as g, parseTenantShardRoles as h, pushSecretsForDeploy as i, wranglerConfigCliArgs as j, rewriteIntraStackServiceTargets as k, createDeploySecretsResources as l, StateManager as m, parseSecretsArgs as n, secretsDrift as o, scanConfigForImports as p, runSecrets as r, secretsPlanItems as s, SECRETS_USAGE as t, requiredSecretsForWorker as u, tenantStateKey as v, buildIntraStackScriptNameMap as w, ensureTamerStateDatabase as x, createEmptyCfiState as y, cloudflareApiTokenFromEnv as z };
4567
8769
  //# sourceMappingURL=tamer.mjs.map