@jaggr2/cdk-cf-dns 0.1.0
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/API.md +411 -0
- package/README.md +194 -0
- package/lib/certificate.d.ts +41 -0
- package/lib/certificate.js +83 -0
- package/lib/certificate.js.map +1 -0
- package/lib/handler/acm.d.ts +12 -0
- package/lib/handler/acm.js +131 -0
- package/lib/handler/acm.js.map +1 -0
- package/lib/handler/cloudflare.d.ts +133 -0
- package/lib/handler/cloudflare.js +264 -0
- package/lib/handler/cloudflare.js.map +1 -0
- package/lib/handler/index.d.ts +11 -0
- package/lib/handler/index.js +114 -0
- package/lib/handler/index.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +21 -0
- package/lib/index.js.map +1 -0
- package/lib/provider.d.ts +75 -0
- package/lib/provider.js +234 -0
- package/lib/provider.js.map +1 -0
- package/lib/record.d.ts +225 -0
- package/lib/record.js +297 -0
- package/lib/record.js.map +1 -0
- package/lib/zone.d.ts +72 -0
- package/lib/zone.js +90 -0
- package/lib/zone.js.map +1 -0
- package/package.json +61 -0
- package/src/certificate.ts +74 -0
- package/src/handler/acm.ts +160 -0
- package/src/handler/cloudflare.ts +305 -0
- package/src/handler/deps.lock.json +3 -0
- package/src/handler/index.ts +145 -0
- package/src/index.ts +4 -0
- package/src/provider.ts +223 -0
- package/src/record.ts +433 -0
- package/src/zone.ts +115 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CloudflareValidatedCertificate = void 0;
|
|
37
|
+
const acm = __importStar(require("aws-cdk-lib/aws-certificatemanager"));
|
|
38
|
+
const cdk = __importStar(require("aws-cdk-lib"));
|
|
39
|
+
const constructs_1 = require("constructs");
|
|
40
|
+
const provider_1 = require("./provider");
|
|
41
|
+
/**
|
|
42
|
+
* An ACM certificate whose DNS validation records are written into Cloudflare
|
|
43
|
+
* automatically, removing the manual copy-paste step.
|
|
44
|
+
*
|
|
45
|
+
* CloudFormation does not expose the validation `ResourceRecord` as a
|
|
46
|
+
* certificate attribute, so a custom resource calls `acm:DescribeCertificate`,
|
|
47
|
+
* polls until the records are populated (they are absent for a few seconds
|
|
48
|
+
* after creation), and writes each unique CNAME into Cloudflare.
|
|
49
|
+
*
|
|
50
|
+
* Note: if the certificate is attached to CloudFront it must live in
|
|
51
|
+
* `us-east-1`; this construct does not solve cross-region certificates.
|
|
52
|
+
*/
|
|
53
|
+
class CloudflareValidatedCertificate extends constructs_1.Construct {
|
|
54
|
+
/**
|
|
55
|
+
* The underlying ACM certificate.
|
|
56
|
+
*/
|
|
57
|
+
certificate;
|
|
58
|
+
constructor(scope, id, props) {
|
|
59
|
+
super(scope, id);
|
|
60
|
+
// Create the certificate with DNS validation but no hosted zone: the
|
|
61
|
+
// validation records are written to Cloudflare by the custom resource.
|
|
62
|
+
this.certificate = new acm.Certificate(this, 'Certificate', {
|
|
63
|
+
domainName: props.domainName,
|
|
64
|
+
subjectAlternativeNames: props.subjectAlternativeNames,
|
|
65
|
+
validation: acm.CertificateValidation.fromDns(),
|
|
66
|
+
});
|
|
67
|
+
const provider = provider_1.CloudflareCertificateProvider.getOrCreate(this);
|
|
68
|
+
provider.grantSecretRead(props.zone.apiToken);
|
|
69
|
+
const validation = new cdk.CustomResource(this, 'DnsValidation', {
|
|
70
|
+
serviceToken: provider.serviceToken,
|
|
71
|
+
resourceType: 'Custom::CloudflareCertificateDnsValidation',
|
|
72
|
+
properties: {
|
|
73
|
+
certificateArn: this.certificate.certificateArn,
|
|
74
|
+
zoneId: props.zone.zoneId,
|
|
75
|
+
apiTokenSecretArn: props.zone.apiToken.secretArn,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// The validation resource can only read records once the certificate exists.
|
|
79
|
+
validation.node.addDependency(this.certificate);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.CloudflareValidatedCertificate = CloudflareValidatedCertificate;
|
|
83
|
+
//# sourceMappingURL=certificate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"certificate.js","sourceRoot":"","sources":["../src/certificate.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,wEAA0D;AAC1D,iDAAmC;AACnC,2CAAuC;AACvC,yCAA2D;AAyB3D;;;;;;;;;;;GAWG;AACH,MAAa,8BAA+B,SAAQ,sBAAS;IAC3D;;OAEG;IACa,WAAW,CAAkB;IAE7C,YAAmB,KAAgB,EAAE,EAAU,EAAE,KAA0C;QACzF,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAEjB,qEAAqE;QACrE,uEAAuE;QACvE,IAAI,CAAC,WAAW,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,IAAI,EAAE,aAAa,EAAE;YAC1D,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,uBAAuB,EAAE,KAAK,CAAC,uBAAuB;YACtD,UAAU,EAAE,GAAG,CAAC,qBAAqB,CAAC,OAAO,EAAE;SAChD,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,wCAA6B,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QACjE,QAAQ,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAE9C,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,eAAe,EAAE;YAC/D,YAAY,EAAE,QAAQ,CAAC,YAAY;YACnC,YAAY,EAAE,4CAA4C;YAC1D,UAAU,EAAE;gBACV,cAAc,EAAE,IAAI,CAAC,WAAW,CAAC,cAAc;gBAC/C,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,MAAM;gBACzB,iBAAiB,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS;aACjD;SACF,CAAC,CAAC;QAEH,6EAA6E;QAC7E,UAAU,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAClD,CAAC;CACF;AAjCD,wEAiCC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CloudFormationCustomResourceEvent } from 'aws-lambda';
|
|
2
|
+
/**
|
|
3
|
+
* The Lambda handler for the `Custom::CloudflareCertificateDnsValidation`
|
|
4
|
+
* custom resource.
|
|
5
|
+
*
|
|
6
|
+
* @param event The CloudFormation custom resource event.
|
|
7
|
+
* @returns The custom resource result with `PhysicalResourceId` and `Data`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handler(event: CloudFormationCustomResourceEvent): Promise<{
|
|
10
|
+
PhysicalResourceId: string;
|
|
11
|
+
Data: Record<string, unknown>;
|
|
12
|
+
}>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handler = handler;
|
|
4
|
+
const client_acm_1 = require("@aws-sdk/client-acm");
|
|
5
|
+
const cloudflare_1 = require("./cloudflare");
|
|
6
|
+
const acmClient = new client_acm_1.ACMClient({});
|
|
7
|
+
/**
|
|
8
|
+
* Sleeps for the given number of milliseconds.
|
|
9
|
+
*/
|
|
10
|
+
async function sleep(ms) {
|
|
11
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Describes the certificate and returns the deduplicated DNS validation records
|
|
15
|
+
* that ACM currently exposes. A wildcard SAN and its apex share one record, so
|
|
16
|
+
* records are deduplicated on the name/value pair.
|
|
17
|
+
*/
|
|
18
|
+
async function describeValidationRecords(certificateArn) {
|
|
19
|
+
const response = await acmClient.send(new client_acm_1.DescribeCertificateCommand({ CertificateArn: certificateArn }));
|
|
20
|
+
const options = response.Certificate?.DomainValidationOptions ?? [];
|
|
21
|
+
const records = [];
|
|
22
|
+
for (const option of options) {
|
|
23
|
+
const resourceRecord = option.ResourceRecord;
|
|
24
|
+
if (resourceRecord?.Name && resourceRecord.Value) {
|
|
25
|
+
const name = resourceRecord.Name.replace(/\.$/, '');
|
|
26
|
+
const value = resourceRecord.Value.replace(/\.$/, '');
|
|
27
|
+
const duplicate = records.some((r) => r.name === name && r.value === value);
|
|
28
|
+
if (!duplicate) {
|
|
29
|
+
records.push({ name, value });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return records;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Polls ACM until the validation `ResourceRecord`s are populated. They are
|
|
37
|
+
* absent for a few seconds after the certificate is created, so the handler
|
|
38
|
+
* retries until they appear or the deadline is reached.
|
|
39
|
+
*/
|
|
40
|
+
async function pollValidationRecords(certificateArn, delaySeconds, timeoutSeconds) {
|
|
41
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
42
|
+
while (Date.now() < deadline) {
|
|
43
|
+
const records = await describeValidationRecords(certificateArn);
|
|
44
|
+
if (records.length > 0) {
|
|
45
|
+
return records;
|
|
46
|
+
}
|
|
47
|
+
await sleep(delaySeconds * 1000);
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Timed out waiting for ACM DNS validation records for ${certificateArn}`);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates (or adopts) a CNAME validation record in Cloudflare. Validation
|
|
53
|
+
* records must never be proxied and must use a plain TTL.
|
|
54
|
+
*/
|
|
55
|
+
async function upsertCname(zoneId, name, value, token) {
|
|
56
|
+
const payload = {
|
|
57
|
+
name,
|
|
58
|
+
type: 'CNAME',
|
|
59
|
+
content: value,
|
|
60
|
+
ttl: 60,
|
|
61
|
+
proxied: false,
|
|
62
|
+
};
|
|
63
|
+
const response = await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records`, { method: 'POST', token, body: payload });
|
|
64
|
+
if ((0, cloudflare_1.isAlreadyExists)(response)) {
|
|
65
|
+
(0, cloudflare_1.log)('validation', { message: 'validation record already exists; adopting', zoneId, name });
|
|
66
|
+
const existing = await (0, cloudflare_1.findRecord)(zoneId, name, 'CNAME', token);
|
|
67
|
+
if (!existing) {
|
|
68
|
+
throw new Error(`Cloudflare reported ${name} already exists but the lookup found no matching record`);
|
|
69
|
+
}
|
|
70
|
+
await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records/${existing.id}`, { method: 'PATCH', token, body: payload });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
(0, cloudflare_1.assertSuccess)(response);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Handles both `Create` and `Update`: reads the certificate's validation
|
|
77
|
+
* records from ACM and writes each unique CNAME into Cloudflare.
|
|
78
|
+
*/
|
|
79
|
+
async function onUpsert(event) {
|
|
80
|
+
const properties = event.ResourceProperties;
|
|
81
|
+
const certificateArn = String(properties.certificateArn);
|
|
82
|
+
const zoneId = String(properties.zoneId);
|
|
83
|
+
const token = await (0, cloudflare_1.getApiToken)(String(properties.apiTokenSecretArn));
|
|
84
|
+
const delaySeconds = Number(properties.pollDelaySeconds ?? 5);
|
|
85
|
+
const timeoutSeconds = Number(properties.pollTimeoutSeconds ?? 120);
|
|
86
|
+
(0, cloudflare_1.log)('validation', { certificateArn, zoneId });
|
|
87
|
+
const records = await pollValidationRecords(certificateArn, delaySeconds, timeoutSeconds);
|
|
88
|
+
for (const record of records) {
|
|
89
|
+
await upsertCname(zoneId, record.name, record.value, token);
|
|
90
|
+
}
|
|
91
|
+
(0, cloudflare_1.log)('validation', { message: `wrote ${records.length} validation record(s)`, names: records.map((r) => r.name) });
|
|
92
|
+
return {
|
|
93
|
+
PhysicalResourceId: certificateArn,
|
|
94
|
+
Data: { ValidationRecords: records.map((r) => r.name) },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Handles `Delete`. The validation CNAMEs are left in Cloudflare — they are
|
|
99
|
+
* harmless after issuance and cleaning them up would add fragile delete logic.
|
|
100
|
+
*/
|
|
101
|
+
async function onDelete(event) {
|
|
102
|
+
(0, cloudflare_1.log)('validation', { message: 'delete; leaving validation records in Cloudflare' });
|
|
103
|
+
return { PhysicalResourceId: event.PhysicalResourceId, Data: {} };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The Lambda handler for the `Custom::CloudflareCertificateDnsValidation`
|
|
107
|
+
* custom resource.
|
|
108
|
+
*
|
|
109
|
+
* @param event The CloudFormation custom resource event.
|
|
110
|
+
* @returns The custom resource result with `PhysicalResourceId` and `Data`.
|
|
111
|
+
*/
|
|
112
|
+
async function handler(event) {
|
|
113
|
+
try {
|
|
114
|
+
(0, cloudflare_1.log)('event', { requestType: event.RequestType });
|
|
115
|
+
switch (event.RequestType) {
|
|
116
|
+
case 'Create':
|
|
117
|
+
return await onUpsert(event);
|
|
118
|
+
case 'Update':
|
|
119
|
+
return await onUpsert(event);
|
|
120
|
+
case 'Delete':
|
|
121
|
+
return await onDelete(event);
|
|
122
|
+
default:
|
|
123
|
+
throw new Error(`Unsupported RequestType: ${event.RequestType}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
128
|
+
throw new Error(`Cloudflare ACM DNS validation custom resource failed during ${event.RequestType}: ${message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=acm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acm.js","sourceRoot":"","sources":["../../src/handler/acm.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the Lambda handlers. This module is bundled into each
|
|
3
|
+
* handler entry point by esbuild; it is never imported by CDK library code.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The base URL for the Cloudflare API v4.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CF_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
9
|
+
/**
|
|
10
|
+
* How many attempts the retrying HTTP helper makes before giving up.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_ATTEMPTS = 5;
|
|
13
|
+
/**
|
|
14
|
+
* Base backoff delay in milliseconds, doubled each attempt with full jitter.
|
|
15
|
+
*/
|
|
16
|
+
export declare const BACKOFF_BASE_MS = 500;
|
|
17
|
+
/**
|
|
18
|
+
* Upper bound for a single backoff sleep in milliseconds.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BACKOFF_CAP_MS = 4000;
|
|
21
|
+
/**
|
|
22
|
+
* Cloudflare record IDs are 32 lowercase hex characters.
|
|
23
|
+
*/
|
|
24
|
+
export declare const RECORD_ID_PATTERN: RegExp;
|
|
25
|
+
/**
|
|
26
|
+
* The shape of a Cloudflare API response envelope.
|
|
27
|
+
*/
|
|
28
|
+
export interface CloudflareResponse {
|
|
29
|
+
success: boolean;
|
|
30
|
+
errors: Array<{
|
|
31
|
+
code: number;
|
|
32
|
+
message: string;
|
|
33
|
+
}>;
|
|
34
|
+
messages: Array<{
|
|
35
|
+
code: number;
|
|
36
|
+
message: string;
|
|
37
|
+
}>;
|
|
38
|
+
result: unknown;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A DNS record as returned by the Cloudflare API.
|
|
42
|
+
*/
|
|
43
|
+
export interface CloudflareDnsRecord {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
type: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Redacts strings that look like Cloudflare credentials (and known token
|
|
50
|
+
* values) from arbitrary log payloads.
|
|
51
|
+
*
|
|
52
|
+
* @param value The value to redact.
|
|
53
|
+
* @returns A copy of `value` with token-shaped strings replaced.
|
|
54
|
+
*/
|
|
55
|
+
export declare function redact(value: unknown): unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Logs a message to CloudWatch with all token-shaped strings redacted.
|
|
58
|
+
*/
|
|
59
|
+
export declare function log(...args: unknown[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* Resolves the Cloudflare API token from Secrets Manager, caching it per ARN so
|
|
62
|
+
* warm invocations skip the API call. Supports the secret being either a raw
|
|
63
|
+
* string or a JSON blob with an `apiToken` key.
|
|
64
|
+
*
|
|
65
|
+
* @param secretArn The ARN of the secret holding the token.
|
|
66
|
+
*/
|
|
67
|
+
export declare function getApiToken(secretArn: string): Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* A Cloudflare API error, carrying the HTTP status and the error payload so the
|
|
70
|
+
* lifecycle logic can inspect specific error codes.
|
|
71
|
+
*/
|
|
72
|
+
export declare class CloudflareApiError extends Error {
|
|
73
|
+
readonly status: number;
|
|
74
|
+
readonly errors: Array<{
|
|
75
|
+
code: number;
|
|
76
|
+
message: string;
|
|
77
|
+
}>;
|
|
78
|
+
constructor(status: number, errors: Array<{
|
|
79
|
+
code: number;
|
|
80
|
+
message: string;
|
|
81
|
+
}>);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Performs an HTTP request against the Cloudflare API with retries.
|
|
85
|
+
*
|
|
86
|
+
* Retries with exponential backoff + full jitter on HTTP 429, 5xx and network
|
|
87
|
+
* errors, honouring the `Retry-After` header when present.
|
|
88
|
+
*
|
|
89
|
+
* @param path The API path, e.g. `/zones/{id}/dns_records`.
|
|
90
|
+
* @param options Request options. A resolved token is sent as `token`.
|
|
91
|
+
*/
|
|
92
|
+
export declare function request(path: string, options: {
|
|
93
|
+
method: string;
|
|
94
|
+
token?: string;
|
|
95
|
+
body?: unknown;
|
|
96
|
+
}): Promise<{
|
|
97
|
+
status: number;
|
|
98
|
+
body: CloudflareResponse;
|
|
99
|
+
}>;
|
|
100
|
+
/**
|
|
101
|
+
* Throws a descriptive error when the Cloudflare response has `success: false`.
|
|
102
|
+
*/
|
|
103
|
+
export declare function assertSuccess(response: {
|
|
104
|
+
status: number;
|
|
105
|
+
body: CloudflareResponse;
|
|
106
|
+
}): void;
|
|
107
|
+
/**
|
|
108
|
+
* Returns true when the response indicates the record already exists.
|
|
109
|
+
*/
|
|
110
|
+
export declare function isAlreadyExists(response: {
|
|
111
|
+
status: number;
|
|
112
|
+
body: CloudflareResponse;
|
|
113
|
+
}): boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Returns true when the response indicates the record no longer exists.
|
|
116
|
+
*/
|
|
117
|
+
export declare function isRecordDoesNotExist(response: {
|
|
118
|
+
status: number;
|
|
119
|
+
body: CloudflareResponse;
|
|
120
|
+
}): boolean;
|
|
121
|
+
/**
|
|
122
|
+
* Builds the DNS record payload sent to the Cloudflare API from the custom
|
|
123
|
+
* resource properties.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildRecordPayload(properties: Record<string, unknown>): Record<string, unknown>;
|
|
126
|
+
/**
|
|
127
|
+
* Creates a DNS record and returns the Cloudflare record.
|
|
128
|
+
*/
|
|
129
|
+
export declare function createRecord(zoneId: string, payload: Record<string, unknown>, token: string): Promise<CloudflareDnsRecord>;
|
|
130
|
+
/**
|
|
131
|
+
* Looks up a record by name and type in the zone.
|
|
132
|
+
*/
|
|
133
|
+
export declare function findRecord(zoneId: string, name: string, type: string, token: string): Promise<CloudflareDnsRecord | undefined>;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CloudflareApiError = exports.RECORD_ID_PATTERN = exports.BACKOFF_CAP_MS = exports.BACKOFF_BASE_MS = exports.MAX_ATTEMPTS = exports.CF_BASE_URL = void 0;
|
|
4
|
+
exports.redact = redact;
|
|
5
|
+
exports.log = log;
|
|
6
|
+
exports.getApiToken = getApiToken;
|
|
7
|
+
exports.request = request;
|
|
8
|
+
exports.assertSuccess = assertSuccess;
|
|
9
|
+
exports.isAlreadyExists = isAlreadyExists;
|
|
10
|
+
exports.isRecordDoesNotExist = isRecordDoesNotExist;
|
|
11
|
+
exports.buildRecordPayload = buildRecordPayload;
|
|
12
|
+
exports.createRecord = createRecord;
|
|
13
|
+
exports.findRecord = findRecord;
|
|
14
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
15
|
+
/**
|
|
16
|
+
* Shared helpers for the Lambda handlers. This module is bundled into each
|
|
17
|
+
* handler entry point by esbuild; it is never imported by CDK library code.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The base URL for the Cloudflare API v4.
|
|
21
|
+
*/
|
|
22
|
+
exports.CF_BASE_URL = 'https://api.cloudflare.com/client/v4';
|
|
23
|
+
/**
|
|
24
|
+
* How many attempts the retrying HTTP helper makes before giving up.
|
|
25
|
+
*/
|
|
26
|
+
exports.MAX_ATTEMPTS = 5;
|
|
27
|
+
/**
|
|
28
|
+
* Base backoff delay in milliseconds, doubled each attempt with full jitter.
|
|
29
|
+
*/
|
|
30
|
+
exports.BACKOFF_BASE_MS = 500;
|
|
31
|
+
/**
|
|
32
|
+
* Upper bound for a single backoff sleep in milliseconds.
|
|
33
|
+
*/
|
|
34
|
+
exports.BACKOFF_CAP_MS = 4000;
|
|
35
|
+
/**
|
|
36
|
+
* A regular expression matching strings that look like Cloudflare credentials
|
|
37
|
+
* (API tokens are 40 characters, global API keys 37). Anything matching is
|
|
38
|
+
* redacted from logs.
|
|
39
|
+
*/
|
|
40
|
+
const TOKEN_PATTERN = /[A-Za-z0-9_-]{37,}/g;
|
|
41
|
+
/**
|
|
42
|
+
* Cloudflare record IDs are 32 lowercase hex characters.
|
|
43
|
+
*/
|
|
44
|
+
exports.RECORD_ID_PATTERN = /^[a-f0-9]{32}$/;
|
|
45
|
+
const secretCache = new Map();
|
|
46
|
+
let secretsClient;
|
|
47
|
+
/**
|
|
48
|
+
* Redacts strings that look like Cloudflare credentials (and known token
|
|
49
|
+
* values) from arbitrary log payloads.
|
|
50
|
+
*
|
|
51
|
+
* @param value The value to redact.
|
|
52
|
+
* @returns A copy of `value` with token-shaped strings replaced.
|
|
53
|
+
*/
|
|
54
|
+
function redact(value) {
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
return value.replace(TOKEN_PATTERN, '[REDACTED]');
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map((item) => redact(item));
|
|
60
|
+
}
|
|
61
|
+
if (value !== null && typeof value === 'object') {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, item] of Object.entries(value)) {
|
|
64
|
+
out[key] = redact(item);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Logs a message to CloudWatch with all token-shaped strings redacted.
|
|
72
|
+
*/
|
|
73
|
+
function log(...args) {
|
|
74
|
+
console.log(...args.map((arg) => redact(arg)));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolves the Cloudflare API token from Secrets Manager, caching it per ARN so
|
|
78
|
+
* warm invocations skip the API call. Supports the secret being either a raw
|
|
79
|
+
* string or a JSON blob with an `apiToken` key.
|
|
80
|
+
*
|
|
81
|
+
* @param secretArn The ARN of the secret holding the token.
|
|
82
|
+
*/
|
|
83
|
+
async function getApiToken(secretArn) {
|
|
84
|
+
const cached = secretCache.get(secretArn);
|
|
85
|
+
if (cached !== undefined) {
|
|
86
|
+
return cached;
|
|
87
|
+
}
|
|
88
|
+
if (!secretsClient) {
|
|
89
|
+
secretsClient = new client_secrets_manager_1.SecretsManagerClient({});
|
|
90
|
+
}
|
|
91
|
+
const response = await secretsClient.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
|
|
92
|
+
const secretString = response.SecretString;
|
|
93
|
+
if (secretString === undefined) {
|
|
94
|
+
throw new Error(`Secret ${secretArn} has no SecretString; store the API token as a raw string or a JSON blob with an "apiToken" key`);
|
|
95
|
+
}
|
|
96
|
+
let token = secretString;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(secretString);
|
|
99
|
+
if (parsed !== null && typeof parsed === 'object' && typeof parsed.apiToken === 'string') {
|
|
100
|
+
token = parsed.apiToken;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Not JSON: treat the raw string as the token.
|
|
105
|
+
}
|
|
106
|
+
if (token.length === 0) {
|
|
107
|
+
throw new Error(`Secret ${secretArn} resolved to an empty API token`);
|
|
108
|
+
}
|
|
109
|
+
secretCache.set(secretArn, token);
|
|
110
|
+
return token;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A Cloudflare API error, carrying the HTTP status and the error payload so the
|
|
114
|
+
* lifecycle logic can inspect specific error codes.
|
|
115
|
+
*/
|
|
116
|
+
class CloudflareApiError extends Error {
|
|
117
|
+
status;
|
|
118
|
+
errors;
|
|
119
|
+
constructor(status, errors) {
|
|
120
|
+
const details = errors.map((error) => `${error.code}: ${error.message}`).join('; ');
|
|
121
|
+
super(`Cloudflare API error (HTTP ${status}): ${String(redact(details))}`);
|
|
122
|
+
this.status = status;
|
|
123
|
+
this.errors = errors;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.CloudflareApiError = CloudflareApiError;
|
|
127
|
+
/**
|
|
128
|
+
* Performs an HTTP request against the Cloudflare API with retries.
|
|
129
|
+
*
|
|
130
|
+
* Retries with exponential backoff + full jitter on HTTP 429, 5xx and network
|
|
131
|
+
* errors, honouring the `Retry-After` header when present.
|
|
132
|
+
*
|
|
133
|
+
* @param path The API path, e.g. `/zones/{id}/dns_records`.
|
|
134
|
+
* @param options Request options. A resolved token is sent as `token`.
|
|
135
|
+
*/
|
|
136
|
+
async function request(path, options) {
|
|
137
|
+
const headers = {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
};
|
|
140
|
+
if (options.token !== undefined) {
|
|
141
|
+
headers['Authorization'] = `Bearer ${options.token}`;
|
|
142
|
+
}
|
|
143
|
+
const url = `${exports.CF_BASE_URL}${path}`;
|
|
144
|
+
const body = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
145
|
+
let lastError;
|
|
146
|
+
for (let attempt = 0; attempt < exports.MAX_ATTEMPTS; attempt++) {
|
|
147
|
+
try {
|
|
148
|
+
const response = await fetch(url, {
|
|
149
|
+
method: options.method,
|
|
150
|
+
headers,
|
|
151
|
+
body,
|
|
152
|
+
});
|
|
153
|
+
const text = await response.text();
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(text);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
parsed = {
|
|
160
|
+
success: false,
|
|
161
|
+
errors: [{ code: 0, message: `Non-JSON response (HTTP ${response.status}): ${truncate(text, 500)}` }],
|
|
162
|
+
messages: [],
|
|
163
|
+
result: null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
167
|
+
if (retryable && attempt < exports.MAX_ATTEMPTS - 1) {
|
|
168
|
+
await sleepForRetry(response, attempt);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
return { status: response.status, body: parsed };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
175
|
+
if (attempt < exports.MAX_ATTEMPTS - 1) {
|
|
176
|
+
log('retry', { path, attempt, reason: `network error: ${lastError.message}` });
|
|
177
|
+
await sleepForRetry(undefined, attempt);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
throw lastError ?? new Error(`Request to ${path} failed after ${exports.MAX_ATTEMPTS} attempts`);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Sleeps for a backoff interval, honouring `Retry-After` when the server
|
|
185
|
+
* provided one and otherwise using exponential backoff with full jitter.
|
|
186
|
+
*/
|
|
187
|
+
async function sleepForRetry(response, attempt) {
|
|
188
|
+
let delayMs;
|
|
189
|
+
const retryAfter = response?.headers.get('Retry-After');
|
|
190
|
+
if (retryAfter !== null && retryAfter !== undefined) {
|
|
191
|
+
const seconds = Number.parseInt(retryAfter, 10);
|
|
192
|
+
delayMs = Number.isFinite(seconds) ? Math.min(seconds * 1000, exports.BACKOFF_CAP_MS) : exports.BACKOFF_CAP_MS;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const cap = Math.min(exports.BACKOFF_CAP_MS, exports.BACKOFF_BASE_MS * 2 ** attempt);
|
|
196
|
+
delayMs = Math.random() * cap;
|
|
197
|
+
}
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Truncates a long string for inclusion in an error message.
|
|
202
|
+
*/
|
|
203
|
+
function truncate(value, max) {
|
|
204
|
+
return value.length > max ? `${value.slice(0, max)}...` : value;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Throws a descriptive error when the Cloudflare response has `success: false`.
|
|
208
|
+
*/
|
|
209
|
+
function assertSuccess(response) {
|
|
210
|
+
if (!response.body.success) {
|
|
211
|
+
throw new CloudflareApiError(response.status, response.body.errors);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Returns true when the response indicates the record already exists.
|
|
216
|
+
*/
|
|
217
|
+
function isAlreadyExists(response) {
|
|
218
|
+
if (response.body.success) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
return response.body.errors.some((error) => {
|
|
222
|
+
const codeMatches = error.code === 81053 || error.code === 81057;
|
|
223
|
+
const messageMatches = /already exists/i.test(error.message);
|
|
224
|
+
return codeMatches || messageMatches;
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Returns true when the response indicates the record no longer exists.
|
|
229
|
+
*/
|
|
230
|
+
function isRecordDoesNotExist(response) {
|
|
231
|
+
return response.body.errors.some((error) => error.code === 81044);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Builds the DNS record payload sent to the Cloudflare API from the custom
|
|
235
|
+
* resource properties.
|
|
236
|
+
*/
|
|
237
|
+
function buildRecordPayload(properties) {
|
|
238
|
+
const record = (properties.record ?? {});
|
|
239
|
+
const payload = {};
|
|
240
|
+
for (const key of ['name', 'type', 'content', 'data', 'ttl', 'proxied', 'priority', 'comment', 'tags']) {
|
|
241
|
+
if (record[key] !== undefined) {
|
|
242
|
+
payload[key] = record[key];
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return payload;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Creates a DNS record and returns the Cloudflare record.
|
|
249
|
+
*/
|
|
250
|
+
async function createRecord(zoneId, payload, token) {
|
|
251
|
+
const response = await request(`/zones/${zoneId}/dns_records`, { method: 'POST', token, body: payload });
|
|
252
|
+
assertSuccess(response);
|
|
253
|
+
return response.body.result;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Looks up a record by name and type in the zone.
|
|
257
|
+
*/
|
|
258
|
+
async function findRecord(zoneId, name, type, token) {
|
|
259
|
+
const response = await request(`/zones/${zoneId}/dns_records?name=${encodeURIComponent(name)}&type=${type}`, { method: 'GET', token });
|
|
260
|
+
assertSuccess(response);
|
|
261
|
+
const results = response.body.result;
|
|
262
|
+
return results[0];
|
|
263
|
+
}
|
|
264
|
+
//# sourceMappingURL=cloudflare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare.js","sourceRoot":"","sources":["../../src/handler/cloudflare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CloudFormationCustomResourceEvent } from 'aws-lambda';
|
|
2
|
+
/**
|
|
3
|
+
* The Lambda handler for the `Custom::CloudflareDnsRecord` custom resource.
|
|
4
|
+
*
|
|
5
|
+
* @param event The CloudFormation custom resource event.
|
|
6
|
+
* @returns The custom resource result with `PhysicalResourceId` and `Data`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function handler(event: CloudFormationCustomResourceEvent): Promise<{
|
|
9
|
+
PhysicalResourceId: string;
|
|
10
|
+
Data: Record<string, unknown>;
|
|
11
|
+
}>;
|