@dodomain/node

中文文档
0.8.0发布于 2026/10/16MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 9146

Custom domains for SaaS — the official DoDomain server SDK: mint connect sessions (Cloudflare one-click, Domain Connect, guided DNS), read connections, and verify signed webhooks.

原始使用文档
文档目录27 节
  1. Install
  2. Usage
  3. Managing connections
  4. Reading a session back
  5. Pre-flighting a domain
  6. Managing webhook endpoints
  7. Rotating your secret key
  8. Verifying webhooks
  9. API
  10. new DoDomain(options)
  11. dodomain.sessions.create(input) → Promise<ConnectSessionSum…
  12. dodomain.connections.list(filters?) → Promise<ListConnectio…
  13. dodomain.connections.get(connectionId) → Promise<Connection>
  14. dodomain.connections.delete(connectionId) → Promise<Disconn…
  15. dodomain.connections.reverify(connectionId) → Promise<Rever…
  16. dodomain.sessions.get(sessionId) → Promise<IntegratorSessio…
  17. dodomain.apps.list() → Promise<ListAppsResult>
  18. dodomain.domains.check(input) → Promise<CheckDomainResult>
  19. dodomain.webhookEndpoints.list() → Promise<ListWebhookEndpo…
  20. dodomain.webhookEndpoints.create(input) → Promise<WebhookEn…
  21. dodomain.webhookEndpoints.update(endpointId, input) → Promi…
  22. dodomain.webhookEndpoints.resume(endpointId) → Promise<Webh…
  23. dodomain.webhookEndpoints.delete(endpointId) → Promise<Dele…
  24. dodomain.webhookEndpoints.rotateSecret(endpointId) → Promis…
  25. dodomain.keys.rotate() → Promise<RotateSecretKeyResult>
  26. verifyWebhook(secret, body, header, toleranceMs?, nowMs?) →…
  27. Types

@dodomain/node

Custom domains for SaaS, from your server: the official Node/TypeScript SDK for DoDomain. A thin, typed client over the DoDomain REST API — mint connect sessions that walk your customer through a one-click Cloudflare OAuth apply, a signed Domain Connect apply where their DNS provider supports it, or guided records everywhere else; then verify the signed webhooks DoDomain sends once the records are live at the authoritative nameservers. The browser @dodomain/connect widget drives the session on the front end.

Uses your secret key (dd_sk_…). Keep it on the server. Never ship it to the browser.

📖 Guide with runnable examples: dodomain.io/docs/node-sdk · all docs: dodomain.io/docs.

Install

npm install @dodomain/node
# or: pnpm add @dodomain/node · yarn add @dodomain/node

Zero runtime dependencies — the build inlines everything (dual ESM+CJS + self-contained .d.ts). Node 20+.

Usage

import { DoDomain } from "@dodomain/node";

const dodomain = new DoDomain({ secretKey: process.env.DODOMAIN_SECRET_KEY! });

const session = await dodomain.sessions.create({
  domain: "customer.com",
  records: [
    { type: "CNAME", host: "app", value: "cname.sendly.io" },
    { type: "TXT", host: "_verify", value: "sendly-domain-abc123" },
  ],
  returnUrl: "https://dashboard.sendly.io/domains",
});

// session.token   → hand to @dodomain/connect in the browser
// session.connectUrl → or redirect the user to the hosted flow directly

Managing connections

// One page of live connections, newest first.
const { connections, nextCursor } = await dodomain.connections.list({ limit: 50 });

// Walk every page (nextCursor is null on the last one).
let cursor: string | undefined;
do {
  const page = await dodomain.connections.list({ cursor, limit: 100 });
  cursor = page.nextCursor ?? undefined;
} while (cursor);

// Read ONE — the id every connection.* webhook payload carries.
const connection = await dodomain.connections.get(connectionId);
connection.status; // "active" | "broken"

// Queue an on-demand DNS recheck; the verdict arrives as a webhook.
await dodomain.connections.reverify(connectionId);

// The customer removed their domain from your product — stop the monitoring
// (idempotent; a repeat reports alreadyDisconnected).
const { disconnectedAt, alreadyDisconnected } = await dodomain.connections.delete(connectionId);

Reading a session back

// Sessions are addressable by the id every webhook payload carries — the token
// never appears in one. This is how a session.abandoned receiver asks what
// actually happened.
const session = await dodomain.sessions.get(event.data.sessionId as string);

session.expired; // derived at read — do NOT infer expiry from session.status
session.connectionId; // non-null once the session finalized
session.records; // the composed fqdns DoDomain verifies, not the raw echo

Pre-flighting a domain

// Which provider, which zone, which connect flow — without creating a session.
const check = await dodomain.domains.check({ domain: "app.customer.com" });
check.tier; // 1 = Cloudflare one-click · 2 = Domain Connect · 3 = manual
check.oneClickAvailable; // false on tier 2 when the provider serves none of our templates (e.g. GoDaddy)
check.guide.steps; // what to tell the user if it comes to manual records

Managing webhook endpoints

// Register a delivery target. The secret is returned ONCE — store it here.
const endpoint = await dodomain.webhookEndpoints.create({
  url: "https://hooks.sendly.io/dodomain",
});
await saveWebhookSecret(endpoint.secret); // whsec_… — feed to verifyWebhook

const { endpoints } = await dodomain.webhookEndpoints.list(); // never carries secrets
await dodomain.webhookEndpoints.update(endpoint.id, { url: "https://hooks2.sendly.io/dodomain" });

// Rotation cuts over IMMEDIATELY — deploy the new secret before you rotate.
const rekeyed = await dodomain.webhookEndpoints.rotateSecret(endpoint.id);
await dodomain.webhookEndpoints.delete(endpoint.id);

Rotating your secret key

// The calling key replaces itself. By DEFAULT there is NO overlap window: the
// old key stops working the instant this returns, and this is the only copy of
// the new one — persist it before doing anything else.
const rotated = await dodomain.keys.rotate();
await saveSecretKey(rotated.secretKey); // dd_sk_…
const next = new DoDomain({ secretKey: rotated.secretKey }); // this client still holds the old key

// Opt-in zero-downtime rotation: both keys work until previousKeyExpiresAt
// (1 or 24 hours). Exactly ONE previous key is kept — rotating again replaces
// it (killing key n-1 instantly), and rotating with the default overlapHours: 0
// ends a live window early: for a leaked key, rotation IS the revoke.
const overlapped = await dodomain.keys.rotate({ overlapHours: 1 });
await saveSecretKey(overlapped.secretKey);
console.log(`old key dies at ${overlapped.previousKeyExpiresAt}`);

Verifying webhooks

import { verifyWebhook } from "@dodomain/node";
import type { WebhookEvent } from "@dodomain/node";

// req.headers["x-dodomain-signature"] is `t=<unixMs>,v1=<hex hmac>`
const ok = verifyWebhook(process.env.DODOMAIN_WEBHOOK_SECRET!, rawBody, signatureHeader);
if (!ok) return new Response("bad signature", { status: 400 });

const event = JSON.parse(rawBody) as WebhookEvent; // { id, type, occurredAt, data }
// Dedupe on event.id (retries reuse it); branch on event.type.

This is the only package an integrator needs: it covers the whole /api/v1 integrator surface — sessions, connections, apps, the domain pre-flight, webhook-endpoint lifecycle and key rotation — plus webhook signature verification.

The remaining /api/v1 routes (detect, verify, and the Cloudflare / Domain Connect one-click entry points) are browser surfaces: they are authenticated by the session token in the URL rather than by your secret key, and the hosted flow and @dodomain/connect widget drive them. They are deliberately absent from this server SDK.

API

new DoDomain(options)

OptionTypeNotes
secretKeystringRequired. Must start with dd_sk_.
baseUrlstringAPI base. Defaults to https://app.dodomain.io (self-hosted/custom deployments override this).
fetchImpltypeof fetchOverride the fetch used (tests, custom agents).

dodomain.sessions.create(input) → Promise<ConnectSessionSummary>

input: { domain, records, returnUrl?, recipe? } — domain (two or more DNS labels, no scheme/port/trailing dot) and records are both required; returnUrl must be http(s). recipe is accepted for wire compatibility but is consumed by nothing today and does not substitute for records. Returns { id, token, expiresAt, connectUrl, records, warnings? }. The return type was named Session before 0.5.0; that alias is still exported, marked @deprecated, and will be removed in the next major — import ConnectSessionSummary.

Errors throw DoDomainError with .status and .body. A malformed (non-JSON) or schema-invalid response also throws DoDomainError — never a raw platform error.

dodomain.connections.list(filters?) → Promise<ListConnectionsResult>

filters: { appId?, domain?, limit?, cursor?, includeDisconnected? } — all optional, and only the ones you pass go on the wire (the server owns the defaults: limit 50, max 100; disconnected connections hidden). Returns { connections, nextCursor }, newest first; pass nextCursor back as cursor for the next page, and stop when it is null.

dodomain.connections.get(connectionId) → Promise<Connection>

Reads one connection — including one you already disconnected. Same object shape as an element of list. Unknown and not-yours both throw DoDomainError with .status === 404.

dodomain.connections.delete(connectionId) → Promise<DisconnectConnectionResult>

Disconnects: archives the connection and stops its DNS monitoring, so a domain your customer removed stops firing drift webhooks. Idempotent — a repeat returns the original disconnectedAt with alreadyDisconnected: true and emits no second webhook.

dodomain.connections.reverify(connectionId) → Promise<ReverifyConnectionResult>

Queues an on-demand DNS recheck. Resolves once the job is accepted (HTTP 202); the verdict arrives as a connection.verified / connection.failed webhook. A connection checked within the last 10 minutes throws DoDomainError with .status === 429.

dodomain.sessions.get(sessionId) → Promise<IntegratorSession>

Reads a session by id — the sessionId every webhook payload carries (the token appears in none of them). Unlike the public token route it returns an expired session rather than refusing it: read the expired flag, which is derived at read time and is therefore already true before the reaper rewrites status. Passing a dd_sess_… token throws DoDomainError with .status === 0 — that token addresses the same path's public arm, which answers with a different shape. Unknown and not-yours both throw .status === 404.

dodomain.apps.list() → Promise<ListAppsResult>

The apps this credential can see. A secret key sees exactly its own app; an OAuth token with apps:read sees the whole team. Only the publishable pk_* key is ever returned.

dodomain.domains.check(input) → Promise<CheckDomainResult>

input: { domain }. Stateless pre-flight — provider, owning zone, connect tier and flow, whether one-click is actually available there (oneClickAvailable: tier: 2 alone only means the provider speaks Domain Connect), and the manual guide — without creating a session. Nothing is persisted; available on every plan.

dodomain.webhookEndpoints.list() → Promise<ListWebhookEndpointsResult>

This app's delivery targets. Never returns secret — no read surface does.

dodomain.webhookEndpoints.create(input) → Promise<WebhookEndpointWithSecret>

input: { url } (https, not a localhost/private address, not a duplicate — each refusal throws .status === 400 with the reason in .body.message; the plan cap throws .status === 402). The returned secret is shown once and appears in no read surface again — persist it in this same code path.

dodomain.webhookEndpoints.update(endpointId, input) → Promise<WebhookEndpoint>

Repoints an endpoint at a new url. The signing secret is deliberately untouched, so moving hosts never forces your receiver to re-key. A url that actually changes also resumes an auto-paused endpoint.

dodomain.webhookEndpoints.resume(endpointId) → Promise<WebhookEndpoint>

Resumes an endpoint DoDomain paused automatically — one that had no successful delivery for 7 days and at least 5 dead-lettered deliveries in that span (pausedAt is set while it is paused). Clears pausedAt and restarts the 7-day clock; idempotent on an endpoint that is not paused. Events that happened while it was paused were recorded as skipped deliveries and are not resent by this call — redrive them from the dashboard.

dodomain.webhookEndpoints.delete(endpointId) → Promise<DeleteWebhookEndpointResult>

Stops delivering to the endpoint. Past delivery rows survive as evidence, but a failed delivery to a deleted endpoint can no longer be redriven.

dodomain.webhookEndpoints.rotateSecret(endpointId) → Promise<WebhookEndpointWithSecret>

Mints a new signing secret and returns it once. Immediate cutover, no dual-secret window — the worker reads the secret live at delivery time, so signatures switch at once, retries of older deliveries included. Deploy the new secret to your receiver promptly.

dodomain.keys.rotate() → Promise<RotateSecretKeyResult>

The calling app's secret key rotates itself — the automatable half of credential rotation. No grace window: the old key stops authenticating the instant this returns, and the returned secretKey is the only copy of the new one. Persist it, then build a new DoDomain with it — the client you called this on still holds the old key. publicKey is echoed unchanged so a CI job can assert it rewrote the right app. There is deliberately no create/list/revoke of other keys.

verifyWebhook(secret, body, header, toleranceMs?, nowMs?) → boolean

Verifies the t=<unixMs>,v1=<hex hmac> signature DoDomain sends with every webhook delivery.

Types

DnsRecordType = "A" | "AAAA" | "CNAME" | "TXT" | "MX". DnsRecord = { type, host, value, priority?, ttl? }. Connection = { id, appId, sessionId, domain, fqdn, recordFqdns, status, verifiedAt, lastCheckedAt, brokenAt, disconnectedAt, createdAt }. IntegratorSession = { id, appId, domain, records, recipe, status, tier, detectedProvider, connectionId, createdAt, expiresAt, expired }. WebhookEndpoint = { id, appId, url, createdAt, pausedAt }; WebhookEndpointWithSecret adds the show-once secret. RotateSecretKeyResult = { appId, publicKey, secretKey, rotatedAt }. WebhookEvent = { id, type, occurredAt, data } — the envelope DoDomain delivers. WebhookEventWire = WebhookEvent plus the deprecated event alias of type, for receivers written against the pre-2026-08-06 body.

See src/index.ts for the full surface.

版本与发布记录

@dodomain/node 共发布 8 个版本,首次发布于 2026/07/31;过去 12 个月发布了 8 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.0latest2026/10/1628 天前
0.7.02026/10/021 个月前
0.6.02026/09/092 个月前
0.4.02026/08/202 个月前
0.3.22026/08/202 个月前
0.3.12026/08/202 个月前

关于 @dodomain/node 的常见问题

@dodomain/node 是什么?

Custom domains for SaaS — the official DoDomain server SDK: mint connect sessions (Cloudflare one-click, Domain Connect, guided DNS), read connections, and verify signed webhooks.

如何安装 @dodomain/node?

在项目目录运行 npm install @dodomain/node,或使用 pnpm add / yarn add / bun add。

@dodomain/node 支持 TypeScript 吗?

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

@dodomain/node 支持 ESM 还是 CommonJS?

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

@dodomain/node 需要什么版本的 Node.js?

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

@dodomain/node 可以商用吗?

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

@dodomain/node 的下载量有多少?

根据 npm 官方下载统计,@dodomain/node 在 2026/10/13 – 2026/11/11 这 30 天内被下载 9,146 次,日均约 305 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@dodomain/node 还在维护吗?

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