@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.
Files changed (139) hide show
  1. package/dist/ai/aiManager.d.ts +101 -0
  2. package/dist/ai/aiManager.d.ts.map +1 -0
  3. package/dist/ai/aiManager.js +150 -0
  4. package/dist/ai/vectorizeManager.d.ts +69 -0
  5. package/dist/ai/vectorizeManager.d.ts.map +1 -0
  6. package/dist/ai/vectorizeManager.js +103 -0
  7. package/dist/ai/vectorizeProvisioner.d.ts +125 -0
  8. package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
  9. package/dist/ai/vectorizeProvisioner.js +197 -0
  10. package/dist/client/accounts.d.ts +50 -0
  11. package/dist/client/accounts.d.ts.map +1 -0
  12. package/dist/client/accounts.js +57 -0
  13. package/dist/client/clients.d.ts +124 -0
  14. package/dist/client/clients.d.ts.map +1 -0
  15. package/dist/client/clients.js +218 -0
  16. package/dist/client/errors.d.ts +75 -0
  17. package/dist/client/errors.d.ts.map +1 -0
  18. package/dist/client/errors.js +100 -0
  19. package/dist/client/manager.d.ts +47 -0
  20. package/dist/client/manager.d.ts.map +1 -0
  21. package/dist/client/manager.js +53 -0
  22. package/dist/d1/d1Manager.d.ts +76 -0
  23. package/dist/d1/d1Manager.d.ts.map +1 -0
  24. package/dist/d1/d1Manager.js +130 -0
  25. package/dist/d1/d1PreparedStatement.d.ts +26 -0
  26. package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
  27. package/dist/d1/d1PreparedStatement.js +93 -0
  28. package/dist/d1/d1Provisioner.d.ts +30 -0
  29. package/dist/d1/d1Provisioner.d.ts.map +1 -0
  30. package/dist/d1/d1Provisioner.js +69 -0
  31. package/dist/email/emailRoutingManager.d.ts +38 -0
  32. package/dist/email/emailRoutingManager.d.ts.map +1 -0
  33. package/dist/email/emailRoutingManager.js +126 -0
  34. package/dist/email/emailSendManager.d.ts +47 -0
  35. package/dist/email/emailSendManager.d.ts.map +1 -0
  36. package/dist/email/emailSendManager.js +54 -0
  37. package/dist/env/devVars.d.ts +67 -0
  38. package/dist/env/devVars.d.ts.map +1 -0
  39. package/dist/env/devVars.js +86 -0
  40. package/dist/hostnames/customHostnamesManager.d.ts +53 -0
  41. package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
  42. package/dist/hostnames/customHostnamesManager.js +86 -0
  43. package/dist/kv/kvManager.d.ts +78 -0
  44. package/dist/kv/kvManager.d.ts.map +1 -0
  45. package/dist/kv/kvManager.js +137 -0
  46. package/dist/kv/kvProvisioner.d.ts +30 -0
  47. package/dist/kv/kvProvisioner.d.ts.map +1 -0
  48. package/dist/kv/kvProvisioner.js +73 -0
  49. package/dist/media/assetSeeder.d.ts +33 -0
  50. package/dist/media/assetSeeder.d.ts.map +1 -0
  51. package/dist/media/assetSeeder.js +72 -0
  52. package/dist/media/imageManager.d.ts +36 -0
  53. package/dist/media/imageManager.d.ts.map +1 -0
  54. package/dist/media/imageManager.js +105 -0
  55. package/dist/media/ownership.d.ts +42 -0
  56. package/dist/media/ownership.d.ts.map +1 -0
  57. package/dist/media/ownership.js +48 -0
  58. package/dist/media/streamManager.d.ts +85 -0
  59. package/dist/media/streamManager.d.ts.map +1 -0
  60. package/dist/media/streamManager.js +147 -0
  61. package/dist/queue/queueManager.d.ts +86 -0
  62. package/dist/queue/queueManager.d.ts.map +1 -0
  63. package/dist/queue/queueManager.js +116 -0
  64. package/dist/r2/r2Credentials.d.ts +14 -0
  65. package/dist/r2/r2Credentials.d.ts.map +1 -0
  66. package/dist/r2/r2Credentials.js +15 -0
  67. package/dist/r2/r2Manager.d.ts +228 -0
  68. package/dist/r2/r2Manager.d.ts.map +1 -0
  69. package/dist/r2/r2Manager.js +407 -0
  70. package/dist/r2/r2Provisioner.d.ts +41 -0
  71. package/dist/r2/r2Provisioner.d.ts.map +1 -0
  72. package/dist/r2/r2Provisioner.js +88 -0
  73. package/dist/secrets/secretsStoreManager.d.ts +84 -0
  74. package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
  75. package/dist/secrets/secretsStoreManager.js +149 -0
  76. package/dist/secrets/secretsStores.d.ts +49 -0
  77. package/dist/secrets/secretsStores.d.ts.map +1 -0
  78. package/dist/secrets/secretsStores.js +67 -0
  79. package/dist/test-utils/emailRoutingRules.d.ts +41 -0
  80. package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
  81. package/dist/test-utils/emailRoutingRules.js +91 -0
  82. package/dist/test-utils/fixtureReportSetup.d.ts +27 -0
  83. package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
  84. package/dist/test-utils/fixtureReportSetup.js +32 -0
  85. package/dist/test-utils/fixtures.d.ts +226 -0
  86. package/dist/test-utils/fixtures.d.ts.map +1 -0
  87. package/dist/test-utils/fixtures.js +279 -0
  88. package/dist/test-utils/harness.d.ts +194 -0
  89. package/dist/test-utils/harness.d.ts.map +1 -0
  90. package/dist/test-utils/harness.js +353 -0
  91. package/dist/test-utils/inboundRecorder.d.ts +93 -0
  92. package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
  93. package/dist/test-utils/inboundRecorder.js +143 -0
  94. package/dist/test-utils/integrationSetup.d.ts +24 -0
  95. package/dist/test-utils/integrationSetup.d.ts.map +1 -0
  96. package/dist/test-utils/integrationSetup.js +39 -0
  97. package/dist/test-utils/reap.d.ts +114 -0
  98. package/dist/test-utils/reap.d.ts.map +1 -0
  99. package/dist/test-utils/reap.js +187 -0
  100. package/dist/tokens/accountTokensManager.d.ts +159 -0
  101. package/dist/tokens/accountTokensManager.d.ts.map +1 -0
  102. package/dist/tokens/accountTokensManager.js +282 -0
  103. package/dist/tokens/permissions.d.ts +38 -0
  104. package/dist/tokens/permissions.d.ts.map +1 -0
  105. package/dist/tokens/permissions.js +52 -0
  106. package/dist/tokens/profiles.d.ts +104 -0
  107. package/dist/tokens/profiles.d.ts.map +1 -0
  108. package/dist/tokens/profiles.js +139 -0
  109. package/dist/turnstile/turnstileManager.d.ts +77 -0
  110. package/dist/turnstile/turnstileManager.d.ts.map +1 -0
  111. package/dist/turnstile/turnstileManager.js +140 -0
  112. package/dist/user/userManager.d.ts +38 -0
  113. package/dist/user/userManager.d.ts.map +1 -0
  114. package/dist/user/userManager.js +60 -0
  115. package/dist/workers/buildsManager.d.ts +118 -0
  116. package/dist/workers/buildsManager.d.ts.map +1 -0
  117. package/dist/workers/buildsManager.js +239 -0
  118. package/dist/workers/buildsTypes.d.ts +104 -0
  119. package/dist/workers/buildsTypes.d.ts.map +1 -0
  120. package/dist/workers/buildsTypes.js +89 -0
  121. package/dist/workers/workersBuildEvent.d.ts +66 -0
  122. package/dist/workers/workersBuildEvent.d.ts.map +1 -0
  123. package/dist/workers/workersBuildEvent.js +49 -0
  124. package/dist/workers/workersManager.d.ts +138 -0
  125. package/dist/workers/workersManager.d.ts.map +1 -0
  126. package/dist/workers/workersManager.js +314 -0
  127. package/dist/workers/workersProvisioner.d.ts +82 -0
  128. package/dist/workers/workersProvisioner.d.ts.map +1 -0
  129. package/dist/workers/workersProvisioner.js +108 -0
  130. package/dist/workflows/stepFailure.d.ts +182 -0
  131. package/dist/workflows/stepFailure.d.ts.map +1 -0
  132. package/dist/workflows/stepFailure.js +229 -0
  133. package/dist/workflows/workflowsClient.d.ts +89 -0
  134. package/dist/workflows/workflowsClient.d.ts.map +1 -0
  135. package/dist/workflows/workflowsClient.js +158 -0
  136. package/dist/zones/zonesManager.d.ts +52 -0
  137. package/dist/zones/zonesManager.d.ts.map +1 -0
  138. package/dist/zones/zonesManager.js +76 -0
  139. 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.2",
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/*": "./src/*.ts"
21
+ "./src/*": {
22
+ "types": "./dist/*.d.ts",
23
+ "default": "./dist/*.js"
24
+ }
20
25
  },
21
26
  "scripts": {
22
- "build": "tsc -p tsconfig.json --noEmit false --outDir dist",
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.2",
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
  }