@pithy-sh/cloudflare 0.1.2 → 0.1.4
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/ai/aiManager.d.ts +101 -0
- package/dist/ai/aiManager.d.ts.map +1 -0
- package/dist/ai/aiManager.js +150 -0
- package/dist/ai/vectorizeManager.d.ts +69 -0
- package/dist/ai/vectorizeManager.d.ts.map +1 -0
- package/dist/ai/vectorizeManager.js +103 -0
- package/dist/ai/vectorizeProvisioner.d.ts +125 -0
- package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
- package/dist/ai/vectorizeProvisioner.js +197 -0
- package/dist/client/accounts.d.ts +50 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +57 -0
- package/dist/client/clients.d.ts +124 -0
- package/dist/client/clients.d.ts.map +1 -0
- package/dist/client/clients.js +218 -0
- package/dist/client/errors.d.ts +75 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +100 -0
- package/dist/client/manager.d.ts +47 -0
- package/dist/client/manager.d.ts.map +1 -0
- package/dist/client/manager.js +53 -0
- package/dist/d1/d1Manager.d.ts +76 -0
- package/dist/d1/d1Manager.d.ts.map +1 -0
- package/dist/d1/d1Manager.js +130 -0
- package/dist/d1/d1PreparedStatement.d.ts +26 -0
- package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
- package/dist/d1/d1PreparedStatement.js +93 -0
- package/dist/d1/d1Provisioner.d.ts +30 -0
- package/dist/d1/d1Provisioner.d.ts.map +1 -0
- package/dist/d1/d1Provisioner.js +69 -0
- package/dist/email/emailRoutingManager.d.ts +38 -0
- package/dist/email/emailRoutingManager.d.ts.map +1 -0
- package/dist/email/emailRoutingManager.js +126 -0
- package/dist/email/emailSendManager.d.ts +47 -0
- package/dist/email/emailSendManager.d.ts.map +1 -0
- package/dist/email/emailSendManager.js +54 -0
- package/dist/env/devVars.d.ts +67 -0
- package/dist/env/devVars.d.ts.map +1 -0
- package/dist/env/devVars.js +86 -0
- package/dist/hostnames/customHostnamesManager.d.ts +53 -0
- package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
- package/dist/hostnames/customHostnamesManager.js +86 -0
- package/dist/kv/kvManager.d.ts +78 -0
- package/dist/kv/kvManager.d.ts.map +1 -0
- package/dist/kv/kvManager.js +137 -0
- package/dist/kv/kvProvisioner.d.ts +30 -0
- package/dist/kv/kvProvisioner.d.ts.map +1 -0
- package/dist/kv/kvProvisioner.js +73 -0
- package/dist/media/assetSeeder.d.ts +33 -0
- package/dist/media/assetSeeder.d.ts.map +1 -0
- package/dist/media/assetSeeder.js +72 -0
- package/dist/media/imageManager.d.ts +36 -0
- package/dist/media/imageManager.d.ts.map +1 -0
- package/dist/media/imageManager.js +105 -0
- package/dist/media/ownership.d.ts +42 -0
- package/dist/media/ownership.d.ts.map +1 -0
- package/dist/media/ownership.js +48 -0
- package/dist/media/streamManager.d.ts +85 -0
- package/dist/media/streamManager.d.ts.map +1 -0
- package/dist/media/streamManager.js +147 -0
- package/dist/queue/queueManager.d.ts +86 -0
- package/dist/queue/queueManager.d.ts.map +1 -0
- package/dist/queue/queueManager.js +116 -0
- package/dist/r2/r2Credentials.d.ts +14 -0
- package/dist/r2/r2Credentials.d.ts.map +1 -0
- package/dist/r2/r2Credentials.js +15 -0
- package/dist/r2/r2Manager.d.ts +228 -0
- package/dist/r2/r2Manager.d.ts.map +1 -0
- package/dist/r2/r2Manager.js +407 -0
- package/dist/r2/r2Provisioner.d.ts +41 -0
- package/dist/r2/r2Provisioner.d.ts.map +1 -0
- package/dist/r2/r2Provisioner.js +88 -0
- package/dist/secrets/secretsStoreManager.d.ts +84 -0
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
- package/dist/secrets/secretsStoreManager.js +149 -0
- package/dist/secrets/secretsStores.d.ts +49 -0
- package/dist/secrets/secretsStores.d.ts.map +1 -0
- package/dist/secrets/secretsStores.js +67 -0
- package/dist/test-utils/emailRoutingRules.d.ts +41 -0
- package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
- package/dist/test-utils/emailRoutingRules.js +91 -0
- package/dist/test-utils/fixtureReportSetup.d.ts +27 -0
- package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
- package/dist/test-utils/fixtureReportSetup.js +32 -0
- package/dist/test-utils/fixtures.d.ts +226 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/fixtures.js +279 -0
- package/dist/test-utils/harness.d.ts +194 -0
- package/dist/test-utils/harness.d.ts.map +1 -0
- package/dist/test-utils/harness.js +353 -0
- package/dist/test-utils/inboundRecorder.d.ts +93 -0
- package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
- package/dist/test-utils/inboundRecorder.js +143 -0
- package/dist/test-utils/integrationSetup.d.ts +24 -0
- package/dist/test-utils/integrationSetup.d.ts.map +1 -0
- package/dist/test-utils/integrationSetup.js +39 -0
- package/dist/test-utils/reap.d.ts +114 -0
- package/dist/test-utils/reap.d.ts.map +1 -0
- package/dist/test-utils/reap.js +187 -0
- package/dist/tokens/accountTokensManager.d.ts +159 -0
- package/dist/tokens/accountTokensManager.d.ts.map +1 -0
- package/dist/tokens/accountTokensManager.js +282 -0
- package/dist/tokens/permissions.d.ts +38 -0
- package/dist/tokens/permissions.d.ts.map +1 -0
- package/dist/tokens/permissions.js +52 -0
- package/dist/tokens/profiles.d.ts +104 -0
- package/dist/tokens/profiles.d.ts.map +1 -0
- package/dist/tokens/profiles.js +139 -0
- package/dist/turnstile/turnstileManager.d.ts +77 -0
- package/dist/turnstile/turnstileManager.d.ts.map +1 -0
- package/dist/turnstile/turnstileManager.js +140 -0
- package/dist/user/userManager.d.ts +38 -0
- package/dist/user/userManager.d.ts.map +1 -0
- package/dist/user/userManager.js +60 -0
- package/dist/workers/buildsManager.d.ts +118 -0
- package/dist/workers/buildsManager.d.ts.map +1 -0
- package/dist/workers/buildsManager.js +239 -0
- package/dist/workers/buildsTypes.d.ts +104 -0
- package/dist/workers/buildsTypes.d.ts.map +1 -0
- package/dist/workers/buildsTypes.js +89 -0
- package/dist/workers/workersBuildEvent.d.ts +66 -0
- package/dist/workers/workersBuildEvent.d.ts.map +1 -0
- package/dist/workers/workersBuildEvent.js +49 -0
- package/dist/workers/workersManager.d.ts +138 -0
- package/dist/workers/workersManager.d.ts.map +1 -0
- package/dist/workers/workersManager.js +314 -0
- package/dist/workers/workersProvisioner.d.ts +82 -0
- package/dist/workers/workersProvisioner.d.ts.map +1 -0
- package/dist/workers/workersProvisioner.js +108 -0
- package/dist/workflows/stepFailure.d.ts +182 -0
- package/dist/workflows/stepFailure.d.ts.map +1 -0
- package/dist/workflows/stepFailure.js +229 -0
- package/dist/workflows/workflowsClient.d.ts +89 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -0
- package/dist/workflows/workflowsClient.js +158 -0
- package/dist/zones/zonesManager.d.ts +52 -0
- package/dist/zones/zonesManager.d.ts.map +1 -0
- package/dist/zones/zonesManager.js +76 -0
- package/package.json +18 -9
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { CloudflareInvalidResponseError, cloudflareRequest, isNotFoundError } from "../client/errors.js";
|
|
4
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
5
|
+
import { stepFailure, terminalWorkflowError } from "./stepFailure.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { UpstreamTimeoutError } from "@pithy-sh/core/src/error/pithyError";
|
|
8
|
+
//#region src/workflows/workflowsClient.ts
|
|
9
|
+
const WorkflowInstanceStatus = z.enum([
|
|
10
|
+
"queued",
|
|
11
|
+
"running",
|
|
12
|
+
"paused",
|
|
13
|
+
"errored",
|
|
14
|
+
"terminated",
|
|
15
|
+
"complete",
|
|
16
|
+
"waitingForPause",
|
|
17
|
+
"waiting",
|
|
18
|
+
"rollingBack",
|
|
19
|
+
"unknown"
|
|
20
|
+
]).describe("Lifecycle state of a Cloudflare Workflow instance, from the status endpoint.");
|
|
21
|
+
const WorkflowInstance = z.object({
|
|
22
|
+
status: WorkflowInstanceStatus.describe("The instance's current lifecycle state."),
|
|
23
|
+
output: z.unknown().optional().describe("The Workflow's return value, present once `status` is `complete`."),
|
|
24
|
+
error: z.union([
|
|
25
|
+
z.string(),
|
|
26
|
+
z.object({ message: z.string().optional().describe("Failure message.") }).describe("The structured form of the failure, when the API sends an object rather than a string."),
|
|
27
|
+
z.null()
|
|
28
|
+
]).optional().describe("The instance-level failure, present when `status` is `errored` or `terminated`."),
|
|
29
|
+
steps: z.array(z.unknown()).optional().describe("The instance's steps, each validated where it is read (`stepFailure`). Held as `unknown` here on purpose: the API has four step shapes, this client reads one field of one of them, and a shape nobody anticipated must not turn a failed Workflow into a parse error about the failure report.")
|
|
30
|
+
}).describe("A Cloudflare Workflow instance's status, as returned by the instance-detail endpoint.");
|
|
31
|
+
/** Terminal states a poll stops on. */
|
|
32
|
+
const TERMINAL = /* @__PURE__ */ new Set([
|
|
33
|
+
"complete",
|
|
34
|
+
"errored",
|
|
35
|
+
"terminated"
|
|
36
|
+
]);
|
|
37
|
+
const defaultSleeper = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
+
/**
|
|
39
|
+
* A client over the Cloudflare Workflows API — the only sanctioned way to reach CF Workflows from
|
|
40
|
+
* outside a Worker (CLAUDE.md: no hand-rolled CF `fetch` outside `@pithy-sh/cloudflare`). The
|
|
41
|
+
* `pithy secrets` CLI uses it to trigger a per-env manager's Workflow and poll to completion: the
|
|
42
|
+
* CLI never runs secret logic locally, since the master key is worker-only.
|
|
43
|
+
*
|
|
44
|
+
* The SDK owns transport and envelope handling. Instance status is still re-validated through
|
|
45
|
+
* {@link WorkflowInstance} rather than taken from the SDK's types: the SDK narrows `output` to
|
|
46
|
+
* `string | number`, but a Workflow's return value is arbitrary JSON, and the status enum is a wire
|
|
47
|
+
* value we decode at the boundary like every other external input.
|
|
48
|
+
*/
|
|
49
|
+
var CloudflareWorkflowsClient = class extends CloudflareManager {
|
|
50
|
+
#sleeper;
|
|
51
|
+
constructor(config) {
|
|
52
|
+
super(config);
|
|
53
|
+
this.#sleeper = config.sleeper ?? defaultSleeper;
|
|
54
|
+
}
|
|
55
|
+
getServiceType() {
|
|
56
|
+
return "Workflows";
|
|
57
|
+
}
|
|
58
|
+
/** Prove reach by listing the account's Workflows; never throws. */
|
|
59
|
+
async validateServiceAccess() {
|
|
60
|
+
try {
|
|
61
|
+
for await (const _workflow of this.getClient().workflows.list({ account_id: this.accountId })) break;
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Create (trigger) a Workflow instance and return its id. */
|
|
68
|
+
async createInstance(workflowName, params, instanceId) {
|
|
69
|
+
return cloudflareRequest(`create instance of ${workflowName}`, async () => {
|
|
70
|
+
const created = await this.getClient().workflows.instances.create(workflowName, {
|
|
71
|
+
account_id: this.accountId,
|
|
72
|
+
params,
|
|
73
|
+
...instanceId ? { instance_id: instanceId } : {}
|
|
74
|
+
});
|
|
75
|
+
if (!created.id) throw new CloudflareInvalidResponseError({ detail: `create instance of ${workflowName}: missing instance id` });
|
|
76
|
+
return created.id;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Fetch an instance's status. Returns `null` on a 404 (a just-created instance can briefly lag). */
|
|
80
|
+
async getInstanceStatus(workflowName, instanceId) {
|
|
81
|
+
const operation = `get status of ${workflowName}/${instanceId}`;
|
|
82
|
+
return cloudflareRequest(operation, async () => {
|
|
83
|
+
let response;
|
|
84
|
+
try {
|
|
85
|
+
response = await this.getClient().workflows.instances.get(instanceId, {
|
|
86
|
+
account_id: this.accountId,
|
|
87
|
+
workflow_name: workflowName
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (isNotFoundError(error)) return null;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const parsed = WorkflowInstance.safeParse(response);
|
|
94
|
+
if (!parsed.success) throw new CloudflareInvalidResponseError({ detail: `${operation}: unexpected shape` });
|
|
95
|
+
return parsed.data;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Trigger a Workflow and poll until it reaches a terminal state. Resolves with the instance
|
|
100
|
+
* `output` on `complete`. The error never carries the dispatched params (which may be secret).
|
|
101
|
+
*
|
|
102
|
+
* **Three failures, three codes, and a caller can tell them apart without reading a word of prose**
|
|
103
|
+
* (pithy-sh/pithy#365):
|
|
104
|
+
*
|
|
105
|
+
* | What happened | Code | Status |
|
|
106
|
+
* |---|---|---|
|
|
107
|
+
* | A step raised, and the kit pins a status for its code | that code | that status |
|
|
108
|
+
* | The run ended terminally, nothing attributable | `core/workflow_failed` | 500 |
|
|
109
|
+
* | The dispatch or a poll could not be delivered | `cloudflare/request_failed` | 502 |
|
|
110
|
+
* | We stopped waiting; the instance may still finish | `core/upstream_timeout` | 504 |
|
|
111
|
+
*
|
|
112
|
+
* The first two are terminal — the run is over and re-driving it reaches the same end. The last two
|
|
113
|
+
* are not. Reporting all of them as 502 told an operator to wait for a recovery that would never
|
|
114
|
+
* come; see `terminalWorkflowError` for the argument in full.
|
|
115
|
+
*/
|
|
116
|
+
async dispatchAndPoll(workflowName, params, options = {}) {
|
|
117
|
+
const pollIntervalMs = options.pollIntervalMs ?? 1e3;
|
|
118
|
+
const maxPolls = options.maxPolls ?? 120;
|
|
119
|
+
const id = await this.createInstance(workflowName, params, options.instanceId);
|
|
120
|
+
for (let poll = 0; poll < maxPolls; poll++) {
|
|
121
|
+
await this.#sleeper(pollIntervalMs);
|
|
122
|
+
const instance = await this.getInstanceStatus(workflowName, id);
|
|
123
|
+
if (!instance) continue;
|
|
124
|
+
if (!TERMINAL.has(instance.status)) continue;
|
|
125
|
+
if (instance.status === "complete") return instance.output;
|
|
126
|
+
const failure = stepFailure(instance.steps);
|
|
127
|
+
throw terminalWorkflowError({
|
|
128
|
+
failure,
|
|
129
|
+
fallbackMessage: `Workflow ${workflowName} did not complete (${instance.status}).`,
|
|
130
|
+
detail: `instance ${id} ended ${instance.status}: ${describeFailure(failure, instance.error)}`
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
throw new UpstreamTimeoutError({
|
|
134
|
+
message: `Workflow ${workflowName} did not finish in time.`,
|
|
135
|
+
action: `Check the instance in the Cloudflare dashboard; it may still complete.`,
|
|
136
|
+
detail: `instance ${id} still running after ${maxPolls} polls`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/** The most specific instance-error text, for a failure `detail`. Never the dispatched params. */
|
|
141
|
+
function describeError(error) {
|
|
142
|
+
if (!error) return "no error detail";
|
|
143
|
+
if (typeof error === "string") return error;
|
|
144
|
+
return error.message ?? "no error detail";
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Everything known about why, for the `detail` line: the failed step's name and its recorded text
|
|
148
|
+
* verbatim, then the instance's own. Both, because they differ — that difference is the bug this
|
|
149
|
+
* function exists because of — and `detail` is the operator's side of the boundary, where the raw
|
|
150
|
+
* platform text belongs. Never the dispatched params.
|
|
151
|
+
*/
|
|
152
|
+
function describeFailure(failure, error) {
|
|
153
|
+
const instance = describeError(error);
|
|
154
|
+
if (failure === null) return instance;
|
|
155
|
+
return `${failure.step === void 0 ? "step" : `step '${failure.step}'`} raised${failure.code === void 0 ? "" : ` [${failure.code}]`} ${failure.raw} — instance error: ${instance}`;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { CloudflareWorkflowsClient, WorkflowInstance, WorkflowInstanceStatus };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { CloudflareManager } from "../client/manager";
|
|
5
|
+
/**
|
|
6
|
+
* The account's zones — the registrable domains a custom domain can attach to.
|
|
7
|
+
*
|
|
8
|
+
* **Nothing here listed zones before.** Every zone-scoped operation in this package takes a `zoneId` the
|
|
9
|
+
* caller already knows: `CloudflareCustomHostnamesManager` takes it as constructor config,
|
|
10
|
+
* `CloudflareEmailRoutingManager` takes it per method, `CloudflareWorkersManager.addRoute` takes it
|
|
11
|
+
* positionally — and `pithy email provision` makes a human paste one, telling them to "find it on the
|
|
12
|
+
* zone's Overview page". So the CLI never *discovered* a zone; it demanded one.
|
|
13
|
+
*
|
|
14
|
+
* That is what this exists to change. When `pithy init` and `pithy worker add` ask where a Worker will
|
|
15
|
+
* answer, offering the account's real zones means a typo fails at `init` with a list of what exists,
|
|
16
|
+
* rather than at `deploy` with a Cloudflare error to decode.
|
|
17
|
+
*
|
|
18
|
+
* Read-only, deliberately. Pithy attaches routes to zones and never creates, transfers, or deletes one —
|
|
19
|
+
* a zone is the adopter's relationship with their registrar, not a resource this toolset provisions. The
|
|
20
|
+
* scoped token needs only `Zone:Read`.
|
|
21
|
+
*/
|
|
22
|
+
/** One zone on the account. */
|
|
23
|
+
export declare const ZoneInfo: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
name: z.ZodString;
|
|
26
|
+
status: z.ZodString;
|
|
27
|
+
}, z.core.$strip>;
|
|
28
|
+
export type ZoneInfo = z.output<typeof ZoneInfo>;
|
|
29
|
+
/** Read the account's zones. Never creates or deletes — a zone is the adopter's, not ours to provision. */
|
|
30
|
+
export declare class CloudflareZonesManager extends CloudflareManager {
|
|
31
|
+
getServiceType(): string;
|
|
32
|
+
/** Prove access by listing. A read, and never throws — the caller decides what an inaccessible account means. */
|
|
33
|
+
validateServiceAccess(): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Every zone on this account, name-sorted so a picker is stable between runs.
|
|
36
|
+
*
|
|
37
|
+
* Filtered to the account, because a user-bound token can see zones on accounts this project has
|
|
38
|
+
* nothing to do with — offering those would invite someone to attach a Worker to a zone that another
|
|
39
|
+
* account owns, which fails at deploy with an error that names neither problem.
|
|
40
|
+
*/
|
|
41
|
+
listZones(): Promise<ZoneInfo[]>;
|
|
42
|
+
/**
|
|
43
|
+
* The zone a hostname belongs to, or `null`.
|
|
44
|
+
*
|
|
45
|
+
* Matches the **longest** zone name that the hostname sits under, which is the only correct rule when
|
|
46
|
+
* an account holds both `example.com` and `eu.example.com`: `api.eu.example.com` belongs to the latter,
|
|
47
|
+
* and picking the first match would attach it to the wrong zone. A public-suffix guess is not used at
|
|
48
|
+
* all — a zone can itself be a subdomain, and the account's own list is the authority.
|
|
49
|
+
*/
|
|
50
|
+
findZoneForHostname(hostname: string): Promise<ZoneInfo | null>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=zonesManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zonesManager.d.ts","sourceRoot":"","sources":["../../src/zones/zonesManager.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;;;;;;;GAgBG;AAEH,+BAA+B;AAC/B,eAAO,MAAM,QAAQ;;;;iBAc4F,CAAC;AAClH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,QAAQ,CAAC,CAAC;AAEjD,2GAA2G;AAC3G,qBAAa,sBAAuB,SAAQ,iBAAiB;IAC3D,cAAc,IAAI,MAAM,CAEvB;IAED,iHAAiH;IAC3G,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;OAMG;IACG,SAAS,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CASrC;IAED;;;;;;;OAOG;IACG,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAKpE;CACF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Pithy
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { cloudflareRequest } from "../client/errors.js";
|
|
4
|
+
import { CloudflareManager } from "../client/manager.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
//#region src/zones/zonesManager.ts
|
|
7
|
+
/**
|
|
8
|
+
* The account's zones — the registrable domains a custom domain can attach to.
|
|
9
|
+
*
|
|
10
|
+
* **Nothing here listed zones before.** Every zone-scoped operation in this package takes a `zoneId` the
|
|
11
|
+
* caller already knows: `CloudflareCustomHostnamesManager` takes it as constructor config,
|
|
12
|
+
* `CloudflareEmailRoutingManager` takes it per method, `CloudflareWorkersManager.addRoute` takes it
|
|
13
|
+
* positionally — and `pithy email provision` makes a human paste one, telling them to "find it on the
|
|
14
|
+
* zone's Overview page". So the CLI never *discovered* a zone; it demanded one.
|
|
15
|
+
*
|
|
16
|
+
* That is what this exists to change. When `pithy init` and `pithy worker add` ask where a Worker will
|
|
17
|
+
* answer, offering the account's real zones means a typo fails at `init` with a list of what exists,
|
|
18
|
+
* rather than at `deploy` with a Cloudflare error to decode.
|
|
19
|
+
*
|
|
20
|
+
* Read-only, deliberately. Pithy attaches routes to zones and never creates, transfers, or deletes one —
|
|
21
|
+
* a zone is the adopter's relationship with their registrar, not a resource this toolset provisions. The
|
|
22
|
+
* scoped token needs only `Zone:Read`.
|
|
23
|
+
*/
|
|
24
|
+
/** One zone on the account. */
|
|
25
|
+
const ZoneInfo = z.object({
|
|
26
|
+
id: z.string().describe("The CF-assigned zone id, which every zone-scoped API call is addressed by."),
|
|
27
|
+
name: z.string().describe("The registrable domain, e.g. `example.com`. This is the value a Worker's `domains` declaration names as its `zone`, and what wrangler writes as `zone_name`."),
|
|
28
|
+
status: z.string().describe("The zone's lifecycle status — `active` once Cloudflare is serving it, otherwise `pending`, `initializing`, or `moved`. A non-active zone cannot carry a custom domain yet, so a picker shows it and says so rather than hiding it.")
|
|
29
|
+
}).describe("One Cloudflare zone: the registrable domain a custom domain can attach to, and whether it is live.");
|
|
30
|
+
/** Read the account's zones. Never creates or deletes — a zone is the adopter's, not ours to provision. */
|
|
31
|
+
var CloudflareZonesManager = class extends CloudflareManager {
|
|
32
|
+
getServiceType() {
|
|
33
|
+
return "Cloudflare Zones";
|
|
34
|
+
}
|
|
35
|
+
/** Prove access by listing. A read, and never throws — the caller decides what an inaccessible account means. */
|
|
36
|
+
async validateServiceAccess() {
|
|
37
|
+
try {
|
|
38
|
+
await this.listZones();
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Every zone on this account, name-sorted so a picker is stable between runs.
|
|
46
|
+
*
|
|
47
|
+
* Filtered to the account, because a user-bound token can see zones on accounts this project has
|
|
48
|
+
* nothing to do with — offering those would invite someone to attach a Worker to a zone that another
|
|
49
|
+
* account owns, which fails at deploy with an error that names neither problem.
|
|
50
|
+
*/
|
|
51
|
+
async listZones() {
|
|
52
|
+
return cloudflareRequest("list zones", async () => {
|
|
53
|
+
const zones = [];
|
|
54
|
+
for await (const zone of this.getClient().zones.list({ account: { id: this.accountId } })) {
|
|
55
|
+
const parsed = ZoneInfo.safeParse(zone);
|
|
56
|
+
if (parsed.success) zones.push(parsed.data);
|
|
57
|
+
}
|
|
58
|
+
return zones.sort((a, b) => a.name.localeCompare(b.name));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The zone a hostname belongs to, or `null`.
|
|
63
|
+
*
|
|
64
|
+
* Matches the **longest** zone name that the hostname sits under, which is the only correct rule when
|
|
65
|
+
* an account holds both `example.com` and `eu.example.com`: `api.eu.example.com` belongs to the latter,
|
|
66
|
+
* and picking the first match would attach it to the wrong zone. A public-suffix guess is not used at
|
|
67
|
+
* all — a zone can itself be a subdomain, and the account's own list is the authority.
|
|
68
|
+
*/
|
|
69
|
+
async findZoneForHostname(hostname) {
|
|
70
|
+
const candidates = (await this.listZones()).filter((zone) => hostname === zone.name || hostname.endsWith(`.${zone.name}`));
|
|
71
|
+
if (candidates.length === 0) return null;
|
|
72
|
+
return candidates.reduce((longest, zone) => zone.name.length > longest.name.length ? zone : longest);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { CloudflareZonesManager, ZoneInfo };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pithy-sh/cloudflare",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,18 +8,23 @@
|
|
|
8
8
|
"directory": "packages/cloudflare"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
+
"dist",
|
|
11
12
|
"src",
|
|
12
|
-
"!src/**/*.test.*"
|
|
13
|
+
"!src/**/*.test.*",
|
|
14
|
+
"!dist/**/*.test.*"
|
|
13
15
|
],
|
|
14
16
|
"type": "module",
|
|
15
17
|
"engines": {
|
|
16
18
|
"node": ">=22"
|
|
17
19
|
},
|
|
18
20
|
"exports": {
|
|
19
|
-
"./src/*":
|
|
21
|
+
"./src/*": {
|
|
22
|
+
"types": "./dist/*.d.ts",
|
|
23
|
+
"default": "./dist/*.js"
|
|
24
|
+
}
|
|
20
25
|
},
|
|
21
26
|
"scripts": {
|
|
22
|
-
"build": "tsc -p tsconfig.json
|
|
27
|
+
"build": "tsdown && tsc -p tsconfig.build.json && bun ../../tooling/build/src/stampDeclarations.ts",
|
|
23
28
|
"typecheck": "tsc -p tsconfig.json",
|
|
24
29
|
"test": "vitest run",
|
|
25
30
|
"test:node": "vitest run --project=node",
|
|
@@ -31,18 +36,22 @@
|
|
|
31
36
|
"@aws-sdk/client-s3": "^3.1111.0",
|
|
32
37
|
"@aws-sdk/s3-request-presigner": "^3.1111.0",
|
|
33
38
|
"@cloudflare/workers-types": "^5.20260729.1",
|
|
34
|
-
"@pithy-sh/core": "^0.1.
|
|
39
|
+
"@pithy-sh/core": "^0.1.5",
|
|
35
40
|
"cloudflare": "^7.0.0",
|
|
36
|
-
"js-base64": "^3.9.2"
|
|
37
|
-
"kysely": "^0.29.0",
|
|
38
|
-
"zod": "^4.0.0"
|
|
41
|
+
"js-base64": "^3.9.2"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@pithy-sh/tsconfig": "workspace:*",
|
|
42
45
|
"@types/node": "^22.15.0",
|
|
43
46
|
"@vitest/coverage-v8": "^4.1.0",
|
|
47
|
+
"kysely": "^0.29.0",
|
|
44
48
|
"kysely-d1": "^0.4.0",
|
|
49
|
+
"tsdown": "^0.23.0",
|
|
45
50
|
"typescript": "^7.0.2",
|
|
46
|
-
"vitest": "^4.1.0"
|
|
51
|
+
"vitest": "^4.1.0",
|
|
52
|
+
"zod": "^4.4.0"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"zod": "^4.4.0"
|
|
47
56
|
}
|
|
48
57
|
}
|