@ dodomain/ 节点
自定义 SaaS 域名,来自您的服务器: 官方节点/ TypeScript SDK
域名。在 DoDomain REST API 上输入的瘦客户端——薄荷
连接会话 让你的客户通过 Cloudflare OAuth 申请
签名 域连接 在其 DNS 提供者支持的情况下应用
它,或者在其它任何地方的引导记录;然后验证签名的webhooks域名发送一次
记录在权威的命名服务器现场直播. 浏览器 @dodomain/connect
部件驱动前端的会话 .
用你们的 密钥 (单位:千美元)
dd_sk_…) (中文(简体) ). 放在服务器上 永远不要把它送到浏览器.
📖 附有可操作示例的指南 : dodomain.io/docs/节点-sdk · 所有文件: dodomain.io/docs (英语)。 。 。 .
安装
npm install @dodomain/node
# or: pnpm add @dodomain/node · yarn add @dodomain/node
零运行时间依赖性——构建内含一切(双重无害环境管理+CJS+自成一体)
.d.ts. 节点20+.
使用量
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
管理连接
// 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);
回读一个会话
// 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
飞行前域
// 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
管理网点
// 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);
旋转您的密钥
// 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}`);
正在校验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.
这是拼接器唯一需要的包: 它覆盖整个 /api/v1 整合器表面——
会话,连接,应用,域名飞行前,webhook-endpoint生命周期和密钥旋转 -
加上 webhook 签名验证.
其余部分
/api/v1路线(detect, (中文(简体) ),verify,和 Cloudflare / 域连接 一击入点)为 浏览器 表面:这些表面由会话符号认证 uRL 而不是您的密钥,以及主机流和@dodomain/connect部件驱动器 他们。 他们被故意缺席 在服务器SDK.
API 密码
new DoDomain(options)
| 选项 | 类型 | 页:1 |
|---|---|---|
secretKey | string | 要求。 必须从 dd_sk_。 。 。 . |
baseUrl | string | API基础. 默认为 https://app.dodomain.io (自备/海关部署超过此数). |
fetchImpl | typeof fetch | 覆盖使用的获取( 测试, 自定义代理) . |
dodomain.sessions.create(input) → Promise<ConnectSessionSummary>
input数字 : { domain, records, returnUrl?, recipe? } —— 说 domain (两个或两个以上DNS标签,无)
方案/端口/端口点)和 records 两者都需要; returnUrl 必须是https.
recipe 被接受为电线相容性,但今天没有被任何消耗,没有
替换为 records. 回归 { id, token, expiresAt, connectUrl, records, warnings? }。 。 。 .
返回类型被命名 Session 在0.5.0之前;该别名仍然被出口,标记
@deprecated,并在下一个主要进口中删除 ConnectSessionSummary。 。 。 .
丢出错误 DoDomainError 与 .status 财务报告和审定财务报表 .body畸形(非JSON)或
系统无效反应也投出 DoDomainError ——绝不是原始站台出错.
dodomain.connections.list(filters?) → Promise<ListConnectionsResult>
filters数字 : { appId?, domain?, limit?, cursor?, includeDisconnected? } ——都是可选的,而且只有
你所通过者去接线(服务器拥有默认值 : limit 50,最大为100;
隐藏断开的连接). 回返 { connections, nextCursor },最先更新;通过
nextCursor 恢复为 cursor 下一页,当它是 null。 。 。 .
dodomain.connections.get(connectionId) → Promise<Connection>
读取一个连接, 包括您已经断开的连接 。 与元素相同的对象形状
联 合 国 list。未知且不是您的两个抛出 DoDomainError 与 .status === 404。 。 。 .
dodomain.connections.delete(connectionId) → Promise<DisconnectConnectionResult>
断开连接: 归档连接并停止其 DNS 监控, 因此您客户的域名
停止放出漂流的网点。 同上,重复返回原样
disconnectedAt 与 alreadyDisconnected: true 并且不会发出第二次网络呼号.
dodomain.connections.reverify(connectionId) → Promise<ReverifyConnectionResult>
点播 DNS 重新检查 。 一旦任务完成,就决定 已接受 (HTTP 202);判决
以一个 connection.verified 编号 : connection.failed 网瘾。 检查内部的连接
最后的10分钟抛出 DoDomainError 与 .status === 429。 。 。 .
dodomain.sessions.get(sessionId) → Promise<IntegratorSession>
读取会话 编号 - 那个 sessionId 每一个网钩有效载荷(符号出现在
- 没有一个。 与公用代号路径不同,它返回一个 过期 届会而不是
拒绝:读一下 expired 旗帜,在读取时生成,因此已经是真的
在死神重写之前 status。通过 a dd_sess_… 符号抛出 DoDomainError 与
.status === 0 ——该标志处理同一路径的公臂,它用不同的回答
形状。 未知的不是你们的 .status === 404。 。 。 .
dodomain.apps.list() → Promise<ListAppsResult>
这个证书可以看见的应用程序. 密钥可以完全看到它自己的应用程序; OAuth 标志
apps:read 看到整个团队。 只有可出版的 pk_* 键被返回.
dodomain.domains.check(input) → Promise<CheckDomainResult>
input数字 : { domain }. 无国籍飞行前-提供方、拥有区域、连接层级和流量,是否单击
实际可用oneClickAvailable数字 : tier: 2 只代表提供商说话
Domain Connect),和手动指南——不创建会话. 没有任何东西存在;每个计划都有.
dodomain.webhookEndpoints.list() → Promise<ListWebhookEndpointsResult>
这个应用程序的传送目标. 从不 返回时 secret - 没有读取表面.
dodomain.webhookEndpoints.create(input) → Promise<WebhookEndpointWithSecret>
input数字 : { url } (https,不是本地主机/私人地址,不是复制件——每次拒绝都扔出)
.status === 400 与原因在内 .body.message;计划会投出 .status === 402) (中文(简体) ).
回来的 secret 显示一次 并且不再出现
同样的代码路径.
dodomain.webhookEndpoints.update(endpointId, input) → Promise<WebhookEndpoint>
重新指定新的终点 url签名的秘密是故意的 所以移动
主机从不强迫接收器重键. A 类 url 实际更改也恢复
自动停用端点.
dodomain.webhookEndpoints.resume(endpointId) → Promise<WebhookEndpoint>
恢复自动暂停的结束点域名——一个在7天内没有成功交付的域名
以及在该期间内至少5起无编号的交货(pausedAt 当它被暂停时,它将被设置。 清除
pausedAt 并重新启动7天的时钟;在不暂停的端点上具有偶能。 事件
当它被暂停时,被记录为漏运,而不因这一呼吁而感到不满
把它们从仪表板上重新推开.
dodomain.webhookEndpoints.delete(endpointId) → Promise<DeleteWebhookEndpointResult>
停止发送到终点。 过去交付的一行作为证据幸存下来,但交付失败 至已删除的终点,则不再可以重新驱动.
dodomain.webhookEndpoints.rotateSecret(endpointId) → Promise<WebhookEndpointWithSecret>
刻出新的签名秘密并返回 一次。 。 。 . 立即切换,没有双层秘密窗口 —— 说 工人们在交货时读到秘密现场, 所以签名立即切换, 重复旧的 包括交货。 尽快把新秘密布置到接收器上.
dodomain.keys.rotate() → Promise<RotateSecretKeyResult>
调用应用程序的秘密密钥自旋——可自动调用证书自旋的一半.
无优雅窗口 : 旧密钥停止认证此返回, 而返回
secretKey 是新书的唯一副本。 坚持下去,然后建立一个新的 DoDomain 与它——
你调用的客户端仍然拿着旧钥匙. publicKey 反射没有改变 所以CI工作可以
说它改写了正确的应用。 故意不创建/列出/撤销其他密钥.
verifyWebhook(secret, body, header, toleranceMs?, nowMs?) → boolean
核实 t=<unixMs>,v1=<hex hmac> 签名域名随每个 Webhook 发送.
类型
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 添加
一次显示 secret。 。 。 .
RotateSecretKeyResult =: { appId, publicKey, secretKey, rotatedAt }。 。 。 .
WebhookEvent =: { id, type, occurredAt, data } - 信封域名发送.
WebhookEventWire =: WebhookEvent 加上被腐烂的 event 的别名 type,用于接收器
写作反对2026-08-06前身.
见 src/index.ts 为整个地表.