@aigne/afs-dns 1.11.0-beta.10

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/index.mjs ADDED
@@ -0,0 +1,2013 @@
1
+ import { createRequire } from "node:module";
2
+ import { minimatch } from "minimatch";
3
+ import { z } from "zod";
4
+ import { ChangeResourceRecordSetsCommand, GetHostedZoneCommand, ListHostedZonesCommand, ListResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
5
+
6
+ //#region rolldown:runtime
7
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
8
+
9
+ //#endregion
10
+ //#region src/clouddns/adapter.ts
11
+ var CloudDNSAdapter = class {
12
+ client;
13
+ zoneCache = /* @__PURE__ */ new Map();
14
+ constructor(options = {}) {
15
+ if (options.client) this.client = options.client;
16
+ else {
17
+ const { DNS } = __require("@google-cloud/dns");
18
+ this.client = new DNS({
19
+ projectId: options.projectId,
20
+ keyFilename: options.keyFilename
21
+ });
22
+ }
23
+ }
24
+ /**
25
+ * List all managed zones
26
+ */
27
+ async listZones() {
28
+ const [zones] = await this.client.getZones();
29
+ return zones.map((zone) => ({
30
+ domain: zone.meta.dnsName.replace(/\.$/, ""),
31
+ id: zone.meta.id,
32
+ provider: "clouddns",
33
+ nameservers: zone.meta.nameServers ?? [],
34
+ recordCount: void 0
35
+ }));
36
+ }
37
+ /**
38
+ * List all record names in a zone
39
+ */
40
+ async listRecords(zoneDomain) {
41
+ const [records] = await (await this.getZone(zoneDomain)).getRecords();
42
+ const normalizedDomain = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
43
+ const names = /* @__PURE__ */ new Set();
44
+ for (const record of records) {
45
+ const name = this.extractRecordName(record.name, normalizedDomain);
46
+ names.add(name);
47
+ }
48
+ names.add("_zone");
49
+ return Array.from(names);
50
+ }
51
+ /**
52
+ * Get all records for a specific name
53
+ */
54
+ async getRecord(zoneDomain, name) {
55
+ const [records] = await (await this.getZone(zoneDomain)).getRecords();
56
+ const normalizedDomain = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
57
+ const fqdn = name === "@" ? zoneDomain : `${name}.${zoneDomain}`;
58
+ const targetName = name === "@" ? normalizedDomain : `${name}.${normalizedDomain}`;
59
+ return {
60
+ fqdn,
61
+ records: records.filter((r) => r.name === targetName).map((r) => this.parseRecord(r))
62
+ };
63
+ }
64
+ /**
65
+ * Get zone metadata
66
+ */
67
+ async getZoneMetadata(zoneDomain) {
68
+ const zone = await this.getZone(zoneDomain);
69
+ return {
70
+ domain: zone.meta.dnsName.replace(/\.$/, ""),
71
+ id: zone.meta.id,
72
+ provider: "clouddns",
73
+ nameservers: zone.meta.nameServers ?? [],
74
+ recordCount: void 0
75
+ };
76
+ }
77
+ /**
78
+ * Set records for a name (create or update)
79
+ */
80
+ async setRecord(zoneDomain, name, records, _options) {
81
+ const zone = await this.getZone(zoneDomain);
82
+ const normalizedDomain = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
83
+ const fqdn = name === "@" ? normalizedDomain : `${name}.${normalizedDomain}`;
84
+ const [existingRecords] = await zone.getRecords();
85
+ const toDelete = existingRecords.filter((r) => {
86
+ if (r.name !== fqdn) return false;
87
+ return records.some((newRec) => newRec.type === r.type);
88
+ });
89
+ for (const record of records) {
90
+ const changeConfig = { add: this.buildCloudRecord(zone, fqdn, record) };
91
+ const matchingDeletes = toDelete.filter((r) => r.type === record.type);
92
+ if (matchingDeletes.length > 0) changeConfig.delete = matchingDeletes;
93
+ await zone.createChange(changeConfig);
94
+ }
95
+ }
96
+ /**
97
+ * Delete records for a name
98
+ */
99
+ async deleteRecord(zoneDomain, name, type, _options) {
100
+ const zone = await this.getZone(zoneDomain);
101
+ const normalizedDomain = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
102
+ const fqdn = name === "@" ? normalizedDomain : `${name}.${normalizedDomain}`;
103
+ const [existingRecords] = await zone.getRecords();
104
+ const toDelete = existingRecords.filter((r) => {
105
+ if (r.name !== fqdn) return false;
106
+ if (type && r.type !== type) return false;
107
+ return true;
108
+ });
109
+ if (toDelete.length === 0) return;
110
+ await zone.createChange({ delete: toDelete });
111
+ }
112
+ /**
113
+ * Get zone by domain name
114
+ */
115
+ async getZone(zoneDomain) {
116
+ if (this.zoneCache.has(zoneDomain)) return this.zoneCache.get(zoneDomain);
117
+ const [zones] = await this.client.getZones();
118
+ const normalizedDomain = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
119
+ const zone = zones.find((z$1) => z$1.meta.dnsName === normalizedDomain);
120
+ if (!zone) {
121
+ const error = /* @__PURE__ */ new Error(`Zone not found: ${zoneDomain}`);
122
+ error.name = "ZoneNotFound";
123
+ throw error;
124
+ }
125
+ this.zoneCache.set(zoneDomain, zone);
126
+ return zone;
127
+ }
128
+ /**
129
+ * Extract record name from FQDN
130
+ */
131
+ extractRecordName(fqdn, zoneDomain) {
132
+ const normalizedFqdn = fqdn.endsWith(".") ? fqdn : `${fqdn}.`;
133
+ const normalizedZone = zoneDomain.endsWith(".") ? zoneDomain : `${zoneDomain}.`;
134
+ if (normalizedFqdn === normalizedZone) return "@";
135
+ return normalizedFqdn.replace(/* @__PURE__ */ new RegExp(`\\.?${normalizedZone.replace(/\./g, "\\.")}$`), "") || "@";
136
+ }
137
+ /**
138
+ * Parse Cloud DNS record to DNSRecord
139
+ */
140
+ parseRecord(record) {
141
+ const { type, ttl, data } = record;
142
+ switch (type) {
143
+ case "A":
144
+ case "AAAA":
145
+ case "NS":
146
+ case "TXT":
147
+ case "PTR": return {
148
+ type,
149
+ ttl,
150
+ values: data
151
+ };
152
+ case "CNAME": return {
153
+ type: "CNAME",
154
+ ttl,
155
+ values: data[0] ?? ""
156
+ };
157
+ case "MX": return {
158
+ type: "MX",
159
+ ttl,
160
+ values: data.map((v) => {
161
+ const [priority, value] = v.split(" ", 2);
162
+ return {
163
+ priority: Number.parseInt(priority ?? "0", 10),
164
+ value: value ?? ""
165
+ };
166
+ })
167
+ };
168
+ case "SRV": return {
169
+ type: "SRV",
170
+ ttl,
171
+ values: data.map((v) => {
172
+ const [priority, weight, port, target] = v.split(" ", 4);
173
+ return {
174
+ priority: Number.parseInt(priority ?? "0", 10),
175
+ weight: Number.parseInt(weight ?? "0", 10),
176
+ port: Number.parseInt(port ?? "0", 10),
177
+ target: target ?? ""
178
+ };
179
+ })
180
+ };
181
+ case "CAA": return {
182
+ type: "CAA",
183
+ ttl,
184
+ values: data.map((v) => {
185
+ const match = v.match(/^(\d+)\s+(\w+)\s+"?(.+?)"?$/);
186
+ if (match) return {
187
+ flags: Number.parseInt(match[1] ?? "0", 10),
188
+ tag: match[2] ?? "",
189
+ value: match[3] ?? ""
190
+ };
191
+ return {
192
+ flags: 0,
193
+ tag: "",
194
+ value: v
195
+ };
196
+ })
197
+ };
198
+ case "SOA": {
199
+ const soaParts = data[0]?.split(" ") ?? [];
200
+ return {
201
+ type: "SOA",
202
+ ttl,
203
+ values: {
204
+ mname: soaParts[0] ?? "",
205
+ rname: soaParts[1] ?? "",
206
+ serial: Number.parseInt(soaParts[2] ?? "0", 10),
207
+ refresh: Number.parseInt(soaParts[3] ?? "0", 10),
208
+ retry: Number.parseInt(soaParts[4] ?? "0", 10),
209
+ expire: Number.parseInt(soaParts[5] ?? "0", 10),
210
+ minimum: Number.parseInt(soaParts[6] ?? "0", 10)
211
+ }
212
+ };
213
+ }
214
+ default: return {
215
+ type,
216
+ ttl,
217
+ values: data
218
+ };
219
+ }
220
+ }
221
+ /**
222
+ * Build Cloud DNS record from DNSRecord
223
+ */
224
+ buildCloudRecord(zone, fqdn, record) {
225
+ const { type, ttl, values } = record;
226
+ let data;
227
+ switch (type) {
228
+ case "A":
229
+ case "AAAA":
230
+ case "NS":
231
+ case "TXT":
232
+ case "PTR":
233
+ data = Array.isArray(values) ? values : [values];
234
+ break;
235
+ case "CNAME":
236
+ data = [typeof values === "string" ? values : values[0] ?? ""];
237
+ break;
238
+ case "MX":
239
+ data = values.map((mx) => `${mx.priority} ${mx.value}`);
240
+ break;
241
+ case "SRV":
242
+ data = values.map((srv) => `${srv.priority} ${srv.weight} ${srv.port} ${srv.target}`);
243
+ break;
244
+ case "CAA":
245
+ data = values.map((caa) => `${caa.flags} ${caa.tag} "${caa.value}"`);
246
+ break;
247
+ default: data = Array.isArray(values) ? values : [values];
248
+ }
249
+ return zone.record(type.toLowerCase(), {
250
+ name: fqdn,
251
+ ttl,
252
+ data
253
+ });
254
+ }
255
+ };
256
+
257
+ //#endregion
258
+ //#region src/errors/index.ts
259
+ /**
260
+ * DNS Provider Errors
261
+ *
262
+ * Custom error types and Route53 error mapping.
263
+ */
264
+ var AFSDNSError = class extends Error {
265
+ code;
266
+ cause;
267
+ constructor(message, options) {
268
+ super(message);
269
+ this.name = "AFSDNSError";
270
+ this.code = options?.code ?? "DNS_ERROR";
271
+ this.cause = options?.cause;
272
+ }
273
+ };
274
+ var AFSDNSNotFoundError = class extends AFSDNSError {
275
+ constructor(message, options) {
276
+ super(message, {
277
+ code: "DNS_NOT_FOUND",
278
+ ...options
279
+ });
280
+ this.name = "AFSDNSNotFoundError";
281
+ }
282
+ };
283
+ var AFSDNSPermissionDeniedError = class extends AFSDNSError {
284
+ constructor(message, options) {
285
+ super(message, {
286
+ code: "DNS_PERMISSION_DENIED",
287
+ ...options
288
+ });
289
+ this.name = "AFSDNSPermissionDeniedError";
290
+ }
291
+ };
292
+ var AFSDNSRateLimitError = class extends AFSDNSError {
293
+ retryAfter;
294
+ constructor(message, options) {
295
+ super(message, {
296
+ code: "DNS_RATE_LIMIT",
297
+ ...options
298
+ });
299
+ this.name = "AFSDNSRateLimitError";
300
+ this.retryAfter = options?.retryAfter ?? 1;
301
+ }
302
+ };
303
+ var AFSDNSInvalidArgumentError = class extends AFSDNSError {
304
+ constructor(message, options) {
305
+ super(message, {
306
+ code: "DNS_INVALID_ARGUMENT",
307
+ ...options
308
+ });
309
+ this.name = "AFSDNSInvalidArgumentError";
310
+ }
311
+ };
312
+ var AFSDNSConflictError = class extends AFSDNSError {
313
+ constructor(message, options) {
314
+ super(message, {
315
+ code: "DNS_CONFLICT",
316
+ ...options
317
+ });
318
+ this.name = "AFSDNSConflictError";
319
+ }
320
+ };
321
+ var AFSDNSNetworkError = class extends AFSDNSError {
322
+ constructor(message, options) {
323
+ super(message, {
324
+ code: "DNS_NETWORK_ERROR",
325
+ ...options
326
+ });
327
+ this.name = "AFSDNSNetworkError";
328
+ }
329
+ };
330
+ /**
331
+ * Map Route53 error to AFS DNS error
332
+ */
333
+ function mapRoute53Error(error) {
334
+ const errorName = error.name;
335
+ const sanitizedMessage = sanitizeErrorMessage(error.message);
336
+ switch (errorName) {
337
+ case "NoSuchHostedZone": return new AFSDNSNotFoundError("Zone not found", { cause: error });
338
+ case "AccessDenied":
339
+ case "AccessDeniedException": return new AFSDNSPermissionDeniedError("Access denied to DNS zone", { cause: error });
340
+ case "Throttling":
341
+ case "ThrottlingException": return new AFSDNSRateLimitError("Rate limit exceeded, please retry", {
342
+ cause: error,
343
+ retryAfter: extractRetryAfter(error)
344
+ });
345
+ case "InvalidInput":
346
+ case "InvalidChangeBatch": return new AFSDNSInvalidArgumentError(`Invalid DNS record: ${sanitizedMessage}`, { cause: error });
347
+ case "PriorRequestNotComplete": return new AFSDNSConflictError("Previous DNS change is still processing, please retry", { cause: error });
348
+ case "NetworkingError":
349
+ case "ECONNREFUSED":
350
+ case "ETIMEDOUT": return new AFSDNSNetworkError("Network error connecting to DNS service", { cause: error });
351
+ case "ServiceUnavailable": return new AFSDNSNetworkError("DNS service temporarily unavailable", { cause: error });
352
+ default: return new AFSDNSError(`DNS operation failed: ${sanitizedMessage}`, { cause: error });
353
+ }
354
+ }
355
+ /**
356
+ * Check if an error is retryable
357
+ */
358
+ function isRetryableError(error) {
359
+ return [
360
+ "Throttling",
361
+ "ThrottlingException",
362
+ "PriorRequestNotComplete",
363
+ "ServiceUnavailable",
364
+ "ETIMEDOUT",
365
+ "ECONNRESET"
366
+ ].includes(error.name);
367
+ }
368
+ /**
369
+ * Sanitize error message to remove sensitive information
370
+ */
371
+ function sanitizeErrorMessage(message) {
372
+ let sanitized = message;
373
+ sanitized = sanitized.replace(/\b\d{12}\b/g, "***");
374
+ sanitized = sanitized.replace(/arn:aws:[^:\s]+:[^:\s]*:[^:\s]*:[^\s]+/g, "arn:aws:***");
375
+ sanitized = sanitized.replace(/\/hostedzone\/[A-Z0-9]+/g, "/hostedzone/***");
376
+ sanitized = sanitized.replace(/\bZ[A-Z0-9]{10,}\b/g, "***");
377
+ sanitized = sanitized.replace(/user\/[^\s,]+/g, "user/***");
378
+ sanitized = sanitized.replace(/role\/[^\s,]+/g, "role/***");
379
+ return sanitized;
380
+ }
381
+ /**
382
+ * Extract retry-after hint from error if available
383
+ */
384
+ function extractRetryAfter(_error) {
385
+ return 1;
386
+ }
387
+
388
+ //#endregion
389
+ //#region src/permissions/types.ts
390
+ /**
391
+ * Preset definitions
392
+ */
393
+ const PRESETS = {
394
+ safe: {
395
+ allowDelete: false,
396
+ dangerous: {
397
+ modify_ns: false,
398
+ modify_root: false,
399
+ modify_wildcard: false,
400
+ delete_zone: false
401
+ }
402
+ },
403
+ standard: {
404
+ allowDelete: true,
405
+ dangerous: {
406
+ modify_ns: false,
407
+ modify_root: false,
408
+ modify_wildcard: false,
409
+ delete_zone: false
410
+ }
411
+ },
412
+ full: {
413
+ allowDelete: true,
414
+ dangerous: {
415
+ modify_ns: true,
416
+ modify_root: true,
417
+ modify_wildcard: true,
418
+ delete_zone: false
419
+ }
420
+ }
421
+ };
422
+
423
+ //#endregion
424
+ //#region src/permissions/checker.ts
425
+ /**
426
+ * DNS Permission Checker
427
+ *
428
+ * Validates operations against configured permissions.
429
+ */
430
+ /**
431
+ * Check if an operation is allowed
432
+ */
433
+ function checkPermission(context, config) {
434
+ if (context.operation === "read") return { allowed: true };
435
+ const effective = getEffectivePermissions(config);
436
+ if (context.operation === "delete" && !effective.allowDelete) return {
437
+ allowed: false,
438
+ reason: "Delete operations are not allowed with 'safe' preset",
439
+ requiredConfig: "preset = \"standard\" or \"full\"",
440
+ deniedOperation: "delete"
441
+ };
442
+ const dangerousOp = detectDangerousOperation(context);
443
+ if (dangerousOp) {
444
+ if (!effective.dangerous[dangerousOp]) return {
445
+ allowed: false,
446
+ reason: `Operation '${dangerousOp}' is not allowed`,
447
+ requiredConfig: `[mounts.options.dangerous]\n${dangerousOp} = true`,
448
+ deniedOperation: dangerousOp
449
+ };
450
+ }
451
+ return { allowed: true };
452
+ }
453
+ /**
454
+ * Get effective permissions by merging preset with explicit config
455
+ */
456
+ function getEffectivePermissions(config) {
457
+ const presetConfig = PRESETS[config.preset ?? "standard"];
458
+ const result = {
459
+ allowDelete: presetConfig.allowDelete,
460
+ dangerous: { ...presetConfig.dangerous }
461
+ };
462
+ if (config.dangerous) {
463
+ for (const [key, value] of Object.entries(config.dangerous)) if (value !== void 0) result.dangerous[key] = value;
464
+ }
465
+ return result;
466
+ }
467
+ /**
468
+ * Detect if an operation is dangerous
469
+ */
470
+ function detectDangerousOperation(context) {
471
+ const { name, type, operation } = context;
472
+ if (operation === "read") return null;
473
+ if (name === "@") return "modify_root";
474
+ if (type === "NS") return "modify_ns";
475
+ if (name === "*" || name.startsWith("*.")) return "modify_wildcard";
476
+ return null;
477
+ }
478
+ /**
479
+ * Create a user-friendly error message for permission denial
480
+ */
481
+ function formatPermissionError(result) {
482
+ let message = `Permission denied: ${result.reason}`;
483
+ if (result.requiredConfig) message += `\n\nTo enable this operation, add to your config:\n${result.requiredConfig}`;
484
+ return message;
485
+ }
486
+
487
+ //#endregion
488
+ //#region src/security/audit-log.ts
489
+ var AuditLogger = class {
490
+ config;
491
+ entries = [];
492
+ constructor(config = {}) {
493
+ this.config = config;
494
+ }
495
+ /**
496
+ * Log a write operation
497
+ */
498
+ logWrite(params) {
499
+ const entry = {
500
+ timestamp: Date.now(),
501
+ action: params.action,
502
+ zone: params.zone,
503
+ recordName: params.recordName,
504
+ recordType: params.recordType,
505
+ values: this.config.maskValues ? this.maskValues(params.values) : params.values,
506
+ recordCount: Array.isArray(params.values) ? params.values.length : 1,
507
+ context: params.context
508
+ };
509
+ this.emit(entry);
510
+ }
511
+ /**
512
+ * Log a delete operation
513
+ */
514
+ logDelete(params) {
515
+ const entry = {
516
+ timestamp: Date.now(),
517
+ action: "delete",
518
+ zone: params.zone,
519
+ recordName: params.recordName,
520
+ recordType: params.recordType,
521
+ context: params.context
522
+ };
523
+ this.emit(entry);
524
+ }
525
+ /**
526
+ * Log a permission denied event
527
+ */
528
+ logPermissionDenied(params) {
529
+ const entry = {
530
+ timestamp: Date.now(),
531
+ action: "denied",
532
+ zone: params.zone,
533
+ recordName: params.recordName,
534
+ operation: params.operation,
535
+ reason: params.reason,
536
+ context: params.context
537
+ };
538
+ this.emit(entry);
539
+ }
540
+ /**
541
+ * Get recent log entries (if storeInMemory is enabled)
542
+ */
543
+ getRecentEntries(count) {
544
+ return this.entries.slice(-count).reverse();
545
+ }
546
+ /**
547
+ * Get entries by zone (if storeInMemory is enabled)
548
+ */
549
+ getEntriesByZone(zone) {
550
+ return this.entries.filter((e) => e.zone === zone);
551
+ }
552
+ /**
553
+ * Emit a log entry
554
+ */
555
+ emit(entry) {
556
+ if (this.config.handler) this.config.handler(entry);
557
+ if (this.config.storeInMemory) {
558
+ this.entries.push(entry);
559
+ if (this.config.maxEntries && this.entries.length > this.config.maxEntries) this.entries = this.entries.slice(-this.config.maxEntries);
560
+ }
561
+ }
562
+ /**
563
+ * Mask sensitive values
564
+ */
565
+ maskValues(values) {
566
+ return values.map((v) => {
567
+ if (typeof v === "string") return this.maskString(v);
568
+ return v;
569
+ });
570
+ }
571
+ /**
572
+ * Mask sensitive parts of a string
573
+ */
574
+ maskString(value) {
575
+ return value.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, "***@***.***");
576
+ }
577
+ };
578
+
579
+ //#endregion
580
+ //#region src/security/input-sanitizer.ts
581
+ /**
582
+ * Sanitize a TXT record value
583
+ */
584
+ function sanitizeTXTValue(value) {
585
+ return value;
586
+ }
587
+ /**
588
+ * Reject strings containing null bytes
589
+ */
590
+ function rejectNullBytes(value) {
591
+ const values = Array.isArray(value) ? value : [value];
592
+ for (const v of values) if (v.includes("\0")) throw new Error("Value contains null byte which is not allowed");
593
+ }
594
+ /**
595
+ * Validate input length based on type
596
+ */
597
+ function validateInputLength(value, type) {
598
+ switch (type) {
599
+ case "domain":
600
+ if (value.length > 253) return {
601
+ valid: false,
602
+ error: "Domain name exceeds 253 characters"
603
+ };
604
+ break;
605
+ case "label": {
606
+ const labels = value.split(".");
607
+ for (const label of labels) if (label.length > 63) return {
608
+ valid: false,
609
+ error: "Label exceeds 63 characters"
610
+ };
611
+ break;
612
+ }
613
+ case "txt":
614
+ if (value.length > 4e3) return {
615
+ valid: false,
616
+ error: "TXT value exceeds 4000 characters"
617
+ };
618
+ break;
619
+ }
620
+ return { valid: true };
621
+ }
622
+ /**
623
+ * Escape special characters based on record type
624
+ */
625
+ function escapeSpecialCharacters(value, recordType) {
626
+ if (!value) return "";
627
+ switch (recordType) {
628
+ case "TXT": return value;
629
+ default: return value;
630
+ }
631
+ }
632
+ /**
633
+ * Sanitize a record value based on type
634
+ */
635
+ function sanitizeRecordValue(value, recordType) {
636
+ try {
637
+ rejectNullBytes(value);
638
+ } catch (e) {
639
+ return {
640
+ valid: false,
641
+ error: e.message
642
+ };
643
+ }
644
+ const lengthResult = validateInputLength(value, recordType === "TXT" ? "txt" : "domain");
645
+ if (!lengthResult.valid) return {
646
+ valid: false,
647
+ error: lengthResult.error
648
+ };
649
+ return {
650
+ valid: true,
651
+ value: escapeSpecialCharacters(value, recordType)
652
+ };
653
+ }
654
+
655
+ //#endregion
656
+ //#region src/security/rate-limiter.ts
657
+ var RateLimiter = class {
658
+ tokens;
659
+ lastRefill;
660
+ config;
661
+ queue = [];
662
+ constructor(config) {
663
+ this.config = config;
664
+ this.tokens = config.maxRequests;
665
+ this.lastRefill = Date.now();
666
+ }
667
+ /**
668
+ * Try to acquire a token without waiting
669
+ * @returns true if token acquired, false if rate limited
670
+ */
671
+ async tryAcquire() {
672
+ this.refillTokens();
673
+ if (this.tokens > 0) {
674
+ this.tokens--;
675
+ return true;
676
+ }
677
+ return false;
678
+ }
679
+ /**
680
+ * Acquire a token, waiting if necessary
681
+ * Only works if queueRequests is enabled
682
+ */
683
+ async acquire() {
684
+ this.refillTokens();
685
+ if (this.tokens > 0) {
686
+ this.tokens--;
687
+ return;
688
+ }
689
+ if (!this.config.queueRequests) throw new Error("Rate limit exceeded");
690
+ return new Promise((resolve) => {
691
+ this.queue.push(resolve);
692
+ this.scheduleQueueProcessing();
693
+ });
694
+ }
695
+ /**
696
+ * Get remaining tokens
697
+ */
698
+ getRemaining() {
699
+ this.refillTokens();
700
+ return this.tokens;
701
+ }
702
+ /**
703
+ * Get time until next refill in milliseconds
704
+ */
705
+ getResetTime() {
706
+ const elapsed = Date.now() - this.lastRefill;
707
+ return Math.max(0, this.config.windowMs - elapsed);
708
+ }
709
+ /**
710
+ * Refill tokens based on elapsed time
711
+ */
712
+ refillTokens() {
713
+ const now = Date.now();
714
+ if (now - this.lastRefill >= this.config.windowMs) {
715
+ this.tokens = this.config.maxRequests;
716
+ this.lastRefill = now;
717
+ }
718
+ }
719
+ /**
720
+ * Schedule processing of queued requests
721
+ */
722
+ scheduleQueueProcessing() {
723
+ if (this.queue.length === 0) return;
724
+ const delay = this.getResetTime();
725
+ setTimeout(() => {
726
+ this.refillTokens();
727
+ this.processQueue();
728
+ }, delay);
729
+ }
730
+ /**
731
+ * Process queued requests
732
+ */
733
+ processQueue() {
734
+ while (this.queue.length > 0 && this.tokens > 0) {
735
+ const resolve = this.queue.shift();
736
+ if (resolve) {
737
+ this.tokens--;
738
+ resolve();
739
+ }
740
+ }
741
+ if (this.queue.length > 0) this.scheduleQueueProcessing();
742
+ }
743
+ };
744
+ /**
745
+ * Create a rate limiter with Route53 defaults
746
+ */
747
+ function createRoute53RateLimiter(overrides) {
748
+ return new RateLimiter({
749
+ maxRequests: 5,
750
+ windowMs: 1e3,
751
+ queueRequests: false,
752
+ ...overrides
753
+ });
754
+ }
755
+
756
+ //#endregion
757
+ //#region src/types/record.ts
758
+ const IPV4_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
759
+ /**
760
+ * Check if string is a valid IPv4 address
761
+ */
762
+ function isValidIPv4(ip) {
763
+ const match = ip.match(IPV4_REGEX);
764
+ if (!match) return false;
765
+ for (let i = 1; i <= 4; i++) {
766
+ const octetStr = match[i];
767
+ if (!octetStr) return false;
768
+ const octet = Number.parseInt(octetStr, 10);
769
+ if (octet < 0 || octet > 255) return false;
770
+ }
771
+ return true;
772
+ }
773
+ /**
774
+ * Check if string is a valid IPv6 address
775
+ */
776
+ function isValidIPv6(ip) {
777
+ if (ip === "::1" || ip === "::") return true;
778
+ if ((ip.match(/::/g) || []).length > 1) return false;
779
+ const parts = ip.split(":");
780
+ if (parts.length < 3 || parts.length > 8) return false;
781
+ for (const part of parts) {
782
+ if (part === "") continue;
783
+ if (!/^[0-9a-fA-F]{1,4}$/.test(part)) return false;
784
+ }
785
+ return true;
786
+ }
787
+ /**
788
+ * Check if string is a valid hostname (not IP)
789
+ */
790
+ function isValidHostname(hostname) {
791
+ if (!hostname) return false;
792
+ if (isValidIPv4(hostname) || isValidIPv6(hostname)) return false;
793
+ const labels = (hostname.endsWith(".") ? hostname.slice(0, -1) : hostname).split(".");
794
+ for (const label of labels) {
795
+ if (!label) return false;
796
+ if (label.length > 63) return false;
797
+ if (label.startsWith("-") || label.endsWith("-")) return false;
798
+ if (!/^[a-zA-Z0-9-]+$/.test(label)) return false;
799
+ }
800
+ return true;
801
+ }
802
+ /**
803
+ * Validate A record values
804
+ */
805
+ function validateARecord(values) {
806
+ if (!values || values.length === 0) return {
807
+ valid: false,
808
+ error: "A record requires at least one IPv4 address"
809
+ };
810
+ for (const value of values) if (!isValidIPv4(value)) return {
811
+ valid: false,
812
+ error: `Invalid IPv4 address: ${value}`
813
+ };
814
+ return { valid: true };
815
+ }
816
+ /**
817
+ * Validate AAAA record values
818
+ */
819
+ function validateAAAARecord(values) {
820
+ if (!values || values.length === 0) return {
821
+ valid: false,
822
+ error: "AAAA record requires at least one IPv6 address"
823
+ };
824
+ for (const value of values) if (!isValidIPv6(value)) return {
825
+ valid: false,
826
+ error: `Invalid IPv6 address: ${value}`
827
+ };
828
+ return { valid: true };
829
+ }
830
+ /**
831
+ * Validate CNAME record value
832
+ */
833
+ function validateCNAMERecord(value) {
834
+ if (!value) return {
835
+ valid: false,
836
+ error: "CNAME record requires a hostname"
837
+ };
838
+ if (!isValidHostname(value)) return {
839
+ valid: false,
840
+ error: `Invalid hostname for CNAME: ${value}`
841
+ };
842
+ return { valid: true };
843
+ }
844
+ /**
845
+ * Validate MX record values
846
+ */
847
+ function validateMXRecord(values) {
848
+ if (!values || values.length === 0) return {
849
+ valid: false,
850
+ error: "MX record requires at least one value"
851
+ };
852
+ for (const mx of values) {
853
+ if (mx.priority < 0 || mx.priority > 65535) return {
854
+ valid: false,
855
+ error: `MX priority must be 0-65535, got: ${mx.priority}`
856
+ };
857
+ if (!isValidHostname(mx.value)) return {
858
+ valid: false,
859
+ error: `Invalid hostname for MX: ${mx.value}`
860
+ };
861
+ }
862
+ return { valid: true };
863
+ }
864
+ /**
865
+ * Validate TXT record values
866
+ */
867
+ function validateTXTRecord(values) {
868
+ if (!values) return {
869
+ valid: false,
870
+ error: "TXT record requires values array"
871
+ };
872
+ return { valid: true };
873
+ }
874
+ /**
875
+ * Validate NS record values
876
+ */
877
+ function validateNSRecord(values) {
878
+ if (!values || values.length === 0) return {
879
+ valid: false,
880
+ error: "NS record requires at least one nameserver"
881
+ };
882
+ for (const value of values) if (!isValidHostname(value)) return {
883
+ valid: false,
884
+ error: `Invalid hostname for NS: ${value}`
885
+ };
886
+ return { valid: true };
887
+ }
888
+ /**
889
+ * Validate SRV record values
890
+ */
891
+ function validateSRVRecord(values) {
892
+ if (!values || values.length === 0) return {
893
+ valid: false,
894
+ error: "SRV record requires at least one value"
895
+ };
896
+ for (const srv of values) {
897
+ if (srv.priority < 0 || srv.priority > 65535) return {
898
+ valid: false,
899
+ error: `SRV priority must be 0-65535, got: ${srv.priority}`
900
+ };
901
+ if (srv.weight < 0 || srv.weight > 65535) return {
902
+ valid: false,
903
+ error: `SRV weight must be 0-65535, got: ${srv.weight}`
904
+ };
905
+ if (srv.port < 0 || srv.port > 65535) return {
906
+ valid: false,
907
+ error: `SRV port must be 0-65535, got: ${srv.port}`
908
+ };
909
+ if (!isValidHostname(srv.target) && srv.target !== ".") return {
910
+ valid: false,
911
+ error: `Invalid hostname for SRV target: ${srv.target}`
912
+ };
913
+ }
914
+ return { valid: true };
915
+ }
916
+ /**
917
+ * Validate CAA record values
918
+ */
919
+ function validateCAARecord(values) {
920
+ if (!values || values.length === 0) return {
921
+ valid: false,
922
+ error: "CAA record requires at least one value"
923
+ };
924
+ for (const caa of values) {
925
+ if (caa.flags < 0 || caa.flags > 255) return {
926
+ valid: false,
927
+ error: `CAA flags must be 0-255, got: ${caa.flags}`
928
+ };
929
+ if (!caa.tag) return {
930
+ valid: false,
931
+ error: "CAA record requires a tag"
932
+ };
933
+ }
934
+ return { valid: true };
935
+ }
936
+ /**
937
+ * Validate TTL value
938
+ */
939
+ function validateTTL(ttl) {
940
+ if (!Number.isInteger(ttl)) return {
941
+ valid: false,
942
+ error: "TTL must be an integer"
943
+ };
944
+ if (ttl < 0) return {
945
+ valid: false,
946
+ error: "TTL cannot be negative"
947
+ };
948
+ if (ttl > 2147483647) return {
949
+ valid: false,
950
+ error: "TTL cannot exceed 2147483647"
951
+ };
952
+ return { valid: true };
953
+ }
954
+ /**
955
+ * Validate a complete DNS record
956
+ */
957
+ function validateRecord(record) {
958
+ const errors = [];
959
+ if (!record.type) {
960
+ errors.push("Record type is required");
961
+ return {
962
+ valid: false,
963
+ errors
964
+ };
965
+ }
966
+ if (record.ttl !== void 0) {
967
+ const ttlResult = validateTTL(record.ttl);
968
+ if (!ttlResult.valid) errors.push(ttlResult.error);
969
+ }
970
+ let valuesResult;
971
+ switch (record.type) {
972
+ case "A":
973
+ valuesResult = validateARecord(record.values);
974
+ break;
975
+ case "AAAA":
976
+ valuesResult = validateAAAARecord(record.values);
977
+ break;
978
+ case "CNAME":
979
+ valuesResult = validateCNAMERecord(Array.isArray(record.values) ? record.values[0] ?? "" : record.values);
980
+ break;
981
+ case "MX":
982
+ valuesResult = validateMXRecord(record.values);
983
+ break;
984
+ case "TXT":
985
+ valuesResult = validateTXTRecord(record.values);
986
+ break;
987
+ case "NS":
988
+ valuesResult = validateNSRecord(record.values);
989
+ break;
990
+ case "SRV":
991
+ valuesResult = validateSRVRecord(record.values);
992
+ break;
993
+ case "CAA":
994
+ valuesResult = validateCAARecord(record.values);
995
+ break;
996
+ case "PTR":
997
+ valuesResult = validateCNAMERecord(Array.isArray(record.values) ? record.values[0] ?? "" : record.values);
998
+ break;
999
+ case "SOA":
1000
+ valuesResult = { valid: true };
1001
+ break;
1002
+ default: valuesResult = {
1003
+ valid: false,
1004
+ error: `Unknown record type: ${record.type}`
1005
+ };
1006
+ }
1007
+ if (!valuesResult.valid) errors.push(valuesResult.error);
1008
+ return {
1009
+ valid: errors.length === 0,
1010
+ errors: errors.length > 0 ? errors : void 0
1011
+ };
1012
+ }
1013
+
1014
+ //#endregion
1015
+ //#region src/provider/dns-provider.ts
1016
+ var DNSProvider = class {
1017
+ name;
1018
+ description;
1019
+ accessMode;
1020
+ zone;
1021
+ adapter;
1022
+ permissions;
1023
+ auditLogger;
1024
+ rateLimiter;
1025
+ /**
1026
+ * Schema for configuration validation
1027
+ */
1028
+ static schema() {
1029
+ return z.object({ zone: z.string() });
1030
+ }
1031
+ /**
1032
+ * Provider manifest for URI-based discovery
1033
+ */
1034
+ static manifest() {
1035
+ return {
1036
+ name: "dns",
1037
+ description: "DNS zone management — Route53 and Google Cloud DNS.\n- List, create, update, and delete DNS records (A, AAAA, CNAME, MX, TXT, etc.)\n- Audit logging and rate limiting for safe zone modifications\n- Path structure: `/{zone}/{record-name}`",
1038
+ uriTemplate: "dns://{zone}",
1039
+ category: "cloud-dns",
1040
+ schema: z.object({ zone: z.string() }),
1041
+ tags: ["dns", "cloud"]
1042
+ };
1043
+ }
1044
+ constructor(options) {
1045
+ this.zone = options.zone;
1046
+ this.adapter = options.adapter;
1047
+ this.accessMode = options.accessMode ?? "readonly";
1048
+ this.permissions = options.permissions ?? { preset: "standard" };
1049
+ this.auditLogger = new AuditLogger(options.auditLog);
1050
+ this.rateLimiter = options.rateLimiting !== false ? createRoute53RateLimiter() : null;
1051
+ this.name = `dns:${this.zone}`;
1052
+ this.description = `DNS zone: ${this.zone}`;
1053
+ }
1054
+ async list(path, options) {
1055
+ const normalizedPath = this.normalizePath(path);
1056
+ if ((options?.maxDepth ?? 1) === 0) return { data: [] };
1057
+ if (normalizedPath === "/") {
1058
+ const entries = [];
1059
+ const names = await this.adapter.listRecords(this.zone);
1060
+ for (const name of names) entries.push({
1061
+ id: name,
1062
+ path: `/${name}`,
1063
+ summary: name === "_zone" ? "Zone metadata" : `DNS record: ${name}`,
1064
+ meta: {
1065
+ kind: name === "_zone" ? "dns:zone-meta" : "dns:record",
1066
+ childrenCount: 0
1067
+ }
1068
+ });
1069
+ return { data: entries };
1070
+ }
1071
+ const recordName = this.extractRecordName(normalizedPath);
1072
+ if ((await this.adapter.getRecord(this.zone, recordName)).records.length === 0 && recordName !== "_zone") return { data: [] };
1073
+ return { data: [{
1074
+ id: recordName,
1075
+ path: normalizedPath,
1076
+ summary: recordName === "_zone" ? "Zone metadata" : `DNS record: ${recordName}`,
1077
+ meta: {
1078
+ kind: recordName === "_zone" ? "dns:zone-meta" : "dns:record",
1079
+ childrenCount: 0
1080
+ }
1081
+ }] };
1082
+ }
1083
+ async read(path, _options) {
1084
+ const normalizedPath = this.normalizePath(path);
1085
+ if (normalizedPath === "/.meta/.capabilities") return this.readCapabilities();
1086
+ const { recordName, type } = this.parsePathWithQuery(normalizedPath);
1087
+ if (recordName === "_zone") {
1088
+ const metadata = await this.adapter.getZoneMetadata(this.zone);
1089
+ return { data: {
1090
+ id: "_zone",
1091
+ path: "/_zone",
1092
+ summary: `Zone metadata for ${this.zone}`,
1093
+ meta: { kind: "dns:zone-meta" },
1094
+ content: metadata
1095
+ } };
1096
+ }
1097
+ const recordSet = await this.adapter.getRecord(this.zone, recordName);
1098
+ let records = recordSet.records;
1099
+ if (type) records = records.filter((r) => r.type === type);
1100
+ if (records.length === 0) return { data: void 0 };
1101
+ return { data: {
1102
+ id: recordName,
1103
+ path: normalizedPath.split("?")[0] ?? normalizedPath,
1104
+ summary: `DNS records for ${recordSet.fqdn}`,
1105
+ meta: { kind: "dns:record" },
1106
+ content: {
1107
+ fqdn: recordSet.fqdn,
1108
+ records
1109
+ }
1110
+ } };
1111
+ }
1112
+ async write(path, content, _options) {
1113
+ if (this.accessMode !== "readwrite") throw new Error(`DNS provider is readonly, cannot write to ${path}`);
1114
+ const normalizedPath = this.normalizePath(path);
1115
+ const recordName = this.extractRecordName(normalizedPath);
1116
+ if (recordName === "validate-zone") return this.handleValidateZone();
1117
+ if (recordName === "export-zone") return this.handleExportZone();
1118
+ if (recordName === "_zone") throw new Error("Cannot write to _zone metadata");
1119
+ const record = content.content;
1120
+ if (!record || !record.type) throw new Error("Invalid record content: must include type");
1121
+ this.sanitizeRecordValues(record);
1122
+ const validation = validateRecord(record);
1123
+ if (!validation.valid) throw new Error(`Invalid record: ${validation.errors?.join(", ")}`);
1124
+ const permResult = checkPermission({
1125
+ name: recordName,
1126
+ type: record.type,
1127
+ operation: "write"
1128
+ }, this.permissions);
1129
+ if (!permResult.allowed) {
1130
+ this.auditLogger.logPermissionDenied({
1131
+ zone: this.zone,
1132
+ recordName,
1133
+ operation: permResult.deniedOperation ?? "write",
1134
+ reason: formatPermissionError(permResult)
1135
+ });
1136
+ throw new Error(formatPermissionError(permResult));
1137
+ }
1138
+ await this.acquireRateLimit();
1139
+ const action = (await this.adapter.getRecord(this.zone, recordName)).records.length > 0 ? "update" : "create";
1140
+ await this.adapter.setRecord(this.zone, recordName, [record]);
1141
+ this.auditLogger.logWrite({
1142
+ zone: this.zone,
1143
+ recordName,
1144
+ recordType: record.type,
1145
+ action,
1146
+ values: this.extractValues(record)
1147
+ });
1148
+ return { data: {
1149
+ id: recordName,
1150
+ path: normalizedPath,
1151
+ summary: `Updated DNS record: ${recordName}`,
1152
+ meta: { kind: "dns:record" },
1153
+ content: record
1154
+ } };
1155
+ }
1156
+ async delete(path, _options) {
1157
+ if (this.accessMode !== "readwrite") throw new Error(`DNS provider is readonly, cannot delete ${path}`);
1158
+ const normalizedPath = this.normalizePath(path);
1159
+ const { recordName, type } = this.parsePathWithQuery(normalizedPath);
1160
+ if (recordName === "_zone") throw new Error("Cannot delete _zone metadata");
1161
+ const permResult = checkPermission({
1162
+ name: recordName,
1163
+ type,
1164
+ operation: "delete"
1165
+ }, this.permissions);
1166
+ if (!permResult.allowed) {
1167
+ this.auditLogger.logPermissionDenied({
1168
+ zone: this.zone,
1169
+ recordName,
1170
+ operation: permResult.deniedOperation ?? "delete",
1171
+ reason: formatPermissionError(permResult)
1172
+ });
1173
+ throw new Error(formatPermissionError(permResult));
1174
+ }
1175
+ await this.acquireRateLimit();
1176
+ await this.adapter.deleteRecord(this.zone, recordName, type);
1177
+ this.auditLogger.logDelete({
1178
+ zone: this.zone,
1179
+ recordName,
1180
+ recordType: type
1181
+ });
1182
+ return { message: `Record ${recordName}${type ? ` (${type})` : ""} deleted` };
1183
+ }
1184
+ normalizePath(path) {
1185
+ if (!path.startsWith("/")) return `/${path}`;
1186
+ return path;
1187
+ }
1188
+ extractRecordName(path) {
1189
+ return (path.replace(/^\//, "").split("?")[0] ?? "") || "@";
1190
+ }
1191
+ parsePathWithQuery(path) {
1192
+ const [pathPart, queryPart] = path.split("?");
1193
+ const recordName = this.extractRecordName(pathPart ?? path);
1194
+ let type;
1195
+ if (queryPart) type = new URLSearchParams(queryPart).get("type") ?? void 0;
1196
+ return {
1197
+ recordName,
1198
+ type
1199
+ };
1200
+ }
1201
+ /**
1202
+ * Sanitize record values to prevent injection attacks
1203
+ */
1204
+ sanitizeRecordValues(record) {
1205
+ const { values } = record;
1206
+ if (typeof values === "string") {
1207
+ rejectNullBytes(values);
1208
+ const result = sanitizeRecordValue(values, record.type);
1209
+ if (!result.valid) throw new Error(`Invalid record value: ${result.error}`);
1210
+ } else if (Array.isArray(values)) {
1211
+ for (const value of values) if (typeof value === "string") {
1212
+ rejectNullBytes(value);
1213
+ const result = sanitizeRecordValue(value, record.type);
1214
+ if (!result.valid) throw new Error(`Invalid record value: ${result.error}`);
1215
+ }
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Acquire rate limit token (waits if necessary)
1220
+ */
1221
+ async acquireRateLimit() {
1222
+ if (this.rateLimiter) {
1223
+ if (!await this.rateLimiter.tryAcquire()) throw new Error("Rate limit exceeded. Please retry later.");
1224
+ }
1225
+ }
1226
+ /**
1227
+ * Extract values from a record for audit logging
1228
+ */
1229
+ extractValues(record) {
1230
+ const { values } = record;
1231
+ if (typeof values === "string") return [values];
1232
+ if (Array.isArray(values)) return values;
1233
+ return [values];
1234
+ }
1235
+ async stat(path) {
1236
+ const normalizedPath = this.normalizePath(path);
1237
+ const recordName = this.extractRecordName(normalizedPath);
1238
+ const recordSet = await this.adapter.getRecord(this.zone, recordName);
1239
+ if (recordSet.records.length === 0 && recordName !== "_zone") throw new Error(`Record not found: ${recordName}`);
1240
+ const firstRecord = recordSet.records[0];
1241
+ return { data: {
1242
+ id: recordName,
1243
+ path: normalizedPath,
1244
+ meta: {
1245
+ kind: recordName === "_zone" ? "dns:zone-meta" : "dns:record",
1246
+ recordType: firstRecord?.type,
1247
+ ttl: firstRecord?.ttl,
1248
+ valueCount: recordSet.records.reduce((count, r) => {
1249
+ const v = r.values;
1250
+ if (Array.isArray(v)) return count + v.length;
1251
+ return count + 1;
1252
+ }, 0)
1253
+ }
1254
+ } };
1255
+ }
1256
+ async explain(path) {
1257
+ const normalizedPath = this.normalizePath(path);
1258
+ const recordName = this.extractRecordName(normalizedPath);
1259
+ if (normalizedPath === "/" || recordName === "@") return this.explainRoot();
1260
+ if (recordName === "_zone") return this.explainZone();
1261
+ throw new Error(`Cannot explain path: ${path}`);
1262
+ }
1263
+ async explainRoot() {
1264
+ const metadata = await this.adapter.getZoneMetadata(this.zone);
1265
+ const names = await this.adapter.listRecords(this.zone);
1266
+ return {
1267
+ format: "markdown",
1268
+ content: `# DNS Zone: ${this.zone}
1269
+
1270
+ - **Backend**: ${metadata.provider || "unknown"}
1271
+ - **Zone ID**: ${metadata.id || "unknown"}
1272
+ - **Record Count**: ${names.length}
1273
+ - **Nameservers**: ${(metadata.nameservers || []).join(", ") || "unknown"}
1274
+ `
1275
+ };
1276
+ }
1277
+ async explainZone() {
1278
+ const metadata = await this.adapter.getZoneMetadata(this.zone);
1279
+ const apex = await this.adapter.getRecord(this.zone, "@");
1280
+ const soaRecord = apex.records.find((r) => r.type === "SOA");
1281
+ const nsRecords = apex.records.filter((r) => r.type === "NS");
1282
+ const lines = [`# Zone Details: ${this.zone}`];
1283
+ lines.push("");
1284
+ lines.push(`- **Domain**: ${metadata.domain || this.zone}`);
1285
+ lines.push(`- **Provider**: ${metadata.provider || "unknown"}`);
1286
+ lines.push(`- **Record Count**: ${metadata.recordCount ?? "unknown"}`);
1287
+ lines.push("");
1288
+ if (soaRecord) {
1289
+ lines.push("## SOA Record");
1290
+ lines.push("");
1291
+ const values = Array.isArray(soaRecord.values) ? soaRecord.values.join(" ") : String(soaRecord.values);
1292
+ lines.push(`\`${values}\``);
1293
+ lines.push("");
1294
+ }
1295
+ if (nsRecords.length > 0) {
1296
+ lines.push("## NS Records");
1297
+ lines.push("");
1298
+ for (const ns of nsRecords) {
1299
+ const values = Array.isArray(ns.values) ? ns.values : [ns.values];
1300
+ for (const v of values) lines.push(`- ${v}`);
1301
+ }
1302
+ lines.push("");
1303
+ }
1304
+ return {
1305
+ format: "markdown",
1306
+ content: lines.join("\n")
1307
+ };
1308
+ }
1309
+ async search(_path, query) {
1310
+ const names = await this.adapter.listRecords(this.zone);
1311
+ const results = [];
1312
+ for (const name of names) {
1313
+ if (name === "_zone") continue;
1314
+ const nameMatches = minimatch(name, query);
1315
+ let valueMatches = false;
1316
+ if (!nameMatches) {
1317
+ const recordSet = await this.adapter.getRecord(this.zone, name);
1318
+ for (const record of recordSet.records) {
1319
+ const values = Array.isArray(record.values) ? record.values : [record.values];
1320
+ for (const v of values) if (String(v).includes(query)) {
1321
+ valueMatches = true;
1322
+ break;
1323
+ }
1324
+ if (valueMatches) break;
1325
+ }
1326
+ }
1327
+ if (nameMatches || valueMatches) results.push({
1328
+ id: name,
1329
+ path: `/${name}`,
1330
+ summary: `DNS record: ${name}`,
1331
+ meta: { kind: "dns:record" }
1332
+ });
1333
+ }
1334
+ return { data: results };
1335
+ }
1336
+ readCapabilities() {
1337
+ const actionCatalogs = [];
1338
+ actionCatalogs.push({
1339
+ kind: "dns:zone",
1340
+ description: "Zone-level operations",
1341
+ catalog: [{
1342
+ name: "validate-zone",
1343
+ description: "Validate zone integrity (SOA, NS records)",
1344
+ inputSchema: {
1345
+ type: "object",
1346
+ properties: {},
1347
+ description: "No parameters required"
1348
+ }
1349
+ }, {
1350
+ name: "export-zone",
1351
+ description: "Export zone as BIND zone file format",
1352
+ inputSchema: {
1353
+ type: "object",
1354
+ properties: {},
1355
+ description: "No parameters required"
1356
+ }
1357
+ }],
1358
+ discovery: {
1359
+ pathTemplate: "/{action}",
1360
+ note: "Write to /validate-zone or /export-zone to execute"
1361
+ }
1362
+ });
1363
+ const operations = {
1364
+ read: true,
1365
+ list: true,
1366
+ write: this.accessMode === "readwrite",
1367
+ delete: this.accessMode === "readwrite",
1368
+ search: true,
1369
+ exec: false,
1370
+ stat: true,
1371
+ explain: true
1372
+ };
1373
+ return { data: {
1374
+ id: "/.meta/.capabilities",
1375
+ path: "/.meta/.capabilities",
1376
+ content: {
1377
+ schemaVersion: 1,
1378
+ provider: this.name,
1379
+ version: "1.0.0",
1380
+ description: this.description,
1381
+ tools: [],
1382
+ actions: actionCatalogs,
1383
+ operations
1384
+ },
1385
+ meta: { kind: "afs:capabilities" }
1386
+ } };
1387
+ }
1388
+ async handleValidateZone() {
1389
+ const apex = await this.adapter.getRecord(this.zone, "@");
1390
+ const checks = [];
1391
+ const soaRecord = apex.records.find((r) => r.type === "SOA");
1392
+ checks.push({
1393
+ name: "SOA",
1394
+ type: "SOA",
1395
+ valid: !!soaRecord,
1396
+ message: soaRecord ? "SOA record present" : "Missing SOA record"
1397
+ });
1398
+ const nsRecords = apex.records.filter((r) => r.type === "NS");
1399
+ checks.push({
1400
+ name: "NS",
1401
+ type: "NS",
1402
+ valid: nsRecords.length > 0,
1403
+ message: nsRecords.length > 0 ? `${nsRecords.length} NS record(s) present` : "Missing NS records"
1404
+ });
1405
+ const allValid = checks.every((c) => c.valid);
1406
+ return { data: {
1407
+ id: "validate-zone",
1408
+ path: "/validate-zone",
1409
+ summary: `Zone validation: ${allValid ? "PASSED" : "FAILED"}`,
1410
+ meta: { kind: "dns:action-result" },
1411
+ content: {
1412
+ valid: allValid,
1413
+ checks,
1414
+ zone: this.zone
1415
+ }
1416
+ } };
1417
+ }
1418
+ async handleExportZone() {
1419
+ const names = await this.adapter.listRecords(this.zone);
1420
+ const lines = [];
1421
+ lines.push(`; Zone file for ${this.zone}`);
1422
+ lines.push(`$ORIGIN ${this.zone}.`);
1423
+ lines.push("");
1424
+ for (const name of names) {
1425
+ if (name === "_zone") continue;
1426
+ const recordSet = await this.adapter.getRecord(this.zone, name);
1427
+ const displayName = name === "@" ? "@" : name;
1428
+ for (const record of recordSet.records) {
1429
+ const ttl = record.ttl ?? 300;
1430
+ const values = Array.isArray(record.values) ? record.values : [record.values];
1431
+ for (const value of values) lines.push(`${displayName}\t${ttl}\tIN\t${record.type}\t${value}`);
1432
+ }
1433
+ }
1434
+ lines.push("");
1435
+ return { data: {
1436
+ id: "export-zone",
1437
+ path: "/export-zone",
1438
+ summary: `BIND zone file for ${this.zone}`,
1439
+ meta: { kind: "dns:action-result" },
1440
+ content: lines.join("\n")
1441
+ } };
1442
+ }
1443
+ };
1444
+
1445
+ //#endregion
1446
+ //#region src/route53/parser.ts
1447
+ /**
1448
+ * Parse Route53 hosted zones into DNSZone format
1449
+ */
1450
+ function parseHostedZones(zones) {
1451
+ return zones.map((zone) => ({
1452
+ domain: zone.Name.replace(/\.$/, ""),
1453
+ id: zone.Id.replace("/hostedzone/", ""),
1454
+ provider: "route53",
1455
+ nameservers: [],
1456
+ recordCount: zone.ResourceRecordSetCount
1457
+ }));
1458
+ }
1459
+ /**
1460
+ * Parse Route53 resource record sets into grouped records by name
1461
+ */
1462
+ function parseResourceRecordSets(recordSets, zoneName) {
1463
+ const grouped = /* @__PURE__ */ new Map();
1464
+ for (const rs of recordSets) {
1465
+ const name = normalizeRecordName(rs.Name, zoneName);
1466
+ const record = parseResourceRecordSet(rs);
1467
+ if (!grouped.has(name)) grouped.set(name, []);
1468
+ grouped.get(name).push(record);
1469
+ }
1470
+ return Array.from(grouped.entries()).map(([name, records]) => ({
1471
+ name,
1472
+ records
1473
+ }));
1474
+ }
1475
+ /**
1476
+ * Parse a single Route53 resource record set
1477
+ */
1478
+ function parseResourceRecordSet(rs) {
1479
+ const type = rs.Type;
1480
+ if (rs.AliasTarget) return {
1481
+ type,
1482
+ ttl: 0,
1483
+ values: [],
1484
+ alias: {
1485
+ hostedZoneId: rs.AliasTarget.HostedZoneId,
1486
+ dnsName: rs.AliasTarget.DNSName,
1487
+ evaluateTargetHealth: rs.AliasTarget.EvaluateTargetHealth
1488
+ }
1489
+ };
1490
+ return {
1491
+ type,
1492
+ ttl: rs.TTL ?? 0,
1493
+ values: parseRecordValues(type, rs.ResourceRecords?.map((r) => r.Value) ?? [])
1494
+ };
1495
+ }
1496
+ /**
1497
+ * Parse record values based on type
1498
+ */
1499
+ function parseRecordValues(type, rawValues) {
1500
+ switch (type) {
1501
+ case "A":
1502
+ case "AAAA":
1503
+ case "NS":
1504
+ case "TXT":
1505
+ case "PTR": return rawValues;
1506
+ case "CNAME": return rawValues[0] ?? "";
1507
+ case "MX": return rawValues.map(parseMXValue);
1508
+ case "SRV": return rawValues.map(parseSRVValue);
1509
+ case "CAA": return rawValues.map(parseCAAValue);
1510
+ case "SOA": return parseSOAValue(rawValues[0] ?? "");
1511
+ default: return rawValues;
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Parse MX record value: "10 mail.example.com"
1516
+ */
1517
+ function parseMXValue(value) {
1518
+ const parts = value.split(/\s+/, 2);
1519
+ return {
1520
+ priority: Number.parseInt(parts[0] ?? "0", 10),
1521
+ value: parts[1] ?? ""
1522
+ };
1523
+ }
1524
+ /**
1525
+ * Parse SRV record value: "10 5 5060 sip.example.com"
1526
+ */
1527
+ function parseSRVValue(value) {
1528
+ const parts = value.split(/\s+/, 4);
1529
+ return {
1530
+ priority: Number.parseInt(parts[0] ?? "0", 10),
1531
+ weight: Number.parseInt(parts[1] ?? "0", 10),
1532
+ port: Number.parseInt(parts[2] ?? "0", 10),
1533
+ target: parts[3] ?? ""
1534
+ };
1535
+ }
1536
+ /**
1537
+ * Parse CAA record value: '0 issue "letsencrypt.org"'
1538
+ */
1539
+ function parseCAAValue(value) {
1540
+ const match = value.match(/^(\d+)\s+(\w+)\s+"?([^"]*)"?$/);
1541
+ if (!match) return {
1542
+ flags: 0,
1543
+ tag: "",
1544
+ value: ""
1545
+ };
1546
+ return {
1547
+ flags: Number.parseInt(match[1] ?? "0", 10),
1548
+ tag: match[2] ?? "",
1549
+ value: match[3] ?? ""
1550
+ };
1551
+ }
1552
+ /**
1553
+ * Parse SOA record value
1554
+ */
1555
+ function parseSOAValue(value) {
1556
+ const parts = value.split(/\s+/);
1557
+ return {
1558
+ mname: parts[0] ?? "",
1559
+ rname: parts[1] ?? "",
1560
+ serial: Number.parseInt(parts[2] ?? "0", 10) || 0,
1561
+ refresh: Number.parseInt(parts[3] ?? "0", 10) || 0,
1562
+ retry: Number.parseInt(parts[4] ?? "0", 10) || 0,
1563
+ expire: Number.parseInt(parts[5] ?? "0", 10) || 0,
1564
+ minimum: Number.parseInt(parts[6] ?? "0", 10) || 0
1565
+ };
1566
+ }
1567
+ /**
1568
+ * Normalize a Route53 record name to our format
1569
+ *
1570
+ * - Removes trailing dot
1571
+ * - Converts apex to @
1572
+ * - Extracts subdomain part
1573
+ */
1574
+ function normalizeRecordName(fqdn, zoneName) {
1575
+ const name = fqdn.endsWith(".") ? fqdn.slice(0, -1) : fqdn;
1576
+ const normalizedZone = zoneName.endsWith(".") ? zoneName.slice(0, -1) : zoneName;
1577
+ if (name.toLowerCase() === normalizedZone.toLowerCase()) return "@";
1578
+ const suffix = `.${normalizedZone}`;
1579
+ if (name.toLowerCase().endsWith(suffix.toLowerCase())) return name.slice(0, -suffix.length);
1580
+ return name;
1581
+ }
1582
+
1583
+ //#endregion
1584
+ //#region src/route53/adapter.ts
1585
+ /**
1586
+ * Route53 DNS Adapter
1587
+ *
1588
+ * AWS Route53 implementation of the DNS provider interface.
1589
+ */
1590
+ var Route53Adapter = class {
1591
+ client;
1592
+ zoneIdCache = /* @__PURE__ */ new Map();
1593
+ constructor(options = {}) {
1594
+ if (options.client) this.client = options.client;
1595
+ else this.client = new Route53Client({
1596
+ region: options.region ?? "us-east-1",
1597
+ endpoint: options.endpoint,
1598
+ credentials: options.credentials
1599
+ });
1600
+ }
1601
+ /**
1602
+ * List all hosted zones
1603
+ */
1604
+ async listZones() {
1605
+ const zones = [];
1606
+ let marker;
1607
+ do {
1608
+ const response = await this.client.send(new ListHostedZonesCommand({ Marker: marker }));
1609
+ if (response.HostedZones) zones.push(...response.HostedZones);
1610
+ marker = response.IsTruncated ? response.NextMarker : void 0;
1611
+ } while (marker);
1612
+ return parseHostedZones(zones);
1613
+ }
1614
+ /**
1615
+ * List all record names in a zone
1616
+ */
1617
+ async listRecords(zoneDomain) {
1618
+ const zoneId = await this.getZoneId(zoneDomain);
1619
+ const parsed = parseResourceRecordSets(await this.fetchAllRecordSets(zoneId), zoneDomain);
1620
+ const names = /* @__PURE__ */ new Set();
1621
+ for (const rs of parsed) names.add(rs.name);
1622
+ names.add("_zone");
1623
+ return Array.from(names);
1624
+ }
1625
+ /**
1626
+ * Get all records for a specific name
1627
+ */
1628
+ async getRecord(zoneDomain, name) {
1629
+ const zoneId = await this.getZoneId(zoneDomain);
1630
+ const match = parseResourceRecordSets(await this.fetchAllRecordSets(zoneId), zoneDomain).find((rs) => rs.name === name);
1631
+ return {
1632
+ fqdn: name === "@" ? zoneDomain : `${name}.${zoneDomain}`,
1633
+ records: match?.records ?? []
1634
+ };
1635
+ }
1636
+ /**
1637
+ * Get zone metadata
1638
+ */
1639
+ async getZoneMetadata(zoneDomain) {
1640
+ const zoneId = await this.getZoneId(zoneDomain);
1641
+ const response = await this.client.send(new GetHostedZoneCommand({ Id: zoneId }));
1642
+ const zone = response.HostedZone;
1643
+ const nameservers = response.DelegationSet?.NameServers ?? [];
1644
+ return {
1645
+ domain: zone.Name.replace(/\.$/, ""),
1646
+ id: zone.Id.replace("/hostedzone/", ""),
1647
+ provider: "route53",
1648
+ nameservers,
1649
+ recordCount: zone.ResourceRecordSetCount
1650
+ };
1651
+ }
1652
+ /**
1653
+ * Set records for a name (create or update)
1654
+ */
1655
+ async setRecord(zoneDomain, name, records, _options) {
1656
+ const zoneId = await this.getZoneId(zoneDomain);
1657
+ const fqdn = this.buildFQDN(name, zoneDomain);
1658
+ const changes = [];
1659
+ for (const record of records) changes.push({
1660
+ Action: "UPSERT",
1661
+ ResourceRecordSet: this.buildResourceRecordSet(fqdn, record)
1662
+ });
1663
+ await this.client.send(new ChangeResourceRecordSetsCommand({
1664
+ HostedZoneId: zoneId,
1665
+ ChangeBatch: {
1666
+ Changes: changes,
1667
+ Comment: `AFS DNS Provider: set ${name}`
1668
+ }
1669
+ }));
1670
+ }
1671
+ /**
1672
+ * Delete records for a name
1673
+ */
1674
+ async deleteRecord(zoneDomain, name, type, _options) {
1675
+ const zoneId = await this.getZoneId(zoneDomain);
1676
+ const fqdn = this.buildFQDN(name, zoneDomain);
1677
+ const toDelete = (await this.fetchAllRecordSets(zoneId)).filter((rs) => {
1678
+ if (rs.Name?.replace(/\.$/, "") !== fqdn.replace(/\.$/, "")) return false;
1679
+ if (type && rs.Type !== type) return false;
1680
+ return true;
1681
+ });
1682
+ if (toDelete.length === 0) return;
1683
+ const changes = toDelete.map((rs) => ({
1684
+ Action: "DELETE",
1685
+ ResourceRecordSet: rs
1686
+ }));
1687
+ await this.client.send(new ChangeResourceRecordSetsCommand({
1688
+ HostedZoneId: zoneId,
1689
+ ChangeBatch: {
1690
+ Changes: changes,
1691
+ Comment: `AFS DNS Provider: delete ${name}`
1692
+ }
1693
+ }));
1694
+ }
1695
+ /**
1696
+ * Get zone ID from domain name
1697
+ */
1698
+ async getZoneId(zoneDomain) {
1699
+ if (this.zoneIdCache.has(zoneDomain)) return this.zoneIdCache.get(zoneDomain);
1700
+ const zone = (await this.listZones()).find((z$1) => z$1.domain.toLowerCase() === zoneDomain.toLowerCase());
1701
+ if (!zone) {
1702
+ const error = /* @__PURE__ */ new Error(`Zone not found: ${zoneDomain}`);
1703
+ error.name = "NoSuchHostedZone";
1704
+ throw error;
1705
+ }
1706
+ this.zoneIdCache.set(zoneDomain, zone.id);
1707
+ return zone.id;
1708
+ }
1709
+ /**
1710
+ * Fetch all record sets for a zone (handles pagination)
1711
+ */
1712
+ async fetchAllRecordSets(zoneId) {
1713
+ const recordSets = [];
1714
+ let startRecordName;
1715
+ let startRecordType;
1716
+ do {
1717
+ const response = await this.client.send(new ListResourceRecordSetsCommand({
1718
+ HostedZoneId: zoneId,
1719
+ StartRecordName: startRecordName,
1720
+ StartRecordType: startRecordType
1721
+ }));
1722
+ if (response.ResourceRecordSets) recordSets.push(...response.ResourceRecordSets);
1723
+ if (response.IsTruncated) {
1724
+ startRecordName = response.NextRecordName;
1725
+ startRecordType = response.NextRecordType;
1726
+ } else startRecordName = void 0;
1727
+ } while (startRecordName);
1728
+ return recordSets;
1729
+ }
1730
+ /**
1731
+ * Build FQDN from name and zone
1732
+ */
1733
+ buildFQDN(name, zoneDomain) {
1734
+ if (name === "@") return `${zoneDomain}.`;
1735
+ return `${name}.${zoneDomain}.`;
1736
+ }
1737
+ /**
1738
+ * Build Route53 ResourceRecordSet from DNSRecord
1739
+ */
1740
+ buildResourceRecordSet(fqdn, record) {
1741
+ const base = {
1742
+ Name: fqdn,
1743
+ Type: record.type,
1744
+ TTL: record.ttl
1745
+ };
1746
+ const resourceRecords = this.buildResourceRecords(record);
1747
+ if (resourceRecords.length > 0) base.ResourceRecords = resourceRecords;
1748
+ return base;
1749
+ }
1750
+ /**
1751
+ * Build Route53 ResourceRecords from DNSRecord values
1752
+ */
1753
+ buildResourceRecords(record) {
1754
+ const { type, values } = record;
1755
+ switch (type) {
1756
+ case "A":
1757
+ case "AAAA":
1758
+ case "NS":
1759
+ case "TXT":
1760
+ case "PTR": return values.map((v) => ({ Value: v }));
1761
+ case "CNAME": return [{ Value: typeof values === "string" ? values : values[0] ?? "" }];
1762
+ case "MX": return values.map((mx) => ({ Value: `${mx.priority} ${mx.value}` }));
1763
+ case "SRV": return values.map((srv) => ({ Value: `${srv.priority} ${srv.weight} ${srv.port} ${srv.target}` }));
1764
+ case "CAA": return values.map((caa) => ({ Value: `${caa.flags} ${caa.tag} "${caa.value}"` }));
1765
+ default: return [];
1766
+ }
1767
+ }
1768
+ };
1769
+
1770
+ //#endregion
1771
+ //#region src/validation/name-validator.ts
1772
+ const MAX_LABEL_LENGTH = 63;
1773
+ const MAX_DOMAIN_LENGTH = 253;
1774
+ const LABEL_REGEX = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
1775
+ const SERVICE_LABEL_REGEX = /^_[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^_[a-zA-Z0-9]$/;
1776
+ /**
1777
+ * Validate a domain name per RFC 1035
1778
+ *
1779
+ * @param domain - Domain name to validate (e.g., "example.com")
1780
+ * @returns Validation result
1781
+ */
1782
+ function validateDomainName(domain) {
1783
+ if (!domain) return {
1784
+ valid: false,
1785
+ error: "Domain name cannot be empty"
1786
+ };
1787
+ const normalized = domain.endsWith(".") ? domain.slice(0, -1) : domain;
1788
+ if (normalized.length > MAX_DOMAIN_LENGTH) return {
1789
+ valid: false,
1790
+ error: `Domain name exceeds ${MAX_DOMAIN_LENGTH} characters`
1791
+ };
1792
+ const labels = normalized.split(".");
1793
+ for (let i = 0; i < labels.length; i++) {
1794
+ const label = labels[i];
1795
+ if (!label) return {
1796
+ valid: false,
1797
+ error: "Domain name contains empty label"
1798
+ };
1799
+ if (label.length > MAX_LABEL_LENGTH) return {
1800
+ valid: false,
1801
+ error: `Label "${label}" exceeds ${MAX_LABEL_LENGTH} characters`
1802
+ };
1803
+ if (label.startsWith("-")) return {
1804
+ valid: false,
1805
+ error: `Label "${label}" cannot start with hyphen`
1806
+ };
1807
+ if (label.endsWith("-")) return {
1808
+ valid: false,
1809
+ error: `Label "${label}" cannot end with hyphen`
1810
+ };
1811
+ if (!LABEL_REGEX.test(label) && !label.startsWith("xn--")) return {
1812
+ valid: false,
1813
+ error: `Label "${label}" contains invalid characters`
1814
+ };
1815
+ }
1816
+ return { valid: true };
1817
+ }
1818
+ /**
1819
+ * Normalize a domain name to lowercase
1820
+ *
1821
+ * DNS is case-insensitive per RFC 1035.
1822
+ *
1823
+ * @param domain - Domain name to normalize
1824
+ * @returns Normalized lowercase domain name
1825
+ */
1826
+ function normalizeDomainName(domain) {
1827
+ return (domain.endsWith(".") ? domain.slice(0, -1) : domain).toLowerCase();
1828
+ }
1829
+ /**
1830
+ * Validate a record name within a zone
1831
+ *
1832
+ * Record names can be:
1833
+ * - @ for apex (root domain)
1834
+ * - * for wildcard
1835
+ * - *.subdomain for scoped wildcard
1836
+ * - Simple names like "www", "api"
1837
+ * - Multi-level like "blog.dev"
1838
+ * - Service records like "_dmarc", "_acme-challenge"
1839
+ *
1840
+ * @param name - Record name to validate
1841
+ * @param zone - The zone this record belongs to
1842
+ * @returns Validation result
1843
+ */
1844
+ function validateRecordName(name, zone) {
1845
+ if (!name) return {
1846
+ valid: false,
1847
+ error: "Record name cannot be empty"
1848
+ };
1849
+ if (name === "@") return { valid: true };
1850
+ if (name === "*") return { valid: true };
1851
+ if (name.includes("*")) {
1852
+ if (!name.startsWith("*.") && name !== "*") return {
1853
+ valid: false,
1854
+ error: "Wildcard (*) can only appear at the start of a record name"
1855
+ };
1856
+ }
1857
+ const nameWithoutWildcard = name.startsWith("*.") ? name.slice(2) : name;
1858
+ if (`${nameWithoutWildcard}.${zone}`.length > MAX_DOMAIN_LENGTH) return {
1859
+ valid: false,
1860
+ error: `Full domain name would exceed ${MAX_DOMAIN_LENGTH} characters`
1861
+ };
1862
+ const labels = nameWithoutWildcard.split(".");
1863
+ for (const label of labels) {
1864
+ if (!label) return {
1865
+ valid: false,
1866
+ error: "Record name contains empty label"
1867
+ };
1868
+ if (label.length > MAX_LABEL_LENGTH) return {
1869
+ valid: false,
1870
+ error: `Label "${label}" exceeds ${MAX_LABEL_LENGTH} characters`
1871
+ };
1872
+ if (label.startsWith("_")) {
1873
+ if (!SERVICE_LABEL_REGEX.test(label)) return {
1874
+ valid: false,
1875
+ error: `Invalid service label: ${label}`
1876
+ };
1877
+ } else {
1878
+ if (label.startsWith("-") || label.endsWith("-")) return {
1879
+ valid: false,
1880
+ error: `Label "${label}" cannot start or end with hyphen`
1881
+ };
1882
+ if (!LABEL_REGEX.test(label)) return {
1883
+ valid: false,
1884
+ error: `Label "${label}" contains invalid characters`
1885
+ };
1886
+ }
1887
+ }
1888
+ return { valid: true };
1889
+ }
1890
+
1891
+ //#endregion
1892
+ //#region src/validation/write-validator.ts
1893
+ /**
1894
+ * Validate a complete write operation
1895
+ */
1896
+ function validateWriteOperation(recordName, records) {
1897
+ const setResult = validateRecordSet(records);
1898
+ if (!setResult.valid) return setResult;
1899
+ const soaResult = checkSOAProtection(records);
1900
+ if (!soaResult.valid) return soaResult;
1901
+ const apexResult = checkCNAMEAtApex(recordName, records);
1902
+ if (!apexResult.valid) return apexResult;
1903
+ const conflictResult = checkCNAMEConflict(records);
1904
+ if (!conflictResult.valid) return conflictResult;
1905
+ const dupeResult = checkDuplicateValues(records);
1906
+ if (!dupeResult.valid) return dupeResult;
1907
+ return { valid: true };
1908
+ }
1909
+ /**
1910
+ * Validate a record set (array of records)
1911
+ */
1912
+ function validateRecordSet(records) {
1913
+ if (!records || records.length === 0) return {
1914
+ valid: false,
1915
+ error: "Record set must contain at least one record"
1916
+ };
1917
+ for (const record of records) {
1918
+ const result = validateRecord(record);
1919
+ if (!result.valid) return {
1920
+ valid: false,
1921
+ error: result.errors?.join(", ")
1922
+ };
1923
+ }
1924
+ return { valid: true };
1925
+ }
1926
+ /**
1927
+ * Check for CNAME conflicts - CNAME cannot coexist with other record types
1928
+ */
1929
+ function checkCNAMEConflict(records) {
1930
+ const hasCNAME = records.some((r) => r.type === "CNAME");
1931
+ const hasOtherTypes = records.some((r) => r.type !== "CNAME");
1932
+ if (hasCNAME && hasOtherTypes) return {
1933
+ valid: false,
1934
+ error: "CNAME cannot coexist with other record types at the same name"
1935
+ };
1936
+ if (records.filter((r) => r.type === "CNAME").length > 1) return {
1937
+ valid: false,
1938
+ error: "Only one CNAME record is allowed per name"
1939
+ };
1940
+ return { valid: true };
1941
+ }
1942
+ /**
1943
+ * Check CNAME at apex - CNAME is not allowed at the root domain
1944
+ */
1945
+ function checkCNAMEAtApex(recordName, records) {
1946
+ if (records.some((r) => r.type === "CNAME") && recordName === "@") return {
1947
+ valid: false,
1948
+ error: "CNAME records are not allowed at the apex (root domain)"
1949
+ };
1950
+ return { valid: true };
1951
+ }
1952
+ /**
1953
+ * Check SOA protection - SOA records cannot be written
1954
+ */
1955
+ function checkSOAProtection(records) {
1956
+ if (records.some((r) => r.type === "SOA")) return {
1957
+ valid: false,
1958
+ error: "SOA records are managed by the DNS provider and cannot be modified"
1959
+ };
1960
+ return { valid: true };
1961
+ }
1962
+ /**
1963
+ * Check for duplicate values within records
1964
+ */
1965
+ function checkDuplicateValues(records) {
1966
+ for (const record of records) {
1967
+ const result = checkRecordForDuplicates(record);
1968
+ if (!result.valid) return result;
1969
+ }
1970
+ return { valid: true };
1971
+ }
1972
+ /**
1973
+ * Check a single record for duplicate values
1974
+ */
1975
+ function checkRecordForDuplicates(record) {
1976
+ const { type, values } = record;
1977
+ if (type === "CNAME" || type === "SOA") return { valid: true };
1978
+ if (Array.isArray(values)) {
1979
+ const seen = /* @__PURE__ */ new Set();
1980
+ for (const value of values) {
1981
+ const key = serializeValue(value, type);
1982
+ if (seen.has(key)) return {
1983
+ valid: false,
1984
+ error: `Duplicate value found in ${type} record: ${key}`
1985
+ };
1986
+ seen.add(key);
1987
+ }
1988
+ }
1989
+ return { valid: true };
1990
+ }
1991
+ /**
1992
+ * Serialize a record value to a string for comparison
1993
+ */
1994
+ function serializeValue(value, type) {
1995
+ if (typeof value === "string") return value;
1996
+ if (type === "MX") {
1997
+ const mx = value;
1998
+ return `${mx.priority}:${mx.value}`;
1999
+ }
2000
+ if (type === "SRV") {
2001
+ const srv = value;
2002
+ return `${srv.priority}:${srv.weight}:${srv.port}:${srv.target}`;
2003
+ }
2004
+ if (type === "CAA") {
2005
+ const caa = value;
2006
+ return `${caa.flags}:${caa.tag}:${caa.value}`;
2007
+ }
2008
+ return JSON.stringify(value);
2009
+ }
2010
+
2011
+ //#endregion
2012
+ export { AFSDNSConflictError, AFSDNSError, AFSDNSInvalidArgumentError, AFSDNSNetworkError, AFSDNSNotFoundError, AFSDNSPermissionDeniedError, AFSDNSRateLimitError, AuditLogger, CloudDNSAdapter, DNSProvider, PRESETS, RateLimiter, Route53Adapter, checkCNAMEAtApex, checkCNAMEConflict, checkDuplicateValues, checkPermission, checkSOAProtection, createRoute53RateLimiter, detectDangerousOperation, escapeSpecialCharacters, formatPermissionError, getEffectivePermissions, isRetryableError, mapRoute53Error, normalizeDomainName, normalizeRecordName, parseHostedZones, parseResourceRecordSets, rejectNullBytes, sanitizeErrorMessage, sanitizeRecordValue, sanitizeTXTValue, validateAAAARecord, validateARecord, validateCAARecord, validateCNAMERecord, validateDomainName, validateInputLength, validateMXRecord, validateNSRecord, validateRecord, validateRecordName, validateRecordSet, validateSRVRecord, validateTTL, validateTXTRecord, validateWriteOperation };
2013
+ //# sourceMappingURL=index.mjs.map