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.
- package/dist/{account-cmd-DcR1ywt8.mjs → account-cmd-CDry3B5V.mjs} +4 -4
- package/dist/{application-analysis-BVsVqc11.mjs → application-analysis-DC2yPvzY.mjs} +1 -1
- package/dist/{application-routing-B7QEjqSR.mjs → application-routing-C9khgLiS.mjs} +1 -1
- package/dist/{auth-CdgZ9uag.mjs → auth-B60QDbjg.mjs} +1 -1
- package/dist/{auth-7wuZ5ob8.mjs → auth-D52yG7hl.mjs} +3 -3
- package/dist/{auth-link-Z6E5DYvM.mjs → auth-link-DjeOptYi.mjs} +4 -4
- package/dist/{auth-router-BdM4avI8.mjs → auth-router-BRxIuqDi.mjs} +5 -5
- package/dist/{build-cmd-CEE7D-hM.mjs → build-cmd-Ce3C8huo.mjs} +4 -4
- package/dist/{cache-TcZnBLtH.mjs → cache-CS8Yxr5a.mjs} +4 -4
- package/dist/{cancel-deploy-CYoZmcmR.mjs → cancel-deploy-mZsD88Xa.mjs} +3 -3
- package/dist/{cf-access-AHKBzmuu.mjs → cf-access-YTlqvAsk.mjs} +1 -1
- package/dist/cli/cli.mjs +40 -40
- package/dist/cli/cloudflare-operation-process.mjs +1 -1
- package/dist/cli/env-schema-probe.d.mts +1 -1
- package/dist/client-BxVixJBI.mjs +2 -0
- package/dist/{client-RsPo8wsP.mjs → client-mC-dFGDD.mjs} +5 -5
- package/dist/{cloudflare-operations-CjaV4lZi.mjs → cloudflare-operations-DYa3coIk.mjs} +1 -1
- package/dist/{cloudflare-operations-BhbK6yTq.mjs → cloudflare-operations-DoQrVHAt.mjs} +1 -1
- package/dist/{connect-DCvUmPXY.mjs → connect-H6EvP62X.mjs} +6 -6
- package/dist/{create-project-eAPgYq35.mjs → create-project-B2kk9Q0o.mjs} +4 -4
- package/dist/{create-project-C734I-Mu.mjs → create-project-Dbb4uiN-.mjs} +1 -1
- package/dist/{db-C07GEdKF.mjs → db-DpCKZto0.mjs} +21 -21
- package/dist/{delete-pKMVsrpJ.mjs → delete-CxlSvsvW.mjs} +6 -6
- package/dist/{deploy-DfsAe-T0.mjs → deploy-CeEbnv4b.mjs} +38 -24
- package/dist/{deploy-BDu1vhSH.mjs → deploy-qmiLE1G1.mjs} +1 -1
- package/dist/{domain-D3uA9ize.mjs → domain-B_9VgOm6.mjs} +6 -6
- package/dist/{email-z3o2cwjH.mjs → email-BokHgjKj.mjs} +6 -6
- package/dist/{env-3Q8-_2IH.mjs → env-BdTc-K2L.mjs} +6 -5
- package/dist/{env-public-BX_r8HR6.d.mts → env-public-DR7ZM6d-.d.mts} +1 -1
- package/dist/{fetch-stream-IvCYKyQL.mjs → fetch-stream-Nm3hkZbT.mjs} +8 -1
- package/dist/{gen-NjwoG9FK.mjs → gen-BhDlTpwW.mjs} +1 -1
- package/dist/gen-qCBwH3fZ.mjs +2 -0
- package/dist/{github-cmd-DquUq-b1.mjs → github-cmd-hhkTzlGS.mjs} +4 -4
- package/dist/{handler-CZ4nAylQ.d.mts → handler-BeK3weD4.d.mts} +1 -1
- package/dist/{help-DRZXC4fj.mjs → help-Bv44e2tm.mjs} +13 -9
- package/dist/help-Dw5f9Xrg.mjs +2 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +30 -17
- package/dist/{info-BmA3_IFZ.mjs → info-DhtTKh65.mjs} +1 -1
- package/dist/{init-DT7yuuyl.mjs → init-BMf4T03q.mjs} +10 -9
- package/dist/{limits-Bq5LG8Id.d.mts → limits-CFsDKQvU.d.mts} +1 -10
- package/dist/{limits-Cjuk2VPm.mjs → limits-D0vD1M9A.mjs} +1 -22
- package/dist/{link-CXOlqh2F.mjs → link-BqYsSS_I.mjs} +5 -5
- package/dist/{list-eEWVJEPn.mjs → list-_kBToO-l.mjs} +7 -6
- package/dist/{live-Chw1eIMv.d.mts → live-iAuEH1hd.d.mts} +1 -1
- package/dist/{local-d1-Df4XpwhW.mjs → local-d1-fGxYrVD6.mjs} +1 -1
- package/dist/login-D7meQg_3.mjs +2 -0
- package/dist/{login-pWQHYVu2.mjs → login-DFuo-RWO.mjs} +4 -4
- package/dist/{logs-DnxVX7TR.mjs → logs-DYgnh6dH.mjs} +5 -5
- package/dist/{node-BkyRWRx8.mjs → node-C6Q6e1Es.mjs} +1 -1
- package/dist/{operator-args-CAVcmwUt.mjs → operator-args-DdY54Bgb.mjs} +1 -1
- package/dist/{operator-auth-CkmTE3rt.mjs → operator-auth-2dsGlTT5.mjs} +4 -8
- package/dist/{operator-client-LmwjxT6q.mjs → operator-client-BFqpp6z0.mjs} +3 -3
- package/dist/{operator-cmd-d-IEKgNk.mjs → operator-cmd-CUGHXrQf.mjs} +27 -9
- package/dist/operator-cmd-DRPvhpjM.mjs +2 -0
- package/dist/{output-BBsUwdyT.mjs → output-BvdaehtE.mjs} +26 -2
- package/dist/pages/client.d.mts +1 -1
- package/dist/pages/index.mjs +1 -1
- package/dist/pages/islands-plugin.mjs +1 -1
- package/dist/pages/protocol.d.mts +2 -2
- package/dist/{plan-NPpwZ_kc.mjs → plan-CJGEeTEI.mjs} +2 -2
- package/dist/plan-COtUvdMA.mjs +2 -0
- package/dist/{platform-args-Bzf8JPc2.mjs → platform-args-DSV2p4nY.mjs} +4 -2
- package/dist/platform-args-Dcy_5b_Y.mjs +2 -0
- package/dist/{platform-auth-config-0V0lA3pF.mjs → platform-auth-config-Bosx7U1h.mjs} +18 -17
- package/dist/{platform-auth-protection-DFeJ-JkK.mjs → platform-auth-protection-DcbL_LW_.mjs} +6 -6
- package/dist/{platform-auth-recovery-ICy4muq6.mjs → platform-auth-recovery-WfTZ8K6o.mjs} +5 -5
- package/dist/{platform-cmd-D7vSWMvV.mjs → platform-cmd-DbKtQb_4.mjs} +45 -20
- package/dist/{platform-cmd-BPySFOew.mjs → platform-cmd-dyFUof0t.mjs} +1 -1
- package/dist/{platform-discovery-DDDUvnqp.mjs → platform-discovery-BC0HCKYt.mjs} +18 -4
- package/dist/{platform-domain-CUdY064e.mjs → platform-domain-Bsq43AJp.mjs} +5 -4
- package/dist/{platform-lifecycle-BaUdTRPU.mjs → platform-lifecycle--5gWmDoI.mjs} +1 -1
- package/dist/{platform-lifecycle-Dm09JNkn.mjs → platform-lifecycle-Cfi0o6xM.mjs} +863 -35
- package/dist/{platform-management-Bp6aoH2C.mjs → platform-management-Bbwf6-Dq.mjs} +1 -1
- package/dist/{platform-management-BSEc_ZeG.mjs → platform-management-BrF9STZp.mjs} +173 -30
- package/dist/{platform-plans-config-B0pOVQyW.mjs → platform-plans-config-DU-8nC2Y.mjs} +2 -2
- package/dist/{platform-recovery-WOZ95Uze.mjs → platform-recovery-DQAqAQYu.mjs} +3 -3
- package/dist/{platform-registry-KfNEF84O.mjs → platform-registry-P387v42A.mjs} +17 -10
- package/dist/prepare-BBknoZI1.mjs +2 -0
- package/dist/{prepare-39EvOhI6.mjs → prepare-BMrRWLwP.mjs} +3 -2
- package/dist/{prepare-DnaKBi--.mjs → prepare-WoUubFKL.mjs} +1 -1
- package/dist/{project-IOjBU8R6.mjs → project-BiaPQ7ur.mjs} +1 -1
- package/dist/{project-BkbfDs0V.mjs → project-DQstYTxO.mjs} +1 -1
- package/dist/{project-cmd-Bs5A392E.mjs → project-cmd-BbH2LptN.mjs} +18 -18
- package/dist/{project-team-ohAWWaew.mjs → project-team-Yiww3-go.mjs} +5 -5
- package/dist/{project-token-CP01Jna6.mjs → project-token-BrtT2NkY.mjs} +4 -4
- package/dist/{project-zero-trust-BzkVgx35.mjs → project-zero-trust-nYJ2V2nc.mjs} +5 -5
- package/dist/{protocol-CjF_iI9X.d.mts → protocol-BX8t2jiw.d.mts} +1 -1
- package/dist/{provision-BEv0k-qz.mjs → provision-DMTPG8Rh.mjs} +1 -1
- package/dist/{provision-Btj1jppf.mjs → provision-VeqAC77A.mjs} +13 -1
- package/dist/{requests-B3PBuGlX.mjs → requests-TIz_jtSu.mjs} +4 -4
- package/dist/{rollback-DDub9GsO.mjs → rollback-dBqICPzj.mjs} +5 -5
- package/dist/{runner-Ol0TNjk6.mjs → runner--2KF6Kua.mjs} +1 -1
- package/dist/{runner-CXA9Fh8h.mjs → runner-voIltZrY.mjs} +1 -1
- package/dist/runtime/ai.d.mts +10 -11
- package/dist/runtime/ai.mjs +13 -18
- package/dist/runtime/client-react.mjs +1 -1
- package/dist/runtime/client-solid.mjs +1 -1
- package/dist/runtime/client-svelte.mjs +1 -1
- package/dist/runtime/client-vue.mjs +1 -1
- package/dist/runtime/client.mjs +1 -1
- package/dist/runtime/env-public.d.mts +1 -1
- package/dist/runtime/fetch-stream.mjs +1 -1
- package/dist/runtime/handler.d.mts +1 -1
- package/dist/runtime/limits.d.mts +2 -2
- package/dist/runtime/limits.mjs +2 -2
- package/dist/runtime/live-client.d.mts +1 -1
- package/dist/runtime/live.d.mts +1 -1
- package/dist/runtime/migration-handler.mjs +33 -7
- package/dist/runtime/routing.d.mts +1 -1
- package/dist/runtime/sandbox.d.mts +1 -1
- package/dist/runtime/sandbox.mjs +12 -22
- package/dist/runtime/validator.d.mts +1 -1
- package/dist/runtime/ws-server.d.mts +1 -1
- package/dist/runtime/ws.d.mts +1 -1
- package/dist/{sandbox-XZAqzFlG.d.mts → sandbox-C5LFoJMf.d.mts} +7 -7
- package/dist/{scan-C7okrLyM.mjs → scan-C8dA7iM4.mjs} +10 -6
- package/dist/{secret-3c72lY1U.mjs → secret-AKnNXNP5.mjs} +6 -6
- package/dist/{subcommand-prompt-Dhom39bd.mjs → subcommand-prompt-B6WS9Aoz.mjs} +1 -1
- package/dist/validate-pna7R3Pv.mjs +2 -0
- package/dist/{validate-ctOrgiS3.mjs → validate-ymhap6M_.mjs} +1 -1
- package/dist/{ws-CL1w7GXU.d.mts → ws-C79j4aED.d.mts} +1 -1
- package/package.json +7 -7
- package/skills/void/SKILL.md +5 -2
- package/skills/void/docs/guide/ai.md +65 -62
- package/skills/void/docs/guide/app-types.md +1 -1
- package/skills/void/docs/guide/database.md +15 -0
- package/skills/void/docs/guide/live.md +2 -0
- package/skills/void/docs/guide/pages-routing/actions-and-forms.md +1 -1
- package/skills/void/docs/guide/platform/installation/credentials.md +2 -2
- package/skills/void/docs/guide/platform/installation/first-deployment.md +5 -1
- package/skills/void/docs/guide/platform/installation/maintenance.md +3 -1
- package/skills/void/docs/guide/platform/installation/setup.md +1 -1
- package/skills/void/docs/guide/platform/installation/uninstall.md +44 -11
- package/skills/void/docs/guide/sandboxes.md +39 -20
- package/skills/void/docs/reference/api/resources.md +11 -14
- package/skills/void/docs/reference/api.md +35 -34
- package/skills/void/docs/reference/cli/auth.md +3 -1
- package/skills/void/docs/reference/cli/platform-config.md +2 -2
- package/skills/void/docs/reference/cli/platform-installation.md +14 -12
- package/skills/void/docs/reference/cli/platform.md +4 -2
- package/skills/void/docs/reference/cli.md +1 -1
- package/skills/void/docs/reference/resource-inference.md +1 -0
- package/dist/client-B4Me-9HE.mjs +0 -2
- package/dist/gen-ById9r2j.mjs +0 -2
- package/dist/help-DPGBwomV.mjs +0 -2
- package/dist/login-C98-sw79.mjs +0 -2
- package/dist/plan-D1Q5rf-r.mjs +0 -2
- package/dist/platform-args-Cq6EjQro.mjs +0 -2
- package/dist/prepare-CnjcTV2s.mjs +0 -2
- 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 {
|
|
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-
|
|
5
|
-
import { u as openBrowser } from "./auth-
|
|
6
|
-
import { A as confirm,
|
|
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-
|
|
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-
|
|
11
|
-
import { t as parsePlatformArgs } from "./platform-args-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
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}
|
|
1263
|
-
...
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
5448
|
-
* the same lock, so none can commit until the
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 {
|
|
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 };
|