@pithy-sh/cloudflare 0.1.2 → 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.
- package/dist/ai/aiManager.d.ts +99 -0
- package/dist/ai/aiManager.d.ts.map +1 -0
- package/dist/ai/aiManager.js +148 -0
- package/dist/ai/vectorizeManager.d.ts +67 -0
- package/dist/ai/vectorizeManager.d.ts.map +1 -0
- package/dist/ai/vectorizeManager.js +101 -0
- package/dist/ai/vectorizeProvisioner.d.ts +123 -0
- package/dist/ai/vectorizeProvisioner.d.ts.map +1 -0
- package/dist/ai/vectorizeProvisioner.js +195 -0
- package/dist/client/accounts.d.ts +48 -0
- package/dist/client/accounts.d.ts.map +1 -0
- package/dist/client/accounts.js +55 -0
- package/dist/client/clients.d.ts +122 -0
- package/dist/client/clients.d.ts.map +1 -0
- package/dist/client/clients.js +216 -0
- package/dist/client/errors.d.ts +73 -0
- package/dist/client/errors.d.ts.map +1 -0
- package/dist/client/errors.js +98 -0
- package/dist/client/manager.d.ts +45 -0
- package/dist/client/manager.d.ts.map +1 -0
- package/dist/client/manager.js +51 -0
- package/dist/d1/d1Manager.d.ts +74 -0
- package/dist/d1/d1Manager.d.ts.map +1 -0
- package/dist/d1/d1Manager.js +128 -0
- package/dist/d1/d1PreparedStatement.d.ts +24 -0
- package/dist/d1/d1PreparedStatement.d.ts.map +1 -0
- package/dist/d1/d1PreparedStatement.js +91 -0
- package/dist/d1/d1Provisioner.d.ts +28 -0
- package/dist/d1/d1Provisioner.d.ts.map +1 -0
- package/dist/d1/d1Provisioner.js +67 -0
- package/dist/email/emailRoutingManager.d.ts +36 -0
- package/dist/email/emailRoutingManager.d.ts.map +1 -0
- package/dist/email/emailRoutingManager.js +124 -0
- package/dist/email/emailSendManager.d.ts +45 -0
- package/dist/email/emailSendManager.d.ts.map +1 -0
- package/dist/email/emailSendManager.js +52 -0
- package/dist/env/devVars.d.ts +65 -0
- package/dist/env/devVars.d.ts.map +1 -0
- package/dist/env/devVars.js +84 -0
- package/dist/hostnames/customHostnamesManager.d.ts +51 -0
- package/dist/hostnames/customHostnamesManager.d.ts.map +1 -0
- package/dist/hostnames/customHostnamesManager.js +84 -0
- package/dist/kv/kvManager.d.ts +76 -0
- package/dist/kv/kvManager.d.ts.map +1 -0
- package/dist/kv/kvManager.js +135 -0
- package/dist/kv/kvProvisioner.d.ts +28 -0
- package/dist/kv/kvProvisioner.d.ts.map +1 -0
- package/dist/kv/kvProvisioner.js +71 -0
- package/dist/media/assetSeeder.d.ts +31 -0
- package/dist/media/assetSeeder.d.ts.map +1 -0
- package/dist/media/assetSeeder.js +70 -0
- package/dist/media/imageManager.d.ts +34 -0
- package/dist/media/imageManager.d.ts.map +1 -0
- package/dist/media/imageManager.js +103 -0
- package/dist/media/ownership.d.ts +40 -0
- package/dist/media/ownership.d.ts.map +1 -0
- package/dist/media/ownership.js +46 -0
- package/dist/media/streamManager.d.ts +83 -0
- package/dist/media/streamManager.d.ts.map +1 -0
- package/dist/media/streamManager.js +145 -0
- package/dist/queue/queueManager.d.ts +84 -0
- package/dist/queue/queueManager.d.ts.map +1 -0
- package/dist/queue/queueManager.js +114 -0
- package/dist/r2/r2Credentials.d.ts +12 -0
- package/dist/r2/r2Credentials.d.ts.map +1 -0
- package/dist/r2/r2Credentials.js +13 -0
- package/dist/r2/r2Manager.d.ts +226 -0
- package/dist/r2/r2Manager.d.ts.map +1 -0
- package/dist/r2/r2Manager.js +405 -0
- package/dist/r2/r2Provisioner.d.ts +39 -0
- package/dist/r2/r2Provisioner.d.ts.map +1 -0
- package/dist/r2/r2Provisioner.js +86 -0
- package/dist/secrets/secretsStoreManager.d.ts +82 -0
- package/dist/secrets/secretsStoreManager.d.ts.map +1 -0
- package/dist/secrets/secretsStoreManager.js +147 -0
- package/dist/secrets/secretsStores.d.ts +47 -0
- package/dist/secrets/secretsStores.d.ts.map +1 -0
- package/dist/secrets/secretsStores.js +65 -0
- package/dist/test-utils/emailRoutingRules.d.ts +39 -0
- package/dist/test-utils/emailRoutingRules.d.ts.map +1 -0
- package/dist/test-utils/emailRoutingRules.js +89 -0
- package/dist/test-utils/fixtureReportSetup.d.ts +25 -0
- package/dist/test-utils/fixtureReportSetup.d.ts.map +1 -0
- package/dist/test-utils/fixtureReportSetup.js +30 -0
- package/dist/test-utils/fixtures.d.ts +224 -0
- package/dist/test-utils/fixtures.d.ts.map +1 -0
- package/dist/test-utils/fixtures.js +277 -0
- package/dist/test-utils/harness.d.ts +192 -0
- package/dist/test-utils/harness.d.ts.map +1 -0
- package/dist/test-utils/harness.js +351 -0
- package/dist/test-utils/inboundRecorder.d.ts +91 -0
- package/dist/test-utils/inboundRecorder.d.ts.map +1 -0
- package/dist/test-utils/inboundRecorder.js +141 -0
- package/dist/test-utils/integrationSetup.d.ts +22 -0
- package/dist/test-utils/integrationSetup.d.ts.map +1 -0
- package/dist/test-utils/integrationSetup.js +37 -0
- package/dist/test-utils/reap.d.ts +112 -0
- package/dist/test-utils/reap.d.ts.map +1 -0
- package/dist/test-utils/reap.js +185 -0
- package/dist/tokens/accountTokensManager.d.ts +157 -0
- package/dist/tokens/accountTokensManager.d.ts.map +1 -0
- package/dist/tokens/accountTokensManager.js +280 -0
- package/dist/tokens/permissions.d.ts +36 -0
- package/dist/tokens/permissions.d.ts.map +1 -0
- package/dist/tokens/permissions.js +50 -0
- package/dist/tokens/profiles.d.ts +102 -0
- package/dist/tokens/profiles.d.ts.map +1 -0
- package/dist/tokens/profiles.js +137 -0
- package/dist/turnstile/turnstileManager.d.ts +75 -0
- package/dist/turnstile/turnstileManager.d.ts.map +1 -0
- package/dist/turnstile/turnstileManager.js +138 -0
- package/dist/user/userManager.d.ts +36 -0
- package/dist/user/userManager.d.ts.map +1 -0
- package/dist/user/userManager.js +58 -0
- package/dist/workers/buildsManager.d.ts +116 -0
- package/dist/workers/buildsManager.d.ts.map +1 -0
- package/dist/workers/buildsManager.js +237 -0
- package/dist/workers/buildsTypes.d.ts +102 -0
- package/dist/workers/buildsTypes.d.ts.map +1 -0
- package/dist/workers/buildsTypes.js +87 -0
- package/dist/workers/workersBuildEvent.d.ts +64 -0
- package/dist/workers/workersBuildEvent.d.ts.map +1 -0
- package/dist/workers/workersBuildEvent.js +47 -0
- package/dist/workers/workersManager.d.ts +136 -0
- package/dist/workers/workersManager.d.ts.map +1 -0
- package/dist/workers/workersManager.js +312 -0
- package/dist/workers/workersProvisioner.d.ts +80 -0
- package/dist/workers/workersProvisioner.d.ts.map +1 -0
- package/dist/workers/workersProvisioner.js +106 -0
- package/dist/workflows/stepFailure.d.ts +180 -0
- package/dist/workflows/stepFailure.d.ts.map +1 -0
- package/dist/workflows/stepFailure.js +227 -0
- package/dist/workflows/workflowsClient.d.ts +87 -0
- package/dist/workflows/workflowsClient.d.ts.map +1 -0
- package/dist/workflows/workflowsClient.js +156 -0
- package/dist/zones/zonesManager.d.ts +50 -0
- package/dist/zones/zonesManager.d.ts.map +1 -0
- package/dist/zones/zonesManager.js +74 -0
- 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 };
|