@gusnips/one-click-dns 0.1.3 → 0.1.5

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
@@ -62,17 +62,27 @@ When the write fails, it throws a `DnsWriteError` with a `code`. Word each code
62
62
  | `conflict` | The name already has another record (`params.type`). They decide what to do with it. |
63
63
  | `unavailable` | The host did not answer. Try again. |
64
64
 
65
- Four hosts have a writer today. What the person pastes is different for each. Every test uses a fake network, so none of them has run against a real account here:
65
+ Four hosts have a writer today. What the person pastes is different for each. The tests use a fake network. Cloudflare and DigitalOcean were also run against a real account, with made-up underscore names that were deleted after. Porkbun and Name.com were not:
66
66
 
67
- | Writer | `token` is | What it needs | How far it is checked |
68
- | -------------- | ----------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------ |
69
- | `cloudflare` | An API token | `Zone:Read` and `DNS:Edit` | Lifted from featury's live help-center code |
70
- | `porkbun` | `apikey:secretapikey` | "API access" turned on for the domain in Porkbun (it is off by default) | Written from the host's API spec. Never run against a real account |
71
- | `digitalocean` | A personal access token | `domain:read` and `domain:create`, or full access | Written from the host's API spec. Never run against a real account |
72
- | `name-com` | `username:apitoken` | API access turned on in Account Settings, Security | Written from the host's docs. Never run against a real account |
67
+ | Writer | `token` is | What it needs | How far it is checked |
68
+ | -------------- | ----------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
69
+ | `cloudflare` | An API token | `Zone:Read` and `DNS:Edit` | Run against a real zone on 2026-10-29: create, repeat, TXT, conflicts and a wrong token |
70
+ | `porkbun` | `apikey:secretapikey` | "API access" turned on for the domain in Porkbun (it is off by default) | Written from the host's API spec. Never run against a real account |
71
+ | `digitalocean` | A personal access token | `domain:read` and `domain:create`, or full access | Run against a real domain on 2026-10-29: create, repeat, TXT, conflicts and a wrong token |
72
+ | `name-com` | `username:apitoken` | API access turned on in Account Settings, Security | Written from the host's docs. Never run against a real account |
73
73
 
74
74
  `writers` maps a host id to its writer, so `writers[host]` is `undefined` for a host with no writer.
75
75
 
76
+ ### Check a writer against your own account
77
+
78
+ The repository has a script that tries a writer on a real zone and removes what it made. Use a spare zone and a token limited to it:
79
+
80
+ ```bash
81
+ HOST=cloudflare ZONE=example.com TOKEN=your-token bun run live
82
+ ```
83
+
84
+ It writes a CNAME and a TXT record under made-up underscore names, runs each a second time (both must read as "unchanged"), checks that a clashing record is refused and that a wrong token is reported as one, then deletes its records. Cloudflare and DigitalOcean are supported.
85
+
76
86
  ## One-click links (Domain Connect)
77
87
 
78
88
  A link needs a template that your DNS host has switched on, and a key pair. You publish the public key in DNS and keep the private key on your server.
@@ -102,6 +112,8 @@ When the host has your template, `plan.methods` starts with a `domain-connect` m
102
112
 
103
113
  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
114
 
115
+ A provider listed here is one the package can talk to. It is not a promise that the provider will switch your template on. GoDaddy, for one, told us on 2026-10-29 that it is not accepting new templates for direct onboarding and gave no date. Ask each provider, and use `templateCoverage` to see who said yes. Where there is no link, `plan.methods` still ends with the steps for that host.
116
+
105
117
  ### Which providers have switched your template on
106
118
 
107
119
  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:
@@ -147,6 +159,39 @@ A template belongs to the company that files it. It names your key domain, and t
147
159
 
148
160
  To file a template: test it in the [online editor](https://domainconnect.paulonet.eu/dc/free/templateedit), run `dc-template-linter -merge-or-fail` on it, open a pull request, then ask each DNS host to switch it on.
149
161
 
162
+ ### Keep separate SPF services
163
+
164
+ Two links for the same template can replace each other's records, even if you check DNS before issuing either link. For SPF, that can remove another service's permission to send.
165
+
166
+ When one template serves several sending services, opt in to separate instances:
167
+
168
+ ```ts
169
+ const template = spfTemplate(identity, { multiInstance: true, version: 2 });
170
+ ```
171
+
172
+ File the updated template and wait for the DNS provider to switch it on. Before signing a link, require that provider's copy to advertise the flag:
173
+
174
+ ```ts
175
+ import { discoverConnect, systemDnsLookups } from "@gusnips/one-click-dns/server";
176
+
177
+ const discovery = await discoverConnect(
178
+ { txt: systemDnsLookups(2000).txt },
179
+ {
180
+ zone: "acme.com",
181
+ providerId: "example.com",
182
+ serviceId: "spf",
183
+ budgetMs: 2000,
184
+ requireMultiInstance: true,
185
+ },
186
+ );
187
+ ```
188
+
189
+ This check needs both a provider entry with `supportsMultiInstance: true` and the literal template flag `multiInstance: true`. Missing or false support returns `not_offered`. The template check reads the same size-limited response and makes no extra request. Other discovery calls keep their current behavior.
190
+
191
+ No built-in provider has verified instance support yet. [Cloudflare says it ignores this flag](https://developers.cloudflare.com/dns/reference/domain-connect/#template-definition); the other providers are unverified. Requiring instances therefore returns `not_offered` for each built-in provider. Keep a manual fallback. For a custom `providers` list, set `supportsMultiInstance: true` only after verifying that provider's behavior, not just its template JSON.
192
+
193
+ Only sign when `discovery.found` is true. The signed sync link carries the template flag; the spec defines `instanceId` for the async apply and revert, not for the sync parameters. If you later remove one application through the async API, pass a stable `instanceId` derived from the full sending domain and the service's SPF include: the same pair reuses the same id, and different domains or services use different ids. The package does not choose that id for you. The flag does not check SPF's lookup limit or make a redirect back to your app proof that DNS changed.
194
+
150
195
  ## Steps for the host
151
196
 
152
197
  `DNS_HOSTS` holds 33 hosts: how to recognise them from their name servers, the labels on their DNS page and the traps (a proxy on by default, a trailing dot, a TXT that needs quotes). The package holds data and no sentences, so you write the steps in your own words and languages. `dnsRecordRow` spells a record the way a host wants it typed:
@@ -81,23 +81,26 @@ var API = "https://api.cloudflare.com/client/v4";
81
81
  var TIMEOUT_MS = 1e4;
82
82
  var MAX_BODY_BYTES = 256 * 1024;
83
83
  var TOKEN_CODES = /* @__PURE__ */ new Set([1e3, 6003, 6111, 1e4]);
84
+ var INVALID_TOKEN_MESSAGE = /invalid access token/i;
84
85
  var CONFLICT_CODES = /* @__PURE__ */ new Set([81053, 81054, 81057]);
85
86
  var ID = /^[A-Za-z0-9]{1,64}$/;
86
87
  var RECORD_TYPE = /^[A-Z0-9]{1,12}$/;
87
88
  function readEnvelope(json) {
88
89
  if (!isObject(json) || typeof json.success !== "boolean") return null;
89
90
  const codes = [];
91
+ const messages = [];
90
92
  if (Array.isArray(json.errors)) {
91
93
  for (const error of json.errors) {
92
94
  if (!isObject(error)) return null;
93
95
  if (typeof error.code === "number") codes.push(error.code);
96
+ if (typeof error.message === "string") messages.push(error.message);
94
97
  if (Array.isArray(error.error_chain)) {
95
98
  for (const link of error.error_chain)
96
99
  if (isObject(link) && typeof link.code === "number") codes.push(link.code);
97
100
  }
98
101
  }
99
102
  }
100
- return { success: json.success, codes, result: json.result };
103
+ return { success: json.success, codes, messages, result: json.result };
101
104
  }
102
105
  async function call(doFetch, token, path, send) {
103
106
  const res = await fetchBounded(
@@ -124,7 +127,8 @@ async function call(doFetch, token, path, send) {
124
127
  }
125
128
  function refusal(answer, stage, subject) {
126
129
  const codes = answer.body?.codes ?? [];
127
- const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c));
130
+ const messages = answer.body?.messages ?? [];
131
+ const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c)) || messages.some((m) => INVALID_TOKEN_MESSAGE.test(m));
128
132
  if (stage === "zones" && refusedToken) return new DnsWriteError("token_invalid");
129
133
  if (codes.some((c) => CONFLICT_CODES.has(c)))
130
134
  return new DnsWriteError("conflict", { ...subject });
package/dist/index.cjs CHANGED
@@ -606,6 +606,7 @@ function template(identity, serviceId, fallback, wording, records, hostRequired)
606
606
  syncPubKeyDomain: identity.syncPubKeyDomain,
607
607
  syncRedirectDomain: identity.syncRedirectDomain,
608
608
  ...hostRequired ? { hostRequired } : {},
609
+ ...wording.multiInstance !== void 0 ? { multiInstance: wording.multiInstance } : {},
609
610
  records
610
611
  };
611
612
  }
package/dist/index.d.cts CHANGED
@@ -70,14 +70,17 @@ interface DomainConnectTemplate extends TemplateIdentity {
70
70
  variableDescription?: string;
71
71
  /** The subdomain is required: the template has no meaning at the name itself. */
72
72
  hostRequired?: boolean;
73
+ /** Tell a stateful provider that a re-apply must not remove earlier applications. */
74
+ multiInstance?: boolean;
73
75
  records: TemplateRecord[];
74
76
  }
75
- /** What every preset lets the app word itself. */
77
+ /** What every preset lets the app choose. */
76
78
  interface Wording {
77
79
  serviceId?: string;
78
80
  serviceName?: string;
79
81
  description?: string;
80
82
  version?: number;
83
+ multiInstance?: boolean;
81
84
  }
82
85
  /** The `<providerId>.<serviceId>.json` name the Templates repository files it under. */
83
86
  declare function templateFileName(t: Pick<DomainConnectTemplate, "providerId" | "serviceId">): string;
package/dist/index.d.ts CHANGED
@@ -70,14 +70,17 @@ interface DomainConnectTemplate extends TemplateIdentity {
70
70
  variableDescription?: string;
71
71
  /** The subdomain is required: the template has no meaning at the name itself. */
72
72
  hostRequired?: boolean;
73
+ /** Tell a stateful provider that a re-apply must not remove earlier applications. */
74
+ multiInstance?: boolean;
73
75
  records: TemplateRecord[];
74
76
  }
75
- /** What every preset lets the app word itself. */
77
+ /** What every preset lets the app choose. */
76
78
  interface Wording {
77
79
  serviceId?: string;
78
80
  serviceName?: string;
79
81
  description?: string;
80
82
  version?: number;
83
+ multiInstance?: boolean;
81
84
  }
82
85
  /** The `<providerId>.<serviceId>.json` name the Templates repository files it under. */
83
86
  declare function templateFileName(t: Pick<DomainConnectTemplate, "providerId" | "serviceId">): string;
package/dist/index.js CHANGED
@@ -60,6 +60,7 @@ function template(identity, serviceId, fallback, wording, records, hostRequired)
60
60
  syncPubKeyDomain: identity.syncPubKeyDomain,
61
61
  syncRedirectDomain: identity.syncRedirectDomain,
62
62
  ...hostRequired ? { hostRequired } : {},
63
+ ...wording.multiInstance !== void 0 ? { multiInstance: wording.multiInstance } : {},
63
64
  records
64
65
  };
65
66
  }
@@ -627,7 +627,10 @@ var CONNECT_PROVIDERS = [
627
627
  id: "cloudflare",
628
628
  txt: "api.cloudflare.com/client/v4/dns/domainconnect",
629
629
  api: "https://api.cloudflare.com/client/v4/dns/domainconnect",
630
- syncHost: "dash.cloudflare.com"
630
+ syncHost: "dash.cloudflare.com",
631
+ // Cloudflare documents this flag as unsupported and ignored:
632
+ // https://developers.cloudflare.com/dns/reference/domain-connect/#template-definition
633
+ supportsMultiInstance: false
631
634
  },
632
635
  // `_domainconnect.nextjs.org` read 2026-10-29: a CNAME to `_domainconnect.vercel-dns.com`, whose
633
636
  // TXT is `domainconnect.vercel.com`. The settings name another address for the API.
@@ -647,6 +650,9 @@ var CONNECT_PROVIDERS = [
647
650
  // Settings read 2026-10-29 from domainconnect.api.godaddy.com (`urlSyncUX`
648
651
  // https://dcc.godaddy.com/manage). The TXT value on a customer's zone is not read yet: if it
649
652
  // differs, GoDaddy answers "not offered", which is the safe way to be wrong.
653
+ // GoDaddy told us on 2026-10-29 that it is "not currently accepting new templates for direct
654
+ // onboarding", with no timeline. It supports many templates already (9 of 12 third-party ones
655
+ // we probed), so the entry stays for them, but a template of yours answers "not offered" there.
650
656
  {
651
657
  id: "godaddy",
652
658
  txt: "domainconnect.api.godaddy.com",
@@ -747,6 +753,7 @@ async function ask(io, args, deadline) {
747
753
  const values = published.txts.map(normalizeTxt);
748
754
  const provider = providers.find((entry) => values.includes(entry.txt));
749
755
  if (!provider) return NOT_OFFERED;
756
+ if (args.requireMultiInstance && provider.supportsMultiInstance !== true) return NOT_OFFERED;
750
757
  const settings = await get(`https://${provider.txt}/v2/${zone}/settings`);
751
758
  if (!settings) return UNANSWERED;
752
759
  if (settings.status !== 200) return transient(settings.status) ? UNANSWERED : NOT_OFFERED;
@@ -767,6 +774,15 @@ async function ask(io, args, deadline) {
767
774
  );
768
775
  if (!template) return UNANSWERED;
769
776
  if (template.status !== 200) return transient(template.status) ? UNANSWERED : NOT_OFFERED;
777
+ if (args.requireMultiInstance) {
778
+ let templateBody;
779
+ try {
780
+ templateBody = JSON.parse(template.text);
781
+ } catch {
782
+ return NOT_OFFERED;
783
+ }
784
+ if (!isObject(templateBody) || templateBody.multiInstance !== true) return NOT_OFFERED;
785
+ }
770
786
  return {
771
787
  found: true,
772
788
  id: provider.id,
@@ -858,23 +874,26 @@ var API = "https://api.cloudflare.com/client/v4";
858
874
  var TIMEOUT_MS = 1e4;
859
875
  var MAX_BODY_BYTES = 256 * 1024;
860
876
  var TOKEN_CODES = /* @__PURE__ */ new Set([1e3, 6003, 6111, 1e4]);
877
+ var INVALID_TOKEN_MESSAGE = /invalid access token/i;
861
878
  var CONFLICT_CODES = /* @__PURE__ */ new Set([81053, 81054, 81057]);
862
879
  var ID = /^[A-Za-z0-9]{1,64}$/;
863
880
  var RECORD_TYPE = /^[A-Z0-9]{1,12}$/;
864
881
  function readEnvelope(json) {
865
882
  if (!isObject(json) || typeof json.success !== "boolean") return null;
866
883
  const codes = [];
884
+ const messages = [];
867
885
  if (Array.isArray(json.errors)) {
868
886
  for (const error of json.errors) {
869
887
  if (!isObject(error)) return null;
870
888
  if (typeof error.code === "number") codes.push(error.code);
889
+ if (typeof error.message === "string") messages.push(error.message);
871
890
  if (Array.isArray(error.error_chain)) {
872
891
  for (const link of error.error_chain)
873
892
  if (isObject(link) && typeof link.code === "number") codes.push(link.code);
874
893
  }
875
894
  }
876
895
  }
877
- return { success: json.success, codes, result: json.result };
896
+ return { success: json.success, codes, messages, result: json.result };
878
897
  }
879
898
  async function call(doFetch, token, path, send) {
880
899
  const res = await fetchBounded(
@@ -901,7 +920,8 @@ async function call(doFetch, token, path, send) {
901
920
  }
902
921
  function refusal(answer, stage, subject) {
903
922
  const codes = answer.body?.codes ?? [];
904
- const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c));
923
+ const messages = answer.body?.messages ?? [];
924
+ const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c)) || messages.some((m) => INVALID_TOKEN_MESSAGE.test(m));
905
925
  if (stage === "zones" && refusedToken) return new DnsWriteError("token_invalid");
906
926
  if (codes.some((c) => CONFLICT_CODES.has(c)))
907
927
  return new DnsWriteError("conflict", { ...subject });
@@ -52,6 +52,8 @@ interface ConnectProvider {
52
52
  api: string;
53
53
  /** The one host the settings' `urlSyncUX` may name: the page we send the person's browser to. */
54
54
  syncHost: string;
55
+ /** Verified support for separate template instances. Omitted means unverified. */
56
+ supportsMultiInstance?: boolean;
55
57
  }
56
58
  declare const CONNECT_PROVIDERS: readonly ConnectProvider[];
57
59
 
@@ -81,6 +83,10 @@ declare function discoverConnect(io: DiscoveryIo, args: {
81
83
  providerId: string;
82
84
  serviceId: string;
83
85
  budgetMs: number;
86
+ /** Require verified provider support and template JSON with `multiInstance: true`.
87
+ * Callers that later remove one application through the async API must pass a
88
+ * stable instanceId for each independent service application. */
89
+ requireMultiInstance?: boolean;
84
90
  providers?: readonly ConnectProvider[];
85
91
  }): Promise<Discovery>;
86
92
 
@@ -52,6 +52,8 @@ interface ConnectProvider {
52
52
  api: string;
53
53
  /** The one host the settings' `urlSyncUX` may name: the page we send the person's browser to. */
54
54
  syncHost: string;
55
+ /** Verified support for separate template instances. Omitted means unverified. */
56
+ supportsMultiInstance?: boolean;
55
57
  }
56
58
  declare const CONNECT_PROVIDERS: readonly ConnectProvider[];
57
59
 
@@ -81,6 +83,10 @@ declare function discoverConnect(io: DiscoveryIo, args: {
81
83
  providerId: string;
82
84
  serviceId: string;
83
85
  budgetMs: number;
86
+ /** Require verified provider support and template JSON with `multiInstance: true`.
87
+ * Callers that later remove one application through the async API must pass a
88
+ * stable instanceId for each independent service application. */
89
+ requireMultiInstance?: boolean;
84
90
  providers?: readonly ConnectProvider[];
85
91
  }): Promise<Discovery>;
86
92
 
@@ -5,7 +5,7 @@ import {
5
5
  fetchBounded,
6
6
  isObject,
7
7
  writers
8
- } from "../chunk-4XNRV7QR.js";
8
+ } from "../chunk-LLWYWMJ5.js";
9
9
  import {
10
10
  normalizeDnsName
11
11
  } from "../chunk-P2O7INDB.js";
@@ -92,7 +92,10 @@ var CONNECT_PROVIDERS = [
92
92
  id: "cloudflare",
93
93
  txt: "api.cloudflare.com/client/v4/dns/domainconnect",
94
94
  api: "https://api.cloudflare.com/client/v4/dns/domainconnect",
95
- syncHost: "dash.cloudflare.com"
95
+ syncHost: "dash.cloudflare.com",
96
+ // Cloudflare documents this flag as unsupported and ignored:
97
+ // https://developers.cloudflare.com/dns/reference/domain-connect/#template-definition
98
+ supportsMultiInstance: false
96
99
  },
97
100
  // `_domainconnect.nextjs.org` read 2026-10-29: a CNAME to `_domainconnect.vercel-dns.com`, whose
98
101
  // TXT is `domainconnect.vercel.com`. The settings name another address for the API.
@@ -112,6 +115,9 @@ var CONNECT_PROVIDERS = [
112
115
  // Settings read 2026-10-29 from domainconnect.api.godaddy.com (`urlSyncUX`
113
116
  // https://dcc.godaddy.com/manage). The TXT value on a customer's zone is not read yet: if it
114
117
  // differs, GoDaddy answers "not offered", which is the safe way to be wrong.
118
+ // GoDaddy told us on 2026-10-29 that it is "not currently accepting new templates for direct
119
+ // onboarding", with no timeline. It supports many templates already (9 of 12 third-party ones
120
+ // we probed), so the entry stays for them, but a template of yours answers "not offered" there.
115
121
  {
116
122
  id: "godaddy",
117
123
  txt: "domainconnect.api.godaddy.com",
@@ -183,6 +189,7 @@ async function ask(io, args, deadline) {
183
189
  const values = published.txts.map(normalizeTxt);
184
190
  const provider = providers.find((entry) => values.includes(entry.txt));
185
191
  if (!provider) return NOT_OFFERED;
192
+ if (args.requireMultiInstance && provider.supportsMultiInstance !== true) return NOT_OFFERED;
186
193
  const settings = await get(`https://${provider.txt}/v2/${zone}/settings`);
187
194
  if (!settings) return UNANSWERED;
188
195
  if (settings.status !== 200) return transient(settings.status) ? UNANSWERED : NOT_OFFERED;
@@ -203,6 +210,15 @@ async function ask(io, args, deadline) {
203
210
  );
204
211
  if (!template) return UNANSWERED;
205
212
  if (template.status !== 200) return transient(template.status) ? UNANSWERED : NOT_OFFERED;
213
+ if (args.requireMultiInstance) {
214
+ let templateBody;
215
+ try {
216
+ templateBody = JSON.parse(template.text);
217
+ } catch {
218
+ return NOT_OFFERED;
219
+ }
220
+ if (!isObject(templateBody) || templateBody.multiInstance !== true) return NOT_OFFERED;
221
+ }
206
222
  return {
207
223
  found: true,
208
224
  id: provider.id,
@@ -113,23 +113,26 @@ var API = "https://api.cloudflare.com/client/v4";
113
113
  var TIMEOUT_MS = 1e4;
114
114
  var MAX_BODY_BYTES = 256 * 1024;
115
115
  var TOKEN_CODES = /* @__PURE__ */ new Set([1e3, 6003, 6111, 1e4]);
116
+ var INVALID_TOKEN_MESSAGE = /invalid access token/i;
116
117
  var CONFLICT_CODES = /* @__PURE__ */ new Set([81053, 81054, 81057]);
117
118
  var ID = /^[A-Za-z0-9]{1,64}$/;
118
119
  var RECORD_TYPE = /^[A-Z0-9]{1,12}$/;
119
120
  function readEnvelope(json) {
120
121
  if (!isObject(json) || typeof json.success !== "boolean") return null;
121
122
  const codes = [];
123
+ const messages = [];
122
124
  if (Array.isArray(json.errors)) {
123
125
  for (const error of json.errors) {
124
126
  if (!isObject(error)) return null;
125
127
  if (typeof error.code === "number") codes.push(error.code);
128
+ if (typeof error.message === "string") messages.push(error.message);
126
129
  if (Array.isArray(error.error_chain)) {
127
130
  for (const link of error.error_chain)
128
131
  if (isObject(link) && typeof link.code === "number") codes.push(link.code);
129
132
  }
130
133
  }
131
134
  }
132
- return { success: json.success, codes, result: json.result };
135
+ return { success: json.success, codes, messages, result: json.result };
133
136
  }
134
137
  async function call(doFetch, token, path, send) {
135
138
  const res = await fetchBounded(
@@ -156,7 +159,8 @@ async function call(doFetch, token, path, send) {
156
159
  }
157
160
  function refusal(answer, stage, subject) {
158
161
  const codes = answer.body?.codes ?? [];
159
- const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c));
162
+ const messages = answer.body?.messages ?? [];
163
+ const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c)) || messages.some((m) => INVALID_TOKEN_MESSAGE.test(m));
160
164
  if (stage === "zones" && refusedToken) return new DnsWriteError("token_invalid");
161
165
  if (codes.some((c) => CONFLICT_CODES.has(c)))
162
166
  return new DnsWriteError("conflict", { ...subject });
@@ -5,7 +5,7 @@ import {
5
5
  namecom,
6
6
  porkbun,
7
7
  writers
8
- } from "../chunk-4XNRV7QR.js";
8
+ } from "../chunk-LLWYWMJ5.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.3",
3
+ "version": "0.1.5",
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",
@@ -47,9 +47,10 @@
47
47
  "scripts": {
48
48
  "build": "tsup src/index.ts src/server/index.ts src/writers/index.ts --format cjs,esm --dts --clean",
49
49
  "typecheck": "tsc --noEmit",
50
- "lint": "eslint src",
50
+ "lint": "eslint src scripts",
51
51
  "format": "prettier --write .",
52
52
  "test": "bun test",
53
+ "live": "bun scripts/live-check.ts",
53
54
  "prepublishOnly": "bun run lint && bun run typecheck && bun run build"
54
55
  },
55
56
  "keywords": [