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