把 AIDID 登录接入你的站点

今天可离线自测

今天就能写完并自测 RP 侧集成,全程离线:读 wire 契约、装 SDK、生成请求、校验响应、跑离线端到端 demo。生产登录被阻塞,直到链侧 resolve endpoint 获得授权。

动线总体状态今天可离线自测

步骤

  1. 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.tgz
  2. 2五分钟快速开始(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五分钟快速开始(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/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. 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. 6先读 wire 契约

    今天可用

    RP 契约是四个机器可读 schema:signin-request(你生成的请求)、signin-message(被签名的规范化文本)、signin-response(钱包返回的响应)与 login-error(你必须处理的稳定错误字符串)。

    文字口径的权威是登录规范,随 specs tarball 一起分发。写代码之前先读第 0 节(诚实边界)与第 7 节(11 步有序校验)。

  7. 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. 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. 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. 10生产登录被阻塞(链侧 resolve 未授权)

    今天不可用

    你可以把集成写完并自测,但无法上线:GET {agent_api}/agent/v1/aidid/resolve 在公网尚未获得授权,因此 RP 在生产环境解析不了任何 DID。这就是 status.json 里不可用的能力 didLoginProduction。

    不要把 AIDID 登录描述为已可用于生产;也不要上线一种在 resolve 失败时静默降级为匿名会话的流程。正确行为是以 endpoint_unavailable 失败关闭。

    今天为什么被阻塞

    • didLoginProduction The 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. 11把你的站点登记进目录(只证明域名控制权,仅此而已)

    今天可用

    集成写好后,你可以在自己的 origin 上发布 /.well-known/aidid-rp.txt,并向公开 registry 仓库提交一个 pull request。GitHub Action 会抓取你的文件并与登记的 challenge 比对;当一个 PR 只改这一个文件且 challenge 匹配时,会被自动合并——从你自己的 fork 提的 PR 也一样,不需要邀请,也不需要协作者权限。当前登记免费,并明确标注为过渡方案,直到链上 registry 可用。

    不需要等重建:目录页读取第一方缓存,该缓存按需刷新(缓存超过十分钟时下一次访问会触发刷新,同一缓存至少间隔十分钟),读取失败时回退到本次部署内置的快照——因此合并后的条目通常在一两次访问内出现。

    被列入目录只证明你控制该 origin,并声明接入了 AIDID;不构成 AIDID 对其安全性或信誉的背书。