@pithy-sh/cloudflare 0.1.1 → 0.1.3

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 +99 -0
  2. package/dist/ai/aiManager.d.ts.map +1 -0
  3. package/dist/ai/aiManager.js +148 -0
  4. package/dist/ai/vectorizeManager.d.ts +67 -0
  5. package/dist/ai/vectorizeManager.d.ts.map +1 -0
  6. package/dist/ai/vectorizeManager.js +101 -0
  7. package/dist/ai/vectorizeProvisioner.d.ts +123 -0
  8. package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
  9. package/dist/ai/vectorizeProvisioner.js +195 -0
  10. package/dist/client/accounts.d.ts +48 -0
  11. package/dist/client/accounts.d.ts.map +1 -0
  12. package/dist/client/accounts.js +55 -0
  13. package/dist/client/clients.d.ts +122 -0
  14. package/dist/client/clients.d.ts.map +1 -0
  15. package/dist/client/clients.js +216 -0
  16. package/dist/client/errors.d.ts +73 -0
  17. package/dist/client/errors.d.ts.map +1 -0
  18. package/dist/client/errors.js +98 -0
  19. package/dist/client/manager.d.ts +45 -0
  20. package/dist/client/manager.d.ts.map +1 -0
  21. package/dist/client/manager.js +51 -0
  22. package/dist/d1/d1Manager.d.ts +74 -0
  23. package/dist/d1/d1Manager.d.ts.map +1 -0
  24. package/dist/d1/d1Manager.js +128 -0
  25. package/dist/d1/d1PreparedStatement.d.ts +24 -0
  26. package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
  27. package/dist/d1/d1PreparedStatement.js +91 -0
  28. package/dist/d1/d1Provisioner.d.ts +28 -0
  29. package/dist/d1/d1Provisioner.d.ts.map +1 -0
  30. package/dist/d1/d1Provisioner.js +67 -0
  31. package/dist/email/emailRoutingManager.d.ts +36 -0
  32. package/dist/email/emailRoutingManager.d.ts.map +1 -0
  33. package/dist/email/emailRoutingManager.js +124 -0
  34. package/dist/email/emailSendManager.d.ts +45 -0
  35. package/dist/email/emailSendManager.d.ts.map +1 -0
  36. package/dist/email/emailSendManager.js +52 -0
  37. package/dist/env/devVars.d.ts +65 -0
  38. package/dist/env/devVars.d.ts.map +1 -0
  39. package/dist/env/devVars.js +84 -0
  40. package/dist/hostnames/customHostnamesManager.d.ts +51 -0
  41. package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
  42. package/dist/hostnames/customHostnamesManager.js +84 -0
  43. package/dist/kv/kvManager.d.ts +76 -0
  44. package/dist/kv/kvManager.d.ts.map +1 -0
  45. package/dist/kv/kvManager.js +135 -0
  46. package/dist/kv/kvProvisioner.d.ts +28 -0
  47. package/dist/kv/kvProvisioner.d.ts.map +1 -0
  48. package/dist/kv/kvProvisioner.js +71 -0
  49. package/dist/media/assetSeeder.d.ts +31 -0
  50. package/dist/media/assetSeeder.d.ts.map +1 -0
  51. package/dist/media/assetSeeder.js +70 -0
  52. package/dist/media/imageManager.d.ts +34 -0
  53. package/dist/media/imageManager.d.ts.map +1 -0
  54. package/dist/media/imageManager.js +103 -0
  55. package/dist/media/ownership.d.ts +40 -0
  56. package/dist/media/ownership.d.ts.map +1 -0
  57. package/dist/media/ownership.js +46 -0
  58. package/dist/media/streamManager.d.ts +83 -0
  59. package/dist/media/streamManager.d.ts.map +1 -0
  60. package/dist/media/streamManager.js +145 -0
  61. package/dist/queue/queueManager.d.ts +84 -0
  62. package/dist/queue/queueManager.d.ts.map +1 -0
  63. package/dist/queue/queueManager.js +114 -0
  64. package/dist/r2/r2Credentials.d.ts +12 -0
  65. package/dist/r2/r2Credentials.d.ts.map +1 -0
  66. package/dist/r2/r2Credentials.js +13 -0
  67. package/dist/r2/r2Manager.d.ts +226 -0
  68. package/dist/r2/r2Manager.d.ts.map +1 -0
  69. package/dist/r2/r2Manager.js +405 -0
  70. package/dist/r2/r2Provisioner.d.ts +39 -0
  71. package/dist/r2/r2Provisioner.d.ts.map +1 -0
  72. package/dist/r2/r2Provisioner.js +86 -0
  73. package/dist/secrets/secretsStoreManager.d.ts +82 -0
  74. package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
  75. package/dist/secrets/secretsStoreManager.js +147 -0
  76. package/dist/secrets/secretsStores.d.ts +47 -0
  77. package/dist/secrets/secretsStores.d.ts.map +1 -0
  78. package/dist/secrets/secretsStores.js +65 -0
  79. package/dist/test-utils/emailRoutingRules.d.ts +39 -0
  80. package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
  81. package/dist/test-utils/emailRoutingRules.js +89 -0
  82. package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
  83. package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
  84. package/dist/test-utils/fixtureReportSetup.js +30 -0
  85. package/dist/test-utils/fixtures.d.ts +224 -0
  86. package/dist/test-utils/fixtures.d.ts.map +1 -0
  87. package/dist/test-utils/fixtures.js +277 -0
  88. package/dist/test-utils/harness.d.ts +192 -0
  89. package/dist/test-utils/harness.d.ts.map +1 -0
  90. package/dist/test-utils/harness.js +351 -0
  91. package/dist/test-utils/inboundRecorder.d.ts +91 -0
  92. package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
  93. package/dist/test-utils/inboundRecorder.js +141 -0
  94. package/dist/test-utils/integrationSetup.d.ts +22 -0
  95. package/dist/test-utils/integrationSetup.d.ts.map +1 -0
  96. package/dist/test-utils/integrationSetup.js +37 -0
  97. package/dist/test-utils/reap.d.ts +112 -0
  98. package/dist/test-utils/reap.d.ts.map +1 -0
  99. package/dist/test-utils/reap.js +185 -0
  100. package/dist/tokens/accountTokensManager.d.ts +157 -0
  101. package/dist/tokens/accountTokensManager.d.ts.map +1 -0
  102. package/dist/tokens/accountTokensManager.js +280 -0
  103. package/dist/tokens/permissions.d.ts +36 -0
  104. package/dist/tokens/permissions.d.ts.map +1 -0
  105. package/dist/tokens/permissions.js +50 -0
  106. package/dist/tokens/profiles.d.ts +102 -0
  107. package/dist/tokens/profiles.d.ts.map +1 -0
  108. package/dist/tokens/profiles.js +137 -0
  109. package/dist/turnstile/turnstileManager.d.ts +75 -0
  110. package/dist/turnstile/turnstileManager.d.ts.map +1 -0
  111. package/dist/turnstile/turnstileManager.js +138 -0
  112. package/dist/user/userManager.d.ts +36 -0
  113. package/dist/user/userManager.d.ts.map +1 -0
  114. package/dist/user/userManager.js +58 -0
  115. package/dist/workers/buildsManager.d.ts +116 -0
  116. package/dist/workers/buildsManager.d.ts.map +1 -0
  117. package/dist/workers/buildsManager.js +237 -0
  118. package/dist/workers/buildsTypes.d.ts +102 -0
  119. package/dist/workers/buildsTypes.d.ts.map +1 -0
  120. package/dist/workers/buildsTypes.js +87 -0
  121. package/dist/workers/workersBuildEvent.d.ts +64 -0
  122. package/dist/workers/workersBuildEvent.d.ts.map +1 -0
  123. package/dist/workers/workersBuildEvent.js +47 -0
  124. package/dist/workers/workersManager.d.ts +136 -0
  125. package/dist/workers/workersManager.d.ts.map +1 -0
  126. package/dist/workers/workersManager.js +312 -0
  127. package/dist/workers/workersProvisioner.d.ts +80 -0
  128. package/dist/workers/workersProvisioner.d.ts.map +1 -0
  129. package/dist/workers/workersProvisioner.js +106 -0
  130. package/dist/workflows/stepFailure.d.ts +180 -0
  131. package/dist/workflows/stepFailure.d.ts.map +1 -0
  132. package/dist/workflows/stepFailure.js +227 -0
  133. package/dist/workflows/workflowsClient.d.ts +87 -0
  134. package/dist/workflows/workflowsClient.d.ts.map +1 -0
  135. package/dist/workflows/workflowsClient.js +156 -0
  136. package/dist/zones/zonesManager.d.ts +50 -0
  137. package/dist/zones/zonesManager.d.ts.map +1 -0
  138. package/dist/zones/zonesManager.js +74 -0
  139. package/package.json +18 -9
@@ -0,0 +1,67 @@
1
+ import { cloudflareRequest, decodeResponse } from "../client/errors.js";
2
+ import { CloudflareManager } from "../client/manager.js";
3
+ import { z } from "zod";
4
+ //#region src/d1/d1Provisioner.ts
5
+ /** A D1 database's identity, decoded from the create response. */
6
+ const D1DatabaseInfo = z.object({
7
+ uuid: z.string().describe("The CF-assigned database id (a uuid) used to address the database."),
8
+ name: z.string().describe("The database name.")
9
+ }).describe("A Cloudflare D1 database's identity, as returned by the create endpoint.");
10
+ /**
11
+ * Account-level D1 control plane: **create and delete databases**. Customers use this to stand up
12
+ * and tear down per-environment databases (e.g. ephemeral staging), and `pithy add secrets` uses it
13
+ * to provision each environment's secrets D1. Addressed by account — unlike {@link CloudflareD1Manager},
14
+ * which targets one database id for queries.
15
+ */
16
+ var CloudflareD1Provisioner = class extends CloudflareManager {
17
+ getServiceType() {
18
+ return "Cloudflare D1 (control plane)";
19
+ }
20
+ /** Prove access by listing databases — a read, never a destructive create/delete. Never throws. */
21
+ async validateServiceAccess() {
22
+ try {
23
+ await this.getClient().d1.database.list({ account_id: this.accountId });
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+ /** Every D1 database in the account — for prefix-scan reconcile teardown (`pithy feature destroy`). */
30
+ async listDatabases() {
31
+ return cloudflareRequest("list D1 databases", async () => {
32
+ const databases = [];
33
+ for await (const db of this.getClient().d1.database.list({ account_id: this.accountId })) {
34
+ const parsed = D1DatabaseInfo.safeParse(db);
35
+ if (parsed.success) databases.push(parsed.data);
36
+ }
37
+ return databases;
38
+ });
39
+ }
40
+ /** Find a database by exact name in the account, or `null` — for idempotent provisioning. */
41
+ async findDatabaseByName(name) {
42
+ return cloudflareRequest(`find D1 database ${name}`, async () => {
43
+ for await (const db of this.getClient().d1.database.list({
44
+ account_id: this.accountId,
45
+ name
46
+ })) {
47
+ const parsed = D1DatabaseInfo.safeParse(db);
48
+ if (parsed.success && parsed.data.name === name) return parsed.data;
49
+ }
50
+ return null;
51
+ });
52
+ }
53
+ /** Create a D1 database by name; returns its id and name. */
54
+ async createDatabase(name) {
55
+ const response = await cloudflareRequest(`create D1 database ${name}`, () => this.getClient().d1.database.create({
56
+ account_id: this.accountId,
57
+ name
58
+ }));
59
+ return decodeResponse(D1DatabaseInfo, response, "D1 database create");
60
+ }
61
+ /** Delete a D1 database by id. */
62
+ async deleteDatabase(databaseId) {
63
+ await cloudflareRequest(`delete D1 database ${databaseId}`, () => this.getClient().d1.database.delete(databaseId, { account_id: this.accountId }));
64
+ }
65
+ };
66
+ //#endregion
67
+ export { CloudflareD1Provisioner, D1DatabaseInfo };
@@ -0,0 +1,36 @@
1
+ import { CloudflareManager } from "../client/manager";
2
+ export declare class CloudflareEmailRoutingManager extends CloudflareManager {
3
+ #private;
4
+ getServiceType(): string;
5
+ /** Prove reach by listing a zone's routing rules; never throws. (Account-level reach is a weak proxy here.) */
6
+ validateServiceAccess(): Promise<boolean>;
7
+ /**
8
+ * Ensure a routing rule named `ruleName` exists on `zoneId`, delivering mail addressed to `address`
9
+ * to the Worker `workerName`. Idempotent: if a rule with that name already exists it is left as-is.
10
+ * Assumes Email Routing is already enabled on the zone.
11
+ */
12
+ ensureWorkerRoute(options: {
13
+ zoneId: string;
14
+ address: string;
15
+ workerName: string;
16
+ ruleName: string;
17
+ }): Promise<{
18
+ created: boolean;
19
+ }>;
20
+ /**
21
+ * Remove the rule named `ruleName` from `zoneId`, so mail stops being delivered to the Worker behind it.
22
+ * Idempotent: a zone carrying no such rule is a no-op, which is what lets a teardown be re-run.
23
+ *
24
+ * **Matched on the rule name, never on the address.** The name is the key `ensureWorkerRoute` created
25
+ * under, so the two agree on which rule belongs to which capability even after somebody edited the
26
+ * address in the dashboard — and a teardown that matched on the address would happily delete a rule an
27
+ * operator wrote by hand for the same mailbox.
28
+ */
29
+ removeWorkerRoute(options: {
30
+ zoneId: string;
31
+ ruleName: string;
32
+ }): Promise<{
33
+ removed: boolean;
34
+ }>;
35
+ }
36
+ //# sourceMappingURL=emailRoutingManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailRoutingManager.d.ts","sourceRoot":"","sources":["../../src/email/emailRoutingManager.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AA2CtD,qBAAa,6BAA8B,SAAQ,iBAAiB;;IAClE,cAAc,IAAI,MAAM,CAEvB;IAED,+GAA+G;IACzG,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE9C;IAED;;;;OAIG;IACG,iBAAiB,CAAC,OAAO,EAAE;QAC/B,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAehC;IAED;;;;;;;;OAQG;IACG,iBAAiB,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAQpG;CAwCF"}
@@ -0,0 +1,124 @@
1
+ import { CloudflareInvalidResponseError, cloudflareRequest } from "../client/errors.js";
2
+ import { CloudflareManager } from "../client/manager.js";
3
+ import { z } from "zod";
4
+ //#region src/email/emailRoutingManager.ts
5
+ /**
6
+ * Cloudflare Email Routing rules — the inbound side. Used by email provisioning to point a domain's
7
+ * bounce/complaint mail at the app worker that hosts the `email()` handler. Email Routing is
8
+ * **zone-scoped** (a rule lives on a zone, and routing must already be enabled on that zone — enabling
9
+ * it sets the zone's MX, an operator action we never take automatically, so the apex MX stays put).
10
+ *
11
+ * Rule *creation and deletion* go through the typed SDK. The lookup both of them turn on cannot:
12
+ * `emailRouting.rules` exposes only create/update/delete/get, and `get` addresses a rule by CF id — we
13
+ * match by name, which needs a listing the SDK has no method for. That one read stays on the documented
14
+ * raw-`fetch` escape hatch; drop it the moment a `rules.list` lands.
15
+ */
16
+ /** A routing rule's stored shape — only the fields we match on for idempotency. */
17
+ const RuleEnvelope = z.object({
18
+ success: z.boolean(),
19
+ result: z.array(z.object({
20
+ id: z.string().optional(),
21
+ name: z.string().optional()
22
+ })).nullable().default([]),
23
+ /**
24
+ * The paging block. Present on every list response, and read rather than ignored because both
25
+ * callers ask a question a truncated answer gets **wrong in the dangerous direction**: an unseen
26
+ * rule reads as "no such rule", which makes `ensureWorkerRoute` create a duplicate on every run and
27
+ * `removeWorkerRoute` report a removal while mail is still being delivered.
28
+ */
29
+ result_info: z.object({
30
+ page: z.number().optional(),
31
+ per_page: z.number().optional(),
32
+ total_count: z.number().optional(),
33
+ total_pages: z.number().optional()
34
+ }).nullish()
35
+ });
36
+ /** Rules per page. Cloudflare's maximum, so the common zone is answered in one request. */
37
+ const RULES_PER_PAGE = 50;
38
+ /** A hard stop on the page loop, so a malformed `result_info` cannot spin forever. */
39
+ const MAX_RULE_PAGES = 100;
40
+ var CloudflareEmailRoutingManager = class extends CloudflareManager {
41
+ getServiceType() {
42
+ return "Email Routing";
43
+ }
44
+ /** Prove reach by listing a zone's routing rules; never throws. (Account-level reach is a weak proxy here.) */
45
+ async validateServiceAccess() {
46
+ return true;
47
+ }
48
+ /**
49
+ * Ensure a routing rule named `ruleName` exists on `zoneId`, delivering mail addressed to `address`
50
+ * to the Worker `workerName`. Idempotent: if a rule with that name already exists it is left as-is.
51
+ * Assumes Email Routing is already enabled on the zone.
52
+ */
53
+ async ensureWorkerRoute(options) {
54
+ const { zoneId, address, workerName, ruleName } = options;
55
+ return cloudflareRequest("Email Routing ensure worker rule", async () => {
56
+ if ((await this.#listRules(zoneId)).some((rule) => rule.name === ruleName)) return { created: false };
57
+ await this.getClient().emailRouting.rules.create({
58
+ zone_id: zoneId,
59
+ name: ruleName,
60
+ enabled: true,
61
+ matchers: [{
62
+ type: "literal",
63
+ field: "to",
64
+ value: address
65
+ }],
66
+ actions: [{
67
+ type: "worker",
68
+ value: [workerName]
69
+ }]
70
+ });
71
+ return { created: true };
72
+ });
73
+ }
74
+ /**
75
+ * Remove the rule named `ruleName` from `zoneId`, so mail stops being delivered to the Worker behind it.
76
+ * Idempotent: a zone carrying no such rule is a no-op, which is what lets a teardown be re-run.
77
+ *
78
+ * **Matched on the rule name, never on the address.** The name is the key `ensureWorkerRoute` created
79
+ * under, so the two agree on which rule belongs to which capability even after somebody edited the
80
+ * address in the dashboard — and a teardown that matched on the address would happily delete a rule an
81
+ * operator wrote by hand for the same mailbox.
82
+ */
83
+ async removeWorkerRoute(options) {
84
+ const { zoneId, ruleName } = options;
85
+ return cloudflareRequest("Email Routing remove worker rule", async () => {
86
+ const rule = (await this.#listRules(zoneId)).find((candidate) => candidate.name === ruleName);
87
+ if (!rule?.id) return { removed: false };
88
+ await this.getClient().emailRouting.rules.delete(rule.id, { zone_id: zoneId });
89
+ return { removed: true };
90
+ });
91
+ }
92
+ /**
93
+ * **Every** routing rule on a zone, by the raw-`fetch` escape hatch this file's header documents —
94
+ * the SDK has no listing, and both matching by name and deleting by id need one.
95
+ *
96
+ * Paged to exhaustion rather than reading the first page. A zone with more rules than fit one page
97
+ * is unusual but entirely legal, and a partial read is not a smaller answer here — it is a *wrong*
98
+ * one, because both callers treat "not in the list" as "does not exist". Provisioning would then
99
+ * create a second rule for the same address on every run, and a teardown would report mail stopped
100
+ * while it kept arriving.
101
+ *
102
+ * A failed read **throws**, for the same reason: silently degrading to an empty list would produce
103
+ * exactly the two failures above.
104
+ */
105
+ async #listRules(zoneId) {
106
+ const rules = [];
107
+ for (let page = 1; page <= MAX_RULE_PAGES; page += 1) {
108
+ const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/email/routing/rules?page=${page}&per_page=${RULES_PER_PAGE}`;
109
+ const listed = await fetch(url, { headers: { Authorization: `Bearer ${this.getApiToken()}` } });
110
+ if (!listed.ok) throw new CloudflareInvalidResponseError({
111
+ message: "Could not read the zone's Email Routing rules.",
112
+ detail: `Email Routing rule list returned ${listed.status}: ${await listed.text()}`
113
+ });
114
+ const envelope = RuleEnvelope.parse(await listed.json());
115
+ const batch = envelope.result ?? [];
116
+ rules.push(...batch);
117
+ const totalPages = envelope.result_info?.total_pages;
118
+ if (totalPages !== void 0 ? page >= totalPages : batch.length < RULES_PER_PAGE) break;
119
+ }
120
+ return rules;
121
+ }
122
+ };
123
+ //#endregion
124
+ export { CloudflareEmailRoutingManager };
@@ -0,0 +1,45 @@
1
+ import { CloudflareManager } from "../client/manager";
2
+ /**
3
+ * Out-of-Worker sends through the Cloudflare Email Service REST API — the control-plane counterpart to
4
+ * the in-Worker `send_email` binding. Used by the CLI's `pithy email test` to render and deliver one
5
+ * template through a project's configuration without deploying. Inside a Worker, always prefer the
6
+ * binding (no token, principle-1 aligned).
7
+ */
8
+ /** A message to send over REST. `from` mirrors the binding's `{ email, name }`; the wire uses `address`. */
9
+ export interface EmailSendInput {
10
+ to: string | string[];
11
+ from: {
12
+ email: string;
13
+ name?: string;
14
+ };
15
+ subject: string;
16
+ html: string;
17
+ text: string;
18
+ replyTo?: string;
19
+ }
20
+ /** What the send returned: the assigned message id and any synchronous delivery breakdown. */
21
+ export interface EmailSendResult {
22
+ messageId?: string;
23
+ delivered: string[];
24
+ queued: string[];
25
+ permanentBounces: string[];
26
+ }
27
+ export declare class CloudflareEmailSendManager extends CloudflareManager {
28
+ getServiceType(): string;
29
+ /**
30
+ * Prove reach by reading the account's sending limits; never throws (returns false on any failure).
31
+ * `/email/sending/limits` has no typed SDK method — only `send` and `subdomains` do — so this stays
32
+ * on the documented raw-`fetch` escape hatch. Probing `subdomains.list` instead would change what
33
+ * the check proves: a token scoped to send but not to read subdomains would start reporting false.
34
+ */
35
+ validateServiceAccess(): Promise<boolean>;
36
+ /**
37
+ * Send one message through the Email Sending API. Throws `cloudflare/request_failed` on any API
38
+ * error — the SDK unwraps the envelope and raises on `success: false`, so a resolved call is a send.
39
+ *
40
+ * `from` is sent as the object form only when a display name exists; the SDK's object form requires
41
+ * `name`, and the bare-string form is the wire's own representation of "address, no display name".
42
+ */
43
+ send(message: EmailSendInput): Promise<EmailSendResult>;
44
+ }
45
+ //# sourceMappingURL=emailSendManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emailSendManager.d.ts","sourceRoot":"","sources":["../../src/email/emailSendManager.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtD;;;;;GAKG;AAEH,4GAA4G;AAC5G,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,8FAA8F;AAC9F,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,qBAAa,0BAA2B,SAAQ,iBAAiB;IAC/D,cAAc,IAAI,MAAM,CAEvB;IAED;;;;;OAKG;IACG,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAU9C;IAED;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAkB5D;CACF"}
@@ -0,0 +1,52 @@
1
+ import { cloudflareRequest } from "../client/errors.js";
2
+ import { CloudflareManager } from "../client/manager.js";
3
+ //#region src/email/emailSendManager.ts
4
+ var CloudflareEmailSendManager = class extends CloudflareManager {
5
+ getServiceType() {
6
+ return "Email Sending";
7
+ }
8
+ /**
9
+ * Prove reach by reading the account's sending limits; never throws (returns false on any failure).
10
+ * `/email/sending/limits` has no typed SDK method — only `send` and `subdomains` do — so this stays
11
+ * on the documented raw-`fetch` escape hatch. Probing `subdomains.list` instead would change what
12
+ * the check proves: a token scoped to send but not to read subdomains would start reporting false.
13
+ */
14
+ async validateServiceAccess() {
15
+ try {
16
+ return (await fetch(`https://api.cloudflare.com/client/v4/accounts/${this.getAccountId()}/email/sending/limits`, { headers: { Authorization: `Bearer ${this.getApiToken()}` } })).ok;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Send one message through the Email Sending API. Throws `cloudflare/request_failed` on any API
23
+ * error — the SDK unwraps the envelope and raises on `success: false`, so a resolved call is a send.
24
+ *
25
+ * `from` is sent as the object form only when a display name exists; the SDK's object form requires
26
+ * `name`, and the bare-string form is the wire's own representation of "address, no display name".
27
+ */
28
+ async send(message) {
29
+ return cloudflareRequest("Email Service send", async () => {
30
+ const result = await this.getClient().emailSending.send({
31
+ account_id: this.getAccountId(),
32
+ to: message.to,
33
+ from: message.from.name ? {
34
+ address: message.from.email,
35
+ name: message.from.name
36
+ } : message.from.email,
37
+ subject: message.subject,
38
+ html: message.html,
39
+ text: message.text,
40
+ ...message.replyTo ? { reply_to: message.replyTo } : {}
41
+ });
42
+ return {
43
+ messageId: result.message_id,
44
+ delivered: result.delivered,
45
+ queued: result.queued,
46
+ permanentBounces: result.permanent_bounces
47
+ };
48
+ });
49
+ }
50
+ };
51
+ //#endregion
52
+ export { CloudflareEmailSendManager };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The Cloudflare credential keys pithy reads out-of-Worker (CLI, provisioning, live tests). These are
3
+ * wrangler's own env-var names plus the Secrets Store id. From this single bootstrap token
4
+ * (`CLOUDFLARE_API_TOKEN`) pithy **mints** the scoped, least-privilege tokens each use case needs —
5
+ * most notably the secrets manager's runtime token, minted at provision time and written straight into
6
+ * the Secrets Store, so no scoped token is ever supplied or kept here.
7
+ *
8
+ * **Where they are read from is not this module's answer any more (#182).** They are account-scoped, so
9
+ * they live in `<config>/cloudflare.json` — or, since #206, in `<config>/cloudflare.<name>.json` when a
10
+ * project's root config names its account. See `@pithy-sh/cli`'s `cloudflare/config`, which owns the
11
+ * file, which file, the `process.env` overlay, and the split diagnostic. The names stay here because
12
+ * both ends need them and the Worker-side package is the one both can import.
13
+ *
14
+ * **The overlay has an off switch, and it is not `PITHY_CONFIG_DIR` (#218).** Relocating the config
15
+ * directory moves the *file*; these four names are still read out of the ambient environment, which is
16
+ * how a CLI run in an empty scratch directory reached a real account off a token a shell had exported
17
+ * hours earlier. `PITHY_OFFLINE` is the word that stops it, and it lives with the overlay it governs —
18
+ * `PITHY_OFFLINE_ENV` in `@pithy-sh/cli`'s `cloudflare/config`. Anything outside the CLI that grows a
19
+ * reason to read one of these names off `process.env` inherits that obligation with it.
20
+ */
21
+ export declare const CLOUDFLARE_ENV_KEYS: readonly ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN", "SECRETS_STORE_ID", "R2_CREDENTIALS"];
22
+ /**
23
+ * Which of {@link CLOUDFLARE_ENV_KEYS} an environment actually carries a value for. The one place that
24
+ * list is turned into a check.
25
+ *
26
+ * **Non-empty, not merely present.** `vitest.shared.ts`'s `NO_ACCOUNT` pins all four keys to `""`, and
27
+ * the `process.env` overlay in `@pithy-sh/cli`'s `cloudflare/config` already reads a blank as unset. A
28
+ * predicate keying on presence would report every guarded unit project as leaking.
29
+ *
30
+ * **This module is bundled into workerd as well as run on the host, and that is why it imports nothing.**
31
+ * The repository-root `vitest.workers.setup.ts` imports this function relatively and every workers
32
+ * project loads it, so a `node:` import here breaks seventeen suites at collection. `vitest.shared.ts`
33
+ * already leans on the same property for {@link CLOUDFLARE_ENV_KEYS} and says so. Nothing gates the
34
+ * import half — `@pithy-sh/core` has `worker-safety.test.ts` and this package has no equivalent — so it
35
+ * is a constraint a reader has to be told, which is what this paragraph is. The other half is gated:
36
+ * `packages/cli/src/ci/testIsolation.test.ts` walks what every workers config imports, so this module
37
+ * is in that scan and a `process.env` read here is red (#437).
38
+ */
39
+ export declare function visibleCredentialKeys(env: Readonly<Record<string, string | undefined>>): readonly string[];
40
+ /**
41
+ * Parse a `.dev.vars` file body into a map: `KEY=value` lines, `#` comments and blanks skipped, and
42
+ * a single layer of surrounding quotes stripped. Pure — the caller owns the file read.
43
+ */
44
+ export declare function parseDevVars(content: string): Record<string, string>;
45
+ /**
46
+ * The keys that only mean anything **together, from one account**: the account id, and a token minted in
47
+ * that account. Deliberately just the pair.
48
+ *
49
+ * The other {@link CLOUDFLARE_ENV_KEYS} stay out, because each fails loudly and immediately when it comes
50
+ * from the wrong account: a store id this account does not hold 404s on the first call, and an S3 key
51
+ * that does not belong to the account in the endpoint will not sign. Only the pair can *quietly* succeed
52
+ * somewhere unintended — a live token against another account's id is a 403 or an empty listing much
53
+ * later — so only the pair is a group. That is the whole test for membership.
54
+ *
55
+ * **A whole file can be the wrong account too.** This pair is the *half-file, half-environment* case; one
56
+ * level up, a file that is entirely coherent in itself can belong to a different company than the project
57
+ * reading it, which is what a project's `cloudflare.accountName` and its `accountId` pin answer (#206).
58
+ *
59
+ * `SECRETS_STORE_ID` used to be excluded on the grounds that it is "routinely passed per environment
60
+ * while the pair sits in the file". That was never true: Cloudflare permits **one Secrets Store per
61
+ * account**, so nothing about the id is per-environment, and it now sits in `cloudflare.json` beside the
62
+ * pair for exactly that reason (#182). The conclusion was right; the stated reason was not.
63
+ */
64
+ export declare const CLOUDFLARE_CREDENTIAL_KEYS: readonly ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"];
65
+ //# sourceMappingURL=devVars.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devVars.d.ts","sourceRoot":"","sources":["../../src/env/devVars.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mBAAmB,YAC9B,uBAAuB,EACvB,sBAAsB,EACtB,kBAAkB,EAClB,gBAAgB,CACR,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,MAAM,EAAE,CAE1G;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAapE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,0BAA0B,YAAI,uBAAuB,EAAE,sBAAsB,CAAU,CAAC"}
@@ -0,0 +1,84 @@
1
+ //#region src/env/devVars.ts
2
+ /**
3
+ * The Cloudflare credential keys pithy reads out-of-Worker (CLI, provisioning, live tests). These are
4
+ * wrangler's own env-var names plus the Secrets Store id. From this single bootstrap token
5
+ * (`CLOUDFLARE_API_TOKEN`) pithy **mints** the scoped, least-privilege tokens each use case needs —
6
+ * most notably the secrets manager's runtime token, minted at provision time and written straight into
7
+ * the Secrets Store, so no scoped token is ever supplied or kept here.
8
+ *
9
+ * **Where they are read from is not this module's answer any more (#182).** They are account-scoped, so
10
+ * they live in `<config>/cloudflare.json` — or, since #206, in `<config>/cloudflare.<name>.json` when a
11
+ * project's root config names its account. See `@pithy-sh/cli`'s `cloudflare/config`, which owns the
12
+ * file, which file, the `process.env` overlay, and the split diagnostic. The names stay here because
13
+ * both ends need them and the Worker-side package is the one both can import.
14
+ *
15
+ * **The overlay has an off switch, and it is not `PITHY_CONFIG_DIR` (#218).** Relocating the config
16
+ * directory moves the *file*; these four names are still read out of the ambient environment, which is
17
+ * how a CLI run in an empty scratch directory reached a real account off a token a shell had exported
18
+ * hours earlier. `PITHY_OFFLINE` is the word that stops it, and it lives with the overlay it governs —
19
+ * `PITHY_OFFLINE_ENV` in `@pithy-sh/cli`'s `cloudflare/config`. Anything outside the CLI that grows a
20
+ * reason to read one of these names off `process.env` inherits that obligation with it.
21
+ */
22
+ const CLOUDFLARE_ENV_KEYS = [
23
+ "CLOUDFLARE_ACCOUNT_ID",
24
+ "CLOUDFLARE_API_TOKEN",
25
+ "SECRETS_STORE_ID",
26
+ "R2_CREDENTIALS"
27
+ ];
28
+ /**
29
+ * Which of {@link CLOUDFLARE_ENV_KEYS} an environment actually carries a value for. The one place that
30
+ * list is turned into a check.
31
+ *
32
+ * **Non-empty, not merely present.** `vitest.shared.ts`'s `NO_ACCOUNT` pins all four keys to `""`, and
33
+ * the `process.env` overlay in `@pithy-sh/cli`'s `cloudflare/config` already reads a blank as unset. A
34
+ * predicate keying on presence would report every guarded unit project as leaking.
35
+ *
36
+ * **This module is bundled into workerd as well as run on the host, and that is why it imports nothing.**
37
+ * The repository-root `vitest.workers.setup.ts` imports this function relatively and every workers
38
+ * project loads it, so a `node:` import here breaks seventeen suites at collection. `vitest.shared.ts`
39
+ * already leans on the same property for {@link CLOUDFLARE_ENV_KEYS} and says so. Nothing gates the
40
+ * import half — `@pithy-sh/core` has `worker-safety.test.ts` and this package has no equivalent — so it
41
+ * is a constraint a reader has to be told, which is what this paragraph is. The other half is gated:
42
+ * `packages/cli/src/ci/testIsolation.test.ts` walks what every workers config imports, so this module
43
+ * is in that scan and a `process.env` read here is red (#437).
44
+ */
45
+ function visibleCredentialKeys(env) {
46
+ return CLOUDFLARE_ENV_KEYS.filter((key) => (env[key] ?? "").length > 0);
47
+ }
48
+ /**
49
+ * Parse a `.dev.vars` file body into a map: `KEY=value` lines, `#` comments and blanks skipped, and
50
+ * a single layer of surrounding quotes stripped. Pure — the caller owns the file read.
51
+ */
52
+ function parseDevVars(content) {
53
+ const vars = {};
54
+ for (const line of content.split("\n")) {
55
+ const trimmed = line.trim();
56
+ if (!trimmed || trimmed.startsWith("#")) continue;
57
+ const eq = trimmed.indexOf("=");
58
+ if (eq === -1) continue;
59
+ vars[trimmed.slice(0, eq).trim()] = trimmed.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
60
+ }
61
+ return vars;
62
+ }
63
+ /**
64
+ * The keys that only mean anything **together, from one account**: the account id, and a token minted in
65
+ * that account. Deliberately just the pair.
66
+ *
67
+ * The other {@link CLOUDFLARE_ENV_KEYS} stay out, because each fails loudly and immediately when it comes
68
+ * from the wrong account: a store id this account does not hold 404s on the first call, and an S3 key
69
+ * that does not belong to the account in the endpoint will not sign. Only the pair can *quietly* succeed
70
+ * somewhere unintended — a live token against another account's id is a 403 or an empty listing much
71
+ * later — so only the pair is a group. That is the whole test for membership.
72
+ *
73
+ * **A whole file can be the wrong account too.** This pair is the *half-file, half-environment* case; one
74
+ * level up, a file that is entirely coherent in itself can belong to a different company than the project
75
+ * reading it, which is what a project's `cloudflare.accountName` and its `accountId` pin answer (#206).
76
+ *
77
+ * `SECRETS_STORE_ID` used to be excluded on the grounds that it is "routinely passed per environment
78
+ * while the pair sits in the file". That was never true: Cloudflare permits **one Secrets Store per
79
+ * account**, so nothing about the id is per-environment, and it now sits in `cloudflare.json` beside the
80
+ * pair for exactly that reason (#182). The conclusion was right; the stated reason was not.
81
+ */
82
+ const CLOUDFLARE_CREDENTIAL_KEYS = ["CLOUDFLARE_ACCOUNT_ID", "CLOUDFLARE_API_TOKEN"];
83
+ //#endregion
84
+ export { CLOUDFLARE_CREDENTIAL_KEYS, CLOUDFLARE_ENV_KEYS, parseDevVars, visibleCredentialKeys };
@@ -0,0 +1,51 @@
1
+ import type { CustomHostnameCreateResponse, CustomHostnameDeleteResponse, CustomHostnameGetResponse, CustomHostnameListResponse } from "cloudflare/resources/custom-hostnames/custom-hostnames";
2
+ import { CloudflareManager, type CloudflareManagerConfig } from "../client/manager";
3
+ /**
4
+ * Config for the Custom Hostnames manager: the shared client config plus the zone the hostnames
5
+ * live under. Custom hostnames are a zone-scoped resource — the REST API addresses them by
6
+ * `zone_id`, so the zone is required configuration here.
7
+ */
8
+ export interface CustomHostnamesManagerConfig extends CloudflareManagerConfig {
9
+ /** The zone id custom hostnames are created and managed under (the CF zone serving the apex). */
10
+ zoneId: string;
11
+ }
12
+ /** Options for creating a custom hostname. */
13
+ export interface CreateCustomHostnameOptions {
14
+ /**
15
+ * The minimum TLS version the issued certificate negotiates. Defaults to `1.2`. Custom hostnames
16
+ * use HTTP DCV with a DV certificate.
17
+ */
18
+ minTlsVersion?: "1.0" | "1.1" | "1.2" | "1.3";
19
+ }
20
+ /**
21
+ * Out-of-Worker Custom Hostnames access over the REST API: create, look up, and remove custom
22
+ * hostnames on a zone from a CLI/CI/provisioning context. Custom hostnames let a zone serve domains
23
+ * it does not own DNS for — a customer CNAMEs their domain to a fallback origin on the zone, and CF
24
+ * issues a certificate via HTTP domain-control validation. Addressed by zone id.
25
+ */
26
+ export declare class CloudflareCustomHostnamesManager extends CloudflareManager {
27
+ private readonly zoneId;
28
+ constructor(config: CustomHostnamesManagerConfig);
29
+ /**
30
+ * Create a custom hostname with HTTP DCV and a DV certificate. Idempotent: if a custom hostname
31
+ * already exists for this hostname, the existing record is returned instead of creating a duplicate.
32
+ */
33
+ addCustomHostname(hostname: string, options?: CreateCustomHostnameOptions): Promise<CustomHostnameCreateResponse | CustomHostnameListResponse>;
34
+ /** Look up a custom hostname by its fully qualified name. Returns null when none matches. */
35
+ getCustomHostname(hostname: string): Promise<CustomHostnameListResponse | null>;
36
+ /** Fetch a custom hostname by its id. */
37
+ getCustomHostnameById(customHostnameId: string): Promise<CustomHostnameGetResponse>;
38
+ /** List every custom hostname on the zone. */
39
+ listCustomHostnames(): Promise<CustomHostnameListResponse[]>;
40
+ /** Remove a custom hostname by its id. */
41
+ removeCustomHostname(customHostnameId: string): Promise<CustomHostnameDeleteResponse>;
42
+ /** The zone this manager targets and the account it lives in. */
43
+ getCustomHostnamesInfo(): {
44
+ zoneId: string;
45
+ accountId: string;
46
+ };
47
+ getServiceType(): string;
48
+ /** Prove access by listing custom hostnames on the zone. Never throws. */
49
+ validateServiceAccess(): Promise<boolean>;
50
+ }
51
+ //# sourceMappingURL=customHostnamesManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"customHostnamesManager.d.ts","sourceRoot":"","sources":["../../src/hostnames/customHostnamesManager.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,4BAA4B,EAC5B,4BAA4B,EAC5B,yBAAyB,EACzB,0BAA0B,EAC3B,MAAM,wDAAwD,CAAC;AAEhE,OAAO,EAAE,iBAAiB,EAAE,KAAK,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAEpF;;;;GAIG;AACH,MAAM,WAAW,4BAA6B,SAAQ,uBAAuB;IAC3E,iGAAiG;IACjG,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,8CAA8C;AAC9C,MAAM,WAAW,2BAA2B;IAC1C;;;OAGG;IACH,aAAa,CAAC,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;CAC/C;AAED;;;;;GAKG;AACH,qBAAa,gCAAiC,SAAQ,iBAAiB;IACrE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,YAAY,MAAM,EAAE,4BAA4B,EAM/C;IAED;;;OAGG;IACG,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,4BAA4B,GAAG,0BAA0B,CAAC,CAepE;IAED,6FAA6F;IACvF,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,0BAA0B,GAAG,IAAI,CAAC,CAapF;IAED,yCAAyC;IACnC,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAIxF;IAED,8CAA8C;IACxC,mBAAmB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC,CAQjE;IAED,0CAA0C;IACpC,oBAAoB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAI1F;IAED,iEAAiE;IACjE,sBAAsB,IAAI;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAE9D;IAED,cAAc,IAAI,MAAM,CAEvB;IAED,0EAA0E;IACpE,qBAAqB,IAAI,OAAO,CAAC,OAAO,CAAC,CAS9C;CACF"}
@@ -0,0 +1,84 @@
1
+ import { CloudflareNotConfiguredError, cloudflareRequest } from "../client/errors.js";
2
+ import { CloudflareManager } from "../client/manager.js";
3
+ //#region src/hostnames/customHostnamesManager.ts
4
+ /**
5
+ * Out-of-Worker Custom Hostnames access over the REST API: create, look up, and remove custom
6
+ * hostnames on a zone from a CLI/CI/provisioning context. Custom hostnames let a zone serve domains
7
+ * it does not own DNS for — a customer CNAMEs their domain to a fallback origin on the zone, and CF
8
+ * issues a certificate via HTTP domain-control validation. Addressed by zone id.
9
+ */
10
+ var CloudflareCustomHostnamesManager = class extends CloudflareManager {
11
+ zoneId;
12
+ constructor(config) {
13
+ super(config);
14
+ if (!config.zoneId) throw new CloudflareNotConfiguredError({ detail: "Missing zoneId for Custom Hostnames REST access." });
15
+ this.zoneId = config.zoneId;
16
+ }
17
+ /**
18
+ * Create a custom hostname with HTTP DCV and a DV certificate. Idempotent: if a custom hostname
19
+ * already exists for this hostname, the existing record is returned instead of creating a duplicate.
20
+ */
21
+ async addCustomHostname(hostname, options) {
22
+ const existing = await this.getCustomHostname(hostname);
23
+ if (existing) return existing;
24
+ return cloudflareRequest(`add custom hostname '${hostname}'`, () => this.getClient().customHostnames.create({
25
+ zone_id: this.zoneId,
26
+ hostname,
27
+ ssl: {
28
+ method: "http",
29
+ type: "dv",
30
+ settings: { min_tls_version: options?.minTlsVersion ?? "1.2" }
31
+ }
32
+ }));
33
+ }
34
+ /** Look up a custom hostname by its fully qualified name. Returns null when none matches. */
35
+ async getCustomHostname(hostname) {
36
+ return cloudflareRequest(`get custom hostname '${hostname}'`, async () => {
37
+ for await (const candidate of this.getClient().customHostnames.list({
38
+ zone_id: this.zoneId,
39
+ hostname: { exact: hostname }
40
+ })) if (candidate.hostname === hostname) return candidate;
41
+ return null;
42
+ });
43
+ }
44
+ /** Fetch a custom hostname by its id. */
45
+ async getCustomHostnameById(customHostnameId) {
46
+ return cloudflareRequest(`get custom hostname by id '${customHostnameId}'`, () => this.getClient().customHostnames.get(customHostnameId, { zone_id: this.zoneId }));
47
+ }
48
+ /** List every custom hostname on the zone. */
49
+ async listCustomHostnames() {
50
+ return cloudflareRequest("list custom hostnames", async () => {
51
+ const hostnames = [];
52
+ for await (const candidate of this.getClient().customHostnames.list({ zone_id: this.zoneId })) hostnames.push(candidate);
53
+ return hostnames;
54
+ });
55
+ }
56
+ /** Remove a custom hostname by its id. */
57
+ async removeCustomHostname(customHostnameId) {
58
+ return cloudflareRequest(`remove custom hostname '${customHostnameId}'`, () => this.getClient().customHostnames.delete(customHostnameId, { zone_id: this.zoneId }));
59
+ }
60
+ /** The zone this manager targets and the account it lives in. */
61
+ getCustomHostnamesInfo() {
62
+ return {
63
+ zoneId: this.zoneId,
64
+ accountId: this.accountId
65
+ };
66
+ }
67
+ getServiceType() {
68
+ return "Cloudflare Custom Hostnames";
69
+ }
70
+ /** Prove access by listing custom hostnames on the zone. Never throws. */
71
+ async validateServiceAccess() {
72
+ try {
73
+ for await (const _candidate of this.getClient().customHostnames.list({
74
+ zone_id: this.zoneId,
75
+ per_page: 1
76
+ })) break;
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ };
83
+ //#endregion
84
+ export { CloudflareCustomHostnamesManager };