@gusnips / one-click-dns
将DNS记录放在客户的区域中。该包找到它们的DNS所在的位置,然后为您提供添加记录的最佳方式:签名的Domain Connect链接、粘贴的API令牌或该主机的步骤。
npm install @gusnips/one-click-dns
# or
bun add @gusnips/one-click-dns
它有三个切入点:
@gusnips/one-click-dns:在浏览器中运行。主机列表,记录拼写和模板预设。@gusnips/one-click-dns/server读取DNS,建立签名链接。Node或Bun。@gusnips/one-click-dns/writers:写入带有API令牌的记录。服务器只。
将密钥和令牌保存在服务器上。浏览器条目中不需要它们。
第一个电话
import { planConnection, systemDnsLookups } from "@gusnips/one-click-dns/server";
const plan = await planConnection({ hostname: "help.acme.com" }, systemDnsLookups(2000));
console.log(plan.reading?.hosts); // ["cloudflare"]
console.log(plan.methods.map((method) => method.kind)); // ["api-token", "manual"]
methods 列出了联系的方式,最好是第一个:
kind | 这意味着什么? |
|---|---|
domain-connect | 一个点击。你把这个人送到 url,他们的DNS主机要求他们批准,记录就会出现。 |
api-token | 此人粘贴一个令牌,然后您的服务器写入该记录。 |
manual | 这个人用手抄写记录。主人的名字在里面 hosts. 总是最后一个。 |
plan.reading 是 null DNS没有应答。这与“主机未知”不同,所以不要将其显示为一个。
添加带有令牌的记录
import { cloudflare } from "@gusnips/one-click-dns/writers";
const outcome = await cloudflare.addRecord({
token, // the person's Cloudflare API token
record: { type: "CNAME", name: "help.acme.com", value: "abc123.domains.example.app" },
});
// "created", "unchanged" or "unproxied"
令牌仅用于此调用。它永远不会被存储、记录或放入错误中。这个人已经有的记录永远不会被覆盖:你得到一个 conflict 代替。
当写入失败时,它抛出 DnsWriteError 与一个 code. 用你自己的语言编写代码:
code | 告诉那个人什么 |
|---|---|
token_invalid | 主机不接受令牌。再次粘贴。 |
zone_read_missing | 令牌不能列出域。给它许可吧。 |
dns_edit_missing | 令牌不能改变DNS记录。给它许可吧。 |
zone_not_found | 令牌的帐户不包含此域。 |
conflict | 这个名字已经有另一个记录了(params.type). 他们决定如何处理它。 |
unavailable | 主人没有回答。再试一次。 |
四位主持人今天有一位作家。每个人粘贴的内容是不同的。测试用的是假网络。Cloudflare和DigitalOcean也是在一个真实账户上运行的,使用的是虚构的下划线名称,之后会被删除。而Porkbun和Name.com则不然。
| 作家 | token 是 | 它需要什么 | 检查了多少 |
|---|---|---|---|
cloudflare | API令牌 | Zone:Read 和 DNS:Edit | 在2026-10-29对真实区域运行:创建、重复、TXT、冲突和错误的令牌 |
porkbun | apikey:secretapikey | “API访问”在Porkbun中为域打开(默认是关闭的) | 根据主机的API规范编写。从不针对真实帐户运行 |
digitalocean | 个人访问令牌 | domain:read 和 domain:create,或完全访问 | 2026-10-29对一个真实的域运行:创建、重复、TXT、冲突和一个错误的令牌 |
name-com | username:apitoken | 在帐户设置、安全中打开API访问 | 根据主持人的文档写的。永远不要与真实账户对账 |
writers 将主机id映射到它的写入器,所以 writers[host] 是 undefined 对于一个没有编剧的主持人来说。
根据你自己的账户核对一个作家
存储库有一个脚本,可以在真实区域上尝试写入器并删除它所创建的内容。使用一个备用区域和一个限制于此的令牌:
HOST=cloudflare ZONE=example.com TOKEN=your-token bun run live
它将CNAME和TXT记录写入虚构的下划线名称下,分别运行第二次(两者都必须读为“未改变”),检查冲突记录是否被拒绝,并报告错误的令牌,然后删除其记录。支持Cloudflare和DigitalOcean。
一键链接(域连接)
一个链接需要一个你的DNS主机已经开启的模板和一个密钥对。您在DNS中发布公钥,并将私钥保存在服务器上。
const plan = await planConnection(
{
hostname: "help.acme.com",
connect: {
providerId: "example.com", // your id in the Templates repository
keyId: "_dck1", // the TXT record name that holds your public key
serviceId: "custom-domain", // your template
privateKeyPem,
params: (zone) => ({
domain: zone,
host: "help",
target: "abc123",
redirect_uri: "https://app.example.com/done",
}),
},
},
systemDnsLookups(2000),
);
当主机有你的模板时, plan.methods 从a开始 domain-connect 方法。把人送回 redirect_uri 证明不了什么。在标记域完成之前,自己检查DNS。
只有这些提供者被要求,因为他们 _domainconnect 值和设置从DNS: Cloudflare, Vercel, Simply.com和GoDaddy读取。永远不会获取客户DNS中的名称。更多的提供者以同样的方式加入,一次一个值。
这里列出的提供商是包可以与之通信的提供商。这并不是说提供程序会打开你的模板。例如,GoDaddy在2026-10-29告诉我们,它不接受直接登录的新模板,也没有给出日期。询问每个提供者,并使用 templateCoverage 看看是谁答应了。没有联系的地方, plan.methods 仍然以主机的步骤结束。
哪些提供程序打开了您的模板
合并模板不是提供程序使用的模板。每个提供程序都手动打开模板,唯一的方法就是询问它。 templateCoverage 请求列表中的每个提供者一次:
import { templateCoverage } from "@gusnips/one-click-dns/server";
const coverage = await templateCoverage(
{},
{ providerId: "example.com", serviceId: "custom-domain" },
);
// { cloudflare: "not_supported", godaddy: "supported", vercel: "not_supported", simply: "not_supported" }
"unanswered" 表示提供者很忙或没有回复。它从不意味着不。时不时地运行它,看看什么时候提供程序开始支持你的模板。
模板
模板是一个公开的小JSON文件 域连接模板 存储库。它列出了您可能要求主机添加的记录。预设构建一个遵循存储库规则的记录:
import { cnameTemplate, templateFileName } from "@gusnips/one-click-dns";
const template = cnameTemplate(
{
providerId: "example.com",
providerName: "Example",
logoUrl: "https://example.com/logo.svg",
syncPubKeyDomain: "example.com",
syncRedirectDomain: "app.example.com",
},
{ pointsTo: "%target%.domains.example.com" },
);
console.log(templateFileName(template)); // "example.com.custom-domain.json"
有CNAME, DMARC, SPF, DKIM, MX和验证TXT的预设。
其中两个改变了域名电子邮件的工作方式。MX预设将替换域拥有的MX记录,因此它的邮件将转到其他地方。SPF预置将发送者添加到域的SPF记录中。在你提供任何一个链接之前,在你自己的屏幕上征求对方的同意,并说明模板的变化 description, DNS主机在批准时显示。
模板属于归档它的公司。它命名您的密钥域,主机根据在那里发布的密钥检查每个链接。所以你不能使用别人的模板,也没有人可以使用你的模板。客户的域不在模板中:它随每个链接一起提供,因此一个模板为每个客户服务。
文件中测试模板 在线编辑器、运行 dc-template-linter -merge-or-fail 打开一个pull request,然后要求每个DNS主机打开它。
保持单独的SPF服务
同一模板的两个链接可以替换彼此的记录,即使在发布任何一个链接之前检查DNS也是如此。对于SPF,这可以删除其他服务的发送权限。
当一个模板提供多个发送服务时,选择加入单独的实例:
const template = spfTemplate(identity, { multiInstance: true, version: 2 });
将更新后的模板归档,并等待DNS提供程序将其打开。在签署链接之前,要求该提供商的副本发布该标志:
import { discoverConnect, systemDnsLookups } from "@gusnips/one-click-dns/server";
const discovery = await discoverConnect(
{ txt: systemDnsLookups(2000).txt },
{
zone: "acme.com",
providerId: "example.com",
serviceId: "spf",
budgetMs: 2000,
requireMultiInstance: true,
},
);
此检查需要提供程序条目和 supportsMultiInstance: true 和字面模板标志 multiInstance: true. 缺少或错误的支持返回 not_offered. 模板检查读取相同的受大小限制的响应,并且不发出额外的请求。其他发现调用保持当前的行为。
尚未有内置提供程序验证实例支持。 Cloudflare说它忽略了这个标志;其他提供程序未经验证。因此,要求实例返回 not_offered 对于每个内置提供程序。保持手动撤退。对于一个习惯 providers 列表,设置 supportsMultiInstance: true 只有在验证该提供者的行为之后,而不仅仅是它的模板JSON。
只有在需要的时候才签字 discovery.found 是真的。签名同步链接带有模板标志;规范定义了 instanceId 对于异步应用和恢复,而不是对于同步参数。如果稍后通过异步API删除一个应用程序,则传递一个稳定的 instanceId 来自完整发送域和服务的SPF包括:同一对重复使用相同的id,不同的域或服务使用不同的id。包不会为您选择该id。该标志不会检查SPF的查找限制,也不会重定向到您的应用程序,以证明DNS已更改。
主机步骤
DNS_HOSTS 拥有33个主机:如何从名称服务器,DNS页面上的标签和陷阱(默认情况下启用代理,尾随点,需要引号的TXT)中识别它们。该包包含数据而不包含句子,因此您可以用自己的单词和语言编写步骤。 dnsRecordRow 按主机希望输入的方式拼写记录:
import { dnsRecordRow } from "@gusnips/one-click-dns";
dnsRecordRow("route53", "acme.com", { type: "TXT", name: "acme.com", value: "v=spf1 ~all" });
// { type: "TXT", name: "", value: "\"v=spf1 ~all\"", atRoot: true, rootUnsure: false }
名称服务器模式只有在主机记录它或在DNS中看到它时才会出现在列表中。只有当主机自己的页面显示标签时,标签才会被引用。
检查
bun run lint
bun run typecheck
bun run build
bun test src/path/to/file.test.ts