void 0.25.0 → 0.26.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 (151) hide show
  1. package/dist/{account-cmd-DcR1ywt8.mjs → account-cmd-CDry3B5V.mjs} +4 -4
  2. package/dist/{application-analysis-BVsVqc11.mjs → application-analysis-DC2yPvzY.mjs} +1 -1
  3. package/dist/{application-routing-B7QEjqSR.mjs → application-routing-C9khgLiS.mjs} +1 -1
  4. package/dist/{auth-CdgZ9uag.mjs → auth-B60QDbjg.mjs} +1 -1
  5. package/dist/{auth-7wuZ5ob8.mjs → auth-D52yG7hl.mjs} +3 -3
  6. package/dist/{auth-link-Z6E5DYvM.mjs → auth-link-DjeOptYi.mjs} +4 -4
  7. package/dist/{auth-router-BdM4avI8.mjs → auth-router-BRxIuqDi.mjs} +5 -5
  8. package/dist/{build-cmd-CEE7D-hM.mjs → build-cmd-Ce3C8huo.mjs} +4 -4
  9. package/dist/{cache-TcZnBLtH.mjs → cache-CS8Yxr5a.mjs} +4 -4
  10. package/dist/{cancel-deploy-CYoZmcmR.mjs → cancel-deploy-mZsD88Xa.mjs} +3 -3
  11. package/dist/{cf-access-AHKBzmuu.mjs → cf-access-YTlqvAsk.mjs} +1 -1
  12. package/dist/cli/cli.mjs +40 -40
  13. package/dist/cli/cloudflare-operation-process.mjs +1 -1
  14. package/dist/cli/env-schema-probe.d.mts +1 -1
  15. package/dist/client-BxVixJBI.mjs +2 -0
  16. package/dist/{client-RsPo8wsP.mjs → client-mC-dFGDD.mjs} +5 -5
  17. package/dist/{cloudflare-operations-CjaV4lZi.mjs → cloudflare-operations-DYa3coIk.mjs} +1 -1
  18. package/dist/{cloudflare-operations-BhbK6yTq.mjs → cloudflare-operations-DoQrVHAt.mjs} +1 -1
  19. package/dist/{connect-DCvUmPXY.mjs → connect-H6EvP62X.mjs} +6 -6
  20. package/dist/{create-project-eAPgYq35.mjs → create-project-B2kk9Q0o.mjs} +4 -4
  21. package/dist/{create-project-C734I-Mu.mjs → create-project-Dbb4uiN-.mjs} +1 -1
  22. package/dist/{db-C07GEdKF.mjs → db-DpCKZto0.mjs} +21 -21
  23. package/dist/{delete-pKMVsrpJ.mjs → delete-CxlSvsvW.mjs} +6 -6
  24. package/dist/{deploy-DfsAe-T0.mjs → deploy-CeEbnv4b.mjs} +38 -24
  25. package/dist/{deploy-BDu1vhSH.mjs → deploy-qmiLE1G1.mjs} +1 -1
  26. package/dist/{domain-D3uA9ize.mjs → domain-B_9VgOm6.mjs} +6 -6
  27. package/dist/{email-z3o2cwjH.mjs → email-BokHgjKj.mjs} +6 -6
  28. package/dist/{env-3Q8-_2IH.mjs → env-BdTc-K2L.mjs} +6 -5
  29. package/dist/{env-public-BX_r8HR6.d.mts → env-public-DR7ZM6d-.d.mts} +1 -1
  30. package/dist/{fetch-stream-IvCYKyQL.mjs → fetch-stream-Nm3hkZbT.mjs} +8 -1
  31. package/dist/{gen-NjwoG9FK.mjs → gen-BhDlTpwW.mjs} +1 -1
  32. package/dist/gen-qCBwH3fZ.mjs +2 -0
  33. package/dist/{github-cmd-DquUq-b1.mjs → github-cmd-hhkTzlGS.mjs} +4 -4
  34. package/dist/{handler-CZ4nAylQ.d.mts → handler-BeK3weD4.d.mts} +1 -1
  35. package/dist/{help-DRZXC4fj.mjs → help-Bv44e2tm.mjs} +13 -9
  36. package/dist/help-Dw5f9Xrg.mjs +2 -0
  37. package/dist/index.d.mts +1 -1
  38. package/dist/index.mjs +30 -17
  39. package/dist/{info-BmA3_IFZ.mjs → info-DhtTKh65.mjs} +1 -1
  40. package/dist/{init-DT7yuuyl.mjs → init-BMf4T03q.mjs} +10 -9
  41. package/dist/{limits-Bq5LG8Id.d.mts → limits-CFsDKQvU.d.mts} +1 -10
  42. package/dist/{limits-Cjuk2VPm.mjs → limits-D0vD1M9A.mjs} +1 -22
  43. package/dist/{link-CXOlqh2F.mjs → link-BqYsSS_I.mjs} +5 -5
  44. package/dist/{list-eEWVJEPn.mjs → list-_kBToO-l.mjs} +7 -6
  45. package/dist/{live-Chw1eIMv.d.mts → live-iAuEH1hd.d.mts} +1 -1
  46. package/dist/{local-d1-Df4XpwhW.mjs → local-d1-fGxYrVD6.mjs} +1 -1
  47. package/dist/login-D7meQg_3.mjs +2 -0
  48. package/dist/{login-pWQHYVu2.mjs → login-DFuo-RWO.mjs} +4 -4
  49. package/dist/{logs-DnxVX7TR.mjs → logs-DYgnh6dH.mjs} +5 -5
  50. package/dist/{node-BkyRWRx8.mjs → node-C6Q6e1Es.mjs} +1 -1
  51. package/dist/{operator-args-CAVcmwUt.mjs → operator-args-DdY54Bgb.mjs} +1 -1
  52. package/dist/{operator-auth-CkmTE3rt.mjs → operator-auth-2dsGlTT5.mjs} +4 -8
  53. package/dist/{operator-client-LmwjxT6q.mjs → operator-client-BFqpp6z0.mjs} +3 -3
  54. package/dist/{operator-cmd-d-IEKgNk.mjs → operator-cmd-CUGHXrQf.mjs} +27 -9
  55. package/dist/operator-cmd-DRPvhpjM.mjs +2 -0
  56. package/dist/{output-BBsUwdyT.mjs → output-BvdaehtE.mjs} +26 -2
  57. package/dist/pages/client.d.mts +1 -1
  58. package/dist/pages/index.mjs +1 -1
  59. package/dist/pages/islands-plugin.mjs +1 -1
  60. package/dist/pages/protocol.d.mts +2 -2
  61. package/dist/{plan-NPpwZ_kc.mjs → plan-CJGEeTEI.mjs} +2 -2
  62. package/dist/plan-COtUvdMA.mjs +2 -0
  63. package/dist/{platform-args-Bzf8JPc2.mjs → platform-args-DSV2p4nY.mjs} +4 -2
  64. package/dist/platform-args-Dcy_5b_Y.mjs +2 -0
  65. package/dist/{platform-auth-config-0V0lA3pF.mjs → platform-auth-config-Bosx7U1h.mjs} +18 -17
  66. package/dist/{platform-auth-protection-DFeJ-JkK.mjs → platform-auth-protection-DcbL_LW_.mjs} +6 -6
  67. package/dist/{platform-auth-recovery-ICy4muq6.mjs → platform-auth-recovery-WfTZ8K6o.mjs} +5 -5
  68. package/dist/{platform-cmd-D7vSWMvV.mjs → platform-cmd-DbKtQb_4.mjs} +45 -20
  69. package/dist/{platform-cmd-BPySFOew.mjs → platform-cmd-dyFUof0t.mjs} +1 -1
  70. package/dist/{platform-discovery-DDDUvnqp.mjs → platform-discovery-BC0HCKYt.mjs} +18 -4
  71. package/dist/{platform-domain-CUdY064e.mjs → platform-domain-Bsq43AJp.mjs} +5 -4
  72. package/dist/{platform-lifecycle-BaUdTRPU.mjs → platform-lifecycle--5gWmDoI.mjs} +1 -1
  73. package/dist/{platform-lifecycle-Dm09JNkn.mjs → platform-lifecycle-Cfi0o6xM.mjs} +863 -35
  74. package/dist/{platform-management-Bp6aoH2C.mjs → platform-management-Bbwf6-Dq.mjs} +1 -1
  75. package/dist/{platform-management-BSEc_ZeG.mjs → platform-management-BrF9STZp.mjs} +173 -30
  76. package/dist/{platform-plans-config-B0pOVQyW.mjs → platform-plans-config-DU-8nC2Y.mjs} +2 -2
  77. package/dist/{platform-recovery-WOZ95Uze.mjs → platform-recovery-DQAqAQYu.mjs} +3 -3
  78. package/dist/{platform-registry-KfNEF84O.mjs → platform-registry-P387v42A.mjs} +17 -10
  79. package/dist/prepare-BBknoZI1.mjs +2 -0
  80. package/dist/{prepare-39EvOhI6.mjs → prepare-BMrRWLwP.mjs} +3 -2
  81. package/dist/{prepare-DnaKBi--.mjs → prepare-WoUubFKL.mjs} +1 -1
  82. package/dist/{project-IOjBU8R6.mjs → project-BiaPQ7ur.mjs} +1 -1
  83. package/dist/{project-BkbfDs0V.mjs → project-DQstYTxO.mjs} +1 -1
  84. package/dist/{project-cmd-Bs5A392E.mjs → project-cmd-BbH2LptN.mjs} +18 -18
  85. package/dist/{project-team-ohAWWaew.mjs → project-team-Yiww3-go.mjs} +5 -5
  86. package/dist/{project-token-CP01Jna6.mjs → project-token-BrtT2NkY.mjs} +4 -4
  87. package/dist/{project-zero-trust-BzkVgx35.mjs → project-zero-trust-nYJ2V2nc.mjs} +5 -5
  88. package/dist/{protocol-CjF_iI9X.d.mts → protocol-BX8t2jiw.d.mts} +1 -1
  89. package/dist/{provision-BEv0k-qz.mjs → provision-DMTPG8Rh.mjs} +1 -1
  90. package/dist/{provision-Btj1jppf.mjs → provision-VeqAC77A.mjs} +13 -1
  91. package/dist/{requests-B3PBuGlX.mjs → requests-TIz_jtSu.mjs} +4 -4
  92. package/dist/{rollback-DDub9GsO.mjs → rollback-dBqICPzj.mjs} +5 -5
  93. package/dist/{runner-Ol0TNjk6.mjs → runner--2KF6Kua.mjs} +1 -1
  94. package/dist/{runner-CXA9Fh8h.mjs → runner-voIltZrY.mjs} +1 -1
  95. package/dist/runtime/ai.d.mts +10 -11
  96. package/dist/runtime/ai.mjs +13 -18
  97. package/dist/runtime/client-react.mjs +1 -1
  98. package/dist/runtime/client-solid.mjs +1 -1
  99. package/dist/runtime/client-svelte.mjs +1 -1
  100. package/dist/runtime/client-vue.mjs +1 -1
  101. package/dist/runtime/client.mjs +1 -1
  102. package/dist/runtime/env-public.d.mts +1 -1
  103. package/dist/runtime/fetch-stream.mjs +1 -1
  104. package/dist/runtime/handler.d.mts +1 -1
  105. package/dist/runtime/limits.d.mts +2 -2
  106. package/dist/runtime/limits.mjs +2 -2
  107. package/dist/runtime/live-client.d.mts +1 -1
  108. package/dist/runtime/live.d.mts +1 -1
  109. package/dist/runtime/migration-handler.mjs +33 -7
  110. package/dist/runtime/routing.d.mts +1 -1
  111. package/dist/runtime/sandbox.d.mts +1 -1
  112. package/dist/runtime/sandbox.mjs +12 -22
  113. package/dist/runtime/validator.d.mts +1 -1
  114. package/dist/runtime/ws-server.d.mts +1 -1
  115. package/dist/runtime/ws.d.mts +1 -1
  116. package/dist/{sandbox-XZAqzFlG.d.mts → sandbox-C5LFoJMf.d.mts} +7 -7
  117. package/dist/{scan-C7okrLyM.mjs → scan-C8dA7iM4.mjs} +10 -6
  118. package/dist/{secret-3c72lY1U.mjs → secret-AKnNXNP5.mjs} +6 -6
  119. package/dist/{subcommand-prompt-Dhom39bd.mjs → subcommand-prompt-B6WS9Aoz.mjs} +1 -1
  120. package/dist/validate-pna7R3Pv.mjs +2 -0
  121. package/dist/{validate-ctOrgiS3.mjs → validate-ymhap6M_.mjs} +1 -1
  122. package/dist/{ws-CL1w7GXU.d.mts → ws-C79j4aED.d.mts} +1 -1
  123. package/package.json +7 -7
  124. package/skills/void/SKILL.md +5 -2
  125. package/skills/void/docs/guide/ai.md +65 -62
  126. package/skills/void/docs/guide/app-types.md +1 -1
  127. package/skills/void/docs/guide/database.md +15 -0
  128. package/skills/void/docs/guide/live.md +2 -0
  129. package/skills/void/docs/guide/pages-routing/actions-and-forms.md +1 -1
  130. package/skills/void/docs/guide/platform/installation/credentials.md +2 -2
  131. package/skills/void/docs/guide/platform/installation/first-deployment.md +5 -1
  132. package/skills/void/docs/guide/platform/installation/maintenance.md +3 -1
  133. package/skills/void/docs/guide/platform/installation/setup.md +1 -1
  134. package/skills/void/docs/guide/platform/installation/uninstall.md +44 -11
  135. package/skills/void/docs/guide/sandboxes.md +39 -20
  136. package/skills/void/docs/reference/api/resources.md +11 -14
  137. package/skills/void/docs/reference/api.md +35 -34
  138. package/skills/void/docs/reference/cli/auth.md +3 -1
  139. package/skills/void/docs/reference/cli/platform-config.md +2 -2
  140. package/skills/void/docs/reference/cli/platform-installation.md +14 -12
  141. package/skills/void/docs/reference/cli/platform.md +4 -2
  142. package/skills/void/docs/reference/cli.md +1 -1
  143. package/skills/void/docs/reference/resource-inference.md +1 -0
  144. package/dist/client-B4Me-9HE.mjs +0 -2
  145. package/dist/gen-ById9r2j.mjs +0 -2
  146. package/dist/help-DPGBwomV.mjs +0 -2
  147. package/dist/login-C98-sw79.mjs +0 -2
  148. package/dist/plan-D1Q5rf-r.mjs +0 -2
  149. package/dist/platform-args-Cq6EjQro.mjs +0 -2
  150. package/dist/prepare-CnjcTV2s.mjs +0 -2
  151. package/dist/validate-Dq_L3s0S.mjs +0 -2
@@ -1,14 +1,14 @@
1
1
  import { a as join, n as dirname, u as resolve } from "./dist-BVqXeR_h.mjs";
2
- import { _ as savePlatformConnection, a as listInstallationManifests, b as writePendingInstallationSecrets, d as promoteInstallationSecrets, f as readInstallationRecoverySecrets, g as resolveInstallationManifest, i as latestPlatformWorkerCutoverGeneration, l as platformRegistryDirectory, n as assertPlatformInstallationSlugAvailable, p as readPendingInstallationSecrets, s as normalizePlatformApiUrl, t as acquirePlatformLifecycleLock, u as platformWorkerSecretUpdateIntentState, y as writeInstallationManifest } from "./platform-registry-KfNEF84O.mjs";
2
+ import { S as writePendingInstallationSecrets, a as latestPlatformWorkerCutoverGeneration, c as normalizePlatformApiUrl, d as platformRegistryDirectory, f as platformWorkerSecretUpdateIntentState, h as readPendingInstallationSecrets, m as readInstallationRecoverySecrets, n as assertPlatformInstallationSlugAvailable, o as listInstallationManifests, p as promoteInstallationSecrets, t as acquirePlatformLifecycleLock, v as resolveInstallationManifest, x as writeInstallationManifest, y as savePlatformConnection } from "./platform-registry-P387v42A.mjs";
3
3
  import { t as cfAccessHeaders } from "./cf-access-TWNzXltp.mjs";
4
- import { n as ensurePlatformAccessToken } from "./cf-access-AHKBzmuu.mjs";
5
- import { u as openBrowser } from "./auth-7wuZ5ob8.mjs";
6
- import { A as confirm, B as note, H as outro, K as select, L as log, U as password, X as text$1, tt as isCancel, z as multiselect } from "./prompts-6OSVA7DJ.mjs";
4
+ import { n as ensurePlatformAccessToken } from "./cf-access-YTlqvAsk.mjs";
5
+ import { u as openBrowser } from "./auth-D52yG7hl.mjs";
6
+ import { A as confirm, H as outro, K as select, L as log, U as password, X as text$1, tt as isCancel, z as multiselect } from "./prompts-6OSVA7DJ.mjs";
7
7
  import { c as resolveCloudflareOperationArgv } from "./cloudflare-process-B-wekeR6.mjs";
8
- import { c as import_picocolors, n as cliCommandName, r as createSpinner } from "./output-BBsUwdyT.mjs";
8
+ import { c as import_picocolors, l as note, n as cliCommandName, r as createSpinner } from "./output-BvdaehtE.mjs";
9
9
  import { o as runCloudflareAuthCommand } from "./cloudflare-auth-Qdc7tw8F.mjs";
10
- import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-Btj1jppf.mjs";
11
- import { t as parsePlatformArgs } from "./platform-args-Bzf8JPc2.mjs";
10
+ import { _ as resolvePinnedApiBaseUrl, o as createDefaultRunWrangler } from "./provision-VeqAC77A.mjs";
11
+ import { t as parsePlatformArgs } from "./platform-args-DSV2p4nY.mjs";
12
12
  import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { isRecord } from "@void/deploy-core";
@@ -116,7 +116,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
116
116
  } catch {
117
117
  throw new PlatformCloudflareApiError(`Cloudflare returned a non-JSON response (HTTP ${response.status}).`, response.status, path.split("?")[0]);
118
118
  }
119
- if (!response.ok || envelope.success !== true || envelope.result === void 0) throw new PlatformCloudflareApiError(`Cloudflare API error: ${envelope.errors?.map((error) => error.message).filter(Boolean).join(", ") || `HTTP ${response.status}`} (HTTP ${response.status}; ${init?.method ?? "GET"} ${path.split("?")[0]}).`, response.status, path.split("?")[0]);
119
+ if (!response.ok || envelope.success !== true || envelope.result === void 0) throw new PlatformCloudflareApiError(`Cloudflare API error: ${cloudflareErrorDetails(envelope, response.status)} (HTTP ${response.status}; ${init?.method ?? "GET"} ${path.split("?")[0]}).`, response.status, path.split("?")[0]);
120
120
  return {
121
121
  result: envelope.result,
122
122
  resultInfo: envelope.result_info
@@ -157,6 +157,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
157
157
  if (result.length < 50) break;
158
158
  }
159
159
  return entries.map((entry) => ({
160
+ ...withCreatedAt(entry.created_at),
160
161
  id: requiredString(entry.id, "AI Gateway id"),
161
162
  name: requiredString(entry.id, "AI Gateway id")
162
163
  }));
@@ -548,7 +549,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
548
549
  throw error;
549
550
  }
550
551
  }
551
- async emptyR2Bucket(accountId, bucketName, preservedKeys = /* @__PURE__ */ new Set()) {
552
+ async emptyR2Bucket(accountId, bucketName, preservedKeys = /* @__PURE__ */ new Set(), beforeDelete) {
552
553
  let cursor = "";
553
554
  const observedCursors = /* @__PURE__ */ new Set();
554
555
  for (;;) {
@@ -565,6 +566,7 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
565
566
  const envelope = await response.json();
566
567
  if (!response.ok || envelope.success !== true || !Array.isArray(envelope.result)) throw new PlatformCloudflareApiError(`Cloudflare API error listing R2 objects: ${envelope.errors?.map((error) => error.message).filter(Boolean).join(", ") || `HTTP ${response.status}`}`, response.status);
567
568
  const keys = envelope.result.map((object) => requiredString(object.key, "R2 object key")).filter((key) => !preservedKeys.has(key));
569
+ if (keys.length > 0) await beforeDelete?.();
568
570
  for (let offset = 0; offset < keys.length; offset += 1e3) await this.deleteR2Objects(accountId, bucketName, keys.slice(offset, offset + 1e3));
569
571
  const nextCursor = envelope.result_info?.cursor ?? "";
570
572
  if (!nextCursor) return;
@@ -582,18 +584,141 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
582
584
  const deletedKeys = new Set(deleted.map((entry) => requiredString(entry.key, "deleted R2 object key")));
583
585
  if (keys.find((key) => !deletedKeys.has(key)) !== void 0) throw new Error("platform: Cloudflare did not confirm every requested R2 object deletion.");
584
586
  }
585
- async deleteResource(accountId, kind, id, zoneId) {
587
+ async deleteResource(accountId, resource, zoneId) {
588
+ const { kind } = resource;
589
+ if (kind === "ai-gateway" && this.#aiGatewayClient) return this.#aiGatewayClient.deleteResource(accountId, resource, zoneId);
586
590
  const account = encodeURIComponent(accountId);
587
- const encoded = encodeURIComponent(id);
591
+ const encoded = encodeURIComponent(kind === "dispatch-namespace" || kind === "worker" ? resource.name : resource.id);
588
592
  const path = kind === "kv-namespace" ? `/accounts/${account}/storage/kv/namespaces/${encoded}` : kind === "dns-record" ? zoneId ? `/zones/${encodeURIComponent(zoneId)}/dns_records/${encoded}` : null : kind === "ai-gateway" ? `/accounts/${account}/ai-gateway/gateways/${encoded}` : kind === "r2-bucket" ? `/accounts/${account}/r2/buckets/${encoded}` : kind === "queue" ? `/accounts/${account}/queues/${encoded}` : kind === "dispatch-namespace" ? `/accounts/${account}/workers/dispatch/namespaces/${encoded}` : kind === "hyperdrive" ? `/accounts/${account}/hyperdrive/configs/${encoded}` : kind === "worker" ? `/accounts/${account}/workers/scripts/${encoded}` : kind === "zone" ? `/zones/${encoded}` : null;
589
593
  if (!path) throw new Error(`platform: Unsupported Cloudflare deletion kind ${kind}.`);
594
+ await this.#deleteIfPresent(path);
595
+ }
596
+ /**
597
+ * DELETE one resource by the key Cloudflare addresses it with. Only
598
+ * `forceDeleteWorker` sends `force`; otherwise Cloudflare refuses a
599
+ * resource another Worker still uses. A 404 means it is already gone.
600
+ */
601
+ async #deleteIfPresent(path) {
602
+ const response = await this.#fetcher(`${this.apiBaseUrl}${path}`, {
603
+ headers: {
604
+ Accept: "application/json",
605
+ Authorization: `Bearer ${this.#token}`
606
+ },
607
+ method: "DELETE",
608
+ redirect: "manual",
609
+ signal: AbortSignal.timeout(3e4)
610
+ });
611
+ if (response.status === 404) return;
612
+ let envelope = {};
613
+ try {
614
+ envelope = await response.json();
615
+ } catch {
616
+ envelope = { success: response.ok };
617
+ }
618
+ if (!response.ok || envelope.success !== true) throw new PlatformCloudflareApiError(`Cloudflare API error: ${cloudflareErrorDetails(envelope, response.status)} (HTTP ${response.status}; DELETE ${path}).`, response.status, path);
619
+ }
620
+ async deleteWorkerRoute(zoneId, routeId) {
621
+ await this.#deleteIfPresent(`/zones/${encodeURIComponent(zoneId)}/workers/routes/${encodeURIComponent(routeId)}`);
622
+ }
623
+ async deleteWorkerDomain(accountId, domainId) {
624
+ await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/domains/${encodeURIComponent(domainId)}`);
625
+ }
626
+ async listQueueConsumers(accountId, queueId) {
627
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queueId)}/consumers`)).map((entry) => ({
628
+ id: requiredString(entry.consumer_id, "Queue consumer id"),
629
+ script: entry.type === "http_pull" ? null : requiredString(entry.script_name ?? entry.script ?? entry.service, "Queue consumer Worker name")
630
+ }));
631
+ }
632
+ async listWorkerDependents(accountId, workerName) {
633
+ const account = encodeURIComponent(accountId);
634
+ const script = encodeURIComponent(workerName);
635
+ const references = await this.request(`/accounts/${account}/workers/scripts/${script}/references`);
636
+ const tails = await this.request(`/accounts/${account}/workers/tails/by-consumer/${script}`);
637
+ const name = (reference) => requiredString(reference?.script ?? reference?.service, "Dependent Worker name");
638
+ const workers = [
639
+ ...(references.services?.incoming ?? []).map(name),
640
+ ...(references.durable_objects ?? []).map(name).filter((entry) => entry !== workerName),
641
+ ...(references.dispatch_outbounds ?? []).map(name),
642
+ ...tails.map((tail) => name(tail.producer))
643
+ ];
644
+ return {
645
+ pagesFunction: references.services?.pages_function === true,
646
+ workers: [...new Set(workers)]
647
+ };
648
+ }
649
+ /**
650
+ * DELETE a Worker even though other Workers still bind to it. Cloudflare
651
+ * leaves those bindings pointing at nothing, so callers first check that
652
+ * every dependent is going away too.
653
+ */
654
+ async forceDeleteWorker(accountId, workerName) {
655
+ await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/scripts/${encodeURIComponent(workerName)}?force=true`);
656
+ }
657
+ async deleteQueueConsumer(accountId, queueId, consumerId) {
658
+ await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/queues/${encodeURIComponent(queueId)}/consumers/${encodeURIComponent(consumerId)}`);
659
+ }
660
+ async listDispatchNamespaceScripts(accountId, namespace) {
661
+ return (await this.request(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces/${encodeURIComponent(namespace)}/scripts`)).map((entry) => requiredString(entry.id, "Dispatch namespace script name"));
662
+ }
663
+ async deleteDispatchNamespaceScript(accountId, namespace, scriptName) {
664
+ await this.#deleteIfPresent(`/accounts/${encodeURIComponent(accountId)}/workers/dispatch/namespaces/${encodeURIComponent(namespace)}/scripts/${encodeURIComponent(scriptName)}`);
665
+ }
666
+ /** Read one Access application or service token; null when it is gone. */
667
+ async getAccessResource(accountId, kind, id) {
590
668
  try {
591
- await this.request(path, { method: "DELETE" });
669
+ const result = await this.request(accessResourcePath(accountId, kind, id));
670
+ return {
671
+ id: requiredString(result.id, "Access resource id"),
672
+ name: requiredString(result.name, "Access resource name")
673
+ };
592
674
  } catch (error) {
593
- if (error instanceof PlatformCloudflareApiError && error.status === 404) return;
675
+ if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
676
+ throw error;
677
+ }
678
+ }
679
+ async deleteAccessResource(accountId, kind, id) {
680
+ await this.#deleteIfPresent(accessResourcePath(accountId, kind, id));
681
+ }
682
+ /** Email Routing rules in a zone, without the catch-all, which has its own endpoint. */
683
+ async listEmailRoutingRules(zoneId) {
684
+ const entries = [];
685
+ for (let page = 1;; page++) {
686
+ const query = new URLSearchParams({
687
+ page: String(page),
688
+ per_page: "50"
689
+ });
690
+ const result = await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules?${query}`);
691
+ entries.push(...result);
692
+ if (result.length < 50) break;
693
+ }
694
+ return entries.filter((entry) => !(Array.isArray(entry.matchers) ? entry.matchers : []).some((matcher) => matcher?.type === "all")).map((entry) => ({
695
+ id: requiredString(entry.id ?? entry.tag, "Email Routing rule id"),
696
+ ...parseEmailRoutingTarget(entry)
697
+ }));
698
+ }
699
+ async deleteEmailRoutingRule(zoneId, ruleId) {
700
+ await this.#deleteIfPresent(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/${encodeURIComponent(ruleId)}`);
701
+ }
702
+ /** The zone's Email Routing catch-all rule; null when the zone has none. */
703
+ async getEmailRoutingCatchAll(zoneId) {
704
+ try {
705
+ return parseEmailRoutingTarget(await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/catch_all`));
706
+ } catch (error) {
707
+ if (error instanceof PlatformCloudflareApiError && error.status === 404) return null;
594
708
  throw error;
595
709
  }
596
710
  }
711
+ /** Point the catch-all back at its disabled default, which drops mail. */
712
+ async resetEmailRoutingCatchAll(zoneId) {
713
+ await this.request(`/zones/${encodeURIComponent(zoneId)}/email/routing/rules/catch_all`, {
714
+ method: "PUT",
715
+ body: JSON.stringify({
716
+ matchers: [{ type: "all" }],
717
+ actions: [{ type: "drop" }],
718
+ enabled: false
719
+ })
720
+ });
721
+ }
597
722
  async removeRoutes(_accountId, _zoneId, tracked) {
598
723
  if (tracked.routes.length > 0 || tracked.customDomains.length > 0) throw new Error("platform: Cloudflare does not support conditional Worker traffic deletion. Retaining routes and custom domains for explicit dashboard cleanup.");
599
724
  }
@@ -613,9 +738,34 @@ var PlatformCloudflareClient = class PlatformCloudflareClient {
613
738
  }
614
739
  }
615
740
  };
741
+ function accessResourcePath(accountId, kind, id) {
742
+ return `/accounts/${encodeURIComponent(accountId)}/access/${kind === "application" ? "apps" : "service_tokens"}/${encodeURIComponent(id)}`;
743
+ }
744
+ function parseEmailRoutingTarget(rule) {
745
+ const actions = Array.isArray(rule.actions) ? rule.actions : [];
746
+ const action = actions.length === 1 ? actions[0] : null;
747
+ const value = Array.isArray(action?.value) ? action.value : [];
748
+ return {
749
+ enabled: rule.enabled === true,
750
+ workerName: action?.type === "worker" && value.length === 1 && typeof value[0] === "string" ? value[0] : null
751
+ };
752
+ }
753
+ function cloudflareErrorDetails(envelope, status) {
754
+ return envelope.errors?.filter((error) => error.message).map((error) => error.code === void 0 ? error.message : `${error.message} (code ${error.code})`).join(", ") || `HTTP ${status}`;
755
+ }
616
756
  function encodeR2ObjectKey(key) {
617
757
  return key.split("/").map(encodeURIComponent).join("/");
618
758
  }
759
+ /**
760
+ * An AI Gateway creation time as ISO 8601 UTC. Cloudflare reports it without a
761
+ * zone ("2026-10-05 18:29:14"), which JavaScript would read as local time.
762
+ */
763
+ function withCreatedAt(value) {
764
+ if (typeof value !== "string") return {};
765
+ const zoneless = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(value);
766
+ const time = Date.parse(zoneless ? `${value.replace(" ", "T")}Z` : value);
767
+ return Number.isNaN(time) ? {} : { createdAt: new Date(time).toISOString() };
768
+ }
619
769
  function requiredString(value, label) {
620
770
  if (typeof value !== "string" || value === "") throw new Error(`platform: ${label} is missing from the Cloudflare response.`);
621
771
  return value;
@@ -1203,14 +1353,26 @@ function platformAccountTokenPermissions(includeAiGateway) {
1203
1353
  }] : []
1204
1354
  ];
1205
1355
  }
1356
+ /**
1357
+ * Account token links cannot preselect a zone: Cloudflare documents only
1358
+ * `permissionGroupKeys` and `name`, and the form puts zone permissions in an
1359
+ * "All Domains" policy. Tell the user which dashboard choice to change.
1360
+ */
1361
+ function specifiedDomainsInstruction(zones) {
1362
+ return `Change the zone policy from All Domains to Specified Domains → ${zones.join(" and ")}.`;
1363
+ }
1364
+ function plansZoneCreation(plan) {
1365
+ return plan.actions.some((action) => action.kind === "zone" && action.action === "create");
1366
+ }
1206
1367
  function platformManagementSetupPage(plan, options = {}) {
1207
- const createsZone = plan.actions.some((action) => action.kind === "zone" && action.action === "create");
1368
+ const createsZone = plansZoneCreation(plan);
1208
1369
  const zoneName = plan.deployment.application.zoneName;
1209
1370
  const apiHostname = plan.deployment.controlPlane.apiHostname;
1210
1371
  const access = options.access ?? (plan.deployment.authentication.accessProtection || plan.deployment.authentication.providers.includes("cloudflare-access") ? {
1211
1372
  protection: !!plan.deployment.authentication.accessProtection,
1212
1373
  readOnly: false
1213
1374
  } : void 0);
1375
+ const zones = [...zoneName ? [zoneName] : [], ...apiHostname && apiHostname !== zoneName && !apiHostname.endsWith(`.${zoneName}`) ? [`the zone for ${apiHostname}`] : []];
1214
1376
  const accessLevel = access && access.readOnly ? "read" : "edit";
1215
1377
  const appAccess = !!plan.deployment.appAccess;
1216
1378
  const appAccessLevel = appAccess ? "edit" : accessLevel;
@@ -1259,8 +1421,8 @@ function platformManagementSetupPage(plan, options = {}) {
1259
1421
  }),
1260
1422
  instructions: [
1261
1423
  "This token lets the Void CLI install your platform. Void will now open a pre-configured token creation site. Please review the permissions and then paste the token here.",
1262
- `Scope it to ${plan.account.name}${createsZone ? "; allow zone creation." : zoneName ? ` and zone ${zoneName}.` : "."}`,
1263
- ...apiHostname && apiHostname !== zoneName && !apiHostname.endsWith(`.${zoneName}`) ? [`Also include the zone for ${apiHostname}.`] : []
1424
+ `Scope it to ${plan.account.name}.`,
1425
+ ...createsZone ? [`Keep the zone policy on All Domains so Void can create ${zoneName}.`] : zones.length ? [specifiedDomainsInstruction(zones)] : []
1264
1426
  ].join("\n")
1265
1427
  };
1266
1428
  }
@@ -1298,6 +1460,7 @@ function platformSetupPages(plan) {
1298
1460
  }
1299
1461
  ] : []
1300
1462
  ];
1463
+ const zoneName = plan.deployment.application.zoneName;
1301
1464
  return {
1302
1465
  runtime: {
1303
1466
  title: "Cloudflare runtime token",
@@ -1307,7 +1470,7 @@ function platformSetupPages(plan) {
1307
1470
  name: plan.deployment.displayName + " Runtime",
1308
1471
  permissionGroupKeys: JSON.stringify(permissions)
1309
1472
  }),
1310
- instructions: "Create a Cloudflare runtime token that Void can use to manage the platform. Please review the permissions and then paste the token here."
1473
+ instructions: ["Create a Cloudflare runtime token that Void can use to manage the platform. Please review the permissions and then paste the token here.", ...zoneName && plansZoneCreation(plan) ? [`Keep the zone policy on All Domains so the token can purge cache on ${zoneName} after Void creates it.`] : zoneName ? [specifiedDomainsInstruction([zoneName, ...plan.deployment.capabilities.email ? ["every zone that will carry mail"] : []])] : []].join("\n")
1311
1474
  },
1312
1475
  github: {
1313
1476
  title: "GitHub OAuth App",
@@ -1746,6 +1909,28 @@ async function preparePlatformAccessSetup(input) {
1746
1909
  }
1747
1910
  //#endregion
1748
1911
  //#region src/cli/platform-auth.ts
1912
+ /** Use the selected credential for read-only checks before taking lifecycle locks. */
1913
+ async function verifyPlatformCloudflareCredential(client, input) {
1914
+ const source = input.source === "saved" ? "saved management token" : input.source === "environment" ? "environment API token" : "Cloudflare browser login";
1915
+ const retry = input.source === "oauth" ? "Run `void cloudflare login`, or set CLOUDFLARE_API_TOKEN for this account, then retry." : "Set CLOUDFLARE_API_TOKEN for this account, then retry.";
1916
+ const check = async (label, permission, action, allowMissing = false) => {
1917
+ try {
1918
+ await action();
1919
+ } catch (cause) {
1920
+ const status = cause instanceof PlatformCloudflareApiError ? cause.status : null;
1921
+ if (status === 404 && allowMissing) return;
1922
+ const detail = status === 401 || status === 403 ? `Check that the credential is valid, scoped to this account, and includes ${permission}. ${retry}` : status === 404 ? "Check the bucket name and account scope before retrying." : status === 429 ? "Cloudflare rate-limited this check. Retry shortly." : "Retry the command after Cloudflare connectivity is restored.";
1923
+ throw new Error(`platform: ${label} is unavailable${status === null ? "" : ` (HTTP ${status})`} using the ${source}. ${detail}`, { cause });
1924
+ }
1925
+ };
1926
+ let accounts = [];
1927
+ await check("Cloudflare account access", "Account → Account Settings → Read", async () => {
1928
+ accounts = await client.listAccounts();
1929
+ });
1930
+ if (!accounts.some((account) => account.id === input.account.id)) throw new Error(`platform: The ${source} cannot access Cloudflare account ${JSON.stringify(input.account.name)} (${input.account.id}). ${retry}`);
1931
+ await check("Cloudflare Workers access", "Account → Workers Scripts → Edit", () => client.listWorkerNames(input.account.id));
1932
+ for (const name of new Set(input.r2Buckets)) await check(`R2 bucket ${JSON.stringify(name)}`, "Account → Workers R2 Storage → Edit", () => client.request(`/accounts/${encodeURIComponent(input.account.id)}/r2/buckets/${encodeURIComponent(name)}`), input.allowMissingR2Buckets);
1933
+ }
1749
1934
  /** Check Access inventory permissions before the installer starts provisioning. */
1750
1935
  async function verifyPlatformManagementAccess(client, accountId, access) {
1751
1936
  const base = `/accounts/${encodeURIComponent(accountId)}/access`;
@@ -2516,7 +2701,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2516
2701
  client.listHyperdriveConfigs(account.id),
2517
2702
  client.listWorkerNames(account.id)
2518
2703
  ]);
2519
- const dnsRecords = zone ? await client.listDnsRecords(zone.id, wildcardHostname) : [];
2704
+ const dnsRecords = zone ? await listZoneDnsRecords(client, zone, wildcardHostname) : [];
2520
2705
  const remoteOwnership = existing ? await readRemoteOwnership(client, account.id, routingKv, existing) : null;
2521
2706
  const intentProofs = /* @__PURE__ */ new Set();
2522
2707
  const ambiguousResources = /* @__PURE__ */ new Set();
@@ -2560,7 +2745,7 @@ async function createPlatformInstallPlan(client, input, existing, options) {
2560
2745
  const matchingDomain = workerDomains.find((domain) => domain.hostname === apiHostname);
2561
2746
  const expectedWorker = deployment.resources.apiWorker;
2562
2747
  if (matchingDomain && matchingDomain.service !== expectedWorker) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} is already assigned to Worker ${JSON.stringify(matchingDomain.service)}. Choose another hostname.`);
2563
- const records = controlPlaneZone ? await client.listDnsRecords(controlPlaneZone.id, apiHostname) : [];
2748
+ const records = controlPlaneZone ? await listZoneDnsRecords(client, controlPlaneZone, apiHostname) : [];
2564
2749
  const ownedApiWorker = intentProofs.has("api-worker") || existing?.resources.some((resource) => resource.key === "api-worker" && resource.kind === "worker" && resource.name === expectedWorker && resource.state === "ready");
2565
2750
  if (records.length > 0 && (!matchingDomain || !ownedApiWorker)) throw new Error(`platform: Custom API hostname ${JSON.stringify(apiHostname)} already has a DNS record that installation would replace. Choose another hostname or remove the record explicitly.`);
2566
2751
  }
@@ -2926,6 +3111,14 @@ function platformAdoptionIntentHash(manifest, key, kind, id, name) {
2926
3111
  ownershipNonce: manifest.ownershipNonce
2927
3112
  })).digest("hex");
2928
3113
  }
3114
+ async function listZoneDnsRecords(client, zone, hostname) {
3115
+ try {
3116
+ return await client.listDnsRecords(zone.id, hostname);
3117
+ } catch (error) {
3118
+ if (error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403)) throw new Error(`platform: Cloudflare denied reading DNS records for ${zone.name} (HTTP ${error.status}). Add Zone → DNS → Edit for this zone to the management token, then rerun.`, { cause: error });
3119
+ throw error;
3120
+ }
3121
+ }
2929
3122
  async function readRemoteOwnership(client, accountId, routingNamespaces, existing) {
2930
3123
  for (const namespace of routingNamespaces) {
2931
3124
  const value = await client.getKvValue(accountId, namespace.id, PLATFORM_REMOTE_INSTALLATION_MANIFEST_KEY);
@@ -2991,6 +3184,22 @@ function trafficConfigHash(kind, value) {
2991
3184
  value
2992
3185
  })).digest("hex");
2993
3186
  }
3187
+ /**
3188
+ * The installer Worker that a recorded route or custom domain serves, when the
3189
+ * record matches exactly what the installer publishes for this deployment.
3190
+ */
3191
+ function platformTrafficAttachmentWorker(deployment, resource) {
3192
+ const managed = platformManagedWorkerTraffic(deployment);
3193
+ if (resource.kind === "worker-route") {
3194
+ const target = managed.routes.find((route) => route.pattern === resource.name);
3195
+ return target && trafficConfigHash("worker-route", target) === resource.configHash ? target.script : null;
3196
+ }
3197
+ if (resource.kind === "worker-custom-domain") {
3198
+ const target = managed.customDomains.find((domain) => domain.hostname === resource.name);
3199
+ return target && trafficConfigHash("worker-custom-domain", target) === resource.configHash ? target.service : null;
3200
+ }
3201
+ return null;
3202
+ }
2994
3203
  function trackedPlatformWorkerTraffic(_manifest, _workers) {
2995
3204
  return {
2996
3205
  customDomains: [],
@@ -3662,7 +3871,7 @@ async function runPlatformWrangler(operation, accountId, cwd, capture) {
3662
3871
  return runPlatformWranglerAtApiBase(operation, accountId, cwd, capture);
3663
3872
  }
3664
3873
  const PLATFORM_WRANGLER_TIMEOUT_MS = 6e5;
3665
- async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false) {
3874
+ async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl = resolvePinnedApiBaseUrl(), parallelWorkerUpload = false, apiToken) {
3666
3875
  const startedAt = performance.now();
3667
3876
  const result = await createDefaultRunWrangler(process.cwd(), {
3668
3877
  launcher: resolveCloudflareOperationArgv(process.cwd()),
@@ -3671,6 +3880,7 @@ async function runPlatformWranglerAtApiBase(operation, accountId, cwd, capture,
3671
3880
  })(operation, {
3672
3881
  accountId,
3673
3882
  apiBaseUrl,
3883
+ apiToken,
3674
3884
  cwd
3675
3885
  });
3676
3886
  if (process.env.VOID_PLATFORM_TIMINGS === "1") process.stderr.write(`[platform timing] Cloudflare ${operation.type} ${(performance.now() - startedAt).toFixed(0)} ms\n`);
@@ -3802,8 +4012,8 @@ async function withConditionalPlatformWorkerCreate(accountId, workerName, operat
3802
4012
  if (proxy.state.conflictStatus !== null) throw new Error(`platform: Worker ${workerName} was created concurrently (HTTP ${proxy.state.conflictStatus}); no existing Worker was changed. Re-run to inspect the collision.`);
3803
4013
  return result;
3804
4014
  }
3805
- async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl) {
3806
- return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl), upstreamApiBaseUrl);
4015
+ async function runPlatformWranglerForConditionalCreate(operation, accountId, cwd, capture, workerName, upstreamApiBaseUrl, apiToken) {
4016
+ return withConditionalPlatformWorkerCreate(accountId, workerName, (apiBaseUrl) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, apiBaseUrl, false, apiToken), upstreamApiBaseUrl);
3807
4017
  }
3808
4018
  //#endregion
3809
4019
  //#region src/cli/platform-installer-inspection.ts
@@ -4563,7 +4773,7 @@ async function deployPlatformWorker(config, secrets, options) {
4563
4773
  let uploaded;
4564
4774
  let uploadError;
4565
4775
  try {
4566
- uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl);
4776
+ uploaded = options.workerExisted ? await options.runner(operation, accountId, directory, true) : await runPlatformWranglerForConditionalCreate(operation, accountId, directory, true, workerName, options.apiBaseUrl, options.operatorApiToken);
4567
4777
  assertCleanPlatformWrangler(uploaded, `${options.workerExisted ? "upload" : "bootstrap"} ${workerName}`);
4568
4778
  } catch (error) {
4569
4779
  uploadError = error;
@@ -5082,7 +5292,7 @@ async function verifyInstalledPlatformAccessProtection(input) {
5082
5292
  }
5083
5293
  const existingWorkers = input.plan.actions.filter((action) => action.kind === "worker" && action.action === "update").map((action) => action.name);
5084
5294
  if (existingWorkers.length) {
5085
- const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-DFeJ-JkK.mjs");
5295
+ const { verifyProtectionWorkerOrigins } = await import("./platform-auth-protection-DcbL_LW_.mjs");
5086
5296
  await verifyProtectionWorkerOrigins({
5087
5297
  request: input.client.request.bind(input.client),
5088
5298
  listWorkerDomains: input.client.listWorkerDomains.bind(input.client),
@@ -5343,6 +5553,605 @@ function verifyPlatformDatabaseArtifacts(runtimeDirectory, database) {
5343
5553
  }
5344
5554
  }
5345
5555
  //#endregion
5556
+ //#region src/cli/platform-teardown.ts
5557
+ /**
5558
+ * `void platform uninstall --purge-resources` deletes every Cloudflare resource the
5559
+ * installation created, in an order Cloudflare accepts:
5560
+ *
5561
+ * traffic (routes, custom domains, DNS) -> Access -> Email Routing rules
5562
+ * -> queue consumers -> Workers except tail -> Queues -> dispatch namespace
5563
+ * -> tail Worker -> Hyperdrive and KV -> R2 -> AI Gateway -> routing KV (last)
5564
+ *
5565
+ * Each resource is proven to belong to this installation right before its
5566
+ * DELETE, and Cloudflare refuses a resource that something outside the
5567
+ * installation still uses. The only forced DELETE is for a platform Worker
5568
+ * that Cloudflare refused while every Worker it names as a dependent is one
5569
+ * this installation created: the API and proxy Workers bind to each other, so
5570
+ * neither can go first without it. A route, custom domain, DNS
5571
+ * record, or Access resource that no longer matches what was recorded is kept
5572
+ * and listed with the reason instead of stopping the run. Every finished
5573
+ * deletion is checkpointed, so rerunning the command continues where it
5574
+ * stopped. The routing KV namespace holds that checkpoint, so uninstall
5575
+ * deletes it last.
5576
+ *
5577
+ * An uninstall without --purge-resources records what it leaves as retained. If it
5578
+ * stopped before it finished, --purge-resources considers those resources again.
5579
+ */
5580
+ const PURGE_RESOURCES_STEP_PREFIX = "uninstall:purge-resources:";
5581
+ const WORKER_ORDER = [
5582
+ "email-gateway-worker",
5583
+ "proxy-worker",
5584
+ "dispatch-worker",
5585
+ "api-worker"
5586
+ ];
5587
+ const PERMISSIONS = {
5588
+ access: "Account → Access: Apps and Policies → Edit",
5589
+ aiGateway: "Account → AI Gateway → Edit",
5590
+ dns: "Zone → DNS → Edit",
5591
+ emailRouting: "Zone → Email Routing Rules → Edit",
5592
+ hyperdrive: "Account → Hyperdrive → Edit",
5593
+ kv: "Account → Workers KV Storage → Edit",
5594
+ queues: "Account → Queues → Edit",
5595
+ r2: "Account → Workers R2 Storage → Edit",
5596
+ routes: "Zone → Workers Routes → Edit",
5597
+ serviceTokens: "Account → Access: Service Tokens → Edit",
5598
+ workers: "Account → Workers Scripts → Edit"
5599
+ };
5600
+ function isPlatformTailWorker(manifest, resource) {
5601
+ return resource.kind === "worker" && (resource.key === "tail-worker" || resource.name === manifest.deployment.resources.tailWorker);
5602
+ }
5603
+ function purgeResourcesResumeCommand(manifest) {
5604
+ return `void platform uninstall ${manifest.id} --purge-resources`;
5605
+ }
5606
+ function accessStepKey(access) {
5607
+ return `${PURGE_RESOURCES_STEP_PREFIX}access:${access.key}`;
5608
+ }
5609
+ /** Access resources that --purge-resources already deleted. */
5610
+ function isPurgedAccessResource(manifest, access) {
5611
+ return manifest.steps.some((step) => step.key === accessStepKey(access));
5612
+ }
5613
+ function hasStep(manifest, key) {
5614
+ return manifest.steps.some((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${key}`);
5615
+ }
5616
+ function keptStepKey(key) {
5617
+ return `kept:${key}`;
5618
+ }
5619
+ /**
5620
+ * Why --purge-resources kept a resource it was going to delete, for example because
5621
+ * a route now serves another Worker; null when it did not keep it. Access
5622
+ * resources use the key `access:<key>`.
5623
+ */
5624
+ function purgeResourcesKeptReason(manifest, key) {
5625
+ return manifest.steps.find((step) => step.key === `uninstall:purge-resources:${keptStepKey(key)}`)?.resultHash ?? null;
5626
+ }
5627
+ /** An uninstall that started has not finished, so what it kept can be reconsidered. */
5628
+ function isReopenable(manifest) {
5629
+ return manifest.status !== "uninstalled";
5630
+ }
5631
+ /**
5632
+ * A resource the installation created that --purge-resources may still delete: one
5633
+ * that is ready, or one that an uninstall which stopped before it finished
5634
+ * recorded as retained. Resources --purge-resources itself kept, and earlier
5635
+ * versions of a resource kept when it was replaced, are never reconsidered.
5636
+ */
5637
+ function isPurgeResourcesCandidate(manifest, resource) {
5638
+ return resource.ownership === "created" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest) && !resource.key.startsWith("retained:") && purgeResourcesKeptReason(manifest, resource.key) === null);
5639
+ }
5640
+ /** The zone that routes and DNS records live in, if it can still be used. */
5641
+ function usableZone(manifest) {
5642
+ return manifest.resources.find((resource) => resource.key === "zone" && (resource.state === "ready" || resource.state === "retained" && isReopenable(manifest)));
5643
+ }
5644
+ /**
5645
+ * A recorded resource whose live configuration no longer matches the
5646
+ * installation's record. --purge-resources keeps such a DNS record and continues.
5647
+ */
5648
+ var PlatformResourceChangedError = class extends Error {
5649
+ resourceKey;
5650
+ reason;
5651
+ constructor(resourceKey, reason, message) {
5652
+ super(message);
5653
+ this.resourceKey = resourceKey;
5654
+ this.reason = reason;
5655
+ this.name = "PlatformResourceChangedError";
5656
+ }
5657
+ };
5658
+ /** The zone whose Email Routing rules may send mail to the gateway Worker. */
5659
+ function emailRoutingZone(manifest) {
5660
+ return manifest.deployment.capabilities.email ? manifest.deployment.emailSharedZoneId ?? null : null;
5661
+ }
5662
+ /** Everything --purge-resources will delete, in deletion order. */
5663
+ function purgeResourcesTargets(manifest) {
5664
+ const resources = manifest.resources;
5665
+ const routing = resources.find((resource) => resource.key === "routing-kv");
5666
+ const candidate = (resource) => isPurgeResourcesCandidate(manifest, resource);
5667
+ if (!routing || routing.state !== "ready" && !candidate(routing)) return [];
5668
+ const zone = usableZone(manifest);
5669
+ const workers = resources.filter((resource) => resource.kind === "worker" && candidate(resource));
5670
+ const targets = [];
5671
+ for (const resource of resources) {
5672
+ if (resource.kind !== "worker-route" && resource.kind !== "worker-custom-domain" || resource.state !== "ready" && resource.state !== "retained" || resource.kind === "worker-route" && !zone || purgeResourcesKeptReason(manifest, resource.key) !== null) continue;
5673
+ const name = platformTrafficAttachmentWorker(manifest.deployment, resource);
5674
+ const worker = workers.find((entry) => entry.name === name);
5675
+ if (worker) targets.push({
5676
+ phase: "traffic",
5677
+ resource,
5678
+ worker
5679
+ });
5680
+ }
5681
+ for (const resource of resources) if (resource.kind === "dns-record" && candidate(resource) && zone) targets.push({
5682
+ phase: "dns",
5683
+ resource
5684
+ });
5685
+ const access = (manifest.authenticationResources ?? []).filter((entry) => entry.ownership === "created" && !isPurgedAccessResource(manifest, entry) && purgeResourcesKeptReason(manifest, `access:${entry.key}`) === null);
5686
+ for (const kind of ["application", "service-token"]) for (const entry of access.filter((candidate) => candidate.kind === kind)) targets.push({
5687
+ phase: "access",
5688
+ access: entry
5689
+ });
5690
+ const gateway = workers.find((worker) => worker.key === "email-gateway-worker");
5691
+ if (gateway && emailRoutingZone(manifest) && !hasStep(manifest, "email-routing")) targets.push({
5692
+ phase: "email",
5693
+ resource: gateway
5694
+ });
5695
+ const queues = resources.filter((resource) => resource.kind === "queue" && candidate(resource));
5696
+ for (const resource of queues) if (!hasStep(manifest, `consumers:${resource.key}`)) targets.push({
5697
+ phase: "queue-consumers",
5698
+ resource
5699
+ });
5700
+ const rank = (resource) => {
5701
+ const index = WORKER_ORDER.indexOf(resource.key);
5702
+ return index < 0 ? WORKER_ORDER.length : index;
5703
+ };
5704
+ for (const resource of workers.filter((worker) => !isPlatformTailWorker(manifest, worker)).sort((left, right) => rank(left) - rank(right) || left.key.localeCompare(right.key))) targets.push({
5705
+ phase: "workers",
5706
+ resource
5707
+ });
5708
+ for (const resource of queues) targets.push({
5709
+ phase: "queues",
5710
+ resource
5711
+ });
5712
+ for (const resource of resources) if (resource.kind === "dispatch-namespace" && candidate(resource)) targets.push({
5713
+ phase: "dispatch",
5714
+ resource
5715
+ });
5716
+ for (const resource of workers.filter((worker) => isPlatformTailWorker(manifest, worker))) targets.push({
5717
+ phase: "tail",
5718
+ resource
5719
+ });
5720
+ for (const kind of ["hyperdrive", "kv-namespace"]) for (const resource of resources) if (resource.kind === kind && resource.key !== "routing-kv" && candidate(resource)) targets.push({
5721
+ phase: "data",
5722
+ resource
5723
+ });
5724
+ for (const kind of ["r2-bucket", "ai-gateway"]) for (const resource of resources) if (resource.kind === kind && candidate(resource)) targets.push({
5725
+ phase: kind === "r2-bucket" ? "r2" : "ai-gateway",
5726
+ resource
5727
+ });
5728
+ if (candidate(routing)) targets.push({
5729
+ phase: "routing",
5730
+ resource: routing
5731
+ });
5732
+ return targets;
5733
+ }
5734
+ /** Whether --purge-resources removes, or has removed, the rules that send mail to the gateway Worker. */
5735
+ function purgeResourcesHandlesEmailRouting(manifest) {
5736
+ return hasStep(manifest, "email-routing") || purgeResourcesTargets(manifest).some((target) => target.phase === "email");
5737
+ }
5738
+ /** Manifest resources that --purge-resources leaves in the account. */
5739
+ function purgeResourcesKeptResources(manifest) {
5740
+ const targets = new Set(purgeResourcesTargets(manifest).flatMap((target) => "resource" in target ? [target.resource.key] : []));
5741
+ return manifest.resources.filter((resource) => (resource.state === "ready" || resource.state === "retained") && !targets.has(resource.key));
5742
+ }
5743
+ function resourceLabel(resource) {
5744
+ return `${resource.kind}: ${resource.name}${resource.name === resource.id ? "" : ` (${resource.id})`}`;
5745
+ }
5746
+ /** Plan lines for --purge-resources: what is deleted, in order, then what is kept. */
5747
+ function describePurgeResourcesPlan(manifest) {
5748
+ const targets = purgeResourcesTargets(manifest);
5749
+ const lines = [];
5750
+ let step = 0;
5751
+ let previous = null;
5752
+ const consumerQueues = [];
5753
+ for (const target of targets) {
5754
+ if (target.phase === "email") {
5755
+ step++;
5756
+ previous = target.phase;
5757
+ lines.push(` ${step}. Email Routing rules in zone ${emailRoutingZone(manifest)} that send mail to ${target.resource.name}, and its catch-all rule if it does (reset to drop mail, disabled)`);
5758
+ continue;
5759
+ }
5760
+ if (target.phase !== previous) {
5761
+ step++;
5762
+ previous = target.phase;
5763
+ }
5764
+ const prefix = ` ${step}. `;
5765
+ if (target.phase === "traffic") lines.push(`${prefix}${resourceLabel(target.resource)} -> ${target.worker.name}`);
5766
+ else if (target.phase === "access") lines.push(`${prefix}Access ${target.access.kind}: ${target.access.name} (${target.access.id})`);
5767
+ else if (target.phase === "queue-consumers") {
5768
+ consumerQueues.push(target.resource.name);
5769
+ if (consumerQueues.length === 1) lines.push(`${prefix}queue consumers run by this installation's Workers on:`);
5770
+ lines.push(` ${target.resource.name}`);
5771
+ } else if (target.phase === "dispatch") lines.push(`${prefix}${resourceLabel(target.resource)}, with any application Workers still in it`);
5772
+ else if (target.phase === "tail") lines.push(`${prefix}${resourceLabel(target.resource)}, the tail Worker, after the others`);
5773
+ else if (target.phase === "workers" && target.resource.key === "api-worker") lines.push(`${prefix}${resourceLabel(target.resource)}, with its Durable Object data`);
5774
+ else if (target.phase === "r2") lines.push(`${prefix}${resourceLabel(target.resource)}, emptied first`);
5775
+ else if (target.phase === "ai-gateway") lines.push(`${prefix}${resourceLabel(target.resource)}, if Cloudflare shows it was created during installation; otherwise kept`);
5776
+ else if (target.phase === "routing") lines.push(`${prefix}${resourceLabel(target.resource)}, last; it holds uninstall progress`);
5777
+ else lines.push(`${prefix}${resourceLabel(target.resource)}`);
5778
+ }
5779
+ return lines.length ? ["Delete every resource this installation created (--purge-resources), in this order:", ...lines] : ["--purge-resources finds no resource it can prove this installation created."];
5780
+ }
5781
+ /** Lines naming what --purge-resources never removes. */
5782
+ function purgeResourcesAlwaysKept() {
5783
+ return [
5784
+ " The external PostgreSQL database and its data.",
5785
+ " Analytics Engine datasets; Cloudflare has no API to delete them.",
5786
+ " Access policies and identity providers you chose.",
5787
+ " API tokens you created for this platform. Revoke them in the Cloudflare dashboard."
5788
+ ];
5789
+ }
5790
+ /** Token permissions --purge-resources needs for this manifest. */
5791
+ function purgeResourcesPermissions(manifest) {
5792
+ const targets = purgeResourcesTargets(manifest);
5793
+ const needed = /* @__PURE__ */ new Set();
5794
+ for (const target of targets) {
5795
+ if (target.phase === "access") {
5796
+ needed.add(target.access.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens);
5797
+ continue;
5798
+ }
5799
+ if (target.phase === "email") {
5800
+ needed.add(PERMISSIONS.emailRouting);
5801
+ continue;
5802
+ }
5803
+ const { kind } = target.resource;
5804
+ if (kind === "worker-route") needed.add(PERMISSIONS.routes);
5805
+ else if (kind === "worker" || kind === "worker-custom-domain" || kind === "dispatch-namespace") needed.add(PERMISSIONS.workers);
5806
+ else if (kind === "dns-record") needed.add(PERMISSIONS.dns);
5807
+ else if (kind === "queue") needed.add(PERMISSIONS.queues);
5808
+ else if (kind === "kv-namespace") needed.add(PERMISSIONS.kv);
5809
+ else if (kind === "hyperdrive") needed.add(PERMISSIONS.hyperdrive);
5810
+ else if (kind === "r2-bucket") needed.add(PERMISSIONS.r2);
5811
+ else if (kind === "ai-gateway") needed.add(PERMISSIONS.aiGateway);
5812
+ }
5813
+ return Object.values(PERMISSIONS).filter((permission) => needed.has(permission));
5814
+ }
5815
+ function deniedStatus(error) {
5816
+ return error instanceof PlatformCloudflareApiError && (error.status === 401 || error.status === 403);
5817
+ }
5818
+ /**
5819
+ * Check resource reads before the first change. Browser login cannot change
5820
+ * DNS or Access, so those need an API token. Read probes cannot prove Edit;
5821
+ * the plan lists the required permissions.
5822
+ */
5823
+ async function checkPurgeResourcesCredentials(client, manifest, credential) {
5824
+ const targets = purgeResourcesTargets(manifest);
5825
+ const resources = targets.flatMap((target) => "resource" in target ? [target.resource] : []);
5826
+ const access = targets.flatMap((target) => "access" in target ? [target.access] : []);
5827
+ const has = (kind) => resources.some((resource) => resource.kind === kind);
5828
+ const zone = manifest.resources.find((resource) => resource.key === "zone");
5829
+ const emailZone = targets.some((target) => target.phase === "email") ? emailRoutingZone(manifest) : null;
5830
+ const accounts = [.../* @__PURE__ */ new Set([manifest.account.id, ...access.map((entry) => entry.accountId)])];
5831
+ const scope = `${accounts.length === 1 ? "account" : "accounts"} ${accounts.join(" and ")}${has("dns-record") || has("worker-route") ? ` and zone ${zone?.name ?? ""}` : ""}${emailZone ? ` and Email Routing zone ${emailZone}` : ""}`;
5832
+ if (credential.source === "oauth") {
5833
+ const browserMissing = [
5834
+ ...has("dns-record") ? [PERMISSIONS.dns] : [],
5835
+ ...access.some((entry) => entry.kind === "application") ? [PERMISSIONS.access] : [],
5836
+ ...access.some((entry) => entry.kind === "service-token") ? [PERMISSIONS.serviceTokens] : [],
5837
+ ...has("ai-gateway") && !credential.aiGatewayToken ? [PERMISSIONS.aiGateway] : []
5838
+ ];
5839
+ if (browserMissing.length) throw new Error(`platform: Cloudflare browser login cannot delete every resource --purge-resources plans. Set CLOUDFLARE_API_TOKEN to a token with ${purgeResourcesPermissions(manifest).join(", ")}, scoped to ${scope}, then rerun. Browser login is missing: ${browserMissing.join(", ")}. No Cloudflare resource was changed.`);
5840
+ }
5841
+ const probes = [];
5842
+ if (has("worker") || has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerNames(manifest.account.id)]);
5843
+ if (has("worker-custom-domain")) probes.push([PERMISSIONS.workers, () => client.listWorkerDomains(manifest.account.id)]);
5844
+ if (has("worker-route") && zone) probes.push([PERMISSIONS.routes, () => client.listWorkerRoutes(zone.id)]);
5845
+ for (const resource of resources) if (resource.kind === "dns-record" && zone) probes.push([PERMISSIONS.dns, () => client.listDnsRecords(zone.id, resource.name)]);
5846
+ else if (resource.kind === "queue") probes.push([PERMISSIONS.queues, () => client.listQueueConsumers(manifest.account.id, resource.id)]);
5847
+ for (const entry of access) probes.push([entry.kind === "application" ? PERMISSIONS.access : PERMISSIONS.serviceTokens, () => client.getAccessResource(entry.accountId, entry.kind, entry.id)]);
5848
+ if (emailZone) probes.push([PERMISSIONS.emailRouting, () => client.listEmailRoutingRules(emailZone)]);
5849
+ if (has("dispatch-namespace")) probes.push([PERMISSIONS.workers, () => client.listDispatchNamespaces(manifest.account.id)]);
5850
+ if (has("kv-namespace")) probes.push([PERMISSIONS.kv, () => client.listKvNamespaces(manifest.account.id)]);
5851
+ if (has("hyperdrive")) probes.push([PERMISSIONS.hyperdrive, () => client.listHyperdriveConfigs(manifest.account.id)]);
5852
+ if (has("r2-bucket")) probes.push([PERMISSIONS.r2, () => client.listR2Buckets(manifest.account.id)]);
5853
+ if (has("ai-gateway")) probes.push([PERMISSIONS.aiGateway, () => client.listAiGateways(manifest.account.id)]);
5854
+ const missing = /* @__PURE__ */ new Set();
5855
+ for (const [permission, probe] of probes) try {
5856
+ await probe();
5857
+ } catch (error) {
5858
+ if (!deniedStatus(error)) throw error;
5859
+ missing.add(permission);
5860
+ }
5861
+ if (missing.size) throw new Error(`platform: The Cloudflare credential cannot read every resource --purge-resources would delete. Add ${[...missing].join(", ")} for ${scope}, then rerun. No Cloudflare resource was changed.`);
5862
+ }
5863
+ const LISTED_PROJECTS = 20;
5864
+ /** Slugs of every project row, including projects whose deletion stopped partway. */
5865
+ async function readPlatformProjectSlugsConnection(client) {
5866
+ if ((await client.query(`SELECT to_regclass('public.projects') IS NOT NULL AS "present"`)).rows[0]?.present !== true) return [];
5867
+ return (await client.query("SELECT \"slug\" FROM \"projects\" ORDER BY \"slug\"")).rows.map((row) => row.slug);
5868
+ }
5869
+ /**
5870
+ * Projects own Workers, storage, and domains that only the running platform
5871
+ * can delete. --purge-resources deletes the platform, so it refuses before any
5872
+ * change while a project remains.
5873
+ */
5874
+ async function assertNoPlatformProjects(read) {
5875
+ let slugs;
5876
+ try {
5877
+ if (!read) throw new Error("no platform database connection is available");
5878
+ slugs = await read();
5879
+ } catch (error) {
5880
+ const reason = (error instanceof Error ? error.message : String(error)).replace(/^platform: /, "");
5881
+ throw new Error(`platform: Refusing --purge-resources because the project list could not be read from the platform database: ${reason}\nMake the installation database reachable (export VOID_PLATFORM_DATABASE_URL if this machine has not recorded it), then retry.`);
5882
+ }
5883
+ if (slugs.length) throw new Error([
5884
+ `platform: Refusing --purge-resources while the platform still has ${slugs.length === 1 ? "1 project" : `${slugs.length} projects`}.`,
5885
+ "--purge-resources deletes the platform, and only the running platform can delete each project's Workers, storage, and domains.",
5886
+ "Delete each project first with `void project delete <name>`, then retry:",
5887
+ ...slugs.slice(0, LISTED_PROJECTS).map((slug) => ` ${slug}`),
5888
+ ...slugs.length > LISTED_PROJECTS ? [` …and ${slugs.length - LISTED_PROJECTS} more`] : []
5889
+ ].join("\n"));
5890
+ }
5891
+ const AI_GATEWAY_CREATION_WINDOW_MS = 6e5;
5892
+ /**
5893
+ * AI Gateway ids are chosen by the caller and can be reused after a delete,
5894
+ * and gateways carry no ownership marker. Only a creation time next to the
5895
+ * installer's provision intent shows that this is the gateway it created.
5896
+ */
5897
+ function createdDuringInstallation(manifest, resource, createdAt) {
5898
+ const created = createdAt === void 0 ? NaN : Date.parse(createdAt);
5899
+ if (Number.isNaN(created)) return false;
5900
+ return manifest.steps.some((step) => (step.key === `provision-intent:${resource.key}` || step.key.startsWith(`provision-intent:${resource.key}:`)) && Math.abs(created - Date.parse(step.completedAt)) <= AI_GATEWAY_CREATION_WINDOW_MS);
5901
+ }
5902
+ function ownershipRefusal(manifest, description, reason) {
5903
+ return /* @__PURE__ */ new Error(`platform: Refusing to delete ${description} because ${reason}. It may no longer belong to this installation. Delete or fix it in the Cloudflare dashboard if it is yours, then rerun: ${purgeResourcesResumeCommand(manifest)}`);
5904
+ }
5905
+ function cloudflareRefusal(manifest, description, error, hint) {
5906
+ if (!(error instanceof PlatformCloudflareApiError)) return error;
5907
+ return new Error(`platform: Cloudflare refused to delete ${description}: ${error.message}\n${hint}\nProgress is saved. Rerun: ${purgeResourcesResumeCommand(manifest)}`, { cause: error });
5908
+ }
5909
+ /**
5910
+ * Delete every target in order. Returns the manifest with each deleted
5911
+ * resource recorded; routing KV is left for the caller to delete last.
5912
+ */
5913
+ async function runPurgeResourcesTeardown(context, initial) {
5914
+ const { client } = context;
5915
+ let next = initial;
5916
+ const accountId = next.account.id;
5917
+ const save = async (manifest) => {
5918
+ next = manifest;
5919
+ await context.checkpoint(next);
5920
+ };
5921
+ const markDeleted = async (key) => {
5922
+ const resource = next.resources.find((entry) => entry.key === key);
5923
+ await save(recordInstallationResource(next, {
5924
+ ...resource,
5925
+ state: "deleted"
5926
+ }, (/* @__PURE__ */ new Date()).toISOString()));
5927
+ };
5928
+ const recordStep = async (key, resultHash = "completed") => {
5929
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5930
+ await save(recordInstallationStep(next, {
5931
+ completedAt: now,
5932
+ key: `${PURGE_RESOURCES_STEP_PREFIX}${key}`,
5933
+ resultHash
5934
+ }, now));
5935
+ };
5936
+ /**
5937
+ * Live proof for these keys. A resource an earlier uninstall recorded as
5938
+ * retained is proven like a ready one; only its recorded state differs.
5939
+ */
5940
+ const verify = (keys) => context.verify({
5941
+ ...next,
5942
+ resources: next.resources.map((resource) => keys.has(resource.key) && resource.state === "retained" ? {
5943
+ ...resource,
5944
+ state: "ready"
5945
+ } : resource)
5946
+ }, keys);
5947
+ /**
5948
+ * Prove the resource; false when it is already gone (and recorded).
5949
+ *
5950
+ * Cloudflare has no conditional DELETE: Workers and dispatch namespaces are
5951
+ * addressed by name, and a route or custom domain can be changed in place.
5952
+ * Something replaced between this proof and the DELETE can still be removed.
5953
+ * That one-request window is accepted because --purge-resources is opt-in and its
5954
+ * plan lists every resource first. A forced Worker DELETE has the same
5955
+ * window between reading its dependents and the DELETE.
5956
+ */
5957
+ const prove = async (resource) => {
5958
+ if ((await verify(/* @__PURE__ */ new Set(["routing-kv", resource.key]))).has(resource.key)) {
5959
+ await markDeleted(resource.key);
5960
+ return false;
5961
+ }
5962
+ return true;
5963
+ };
5964
+ /** Keep a resource that no longer matches the record, and save why. */
5965
+ const keep = async (key, reason) => {
5966
+ const resource = next.resources.find((entry) => entry.key === key);
5967
+ if (resource?.state === "ready") await save(recordInstallationResource(next, {
5968
+ ...resource,
5969
+ state: "retained"
5970
+ }, (/* @__PURE__ */ new Date()).toISOString()));
5971
+ await recordStep(keptStepKey(key), reason);
5972
+ };
5973
+ const remove = async (resource, hint, zoneId) => {
5974
+ if (!await prove(resource)) return;
5975
+ try {
5976
+ await client.deleteResource(accountId, resource, zoneId);
5977
+ } catch (error) {
5978
+ throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, hint);
5979
+ }
5980
+ await markDeleted(resource.key);
5981
+ };
5982
+ const zone = usableZone(next);
5983
+ const createdWorkerNames = new Set(next.resources.filter((resource) => resource.kind === "worker" && resource.ownership === "created").map((resource) => resource.name));
5984
+ /** The recorded Worker under this name that this run still deletes, if any. */
5985
+ const workerTarget = (name) => next.resources.find((resource) => resource.kind === "worker" && resource.name === name && isPurgeResourcesCandidate(next, resource));
5986
+ const workerHint = "A Worker this installation did not create may still use it, for example by sending it logs as a tail Worker or through a service binding. Remove that link in the Cloudflare dashboard.";
5987
+ /**
5988
+ * Delete a platform Worker. When Cloudflare refuses because other Workers
5989
+ * still bind to it, force the DELETE only if each of them is a Worker this
5990
+ * run deletes too and still carries this installation's ownership marker.
5991
+ */
5992
+ const removeWorker = async (resource) => {
5993
+ if (!await prove(resource)) return;
5994
+ try {
5995
+ await client.deleteResource(accountId, resource);
5996
+ } catch (error) {
5997
+ if (!(error instanceof PlatformCloudflareApiError)) throw error;
5998
+ const dependents = await client.listWorkerDependents(accountId, resource.name).catch(() => null);
5999
+ const outside = dependents?.workers.filter((name) => !workerTarget(name)) ?? [];
6000
+ const keys = outside.length ? [] : dependents?.workers.map((name) => workerTarget(name).key) ?? [];
6001
+ const missing = dependents && !dependents.pagesFunction && keys.length ? await verify(/* @__PURE__ */ new Set(["routing-kv", ...keys])) : null;
6002
+ if (!dependents || dependents.pagesFunction || dependents.workers.length === 0 || outside.length > 0 || !missing || keys.some((key) => missing.has(key))) throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, error, outside.length ? `Workers outside this installation still use it: ${outside.join(", ")}. Remove that link in the Cloudflare dashboard.` : workerHint);
6003
+ try {
6004
+ await client.forceDeleteWorker(accountId, resource.name);
6005
+ } catch (forced) {
6006
+ throw cloudflareRefusal(next, `${resource.kind} ${resource.name}`, forced, workerHint);
6007
+ }
6008
+ }
6009
+ await markDeleted(resource.key);
6010
+ };
6011
+ for (const target of purgeResourcesTargets(initial)) {
6012
+ if (target.phase === "access") {
6013
+ const { access } = target;
6014
+ const live = await client.getAccessResource(access.accountId, access.kind, access.id);
6015
+ if (live) {
6016
+ if (live.name !== access.name || !access.name.startsWith(`void-${next.id}-`)) {
6017
+ await keep(`access:${access.key}`, `its live name ${JSON.stringify(live.name)} is not the name this installation recorded`);
6018
+ continue;
6019
+ }
6020
+ try {
6021
+ await client.deleteAccessResource(access.accountId, access.kind, access.id);
6022
+ } catch (error) {
6023
+ throw cloudflareRefusal(next, `Access ${access.kind} ${access.name}`, error, "Check that nothing outside this installation still uses it.");
6024
+ }
6025
+ }
6026
+ await recordStep(`access:${access.key}`);
6027
+ continue;
6028
+ }
6029
+ const current = next.resources.find((resource) => resource.key === target.resource.key);
6030
+ if (!current || current.state === "deleted" || purgeResourcesKeptReason(next, current.key) !== null || target.phase !== "traffic" && !isPurgeResourcesCandidate(next, current)) continue;
6031
+ switch (target.phase) {
6032
+ case "traffic": {
6033
+ const worker = next.resources.find((resource) => resource.key === target.worker.key);
6034
+ if (!worker || !isPurgeResourcesCandidate(next, worker) || !await prove(worker)) break;
6035
+ if (current.kind === "worker-route") {
6036
+ const live = (await client.listWorkerRoutes(zone.id)).find((route) => route.id === current.id);
6037
+ if (live && (live.pattern !== current.name || live.script !== worker.name)) {
6038
+ await keep(current.key, `it now sends ${JSON.stringify(live.pattern)} to ${JSON.stringify(live.script)}`);
6039
+ break;
6040
+ }
6041
+ if (live) await client.deleteWorkerRoute(zone.id, current.id);
6042
+ } else {
6043
+ const live = (await client.listWorkerDomains(accountId)).find((domain) => domain.id === current.id);
6044
+ if (live && (live.hostname !== current.name || live.service !== worker.name)) {
6045
+ await keep(current.key, `it now sends ${JSON.stringify(live.hostname)} to ${JSON.stringify(live.service)}`);
6046
+ break;
6047
+ }
6048
+ if (live) await client.deleteWorkerDomain(accountId, current.id);
6049
+ }
6050
+ await markDeleted(current.key);
6051
+ break;
6052
+ }
6053
+ case "dns":
6054
+ try {
6055
+ await remove(current, "Check the record in the Cloudflare dashboard.", zone.id);
6056
+ } catch (error) {
6057
+ if (!(error instanceof PlatformResourceChangedError && error.resourceKey === current.key)) throw error;
6058
+ await keep(current.key, error.reason);
6059
+ }
6060
+ break;
6061
+ case "email": {
6062
+ if (!await prove(current)) break;
6063
+ const zoneId = emailRoutingZone(next);
6064
+ const hint = "Check the Email Routing rules for this zone in the Cloudflare dashboard. Never disable Email Routing for the zone.";
6065
+ try {
6066
+ for (const rule of await client.listEmailRoutingRules(zoneId)) if (rule.workerName === current.name) await client.deleteEmailRoutingRule(zoneId, rule.id);
6067
+ if ((await client.getEmailRoutingCatchAll(zoneId))?.workerName === current.name) await client.resetEmailRoutingCatchAll(zoneId);
6068
+ } catch (error) {
6069
+ throw cloudflareRefusal(next, `Email Routing rules that send mail to ${current.name}`, error, hint);
6070
+ }
6071
+ await recordStep("email-routing");
6072
+ break;
6073
+ }
6074
+ case "queue-consumers":
6075
+ if (!await prove(current)) break;
6076
+ for (const consumer of await client.listQueueConsumers(accountId, current.id)) if (consumer.script && createdWorkerNames.has(consumer.script)) await client.deleteQueueConsumer(accountId, current.id, consumer.id);
6077
+ await recordStep(`consumers:${current.key}`);
6078
+ break;
6079
+ case "workers":
6080
+ await removeWorker(current);
6081
+ break;
6082
+ case "tail":
6083
+ await remove(current, workerHint);
6084
+ break;
6085
+ case "queues":
6086
+ await remove(current, "A Worker this installation did not create may still use the Queue as a producer or consumer. Remove it from that Worker in the Cloudflare dashboard.");
6087
+ break;
6088
+ case "dispatch": {
6089
+ if (!await prove(current)) break;
6090
+ const projects = await context.readProjects();
6091
+ if (projects.length) throw new Error(`platform: Stopped before deleting the application Workers in dispatch namespace ${current.name} because ${projects.length === 1 ? "a project was" : "projects were"} created while this uninstall ran: ${projects.join(", ")}. Nothing in the namespace was deleted. --purge-resources refuses while the platform database lists a project. Remove each one from the "projects" table once you have dealt with its Cloudflare resources, then rerun: ${purgeResourcesResumeCommand(next)}`);
6092
+ for (const script of await client.listDispatchNamespaceScripts(accountId, current.name)) try {
6093
+ await client.deleteDispatchNamespaceScript(accountId, current.name, script);
6094
+ } catch (error) {
6095
+ throw cloudflareRefusal(next, `application Worker ${script}`, error, workerHint);
6096
+ }
6097
+ try {
6098
+ await client.deleteResource(accountId, current);
6099
+ } catch (error) {
6100
+ throw cloudflareRefusal(next, `dispatch-namespace ${current.name}`, error, "Check the namespace in the Cloudflare dashboard.");
6101
+ }
6102
+ await markDeleted(current.key);
6103
+ break;
6104
+ }
6105
+ case "ai-gateway": {
6106
+ const live = (await client.listAiGateways(accountId)).find((gateway) => gateway.id === current.id && gateway.name === current.name);
6107
+ if (live && !createdDuringInstallation(next, current, live.createdAt)) {
6108
+ await keep(current.key, "Cloudflare does not show it was created during installation");
6109
+ break;
6110
+ }
6111
+ await remove(current, "Check that nothing outside this installation still uses it.");
6112
+ break;
6113
+ }
6114
+ case "data":
6115
+ await remove(current, "Check that nothing outside this installation still uses it.");
6116
+ break;
6117
+ case "r2": await purgeBucket(current);
6118
+ }
6119
+ }
6120
+ return next;
6121
+ async function purgeBucket(bucket) {
6122
+ const markerStep = `r2-marker:${bucket.key}`;
6123
+ if (!hasStep(next, markerStep)) {
6124
+ if (!await prove(bucket)) return;
6125
+ await client.emptyR2Bucket(accountId, bucket.name, /* @__PURE__ */ new Set([PLATFORM_R2_OWNERSHIP_MARKER_KEY]), async () => {
6126
+ if ((await verify(/* @__PURE__ */ new Set(["routing-kv", bucket.key]))).has(bucket.key)) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "it disappeared while it was being emptied");
6127
+ });
6128
+ if (!await prove(bucket)) return;
6129
+ const generation = await context.bucketGeneration(next, bucket);
6130
+ if (generation === null) {
6131
+ await markDeleted(bucket.key);
6132
+ return;
6133
+ }
6134
+ await recordStep(markerStep, generation);
6135
+ await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
6136
+ } else {
6137
+ const saved = next.steps.find((step) => step.key === `${PURGE_RESOURCES_STEP_PREFIX}${markerStep}`);
6138
+ const generation = await context.bucketGeneration(next, bucket);
6139
+ if (generation === null) {
6140
+ await markDeleted(bucket.key);
6141
+ return;
6142
+ }
6143
+ if (generation !== saved?.resultHash) throw ownershipRefusal(next, `R2 bucket ${bucket.name}`, "its creation fingerprint changed");
6144
+ await client.deleteR2Object(accountId, bucket.name, PLATFORM_R2_OWNERSHIP_MARKER_KEY);
6145
+ }
6146
+ try {
6147
+ await client.deleteResource(accountId, bucket);
6148
+ } catch (error) {
6149
+ throw cloudflareRefusal(next, `r2-bucket ${bucket.name}`, error, "Cloudflare deletes only empty buckets. If objects were added after it was emptied, delete them in the Cloudflare dashboard.");
6150
+ }
6151
+ await markDeleted(bucket.key);
6152
+ }
6153
+ }
6154
+ //#endregion
5346
6155
  //#region src/cli/platform-zero-trust-uninstall.ts
5347
6156
  /**
5348
6157
  * Uninstall turns off the platform API, and only that API can remove the
@@ -5444,8 +6253,9 @@ async function readPlatformProjectZeroTrustStateConnection(client) {
5444
6253
  }
5445
6254
  /**
5446
6255
  * Take the Zero Trust configuration lock exclusively on this session. Every
5447
- * API path that records or creates a Void-managed Access application holds
5448
- * the same lock, so none can commit until the returned release runs.
6256
+ * API path that records or creates a Void-managed Access application or
6257
+ * creates a project holds the same lock, so none can commit until the
6258
+ * returned release runs.
5449
6259
  */
5450
6260
  async function lockPlatformProjectZeroTrustConnection(client) {
5451
6261
  await client.query(`SELECT pg_advisory_lock(${ZERO_TRUST_LOCK})`);
@@ -5689,6 +6499,21 @@ async function readPlatformProjectZeroTrustState(databaseUrl, installationId, ow
5689
6499
  await pool.end();
5690
6500
  }
5691
6501
  }
6502
+ /** Read project slugs for a --purge-resources plan, which holds no lifecycle lock. */
6503
+ async function readPlatformProjectSlugs(databaseUrl, installationId, ownershipNonce) {
6504
+ if (!databaseUrl) throw new Error("the installation database URL is not recorded on this machine");
6505
+ const { Pool } = await import("pg");
6506
+ const pool = new Pool({
6507
+ connectionString: platformDatabaseClientUrl(databaseUrl),
6508
+ max: 1
6509
+ });
6510
+ try {
6511
+ await readPlatformDatabaseLifecycleManifestConnection(pool, installationId, ownershipNonce);
6512
+ return await readPlatformProjectSlugsConnection(pool);
6513
+ } finally {
6514
+ await pool.end();
6515
+ }
6516
+ }
5692
6517
  async function inspectPlatformDatabaseStateConnection(client, database) {
5693
6518
  const relation = await client.query(`SELECT to_regclass('"drizzle"."__drizzle_migrations"')::text AS "relation"`);
5694
6519
  if (relation.rows.length === 0 || relation.rows[0]?.relation === null) return {
@@ -5817,6 +6642,7 @@ async function acquirePlatformDatabaseLifecycleLockConnection(client, installati
5817
6642
  } : null;
5818
6643
  };
5819
6644
  release.readProjectZeroTrust = () => readPlatformProjectZeroTrustStateConnection(client);
6645
+ release.readProjects = () => readPlatformProjectSlugsConnection(client);
5820
6646
  release.lockProjectZeroTrust = () => lockPlatformProjectZeroTrustConnection(client);
5821
6647
  release.fenceProjectZeroTrust = () => fencePlatformProjectZeroTrustConnection(client);
5822
6648
  release.liftProjectZeroTrustFence = () => liftPlatformProjectZeroTrustFenceConnection(client);
@@ -6005,7 +6831,7 @@ async function claimPlatformDatabaseConnection(client, installationId, ownership
6005
6831
  if (!claim) {
6006
6832
  const contents = await client.query(`SELECT "schemaname", "tablename"
6007
6833
  FROM "pg_catalog"."pg_tables"
6008
- WHERE "schemaname" = ANY (current_schemas(false))
6834
+ WHERE ("schemaname" = ANY (current_schemas(false)) OR "schemaname" = 'drizzle')
6009
6835
  AND "tablename" <> '__void_platform_installation'
6010
6836
  ORDER BY "schemaname", "tablename"
6011
6837
  LIMIT 1`);
@@ -6209,6 +7035,7 @@ async function uploadInstallationWorkers(input) {
6209
7035
  const previousWorkerConfig = mergePlatformWorkerTrafficConfig(previousConfigs[key], preservedRoutes);
6210
7036
  const rollback = dependencies.deployWorker ? await dependencies.deployWorker(workerConfig, workerSecrets, previousWorkerConfig) : await deployPlatformWorker(workerConfig, workerSecrets, {
6211
7037
  apiBaseUrl: cloudflareApiBaseUrl,
7038
+ operatorApiToken: dependencies.operatorApiToken,
6212
7039
  ...parallelWorkerUploads ? {
6213
7040
  acquireUploadSlot,
6214
7041
  onReadyForActivation,
@@ -6388,8 +7215,8 @@ async function installPlatform(plan, secrets, existing, dependencies) {
6388
7215
  if (plan.actions.some((action) => action.action === "inspect")) throw new Error("platform: AI Gateway access must be verified before provisioning. Provide AI Gateway: Edit on the runtime token, then retry.");
6389
7216
  const now = dependencies.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
6390
7217
  const cloudflareApiBaseUrl = dependencies.client.apiBaseUrl ?? resolvePinnedApiBaseUrl();
6391
- const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl);
6392
- const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true);
7218
+ const runPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, false, dependencies.operatorApiToken);
7219
+ const runParallelPinnedPlatformWrangler = (operation, accountId, cwd, capture) => runPlatformWranglerAtApiBase(operation, accountId, cwd, capture, cloudflareApiBaseUrl, true, dependencies.operatorApiToken);
6393
7220
  const resolvedRuntime = dependencies.runtime ?? resolvePlatformRuntime();
6394
7221
  const runtimeDirectory = resolvedRuntime.directory;
6395
7222
  const runtime = resolvedRuntime.manifest;
@@ -7361,14 +8188,14 @@ function resolvePlatformInstallDatabaseUrl(checkpoint, manifest = null) {
7361
8188
  }
7362
8189
  async function runPlatformLifecycleCommand(commandArgs) {
7363
8190
  if (commandArgs.subcommand === "domain") {
7364
- const { runPlatformDomainCommand } = await import("./platform-domain-CUdY064e.mjs");
8191
+ const { runPlatformDomainCommand } = await import("./platform-domain-Bsq43AJp.mjs");
7365
8192
  await runPlatformDomainCommand(commandArgs);
7366
8193
  return;
7367
8194
  }
7368
8195
  if (commandArgs.subcommand === "discover") {
7369
8196
  const client = new PlatformCloudflareClient((await resolvePlatformCloudflareCredential()).token);
7370
8197
  const accountId = await selectAccount(await readAccounts(client), commandArgs.account);
7371
- const { recoverPlatformInstallations } = await import("./platform-recovery-WOZ95Uze.mjs");
8198
+ const { recoverPlatformInstallations } = await import("./platform-recovery-DQAqAQYu.mjs");
7372
8199
  const recovered = await recoverPlatformInstallations(client, accountId, void 0, commandArgs.selector);
7373
8200
  if (recovered.length === 0) {
7374
8201
  log.info(`No recoverable Void platform installations were found in ${accountId}.`);
@@ -7378,7 +8205,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
7378
8205
  return;
7379
8206
  }
7380
8207
  if (commandArgs.subcommand !== "install") {
7381
- const { runInstalledPlatformCommand } = await import("./platform-management-Bp6aoH2C.mjs");
8208
+ const { runInstalledPlatformCommand } = await import("./platform-management-Bbwf6-Dq.mjs");
7382
8209
  await runInstalledPlatformCommand(commandArgs);
7383
8210
  return;
7384
8211
  }
@@ -7551,7 +8378,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
7551
8378
  progress?.stop("Read the saved installation.");
7552
8379
  progress = void 0;
7553
8380
  if (authoritative) {
7554
- const { selectAuthoritativePlatformManifest } = await import("./platform-management-Bp6aoH2C.mjs");
8381
+ const { selectAuthoritativePlatformManifest } = await import("./platform-management-Bbwf6-Dq.mjs");
7555
8382
  const selected = selectAuthoritativePlatformManifest(existing, authoritative);
7556
8383
  authoritativeProvisionIntents = true;
7557
8384
  if (!args.plan && JSON.stringify(selected) !== JSON.stringify(existing)) writeInstallationManifest(selected);
@@ -7914,6 +8741,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
7914
8741
  const result = await installPlatform(plan, secrets, existing, {
7915
8742
  acquireDatabaseLifecycleLock: acquirePlatformDatabaseLifecycleLock,
7916
8743
  client,
8744
+ operatorApiToken: managementApiToken || operatorCredential.token,
7917
8745
  runtime,
7918
8746
  healthCheckResumeCommand: `${cliCommandName()} platform install --resume --name ${existing.id}`,
7919
8747
  onProgress: (message) => progress?.message(message),
@@ -7955,7 +8783,7 @@ async function runPlatformLifecycleCommand(commandArgs) {
7955
8783
  })).actions.filter((action) => action.kind === "worker"),
7956
8784
  revalidateBeforeWorkerDeploy: (manifest, action) => revalidatePlatformInstallAction(client, plan, manifest, action),
7957
8785
  verifyExistingOwnership: async (manifest, databaseCheckpointExists) => {
7958
- const { verifyPlatformInstallationOwnership } = await import("./platform-management-Bp6aoH2C.mjs");
8786
+ const { verifyPlatformInstallationOwnership } = await import("./platform-management-Bbwf6-Dq.mjs");
7959
8787
  const retainedReplacementKeys = new Set(plan.actions.filter((action) => {
7960
8788
  const previous = manifest.resources.find((resource) => resource.key === action.key);
7961
8789
  return action.action === "create" && previous && previous.name !== action.name;
@@ -8107,4 +8935,4 @@ async function promptSecret(message, masked = true, validate, placeholder) {
8107
8935
  return value;
8108
8936
  }
8109
8937
  //#endregion
8110
- export { PlatformCloudflareClient as A, revalidatePlatformInstallAction as C, checkpointPlatformAccessResources as D, resolvePlatformCloudflareCredential as E, hyperdriveOriginFromConnectionString as M, hyperdriveOriginMatches as N, parsePlatformAccessSetupSpec as O, verifyPlatformRuntimeTokenPermissions as P, platformProvisionIntentHash as S, assertPlatformOperatorCredentialCanApplyPlan as T, recordPlatformWorkerTraffic as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, formatPlatformInstallPlan as b, acquirePlatformDatabaseLifecycleLockConnection as c, assertProjectZeroTrustAllowsUninstall as d, loadPlatformRuntime as f, recordResource as g, resolveBootstrapGitHubId as h, verifyPlatformAiGatewayPlan as i, hyperdriveOriginConfigHash as j, preparePlatformAccessSetup as k, platformDatabaseClientUrl as l, createPlatformInstallationSecrets as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, createPlatformHealthProbeToken as p, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectZeroTrustState as u, trackedPlatformWorkerTraffic as v, platformDatabaseNameFromOwnerUrl as w, platformAdoptionIntentHash as x, createPlatformInstallPlan as y };
8938
+ export { resolveBootstrapGitHubId as A, assertPlatformOperatorCredentialCanApplyPlan as B, purgeResourcesKeptReason as C, loadPlatformRuntime as D, runPurgeResourcesTeardown as E, formatPlatformInstallPlan as F, preparePlatformAccessSetup as G, verifyPlatformCloudflareCredential as H, platformAdoptionIntentHash as I, hyperdriveOriginFromConnectionString as J, PlatformCloudflareClient as K, platformProvisionIntentHash as L, recordPlatformWorkerTraffic as M, trackedPlatformWorkerTraffic as N, createPlatformHealthProbeToken as O, createPlatformInstallPlan as P, revalidatePlatformInstallAction as R, purgeResourcesHandlesEmailRouting as S, purgeResourcesPermissions as T, checkpointPlatformAccessResources as U, resolvePlatformCloudflareCredential as V, parsePlatformAccessSetupSpec as W, verifyPlatformRuntimeTokenPermissions as X, hyperdriveOriginMatches as Y, describePurgeResourcesPlan as _, REMOTE_INSTALLATION_MANIFEST_KEY as a, isPurgedAccessResource as b, acquirePlatformDatabaseLifecycleLockConnection as c, readPlatformProjectZeroTrustState as d, assertProjectZeroTrustAllowsUninstall as f, checkPurgeResourcesCredentials as g, assertNoPlatformProjects as h, verifyPlatformAiGatewayPlan as i, recordResource as j, createPlatformInstallationSecrets as k, platformDatabaseClientUrl as l, PlatformResourceChangedError as m, resolvePlatformInstallDatabaseUrl as n, installPlatform as o, PURGE_RESOURCES_STEP_PREFIX as p, hyperdriveOriginConfigHash as q, runPlatformLifecycleCommand as r, acquirePlatformDatabaseLifecycleLock as s, assertNoninteractivePlatformMutationAcknowledged as t, readPlatformProjectSlugs as u, isPlatformTailWorker as v, purgeResourcesKeptResources as w, purgeResourcesAlwaysKept as x, isPurgeResourcesCandidate as y, platformDatabaseNameFromOwnerUrl as z };