@gusnips/one-click-dns 0.1.0 → 0.1.2
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/README.md +26 -1
- package/dist/bounded-fetch-nozW2egX.d.cts +5 -0
- package/dist/bounded-fetch-nozW2egX.d.ts +5 -0
- package/dist/chunk-4XNRV7QR.js +528 -0
- package/dist/server/index.cjs +362 -38
- package/dist/server/index.d.cts +15 -2
- package/dist/server/index.d.ts +15 -2
- package/dist/server/index.js +22 -2
- package/dist/writers/index.cjs +345 -36
- package/dist/writers/index.d.cts +9 -2
- package/dist/writers/index.d.ts +9 -2
- package/dist/writers/index.js +7 -1
- package/package.json +2 -2
- package/dist/chunk-FGM7S7GQ.js +0 -222
package/dist/server/index.cjs
CHANGED
|
@@ -27,7 +27,8 @@ __export(server_exports, {
|
|
|
27
27
|
readDnsZone: () => readDnsZone,
|
|
28
28
|
signedApplyUrl: () => signedApplyUrl,
|
|
29
29
|
systemDnsLookups: () => systemDnsLookups,
|
|
30
|
-
systemResolver: () => systemResolver
|
|
30
|
+
systemResolver: () => systemResolver,
|
|
31
|
+
templateCoverage: () => templateCoverage
|
|
31
32
|
});
|
|
32
33
|
module.exports = __toCommonJS(server_exports);
|
|
33
34
|
|
|
@@ -657,22 +658,25 @@ var CONNECT_PROVIDERS = [
|
|
|
657
658
|
// src/bounded-fetch.ts
|
|
658
659
|
var import_server = require("@gusnips/server");
|
|
659
660
|
async function fetchBounded(doFetch, url, init, limits) {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
661
|
+
const signal = AbortSignal.timeout(limits.timeoutMs);
|
|
662
|
+
const outOfTime = new Promise((resolve) => {
|
|
663
|
+
signal.addEventListener("abort", () => resolve(null), { once: true });
|
|
664
|
+
});
|
|
665
|
+
const attempt = async () => {
|
|
666
|
+
try {
|
|
667
|
+
const res = await doFetch(url, { ...init, redirect: "manual", signal });
|
|
668
|
+
if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
|
|
669
|
+
await res.body?.cancel().catch(() => void 0);
|
|
670
|
+
return { status: res.status || 302, text: "" };
|
|
671
|
+
}
|
|
672
|
+
const { bytes, truncated } = await (0, import_server.readBounded)(res.body, limits.maxBytes);
|
|
673
|
+
if (truncated) return null;
|
|
674
|
+
return { status: res.status, text: new TextDecoder().decode(bytes) };
|
|
675
|
+
} catch {
|
|
676
|
+
return null;
|
|
669
677
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
return { status: res.status, text: new TextDecoder().decode(bytes) };
|
|
673
|
-
} catch {
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
678
|
+
};
|
|
679
|
+
return Promise.race([attempt(), outOfTime]);
|
|
676
680
|
}
|
|
677
681
|
|
|
678
682
|
// src/guards.ts
|
|
@@ -771,6 +775,25 @@ async function ask(io, args, deadline) {
|
|
|
771
775
|
};
|
|
772
776
|
}
|
|
773
777
|
|
|
778
|
+
// src/server/connect/coverage.ts
|
|
779
|
+
async function templateCoverage(io, args) {
|
|
780
|
+
const { providerId, serviceId, timeoutMs = 5e3, providers = CONNECT_PROVIDERS } = args;
|
|
781
|
+
const answers = await Promise.all(
|
|
782
|
+
providers.map(async (provider) => {
|
|
783
|
+
const answer = await fetchBounded(
|
|
784
|
+
io.fetch ?? fetch,
|
|
785
|
+
`${provider.api}/v2/domainTemplates/providers/${providerId}/services/${serviceId}`,
|
|
786
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
787
|
+
{ timeoutMs, maxBytes: 64 * 1024 }
|
|
788
|
+
);
|
|
789
|
+
if (answer?.status === 200) return [provider.id, "supported"];
|
|
790
|
+
if (answer?.status === 404) return [provider.id, "not_supported"];
|
|
791
|
+
return [provider.id, "unanswered"];
|
|
792
|
+
})
|
|
793
|
+
);
|
|
794
|
+
return Object.fromEntries(answers);
|
|
795
|
+
}
|
|
796
|
+
|
|
774
797
|
// src/server/connect/sign.ts
|
|
775
798
|
var import_node_crypto = require("crypto");
|
|
776
799
|
var encode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%20/g, "+");
|
|
@@ -801,10 +824,35 @@ var MESSAGES = {
|
|
|
801
824
|
zone_read_missing: () => "That token can't read your domains",
|
|
802
825
|
dns_edit_missing: () => "That token can't change DNS records",
|
|
803
826
|
zone_not_found: (p) => `No zone holds ${p.hostname ?? "that name"}`,
|
|
804
|
-
conflict: (p) => `${p.hostname ?? "That name"} already has a record (type ${p.type
|
|
827
|
+
conflict: (p) => `${p.hostname ?? "That name"} already has a record${p.type ? ` (type ${p.type})` : ""}`,
|
|
805
828
|
unavailable: () => "Couldn't reach the DNS host"
|
|
806
829
|
};
|
|
807
830
|
|
|
831
|
+
// src/writers/policy.ts
|
|
832
|
+
function zoneCandidates(hostname) {
|
|
833
|
+
const labels = hostname.split(".");
|
|
834
|
+
const names = [];
|
|
835
|
+
for (let start = 0; start < labels.length - 1; start++) names.push(labels.slice(start).join("."));
|
|
836
|
+
return names;
|
|
837
|
+
}
|
|
838
|
+
var unquoted = (content) => content.replace(/"\s+"/g, "").replace(/^"|"$/g, "");
|
|
839
|
+
var sameValue = (record, existing) => record.type === "CNAME" ? normalizeDnsName(existing.content) === normalizeDnsName(record.value) : unquoted(existing.content) === record.value;
|
|
840
|
+
var isSpf = (text2) => /^v=spf1(?:\s|$)/i.test(unquoted(text2).trim());
|
|
841
|
+
function blocks(record, existing) {
|
|
842
|
+
if (record.type === "CNAME" || existing.type === "CNAME") return true;
|
|
843
|
+
return existing.type === "TXT" && isSpf(record.value) && isSpf(existing.content);
|
|
844
|
+
}
|
|
845
|
+
function decide(record, existing, hostname) {
|
|
846
|
+
const ours = existing.find((r) => r.type === record.type && sameValue(record, r));
|
|
847
|
+
if (ours) {
|
|
848
|
+
if (record.type === "CNAME" && ours.proxied === true) return { kind: "unproxy", id: ours.id };
|
|
849
|
+
return { kind: "unchanged" };
|
|
850
|
+
}
|
|
851
|
+
const blocker = existing.find((r) => blocks(record, r));
|
|
852
|
+
if (blocker) throw new DnsWriteError("conflict", { hostname, type: blocker.type });
|
|
853
|
+
return { kind: "create" };
|
|
854
|
+
}
|
|
855
|
+
|
|
808
856
|
// src/writers/cloudflare.ts
|
|
809
857
|
var API = "https://api.cloudflare.com/client/v4";
|
|
810
858
|
var TIMEOUT_MS = 1e4;
|
|
@@ -890,19 +938,6 @@ function recordsOf(result) {
|
|
|
890
938
|
};
|
|
891
939
|
});
|
|
892
940
|
}
|
|
893
|
-
function zoneCandidates(hostname) {
|
|
894
|
-
const labels = hostname.split(".");
|
|
895
|
-
const names = [];
|
|
896
|
-
for (let start = 0; start < labels.length - 1; start++) names.push(labels.slice(start).join("."));
|
|
897
|
-
return names;
|
|
898
|
-
}
|
|
899
|
-
var unquoted = (content) => content.replace(/"\s+"/g, "").replace(/^"|"$/g, "");
|
|
900
|
-
var sameValue = (record, existing) => record.type === "CNAME" ? normalizeDnsName(existing.content) === normalizeDnsName(record.value) : unquoted(existing.content) === record.value;
|
|
901
|
-
var isSpf = (text2) => /^v=spf1(?:\s|$)/i.test(unquoted(text2).trim());
|
|
902
|
-
function blocks(record, existing) {
|
|
903
|
-
if (record.type === "CNAME" || existing.type === "CNAME") return true;
|
|
904
|
-
return existing.type === "TXT" && isSpf(record.value) && isSpf(existing.content);
|
|
905
|
-
}
|
|
906
941
|
var cloudflare = {
|
|
907
942
|
host: "cloudflare",
|
|
908
943
|
/** Add the record (DNS only) to the Cloudflare zone that holds its name. An identical record
|
|
@@ -931,11 +966,11 @@ var cloudflare = {
|
|
|
931
966
|
subject
|
|
932
967
|
)
|
|
933
968
|
);
|
|
934
|
-
const
|
|
935
|
-
if (
|
|
936
|
-
|
|
969
|
+
const decision = decide(record, existing, hostname);
|
|
970
|
+
if (decision.kind === "unchanged") return "unchanged";
|
|
971
|
+
if (decision.kind === "unproxy") {
|
|
937
972
|
resultOf(
|
|
938
|
-
await call(doFetch, token, `/zones/${zoneId}/dns_records/${
|
|
973
|
+
await call(doFetch, token, `/zones/${zoneId}/dns_records/${decision.id}`, {
|
|
939
974
|
method: "PATCH",
|
|
940
975
|
body: { proxied: false }
|
|
941
976
|
}),
|
|
@@ -944,8 +979,6 @@ var cloudflare = {
|
|
|
944
979
|
);
|
|
945
980
|
return "unproxied";
|
|
946
981
|
}
|
|
947
|
-
const blocker = existing.find((r) => blocks(record, r));
|
|
948
|
-
if (blocker) throw new DnsWriteError("conflict", { hostname, type: blocker.type });
|
|
949
982
|
resultOf(
|
|
950
983
|
await call(doFetch, token, `/zones/${zoneId}/dns_records`, {
|
|
951
984
|
method: "POST",
|
|
@@ -967,8 +1000,298 @@ var cloudflare = {
|
|
|
967
1000
|
}
|
|
968
1001
|
};
|
|
969
1002
|
|
|
1003
|
+
// src/writers/digitalocean.ts
|
|
1004
|
+
var API2 = "https://api.digitalocean.com/v2";
|
|
1005
|
+
var TIMEOUT_MS2 = 1e4;
|
|
1006
|
+
var MAX_BODY_BYTES2 = 512 * 1024;
|
|
1007
|
+
var RECORD_TYPE2 = /^[A-Z0-9]{1,12}$/;
|
|
1008
|
+
async function call2(doFetch, token, path, send) {
|
|
1009
|
+
const res = await fetchBounded(
|
|
1010
|
+
doFetch,
|
|
1011
|
+
`${API2}${path}`,
|
|
1012
|
+
{
|
|
1013
|
+
method: send ? "POST" : "GET",
|
|
1014
|
+
headers: {
|
|
1015
|
+
Authorization: `Bearer ${token}`,
|
|
1016
|
+
Accept: "application/json",
|
|
1017
|
+
...send ? { "Content-Type": "application/json" } : {}
|
|
1018
|
+
},
|
|
1019
|
+
body: send ? JSON.stringify(send.body) : void 0
|
|
1020
|
+
},
|
|
1021
|
+
{ timeoutMs: TIMEOUT_MS2, maxBytes: MAX_BODY_BYTES2 }
|
|
1022
|
+
);
|
|
1023
|
+
if (!res) return { status: 0, body: null };
|
|
1024
|
+
let json = null;
|
|
1025
|
+
try {
|
|
1026
|
+
json = JSON.parse(res.text);
|
|
1027
|
+
} catch {
|
|
1028
|
+
}
|
|
1029
|
+
return { status: res.status, body: isObject(json) ? json : null };
|
|
1030
|
+
}
|
|
1031
|
+
function refusal2(answer, stage, hostname) {
|
|
1032
|
+
if (answer.status === 401) return new DnsWriteError("token_invalid");
|
|
1033
|
+
if (answer.status === 403)
|
|
1034
|
+
return new DnsWriteError(stage === "reading" ? "zone_read_missing" : "dns_edit_missing");
|
|
1035
|
+
if (answer.status === 422 && stage === "writing")
|
|
1036
|
+
return new DnsWriteError("conflict", { hostname });
|
|
1037
|
+
return new DnsWriteError("unavailable");
|
|
1038
|
+
}
|
|
1039
|
+
function recordsOf2(body) {
|
|
1040
|
+
const rows = body?.domain_records;
|
|
1041
|
+
if (!Array.isArray(rows)) throw new DnsWriteError("unavailable");
|
|
1042
|
+
return rows.map((row) => {
|
|
1043
|
+
if (!isObject(row) || typeof row.id !== "number" || typeof row.type !== "string" || typeof row.data !== "string" || !RECORD_TYPE2.test(row.type))
|
|
1044
|
+
throw new DnsWriteError("unavailable");
|
|
1045
|
+
return { id: String(row.id), type: row.type, content: row.data };
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
var digitalocean = {
|
|
1049
|
+
host: "digitalocean",
|
|
1050
|
+
/** Add the record to the DigitalOcean domain that holds its name. An identical record is left
|
|
1051
|
+
* as it is. DigitalOcean has no proxy, so the outcome is `created` or `unchanged`. */
|
|
1052
|
+
async addRecord({ token, record, fetch: doFetch = fetch }) {
|
|
1053
|
+
const hostname = normalizeDnsName(record.name);
|
|
1054
|
+
let zone;
|
|
1055
|
+
let existing = [];
|
|
1056
|
+
for (const candidate of zoneCandidates(hostname)) {
|
|
1057
|
+
const query = new URLSearchParams({ name: hostname, per_page: "200" });
|
|
1058
|
+
const answer = await call2(
|
|
1059
|
+
doFetch,
|
|
1060
|
+
token,
|
|
1061
|
+
`/domains/${encodeURIComponent(candidate)}/records?${query}`
|
|
1062
|
+
);
|
|
1063
|
+
if (answer.status === 200) {
|
|
1064
|
+
zone = candidate;
|
|
1065
|
+
existing = recordsOf2(answer.body);
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
if (answer.status !== 404) throw refusal2(answer, "reading", hostname);
|
|
1069
|
+
}
|
|
1070
|
+
if (!zone) throw new DnsWriteError("zone_not_found", { hostname });
|
|
1071
|
+
const decision = decide(record, existing, hostname);
|
|
1072
|
+
if (decision.kind === "unchanged") return "unchanged";
|
|
1073
|
+
const name = hostname === zone ? "@" : hostname.slice(0, -(zone.length + 1));
|
|
1074
|
+
const created = await call2(doFetch, token, `/domains/${encodeURIComponent(zone)}/records`, {
|
|
1075
|
+
body: {
|
|
1076
|
+
type: record.type,
|
|
1077
|
+
name,
|
|
1078
|
+
data: record.type === "CNAME" ? `${normalizeDnsName(record.value)}.` : record.value,
|
|
1079
|
+
// The API lists these as required for a TXT; they mean nothing for one.
|
|
1080
|
+
...record.type === "TXT" ? { flags: null, tag: null } : {}
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
if (created.status !== 201) throw refusal2(created, "writing", hostname);
|
|
1084
|
+
return "created";
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
|
|
1088
|
+
// src/writers/namecom.ts
|
|
1089
|
+
var API3 = "https://api.name.com/core/v1";
|
|
1090
|
+
var TIMEOUT_MS3 = 1e4;
|
|
1091
|
+
var MAX_BODY_BYTES3 = 512 * 1024;
|
|
1092
|
+
var PER_PAGE = 500;
|
|
1093
|
+
var MAX_PAGES = 10;
|
|
1094
|
+
var TTL = 300;
|
|
1095
|
+
var RECORD_TYPE3 = /^[A-Z0-9]{1,12}$/;
|
|
1096
|
+
function basicOf(token) {
|
|
1097
|
+
const at = token.indexOf(":");
|
|
1098
|
+
return at > 0 && at < token.length - 1 && !/\s/.test(token) ? btoa(token) : null;
|
|
1099
|
+
}
|
|
1100
|
+
async function call3(doFetch, basic, path, send) {
|
|
1101
|
+
const res = await fetchBounded(
|
|
1102
|
+
doFetch,
|
|
1103
|
+
`${API3}${path}`,
|
|
1104
|
+
{
|
|
1105
|
+
method: send ? "POST" : "GET",
|
|
1106
|
+
headers: {
|
|
1107
|
+
Authorization: `Basic ${basic}`,
|
|
1108
|
+
Accept: "application/json",
|
|
1109
|
+
...send ? { "Content-Type": "application/json" } : {}
|
|
1110
|
+
},
|
|
1111
|
+
body: send ? JSON.stringify(send.body) : void 0
|
|
1112
|
+
},
|
|
1113
|
+
{ timeoutMs: TIMEOUT_MS3, maxBytes: MAX_BODY_BYTES3 }
|
|
1114
|
+
);
|
|
1115
|
+
if (!res) return { status: 0, body: null };
|
|
1116
|
+
let json = null;
|
|
1117
|
+
try {
|
|
1118
|
+
json = JSON.parse(res.text);
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
return { status: res.status, body: isObject(json) ? json : null };
|
|
1122
|
+
}
|
|
1123
|
+
function refusal3(answer, stage, hostname) {
|
|
1124
|
+
if (answer.status === 401) return new DnsWriteError("token_invalid");
|
|
1125
|
+
if (answer.status === 403)
|
|
1126
|
+
return new DnsWriteError(stage === "reading" ? "zone_read_missing" : "dns_edit_missing");
|
|
1127
|
+
if (answer.status === 409) return new DnsWriteError("conflict", { hostname });
|
|
1128
|
+
return new DnsWriteError("unavailable");
|
|
1129
|
+
}
|
|
1130
|
+
function pageOf(body) {
|
|
1131
|
+
const rows = body?.records;
|
|
1132
|
+
if (!Array.isArray(rows)) throw new DnsWriteError("unavailable");
|
|
1133
|
+
const records = rows.map((row) => {
|
|
1134
|
+
if (!isObject(row) || typeof row.id !== "number" || typeof row.type !== "string" || typeof row.answer !== "string" || typeof row.fqdn !== "string" || !RECORD_TYPE3.test(row.type))
|
|
1135
|
+
throw new DnsWriteError("unavailable");
|
|
1136
|
+
return { id: String(row.id), type: row.type, content: row.answer, name: row.fqdn };
|
|
1137
|
+
});
|
|
1138
|
+
return { records, more: typeof body?.nextPage === "number" };
|
|
1139
|
+
}
|
|
1140
|
+
var namecom = {
|
|
1141
|
+
host: "name-com",
|
|
1142
|
+
/** Add the record to the Name.com domain that holds its name. An identical record is left as
|
|
1143
|
+
* it is. Name.com has no proxy, so the outcome is `created` or `unchanged`. */
|
|
1144
|
+
async addRecord({ token, record, fetch: doFetch = fetch }) {
|
|
1145
|
+
const hostname = normalizeDnsName(record.name);
|
|
1146
|
+
const basic = basicOf(token);
|
|
1147
|
+
if (!basic) throw new DnsWriteError("token_invalid");
|
|
1148
|
+
let zone;
|
|
1149
|
+
let existing = [];
|
|
1150
|
+
for (const candidate of zoneCandidates(hostname)) {
|
|
1151
|
+
const path = `/domains/${encodeURIComponent(candidate)}/records`;
|
|
1152
|
+
const first = await call3(doFetch, basic, `${path}?perPage=${PER_PAGE}`);
|
|
1153
|
+
if (first.status === 404) continue;
|
|
1154
|
+
if (first.status !== 200) throw refusal3(first, "reading", hostname);
|
|
1155
|
+
zone = candidate;
|
|
1156
|
+
let page = pageOf(first.body);
|
|
1157
|
+
existing = page.records;
|
|
1158
|
+
for (let next = 2; page.more && next <= MAX_PAGES; next++) {
|
|
1159
|
+
const answer = await call3(doFetch, basic, `${path}?perPage=${PER_PAGE}&page=${next}`);
|
|
1160
|
+
if (answer.status !== 200) throw refusal3(answer, "reading", hostname);
|
|
1161
|
+
page = pageOf(answer.body);
|
|
1162
|
+
existing = existing.concat(page.records);
|
|
1163
|
+
}
|
|
1164
|
+
if (page.more) throw new DnsWriteError("unavailable");
|
|
1165
|
+
break;
|
|
1166
|
+
}
|
|
1167
|
+
if (!zone) throw new DnsWriteError("zone_not_found", { hostname });
|
|
1168
|
+
const here = existing.filter(
|
|
1169
|
+
(r) => r.name !== void 0 && normalizeDnsName(r.name) === hostname
|
|
1170
|
+
);
|
|
1171
|
+
const decision = decide(record, here, hostname);
|
|
1172
|
+
if (decision.kind === "unchanged") return "unchanged";
|
|
1173
|
+
const host = hostname === zone ? "@" : hostname.slice(0, -(zone.length + 1));
|
|
1174
|
+
const created = await call3(doFetch, basic, `/domains/${encodeURIComponent(zone)}/records`, {
|
|
1175
|
+
body: { host, type: record.type, answer: record.value, ttl: TTL }
|
|
1176
|
+
});
|
|
1177
|
+
if (created.status !== 200) throw refusal3(created, "writing", hostname);
|
|
1178
|
+
const stored = created.body?.answer;
|
|
1179
|
+
if (typeof stored !== "string" || !sameValue(record, { content: stored }))
|
|
1180
|
+
throw new DnsWriteError("unavailable");
|
|
1181
|
+
return "created";
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
// src/writers/porkbun.ts
|
|
1186
|
+
var API4 = "https://api.porkbun.com/api/json/v3";
|
|
1187
|
+
var TIMEOUT_MS4 = 1e4;
|
|
1188
|
+
var MAX_BODY_BYTES4 = 256 * 1024;
|
|
1189
|
+
var KEY_CODES = /* @__PURE__ */ new Set([
|
|
1190
|
+
"API_KEY_REQUIRED",
|
|
1191
|
+
"INVALID_API_KEYS_001",
|
|
1192
|
+
"INVALID_TOKEN",
|
|
1193
|
+
"INVALID_USER"
|
|
1194
|
+
]);
|
|
1195
|
+
var ACCESS_CODES = /* @__PURE__ */ new Set(["API_ACCESS_DISABLED", "IP_NOT_ALLOWED", "DOMAIN_NOT_ALLOWED"]);
|
|
1196
|
+
var NOT_A_ZONE_CODES = /* @__PURE__ */ new Set(["INVALID_DOMAIN", "DOMAIN_NOT_FOUND"]);
|
|
1197
|
+
var ID2 = /^[0-9]{1,20}$/;
|
|
1198
|
+
var RECORD_TYPE4 = /^[A-Z0-9]{1,12}$/;
|
|
1199
|
+
function keysOf(token) {
|
|
1200
|
+
const at = token.indexOf(":");
|
|
1201
|
+
const key = token.slice(0, at);
|
|
1202
|
+
const secret = token.slice(at + 1);
|
|
1203
|
+
return at > 0 && secret && !/\s/.test(token) ? { key, secret } : null;
|
|
1204
|
+
}
|
|
1205
|
+
async function call4(doFetch, keys, path, send) {
|
|
1206
|
+
const res = await fetchBounded(
|
|
1207
|
+
doFetch,
|
|
1208
|
+
`${API4}${path}`,
|
|
1209
|
+
{
|
|
1210
|
+
method: send ? "POST" : "GET",
|
|
1211
|
+
headers: {
|
|
1212
|
+
"X-API-Key": keys.key,
|
|
1213
|
+
"X-Secret-API-Key": keys.secret,
|
|
1214
|
+
Accept: "application/json",
|
|
1215
|
+
...send ? { "Content-Type": "application/json" } : {}
|
|
1216
|
+
},
|
|
1217
|
+
body: send ? JSON.stringify(send.body) : void 0
|
|
1218
|
+
},
|
|
1219
|
+
{ timeoutMs: TIMEOUT_MS4, maxBytes: MAX_BODY_BYTES4 }
|
|
1220
|
+
);
|
|
1221
|
+
if (!res) return { code: "", ok: false, body: null };
|
|
1222
|
+
let json = null;
|
|
1223
|
+
try {
|
|
1224
|
+
json = JSON.parse(res.text);
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
const body = isObject(json) ? json : null;
|
|
1228
|
+
return {
|
|
1229
|
+
code: typeof body?.code === "string" ? body.code : "",
|
|
1230
|
+
ok: res.status === 200 && body?.status === "SUCCESS",
|
|
1231
|
+
body
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
function refusal4(answer, hostname) {
|
|
1235
|
+
if (KEY_CODES.has(answer.code)) return new DnsWriteError("token_invalid");
|
|
1236
|
+
if (ACCESS_CODES.has(answer.code)) return new DnsWriteError("dns_edit_missing");
|
|
1237
|
+
if (answer.code === "RECORD_CONFLICT") return new DnsWriteError("conflict", { hostname });
|
|
1238
|
+
return new DnsWriteError("unavailable");
|
|
1239
|
+
}
|
|
1240
|
+
function recordsOf3(body) {
|
|
1241
|
+
const rows = body?.records;
|
|
1242
|
+
if (!Array.isArray(rows)) throw new DnsWriteError("unavailable");
|
|
1243
|
+
return rows.map((row) => {
|
|
1244
|
+
if (!isObject(row) || typeof row.id !== "string" || typeof row.name !== "string" || typeof row.type !== "string" || typeof row.content !== "string" || !ID2.test(row.id) || !RECORD_TYPE4.test(row.type))
|
|
1245
|
+
throw new DnsWriteError("unavailable");
|
|
1246
|
+
return { id: row.id, type: row.type, content: row.content, name: row.name };
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
var porkbun = {
|
|
1250
|
+
host: "porkbun",
|
|
1251
|
+
/** Add the record to the Porkbun domain that holds its name. An identical record is left as
|
|
1252
|
+
* it is. Porkbun has no proxy, so the outcome is `created` or `unchanged`. */
|
|
1253
|
+
async addRecord({ token, record, comment, fetch: doFetch = fetch }) {
|
|
1254
|
+
const hostname = normalizeDnsName(record.name);
|
|
1255
|
+
const keys = keysOf(token);
|
|
1256
|
+
if (!keys) throw new DnsWriteError("token_invalid");
|
|
1257
|
+
let zone;
|
|
1258
|
+
let existing = [];
|
|
1259
|
+
for (const candidate of zoneCandidates(hostname)) {
|
|
1260
|
+
const answer = await call4(doFetch, keys, `/dns/retrieve/${encodeURIComponent(candidate)}`);
|
|
1261
|
+
if (answer.ok) {
|
|
1262
|
+
zone = candidate;
|
|
1263
|
+
existing = recordsOf3(answer.body).filter(
|
|
1264
|
+
(r) => r.name !== void 0 && normalizeDnsName(r.name) === hostname
|
|
1265
|
+
);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
if (!NOT_A_ZONE_CODES.has(answer.code)) throw refusal4(answer, hostname);
|
|
1269
|
+
}
|
|
1270
|
+
if (!zone) throw new DnsWriteError("zone_not_found", { hostname });
|
|
1271
|
+
const decision = decide(record, existing, hostname);
|
|
1272
|
+
if (decision.kind === "unchanged") return "unchanged";
|
|
1273
|
+
const subdomain = hostname === zone ? "" : hostname.slice(0, -(zone.length + 1));
|
|
1274
|
+
const created = await call4(doFetch, keys, `/dns/create/${encodeURIComponent(zone)}`, {
|
|
1275
|
+
body: {
|
|
1276
|
+
name: subdomain,
|
|
1277
|
+
type: record.type,
|
|
1278
|
+
content: record.value,
|
|
1279
|
+
...comment ? { notes: comment } : {}
|
|
1280
|
+
}
|
|
1281
|
+
});
|
|
1282
|
+
if (created.code === "DUPLICATE_RECORD") return "unchanged";
|
|
1283
|
+
if (!created.ok) throw refusal4(created, hostname);
|
|
1284
|
+
return "created";
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
970
1288
|
// src/writers/index.ts
|
|
971
|
-
var writers = {
|
|
1289
|
+
var writers = {
|
|
1290
|
+
cloudflare,
|
|
1291
|
+
digitalocean,
|
|
1292
|
+
"name-com": namecom,
|
|
1293
|
+
porkbun
|
|
1294
|
+
};
|
|
972
1295
|
|
|
973
1296
|
// src/server/plan.ts
|
|
974
1297
|
async function planConnection(input, io) {
|
|
@@ -1016,5 +1339,6 @@ async function planConnection(input, io) {
|
|
|
1016
1339
|
readDnsZone,
|
|
1017
1340
|
signedApplyUrl,
|
|
1018
1341
|
systemDnsLookups,
|
|
1019
|
-
systemResolver
|
|
1342
|
+
systemResolver,
|
|
1343
|
+
templateCoverage
|
|
1020
1344
|
});
|
package/dist/server/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Resolver } from 'node:dns/promises';
|
|
2
|
+
import { F as FetchFn } from '../bounded-fetch-nozW2egX.cjs';
|
|
2
3
|
import { h as DnsZone, e as DnsHostReading, D as DnsHostId } from '../hosts-B0_V4Ned.cjs';
|
|
3
4
|
|
|
4
5
|
/** What one question came back with. `absent` is DNS answering that nothing is there; `failed`
|
|
@@ -56,7 +57,7 @@ declare const CONNECT_PROVIDERS: readonly ConnectProvider[];
|
|
|
56
57
|
|
|
57
58
|
interface DiscoveryIo {
|
|
58
59
|
txt: DnsLookups["txt"];
|
|
59
|
-
fetch?:
|
|
60
|
+
fetch?: FetchFn;
|
|
60
61
|
}
|
|
61
62
|
type Discovery = {
|
|
62
63
|
found: true;
|
|
@@ -83,6 +84,18 @@ declare function discoverConnect(io: DiscoveryIo, args: {
|
|
|
83
84
|
providers?: readonly ConnectProvider[];
|
|
84
85
|
}): Promise<Discovery>;
|
|
85
86
|
|
|
87
|
+
type TemplateSupport = "supported" | "not_supported" | "unanswered";
|
|
88
|
+
/** The answer of each provider, by our id for it (`cloudflare`). A status that says "not now"
|
|
89
|
+
* (429, 5xx), a failed request or a redirect is `unanswered`, never "no". */
|
|
90
|
+
declare function templateCoverage(io: {
|
|
91
|
+
fetch?: FetchFn;
|
|
92
|
+
}, args: {
|
|
93
|
+
providerId: string;
|
|
94
|
+
serviceId: string;
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
providers?: readonly ConnectProvider[];
|
|
97
|
+
}): Promise<Record<string, TemplateSupport>>;
|
|
98
|
+
|
|
86
99
|
/** Who is asking, as the templates name it. */
|
|
87
100
|
interface DomainConnectIdentity {
|
|
88
101
|
/** The provider id our templates are filed under in the Templates repository
|
|
@@ -141,4 +154,4 @@ interface PlanInput {
|
|
|
141
154
|
}
|
|
142
155
|
declare function planConnection(input: PlanInput, io: DiscoveryIo & ZoneLookups): Promise<ConnectPlan>;
|
|
143
156
|
|
|
144
|
-
export { CONNECT_PROVIDERS, type ConnectMethod, type ConnectPlan, type ConnectProvider, type Discovery, type DiscoveryIo, type DnsAnswer, type DnsAsk, type DnsLookups, type DomainConnectIdentity, type PlanInput, type ZoneLookups, discoverConnect, dnsHostOf, planConnection, readDnsZone, signedApplyUrl, systemDnsLookups, systemResolver };
|
|
157
|
+
export { CONNECT_PROVIDERS, type ConnectMethod, type ConnectPlan, type ConnectProvider, type Discovery, type DiscoveryIo, type DnsAnswer, type DnsAsk, type DnsLookups, type DomainConnectIdentity, FetchFn, type PlanInput, type TemplateSupport, type ZoneLookups, discoverConnect, dnsHostOf, planConnection, readDnsZone, signedApplyUrl, systemDnsLookups, systemResolver, templateCoverage };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Resolver } from 'node:dns/promises';
|
|
2
|
+
import { F as FetchFn } from '../bounded-fetch-nozW2egX.js';
|
|
2
3
|
import { h as DnsZone, e as DnsHostReading, D as DnsHostId } from '../hosts-B0_V4Ned.js';
|
|
3
4
|
|
|
4
5
|
/** What one question came back with. `absent` is DNS answering that nothing is there; `failed`
|
|
@@ -56,7 +57,7 @@ declare const CONNECT_PROVIDERS: readonly ConnectProvider[];
|
|
|
56
57
|
|
|
57
58
|
interface DiscoveryIo {
|
|
58
59
|
txt: DnsLookups["txt"];
|
|
59
|
-
fetch?:
|
|
60
|
+
fetch?: FetchFn;
|
|
60
61
|
}
|
|
61
62
|
type Discovery = {
|
|
62
63
|
found: true;
|
|
@@ -83,6 +84,18 @@ declare function discoverConnect(io: DiscoveryIo, args: {
|
|
|
83
84
|
providers?: readonly ConnectProvider[];
|
|
84
85
|
}): Promise<Discovery>;
|
|
85
86
|
|
|
87
|
+
type TemplateSupport = "supported" | "not_supported" | "unanswered";
|
|
88
|
+
/** The answer of each provider, by our id for it (`cloudflare`). A status that says "not now"
|
|
89
|
+
* (429, 5xx), a failed request or a redirect is `unanswered`, never "no". */
|
|
90
|
+
declare function templateCoverage(io: {
|
|
91
|
+
fetch?: FetchFn;
|
|
92
|
+
}, args: {
|
|
93
|
+
providerId: string;
|
|
94
|
+
serviceId: string;
|
|
95
|
+
timeoutMs?: number;
|
|
96
|
+
providers?: readonly ConnectProvider[];
|
|
97
|
+
}): Promise<Record<string, TemplateSupport>>;
|
|
98
|
+
|
|
86
99
|
/** Who is asking, as the templates name it. */
|
|
87
100
|
interface DomainConnectIdentity {
|
|
88
101
|
/** The provider id our templates are filed under in the Templates repository
|
|
@@ -141,4 +154,4 @@ interface PlanInput {
|
|
|
141
154
|
}
|
|
142
155
|
declare function planConnection(input: PlanInput, io: DiscoveryIo & ZoneLookups): Promise<ConnectPlan>;
|
|
143
156
|
|
|
144
|
-
export { CONNECT_PROVIDERS, type ConnectMethod, type ConnectPlan, type ConnectProvider, type Discovery, type DiscoveryIo, type DnsAnswer, type DnsAsk, type DnsLookups, type DomainConnectIdentity, type PlanInput, type ZoneLookups, discoverConnect, dnsHostOf, planConnection, readDnsZone, signedApplyUrl, systemDnsLookups, systemResolver };
|
|
157
|
+
export { CONNECT_PROVIDERS, type ConnectMethod, type ConnectPlan, type ConnectProvider, type Discovery, type DiscoveryIo, type DnsAnswer, type DnsAsk, type DnsLookups, type DomainConnectIdentity, FetchFn, type PlanInput, type TemplateSupport, type ZoneLookups, discoverConnect, dnsHostOf, planConnection, readDnsZone, signedApplyUrl, systemDnsLookups, systemResolver, templateCoverage };
|
package/dist/server/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
fetchBounded,
|
|
6
6
|
isObject,
|
|
7
7
|
writers
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-4XNRV7QR.js";
|
|
9
9
|
import {
|
|
10
10
|
normalizeDnsName
|
|
11
11
|
} from "../chunk-P2O7INDB.js";
|
|
@@ -211,6 +211,25 @@ async function ask(io, args, deadline) {
|
|
|
211
211
|
};
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
// src/server/connect/coverage.ts
|
|
215
|
+
async function templateCoverage(io, args) {
|
|
216
|
+
const { providerId, serviceId, timeoutMs = 5e3, providers = CONNECT_PROVIDERS } = args;
|
|
217
|
+
const answers = await Promise.all(
|
|
218
|
+
providers.map(async (provider) => {
|
|
219
|
+
const answer = await fetchBounded(
|
|
220
|
+
io.fetch ?? fetch,
|
|
221
|
+
`${provider.api}/v2/domainTemplates/providers/${providerId}/services/${serviceId}`,
|
|
222
|
+
{ method: "GET", headers: { Accept: "application/json" } },
|
|
223
|
+
{ timeoutMs, maxBytes: 64 * 1024 }
|
|
224
|
+
);
|
|
225
|
+
if (answer?.status === 200) return [provider.id, "supported"];
|
|
226
|
+
if (answer?.status === 404) return [provider.id, "not_supported"];
|
|
227
|
+
return [provider.id, "unanswered"];
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
return Object.fromEntries(answers);
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
// src/server/connect/sign.ts
|
|
215
234
|
import { createPrivateKey, sign } from "crypto";
|
|
216
235
|
var encode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`).replace(/%20/g, "+");
|
|
@@ -270,5 +289,6 @@ export {
|
|
|
270
289
|
readDnsZone,
|
|
271
290
|
signedApplyUrl,
|
|
272
291
|
systemDnsLookups,
|
|
273
|
-
systemResolver
|
|
292
|
+
systemResolver,
|
|
293
|
+
templateCoverage
|
|
274
294
|
};
|