@gusnips/one-click-dns 0.1.4 → 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 +33 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/server/index.cjs +14 -1
- package/dist/server/index.d.cts +6 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.js +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,6 +159,39 @@ A template belongs to the company that files it. It names your key domain, and t
|
|
|
159
159
|
|
|
160
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.
|
|
161
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
|
+
|
|
162
195
|
## Steps for the host
|
|
163
196
|
|
|
164
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:
|
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
|
|
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
|
|
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
|
}
|
package/dist/server/index.cjs
CHANGED
|
@@ -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.
|
|
@@ -750,6 +753,7 @@ async function ask(io, args, deadline) {
|
|
|
750
753
|
const values = published.txts.map(normalizeTxt);
|
|
751
754
|
const provider = providers.find((entry) => values.includes(entry.txt));
|
|
752
755
|
if (!provider) return NOT_OFFERED;
|
|
756
|
+
if (args.requireMultiInstance && provider.supportsMultiInstance !== true) return NOT_OFFERED;
|
|
753
757
|
const settings = await get(`https://${provider.txt}/v2/${zone}/settings`);
|
|
754
758
|
if (!settings) return UNANSWERED;
|
|
755
759
|
if (settings.status !== 200) return transient(settings.status) ? UNANSWERED : NOT_OFFERED;
|
|
@@ -770,6 +774,15 @@ async function ask(io, args, deadline) {
|
|
|
770
774
|
);
|
|
771
775
|
if (!template) return UNANSWERED;
|
|
772
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
|
+
}
|
|
773
786
|
return {
|
|
774
787
|
found: true,
|
|
775
788
|
id: provider.id,
|
package/dist/server/index.d.cts
CHANGED
|
@@ -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
|
|
package/dist/server/index.d.ts
CHANGED
|
@@ -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
|
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
@@ -186,6 +189,7 @@ async function ask(io, args, deadline) {
|
|
|
186
189
|
const values = published.txts.map(normalizeTxt);
|
|
187
190
|
const provider = providers.find((entry) => values.includes(entry.txt));
|
|
188
191
|
if (!provider) return NOT_OFFERED;
|
|
192
|
+
if (args.requireMultiInstance && provider.supportsMultiInstance !== true) return NOT_OFFERED;
|
|
189
193
|
const settings = await get(`https://${provider.txt}/v2/${zone}/settings`);
|
|
190
194
|
if (!settings) return UNANSWERED;
|
|
191
195
|
if (settings.status !== 200) return transient(settings.status) ? UNANSWERED : NOT_OFFERED;
|
|
@@ -206,6 +210,15 @@ async function ask(io, args, deadline) {
|
|
|
206
210
|
);
|
|
207
211
|
if (!template) return UNANSWERED;
|
|
208
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
|
+
}
|
|
209
222
|
return {
|
|
210
223
|
found: true,
|
|
211
224
|
id: provider.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gusnips/one-click-dns",
|
|
3
|
-
"version": "0.1.
|
|
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",
|