@dragonmastery/tamer 0.1.2 → 0.29.0

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 (89) hide show
  1. package/README.md +570 -18
  2. package/dist/CFApiClient-DhbyyV71.mjs +868 -0
  3. package/dist/CFApiClient-DhbyyV71.mjs.map +1 -0
  4. package/dist/StateManager-DTqtLLVX.mjs +760 -0
  5. package/dist/StateManager-DTqtLLVX.mjs.map +1 -0
  6. package/dist/apply-BOABC3UB.mjs +423 -0
  7. package/dist/apply-BOABC3UB.mjs.map +1 -0
  8. package/dist/applyTarget-GWDEOXeY.mjs +152 -0
  9. package/dist/applyTarget-GWDEOXeY.mjs.map +1 -0
  10. package/dist/bootstrap-BxwxC_2Z.mjs +33 -0
  11. package/dist/bootstrap-BxwxC_2Z.mjs.map +1 -0
  12. package/dist/buildDispatchUploadForm-BoUB93b3.mjs +38 -0
  13. package/dist/buildDispatchUploadForm-BoUB93b3.mjs.map +1 -0
  14. package/dist/cloudflareSnapshot-DzPuCRTh.mjs +163 -0
  15. package/dist/cloudflareSnapshot-DzPuCRTh.mjs.map +1 -0
  16. package/dist/deploy-C0edCpn9.mjs +119 -0
  17. package/dist/deploy-C0edCpn9.mjs.map +1 -0
  18. package/dist/destroy-DzgA4lCA.mjs +215 -0
  19. package/dist/destroy-DzgA4lCA.mjs.map +1 -0
  20. package/dist/destroy-tenant-U0t7BeJ0.mjs +103 -0
  21. package/dist/destroy-tenant-U0t7BeJ0.mjs.map +1 -0
  22. package/dist/dev-CZbKfdFw.mjs +103 -0
  23. package/dist/dev-CZbKfdFw.mjs.map +1 -0
  24. package/dist/dns-records.resolve-C2T0m4NG.mjs +3 -0
  25. package/dist/dns-records.resolve-DwBR_1WI.mjs +47 -0
  26. package/dist/dns-records.resolve-DwBR_1WI.mjs.map +1 -0
  27. package/dist/dns-records.sync-Bpzz9H0s.mjs +75 -0
  28. package/dist/dns-records.sync-Bpzz9H0s.mjs.map +1 -0
  29. package/dist/doctor-C_hs7k2D.mjs +34 -0
  30. package/dist/doctor-C_hs7k2D.mjs.map +1 -0
  31. package/dist/drift-B5bpkI0i.mjs +323 -0
  32. package/dist/drift-B5bpkI0i.mjs.map +1 -0
  33. package/dist/drift-BNa92AK5.mjs +10 -0
  34. package/dist/events-BIznt8Sj.mjs +68 -0
  35. package/dist/events-BIznt8Sj.mjs.map +1 -0
  36. package/dist/fetchStackImports-C-1THPYL.mjs +3826 -0
  37. package/dist/fetchStackImports-C-1THPYL.mjs.map +1 -0
  38. package/dist/generator-Ba-vqyBG.mjs +77 -0
  39. package/dist/generator-Ba-vqyBG.mjs.map +1 -0
  40. package/dist/import-B0dlwKoQ.mjs +164 -0
  41. package/dist/import-B0dlwKoQ.mjs.map +1 -0
  42. package/dist/index.d.mts +5673 -1290
  43. package/dist/index.d.mts.map +1 -1
  44. package/dist/index.mjs +18 -1
  45. package/dist/index.mjs.map +1 -0
  46. package/dist/loader-DAvCKLTT.mjs +518 -0
  47. package/dist/loader-DAvCKLTT.mjs.map +1 -0
  48. package/dist/logpush-job-DsRkOORJ.mjs +1106 -0
  49. package/dist/logpush-job-DsRkOORJ.mjs.map +1 -0
  50. package/dist/migrate-BpW6JkIg.mjs +87 -0
  51. package/dist/migrate-BpW6JkIg.mjs.map +1 -0
  52. package/dist/normalize-DVSTRZhO.mjs +253 -0
  53. package/dist/normalize-DVSTRZhO.mjs.map +1 -0
  54. package/dist/plan-Do5rE-c5.mjs +453 -0
  55. package/dist/plan-Do5rE-c5.mjs.map +1 -0
  56. package/dist/planFormat-CJw8Kq2s.mjs +119 -0
  57. package/dist/planFormat-CJw8Kq2s.mjs.map +1 -0
  58. package/dist/provision-tenant-Wfck-2Oa.mjs +192 -0
  59. package/dist/provision-tenant-Wfck-2Oa.mjs.map +1 -0
  60. package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs +92 -0
  61. package/dist/r2S3EmptyBucket-DD81ZWQ7.mjs.map +1 -0
  62. package/dist/stackOutputs-CQQHtdPA.mjs +69 -0
  63. package/dist/stackOutputs-CQQHtdPA.mjs.map +1 -0
  64. package/dist/status-D5GLpWyn.mjs +198 -0
  65. package/dist/status-D5GLpWyn.mjs.map +1 -0
  66. package/dist/sync-B_pyPi7Z.mjs +90 -0
  67. package/dist/sync-B_pyPi7Z.mjs.map +1 -0
  68. package/dist/tamer.d.mts +1 -0
  69. package/dist/tamer.mjs +4553 -0
  70. package/dist/tamer.mjs.map +1 -0
  71. package/dist/tamerArtifactsR2-Ccgplu2Q.mjs +52 -0
  72. package/dist/tamerArtifactsR2-Ccgplu2Q.mjs.map +1 -0
  73. package/dist/types-JrdlG7Dy.mjs +44 -0
  74. package/dist/types-JrdlG7Dy.mjs.map +1 -0
  75. package/dist/verifyPlanFile-ah_4tvTu.mjs +33 -0
  76. package/dist/verifyPlanFile-ah_4tvTu.mjs.map +1 -0
  77. package/dist/wfp-delete-BhuUrBUA.mjs +36 -0
  78. package/dist/wfp-delete-BhuUrBUA.mjs.map +1 -0
  79. package/dist/wfp-put-DL0mJNNz.mjs +52 -0
  80. package/dist/wfp-put-DL0mJNNz.mjs.map +1 -0
  81. package/dist/worker-route-CMbtozNa.mjs +263 -0
  82. package/dist/worker-route-CMbtozNa.mjs.map +1 -0
  83. package/dist/workers-C-oeZhdD.mjs +87 -0
  84. package/dist/workers-C-oeZhdD.mjs.map +1 -0
  85. package/dist/wranglerSpawn-DmEz0ldT.mjs +24 -0
  86. package/dist/wranglerSpawn-DmEz0ldT.mjs.map +1 -0
  87. package/dist/zoneResolver-VoxLHM4N.mjs +32 -0
  88. package/dist/zoneResolver-VoxLHM4N.mjs.map +1 -0
  89. package/package.json +38 -3
@@ -0,0 +1,3826 @@
1
+ import { f as getDispatchNamespaces, n as materializeTamerResolvable, r as materializeVars } from "./normalize-DVSTRZhO.mjs";
2
+ import { t as getWorkers } from "./loader-DAvCKLTT.mjs";
3
+ import { f as stackNameForConfig, o as effectiveDispatchNamespaceName, s as isEphemeralEnv, t as StateManager } from "./StateManager-DTqtLLVX.mjs";
4
+ import { n as r2S3CredentialsFromEnv, t as emptyR2BucketViaS3 } from "./r2S3EmptyBucket-DD81ZWQ7.mjs";
5
+ import { n as logApplyChange } from "./planFormat-CJw8Kq2s.mjs";
6
+ import { resolve } from "path";
7
+
8
+ //#region src/core/naming/NamingEngine.ts
9
+ function reLiteral(s) {
10
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11
+ }
12
+ var NamingEngine = class {
13
+ constructor(tenant, conventions) {
14
+ this.tenant = tenant;
15
+ this.conventions = conventions;
16
+ }
17
+ d1SingleName(logicalName, env) {
18
+ if (this.conventions?.d1Single) return this.conventions.d1Single(logicalName, this.tenant.id, env);
19
+ return `db_${logicalName}_t_${this.tenant.id}_${env}`;
20
+ }
21
+ d1ShardName(logicalName, shardDate, env) {
22
+ if (this.conventions?.d1Shard) return this.conventions.d1Shard(logicalName, shardDate, this.tenant.id, env);
23
+ const dateNoDashes = shardDate.replace(/-/g, "");
24
+ if (logicalName === "default" || logicalName === "") return `db_${dateNoDashes}_t_${this.tenant.id}_${env}`;
25
+ return `db_${logicalName}_${dateNoDashes}_t_${this.tenant.id}_${env}`;
26
+ }
27
+ d1SingleBindingKey(logicalName) {
28
+ return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
29
+ }
30
+ d1ShardBindingKey(logicalName, shardDate) {
31
+ const dateNoDashes = shardDate.replace(/-/g, "");
32
+ return `DB_${logicalName.toUpperCase().replace(/-/g, "_")}_${dateNoDashes}_T_${this.tenant.id.toUpperCase()}`;
33
+ }
34
+ r2BucketName(logicalName, env) {
35
+ if (this.conventions?.r2Bucket) {
36
+ const dateNoDashes = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
37
+ return this.conventions.r2Bucket(logicalName, dateNoDashes, this.tenant.id, env);
38
+ }
39
+ return `r2-${logicalName}-t-${this.tenant.id}-${env}`;
40
+ }
41
+ /**
42
+ * Wrangler `r2_buckets[].binding`: logical + tenant only (same idea as {@link kvBindingKey}).
43
+ * Stable across envs; default {@link r2BucketName} is `r2-{logical}-t-{tenant}-{env}`.
44
+ */
45
+ r2BindingKey(logicalName) {
46
+ return `R2_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
47
+ }
48
+ kvNamespaceName(logicalName, env) {
49
+ return `kv_${logicalName}_t_${this.tenant.id}_${env}`;
50
+ }
51
+ kvBindingKey(logicalName) {
52
+ return `KV_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
53
+ }
54
+ queueName(logicalName, env) {
55
+ return `q-${logicalName}-t-${this.tenant.id}-${env}`;
56
+ }
57
+ queueBindingKey(logicalName) {
58
+ return `Q_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
59
+ }
60
+ hyperdriveName(logicalName, env) {
61
+ return `hd-${logicalName}-t-${this.tenant.id}-${env}`;
62
+ }
63
+ hyperdriveBindingKey(logicalName) {
64
+ return `HD_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
65
+ }
66
+ vectorizeName(logicalName, env) {
67
+ return `vec-${logicalName}-t-${this.tenant.id}-${env}`;
68
+ }
69
+ vectorizeBindingKey(logicalName) {
70
+ return `VEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
71
+ }
72
+ /**
73
+ * AI Gateway slug (== Cloudflare gateway id). Per the Cloudflare API,
74
+ * gateway ids are case-sensitive lowercase ascii with `-`/`_`. Format:
75
+ * `aigw-{logical}-t-{tenantId}-{env}`.
76
+ */
77
+ aiGatewayId(logicalName, env) {
78
+ return `aigw-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
79
+ }
80
+ /**
81
+ * Stable cross-reference binding key for AI Gateway. AI Gateway has no
82
+ * Wrangler binding kind today, so this is only consumed by
83
+ * `${tamer:ai_gateway:<logical>.binding}` interpolations.
84
+ */
85
+ aiGatewayBindingKey(logicalName) {
86
+ return `AI_GW_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
87
+ }
88
+ /**
89
+ * Cloudflare-side pipeline name. Pipelines V1 names must be lowercase
90
+ * alphanumerics + hyphens (no underscores), so we mirror the R2 scheme.
91
+ * Pattern: `pipe-{logical}-t-{tenantId}-{env}`.
92
+ */
93
+ pipelineName(logicalName, env) {
94
+ return `pipe-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
95
+ }
96
+ /**
97
+ * Wrangler binding key emitted in `pipelines[]`. Uppercased logical with
98
+ * the tenant id appended so two tenants can share a worker namespace
99
+ * without colliding bindings.
100
+ */
101
+ pipelineBindingKey(logicalName) {
102
+ return `PIPE_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
103
+ }
104
+ pipelineMatchPattern(logicalName, env) {
105
+ const exact = this.pipelineName(logicalName, env);
106
+ return (name) => name === exact;
107
+ }
108
+ /**
109
+ * Cloudflare-side workflow name. Workflow names accept lowercase
110
+ * alphanumerics + hyphens; we mirror the pipelines/AI-Gateway hyphen
111
+ * scheme. Pattern: `wf-{logical}-t-{tenantId}-{env}`.
112
+ */
113
+ workflowName(logicalName, env) {
114
+ if (this.conventions?.workflow) return this.conventions.workflow(logicalName, this.tenant.id, env);
115
+ return `wf-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
116
+ }
117
+ /**
118
+ * Wrangler binding key emitted in `workflows[]`. Uppercased logical with
119
+ * the tenant id appended so two tenants sharing a script can't collide.
120
+ */
121
+ workflowBindingKey(logicalName) {
122
+ return `WF_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
123
+ }
124
+ workflowMatchPattern(logicalName, env) {
125
+ const exact = this.workflowName(logicalName, env);
126
+ return (name) => name === exact;
127
+ }
128
+ /**
129
+ * Cloudflare Secrets Store name. Account-scoped — the API allows free-form
130
+ * names (the dashboard examples use both `service_x_keys` and dashed
131
+ * variants), so we mirror our hyphen convention for consistency with
132
+ * pipelines / workflows / AI Gateway. Pattern:
133
+ * `sec-{logical}-t-{tenantId}-{env}`.
134
+ */
135
+ secretsStoreName(logicalName, env) {
136
+ return `sec-${logicalName}-t-${this.tenant.id}-${env}`.toLowerCase();
137
+ }
138
+ /**
139
+ * Stable cross-reference key for the store. Secrets Store has no Wrangler
140
+ * binding kind directly — `secrets_store_secrets[]` references the
141
+ * resolved `store_id`, not the store name — so this only powers
142
+ * `${tamer:secret_store:<n>.binding}` interpolations.
143
+ */
144
+ secretsStoreBindingKey(logicalName) {
145
+ return `SEC_${logicalName.toUpperCase().replace(/-/g, "_")}_T_${this.tenant.id.toUpperCase()}`;
146
+ }
147
+ secretsStoreMatchPattern(logicalName, env) {
148
+ const exact = this.secretsStoreName(logicalName, env);
149
+ return (name) => name === exact;
150
+ }
151
+ workerName(workerKey, env) {
152
+ if (this.conventions?.workerName) return this.conventions.workerName(this.tenant.slug, workerKey, env, this.tenant.id);
153
+ if (env === "local") return `${this.tenant.slug}-${workerKey}-${this.tenant.id}`;
154
+ return `${this.tenant.slug}-${workerKey}-${env}-${this.tenant.id}`;
155
+ }
156
+ d1MatchPattern(logicalName, env) {
157
+ if (this.conventions?.d1Shard) return (name) => {
158
+ const shardDate = this.extractD1ShardDate(name);
159
+ if (!shardDate) return false;
160
+ return this.d1ShardName(logicalName, shardDate, env) === name;
161
+ };
162
+ const suffix = `_t_${this.tenant.id}_${env}`;
163
+ if (logicalName === "default" || logicalName === "") return (name) => /^db_\d{8}_t_/.test(name) && name.endsWith(suffix);
164
+ const prefix = `db_${logicalName}_`;
165
+ return (name) => name.startsWith(prefix) && name.endsWith(suffix);
166
+ }
167
+ /**
168
+ * Default: exact {@link r2BucketName}, or legacy dated buckets
169
+ * `r2-{logical}-{YYYYMMDD}-t-{tenant}-{env}` from older Tamer versions.
170
+ * Custom {@link NamingConventions.r2Bucket}: exact name only (uses today's date
171
+ * when calling the hook, same as apply).
172
+ */
173
+ r2MatchPattern(logicalName, env) {
174
+ if (this.conventions?.r2Bucket) {
175
+ const expected = this.r2BucketName(logicalName, env);
176
+ return (name) => name === expected;
177
+ }
178
+ const exactNew = `r2-${logicalName}-t-${this.tenant.id}-${env}`;
179
+ const legacyDated = /* @__PURE__ */ new RegExp(`^r2-${reLiteral(logicalName)}-\\d{8}-t-${reLiteral(this.tenant.id)}-${reLiteral(env)}$`);
180
+ return (name) => name === exactNew || legacyDated.test(name);
181
+ }
182
+ extractD1ShardDate(name) {
183
+ const compact = name.match(/_(\d{8})_t_/);
184
+ if (compact) {
185
+ const d = compact[1];
186
+ return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
187
+ }
188
+ const underscored = name.match(/_(\d{4})_(\d{2})_(\d{2})_t_/);
189
+ if (underscored) return `${underscored[1]}-${underscored[2]}-${underscored[3]}`;
190
+ return null;
191
+ }
192
+ extractR2Date(name) {
193
+ const match = name.match(/r2-\w+-(\d{8})-t-/);
194
+ if (match) {
195
+ const d = match[1];
196
+ return `${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
197
+ }
198
+ return null;
199
+ }
200
+ };
201
+
202
+ //#endregion
203
+ //#region src/core/config/namingFromConfig.ts
204
+ function namingFromConfig(config) {
205
+ const conventions = "naming" in config && config.naming ? config.naming : void 0;
206
+ return new NamingEngine(config.tenant, conventions);
207
+ }
208
+
209
+ //#endregion
210
+ //#region src/core/routes/routes.resolve.ts
211
+ const DEFAULT_PROD_ENVS = ["prod", "production"];
212
+ const DEFAULT_SKIP_ENVS = ["local"];
213
+ /**
214
+ * Per `docs/handoff.md` §6:
215
+ * - prod / production → bare apex (`todo.com`)
216
+ * - any other env (including ephemeral `pr-*`) → `{env}.{apex}`
217
+ * - `local` (or anything in `skipEnvs`) → no route
218
+ *
219
+ * The resource-name `-{env}` suffix on workers/D1/R2/KV is decoupled from
220
+ * URLs; this function only computes the URL.
221
+ */
222
+ function effectiveHostForEnv(route, env) {
223
+ if ((route.skipEnvs ?? DEFAULT_SKIP_ENVS).includes(env)) return void 0;
224
+ if ((route.prodEnvs ?? DEFAULT_PROD_ENVS).includes(env)) return route.host;
225
+ return `${env}.${route.host}`;
226
+ }
227
+ /**
228
+ * Expand one Tamer route into a wrangler `Route` (or `undefined` if the env
229
+ * should not receive the route at all).
230
+ */
231
+ function expandRouteForEnv(route, env) {
232
+ const host = effectiveHostForEnv(route, env);
233
+ if (!host) return void 0;
234
+ const zone = route.zone ?? route.host;
235
+ if (route.customDomain) return {
236
+ pattern: host,
237
+ custom_domain: true
238
+ };
239
+ return {
240
+ pattern: `${host}${route.path ?? "/*"}`,
241
+ zone_name: zone
242
+ };
243
+ }
244
+ /**
245
+ * Expand `tamerRoutes` for the given env, dropping any that resolve to
246
+ * `undefined` (`local`, `skipEnvs`).
247
+ *
248
+ * Note: ephemeral envs (matching `tenant.ephemeralEnvPattern`) follow the
249
+ * same `{env}.{apex}` prefix rule as any non-prod env — e.g. an env named
250
+ * `pr-1234` resolves to `pr-1234.todo.com`. Callers that want a different
251
+ * URL scheme for ephemeral envs should special-case before calling.
252
+ */
253
+ function effectiveRoutesForEnv(tamerRoutes, env) {
254
+ if (!tamerRoutes || tamerRoutes.length === 0) return [];
255
+ const out = [];
256
+ for (const r of tamerRoutes) {
257
+ const expanded = expandRouteForEnv(r, env);
258
+ if (expanded) out.push(expanded);
259
+ }
260
+ return out;
261
+ }
262
+ /**
263
+ * Zone-name routes are attached via the Cloudflare Workers Routes HTTP API
264
+ * (`/zones/{id}/workers/routes`), not via `wrangler.json`, so deploys stay
265
+ * consistent with `tamer drift` / destroy.
266
+ */
267
+ function isApiManagedZoneRoute(r) {
268
+ 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);
269
+ }
270
+ /** Custom-domain `tamerRoutes` stay in wrangler until a dedicated API path exists. */
271
+ function isWranglerOnlyTamerRoute(r) {
272
+ return typeof r === "object" && r !== null && "custom_domain" in r && r.custom_domain === true;
273
+ }
274
+
275
+ //#endregion
276
+ //#region src/core/wrangler/wranglerOutFile.ts
277
+ /** Reject path segments in generated Wrangler config filename. */
278
+ function assertSafeWranglerOutFile(name) {
279
+ const trimmed = name.trim();
280
+ if (!trimmed) throw new Error("wranglerOutFile cannot be empty");
281
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.startsWith("..")) throw new Error(`Invalid wranglerOutFile "${name}": use a basename only (e.g. wrangler.json)`);
282
+ return trimmed;
283
+ }
284
+ /** Extra CLI args so Wrangler uses a non-default config file. */
285
+ function wranglerConfigCliArgs(outFile) {
286
+ if (outFile === "wrangler.json") return [];
287
+ return ["--config", outFile];
288
+ }
289
+
290
+ //#endregion
291
+ //#region src/core/references/references.ts
292
+ var TamerReferenceError = class extends Error {
293
+ constructor(message, fieldPath) {
294
+ super(`${message} (at ${fieldPath})`);
295
+ this.fieldPath = fieldPath;
296
+ this.name = "TamerReferenceError";
297
+ }
298
+ };
299
+ const REF_RE = /\$\{tamer:([a-z0-9_]+):([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
300
+ /**
301
+ * Scan a string for any `${tamer:...}` references. Returns true when at
302
+ * least one reference is present (used for cheap pre-checks).
303
+ */
304
+ function stringHasReference(s) {
305
+ REF_RE.lastIndex = 0;
306
+ return REF_RE.test(s);
307
+ }
308
+ /**
309
+ * Resolve every `${tamer:...}` reference in `value`. Replacement preserves
310
+ * surrounding text for partial-string interpolation. `fieldPath` is included
311
+ * in any thrown {@link TamerReferenceError} for actionable diagnostics.
312
+ */
313
+ function resolveReferencesInString(value, ctx, fieldPath) {
314
+ if (!stringHasReference(value)) return value;
315
+ return value.replace(REF_RE, (match, kind, logicalName, field) => {
316
+ try {
317
+ return lookupReference(kind, logicalName, field, ctx, fieldPath);
318
+ } catch (err) {
319
+ if (ctx.tolerant && err instanceof TamerReferenceError) return match;
320
+ throw err;
321
+ }
322
+ });
323
+ }
324
+ /**
325
+ * Walk a `vars` record (or any flat string→string map) replacing references
326
+ * in every value. Returns a new object; the input is not mutated.
327
+ */
328
+ function resolveReferencesInVars(vars, ctx, fieldPathPrefix) {
329
+ if (!vars) return vars;
330
+ const out = {};
331
+ for (const [key, value] of Object.entries(vars)) {
332
+ if (typeof value !== "string") {
333
+ out[key] = value;
334
+ continue;
335
+ }
336
+ out[key] = resolveReferencesInString(value, ctx, `${fieldPathPrefix}.${key}`);
337
+ }
338
+ return out;
339
+ }
340
+ function lookupReference(kind, logicalName, field, ctx, fieldPath) {
341
+ switch (kind) {
342
+ case "d1": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "d1_database" && entry.logicalName === logicalName);
343
+ case "r2": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "r2_bucket" && entry.logicalName === logicalName);
344
+ case "kv": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "kv_namespace" && entry.logicalName === logicalName);
345
+ case "queue": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "queue" && entry.logicalName === logicalName);
346
+ case "hyperdrive": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "hyperdrive" && entry.logicalName === logicalName);
347
+ case "vectorize": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "vectorize" && entry.logicalName === logicalName);
348
+ case "ai_gateway": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "ai_gateway" && entry.logicalName === logicalName);
349
+ case "pipeline": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "pipeline" && entry.logicalName === logicalName);
350
+ case "workflow": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "workflow" && entry.logicalName === logicalName);
351
+ case "secret_store": return lookupResource(ctx, kind, logicalName, field, fieldPath, (entry) => entry.type === "secrets_store" && entry.logicalName === logicalName);
352
+ case "dispatch_namespace": return lookupDispatchNamespace(ctx, logicalName, field, fieldPath);
353
+ case "worker": return lookupWorker(ctx, logicalName, field, fieldPath);
354
+ case "import": return lookupImport(ctx, logicalName, field, fieldPath);
355
+ case "logpush_pipelines": return lookupLogpushPipelines(ctx, logicalName, field, fieldPath);
356
+ case "config": return lookupConfigField(ctx, logicalName, field, fieldPath);
357
+ 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);
358
+ }
359
+ }
360
+ function lookupConfigField(ctx, _logicalName, field, fieldPath) {
361
+ if (field === "account_id") {
362
+ const id = (ctx.accountId ?? ctx.config.account_id ?? "").trim();
363
+ 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);
364
+ return id;
365
+ }
366
+ throw new TamerReferenceError(`Unknown field "${field}" on config reference — expected account_id`, fieldPath);
367
+ }
368
+ function getLogpushPipelinesEntry(ctx, logicalName, fieldPath) {
369
+ const all = ctx.state.getAll();
370
+ const entry = Object.values(all).find((e) => e.type === "logpush_pipelines" && e.logicalName === logicalName);
371
+ 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);
372
+ return entry;
373
+ }
374
+ /**
375
+ * Exposes fields from the `logpush_pipelines:*` D1 state row (after
376
+ * `ensurePipelinesLogpushProvision` has run) for `outputs` and `vars`, e.g.
377
+ * `${tamer:logpush_pipelines:workers-trace.r2_data_catalog_table_name}`.
378
+ */
379
+ function lookupLogpushPipelines(ctx, logicalName, field, fieldPath) {
380
+ const entry = getLogpushPipelinesEntry(ctx, logicalName, fieldPath);
381
+ switch (field) {
382
+ case "r2_data_catalog_table_name":
383
+ case "iceberg_table": {
384
+ const v = entry.r2DataCatalogTableName?.trim();
385
+ 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);
386
+ return v;
387
+ }
388
+ case "r2_data_catalog_table_name_pipelines":
389
+ case "iceberg_table_pipelines": {
390
+ const v = entry.r2DataCatalogTableNamePipelines?.trim();
391
+ if (v) return v;
392
+ const fallback = entry.r2DataCatalogTableName?.trim();
393
+ 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);
394
+ return fallback;
395
+ }
396
+ case "r2_data_catalog_namespace":
397
+ case "iceberg_namespace": return (entry.r2DataCatalogNamespace ?? "default").trim() || "default";
398
+ case "name": return entry.pipelineName;
399
+ case "id": return entry.pipelineId;
400
+ 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);
401
+ }
402
+ }
403
+ /**
404
+ * Resolve a `${tamer:import:<stack>.<output>}` against pre-fetched sibling
405
+ * stack outputs. The pre-fetch (`fetchStackImports`) loads every imported
406
+ * stack's `cfi_state:{stack}` row before resolution begins; this lookup is
407
+ * pure map access. Throws if the stack isn't in `ctx.imports` (config
408
+ * never declared it / pre-fetch wasn't wired in for this command) or if
409
+ * the named output hasn't been published yet (sibling stack hasn't run
410
+ * `apply` since the output was declared, or has been destroyed).
411
+ */
412
+ function lookupImport(ctx, stackName, outputName, fieldPath) {
413
+ const imports = ctx.imports;
414
+ 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);
415
+ const stackOutputs = imports[stackName];
416
+ if (!(outputName in stackOutputs)) {
417
+ const available = Object.keys(stackOutputs).sort();
418
+ 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);
419
+ }
420
+ return stackOutputs[outputName];
421
+ }
422
+ function lookupResource(ctx, kind, logicalName, field, fieldPath, predicate) {
423
+ const all = ctx.state.getAll();
424
+ const entry = Object.values(all).find(predicate);
425
+ 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);
426
+ switch (field) {
427
+ case "name": return resourceName(entry);
428
+ case "id": return resourceId(entry, kind, logicalName, fieldPath);
429
+ case "binding": return resourceBinding(entry);
430
+ default: throw new TamerReferenceError(`Unknown field "${field}" on ${kind} reference — expected name | id | binding`, fieldPath);
431
+ }
432
+ }
433
+ function resourceName(entry) {
434
+ switch (entry.type) {
435
+ case "d1_database":
436
+ case "kv_namespace":
437
+ case "queue":
438
+ case "hyperdrive":
439
+ case "vectorize":
440
+ case "ai_gateway":
441
+ case "pipeline":
442
+ case "workflow":
443
+ case "secrets_store":
444
+ case "dispatch_namespace":
445
+ case "r2_bucket": return entry.derivedName;
446
+ case "dns_record": return entry.name;
447
+ case "logpush_job": return entry.derivedName;
448
+ case "logpush_pipelines": return entry.pipelineName;
449
+ case "worker_route": return entry.pattern;
450
+ }
451
+ }
452
+ function resourceId(entry, kind, logicalName, fieldPath) {
453
+ switch (entry.type) {
454
+ case "d1_database":
455
+ case "kv_namespace":
456
+ case "queue":
457
+ case "hyperdrive":
458
+ case "vectorize":
459
+ case "ai_gateway":
460
+ case "pipeline":
461
+ case "workflow":
462
+ case "secrets_store": return entry.cfId;
463
+ case "r2_bucket": throw new TamerReferenceError(`R2 bucket "${logicalName}" has no .id (R2 buckets are addressed by name); use \${tamer:${kind}:${logicalName}.name}`, fieldPath);
464
+ case "dispatch_namespace": return entry.derivedName;
465
+ case "dns_record": return entry.recordId;
466
+ case "logpush_job": return String(entry.cfJobId);
467
+ case "logpush_pipelines": return entry.pipelineId;
468
+ case "worker_route": return entry.routeId;
469
+ }
470
+ }
471
+ function resourceBinding(entry) {
472
+ switch (entry.type) {
473
+ case "d1_database":
474
+ case "r2_bucket":
475
+ case "kv_namespace":
476
+ case "queue":
477
+ case "hyperdrive":
478
+ case "vectorize":
479
+ case "ai_gateway":
480
+ case "pipeline":
481
+ case "workflow":
482
+ case "secrets_store": return entry.bindingKey;
483
+ case "dispatch_namespace": return entry.derivedName;
484
+ case "dns_record": throw new Error("internal: dns_record has no .binding — use .name or .id in config references");
485
+ case "logpush_job": throw new Error("internal: logpush_job has no .binding — use .name or .id in config references");
486
+ case "logpush_pipelines": throw new Error("internal: logpush_pipelines has no .binding — use .name or .id in config references");
487
+ case "worker_route": return entry.routeId;
488
+ }
489
+ }
490
+ function lookupDispatchNamespace(ctx, logicalName, field, fieldPath) {
491
+ 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);
492
+ if (field !== "name" && field !== "id") throw new TamerReferenceError(`Unknown field "${field}" on dispatch_namespace reference — expected name | id`, fieldPath);
493
+ const all = ctx.state.getAll();
494
+ const stateEntry = Object.values(all).find((e) => e.type === "dispatch_namespace" && e.logicalName === logicalName);
495
+ 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);
496
+ return stateEntry.derivedName;
497
+ }
498
+ function lookupWorker(ctx, workerKey, field, fieldPath) {
499
+ let target;
500
+ if (ctx.config.workers && ctx.config.workers[workerKey]) target = ctx.config.workers[workerKey];
501
+ else if (ctx.config.worker && workerKey === "default") target = ctx.config.worker;
502
+ if (!target) throw new TamerReferenceError(`Reference \${tamer:worker:${workerKey}.${field}} cannot be resolved — no worker key "${workerKey}" in the Tamer project config`, fieldPath);
503
+ if (field !== "name") throw new TamerReferenceError(`Unknown field "${field}" on worker reference — only "name" is supported (the env-suffixed deployed script name)`, fieldPath);
504
+ return resolveDeployedWorkerName(ctx.config, workerKey, target, ctx.env, ctx.naming);
505
+ }
506
+
507
+ //#endregion
508
+ //#region src/core/config/resolver.ts
509
+ /** Wrangler script name after env suffix rules (matches `tamer deploy`). */
510
+ function resolveDeployedWorkerName(_config, workerKey, workerConfig, env, naming) {
511
+ const sn = workerConfig.scriptName?.trim();
512
+ if (sn) {
513
+ if (env === "local") return sn;
514
+ return `${sn}-${env}`;
515
+ }
516
+ return naming.workerName(workerKey, env);
517
+ }
518
+ /**
519
+ * Map from base service-binding target name (`scriptName` for envs other than
520
+ * `local`, or the local-deployed name) → env-suffixed deployed name for every
521
+ * worker in `config`. Used to auto-rewrite intra-stack `services[].service`
522
+ * fields so fixtures don't have to repeat env overrides for every env.
523
+ */
524
+ function buildIntraStackScriptNameMap(config, env, naming) {
525
+ const map = /* @__PURE__ */ new Map();
526
+ if (config.workers) for (const [key, wc] of Object.entries(config.workers)) {
527
+ const baseName = wc.scriptName?.trim() ?? naming.workerName(key, "local");
528
+ const deployed = resolveDeployedWorkerName(config, key, wc, env, naming);
529
+ map.set(baseName, deployed);
530
+ }
531
+ if (config.worker) {
532
+ const w = config.worker;
533
+ const baseName = w.scriptName?.trim() ?? naming.workerName("default", "local");
534
+ const deployed = resolveDeployedWorkerName(config, "default", w, env, naming);
535
+ map.set(baseName, deployed);
536
+ }
537
+ return map;
538
+ }
539
+ /** Returns a worker config copy whose `services[].service` is rewritten to env-suffixed deployed names. */
540
+ function rewriteIntraStackServiceTargets(workerConfig, baseToDeployed) {
541
+ const services = workerConfig.services;
542
+ if (!services || services.length === 0) return workerConfig;
543
+ let rewroteAny = false;
544
+ const rewritten = services.map((s) => {
545
+ const target = baseToDeployed.get(s.service);
546
+ if (!target || target === s.service) return s;
547
+ rewroteAny = true;
548
+ return {
549
+ ...s,
550
+ service: target
551
+ };
552
+ });
553
+ if (!rewroteAny) return workerConfig;
554
+ return {
555
+ ...workerConfig,
556
+ services: rewritten
557
+ };
558
+ }
559
+ function mergeVars(base = {}, override = {}) {
560
+ return {
561
+ ...base,
562
+ ...override
563
+ };
564
+ }
565
+ /** Same env merge as `resolveWorkerConfig` (for deploy topo-sort service edges). */
566
+ function mergedWorkerConfigForEnv(workerConfig, env, tenant) {
567
+ let merged = { ...workerConfig };
568
+ if (env === "local" && workerConfig.local) merged = {
569
+ ...merged,
570
+ ...workerConfig.local,
571
+ vars: mergeVars(workerConfig.vars, workerConfig.local.vars)
572
+ };
573
+ else if (workerConfig.env?.[env]) {
574
+ const envOverride = workerConfig.env[env];
575
+ merged = {
576
+ ...merged,
577
+ ...envOverride,
578
+ vars: mergeVars(workerConfig.vars, envOverride.vars)
579
+ };
580
+ }
581
+ const mv = merged.vars;
582
+ if (mv && Object.prototype.hasOwnProperty.call(mv, "BRANCH_SUFFIX")) merged = {
583
+ ...merged,
584
+ vars: {
585
+ ...mv,
586
+ BRANCH_SUFFIX: isEphemeralEnv(env, tenant) ? env : ""
587
+ }
588
+ };
589
+ return merged;
590
+ }
591
+ /**
592
+ * Align `dispatch_namespaces[].namespace` and `vars.WFP_NAMESPACE` with
593
+ * {@link effectiveDispatchNamespaceName} for envs that have no explicit
594
+ * `worker.env[env]` block (e.g. `pr-*` shared namespace).
595
+ */
596
+ function applyDispatchNamespaceEnvOverrides(config, merged, env) {
597
+ const dns = getDispatchNamespaces(config);
598
+ if (dns.length === 0) return merged;
599
+ const resolved = effectiveDispatchNamespaceName(dns[0], env, config.tenant);
600
+ const m = merged;
601
+ let next = merged;
602
+ if (m.dispatch_namespaces?.length) next = {
603
+ ...next,
604
+ dispatch_namespaces: m.dispatch_namespaces.map((d) => ({
605
+ ...d,
606
+ namespace: resolved
607
+ }))
608
+ };
609
+ if (m.vars && typeof m.vars.WFP_NAMESPACE === "string" && m.vars.WFP_NAMESPACE === dns[0].namespace) {
610
+ const v = next.vars;
611
+ next = {
612
+ ...next,
613
+ vars: {
614
+ ...v,
615
+ WFP_NAMESPACE: resolved
616
+ }
617
+ };
618
+ }
619
+ return next;
620
+ }
621
+ /**
622
+ * Walk the merged worker config and replace `${tamer:<kind>:<logical>.<field>}`
623
+ * references in `vars` and `tamerRoutes[].host` / `.zone` against the current
624
+ * state snapshot. Also resolves `r2_buckets[].bucket_name`,
625
+ * **`services[].service`**, **`dispatch_namespaces[].namespace`**, and
626
+ * `resources.d1[].databaseName` when `ownership` is `external`. Throws
627
+ * `TamerReferenceError` (with field path) if any
628
+ * reference is unresolved — bubbles up to the caller as a fatal.
629
+ */
630
+ function resolveCrossResourceReferences(merged, ctx) {
631
+ const refCtx = {
632
+ config: ctx.config,
633
+ env: ctx.env,
634
+ state: ctx.state,
635
+ naming: ctx.naming,
636
+ tolerant: ctx.tolerant,
637
+ imports: ctx.imports,
638
+ accountId: ctx.accountId
639
+ };
640
+ const m = merged;
641
+ let next = merged;
642
+ if (m.vars) {
643
+ const resolvedVars = resolveReferencesInVars(materializeVars(m.vars), refCtx, `worker[${ctx.workerKey}].vars`);
644
+ if (resolvedVars && resolvedVars !== m.vars) next = {
645
+ ...next,
646
+ vars: resolvedVars
647
+ };
648
+ }
649
+ const tamerRoutes = next.tamerRoutes;
650
+ if (tamerRoutes && tamerRoutes.length > 0) {
651
+ let mutated = false;
652
+ const resolvedRoutes = tamerRoutes.map((r, idx) => {
653
+ const fieldBase = `worker[${ctx.workerKey}].tamerRoutes[${idx}]`;
654
+ const host = resolveReferencesInString(r.host, refCtx, `${fieldBase}.host`);
655
+ const zone = r.zone ? resolveReferencesInString(r.zone, refCtx, `${fieldBase}.zone`) : r.zone;
656
+ if (host !== r.host || zone !== r.zone) mutated = true;
657
+ return {
658
+ ...r,
659
+ host,
660
+ ...zone !== void 0 ? { zone } : {}
661
+ };
662
+ });
663
+ if (mutated) next = {
664
+ ...next,
665
+ tamerRoutes: resolvedRoutes
666
+ };
667
+ }
668
+ const r2Buckets = next.r2_buckets;
669
+ if (r2Buckets && r2Buckets.length > 0) {
670
+ let mutated = false;
671
+ const resolvedBuckets = r2Buckets.map((b, idx) => {
672
+ const raw = b.bucket_name;
673
+ if (raw === void 0) return b;
674
+ const fieldBase = `worker[${ctx.workerKey}].r2_buckets[${idx}].bucket_name`;
675
+ const bucket_name = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
676
+ if (bucket_name !== raw) mutated = true;
677
+ return {
678
+ ...b,
679
+ bucket_name
680
+ };
681
+ });
682
+ if (mutated) next = {
683
+ ...next,
684
+ r2_buckets: resolvedBuckets
685
+ };
686
+ }
687
+ const svc = next.services;
688
+ if (svc && svc.length > 0) {
689
+ let mutated = false;
690
+ const resolvedSvc = svc.map((s, idx) => {
691
+ const raw = s.service;
692
+ const fieldBase = `worker[${ctx.workerKey}].services[${idx}].service`;
693
+ const service = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
694
+ if (service !== raw) mutated = true;
695
+ return {
696
+ ...s,
697
+ service
698
+ };
699
+ });
700
+ if (mutated) next = {
701
+ ...next,
702
+ services: resolvedSvc
703
+ };
704
+ }
705
+ const dispatchNsMerged = next.dispatch_namespaces;
706
+ if (dispatchNsMerged && dispatchNsMerged.length > 0) {
707
+ let mutated = false;
708
+ const resolvedDn = dispatchNsMerged.map((d, idx) => {
709
+ const raw = d.namespace;
710
+ const fieldBase = `worker[${ctx.workerKey}].dispatch_namespaces[${idx}].namespace`;
711
+ const namespace = resolveReferencesInString(materializeTamerResolvable(raw), refCtx, fieldBase);
712
+ if (namespace !== raw) mutated = true;
713
+ return {
714
+ ...d,
715
+ namespace
716
+ };
717
+ });
718
+ if (mutated) next = {
719
+ ...next,
720
+ dispatch_namespaces: resolvedDn
721
+ };
722
+ }
723
+ const resBlock = next.resources;
724
+ if (resBlock?.d1 && resBlock.d1.length > 0) {
725
+ let mutated = false;
726
+ const d1Resolved = resBlock.d1.map((d1c, idx) => {
727
+ if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
728
+ const fieldBase = `worker[${ctx.workerKey}].resources.d1[${idx}].databaseName`;
729
+ const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
730
+ if (databaseName !== d1c.databaseName) mutated = true;
731
+ return {
732
+ ...d1c,
733
+ databaseName
734
+ };
735
+ });
736
+ if (mutated) next = {
737
+ ...next,
738
+ resources: {
739
+ ...resBlock,
740
+ d1: d1Resolved
741
+ }
742
+ };
743
+ }
744
+ return next;
745
+ }
746
+ function stripTamerFields(config) {
747
+ const { path, config: configPath, resources, local, env, scriptName: _scriptName, wranglerOutFile: _out, dispatchNamespace: _dispatchNs, tamerRoutes: _tamerRoutes, tamerStaleRouteSweepZones: _tamerStaleRouteSweepZones, ...rest } = config;
748
+ return rest;
749
+ }
750
+ /**
751
+ * Env merge + intra-stack `services` rewrite + sibling-stack resolution for
752
+ * **`resources.d1[].databaseName` only** when `ownership: "external"`.
753
+ *
754
+ * Use before {@link ResourceModule.pickResources} during `apply` / `sync` /
755
+ * `drift` so imported D1 names are known **without** resolving worker `vars`
756
+ * (those references often need resources created earlier in the same `apply`).
757
+ */
758
+ function mergeWorkerConfigForResourcePick(config, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
759
+ let merged = mergedWorkerConfigForEnv(workerConfig, env, config.tenant);
760
+ merged = applyDispatchNamespaceEnvOverrides(config, merged, env);
761
+ const intraMap = buildIntraStackScriptNameMap(config, env, naming);
762
+ merged = rewriteIntraStackServiceTargets(merged, intraMap);
763
+ const refCtx = {
764
+ config,
765
+ env,
766
+ state,
767
+ naming,
768
+ tolerant: opts.referencesMode === "tolerant",
769
+ imports: opts.imports,
770
+ accountId
771
+ };
772
+ const resBlock = merged.resources;
773
+ if (!resBlock?.d1?.length) return merged;
774
+ const d1Resolved = resBlock.d1.map((d1c, idx) => {
775
+ if (d1c.ownership !== "external" || !d1c.databaseName) return d1c;
776
+ const fieldBase = `worker[${workerKey}].resources.d1[${idx}].databaseName`;
777
+ const databaseName = resolveReferencesInString(materializeTamerResolvable(d1c.databaseName), refCtx, fieldBase);
778
+ return {
779
+ ...d1c,
780
+ databaseName
781
+ };
782
+ });
783
+ return {
784
+ ...merged,
785
+ resources: {
786
+ ...resBlock,
787
+ d1: d1Resolved
788
+ }
789
+ };
790
+ }
791
+ /**
792
+ * Env-merged worker config with every `${tamer:…}` site resolved for wrangler
793
+ * (`vars`, `tamerRoutes`, `r2_buckets[].bucket_name`, external D1 names).
794
+ * Used by {@link resolveWorkerConfig} after resource state is up to date.
795
+ */
796
+ function mergeWorkerConfigWithResolvedRefs(config, workerKey, workerConfig, env, accountId, naming, state, opts = {}) {
797
+ let merged = mergedWorkerConfigForEnv(workerConfig, env, config.tenant);
798
+ merged = applyDispatchNamespaceEnvOverrides(config, merged, env);
799
+ const intraMap = buildIntraStackScriptNameMap(config, env, naming);
800
+ merged = rewriteIntraStackServiceTargets(merged, intraMap);
801
+ return resolveCrossResourceReferences(merged, {
802
+ config,
803
+ env,
804
+ state,
805
+ naming,
806
+ workerKey,
807
+ tolerant: opts.referencesMode === "tolerant",
808
+ imports: opts.imports,
809
+ accountId
810
+ });
811
+ }
812
+ async function resolveWorkerConfig(config, workerKey, workerConfig, env, baseDir, accountId, naming, state, opts = {}) {
813
+ const merged = mergeWorkerConfigWithResolvedRefs(config, workerKey, workerConfig, env, accountId, naming, state, opts);
814
+ const workerDir = workerConfig.path ? resolve(baseDir, workerConfig.path) : baseDir;
815
+ const m = merged;
816
+ const workerName = resolveDeployedWorkerName(config, workerKey, merged, env, naming);
817
+ const wranglerOutFile = assertSafeWranglerOutFile(m.wranglerOutFile?.trim() || "wrangler.json");
818
+ const dispatchNamespace = m.dispatchNamespace?.trim() || void 0;
819
+ const stripped = stripTamerFields(merged);
820
+ const tamerRoutes = merged.tamerRoutes;
821
+ const expandedRoutes = effectiveRoutesForEnv(tamerRoutes, env);
822
+ const apiManagedRoutes = expandedRoutes.filter(isApiManagedZoneRoute);
823
+ const wranglerTamerRoutes = expandedRoutes.filter(isWranglerOnlyTamerRoute);
824
+ const mergedRoutes = [...stripped.routes ?? [], ...wranglerTamerRoutes];
825
+ /** Non-local deploys emit `routes: []` when there are none — omitting `routes` can leave stale Wrangler-published custom domains attached from a prior config. */
826
+ const wranglerRoutes = mergedRoutes.length > 0 ? mergedRoutes : env === "local" ? void 0 : [];
827
+ return {
828
+ workerKey,
829
+ workerName,
830
+ workerDir,
831
+ env,
832
+ wranglerOutFile,
833
+ dispatchNamespace,
834
+ wranglerConfig: {
835
+ ...stripped,
836
+ ...wranglerRoutes !== void 0 ? { routes: wranglerRoutes } : {},
837
+ name: workerName,
838
+ account_id: accountId,
839
+ compatibility_date: stripped.compatibility_date ?? config.compatibility_date
840
+ },
841
+ resources: merged.resources ?? workerConfig.resources ?? {},
842
+ apiManagedRoutes
843
+ };
844
+ }
845
+
846
+ //#endregion
847
+ //#region src/features/d1/d1.ownership.ts
848
+ function d1IsExternal(config) {
849
+ return config.ownership === "external";
850
+ }
851
+ /**
852
+ * Skip provision / migrate / Cloudflare delete for this binding.
853
+ * `preserveOnDestroy` alone keeps legacy "same derived name as owner" behaviour.
854
+ */
855
+ function d1SkipsProvisionAndMigrate(config) {
856
+ return d1IsExternal(config) || config.preserveOnDestroy === true;
857
+ }
858
+ /** Cloudflare D1 database name for state keys, sync, and wrangler `database_name`. */
859
+ function d1CloudflareDatabaseName(config, env, naming) {
860
+ if (config.type !== "single") throw new Error(`D1 "${config.logicalName}": expected type "single" for derived database name`);
861
+ if (d1IsExternal(config)) {
862
+ const n = config.databaseName;
863
+ if (typeof n !== "string" || !n.trim()) throw new Error(`D1 "${config.logicalName}" has ownership "external" but databaseName is empty — ensure imports are pre-fetched and config is merged with resolveWorkerConfig / mergeWorkerConfigWithResolvedRefs.`);
864
+ return n.trim();
865
+ }
866
+ return naming.d1SingleName(config.logicalName, env);
867
+ }
868
+
869
+ //#endregion
870
+ //#region src/features/d1/d1.apply.ts
871
+ function todayNoDashes() {
872
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10).replace(/-/g, "");
873
+ }
874
+ function todayIso$1() {
875
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
876
+ }
877
+ async function d1Apply(resources, tenant, env, api, state, naming, addShard) {
878
+ for (const config of resources) {
879
+ if (config.type === "single") {
880
+ const derivedName$1 = d1CloudflareDatabaseName(config, env, naming);
881
+ if (state.get(derivedName$1)) continue;
882
+ if (d1SkipsProvisionAndMigrate(config)) {
883
+ console.warn(`[apply] Skipping D1 create for "${config.logicalName}" (external / preserveOnDestroy). Run \`tamer sync\` after the owning stack has created ${derivedName$1}.`);
884
+ continue;
885
+ }
886
+ const { uuid: uuid$1 } = await api.d1Create(derivedName$1);
887
+ state.set(derivedName$1, {
888
+ type: "d1_database",
889
+ logicalName: config.logicalName,
890
+ derivedName: derivedName$1,
891
+ bindingKey: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
892
+ cfId: uuid$1,
893
+ migrationsDir: config.migrationsDir,
894
+ preserveOnDestroy: false,
895
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
896
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
897
+ });
898
+ continue;
899
+ }
900
+ if (addShard && addShard !== config.logicalName) continue;
901
+ if (addShard && addShard === config.logicalName) {
902
+ const shardDate$1 = todayNoDashes();
903
+ const derivedName$1 = naming.d1ShardName(config.logicalName, shardDate$1, env);
904
+ if (state.get(derivedName$1)) continue;
905
+ const { uuid: uuid$1 } = await api.d1Create(derivedName$1);
906
+ state.set(derivedName$1, {
907
+ type: "d1_database",
908
+ logicalName: config.logicalName,
909
+ shardDate: todayIso$1(),
910
+ derivedName: derivedName$1,
911
+ bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate$1),
912
+ cfId: uuid$1,
913
+ migrationsDir: config.migrationsDir,
914
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
915
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
916
+ });
917
+ continue;
918
+ }
919
+ const allResources = state.getAll();
920
+ if (Object.values(allResources).filter((e) => e.type === "d1_database" && "logicalName" in e && e.logicalName === config.logicalName).length > 0) continue;
921
+ const shardDate = todayNoDashes();
922
+ const derivedName = naming.d1ShardName(config.logicalName, shardDate, env);
923
+ const { uuid } = await api.d1Create(derivedName);
924
+ state.set(derivedName, {
925
+ type: "d1_database",
926
+ logicalName: config.logicalName,
927
+ shardDate: todayIso$1(),
928
+ derivedName,
929
+ bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate),
930
+ cfId: uuid,
931
+ migrationsDir: config.migrationsDir,
932
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
933
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
934
+ });
935
+ }
936
+ }
937
+
938
+ //#endregion
939
+ //#region src/features/d1/d1.naming.ts
940
+ function d1MatchPattern(config, env, naming) {
941
+ if (config.type === "single") {
942
+ const exactName = naming.d1SingleName(config.logicalName, env);
943
+ return (name) => name === exactName;
944
+ }
945
+ return naming.d1MatchPattern(config.logicalName, env);
946
+ }
947
+ function d1ExtractShardDate(name, naming) {
948
+ return naming.extractD1ShardDate(name);
949
+ }
950
+
951
+ //#endregion
952
+ //#region src/features/d1/d1.sync.ts
953
+ function d1Sync(allD1, resources, tenant, env, state, naming) {
954
+ for (const config of resources) {
955
+ const pattern = d1MatchPattern(config, env, naming);
956
+ if (config.type === "single") {
957
+ const derivedKey = d1CloudflareDatabaseName(config, env, naming);
958
+ const match = allD1.find((db) => db.name === derivedKey);
959
+ if (match) state.set(match.name, {
960
+ type: "d1_database",
961
+ logicalName: config.logicalName,
962
+ derivedName: match.name,
963
+ bindingKey: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
964
+ cfId: match.uuid,
965
+ migrationsDir: config.migrationsDir,
966
+ preserveOnDestroy: d1SkipsProvisionAndMigrate(config),
967
+ createdAt: match.created_at,
968
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
969
+ });
970
+ continue;
971
+ }
972
+ const shards = allD1.filter((db) => pattern(db.name));
973
+ for (const shard of shards) {
974
+ const shardDate = d1ExtractShardDate(shard.name, naming);
975
+ if (!shardDate) continue;
976
+ state.set(shard.name, {
977
+ type: "d1_database",
978
+ logicalName: config.logicalName,
979
+ shardDate,
980
+ derivedName: shard.name,
981
+ bindingKey: naming.d1ShardBindingKey(config.logicalName, shardDate),
982
+ cfId: shard.uuid,
983
+ migrationsDir: config.migrationsDir,
984
+ createdAt: shard.created_at,
985
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
986
+ });
987
+ }
988
+ }
989
+ }
990
+
991
+ //#endregion
992
+ //#region src/features/d1/d1.drift.ts
993
+ /**
994
+ * Diff D1 state against the Cloudflare list and the current config.
995
+ *
996
+ * - **single**: one expected derived name per resource.
997
+ * - **sharded**: every CF database matching the pattern is "expected"; state
998
+ * should contain an entry for each.
999
+ */
1000
+ function d1Drift(allD1, resources, env, state, naming) {
1001
+ const drift = {
1002
+ kind: "d1",
1003
+ missingFromCloudflare: [],
1004
+ unrecordedInState: [],
1005
+ undeployed: []
1006
+ };
1007
+ const cfByName = new Map(allD1.map((db) => [db.name, db.uuid]));
1008
+ const allState = state.getAll();
1009
+ const d1State = Object.values(allState).filter((e) => e.type === "d1_database");
1010
+ for (const config of resources) {
1011
+ if (config.type === "single") {
1012
+ const derivedName = d1CloudflareDatabaseName(config, env, naming);
1013
+ const cfId = cfByName.get(derivedName);
1014
+ const stateEntry = d1State.find((e) => e.logicalName === config.logicalName && !e.shardDate);
1015
+ if (stateEntry && !cfId) drift.missingFromCloudflare.push(toEntry(stateEntry));
1016
+ else if (cfId && !stateEntry) drift.unrecordedInState.push({
1017
+ logicalName: config.logicalName,
1018
+ derivedName,
1019
+ cfId
1020
+ });
1021
+ else if (!cfId && !stateEntry) drift.undeployed.push({
1022
+ logicalName: config.logicalName,
1023
+ derivedName
1024
+ });
1025
+ continue;
1026
+ }
1027
+ const pattern = naming.d1MatchPattern(config.logicalName, env);
1028
+ const cfShards = allD1.filter((db) => pattern(db.name));
1029
+ const cfShardNames = new Set(cfShards.map((s) => s.name));
1030
+ const stateShards = d1State.filter((e) => e.logicalName === config.logicalName && !!e.shardDate);
1031
+ const stateShardNames = new Set(stateShards.map((s) => s.derivedName));
1032
+ for (const shard of stateShards) if (!cfShardNames.has(shard.derivedName)) drift.missingFromCloudflare.push(toEntry(shard));
1033
+ for (const shard of cfShards) if (!stateShardNames.has(shard.name)) drift.unrecordedInState.push({
1034
+ logicalName: config.logicalName,
1035
+ derivedName: shard.name,
1036
+ cfId: shard.uuid,
1037
+ detail: d1ExtractShardDate(shard.name, naming) ?? void 0
1038
+ });
1039
+ if (cfShards.length === 0 && stateShards.length === 0) drift.undeployed.push({
1040
+ logicalName: config.logicalName,
1041
+ derivedName: `(no shards for ${config.logicalName})`
1042
+ });
1043
+ }
1044
+ return drift;
1045
+ }
1046
+ function toEntry(e) {
1047
+ return {
1048
+ logicalName: e.logicalName,
1049
+ derivedName: e.derivedName,
1050
+ cfId: e.cfId,
1051
+ detail: e.shardDate
1052
+ };
1053
+ }
1054
+
1055
+ //#endregion
1056
+ //#region src/core/config/resourcesFromConfig.ts
1057
+ /**
1058
+ * Logical resource names declared on workers in this config (for
1059
+ * stack-scoped destroy / drift). Iterates the resource registry — adding a
1060
+ * new kind doesn't require editing this file.
1061
+ */
1062
+ async function logicalNamesForResourceKind(config, baseDir, kind) {
1063
+ const mod = getResourceModule(kind);
1064
+ if (!mod) return /* @__PURE__ */ new Set();
1065
+ const workers = await getWorkers(config, baseDir);
1066
+ const set = /* @__PURE__ */ new Set();
1067
+ for (const [, wc] of workers) for (const r of mod.pickResources(wc)) {
1068
+ const logical = r.logicalName;
1069
+ if (logical) set.add(logical);
1070
+ }
1071
+ return set;
1072
+ }
1073
+
1074
+ //#endregion
1075
+ //#region src/features/d1/d1.destroy.ts
1076
+ async function d1Destroy(_env, state, api, config, baseDir, _force) {
1077
+ const owned = await logicalNamesForResourceKind(config, baseDir, "d1");
1078
+ const resources = state.getAll();
1079
+ const d1Entries = Object.values(resources).filter((e) => e.type === "d1_database");
1080
+ for (const entry of d1Entries) {
1081
+ if (!owned.has(entry.logicalName)) continue;
1082
+ if (entry.preserveOnDestroy) {
1083
+ console.log(`Skipping D1 destroy for ${entry.derivedName} (preserveOnDestroy / cross-stack binding).`);
1084
+ continue;
1085
+ }
1086
+ try {
1087
+ await api.d1Delete(entry.cfId);
1088
+ state.delete(entry.derivedName);
1089
+ } catch (err) {
1090
+ console.warn(`Failed to delete D1 ${entry.derivedName}:`, err);
1091
+ }
1092
+ }
1093
+ }
1094
+
1095
+ //#endregion
1096
+ //#region src/features/d1/d1.generate.ts
1097
+ function d1Generate(resources, env, state, naming) {
1098
+ const bindings = [];
1099
+ for (const config of resources) {
1100
+ if (config.type === "single") {
1101
+ const derivedName = d1CloudflareDatabaseName(config, env, naming);
1102
+ const entry = state.get(derivedName);
1103
+ if (!entry || entry.type !== "d1_database") {
1104
+ if (d1SkipsProvisionAndMigrate(config)) throw new Error(`Cross-stack D1 "${config.logicalName}" (${derivedName}) not in state. Deploy the owning stack first, then re-run from this stack (apply now syncs automatically for env "${env}").`);
1105
+ throw new Error(`D1 "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
1106
+ }
1107
+ bindings.push({
1108
+ binding: entry.bindingKey,
1109
+ database_id: entry.cfId,
1110
+ database_name: derivedName,
1111
+ migrations_dir: config.migrationsDir,
1112
+ migrations_table: config.migrationsTable
1113
+ });
1114
+ continue;
1115
+ }
1116
+ const allResources = state.getAll();
1117
+ const shards = Object.values(allResources).filter((e) => e.type === "d1_database" && e.logicalName === config.logicalName);
1118
+ for (const shard of shards) bindings.push({
1119
+ binding: shard.bindingKey,
1120
+ database_id: shard.cfId,
1121
+ database_name: shard.derivedName,
1122
+ migrations_dir: config.migrationsDir,
1123
+ migrations_table: config.migrationsTable
1124
+ });
1125
+ }
1126
+ return bindings;
1127
+ }
1128
+
1129
+ //#endregion
1130
+ //#region src/features/d1/d1.status.ts
1131
+ function d1Status(resources, env, state, naming) {
1132
+ const results = [];
1133
+ for (const config of resources) {
1134
+ if (config.type === "single") {
1135
+ const derivedName = d1CloudflareDatabaseName(config, env, naming);
1136
+ const entry = state.get(derivedName);
1137
+ results.push({
1138
+ binding: config.binding?.trim() || naming.d1SingleBindingKey(config.logicalName),
1139
+ name: derivedName,
1140
+ cfId: entry?.cfId ?? "",
1141
+ status: entry ? "ok" : "missing"
1142
+ });
1143
+ continue;
1144
+ }
1145
+ const allResources = state.getAll();
1146
+ const shards = Object.values(allResources).filter((e) => e.type === "d1_database" && e.logicalName === config.logicalName);
1147
+ for (const shard of shards) results.push({
1148
+ binding: shard.bindingKey,
1149
+ name: shard.derivedName,
1150
+ cfId: shard.cfId,
1151
+ status: "ok"
1152
+ });
1153
+ if (shards.length === 0) results.push({
1154
+ binding: naming.d1ShardBindingKey(config.logicalName, "00000000"),
1155
+ name: `(no shards for ${config.logicalName})`,
1156
+ cfId: "",
1157
+ status: "missing"
1158
+ });
1159
+ }
1160
+ return results;
1161
+ }
1162
+
1163
+ //#endregion
1164
+ //#region src/core/registry/pickResources.ts
1165
+ /**
1166
+ * Helper used by every {@link ResourceModule.pickResources} so module files
1167
+ * can accept either a full `WorkerConfig` (the common path from `apply`,
1168
+ * `sync`, `drift`, `status`) or a bare `WorkerResources` (the wrangler
1169
+ * generator passes `resolved.resources` directly). Returns `undefined`
1170
+ * when no resource map is present.
1171
+ */
1172
+ function resourcesFrom(source) {
1173
+ if (!source) return void 0;
1174
+ if ("resources" in source) return source.resources;
1175
+ return source;
1176
+ }
1177
+
1178
+ //#endregion
1179
+ //#region src/features/d1/d1.module.ts
1180
+ const d1Module = {
1181
+ kind: "d1",
1182
+ label: "D1",
1183
+ configKey: "d1",
1184
+ stateEntryType: "d1_database",
1185
+ async fetchAll(api) {
1186
+ return api.d1ListAll();
1187
+ },
1188
+ async apply(ctx) {
1189
+ await d1Apply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming, ctx.addShard);
1190
+ },
1191
+ sync(ctx) {
1192
+ d1Sync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
1193
+ },
1194
+ drift(ctx) {
1195
+ return d1Drift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
1196
+ },
1197
+ async destroy(ctx) {
1198
+ await d1Destroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
1199
+ },
1200
+ status(ctx) {
1201
+ return d1Status(ctx.resources, ctx.env, ctx.state, ctx.naming);
1202
+ },
1203
+ generate(ctx) {
1204
+ if (ctx.resources.length === 0) return {};
1205
+ const generated = d1Generate(ctx.resources, ctx.env, ctx.state, ctx.naming);
1206
+ return { d1_databases: [...ctx.passthrough?.d1_databases ?? [], ...generated] };
1207
+ },
1208
+ pickResources(source) {
1209
+ return resourcesFrom(source)?.d1 ?? [];
1210
+ },
1211
+ async destroyOne({ api, state, key, entry }) {
1212
+ if (entry.type !== "d1_database") return;
1213
+ try {
1214
+ await api.d1Delete(entry.cfId);
1215
+ state.delete(key);
1216
+ } catch (err) {
1217
+ console.warn(`Rollback: failed to delete D1 ${entry.derivedName}:`, err);
1218
+ }
1219
+ },
1220
+ async importOne({ options, env, api, state, naming, ts }) {
1221
+ if (!options.cfId) throw new Error("import d1: --cf-id <uuid> is required");
1222
+ const hit = (await api.d1ListAll()).find((d) => d.uuid === options.cfId);
1223
+ if (!hit) throw new Error(`import d1: D1 database with uuid "${options.cfId}" not found in account`);
1224
+ const derivedName = options.shardDate ? naming.d1ShardName(options.logical, options.shardDate, env) : naming.d1SingleName(options.logical, env);
1225
+ if (hit.name !== derivedName) throw new Error(`import d1: cf name "${hit.name}" does not match derived "${derivedName}" — wrong --logical, missing --shard-date, or naming convention drift?`);
1226
+ const bindingKey = options.shardDate ? naming.d1ShardBindingKey(options.logical, options.shardDate) : naming.d1SingleBindingKey(options.logical);
1227
+ const existing = state.get(derivedName);
1228
+ if (existing && existing.type === "d1_database" && existing.cfId !== options.cfId) throw new Error(`import d1: state already tracks "${derivedName}" with a different cfId`);
1229
+ const entry = {
1230
+ type: "d1_database",
1231
+ logicalName: options.logical,
1232
+ shardDate: options.shardDate,
1233
+ derivedName,
1234
+ bindingKey,
1235
+ cfId: options.cfId,
1236
+ createdAt: existing?.type === "d1_database" ? existing.createdAt : ts,
1237
+ updatedAt: ts
1238
+ };
1239
+ state.set(derivedName, entry);
1240
+ }
1241
+ };
1242
+
1243
+ //#endregion
1244
+ //#region src/features/r2/r2.apply.ts
1245
+ function existingR2ForLogical(state, logicalName) {
1246
+ for (const e of Object.values(state.getAll())) if (e.type === "r2_bucket" && e.logicalName === logicalName) return e;
1247
+ }
1248
+ function todayIso() {
1249
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1250
+ }
1251
+ function assertValidR2BucketName(name, logicalName) {
1252
+ const errors = [];
1253
+ if (name.length < 3 || name.length > 63) errors.push(`length must be 3-63 chars (got ${name.length})`);
1254
+ if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(name)) errors.push("must contain only lowercase letters, numbers, and hyphens, and start/end with a letter or number");
1255
+ if (/--/.test(name)) errors.push("must not contain consecutive hyphens");
1256
+ if (errors.length > 0) throw new Error(`Invalid R2 bucket name "${name}" (derived for logical resource "${logicalName}"): ${errors.join("; ")}. Adjust your naming.r2Bucket hook (e.g., replace underscores with hyphens and lowercase the result).`);
1257
+ }
1258
+ async function r2Apply(resources, tenant, env, api, state, naming) {
1259
+ for (const config of resources) {
1260
+ const derivedName = naming.r2BucketName(config.logicalName, env);
1261
+ if (state.get(derivedName)) {
1262
+ console.log(`R2: skip "${derivedName}" (already in state; logical "${config.logicalName}")`);
1263
+ continue;
1264
+ }
1265
+ const prior = existingR2ForLogical(state, config.logicalName);
1266
+ if (prior) {
1267
+ console.log(`R2: skip "${derivedName}" (logical "${config.logicalName}" already in state as "${prior.derivedName}")`);
1268
+ continue;
1269
+ }
1270
+ assertValidR2BucketName(derivedName, config.logicalName);
1271
+ await api.r2Create(derivedName);
1272
+ console.log(`R2: created "${derivedName}" (logical "${config.logicalName}")`);
1273
+ state.set(derivedName, {
1274
+ type: "r2_bucket",
1275
+ logicalName: config.logicalName,
1276
+ createdDate: todayIso(),
1277
+ derivedName,
1278
+ bindingKey: config.binding?.trim() || naming.r2BindingKey(config.logicalName),
1279
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1280
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1281
+ });
1282
+ }
1283
+ }
1284
+
1285
+ //#endregion
1286
+ //#region src/features/r2/r2.naming.ts
1287
+ function r2ExtractDate(name, naming) {
1288
+ return naming.extractR2Date(name);
1289
+ }
1290
+
1291
+ //#endregion
1292
+ //#region src/features/r2/r2.sync.ts
1293
+ function r2Sync(allR2, resources, tenant, env, state, naming) {
1294
+ for (const config of resources) {
1295
+ const pattern = naming.r2MatchPattern(config.logicalName, env);
1296
+ const match = allR2.find((b) => pattern(b.name));
1297
+ if (match) {
1298
+ const createdDate = r2ExtractDate(match.name, naming) ?? match.creation_date.slice(0, 10);
1299
+ const prev = state.get(match.name);
1300
+ state.set(match.name, {
1301
+ type: "r2_bucket",
1302
+ logicalName: config.logicalName,
1303
+ createdDate,
1304
+ derivedName: match.name,
1305
+ bindingKey: config.binding?.trim() || (prev?.type === "r2_bucket" && prev.bindingKey ? prev.bindingKey : naming.r2BindingKey(config.logicalName)),
1306
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1307
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1308
+ });
1309
+ }
1310
+ }
1311
+ }
1312
+
1313
+ //#endregion
1314
+ //#region src/features/r2/r2.drift.ts
1315
+ /**
1316
+ * Diff R2 state against the Cloudflare list and the current config.
1317
+ *
1318
+ * R2 buckets carry a date stamp, so each declared resource matches at most one
1319
+ * bucket (the one whose name matches the logical-name pattern).
1320
+ */
1321
+ function r2Drift(allR2, resources, env, state, naming) {
1322
+ const drift = {
1323
+ kind: "r2",
1324
+ missingFromCloudflare: [],
1325
+ unrecordedInState: [],
1326
+ undeployed: []
1327
+ };
1328
+ const cfNames = new Set(allR2.map((b) => b.name));
1329
+ const allState = state.getAll();
1330
+ const r2State = Object.values(allState).filter((e) => e.type === "r2_bucket");
1331
+ for (const config of resources) {
1332
+ const pattern = naming.r2MatchPattern(config.logicalName, env);
1333
+ const cfMatch = allR2.find((b) => pattern(b.name));
1334
+ const stateEntry = r2State.find((e) => e.logicalName === config.logicalName);
1335
+ if (stateEntry && !cfNames.has(stateEntry.derivedName)) {
1336
+ drift.missingFromCloudflare.push({
1337
+ logicalName: stateEntry.logicalName,
1338
+ derivedName: stateEntry.derivedName
1339
+ });
1340
+ continue;
1341
+ }
1342
+ if (cfMatch && !stateEntry) {
1343
+ drift.unrecordedInState.push({
1344
+ logicalName: config.logicalName,
1345
+ derivedName: cfMatch.name
1346
+ });
1347
+ continue;
1348
+ }
1349
+ if (!cfMatch && !stateEntry) drift.undeployed.push({
1350
+ logicalName: config.logicalName,
1351
+ derivedName: `(no bucket for ${config.logicalName})`
1352
+ });
1353
+ }
1354
+ return drift;
1355
+ }
1356
+
1357
+ //#endregion
1358
+ //#region src/features/r2/r2.destroy.ts
1359
+ async function r2Destroy(_env, state, api, config, baseDir, _force) {
1360
+ const owned = await logicalNamesForResourceKind(config, baseDir, "r2");
1361
+ const resources = state.getAll();
1362
+ const r2Entries = Object.values(resources).filter((e) => e.type === "r2_bucket");
1363
+ const accountId = api.getAccountId();
1364
+ const s3creds = r2S3CredentialsFromEnv();
1365
+ for (const entry of r2Entries) {
1366
+ if (!owned.has(entry.logicalName)) continue;
1367
+ const name = entry.derivedName;
1368
+ if (s3creds) try {
1369
+ console.log(`R2: emptying bucket "${name}" via S3 API (incomplete multipart uploads, then objects)…`);
1370
+ const { uploadsAborted, objectsDeleted } = await emptyR2BucketViaS3(accountId, name, s3creds);
1371
+ console.log(`R2: bucket "${name}" — aborted ${uploadsAborted} multipart upload(s), deleted ${objectsDeleted} object(s).`);
1372
+ } catch (err) {
1373
+ console.warn(`R2: S3 empty failed for "${name}" (fix credentials or use the dashboard; still attempting API bucket delete):`, err instanceof Error ? err.message : err);
1374
+ }
1375
+ else console.warn(`R2: skipping S3 empty for "${name}" (set R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY to remove all objects and incomplete multipart uploads before bucket delete). Create an R2 API token in the dashboard (R2 → Manage R2 API Tokens) with object read+write; those keys are not the same as CLOUDFLARE_API_TOKEN.`);
1376
+ try {
1377
+ await api.r2Delete(name);
1378
+ state.delete(name);
1379
+ } catch (err) {
1380
+ console.warn(`Failed to delete R2 ${name}:`, err);
1381
+ }
1382
+ }
1383
+ }
1384
+
1385
+ //#endregion
1386
+ //#region src/features/r2/r2.generate.ts
1387
+ function r2Generate(resources, env, state, naming) {
1388
+ const bindings = [];
1389
+ for (const config of resources) {
1390
+ const allResources = state.getAll();
1391
+ const entry = Object.values(allResources).find((e) => e.type === "r2_bucket" && e.logicalName === config.logicalName);
1392
+ if (!entry) throw new Error(`R2 "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
1393
+ bindings.push({
1394
+ binding: entry.bindingKey,
1395
+ bucket_name: entry.derivedName
1396
+ });
1397
+ }
1398
+ return bindings;
1399
+ }
1400
+
1401
+ //#endregion
1402
+ //#region src/features/r2/r2.status.ts
1403
+ function r2Status(resources, env, state, naming) {
1404
+ const results = [];
1405
+ for (const config of resources) {
1406
+ const allResources = state.getAll();
1407
+ const entry = Object.values(allResources).find((e) => e.type === "r2_bucket" && e.logicalName === config.logicalName);
1408
+ results.push({
1409
+ binding: entry?.bindingKey ?? naming.r2BindingKey(config.logicalName),
1410
+ name: entry?.derivedName ?? naming.r2BucketName(config.logicalName, env),
1411
+ status: entry ? "ok" : "missing"
1412
+ });
1413
+ }
1414
+ return results;
1415
+ }
1416
+
1417
+ //#endregion
1418
+ //#region src/features/r2/r2.module.ts
1419
+ const r2Module = {
1420
+ kind: "r2",
1421
+ label: "R2",
1422
+ configKey: "r2",
1423
+ stateEntryType: "r2_bucket",
1424
+ async fetchAll(api) {
1425
+ return api.r2ListAll();
1426
+ },
1427
+ async apply(ctx) {
1428
+ await r2Apply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
1429
+ },
1430
+ sync(ctx) {
1431
+ r2Sync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
1432
+ },
1433
+ drift(ctx) {
1434
+ return r2Drift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
1435
+ },
1436
+ async destroy(ctx) {
1437
+ await r2Destroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
1438
+ },
1439
+ status(ctx) {
1440
+ return r2Status(ctx.resources, ctx.env, ctx.state, ctx.naming);
1441
+ },
1442
+ generate(ctx) {
1443
+ if (ctx.resources.length === 0) return {};
1444
+ return { r2_buckets: r2Generate(ctx.resources, ctx.env, ctx.state, ctx.naming) };
1445
+ },
1446
+ pickResources(source) {
1447
+ return resourcesFrom(source)?.r2 ?? [];
1448
+ },
1449
+ async destroyOne({ api, state, key, entry }) {
1450
+ if (entry.type !== "r2_bucket") return;
1451
+ try {
1452
+ await api.r2Delete(entry.derivedName);
1453
+ state.delete(key);
1454
+ } catch (err) {
1455
+ console.warn(`Rollback: failed to delete R2 ${entry.derivedName}:`, err);
1456
+ }
1457
+ },
1458
+ async importOne({ options, env, api, state, naming, ts }) {
1459
+ const bucketName = options.cfId;
1460
+ if (!bucketName) throw new Error("import r2: --cf-id <bucket-name> is required");
1461
+ const hit = (await api.r2ListAll()).find((b) => b.name === bucketName);
1462
+ if (!hit) throw new Error(`import r2: bucket "${bucketName}" not found in account`);
1463
+ if (!naming.r2MatchPattern(options.logical, env)(hit.name)) throw new Error(`import r2: bucket name "${hit.name}" does not match expected pattern for logical "${options.logical}" and env "${env}"`);
1464
+ const derivedName = hit.name;
1465
+ const createdDate = options.createdDate ?? naming.extractR2Date(hit.name) ?? hit.creation_date.slice(0, 10);
1466
+ const bindingKey = naming.r2BindingKey(options.logical);
1467
+ const existing = state.get(derivedName);
1468
+ const entry = {
1469
+ type: "r2_bucket",
1470
+ logicalName: options.logical,
1471
+ derivedName,
1472
+ bindingKey,
1473
+ createdDate,
1474
+ createdAt: existing?.type === "r2_bucket" ? existing.createdAt : ts,
1475
+ updatedAt: ts
1476
+ };
1477
+ state.set(derivedName, entry);
1478
+ }
1479
+ };
1480
+
1481
+ //#endregion
1482
+ //#region src/features/kv/kv.apply.ts
1483
+ async function kvApply(resources, tenant, env, api, state, naming) {
1484
+ for (const config of resources) {
1485
+ const derivedName = naming.kvNamespaceName(config.logicalName, env);
1486
+ if (state.get(derivedName)) continue;
1487
+ const { id } = await api.kvCreate(derivedName);
1488
+ state.set(derivedName, {
1489
+ type: "kv_namespace",
1490
+ logicalName: config.logicalName,
1491
+ derivedName,
1492
+ bindingKey: config.binding?.trim() || naming.kvBindingKey(config.logicalName),
1493
+ cfId: id,
1494
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1495
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1496
+ });
1497
+ }
1498
+ }
1499
+
1500
+ //#endregion
1501
+ //#region src/features/kv/kv.sync.ts
1502
+ function kvSync(allKV, resources, tenant, env, state, naming) {
1503
+ for (const config of resources) {
1504
+ const derivedName = naming.kvNamespaceName(config.logicalName, env);
1505
+ const match = allKV.find((ns) => ns.title === derivedName);
1506
+ if (match) state.set(derivedName, {
1507
+ type: "kv_namespace",
1508
+ logicalName: config.logicalName,
1509
+ derivedName,
1510
+ bindingKey: config.binding?.trim() || naming.kvBindingKey(config.logicalName),
1511
+ cfId: match.id,
1512
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1513
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1514
+ });
1515
+ }
1516
+ }
1517
+
1518
+ //#endregion
1519
+ //#region src/features/kv/kv.drift.ts
1520
+ function kvDrift(allKV, resources, env, state, naming) {
1521
+ const drift = {
1522
+ kind: "kv",
1523
+ missingFromCloudflare: [],
1524
+ unrecordedInState: [],
1525
+ undeployed: []
1526
+ };
1527
+ const cfByTitle = new Map(allKV.map((ns) => [ns.title, ns.id]));
1528
+ const allState = state.getAll();
1529
+ const kvState = Object.values(allState).filter((e) => e.type === "kv_namespace");
1530
+ for (const config of resources) {
1531
+ const derivedName = naming.kvNamespaceName(config.logicalName, env);
1532
+ const cfId = cfByTitle.get(derivedName);
1533
+ const stateEntry = kvState.find((e) => e.logicalName === config.logicalName);
1534
+ if (stateEntry && !cfId) drift.missingFromCloudflare.push({
1535
+ logicalName: stateEntry.logicalName,
1536
+ derivedName: stateEntry.derivedName,
1537
+ cfId: stateEntry.cfId
1538
+ });
1539
+ else if (cfId && !stateEntry) drift.unrecordedInState.push({
1540
+ logicalName: config.logicalName,
1541
+ derivedName,
1542
+ cfId
1543
+ });
1544
+ else if (!cfId && !stateEntry) drift.undeployed.push({
1545
+ logicalName: config.logicalName,
1546
+ derivedName
1547
+ });
1548
+ }
1549
+ return drift;
1550
+ }
1551
+
1552
+ //#endregion
1553
+ //#region src/features/kv/kv.destroy.ts
1554
+ async function kvDestroy(_env, state, api, config, baseDir, _force) {
1555
+ const owned = await logicalNamesForResourceKind(config, baseDir, "kv");
1556
+ const resources = state.getAll();
1557
+ const kvEntries = Object.values(resources).filter((e) => e.type === "kv_namespace");
1558
+ for (const entry of kvEntries) {
1559
+ if (!owned.has(entry.logicalName)) continue;
1560
+ try {
1561
+ await api.kvDelete(entry.cfId);
1562
+ state.delete(entry.derivedName);
1563
+ } catch (err) {
1564
+ console.warn(`Failed to delete KV ${entry.derivedName}:`, err);
1565
+ }
1566
+ }
1567
+ }
1568
+
1569
+ //#endregion
1570
+ //#region src/features/kv/kv.generate.ts
1571
+ function kvGenerate(resources, env, state, naming) {
1572
+ const bindings = [];
1573
+ for (const config of resources) {
1574
+ const derivedName = naming.kvNamespaceName(config.logicalName, env);
1575
+ const entry = state.get(derivedName);
1576
+ if (!entry || entry.type !== "kv_namespace") throw new Error(`KV "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
1577
+ bindings.push({
1578
+ binding: entry.bindingKey,
1579
+ id: entry.cfId
1580
+ });
1581
+ }
1582
+ return bindings;
1583
+ }
1584
+
1585
+ //#endregion
1586
+ //#region src/features/kv/kv.status.ts
1587
+ function kvStatus(resources, env, state, naming) {
1588
+ const results = [];
1589
+ for (const config of resources) {
1590
+ const derivedName = naming.kvNamespaceName(config.logicalName, env);
1591
+ const entry = state.get(derivedName);
1592
+ results.push({
1593
+ binding: naming.kvBindingKey(config.logicalName),
1594
+ name: derivedName,
1595
+ status: entry ? "ok" : "missing"
1596
+ });
1597
+ }
1598
+ return results;
1599
+ }
1600
+
1601
+ //#endregion
1602
+ //#region src/features/kv/kv.module.ts
1603
+ const kvModule = {
1604
+ kind: "kv",
1605
+ label: "KV",
1606
+ configKey: "kv",
1607
+ stateEntryType: "kv_namespace",
1608
+ async fetchAll(api) {
1609
+ return (await api.kvListAll()).map((k) => ({
1610
+ id: k.id,
1611
+ title: k.title
1612
+ }));
1613
+ },
1614
+ async apply(ctx) {
1615
+ await kvApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
1616
+ },
1617
+ sync(ctx) {
1618
+ kvSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
1619
+ },
1620
+ drift(ctx) {
1621
+ return kvDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
1622
+ },
1623
+ async destroy(ctx) {
1624
+ await kvDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
1625
+ },
1626
+ status(ctx) {
1627
+ return kvStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
1628
+ },
1629
+ generate(ctx) {
1630
+ if (ctx.resources.length === 0) return {};
1631
+ return { kv_namespaces: kvGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) };
1632
+ },
1633
+ pickResources(source) {
1634
+ return resourcesFrom(source)?.kv ?? [];
1635
+ },
1636
+ async destroyOne({ api, state, key, entry }) {
1637
+ if (entry.type !== "kv_namespace") return;
1638
+ try {
1639
+ await api.kvDelete(entry.cfId);
1640
+ state.delete(key);
1641
+ } catch (err) {
1642
+ console.warn(`Rollback: failed to delete KV ${entry.derivedName}:`, err);
1643
+ }
1644
+ },
1645
+ async importOne({ options, env, api, state, naming, ts }) {
1646
+ if (!options.cfId) throw new Error("import kv: --cf-id <namespace-id> is required");
1647
+ const hit = (await api.kvListAll()).find((k) => k.id === options.cfId);
1648
+ if (!hit) throw new Error(`import kv: KV namespace id "${options.cfId}" not found in account`);
1649
+ const derivedName = naming.kvNamespaceName(options.logical, env);
1650
+ if (hit.title !== derivedName) throw new Error(`import kv: cf title "${hit.title}" does not match derived "${derivedName}"`);
1651
+ const bindingKey = naming.kvBindingKey(options.logical);
1652
+ const existing = state.get(derivedName);
1653
+ const entry = {
1654
+ type: "kv_namespace",
1655
+ logicalName: options.logical,
1656
+ derivedName,
1657
+ bindingKey,
1658
+ cfId: options.cfId,
1659
+ createdAt: existing?.type === "kv_namespace" ? existing.createdAt : ts,
1660
+ updatedAt: ts
1661
+ };
1662
+ state.set(derivedName, entry);
1663
+ }
1664
+ };
1665
+
1666
+ //#endregion
1667
+ //#region src/features/queues/queues.apply.ts
1668
+ const STATE_KEY_PREFIX$6 = "queue:";
1669
+ function stateKey$6(derivedName) {
1670
+ return `${STATE_KEY_PREFIX$6}${derivedName}`;
1671
+ }
1672
+ async function queuesApply(resources, _tenant, env, api, state, naming) {
1673
+ for (const config of resources) {
1674
+ const derivedName = naming.queueName(config.logicalName, env);
1675
+ const key = stateKey$6(derivedName);
1676
+ if (state.get(key)) continue;
1677
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1678
+ const { queue_id } = await api.queueCreate(derivedName);
1679
+ state.set(key, {
1680
+ type: "queue",
1681
+ logicalName: config.logicalName,
1682
+ derivedName,
1683
+ bindingKey: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
1684
+ cfId: queue_id,
1685
+ producerBinding: !config.consumerOnly,
1686
+ createdAt: ts,
1687
+ updatedAt: ts
1688
+ });
1689
+ }
1690
+ }
1691
+
1692
+ //#endregion
1693
+ //#region src/features/queues/queues.sync.ts
1694
+ function queuesSync(allQueues, resources, _tenant, env, state, naming) {
1695
+ for (const config of resources) {
1696
+ const derivedName = naming.queueName(config.logicalName, env);
1697
+ const match = allQueues.find((q) => q.queue_name === derivedName);
1698
+ if (!match) continue;
1699
+ const key = `queue:${derivedName}`;
1700
+ const existing = state.get(key);
1701
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1702
+ state.set(key, {
1703
+ type: "queue",
1704
+ logicalName: config.logicalName,
1705
+ derivedName,
1706
+ bindingKey: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
1707
+ cfId: match.queue_id,
1708
+ producerBinding: !config.consumerOnly,
1709
+ createdAt: existing?.type === "queue" ? existing.createdAt : ts,
1710
+ updatedAt: ts
1711
+ });
1712
+ }
1713
+ }
1714
+
1715
+ //#endregion
1716
+ //#region src/features/queues/queues.drift.ts
1717
+ function queuesDrift(allQueues, resources, env, state, naming) {
1718
+ const drift = {
1719
+ kind: "queue",
1720
+ missingFromCloudflare: [],
1721
+ unrecordedInState: [],
1722
+ undeployed: []
1723
+ };
1724
+ const cfByName = new Map(allQueues.map((q) => [q.queue_name, q.queue_id]));
1725
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "queue");
1726
+ for (const config of resources) {
1727
+ const derivedName = naming.queueName(config.logicalName, env);
1728
+ const cfId = cfByName.get(derivedName);
1729
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
1730
+ if (stateEntry && !cfId) drift.missingFromCloudflare.push({
1731
+ logicalName: stateEntry.logicalName,
1732
+ derivedName: stateEntry.derivedName,
1733
+ cfId: stateEntry.cfId
1734
+ });
1735
+ else if (cfId && !stateEntry) drift.unrecordedInState.push({
1736
+ logicalName: config.logicalName,
1737
+ derivedName,
1738
+ cfId
1739
+ });
1740
+ else if (!cfId && !stateEntry) drift.undeployed.push({
1741
+ logicalName: config.logicalName,
1742
+ derivedName
1743
+ });
1744
+ }
1745
+ return drift;
1746
+ }
1747
+
1748
+ //#endregion
1749
+ //#region src/features/queues/queues.destroy.ts
1750
+ async function queuesDestroy(_env, state, api, config, baseDir, _force) {
1751
+ const owned = await logicalNamesForResourceKind(config, baseDir, "queue");
1752
+ const resources = state.getAll();
1753
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "queue");
1754
+ for (const [key, entry] of entries) {
1755
+ if (!owned.has(entry.logicalName)) continue;
1756
+ try {
1757
+ await api.queueDelete(entry.cfId);
1758
+ state.delete(key);
1759
+ } catch (err) {
1760
+ console.warn(`Failed to delete queue ${entry.derivedName}:`, err);
1761
+ }
1762
+ }
1763
+ }
1764
+
1765
+ //#endregion
1766
+ //#region src/features/queues/queues.generate.ts
1767
+ /**
1768
+ * Emit `queues.producers[]` for every declared queue (skips entries marked
1769
+ * `consumerOnly`). Consumers stay wrangler-side and can be set on the worker
1770
+ * config under `queues.consumers` (passed through unchanged).
1771
+ */
1772
+ function queuesGenerate(resources, env, state, naming) {
1773
+ const producers = [];
1774
+ for (const config of resources) {
1775
+ if (config.consumerOnly) continue;
1776
+ const derivedName = naming.queueName(config.logicalName, env);
1777
+ const entry = state.get(`queue:${derivedName}`);
1778
+ if (!entry || entry.type !== "queue") throw new Error(`Queue "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
1779
+ producers.push({
1780
+ binding: entry.bindingKey,
1781
+ queue: entry.derivedName
1782
+ });
1783
+ }
1784
+ if (producers.length === 0) return void 0;
1785
+ return { producers };
1786
+ }
1787
+
1788
+ //#endregion
1789
+ //#region src/features/queues/queues.status.ts
1790
+ function queuesStatus(resources, env, state, naming) {
1791
+ const out = [];
1792
+ for (const config of resources) {
1793
+ const derivedName = naming.queueName(config.logicalName, env);
1794
+ const entry = state.get(`queue:${derivedName}`);
1795
+ out.push({
1796
+ binding: config.binding?.trim() || naming.queueBindingKey(config.logicalName),
1797
+ name: derivedName,
1798
+ status: entry ? "ok" : "missing"
1799
+ });
1800
+ }
1801
+ return out;
1802
+ }
1803
+
1804
+ //#endregion
1805
+ //#region src/features/queues/queues.module.ts
1806
+ const queuesModule = {
1807
+ kind: "queue",
1808
+ label: "Queues",
1809
+ configKey: "queues",
1810
+ stateEntryType: "queue",
1811
+ async fetchAll(api) {
1812
+ return (await api.queuesListAll()).map((q) => ({
1813
+ queue_id: q.queue_id,
1814
+ queue_name: q.queue_name
1815
+ }));
1816
+ },
1817
+ async apply(ctx) {
1818
+ await queuesApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
1819
+ },
1820
+ sync(ctx) {
1821
+ queuesSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
1822
+ },
1823
+ drift(ctx) {
1824
+ return queuesDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
1825
+ },
1826
+ async destroy(ctx) {
1827
+ await queuesDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
1828
+ },
1829
+ status(ctx) {
1830
+ return queuesStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
1831
+ },
1832
+ generate(ctx) {
1833
+ const generated = ctx.resources.length > 0 ? queuesGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : void 0;
1834
+ const passthrough = ctx.passthrough?.queues;
1835
+ if (!generated && !passthrough) return {};
1836
+ const producers = [...passthrough?.producers ?? [], ...generated?.producers ?? []];
1837
+ const consumers = passthrough?.consumers ?? [];
1838
+ if (producers.length === 0 && consumers.length === 0) return {};
1839
+ const out = {};
1840
+ if (producers.length > 0) out.producers = producers;
1841
+ if (consumers.length > 0) out.consumers = consumers;
1842
+ return { queues: out };
1843
+ },
1844
+ pickResources(source) {
1845
+ return resourcesFrom(source)?.queues ?? [];
1846
+ },
1847
+ async destroyOne({ api, state, key, entry }) {
1848
+ if (entry.type !== "queue") return;
1849
+ try {
1850
+ await api.queueDelete(entry.cfId);
1851
+ state.delete(key);
1852
+ } catch (err) {
1853
+ console.warn(`Rollback: failed to delete queue ${entry.derivedName}:`, err);
1854
+ }
1855
+ },
1856
+ async importOne({ options, env, api, state, naming, ts }) {
1857
+ if (!options.cfId) throw new Error("import queue: --cf-id <queue-id> is required");
1858
+ const hit = (await api.queuesListAll()).find((q) => q.queue_id === options.cfId);
1859
+ if (!hit) throw new Error(`import queue: queue id "${options.cfId}" not found in account`);
1860
+ const derivedName = naming.queueName(options.logical, env);
1861
+ if (hit.queue_name !== derivedName) throw new Error(`import queue: cf name "${hit.queue_name}" does not match derived "${derivedName}"`);
1862
+ const bindingKey = naming.queueBindingKey(options.logical);
1863
+ const key = `queue:${derivedName}`;
1864
+ const existing = state.get(key);
1865
+ const entry = {
1866
+ type: "queue",
1867
+ logicalName: options.logical,
1868
+ derivedName,
1869
+ bindingKey,
1870
+ cfId: options.cfId,
1871
+ producerBinding: existing?.type === "queue" ? existing.producerBinding : true,
1872
+ createdAt: existing?.type === "queue" ? existing.createdAt : ts,
1873
+ updatedAt: ts
1874
+ };
1875
+ state.set(key, entry);
1876
+ }
1877
+ };
1878
+
1879
+ //#endregion
1880
+ //#region src/features/hyperdrive/hyperdrive.secrets.ts
1881
+ /**
1882
+ * Resolve a value that is either a literal string or `{ fromEnv: "VAR" }`
1883
+ * lookup against `process.env`. Throws a clear error when the env var is
1884
+ * missing — these values are sent to Cloudflare on create and never
1885
+ * persisted in Tamer state.
1886
+ */
1887
+ function resolveSecret(value, label) {
1888
+ if (typeof value === "string") return value;
1889
+ const envName = value.fromEnv;
1890
+ const v = process.env[envName];
1891
+ if (!v) throw new Error(`hyperdrive: ${label} requires env var ${envName} (set it before running 'tamer apply')`);
1892
+ return v;
1893
+ }
1894
+ function resolveHyperdriveOrigin(config) {
1895
+ const o = config.origin;
1896
+ return {
1897
+ scheme: o.scheme,
1898
+ host: o.host,
1899
+ port: o.port,
1900
+ database: o.database,
1901
+ user: o.user,
1902
+ password: resolveSecret(o.password, `${config.logicalName}.origin.password`),
1903
+ access_client_id: o.access_client_id,
1904
+ access_client_secret: o.access_client_secret ? resolveSecret(o.access_client_secret, `${config.logicalName}.origin.access_client_secret`) : void 0
1905
+ };
1906
+ }
1907
+
1908
+ //#endregion
1909
+ //#region src/features/hyperdrive/hyperdrive.diff.ts
1910
+ function hyperdriveDiffPlanItems(args) {
1911
+ const { resources, env, state, naming } = args;
1912
+ const items = [];
1913
+ for (const config of resources) {
1914
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
1915
+ const entry = state.get(`hyperdrive:${derivedName}`);
1916
+ if (!entry || entry.type !== "hyperdrive") continue;
1917
+ const changes = computeChanges$1(entry, config);
1918
+ if (changes.length === 0) continue;
1919
+ items.push({
1920
+ kind: "hyperdrive",
1921
+ action: "update",
1922
+ logicalName: config.logicalName,
1923
+ derivedName,
1924
+ detail: changes.map((c) => `${c.field}: ${formatVal$2(c.from)} -> ${formatVal$2(c.to)}`).join(", "),
1925
+ changes
1926
+ });
1927
+ }
1928
+ return items;
1929
+ }
1930
+ /**
1931
+ * Pure comparison shared with `hyperdriveApply` so plan and apply agree
1932
+ * on what counts as drift on the origin fields Tamer persists.
1933
+ */
1934
+ function computeHyperdriveChanges(state, config) {
1935
+ return computeChanges$1(state, config);
1936
+ }
1937
+ function computeChanges$1(state, config) {
1938
+ const changes = [];
1939
+ if (state.scheme !== config.origin.scheme) changes.push({
1940
+ field: "origin.scheme",
1941
+ from: state.scheme,
1942
+ to: config.origin.scheme,
1943
+ kind: "mutable"
1944
+ });
1945
+ if (state.originHost !== config.origin.host) changes.push({
1946
+ field: "origin.host",
1947
+ from: state.originHost,
1948
+ to: config.origin.host,
1949
+ kind: "mutable"
1950
+ });
1951
+ if (state.originDatabase !== config.origin.database) changes.push({
1952
+ field: "origin.database",
1953
+ from: state.originDatabase,
1954
+ to: config.origin.database,
1955
+ kind: "mutable"
1956
+ });
1957
+ return changes;
1958
+ }
1959
+ function formatVal$2(v) {
1960
+ if (v === void 0) return "(unset)";
1961
+ if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
1962
+ return String(v);
1963
+ }
1964
+
1965
+ //#endregion
1966
+ //#region src/features/hyperdrive/hyperdrive.apply.ts
1967
+ const STATE_KEY_PREFIX$5 = "hyperdrive:";
1968
+ function stateKey$5(derivedName) {
1969
+ return `${STATE_KEY_PREFIX$5}${derivedName}`;
1970
+ }
1971
+ async function hyperdriveApply(resources, _tenant, env, api, state, naming) {
1972
+ for (const config of resources) {
1973
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
1974
+ const key = stateKey$5(derivedName);
1975
+ const existing = state.get(key);
1976
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
1977
+ if (existing && existing.type === "hyperdrive") {
1978
+ const stateEntry = existing;
1979
+ const changes = computeHyperdriveChanges(stateEntry, config);
1980
+ if (changes.length === 0) continue;
1981
+ logApplyChange({
1982
+ kind: "hyperdrive",
1983
+ action: "update",
1984
+ logical: config.logicalName,
1985
+ derived: derivedName,
1986
+ changes
1987
+ });
1988
+ const origin$1 = resolveHyperdriveOrigin(config);
1989
+ await api.hyperdrivePatch(stateEntry.cfId, { origin: origin$1 });
1990
+ state.set(key, {
1991
+ ...stateEntry,
1992
+ scheme: config.origin.scheme,
1993
+ originHost: config.origin.host,
1994
+ originDatabase: config.origin.database,
1995
+ updatedAt: ts
1996
+ });
1997
+ continue;
1998
+ }
1999
+ logApplyChange({
2000
+ kind: "hyperdrive",
2001
+ action: "create",
2002
+ logical: config.logicalName,
2003
+ derived: derivedName
2004
+ });
2005
+ const origin = resolveHyperdriveOrigin(config);
2006
+ const { id } = await api.hyperdriveCreate({
2007
+ name: derivedName,
2008
+ origin,
2009
+ caching: config.caching,
2010
+ mtls: config.mtls
2011
+ });
2012
+ state.set(key, {
2013
+ type: "hyperdrive",
2014
+ logicalName: config.logicalName,
2015
+ derivedName,
2016
+ bindingKey: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
2017
+ cfId: id,
2018
+ scheme: config.origin.scheme,
2019
+ originHost: config.origin.host,
2020
+ originDatabase: config.origin.database,
2021
+ createdAt: ts,
2022
+ updatedAt: ts
2023
+ });
2024
+ }
2025
+ }
2026
+
2027
+ //#endregion
2028
+ //#region src/features/hyperdrive/hyperdrive.sync.ts
2029
+ function hyperdriveSync(allHyperdrive, resources, _tenant, env, state, naming) {
2030
+ for (const config of resources) {
2031
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
2032
+ const match = allHyperdrive.find((h) => h.name === derivedName);
2033
+ if (!match) continue;
2034
+ const key = `hyperdrive:${derivedName}`;
2035
+ const existing = state.get(key);
2036
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2037
+ state.set(key, {
2038
+ type: "hyperdrive",
2039
+ logicalName: config.logicalName,
2040
+ derivedName,
2041
+ bindingKey: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
2042
+ cfId: match.id,
2043
+ scheme: config.origin.scheme,
2044
+ originHost: match.origin?.host ?? config.origin.host,
2045
+ originDatabase: match.origin?.database ?? config.origin.database,
2046
+ createdAt: existing?.type === "hyperdrive" ? existing.createdAt : ts,
2047
+ updatedAt: ts
2048
+ });
2049
+ }
2050
+ }
2051
+
2052
+ //#endregion
2053
+ //#region src/features/hyperdrive/hyperdrive.drift.ts
2054
+ function hyperdriveDrift(allHyperdrive, resources, env, state, naming) {
2055
+ const drift = {
2056
+ kind: "hyperdrive",
2057
+ missingFromCloudflare: [],
2058
+ unrecordedInState: [],
2059
+ undeployed: []
2060
+ };
2061
+ const cfByName = new Map(allHyperdrive.map((h) => [h.name, h.id]));
2062
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "hyperdrive");
2063
+ for (const config of resources) {
2064
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
2065
+ const cfId = cfByName.get(derivedName);
2066
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
2067
+ if (stateEntry && !cfId) drift.missingFromCloudflare.push({
2068
+ logicalName: stateEntry.logicalName,
2069
+ derivedName: stateEntry.derivedName,
2070
+ cfId: stateEntry.cfId
2071
+ });
2072
+ else if (cfId && !stateEntry) drift.unrecordedInState.push({
2073
+ logicalName: config.logicalName,
2074
+ derivedName,
2075
+ cfId
2076
+ });
2077
+ else if (!cfId && !stateEntry) drift.undeployed.push({
2078
+ logicalName: config.logicalName,
2079
+ derivedName
2080
+ });
2081
+ }
2082
+ return drift;
2083
+ }
2084
+
2085
+ //#endregion
2086
+ //#region src/features/hyperdrive/hyperdrive.destroy.ts
2087
+ async function hyperdriveDestroy(_env, state, api, config, baseDir, _force) {
2088
+ const owned = await logicalNamesForResourceKind(config, baseDir, "hyperdrive");
2089
+ const resources = state.getAll();
2090
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "hyperdrive");
2091
+ for (const [key, entry] of entries) {
2092
+ if (!owned.has(entry.logicalName)) continue;
2093
+ try {
2094
+ await api.hyperdriveDelete(entry.cfId);
2095
+ state.delete(key);
2096
+ } catch (err) {
2097
+ console.warn(`Failed to delete hyperdrive ${entry.derivedName}:`, err);
2098
+ }
2099
+ }
2100
+ }
2101
+
2102
+ //#endregion
2103
+ //#region src/features/hyperdrive/hyperdrive.generate.ts
2104
+ function hyperdriveGenerate(resources, env, state, naming) {
2105
+ const out = [];
2106
+ for (const config of resources) {
2107
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
2108
+ const entry = state.get(`hyperdrive:${derivedName}`);
2109
+ if (!entry || entry.type !== "hyperdrive") throw new Error(`Hyperdrive "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
2110
+ out.push({
2111
+ binding: entry.bindingKey,
2112
+ id: entry.cfId,
2113
+ localConnectionString: config.localConnectionString
2114
+ });
2115
+ }
2116
+ return out;
2117
+ }
2118
+
2119
+ //#endregion
2120
+ //#region src/features/hyperdrive/hyperdrive.status.ts
2121
+ function hyperdriveStatus(resources, env, state, naming) {
2122
+ const out = [];
2123
+ for (const config of resources) {
2124
+ const derivedName = naming.hyperdriveName(config.logicalName, env);
2125
+ const entry = state.get(`hyperdrive:${derivedName}`);
2126
+ out.push({
2127
+ binding: config.binding?.trim() || naming.hyperdriveBindingKey(config.logicalName),
2128
+ name: derivedName,
2129
+ status: entry ? "ok" : "missing"
2130
+ });
2131
+ }
2132
+ return out;
2133
+ }
2134
+
2135
+ //#endregion
2136
+ //#region src/features/hyperdrive/hyperdrive.module.ts
2137
+ const hyperdriveModule = {
2138
+ kind: "hyperdrive",
2139
+ label: "Hyperdrive",
2140
+ configKey: "hyperdrive",
2141
+ stateEntryType: "hyperdrive",
2142
+ async fetchAll(api) {
2143
+ return (await api.hyperdriveListAll()).map((h) => ({
2144
+ id: h.id,
2145
+ name: h.name,
2146
+ origin: h.origin
2147
+ }));
2148
+ },
2149
+ async apply(ctx) {
2150
+ await hyperdriveApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
2151
+ },
2152
+ sync(ctx) {
2153
+ hyperdriveSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
2154
+ },
2155
+ drift(ctx) {
2156
+ return hyperdriveDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
2157
+ },
2158
+ diff(ctx) {
2159
+ return hyperdriveDiffPlanItems({
2160
+ resources: ctx.resources,
2161
+ env: ctx.env,
2162
+ state: ctx.state,
2163
+ naming: ctx.naming
2164
+ });
2165
+ },
2166
+ async destroy(ctx) {
2167
+ await hyperdriveDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
2168
+ },
2169
+ status(ctx) {
2170
+ return hyperdriveStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
2171
+ },
2172
+ generate(ctx) {
2173
+ const generated = ctx.resources.length > 0 ? hyperdriveGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
2174
+ const passthrough = ctx.passthrough?.hyperdrive ?? [];
2175
+ if (generated.length === 0 && passthrough.length === 0) return {};
2176
+ return { hyperdrive: [...passthrough, ...generated] };
2177
+ },
2178
+ pickResources(source) {
2179
+ return resourcesFrom(source)?.hyperdrive ?? [];
2180
+ },
2181
+ async destroyOne({ api, state, key, entry }) {
2182
+ if (entry.type !== "hyperdrive") return;
2183
+ try {
2184
+ await api.hyperdriveDelete(entry.cfId);
2185
+ state.delete(key);
2186
+ } catch (err) {
2187
+ console.warn(`Rollback: failed to delete Hyperdrive ${entry.derivedName}:`, err);
2188
+ }
2189
+ },
2190
+ async importOne({ options, env, api, state, naming, ts }) {
2191
+ if (!options.cfId) throw new Error("import hyperdrive: --cf-id <config-id> is required");
2192
+ const hit = (await api.hyperdriveListAll()).find((h) => h.id === options.cfId);
2193
+ if (!hit) throw new Error(`import hyperdrive: config id "${options.cfId}" not found in account`);
2194
+ const derivedName = naming.hyperdriveName(options.logical, env);
2195
+ if (hit.name !== derivedName) throw new Error(`import hyperdrive: cf name "${hit.name}" does not match derived "${derivedName}"`);
2196
+ const bindingKey = naming.hyperdriveBindingKey(options.logical);
2197
+ const key = `hyperdrive:${derivedName}`;
2198
+ const existing = state.get(key);
2199
+ const scheme = hit.origin?.scheme === "mysql" ? "mysql" : hit.origin?.scheme === "postgresql" ? "postgresql" : "postgres";
2200
+ const entry = {
2201
+ type: "hyperdrive",
2202
+ logicalName: options.logical,
2203
+ derivedName,
2204
+ bindingKey,
2205
+ cfId: options.cfId,
2206
+ scheme,
2207
+ originHost: hit.origin?.host ?? "",
2208
+ originDatabase: hit.origin?.database ?? "",
2209
+ createdAt: existing?.type === "hyperdrive" ? existing.createdAt : ts,
2210
+ updatedAt: ts
2211
+ };
2212
+ state.set(key, entry);
2213
+ }
2214
+ };
2215
+
2216
+ //#endregion
2217
+ //#region src/features/vectorize/vectorize.apply.ts
2218
+ const STATE_KEY_PREFIX$4 = "vectorize:";
2219
+ function stateKey$4(derivedName) {
2220
+ return `${STATE_KEY_PREFIX$4}${derivedName}`;
2221
+ }
2222
+ async function vectorizeApply(resources, _tenant, env, api, state, naming) {
2223
+ for (const config of resources) {
2224
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2225
+ const key = stateKey$4(derivedName);
2226
+ const existing = state.get(key);
2227
+ if (existing) {
2228
+ if (existing.type === "vectorize") {
2229
+ if (existing.dimensions !== config.dimensions || existing.metric !== config.metric) throw new Error(`Vectorize index "${config.logicalName}" config changed (dimensions/metric are immutable). Run 'tamer destroy --resource ${config.logicalName}' first, then re-apply.`);
2230
+ }
2231
+ continue;
2232
+ }
2233
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2234
+ const created = await api.vectorizeCreate({
2235
+ name: derivedName,
2236
+ description: config.description,
2237
+ config: {
2238
+ dimensions: config.dimensions,
2239
+ metric: config.metric
2240
+ }
2241
+ });
2242
+ state.set(key, {
2243
+ type: "vectorize",
2244
+ logicalName: config.logicalName,
2245
+ derivedName,
2246
+ bindingKey: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
2247
+ cfId: created.id ?? derivedName,
2248
+ dimensions: config.dimensions,
2249
+ metric: config.metric,
2250
+ createdAt: ts,
2251
+ updatedAt: ts
2252
+ });
2253
+ }
2254
+ }
2255
+
2256
+ //#endregion
2257
+ //#region src/features/vectorize/vectorize.sync.ts
2258
+ function vectorizeSync(allIndexes, resources, _tenant, env, state, naming) {
2259
+ for (const config of resources) {
2260
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2261
+ const match = allIndexes.find((i) => i.name === derivedName);
2262
+ if (!match) continue;
2263
+ const key = `vectorize:${derivedName}`;
2264
+ const existing = state.get(key);
2265
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2266
+ state.set(key, {
2267
+ type: "vectorize",
2268
+ logicalName: config.logicalName,
2269
+ derivedName,
2270
+ bindingKey: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
2271
+ cfId: match.id ?? derivedName,
2272
+ dimensions: match.config?.dimensions ?? config.dimensions,
2273
+ metric: (match.config?.metric ?? null) || config.metric,
2274
+ createdAt: existing?.type === "vectorize" ? existing.createdAt : ts,
2275
+ updatedAt: ts
2276
+ });
2277
+ }
2278
+ }
2279
+
2280
+ //#endregion
2281
+ //#region src/features/vectorize/vectorize.drift.ts
2282
+ function vectorizeDrift(allIndexes, resources, env, state, naming) {
2283
+ const drift = {
2284
+ kind: "vectorize",
2285
+ missingFromCloudflare: [],
2286
+ unrecordedInState: [],
2287
+ undeployed: []
2288
+ };
2289
+ const cfByName = new Map(allIndexes.map((i) => [i.name, i.id ?? i.name]));
2290
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "vectorize");
2291
+ for (const config of resources) {
2292
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2293
+ const cfId = cfByName.get(derivedName);
2294
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
2295
+ if (stateEntry && !cfId) drift.missingFromCloudflare.push({
2296
+ logicalName: stateEntry.logicalName,
2297
+ derivedName: stateEntry.derivedName,
2298
+ cfId: stateEntry.cfId
2299
+ });
2300
+ else if (cfId && !stateEntry) drift.unrecordedInState.push({
2301
+ logicalName: config.logicalName,
2302
+ derivedName,
2303
+ cfId
2304
+ });
2305
+ else if (!cfId && !stateEntry) drift.undeployed.push({
2306
+ logicalName: config.logicalName,
2307
+ derivedName
2308
+ });
2309
+ }
2310
+ return drift;
2311
+ }
2312
+
2313
+ //#endregion
2314
+ //#region src/features/vectorize/vectorize.destroy.ts
2315
+ async function vectorizeDestroy(_env, state, api, config, baseDir, _force) {
2316
+ const owned = await logicalNamesForResourceKind(config, baseDir, "vectorize");
2317
+ const resources = state.getAll();
2318
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "vectorize");
2319
+ for (const [key, entry] of entries) {
2320
+ if (!owned.has(entry.logicalName)) continue;
2321
+ try {
2322
+ await api.vectorizeDelete(entry.derivedName);
2323
+ state.delete(key);
2324
+ } catch (err) {
2325
+ console.warn(`Failed to delete vectorize index ${entry.derivedName}:`, err);
2326
+ }
2327
+ }
2328
+ }
2329
+
2330
+ //#endregion
2331
+ //#region src/features/vectorize/vectorize.generate.ts
2332
+ /**
2333
+ * Emit `vectorize[]` bindings for declared indexes. Each binding requires
2334
+ * the index to exist in state (i.e. `tamer apply` has run). Throws otherwise
2335
+ * so the operator gets an actionable error before `wrangler deploy`.
2336
+ */
2337
+ function vectorizeGenerate(resources, env, state, naming) {
2338
+ const out = [];
2339
+ for (const config of resources) {
2340
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2341
+ const entry = state.get(`vectorize:${derivedName}`);
2342
+ if (!entry || entry.type !== "vectorize") throw new Error(`Vectorize index "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
2343
+ out.push({
2344
+ binding: entry.bindingKey,
2345
+ index_name: entry.derivedName
2346
+ });
2347
+ }
2348
+ return out;
2349
+ }
2350
+
2351
+ //#endregion
2352
+ //#region src/features/vectorize/vectorize.status.ts
2353
+ function vectorizeStatus(resources, env, state, naming) {
2354
+ const out = [];
2355
+ for (const config of resources) {
2356
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2357
+ const entry = state.get(`vectorize:${derivedName}`);
2358
+ out.push({
2359
+ binding: config.binding?.trim() || naming.vectorizeBindingKey(config.logicalName),
2360
+ name: derivedName,
2361
+ status: entry ? "ok" : "missing"
2362
+ });
2363
+ }
2364
+ return out;
2365
+ }
2366
+
2367
+ //#endregion
2368
+ //#region src/features/vectorize/vectorize.diff.ts
2369
+ function vectorizeDiffPlanItems(args) {
2370
+ const { resources, env, state, naming } = args;
2371
+ const items = [];
2372
+ for (const config of resources) {
2373
+ const derivedName = naming.vectorizeName(config.logicalName, env);
2374
+ const key = `vectorize:${derivedName}`;
2375
+ const entry = state.get(key);
2376
+ if (!entry || entry.type !== "vectorize") continue;
2377
+ const stateEntry = entry;
2378
+ const changes = [];
2379
+ if (stateEntry.dimensions !== config.dimensions) changes.push({
2380
+ field: "dimensions",
2381
+ from: stateEntry.dimensions,
2382
+ to: config.dimensions,
2383
+ kind: "immutable"
2384
+ });
2385
+ if (stateEntry.metric !== config.metric) changes.push({
2386
+ field: "metric",
2387
+ from: stateEntry.metric,
2388
+ to: config.metric,
2389
+ kind: "immutable"
2390
+ });
2391
+ if (changes.length === 0) continue;
2392
+ items.push({
2393
+ kind: "vectorize",
2394
+ action: "replace",
2395
+ logicalName: config.logicalName,
2396
+ derivedName,
2397
+ detail: changes.map((c) => `${c.field}: ${c.from} -> ${c.to}`).join(", "),
2398
+ changes
2399
+ });
2400
+ }
2401
+ return items;
2402
+ }
2403
+
2404
+ //#endregion
2405
+ //#region src/features/vectorize/vectorize.module.ts
2406
+ const vectorizeModule = {
2407
+ kind: "vectorize",
2408
+ label: "Vectorize",
2409
+ configKey: "vectorize",
2410
+ stateEntryType: "vectorize",
2411
+ async fetchAll(api) {
2412
+ return (await api.vectorizeListAll()).map((v) => ({
2413
+ id: v.id,
2414
+ name: v.name,
2415
+ config: v.config
2416
+ }));
2417
+ },
2418
+ async apply(ctx) {
2419
+ await vectorizeApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
2420
+ },
2421
+ sync(ctx) {
2422
+ vectorizeSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
2423
+ },
2424
+ drift(ctx) {
2425
+ return vectorizeDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
2426
+ },
2427
+ diff(ctx) {
2428
+ return vectorizeDiffPlanItems({
2429
+ resources: ctx.resources,
2430
+ env: ctx.env,
2431
+ state: ctx.state,
2432
+ naming: ctx.naming
2433
+ });
2434
+ },
2435
+ async destroy(ctx) {
2436
+ await vectorizeDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
2437
+ },
2438
+ status(ctx) {
2439
+ return vectorizeStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
2440
+ },
2441
+ generate(ctx) {
2442
+ const generated = ctx.resources.length > 0 ? vectorizeGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
2443
+ const passthrough = ctx.passthrough?.vectorize ?? [];
2444
+ if (generated.length === 0 && passthrough.length === 0) return {};
2445
+ return { vectorize: [...passthrough, ...generated] };
2446
+ },
2447
+ pickResources(source) {
2448
+ return resourcesFrom(source)?.vectorize ?? [];
2449
+ },
2450
+ async destroyOne({ api, state, key, entry }) {
2451
+ if (entry.type !== "vectorize") return;
2452
+ try {
2453
+ await api.vectorizeDelete(entry.derivedName);
2454
+ state.delete(key);
2455
+ } catch (err) {
2456
+ console.warn(`Rollback: failed to delete Vectorize ${entry.derivedName}:`, err);
2457
+ }
2458
+ },
2459
+ async importOne({ options, env, api, state, naming, ts }) {
2460
+ const indexName = options.cfId;
2461
+ if (!indexName) throw new Error("import vectorize: --cf-id <index-name> is required");
2462
+ const hit = (await api.vectorizeListAll()).find((v) => v.name === indexName);
2463
+ if (!hit) throw new Error(`import vectorize: index "${indexName}" not found in account`);
2464
+ const derivedName = naming.vectorizeName(options.logical, env);
2465
+ if (hit.name !== derivedName) throw new Error(`import vectorize: cf name "${hit.name}" does not match derived "${derivedName}"`);
2466
+ if (!hit.config) throw new Error(`import vectorize: cf index "${hit.name}" returned no config (dimensions/metric)`);
2467
+ const metric = hit.config.metric;
2468
+ const bindingKey = naming.vectorizeBindingKey(options.logical);
2469
+ const key = `vectorize:${derivedName}`;
2470
+ const existing = state.get(key);
2471
+ const entry = {
2472
+ type: "vectorize",
2473
+ logicalName: options.logical,
2474
+ derivedName,
2475
+ bindingKey,
2476
+ cfId: hit.id ?? derivedName,
2477
+ dimensions: hit.config.dimensions,
2478
+ metric,
2479
+ createdAt: existing?.type === "vectorize" ? existing.createdAt : ts,
2480
+ updatedAt: ts
2481
+ };
2482
+ state.set(key, entry);
2483
+ }
2484
+ };
2485
+
2486
+ //#endregion
2487
+ //#region src/features/ai-gateway/ai-gateway.types.ts
2488
+ /**
2489
+ * Default values applied to optional `AIGatewayResourceConfig` fields. Kept
2490
+ * here so apply / sync / drift agree on what "no override" actually means
2491
+ * when comparing config to Cloudflare state.
2492
+ */
2493
+ const AI_GATEWAY_DEFAULTS = {
2494
+ cacheTtl: 0,
2495
+ cacheInvalidateOnUpdate: false,
2496
+ collectLogs: true,
2497
+ authentication: false,
2498
+ rateLimitingInterval: 0,
2499
+ rateLimitingLimit: 0,
2500
+ rateLimitingTechnique: "fixed"
2501
+ };
2502
+ function resolveAIGatewayConfig(config) {
2503
+ return {
2504
+ cacheTtl: config.cacheTtl ?? AI_GATEWAY_DEFAULTS.cacheTtl,
2505
+ cacheInvalidateOnUpdate: config.cacheInvalidateOnUpdate ?? AI_GATEWAY_DEFAULTS.cacheInvalidateOnUpdate,
2506
+ collectLogs: config.collectLogs ?? AI_GATEWAY_DEFAULTS.collectLogs,
2507
+ authentication: config.authentication ?? AI_GATEWAY_DEFAULTS.authentication,
2508
+ rateLimitingInterval: config.rateLimitingInterval ?? AI_GATEWAY_DEFAULTS.rateLimitingInterval,
2509
+ rateLimitingLimit: config.rateLimitingLimit ?? AI_GATEWAY_DEFAULTS.rateLimitingLimit,
2510
+ rateLimitingTechnique: config.rateLimitingTechnique ?? AI_GATEWAY_DEFAULTS.rateLimitingTechnique
2511
+ };
2512
+ }
2513
+
2514
+ //#endregion
2515
+ //#region src/features/ai-gateway/ai-gateway.diff.ts
2516
+ function aiGatewayDiffPlanItems(args) {
2517
+ const { resources, env, state, naming } = args;
2518
+ const items = [];
2519
+ for (const config of resources) {
2520
+ const derivedName = naming.aiGatewayId(config.logicalName, env);
2521
+ const entry = state.get(`ai_gateway:${derivedName}`);
2522
+ if (!entry || entry.type !== "ai_gateway") continue;
2523
+ const changes = computeChanges(entry, config);
2524
+ if (changes.length === 0) continue;
2525
+ items.push({
2526
+ kind: "ai_gateway",
2527
+ action: "update",
2528
+ logicalName: config.logicalName,
2529
+ derivedName,
2530
+ detail: changes.map((c) => `${c.field}: ${formatVal$1(c.from)} -> ${formatVal$1(c.to)}`).join(", "),
2531
+ changes
2532
+ });
2533
+ }
2534
+ return items;
2535
+ }
2536
+ /**
2537
+ * Pure comparison shared with `aiGatewayApply` — keeps the plan and the
2538
+ * apply path in lockstep about which fields count as drift.
2539
+ */
2540
+ function computeAIGatewayChanges(state, config) {
2541
+ return computeChanges(state, config);
2542
+ }
2543
+ function computeChanges(state, config) {
2544
+ const resolved = resolveAIGatewayConfig(config);
2545
+ const changes = [];
2546
+ if (state.cacheTtl !== resolved.cacheTtl) changes.push({
2547
+ field: "cacheTtl",
2548
+ from: state.cacheTtl,
2549
+ to: resolved.cacheTtl,
2550
+ kind: "mutable"
2551
+ });
2552
+ if (state.cacheInvalidateOnUpdate !== resolved.cacheInvalidateOnUpdate) changes.push({
2553
+ field: "cacheInvalidateOnUpdate",
2554
+ from: state.cacheInvalidateOnUpdate,
2555
+ to: resolved.cacheInvalidateOnUpdate,
2556
+ kind: "mutable"
2557
+ });
2558
+ if (state.collectLogs !== resolved.collectLogs) changes.push({
2559
+ field: "collectLogs",
2560
+ from: state.collectLogs,
2561
+ to: resolved.collectLogs,
2562
+ kind: "mutable"
2563
+ });
2564
+ if (state.authentication !== resolved.authentication) changes.push({
2565
+ field: "authentication",
2566
+ from: state.authentication,
2567
+ to: resolved.authentication,
2568
+ kind: "mutable"
2569
+ });
2570
+ if (state.rateLimitingInterval !== resolved.rateLimitingInterval) changes.push({
2571
+ field: "rateLimitingInterval",
2572
+ from: state.rateLimitingInterval,
2573
+ to: resolved.rateLimitingInterval,
2574
+ kind: "mutable"
2575
+ });
2576
+ if (state.rateLimitingLimit !== resolved.rateLimitingLimit) changes.push({
2577
+ field: "rateLimitingLimit",
2578
+ from: state.rateLimitingLimit,
2579
+ to: resolved.rateLimitingLimit,
2580
+ kind: "mutable"
2581
+ });
2582
+ if (state.rateLimitingTechnique !== resolved.rateLimitingTechnique) changes.push({
2583
+ field: "rateLimitingTechnique",
2584
+ from: state.rateLimitingTechnique,
2585
+ to: resolved.rateLimitingTechnique,
2586
+ kind: "mutable"
2587
+ });
2588
+ return changes;
2589
+ }
2590
+ function formatVal$1(v) {
2591
+ if (v === void 0) return "(unset)";
2592
+ if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
2593
+ return String(v);
2594
+ }
2595
+
2596
+ //#endregion
2597
+ //#region src/features/ai-gateway/ai-gateway.apply.ts
2598
+ const STATE_KEY_PREFIX$3 = "ai_gateway:";
2599
+ function stateKey$3(derivedName) {
2600
+ return `${STATE_KEY_PREFIX$3}${derivedName}`;
2601
+ }
2602
+ async function aiGatewayApply(resources, _tenant, env, api, state, naming) {
2603
+ for (const config of resources) {
2604
+ const derivedName = naming.aiGatewayId(config.logicalName, env);
2605
+ const key = stateKey$3(derivedName);
2606
+ const existing = state.get(key);
2607
+ const resolved = resolveAIGatewayConfig(config);
2608
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2609
+ if (existing && existing.type === "ai_gateway") {
2610
+ const changes = computeAIGatewayChanges(existing, config);
2611
+ if (changes.length === 0) continue;
2612
+ logApplyChange({
2613
+ kind: "ai_gateway",
2614
+ action: "update",
2615
+ logical: config.logicalName,
2616
+ derived: derivedName,
2617
+ changes
2618
+ });
2619
+ await api.aiGatewayUpdate(derivedName, {
2620
+ cache_ttl: resolved.cacheTtl,
2621
+ cache_invalidate_on_update: resolved.cacheInvalidateOnUpdate,
2622
+ collect_logs: resolved.collectLogs,
2623
+ authentication: resolved.authentication,
2624
+ rate_limiting_interval: resolved.rateLimitingInterval,
2625
+ rate_limiting_limit: resolved.rateLimitingLimit,
2626
+ rate_limiting_technique: resolved.rateLimitingTechnique
2627
+ });
2628
+ state.set(key, {
2629
+ ...existing,
2630
+ cacheTtl: resolved.cacheTtl,
2631
+ cacheInvalidateOnUpdate: resolved.cacheInvalidateOnUpdate,
2632
+ collectLogs: resolved.collectLogs,
2633
+ authentication: resolved.authentication,
2634
+ rateLimitingInterval: resolved.rateLimitingInterval,
2635
+ rateLimitingLimit: resolved.rateLimitingLimit,
2636
+ rateLimitingTechnique: resolved.rateLimitingTechnique,
2637
+ updatedAt: ts
2638
+ });
2639
+ continue;
2640
+ }
2641
+ logApplyChange({
2642
+ kind: "ai_gateway",
2643
+ action: "create",
2644
+ logical: config.logicalName,
2645
+ derived: derivedName
2646
+ });
2647
+ await api.aiGatewayCreate({
2648
+ id: derivedName,
2649
+ cache_ttl: resolved.cacheTtl,
2650
+ cache_invalidate_on_update: resolved.cacheInvalidateOnUpdate,
2651
+ collect_logs: resolved.collectLogs,
2652
+ authentication: resolved.authentication,
2653
+ rate_limiting_interval: resolved.rateLimitingInterval,
2654
+ rate_limiting_limit: resolved.rateLimitingLimit,
2655
+ rate_limiting_technique: resolved.rateLimitingTechnique
2656
+ });
2657
+ state.set(key, {
2658
+ type: "ai_gateway",
2659
+ logicalName: config.logicalName,
2660
+ derivedName,
2661
+ bindingKey: naming.aiGatewayBindingKey(config.logicalName),
2662
+ cfId: derivedName,
2663
+ cacheTtl: resolved.cacheTtl,
2664
+ cacheInvalidateOnUpdate: resolved.cacheInvalidateOnUpdate,
2665
+ collectLogs: resolved.collectLogs,
2666
+ authentication: resolved.authentication,
2667
+ rateLimitingInterval: resolved.rateLimitingInterval,
2668
+ rateLimitingLimit: resolved.rateLimitingLimit,
2669
+ rateLimitingTechnique: resolved.rateLimitingTechnique,
2670
+ createdAt: ts,
2671
+ updatedAt: ts
2672
+ });
2673
+ }
2674
+ }
2675
+
2676
+ //#endregion
2677
+ //#region src/features/ai-gateway/ai-gateway.sync.ts
2678
+ function aiGatewaySync(allGateways, resources, _tenant, env, state, naming) {
2679
+ for (const config of resources) {
2680
+ const derivedName = naming.aiGatewayId(config.logicalName, env);
2681
+ const match = allGateways.find((g) => g.id === derivedName);
2682
+ if (!match) continue;
2683
+ const key = `ai_gateway:${derivedName}`;
2684
+ const existing = state.get(key);
2685
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2686
+ const resolved = resolveAIGatewayConfig(config);
2687
+ state.set(key, {
2688
+ type: "ai_gateway",
2689
+ logicalName: config.logicalName,
2690
+ derivedName,
2691
+ bindingKey: naming.aiGatewayBindingKey(config.logicalName),
2692
+ cfId: derivedName,
2693
+ cacheTtl: match.cache_ttl ?? resolved.cacheTtl,
2694
+ cacheInvalidateOnUpdate: match.cache_invalidate_on_update ?? resolved.cacheInvalidateOnUpdate,
2695
+ collectLogs: match.collect_logs ?? resolved.collectLogs,
2696
+ authentication: match.authentication ?? resolved.authentication,
2697
+ rateLimitingInterval: match.rate_limiting_interval ?? resolved.rateLimitingInterval,
2698
+ rateLimitingLimit: match.rate_limiting_limit ?? resolved.rateLimitingLimit,
2699
+ rateLimitingTechnique: match.rate_limiting_technique ?? resolved.rateLimitingTechnique,
2700
+ createdAt: existing?.type === "ai_gateway" ? existing.createdAt : ts,
2701
+ updatedAt: ts
2702
+ });
2703
+ }
2704
+ }
2705
+
2706
+ //#endregion
2707
+ //#region src/features/ai-gateway/ai-gateway.drift.ts
2708
+ function aiGatewayDrift(allGateways, resources, env, state, naming) {
2709
+ const drift = {
2710
+ kind: "ai_gateway",
2711
+ missingFromCloudflare: [],
2712
+ unrecordedInState: [],
2713
+ undeployed: []
2714
+ };
2715
+ const cfIds = new Set(allGateways.map((g) => g.id));
2716
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "ai_gateway");
2717
+ for (const config of resources) {
2718
+ const derivedName = naming.aiGatewayId(config.logicalName, env);
2719
+ const inCloudflare = cfIds.has(derivedName);
2720
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
2721
+ if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
2722
+ logicalName: stateEntry.logicalName,
2723
+ derivedName: stateEntry.derivedName,
2724
+ cfId: stateEntry.cfId
2725
+ });
2726
+ else if (inCloudflare && !stateEntry) drift.unrecordedInState.push({
2727
+ logicalName: config.logicalName,
2728
+ derivedName,
2729
+ cfId: derivedName
2730
+ });
2731
+ else if (!inCloudflare && !stateEntry) drift.undeployed.push({
2732
+ logicalName: config.logicalName,
2733
+ derivedName
2734
+ });
2735
+ }
2736
+ return drift;
2737
+ }
2738
+
2739
+ //#endregion
2740
+ //#region src/features/ai-gateway/ai-gateway.destroy.ts
2741
+ async function aiGatewayDestroy(_env, state, api, config, baseDir, _force) {
2742
+ const owned = await logicalNamesForResourceKind(config, baseDir, "ai_gateway");
2743
+ const resources = state.getAll();
2744
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "ai_gateway");
2745
+ for (const [key, entry] of entries) {
2746
+ if (!owned.has(entry.logicalName)) continue;
2747
+ try {
2748
+ await api.aiGatewayDelete(entry.derivedName);
2749
+ state.delete(key);
2750
+ } catch (err) {
2751
+ console.warn(`Failed to delete AI Gateway ${entry.derivedName}:`, err);
2752
+ }
2753
+ }
2754
+ }
2755
+
2756
+ //#endregion
2757
+ //#region src/features/ai-gateway/ai-gateway.status.ts
2758
+ function aiGatewayStatus(resources, env, state, naming) {
2759
+ const out = [];
2760
+ for (const config of resources) {
2761
+ const derivedName = naming.aiGatewayId(config.logicalName, env);
2762
+ const entry = state.get(`ai_gateway:${derivedName}`);
2763
+ out.push({
2764
+ binding: naming.aiGatewayBindingKey(config.logicalName),
2765
+ name: derivedName,
2766
+ status: entry ? "ok" : "missing"
2767
+ });
2768
+ }
2769
+ return out;
2770
+ }
2771
+
2772
+ //#endregion
2773
+ //#region src/features/ai-gateway/ai-gateway.module.ts
2774
+ const aiGatewayModule = {
2775
+ kind: "ai_gateway",
2776
+ label: "AI Gateway",
2777
+ configKey: "aiGateway",
2778
+ stateEntryType: "ai_gateway",
2779
+ async fetchAll(api) {
2780
+ return await api.aiGatewayListAll();
2781
+ },
2782
+ async apply(ctx) {
2783
+ await aiGatewayApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
2784
+ },
2785
+ sync(ctx) {
2786
+ aiGatewaySync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
2787
+ },
2788
+ drift(ctx) {
2789
+ return aiGatewayDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
2790
+ },
2791
+ diff(ctx) {
2792
+ return aiGatewayDiffPlanItems({
2793
+ resources: ctx.resources,
2794
+ env: ctx.env,
2795
+ state: ctx.state,
2796
+ naming: ctx.naming
2797
+ });
2798
+ },
2799
+ async destroy(ctx) {
2800
+ await aiGatewayDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
2801
+ },
2802
+ status(ctx) {
2803
+ return aiGatewayStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
2804
+ },
2805
+ generate() {
2806
+ return {};
2807
+ },
2808
+ pickResources(source) {
2809
+ return resourcesFrom(source)?.aiGateway ?? [];
2810
+ },
2811
+ async destroyOne({ api, state, key, entry }) {
2812
+ if (entry.type !== "ai_gateway") return;
2813
+ try {
2814
+ await api.aiGatewayDelete(entry.derivedName);
2815
+ state.delete(key);
2816
+ } catch (err) {
2817
+ console.warn(`Rollback: failed to delete AI Gateway ${entry.derivedName}:`, err);
2818
+ }
2819
+ },
2820
+ async importOne({ options, env, api, state, naming, ts }) {
2821
+ const gatewayId = options.cfId;
2822
+ if (!gatewayId) throw new Error("import ai_gateway: --cf-id <gateway-id> is required");
2823
+ const hit = (await api.aiGatewayListAll()).find((g) => g.id === gatewayId);
2824
+ if (!hit) throw new Error(`import ai_gateway: gateway "${gatewayId}" not found in account`);
2825
+ const derivedName = naming.aiGatewayId(options.logical, env);
2826
+ if (hit.id !== derivedName) throw new Error(`import ai_gateway: cf id "${hit.id}" does not match derived "${derivedName}"`);
2827
+ const key = `ai_gateway:${derivedName}`;
2828
+ const existing = state.get(key);
2829
+ const defaults = resolveAIGatewayConfig({ logicalName: options.logical });
2830
+ const entry = {
2831
+ type: "ai_gateway",
2832
+ logicalName: options.logical,
2833
+ derivedName,
2834
+ bindingKey: naming.aiGatewayBindingKey(options.logical),
2835
+ cfId: hit.id,
2836
+ cacheTtl: hit.cache_ttl ?? defaults.cacheTtl,
2837
+ cacheInvalidateOnUpdate: hit.cache_invalidate_on_update ?? defaults.cacheInvalidateOnUpdate,
2838
+ collectLogs: hit.collect_logs ?? defaults.collectLogs,
2839
+ authentication: hit.authentication ?? defaults.authentication,
2840
+ rateLimitingInterval: hit.rate_limiting_interval ?? defaults.rateLimitingInterval,
2841
+ rateLimitingLimit: hit.rate_limiting_limit ?? defaults.rateLimitingLimit,
2842
+ rateLimitingTechnique: hit.rate_limiting_technique ?? defaults.rateLimitingTechnique,
2843
+ createdAt: existing?.type === "ai_gateway" ? existing.createdAt : ts,
2844
+ updatedAt: ts
2845
+ };
2846
+ state.set(key, entry);
2847
+ }
2848
+ };
2849
+
2850
+ //#endregion
2851
+ //#region src/features/pipelines/pipelines.apply.ts
2852
+ const STATE_KEY_PREFIX$2 = "pipeline:";
2853
+ function stateKey$2(derivedName) {
2854
+ return `${STATE_KEY_PREFIX$2}${derivedName}`;
2855
+ }
2856
+ /**
2857
+ * Idempotent create — skips when a state entry already exists. Pipeline SQL
2858
+ * updates on existing pipelines are deliberately not handled here (Cloudflare's
2859
+ * V1 endpoint has no PATCH today; SQL changes require a destroy + recreate).
2860
+ * Drift surfaces SQL mismatches so operators can recover explicitly.
2861
+ */
2862
+ async function pipelinesApply(resources, _tenant, env, api, state, naming) {
2863
+ for (const config of resources) {
2864
+ const derivedName = naming.pipelineName(config.logicalName, env);
2865
+ const key = stateKey$2(derivedName);
2866
+ if (state.get(key)) continue;
2867
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2868
+ const created = await api.pipelineCreate({
2869
+ name: derivedName,
2870
+ sql: config.sql
2871
+ });
2872
+ state.set(key, {
2873
+ type: "pipeline",
2874
+ logicalName: config.logicalName,
2875
+ derivedName,
2876
+ bindingKey: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
2877
+ cfId: created.id,
2878
+ sql: created.sql ?? config.sql,
2879
+ status: created.status,
2880
+ createdAt: ts,
2881
+ updatedAt: ts
2882
+ });
2883
+ }
2884
+ }
2885
+
2886
+ //#endregion
2887
+ //#region src/features/pipelines/pipelines.sync.ts
2888
+ function pipelinesSync(allPipelines, resources, _tenant, env, state, naming) {
2889
+ for (const config of resources) {
2890
+ const derivedName = naming.pipelineName(config.logicalName, env);
2891
+ const match = allPipelines.find((p) => p.name === derivedName);
2892
+ if (!match) continue;
2893
+ const key = `pipeline:${derivedName}`;
2894
+ const existing = state.get(key);
2895
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
2896
+ state.set(key, {
2897
+ type: "pipeline",
2898
+ logicalName: config.logicalName,
2899
+ derivedName,
2900
+ bindingKey: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
2901
+ cfId: match.id,
2902
+ sql: match.sql,
2903
+ status: match.status,
2904
+ createdAt: existing?.type === "pipeline" ? existing.createdAt : ts,
2905
+ updatedAt: ts
2906
+ });
2907
+ }
2908
+ }
2909
+
2910
+ //#endregion
2911
+ //#region src/features/pipelines/pipelines.drift.ts
2912
+ function pipelinesDrift(allPipelines, resources, env, state, naming) {
2913
+ const drift = {
2914
+ kind: "pipeline",
2915
+ missingFromCloudflare: [],
2916
+ unrecordedInState: [],
2917
+ undeployed: []
2918
+ };
2919
+ const cfByName = new Map(allPipelines.map((p) => [p.name, p]));
2920
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "pipeline");
2921
+ for (const config of resources) {
2922
+ const derivedName = naming.pipelineName(config.logicalName, env);
2923
+ const inCloudflare = cfByName.has(derivedName);
2924
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
2925
+ if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
2926
+ logicalName: stateEntry.logicalName,
2927
+ derivedName: stateEntry.derivedName,
2928
+ cfId: stateEntry.cfId
2929
+ });
2930
+ else if (inCloudflare && !stateEntry) {
2931
+ const cf = cfByName.get(derivedName);
2932
+ drift.unrecordedInState.push({
2933
+ logicalName: config.logicalName,
2934
+ derivedName,
2935
+ cfId: cf.id
2936
+ });
2937
+ } else if (!inCloudflare && !stateEntry) drift.undeployed.push({
2938
+ logicalName: config.logicalName,
2939
+ derivedName
2940
+ });
2941
+ }
2942
+ return drift;
2943
+ }
2944
+
2945
+ //#endregion
2946
+ //#region src/features/pipelines/pipelines.destroy.ts
2947
+ async function pipelinesDestroy(_env, state, api, config, baseDir, _force) {
2948
+ const owned = await logicalNamesForResourceKind(config, baseDir, "pipeline");
2949
+ const resources = state.getAll();
2950
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "pipeline");
2951
+ for (const [key, entry] of entries) {
2952
+ if (!owned.has(entry.logicalName)) continue;
2953
+ try {
2954
+ await api.pipelineDelete(entry.cfId);
2955
+ state.delete(key);
2956
+ } catch (err) {
2957
+ console.warn(`Failed to delete Pipeline ${entry.derivedName}:`, err);
2958
+ }
2959
+ }
2960
+ }
2961
+
2962
+ //#endregion
2963
+ //#region src/features/pipelines/pipelines.generate.ts
2964
+ /**
2965
+ * Emit `pipelines[]` bindings for declared pipelines. Each binding requires
2966
+ * the pipeline to exist in state (i.e. `tamer apply` has run). Throws
2967
+ * otherwise so the operator gets an actionable error before `wrangler deploy`.
2968
+ *
2969
+ * Note: Wrangler's `pipeline` field accepts the pipeline **name**, not the
2970
+ * server-assigned id, so we emit the derived name here.
2971
+ */
2972
+ function pipelinesGenerate(resources, env, state, naming) {
2973
+ const out = [];
2974
+ for (const config of resources) {
2975
+ const derivedName = naming.pipelineName(config.logicalName, env);
2976
+ const entry = state.get(`pipeline:${derivedName}`);
2977
+ if (!entry || entry.type !== "pipeline") throw new Error(`Pipeline "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
2978
+ out.push({
2979
+ binding: entry.bindingKey,
2980
+ pipeline: entry.derivedName
2981
+ });
2982
+ }
2983
+ return out;
2984
+ }
2985
+
2986
+ //#endregion
2987
+ //#region src/features/pipelines/pipelines.status.ts
2988
+ function pipelinesStatus(resources, env, state, naming) {
2989
+ const out = [];
2990
+ for (const config of resources) {
2991
+ const derivedName = naming.pipelineName(config.logicalName, env);
2992
+ const entry = state.get(`pipeline:${derivedName}`);
2993
+ out.push({
2994
+ binding: config.binding?.trim() || naming.pipelineBindingKey(config.logicalName),
2995
+ name: derivedName,
2996
+ status: entry ? "ok" : "missing"
2997
+ });
2998
+ }
2999
+ return out;
3000
+ }
3001
+
3002
+ //#endregion
3003
+ //#region src/features/pipelines/pipelines.diff.ts
3004
+ function pipelinesDiffPlanItems(args) {
3005
+ const { resources, env, state, naming } = args;
3006
+ const items = [];
3007
+ for (const config of resources) {
3008
+ const derivedName = naming.pipelineName(config.logicalName, env);
3009
+ const key = `pipeline:${derivedName}`;
3010
+ const entry = state.get(key);
3011
+ if (!entry || entry.type !== "pipeline") continue;
3012
+ const stateEntry = entry;
3013
+ const expectedBinding = config.binding?.trim() || naming.pipelineBindingKey(config.logicalName);
3014
+ const changes = [];
3015
+ if (stateEntry.sql !== config.sql) changes.push({
3016
+ field: "sql",
3017
+ from: truncate(stateEntry.sql),
3018
+ to: truncate(config.sql),
3019
+ kind: "immutable"
3020
+ });
3021
+ if (stateEntry.bindingKey !== expectedBinding) changes.push({
3022
+ field: "binding",
3023
+ from: stateEntry.bindingKey,
3024
+ to: expectedBinding,
3025
+ kind: "mutable"
3026
+ });
3027
+ if (changes.length === 0) continue;
3028
+ const hasImmutable = changes.some((c) => c.kind === "immutable");
3029
+ items.push({
3030
+ kind: "pipeline",
3031
+ action: hasImmutable ? "replace" : "update",
3032
+ logicalName: config.logicalName,
3033
+ derivedName,
3034
+ detail: changes.map((c) => c.field).join(", "),
3035
+ changes
3036
+ });
3037
+ }
3038
+ return items;
3039
+ }
3040
+ function truncate(v) {
3041
+ return v.length > 60 ? `${v.slice(0, 57)}...` : v;
3042
+ }
3043
+
3044
+ //#endregion
3045
+ //#region src/features/pipelines/pipelines.module.ts
3046
+ const pipelinesModule = {
3047
+ kind: "pipeline",
3048
+ label: "Pipeline",
3049
+ configKey: "pipelines",
3050
+ stateEntryType: "pipeline",
3051
+ async fetchAll(api) {
3052
+ return (await api.pipelineListAll()).map((p) => ({
3053
+ id: p.id,
3054
+ name: p.name,
3055
+ sql: p.sql,
3056
+ status: p.status
3057
+ }));
3058
+ },
3059
+ async apply(ctx) {
3060
+ await pipelinesApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
3061
+ },
3062
+ sync(ctx) {
3063
+ pipelinesSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
3064
+ },
3065
+ drift(ctx) {
3066
+ return pipelinesDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
3067
+ },
3068
+ diff(ctx) {
3069
+ return pipelinesDiffPlanItems({
3070
+ resources: ctx.resources,
3071
+ env: ctx.env,
3072
+ state: ctx.state,
3073
+ naming: ctx.naming
3074
+ });
3075
+ },
3076
+ async destroy(ctx) {
3077
+ await pipelinesDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
3078
+ },
3079
+ status(ctx) {
3080
+ return pipelinesStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
3081
+ },
3082
+ generate(ctx) {
3083
+ const generated = ctx.resources.length > 0 ? pipelinesGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming) : [];
3084
+ const passthrough = ctx.passthrough?.pipelines ?? [];
3085
+ if (generated.length === 0 && passthrough.length === 0) return {};
3086
+ return { pipelines: [...passthrough, ...generated] };
3087
+ },
3088
+ pickResources(source) {
3089
+ return resourcesFrom(source)?.pipelines ?? [];
3090
+ },
3091
+ async destroyOne({ api, state, key, entry }) {
3092
+ if (entry.type !== "pipeline") return;
3093
+ try {
3094
+ await api.pipelineDelete(entry.cfId);
3095
+ state.delete(key);
3096
+ } catch (err) {
3097
+ console.warn(`Rollback: failed to delete Pipeline ${entry.derivedName}:`, err);
3098
+ }
3099
+ },
3100
+ async importOne({ options, env, api, state, naming, ts }) {
3101
+ const pipelineId = options.cfId;
3102
+ if (!pipelineId) throw new Error("import pipeline: --cf-id <pipeline-id> is required");
3103
+ const hit = (await api.pipelineListAll()).find((p) => p.id === pipelineId);
3104
+ if (!hit) throw new Error(`import pipeline: pipeline "${pipelineId}" not found in account`);
3105
+ const derivedName = naming.pipelineName(options.logical, env);
3106
+ if (hit.name !== derivedName) throw new Error(`import pipeline: cf name "${hit.name}" does not match derived "${derivedName}"`);
3107
+ const key = `pipeline:${derivedName}`;
3108
+ const existing = state.get(key);
3109
+ const entry = {
3110
+ type: "pipeline",
3111
+ logicalName: options.logical,
3112
+ derivedName,
3113
+ bindingKey: naming.pipelineBindingKey(options.logical),
3114
+ cfId: hit.id,
3115
+ sql: hit.sql,
3116
+ status: hit.status,
3117
+ createdAt: existing?.type === "pipeline" ? existing.createdAt : ts,
3118
+ updatedAt: ts
3119
+ };
3120
+ state.set(key, entry);
3121
+ }
3122
+ };
3123
+
3124
+ //#endregion
3125
+ //#region src/features/workflows/workflows.diff.ts
3126
+ function workflowsDiffPlanItems(args) {
3127
+ const { resources, env, state, naming, worker } = args;
3128
+ const items = [];
3129
+ for (const config of resources) {
3130
+ const derivedName = naming.workflowName(config.logicalName, env);
3131
+ const key = `workflow:${derivedName}`;
3132
+ const entry = state.get(key);
3133
+ if (!entry || entry.type !== "workflow") continue;
3134
+ const changes = computeWorkflowChanges(entry, config, naming, worker);
3135
+ if (changes.length === 0) continue;
3136
+ items.push({
3137
+ kind: "workflow",
3138
+ action: "update",
3139
+ logicalName: config.logicalName,
3140
+ derivedName,
3141
+ detail: changes.map((c) => `${c.field}: ${formatVal(c.from)} -> ${formatVal(c.to)}`).join(", "),
3142
+ changes
3143
+ });
3144
+ }
3145
+ return items;
3146
+ }
3147
+ /**
3148
+ * Pure comparison shared with `workflowsApply` so plan and apply agree
3149
+ * on what counts as drift on a registered workflow. Drift in any of
3150
+ * `className`, `scriptName` (defaulting to the owning worker's
3151
+ * deployed name), `limits.steps`, or the binding key triggers an
3152
+ * in-place PUT — Cloudflare treats `PUT /accounts/{id}/workflows/{name}`
3153
+ * as create-or-update.
3154
+ */
3155
+ function computeWorkflowChanges(state, config, naming, worker) {
3156
+ const expectedScriptName = config.scriptName?.trim() ?? worker?.deployedName;
3157
+ const expectedBindingKey = config.binding?.trim() || naming.workflowBindingKey(config.logicalName);
3158
+ const changes = [];
3159
+ if (state.className !== config.className) changes.push({
3160
+ field: "className",
3161
+ from: state.className,
3162
+ to: config.className,
3163
+ kind: "mutable"
3164
+ });
3165
+ if (expectedScriptName !== void 0 && state.scriptName !== expectedScriptName) changes.push({
3166
+ field: "scriptName",
3167
+ from: state.scriptName,
3168
+ to: expectedScriptName,
3169
+ kind: "mutable"
3170
+ });
3171
+ const stateSteps = state.limits?.steps;
3172
+ const configSteps = config.limits?.steps;
3173
+ if ((stateSteps ?? null) !== (configSteps ?? null)) changes.push({
3174
+ field: "limits.steps",
3175
+ from: stateSteps,
3176
+ to: configSteps,
3177
+ kind: "mutable"
3178
+ });
3179
+ if (state.bindingKey !== expectedBindingKey) changes.push({
3180
+ field: "binding",
3181
+ from: state.bindingKey,
3182
+ to: expectedBindingKey,
3183
+ kind: "mutable"
3184
+ });
3185
+ return changes;
3186
+ }
3187
+ function formatVal(v) {
3188
+ if (v === void 0 || v === null) return "(unset)";
3189
+ if (typeof v === "string") return v.length > 32 ? `${v.slice(0, 29)}...` : v;
3190
+ return String(v);
3191
+ }
3192
+
3193
+ //#endregion
3194
+ //#region src/features/workflows/workflows.apply.ts
3195
+ const STATE_KEY_PREFIX$1 = "workflow:";
3196
+ function stateKey$1(derivedName) {
3197
+ return `${STATE_KEY_PREFIX$1}${derivedName}`;
3198
+ }
3199
+ function resolveScriptName(config, worker) {
3200
+ const explicit = config.scriptName?.trim();
3201
+ if (explicit) return explicit;
3202
+ if (!worker) throw new Error(`workflow "${config.logicalName}": no scriptName given and module ran without a worker scope — set scriptName on the resource or invoke apply per-worker.`);
3203
+ return worker.deployedName;
3204
+ }
3205
+ function limitsEqual(a, b) {
3206
+ return (a?.steps ?? null) === (b?.steps ?? null);
3207
+ }
3208
+ /**
3209
+ * Idempotent upsert via `PUT /accounts/{id}/workflows/{name}`. Cloudflare
3210
+ * treats PUT as create-or-update, so we issue it whenever the recorded
3211
+ * `(class_name, script_name, limits)` triple has drifted from config (or
3212
+ * the workflow has never been registered at all). Drift between recorded
3213
+ * state and *live* Cloudflare is surfaced separately by `workflowsDrift`.
3214
+ */
3215
+ async function workflowsApply(resources, _tenant, env, api, state, naming, worker) {
3216
+ for (const config of resources) {
3217
+ const derivedName = naming.workflowName(config.logicalName, env);
3218
+ const scriptName = resolveScriptName(config, worker);
3219
+ const key = stateKey$1(derivedName);
3220
+ const existing = state.get(key);
3221
+ const wantsBindingKey = config.binding?.trim() || naming.workflowBindingKey(config.logicalName);
3222
+ if (existing && existing.type === "workflow" && existing.className === config.className && existing.scriptName === scriptName && limitsEqual(existing.limits, config.limits) && existing.bindingKey === wantsBindingKey) continue;
3223
+ if (existing && existing.type === "workflow") {
3224
+ const changes = computeWorkflowChanges(existing, config, naming, worker);
3225
+ logApplyChange({
3226
+ kind: "workflow",
3227
+ action: "update",
3228
+ logical: config.logicalName,
3229
+ derived: derivedName,
3230
+ changes
3231
+ });
3232
+ } else logApplyChange({
3233
+ kind: "workflow",
3234
+ action: "create",
3235
+ logical: config.logicalName,
3236
+ derived: derivedName
3237
+ });
3238
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3239
+ const upserted = await api.workflowUpsert(derivedName, {
3240
+ class_name: config.className,
3241
+ script_name: scriptName,
3242
+ ...config.limits ? { limits: config.limits } : {}
3243
+ });
3244
+ const entry = {
3245
+ type: "workflow",
3246
+ logicalName: config.logicalName,
3247
+ derivedName,
3248
+ bindingKey: wantsBindingKey,
3249
+ cfId: upserted.id,
3250
+ className: upserted.class_name,
3251
+ scriptName: upserted.script_name,
3252
+ limits: config.limits,
3253
+ createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
3254
+ updatedAt: ts
3255
+ };
3256
+ state.set(key, entry);
3257
+ }
3258
+ }
3259
+
3260
+ //#endregion
3261
+ //#region src/features/workflows/workflows.sync.ts
3262
+ /**
3263
+ * Match each declared workflow against `GET /accounts/{id}/workflows` and
3264
+ * upsert into state. Stack-scoped: `resources` is already filtered to the
3265
+ * current config, so we never persist a workflow owned by another stack
3266
+ * sharing this state row.
3267
+ */
3268
+ function workflowsSync(allWorkflows, resources, _tenant, env, state, naming) {
3269
+ for (const config of resources) {
3270
+ const derivedName = naming.workflowName(config.logicalName, env);
3271
+ const pattern = naming.workflowMatchPattern(config.logicalName, env);
3272
+ const match = allWorkflows.find((w) => pattern(w.name));
3273
+ if (!match) continue;
3274
+ const key = `workflow:${derivedName}`;
3275
+ const existing = state.get(key);
3276
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3277
+ const entry = {
3278
+ type: "workflow",
3279
+ logicalName: config.logicalName,
3280
+ derivedName,
3281
+ bindingKey: config.binding?.trim() || naming.workflowBindingKey(config.logicalName),
3282
+ cfId: match.id,
3283
+ className: match.class_name,
3284
+ scriptName: match.script_name,
3285
+ limits: existing?.type === "workflow" ? existing.limits : config.limits,
3286
+ createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
3287
+ updatedAt: ts
3288
+ };
3289
+ state.set(key, entry);
3290
+ }
3291
+ }
3292
+
3293
+ //#endregion
3294
+ //#region src/features/workflows/workflows.drift.ts
3295
+ function workflowsDrift(allWorkflows, resources, env, state, naming) {
3296
+ const drift = {
3297
+ kind: "workflow",
3298
+ missingFromCloudflare: [],
3299
+ unrecordedInState: [],
3300
+ undeployed: []
3301
+ };
3302
+ const cfByName = new Map(allWorkflows.map((w) => [w.name, w]));
3303
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "workflow");
3304
+ for (const config of resources) {
3305
+ const derivedName = naming.workflowName(config.logicalName, env);
3306
+ const inCloudflare = cfByName.has(derivedName);
3307
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
3308
+ if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
3309
+ logicalName: stateEntry.logicalName,
3310
+ derivedName: stateEntry.derivedName,
3311
+ cfId: stateEntry.cfId
3312
+ });
3313
+ else if (inCloudflare && !stateEntry) {
3314
+ const cf = cfByName.get(derivedName);
3315
+ drift.unrecordedInState.push({
3316
+ logicalName: config.logicalName,
3317
+ derivedName,
3318
+ cfId: cf.id
3319
+ });
3320
+ } else if (!inCloudflare && !stateEntry) drift.undeployed.push({
3321
+ logicalName: config.logicalName,
3322
+ derivedName
3323
+ });
3324
+ }
3325
+ return drift;
3326
+ }
3327
+
3328
+ //#endregion
3329
+ //#region src/features/workflows/workflows.destroy.ts
3330
+ async function workflowsDestroy(_env, state, api, config, baseDir, _force) {
3331
+ const owned = await logicalNamesForResourceKind(config, baseDir, "workflow");
3332
+ const resources = state.getAll();
3333
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "workflow");
3334
+ for (const [key, entry] of entries) {
3335
+ if (!owned.has(entry.logicalName)) continue;
3336
+ try {
3337
+ await api.workflowDelete(entry.derivedName);
3338
+ state.delete(key);
3339
+ } catch (err) {
3340
+ console.warn(`Failed to delete Workflow ${entry.derivedName}:`, err);
3341
+ }
3342
+ }
3343
+ }
3344
+
3345
+ //#endregion
3346
+ //#region src/features/workflows/workflows.generate.ts
3347
+ /**
3348
+ * Emit `workflows[]` bindings for declared workflows. Requires `tamer apply`
3349
+ * to have run (state row present) so we can fail fast with a clear message
3350
+ * before `wrangler deploy` would surface a less actionable error.
3351
+ *
3352
+ * `script_name` is only emitted when the user pinned an explicit override
3353
+ * — without it, Wrangler binds to the current worker's own script, which
3354
+ * matches our default-to-owning-worker semantics in apply.
3355
+ */
3356
+ function workflowsGenerate(resources, env, state, naming, worker) {
3357
+ const out = [];
3358
+ for (const config of resources) {
3359
+ const derivedName = naming.workflowName(config.logicalName, env);
3360
+ const entry = state.get(`workflow:${derivedName}`);
3361
+ if (!entry || entry.type !== "workflow") throw new Error(`Workflow "${config.logicalName}" not in state. Run 'tamer apply --env ${env}' first.`);
3362
+ const explicitScript = config.scriptName?.trim();
3363
+ const ownerScript = worker?.deployedName;
3364
+ const binding = {
3365
+ binding: entry.bindingKey,
3366
+ name: entry.derivedName,
3367
+ class_name: entry.className
3368
+ };
3369
+ if (explicitScript) binding.script_name = explicitScript;
3370
+ else if (ownerScript && ownerScript !== entry.scriptName) binding.script_name = entry.scriptName;
3371
+ out.push(binding);
3372
+ }
3373
+ return out;
3374
+ }
3375
+
3376
+ //#endregion
3377
+ //#region src/features/workflows/workflows.status.ts
3378
+ function workflowsStatus(resources, env, state, naming) {
3379
+ const out = [];
3380
+ for (const config of resources) {
3381
+ const derivedName = naming.workflowName(config.logicalName, env);
3382
+ const entry = state.get(`workflow:${derivedName}`);
3383
+ out.push({
3384
+ binding: config.binding?.trim() || naming.workflowBindingKey(config.logicalName),
3385
+ name: derivedName,
3386
+ status: entry ? "ok" : "missing"
3387
+ });
3388
+ }
3389
+ return out;
3390
+ }
3391
+
3392
+ //#endregion
3393
+ //#region src/features/workflows/workflows.module.ts
3394
+ const workflowsModule = {
3395
+ kind: "workflow",
3396
+ label: "Workflow",
3397
+ configKey: "workflows",
3398
+ stateEntryType: "workflow",
3399
+ async fetchAll(api) {
3400
+ return (await api.workflowListAll()).map((w) => ({
3401
+ id: w.id,
3402
+ name: w.name,
3403
+ class_name: w.class_name,
3404
+ script_name: w.script_name
3405
+ }));
3406
+ },
3407
+ async apply(ctx) {
3408
+ await workflowsApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming, ctx.worker);
3409
+ },
3410
+ sync(ctx) {
3411
+ workflowsSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
3412
+ },
3413
+ drift(ctx) {
3414
+ return workflowsDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
3415
+ },
3416
+ diff(ctx) {
3417
+ return workflowsDiffPlanItems({
3418
+ resources: ctx.resources,
3419
+ env: ctx.env,
3420
+ state: ctx.state,
3421
+ naming: ctx.naming,
3422
+ worker: ctx.worker
3423
+ });
3424
+ },
3425
+ async destroy(ctx) {
3426
+ await workflowsDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
3427
+ },
3428
+ status(ctx) {
3429
+ return workflowsStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
3430
+ },
3431
+ generate(ctx) {
3432
+ const generated = ctx.resources.length > 0 ? workflowsGenerate(ctx.resources, ctx.env, ctx.state, ctx.naming, ctx.worker) : [];
3433
+ const passthrough = ctx.passthrough?.workflows ?? [];
3434
+ if (generated.length === 0 && passthrough.length === 0) return {};
3435
+ return { workflows: [...passthrough, ...generated] };
3436
+ },
3437
+ pickResources(source) {
3438
+ return resourcesFrom(source)?.workflows ?? [];
3439
+ },
3440
+ async destroyOne({ api, state, key, entry }) {
3441
+ if (entry.type !== "workflow") return;
3442
+ try {
3443
+ await api.workflowDelete(entry.derivedName);
3444
+ state.delete(key);
3445
+ } catch (err) {
3446
+ console.warn(`Rollback: failed to delete Workflow ${entry.derivedName}:`, err);
3447
+ }
3448
+ },
3449
+ async importOne({ options, env, api, state, naming, ts }) {
3450
+ const all = await api.workflowListAll();
3451
+ const derivedName = naming.workflowName(options.logical, env);
3452
+ const hit = all.find((w) => w.name === derivedName);
3453
+ if (!hit) throw new Error(`import workflow: no workflow named "${derivedName}" found in account`);
3454
+ if (options.cfId && hit.id !== options.cfId) throw new Error(`import workflow: cf id "${hit.id}" does not match --cf-id "${options.cfId}"`);
3455
+ const key = `workflow:${derivedName}`;
3456
+ const existing = state.get(key);
3457
+ const entry = {
3458
+ type: "workflow",
3459
+ logicalName: options.logical,
3460
+ derivedName,
3461
+ bindingKey: naming.workflowBindingKey(options.logical),
3462
+ cfId: hit.id,
3463
+ className: hit.class_name,
3464
+ scriptName: hit.script_name,
3465
+ limits: existing?.type === "workflow" ? existing.limits : void 0,
3466
+ createdAt: existing?.type === "workflow" ? existing.createdAt : ts,
3467
+ updatedAt: ts
3468
+ };
3469
+ state.set(key, entry);
3470
+ }
3471
+ };
3472
+
3473
+ //#endregion
3474
+ //#region src/features/secrets-store/secrets-store.apply.ts
3475
+ const STATE_KEY_PREFIX = "secrets_store:";
3476
+ function stateKey(derivedName) {
3477
+ return `${STATE_KEY_PREFIX}${derivedName}`;
3478
+ }
3479
+ /**
3480
+ * Idempotent provisioning for Secrets Store stores. The Cloudflare create
3481
+ * endpoint is non-idempotent (a duplicate name returns 409), so we list
3482
+ * first and short-circuit when a matching name already exists. Secret
3483
+ * **values** are deliberately out of scope — Tamer only manages the
3484
+ * account-scoped container. See `SecretsStoreSecretBinding` for how
3485
+ * worker bindings reference the resolved `store_id`.
3486
+ */
3487
+ async function secretsStoreApply(resources, _tenant, env, api, state, naming) {
3488
+ if (resources.length === 0) return;
3489
+ let allStores = null;
3490
+ for (const config of resources) {
3491
+ const derivedName = naming.secretsStoreName(config.logicalName, env);
3492
+ const key = stateKey(derivedName);
3493
+ const existing = state.get(key);
3494
+ const wantsBindingKey = naming.secretsStoreBindingKey(config.logicalName);
3495
+ if (existing && existing.type === "secrets_store" && existing.bindingKey === wantsBindingKey) continue;
3496
+ if (allStores === null) allStores = await api.secretsStoreListAll();
3497
+ const match = allStores.find((s) => s.name === derivedName);
3498
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3499
+ let cfId;
3500
+ if (match) cfId = match.id;
3501
+ else cfId = (await api.secretsStoreCreate(derivedName)).id;
3502
+ const entry = {
3503
+ type: "secrets_store",
3504
+ logicalName: config.logicalName,
3505
+ derivedName,
3506
+ bindingKey: wantsBindingKey,
3507
+ cfId,
3508
+ createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
3509
+ updatedAt: ts
3510
+ };
3511
+ state.set(key, entry);
3512
+ }
3513
+ }
3514
+
3515
+ //#endregion
3516
+ //#region src/features/secrets-store/secrets-store.sync.ts
3517
+ /**
3518
+ * Match each declared store against `GET /accounts/{id}/secrets_store/stores`
3519
+ * and upsert into state. Stack-scoped: `resources` is already filtered to
3520
+ * the current config so we never persist a store owned by another stack
3521
+ * sharing this state row.
3522
+ */
3523
+ function secretsStoreSync(allStores, resources, _tenant, env, state, naming) {
3524
+ for (const config of resources) {
3525
+ const derivedName = naming.secretsStoreName(config.logicalName, env);
3526
+ const match = allStores.find((s) => s.name === derivedName);
3527
+ if (!match) continue;
3528
+ const key = `secrets_store:${derivedName}`;
3529
+ const existing = state.get(key);
3530
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
3531
+ const entry = {
3532
+ type: "secrets_store",
3533
+ logicalName: config.logicalName,
3534
+ derivedName,
3535
+ bindingKey: naming.secretsStoreBindingKey(config.logicalName),
3536
+ cfId: match.id,
3537
+ createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
3538
+ updatedAt: ts
3539
+ };
3540
+ state.set(key, entry);
3541
+ }
3542
+ }
3543
+
3544
+ //#endregion
3545
+ //#region src/features/secrets-store/secrets-store.drift.ts
3546
+ function secretsStoreDrift(allStores, resources, env, state, naming) {
3547
+ const drift = {
3548
+ kind: "secret_store",
3549
+ missingFromCloudflare: [],
3550
+ unrecordedInState: [],
3551
+ undeployed: []
3552
+ };
3553
+ const cfByName = new Map(allStores.map((s) => [s.name, s]));
3554
+ const stateEntries = Object.values(state.getAll()).filter((e) => e.type === "secrets_store");
3555
+ for (const config of resources) {
3556
+ const derivedName = naming.secretsStoreName(config.logicalName, env);
3557
+ const inCloudflare = cfByName.has(derivedName);
3558
+ const stateEntry = stateEntries.find((e) => e.logicalName === config.logicalName);
3559
+ if (stateEntry && !inCloudflare) drift.missingFromCloudflare.push({
3560
+ logicalName: stateEntry.logicalName,
3561
+ derivedName: stateEntry.derivedName,
3562
+ cfId: stateEntry.cfId
3563
+ });
3564
+ else if (inCloudflare && !stateEntry) {
3565
+ const cf = cfByName.get(derivedName);
3566
+ drift.unrecordedInState.push({
3567
+ logicalName: config.logicalName,
3568
+ derivedName,
3569
+ cfId: cf.id
3570
+ });
3571
+ } else if (!inCloudflare && !stateEntry) drift.undeployed.push({
3572
+ logicalName: config.logicalName,
3573
+ derivedName
3574
+ });
3575
+ }
3576
+ return drift;
3577
+ }
3578
+
3579
+ //#endregion
3580
+ //#region src/features/secrets-store/secrets-store.destroy.ts
3581
+ async function secretsStoreDestroy(_env, state, api, config, baseDir, _force) {
3582
+ const owned = await logicalNamesForResourceKind(config, baseDir, "secret_store");
3583
+ const resources = state.getAll();
3584
+ const entries = Object.entries(resources).filter((kv) => kv[1].type === "secrets_store");
3585
+ for (const [key, entry] of entries) {
3586
+ if (!owned.has(entry.logicalName)) continue;
3587
+ try {
3588
+ await api.secretsStoreDelete(entry.cfId);
3589
+ state.delete(key);
3590
+ } catch (err) {
3591
+ console.warn(`Failed to delete Secrets Store ${entry.derivedName}:`, err);
3592
+ }
3593
+ }
3594
+ }
3595
+
3596
+ //#endregion
3597
+ //#region src/features/secrets-store/secrets-store.status.ts
3598
+ function secretsStoreStatus(resources, env, state, naming) {
3599
+ const out = [];
3600
+ for (const config of resources) {
3601
+ const derivedName = naming.secretsStoreName(config.logicalName, env);
3602
+ const entry = state.get(`secrets_store:${derivedName}`);
3603
+ out.push({
3604
+ binding: naming.secretsStoreBindingKey(config.logicalName),
3605
+ name: derivedName,
3606
+ cfId: entry?.cfId,
3607
+ status: entry ? "ok" : "missing"
3608
+ });
3609
+ }
3610
+ return out;
3611
+ }
3612
+
3613
+ //#endregion
3614
+ //#region src/features/secrets-store/secrets-store.module.ts
3615
+ const secretsStoreModule = {
3616
+ kind: "secret_store",
3617
+ label: "Secrets Store",
3618
+ configKey: "secretsStores",
3619
+ stateEntryType: "secrets_store",
3620
+ async fetchAll(api) {
3621
+ return (await api.secretsStoreListAll()).map((s) => ({
3622
+ id: s.id,
3623
+ name: s.name
3624
+ }));
3625
+ },
3626
+ async apply(ctx) {
3627
+ await secretsStoreApply(ctx.resources, ctx.tenant, ctx.env, ctx.api, ctx.state, ctx.naming);
3628
+ },
3629
+ sync(ctx) {
3630
+ secretsStoreSync(ctx.all, ctx.resources, ctx.tenant, ctx.env, ctx.state, ctx.naming);
3631
+ },
3632
+ drift(ctx) {
3633
+ return secretsStoreDrift(ctx.all, ctx.resources, ctx.env, ctx.state, ctx.naming);
3634
+ },
3635
+ async destroy(ctx) {
3636
+ await secretsStoreDestroy(ctx.env, ctx.state, ctx.api, ctx.config, ctx.baseDir, ctx.force);
3637
+ },
3638
+ status(ctx) {
3639
+ return secretsStoreStatus(ctx.resources, ctx.env, ctx.state, ctx.naming);
3640
+ },
3641
+ generate() {
3642
+ return {};
3643
+ },
3644
+ pickResources(source) {
3645
+ return resourcesFrom(source)?.secretsStores ?? [];
3646
+ },
3647
+ async destroyOne({ api, state, key, entry }) {
3648
+ if (entry.type !== "secrets_store") return;
3649
+ try {
3650
+ await api.secretsStoreDelete(entry.cfId);
3651
+ state.delete(key);
3652
+ } catch (err) {
3653
+ console.warn(`Rollback: failed to delete Secrets Store ${entry.derivedName}:`, err);
3654
+ }
3655
+ },
3656
+ async importOne({ options, env, api, state, naming, ts }) {
3657
+ const all = await api.secretsStoreListAll();
3658
+ const derivedName = naming.secretsStoreName(options.logical, env);
3659
+ const hit = all.find((s) => s.name === derivedName);
3660
+ if (!hit) throw new Error(`import secret_store: no store named "${derivedName}" found in account`);
3661
+ if (options.cfId && hit.id !== options.cfId) throw new Error(`import secret_store: cf id "${hit.id}" does not match --cf-id "${options.cfId}"`);
3662
+ const key = `secrets_store:${derivedName}`;
3663
+ const existing = state.get(key);
3664
+ const entry = {
3665
+ type: "secrets_store",
3666
+ logicalName: options.logical,
3667
+ derivedName,
3668
+ bindingKey: naming.secretsStoreBindingKey(options.logical),
3669
+ cfId: hit.id,
3670
+ createdAt: existing?.type === "secrets_store" ? existing.createdAt : ts,
3671
+ updatedAt: ts
3672
+ };
3673
+ state.set(key, entry);
3674
+ }
3675
+ };
3676
+
3677
+ //#endregion
3678
+ //#region src/core/registry/registry.ts
3679
+ const resourceModules = [
3680
+ d1Module,
3681
+ r2Module,
3682
+ kvModule,
3683
+ queuesModule,
3684
+ hyperdriveModule,
3685
+ vectorizeModule,
3686
+ aiGatewayModule,
3687
+ pipelinesModule,
3688
+ workflowsModule,
3689
+ secretsStoreModule
3690
+ ];
3691
+ const moduleByKind = new Map(resourceModules.map((m) => [m.kind, m]));
3692
+ /** Lookup by stable kind. Returns `undefined` for unknown kinds. */
3693
+ function getResourceModule(kind) {
3694
+ return moduleByKind.get(kind);
3695
+ }
3696
+
3697
+ //#endregion
3698
+ //#region src/core/imports/fetchStackImports.ts
3699
+ const IMPORT_RE = /\$\{tamer:import:([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\}/g;
3700
+ /**
3701
+ * Walk the merged `CfiConfig` and collect every `${tamer:import:…}` ref
3702
+ * site along with where it was found. Used both to drive the pre-fetch
3703
+ * (which sibling stacks to load) and by `tamer status` to render an
3704
+ * "inbound imports" panel even before any `apply` has run.
3705
+ *
3706
+ * Self-imports (current stack importing from its own name) are filtered
3707
+ * out — they are almost always a config typo and would otherwise
3708
+ * silently resolve via the same row this command is about to write.
3709
+ */
3710
+ function scanConfigForImports(config) {
3711
+ const selfStack = stackNameForConfig(config);
3712
+ const refs = [];
3713
+ const seen = /* @__PURE__ */ new Set();
3714
+ const push = (raw, fieldPath) => {
3715
+ if (!raw) return;
3716
+ IMPORT_RE.lastIndex = 0;
3717
+ let m;
3718
+ while ((m = IMPORT_RE.exec(raw)) !== null) {
3719
+ const stack = m[1];
3720
+ const output = m[2];
3721
+ if (stack === selfStack) continue;
3722
+ const key = `${fieldPath}::${stack}.${output}`;
3723
+ if (seen.has(key)) continue;
3724
+ seen.add(key);
3725
+ refs.push({
3726
+ stack,
3727
+ output,
3728
+ fieldPath
3729
+ });
3730
+ }
3731
+ };
3732
+ const walkVars = (vars, pathPrefix) => {
3733
+ if (!vars) return;
3734
+ for (const [k, v] of Object.entries(vars)) push(materializeTamerResolvable(v), `${pathPrefix}.${k}`);
3735
+ };
3736
+ const walkR2BucketNames = (w, pathPrefix) => {
3737
+ if (!w.r2_buckets) return;
3738
+ w.r2_buckets.forEach((b, i) => {
3739
+ if (b.bucket_name === void 0) return;
3740
+ push(materializeTamerResolvable(b.bucket_name), `${pathPrefix}.r2_buckets[${i}].bucket_name`);
3741
+ });
3742
+ };
3743
+ const walkD1DatabaseNames = (w, pathPrefix) => {
3744
+ const d1 = w.resources?.d1;
3745
+ if (!d1) return;
3746
+ d1.forEach((d, i) => {
3747
+ if (d.databaseName === void 0) return;
3748
+ push(materializeTamerResolvable(d.databaseName), `${pathPrefix}.resources.d1[${i}].databaseName`);
3749
+ });
3750
+ };
3751
+ /** Service bindings / WfP namespace strings may carry `${tamer:import:…}`. */
3752
+ const walkBindingsWithRefs = (w, pathPrefix) => {
3753
+ w.services?.forEach((s, i) => {
3754
+ if (s.service === void 0) return;
3755
+ push(materializeTamerResolvable(s.service), `${pathPrefix}.services[${i}].service`);
3756
+ });
3757
+ w.dispatch_namespaces?.forEach((d, i) => {
3758
+ if (d.namespace === void 0) return;
3759
+ push(materializeTamerResolvable(d.namespace), `${pathPrefix}.dispatch_namespaces[${i}].namespace`);
3760
+ });
3761
+ };
3762
+ if (config.worker) {
3763
+ walkVars(config.worker.vars, "worker.vars");
3764
+ walkR2BucketNames(config.worker, "worker");
3765
+ walkD1DatabaseNames(config.worker, "worker");
3766
+ walkBindingsWithRefs(config.worker, "worker");
3767
+ if (config.worker.tamerRoutes) config.worker.tamerRoutes.forEach((r, i) => {
3768
+ push(r.host, `worker.tamerRoutes[${i}].host`);
3769
+ push(r.zone, `worker.tamerRoutes[${i}].zone`);
3770
+ });
3771
+ }
3772
+ if (config.workers) for (const [key, w] of Object.entries(config.workers)) {
3773
+ walkVars(w.vars, `worker[${key}].vars`);
3774
+ walkR2BucketNames(w, `worker[${key}]`);
3775
+ walkD1DatabaseNames(w, `worker[${key}]`);
3776
+ walkBindingsWithRefs(w, `worker[${key}]`);
3777
+ if (w.tamerRoutes) w.tamerRoutes.forEach((r, i) => {
3778
+ push(r.host, `worker[${key}].tamerRoutes[${i}].host`);
3779
+ push(r.zone, `worker[${key}].tamerRoutes[${i}].zone`);
3780
+ });
3781
+ }
3782
+ if (config.outputs) for (const [name, source] of Object.entries(config.outputs)) push(materializeTamerResolvable(source), `outputs.${name}`);
3783
+ return refs;
3784
+ }
3785
+ /** Distinct sibling stack names referenced anywhere in `config`. */
3786
+ function importedStackNames(config) {
3787
+ const refs = scanConfigForImports(config);
3788
+ return [...new Set(refs.map((r) => r.stack))].sort();
3789
+ }
3790
+ /**
3791
+ * Hydrate every imported sibling stack's persisted outputs and return
3792
+ * them shaped for {@link ReferenceContext.imports}.
3793
+ *
3794
+ * - `local` env: returns `{}` immediately. Local mode never persists
3795
+ * state, so cross-stack imports are inherently unresolvable. Callers
3796
+ * running in tolerant mode (`plan`, `status`) will see the placeholder
3797
+ * verbatim; strict callers (`apply`, `deploy`) will fail at resolution
3798
+ * with a clear "no imported stack" message.
3799
+ * - Missing sibling state row: recorded as an empty outputs map, so
3800
+ * `lookupImport` can produce a "no published outputs" diagnostic
3801
+ * (vs. the generic "stack not pre-fetched" error).
3802
+ *
3803
+ * The {@link CFApiClient} is shared with the caller for socket reuse.
3804
+ */
3805
+ async function fetchStackImports(api, config, env) {
3806
+ const stacks = importedStackNames(config);
3807
+ if (stacks.length === 0 || env === "local") return {};
3808
+ const out = {};
3809
+ for (const stack of stacks) {
3810
+ const sibling = new StateManager(config.tenant.id, env, stack);
3811
+ try {
3812
+ await sibling.hydrate(api);
3813
+ } catch (err) {
3814
+ throw new Error(`Failed to hydrate imported stack "${stack}" from env "${env}": ${err instanceof Error ? err.message : String(err)}`);
3815
+ }
3816
+ const persisted = sibling.getStackOutputs();
3817
+ const flat = {};
3818
+ for (const [name, v] of Object.entries(persisted)) flat[name] = v.value;
3819
+ out[stack] = flat;
3820
+ }
3821
+ return out;
3822
+ }
3823
+
3824
+ //#endregion
3825
+ export { namingFromConfig as _, resourceModules as a, d1SkipsProvisionAndMigrate as c, mergedWorkerConfigForEnv as d, resolveDeployedWorkerName as f, wranglerConfigCliArgs as g, resolveReferencesInString as h, getResourceModule as i, buildIntraStackScriptNameMap as l, rewriteIntraStackServiceTargets as m, importedStackNames as n, logicalNamesForResourceKind as o, resolveWorkerConfig as p, scanConfigForImports as r, d1CloudflareDatabaseName as s, fetchStackImports as t, mergeWorkerConfigForResourcePick as u };
3826
+ //# sourceMappingURL=fetchStackImports-C-1THPYL.mjs.map