@aigne/afs-dns 1.11.0-beta.11 → 1.11.0-beta.13
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.d.mts +47 -17
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +352 -186
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -5
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { AFSNotFoundError } from "@aigne/afs";
|
|
3
|
+
import { AFSBaseProvider, Actions, Delete, Explain, List, Meta, Read, Search, Stat, Write } from "@aigne/afs/provider";
|
|
2
4
|
import { minimatch } from "minimatch";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { ChangeResourceRecordSetsCommand, GetHostedZoneCommand, ListHostedZonesCommand, ListResourceRecordSetsCommand, Route53Client } from "@aws-sdk/client-route-53";
|
|
@@ -1011,9 +1013,23 @@ function validateRecord(record) {
|
|
|
1011
1013
|
};
|
|
1012
1014
|
}
|
|
1013
1015
|
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
|
|
1018
|
+
function __decorate(decorators, target, key, desc) {
|
|
1019
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
1020
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
1021
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
1022
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1014
1025
|
//#endregion
|
|
1015
1026
|
//#region src/provider/dns-provider.ts
|
|
1016
|
-
|
|
1027
|
+
/**
|
|
1028
|
+
* DNS Provider
|
|
1029
|
+
*
|
|
1030
|
+
* AFS module for DNS zone management via various providers.
|
|
1031
|
+
*/
|
|
1032
|
+
var DNSProvider = class extends AFSBaseProvider {
|
|
1017
1033
|
name;
|
|
1018
1034
|
description;
|
|
1019
1035
|
accessMode;
|
|
@@ -1028,6 +1044,48 @@ var DNSProvider = class {
|
|
|
1028
1044
|
static schema() {
|
|
1029
1045
|
return z.object({ zone: z.string() });
|
|
1030
1046
|
}
|
|
1047
|
+
static treeSchema() {
|
|
1048
|
+
return {
|
|
1049
|
+
operations: [
|
|
1050
|
+
"list",
|
|
1051
|
+
"read",
|
|
1052
|
+
"write",
|
|
1053
|
+
"delete",
|
|
1054
|
+
"search",
|
|
1055
|
+
"stat",
|
|
1056
|
+
"explain"
|
|
1057
|
+
],
|
|
1058
|
+
tree: {
|
|
1059
|
+
"/": {
|
|
1060
|
+
kind: "dns:zone",
|
|
1061
|
+
operations: [
|
|
1062
|
+
"list",
|
|
1063
|
+
"read",
|
|
1064
|
+
"search"
|
|
1065
|
+
]
|
|
1066
|
+
},
|
|
1067
|
+
"/_zone": {
|
|
1068
|
+
kind: "dns:zone-meta",
|
|
1069
|
+
operations: ["read"]
|
|
1070
|
+
},
|
|
1071
|
+
"/{record-name}": {
|
|
1072
|
+
kind: "dns:record",
|
|
1073
|
+
operations: [
|
|
1074
|
+
"read",
|
|
1075
|
+
"write",
|
|
1076
|
+
"delete"
|
|
1077
|
+
],
|
|
1078
|
+
destructive: ["delete"]
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
auth: {
|
|
1082
|
+
type: "aws",
|
|
1083
|
+
env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"]
|
|
1084
|
+
},
|
|
1085
|
+
bestFor: ["DNS zone management", "record CRUD"],
|
|
1086
|
+
notFor: ["domain registration"]
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1031
1089
|
/**
|
|
1032
1090
|
* Provider manifest for URI-based discovery
|
|
1033
1091
|
*/
|
|
@@ -1036,12 +1094,40 @@ var DNSProvider = class {
|
|
|
1036
1094
|
name: "dns",
|
|
1037
1095
|
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
1096
|
uriTemplate: "dns://{zone}",
|
|
1039
|
-
category: "
|
|
1097
|
+
category: "network",
|
|
1040
1098
|
schema: z.object({ zone: z.string() }),
|
|
1041
|
-
tags: ["dns", "cloud"]
|
|
1099
|
+
tags: ["dns", "cloud"],
|
|
1100
|
+
capabilityTags: [
|
|
1101
|
+
"read-write",
|
|
1102
|
+
"crud",
|
|
1103
|
+
"destructive",
|
|
1104
|
+
"auth:aws",
|
|
1105
|
+
"remote",
|
|
1106
|
+
"cloud",
|
|
1107
|
+
"http"
|
|
1108
|
+
],
|
|
1109
|
+
security: {
|
|
1110
|
+
riskLevel: "external",
|
|
1111
|
+
resourceAccess: ["cloud-api"],
|
|
1112
|
+
requires: ["cloud-credentials"],
|
|
1113
|
+
dataSensitivity: ["system-config"],
|
|
1114
|
+
notes: ["Can modify DNS records — misconfiguration may cause service outages"]
|
|
1115
|
+
},
|
|
1116
|
+
capabilities: {
|
|
1117
|
+
network: {
|
|
1118
|
+
egress: true,
|
|
1119
|
+
allowedDomains: ["*.amazonaws.com", "*.googleapis.com"]
|
|
1120
|
+
},
|
|
1121
|
+
secrets: [
|
|
1122
|
+
"aws/access-key-id",
|
|
1123
|
+
"aws/secret-access-key",
|
|
1124
|
+
"gcp/credentials"
|
|
1125
|
+
]
|
|
1126
|
+
}
|
|
1042
1127
|
};
|
|
1043
1128
|
}
|
|
1044
1129
|
constructor(options) {
|
|
1130
|
+
super();
|
|
1045
1131
|
this.zone = options.zone;
|
|
1046
1132
|
this.adapter = options.adapter;
|
|
1047
1133
|
this.accessMode = options.accessMode ?? "readonly";
|
|
@@ -1051,70 +1137,163 @@ var DNSProvider = class {
|
|
|
1051
1137
|
this.name = `dns:${this.zone}`;
|
|
1052
1138
|
this.description = `DNS zone: ${this.zone}`;
|
|
1053
1139
|
}
|
|
1054
|
-
async
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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}`,
|
|
1140
|
+
async listRoot(_ctx) {
|
|
1141
|
+
const entries = [];
|
|
1142
|
+
const names = await this.adapter.listRecords(this.zone);
|
|
1143
|
+
for (const name of names) entries.push({
|
|
1144
|
+
id: name,
|
|
1145
|
+
path: `/${name}`,
|
|
1146
|
+
summary: name === "_zone" ? "Zone metadata" : `DNS record: ${name}`,
|
|
1077
1147
|
meta: {
|
|
1078
|
-
kind:
|
|
1148
|
+
kind: name === "_zone" ? "dns:zone-meta" : "dns:record",
|
|
1079
1149
|
childrenCount: 0
|
|
1080
1150
|
}
|
|
1081
|
-
}
|
|
1151
|
+
});
|
|
1152
|
+
return { data: entries };
|
|
1082
1153
|
}
|
|
1083
|
-
async
|
|
1084
|
-
const
|
|
1085
|
-
if (
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1154
|
+
async listRecord(ctx) {
|
|
1155
|
+
const recordName = ctx.params.recordName;
|
|
1156
|
+
if (recordName === "_zone") return { data: [] };
|
|
1157
|
+
if ((await this.adapter.getRecord(this.zone, recordName)).records.length === 0) throw new AFSNotFoundError(`/${recordName}`);
|
|
1158
|
+
return { data: [] };
|
|
1159
|
+
}
|
|
1160
|
+
async readRoot(_ctx) {
|
|
1161
|
+
const names = await this.adapter.listRecords(this.zone);
|
|
1162
|
+
return {
|
|
1163
|
+
id: this.zone,
|
|
1164
|
+
path: "/",
|
|
1165
|
+
summary: `DNS zone: ${this.zone}`,
|
|
1166
|
+
meta: {
|
|
1167
|
+
kind: "dns:zone",
|
|
1168
|
+
childrenCount: names.length
|
|
1169
|
+
}
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
async readRootMeta(_ctx) {
|
|
1173
|
+
const names = await this.adapter.listRecords(this.zone);
|
|
1174
|
+
return {
|
|
1175
|
+
id: "meta",
|
|
1176
|
+
path: "/.meta",
|
|
1177
|
+
summary: `DNS zone: ${this.zone}`,
|
|
1178
|
+
meta: {
|
|
1179
|
+
kind: "dns:zone",
|
|
1180
|
+
childrenCount: names.length
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
readCapabilities(_ctx) {
|
|
1185
|
+
const actionCatalogs = [];
|
|
1186
|
+
actionCatalogs.push({
|
|
1187
|
+
kind: "dns:zone",
|
|
1188
|
+
description: "Zone-level operations",
|
|
1189
|
+
catalog: [{
|
|
1190
|
+
name: "validate-zone",
|
|
1191
|
+
description: "Validate zone integrity (SOA, NS records)",
|
|
1192
|
+
inputSchema: {
|
|
1193
|
+
type: "object",
|
|
1194
|
+
properties: {},
|
|
1195
|
+
description: "No parameters required"
|
|
1196
|
+
}
|
|
1197
|
+
}, {
|
|
1198
|
+
name: "export-zone",
|
|
1199
|
+
description: "Export zone as BIND zone file format",
|
|
1200
|
+
inputSchema: {
|
|
1201
|
+
type: "object",
|
|
1202
|
+
properties: {},
|
|
1203
|
+
description: "No parameters required"
|
|
1204
|
+
}
|
|
1205
|
+
}],
|
|
1206
|
+
discovery: {
|
|
1207
|
+
pathTemplate: "/.actions/{action}",
|
|
1208
|
+
note: "Exec /.actions/validate-zone or /.actions/export-zone to execute"
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
const operations = {
|
|
1212
|
+
read: true,
|
|
1213
|
+
list: true,
|
|
1214
|
+
write: this.accessMode === "readwrite",
|
|
1215
|
+
delete: this.accessMode === "readwrite",
|
|
1216
|
+
search: true,
|
|
1217
|
+
exec: true,
|
|
1218
|
+
stat: true,
|
|
1219
|
+
explain: true
|
|
1220
|
+
};
|
|
1221
|
+
return {
|
|
1222
|
+
id: "/.meta/.capabilities",
|
|
1223
|
+
path: "/.meta/.capabilities",
|
|
1224
|
+
content: {
|
|
1225
|
+
schemaVersion: 1,
|
|
1226
|
+
provider: this.name,
|
|
1227
|
+
version: "1.0.0",
|
|
1228
|
+
description: this.description,
|
|
1229
|
+
tools: [],
|
|
1230
|
+
actions: actionCatalogs,
|
|
1231
|
+
operations
|
|
1232
|
+
},
|
|
1233
|
+
meta: { kind: "afs:capabilities" }
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
async readZone(_ctx) {
|
|
1237
|
+
const metadata = await this.adapter.getZoneMetadata(this.zone);
|
|
1238
|
+
return {
|
|
1239
|
+
id: "_zone",
|
|
1240
|
+
path: "/_zone",
|
|
1241
|
+
summary: `Zone metadata for ${this.zone}`,
|
|
1242
|
+
meta: {
|
|
1243
|
+
kind: "dns:zone-meta",
|
|
1244
|
+
childrenCount: 0
|
|
1245
|
+
},
|
|
1246
|
+
content: metadata
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
async readRecord(ctx) {
|
|
1250
|
+
const { name: recordName, type } = this.parseRecordParam(ctx.params.recordName);
|
|
1097
1251
|
const recordSet = await this.adapter.getRecord(this.zone, recordName);
|
|
1098
1252
|
let records = recordSet.records;
|
|
1099
1253
|
if (type) records = records.filter((r) => r.type === type);
|
|
1100
|
-
if (records.length === 0)
|
|
1101
|
-
return {
|
|
1254
|
+
if (records.length === 0) throw new AFSNotFoundError(`/${recordName}`);
|
|
1255
|
+
return {
|
|
1102
1256
|
id: recordName,
|
|
1103
|
-
path:
|
|
1257
|
+
path: ctx.path.split("?")[0] ?? ctx.path,
|
|
1104
1258
|
summary: `DNS records for ${recordSet.fqdn}`,
|
|
1105
|
-
meta: {
|
|
1259
|
+
meta: {
|
|
1260
|
+
kind: "dns:record",
|
|
1261
|
+
childrenCount: 0
|
|
1262
|
+
},
|
|
1106
1263
|
content: {
|
|
1107
1264
|
fqdn: recordSet.fqdn,
|
|
1108
1265
|
records
|
|
1109
1266
|
}
|
|
1110
|
-
}
|
|
1267
|
+
};
|
|
1111
1268
|
}
|
|
1112
|
-
async
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1269
|
+
async readRecordMeta(ctx) {
|
|
1270
|
+
const recordName = ctx.params.recordName;
|
|
1271
|
+
if (recordName === "_zone") return {
|
|
1272
|
+
id: "_zone",
|
|
1273
|
+
path: "/_zone/.meta",
|
|
1274
|
+
summary: "Zone metadata",
|
|
1275
|
+
meta: {
|
|
1276
|
+
kind: "dns:zone-meta",
|
|
1277
|
+
childrenCount: 0
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
const recordSet = await this.adapter.getRecord(this.zone, recordName);
|
|
1281
|
+
if (recordSet.records.length === 0) throw new AFSNotFoundError(`/${recordName}`);
|
|
1282
|
+
const firstRecord = recordSet.records[0];
|
|
1283
|
+
return {
|
|
1284
|
+
id: recordName,
|
|
1285
|
+
path: `/${recordName}/.meta`,
|
|
1286
|
+
summary: `DNS record: ${recordName}`,
|
|
1287
|
+
meta: {
|
|
1288
|
+
kind: "dns:record",
|
|
1289
|
+
childrenCount: 0,
|
|
1290
|
+
recordType: firstRecord?.type,
|
|
1291
|
+
ttl: firstRecord?.ttl
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
async writeRecord(ctx, content) {
|
|
1296
|
+
const recordName = ctx.params.recordName;
|
|
1118
1297
|
if (recordName === "_zone") throw new Error("Cannot write to _zone metadata");
|
|
1119
1298
|
const record = content.content;
|
|
1120
1299
|
if (!record || !record.type) throw new Error("Invalid record content: must include type");
|
|
@@ -1147,16 +1326,14 @@ var DNSProvider = class {
|
|
|
1147
1326
|
});
|
|
1148
1327
|
return { data: {
|
|
1149
1328
|
id: recordName,
|
|
1150
|
-
path:
|
|
1329
|
+
path: ctx.path,
|
|
1151
1330
|
summary: `Updated DNS record: ${recordName}`,
|
|
1152
1331
|
meta: { kind: "dns:record" },
|
|
1153
1332
|
content: record
|
|
1154
1333
|
} };
|
|
1155
1334
|
}
|
|
1156
|
-
async
|
|
1157
|
-
|
|
1158
|
-
const normalizedPath = this.normalizePath(path);
|
|
1159
|
-
const { recordName, type } = this.parsePathWithQuery(normalizedPath);
|
|
1335
|
+
async deleteRecord(ctx) {
|
|
1336
|
+
const { name: recordName, type } = this.parseRecordParam(ctx.params.recordName);
|
|
1160
1337
|
if (recordName === "_zone") throw new Error("Cannot delete _zone metadata");
|
|
1161
1338
|
const permResult = checkPermission({
|
|
1162
1339
|
name: recordName,
|
|
@@ -1172,6 +1349,9 @@ var DNSProvider = class {
|
|
|
1172
1349
|
});
|
|
1173
1350
|
throw new Error(formatPermissionError(permResult));
|
|
1174
1351
|
}
|
|
1352
|
+
const existing = await this.adapter.getRecord(this.zone, recordName);
|
|
1353
|
+
if (existing.records.length === 0) throw new AFSNotFoundError(`/${recordName}`);
|
|
1354
|
+
if (type && !existing.records.some((r) => r.type === type)) throw new AFSNotFoundError(`/${recordName}?type=${type}`, `No ${type} record found for ${recordName}`);
|
|
1175
1355
|
await this.acquireRateLimit();
|
|
1176
1356
|
await this.adapter.deleteRecord(this.zone, recordName, type);
|
|
1177
1357
|
this.auditLogger.logDelete({
|
|
@@ -1181,68 +1361,34 @@ var DNSProvider = class {
|
|
|
1181
1361
|
});
|
|
1182
1362
|
return { message: `Record ${recordName}${type ? ` (${type})` : ""} deleted` };
|
|
1183
1363
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
return
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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}`);
|
|
1364
|
+
async statRoot(_ctx) {
|
|
1365
|
+
const names = await this.adapter.listRecords(this.zone);
|
|
1366
|
+
return { data: {
|
|
1367
|
+
id: this.zone,
|
|
1368
|
+
path: "/",
|
|
1369
|
+
meta: {
|
|
1370
|
+
kind: "dns:zone",
|
|
1371
|
+
childrenCount: names.length
|
|
1215
1372
|
}
|
|
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
|
-
}
|
|
1373
|
+
} };
|
|
1225
1374
|
}
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (Array.isArray(values)) return values;
|
|
1233
|
-
return [values];
|
|
1375
|
+
async statZone(_ctx) {
|
|
1376
|
+
return { data: {
|
|
1377
|
+
id: "_zone",
|
|
1378
|
+
path: "/_zone",
|
|
1379
|
+
meta: { kind: "dns:zone-meta" }
|
|
1380
|
+
} };
|
|
1234
1381
|
}
|
|
1235
|
-
async
|
|
1236
|
-
const
|
|
1237
|
-
const recordName = this.extractRecordName(normalizedPath);
|
|
1382
|
+
async statRecord(ctx) {
|
|
1383
|
+
const recordName = ctx.params.recordName;
|
|
1238
1384
|
const recordSet = await this.adapter.getRecord(this.zone, recordName);
|
|
1239
|
-
if (recordSet.records.length === 0
|
|
1385
|
+
if (recordSet.records.length === 0) throw new AFSNotFoundError(`/${recordName}`);
|
|
1240
1386
|
const firstRecord = recordSet.records[0];
|
|
1241
1387
|
return { data: {
|
|
1242
1388
|
id: recordName,
|
|
1243
|
-
path:
|
|
1389
|
+
path: ctx.path,
|
|
1244
1390
|
meta: {
|
|
1245
|
-
kind:
|
|
1391
|
+
kind: "dns:record",
|
|
1246
1392
|
recordType: firstRecord?.type,
|
|
1247
1393
|
ttl: firstRecord?.ttl,
|
|
1248
1394
|
valueCount: recordSet.records.reduce((count, r) => {
|
|
@@ -1253,14 +1399,7 @@ var DNSProvider = class {
|
|
|
1253
1399
|
}
|
|
1254
1400
|
} };
|
|
1255
1401
|
}
|
|
1256
|
-
async
|
|
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() {
|
|
1402
|
+
async explainRoot(_ctx) {
|
|
1264
1403
|
const metadata = await this.adapter.getZoneMetadata(this.zone);
|
|
1265
1404
|
const names = await this.adapter.listRecords(this.zone);
|
|
1266
1405
|
return {
|
|
@@ -1274,7 +1413,7 @@ var DNSProvider = class {
|
|
|
1274
1413
|
`
|
|
1275
1414
|
};
|
|
1276
1415
|
}
|
|
1277
|
-
async explainZone() {
|
|
1416
|
+
async explainZone(_ctx) {
|
|
1278
1417
|
const metadata = await this.adapter.getZoneMetadata(this.zone);
|
|
1279
1418
|
const apex = await this.adapter.getRecord(this.zone, "@");
|
|
1280
1419
|
const soaRecord = apex.records.find((r) => r.type === "SOA");
|
|
@@ -1306,7 +1445,7 @@ var DNSProvider = class {
|
|
|
1306
1445
|
content: lines.join("\n")
|
|
1307
1446
|
};
|
|
1308
1447
|
}
|
|
1309
|
-
async
|
|
1448
|
+
async searchRecords(_ctx, query) {
|
|
1310
1449
|
const names = await this.adapter.listRecords(this.zone);
|
|
1311
1450
|
const results = [];
|
|
1312
1451
|
for (const name of names) {
|
|
@@ -1333,59 +1472,28 @@ var DNSProvider = class {
|
|
|
1333
1472
|
}
|
|
1334
1473
|
return { data: results };
|
|
1335
1474
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1475
|
+
listActions(_ctx) {
|
|
1476
|
+
return { data: [{
|
|
1477
|
+
id: "validate-zone",
|
|
1478
|
+
path: "/.actions/validate-zone",
|
|
1479
|
+
summary: "validate-zone",
|
|
1480
|
+
meta: {
|
|
1481
|
+
kind: "afs:action",
|
|
1342
1482
|
name: "validate-zone",
|
|
1343
|
-
description: "Validate zone integrity (SOA, NS records)"
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1483
|
+
description: "Validate zone integrity (SOA, NS records)"
|
|
1484
|
+
}
|
|
1485
|
+
}, {
|
|
1486
|
+
id: "export-zone",
|
|
1487
|
+
path: "/.actions/export-zone",
|
|
1488
|
+
summary: "export-zone",
|
|
1489
|
+
meta: {
|
|
1490
|
+
kind: "afs:action",
|
|
1350
1491
|
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"
|
|
1492
|
+
description: "Export zone as BIND zone file format"
|
|
1361
1493
|
}
|
|
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
|
-
} };
|
|
1494
|
+
}] };
|
|
1387
1495
|
}
|
|
1388
|
-
async
|
|
1496
|
+
async execValidateZone(_ctx, _args) {
|
|
1389
1497
|
const apex = await this.adapter.getRecord(this.zone, "@");
|
|
1390
1498
|
const checks = [];
|
|
1391
1499
|
const soaRecord = apex.records.find((r) => r.type === "SOA");
|
|
@@ -1404,18 +1512,13 @@ var DNSProvider = class {
|
|
|
1404
1512
|
});
|
|
1405
1513
|
const allValid = checks.every((c) => c.valid);
|
|
1406
1514
|
return { data: {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
content: {
|
|
1412
|
-
valid: allValid,
|
|
1413
|
-
checks,
|
|
1414
|
-
zone: this.zone
|
|
1415
|
-
}
|
|
1515
|
+
valid: allValid,
|
|
1516
|
+
checks,
|
|
1517
|
+
zone: this.zone,
|
|
1518
|
+
summary: `Zone validation: ${allValid ? "PASSED" : "FAILED"}`
|
|
1416
1519
|
} };
|
|
1417
1520
|
}
|
|
1418
|
-
async
|
|
1521
|
+
async execExportZone(_ctx, _args) {
|
|
1419
1522
|
const names = await this.adapter.listRecords(this.zone);
|
|
1420
1523
|
const lines = [];
|
|
1421
1524
|
lines.push(`; Zone file for ${this.zone}`);
|
|
@@ -1433,14 +1536,77 @@ var DNSProvider = class {
|
|
|
1433
1536
|
}
|
|
1434
1537
|
lines.push("");
|
|
1435
1538
|
return { data: {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
summary: `BIND zone file for ${this.zone}`,
|
|
1439
|
-
meta: { kind: "dns:action-result" },
|
|
1440
|
-
content: lines.join("\n")
|
|
1539
|
+
content: lines.join("\n"),
|
|
1540
|
+
summary: `BIND zone file for ${this.zone}`
|
|
1441
1541
|
} };
|
|
1442
1542
|
}
|
|
1543
|
+
/**
|
|
1544
|
+
* Parse a record param that may contain a query string (e.g. "www?type=A")
|
|
1545
|
+
*/
|
|
1546
|
+
parseRecordParam(param) {
|
|
1547
|
+
const queryIndex = param.indexOf("?");
|
|
1548
|
+
if (queryIndex === -1) return { name: param };
|
|
1549
|
+
const name = param.slice(0, queryIndex);
|
|
1550
|
+
const queryPart = param.slice(queryIndex + 1);
|
|
1551
|
+
return {
|
|
1552
|
+
name,
|
|
1553
|
+
type: new URLSearchParams(queryPart).get("type") ?? void 0
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* Sanitize record values to prevent injection attacks
|
|
1558
|
+
*/
|
|
1559
|
+
sanitizeRecordValues(record) {
|
|
1560
|
+
const { values } = record;
|
|
1561
|
+
if (typeof values === "string") {
|
|
1562
|
+
rejectNullBytes(values);
|
|
1563
|
+
const result = sanitizeRecordValue(values, record.type);
|
|
1564
|
+
if (!result.valid) throw new Error(`Invalid record value: ${result.error}`);
|
|
1565
|
+
} else if (Array.isArray(values)) {
|
|
1566
|
+
for (const value of values) if (typeof value === "string") {
|
|
1567
|
+
rejectNullBytes(value);
|
|
1568
|
+
const result = sanitizeRecordValue(value, record.type);
|
|
1569
|
+
if (!result.valid) throw new Error(`Invalid record value: ${result.error}`);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Acquire rate limit token (waits if necessary)
|
|
1575
|
+
*/
|
|
1576
|
+
async acquireRateLimit() {
|
|
1577
|
+
if (this.rateLimiter) {
|
|
1578
|
+
if (!await this.rateLimiter.tryAcquire()) throw new Error("Rate limit exceeded. Please retry later.");
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* Extract values from a record for audit logging
|
|
1583
|
+
*/
|
|
1584
|
+
extractValues(record) {
|
|
1585
|
+
const { values } = record;
|
|
1586
|
+
if (typeof values === "string") return [values];
|
|
1587
|
+
if (Array.isArray(values)) return values;
|
|
1588
|
+
return [values];
|
|
1589
|
+
}
|
|
1443
1590
|
};
|
|
1591
|
+
__decorate([List("/")], DNSProvider.prototype, "listRoot", null);
|
|
1592
|
+
__decorate([List("/:recordName")], DNSProvider.prototype, "listRecord", null);
|
|
1593
|
+
__decorate([Read("/")], DNSProvider.prototype, "readRoot", null);
|
|
1594
|
+
__decorate([Meta("/")], DNSProvider.prototype, "readRootMeta", null);
|
|
1595
|
+
__decorate([Read("/.meta/.capabilities")], DNSProvider.prototype, "readCapabilities", null);
|
|
1596
|
+
__decorate([Read("/_zone")], DNSProvider.prototype, "readZone", null);
|
|
1597
|
+
__decorate([Read("/:recordName")], DNSProvider.prototype, "readRecord", null);
|
|
1598
|
+
__decorate([Meta("/:recordName")], DNSProvider.prototype, "readRecordMeta", null);
|
|
1599
|
+
__decorate([Write("/:recordName")], DNSProvider.prototype, "writeRecord", null);
|
|
1600
|
+
__decorate([Delete("/:recordName")], DNSProvider.prototype, "deleteRecord", null);
|
|
1601
|
+
__decorate([Stat("/")], DNSProvider.prototype, "statRoot", null);
|
|
1602
|
+
__decorate([Stat("/_zone")], DNSProvider.prototype, "statZone", null);
|
|
1603
|
+
__decorate([Stat("/:recordName")], DNSProvider.prototype, "statRecord", null);
|
|
1604
|
+
__decorate([Explain("/")], DNSProvider.prototype, "explainRoot", null);
|
|
1605
|
+
__decorate([Explain("/_zone")], DNSProvider.prototype, "explainZone", null);
|
|
1606
|
+
__decorate([Search("/")], DNSProvider.prototype, "searchRecords", null);
|
|
1607
|
+
__decorate([Actions("/")], DNSProvider.prototype, "listActions", null);
|
|
1608
|
+
__decorate([Actions.Exec("/", "validate-zone")], DNSProvider.prototype, "execValidateZone", null);
|
|
1609
|
+
__decorate([Actions.Exec("/", "export-zone")], DNSProvider.prototype, "execExportZone", null);
|
|
1444
1610
|
|
|
1445
1611
|
//#endregion
|
|
1446
1612
|
//#region src/route53/parser.ts
|