@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,41 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ import { CloudflareManager } from "../client/manager";
5
+ /** An R2 bucket's identity, decoded from the create/list response. Buckets are addressed by name, not id. */
6
+ export declare const R2BucketInfo: z.ZodObject<{
7
+ name: z.ZodString;
8
+ }, z.core.$strip>;
9
+ export type R2BucketInfo = z.output<typeof R2BucketInfo>;
10
+ /**
11
+ * Account-level R2 control plane: **create and delete buckets**. Customers use this to stand up and
12
+ * tear down per-environment buckets (e.g. ephemeral staging), and worktree teardown uses it to
13
+ * reconcile/prune feature-scoped buckets. Unlike {@link CloudflareR2Manager}, which operates within
14
+ * one already-provisioned bucket (presigned object URLs), this manager is account-scoped and
15
+ * addresses buckets **by name** — R2 buckets carry no separate uuid id the way D1 databases do.
16
+ */
17
+ export declare class CloudflareR2Provisioner extends CloudflareManager {
18
+ getServiceType(): string;
19
+ /** Prove access by listing buckets — a read, never a destructive create/delete. Never throws. */
20
+ validateServiceAccess(): Promise<boolean>;
21
+ /** Find a bucket by exact name in the account, or `null` — for idempotent provisioning. */
22
+ findBucketByName(name: string): Promise<R2BucketInfo | null>;
23
+ /**
24
+ * List every bucket in the account. R2's bucket list is cursor-paginated (a single call returns only
25
+ * one page, unlike D1/KV whose SDK list auto-paginates), so this drains every page via `start_after` —
26
+ * the name of the last bucket seen — until a page comes back empty, then returns them all. Skips entries
27
+ * with no name.
28
+ */
29
+ listBuckets(): Promise<R2BucketInfo[]>;
30
+ /** Create an R2 bucket by name; returns its name. */
31
+ createBucket(name: string): Promise<R2BucketInfo>;
32
+ /**
33
+ * Delete an R2 bucket by name. Idempotent — a missing bucket is not an error, so teardown can re-run safely.
34
+ *
35
+ * **The bucket must already be empty.** R2 rejects the delete otherwise, and "empty" includes multipart
36
+ * uploads that were never completed — parts nothing in this control plane can even see. Emptying is an
37
+ * S3-protocol operation: call `CloudflareR2Manager.emptyBucket` first, which needs the S3 key pair.
38
+ */
39
+ deleteBucket(name: string): Promise<void>;
40
+ }
41
+ //# sourceMappingURL=r2Provisioner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2Provisioner.d.ts","sourceRoot":"","sources":["../../src/r2/r2Provisioner.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD,6GAA6G;AAC7G,eAAO,MAAM,YAAY;;iBAIkE,CAAC;AAC5F,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,qBAAa,uBAAwB,SAAQ,iBAAiB;IAC5D,cAAc,IAAI,MAAM,CAEvB;IAED,iGAAiG;IAC3F,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED,2FAA2F;IACrF,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAGjE;IAED;;;;;OAKG;IACG,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAsB3C;IAED,qDAAqD;IAC/C,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAKtD;IAED;;;;;;OAMG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAS9C;CACF"}
@@ -0,0 +1,88 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { cloudflareRequest, decodeResponse, isNotFoundError } from "../client/errors.js";
4
+ import { CloudflareManager } from "../client/manager.js";
5
+ import { z } from "zod";
6
+ //#region src/r2/r2Provisioner.ts
7
+ /** An R2 bucket's identity, decoded from the create/list response. Buckets are addressed by name, not id. */
8
+ const R2BucketInfo = z.object({ name: z.string().describe("The bucket name — R2's sole address for a bucket (no separate uuid id).") }).describe("A Cloudflare R2 bucket's identity, as returned by the create/list endpoints.");
9
+ /**
10
+ * Account-level R2 control plane: **create and delete buckets**. Customers use this to stand up and
11
+ * tear down per-environment buckets (e.g. ephemeral staging), and worktree teardown uses it to
12
+ * reconcile/prune feature-scoped buckets. Unlike {@link CloudflareR2Manager}, which operates within
13
+ * one already-provisioned bucket (presigned object URLs), this manager is account-scoped and
14
+ * addresses buckets **by name** — R2 buckets carry no separate uuid id the way D1 databases do.
15
+ */
16
+ var CloudflareR2Provisioner = class extends CloudflareManager {
17
+ getServiceType() {
18
+ return "Cloudflare R2 (control plane)";
19
+ }
20
+ /** Prove access by listing buckets — a read, never a destructive create/delete. Never throws. */
21
+ async validateServiceAccess() {
22
+ try {
23
+ await this.getClient().r2.buckets.list({ account_id: this.accountId });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Find a bucket by exact name in the account, or `null` — for idempotent provisioning. */
30
+ async findBucketByName(name) {
31
+ return (await this.listBuckets()).find((bucket) => bucket.name === name) ?? null;
32
+ }
33
+ /**
34
+ * List every bucket in the account. R2's bucket list is cursor-paginated (a single call returns only
35
+ * one page, unlike D1/KV whose SDK list auto-paginates), so this drains every page via `start_after` —
36
+ * the name of the last bucket seen — until a page comes back empty, then returns them all. Skips entries
37
+ * with no name.
38
+ */
39
+ async listBuckets() {
40
+ return cloudflareRequest("list R2 buckets", async () => {
41
+ const buckets = [];
42
+ let startAfter;
43
+ for (;;) {
44
+ const page = (await this.getClient().r2.buckets.list({
45
+ account_id: this.accountId,
46
+ per_page: 1e3,
47
+ ...startAfter ? { start_after: startAfter } : {}
48
+ })).buckets ?? [];
49
+ if (page.length === 0) break;
50
+ for (const bucket of page) {
51
+ const parsed = R2BucketInfo.safeParse(bucket);
52
+ if (parsed.success) buckets.push(parsed.data);
53
+ }
54
+ const last = page[page.length - 1]?.name;
55
+ if (!last || last === startAfter) break;
56
+ startAfter = last;
57
+ }
58
+ return buckets;
59
+ });
60
+ }
61
+ /** Create an R2 bucket by name; returns its name. */
62
+ async createBucket(name) {
63
+ const response = await cloudflareRequest(`create R2 bucket ${name}`, () => this.getClient().r2.buckets.create({
64
+ account_id: this.accountId,
65
+ name
66
+ }));
67
+ return decodeResponse(R2BucketInfo, response, "R2 bucket create");
68
+ }
69
+ /**
70
+ * Delete an R2 bucket by name. Idempotent — a missing bucket is not an error, so teardown can re-run safely.
71
+ *
72
+ * **The bucket must already be empty.** R2 rejects the delete otherwise, and "empty" includes multipart
73
+ * uploads that were never completed — parts nothing in this control plane can even see. Emptying is an
74
+ * S3-protocol operation: call `CloudflareR2Manager.emptyBucket` first, which needs the S3 key pair.
75
+ */
76
+ async deleteBucket(name) {
77
+ await cloudflareRequest(`delete R2 bucket ${name}`, async () => {
78
+ try {
79
+ await this.getClient().r2.buckets.delete(name, { account_id: this.accountId });
80
+ } catch (error) {
81
+ if (isNotFoundError(error)) return;
82
+ throw error;
83
+ }
84
+ });
85
+ }
86
+ };
87
+ //#endregion
88
+ export { CloudflareR2Provisioner, R2BucketInfo };
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ import { CloudflareManager, type CloudflareManagerConfig } from "../client/manager";
5
+ /**
6
+ * One secret in the store, decoded from the CF list response. CF Secrets Store never returns
7
+ * plaintext over REST — values are bind-only by design — so this carries only metadata. The
8
+ * `created`/`modified` ISO strings decode through `JsonDate` to real `Date`s at the wire boundary.
9
+ */
10
+ export declare const CfSecretEntry: z.ZodObject<{
11
+ id: z.ZodString;
12
+ name: z.ZodString;
13
+ status: z.ZodEnum<{
14
+ active: "active";
15
+ deleted: "deleted";
16
+ pending: "pending";
17
+ }>;
18
+ created: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
19
+ modified: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
20
+ }, z.core.$strip>;
21
+ export type CfSecretEntry = z.output<typeof CfSecretEntry>;
22
+ /** Config for the Secrets Store manager: the shared client config plus the store it targets. */
23
+ export interface SecretsStoreManagerConfig extends CloudflareManagerConfig {
24
+ /** The CF Secrets Store id (the REST API addresses stores by id). */
25
+ storeId: string;
26
+ }
27
+ /**
28
+ * Out-of-Worker access to the account-level Cloudflare Secrets Store over the REST API: provisioning
29
+ * and audit from a CLI/CI context. Inside a Worker, secret values resolve via bindings — this manager
30
+ * is the REST counterpart for managing the store, addressed by store id.
31
+ *
32
+ * CF Secrets Store does not expose secret plaintext via REST (values are bind-only by design), so
33
+ * there is no `getSecret`. The provisioning and audit flows only need put, delete, and list.
34
+ */
35
+ export declare class CloudflareSecretsStoreManager extends CloudflareManager {
36
+ private readonly storeId;
37
+ constructor(config: SecretsStoreManagerConfig);
38
+ getServiceType(): string;
39
+ /** Prove access by listing the store's secrets. Never throws. */
40
+ validateServiceAccess(): Promise<boolean>;
41
+ /**
42
+ * Insert or update a secret. An existing entry is updated in place via `edit`; otherwise a fresh
43
+ * secret is created.
44
+ *
45
+ * `edit` is what makes this safe. The old value is overwritten, never deleted first, so a failed
46
+ * update leaves the prior value intact and bound — there is no window where the secret is absent
47
+ * from the store (which, for a secret like the master encryption key, is a platform-level outage).
48
+ * Scopes are re-sent so an entry converges on the same shape whichever branch wrote it.
49
+ */
50
+ putSecret(name: string, value: string): Promise<void>;
51
+ /** Delete a secret by name. Resolves the id via `listSecrets`, then issues DELETE by id. */
52
+ deleteSecret(name: string): Promise<void>;
53
+ /**
54
+ * Delete a secret, treating an absent one as already done.
55
+ *
56
+ * The typed not-found on {@link deleteSecret} is right for a caller that named a specific secret and
57
+ * needs to hear it was not there. It is wrong for anything reconciling toward absence — teardown, a
58
+ * reaper, a re-run of a provisioning step — where "gone" is the goal and a second delete is a no-op,
59
+ * not a failure. Two callers race on the store all the time: another runner's sweep, or a listing that
60
+ * has not caught up.
61
+ *
62
+ * Given as its own method rather than a flag, so which semantics a call site wants is legible at the
63
+ * call site. The alternative every caller reaches for otherwise is `.catch(() => {})`, which also
64
+ * swallows the auth failure and the outage — and a teardown that swallows is how debris becomes
65
+ * permanent with no signal at all.
66
+ */
67
+ deleteSecretIfPresent(name: string): Promise<boolean>;
68
+ /**
69
+ * List every secret in the store. The SDK auto-paginates via `for await`, so callers receive the
70
+ * full set in one array. Each entry is Zod-validated (`CfSecretEntry`) at the wire boundary.
71
+ */
72
+ listSecrets(): Promise<CfSecretEntry[]>;
73
+ /** Whether a secret with the given name currently exists in the store. */
74
+ exists(name: string): Promise<boolean>;
75
+ /** The store this manager targets and the account it lives in. */
76
+ getSecretsStoreInfo(): {
77
+ storeId: string;
78
+ accountId: string;
79
+ };
80
+ /** Create a single secret with the default scopes. */
81
+ private createSecret;
82
+ private findByName;
83
+ }
84
+ //# sourceMappingURL=secretsStoreManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secretsStoreManager.d.ts","sourceRoot":"","sources":["../../src/secrets/secretsStoreManager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAKpF;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;iBAQqE,CAAC;AAChG,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,aAAa,CAAC,CAAC;AAE3D,gGAAgG;AAChG,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACxE,qEAAqE;IACrE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,qBAAa,6BAA8B,SAAQ,iBAAiB;IAClE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IAEjC,YAAY,MAAM,EAAE,yBAAyB,EAM5C;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,iEAAiE;IAC3D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;;;OAQG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe1D;IAED,4FAA4F;IACtF,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc9C;IAED;;;;;;;;;;;;;OAaG;IACG,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1D;IAED;;;OAGG;IACG,WAAW,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAU5C;IAED,0EAA0E;IACpE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3C;IAED,kEAAkE;IAClE,mBAAmB,IAAI;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAE5D;IAED,sDAAsD;YACxC,YAAY;YASZ,UAAU;CAIzB"}
@@ -0,0 +1,149 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { CloudflareNotConfiguredError, cloudflareRequest, decodeResponse } from "../client/errors.js";
4
+ import { CloudflareManager } from "../client/manager.js";
5
+ import { z } from "zod";
6
+ import { NotFoundError } from "@pithy-sh/core/src/error/pithyError";
7
+ import { JsonDate } from "@pithy-sh/core/src/data/codecs";
8
+ //#region src/secrets/secretsStoreManager.ts
9
+ /** Scopes attached to every secret we create. CF requires at least one; "workers" is the bind target. */
10
+ const DEFAULT_SCOPES = ["workers"];
11
+ /**
12
+ * One secret in the store, decoded from the CF list response. CF Secrets Store never returns
13
+ * plaintext over REST — values are bind-only by design — so this carries only metadata. The
14
+ * `created`/`modified` ISO strings decode through `JsonDate` to real `Date`s at the wire boundary.
15
+ */
16
+ const CfSecretEntry = z.object({
17
+ id: z.string().describe("The CF-assigned secret identifier, used to address delete by id."),
18
+ name: z.string().describe("The secret's name within the store (the key the CLI references)."),
19
+ status: z.enum([
20
+ "pending",
21
+ "active",
22
+ "deleted"
23
+ ]).describe("The secret's lifecycle status in the store."),
24
+ created: JsonDate.describe("When the secret was created (ISO string on the wire, Date in app)."),
25
+ modified: JsonDate.describe("When the secret was last modified (ISO string on the wire, Date in app).")
26
+ }).describe("A single Cloudflare Secrets Store secret's metadata (never its plaintext value).");
27
+ /**
28
+ * Out-of-Worker access to the account-level Cloudflare Secrets Store over the REST API: provisioning
29
+ * and audit from a CLI/CI context. Inside a Worker, secret values resolve via bindings — this manager
30
+ * is the REST counterpart for managing the store, addressed by store id.
31
+ *
32
+ * CF Secrets Store does not expose secret plaintext via REST (values are bind-only by design), so
33
+ * there is no `getSecret`. The provisioning and audit flows only need put, delete, and list.
34
+ */
35
+ var CloudflareSecretsStoreManager = class extends CloudflareManager {
36
+ storeId;
37
+ constructor(config) {
38
+ super(config);
39
+ if (!config.storeId) throw new CloudflareNotConfiguredError({ detail: "Missing storeId for Secrets Store REST access." });
40
+ this.storeId = config.storeId;
41
+ }
42
+ getServiceType() {
43
+ return "Cloudflare Secrets Store";
44
+ }
45
+ /** Prove access by listing the store's secrets. Never throws. */
46
+ async validateServiceAccess() {
47
+ try {
48
+ await this.listSecrets();
49
+ return true;
50
+ } catch {
51
+ return false;
52
+ }
53
+ }
54
+ /**
55
+ * Insert or update a secret. An existing entry is updated in place via `edit`; otherwise a fresh
56
+ * secret is created.
57
+ *
58
+ * `edit` is what makes this safe. The old value is overwritten, never deleted first, so a failed
59
+ * update leaves the prior value intact and bound — there is no window where the secret is absent
60
+ * from the store (which, for a secret like the master encryption key, is a platform-level outage).
61
+ * Scopes are re-sent so an entry converges on the same shape whichever branch wrote it.
62
+ */
63
+ async putSecret(name, value) {
64
+ const existing = await this.findByName(name);
65
+ if (!existing) {
66
+ await this.createSecret(name, value);
67
+ return;
68
+ }
69
+ await cloudflareRequest(`put secret ${name}`, () => this.getClient().secretsStore.stores.secrets.edit(existing.id, {
70
+ account_id: this.accountId,
71
+ store_id: this.storeId,
72
+ value,
73
+ scopes: [...DEFAULT_SCOPES]
74
+ }));
75
+ }
76
+ /** Delete a secret by name. Resolves the id via `listSecrets`, then issues DELETE by id. */
77
+ async deleteSecret(name) {
78
+ const existing = await this.findByName(name);
79
+ if (!existing) throw new NotFoundError({
80
+ message: `Secret '${name}' was not found in the store.`,
81
+ detail: `delete secret ${name}: no entry with that name`
82
+ });
83
+ await cloudflareRequest(`delete secret ${name}`, () => this.getClient().secretsStore.stores.secrets.delete(existing.id, {
84
+ account_id: this.accountId,
85
+ store_id: this.storeId
86
+ }));
87
+ }
88
+ /**
89
+ * Delete a secret, treating an absent one as already done.
90
+ *
91
+ * The typed not-found on {@link deleteSecret} is right for a caller that named a specific secret and
92
+ * needs to hear it was not there. It is wrong for anything reconciling toward absence — teardown, a
93
+ * reaper, a re-run of a provisioning step — where "gone" is the goal and a second delete is a no-op,
94
+ * not a failure. Two callers race on the store all the time: another runner's sweep, or a listing that
95
+ * has not caught up.
96
+ *
97
+ * Given as its own method rather than a flag, so which semantics a call site wants is legible at the
98
+ * call site. The alternative every caller reaches for otherwise is `.catch(() => {})`, which also
99
+ * swallows the auth failure and the outage — and a teardown that swallows is how debris becomes
100
+ * permanent with no signal at all.
101
+ */
102
+ async deleteSecretIfPresent(name) {
103
+ const existing = await this.findByName(name);
104
+ if (!existing) return false;
105
+ await cloudflareRequest(`delete secret ${name}`, () => this.getClient().secretsStore.stores.secrets.delete(existing.id, {
106
+ account_id: this.accountId,
107
+ store_id: this.storeId
108
+ }));
109
+ return true;
110
+ }
111
+ /**
112
+ * List every secret in the store. The SDK auto-paginates via `for await`, so callers receive the
113
+ * full set in one array. Each entry is Zod-validated (`CfSecretEntry`) at the wire boundary.
114
+ */
115
+ async listSecrets() {
116
+ return cloudflareRequest("list secrets", async () => {
117
+ const out = [];
118
+ for await (const entry of this.getClient().secretsStore.stores.secrets.list(this.storeId, { account_id: this.accountId })) out.push(decodeResponse(CfSecretEntry, entry, "Secrets Store list entry"));
119
+ return out;
120
+ });
121
+ }
122
+ /** Whether a secret with the given name currently exists in the store. */
123
+ async exists(name) {
124
+ return await this.findByName(name) !== void 0;
125
+ }
126
+ /** The store this manager targets and the account it lives in. */
127
+ getSecretsStoreInfo() {
128
+ return {
129
+ storeId: this.storeId,
130
+ accountId: this.accountId
131
+ };
132
+ }
133
+ /** Create a single secret with the default scopes. */
134
+ async createSecret(name, value) {
135
+ await cloudflareRequest(`create secret ${name}`, () => this.getClient().secretsStore.stores.secrets.create(this.storeId, {
136
+ account_id: this.accountId,
137
+ body: [{
138
+ name,
139
+ value,
140
+ scopes: [...DEFAULT_SCOPES]
141
+ }]
142
+ }));
143
+ }
144
+ async findByName(name) {
145
+ return (await this.listSecrets()).find((entry) => entry.name === name);
146
+ }
147
+ };
148
+ //#endregion
149
+ export { CfSecretEntry, CloudflareSecretsStoreManager };
@@ -0,0 +1,49 @@
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-level** view of Cloudflare Secrets Store: which stores exist, and nothing else.
7
+ *
8
+ * Its sibling {@link CloudflareSecretsStoreManager} is addressed *by store id* — it demands one in its
9
+ * constructor, because every operation it has is inside a store. That is the right shape for putting and
10
+ * deleting secrets and the wrong shape for the one question asked before any store id is known: which
11
+ * store does this account have?
12
+ *
13
+ * **One store per account**, which is what makes the answer usable at all. `pithy add secrets` resolves
14
+ * the id here, once, at provisioning time, and writes it into `<config>/cloudflare.json` — so every later
15
+ * run is a plain file read. Discovery on every invocation was considered and rejected (#182): a CLI that
16
+ * cannot run offline because it has to ask Cloudflare where its own store is has lost more than the key
17
+ * was costing.
18
+ */
19
+ /**
20
+ * One store in the account, decoded from the CF list response.
21
+ *
22
+ * Metadata only — a store holds secrets whose plaintext the REST API never returns. The `created` and
23
+ * `modified` ISO strings decode through `JsonDate` at the wire boundary, as every other response here
24
+ * does, so nothing downstream parses a date string a second time.
25
+ */
26
+ export declare const CfSecretsStore: z.ZodObject<{
27
+ id: z.ZodString;
28
+ name: z.ZodString;
29
+ created: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
30
+ modified: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
31
+ }, z.core.$strip>;
32
+ export type CfSecretsStore = z.output<typeof CfSecretsStore>;
33
+ /** Account-level Secrets Store operations: listing the account's stores. Addressed by account, not store. */
34
+ export declare class CloudflareSecretsStoresManager extends CloudflareManager {
35
+ getServiceType(): string;
36
+ /** Prove access by listing the account's stores. Never throws. */
37
+ validateServiceAccess(): Promise<boolean>;
38
+ /**
39
+ * Every Secrets Store in the account. The SDK auto-paginates via `for await`, so a caller receives
40
+ * the full set in one array, and each entry is Zod-validated at the wire boundary.
41
+ *
42
+ * **Returns the list, never a choice.** Cloudflare permits one store per account, so the ordinary
43
+ * answer has exactly one element — but "ordinarily one" is not "always one", and picking the first of
44
+ * two would be guessing which store holds an adopter's production secrets. The caller decides, and the
45
+ * only correct decision for two is to refuse and name them.
46
+ */
47
+ listStores(): Promise<CfSecretsStore[]>;
48
+ }
49
+ //# sourceMappingURL=secretsStores.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secretsStores.d.ts","sourceRoot":"","sources":["../../src/secrets/secretsStores.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;;iBAO6E,CAAC;AACzG,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,cAAc,CAAC,CAAC;AAE7D,6GAA6G;AAC7G,qBAAa,8BAA+B,SAAQ,iBAAiB;IACnE,cAAc,IAAI,MAAM,CAEvB;IAED,kEAAkE;IAC5D,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO9C;IAED;;;;;;;;OAQG;IACG,UAAU,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAQ5C;CACF"}
@@ -0,0 +1,67 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { cloudflareRequest, decodeResponse } from "../client/errors.js";
4
+ import { CloudflareManager } from "../client/manager.js";
5
+ import { z } from "zod";
6
+ import { JsonDate } from "@pithy-sh/core/src/data/codecs";
7
+ //#region src/secrets/secretsStores.ts
8
+ /**
9
+ * The **account-level** view of Cloudflare Secrets Store: which stores exist, and nothing else.
10
+ *
11
+ * Its sibling {@link CloudflareSecretsStoreManager} is addressed *by store id* — it demands one in its
12
+ * constructor, because every operation it has is inside a store. That is the right shape for putting and
13
+ * deleting secrets and the wrong shape for the one question asked before any store id is known: which
14
+ * store does this account have?
15
+ *
16
+ * **One store per account**, which is what makes the answer usable at all. `pithy add secrets` resolves
17
+ * the id here, once, at provisioning time, and writes it into `<config>/cloudflare.json` — so every later
18
+ * run is a plain file read. Discovery on every invocation was considered and rejected (#182): a CLI that
19
+ * cannot run offline because it has to ask Cloudflare where its own store is has lost more than the key
20
+ * was costing.
21
+ */
22
+ /**
23
+ * One store in the account, decoded from the CF list response.
24
+ *
25
+ * Metadata only — a store holds secrets whose plaintext the REST API never returns. The `created` and
26
+ * `modified` ISO strings decode through `JsonDate` at the wire boundary, as every other response here
27
+ * does, so nothing downstream parses a date string a second time.
28
+ */
29
+ const CfSecretsStore = z.object({
30
+ id: z.string().describe("The CF-assigned store identifier — the value that becomes SECRETS_STORE_ID."),
31
+ name: z.string().describe("The store's display name, for naming it back to an operator who has two."),
32
+ created: JsonDate.describe("When the store was created (ISO string on the wire, Date in app)."),
33
+ modified: JsonDate.describe("When the store was last modified (ISO string on the wire, Date in app).")
34
+ }).describe("A single Cloudflare Secrets Store's metadata. Never a secret, and never a secret's value.");
35
+ /** Account-level Secrets Store operations: listing the account's stores. Addressed by account, not store. */
36
+ var CloudflareSecretsStoresManager = class extends CloudflareManager {
37
+ getServiceType() {
38
+ return "Cloudflare Secrets Store (account)";
39
+ }
40
+ /** Prove access by listing the account's stores. Never throws. */
41
+ async validateServiceAccess() {
42
+ try {
43
+ await this.listStores();
44
+ return true;
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+ /**
50
+ * Every Secrets Store in the account. The SDK auto-paginates via `for await`, so a caller receives
51
+ * the full set in one array, and each entry is Zod-validated at the wire boundary.
52
+ *
53
+ * **Returns the list, never a choice.** Cloudflare permits one store per account, so the ordinary
54
+ * answer has exactly one element — but "ordinarily one" is not "always one", and picking the first of
55
+ * two would be guessing which store holds an adopter's production secrets. The caller decides, and the
56
+ * only correct decision for two is to refuse and name them.
57
+ */
58
+ async listStores() {
59
+ return cloudflareRequest("list secrets stores", async () => {
60
+ const out = [];
61
+ for await (const store of this.getClient().secretsStore.stores.list({ account_id: this.accountId })) out.push(decodeResponse(CfSecretsStore, store, "Secrets Store list entry"));
62
+ return out;
63
+ });
64
+ }
65
+ };
66
+ //#endregion
67
+ export { CfSecretsStore, CloudflareSecretsStoresManager };
@@ -0,0 +1,41 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { z } from "zod";
4
+ import type { IntegrationCreds } from "./harness";
5
+ /** One matcher on a routing rule: which mail the rule claims. */
6
+ export declare const EmailRoutingMatcher: z.ZodObject<{
7
+ type: z.ZodString;
8
+ field: z.ZodOptional<z.ZodString>;
9
+ value: z.ZodOptional<z.ZodString>;
10
+ }, z.core.$strip>;
11
+ /** One action on a routing rule: what happens to the mail it claimed. */
12
+ export declare const EmailRoutingAction: z.ZodObject<{
13
+ type: z.ZodString;
14
+ value: z.ZodDefault<z.ZodArray<z.ZodString>>;
15
+ }, z.core.$strip>;
16
+ /** One Email Routing rule, as the zone stores it. */
17
+ export declare const EmailRoutingRule: z.ZodObject<{
18
+ id: z.ZodString;
19
+ name: z.ZodDefault<z.ZodString>;
20
+ enabled: z.ZodDefault<z.ZodBoolean>;
21
+ priority: z.ZodDefault<z.ZodNumber>;
22
+ matchers: z.ZodDefault<z.ZodArray<z.ZodObject<{
23
+ type: z.ZodString;
24
+ field: z.ZodOptional<z.ZodString>;
25
+ value: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>>;
27
+ actions: z.ZodDefault<z.ZodArray<z.ZodObject<{
28
+ type: z.ZodString;
29
+ value: z.ZodDefault<z.ZodArray<z.ZodString>>;
30
+ }, z.core.$strip>>>;
31
+ }, z.core.$strip>;
32
+ /** One Email Routing rule, as the zone stores it. */
33
+ export type EmailRoutingRule = z.output<typeof EmailRoutingRule>;
34
+ /** Every Email Routing rule on a zone, paged to exhaustion. */
35
+ export declare function listEmailRoutingRules(creds: IntegrationCreds, zoneId: string): Promise<EmailRoutingRule[]>;
36
+ /**
37
+ * The rules somebody named — the zone's own rules, without the unnamed catch-all Cloudflare puts on
38
+ * every zone configured or not.
39
+ */
40
+ export declare function namedRules(rules: readonly EmailRoutingRule[]): EmailRoutingRule[];
41
+ //# sourceMappingURL=emailRoutingRules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailRoutingRules.d.ts","sourceRoot":"","sources":["../../src/test-utils/emailRoutingRules.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAkClD,iEAAiE;AACjE,eAAO,MAAM,mBAAmB;;;;iBAMoB,CAAC;AAErD,yEAAyE;AACzE,eAAO,MAAM,kBAAkB;;;iBAKoB,CAAC;AAEpD,qDAAqD;AACrD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;iBAYsC,CAAC;AAEpE,qDAAqD;AACrD,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAiBjE,+DAA+D;AAC/D,wBAAsB,qBAAqB,CAAC,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAuBhH;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAEjF"}
@@ -0,0 +1,91 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+ import { CloudflareRequestError } from "../client/errors.js";
4
+ import { z } from "zod";
5
+ //#region src/test-utils/emailRoutingRules.ts
6
+ /**
7
+ * A zone's Email Routing rules, read whole.
8
+ *
9
+ * `CloudflareEmailRoutingManager` reads the same endpoint and keeps only `id` and `name`, because
10
+ * matching by name and deleting by id is all provisioning needs. Two callers here need more.
11
+ *
12
+ * **A live test has to see what Cloudflare stored, not what we sent.** #47's open question is whether
13
+ * the `matchers`/`actions` shape `ensureWorkerRoute` posts still matches the current API — and a test
14
+ * that asserts on its own request argument answers nothing. So the rule comes back in full and the
15
+ * assertions are on the stored copy.
16
+ *
17
+ * **The reaper needs names it can hand back to `removeWorkerRoute`.** A run that dies between creating
18
+ * a rule and tearing it down leaves live mail routing to a Worker that is about to be deleted, which is
19
+ * the one piece of debris in this repo that changes what happens to somebody's mail rather than costing
20
+ * a few cents.
21
+ *
22
+ * ## Two documented traps, both live here so nobody has to rediscover them
23
+ *
24
+ * **Every zone has a catch-all rule whether or not anything is configured** — no name, priority
25
+ * `2147483647`, `matchers: [{ type: "all" }]`. A rule count of 1 means nothing. {@link namedRules}
26
+ * drops it, so a caller counting *our* rules counts ours.
27
+ *
28
+ * **`result_info` on this endpoint carries no `total_pages`** on a single-page zone. The paging loop
29
+ * therefore stops on a short page, exactly as the manager's does.
30
+ */
31
+ /** Rules per page. Cloudflare's maximum, so the common zone is answered in one request. */
32
+ const RULES_PER_PAGE = 50;
33
+ /** A hard stop on the page loop, so a malformed `result_info` cannot spin forever. */
34
+ const MAX_RULE_PAGES = 100;
35
+ /** One matcher on a routing rule: which mail the rule claims. */
36
+ const EmailRoutingMatcher = z.object({
37
+ type: z.string().describe("`literal` for one address, `all` for the zone's catch-all."),
38
+ field: z.string().optional().describe("The header the literal matches on — `to` for every rule Pithy writes."),
39
+ value: z.string().optional().describe("The address a `literal` matcher claims. Absent on a catch-all.")
40
+ }).describe("One matcher on an Email Routing rule.");
41
+ /** One action on a routing rule: what happens to the mail it claimed. */
42
+ const EmailRoutingAction = z.object({
43
+ type: z.string().describe("`worker` for a Worker delivery, `drop` for the default catch-all, `forward` otherwise."),
44
+ value: z.array(z.string()).default([]).describe("The action's targets — a single Worker script name for `worker`.")
45
+ }).describe("One action on an Email Routing rule.");
46
+ /** One Email Routing rule, as the zone stores it. */
47
+ const EmailRoutingRule = z.object({
48
+ id: z.string().describe("Cloudflare's id for the rule — what a delete addresses."),
49
+ name: z.string().default("").describe("The rule's name. Empty on the default catch-all every zone carries."),
50
+ enabled: z.boolean().default(false).describe("Whether the rule is live. The catch-all's flag doubles as the zone's."),
51
+ priority: z.number().default(0).describe("Match order, lowest first. The catch-all sits at 2147483647."),
52
+ matchers: z.array(EmailRoutingMatcher).default([]).describe("Which mail this rule claims."),
53
+ actions: z.array(EmailRoutingAction).default([]).describe("What happens to the mail this rule claimed.")
54
+ }).describe("One Email Routing rule on a zone, read back in full.");
55
+ /** The list envelope, validated because a response read is a boundary. */
56
+ const RuleListEnvelope = z.object({
57
+ success: z.boolean().describe("Cloudflare's own verdict on the request."),
58
+ result: z.array(EmailRoutingRule).nullable().default([]).describe("This page of rules. Null on a failed call."),
59
+ result_info: z.object({
60
+ page: z.number().optional().describe("The page just returned."),
61
+ total_pages: z.number().optional().describe("Total pages, when Cloudflare states one. Often absent.")
62
+ }).nullish().describe("The paging block, when the response carries one.")
63
+ }).describe("A page of a zone's Email Routing rules.");
64
+ /** Every Email Routing rule on a zone, paged to exhaustion. */
65
+ async function listEmailRoutingRules(creds, zoneId) {
66
+ const rules = [];
67
+ for (let page = 1; page <= MAX_RULE_PAGES; page += 1) {
68
+ const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/email/routing/rules?page=${page}&per_page=${RULES_PER_PAGE}`;
69
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${creds.apiToken}` } });
70
+ if (!response.ok) throw new CloudflareRequestError({
71
+ message: "Could not read the zone's Email Routing rules.",
72
+ action: "Check the token carries Email Routing Rules: Read on this zone.",
73
+ detail: `Email Routing rule list returned ${response.status}.`
74
+ });
75
+ const envelope = RuleListEnvelope.parse(await response.json());
76
+ const batch = envelope.result ?? [];
77
+ rules.push(...batch);
78
+ const totalPages = envelope.result_info?.total_pages;
79
+ if (totalPages !== void 0 ? page >= totalPages : batch.length < RULES_PER_PAGE) break;
80
+ }
81
+ return rules;
82
+ }
83
+ /**
84
+ * The rules somebody named — the zone's own rules, without the unnamed catch-all Cloudflare puts on
85
+ * every zone configured or not.
86
+ */
87
+ function namedRules(rules) {
88
+ return rules.filter((rule) => rule.name !== "");
89
+ }
90
+ //#endregion
91
+ export { EmailRoutingAction, EmailRoutingMatcher, EmailRoutingRule, listEmailRoutingRules, namedRules };