@dragonmastery/tamer 0.42.2 → 0.43.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 (103) hide show
  1. package/dist/SecretsVault-D_JTJm5s.mjs +3 -0
  2. package/dist/{apply-Mk9usR-U.mjs → apply-J081Hfqc.mjs} +15 -15
  3. package/dist/{apply-Mk9usR-U.mjs.map → apply-J081Hfqc.mjs.map} +1 -1
  4. package/dist/{applyTarget-CJSn9DeJ.mjs → applyTarget-CTsOrwco.mjs} +3 -3
  5. package/dist/{applyTarget-CJSn9DeJ.mjs.map → applyTarget-CTsOrwco.mjs.map} +1 -1
  6. package/dist/{bootstrap-BCy6B-B6.mjs → bootstrap-D663FPHF.mjs} +4 -4
  7. package/dist/{bootstrap-BCy6B-B6.mjs.map → bootstrap-D663FPHF.mjs.map} +1 -1
  8. package/dist/{cloudflareSnapshot-DqP8v_xG.mjs → cloudflareSnapshot-dKYupX1L.mjs} +5 -5
  9. package/dist/{cloudflareSnapshot-DqP8v_xG.mjs.map → cloudflareSnapshot-dKYupX1L.mjs.map} +1 -1
  10. package/dist/crypto-5HwDmS4B.mjs +3 -0
  11. package/dist/{deploy-DbXBHpCG.mjs → deploy-N8cVT5M9.mjs} +10 -10
  12. package/dist/{deploy-DbXBHpCG.mjs.map → deploy-N8cVT5M9.mjs.map} +1 -1
  13. package/dist/{destroy-CesVkyxf.mjs → destroy-kkOR_ZBj.mjs} +11 -11
  14. package/dist/{destroy-CesVkyxf.mjs.map → destroy-kkOR_ZBj.mjs.map} +1 -1
  15. package/dist/{destroy-tenant-DMynv_u-.mjs → destroy-tenant-DdObj9pd.mjs} +2 -2
  16. package/dist/{destroy-tenant-DMynv_u-.mjs.map → destroy-tenant-DdObj9pd.mjs.map} +1 -1
  17. package/dist/{dev-sk6JGKe4.mjs → dev-zQRvUhdd.mjs} +8 -8
  18. package/dist/{dev-sk6JGKe4.mjs.map → dev-zQRvUhdd.mjs.map} +1 -1
  19. package/dist/{dns-records.resolve-SPYGYNHa.mjs → dns-records.resolve-CUQz6Ivz.mjs} +1 -1
  20. package/dist/{dns-records.resolve-SPYGYNHa.mjs.map → dns-records.resolve-CUQz6Ivz.mjs.map} +1 -1
  21. package/dist/{dns-records.resolve-jECsH6N-.mjs → dns-records.resolve-Dge2kukJ.mjs} +1 -1
  22. package/dist/{dns-records.sync-BORRvLky.mjs → dns-records.sync-BnOga2LS.mjs} +3 -3
  23. package/dist/{dns-records.sync-BORRvLky.mjs.map → dns-records.sync-BnOga2LS.mjs.map} +1 -1
  24. package/dist/{doctor-BgqnscIE.mjs → doctor-yTe9cUO5.mjs} +2 -2
  25. package/dist/{doctor-BgqnscIE.mjs.map → doctor-yTe9cUO5.mjs.map} +1 -1
  26. package/dist/{drift-C5r1cKxV.mjs → drift-B-oPsgPW.mjs} +7 -7
  27. package/dist/{drift-C5r1cKxV.mjs.map → drift-B-oPsgPW.mjs.map} +1 -1
  28. package/dist/drift-RsT8UGQw.mjs +8 -0
  29. package/dist/{emit-C7KXAVP0.mjs → emit-DDH99lzS.mjs} +3 -3
  30. package/dist/{emit-C7KXAVP0.mjs.map → emit-DDH99lzS.mjs.map} +1 -1
  31. package/dist/{env-gc-DE4EV7j7.mjs → env-gc-DrXu7DlG.mjs} +11 -11
  32. package/dist/{env-gc-DE4EV7j7.mjs.map → env-gc-DrXu7DlG.mjs.map} +1 -1
  33. package/dist/{env-list-DKseThiA.mjs → env-list-D-wnLl8W.mjs} +2 -2
  34. package/dist/{env-list-DKseThiA.mjs.map → env-list-D-wnLl8W.mjs.map} +1 -1
  35. package/dist/{events-CqbN9sbT.mjs → events-C86vo1gV.mjs} +2 -2
  36. package/dist/{events-CqbN9sbT.mjs.map → events-C86vo1gV.mjs.map} +1 -1
  37. package/dist/fingerprint-D6E5FTkq.mjs +3 -0
  38. package/dist/{generator-CbH3UZ3K.mjs → generator-Cu5XUkjr.mjs} +2 -2
  39. package/dist/{generator-CbH3UZ3K.mjs.map → generator-Cu5XUkjr.mjs.map} +1 -1
  40. package/dist/{import-leVD9Ryg.mjs → import-QJc0fI1h.mjs} +6 -6
  41. package/dist/{import-leVD9Ryg.mjs.map → import-QJc0fI1h.mjs.map} +1 -1
  42. package/dist/index.d.mts +9 -0
  43. package/dist/index.d.mts.map +1 -1
  44. package/dist/loader-DY6STQU3.mjs +3 -0
  45. package/dist/{logpush-job-Dqlt-wEw.mjs → logpush-job-DZG-3nkJ.mjs} +2 -2
  46. package/dist/{logpush-job-Dqlt-wEw.mjs.map → logpush-job-DZG-3nkJ.mjs.map} +1 -1
  47. package/dist/masterKey-D9gq3eNP.mjs +3 -0
  48. package/dist/{migrate-DyrTw9ep.mjs → migrate-v1UQCzNi.mjs} +6 -6
  49. package/dist/{migrate-DyrTw9ep.mjs.map → migrate-v1UQCzNi.mjs.map} +1 -1
  50. package/dist/normalize-DVSTRZhO.mjs.map +1 -1
  51. package/dist/{plan-D7UyLznb.mjs → plan-Dp2aTzEU.mjs} +12 -12
  52. package/dist/{plan-D7UyLznb.mjs.map → plan-Dp2aTzEU.mjs.map} +1 -1
  53. package/dist/{planFormat-CJw8Kq2s.mjs → planFormat-C8mq3sbD.mjs} +1 -1
  54. package/dist/{planFormat-CJw8Kq2s.mjs.map → planFormat-C8mq3sbD.mjs.map} +1 -1
  55. package/dist/{provision-tenant-DoWNQJvY.mjs → provision-tenant-DEscl-jo.mjs} +36 -12
  56. package/dist/{provision-tenant-DoWNQJvY.mjs.map → provision-tenant-DEscl-jo.mjs.map} +1 -1
  57. package/dist/{r2S3EmptyBucket-DD81ZWQ7.mjs → r2S3EmptyBucket-CDE2lTM8.mjs} +1 -1
  58. package/dist/{r2S3EmptyBucket-DD81ZWQ7.mjs.map → r2S3EmptyBucket-CDE2lTM8.mjs.map} +1 -1
  59. package/dist/{registry-CTerXUza.mjs → registry-CRgobiU4.mjs} +4 -4
  60. package/dist/{registry-CTerXUza.mjs.map → registry-CRgobiU4.mjs.map} +1 -1
  61. package/dist/{resolveTenantBindings-86AmRQcW.mjs → resolveTenantBindings-CXLwAG0F.mjs} +2 -2
  62. package/dist/{resolveTenantBindings-86AmRQcW.mjs.map → resolveTenantBindings-CXLwAG0F.mjs.map} +1 -1
  63. package/dist/secretsDb-CMEcKvSi.mjs +3 -0
  64. package/dist/{stackOutputs-BLp-dyzl.mjs → stackOutputs-BMPahRAl.mjs} +2 -2
  65. package/dist/{stackOutputs-BLp-dyzl.mjs.map → stackOutputs-BMPahRAl.mjs.map} +1 -1
  66. package/dist/{status-CL4MEGbn.mjs → status-CQpgTTeG.mjs} +7 -7
  67. package/dist/{status-CL4MEGbn.mjs.map → status-CQpgTTeG.mjs.map} +1 -1
  68. package/dist/sync-Blz5emYO.mjs +7 -0
  69. package/dist/{sync-Dii9n2nJ.mjs → sync-DqMMQ7mQ.mjs} +6 -6
  70. package/dist/{sync-Dii9n2nJ.mjs.map → sync-DqMMQ7mQ.mjs.map} +1 -1
  71. package/dist/tamer.mjs +52 -27
  72. package/dist/tamer.mjs.map +1 -1
  73. package/dist/{tamerArtifactsR2-B3X21TGV.mjs → tamerArtifactsR2-BePICoT8.mjs} +2 -2
  74. package/dist/{tamerArtifactsR2-B3X21TGV.mjs.map → tamerArtifactsR2-BePICoT8.mjs.map} +1 -1
  75. package/dist/{tenant-MWIs0esz.mjs → tenant-2XTWe9j0.mjs} +2 -2
  76. package/dist/{tenant-MWIs0esz.mjs.map → tenant-2XTWe9j0.mjs.map} +1 -1
  77. package/dist/{tenant-migrate-CM6cb1zo.mjs → tenant-migrate-B-n4ppSk.mjs} +5 -5
  78. package/dist/{tenant-migrate-CM6cb1zo.mjs.map → tenant-migrate-B-n4ppSk.mjs.map} +1 -1
  79. package/dist/{types-DCL0mEjS.mjs → types-BCTgIotG.mjs} +6 -6
  80. package/dist/{types-DCL0mEjS.mjs.map → types-BCTgIotG.mjs.map} +1 -1
  81. package/dist/{verifyPlanFile-B9VCcFIJ.mjs → verifyPlanFile-DrFZjkVY.mjs} +2 -2
  82. package/dist/{verifyPlanFile-B9VCcFIJ.mjs.map → verifyPlanFile-DrFZjkVY.mjs.map} +1 -1
  83. package/dist/{wfp-delete-CwWQFxxj.mjs → wfp-delete-BblKos51.mjs} +2 -2
  84. package/dist/{wfp-delete-CwWQFxxj.mjs.map → wfp-delete-BblKos51.mjs.map} +1 -1
  85. package/dist/{wfp-put-Bl5K3snx.mjs → wfp-put-oYvLiM_c.mjs} +2 -2
  86. package/dist/{wfp-put-Bl5K3snx.mjs.map → wfp-put-oYvLiM_c.mjs.map} +1 -1
  87. package/dist/{worker-route-BapxsQyX.mjs → worker-route-BPDAwUL0.mjs} +3 -3
  88. package/dist/{worker-route-BapxsQyX.mjs.map → worker-route-BPDAwUL0.mjs.map} +1 -1
  89. package/dist/{workers-D7ow_joN.mjs → workers-D08-ri2Q.mjs} +2 -2
  90. package/dist/{workers-D7ow_joN.mjs.map → workers-D08-ri2Q.mjs.map} +1 -1
  91. package/dist/wranglerOutFile-eViugwTw.mjs +3 -0
  92. package/dist/wranglerSpawn-BUOsdo5R.mjs +3 -0
  93. package/dist/{wranglerSpawn-VkSL0gZd.mjs → wranglerSpawn-Dg-lr-rf.mjs} +1 -1
  94. package/dist/{wranglerSpawn-VkSL0gZd.mjs.map → wranglerSpawn-Dg-lr-rf.mjs.map} +1 -1
  95. package/dist/{zoneResolver-CamXJpSB.mjs → zoneResolver-Dfp2glm8.mjs} +1 -1
  96. package/dist/{zoneResolver-CamXJpSB.mjs.map → zoneResolver-Dfp2glm8.mjs.map} +1 -1
  97. package/package.json +1 -1
  98. package/dist/drift-oOUlh0u8.mjs +0 -8
  99. package/dist/loader-B5iVsP6t.mjs +0 -3
  100. package/dist/sync-DSgJGQh1.mjs +0 -7
  101. package/dist/wranglerOutFile-DDFKeKwO.mjs +0 -3
  102. package/dist/wranglerSpawn-CfPkFLP3.mjs +0 -3
  103. /package/dist/{secrets-DJ1yUy01.mjs → secrets-DVNZwGUS.mjs} +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"normalize-DVSTRZhO.mjs","names":["spec: CfBindingSpec","out: Record<string, unknown>"],"sources":["../src/types.ts","../src/dx/cloudflare-bindings.ts","../src/dx/normalize.ts"],"sourcesContent":["import type { CfBinding } from \"./dx/cloudflare-bindings.js\";\nimport type {\n WranglerConfig,\n WranglerR2Bucket,\n} from \"./generated/wrangler-types.js\";\n\nexport type { WranglerConfig };\n\n/**\n * Worker `vars`, `outputs`, and similar fields accept plain strings or\n * {@link CfBinding} values; `loadConfig` materializes bindings to `${tamer:…}`\n * before validation.\n */\nexport type TamerResolvableString = string | CfBinding;\n\n/** Context for per-resource Cloudflare name resolution (e.g. sharded D1 shard date). */\nexport interface CloudflareNameContext {\n /** ISO `YYYY-MM-DD` shard stamp for sharded D1. */\n shardDate?: string;\n}\n\n/**\n * Per-resource Cloudflare name for the current env.\n * No UUIDs — name only. Used for sync, plan, apply, drift, import, and generate.\n *\n * Resolution order when unset: stack {@link NamingConventions} hook → {@link NamingEngine} default.\n */\nexport type CloudflareNameFn = (\n tenantId: string,\n env: string,\n ctx?: CloudflareNameContext,\n) => string;\n\n/** Wrangler R2 binding with optional {@link TamerResolvableString} `bucket_name`. */\nexport type WranglerR2BucketResolvable = Omit<\n WranglerR2Bucket,\n \"bucket_name\"\n> & {\n bucket_name?: TamerResolvableString;\n};\n\n// ── Tenant ───────────────────────────────────────────────────────────────────\n\n/**\n * Stack identity in `defineConfig({ tenant: … })`. `id` and `slug` feed\n * default resource names and state keys; `slug` is also the default\n * {@link CfiStackConfig.name} when `stack.name` is omitted.\n */\nexport interface TenantMeta {\n id: string;\n name: string;\n slug: string;\n /**\n * Optional per-stack tenant D1 shard layout. Each entry is a free-form\n * role name (lowercase letters, digits, `_`, `-`) that becomes part of\n * the per-tenant D1 database name `db_{role}_{w}_{p}_t_{tid}_{env}`\n * when `tamer provision-tenant` runs.\n *\n * Tamer is opinion-free about how many shards a product wants and what\n * they're called — a Dragoncore-style product might use `[\"system\",\n * \"app\", \"history\"]`, a single-DB tenant `[\"main\"]`, a billing-style\n * product `[\"billing\", \"content\"]`, or omit `d1Shards` entirely (in\n * which case `provision-tenant` only uploads the dispatch script — no\n * per-tenant D1 fan-out).\n *\n * The order here is **canonical provisioning order**: shards are\n * created left-to-right and listed in plan/status output in the same\n * order, so a partial failure leaves a deterministic recoverable\n * state. `--shards <subset>` on the CLI must be a subset of this list\n * — the config is the source of truth, the flag only trims.\n */\n d1Shards?: string[];\n /**\n * Maps each shard role (from `d1Shards[]`) to its binding name and\n * migration directory. Used by `wfp tenant provision` to resolve D1\n * bindings on the dispatch script, and by `wfp tenant migrate` to find\n * the migration source per shard.\n *\n * Example:\n * ```ts\n * shardBindings: {\n * system: { binding: \"DB_SYSTEM\", migrationsDir: \"db/system\" },\n * app: { binding: \"DB_APP\", migrationsDir: \"db/app\" },\n * history: { binding: \"DB_HISTORY\", migrationsDir: \"db/history\" },\n * }\n * ```\n */\n shardBindings?: Record<string, {\n binding: string;\n migrationsDir: string;\n migrationsTable?: string;\n }>;\n /**\n * Envs that require an explicit `--confirm-tenant <workspace>` (or\n * `--force`) before `destroy-tenant` will run. Defaults to\n * `[\"prod\", \"production\"]`. Add e.g. `\"production-eu\"`,\n * `\"production-us\"`, `\"qa\"`, `\"uat\"`, `\"canary\"` here for any env\n * whose accidental teardown would be a real-world outage.\n *\n * `local` is never protected (it's a wrangler-dev concept, not a\n * deployed env). Override with `[]` to disable the prompt entirely\n * (only sensible for personal accounts).\n */\n protectedEnvs?: string[];\n /**\n * Optional regex (passed as a string) that decides which env names\n * are \"ephemeral\" — i.e. share **one** dispatch namespace\n * (`{ns}-ephemeral`) instead of getting their own (`{ns}-{env}`),\n * include the env in their dispatch-script name so multiple\n * ephemeral previews can coexist, and get a `BRANCH_SUFFIX`\n * environment variable injected at resolve time.\n *\n * Examples: `\"^pr-\"` (PR previews), `\"^(pr|feature|branch)-\"`,\n * `\"^canary-\"`. When omitted (the default), no env is ephemeral —\n * every env owns its own dispatch namespace. Compiled once at\n * config-load time; an invalid regex fails at parse, not at apply.\n */\n ephemeralEnvPattern?: string;\n /**\n * Environment variables injected into the tenant dispatch script's\n * metadata on `wfp tenant provision`. Values may contain\n * `${tamer:...}` references (resolved against state at provision time).\n * D1 bindings are derived automatically from `resources.d1[].registryRole`\n * matched to `d1Shards[]` — only non-D1 vars go here.\n */\n dispatchVars?: Record<string, TamerResolvableString>;\n /**\n * Service bindings injected into the tenant dispatch script's metadata.\n * Each entry produces `env.BINDING` on the tenant Worker pointing at\n * the named service (e.g. the portal-api Worker). `service` may contain\n * `${tamer:...}` references.\n */\n dispatchServices?: Array<{\n name: string;\n service: TamerResolvableString;\n environment?: string;\n }>;\n}\n\n// ── Resources ────────────────────────────────────────────────────────────────\n\n/**\n * D1 database declared on a worker's `resources.d1[]`.\n * Cloudflare **name** for managed databases follows\n * {@link D1ResourceConfig.cloudflareName} → {@link NamingConventions.d1Single} /\n * {@link NamingConventions.d1Shard} → defaults; **id** is stored in state after `sync` / `apply`.\n */\nexport interface D1ResourceConfig {\n logicalName: string;\n /** `\"single\"`: one DB per logical name. `\"sharded\"`: date-stamped shards (see `d1Shard` naming hook). */\n type: \"single\" | \"sharded\";\n /**\n * Optional per-resource Cloudflare database name. For sharded D1, receives\n * {@link CloudflareNameContext.shardDate}. External D1 uses {@link databaseName} instead.\n */\n cloudflareName?: CloudflareNameFn;\n /**\n * `managed` (default): Tamer creates the database; the Cloudflare name follows\n * `naming.d1Single` / shard rules.\n *\n * `external`: owned by another stack. Requires {@link databaseName} resolving\n * to the live D1 name (e.g. `${tamer:import:platform.platform_db_name}`). Skips\n * create, migrate, and destroy on Cloudflare for this binding.\n */\n ownership?: \"managed\" | \"external\";\n /**\n * Required when `ownership` is `external`. May contain `${tamer:import:…}`; must\n * be resolved (via mergeWorkerConfigWithResolvedRefs / resolveWorkerConfig)\n * before apply / sync / wrangler generation.\n */\n databaseName?: TamerResolvableString;\n /**\n * When set, used as the Wrangler D1 `binding` instead of the generated name.\n * Applies to `type: \"single\"` and `type: \"sharded\"`.\n */\n binding?: string;\n /**\n * Pin the shard date for `type: \"sharded\"` (ISO `YYYY-MM-DD` or `YYYYMMDD`).\n * Brownfield: match one physical shard during sync/apply. Greenfield: first shard date.\n */\n shardDate?: string;\n /**\n * Role label in the generated shard registry (defaults to {@link logicalName}).\n */\n registryRole?: string;\n /** Prior shard id strings that decode to this shard (brownfield aliases). */\n legacyIds?: string[];\n /** Prior wrangler binding keys for this shard (dual-bind / rename history). */\n legacyBindings?: string[];\n /** When false, shard is read-only for new writes (multi-shard same role). Default true. */\n current?: boolean;\n migrationsDir?: string;\n migrationsTable?: string;\n /**\n * When true, `tamer destroy` will not delete this database (e.g. created by\n * another stack but bound read-only here). Default false.\n *\n * Legacy cross-stack mode without {@link ownership} `external`: same Cloudflare\n * name as the owning stack ({@link NamingConventions.d1Single} + logicalName).\n * Prefer `ownership: \"external\"` and a sibling-stack output for the name.\n */\n preserveOnDestroy?: boolean;\n}\n\n/** One row in {@link ShardRegistryV1}. */\nexport interface ShardRegistryEntryV1 {\n /** Logical shard id embedded in universal IDs (brownfield: CF database name). */\n id: string;\n /** App vocabulary — typically the D1 `logicalName` or {@link D1ResourceConfig.registryRole}. */\n role: string;\n /** Current Wrangler D1 binding for this env. */\n binding: string;\n /** Physical Cloudflare D1 database name for this env. */\n databaseName: string;\n shardDate?: string;\n /** When multiple shards share a role, marks the write target. Default true. */\n current?: boolean;\n legacyBindings?: string[];\n legacyIds?: string[];\n}\n\n/** Versioned shard registry emitted by `tamer apply` when `codegen.shardRegistry` is set. */\nexport interface ShardRegistryV1 {\n version: 1;\n shards: ShardRegistryEntryV1[];\n}\n\n/** Options for {@link CodegenConfig.shardRegistry}. */\nexport interface ShardRegistryCodegenConfig {\n /**\n * Worker key that receives the generated module. Required when the stack\n * declares `workers` with more than one entry.\n */\n worker?: string;\n /** Path relative to the worker directory. Default: `src/db/shard-registry.ts`. */\n outFile?: string;\n /** Exported constant name. Default: `shardRegistry`. */\n exportName?: string;\n}\n\nexport interface CodegenConfig {\n /** Emit a {@link ShardRegistryV1} module after wrangler generation on `apply`. */\n shardRegistry?: ShardRegistryCodegenConfig;\n}\n\n/** R2 bucket on `resources.r2[]`. Name from {@link cloudflareName} → {@link NamingConventions.r2Bucket} → default. */\nexport interface R2ResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare bucket name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler R2 `binding` instead of the generated stable name. */\n binding?: string;\n}\n\nexport interface KVResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare KV namespace name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler KV `binding` instead of the generated name. */\n binding?: string;\n}\n\n/**\n * Cloudflare Queue (producer binding) managed by Tamer.\n *\n * Tamer creates the queue itself on `apply` and emits a `queues.producers[]`\n * binding for the worker. Consumer subscriptions are wrangler-side only today\n * (set them via `queues.consumers` on the worker config).\n */\nexport interface QueueResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare queue name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler queue `binding` instead of the generated name. */\n binding?: string;\n /**\n * When true, this entry is consumer-only (no producer binding). The queue is\n * still provisioned by Tamer if absent. Default false (producer binding emitted).\n */\n consumerOnly?: boolean;\n}\n\n/**\n * Cloudflare Hyperdrive config managed by Tamer.\n *\n * Tamer creates the Hyperdrive config on `apply` (the origin connection\n * string is sent to the API once, never persisted in state) and emits a\n * `hyperdrive[]` binding for the worker.\n */\nexport interface HyperdriveResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Hyperdrive config name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler hyperdrive `binding` instead of the generated name. */\n binding?: string;\n /** Origin database connection. Sent to Cloudflare on create; not stored in Tamer state. */\n origin: HyperdriveOriginSpec;\n /** Optional caching tweaks passed to Cloudflare. */\n caching?: {\n disabled?: boolean;\n max_age?: number;\n stale_while_revalidate?: number;\n };\n /** Optional `mtls` block passed to Cloudflare. */\n mtls?: { ca_certificate_id?: string; mtls_certificate_id?: string };\n /** `wrangler dev`-time connection string written to the generated config. */\n localConnectionString?: string;\n}\n\n/**\n * Cloudflare Vectorize index managed by Tamer.\n *\n * Tamer creates the index on `apply` (the v2 storage subsystem; legacy v1 is\n * unsupported) and emits a `vectorize[]` binding for the worker. Index\n * configuration (`dimensions`, `metric`) is immutable per Cloudflare's API,\n * so changes after creation are rejected — drop and recreate via `tamer\n * destroy --resource <logicalName>` then `tamer apply`.\n */\nexport interface VectorizeResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Vectorize index name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler vectorize `binding` instead of the generated name. */\n binding?: string;\n /** Vector dimensionality (e.g. 768 for `@cf/baai/bge-base-en-v1.5`). Immutable. */\n dimensions: number;\n /** Distance metric. Immutable after creation. */\n metric: \"cosine\" | \"euclidean\" | \"dot-product\";\n /** Free-form description sent to the Vectorize API on create. */\n description?: string;\n}\n\n/**\n * Cloudflare AI Gateway managed by Tamer.\n *\n * Tamer creates the gateway on `apply` against `/accounts/{id}/ai-gateway/\n * gateways`. AI Gateways have no Wrangler binding kind — Workers reference\n * them per-request via `env.AI.run(model, opts, { gateway: { id } })` (or\n * the OpenAI-compatible endpoint URL). Use cross-resource refs like\n * `${tamer:ai_gateway:my_gw.name}` in worker `vars` to inject the derived\n * gateway slug at deploy time.\n */\nexport interface AIGatewayResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare AI Gateway slug override. */\n cloudflareName?: CloudflareNameFn;\n /** Cache TTL in seconds. Default 0 (caching disabled). */\n cacheTtl?: number;\n /** Invalidate cached entries when upstream model output changes. Default false. */\n cacheInvalidateOnUpdate?: boolean;\n /** Persist request/response logs in the AI Gateway dashboard. Default true. */\n collectLogs?: boolean;\n /** Require an `Authorization: Bearer <token>` header on all gateway requests. Default false. */\n authentication?: boolean;\n /** Rate-limit window in seconds. Default 0 (rate limiting disabled). */\n rateLimitingInterval?: number;\n /** Max requests per window. Default 0 (rate limiting disabled). */\n rateLimitingLimit?: number;\n /** \"fixed\" or \"sliding\" window. Default \"fixed\". */\n rateLimitingTechnique?: \"fixed\" | \"sliding\";\n}\n\n/**\n * Cloudflare Pipeline (V1, SQL-based) managed by Tamer.\n *\n * Tamer creates the pipeline on `apply` against\n * `/accounts/{id}/pipelines/v1/pipelines` with the user-supplied derived\n * name and SQL. The server returns a unique `id` that Tamer stores in\n * state and emits as `pipelines[].pipeline` in generated wrangler config.\n *\n * Pipelines reference streams (sources) and sinks (destinations) by name\n * inside the SQL — those upstream/downstream resources are **not** managed\n * by Tamer in this iteration. Create them via the Cloudflare dashboard or\n * Wrangler before applying, otherwise the pipeline will exist in a\n * non-running status until they are. Drift / status surface this as the\n * pipeline's `status` field.\n */\nexport interface PipelineResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Pipeline name override. */\n cloudflareName?: CloudflareNameFn;\n /**\n * Arroyo SQL describing the processing flow, e.g.\n * `insert into my_sink select * from my_stream;`. Stream and sink names\n * must already exist on Cloudflare (see {@link PipelineResourceConfig}).\n */\n sql: string;\n /**\n * Override the wrangler binding key. Defaults to\n * `PIPE_{LOGICAL}_T_{TENANT}` — uppercased logical with the tenant id\n * appended for cross-stack uniqueness.\n */\n binding?: string;\n}\n\n/** Origin connection for Hyperdrive (postgres / mysql). */\nexport interface HyperdriveOriginSpec {\n scheme: \"postgres\" | \"postgresql\" | \"mysql\";\n host: string;\n port?: number;\n database: string;\n user: string;\n /**\n * Origin password. Either an inline string OR `{ fromEnv: \"VAR\" }` to read\n * from `process.env` at apply time (recommended). Never persisted in state.\n */\n password: string | { fromEnv: string };\n /** Optional access client id (Cloudflare Access protected origins). */\n access_client_id?: string;\n access_client_secret?: string | { fromEnv: string };\n}\n\n/**\n * Declares a Cloudflare Workflow registration for a worker. The workflow\n * **class** itself lives in the worker's source code (extends\n * `WorkflowEntrypoint`); this config binds that class to a stable\n * Cloudflare-side workflow name and emits the `workflows[]` wrangler\n * binding so the worker can `env.{BINDING}.create(...)` instances.\n *\n * Tamer issues `PUT /accounts/{id}/workflows/{name}` on apply (idempotent;\n * Cloudflare treats PUT as upsert with `class_name` + `script_name`) and\n * `DELETE` on destroy. `script_name` defaults to the owning worker's\n * deployed name for `env`; override with {@link scriptName} when the class\n * is implemented in a sibling worker.\n */\nexport interface WorkflowResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare workflow registration name override. */\n cloudflareName?: CloudflareNameFn;\n /**\n * Class name exported by the worker that implements this workflow\n * (`export class BillingWorkflow extends WorkflowEntrypoint {...}` →\n * `className: \"BillingWorkflow\"`). Required by Cloudflare so it knows\n * which class in the bound script to instantiate per workflow run.\n */\n className: string;\n /**\n * Override the script that hosts the workflow class. Defaults to the\n * deployed name of the worker this resource is declared on (so dropping\n * the workflow into a different worker only needs moving the entry).\n * When set, written verbatim into the wrangler binding and the upsert\n * payload — Tamer does not env-suffix it.\n */\n scriptName?: string;\n /**\n * Override the wrangler binding key. Defaults to\n * `WF_{LOGICAL}_T_{TENANT}` — uppercased logical with the tenant id\n * appended for cross-stack uniqueness.\n */\n binding?: string;\n /**\n * Optional per-workflow execution limits. Mirrors Cloudflare's\n * `limits.steps` (max number of `step.do` calls per instance).\n */\n limits?: { steps?: number };\n}\n\n/**\n * Tamer-managed Cloudflare DNS record (zone-scoped).\n *\n * Declared at the **stack root** (`CfiConfigBase.dnsRecords`) rather than\n * per worker, because zone records have global identity (`zone + type +\n * name + content`) and shouldn't be redeclared per-worker. Tamer:\n *\n * - On `apply` for a new entry: `POST /zones/{zone_id}/dns_records` and\n * stores the assigned `recordId`.\n * - On `apply` for an existing entry whose mutable fields drifted from\n * state (`content`, `ttl`, `proxied`, `priority`, `comment`):\n * `PATCH /zones/{zone_id}/dns_records/{record_id}` (Cloudflare's\n * in-place update — works for everything except `type`/`name`).\n * - On `apply` for a `type` change: delete + recreate, since Cloudflare\n * rejects type changes on PATCH (and `name`-only changes are usually\n * semantic deletes anyway).\n * - On `destroy` of the stack (or when the entry is removed from config):\n * `DELETE /zones/{zone_id}/dns_records/{record_id}` and drops the row\n * from state.\n *\n * Tamer attaches a stable comment marker\n * (`tamer:<tenantId>:<env>:<logicalName>`) to every record it creates so\n * `tamer sync` and `tamer import` can rediscover orphaned rows after a\n * state loss.\n */\n/** Declared dataset for account Logpush — only `workers_trace_events` is implemented. */\nexport type LogpushWorkersTraceDataset = \"workers_trace_events\";\n\n/**\n * R2 destination for a Workers trace Logpush job. Requires S3-compatible\n * credentials (R2 API token) via the named environment variables at apply time.\n */\nexport interface LogpushJobR2Destination {\n /** Logical name of a Tamer-managed {@link R2ResourceConfig} in this stack. */\n bucketLogicalName: string;\n /** Path prefix inside the bucket before `{DATE}`; default `workers-trace-events`. */\n pathPrefix?: string;\n /** `process.env[name]` — R2 access key id for Logpush. */\n accessKeyIdEnv: string;\n /** `process.env[name]` — R2 secret access key for Logpush. */\n secretAccessKeyEnv: string;\n}\n\n/**\n * Logpush destination for **Pipelines HTTP ingest** (Workers trace → stream →\n * pipeline → sink). Tamer builds `destination_conf` as:\n * `https://{streamId}.ingest.cloudflare.com?pipeline_id={pipelineId}&header_Authorization=Bearer%20{token}`.\n *\n * The **stream**, **pipeline**, and **sink** must already exist (dashboard\n * wizard or Pipelines API). This block only encodes the Logpush wire format;\n * it does not create Pipelines resources.\n */\nexport interface LogpushJobPipelinesIngestDestination {\n /**\n * Pipelines **stream** id (32 hex characters, with or without UUID dashes).\n * Must match the stream that backs `https://…ingest.cloudflare.com`.\n */\n streamId: string;\n /** Pipelines **pipeline** id (32 hex characters, with or without dashes). */\n pipelineId: string;\n /**\n * `process.env[name]` — **ingest** Bearer token (dashboard “send token” /\n * stream HTTP auth). Becomes Logpush query param `header_Authorization`.\n */\n bearerTokenEnv: string;\n}\n\n/**\n * Fully automated **Workers trace → Pipelines → R2 Data Catalog (Iceberg)** path:\n * `tamer apply` creates the stream (trace schema), enables the catalog on the\n * bucket, mints two **account** API tokens (R2+Data Catalog + Pipelines Send),\n * stores the catalog credential, creates the `r2_data_catalog` sink, the SQL\n * pipeline, then the Logpush job. Mutually exclusive with\n * {@link LogpushJobPipelinesIngestDestination} and the other hand-supplied\n * `destination_conf` options.\n */\nexport interface LogpushJobPipelinesAutoDestination {\n /**\n * Tamer `resources.r2` logical name for the **catalog** bucket used as an\n * Apache Iceberg / **R2 Data Catalog** warehouse (not a separate R2 object\n * bucket used for raw NDJSON Logpush). That bucket must have **R2 Data Catalog** enabled. With\n * `pipelinesAuto`, Tamer calls `POST …/r2-catalog/{bucket}/enable` and\n * `…/credential` for you; if you provision the graph manually instead, enable\n * the catalog on this bucket in the R2 dashboard (or\n * `wrangler r2 bucket catalog enable`) before the `r2_data_catalog` sink can\n * work.\n */\n catalogBucketLogicalName: string;\n /** R2 Data Catalog / Iceberg namespace (default `default`). */\n namespace?: string;\n /**\n * Base Iceberg `table_name` in the catalog. On **new** `r2_data_catalog` sink\n * create, Tamer appends `_${Date.now()}` by default so the create path does\n * not collide (HTTP 422 / 1012) with a leftover table. Set\n * {@link tableNameAppendTimestamp} to `false` to use this string verbatim.\n */\n tableName: string;\n /**\n * When not `false`, new sinks use `${tableName}_${Date.now()}` (default). When\n * `false`, the sink uses `tableName` as-is. Existing sinks are unchanged.\n */\n tableNameAppendTimestamp?: boolean;\n /** Parquet `row_group_bytes` for the sink. Default 134217728. */\n sinkRowGroupBytes?: number;\n /** `rolling_policy.file_size_bytes` for the sink. Default 104857600. */\n sinkRollingFileSizeBytes?: number;\n /** `rolling_policy.interval_seconds` for the sink. Default 300. */\n sinkRollingIntervalSeconds?: number;\n}\n\nexport interface LogpushJobResourceConfig {\n logicalName: string;\n dataset: LogpushWorkersTraceDataset;\n /**\n * Cloudflare Logpush job `name` field. Defaults to\n * `tamer-{tenant.slug}-{logicalName}-{env}`.\n */\n jobName?: string;\n /** Build `destination_conf` for an R2 bucket in this stack. */\n r2?: LogpushJobR2Destination;\n /**\n * Build Logpush `destination_conf` for **Pipelines stream ingest** from stream\n * id, pipeline id, and ingest token (see {@link LogpushJobPipelinesIngestDestination}).\n * Mutually exclusive with {@link r2}, {@link destinationConfEnv},\n * {@link destinationConfFromJobId}, and {@link destinationConfFromJobIdEnv}.\n */\n pipelinesIngest?: LogpushJobPipelinesIngestDestination;\n /**\n * Create stream, R2 Data Catalog sink, and SQL pipeline via API, then Logpush\n * (see {@link LogpushJobPipelinesAutoDestination}).\n */\n pipelinesAuto?: LogpushJobPipelinesAutoDestination;\n /**\n * `process.env[name]` must hold the full Logpush `destination_conf` string.\n * Use for destinations where you paste the exact API value (escape hatch if\n * Cloudflare changes the Pipelines ingest URL shape).\n * Mutually exclusive with {@link r2}, {@link pipelinesIngest}, {@link pipelinesAuto},\n * {@link destinationConfFromJobId}, and {@link destinationConfFromJobIdEnv}.\n */\n destinationConfEnv?: string;\n /**\n * Bootstrap **Pipelines** (or any) `destination_conf` by reading an existing\n * account Logpush job via `GET …/logpush/jobs/{id}` — useful after creating a\n * template job in the dashboard. Tamer creates the managed job (canonical\n * name) with the same `destination_conf`. Mutually exclusive with {@link r2},\n * {@link pipelinesIngest}, {@link pipelinesAuto}, {@link destinationConfEnv}, and {@link destinationConfFromJobIdEnv}.\n */\n destinationConfFromJobId?: number;\n /**\n * `process.env[name]` must hold a positive integer job id; same behavior as\n * {@link destinationConfFromJobId} without hard-coding id in config.\n * Mutually exclusive with {@link r2}, {@link pipelinesIngest}, {@link pipelinesAuto},\n * {@link destinationConfEnv}, and {@link destinationConfFromJobId}.\n */\n destinationConfFromJobIdEnv?: string;\n /**\n * Optional Logpush **`filter`** (escaped JSON string with a top-level `where`\n * key). See [Logpush filters](https://developers.cloudflare.com/logs/logpush/logpush-job/filters/).\n * When using {@link destinationConfFromJobId} / {@link destinationConfFromJobIdEnv},\n * if unset, Tamer copies `filter` from the source job when present.\n */\n filter?: string;\n /**\n * Overrides **`output_options.field_names`** on the Logpush job. When\n * bootstrapping from {@link destinationConfFromJobId} / {@link destinationConfFromJobIdEnv},\n * other template **`output_options`** (e.g. `sample_rate`) are preserved unless\n * you replace them out-of-band on Cloudflare.\n */\n fieldNames?: string[];\n /** Default true. */\n enabled?: boolean;\n}\n\nexport interface DnsRecordResourceConfig {\n /** Stable identifier inside this stack (used in state keys and the comment marker). */\n logicalName: string;\n /** Cloudflare zone id this record lives in (e.g. `0123456789abcdef`). */\n zoneId: string;\n /**\n * Record type. Tamer supports the common edge / app types out of the\n * box. Anything else can still be expressed via the wrangler-side\n * config, but won't have lifecycle management here.\n */\n type:\n | \"A\"\n | \"AAAA\"\n | \"CNAME\"\n | \"TXT\"\n | \"MX\"\n | \"NS\"\n | \"CAA\"\n | \"SRV\"\n | \"PTR\"\n | \"HTTPS\"\n | \"SVCB\";\n /** DNS name (e.g. `app.example.com` or `@` for the apex). */\n name: string;\n /**\n * Record content (RDATA). Format depends on `type`:\n * - A → IPv4 address (`192.0.2.1`)\n * - AAAA → IPv6 address (`2001:db8::1`)\n * - CNAME / NS / PTR → hostname (`origin.example.com`)\n * - TXT → quoted text body (Tamer does not auto-quote — pass the\n * exact value Cloudflare should serve, e.g. `v=spf1 -all`)\n * - MX → mail server hostname (use {@link priority} for the preference)\n * - CAA → flags + tag + value as a single string\n * (`0 issue \"letsencrypt.org\"`)\n * - SRV → `priority weight port target` (e.g. `10 5 5223 server.example.com`)\n */\n content: string;\n /**\n * TTL in seconds. `1` means \"Auto\" (the Cloudflare default). Defaults to\n * `1` when unset.\n */\n ttl?: number;\n /**\n * Whether the record is proxied through Cloudflare (orange cloud).\n * Only meaningful for `A`, `AAAA`, and `CNAME`. Defaults to `false`.\n */\n proxied?: boolean;\n /**\n * Required for `MX`, `SRV`, and `URI` records. Ignored for other types.\n */\n priority?: number;\n /**\n * Free-form user comment appended to Tamer's attribution comment. Useful\n * for runbooks / \"why is this here\". Tamer always prefixes the comment\n * with `tamer:<tenantId>:<env>:<logicalName>` so live records can be\n * matched back to config even after state loss.\n */\n comment?: string;\n /**\n * Skip these envs (e.g. only create the record in `prod`). Default `[]`.\n * `local` is always implicitly skipped — DNS is a real-world side effect.\n */\n skipEnvs?: string[];\n /**\n * When true, `tamer destroy` does not delete this record (e.g. an apex\n * NS that should outlive the stack). Default false.\n */\n preserveOnDestroy?: boolean;\n}\n\n/** Declares a Workers for Platforms dispatch namespace to provision via `tamer apply`. */\nexport interface DispatchNamespaceResourceConfig {\n logicalName: string;\n /** Cloudflare dispatch namespace name (e.g. `workspace-workers`). */\n namespace: string;\n /**\n * When true, the real namespace name is `${namespace}-${env}` for non-`local`\n * envs. Skipped entirely for `local`.\n */\n envSuffix?: boolean;\n}\n\nexport interface WorkerResources {\n d1?: D1ResourceConfig[];\n r2?: R2ResourceConfig[];\n kv?: KVResourceConfig[];\n queues?: QueueResourceConfig[];\n hyperdrive?: HyperdriveResourceConfig[];\n vectorize?: VectorizeResourceConfig[];\n aiGateway?: AIGatewayResourceConfig[];\n pipelines?: PipelineResourceConfig[];\n workflows?: WorkflowResourceConfig[];\n /**\n * Cloudflare Secrets Store **stores** (account-scoped containers) managed by\n * Tamer. The secret values inside a store are intentionally **not** managed\n * here — they are written via Wrangler / dashboard / CI so secret material\n * never enters `tamer.config.ts` or `tamer-state-*`. Wire the actual\n * bindings via {@link secretsStoreSecrets} (which references stores by\n * logical name and resolves `store_id` from state at deploy time).\n */\n secretsStores?: SecretsStoreResourceConfig[];\n /**\n * Wrangler `secrets_store_secrets[]` bindings. **Not** Tamer-managed (no\n * state row, no apply/destroy lifecycle) — these are pure deploy-time\n * config that resolves the store reference (`store: <logicalName>`) into a\n * concrete `store_id` from a {@link SecretsStoreStateEntry} so wrangler\n * receives a stable id per env. The named secret must already exist in the\n * store (created out-of-band via wrangler `secrets-store secret create`).\n */\n secretsStoreSecrets?: SecretsStoreSecretBinding[];\n}\n\n/**\n * Cloudflare Secrets Store store managed by Tamer.\n *\n * Tamer creates the store on `apply` against\n * `POST /accounts/{id}/secrets_store/stores` (idempotent — a matching name\n * found via list short-circuits) and tracks the assigned `store_id` in\n * state. Cross-resource refs (`${tamer:secret_store:<n>.id|name}`) inject\n * the id into worker `vars` or other bindings at deploy time.\n *\n * Secret **values** are deliberately out of scope (rotation belongs in CI\n * / wrangler), so this resource has no `value` / `fromEnv` field — see\n * {@link SecretsStoreSecretBinding} for how to wire a secret into a worker.\n */\nexport interface SecretsStoreResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Secrets Store name override. */\n cloudflareName?: CloudflareNameFn;\n}\n\n/**\n * One row in the worker's wrangler `secrets_store_secrets[]` array. Pure\n * deploy-time config — Tamer resolves `store` (a {@link\n * SecretsStoreResourceConfig.logicalName}) to the recorded `store_id` and\n * passes `secret_name` through verbatim. The secret itself must already\n * exist in the referenced store.\n */\nexport interface SecretsStoreSecretBinding {\n /** Wrangler binding name (e.g. `API_KEY`). Available on `env.API_KEY` at runtime. */\n binding: string;\n /** Logical name of a {@link SecretsStoreResourceConfig} declared on this worker. */\n store: string;\n /** Name of the secret inside the referenced store (created out-of-band). */\n secretName: string;\n}\n\n/**\n * Declarative HTTP route managed by Tamer.\n *\n * Per `docs/handoff.md` §6, hostnames are env-asymmetric: prod uses the bare\n * apex (`todo.com`, `admin.platform.com`); every other env prefixes\n * (`staging.todo.com`, `dev.todo.com`, `pr-1234.todo.com`). Resource names\n * remain `-{env}` suffixed regardless.\n *\n * Tamer expands one `RouteResourceConfig` into a wrangler `Route` per env at\n * `resolveWorkerConfig` time. `local` (and any env in `skipEnvs`) yields no\n * route so `wrangler dev` is unaffected.\n *\n * For routes wrangler should pass through verbatim, set `routes` on the\n * worker (inherited from `WranglerConfig`) instead of `tamerRoutes`.\n */\nexport interface RouteResourceConfig {\n /**\n * Apex hostname. Prod uses this bare; other envs prefix it with `{env}.`.\n */\n host: string;\n /**\n * Cloudflare zone name for wrangler's `ZoneNameRoute`. Defaults to {@link host}.\n * Set explicitly when the apex differs from the zone (e.g. multi-tenant\n * subdomain on a parent zone).\n */\n zone?: string;\n /**\n * Path glob appended after the resolved host. Default `/*`.\n */\n path?: string;\n /**\n * When true, register as a Cloudflare `custom_domain` route (wrangler's\n * `CustomDomainRoute`). The path glob is ignored for custom domains.\n */\n customDomain?: boolean;\n /**\n * Envs that should yield no route. Defaults to `[\"local\"]`.\n */\n skipEnvs?: string[];\n /**\n * Envs treated as \"prod\" (bare host). Defaults to `[\"prod\", \"production\"]`.\n */\n prodEnvs?: string[];\n}\n\n/**\n * Declares secrets a worker requires at deploy/runtime.\n *\n * **Names only — never values.** Secret material lives in the Tamer vault\n * (`tamer secrets set` / `load`) and reaches Cloudflare via `push` / `deploy`.\n * {@link WorkerSecretsConfig.required} is reconciled by `plan` and `drift`\n * against vault fingerprints and deployed worker presence.\n */\nexport interface WorkerSecretsConfig {\n /**\n * Logical secret names this worker needs at runtime (e.g. `STRIPE_KEY`).\n * Values are stored in the Tamer vault via `tamer secrets set` / `load` and\n * pushed to Cloudflare on `tamer secrets push` or `tamer deploy`. Reconciled\n * by `plan`, `drift`, and `tamer secrets verify` against vault fingerprints\n * and {@link SecretStateEntry.lastPushedHash} — never put plaintext here.\n */\n required: string[];\n}\n\n// ── Worker Config ─────────────────────────────────────────────────────────────\n\n/**\n * Pre-deploy build step for a worker (e.g. SPA asset compilation via\n * `vite build`). When set, `tamer deploy` spawns `command` in the worker's\n * `path` directory with the worker's resolved `vars` (references resolved\n * against state for the target env) as environment variables, so compile-time\n * substitution tools (Vite `VITE_*`, esbuild `define`, etc.) read values\n * authored in Tamer config directly — no `.env` file or codegen step.\n * See [docs/spa-build-config.md](../docs/spa-build-config.md).\n */\nexport interface WorkerBuildConfig {\n /**\n * Shell command run in the worker's `path` directory after `wrangler.json`\n * is written but before `wrangler types` / `wrangler deploy`. The worker's\n * resolved `vars` are passed as environment variables (merged onto\n * `process.env`).\n */\n command: string;\n}\n\ntype ManagedFields =\n | \"name\"\n | \"account_id\"\n | \"d1_databases\"\n | \"r2_buckets\"\n | \"kv_namespaces\"\n | \"queues\"\n | \"hyperdrive\"\n | \"vectorize\";\ntype BaseWranglerFields = Omit<WranglerConfig, ManagedFields>;\n\nexport interface EnvOverride extends Omit<\n BaseWranglerFields,\n \"vars\" | \"r2_buckets\"\n> {\n vars?: Record<string, TamerResolvableString>;\n /** Optional R2 buckets (passthrough to Wrangler; cross-stack imports). */\n r2_buckets?: WranglerR2BucketResolvable[];\n /** Overrides default generated Wrangler worker `name` for this environment. */\n scriptName?: string;\n /** Generated Wrangler config filename (default `wrangler.json`). */\n wranglerOutFile?: string;\n /** Per-env Tamer-managed routes (replaces base `tamerRoutes` for this env). */\n tamerRoutes?: RouteResourceConfig[];\n /**\n * Zone names (`route.zone_name`) where **`tamer deploy`** should delete stale\n * API-managed Workers routes still bound to this script but absent from resolved\n * `tamerRoutes`. Omit or leave empty for no pruning.\n */\n tamerStaleRouteSweepZones?: string[];\n /** Per-env override for the worker's pre-deploy {@link WorkerBuildConfig}. */\n build?: WorkerBuildConfig;\n}\n\nexport interface WorkerConfig extends Omit<\n BaseWranglerFields,\n \"vars\" | \"r2_buckets\" | \"env\" | \"local\"\n> {\n path?: string;\n config?: string;\n resources?: WorkerResources;\n /** Optional R2 buckets (passthrough to Wrangler; cross-stack imports). */\n r2_buckets?: WranglerR2BucketResolvable[];\n /** Tamer-managed routes; expanded per env into wrangler `routes[]`. */\n tamerRoutes?: RouteResourceConfig[];\n /**\n * Zone names where **`tamer deploy`** deletes orphaned Workers zone routes for\n * this script (patterns not in resolved `apiManagedRoutes`). Optional.\n */\n tamerStaleRouteSweepZones?: string[];\n alias?: Record<string, string>;\n vars?: Record<string, TamerResolvableString>;\n /**\n * When set, used as the Wrangler worker `name` (script name) instead of the default\n * `{slug}-{workerKey}-{env}-{tenantId}` pattern. Required for stable `services` targets.\n */\n scriptName?: string;\n /**\n * Basename of the generated Wrangler JSON file (default `wrangler.json`).\n * Use when multiple logical workers share one `path` (e.g. `wrangler.notes.json`).\n */\n wranglerOutFile?: string;\n /**\n * When set, `tamer deploy` passes `--dispatch-namespace` to Wrangler (Workers for Platforms user Worker upload).\n */\n dispatchNamespace?: string;\n /**\n * Required worker secrets (names only). See {@link WorkerSecretsConfig}.\n * Absent block means no declared secrets for this worker.\n */\n secrets?: WorkerSecretsConfig;\n /** Pre-deploy build step (e.g. `vite build` for SPA workers). See {@link WorkerBuildConfig}. */\n build?: WorkerBuildConfig;\n local?: EnvOverride;\n env?: Record<string, EnvOverride>;\n}\n\n// ── Top-level Config ──────────────────────────────────────────────────────────\n\n/**\n * CloudFormation-style stack identity. Multiple stacks can coexist in the\n * same `tamer-state-{env}` D1 — each writes to its own `cfi_state:{name}`\n * row — so cross-stack `${tamer:import:<stackName>.<output>}` references\n * can find sibling stacks. When omitted, the stack name defaults to\n * `tenant.slug`, which preserves existing single-stack semantics: a fresh\n * single-stack codebase writes to `cfi_state:{tenant.slug}` (was\n * `cfi_state` pre-0.26 — greenfield rename, no migration).\n */\nexport interface CfiStackConfig {\n /**\n * Stable stack identifier — used as the D1 row-key suffix\n * (`cfi_state:{name}`) and the namespace siblings address with\n * `${tamer:import:<name>.<output>}`. Must match\n * `^[a-zA-Z][a-zA-Z0-9_-]*$` (CloudFormation-style identifier).\n * Defaults to `tenant.slug` when unset.\n */\n name?: string;\n /** Free-form human description; surfaced by `tamer status`. */\n description?: string;\n}\n\ninterface CfiConfigBase {\n tenant: TenantMeta;\n account_id?: string;\n compatibility_date?: string;\n /**\n * Optional overrides for Cloudflare-side **names** (D1, R2, worker scripts,\n * workflows). Use for brownfield stacks whose live names differ from Tamer\n * defaults; IDs stay in state after `sync` / `apply`, never in config.\n * @see {@link NamingConventions}\n * @see docs/brownfield-adoption.md\n */\n naming?: NamingConventions;\n /** CloudFormation-style stack identity. See {@link CfiStackConfig}. */\n stack?: CfiStackConfig;\n /** Workers for Platforms dispatch namespaces to create on `tamer apply` / track in state. */\n dispatchNamespaces?: DispatchNamespaceResourceConfig[];\n /**\n * Cloudflare DNS records to create / track / destroy at the **tenant\n * scope** (zone-scoped, but declared once per stack rather than per\n * worker, so the same record isn't accidentally duplicated across\n * workers). Tamer matches against the live zone via the recorded\n * `recordId` (and a Tamer-attribution comment for sync rediscovery).\n * Updates issue `PATCH /zones/{id}/dns_records/{record_id}` for any\n * mutable field that drifted from state; type changes follow\n * Cloudflare's delete-and-recreate convention (see\n * https://developers.cloudflare.com/fundamentals/api/reference/deprecations/).\n */\n dnsRecords?: DnsRecordResourceConfig[];\n /**\n * Account-scoped [Workers Trace Events Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/)\n * jobs (`workers_trace_events`). Created on `tamer apply` after worker-bound\n * resources (e.g. R2) exist in state. Set `logpush: true` on **each** Worker\n * script whose traces should export (not only dispatch).\n */\n logpushJobs?: LogpushJobResourceConfig[];\n /**\n * Named exports this stack publishes — Tamer's CloudFormation `Outputs`\n * analogue. Each value is a single `${tamer:<kind>:<logical>.<field>}`\n * reference (interpolation also works) resolved against this stack's own\n * state at apply time and persisted into `CfiState.stackOutputs` so it\n * survives across runs and is visible to `tamer status` / external tooling.\n *\n * Cross-stack consumption (a sibling stack reading these via\n * `${tamer:import:<stackName>.<outputName>}`) requires that the producing\n * stack has already run `tamer apply` against the same env/account.\n *\n * Example:\n * ```ts\n * outputs: {\n * userDbId: cf.d1(\"users\").id,\n * queueName: cf.queue(\"events\").name,\n * }\n * ```\n *\n * Output names must match `^[a-zA-Z][a-zA-Z0-9_-]*$` (CloudFormation-ish).\n * Resolution is **strict**: an unresolved reference fails the apply\n * (rolled back if `--rollback-on-failure`). Read-only commands (`plan`,\n * `drift`, `status`) tolerate unresolved refs and show the placeholder\n * verbatim.\n */\n outputs?: Record<string, TamerResolvableString>;\n /**\n * Apply-time code generation hooks. Artifacts are gitignored siblings of\n * `wrangler.json` unless your stack commits them deliberately.\n */\n codegen?: CodegenConfig;\n}\n\n/** Single-worker stack: one top-level `worker` (no `workers` map). */\nexport interface CfiConfigSingle extends CfiConfigBase {\n worker: WorkerConfig;\n workers?: never;\n}\n\n/** Multi-worker stack: `workers` keyed by deploy target (e.g. `api`, `admin`). */\nexport interface CfiConfigMulti extends CfiConfigBase {\n workers: Record<string, WorkerConfig>;\n worker?: never;\n}\n\n/**\n * Root document for `tamer/project.config.ts` (or `tamer.project.config.ts`).\n * Single-worker stacks use {@link CfiConfigSingle}; multi-worker use\n * {@link CfiConfigMulti}.\n */\nexport type CfiConfig = CfiConfigSingle | CfiConfigMulti;\n\n// ── Helper functions ──────────────────────────────────────────────────────────\n\n/**\n * Identity helper for `tamer/project.config.ts` so the config object is typed\n * as {@link CfiConfig} in the IDE. No runtime transformation — the CLI loads\n * and validates the exported default.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * tenant: { id: \"acme\", name: \"Acme\", slug: \"acme\" },\n * workers: { api: { main: \"workers/api/src/index.ts\" } },\n * });\n * ```\n */\nexport function defineConfig(config: CfiConfig): CfiConfig {\n return config;\n}\n\n/**\n * Optional metadata on env overlay objects. Stripped before merge; when set, it must\n * equal the CLI `--env` that selected this overlay (guards wrong file / copy-paste).\n */\nexport const TAMER_OVERLAY_ENV_KEY = \"tamerOverlayEnv\" as const;\n\n/**\n * Fragment merged onto {@link defineConfig} output from `tamer/env/<env>.config.ts`\n * (or flat `tamer.env.<env>.ts`). Typing is intentionally loose — shape is validated\n * after merge via the usual `CfiConfig` schema.\n *\n * Prefer the two-argument form so {@link TAMER_OVERLAY_ENV_KEY} is set and checked\n * against `--env`:\n *\n * ```ts\n * export default defineProjectOverlay(\"dev\", { account_id: \"…\" });\n * ```\n */\nexport function defineProjectOverlay<\n E extends string,\n T extends Record<string, unknown>,\n>(forEnv: E, fragment: T): T & { [K in typeof TAMER_OVERLAY_ENV_KEY]: E };\nexport function defineProjectOverlay<T extends Record<string, unknown>>(\n fragment: T,\n): T;\nexport function defineProjectOverlay(\n forEnvOrFragment: string | Record<string, unknown>,\n fragment?: Record<string, unknown>,\n): Record<string, unknown> {\n if (typeof forEnvOrFragment === \"string\") {\n if (\n fragment === undefined ||\n typeof fragment !== \"object\" ||\n fragment === null ||\n Array.isArray(fragment)\n ) {\n throw new Error(\n \"defineProjectOverlay(forEnv, fragment): fragment must be a plain object\",\n );\n }\n return {\n [TAMER_OVERLAY_ENV_KEY]: forEnvOrFragment,\n ...(fragment as Record<string, unknown>),\n };\n }\n return forEnvOrFragment as Record<string, unknown>;\n}\n\n/**\n * Identity helper for one entry in `workers` (or the sole `worker` field).\n * Same pattern as {@link defineConfig} — improves hover types only.\n */\nexport function defineWorker(config: WorkerConfig): WorkerConfig {\n return config;\n}\n\nexport function getDispatchNamespaces(\n config: CfiConfig,\n): DispatchNamespaceResourceConfig[] {\n return config.dispatchNamespaces ?? [];\n}\n\nexport function getDnsRecords(config: CfiConfig): DnsRecordResourceConfig[] {\n return config.dnsRecords ?? [];\n}\n\nexport function getLogpushJobs(config: CfiConfig): LogpushJobResourceConfig[] {\n return config.logpushJobs ?? [];\n}\n\n// ── State ─────────────────────────────────────────────────────────────────────\n\nexport interface D1StateEntry {\n type: \"d1_database\";\n logicalName: string;\n shardDate?: string;\n derivedName: string;\n bindingKey: string;\n cfId: string;\n migrationsDir?: string;\n /** When true, `tamer destroy` skips deleting this D1. */\n preserveOnDestroy?: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface R2StateEntry {\n type: \"r2_bucket\";\n logicalName: string;\n createdDate: string;\n derivedName: string;\n bindingKey: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface KVStateEntry {\n type: \"kv_namespace\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n cfId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface QueueStateEntry {\n type: \"queue\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Cloudflare queue id (uuid). */\n cfId: string;\n /** Whether Tamer emits a producer binding for this queue (false for consumer-only). */\n producerBinding: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface VectorizeStateEntry {\n type: \"vectorize\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Vectorize index id (uuid) returned by the v2 create endpoint. */\n cfId: string;\n /** Immutable vector dimension. */\n dimensions: number;\n /** Immutable distance metric. */\n metric: \"cosine\" | \"euclidean\" | \"dot-product\";\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HyperdriveStateEntry {\n type: \"hyperdrive\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Cloudflare hyperdrive config id. */\n cfId: string;\n /** Origin database engine (purely informational). */\n scheme: \"postgres\" | \"postgresql\" | \"mysql\";\n /** Origin host (purely informational). */\n originHost: string;\n /** Origin database name (purely informational). */\n originDatabase: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface AIGatewayStateEntry {\n type: \"ai_gateway\";\n logicalName: string;\n /** Cloudflare gateway id (== derived slug). */\n derivedName: string;\n /** Stable cross-reference binding key (no Wrangler binding emitted). */\n bindingKey: string;\n cfId: string;\n cacheTtl: number;\n cacheInvalidateOnUpdate: boolean;\n collectLogs: boolean;\n authentication: boolean;\n rateLimitingInterval: number;\n rateLimitingLimit: number;\n rateLimitingTechnique: \"fixed\" | \"sliding\";\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface PipelineStateEntry {\n type: \"pipeline\";\n logicalName: string;\n /** Tamer-derived pipeline name sent to Cloudflare on create. */\n derivedName: string;\n /** Wrangler binding key emitted in `pipelines[]`. */\n bindingKey: string;\n /** Server-assigned pipeline id (referenced from wrangler `pipeline`). */\n cfId: string;\n /** Pipeline SQL as last applied. Tracked for drift detection. */\n sql: string;\n /** Cloudflare-reported lifecycle status (e.g. \"running\", \"stopped\"). */\n status?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface WorkflowStateEntry {\n type: \"workflow\";\n logicalName: string;\n /** Tamer-derived workflow name sent to Cloudflare on PUT. */\n derivedName: string;\n /** Wrangler binding key emitted in `workflows[]`. */\n bindingKey: string;\n /** Server-assigned workflow id returned by PUT. */\n cfId: string;\n /** Class name of the `WorkflowEntrypoint` subclass (drift target). */\n className: string;\n /**\n * Worker script that hosts the class — either the owning worker's\n * deployed name, or `WorkflowResourceConfig.scriptName` verbatim.\n */\n scriptName: string;\n /** Optional execution-limit override last applied. */\n limits?: { steps?: number };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface SecretsStoreStateEntry {\n type: \"secrets_store\";\n logicalName: string;\n /** Tamer-derived store name sent to Cloudflare on create. */\n derivedName: string;\n /** Stable cross-reference key (no Wrangler binding emitted directly). */\n bindingKey: string;\n /** Server-assigned store id (referenced from wrangler `secrets_store_secrets[].store_id`). */\n cfId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface DnsRecordStateEntry {\n type: \"dns_record\";\n logicalName: string;\n zoneId: string;\n recordType:\n | \"A\"\n | \"AAAA\"\n | \"CNAME\"\n | \"TXT\"\n | \"MX\"\n | \"NS\"\n | \"CAA\"\n | \"SRV\"\n | \"PTR\"\n | \"HTTPS\"\n | \"SVCB\";\n /** DNS name as recorded by Cloudflare (always FQDN; `@` is expanded to the zone apex). */\n name: string;\n /** Last-applied content, kept for drift comparison. */\n content: string;\n /** Last-applied TTL (seconds; `1` means \"Auto\"). */\n ttl: number;\n /** Last-applied proxied flag. */\n proxied: boolean;\n /** Last-applied priority for MX/SRV/URI; `undefined` for record types without one. */\n priority?: number;\n /** Full comment as written to Cloudflare (includes the Tamer attribution prefix). */\n comment: string;\n /** Cloudflare record id (`0123…`). */\n recordId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface DispatchNamespaceStateEntry {\n type: \"dispatch_namespace\";\n logicalName: string;\n /** Cloudflare namespace name. */\n derivedName: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LogpushJobStateEntry {\n type: \"logpush_job\";\n logicalName: string;\n /** Cloudflare Logpush job display name (matches API `name`). */\n derivedName: string;\n /** Numeric job id from `POST /accounts/{id}/logpush/jobs`. */\n cfJobId: number;\n dataset: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Tamer-owned Pipelines graph for {@link LogpushJobPipelinesAutoDestination}\n * (stream, sink, SQL pipeline) once `ensurePipelinesLogpushProvision` runs.\n */\nexport interface LogpushPipelinesStateEntry {\n type: \"logpush_pipelines\";\n logicalName: string;\n streamId: string;\n /**\n * Ingest origin from Pipelines stream `endpoint` when the API returns it\n * (e.g. `https://….ingest.cloudflare.com`). Otherwise Logpush URL is built from {@link streamId}.\n */\n streamIngestBaseUrl?: string;\n sinkId: string;\n pipelineId: string;\n streamName: string;\n sinkName: string;\n pipelineName: string;\n /**\n * Table identifier to use in **R2 SQL** (`SHOW TABLES`, `SELECT`), after\n * Tamer normalizes Pipelines’ suffixed Iceberg name to the catalog/SQL name.\n */\n r2DataCatalogTableName?: string;\n /**\n * Table name **Pipelines / sink GET** reported before normalization (e.g.\n * `worker_trace_events_173…`). For display and debugging; queries use\n * {@link r2DataCatalogTableName}.\n */\n r2DataCatalogTableNamePipelines?: string;\n r2DataCatalogNamespace?: string;\n catalogBucketDerivedName: string;\n /**\n * Account API token id + secret minted for R2 + R2 Data Catalog (catalog\n * credential + `r2_data_catalog` sink). Secrets live only in Tamer state\n * (like dashboard-stored values, but file-local).\n */\n mintedR2CatalogTokenId?: string;\n mintedR2CatalogTokenValue?: string;\n /**\n * Account API token id + secret for [Workers Pipelines\n * Send](https://developers.cloudflare.com/pipelines/streams/writing-to-streams/)\n * (Logpush `destination_conf` stream ingest).\n */\n mintedPipelinesSendTokenId?: string;\n mintedPipelinesSendTokenValue?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface WorkerRouteStateEntry {\n type: \"worker_route\";\n workerKey: string;\n /** Deployed Worker script name (same as wrangler `name`). */\n workerName: string;\n zoneId: string;\n zoneName: string;\n routeId: string;\n pattern: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Last-pushed fingerprint for a worker secret — **no secret material**.\n *\n * Written by `tamer secrets push` and `tamer deploy` after a successful CF API\n * PUT. Stored in {@link CfiState.resources} under key `secret:{worker}:{name}`.\n * Compared to the vault row's `value_hash` to detect rotation (`vault hash !=\n * lastPushedHash` → needs push). Keyed per secret × worker × env so the same\n * secret can be current on one worker and stale on another.\n */\nexport interface SecretStateEntry {\n type: \"secret\";\n /** Worker key from `tamer.config.ts` (e.g. `api`). */\n worker: string;\n /** Logical secret name (e.g. `STRIPE_KEY`). */\n name: string;\n /** Vault `value_hash` last PUT to this worker for this env. */\n lastPushedHash: string;\n /** ISO timestamp of the last successful push. */\n lastPushedAt: string;\n}\n\nexport type StateEntry =\n | D1StateEntry\n | R2StateEntry\n | KVStateEntry\n | QueueStateEntry\n | HyperdriveStateEntry\n | VectorizeStateEntry\n | AIGatewayStateEntry\n | PipelineStateEntry\n | WorkflowStateEntry\n | SecretsStoreStateEntry\n | DnsRecordStateEntry\n | DispatchNamespaceStateEntry\n | LogpushJobStateEntry\n | LogpushPipelinesStateEntry\n | WorkerRouteStateEntry\n | SecretStateEntry;\n\n/** Provisioning lifecycle for a workspace tenant (`product` + `workspace`). */\nexport type ProvisioningStatus =\n | \"pending\"\n | \"d1_created\"\n | \"migrations_applied\"\n | \"script_uploaded\"\n | \"ready\"\n | \"tombstoned\";\n\nexport interface TenantD1ShardRef {\n role: string;\n derivedName: string;\n cfId: string;\n}\n\n/**\n * One runtime-provisioned tenant (dispatch script + optional D1 shards).\n * Keyed in {@link CfiState.tenants} by `product:workspace`.\n */\nexport interface TenantStateEntry {\n product: string;\n workspace: string;\n provisioningStatus: ProvisioningStatus;\n dispatchNamespaceName: string;\n scriptName: string;\n d1Shards?: TenantD1ShardRef[];\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Lightweight CloudFormation-style metadata about the deployment \"stack\" that\n * owns this state row. Optional everywhere — schema-version bump aware, but\n * commands set them when they have the information.\n */\nexport interface CfiStackMeta {\n /**\n * Human label for the stack (e.g. `external`, `internal`). Defaults to the\n * tenant slug when unset.\n */\n name?: string;\n /** Free-form owner string (team / pipeline). */\n owner?: string;\n}\n\nexport type CfiOperationName =\n | \"bootstrap\"\n | \"apply\"\n | \"deploy\"\n | \"destroy\"\n | \"provision-tenant\"\n | \"destroy-tenant\"\n | \"import\"\n | \"sync\";\n\nexport type CfiOperationStatus = \"in_progress\" | \"succeeded\" | \"failed\";\n\nexport interface CfiOperationRecord {\n command: CfiOperationName;\n status: CfiOperationStatus;\n startedAt: string;\n completedAt?: string;\n errorMessage?: string;\n /** Optional free-form context (target worker, tenant, etc.). */\n detail?: string;\n}\n\nexport interface CfiState {\n tenantId: string;\n env: string;\n schemaVersion: number;\n syncedAt: string;\n resources: Record<string, StateEntry>;\n /** Optimistic concurrency for D1 `cfi_state` row (see `StateManager.persist`). */\n revision?: number;\n /** Workspace tenants provisioned at runtime (not in `tamer.config.ts`). */\n tenants?: Record<string, TenantStateEntry>;\n /** Stack metadata (CloudFormation-style). Optional. */\n stack?: CfiStackMeta;\n /**\n * Resolved + persisted values for every entry in `tamer.config.ts > outputs`.\n * Written at the end of a successful `apply`; surfaced by `tamer status`\n * and consumed by sibling stacks via `${tamer:import:<stackName>.<key>}`.\n * Keys mirror `outputs` keys 1:1; entries are dropped when removed from\n * config or when the stack is destroyed.\n */\n stackOutputs?: Record<string, CfiStackOutputValue>;\n /** Last operation that touched this state row. */\n lastOperation?: CfiOperationRecord;\n /**\n * Completed operations only (`succeeded` / `failed`), newest first. Capped\n * at 50 on write when an operation finishes; surfaced by `tamer events`.\n */\n operationHistory?: CfiOperationRecord[];\n}\n\n/**\n * One persisted entry in {@link CfiState.stackOutputs}. Stores the resolved\n * literal alongside the original `${tamer:...}` source for diffing/debugging\n * and a timestamp so `tamer status` can show staleness.\n */\nexport interface CfiStackOutputValue {\n /** Resolved literal value (e.g. a D1 cfId, R2 bucket name, route URL). */\n value: string;\n /** The original `${tamer:...}` reference from `outputs` at resolve time. */\n source: string;\n /** ISO timestamp the value was last resolved + persisted. */\n resolvedAt: string;\n}\n\n// ── Status ────────────────────────────────────────────────────────────────────\n\nexport type ResourceStatus = \"ok\" | \"missing\" | \"pending\" | \"error\";\n\nexport interface WorkerStatus {\n workerKey: string;\n deployedName: string;\n route?: string;\n status: ResourceStatus;\n d1: Array<{\n binding: string;\n name: string;\n cfId: string;\n status: ResourceStatus;\n }>;\n r2: Array<{ binding: string; name: string; status: ResourceStatus }>;\n error?: string;\n}\n\nexport interface TenantStatus {\n tenant: TenantMeta;\n env: string;\n workers: WorkerStatus[];\n}\n\n// ── Naming conventions ────────────────────────────────────────────────────────\n\n/**\n * Optional functions that derive **Cloudflare resource names** from config\n * (`logicalName`, `tenant.id`, `env`, etc.). Used when a resource has no\n * {@link CloudflareNameFn} override on its config.\n *\n * **Resolution order (managed resources):**\n * 1. `resource.cloudflareName?(tenantId, env, ctx?)`\n * 2. stack `naming.{kindHook}?(...)` (this interface)\n * 3. {@link NamingEngine} built-in default\n *\n * **Greenfield:** omit both per-resource overrides and this block.\n *\n * **Brownfield:** use per-resource `cloudflareName` when names differ per\n * logical resource; use stack hooks for shared formulas. Config holds logical\n * names and hooks; **state** holds `cfId` after `tamer sync`.\n *\n * @see docs/brownfield-adoption.md\n * @see docs/per-resource-cloudflare-naming.md\n */\nexport interface NamingConventions {\n /**\n * Cloudflare D1 database name for `resources.d1[]` with `type: \"single\"`.\n *\n * **Default:** `db_{logicalName}_t_{tenantId}_{env}`.\n *\n * **`sync`:** exact match on the derived name.\n */\n d1Single?: (logicalName: string, tenantId: string, env: string) => string;\n /**\n * Cloudflare D1 database name for `resources.d1[]` with `type: \"sharded\"`.\n *\n * **`date`:** shard stamp passed to apply (`YYYY-MM-DD` or compact\n * `YYYYMMDD` after normalization). Your hook should embed it the same way\n * legacy DBs do (often `date.replace(/-/g, \"\")`).\n *\n * **Default:** `db_{logicalName}_{YYYYMMDD}_t_{tenantId}_{env}` (omits\n * `{logicalName}_` when logical is `default` or empty).\n *\n * **`sync`:** with a custom hook, **derive-and-match** — Tamer parses the\n * shard date from each account D1 name (`_YYYYMMDD_t_` or `_YYYY_MM_DD_t_`),\n * re-derives via this function, and adopts when the names are equal. Without\n * a hook, prefix/suffix regex matching is used instead.\n */\n d1Shard?: (\n logicalName: string,\n date: string,\n tenantId: string,\n env: string,\n ) => string;\n /**\n * R2 bucket name for `resources.r2[]`.\n *\n * **`date`:** `YYYYMMDD` (no dashes) from the calendar day **apply** or\n * **sync** runs. Ignore unless your legacy scheme embeds a date stamp.\n *\n * **Default:** `r2-{logicalName}-t-{tenantId}-{env}` (no date segment).\n *\n * **`sync`:** exact match on the name produced by calling this hook with\n * today's date (same as apply). Legacy dated buckets\n * `r2-{logical}-YYYYMMDD-t-{tenant}-{env}` still match when the hook is\n * omitted.\n */\n r2Bucket?: (\n logicalName: string,\n date: string,\n tenantId: string,\n env: string,\n ) => string;\n /**\n * Deployed Worker **script** name (wrangler `name` / `tamer deploy` target).\n *\n * **Default:** `{slug}-{workerKey}-{tenantId}` for `local`; otherwise\n * `{slug}-{workerKey}-{env}-{tenantId}`. Overridable per worker via\n * {@link WorkerConfig.scriptName}.\n *\n * **`sync`:** worker script list is matched by deployed name (routes use\n * the same derivation).\n */\n workerName?: (\n slug: string,\n workerKey: string,\n env: string,\n tenantId: string,\n ) => string;\n /**\n * Cloudflare workflow **registration** name for `resources.workflows[]`.\n * Workflow names are immutable on Cloudflare — brownfield stacks must\n * return the live registration name here.\n *\n * **Default:** `wf-{logicalName}-t-{tenantId}-{env}` (lowercased).\n *\n * **`sync`:** exact match on the derived name (same as D1 single / default\n * R2). Prefer `tamer import --kind workflow` only when a legacy name cannot\n * be expressed as a function of logical name, tenant, and env.\n */\n workflow?: (logicalName: string, tenantId: string, env: string) => string;\n}\n","/**\n * Cloudflare-shaped authoring helpers for Tamer configs.\n *\n * Values are plain objects resolved by `materializeCloudflareBindings()` into the\n * same `${tamer:…}` strings the reference resolver already understands — no\n * second resolution path, no Cloudflare-agnostic indirection.\n */\n\nexport type CfResourceKind =\n | \"d1\"\n | \"r2\"\n | \"kv\"\n | \"queue\"\n | \"hyperdrive\"\n | \"vectorize\"\n | \"ai_gateway\"\n | \"pipeline\"\n | \"workflow\"\n | \"secret_store\";\n\nexport type CfResourceField = \"name\" | \"id\" | \"binding\";\n\nexport type CfLogpushPipelinesField =\n | \"r2_data_catalog_table_name\"\n | \"r2_data_catalog_table_name_pipelines\"\n | \"r2_data_catalog_namespace\"\n | \"name\"\n | \"id\"\n | \"iceberg_table\"\n | \"iceberg_table_pipelines\"\n | \"iceberg_namespace\";\n\nexport type CfBindingSpec =\n | { t: \"resource\"; kind: CfResourceKind; logical: string; field: CfResourceField }\n | { t: \"dispatch_namespace\"; logical: string; field: \"name\" | \"id\" }\n | { t: \"worker\"; workerKey: string; field: \"name\" }\n | { t: \"logpush_pipelines\"; logical: string; field: CfLogpushPipelinesField }\n | { t: \"config\"; logical: \"stack\"; field: \"account_id\" }\n | { t: \"import\"; stack: string; output: string };\n\n/**\n * Opaque handle materialized to a `${tamer:…}` reference before config parse.\n */\nexport class CfBinding {\n constructor(public readonly spec: CfBindingSpec) {}\n\n toRefString(): string {\n return cfBindingSpecToTamerRef(this.spec);\n }\n}\n\nexport function cfBindingSpecToTamerRef(spec: CfBindingSpec): string {\n switch (spec.t) {\n case \"resource\":\n return `\\${tamer:${spec.kind}:${spec.logical}.${spec.field}}`;\n case \"dispatch_namespace\":\n return `\\${tamer:dispatch_namespace:${spec.logical}.${spec.field}}`;\n case \"worker\":\n return `\\${tamer:worker:${spec.workerKey}.${spec.field}}`;\n case \"logpush_pipelines\":\n return `\\${tamer:logpush_pipelines:${spec.logical}.${spec.field}}`;\n case \"config\":\n return `\\${tamer:config:${spec.logical}.${spec.field}}`;\n case \"import\":\n return `\\${tamer:import:${spec.stack}.${spec.output}}`;\n default: {\n const _x: never = spec;\n return _x;\n }\n }\n}\n\nfunction resource(logical: string, kind: CfResourceKind) {\n return {\n get name(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"name\" });\n },\n get id(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"id\" });\n },\n get binding(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"binding\" });\n },\n };\n}\n\n/**\n * Fluent Cloudflare resource references for `vars`, `outputs`, `tamerRoutes`, etc.\n *\n * @example\n * ```ts\n * vars: {\n * BUCKET: cf.r2(\"assets\").name,\n * ACCOUNT: cf.stack.accountId,\n * }\n * ```\n */\nexport const cf = {\n d1: (logical: string) => resource(logical, \"d1\"),\n r2: (logical: string) => resource(logical, \"r2\"),\n kv: (logical: string) => resource(logical, \"kv\"),\n queue: (logical: string) => resource(logical, \"queue\"),\n hyperdrive: (logical: string) => resource(logical, \"hyperdrive\"),\n vectorize: (logical: string) => resource(logical, \"vectorize\"),\n aiGateway: (logical: string) => resource(logical, \"ai_gateway\"),\n pipeline: (logical: string) => resource(logical, \"pipeline\"),\n workflow: (logical: string) => resource(logical, \"workflow\"),\n secretStore: (logical: string) => resource(logical, \"secret_store\"),\n\n dispatchNamespace: (logical: string) => ({\n get name(): CfBinding {\n return new CfBinding({ t: \"dispatch_namespace\", logical, field: \"name\" });\n },\n get id(): CfBinding {\n return new CfBinding({ t: \"dispatch_namespace\", logical, field: \"id\" });\n },\n }),\n\n /** Deployed Worker script name for this env (Wrangler `name`). */\n worker: (workerKey: string) => ({\n get name(): CfBinding {\n return new CfBinding({ t: \"worker\", workerKey, field: \"name\" });\n },\n }),\n\n logpushPipelines: (logical: string) => ({\n get r2DataCatalogTableName(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_table_name\",\n });\n },\n get r2DataCatalogTableNamePipelines(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_table_name_pipelines\",\n });\n },\n get r2DataCatalogNamespace(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_namespace\",\n });\n },\n get pipelineName(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"name\" });\n },\n get pipelineId(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"id\" });\n },\n /** Alias for {@link r2DataCatalogTableName}. */\n get icebergTable(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"iceberg_table\" });\n },\n /** Alias for {@link r2DataCatalogTableNamePipelines}. */\n get icebergTablePipelines(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"iceberg_table_pipelines\",\n });\n },\n /** Alias for {@link r2DataCatalogNamespace}. */\n get icebergNamespace(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"iceberg_namespace\" });\n },\n }),\n\n stack: {\n get accountId(): CfBinding {\n return new CfBinding({ t: \"config\", logical: \"stack\", field: \"account_id\" });\n },\n },\n\n /** Cross-stack: sibling stack name + published output key. */\n import: (stack: string, output: string) =>\n new CfBinding({ t: \"import\", stack, output }),\n} as const;\n\nexport function isCfBinding(x: unknown): x is CfBinding {\n return x instanceof CfBinding;\n}\n","import type { TamerResolvableString } from \"../types.js\";\nimport { CfBinding, isCfBinding } from \"./cloudflare-bindings.js\";\n\n/** Coerce a post-load or inline authoring value to a `${tamer:…}` / literal string. */\nexport function materializeTamerResolvable(v: TamerResolvableString): string {\n return isCfBinding(v) ? v.toRefString() : v;\n}\n\nexport function materializeVars(\n vars: Record<string, TamerResolvableString> | undefined,\n): Record<string, string> | undefined {\n if (!vars) return undefined;\n return Object.fromEntries(\n Object.entries(vars).map(([k, v]) => [k, materializeTamerResolvable(v)]),\n );\n}\n\n/**\n * Walk a config-shaped value and replace every {@link CfBinding} with its\n * `${tamer:…}` string. Preserves functions (e.g. `naming` callbacks) and\n * non-plain objects are left as-is after a shallow binding check.\n */\nexport function materializeCloudflareBindings(value: unknown): unknown {\n if (isCfBinding(value)) {\n return value.toRefString();\n }\n if (value === null || value === undefined) {\n return value;\n }\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"bigint\") {\n return value;\n }\n if (t === \"function\") {\n return value;\n }\n if (value instanceof Date) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(materializeCloudflareBindings);\n }\n if (t !== \"object\") {\n return value;\n }\n\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = materializeCloudflareBindings(v);\n }\n return out;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"normalize-DVSTRZhO.mjs","names":["spec: CfBindingSpec","out: Record<string, unknown>"],"sources":["../src/types.ts","../src/dx/cloudflare-bindings.ts","../src/dx/normalize.ts"],"sourcesContent":["import type { CfBinding } from \"./dx/cloudflare-bindings.js\";\nimport type {\n WranglerConfig,\n WranglerR2Bucket,\n} from \"./generated/wrangler-types.js\";\n\nexport type { WranglerConfig };\n\n/**\n * Worker `vars`, `outputs`, and similar fields accept plain strings or\n * {@link CfBinding} values; `loadConfig` materializes bindings to `${tamer:…}`\n * before validation.\n */\nexport type TamerResolvableString = string | CfBinding;\n\n/** Context for per-resource Cloudflare name resolution (e.g. sharded D1 shard date). */\nexport interface CloudflareNameContext {\n /** ISO `YYYY-MM-DD` shard stamp for sharded D1. */\n shardDate?: string;\n}\n\n/**\n * Per-resource Cloudflare name for the current env.\n * No UUIDs — name only. Used for sync, plan, apply, drift, import, and generate.\n *\n * Resolution order when unset: stack {@link NamingConventions} hook → {@link NamingEngine} default.\n */\nexport type CloudflareNameFn = (\n tenantId: string,\n env: string,\n ctx?: CloudflareNameContext,\n) => string;\n\n/** Wrangler R2 binding with optional {@link TamerResolvableString} `bucket_name`. */\nexport type WranglerR2BucketResolvable = Omit<\n WranglerR2Bucket,\n \"bucket_name\"\n> & {\n bucket_name?: TamerResolvableString;\n};\n\n// ── Tenant ───────────────────────────────────────────────────────────────────\n\n/**\n * Stack identity in `defineConfig({ tenant: … })`. `id` and `slug` feed\n * default resource names and state keys; `slug` is also the default\n * {@link CfiStackConfig.name} when `stack.name` is omitted.\n */\nexport interface TenantMeta {\n id: string;\n name: string;\n slug: string;\n /**\n * Optional per-stack tenant D1 shard layout. Each entry is a free-form\n * role name (lowercase letters, digits, `_`, `-`) that becomes part of\n * the per-tenant D1 database name `db_{role}_{w}_{p}_t_{tid}_{env}`\n * when `tamer provision-tenant` runs.\n *\n * Tamer is opinion-free about how many shards a product wants and what\n * they're called — a Dragoncore-style product might use `[\"system\",\n * \"app\", \"history\"]`, a single-DB tenant `[\"main\"]`, a billing-style\n * product `[\"billing\", \"content\"]`, or omit `d1Shards` entirely (in\n * which case `provision-tenant` only uploads the dispatch script — no\n * per-tenant D1 fan-out).\n *\n * The order here is **canonical provisioning order**: shards are\n * created left-to-right and listed in plan/status output in the same\n * order, so a partial failure leaves a deterministic recoverable\n * state. `--shards <subset>` on the CLI must be a subset of this list\n * — the config is the source of truth, the flag only trims.\n */\n d1Shards?: string[];\n /**\n * Maps each shard role (from `d1Shards[]`) to its binding name and\n * migration directory. Used by `wfp tenant provision` to resolve D1\n * bindings on the dispatch script, and by `wfp tenant migrate` to find\n * the migration source per shard.\n *\n * Example:\n * ```ts\n * shardBindings: {\n * system: { binding: \"DB_SYSTEM\", migrationsDir: \"db/system\" },\n * app: { binding: \"DB_APP\", migrationsDir: \"db/app\" },\n * history: { binding: \"DB_HISTORY\", migrationsDir: \"db/history\" },\n * }\n * ```\n */\n shardBindings?: Record<string, {\n binding: string;\n migrationsDir: string;\n migrationsTable?: string;\n }>;\n /**\n * Envs that require an explicit `--confirm-tenant <workspace>` (or\n * `--force`) before `destroy-tenant` will run. Defaults to\n * `[\"prod\", \"production\"]`. Add e.g. `\"production-eu\"`,\n * `\"production-us\"`, `\"qa\"`, `\"uat\"`, `\"canary\"` here for any env\n * whose accidental teardown would be a real-world outage.\n *\n * `local` is never protected (it's a wrangler-dev concept, not a\n * deployed env). Override with `[]` to disable the prompt entirely\n * (only sensible for personal accounts).\n */\n protectedEnvs?: string[];\n /**\n * Optional regex (passed as a string) that decides which env names\n * are \"ephemeral\" — i.e. share **one** dispatch namespace\n * (`{ns}-ephemeral`) instead of getting their own (`{ns}-{env}`),\n * include the env in their dispatch-script name so multiple\n * ephemeral previews can coexist, and get a `BRANCH_SUFFIX`\n * environment variable injected at resolve time.\n *\n * Examples: `\"^pr-\"` (PR previews), `\"^(pr|feature|branch)-\"`,\n * `\"^canary-\"`. When omitted (the default), no env is ephemeral —\n * every env owns its own dispatch namespace. Compiled once at\n * config-load time; an invalid regex fails at parse, not at apply.\n */\n ephemeralEnvPattern?: string;\n /**\n * Environment variables injected into the tenant dispatch script's\n * metadata on `wfp tenant provision`. Values may contain\n * `${tamer:...}` references (resolved against state at provision time).\n * D1 bindings are derived automatically from `resources.d1[].registryRole`\n * matched to `d1Shards[]` — only non-D1 vars go here.\n */\n dispatchVars?: Record<string, TamerResolvableString>;\n /**\n * Service bindings injected into the tenant dispatch script's metadata.\n * Each entry produces `env.BINDING` on the tenant Worker pointing at\n * the named service (e.g. the portal-api Worker). `service` may contain\n * `${tamer:...}` references.\n */\n dispatchServices?: Array<{\n name: string;\n service: TamerResolvableString;\n environment?: string;\n }>;\n /**\n * Secret names to inject into the tenant dispatch script on\n * `wfp tenant provision`. Values are read from the Tamer vault\n * (set via `tamer secrets set <NAME> --env <env>`), decrypted with\n * the env master key, and pushed to the dispatch namespace script\n * via the CF API after `wrangler deploy`. Same vault as regular\n * worker secrets — just a different delivery target.\n */\n dispatchSecrets?: string[];\n}\n\n// ── Resources ────────────────────────────────────────────────────────────────\n\n/**\n * D1 database declared on a worker's `resources.d1[]`.\n * Cloudflare **name** for managed databases follows\n * {@link D1ResourceConfig.cloudflareName} → {@link NamingConventions.d1Single} /\n * {@link NamingConventions.d1Shard} → defaults; **id** is stored in state after `sync` / `apply`.\n */\nexport interface D1ResourceConfig {\n logicalName: string;\n /** `\"single\"`: one DB per logical name. `\"sharded\"`: date-stamped shards (see `d1Shard` naming hook). */\n type: \"single\" | \"sharded\";\n /**\n * Optional per-resource Cloudflare database name. For sharded D1, receives\n * {@link CloudflareNameContext.shardDate}. External D1 uses {@link databaseName} instead.\n */\n cloudflareName?: CloudflareNameFn;\n /**\n * `managed` (default): Tamer creates the database; the Cloudflare name follows\n * `naming.d1Single` / shard rules.\n *\n * `external`: owned by another stack. Requires {@link databaseName} resolving\n * to the live D1 name (e.g. `${tamer:import:platform.platform_db_name}`). Skips\n * create, migrate, and destroy on Cloudflare for this binding.\n */\n ownership?: \"managed\" | \"external\";\n /**\n * Required when `ownership` is `external`. May contain `${tamer:import:…}`; must\n * be resolved (via mergeWorkerConfigWithResolvedRefs / resolveWorkerConfig)\n * before apply / sync / wrangler generation.\n */\n databaseName?: TamerResolvableString;\n /**\n * When set, used as the Wrangler D1 `binding` instead of the generated name.\n * Applies to `type: \"single\"` and `type: \"sharded\"`.\n */\n binding?: string;\n /**\n * Pin the shard date for `type: \"sharded\"` (ISO `YYYY-MM-DD` or `YYYYMMDD`).\n * Brownfield: match one physical shard during sync/apply. Greenfield: first shard date.\n */\n shardDate?: string;\n /**\n * Role label in the generated shard registry (defaults to {@link logicalName}).\n */\n registryRole?: string;\n /** Prior shard id strings that decode to this shard (brownfield aliases). */\n legacyIds?: string[];\n /** Prior wrangler binding keys for this shard (dual-bind / rename history). */\n legacyBindings?: string[];\n /** When false, shard is read-only for new writes (multi-shard same role). Default true. */\n current?: boolean;\n migrationsDir?: string;\n migrationsTable?: string;\n /**\n * When true, `tamer destroy` will not delete this database (e.g. created by\n * another stack but bound read-only here). Default false.\n *\n * Legacy cross-stack mode without {@link ownership} `external`: same Cloudflare\n * name as the owning stack ({@link NamingConventions.d1Single} + logicalName).\n * Prefer `ownership: \"external\"` and a sibling-stack output for the name.\n */\n preserveOnDestroy?: boolean;\n}\n\n/** One row in {@link ShardRegistryV1}. */\nexport interface ShardRegistryEntryV1 {\n /** Logical shard id embedded in universal IDs (brownfield: CF database name). */\n id: string;\n /** App vocabulary — typically the D1 `logicalName` or {@link D1ResourceConfig.registryRole}. */\n role: string;\n /** Current Wrangler D1 binding for this env. */\n binding: string;\n /** Physical Cloudflare D1 database name for this env. */\n databaseName: string;\n shardDate?: string;\n /** When multiple shards share a role, marks the write target. Default true. */\n current?: boolean;\n legacyBindings?: string[];\n legacyIds?: string[];\n}\n\n/** Versioned shard registry emitted by `tamer apply` when `codegen.shardRegistry` is set. */\nexport interface ShardRegistryV1 {\n version: 1;\n shards: ShardRegistryEntryV1[];\n}\n\n/** Options for {@link CodegenConfig.shardRegistry}. */\nexport interface ShardRegistryCodegenConfig {\n /**\n * Worker key that receives the generated module. Required when the stack\n * declares `workers` with more than one entry.\n */\n worker?: string;\n /** Path relative to the worker directory. Default: `src/db/shard-registry.ts`. */\n outFile?: string;\n /** Exported constant name. Default: `shardRegistry`. */\n exportName?: string;\n}\n\nexport interface CodegenConfig {\n /** Emit a {@link ShardRegistryV1} module after wrangler generation on `apply`. */\n shardRegistry?: ShardRegistryCodegenConfig;\n}\n\n/** R2 bucket on `resources.r2[]`. Name from {@link cloudflareName} → {@link NamingConventions.r2Bucket} → default. */\nexport interface R2ResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare bucket name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler R2 `binding` instead of the generated stable name. */\n binding?: string;\n}\n\nexport interface KVResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare KV namespace name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler KV `binding` instead of the generated name. */\n binding?: string;\n}\n\n/**\n * Cloudflare Queue (producer binding) managed by Tamer.\n *\n * Tamer creates the queue itself on `apply` and emits a `queues.producers[]`\n * binding for the worker. Consumer subscriptions are wrangler-side only today\n * (set them via `queues.consumers` on the worker config).\n */\nexport interface QueueResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare queue name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler queue `binding` instead of the generated name. */\n binding?: string;\n /**\n * When true, this entry is consumer-only (no producer binding). The queue is\n * still provisioned by Tamer if absent. Default false (producer binding emitted).\n */\n consumerOnly?: boolean;\n}\n\n/**\n * Cloudflare Hyperdrive config managed by Tamer.\n *\n * Tamer creates the Hyperdrive config on `apply` (the origin connection\n * string is sent to the API once, never persisted in state) and emits a\n * `hyperdrive[]` binding for the worker.\n */\nexport interface HyperdriveResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Hyperdrive config name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler hyperdrive `binding` instead of the generated name. */\n binding?: string;\n /** Origin database connection. Sent to Cloudflare on create; not stored in Tamer state. */\n origin: HyperdriveOriginSpec;\n /** Optional caching tweaks passed to Cloudflare. */\n caching?: {\n disabled?: boolean;\n max_age?: number;\n stale_while_revalidate?: number;\n };\n /** Optional `mtls` block passed to Cloudflare. */\n mtls?: { ca_certificate_id?: string; mtls_certificate_id?: string };\n /** `wrangler dev`-time connection string written to the generated config. */\n localConnectionString?: string;\n}\n\n/**\n * Cloudflare Vectorize index managed by Tamer.\n *\n * Tamer creates the index on `apply` (the v2 storage subsystem; legacy v1 is\n * unsupported) and emits a `vectorize[]` binding for the worker. Index\n * configuration (`dimensions`, `metric`) is immutable per Cloudflare's API,\n * so changes after creation are rejected — drop and recreate via `tamer\n * destroy --resource <logicalName>` then `tamer apply`.\n */\nexport interface VectorizeResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Vectorize index name override. */\n cloudflareName?: CloudflareNameFn;\n /** When set, used as the Wrangler vectorize `binding` instead of the generated name. */\n binding?: string;\n /** Vector dimensionality (e.g. 768 for `@cf/baai/bge-base-en-v1.5`). Immutable. */\n dimensions: number;\n /** Distance metric. Immutable after creation. */\n metric: \"cosine\" | \"euclidean\" | \"dot-product\";\n /** Free-form description sent to the Vectorize API on create. */\n description?: string;\n}\n\n/**\n * Cloudflare AI Gateway managed by Tamer.\n *\n * Tamer creates the gateway on `apply` against `/accounts/{id}/ai-gateway/\n * gateways`. AI Gateways have no Wrangler binding kind — Workers reference\n * them per-request via `env.AI.run(model, opts, { gateway: { id } })` (or\n * the OpenAI-compatible endpoint URL). Use cross-resource refs like\n * `${tamer:ai_gateway:my_gw.name}` in worker `vars` to inject the derived\n * gateway slug at deploy time.\n */\nexport interface AIGatewayResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare AI Gateway slug override. */\n cloudflareName?: CloudflareNameFn;\n /** Cache TTL in seconds. Default 0 (caching disabled). */\n cacheTtl?: number;\n /** Invalidate cached entries when upstream model output changes. Default false. */\n cacheInvalidateOnUpdate?: boolean;\n /** Persist request/response logs in the AI Gateway dashboard. Default true. */\n collectLogs?: boolean;\n /** Require an `Authorization: Bearer <token>` header on all gateway requests. Default false. */\n authentication?: boolean;\n /** Rate-limit window in seconds. Default 0 (rate limiting disabled). */\n rateLimitingInterval?: number;\n /** Max requests per window. Default 0 (rate limiting disabled). */\n rateLimitingLimit?: number;\n /** \"fixed\" or \"sliding\" window. Default \"fixed\". */\n rateLimitingTechnique?: \"fixed\" | \"sliding\";\n}\n\n/**\n * Cloudflare Pipeline (V1, SQL-based) managed by Tamer.\n *\n * Tamer creates the pipeline on `apply` against\n * `/accounts/{id}/pipelines/v1/pipelines` with the user-supplied derived\n * name and SQL. The server returns a unique `id` that Tamer stores in\n * state and emits as `pipelines[].pipeline` in generated wrangler config.\n *\n * Pipelines reference streams (sources) and sinks (destinations) by name\n * inside the SQL — those upstream/downstream resources are **not** managed\n * by Tamer in this iteration. Create them via the Cloudflare dashboard or\n * Wrangler before applying, otherwise the pipeline will exist in a\n * non-running status until they are. Drift / status surface this as the\n * pipeline's `status` field.\n */\nexport interface PipelineResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Pipeline name override. */\n cloudflareName?: CloudflareNameFn;\n /**\n * Arroyo SQL describing the processing flow, e.g.\n * `insert into my_sink select * from my_stream;`. Stream and sink names\n * must already exist on Cloudflare (see {@link PipelineResourceConfig}).\n */\n sql: string;\n /**\n * Override the wrangler binding key. Defaults to\n * `PIPE_{LOGICAL}_T_{TENANT}` — uppercased logical with the tenant id\n * appended for cross-stack uniqueness.\n */\n binding?: string;\n}\n\n/** Origin connection for Hyperdrive (postgres / mysql). */\nexport interface HyperdriveOriginSpec {\n scheme: \"postgres\" | \"postgresql\" | \"mysql\";\n host: string;\n port?: number;\n database: string;\n user: string;\n /**\n * Origin password. Either an inline string OR `{ fromEnv: \"VAR\" }` to read\n * from `process.env` at apply time (recommended). Never persisted in state.\n */\n password: string | { fromEnv: string };\n /** Optional access client id (Cloudflare Access protected origins). */\n access_client_id?: string;\n access_client_secret?: string | { fromEnv: string };\n}\n\n/**\n * Declares a Cloudflare Workflow registration for a worker. The workflow\n * **class** itself lives in the worker's source code (extends\n * `WorkflowEntrypoint`); this config binds that class to a stable\n * Cloudflare-side workflow name and emits the `workflows[]` wrangler\n * binding so the worker can `env.{BINDING}.create(...)` instances.\n *\n * Tamer issues `PUT /accounts/{id}/workflows/{name}` on apply (idempotent;\n * Cloudflare treats PUT as upsert with `class_name` + `script_name`) and\n * `DELETE` on destroy. `script_name` defaults to the owning worker's\n * deployed name for `env`; override with {@link scriptName} when the class\n * is implemented in a sibling worker.\n */\nexport interface WorkflowResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare workflow registration name override. */\n cloudflareName?: CloudflareNameFn;\n /**\n * Class name exported by the worker that implements this workflow\n * (`export class BillingWorkflow extends WorkflowEntrypoint {...}` →\n * `className: \"BillingWorkflow\"`). Required by Cloudflare so it knows\n * which class in the bound script to instantiate per workflow run.\n */\n className: string;\n /**\n * Override the script that hosts the workflow class. Defaults to the\n * deployed name of the worker this resource is declared on (so dropping\n * the workflow into a different worker only needs moving the entry).\n * When set, written verbatim into the wrangler binding and the upsert\n * payload — Tamer does not env-suffix it.\n */\n scriptName?: string;\n /**\n * Override the wrangler binding key. Defaults to\n * `WF_{LOGICAL}_T_{TENANT}` — uppercased logical with the tenant id\n * appended for cross-stack uniqueness.\n */\n binding?: string;\n /**\n * Optional per-workflow execution limits. Mirrors Cloudflare's\n * `limits.steps` (max number of `step.do` calls per instance).\n */\n limits?: { steps?: number };\n}\n\n/**\n * Tamer-managed Cloudflare DNS record (zone-scoped).\n *\n * Declared at the **stack root** (`CfiConfigBase.dnsRecords`) rather than\n * per worker, because zone records have global identity (`zone + type +\n * name + content`) and shouldn't be redeclared per-worker. Tamer:\n *\n * - On `apply` for a new entry: `POST /zones/{zone_id}/dns_records` and\n * stores the assigned `recordId`.\n * - On `apply` for an existing entry whose mutable fields drifted from\n * state (`content`, `ttl`, `proxied`, `priority`, `comment`):\n * `PATCH /zones/{zone_id}/dns_records/{record_id}` (Cloudflare's\n * in-place update — works for everything except `type`/`name`).\n * - On `apply` for a `type` change: delete + recreate, since Cloudflare\n * rejects type changes on PATCH (and `name`-only changes are usually\n * semantic deletes anyway).\n * - On `destroy` of the stack (or when the entry is removed from config):\n * `DELETE /zones/{zone_id}/dns_records/{record_id}` and drops the row\n * from state.\n *\n * Tamer attaches a stable comment marker\n * (`tamer:<tenantId>:<env>:<logicalName>`) to every record it creates so\n * `tamer sync` and `tamer import` can rediscover orphaned rows after a\n * state loss.\n */\n/** Declared dataset for account Logpush — only `workers_trace_events` is implemented. */\nexport type LogpushWorkersTraceDataset = \"workers_trace_events\";\n\n/**\n * R2 destination for a Workers trace Logpush job. Requires S3-compatible\n * credentials (R2 API token) via the named environment variables at apply time.\n */\nexport interface LogpushJobR2Destination {\n /** Logical name of a Tamer-managed {@link R2ResourceConfig} in this stack. */\n bucketLogicalName: string;\n /** Path prefix inside the bucket before `{DATE}`; default `workers-trace-events`. */\n pathPrefix?: string;\n /** `process.env[name]` — R2 access key id for Logpush. */\n accessKeyIdEnv: string;\n /** `process.env[name]` — R2 secret access key for Logpush. */\n secretAccessKeyEnv: string;\n}\n\n/**\n * Logpush destination for **Pipelines HTTP ingest** (Workers trace → stream →\n * pipeline → sink). Tamer builds `destination_conf` as:\n * `https://{streamId}.ingest.cloudflare.com?pipeline_id={pipelineId}&header_Authorization=Bearer%20{token}`.\n *\n * The **stream**, **pipeline**, and **sink** must already exist (dashboard\n * wizard or Pipelines API). This block only encodes the Logpush wire format;\n * it does not create Pipelines resources.\n */\nexport interface LogpushJobPipelinesIngestDestination {\n /**\n * Pipelines **stream** id (32 hex characters, with or without UUID dashes).\n * Must match the stream that backs `https://…ingest.cloudflare.com`.\n */\n streamId: string;\n /** Pipelines **pipeline** id (32 hex characters, with or without dashes). */\n pipelineId: string;\n /**\n * `process.env[name]` — **ingest** Bearer token (dashboard “send token” /\n * stream HTTP auth). Becomes Logpush query param `header_Authorization`.\n */\n bearerTokenEnv: string;\n}\n\n/**\n * Fully automated **Workers trace → Pipelines → R2 Data Catalog (Iceberg)** path:\n * `tamer apply` creates the stream (trace schema), enables the catalog on the\n * bucket, mints two **account** API tokens (R2+Data Catalog + Pipelines Send),\n * stores the catalog credential, creates the `r2_data_catalog` sink, the SQL\n * pipeline, then the Logpush job. Mutually exclusive with\n * {@link LogpushJobPipelinesIngestDestination} and the other hand-supplied\n * `destination_conf` options.\n */\nexport interface LogpushJobPipelinesAutoDestination {\n /**\n * Tamer `resources.r2` logical name for the **catalog** bucket used as an\n * Apache Iceberg / **R2 Data Catalog** warehouse (not a separate R2 object\n * bucket used for raw NDJSON Logpush). That bucket must have **R2 Data Catalog** enabled. With\n * `pipelinesAuto`, Tamer calls `POST …/r2-catalog/{bucket}/enable` and\n * `…/credential` for you; if you provision the graph manually instead, enable\n * the catalog on this bucket in the R2 dashboard (or\n * `wrangler r2 bucket catalog enable`) before the `r2_data_catalog` sink can\n * work.\n */\n catalogBucketLogicalName: string;\n /** R2 Data Catalog / Iceberg namespace (default `default`). */\n namespace?: string;\n /**\n * Base Iceberg `table_name` in the catalog. On **new** `r2_data_catalog` sink\n * create, Tamer appends `_${Date.now()}` by default so the create path does\n * not collide (HTTP 422 / 1012) with a leftover table. Set\n * {@link tableNameAppendTimestamp} to `false` to use this string verbatim.\n */\n tableName: string;\n /**\n * When not `false`, new sinks use `${tableName}_${Date.now()}` (default). When\n * `false`, the sink uses `tableName` as-is. Existing sinks are unchanged.\n */\n tableNameAppendTimestamp?: boolean;\n /** Parquet `row_group_bytes` for the sink. Default 134217728. */\n sinkRowGroupBytes?: number;\n /** `rolling_policy.file_size_bytes` for the sink. Default 104857600. */\n sinkRollingFileSizeBytes?: number;\n /** `rolling_policy.interval_seconds` for the sink. Default 300. */\n sinkRollingIntervalSeconds?: number;\n}\n\nexport interface LogpushJobResourceConfig {\n logicalName: string;\n dataset: LogpushWorkersTraceDataset;\n /**\n * Cloudflare Logpush job `name` field. Defaults to\n * `tamer-{tenant.slug}-{logicalName}-{env}`.\n */\n jobName?: string;\n /** Build `destination_conf` for an R2 bucket in this stack. */\n r2?: LogpushJobR2Destination;\n /**\n * Build Logpush `destination_conf` for **Pipelines stream ingest** from stream\n * id, pipeline id, and ingest token (see {@link LogpushJobPipelinesIngestDestination}).\n * Mutually exclusive with {@link r2}, {@link destinationConfEnv},\n * {@link destinationConfFromJobId}, and {@link destinationConfFromJobIdEnv}.\n */\n pipelinesIngest?: LogpushJobPipelinesIngestDestination;\n /**\n * Create stream, R2 Data Catalog sink, and SQL pipeline via API, then Logpush\n * (see {@link LogpushJobPipelinesAutoDestination}).\n */\n pipelinesAuto?: LogpushJobPipelinesAutoDestination;\n /**\n * `process.env[name]` must hold the full Logpush `destination_conf` string.\n * Use for destinations where you paste the exact API value (escape hatch if\n * Cloudflare changes the Pipelines ingest URL shape).\n * Mutually exclusive with {@link r2}, {@link pipelinesIngest}, {@link pipelinesAuto},\n * {@link destinationConfFromJobId}, and {@link destinationConfFromJobIdEnv}.\n */\n destinationConfEnv?: string;\n /**\n * Bootstrap **Pipelines** (or any) `destination_conf` by reading an existing\n * account Logpush job via `GET …/logpush/jobs/{id}` — useful after creating a\n * template job in the dashboard. Tamer creates the managed job (canonical\n * name) with the same `destination_conf`. Mutually exclusive with {@link r2},\n * {@link pipelinesIngest}, {@link pipelinesAuto}, {@link destinationConfEnv}, and {@link destinationConfFromJobIdEnv}.\n */\n destinationConfFromJobId?: number;\n /**\n * `process.env[name]` must hold a positive integer job id; same behavior as\n * {@link destinationConfFromJobId} without hard-coding id in config.\n * Mutually exclusive with {@link r2}, {@link pipelinesIngest}, {@link pipelinesAuto},\n * {@link destinationConfEnv}, and {@link destinationConfFromJobId}.\n */\n destinationConfFromJobIdEnv?: string;\n /**\n * Optional Logpush **`filter`** (escaped JSON string with a top-level `where`\n * key). See [Logpush filters](https://developers.cloudflare.com/logs/logpush/logpush-job/filters/).\n * When using {@link destinationConfFromJobId} / {@link destinationConfFromJobIdEnv},\n * if unset, Tamer copies `filter` from the source job when present.\n */\n filter?: string;\n /**\n * Overrides **`output_options.field_names`** on the Logpush job. When\n * bootstrapping from {@link destinationConfFromJobId} / {@link destinationConfFromJobIdEnv},\n * other template **`output_options`** (e.g. `sample_rate`) are preserved unless\n * you replace them out-of-band on Cloudflare.\n */\n fieldNames?: string[];\n /** Default true. */\n enabled?: boolean;\n}\n\nexport interface DnsRecordResourceConfig {\n /** Stable identifier inside this stack (used in state keys and the comment marker). */\n logicalName: string;\n /** Cloudflare zone id this record lives in (e.g. `0123456789abcdef`). */\n zoneId: string;\n /**\n * Record type. Tamer supports the common edge / app types out of the\n * box. Anything else can still be expressed via the wrangler-side\n * config, but won't have lifecycle management here.\n */\n type:\n | \"A\"\n | \"AAAA\"\n | \"CNAME\"\n | \"TXT\"\n | \"MX\"\n | \"NS\"\n | \"CAA\"\n | \"SRV\"\n | \"PTR\"\n | \"HTTPS\"\n | \"SVCB\";\n /** DNS name (e.g. `app.example.com` or `@` for the apex). */\n name: string;\n /**\n * Record content (RDATA). Format depends on `type`:\n * - A → IPv4 address (`192.0.2.1`)\n * - AAAA → IPv6 address (`2001:db8::1`)\n * - CNAME / NS / PTR → hostname (`origin.example.com`)\n * - TXT → quoted text body (Tamer does not auto-quote — pass the\n * exact value Cloudflare should serve, e.g. `v=spf1 -all`)\n * - MX → mail server hostname (use {@link priority} for the preference)\n * - CAA → flags + tag + value as a single string\n * (`0 issue \"letsencrypt.org\"`)\n * - SRV → `priority weight port target` (e.g. `10 5 5223 server.example.com`)\n */\n content: string;\n /**\n * TTL in seconds. `1` means \"Auto\" (the Cloudflare default). Defaults to\n * `1` when unset.\n */\n ttl?: number;\n /**\n * Whether the record is proxied through Cloudflare (orange cloud).\n * Only meaningful for `A`, `AAAA`, and `CNAME`. Defaults to `false`.\n */\n proxied?: boolean;\n /**\n * Required for `MX`, `SRV`, and `URI` records. Ignored for other types.\n */\n priority?: number;\n /**\n * Free-form user comment appended to Tamer's attribution comment. Useful\n * for runbooks / \"why is this here\". Tamer always prefixes the comment\n * with `tamer:<tenantId>:<env>:<logicalName>` so live records can be\n * matched back to config even after state loss.\n */\n comment?: string;\n /**\n * Skip these envs (e.g. only create the record in `prod`). Default `[]`.\n * `local` is always implicitly skipped — DNS is a real-world side effect.\n */\n skipEnvs?: string[];\n /**\n * When true, `tamer destroy` does not delete this record (e.g. an apex\n * NS that should outlive the stack). Default false.\n */\n preserveOnDestroy?: boolean;\n}\n\n/** Declares a Workers for Platforms dispatch namespace to provision via `tamer apply`. */\nexport interface DispatchNamespaceResourceConfig {\n logicalName: string;\n /** Cloudflare dispatch namespace name (e.g. `workspace-workers`). */\n namespace: string;\n /**\n * When true, the real namespace name is `${namespace}-${env}` for non-`local`\n * envs. Skipped entirely for `local`.\n */\n envSuffix?: boolean;\n}\n\nexport interface WorkerResources {\n d1?: D1ResourceConfig[];\n r2?: R2ResourceConfig[];\n kv?: KVResourceConfig[];\n queues?: QueueResourceConfig[];\n hyperdrive?: HyperdriveResourceConfig[];\n vectorize?: VectorizeResourceConfig[];\n aiGateway?: AIGatewayResourceConfig[];\n pipelines?: PipelineResourceConfig[];\n workflows?: WorkflowResourceConfig[];\n /**\n * Cloudflare Secrets Store **stores** (account-scoped containers) managed by\n * Tamer. The secret values inside a store are intentionally **not** managed\n * here — they are written via Wrangler / dashboard / CI so secret material\n * never enters `tamer.config.ts` or `tamer-state-*`. Wire the actual\n * bindings via {@link secretsStoreSecrets} (which references stores by\n * logical name and resolves `store_id` from state at deploy time).\n */\n secretsStores?: SecretsStoreResourceConfig[];\n /**\n * Wrangler `secrets_store_secrets[]` bindings. **Not** Tamer-managed (no\n * state row, no apply/destroy lifecycle) — these are pure deploy-time\n * config that resolves the store reference (`store: <logicalName>`) into a\n * concrete `store_id` from a {@link SecretsStoreStateEntry} so wrangler\n * receives a stable id per env. The named secret must already exist in the\n * store (created out-of-band via wrangler `secrets-store secret create`).\n */\n secretsStoreSecrets?: SecretsStoreSecretBinding[];\n}\n\n/**\n * Cloudflare Secrets Store store managed by Tamer.\n *\n * Tamer creates the store on `apply` against\n * `POST /accounts/{id}/secrets_store/stores` (idempotent — a matching name\n * found via list short-circuits) and tracks the assigned `store_id` in\n * state. Cross-resource refs (`${tamer:secret_store:<n>.id|name}`) inject\n * the id into worker `vars` or other bindings at deploy time.\n *\n * Secret **values** are deliberately out of scope (rotation belongs in CI\n * / wrangler), so this resource has no `value` / `fromEnv` field — see\n * {@link SecretsStoreSecretBinding} for how to wire a secret into a worker.\n */\nexport interface SecretsStoreResourceConfig {\n logicalName: string;\n /** Optional per-resource Cloudflare Secrets Store name override. */\n cloudflareName?: CloudflareNameFn;\n}\n\n/**\n * One row in the worker's wrangler `secrets_store_secrets[]` array. Pure\n * deploy-time config — Tamer resolves `store` (a {@link\n * SecretsStoreResourceConfig.logicalName}) to the recorded `store_id` and\n * passes `secret_name` through verbatim. The secret itself must already\n * exist in the referenced store.\n */\nexport interface SecretsStoreSecretBinding {\n /** Wrangler binding name (e.g. `API_KEY`). Available on `env.API_KEY` at runtime. */\n binding: string;\n /** Logical name of a {@link SecretsStoreResourceConfig} declared on this worker. */\n store: string;\n /** Name of the secret inside the referenced store (created out-of-band). */\n secretName: string;\n}\n\n/**\n * Declarative HTTP route managed by Tamer.\n *\n * Per `docs/handoff.md` §6, hostnames are env-asymmetric: prod uses the bare\n * apex (`todo.com`, `admin.platform.com`); every other env prefixes\n * (`staging.todo.com`, `dev.todo.com`, `pr-1234.todo.com`). Resource names\n * remain `-{env}` suffixed regardless.\n *\n * Tamer expands one `RouteResourceConfig` into a wrangler `Route` per env at\n * `resolveWorkerConfig` time. `local` (and any env in `skipEnvs`) yields no\n * route so `wrangler dev` is unaffected.\n *\n * For routes wrangler should pass through verbatim, set `routes` on the\n * worker (inherited from `WranglerConfig`) instead of `tamerRoutes`.\n */\nexport interface RouteResourceConfig {\n /**\n * Apex hostname. Prod uses this bare; other envs prefix it with `{env}.`.\n */\n host: string;\n /**\n * Cloudflare zone name for wrangler's `ZoneNameRoute`. Defaults to {@link host}.\n * Set explicitly when the apex differs from the zone (e.g. multi-tenant\n * subdomain on a parent zone).\n */\n zone?: string;\n /**\n * Path glob appended after the resolved host. Default `/*`.\n */\n path?: string;\n /**\n * When true, register as a Cloudflare `custom_domain` route (wrangler's\n * `CustomDomainRoute`). The path glob is ignored for custom domains.\n */\n customDomain?: boolean;\n /**\n * Envs that should yield no route. Defaults to `[\"local\"]`.\n */\n skipEnvs?: string[];\n /**\n * Envs treated as \"prod\" (bare host). Defaults to `[\"prod\", \"production\"]`.\n */\n prodEnvs?: string[];\n}\n\n/**\n * Declares secrets a worker requires at deploy/runtime.\n *\n * **Names only — never values.** Secret material lives in the Tamer vault\n * (`tamer secrets set` / `load`) and reaches Cloudflare via `push` / `deploy`.\n * {@link WorkerSecretsConfig.required} is reconciled by `plan` and `drift`\n * against vault fingerprints and deployed worker presence.\n */\nexport interface WorkerSecretsConfig {\n /**\n * Logical secret names this worker needs at runtime (e.g. `STRIPE_KEY`).\n * Values are stored in the Tamer vault via `tamer secrets set` / `load` and\n * pushed to Cloudflare on `tamer secrets push` or `tamer deploy`. Reconciled\n * by `plan`, `drift`, and `tamer secrets verify` against vault fingerprints\n * and {@link SecretStateEntry.lastPushedHash} — never put plaintext here.\n */\n required: string[];\n}\n\n// ── Worker Config ─────────────────────────────────────────────────────────────\n\n/**\n * Pre-deploy build step for a worker (e.g. SPA asset compilation via\n * `vite build`). When set, `tamer deploy` spawns `command` in the worker's\n * `path` directory with the worker's resolved `vars` (references resolved\n * against state for the target env) as environment variables, so compile-time\n * substitution tools (Vite `VITE_*`, esbuild `define`, etc.) read values\n * authored in Tamer config directly — no `.env` file or codegen step.\n * See [docs/spa-build-config.md](../docs/spa-build-config.md).\n */\nexport interface WorkerBuildConfig {\n /**\n * Shell command run in the worker's `path` directory after `wrangler.json`\n * is written but before `wrangler types` / `wrangler deploy`. The worker's\n * resolved `vars` are passed as environment variables (merged onto\n * `process.env`).\n */\n command: string;\n}\n\ntype ManagedFields =\n | \"name\"\n | \"account_id\"\n | \"d1_databases\"\n | \"r2_buckets\"\n | \"kv_namespaces\"\n | \"queues\"\n | \"hyperdrive\"\n | \"vectorize\";\ntype BaseWranglerFields = Omit<WranglerConfig, ManagedFields>;\n\nexport interface EnvOverride extends Omit<\n BaseWranglerFields,\n \"vars\" | \"r2_buckets\"\n> {\n vars?: Record<string, TamerResolvableString>;\n /** Optional R2 buckets (passthrough to Wrangler; cross-stack imports). */\n r2_buckets?: WranglerR2BucketResolvable[];\n /** Overrides default generated Wrangler worker `name` for this environment. */\n scriptName?: string;\n /** Generated Wrangler config filename (default `wrangler.json`). */\n wranglerOutFile?: string;\n /** Per-env Tamer-managed routes (replaces base `tamerRoutes` for this env). */\n tamerRoutes?: RouteResourceConfig[];\n /**\n * Zone names (`route.zone_name`) where **`tamer deploy`** should delete stale\n * API-managed Workers routes still bound to this script but absent from resolved\n * `tamerRoutes`. Omit or leave empty for no pruning.\n */\n tamerStaleRouteSweepZones?: string[];\n /** Per-env override for the worker's pre-deploy {@link WorkerBuildConfig}. */\n build?: WorkerBuildConfig;\n}\n\nexport interface WorkerConfig extends Omit<\n BaseWranglerFields,\n \"vars\" | \"r2_buckets\" | \"env\" | \"local\"\n> {\n path?: string;\n config?: string;\n resources?: WorkerResources;\n /** Optional R2 buckets (passthrough to Wrangler; cross-stack imports). */\n r2_buckets?: WranglerR2BucketResolvable[];\n /** Tamer-managed routes; expanded per env into wrangler `routes[]`. */\n tamerRoutes?: RouteResourceConfig[];\n /**\n * Zone names where **`tamer deploy`** deletes orphaned Workers zone routes for\n * this script (patterns not in resolved `apiManagedRoutes`). Optional.\n */\n tamerStaleRouteSweepZones?: string[];\n alias?: Record<string, string>;\n vars?: Record<string, TamerResolvableString>;\n /**\n * When set, used as the Wrangler worker `name` (script name) instead of the default\n * `{slug}-{workerKey}-{env}-{tenantId}` pattern. Required for stable `services` targets.\n */\n scriptName?: string;\n /**\n * Basename of the generated Wrangler JSON file (default `wrangler.json`).\n * Use when multiple logical workers share one `path` (e.g. `wrangler.notes.json`).\n */\n wranglerOutFile?: string;\n /**\n * When set, `tamer deploy` passes `--dispatch-namespace` to Wrangler (Workers for Platforms user Worker upload).\n */\n dispatchNamespace?: string;\n /**\n * Required worker secrets (names only). See {@link WorkerSecretsConfig}.\n * Absent block means no declared secrets for this worker.\n */\n secrets?: WorkerSecretsConfig;\n /** Pre-deploy build step (e.g. `vite build` for SPA workers). See {@link WorkerBuildConfig}. */\n build?: WorkerBuildConfig;\n local?: EnvOverride;\n env?: Record<string, EnvOverride>;\n}\n\n// ── Top-level Config ──────────────────────────────────────────────────────────\n\n/**\n * CloudFormation-style stack identity. Multiple stacks can coexist in the\n * same `tamer-state-{env}` D1 — each writes to its own `cfi_state:{name}`\n * row — so cross-stack `${tamer:import:<stackName>.<output>}` references\n * can find sibling stacks. When omitted, the stack name defaults to\n * `tenant.slug`, which preserves existing single-stack semantics: a fresh\n * single-stack codebase writes to `cfi_state:{tenant.slug}` (was\n * `cfi_state` pre-0.26 — greenfield rename, no migration).\n */\nexport interface CfiStackConfig {\n /**\n * Stable stack identifier — used as the D1 row-key suffix\n * (`cfi_state:{name}`) and the namespace siblings address with\n * `${tamer:import:<name>.<output>}`. Must match\n * `^[a-zA-Z][a-zA-Z0-9_-]*$` (CloudFormation-style identifier).\n * Defaults to `tenant.slug` when unset.\n */\n name?: string;\n /** Free-form human description; surfaced by `tamer status`. */\n description?: string;\n}\n\ninterface CfiConfigBase {\n tenant: TenantMeta;\n account_id?: string;\n compatibility_date?: string;\n /**\n * Optional overrides for Cloudflare-side **names** (D1, R2, worker scripts,\n * workflows). Use for brownfield stacks whose live names differ from Tamer\n * defaults; IDs stay in state after `sync` / `apply`, never in config.\n * @see {@link NamingConventions}\n * @see docs/brownfield-adoption.md\n */\n naming?: NamingConventions;\n /** CloudFormation-style stack identity. See {@link CfiStackConfig}. */\n stack?: CfiStackConfig;\n /** Workers for Platforms dispatch namespaces to create on `tamer apply` / track in state. */\n dispatchNamespaces?: DispatchNamespaceResourceConfig[];\n /**\n * Cloudflare DNS records to create / track / destroy at the **tenant\n * scope** (zone-scoped, but declared once per stack rather than per\n * worker, so the same record isn't accidentally duplicated across\n * workers). Tamer matches against the live zone via the recorded\n * `recordId` (and a Tamer-attribution comment for sync rediscovery).\n * Updates issue `PATCH /zones/{id}/dns_records/{record_id}` for any\n * mutable field that drifted from state; type changes follow\n * Cloudflare's delete-and-recreate convention (see\n * https://developers.cloudflare.com/fundamentals/api/reference/deprecations/).\n */\n dnsRecords?: DnsRecordResourceConfig[];\n /**\n * Account-scoped [Workers Trace Events Logpush](https://developers.cloudflare.com/workers/observability/logs/logpush/)\n * jobs (`workers_trace_events`). Created on `tamer apply` after worker-bound\n * resources (e.g. R2) exist in state. Set `logpush: true` on **each** Worker\n * script whose traces should export (not only dispatch).\n */\n logpushJobs?: LogpushJobResourceConfig[];\n /**\n * Named exports this stack publishes — Tamer's CloudFormation `Outputs`\n * analogue. Each value is a single `${tamer:<kind>:<logical>.<field>}`\n * reference (interpolation also works) resolved against this stack's own\n * state at apply time and persisted into `CfiState.stackOutputs` so it\n * survives across runs and is visible to `tamer status` / external tooling.\n *\n * Cross-stack consumption (a sibling stack reading these via\n * `${tamer:import:<stackName>.<outputName>}`) requires that the producing\n * stack has already run `tamer apply` against the same env/account.\n *\n * Example:\n * ```ts\n * outputs: {\n * userDbId: cf.d1(\"users\").id,\n * queueName: cf.queue(\"events\").name,\n * }\n * ```\n *\n * Output names must match `^[a-zA-Z][a-zA-Z0-9_-]*$` (CloudFormation-ish).\n * Resolution is **strict**: an unresolved reference fails the apply\n * (rolled back if `--rollback-on-failure`). Read-only commands (`plan`,\n * `drift`, `status`) tolerate unresolved refs and show the placeholder\n * verbatim.\n */\n outputs?: Record<string, TamerResolvableString>;\n /**\n * Apply-time code generation hooks. Artifacts are gitignored siblings of\n * `wrangler.json` unless your stack commits them deliberately.\n */\n codegen?: CodegenConfig;\n}\n\n/** Single-worker stack: one top-level `worker` (no `workers` map). */\nexport interface CfiConfigSingle extends CfiConfigBase {\n worker: WorkerConfig;\n workers?: never;\n}\n\n/** Multi-worker stack: `workers` keyed by deploy target (e.g. `api`, `admin`). */\nexport interface CfiConfigMulti extends CfiConfigBase {\n workers: Record<string, WorkerConfig>;\n worker?: never;\n}\n\n/**\n * Root document for `tamer/project.config.ts` (or `tamer.project.config.ts`).\n * Single-worker stacks use {@link CfiConfigSingle}; multi-worker use\n * {@link CfiConfigMulti}.\n */\nexport type CfiConfig = CfiConfigSingle | CfiConfigMulti;\n\n// ── Helper functions ──────────────────────────────────────────────────────────\n\n/**\n * Identity helper for `tamer/project.config.ts` so the config object is typed\n * as {@link CfiConfig} in the IDE. No runtime transformation — the CLI loads\n * and validates the exported default.\n *\n * @example\n * ```ts\n * export default defineConfig({\n * tenant: { id: \"acme\", name: \"Acme\", slug: \"acme\" },\n * workers: { api: { main: \"workers/api/src/index.ts\" } },\n * });\n * ```\n */\nexport function defineConfig(config: CfiConfig): CfiConfig {\n return config;\n}\n\n/**\n * Optional metadata on env overlay objects. Stripped before merge; when set, it must\n * equal the CLI `--env` that selected this overlay (guards wrong file / copy-paste).\n */\nexport const TAMER_OVERLAY_ENV_KEY = \"tamerOverlayEnv\" as const;\n\n/**\n * Fragment merged onto {@link defineConfig} output from `tamer/env/<env>.config.ts`\n * (or flat `tamer.env.<env>.ts`). Typing is intentionally loose — shape is validated\n * after merge via the usual `CfiConfig` schema.\n *\n * Prefer the two-argument form so {@link TAMER_OVERLAY_ENV_KEY} is set and checked\n * against `--env`:\n *\n * ```ts\n * export default defineProjectOverlay(\"dev\", { account_id: \"…\" });\n * ```\n */\nexport function defineProjectOverlay<\n E extends string,\n T extends Record<string, unknown>,\n>(forEnv: E, fragment: T): T & { [K in typeof TAMER_OVERLAY_ENV_KEY]: E };\nexport function defineProjectOverlay<T extends Record<string, unknown>>(\n fragment: T,\n): T;\nexport function defineProjectOverlay(\n forEnvOrFragment: string | Record<string, unknown>,\n fragment?: Record<string, unknown>,\n): Record<string, unknown> {\n if (typeof forEnvOrFragment === \"string\") {\n if (\n fragment === undefined ||\n typeof fragment !== \"object\" ||\n fragment === null ||\n Array.isArray(fragment)\n ) {\n throw new Error(\n \"defineProjectOverlay(forEnv, fragment): fragment must be a plain object\",\n );\n }\n return {\n [TAMER_OVERLAY_ENV_KEY]: forEnvOrFragment,\n ...(fragment as Record<string, unknown>),\n };\n }\n return forEnvOrFragment as Record<string, unknown>;\n}\n\n/**\n * Identity helper for one entry in `workers` (or the sole `worker` field).\n * Same pattern as {@link defineConfig} — improves hover types only.\n */\nexport function defineWorker(config: WorkerConfig): WorkerConfig {\n return config;\n}\n\nexport function getDispatchNamespaces(\n config: CfiConfig,\n): DispatchNamespaceResourceConfig[] {\n return config.dispatchNamespaces ?? [];\n}\n\nexport function getDnsRecords(config: CfiConfig): DnsRecordResourceConfig[] {\n return config.dnsRecords ?? [];\n}\n\nexport function getLogpushJobs(config: CfiConfig): LogpushJobResourceConfig[] {\n return config.logpushJobs ?? [];\n}\n\n// ── State ─────────────────────────────────────────────────────────────────────\n\nexport interface D1StateEntry {\n type: \"d1_database\";\n logicalName: string;\n shardDate?: string;\n derivedName: string;\n bindingKey: string;\n cfId: string;\n migrationsDir?: string;\n /** When true, `tamer destroy` skips deleting this D1. */\n preserveOnDestroy?: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface R2StateEntry {\n type: \"r2_bucket\";\n logicalName: string;\n createdDate: string;\n derivedName: string;\n bindingKey: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface KVStateEntry {\n type: \"kv_namespace\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n cfId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface QueueStateEntry {\n type: \"queue\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Cloudflare queue id (uuid). */\n cfId: string;\n /** Whether Tamer emits a producer binding for this queue (false for consumer-only). */\n producerBinding: boolean;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface VectorizeStateEntry {\n type: \"vectorize\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Vectorize index id (uuid) returned by the v2 create endpoint. */\n cfId: string;\n /** Immutable vector dimension. */\n dimensions: number;\n /** Immutable distance metric. */\n metric: \"cosine\" | \"euclidean\" | \"dot-product\";\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface HyperdriveStateEntry {\n type: \"hyperdrive\";\n logicalName: string;\n derivedName: string;\n bindingKey: string;\n /** Cloudflare hyperdrive config id. */\n cfId: string;\n /** Origin database engine (purely informational). */\n scheme: \"postgres\" | \"postgresql\" | \"mysql\";\n /** Origin host (purely informational). */\n originHost: string;\n /** Origin database name (purely informational). */\n originDatabase: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface AIGatewayStateEntry {\n type: \"ai_gateway\";\n logicalName: string;\n /** Cloudflare gateway id (== derived slug). */\n derivedName: string;\n /** Stable cross-reference binding key (no Wrangler binding emitted). */\n bindingKey: string;\n cfId: string;\n cacheTtl: number;\n cacheInvalidateOnUpdate: boolean;\n collectLogs: boolean;\n authentication: boolean;\n rateLimitingInterval: number;\n rateLimitingLimit: number;\n rateLimitingTechnique: \"fixed\" | \"sliding\";\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface PipelineStateEntry {\n type: \"pipeline\";\n logicalName: string;\n /** Tamer-derived pipeline name sent to Cloudflare on create. */\n derivedName: string;\n /** Wrangler binding key emitted in `pipelines[]`. */\n bindingKey: string;\n /** Server-assigned pipeline id (referenced from wrangler `pipeline`). */\n cfId: string;\n /** Pipeline SQL as last applied. Tracked for drift detection. */\n sql: string;\n /** Cloudflare-reported lifecycle status (e.g. \"running\", \"stopped\"). */\n status?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface WorkflowStateEntry {\n type: \"workflow\";\n logicalName: string;\n /** Tamer-derived workflow name sent to Cloudflare on PUT. */\n derivedName: string;\n /** Wrangler binding key emitted in `workflows[]`. */\n bindingKey: string;\n /** Server-assigned workflow id returned by PUT. */\n cfId: string;\n /** Class name of the `WorkflowEntrypoint` subclass (drift target). */\n className: string;\n /**\n * Worker script that hosts the class — either the owning worker's\n * deployed name, or `WorkflowResourceConfig.scriptName` verbatim.\n */\n scriptName: string;\n /** Optional execution-limit override last applied. */\n limits?: { steps?: number };\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface SecretsStoreStateEntry {\n type: \"secrets_store\";\n logicalName: string;\n /** Tamer-derived store name sent to Cloudflare on create. */\n derivedName: string;\n /** Stable cross-reference key (no Wrangler binding emitted directly). */\n bindingKey: string;\n /** Server-assigned store id (referenced from wrangler `secrets_store_secrets[].store_id`). */\n cfId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface DnsRecordStateEntry {\n type: \"dns_record\";\n logicalName: string;\n zoneId: string;\n recordType:\n | \"A\"\n | \"AAAA\"\n | \"CNAME\"\n | \"TXT\"\n | \"MX\"\n | \"NS\"\n | \"CAA\"\n | \"SRV\"\n | \"PTR\"\n | \"HTTPS\"\n | \"SVCB\";\n /** DNS name as recorded by Cloudflare (always FQDN; `@` is expanded to the zone apex). */\n name: string;\n /** Last-applied content, kept for drift comparison. */\n content: string;\n /** Last-applied TTL (seconds; `1` means \"Auto\"). */\n ttl: number;\n /** Last-applied proxied flag. */\n proxied: boolean;\n /** Last-applied priority for MX/SRV/URI; `undefined` for record types without one. */\n priority?: number;\n /** Full comment as written to Cloudflare (includes the Tamer attribution prefix). */\n comment: string;\n /** Cloudflare record id (`0123…`). */\n recordId: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface DispatchNamespaceStateEntry {\n type: \"dispatch_namespace\";\n logicalName: string;\n /** Cloudflare namespace name. */\n derivedName: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface LogpushJobStateEntry {\n type: \"logpush_job\";\n logicalName: string;\n /** Cloudflare Logpush job display name (matches API `name`). */\n derivedName: string;\n /** Numeric job id from `POST /accounts/{id}/logpush/jobs`. */\n cfJobId: number;\n dataset: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Tamer-owned Pipelines graph for {@link LogpushJobPipelinesAutoDestination}\n * (stream, sink, SQL pipeline) once `ensurePipelinesLogpushProvision` runs.\n */\nexport interface LogpushPipelinesStateEntry {\n type: \"logpush_pipelines\";\n logicalName: string;\n streamId: string;\n /**\n * Ingest origin from Pipelines stream `endpoint` when the API returns it\n * (e.g. `https://….ingest.cloudflare.com`). Otherwise Logpush URL is built from {@link streamId}.\n */\n streamIngestBaseUrl?: string;\n sinkId: string;\n pipelineId: string;\n streamName: string;\n sinkName: string;\n pipelineName: string;\n /**\n * Table identifier to use in **R2 SQL** (`SHOW TABLES`, `SELECT`), after\n * Tamer normalizes Pipelines’ suffixed Iceberg name to the catalog/SQL name.\n */\n r2DataCatalogTableName?: string;\n /**\n * Table name **Pipelines / sink GET** reported before normalization (e.g.\n * `worker_trace_events_173…`). For display and debugging; queries use\n * {@link r2DataCatalogTableName}.\n */\n r2DataCatalogTableNamePipelines?: string;\n r2DataCatalogNamespace?: string;\n catalogBucketDerivedName: string;\n /**\n * Account API token id + secret minted for R2 + R2 Data Catalog (catalog\n * credential + `r2_data_catalog` sink). Secrets live only in Tamer state\n * (like dashboard-stored values, but file-local).\n */\n mintedR2CatalogTokenId?: string;\n mintedR2CatalogTokenValue?: string;\n /**\n * Account API token id + secret for [Workers Pipelines\n * Send](https://developers.cloudflare.com/pipelines/streams/writing-to-streams/)\n * (Logpush `destination_conf` stream ingest).\n */\n mintedPipelinesSendTokenId?: string;\n mintedPipelinesSendTokenValue?: string;\n createdAt: string;\n updatedAt: string;\n}\n\nexport interface WorkerRouteStateEntry {\n type: \"worker_route\";\n workerKey: string;\n /** Deployed Worker script name (same as wrangler `name`). */\n workerName: string;\n zoneId: string;\n zoneName: string;\n routeId: string;\n pattern: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Last-pushed fingerprint for a worker secret — **no secret material**.\n *\n * Written by `tamer secrets push` and `tamer deploy` after a successful CF API\n * PUT. Stored in {@link CfiState.resources} under key `secret:{worker}:{name}`.\n * Compared to the vault row's `value_hash` to detect rotation (`vault hash !=\n * lastPushedHash` → needs push). Keyed per secret × worker × env so the same\n * secret can be current on one worker and stale on another.\n */\nexport interface SecretStateEntry {\n type: \"secret\";\n /** Worker key from `tamer.config.ts` (e.g. `api`). */\n worker: string;\n /** Logical secret name (e.g. `STRIPE_KEY`). */\n name: string;\n /** Vault `value_hash` last PUT to this worker for this env. */\n lastPushedHash: string;\n /** ISO timestamp of the last successful push. */\n lastPushedAt: string;\n}\n\nexport type StateEntry =\n | D1StateEntry\n | R2StateEntry\n | KVStateEntry\n | QueueStateEntry\n | HyperdriveStateEntry\n | VectorizeStateEntry\n | AIGatewayStateEntry\n | PipelineStateEntry\n | WorkflowStateEntry\n | SecretsStoreStateEntry\n | DnsRecordStateEntry\n | DispatchNamespaceStateEntry\n | LogpushJobStateEntry\n | LogpushPipelinesStateEntry\n | WorkerRouteStateEntry\n | SecretStateEntry;\n\n/** Provisioning lifecycle for a workspace tenant (`product` + `workspace`). */\nexport type ProvisioningStatus =\n | \"pending\"\n | \"d1_created\"\n | \"migrations_applied\"\n | \"script_uploaded\"\n | \"ready\"\n | \"tombstoned\";\n\nexport interface TenantD1ShardRef {\n role: string;\n derivedName: string;\n cfId: string;\n}\n\n/**\n * One runtime-provisioned tenant (dispatch script + optional D1 shards).\n * Keyed in {@link CfiState.tenants} by `product:workspace`.\n */\nexport interface TenantStateEntry {\n product: string;\n workspace: string;\n provisioningStatus: ProvisioningStatus;\n dispatchNamespaceName: string;\n scriptName: string;\n d1Shards?: TenantD1ShardRef[];\n createdAt: string;\n updatedAt: string;\n}\n\n/**\n * Lightweight CloudFormation-style metadata about the deployment \"stack\" that\n * owns this state row. Optional everywhere — schema-version bump aware, but\n * commands set them when they have the information.\n */\nexport interface CfiStackMeta {\n /**\n * Human label for the stack (e.g. `external`, `internal`). Defaults to the\n * tenant slug when unset.\n */\n name?: string;\n /** Free-form owner string (team / pipeline). */\n owner?: string;\n}\n\nexport type CfiOperationName =\n | \"bootstrap\"\n | \"apply\"\n | \"deploy\"\n | \"destroy\"\n | \"provision-tenant\"\n | \"destroy-tenant\"\n | \"import\"\n | \"sync\";\n\nexport type CfiOperationStatus = \"in_progress\" | \"succeeded\" | \"failed\";\n\nexport interface CfiOperationRecord {\n command: CfiOperationName;\n status: CfiOperationStatus;\n startedAt: string;\n completedAt?: string;\n errorMessage?: string;\n /** Optional free-form context (target worker, tenant, etc.). */\n detail?: string;\n}\n\nexport interface CfiState {\n tenantId: string;\n env: string;\n schemaVersion: number;\n syncedAt: string;\n resources: Record<string, StateEntry>;\n /** Optimistic concurrency for D1 `cfi_state` row (see `StateManager.persist`). */\n revision?: number;\n /** Workspace tenants provisioned at runtime (not in `tamer.config.ts`). */\n tenants?: Record<string, TenantStateEntry>;\n /** Stack metadata (CloudFormation-style). Optional. */\n stack?: CfiStackMeta;\n /**\n * Resolved + persisted values for every entry in `tamer.config.ts > outputs`.\n * Written at the end of a successful `apply`; surfaced by `tamer status`\n * and consumed by sibling stacks via `${tamer:import:<stackName>.<key>}`.\n * Keys mirror `outputs` keys 1:1; entries are dropped when removed from\n * config or when the stack is destroyed.\n */\n stackOutputs?: Record<string, CfiStackOutputValue>;\n /** Last operation that touched this state row. */\n lastOperation?: CfiOperationRecord;\n /**\n * Completed operations only (`succeeded` / `failed`), newest first. Capped\n * at 50 on write when an operation finishes; surfaced by `tamer events`.\n */\n operationHistory?: CfiOperationRecord[];\n}\n\n/**\n * One persisted entry in {@link CfiState.stackOutputs}. Stores the resolved\n * literal alongside the original `${tamer:...}` source for diffing/debugging\n * and a timestamp so `tamer status` can show staleness.\n */\nexport interface CfiStackOutputValue {\n /** Resolved literal value (e.g. a D1 cfId, R2 bucket name, route URL). */\n value: string;\n /** The original `${tamer:...}` reference from `outputs` at resolve time. */\n source: string;\n /** ISO timestamp the value was last resolved + persisted. */\n resolvedAt: string;\n}\n\n// ── Status ────────────────────────────────────────────────────────────────────\n\nexport type ResourceStatus = \"ok\" | \"missing\" | \"pending\" | \"error\";\n\nexport interface WorkerStatus {\n workerKey: string;\n deployedName: string;\n route?: string;\n status: ResourceStatus;\n d1: Array<{\n binding: string;\n name: string;\n cfId: string;\n status: ResourceStatus;\n }>;\n r2: Array<{ binding: string; name: string; status: ResourceStatus }>;\n error?: string;\n}\n\nexport interface TenantStatus {\n tenant: TenantMeta;\n env: string;\n workers: WorkerStatus[];\n}\n\n// ── Naming conventions ────────────────────────────────────────────────────────\n\n/**\n * Optional functions that derive **Cloudflare resource names** from config\n * (`logicalName`, `tenant.id`, `env`, etc.). Used when a resource has no\n * {@link CloudflareNameFn} override on its config.\n *\n * **Resolution order (managed resources):**\n * 1. `resource.cloudflareName?(tenantId, env, ctx?)`\n * 2. stack `naming.{kindHook}?(...)` (this interface)\n * 3. {@link NamingEngine} built-in default\n *\n * **Greenfield:** omit both per-resource overrides and this block.\n *\n * **Brownfield:** use per-resource `cloudflareName` when names differ per\n * logical resource; use stack hooks for shared formulas. Config holds logical\n * names and hooks; **state** holds `cfId` after `tamer sync`.\n *\n * @see docs/brownfield-adoption.md\n * @see docs/per-resource-cloudflare-naming.md\n */\nexport interface NamingConventions {\n /**\n * Cloudflare D1 database name for `resources.d1[]` with `type: \"single\"`.\n *\n * **Default:** `db_{logicalName}_t_{tenantId}_{env}`.\n *\n * **`sync`:** exact match on the derived name.\n */\n d1Single?: (logicalName: string, tenantId: string, env: string) => string;\n /**\n * Cloudflare D1 database name for `resources.d1[]` with `type: \"sharded\"`.\n *\n * **`date`:** shard stamp passed to apply (`YYYY-MM-DD` or compact\n * `YYYYMMDD` after normalization). Your hook should embed it the same way\n * legacy DBs do (often `date.replace(/-/g, \"\")`).\n *\n * **Default:** `db_{logicalName}_{YYYYMMDD}_t_{tenantId}_{env}` (omits\n * `{logicalName}_` when logical is `default` or empty).\n *\n * **`sync`:** with a custom hook, **derive-and-match** — Tamer parses the\n * shard date from each account D1 name (`_YYYYMMDD_t_` or `_YYYY_MM_DD_t_`),\n * re-derives via this function, and adopts when the names are equal. Without\n * a hook, prefix/suffix regex matching is used instead.\n */\n d1Shard?: (\n logicalName: string,\n date: string,\n tenantId: string,\n env: string,\n ) => string;\n /**\n * R2 bucket name for `resources.r2[]`.\n *\n * **`date`:** `YYYYMMDD` (no dashes) from the calendar day **apply** or\n * **sync** runs. Ignore unless your legacy scheme embeds a date stamp.\n *\n * **Default:** `r2-{logicalName}-t-{tenantId}-{env}` (no date segment).\n *\n * **`sync`:** exact match on the name produced by calling this hook with\n * today's date (same as apply). Legacy dated buckets\n * `r2-{logical}-YYYYMMDD-t-{tenant}-{env}` still match when the hook is\n * omitted.\n */\n r2Bucket?: (\n logicalName: string,\n date: string,\n tenantId: string,\n env: string,\n ) => string;\n /**\n * Deployed Worker **script** name (wrangler `name` / `tamer deploy` target).\n *\n * **Default:** `{slug}-{workerKey}-{tenantId}` for `local`; otherwise\n * `{slug}-{workerKey}-{env}-{tenantId}`. Overridable per worker via\n * {@link WorkerConfig.scriptName}.\n *\n * **`sync`:** worker script list is matched by deployed name (routes use\n * the same derivation).\n */\n workerName?: (\n slug: string,\n workerKey: string,\n env: string,\n tenantId: string,\n ) => string;\n /**\n * Cloudflare workflow **registration** name for `resources.workflows[]`.\n * Workflow names are immutable on Cloudflare — brownfield stacks must\n * return the live registration name here.\n *\n * **Default:** `wf-{logicalName}-t-{tenantId}-{env}` (lowercased).\n *\n * **`sync`:** exact match on the derived name (same as D1 single / default\n * R2). Prefer `tamer import --kind workflow` only when a legacy name cannot\n * be expressed as a function of logical name, tenant, and env.\n */\n workflow?: (logicalName: string, tenantId: string, env: string) => string;\n}\n","/**\n * Cloudflare-shaped authoring helpers for Tamer configs.\n *\n * Values are plain objects resolved by `materializeCloudflareBindings()` into the\n * same `${tamer:…}` strings the reference resolver already understands — no\n * second resolution path, no Cloudflare-agnostic indirection.\n */\n\nexport type CfResourceKind =\n | \"d1\"\n | \"r2\"\n | \"kv\"\n | \"queue\"\n | \"hyperdrive\"\n | \"vectorize\"\n | \"ai_gateway\"\n | \"pipeline\"\n | \"workflow\"\n | \"secret_store\";\n\nexport type CfResourceField = \"name\" | \"id\" | \"binding\";\n\nexport type CfLogpushPipelinesField =\n | \"r2_data_catalog_table_name\"\n | \"r2_data_catalog_table_name_pipelines\"\n | \"r2_data_catalog_namespace\"\n | \"name\"\n | \"id\"\n | \"iceberg_table\"\n | \"iceberg_table_pipelines\"\n | \"iceberg_namespace\";\n\nexport type CfBindingSpec =\n | { t: \"resource\"; kind: CfResourceKind; logical: string; field: CfResourceField }\n | { t: \"dispatch_namespace\"; logical: string; field: \"name\" | \"id\" }\n | { t: \"worker\"; workerKey: string; field: \"name\" }\n | { t: \"logpush_pipelines\"; logical: string; field: CfLogpushPipelinesField }\n | { t: \"config\"; logical: \"stack\"; field: \"account_id\" }\n | { t: \"import\"; stack: string; output: string };\n\n/**\n * Opaque handle materialized to a `${tamer:…}` reference before config parse.\n */\nexport class CfBinding {\n constructor(public readonly spec: CfBindingSpec) {}\n\n toRefString(): string {\n return cfBindingSpecToTamerRef(this.spec);\n }\n}\n\nexport function cfBindingSpecToTamerRef(spec: CfBindingSpec): string {\n switch (spec.t) {\n case \"resource\":\n return `\\${tamer:${spec.kind}:${spec.logical}.${spec.field}}`;\n case \"dispatch_namespace\":\n return `\\${tamer:dispatch_namespace:${spec.logical}.${spec.field}}`;\n case \"worker\":\n return `\\${tamer:worker:${spec.workerKey}.${spec.field}}`;\n case \"logpush_pipelines\":\n return `\\${tamer:logpush_pipelines:${spec.logical}.${spec.field}}`;\n case \"config\":\n return `\\${tamer:config:${spec.logical}.${spec.field}}`;\n case \"import\":\n return `\\${tamer:import:${spec.stack}.${spec.output}}`;\n default: {\n const _x: never = spec;\n return _x;\n }\n }\n}\n\nfunction resource(logical: string, kind: CfResourceKind) {\n return {\n get name(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"name\" });\n },\n get id(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"id\" });\n },\n get binding(): CfBinding {\n return new CfBinding({ t: \"resource\", kind, logical, field: \"binding\" });\n },\n };\n}\n\n/**\n * Fluent Cloudflare resource references for `vars`, `outputs`, `tamerRoutes`, etc.\n *\n * @example\n * ```ts\n * vars: {\n * BUCKET: cf.r2(\"assets\").name,\n * ACCOUNT: cf.stack.accountId,\n * }\n * ```\n */\nexport const cf = {\n d1: (logical: string) => resource(logical, \"d1\"),\n r2: (logical: string) => resource(logical, \"r2\"),\n kv: (logical: string) => resource(logical, \"kv\"),\n queue: (logical: string) => resource(logical, \"queue\"),\n hyperdrive: (logical: string) => resource(logical, \"hyperdrive\"),\n vectorize: (logical: string) => resource(logical, \"vectorize\"),\n aiGateway: (logical: string) => resource(logical, \"ai_gateway\"),\n pipeline: (logical: string) => resource(logical, \"pipeline\"),\n workflow: (logical: string) => resource(logical, \"workflow\"),\n secretStore: (logical: string) => resource(logical, \"secret_store\"),\n\n dispatchNamespace: (logical: string) => ({\n get name(): CfBinding {\n return new CfBinding({ t: \"dispatch_namespace\", logical, field: \"name\" });\n },\n get id(): CfBinding {\n return new CfBinding({ t: \"dispatch_namespace\", logical, field: \"id\" });\n },\n }),\n\n /** Deployed Worker script name for this env (Wrangler `name`). */\n worker: (workerKey: string) => ({\n get name(): CfBinding {\n return new CfBinding({ t: \"worker\", workerKey, field: \"name\" });\n },\n }),\n\n logpushPipelines: (logical: string) => ({\n get r2DataCatalogTableName(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_table_name\",\n });\n },\n get r2DataCatalogTableNamePipelines(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_table_name_pipelines\",\n });\n },\n get r2DataCatalogNamespace(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"r2_data_catalog_namespace\",\n });\n },\n get pipelineName(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"name\" });\n },\n get pipelineId(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"id\" });\n },\n /** Alias for {@link r2DataCatalogTableName}. */\n get icebergTable(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"iceberg_table\" });\n },\n /** Alias for {@link r2DataCatalogTableNamePipelines}. */\n get icebergTablePipelines(): CfBinding {\n return new CfBinding({\n t: \"logpush_pipelines\",\n logical,\n field: \"iceberg_table_pipelines\",\n });\n },\n /** Alias for {@link r2DataCatalogNamespace}. */\n get icebergNamespace(): CfBinding {\n return new CfBinding({ t: \"logpush_pipelines\", logical, field: \"iceberg_namespace\" });\n },\n }),\n\n stack: {\n get accountId(): CfBinding {\n return new CfBinding({ t: \"config\", logical: \"stack\", field: \"account_id\" });\n },\n },\n\n /** Cross-stack: sibling stack name + published output key. */\n import: (stack: string, output: string) =>\n new CfBinding({ t: \"import\", stack, output }),\n} as const;\n\nexport function isCfBinding(x: unknown): x is CfBinding {\n return x instanceof CfBinding;\n}\n","import type { TamerResolvableString } from \"../types.js\";\nimport { CfBinding, isCfBinding } from \"./cloudflare-bindings.js\";\n\n/** Coerce a post-load or inline authoring value to a `${tamer:…}` / literal string. */\nexport function materializeTamerResolvable(v: TamerResolvableString): string {\n return isCfBinding(v) ? v.toRefString() : v;\n}\n\nexport function materializeVars(\n vars: Record<string, TamerResolvableString> | undefined,\n): Record<string, string> | undefined {\n if (!vars) return undefined;\n return Object.fromEntries(\n Object.entries(vars).map(([k, v]) => [k, materializeTamerResolvable(v)]),\n );\n}\n\n/**\n * Walk a config-shaped value and replace every {@link CfBinding} with its\n * `${tamer:…}` string. Preserves functions (e.g. `naming` callbacks) and\n * non-plain objects are left as-is after a shallow binding check.\n */\nexport function materializeCloudflareBindings(value: unknown): unknown {\n if (isCfBinding(value)) {\n return value.toRefString();\n }\n if (value === null || value === undefined) {\n return value;\n }\n const t = typeof value;\n if (t === \"string\" || t === \"number\" || t === \"boolean\" || t === \"bigint\") {\n return value;\n }\n if (t === \"function\") {\n return value;\n }\n if (value instanceof Date) {\n return value;\n }\n if (Array.isArray(value)) {\n return value.map(materializeCloudflareBindings);\n }\n if (t !== \"object\") {\n return value;\n }\n\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n out[k] = materializeCloudflareBindings(v);\n }\n return out;\n}\n"],"mappings":"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"}
@@ -1,15 +1,15 @@
1
1
  import { f as getDispatchNamespaces, m as getLogpushJobs, p as getDnsRecords } from "./normalize-DVSTRZhO.mjs";
2
- import { H as getWorkers, O as resolveDeployedWorkerName, R as CFApiClient, U as loadConfig, a as reconcileSecrets, b as createEmptyCfiState, c as vaultReaderFromMap, d as requiredSecretsForWorker, f as fetchStackImports, h as StateManager, k as resolveWorkerConfig, s as secretsPlanItems, u as namingFromConfig, w as stackNameForConfig, z as cloudflareAccountIdFromEnv } from "./tamer.mjs";
3
- import { a as logicalNamesForResourceKind, n as resourceModules } from "./registry-CTerXUza.mjs";
4
- import "./r2S3EmptyBucket-DD81ZWQ7.mjs";
5
- import { a as signFor, t as formatPlanFieldChanges } from "./planFormat-CJw8Kq2s.mjs";
6
- import { t as computeDriftReport } from "./drift-C5r1cKxV.mjs";
7
- import { r as dnsRecordStateKey } from "./dns-records.resolve-SPYGYNHa.mjs";
8
- import { a as dnsRecordDiffPlanItems, n as filterPlanItemsForTarget, r as parseApplyTarget, t as assertApplyTargetDeclared } from "./applyTarget-CJSn9DeJ.mjs";
9
- import { o as logpushJobDiffPlanItems, s as logpushJobStateKey } from "./logpush-job-Dqlt-wEw.mjs";
10
- import "./worker-route-BapxsQyX.mjs";
11
- import { n as PLAN_FILE_FORMAT, o as writePlanFile, r as computeAttestation, t as buildCloudflareSnapshot } from "./cloudflareSnapshot-DqP8v_xG.mjs";
12
- import "./workers-D7ow_joN.mjs";
2
+ import { $ as CFApiClient, N as resolveDeployedWorkerName, P as resolveWorkerConfig, T as createEmptyCfiState, _ as fetchStackImports, a as reconcileSecrets, b as StateManager, c as vaultReaderFromMap, et as cloudflareAccountIdFromEnv, g as requiredSecretsForWorker, h as namingFromConfig, it as loadConfig, k as stackNameForConfig, rt as getWorkers, s as secretsPlanItems } from "./tamer.mjs";
3
+ import { a as logicalNamesForResourceKind, n as resourceModules } from "./registry-CRgobiU4.mjs";
4
+ import "./r2S3EmptyBucket-CDE2lTM8.mjs";
5
+ import { a as signFor, t as formatPlanFieldChanges } from "./planFormat-C8mq3sbD.mjs";
6
+ import { t as computeDriftReport } from "./drift-B-oPsgPW.mjs";
7
+ import { r as dnsRecordStateKey } from "./dns-records.resolve-CUQz6Ivz.mjs";
8
+ import { a as dnsRecordDiffPlanItems, n as filterPlanItemsForTarget, r as parseApplyTarget, t as assertApplyTargetDeclared } from "./applyTarget-CTsOrwco.mjs";
9
+ import { o as logpushJobDiffPlanItems, s as logpushJobStateKey } from "./logpush-job-DZG-3nkJ.mjs";
10
+ import "./worker-route-BPDAwUL0.mjs";
11
+ import { n as PLAN_FILE_FORMAT, o as writePlanFile, r as computeAttestation, t as buildCloudflareSnapshot } from "./cloudflareSnapshot-dKYupX1L.mjs";
12
+ import "./workers-D08-ri2Q.mjs";
13
13
 
14
14
  //#region src/core/plan/computePlan.ts
15
15
  /**
@@ -475,4 +475,4 @@ function labelFor(kind) {
475
475
 
476
476
  //#endregion
477
477
  export { runPlan };
478
- //# sourceMappingURL=plan-D7UyLznb.mjs.map
478
+ //# sourceMappingURL=plan-Dp2aTzEU.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"plan-D7UyLznb.mjs","names":["items: PlanItem[]","byKind: Record<PlanResourceKind, number>","byAction: Record<PlanAction, number>","attestation"],"sources":["../src/core/plan/computePlan.ts","../src/core/plan/plan.types.ts","../src/cli/commands/plan.ts"],"sourcesContent":["import { loadConfig, getWorkers } from \"../config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../cloudflareEnv.js\";\nimport { namingFromConfig } from \"../config/namingFromConfig.js\";\nimport { StateManager } from \"../state/StateManager.js\";\nimport { stackNameForConfig } from \"../state/stackName.js\";\nimport { fetchStackImports } from \"../imports/fetchStackImports.js\";\nimport { CFApiClient } from \"../api/CFApiClient.js\";\nimport { computeDriftReport } from \"../../cli/commands/drift.js\";\nimport { resolveWorkerConfig } from \"../config/resolver.js\";\nimport { resourceModules } from \"../registry/registry.js\";\nimport { logicalNamesForResourceKind } from \"../config/resourcesFromConfig.js\";\nimport { resolveDeployedWorkerName } from \"../config/resolver.js\";\nimport { requiredSecretsForWorker } from \"../secrets/declared.js\";\nimport {\n reconcileSecrets,\n secretsPlanItems,\n vaultReaderFromMap,\n type SecretsVaultReader,\n type VaultSecretMeta,\n} from \"../secrets/reconcile.js\";\nimport { getDispatchNamespaces, getDnsRecords, getLogpushJobs } from \"../../types.js\";\nimport { logpushJobStateKey } from \"../../features/logpush-job/logpush-job.resolve.js\";\nimport { logpushJobDiffPlanItems } from \"../../features/logpush-job/logpush-job.diff.js\";\nimport {\n dnsRecordDiffPlanItems,\n} from \"../../features/dns-records/dns-records.diff.js\";\nimport { dnsRecordStateKey } from \"../../features/dns-records/dns-records.resolve.js\";\nimport type { StateEntry } from \"../../types.js\";\nimport type { PlanItem, PlanReport, PlanResourceKind } from \"./plan.types.js\";\nimport {\n filterPlanItemsForTarget,\n type ApplyTarget,\n} from \"../apply/applyTarget.js\";\n\n/**\n * Compute the planned set of changes for `env` given the current\n * `tamer.config.ts` and recorded state. Read-only: hits the Cloudflare API to\n * detect what is missing but never writes.\n *\n * The \"would create\" set is derived from `computeDriftReport().undeployed`:\n * declared in config, present neither in state nor on Cloudflare. Worker\n * **scripts** are queried directly (`workersScriptGet`) since they are not\n * tracked as state resources today.\n */\nexport async function computePlan(options: {\n env?: string;\n configPath?: string;\n /** When set, only plan items for this `kind:logical` are returned. */\n target?: ApplyTarget;\n /** Optional vault reader for secret reconciliation (defaults to empty). */\n secretsVault?: SecretsVaultReader;\n}): Promise<PlanReport> {\n const env = options.env ?? \"local\";\n const configPath = options.configPath;\n const baseDir = process.cwd();\n const target = options.target;\n\n const config = await loadConfig(configPath, { env });\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n const drift = await computeDriftReport({ env, configPath });\n\n const items: PlanItem[] = [];\n\n for (const r of drift.resources) {\n const kind = mapDriftKind(r.kind);\n if (!kind) continue;\n for (const e of r.undeployed) {\n items.push({\n kind,\n action: \"create\",\n logicalName: e.logicalName,\n derivedName: e.derivedName,\n detail: e.detail,\n });\n }\n }\n\n if (env !== \"local\") {\n const api = new CFApiClient(accountId);\n const naming = namingFromConfig(config);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n // Tolerant pre-fetch: missing sibling state isn't fatal here (plan\n // is read-only) but populating imports lets the resolver substitute\n // real values instead of leaving placeholders.\n const imports = await fetchStackImports(api, config, env);\n\n const dnsItems = dnsRecordDiffPlanItems({\n resources: getDnsRecords(config),\n tenant: config.tenant,\n env,\n state,\n });\n items.push(...dnsItems);\n\n const logpushPlanItems = await logpushJobDiffPlanItems({\n resources: getLogpushJobs(config),\n tenant: config.tenant,\n env,\n state,\n api,\n accountId,\n });\n items.push(...logpushPlanItems);\n\n const workers = await getWorkers(config, baseDir);\n for (const [workerKey, wc] of workers) {\n const resolved = await resolveWorkerConfig(\n config,\n workerKey,\n wc,\n env,\n baseDir,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n );\n if (resolved.dispatchNamespace) continue;\n\n const deployedName = resolveDeployedWorkerName(\n config,\n workerKey,\n wc,\n env,\n naming,\n );\n for (const mod of resourceModules) {\n if (!mod.diff) continue;\n const resources = mod.pickResources(wc);\n if (resources.length === 0) continue;\n const diffItems = mod.diff({\n resources,\n tenant: config.tenant,\n env,\n state,\n naming,\n worker: { workerKey, deployedName },\n });\n items.push(...diffItems);\n }\n\n let script;\n try {\n script = await api.workersScriptGet(resolved.workerName);\n } catch {\n continue;\n }\n if (!script) {\n items.push({\n kind: \"worker_script\",\n action: \"create\",\n logicalName: workerKey,\n derivedName: resolved.workerName,\n });\n }\n\n const required = requiredSecretsForWorker(wc);\n if (required.length > 0) {\n const workerSecretNames = await safeWorkersSecretsList(\n api,\n resolved.workerName,\n );\n const secretEntries = await reconcileSecrets({\n workers: [\n {\n workerKey,\n required,\n workerSecretNames,\n },\n ],\n vault: options.secretsVault ?? emptySecretsVault(),\n state,\n });\n items.push(...secretsPlanItems(secretEntries));\n }\n }\n }\n\n const filtered = target ? filterPlanItemsForTarget(items, target) : items;\n\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt: new Date().toISOString(),\n items: filtered,\n hasChanges: filtered.some((i) => i.action !== \"no_change\"),\n mode: \"forward\",\n };\n}\n\n/**\n * Compute the planned set of **deletions** for `env` — what `tamer destroy`\n * would remove given the current `tamer.config.ts` and recorded state.\n *\n * Read-only: hits the Cloudflare API only to confirm worker scripts exist,\n * never writes. Items are stack-scoped: only state entries whose\n * `logicalName` is declared on a worker in the current config are emitted —\n * resources owned by another stack sharing the same `tamer-state-{env}` row\n * are **not** included (matches `runDestroy` semantics).\n *\n * Worker scripts are listed when the deployed name actually exists on\n * Cloudflare; dispatch-namespace tenant scripts are intentionally excluded\n * (they are managed via `provision-tenant` / `destroy-tenant`).\n */\nexport async function computeDestroyPlan(options: {\n env?: string;\n configPath?: string;\n target?: ApplyTarget;\n}): Promise<PlanReport> {\n const env = options.env ?? \"local\";\n const configPath = options.configPath;\n const baseDir = process.cwd();\n const target = options.target;\n\n const config = await loadConfig(configPath, { env });\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n const items: PlanItem[] = [];\n const generatedAt = new Date().toISOString();\n\n if (env === \"local\") {\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt,\n items,\n hasChanges: false,\n mode: \"destroy\",\n };\n }\n\n const api = new CFApiClient(accountId);\n const naming = namingFromConfig(config);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n const imports = await fetchStackImports(api, config, env);\n\n const stateEntries = state.getAll();\n\n for (const mod of resourceModules) {\n const owned = await logicalNamesForResourceKind(config, baseDir, mod.kind);\n if (owned.size === 0) continue;\n const kind = mapDriftKind(mod.kind);\n if (!kind) continue;\n for (const [, entry] of Object.entries(stateEntries)) {\n if (entry.type !== mod.stateEntryType) continue;\n const logicalName = (entry as { logicalName?: string }).logicalName;\n if (!logicalName || !owned.has(logicalName)) continue;\n items.push({\n kind,\n action: \"delete\",\n logicalName,\n derivedName: derivedNameOf(entry),\n });\n }\n }\n\n for (const [, entry] of Object.entries(stateEntries)) {\n if (entry.type !== \"worker_route\") continue;\n items.push({\n kind: \"worker_route\",\n action: \"delete\",\n logicalName: entry.workerKey,\n derivedName: entry.pattern,\n detail: entry.zoneName,\n });\n }\n\n for (const ns of getDispatchNamespaces(config)) {\n const stateKey = `dispatch_namespace:${ns.logicalName}`;\n const entry = stateEntries[stateKey];\n if (!entry || entry.type !== \"dispatch_namespace\") continue;\n items.push({\n kind: \"dispatch_namespace\",\n action: \"delete\",\n logicalName: ns.logicalName,\n derivedName: entry.derivedName,\n });\n }\n\n for (const dns of getDnsRecords(config)) {\n if (dns.preserveOnDestroy) continue;\n const key = dnsRecordStateKey(dns.zoneId, dns.type, dns.name);\n const entry = stateEntries[key];\n if (!entry || entry.type !== \"dns_record\") continue;\n items.push({\n kind: \"dns_record\",\n action: \"delete\",\n logicalName: dns.logicalName,\n derivedName: `${entry.recordType} ${entry.name}`,\n detail: entry.recordId,\n });\n }\n\n for (const lp of getLogpushJobs(config)) {\n const key = logpushJobStateKey(config.tenant.id, lp.logicalName, env);\n const entry = stateEntries[key];\n if (!entry || entry.type !== \"logpush_job\") continue;\n items.push({\n kind: \"logpush_job\",\n action: \"delete\",\n logicalName: lp.logicalName,\n derivedName: entry.derivedName,\n detail: String(entry.cfJobId),\n });\n }\n\n const workers = await getWorkers(config, baseDir);\n for (const [workerKey, wc] of workers) {\n const resolved = await resolveWorkerConfig(\n config,\n workerKey,\n wc,\n env,\n baseDir,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n );\n if (resolved.dispatchNamespace) continue;\n const deployedName = resolveDeployedWorkerName(\n config,\n workerKey,\n wc,\n env,\n naming,\n );\n let script;\n try {\n script = await api.workersScriptGet(deployedName);\n } catch {\n continue;\n }\n if (script) {\n items.push({\n kind: \"worker_script\",\n action: \"delete\",\n logicalName: workerKey,\n derivedName: deployedName,\n });\n }\n }\n\n const filtered = target ? filterPlanItemsForTarget(items, target) : items;\n\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt,\n items: filtered,\n hasChanges: filtered.length > 0,\n mode: \"destroy\",\n };\n}\n\nfunction derivedNameOf(entry: StateEntry): string {\n switch (entry.type) {\n case \"d1_database\":\n case \"r2_bucket\":\n case \"kv_namespace\":\n case \"queue\":\n case \"hyperdrive\":\n case \"vectorize\":\n case \"ai_gateway\":\n case \"pipeline\":\n case \"workflow\":\n case \"secrets_store\":\n case \"dispatch_namespace\":\n return entry.derivedName;\n case \"dns_record\":\n return `${entry.recordType} ${entry.name}`;\n case \"worker_route\":\n return entry.pattern;\n case \"logpush_job\":\n return entry.derivedName;\n case \"logpush_pipelines\":\n return entry.pipelineName;\n case \"secret\":\n return `${entry.worker}:${entry.name}`;\n }\n}\n\nfunction emptySecretsVault(): SecretsVaultReader {\n return vaultReaderFromMap({});\n}\n\nasync function safeWorkersSecretsList(\n api: CFApiClient,\n scriptName: string,\n): Promise<string[]> {\n try {\n return await api.workersSecretsList(scriptName);\n } catch {\n return [];\n }\n}\n\nfunction mapDriftKind(kind: string): PlanResourceKind | undefined {\n switch (kind) {\n case \"d1\":\n case \"r2\":\n case \"kv\":\n case \"queue\":\n case \"hyperdrive\":\n case \"vectorize\":\n case \"ai_gateway\":\n case \"pipeline\":\n case \"workflow\":\n case \"secret_store\":\n case \"secret\":\n case \"dns_record\":\n case \"dispatch_namespace\":\n case \"logpush_job\":\n case \"worker_route\":\n return kind;\n default:\n return undefined;\n }\n}\n","/**\n * Read-only \"plan\" — the CloudFormation-style preview of changes Tamer would\n * make if you ran `apply` and then `deploy` against the current state +\n * config. Pure: never writes to state or to Cloudflare.\n */\n\nexport type PlanResourceKind =\n | \"d1\"\n | \"r2\"\n | \"kv\"\n | \"queue\"\n | \"hyperdrive\"\n | \"vectorize\"\n | \"ai_gateway\"\n | \"pipeline\"\n | \"workflow\"\n | \"secret_store\"\n | \"secret\"\n | \"dns_record\"\n | \"dispatch_namespace\"\n | \"logpush_job\"\n | \"worker_route\"\n | \"worker_script\";\n\n/**\n * Plan actions, in CloudFormation / Terraform terms:\n *\n * - `create` — declared in config, missing on Cloudflare and in state.\n * - `update` — tracked + present, but **mutable** fields drifted from config →\n * `apply` would PATCH in place. No replacement.\n * - `replace` — tracked + present, but **immutable** fields drifted from\n * config → `apply` must delete + recreate (Cloudflare APIs that reject\n * PATCH on the changed field, e.g. DNS record `type`, Vectorize\n * `dimensions` / `metric`, Pipelines V1 `sql`). The state row's `cfId`\n * will change.\n * - `delete` — destroy-plan only.\n * - `no_change` — declared, present, fully in sync.\n */\nexport type PlanAction =\n | \"create\"\n | \"update\"\n | \"replace\"\n | \"delete\"\n | \"no_change\";\n\n/**\n * Direction of the plan: forward (the default — what `apply` + `deploy` would\n * change) or destroy (what `destroy` would remove). Plans are mutually\n * exclusive: a forward plan never includes `delete`, and a destroy plan\n * never includes `create` / `update` / `replace`.\n */\nexport type PlanMode = \"forward\" | \"destroy\";\n\n/**\n * Describes one drifted field in an `update` / `replace` plan item — the\n * field path, what state currently holds, and what config wants. Surfaced in\n * `tamer plan --json` so reviewers can audit the change exactly the way\n * `terraform plan` shows attribute diffs.\n */\nexport interface PlanFieldChange {\n field: string;\n from: unknown;\n to: unknown;\n /**\n * `mutable` → would patch in place; `immutable` → forces `replace` (the\n * underlying Cloudflare resource will be destroyed + recreated, and the\n * state row's `cfId` will change).\n */\n kind: \"mutable\" | \"immutable\";\n}\n\nexport interface PlanItem {\n kind: PlanResourceKind;\n action: PlanAction;\n /** Logical name from `tamer.config.ts` (or worker key for scripts/routes). */\n logicalName: string;\n /** Cloudflare-side identifier (resource name or pattern). */\n derivedName: string;\n /** Optional human-readable detail. */\n detail?: string;\n /**\n * Field-level diff for `update` / `replace` items. Empty for `create` /\n * `delete` / `no_change`. Each entry shows what state has (`from`) and what\n * config asks for (`to`).\n */\n changes?: PlanFieldChange[];\n}\n\nexport interface PlanReport {\n tenantId: string;\n env: string;\n generatedAt: string;\n items: PlanItem[];\n /** True iff at least one item has `action !== \"no_change\"`. */\n hasChanges: boolean;\n /** Plan direction. Defaults to `\"forward\"` for back-compat with v0.11 plans. */\n mode?: PlanMode;\n}\n\nexport function planSummary(items: PlanItem[]): {\n byKind: Record<PlanResourceKind, number>;\n byAction: Record<PlanAction, number>;\n} {\n const byKind: Record<PlanResourceKind, number> = {\n d1: 0,\n r2: 0,\n kv: 0,\n queue: 0,\n hyperdrive: 0,\n vectorize: 0,\n ai_gateway: 0,\n pipeline: 0,\n workflow: 0,\n secret_store: 0,\n secret: 0,\n dns_record: 0,\n dispatch_namespace: 0,\n logpush_job: 0,\n worker_route: 0,\n worker_script: 0,\n };\n const byAction: Record<PlanAction, number> = {\n create: 0,\n update: 0,\n replace: 0,\n delete: 0,\n no_change: 0,\n };\n for (const it of items) {\n if (it.action === \"no_change\") continue;\n byKind[it.kind]++;\n byAction[it.action]++;\n }\n return { byKind, byAction };\n}\n","import { computePlan, computeDestroyPlan } from \"../../core/plan/computePlan.js\";\nimport { planSummary } from \"../../core/plan/plan.types.js\";\nimport type {\n PlanItem,\n PlanReport,\n PlanResourceKind,\n} from \"../../core/plan/plan.types.js\";\nimport { formatPlanFieldChanges, signFor } from \"../../core/plan/planFormat.js\";\nimport { loadConfig } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\nimport {\n PLAN_FILE_FORMAT,\n computeAttestation,\n writePlanFile,\n} from \"../../core/plan/planFile.js\";\nimport { createEmptyCfiState } from \"../../core/state/tamerStateDb.js\";\nimport { buildCloudflareSnapshot } from \"../../core/plan/cloudflareSnapshot.js\";\nimport {\n parseApplyTarget,\n assertApplyTargetDeclared,\n} from \"../../core/apply/applyTarget.js\";\n\n/**\n * `tamer plan` — print a CloudFormation-style preview of `apply` + `deploy`\n * against the current state, config, and Cloudflare reality. Never mutates.\n *\n * Exit codes:\n * 0 — plan succeeded; with `--detailed-exitcode`, also \"no changes\".\n * 2 — plan succeeded **and** has at least one create/update (with\n * `--detailed-exitcode`); useful for CI gates that want to fail when\n * infra would change.\n */\nexport async function runPlan(options: {\n env?: string;\n configPath?: string;\n json?: boolean;\n detailedExitcode?: boolean;\n /** When set, write the plan + attestation hashes to this path. */\n out?: string;\n /**\n * When true, compute a **destroy plan** (what `tamer destroy` would remove\n * from state and Cloudflare) instead of the default forward plan. Mutually\n * exclusive with the forward plan — destroy plans only contain `delete`\n * items, never `create` / `update`. Read-only.\n */\n destroy?: boolean;\n /** `kind:logicalName` — restrict plan items to that resource. */\n target?: string;\n}): Promise<number> {\n if (options.out && options.target) {\n throw new Error(\n \"Cannot combine --out with --target: saved plans must cover the full stack.\",\n );\n }\n\n const target = options.target\n ? parseApplyTarget(options.target)\n : undefined;\n if (target) {\n const config = await loadConfig(options.configPath, {\n env: options.env ?? \"local\",\n });\n await assertApplyTargetDeclared(config, process.cwd(), target);\n }\n\n const report = options.destroy\n ? await computeDestroyPlan({\n env: options.env,\n configPath: options.configPath,\n target,\n })\n : await computePlan({\n env: options.env,\n configPath: options.configPath,\n target,\n });\n\n if (options.out) {\n await writePlanForApply(options.out, report, options.configPath);\n }\n\n if (options.json) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printHumanPlan(report);\n if (options.out) {\n console.log(`Plan written to ${options.out}\\n`);\n }\n }\n\n if (options.detailedExitcode && report.hasChanges) {\n return 2;\n }\n return 0;\n}\n\n/**\n * Hydrate the same `(config, state)` pair `computePlan` consumed and write\n * a {@link PlanFile} with the attestation hashes pinned. `apply --plan`\n * recomputes both hashes and refuses to proceed if either drifted, so a\n * stale plan can never silently apply against changed inputs.\n */\nasync function writePlanForApply(\n outPath: string,\n report: PlanReport,\n configPath: string | undefined,\n): Promise<void> {\n const config = await loadConfig(configPath, { env: report.env });\n if (report.env === \"local\") {\n const attestation = computeAttestation(\n config,\n createEmptyCfiState(config.tenant.id, \"local\"),\n );\n writePlanFile(outPath, {\n format: PLAN_FILE_FORMAT,\n tenantId: config.tenant.id,\n env: report.env,\n generatedAt: report.generatedAt,\n report,\n attestation,\n });\n return;\n }\n\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n const api = new CFApiClient(accountId);\n const state = new StateManager(\n config.tenant.id,\n report.env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n const cloudflareSnapshot = await buildCloudflareSnapshot({\n config,\n env: report.env,\n api,\n baseDir: process.cwd(),\n });\n const attestation = computeAttestation(\n config,\n state.load(),\n cloudflareSnapshot,\n );\n writePlanFile(outPath, {\n format: PLAN_FILE_FORMAT,\n tenantId: config.tenant.id,\n env: report.env,\n generatedAt: report.generatedAt,\n report,\n attestation,\n });\n}\n\nfunction printHumanPlan(report: PlanReport): void {\n const isDestroy = report.mode === \"destroy\";\n const heading = isDestroy ? \"Destroy plan\" : \"Plan\";\n console.log(\n `\\n${heading} — tenant ${report.tenantId}, env ${report.env}\\n`,\n );\n if (report.items.length === 0) {\n if (isDestroy) {\n console.log(\" (nothing to destroy — no managed resources in state)\\n\");\n } else {\n console.log(\" (no changes — infrastructure matches config)\\n\");\n }\n return;\n }\n const grouped = new Map<PlanResourceKind, PlanItem[]>();\n for (const it of report.items) {\n const arr = grouped.get(it.kind) ?? [];\n arr.push(it);\n grouped.set(it.kind, arr);\n }\n for (const [kind, items] of grouped) {\n console.log(`${labelFor(kind)} (${items.length}):`);\n for (const it of items) {\n const sign = signFor(it.action);\n // Suppress `detail` on update/replace items when we have a structured\n // `changes[]` block — the per-field renderer below already shows the\n // same drift in terraform-style form, and printing both duplicates\n // the diff and clutters the output.\n const hasChanges = it.changes && it.changes.length > 0;\n const detail = it.detail && !hasChanges ? ` (${it.detail})` : \"\";\n console.log(` ${sign} ${it.logicalName} -> ${it.derivedName}${detail}`);\n for (const line of formatPlanFieldChanges(it.changes)) {\n console.log(line);\n }\n }\n }\n const { byAction } = planSummary(report.items);\n if (isDestroy) {\n console.log(`\\nSummary: ${byAction.delete} to delete.\\n`);\n } else {\n console.log(\n `\\nSummary: ${byAction.create} to create, ${byAction.update} to update, ${byAction.replace} to replace.\\n`,\n );\n }\n}\n\nfunction labelFor(kind: PlanResourceKind): string {\n switch (kind) {\n case \"d1\":\n return \"D1\";\n case \"r2\":\n return \"R2\";\n case \"kv\":\n return \"KV\";\n case \"queue\":\n return \"Queues\";\n case \"hyperdrive\":\n return \"Hyperdrive\";\n case \"vectorize\":\n return \"Vectorize\";\n case \"ai_gateway\":\n return \"AI Gateway\";\n case \"pipeline\":\n return \"Pipeline\";\n case \"workflow\":\n return \"Workflow\";\n case \"secret_store\":\n return \"Secrets Store\";\n case \"secret\":\n return \"Worker secrets\";\n case \"dns_record\":\n return \"DNS records\";\n case \"dispatch_namespace\":\n return \"Dispatch namespaces\";\n case \"logpush_job\":\n return \"Logpush jobs\";\n case \"worker_route\":\n return \"Workers zone routes\";\n case \"worker_script\":\n return \"Worker scripts\";\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"plan-Dp2aTzEU.mjs","names":["items: PlanItem[]","byKind: Record<PlanResourceKind, number>","byAction: Record<PlanAction, number>","attestation"],"sources":["../src/core/plan/computePlan.ts","../src/core/plan/plan.types.ts","../src/cli/commands/plan.ts"],"sourcesContent":["import { loadConfig, getWorkers } from \"../config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../cloudflareEnv.js\";\nimport { namingFromConfig } from \"../config/namingFromConfig.js\";\nimport { StateManager } from \"../state/StateManager.js\";\nimport { stackNameForConfig } from \"../state/stackName.js\";\nimport { fetchStackImports } from \"../imports/fetchStackImports.js\";\nimport { CFApiClient } from \"../api/CFApiClient.js\";\nimport { computeDriftReport } from \"../../cli/commands/drift.js\";\nimport { resolveWorkerConfig } from \"../config/resolver.js\";\nimport { resourceModules } from \"../registry/registry.js\";\nimport { logicalNamesForResourceKind } from \"../config/resourcesFromConfig.js\";\nimport { resolveDeployedWorkerName } from \"../config/resolver.js\";\nimport { requiredSecretsForWorker } from \"../secrets/declared.js\";\nimport {\n reconcileSecrets,\n secretsPlanItems,\n vaultReaderFromMap,\n type SecretsVaultReader,\n type VaultSecretMeta,\n} from \"../secrets/reconcile.js\";\nimport { getDispatchNamespaces, getDnsRecords, getLogpushJobs } from \"../../types.js\";\nimport { logpushJobStateKey } from \"../../features/logpush-job/logpush-job.resolve.js\";\nimport { logpushJobDiffPlanItems } from \"../../features/logpush-job/logpush-job.diff.js\";\nimport {\n dnsRecordDiffPlanItems,\n} from \"../../features/dns-records/dns-records.diff.js\";\nimport { dnsRecordStateKey } from \"../../features/dns-records/dns-records.resolve.js\";\nimport type { StateEntry } from \"../../types.js\";\nimport type { PlanItem, PlanReport, PlanResourceKind } from \"./plan.types.js\";\nimport {\n filterPlanItemsForTarget,\n type ApplyTarget,\n} from \"../apply/applyTarget.js\";\n\n/**\n * Compute the planned set of changes for `env` given the current\n * `tamer.config.ts` and recorded state. Read-only: hits the Cloudflare API to\n * detect what is missing but never writes.\n *\n * The \"would create\" set is derived from `computeDriftReport().undeployed`:\n * declared in config, present neither in state nor on Cloudflare. Worker\n * **scripts** are queried directly (`workersScriptGet`) since they are not\n * tracked as state resources today.\n */\nexport async function computePlan(options: {\n env?: string;\n configPath?: string;\n /** When set, only plan items for this `kind:logical` are returned. */\n target?: ApplyTarget;\n /** Optional vault reader for secret reconciliation (defaults to empty). */\n secretsVault?: SecretsVaultReader;\n}): Promise<PlanReport> {\n const env = options.env ?? \"local\";\n const configPath = options.configPath;\n const baseDir = process.cwd();\n const target = options.target;\n\n const config = await loadConfig(configPath, { env });\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n const drift = await computeDriftReport({ env, configPath });\n\n const items: PlanItem[] = [];\n\n for (const r of drift.resources) {\n const kind = mapDriftKind(r.kind);\n if (!kind) continue;\n for (const e of r.undeployed) {\n items.push({\n kind,\n action: \"create\",\n logicalName: e.logicalName,\n derivedName: e.derivedName,\n detail: e.detail,\n });\n }\n }\n\n if (env !== \"local\") {\n const api = new CFApiClient(accountId);\n const naming = namingFromConfig(config);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n // Tolerant pre-fetch: missing sibling state isn't fatal here (plan\n // is read-only) but populating imports lets the resolver substitute\n // real values instead of leaving placeholders.\n const imports = await fetchStackImports(api, config, env);\n\n const dnsItems = dnsRecordDiffPlanItems({\n resources: getDnsRecords(config),\n tenant: config.tenant,\n env,\n state,\n });\n items.push(...dnsItems);\n\n const logpushPlanItems = await logpushJobDiffPlanItems({\n resources: getLogpushJobs(config),\n tenant: config.tenant,\n env,\n state,\n api,\n accountId,\n });\n items.push(...logpushPlanItems);\n\n const workers = await getWorkers(config, baseDir);\n for (const [workerKey, wc] of workers) {\n const resolved = await resolveWorkerConfig(\n config,\n workerKey,\n wc,\n env,\n baseDir,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n );\n if (resolved.dispatchNamespace) continue;\n\n const deployedName = resolveDeployedWorkerName(\n config,\n workerKey,\n wc,\n env,\n naming,\n );\n for (const mod of resourceModules) {\n if (!mod.diff) continue;\n const resources = mod.pickResources(wc);\n if (resources.length === 0) continue;\n const diffItems = mod.diff({\n resources,\n tenant: config.tenant,\n env,\n state,\n naming,\n worker: { workerKey, deployedName },\n });\n items.push(...diffItems);\n }\n\n let script;\n try {\n script = await api.workersScriptGet(resolved.workerName);\n } catch {\n continue;\n }\n if (!script) {\n items.push({\n kind: \"worker_script\",\n action: \"create\",\n logicalName: workerKey,\n derivedName: resolved.workerName,\n });\n }\n\n const required = requiredSecretsForWorker(wc);\n if (required.length > 0) {\n const workerSecretNames = await safeWorkersSecretsList(\n api,\n resolved.workerName,\n );\n const secretEntries = await reconcileSecrets({\n workers: [\n {\n workerKey,\n required,\n workerSecretNames,\n },\n ],\n vault: options.secretsVault ?? emptySecretsVault(),\n state,\n });\n items.push(...secretsPlanItems(secretEntries));\n }\n }\n }\n\n const filtered = target ? filterPlanItemsForTarget(items, target) : items;\n\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt: new Date().toISOString(),\n items: filtered,\n hasChanges: filtered.some((i) => i.action !== \"no_change\"),\n mode: \"forward\",\n };\n}\n\n/**\n * Compute the planned set of **deletions** for `env` — what `tamer destroy`\n * would remove given the current `tamer.config.ts` and recorded state.\n *\n * Read-only: hits the Cloudflare API only to confirm worker scripts exist,\n * never writes. Items are stack-scoped: only state entries whose\n * `logicalName` is declared on a worker in the current config are emitted —\n * resources owned by another stack sharing the same `tamer-state-{env}` row\n * are **not** included (matches `runDestroy` semantics).\n *\n * Worker scripts are listed when the deployed name actually exists on\n * Cloudflare; dispatch-namespace tenant scripts are intentionally excluded\n * (they are managed via `provision-tenant` / `destroy-tenant`).\n */\nexport async function computeDestroyPlan(options: {\n env?: string;\n configPath?: string;\n target?: ApplyTarget;\n}): Promise<PlanReport> {\n const env = options.env ?? \"local\";\n const configPath = options.configPath;\n const baseDir = process.cwd();\n const target = options.target;\n\n const config = await loadConfig(configPath, { env });\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n\n const items: PlanItem[] = [];\n const generatedAt = new Date().toISOString();\n\n if (env === \"local\") {\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt,\n items,\n hasChanges: false,\n mode: \"destroy\",\n };\n }\n\n const api = new CFApiClient(accountId);\n const naming = namingFromConfig(config);\n const state = new StateManager(\n config.tenant.id,\n env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n const imports = await fetchStackImports(api, config, env);\n\n const stateEntries = state.getAll();\n\n for (const mod of resourceModules) {\n const owned = await logicalNamesForResourceKind(config, baseDir, mod.kind);\n if (owned.size === 0) continue;\n const kind = mapDriftKind(mod.kind);\n if (!kind) continue;\n for (const [, entry] of Object.entries(stateEntries)) {\n if (entry.type !== mod.stateEntryType) continue;\n const logicalName = (entry as { logicalName?: string }).logicalName;\n if (!logicalName || !owned.has(logicalName)) continue;\n items.push({\n kind,\n action: \"delete\",\n logicalName,\n derivedName: derivedNameOf(entry),\n });\n }\n }\n\n for (const [, entry] of Object.entries(stateEntries)) {\n if (entry.type !== \"worker_route\") continue;\n items.push({\n kind: \"worker_route\",\n action: \"delete\",\n logicalName: entry.workerKey,\n derivedName: entry.pattern,\n detail: entry.zoneName,\n });\n }\n\n for (const ns of getDispatchNamespaces(config)) {\n const stateKey = `dispatch_namespace:${ns.logicalName}`;\n const entry = stateEntries[stateKey];\n if (!entry || entry.type !== \"dispatch_namespace\") continue;\n items.push({\n kind: \"dispatch_namespace\",\n action: \"delete\",\n logicalName: ns.logicalName,\n derivedName: entry.derivedName,\n });\n }\n\n for (const dns of getDnsRecords(config)) {\n if (dns.preserveOnDestroy) continue;\n const key = dnsRecordStateKey(dns.zoneId, dns.type, dns.name);\n const entry = stateEntries[key];\n if (!entry || entry.type !== \"dns_record\") continue;\n items.push({\n kind: \"dns_record\",\n action: \"delete\",\n logicalName: dns.logicalName,\n derivedName: `${entry.recordType} ${entry.name}`,\n detail: entry.recordId,\n });\n }\n\n for (const lp of getLogpushJobs(config)) {\n const key = logpushJobStateKey(config.tenant.id, lp.logicalName, env);\n const entry = stateEntries[key];\n if (!entry || entry.type !== \"logpush_job\") continue;\n items.push({\n kind: \"logpush_job\",\n action: \"delete\",\n logicalName: lp.logicalName,\n derivedName: entry.derivedName,\n detail: String(entry.cfJobId),\n });\n }\n\n const workers = await getWorkers(config, baseDir);\n for (const [workerKey, wc] of workers) {\n const resolved = await resolveWorkerConfig(\n config,\n workerKey,\n wc,\n env,\n baseDir,\n accountId,\n naming,\n state,\n { referencesMode: \"tolerant\", imports },\n );\n if (resolved.dispatchNamespace) continue;\n const deployedName = resolveDeployedWorkerName(\n config,\n workerKey,\n wc,\n env,\n naming,\n );\n let script;\n try {\n script = await api.workersScriptGet(deployedName);\n } catch {\n continue;\n }\n if (script) {\n items.push({\n kind: \"worker_script\",\n action: \"delete\",\n logicalName: workerKey,\n derivedName: deployedName,\n });\n }\n }\n\n const filtered = target ? filterPlanItemsForTarget(items, target) : items;\n\n return {\n tenantId: config.tenant.id,\n env,\n generatedAt,\n items: filtered,\n hasChanges: filtered.length > 0,\n mode: \"destroy\",\n };\n}\n\nfunction derivedNameOf(entry: StateEntry): string {\n switch (entry.type) {\n case \"d1_database\":\n case \"r2_bucket\":\n case \"kv_namespace\":\n case \"queue\":\n case \"hyperdrive\":\n case \"vectorize\":\n case \"ai_gateway\":\n case \"pipeline\":\n case \"workflow\":\n case \"secrets_store\":\n case \"dispatch_namespace\":\n return entry.derivedName;\n case \"dns_record\":\n return `${entry.recordType} ${entry.name}`;\n case \"worker_route\":\n return entry.pattern;\n case \"logpush_job\":\n return entry.derivedName;\n case \"logpush_pipelines\":\n return entry.pipelineName;\n case \"secret\":\n return `${entry.worker}:${entry.name}`;\n }\n}\n\nfunction emptySecretsVault(): SecretsVaultReader {\n return vaultReaderFromMap({});\n}\n\nasync function safeWorkersSecretsList(\n api: CFApiClient,\n scriptName: string,\n): Promise<string[]> {\n try {\n return await api.workersSecretsList(scriptName);\n } catch {\n return [];\n }\n}\n\nfunction mapDriftKind(kind: string): PlanResourceKind | undefined {\n switch (kind) {\n case \"d1\":\n case \"r2\":\n case \"kv\":\n case \"queue\":\n case \"hyperdrive\":\n case \"vectorize\":\n case \"ai_gateway\":\n case \"pipeline\":\n case \"workflow\":\n case \"secret_store\":\n case \"secret\":\n case \"dns_record\":\n case \"dispatch_namespace\":\n case \"logpush_job\":\n case \"worker_route\":\n return kind;\n default:\n return undefined;\n }\n}\n","/**\n * Read-only \"plan\" — the CloudFormation-style preview of changes Tamer would\n * make if you ran `apply` and then `deploy` against the current state +\n * config. Pure: never writes to state or to Cloudflare.\n */\n\nexport type PlanResourceKind =\n | \"d1\"\n | \"r2\"\n | \"kv\"\n | \"queue\"\n | \"hyperdrive\"\n | \"vectorize\"\n | \"ai_gateway\"\n | \"pipeline\"\n | \"workflow\"\n | \"secret_store\"\n | \"secret\"\n | \"dns_record\"\n | \"dispatch_namespace\"\n | \"logpush_job\"\n | \"worker_route\"\n | \"worker_script\";\n\n/**\n * Plan actions, in CloudFormation / Terraform terms:\n *\n * - `create` — declared in config, missing on Cloudflare and in state.\n * - `update` — tracked + present, but **mutable** fields drifted from config →\n * `apply` would PATCH in place. No replacement.\n * - `replace` — tracked + present, but **immutable** fields drifted from\n * config → `apply` must delete + recreate (Cloudflare APIs that reject\n * PATCH on the changed field, e.g. DNS record `type`, Vectorize\n * `dimensions` / `metric`, Pipelines V1 `sql`). The state row's `cfId`\n * will change.\n * - `delete` — destroy-plan only.\n * - `no_change` — declared, present, fully in sync.\n */\nexport type PlanAction =\n | \"create\"\n | \"update\"\n | \"replace\"\n | \"delete\"\n | \"no_change\";\n\n/**\n * Direction of the plan: forward (the default — what `apply` + `deploy` would\n * change) or destroy (what `destroy` would remove). Plans are mutually\n * exclusive: a forward plan never includes `delete`, and a destroy plan\n * never includes `create` / `update` / `replace`.\n */\nexport type PlanMode = \"forward\" | \"destroy\";\n\n/**\n * Describes one drifted field in an `update` / `replace` plan item — the\n * field path, what state currently holds, and what config wants. Surfaced in\n * `tamer plan --json` so reviewers can audit the change exactly the way\n * `terraform plan` shows attribute diffs.\n */\nexport interface PlanFieldChange {\n field: string;\n from: unknown;\n to: unknown;\n /**\n * `mutable` → would patch in place; `immutable` → forces `replace` (the\n * underlying Cloudflare resource will be destroyed + recreated, and the\n * state row's `cfId` will change).\n */\n kind: \"mutable\" | \"immutable\";\n}\n\nexport interface PlanItem {\n kind: PlanResourceKind;\n action: PlanAction;\n /** Logical name from `tamer.config.ts` (or worker key for scripts/routes). */\n logicalName: string;\n /** Cloudflare-side identifier (resource name or pattern). */\n derivedName: string;\n /** Optional human-readable detail. */\n detail?: string;\n /**\n * Field-level diff for `update` / `replace` items. Empty for `create` /\n * `delete` / `no_change`. Each entry shows what state has (`from`) and what\n * config asks for (`to`).\n */\n changes?: PlanFieldChange[];\n}\n\nexport interface PlanReport {\n tenantId: string;\n env: string;\n generatedAt: string;\n items: PlanItem[];\n /** True iff at least one item has `action !== \"no_change\"`. */\n hasChanges: boolean;\n /** Plan direction. Defaults to `\"forward\"` for back-compat with v0.11 plans. */\n mode?: PlanMode;\n}\n\nexport function planSummary(items: PlanItem[]): {\n byKind: Record<PlanResourceKind, number>;\n byAction: Record<PlanAction, number>;\n} {\n const byKind: Record<PlanResourceKind, number> = {\n d1: 0,\n r2: 0,\n kv: 0,\n queue: 0,\n hyperdrive: 0,\n vectorize: 0,\n ai_gateway: 0,\n pipeline: 0,\n workflow: 0,\n secret_store: 0,\n secret: 0,\n dns_record: 0,\n dispatch_namespace: 0,\n logpush_job: 0,\n worker_route: 0,\n worker_script: 0,\n };\n const byAction: Record<PlanAction, number> = {\n create: 0,\n update: 0,\n replace: 0,\n delete: 0,\n no_change: 0,\n };\n for (const it of items) {\n if (it.action === \"no_change\") continue;\n byKind[it.kind]++;\n byAction[it.action]++;\n }\n return { byKind, byAction };\n}\n","import { computePlan, computeDestroyPlan } from \"../../core/plan/computePlan.js\";\nimport { planSummary } from \"../../core/plan/plan.types.js\";\nimport type {\n PlanItem,\n PlanReport,\n PlanResourceKind,\n} from \"../../core/plan/plan.types.js\";\nimport { formatPlanFieldChanges, signFor } from \"../../core/plan/planFormat.js\";\nimport { loadConfig } from \"../../core/config/loader.js\";\nimport { cloudflareAccountIdFromEnv } from \"../../core/cloudflareEnv.js\";\nimport { CFApiClient } from \"../../core/api/CFApiClient.js\";\nimport { StateManager } from \"../../core/state/StateManager.js\";\nimport { stackNameForConfig } from \"../../core/state/stackName.js\";\nimport {\n PLAN_FILE_FORMAT,\n computeAttestation,\n writePlanFile,\n} from \"../../core/plan/planFile.js\";\nimport { createEmptyCfiState } from \"../../core/state/tamerStateDb.js\";\nimport { buildCloudflareSnapshot } from \"../../core/plan/cloudflareSnapshot.js\";\nimport {\n parseApplyTarget,\n assertApplyTargetDeclared,\n} from \"../../core/apply/applyTarget.js\";\n\n/**\n * `tamer plan` — print a CloudFormation-style preview of `apply` + `deploy`\n * against the current state, config, and Cloudflare reality. Never mutates.\n *\n * Exit codes:\n * 0 — plan succeeded; with `--detailed-exitcode`, also \"no changes\".\n * 2 — plan succeeded **and** has at least one create/update (with\n * `--detailed-exitcode`); useful for CI gates that want to fail when\n * infra would change.\n */\nexport async function runPlan(options: {\n env?: string;\n configPath?: string;\n json?: boolean;\n detailedExitcode?: boolean;\n /** When set, write the plan + attestation hashes to this path. */\n out?: string;\n /**\n * When true, compute a **destroy plan** (what `tamer destroy` would remove\n * from state and Cloudflare) instead of the default forward plan. Mutually\n * exclusive with the forward plan — destroy plans only contain `delete`\n * items, never `create` / `update`. Read-only.\n */\n destroy?: boolean;\n /** `kind:logicalName` — restrict plan items to that resource. */\n target?: string;\n}): Promise<number> {\n if (options.out && options.target) {\n throw new Error(\n \"Cannot combine --out with --target: saved plans must cover the full stack.\",\n );\n }\n\n const target = options.target\n ? parseApplyTarget(options.target)\n : undefined;\n if (target) {\n const config = await loadConfig(options.configPath, {\n env: options.env ?? \"local\",\n });\n await assertApplyTargetDeclared(config, process.cwd(), target);\n }\n\n const report = options.destroy\n ? await computeDestroyPlan({\n env: options.env,\n configPath: options.configPath,\n target,\n })\n : await computePlan({\n env: options.env,\n configPath: options.configPath,\n target,\n });\n\n if (options.out) {\n await writePlanForApply(options.out, report, options.configPath);\n }\n\n if (options.json) {\n console.log(JSON.stringify(report, null, 2));\n } else {\n printHumanPlan(report);\n if (options.out) {\n console.log(`Plan written to ${options.out}\\n`);\n }\n }\n\n if (options.detailedExitcode && report.hasChanges) {\n return 2;\n }\n return 0;\n}\n\n/**\n * Hydrate the same `(config, state)` pair `computePlan` consumed and write\n * a {@link PlanFile} with the attestation hashes pinned. `apply --plan`\n * recomputes both hashes and refuses to proceed if either drifted, so a\n * stale plan can never silently apply against changed inputs.\n */\nasync function writePlanForApply(\n outPath: string,\n report: PlanReport,\n configPath: string | undefined,\n): Promise<void> {\n const config = await loadConfig(configPath, { env: report.env });\n if (report.env === \"local\") {\n const attestation = computeAttestation(\n config,\n createEmptyCfiState(config.tenant.id, \"local\"),\n );\n writePlanFile(outPath, {\n format: PLAN_FILE_FORMAT,\n tenantId: config.tenant.id,\n env: report.env,\n generatedAt: report.generatedAt,\n report,\n attestation,\n });\n return;\n }\n\n const accountId = config.account_id ?? cloudflareAccountIdFromEnv();\n if (!accountId) {\n throw new Error(\n \"account_id required in config or CLOUDFLARE_ACCOUNT_ID env var\",\n );\n }\n const api = new CFApiClient(accountId);\n const state = new StateManager(\n config.tenant.id,\n report.env,\n stackNameForConfig(config),\n );\n await state.hydrate(api);\n const cloudflareSnapshot = await buildCloudflareSnapshot({\n config,\n env: report.env,\n api,\n baseDir: process.cwd(),\n });\n const attestation = computeAttestation(\n config,\n state.load(),\n cloudflareSnapshot,\n );\n writePlanFile(outPath, {\n format: PLAN_FILE_FORMAT,\n tenantId: config.tenant.id,\n env: report.env,\n generatedAt: report.generatedAt,\n report,\n attestation,\n });\n}\n\nfunction printHumanPlan(report: PlanReport): void {\n const isDestroy = report.mode === \"destroy\";\n const heading = isDestroy ? \"Destroy plan\" : \"Plan\";\n console.log(\n `\\n${heading} — tenant ${report.tenantId}, env ${report.env}\\n`,\n );\n if (report.items.length === 0) {\n if (isDestroy) {\n console.log(\" (nothing to destroy — no managed resources in state)\\n\");\n } else {\n console.log(\" (no changes — infrastructure matches config)\\n\");\n }\n return;\n }\n const grouped = new Map<PlanResourceKind, PlanItem[]>();\n for (const it of report.items) {\n const arr = grouped.get(it.kind) ?? [];\n arr.push(it);\n grouped.set(it.kind, arr);\n }\n for (const [kind, items] of grouped) {\n console.log(`${labelFor(kind)} (${items.length}):`);\n for (const it of items) {\n const sign = signFor(it.action);\n // Suppress `detail` on update/replace items when we have a structured\n // `changes[]` block — the per-field renderer below already shows the\n // same drift in terraform-style form, and printing both duplicates\n // the diff and clutters the output.\n const hasChanges = it.changes && it.changes.length > 0;\n const detail = it.detail && !hasChanges ? ` (${it.detail})` : \"\";\n console.log(` ${sign} ${it.logicalName} -> ${it.derivedName}${detail}`);\n for (const line of formatPlanFieldChanges(it.changes)) {\n console.log(line);\n }\n }\n }\n const { byAction } = planSummary(report.items);\n if (isDestroy) {\n console.log(`\\nSummary: ${byAction.delete} to delete.\\n`);\n } else {\n console.log(\n `\\nSummary: ${byAction.create} to create, ${byAction.update} to update, ${byAction.replace} to replace.\\n`,\n );\n }\n}\n\nfunction labelFor(kind: PlanResourceKind): string {\n switch (kind) {\n case \"d1\":\n return \"D1\";\n case \"r2\":\n return \"R2\";\n case \"kv\":\n return \"KV\";\n case \"queue\":\n return \"Queues\";\n case \"hyperdrive\":\n return \"Hyperdrive\";\n case \"vectorize\":\n return \"Vectorize\";\n case \"ai_gateway\":\n return \"AI Gateway\";\n case \"pipeline\":\n return \"Pipeline\";\n case \"workflow\":\n return \"Workflow\";\n case \"secret_store\":\n return \"Secrets Store\";\n case \"secret\":\n return \"Worker secrets\";\n case \"dns_record\":\n return \"DNS records\";\n case \"dispatch_namespace\":\n return \"Dispatch namespaces\";\n case \"logpush_job\":\n return \"Logpush jobs\";\n case \"worker_route\":\n return \"Workers zone routes\";\n case \"worker_script\":\n return \"Worker scripts\";\n }\n}\n"],"mappings":"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"}