@gusnips/one-click-dns

中文文档
0.1.5发布于 2026/11/01MITTypeScriptESM + CJS零依赖

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

原始使用文档
文档目录9 节
  1. First call
  2. Add a record with a token
  3. Check a writer against your own account
  4. One-click links (Domain Connect)
  5. Which providers have switched your template on
  6. Templates
  7. Keep separate SPF services
  8. Steps for the host
  9. Checks

@gusnips/one-click-dns

Put DNS records in a customer's zone. The package finds where their DNS lives, then gives you the best way to add the record: a signed Domain Connect link, a pasted API token, or the steps for that host.

npm install @gusnips/one-click-dns
# or
bun add @gusnips/one-click-dns

It has three entry points:

  • @gusnips/one-click-dns: runs in the browser. The host list, the record spelling and the template presets.
  • @gusnips/one-click-dns/server: reads DNS, builds signed links. Node or Bun.
  • @gusnips/one-click-dns/writers: writes a record with an API token. Server only.

Keep keys and tokens on the server. Nothing in the browser entry needs them.

First call

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 lists the ways to connect, best first:

kindWhat it means
domain-connectOne click. You send the person to url, their DNS host asks them to approve, and the records appear.
api-tokenThe person pastes a token, and your server writes the record.
manualThe person copies the records by hand. The host's name is in hosts. Always the last one.

plan.reading is null when DNS did not answer. That is not the same as "host unknown", so do not show it as one.

Add a record with a token

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"

The token is used for this call only. It is never stored, logged or put in an error. A record the person already has is never overwritten: you get a conflict instead.

When the write fails, it throws a DnsWriteError with a code. Word each code in your own languages:

codeWhat to tell the person
token_invalidThe host did not accept the token. Paste it again.
zone_read_missingThe token cannot list domains. Give it that permission.
dns_edit_missingThe token cannot change DNS records. Give it that permission.
zone_not_foundThe token's account does not hold this domain.
conflictThe name already has another record (params.type). They decide what to do with it.
unavailableThe host did not answer. Try again.

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:

Writertoken isWhat it needsHow far it is checked
cloudflareAn API tokenZone:Read and DNS:EditRun against a real zone on 2026-10-29: create, repeat, TXT, conflicts and a wrong token
porkbunapikey: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
digitaloceanA personal access tokendomain:read and domain:create, or full accessRun against a real domain on 2026-10-29: create, repeat, TXT, conflicts and a wrong token
name-comusername:apitokenAPI access turned on in Account Settings, SecurityWritten from the host's docs. Never run against a real account

writers maps a host id to its writer, so writers[host] is undefined for a host with no writer.

Check a writer against your own account

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:

HOST=cloudflare ZONE=example.com TOKEN=your-token bun run live

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.

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.

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),
);

When the host has your template, plan.methods starts with a domain-connect method. Sending the person back to redirect_uri proves nothing. Check the DNS yourself before you mark the domain done.

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.

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.

Which providers have switched your template on

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:

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" 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.

Templates

A template is a small JSON file in the public 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:

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"

There are presets for a CNAME, DMARC, SPF, DKIM, MX and a verification TXT.

Two of them change how a domain's email works. The MX preset replaces the MX records the domain has, so its mail goes somewhere else. The SPF preset adds a sender to the domain's SPF record. Ask the person for consent in your own screen before you offer either link, and say what changes in the template's description, which the DNS host shows when they approve.

A template belongs to the company that files it. It names your key domain, and the host checks every link against the key published there. So you cannot use someone else's template, and nobody can use yours. The customer's domain is not in the template: it comes with each link, so one template serves every customer.

To file a template: test it in the online editor, run dc-template-linter -merge-or-fail on it, open a pull request, then ask each DNS host to switch it on.

Keep separate SPF services

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.

When one template serves several sending services, opt in to separate instances:

const template = spfTemplate(identity, { multiInstance: true, version: 2 });

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:

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,
  },
);

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.

No built-in provider has verified instance support yet. Cloudflare says it ignores this flag; 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.

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.

Steps for the host

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:

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 }

A name server pattern is in the list only when the host documents it or it was seen in DNS. A label is quoted only when the host's own page shows it.

Checks

bun run lint
bun run typecheck
bun run build
bun test src/path/to/file.test.ts

版本与发布记录

@gusnips/one-click-dns 共发布 7 个版本,首次发布于 2026/10/29;过去 12 个月发布了 7 个版本。

版本发布日期距今
0.1.5latest2026/11/012 天前
0.1.42026/10/295 天前
0.1.32026/10/295 天前
0.1.22026/10/295 天前
0.1.12026/10/295 天前
0.1.02026/10/295 天前

关于 @gusnips/one-click-dns 的常见问题

@gusnips/one-click-dns 是什么?

将DNS记录放在客户的区域中:找到他们的DNS主机,将它们发送到签名的Domain Connect链接,使用API令牌编写,或显示该主机的步骤

如何安装 @gusnips/one-click-dns?

在项目目录运行 npm install @gusnips/one-click-dns,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@gusnips/server(>=0.14.3)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@gusnips/one-click-dns 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@gusnips/one-click-dns 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@gusnips/one-click-dns 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@gusnips/one-click-dns 可以商用吗?

@gusnips/one-click-dns 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@gusnips/one-click-dns 还在维护吗?

最新版本 0.1.5 发布于 2026/11/01(2 天前);至今共发布 7 个版本,其中过去 12 个月发布了 7 个;首次发布于 2026/10/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。