@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.
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = handler;
4
+ const cloudflare_1 = require("./cloudflare");
5
+ /**
6
+ * Handles the `Create` request type.
7
+ */
8
+ async function onCreate(event) {
9
+ const properties = event.ResourceProperties;
10
+ const zoneId = String(properties.zoneId);
11
+ const payload = (0, cloudflare_1.buildRecordPayload)(properties);
12
+ const token = await (0, cloudflare_1.getApiToken)(String(properties.apiTokenSecretArn));
13
+ (0, cloudflare_1.log)('create', { zoneId, name: payload.name, type: payload.type });
14
+ const response = await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records`, { method: 'POST', token, body: payload });
15
+ if ((0, cloudflare_1.isAlreadyExists)(response)) {
16
+ if (!properties.adoptExisting) {
17
+ throw new Error(`DNS record ${String(payload.name)} (${String(payload.type)}) already exists in zone ${zoneId}. ` +
18
+ 'Either delete it in the Cloudflare dashboard, or set adoptExisting: true to adopt and manage it.');
19
+ }
20
+ (0, cloudflare_1.log)('adopt', { zoneId, name: payload.name, type: payload.type });
21
+ const existing = await (0, cloudflare_1.findRecord)(zoneId, String(payload.name), String(payload.type), token);
22
+ if (!existing) {
23
+ throw new Error(`Cloudflare reported ${String(payload.name)} already exists but the lookup found no matching record`);
24
+ }
25
+ await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records/${existing.id}`, { method: 'PATCH', token, body: payload });
26
+ return { PhysicalResourceId: existing.id, Data: { RecordId: existing.id, DomainName: existing.name } };
27
+ }
28
+ (0, cloudflare_1.assertSuccess)(response);
29
+ const result = response.body.result;
30
+ return { PhysicalResourceId: result.id, Data: { RecordId: result.id, DomainName: result.name } };
31
+ }
32
+ /**
33
+ * Handles the `Update` request type.
34
+ */
35
+ async function onUpdate(event) {
36
+ const properties = event.ResourceProperties;
37
+ const oldProperties = event.OldResourceProperties;
38
+ const zoneId = String(properties.zoneId);
39
+ const payload = (0, cloudflare_1.buildRecordPayload)(properties);
40
+ const token = await (0, cloudflare_1.getApiToken)(String(properties.apiTokenSecretArn));
41
+ (0, cloudflare_1.log)('update', { zoneId, name: payload.name, type: payload.type });
42
+ // If the zone changed, the record must be created in the new zone. Returning a
43
+ // new physical id tells CloudFormation to send a Delete for the old one.
44
+ if (String(oldProperties.zoneId) !== zoneId) {
45
+ (0, cloudflare_1.log)('update', { message: 'zone changed; creating in new zone', zoneId });
46
+ const created = await (0, cloudflare_1.createRecord)(zoneId, payload, token);
47
+ return { PhysicalResourceId: created.id, Data: { RecordId: created.id, DomainName: created.name } };
48
+ }
49
+ const physicalId = event.PhysicalResourceId;
50
+ const response = await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records/${physicalId}`, { method: 'PATCH', token, body: payload });
51
+ // The record was deleted out-of-band (e.g. in the dashboard); recreate it.
52
+ if (response.status === 404 || (response.body.success === false && (0, cloudflare_1.isRecordDoesNotExist)(response))) {
53
+ (0, cloudflare_1.log)('update', { message: 'record not found; recreating', zoneId, name: payload.name });
54
+ const created = await (0, cloudflare_1.createRecord)(zoneId, payload, token);
55
+ return { PhysicalResourceId: created.id, Data: { RecordId: created.id, DomainName: created.name } };
56
+ }
57
+ (0, cloudflare_1.assertSuccess)(response);
58
+ const result = response.body.result;
59
+ return { PhysicalResourceId: physicalId, Data: { RecordId: result.id, DomainName: result.name } };
60
+ }
61
+ /**
62
+ * Handles the `Delete` request type. Deleting is idempotent: a missing record
63
+ * is treated as success so stacks never become undeletable.
64
+ */
65
+ async function onDelete(event) {
66
+ const properties = event.ResourceProperties;
67
+ const zoneId = String(properties.zoneId);
68
+ const physicalId = event.PhysicalResourceId;
69
+ if (properties.retainOnDelete) {
70
+ (0, cloudflare_1.log)('delete', { message: 'retainOnDelete set; leaving record in Cloudflare', zoneId });
71
+ return { PhysicalResourceId: physicalId, Data: {} };
72
+ }
73
+ // A failed Create can leave CloudFormation's arn:... placeholder as the
74
+ // physical id; never throw in that case.
75
+ if (!cloudflare_1.RECORD_ID_PATTERN.test(physicalId)) {
76
+ (0, cloudflare_1.log)('delete', { message: 'physical id is not a Cloudflare record id; skipping delete', physicalId });
77
+ return { PhysicalResourceId: physicalId, Data: {} };
78
+ }
79
+ const token = await (0, cloudflare_1.getApiToken)(String(properties.apiTokenSecretArn));
80
+ (0, cloudflare_1.log)('delete', { zoneId, physicalId });
81
+ const response = await (0, cloudflare_1.request)(`/zones/${zoneId}/dns_records/${physicalId}`, { method: 'DELETE', token });
82
+ if (response.status === 404 || (response.body.success === false && (0, cloudflare_1.isRecordDoesNotExist)(response))) {
83
+ (0, cloudflare_1.log)('delete', { message: 'record already gone; treating delete as success', physicalId });
84
+ return { PhysicalResourceId: physicalId, Data: {} };
85
+ }
86
+ (0, cloudflare_1.assertSuccess)(response);
87
+ return { PhysicalResourceId: physicalId, Data: {} };
88
+ }
89
+ /**
90
+ * The Lambda handler for the `Custom::CloudflareDnsRecord` custom resource.
91
+ *
92
+ * @param event The CloudFormation custom resource event.
93
+ * @returns The custom resource result with `PhysicalResourceId` and `Data`.
94
+ */
95
+ async function handler(event) {
96
+ try {
97
+ (0, cloudflare_1.log)('event', { requestType: event.RequestType });
98
+ switch (event.RequestType) {
99
+ case 'Create':
100
+ return await onCreate(event);
101
+ case 'Update':
102
+ return await onUpdate(event);
103
+ case 'Delete':
104
+ return await onDelete(event);
105
+ default:
106
+ throw new Error(`Unsupported RequestType: ${event.RequestType}`);
107
+ }
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ throw new Error(`Cloudflare DNS custom resource failed during ${event.RequestType}: ${message}`);
112
+ }
113
+ }
114
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/handler/index.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './zone';
2
+ export * from './record';
3
+ export * from './provider';
4
+ export * from './certificate';
package/lib/index.js ADDED
@@ -0,0 +1,21 @@
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./zone"), exports);
18
+ __exportStar(require("./record"), exports);
19
+ __exportStar(require("./provider"), exports);
20
+ __exportStar(require("./certificate"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,yCAAuB;AACvB,2CAAyB;AACzB,6CAA2B;AAC3B,gDAA8B"}
@@ -0,0 +1,75 @@
1
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
2
+ import { Construct } from 'constructs';
3
+ /**
4
+ * The shared custom-resource provider that performs the Cloudflare DNS API
5
+ * calls.
6
+ *
7
+ * One instance exists per stack (a singleton keyed by `Stack`), so a stack with
8
+ * many records still has exactly one Lambda handler. All records route their
9
+ * custom-resource events through this handler.
10
+ */
11
+ export declare class CloudflareDnsProvider extends Construct {
12
+ /**
13
+ * Gets (or lazily creates) the provider for the stack of `scope`.
14
+ *
15
+ * @param scope Any construct within the stack that should own the provider.
16
+ */
17
+ static getOrCreate(scope: Construct): CloudflareDnsProvider;
18
+ /**
19
+ * The custom-resource service token that records use as their `serviceToken`.
20
+ */
21
+ readonly serviceToken: string;
22
+ /**
23
+ * The IAM role used by the handler. Record constructs grant `GetSecretValue`
24
+ * on their zone's secret here.
25
+ */
26
+ private readonly handlerRole;
27
+ /**
28
+ * Secrets that have already been granted, deduplicated by ARN.
29
+ */
30
+ private readonly grantedSecrets;
31
+ private constructor();
32
+ /**
33
+ * Grants the provider's handler `secretsmanager:GetSecretValue` on the given
34
+ * secret. Repeated grants of the same secret are deduplicated.
35
+ *
36
+ * @param secret The secret holding a Cloudflare API token.
37
+ */
38
+ grantSecretRead(secret: secretsmanager.ISecret): void;
39
+ }
40
+ /**
41
+ * The shared custom-resource provider that writes ACM DNS validation records
42
+ * into Cloudflare.
43
+ *
44
+ * Like `CloudflareDnsProvider`, one instance exists per stack. It additionally
45
+ * grants its handler `acm:DescribeCertificate` on all certificates.
46
+ */
47
+ export declare class CloudflareCertificateProvider extends Construct {
48
+ /**
49
+ * Gets (or lazily creates) the provider for the stack of `scope`.
50
+ *
51
+ * @param scope Any construct within the stack that should own the provider.
52
+ */
53
+ static getOrCreate(scope: Construct): CloudflareCertificateProvider;
54
+ /**
55
+ * The custom-resource service token that the certificate construct uses as
56
+ * its `serviceToken`.
57
+ */
58
+ readonly serviceToken: string;
59
+ /**
60
+ * The IAM role used by the handler.
61
+ */
62
+ private readonly handlerRole;
63
+ /**
64
+ * Secrets that have already been granted, deduplicated by ARN.
65
+ */
66
+ private readonly grantedSecrets;
67
+ private constructor();
68
+ /**
69
+ * Grants the provider's handler `secretsmanager:GetSecretValue` on the given
70
+ * secret. Repeated grants of the same secret are deduplicated.
71
+ *
72
+ * @param secret The secret holding a Cloudflare API token.
73
+ */
74
+ grantSecretRead(secret: secretsmanager.ISecret): void;
75
+ }
@@ -0,0 +1,234 @@
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.CloudflareCertificateProvider = exports.CloudflareDnsProvider = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const cdk = __importStar(require("aws-cdk-lib"));
40
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
41
+ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
42
+ const aws_lambda_nodejs_1 = require("aws-cdk-lib/aws-lambda-nodejs");
43
+ const logs = __importStar(require("aws-cdk-lib/aws-logs"));
44
+ const custom_resources = __importStar(require("aws-cdk-lib/custom-resources"));
45
+ const constructs_1 = require("constructs");
46
+ /**
47
+ * The stable construct ID used for the shared DNS provider on each Stack.
48
+ */
49
+ const PROVIDER_ID = 'AcmeCloudflareDnsProvider';
50
+ /**
51
+ * The stable construct ID used for the shared ACM provider on each Stack.
52
+ */
53
+ const CERTIFICATE_PROVIDER_ID = 'AcmeCloudflareCertificateProvider';
54
+ /**
55
+ * Resolves a handler entry point. When the library is consumed from the
56
+ * compiled `lib/` output the source lives at `<package>/src/handler/<file>`;
57
+ * when tests run against `src/` directly it is at `src/handler/<file>`.
58
+ */
59
+ function handlerEntry(file) {
60
+ const fromLib = path.join(__dirname, '..', 'src', 'handler', file);
61
+ if (fs.existsSync(fromLib)) {
62
+ return fromLib;
63
+ }
64
+ return path.join(__dirname, 'handler', file);
65
+ }
66
+ /**
67
+ * Creates the shared handler function with the provider's standard
68
+ * configuration: Node 22 on ARM, 256 MB, a one-month log group that is deleted
69
+ * with the stack, and an esbuild bundle with the AWS SDK kept external.
70
+ */
71
+ function createHandler(scope, id, file, timeout) {
72
+ // `NodejsFunction` requires its entry (and lock file) to live under
73
+ // `projectRoot`. The library is consumed from a different project, so both
74
+ // are pointed at the package's own root. The handler has no runtime npm
75
+ // dependencies, so the shipped `deps.lock.json` marker is never actually
76
+ // read — it only satisfies the path validation.
77
+ const projectRoot = path.join(__dirname, '..');
78
+ const handler = new aws_lambda_nodejs_1.NodejsFunction(scope, id, {
79
+ entry: handlerEntry(file),
80
+ projectRoot,
81
+ depsLockFilePath: path.join(projectRoot, 'src', 'handler', 'deps.lock.json'),
82
+ runtime: lambda.Runtime.NODEJS_22_X,
83
+ architecture: lambda.Architecture.ARM_64,
84
+ timeout,
85
+ memorySize: 256,
86
+ bundling: {
87
+ minify: true,
88
+ sourceMap: true,
89
+ externalModules: ['@aws-sdk/*'],
90
+ },
91
+ });
92
+ const logGroup = new logs.LogGroup(scope, `${id}LogGroup`, {
93
+ logGroupName: `/aws/lambda/${handler.functionName}`,
94
+ retention: logs.RetentionDays.ONE_MONTH,
95
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
96
+ });
97
+ logGroup.node.addDependency(handler);
98
+ if (!handler.role) {
99
+ throw new Error(`Cloudflare provider handler ${id} requires an IAM role`);
100
+ }
101
+ return { handler, role: handler.role };
102
+ }
103
+ /**
104
+ * The shared custom-resource provider that performs the Cloudflare DNS API
105
+ * calls.
106
+ *
107
+ * One instance exists per stack (a singleton keyed by `Stack`), so a stack with
108
+ * many records still has exactly one Lambda handler. All records route their
109
+ * custom-resource events through this handler.
110
+ */
111
+ class CloudflareDnsProvider extends constructs_1.Construct {
112
+ /**
113
+ * Gets (or lazily creates) the provider for the stack of `scope`.
114
+ *
115
+ * @param scope Any construct within the stack that should own the provider.
116
+ */
117
+ static getOrCreate(scope) {
118
+ const stack = cdk.Stack.of(scope);
119
+ const existing = stack.node.tryFindChild(PROVIDER_ID);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ return new CloudflareDnsProvider(stack, PROVIDER_ID);
124
+ }
125
+ /**
126
+ * The custom-resource service token that records use as their `serviceToken`.
127
+ */
128
+ serviceToken;
129
+ /**
130
+ * The IAM role used by the handler. Record constructs grant `GetSecretValue`
131
+ * on their zone's secret here.
132
+ */
133
+ handlerRole;
134
+ /**
135
+ * Secrets that have already been granted, deduplicated by ARN.
136
+ */
137
+ grantedSecrets = new Set();
138
+ constructor(scope, id) {
139
+ super(scope, id);
140
+ const { handler, role } = createHandler(this, 'Handler', 'index.ts', cdk.Duration.minutes(2));
141
+ const provider = new custom_resources.Provider(this, 'Provider', {
142
+ onEventHandler: handler,
143
+ });
144
+ this.serviceToken = provider.serviceToken;
145
+ this.handlerRole = role;
146
+ }
147
+ /**
148
+ * Grants the provider's handler `secretsmanager:GetSecretValue` on the given
149
+ * secret. Repeated grants of the same secret are deduplicated.
150
+ *
151
+ * @param secret The secret holding a Cloudflare API token.
152
+ */
153
+ grantSecretRead(secret) {
154
+ const arn = secret.secretArn;
155
+ if (this.grantedSecrets.has(arn)) {
156
+ return;
157
+ }
158
+ this.grantedSecrets.add(arn);
159
+ this.handlerRole.addToPrincipalPolicy(new iam.PolicyStatement({
160
+ actions: ['secretsmanager:GetSecretValue'],
161
+ resources: [arn],
162
+ }));
163
+ }
164
+ }
165
+ exports.CloudflareDnsProvider = CloudflareDnsProvider;
166
+ /**
167
+ * The shared custom-resource provider that writes ACM DNS validation records
168
+ * into Cloudflare.
169
+ *
170
+ * Like `CloudflareDnsProvider`, one instance exists per stack. It additionally
171
+ * grants its handler `acm:DescribeCertificate` on all certificates.
172
+ */
173
+ class CloudflareCertificateProvider extends constructs_1.Construct {
174
+ /**
175
+ * Gets (or lazily creates) the provider for the stack of `scope`.
176
+ *
177
+ * @param scope Any construct within the stack that should own the provider.
178
+ */
179
+ static getOrCreate(scope) {
180
+ const stack = cdk.Stack.of(scope);
181
+ const existing = stack.node.tryFindChild(CERTIFICATE_PROVIDER_ID);
182
+ if (existing) {
183
+ return existing;
184
+ }
185
+ return new CloudflareCertificateProvider(stack, CERTIFICATE_PROVIDER_ID);
186
+ }
187
+ /**
188
+ * The custom-resource service token that the certificate construct uses as
189
+ * its `serviceToken`.
190
+ */
191
+ serviceToken;
192
+ /**
193
+ * The IAM role used by the handler.
194
+ */
195
+ handlerRole;
196
+ /**
197
+ * Secrets that have already been granted, deduplicated by ARN.
198
+ */
199
+ grantedSecrets = new Set();
200
+ constructor(scope, id) {
201
+ super(scope, id);
202
+ // ACM validation records can take a while to appear after certificate
203
+ // creation, so the handler gets a generous timeout to poll for them.
204
+ const { handler, role } = createHandler(this, 'Handler', 'acm.ts', cdk.Duration.minutes(5));
205
+ role.addToPrincipalPolicy(new iam.PolicyStatement({
206
+ actions: ['acm:DescribeCertificate'],
207
+ resources: ['*'],
208
+ }));
209
+ const provider = new custom_resources.Provider(this, 'Provider', {
210
+ onEventHandler: handler,
211
+ });
212
+ this.serviceToken = provider.serviceToken;
213
+ this.handlerRole = role;
214
+ }
215
+ /**
216
+ * Grants the provider's handler `secretsmanager:GetSecretValue` on the given
217
+ * secret. Repeated grants of the same secret are deduplicated.
218
+ *
219
+ * @param secret The secret holding a Cloudflare API token.
220
+ */
221
+ grantSecretRead(secret) {
222
+ const arn = secret.secretArn;
223
+ if (this.grantedSecrets.has(arn)) {
224
+ return;
225
+ }
226
+ this.grantedSecrets.add(arn);
227
+ this.handlerRole.addToPrincipalPolicy(new iam.PolicyStatement({
228
+ actions: ['secretsmanager:GetSecretValue'],
229
+ resources: [arn],
230
+ }));
231
+ }
232
+ }
233
+ exports.CloudflareCertificateProvider = CloudflareCertificateProvider;
234
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,2CAA6B;AAC7B,iDAAmC;AACnC,yDAA2C;AAC3C,+DAAiD;AACjD,qEAA+D;AAC/D,2DAA6C;AAE7C,+EAAiE;AACjE,2CAAuC;AAEvC;;GAEG;AACH,MAAM,WAAW,GAAG,2BAA2B,CAAC;AAEhD;;GAEG;AACH,MAAM,uBAAuB,GAAG,mCAAmC,CAAC;AAEpE;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IACnE,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAC;IACjB,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CAAC,KAAgB,EAAE,EAAU,EAAE,IAAY,EAAE,OAAqB;IACtF,oEAAoE;IACpE,2EAA2E;IAC3E,wEAAwE;IACxE,yEAAyE;IACzE,gDAAgD;IAChD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAG,IAAI,kCAAc,CAAC,KAAK,EAAE,EAAE,EAAE;QAC5C,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC;QACzB,WAAW;QACX,gBAAgB,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE,gBAAgB,CAAC;QAC5E,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,WAAW;QACnC,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM;QACxC,OAAO;QACP,UAAU,EAAE,GAAG;QACf,QAAQ,EAAE;YACR,MAAM,EAAE,IAAI;YACZ,SAAS,EAAE,IAAI;YACf,eAAe,EAAE,CAAC,YAAY,CAAC;SAChC;KACF,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,EAAE,UAAU,EAAE;QACzD,YAAY,EAAE,eAAe,OAAO,CAAC,YAAY,EAAE;QACnD,SAAS,EAAE,IAAI,CAAC,aAAa,CAAC,SAAS;QACvC,aAAa,EAAE,GAAG,CAAC,aAAa,CAAC,OAAO;KACzC,CAAC,CAAC;IACH,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAErC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,+BAA+B,EAAE,uBAAuB,CAAC,CAAC;IAC5E,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC;AACzC,CAAC;AAED;;;;;;;GAOG;AACH,MAAa,qBAAsB,SAAQ,sBAAS;IAClD;;;;OAIG;IACI,MAAM,CAAC,WAAW,CAAC,KAAgB;QACxC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,WAAW,CAAsC,CAAC;QAC3F,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,IAAI,qBAAqB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;IACvD,CAAC;IAED;;OAEG;IACa,YAAY,CAAS;IAErC;;;OAGG;IACc,WAAW,CAAY;IAExC;;OAEG;IACc,cAAc,GAAgB,IAAI,GAAG,EAAE,CAAC;IAEzD,YAAoB,KAAgB,EAAE,EAAU;QAC9C,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAEjB,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAE9F,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE;YAC/D,cAAc,EAAE,OAAO;SACxB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED;;;;;OAKG;IACI,eAAe,CAAC,MAA8B;QACnD,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC;QAC7B,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC;YAC5D,OAAO,EAAE,CAAC,+BAA+B,CAAC;YAC1C,SAAS,EAAE,CAAC,GAAG,CAAC;SACjB,CAAC,CAAC,CAAC;IACN,CAAC;CACF;AA7DD,sDA6DC;AAED;;;;;;GAMG;AACH,MAAa,6BAA8B,SAAQ,sBAAS;IAC1D;;;;OAIG;IACI,MAAM,CAAC,WAAW,CAAC,KAAgB;QACxC,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,uBAAuB,CAA8C,CAAC;QAC/G,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,OAAO,IAAI,6BAA6B,CAAC,KAAK,EAAE,uBAAuB,CAAC,CAAC;IAC3E,CAAC;IAED;;;OAGG;IACa,YAAY,CAAS;IAErC;;OAEG;IACc,WAAW,CAAY;IAExC;;OAEG;IACc,cAAc,GAAgB,IAAI,GAAG,EAAE,CAAC;IAEzD,YAAoB,KAAgB,EAAE,EAAU;QAC9C,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAEjB,sEAAsE;QACtE,qEAAqE;QACrE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QAE5F,IAAI,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC;YAChD,OAAO,EAAE,CAAC,yBAAyB,CAAC;YACpC,SAAS,EAAE,CAAC,GAAG,CAAC;SACjB,CAAC,CAAC,CAAC;QAEJ,MAAM,QAAQ,GAAG,IAAI,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE;YAC/D,cAAc,EAAE,OAAO;SACxB,CAAC,CAAC;QAEH,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;QAC1C,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED;;;;;OAKG;IACI,eAAe,CAAC,MAA8B;QACnD,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC;QAC7B,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,IAAI,GAAG,CAAC,eAAe,CAAC;YAC5D,OAAO,EAAE,CAAC,+BAA+B,CAAC;YAC1C,SAAS,EAAE,CAAC,GAAG,CAAC;SACjB,CAAC,CAAC,CAAC;IACN,CAAC;CACF;AApED,sEAoEC"}