@gusnips/one-click-dns 0.1.1 → 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 CHANGED
@@ -102,6 +102,22 @@ When the host has your template, `plan.methods` starts with a `domain-connect` m
102
102
 
103
103
  Only these providers are asked, because their `_domainconnect` value and settings were read from DNS: Cloudflare, Vercel, Simply.com and GoDaddy. A name in a customer's DNS is never fetched. More providers join the same way, one value at a time.
104
104
 
105
+ ### Which providers have switched your template on
106
+
107
+ A merged template is not a template a provider uses. Each provider turns templates on by hand, and the only way to know is to ask it. `templateCoverage` asks every provider in the list once:
108
+
109
+ ```ts
110
+ import { templateCoverage } from "@gusnips/one-click-dns/server";
111
+
112
+ const coverage = await templateCoverage(
113
+ {},
114
+ { providerId: "example.com", serviceId: "custom-domain" },
115
+ );
116
+ // { cloudflare: "not_supported", godaddy: "supported", vercel: "not_supported", simply: "not_supported" }
117
+ ```
118
+
119
+ `"unanswered"` means the provider was busy or did not reply. It never means no. Run it now and then to see when a provider starts to support your template.
120
+
105
121
  ## Templates
106
122
 
107
123
  A template is a small JSON file in the public [Domain Connect Templates](https://github.com/Domain-Connect/Templates) repository. It lists the records you may ask a host to add. The presets build one that follows the repository's rules:
@@ -5,22 +5,25 @@ import {
5
5
  // src/bounded-fetch.ts
6
6
  import { readBounded } from "@gusnips/server";
7
7
  async function fetchBounded(doFetch, url, init, limits) {
8
- try {
9
- const res = await doFetch(url, {
10
- ...init,
11
- redirect: "manual",
12
- signal: AbortSignal.timeout(limits.timeoutMs)
13
- });
14
- if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
15
- await res.body?.cancel().catch(() => void 0);
16
- return { status: res.status || 302, text: "" };
8
+ const signal = AbortSignal.timeout(limits.timeoutMs);
9
+ const outOfTime = new Promise((resolve) => {
10
+ signal.addEventListener("abort", () => resolve(null), { once: true });
11
+ });
12
+ const attempt = async () => {
13
+ try {
14
+ const res = await doFetch(url, { ...init, redirect: "manual", signal });
15
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
16
+ await res.body?.cancel().catch(() => void 0);
17
+ return { status: res.status || 302, text: "" };
18
+ }
19
+ const { bytes, truncated } = await readBounded(res.body, limits.maxBytes);
20
+ if (truncated) return null;
21
+ return { status: res.status, text: new TextDecoder().decode(bytes) };
22
+ } catch {
23
+ return null;
17
24
  }
18
- const { bytes, truncated } = await readBounded(res.body, limits.maxBytes);
19
- if (truncated) return null;
20
- return { status: res.status, text: new TextDecoder().decode(bytes) };
21
- } catch {
22
- return null;
23
- }
25
+ };
26
+ return Promise.race([attempt(), outOfTime]);
24
27
  }
25
28
 
26
29
  // src/guards.ts
@@ -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
- try {
661
- const res = await doFetch(url, {
662
- ...init,
663
- redirect: "manual",
664
- signal: AbortSignal.timeout(limits.timeoutMs)
665
- });
666
- if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
667
- await res.body?.cancel().catch(() => void 0);
668
- return { status: res.status || 302, text: "" };
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
- const { bytes, truncated } = await (0, import_server.readBounded)(res.body, limits.maxBytes);
671
- if (truncated) return null;
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, "+");
@@ -1316,5 +1339,6 @@ async function planConnection(input, io) {
1316
1339
  readDnsZone,
1317
1340
  signedApplyUrl,
1318
1341
  systemDnsLookups,
1319
- systemResolver
1342
+ systemResolver,
1343
+ templateCoverage
1320
1344
  });
@@ -84,6 +84,18 @@ declare function discoverConnect(io: DiscoveryIo, args: {
84
84
  providers?: readonly ConnectProvider[];
85
85
  }): Promise<Discovery>;
86
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
+
87
99
  /** Who is asking, as the templates name it. */
88
100
  interface DomainConnectIdentity {
89
101
  /** The provider id our templates are filed under in the Templates repository
@@ -142,4 +154,4 @@ interface PlanInput {
142
154
  }
143
155
  declare function planConnection(input: PlanInput, io: DiscoveryIo & ZoneLookups): Promise<ConnectPlan>;
144
156
 
145
- 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 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 };
@@ -84,6 +84,18 @@ declare function discoverConnect(io: DiscoveryIo, args: {
84
84
  providers?: readonly ConnectProvider[];
85
85
  }): Promise<Discovery>;
86
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
+
87
99
  /** Who is asking, as the templates name it. */
88
100
  interface DomainConnectIdentity {
89
101
  /** The provider id our templates are filed under in the Templates repository
@@ -142,4 +154,4 @@ interface PlanInput {
142
154
  }
143
155
  declare function planConnection(input: PlanInput, io: DiscoveryIo & ZoneLookups): Promise<ConnectPlan>;
144
156
 
145
- 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 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 };
@@ -5,7 +5,7 @@ import {
5
5
  fetchBounded,
6
6
  isObject,
7
7
  writers
8
- } from "../chunk-56ZUD4EQ.js";
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
  };
@@ -32,22 +32,25 @@ module.exports = __toCommonJS(writers_exports);
32
32
  // src/bounded-fetch.ts
33
33
  var import_server = require("@gusnips/server");
34
34
  async function fetchBounded(doFetch, url, init, limits) {
35
- try {
36
- const res = await doFetch(url, {
37
- ...init,
38
- redirect: "manual",
39
- signal: AbortSignal.timeout(limits.timeoutMs)
40
- });
41
- if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
42
- await res.body?.cancel().catch(() => void 0);
43
- return { status: res.status || 302, text: "" };
35
+ const signal = AbortSignal.timeout(limits.timeoutMs);
36
+ const outOfTime = new Promise((resolve) => {
37
+ signal.addEventListener("abort", () => resolve(null), { once: true });
38
+ });
39
+ const attempt = async () => {
40
+ try {
41
+ const res = await doFetch(url, { ...init, redirect: "manual", signal });
42
+ if (res.type === "opaqueredirect" || res.status >= 300 && res.status < 400) {
43
+ await res.body?.cancel().catch(() => void 0);
44
+ return { status: res.status || 302, text: "" };
45
+ }
46
+ const { bytes, truncated } = await (0, import_server.readBounded)(res.body, limits.maxBytes);
47
+ if (truncated) return null;
48
+ return { status: res.status, text: new TextDecoder().decode(bytes) };
49
+ } catch {
50
+ return null;
44
51
  }
45
- const { bytes, truncated } = await (0, import_server.readBounded)(res.body, limits.maxBytes);
46
- if (truncated) return null;
47
- return { status: res.status, text: new TextDecoder().decode(bytes) };
48
- } catch {
49
- return null;
50
- }
52
+ };
53
+ return Promise.race([attempt(), outOfTime]);
51
54
  }
52
55
 
53
56
  // src/dns-name.ts
@@ -5,7 +5,7 @@ import {
5
5
  namecom,
6
6
  porkbun,
7
7
  writers
8
- } from "../chunk-56ZUD4EQ.js";
8
+ } from "../chunk-4XNRV7QR.js";
9
9
  import "../chunk-P2O7INDB.js";
10
10
  export {
11
11
  DnsWriteError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gusnips/one-click-dns",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "packageManager": "bun@1.4.2",
5
5
  "description": "Put DNS records in a customer's zone: find their DNS host, send them to a signed Domain Connect link, write with an API token, or show the steps for that host",
6
6
  "type": "module",