把 AIDID 登录接入你的站点
今天可离线自测今天就能写完并自测 RP 侧集成,全程离线:读 wire 契约、装 SDK、生成请求、校验响应、跑离线端到端 demo。生产登录被阻塞,直到链侧 resolve endpoint 获得授权。
动线总体状态今天可离线自测
步骤
1五分钟快速开始(1/4):安装 SDK
今天可用SDK 作为本站的静态文件提供:不需要 npm 账号、不需要 registry 账号、也没有我们的服务端参与。该包是纯 ESM。
只有签名验签器会导入可选 peer 依赖 @msg-chain/sdk(Dilithium-5 原语)。消息构建、请求与响应校验、回调解析与 drop-in 按钮都不依赖任何第三方包,因此也能在边缘函数里运行。
bash npm install https://aidid.im/download/aidid-login-0.1.0.tgz2五分钟快速开始(2/4):在你的服务端生成请求
今天可用createSignInRequest 会填好 type、version、默认的 https uri、一次性 nonce(32 字节 CSPRNG)与 5 分钟的 expirationTime。TTL 请保持不超过 5 分钟。
请求必须自己保存:响应只是把它回声回来,而这个回声不能证明任何事。网页入口是 https://aidid.im/authorize/?payload=...,这个 URL 不能改。
js // server.js — generate a request and keep YOUR OWN copy of it. import { createSignInRequest, encodeSignInDeeplink } from '@aidid/login'; import { buildAuthorizeUrl } from '@aidid/login/widget'; const WALLET_ORIGIN = 'https://aidid.im'; // must be a bare https origin export const startSignIn = async (kv, domain) => { const request = createSignInRequest({ domain, // e.g. 'rp.example' (a port is allowed) chainId: 'msg-chain-1', // the chain the response must be bound to uri: `https://${domain}/aidid/callback`, // https callback on your origin statement: 'Sign in to my site.', resources: ['did:auth'], // add 'pay:address' only if you need the payout address }); // Store YOUR copy (TTL <= 5 minutes). Never trust the echo inside the response. await kv.put(`req:${request.nonce}`, JSON.stringify(request), { ttlSeconds: 300 }); return { authorizeUrl: buildAuthorizeUrl(request, WALLET_ORIGIN), // same device: redirect here deeplink: encodeSignInDeeplink(request), // cross device: turn this into a QR }; };3五分钟快速开始(3/4):校验回调,失败关闭
今天可离线自测11 步有序校验都在 verifySignInResponse 内部执行。它返回 { ok: false, error },绝不降级放行。
今天它总会在 resolve 这一步以 endpoint_unavailable 失败关闭,因为链侧 resolve endpoint 尚未授权。这是诚实的结果,不是你代码的 bug。只要运行超过一个实例,nonces 就必须是共享存储(Redis、D1、Durable Object),不能用 createMemoryNonceStore。
js // callback.js — verify fail-closed against the request YOU stored. import { createAgentApiResolver, createCachedResolver, verifySignInResponse } from '@aidid/login'; import { createMsgChainVerifier } from '@aidid/login/verifiers/msgChainSdk'; export const onCallback = async (kv, nonces, response) => { const stored = await kv.get(`req:${response.nonce}`); // look up by the returned nonce if (stored === null) return { ok: false, error: 'nonce_unknown' }; return verifySignInResponse({ request: JSON.parse(stored), // rebuild the canonical message from YOUR copy, never the echo response, resolve: createCachedResolver({ resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }), }), // 10 s TTL + single-flight by default; the API has no CORS, so this runs server-side nonces, // shared store: consume each nonce exactly once verifySignature: await createMsgChainVerifier(), chainId: 'msg-chain-1', }); };4五分钟快速开始(4/4):挂上 drop-in 按钮
今天可用该组件渲染一个带内联 SVG 的普通按钮,不需要任何托管资源,因此没有需要拉取或额外样式化的东西。创建时它读一次 #response=(钱包会回跳到 request.uri#response=...),把它从地址栏清除,然后只调用一次 onResponse;失败时调用 onError 并给出稳定 reason(no_fragment 不算错误)。
它不做任何校验、也绝不静默批准,并且从不把 /authorize 放进 iframe。校验仍然在你的服务端完成。
js // app.js — the drop-in button (browser, ES module, no framework). import { mountSignInButton } from '@aidid/login/widget'; const button = mountSignInButton('#login', { request, // the request your server generated and stored walletOrigin: 'https://aidid.im', mode: 'auto', // 'redirect' same device | 'deeplink' cross device onResponse: (response) => post('/aidid/callback', { response }), // fires once, fail-closed onError: (error) => showError(error.reason), labels: { signIn: 'Sign in with AIDID' }, // override any subset, including Chinese }); // Cross device: render your own QR from button.getDeeplink().5两种承载:同设备(顶层跳转)与跨设备(二维码 / deeplink)
今天可用mode: 'auto' 会在有浏览器、且设备看起来就是持有 DID 的那台时选择 redirect(移动 UA,含以 macOS UA 上报的 iPadOS),否则选择 deeplink。auto 规则已写进 SPEC-LOGIN-v1 §9.2(基于 UA / 触点数的启发式);它仍然只是启发式,你比它更了解你的用户时就显式传 mode。
同设备:顶层跳转到钱包。用户批准后,钱包带着 #response=... 回跳到你的回调 URL,组件读取该 fragment。
跨设备:组件展示 web+aidid://auth?payload=... 字符串与复制按钮;二维码请用你自己的库渲染,因为 SDK 不引入任何二维码依赖。持有 DID 的手机会批准并落到你的回调页——另一台机器上的浏览器本身不会看到响应。今天规范没有定义服务端交接,所以回调页必须把 fragment POST 回你的服务端,原始浏览器再轮询会话结果。
尚未在真机验证:iOS PWA(display: standalone)批准后能否自动回跳 RP,没有在真机上测过。请在你自己的目标环境里实测,并提供降级入口(例如让用户手动打开的链接)。
js // 'redirect' — same device: the SDK does a top-level navigation to // https://aidid.im/authorize/?payload=<encoded JSON> const url = button.getAuthorizeUrl(); // 'deeplink' — cross device: put this string in a QR code, or offer copy const deeplink = button.getDeeplink(); // web+aidid://auth?payload=...6先读 wire 契约
今天可用RP 契约是四个机器可读 schema:signin-request(你生成的请求)、signin-message(被签名的规范化文本)、signin-response(钱包返回的响应)与 login-error(你必须处理的稳定错误字符串)。
文字口径的权威是登录规范,随 specs tarball 一起分发。写代码之前先读第 0 节(诚实边界)与第 7 节(11 步有序校验)。
7跑离线端到端 demo(真签名、无链)
今天可离线自测测试向量用一个固定的公开测试种子派生 Dilithium-5 密钥对,并对规范化消息做真实签名。它在没有网络、没有链、没有测试网的情况下演练请求、批准、验签与会话。
三条可跑路径:Node 全流程(tarball 里的 examples/node/offline-demo.mjs)、对照 SDK 浏览器 bundle 的浏览器全流程,以及一个服务端示例,展示 verifySignInResponse 加 HTTP resolver——在没有公网 endpoint 时以 endpoint_unavailable 失败关闭。
demo 不校验链上状态(用 fixture resolver 代替 canonical resolve),所以它不能证明某个 DID 在链上存在。它的测试密钥绝不能用于任何真实身份。
8按 11 步有序校验,全程 fail-closed
今天可离线自测按顺序校验,任一步失败即拒绝:解析响应;把 nonce 与 domain 同你自己保存的请求比对;重建规范化消息并逐字节比较;检查是否过期;单次消费 nonce;检查 chain id;在服务端 resolve DID;确认验证方法在 authentication 列表中;只允许 Dilithium-5 proof 类型;验签;最后建立会话。
resolve 必须由服务端调用,因为 agent API 不返回 CORS 头,浏览器无法直连。链侧没有内置限流、也不承诺 Retry-After,所以要做 5 到 15 秒的短 TTL 缓存、单飞防击穿、失败退避,且不要批量解析。nonce 必须在验签之前消费。
bridge proof 类型 AididP384BridgeSignature2025 仅用于演示;除非显式选择接受,否则一律拒绝;即便接受,也必须标注它不抗量子。
9上线前清单:每一条都为真之前不要发布
今天可离线自测上生产之前,下列每一条都必须成立:
1. 链侧 resolve endpoint 已授权。今天没有(didLoginProduction),所以每一次真实校验都会以 endpoint_unavailable 失败关闭。 2. 签名的用户确实持有 DID。今天大多数用户创建不了 DID,因为普通用户没有获取 MSG 的路径(userCreatesDid)。 3. 你的 nonce store 跨实例共享,并且原子地消费 nonce;每实例内存存储会接受重放。 4. resolve 在服务端运行,带 5 到 15 秒 TTL 缓存与单飞;该 API 没有 CORS、也没有内置限流。 5. 你处理 login-error 的每一个稳定错误(did_not_found、did_deactivated、nonce_replayed、endpoint_unavailable 等),并且绝不降级为匿名会话。 6. 登记进目录是可选的,它只是一句「你控制该 origin」的声明。它不是安全背书,也不是登录的前置条件。
在第 1、2 条为真之前,不要发布任何声称「登录可用」的东西。
10生产登录被阻塞(链侧 resolve 未授权)
今天不可用你可以把集成写完并自测,但无法上线:GET {agent_api}/agent/v1/aidid/resolve 在公网尚未获得授权,因此 RP 在生产环境解析不了任何 DID。这就是 status.json 里不可用的能力 didLoginProduction。
不要把 AIDID 登录描述为已可用于生产;也不要上线一种在 resolve 失败时静默降级为匿名会话的流程。正确行为是以 endpoint_unavailable 失败关闭。
今天为什么被阻塞
didLoginProductionThe chain-side DID resolve endpoint (`GET {agent_api}/agent/v1/aidid/resolve`) is not authorized yet, so an RP cannot resolve any DID in production.
原因取自 status.json#capabilities —— 不是为这个页面手写的。
11把你的站点登记进目录(只证明域名控制权,仅此而已)
今天可用集成写好后,你可以在自己的 origin 上发布 /.well-known/aidid-rp.txt,并向公开 registry 仓库提交一个 pull request。GitHub Action 会抓取你的文件并与登记的 challenge 比对;当一个 PR 只改这一个文件且 challenge 匹配时,会被自动合并——从你自己的 fork 提的 PR 也一样,不需要邀请,也不需要协作者权限。当前登记免费,并明确标注为过渡方案,直到链上 registry 可用。
不需要等重建:目录页读取第一方缓存,该缓存按需刷新(缓存超过十分钟时下一次访问会触发刷新,同一缓存至少间隔十分钟),读取失败时回退到本次部署内置的快照——因此合并后的条目通常在一两次访问内出现。
被列入目录只证明你控制该 origin,并声明接入了 AIDID;不构成 AIDID 对其安全性或信誉的背书。