@gusnips/one-click-dns 0.1.3 → 0.1.4

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:
@@ -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 });
@@ -647,6 +647,9 @@ var CONNECT_PROVIDERS = [
647
647
  // Settings read 2026-10-29 from domainconnect.api.godaddy.com (`urlSyncUX`
648
648
  // https://dcc.godaddy.com/manage). The TXT value on a customer's zone is not read yet: if it
649
649
  // differs, GoDaddy answers "not offered", which is the safe way to be wrong.
650
+ // GoDaddy told us on 2026-10-29 that it is "not currently accepting new templates for direct
651
+ // onboarding", with no timeline. It supports many templates already (9 of 12 third-party ones
652
+ // we probed), so the entry stays for them, but a template of yours answers "not offered" there.
650
653
  {
651
654
  id: "godaddy",
652
655
  txt: "domainconnect.api.godaddy.com",
@@ -858,23 +861,26 @@ var API = "https://api.cloudflare.com/client/v4";
858
861
  var TIMEOUT_MS = 1e4;
859
862
  var MAX_BODY_BYTES = 256 * 1024;
860
863
  var TOKEN_CODES = /* @__PURE__ */ new Set([1e3, 6003, 6111, 1e4]);
864
+ var INVALID_TOKEN_MESSAGE = /invalid access token/i;
861
865
  var CONFLICT_CODES = /* @__PURE__ */ new Set([81053, 81054, 81057]);
862
866
  var ID = /^[A-Za-z0-9]{1,64}$/;
863
867
  var RECORD_TYPE = /^[A-Z0-9]{1,12}$/;
864
868
  function readEnvelope(json) {
865
869
  if (!isObject(json) || typeof json.success !== "boolean") return null;
866
870
  const codes = [];
871
+ const messages = [];
867
872
  if (Array.isArray(json.errors)) {
868
873
  for (const error of json.errors) {
869
874
  if (!isObject(error)) return null;
870
875
  if (typeof error.code === "number") codes.push(error.code);
876
+ if (typeof error.message === "string") messages.push(error.message);
871
877
  if (Array.isArray(error.error_chain)) {
872
878
  for (const link of error.error_chain)
873
879
  if (isObject(link) && typeof link.code === "number") codes.push(link.code);
874
880
  }
875
881
  }
876
882
  }
877
- return { success: json.success, codes, result: json.result };
883
+ return { success: json.success, codes, messages, result: json.result };
878
884
  }
879
885
  async function call(doFetch, token, path, send) {
880
886
  const res = await fetchBounded(
@@ -901,7 +907,8 @@ async function call(doFetch, token, path, send) {
901
907
  }
902
908
  function refusal(answer, stage, subject) {
903
909
  const codes = answer.body?.codes ?? [];
904
- const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c));
910
+ const messages = answer.body?.messages ?? [];
911
+ const refusedToken = answer.status === 401 || codes.some((c) => TOKEN_CODES.has(c)) || messages.some((m) => INVALID_TOKEN_MESSAGE.test(m));
905
912
  if (stage === "zones" && refusedToken) return new DnsWriteError("token_invalid");
906
913
  if (codes.some((c) => CONFLICT_CODES.has(c)))
907
914
  return new DnsWriteError("conflict", { ...subject });
@@ -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";
@@ -112,6 +112,9 @@ var CONNECT_PROVIDERS = [
112
112
  // Settings read 2026-10-29 from domainconnect.api.godaddy.com (`urlSyncUX`
113
113
  // https://dcc.godaddy.com/manage). The TXT value on a customer's zone is not read yet: if it
114
114
  // differs, GoDaddy answers "not offered", which is the safe way to be wrong.
115
+ // GoDaddy told us on 2026-10-29 that it is "not currently accepting new templates for direct
116
+ // onboarding", with no timeline. It supports many templates already (9 of 12 third-party ones
117
+ // we probed), so the entry stays for them, but a template of yours answers "not offered" there.
115
118
  {
116
119
  id: "godaddy",
117
120
  txt: "domainconnect.api.godaddy.com",
@@ -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.4",
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": [