{
  "schema": "aidid.guide.index",
  "version": "1",
  "site": "https://aidid.im",
  "generatedBy": "scripts/generate-guide-surface.mjs",
  "generatedFrom": "data/guide.json",
  "updatedAt": "2026-10-13",
  "statusVocabulary": {
    "available": "Works today.",
    "offline-testable": "You can write and self-test the integration today, but it cannot run in production yet.",
    "blocked": "Not available today; `refs` names the unavailable capabilities in status.json."
  },
  "capabilities": "https://aidid.im/status.json",
  "notes": "Tutorials as data. One source (data/guide.json) generates the human pages and the machine-readable twin at /start/index.json. Every step carries a status from the same vocabulary as status.json#capabilities: 'available' (works today), 'offline-testable' (write and self-test the integration today, but it cannot run in production yet), 'blocked' (unavailable today; refs points at the unavailable capability keys). No journey may claim more than status.json does. See https://aidid.im/status.json for the authoritative per-capability status and reasons.",
  "journeys": [
    {
      "slug": "did",
      "order": 1,
      "status": "blocked",
      "refs": [
        "userCreatesDid"
      ],
      "en": {
        "title": "Get a DID",
        "summary": "A DID is a did:msg identity registered on MSGChain. Creating one needs MSG to pay the chain fee, and there is no end-user way to obtain MSG yet, so read this as a specification walkthrough rather than a working sign-up."
      },
      "zh": {
        "title": "获得一个 DID",
        "summary": "DID 是登记在 MSGChain 上的 did:msg 身份。创建它需要 MSG 支付链上手续费，而目前普通用户没有任何获得 MSG 的路径，所以这条动线今天只能当作规范走查，不是可用的注册流程。"
      },
      "steps": [
        {
          "id": "did-1",
          "status": "available",
          "refs": [],
          "en": {
            "title": "What a DID is",
            "body": "A DID is an identifier of the form did:msg:<suffix>. Its document holds a controller, one or more verification methods, and an authentication list.\n\nAIDID uses CRYSTALS-Dilithium-5 (round-3) keys: public key 2592 bytes, signature 4595 bytes. The private key never leaves the user's device; a login is one signature over a canonical text message.\n\nThe shapes are frozen in machine-readable schemas, so read did-document, msg-did and canonical-resolve in the schemas index before writing any code."
          },
          "zh": {
            "title": "DID 是什么",
            "body": "DID 是形如 did:msg:<后缀> 的标识符。它的文档包含 controller、一个或多个验证方法，以及一个 authentication 列表。\n\nAIDID 使用 CRYSTALS-Dilithium-5（round-3）密钥：公钥 2592 字节、签名 4595 字节。私钥从不离开用户设备；一次登录就是对一段规范化文本消息的一次签名。\n\n这些形状都冻结在机器可读 schema 里，写代码之前请先读 schemas 索引中的 did-document、msg-did 与 canonical-resolve。"
          },
          "links": [
            {
              "href": "/schemas/index.json",
              "kind": "internal",
              "en": {
                "label": "Schemas index"
              },
              "zh": {
                "label": "Schema 索引"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "did-2",
          "status": "blocked",
          "refs": [
            "userCreatesDid"
          ],
          "en": {
            "title": "Registering a DID needs MSG, and no user can get MSG today",
            "body": "Registering a DID is a chain write (register_did on the chain's DID registry) that costs a chain fee paid in the native umsg denomination.\n\nThere is no public faucet and no end-user path to obtain MSG. An ordinary user therefore cannot create a DID, and the login chain starts broken; this is recorded as the unavailable capability userCreatesDid in status.json.\n\nDo not build a sign-up flow that assumes the user already holds a DID and MSG. The honest status today is fail-closed."
          },
          "zh": {
            "title": "注册 DID 需要 MSG，而今天普通用户拿不到 MSG",
            "body": "注册 DID 是一次链上写入（链的 DID registry 上的 register_did），需要以原生 umsg 面额支付链上手续费。\n\n目前没有公开 faucet，也没有任何面向普通用户的获取 MSG 的路径。因此普通用户无法创建 DID，登录链的起点是断的；这在 status.json 里登记为不可用能力 userCreatesDid。\n\n不要构建一个假定用户已经持有 DID 与 MSG 的注册流程。今天诚实的口径是 fail-closed。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            },
            {
              "href": "/download/",
              "kind": "internal",
              "en": {
                "label": "Downloads"
              },
              "zh": {
                "label": "下载"
              }
            }
          ]
        },
        {
          "id": "did-3",
          "status": "blocked",
          "refs": [
            "chainWrite"
          ],
          "en": {
            "title": "Chain writes wait for the window parameters",
            "body": "Any chain write, including DID registration, update and deactivation, or deploying the identity contract, is blocked until MSGChain releases its window parameters. Mainnet is No-Go and no contract is deployed; the blocked capability is chainWrite.\n\nWhat you can do today instead: design against the specs and the JSON Schemas, and rehearse everything that does not need a chain in the offline demo."
          },
          "zh": {
            "title": "链上写入要等《窗口参数》",
            "body": "任何链上写入——包括 DID 的注册、更新、停用，以及部署身份合约——都要等 MSGChain 发布《窗口参数》之后才能进行。主网 No-Go，且没有部署任何合约；被阻塞的能力是 chainWrite。\n\n今天可以做的是：对照规范与 JSON Schema 做设计，并在离线 demo 里演练所有不需要链的环节。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            },
            {
              "href": "/download/",
              "kind": "internal",
              "en": {
                "label": "Downloads"
              },
              "zh": {
                "label": "下载"
              }
            }
          ]
        }
      ]
    },
    {
      "slug": "subdid",
      "order": 2,
      "status": "offline-testable",
      "refs": [
        "offlineEndToEnd"
      ],
      "en": {
        "title": "Delegate a sub-DID (parent to child)",
        "summary": "A parent DID can authorise a restricted-scope sub-DID for a purpose, a resource set and a validity period, and bind an AI agent to it. You can rehearse the whole flow offline today; on-chain binding, revocation and quota need stage B and the window parameters."
      },
      "zh": {
        "title": "母 DID 到子 DID 的授权",
        "summary": "母 DID 可以为一个用途、一组资源和一段有效期授权一个受限范围的子 DID，并绑定一个 AI Agent。整条流程今天可以在离线环境演练；链上权威绑定、吊销与配额要等阶段 B 与《窗口参数》。"
      },
      "steps": [
        {
          "id": "subdid-1",
          "status": "available",
          "refs": [],
          "en": {
            "title": "What a sub-DID is: a restricted-scope grant, not a separate key",
            "body": "A sub-DID is a did:msg whose document has controller: [<parent DID>] and an AididDelegation service entry carrying the grant payload: purpose, resource scope, expiry, bound agent and status.\n\nIn stage A the child document reuses the parent's Dilithium-5 key material: its verification method points at the parent public key. So this is a hand-off of restricted permissions, not a hand-off of keys. Never describe it as giving an agent its own production key.\n\nThe own-key mode, where the parent signs over the agent's own Dilithium-5 public key, exists at the protocol level and is verifiable offline, but it is fail-closed by default and has no on-chain binding or revocation."
          },
          "zh": {
            "title": "子 DID 是什么：受限权限授权，不是一把独立密钥",
            "body": "子 DID 是一个 did:msg，其文档的 controller 为 [<母 DID>]，并带有一个 AididDelegation service 条目，承载授权 payload：用途、资源范围、有效期、绑定的 Agent 与状态。\n\n在阶段 A，子文档复用母 DID 的 Dilithium-5 密钥材料：它的验证方法指向母 DID 公钥。所以这是「受限权限的外包」，不是「密钥的外包」。绝不要把它描述成给 Agent 一把自己的生产密钥。\n\nown-key 模式（母 DID 以自身签名绑定 Agent 自己的 Dilithium-5 公钥）在协议层已存在、可离线验证，但默认 fail-closed，且没有链上绑定与吊销。"
          },
          "links": [
            {
              "href": "/schemas/delegation/delegation-payload.schema.json",
              "kind": "internal",
              "en": {
                "label": "Delegation payload schema"
              },
              "zh": {
                "label": "委派 payload schema"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "subdid-2",
          "status": "offline-testable",
          "refs": [
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Create it locally and run the offline demo",
            "body": "The offline demo in the RP SDK (examples/node/offline-demo.mjs, section 6) walks the whole flow with real Dilithium-5 signatures and no network: create a sub-DID, have the parent sign the delegation, sign in as the sub-DID (all 11 login checks pass), let the delegation gate allow it, have an out-of-scope resource refused, verify the audit receipt, and finally revoke and watch the identity still verify while the login is refused with delegation_revoked.\n\nInstall the published SDK and run the same demo from the tarball if you do not have the repository."
          },
          "zh": {
            "title": "在本地创建它，并跑离线 demo",
            "body": "RP SDK 里的离线 demo（examples/node/offline-demo.mjs 第 6 节）用真实 Dilithium-5 签名、全程无网络地走完整条流程：创建子 DID、母 DID 签名授权、以子 DID 登录（11 步校验全部通过）、委派闸门放行、范围外资源被拒、审计回执验签，最后撤销——身份仍然验证通过，但登录被拒（delegation_revoked）。\n\n如果没有本仓，可以安装已发布的 SDK，并从 tarball 里跑同一个 demo。"
          },
          "links": [
            {
              "href": "/download/",
              "kind": "internal",
              "en": {
                "label": "Downloads"
              },
              "zh": {
                "label": "下载"
              }
            },
            {
              "href": "/download/index.json",
              "kind": "internal",
              "en": {
                "label": "Downloads index"
              },
              "zh": {
                "label": "下载索引"
              }
            }
          ]
        },
        {
          "id": "subdid-3",
          "status": "offline-testable",
          "refs": [
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Verify a sub-DID on the RP side (step 12, fail-closed)",
            "body": "RP verification reuses the 11 login checks and adds step 12: read the delegation entries from the resolved child document (no entry means reject with delegation_not_found), then evaluate the payload for scope, expiry and expected parent.\n\nThe SDK ships verifyDelegation and createDelegationVerifier. It never throws and never downgrades to allow. It needs one of two trust sources: the on-chain child document, or the parent's signature over the canonical delegation text. For own-key payloads it rejects unless the caller explicitly opts in with allowOwnKey: true, because there is no revocation channel offline.\n\nIt does not count quota or run a deny-list. scope.maxUsesPerDay is declared metadata only, so counting, rate limiting and deny-lists are the RP's own responsibility."
          },
          "zh": {
            "title": "在 RP 侧验证子 DID（第 12 步，fail-closed）",
            "body": "RP 侧校验复用登录的 11 步，再追加第 12 步：从已 resolve 的子文档里读取委派条目（没有条目就以 delegation_not_found 拒绝），然后按范围、有效期与预期母 DID 裁决 payload。\n\nSDK 提供 verifyDelegation 与 createDelegationVerifier。它不抛异常、也不降级放行。它需要两种信任来源之一：链上的子文档，或母 DID 对 canonical 委派文本的签名。对 own-key payload，除非调用方显式以 allowOwnKey: true 选择接受，否则一律拒绝，因为离线没有吊销通道。\n\n它不做额度计数、也没有 deny-list。scope.maxUsesPerDay 只是已声明的元数据，计数、限流与 deny-list 必须由 RP 自行实现。"
          },
          "links": [
            {
              "href": "/schemas/delegation/delegation-payload.schema.json",
              "kind": "internal",
              "en": {
                "label": "Delegation payload schema"
              },
              "zh": {
                "label": "委派 payload schema"
              }
            },
            {
              "href": "/schemas/delegation/delegation-error.schema.json",
              "kind": "internal",
              "en": {
                "label": "Delegation error codes"
              },
              "zh": {
                "label": "委派错误码"
              }
            }
          ]
        },
        {
          "id": "subdid-4",
          "status": "blocked",
          "refs": [
            "chainWrite",
            "independentAgentKey"
          ],
          "en": {
            "title": "What is still blocked: on-chain binding, revocation and quota",
            "body": "There is no on-chain authoritative binding of a sub-DID to an agent, no revocation primitive (the chain's deactivate is irreversible and there is no blacklist), and no enforced quota. Those need stage B, our identity contract, which is written but not deployed until the window parameters are released. Stage A writes the grant into the child DID document through the chain's own registry contract, and that write is blocked today too.\n\nUntil then, treat every sub-DID grant as a local arrangement that can only be revoked by updating the document, and keep the honest wording: restricted-scope authorisation, not an independent production key."
          },
          "zh": {
            "title": "仍然被阻塞的部分：链上绑定、吊销与配额",
            "body": "目前没有「子 DID 绑定到某个 Agent」的链上权威登记，没有吊销原语（链上的 deactivate 不可逆，也没有黑名单），也没有被强制的配额。这些都要阶段 B——我们自己的身份合约——在《窗口参数》发布后部署才能承载。阶段 A 是通过链方自有的 registry 合约把授权写进子 DID 文档，而这次写入今天同样被阻塞。\n\n在那之前，请把每一次子 DID 授权当作本机安排：只能通过更新文档来停止引用它。对外保持一致口径——这是受限范围授权，不是一把独立的生产密钥。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        }
      ]
    },
    {
      "slug": "integrate",
      "order": 3,
      "status": "offline-testable",
      "refs": [
        "rpIntegration",
        "offlineEndToEnd"
      ],
      "en": {
        "title": "Integrate AIDID login into your site",
        "summary": "Write and self-test the RP-side integration today, entirely offline: read the wire contract, install the SDK, build a request, verify a response, and run the offline end-to-end demo. Production login is blocked until the chain-side resolve endpoint is authorised."
      },
      "zh": {
        "title": "把 AIDID 登录接入你的站点",
        "summary": "今天就能写完并自测 RP 侧集成，全程离线：读 wire 契约、装 SDK、生成请求、校验响应、跑离线端到端 demo。生产登录被阻塞，直到链侧 resolve endpoint 获得授权。"
      },
      "steps": [
        {
          "id": "integrate-1",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Five-minute quick start (1/4): install the SDK",
            "body": "The SDK is served as a static file from this site: no npm account, no registry account and no backend of ours in the loop. The package is pure ESM.\n\nOnly the signature verifier imports the optional peer @msg-chain/sdk (the Dilithium-5 primitive). Message building, request and response verification, the callback reader and the drop-in button are dependency-free, so they also run inside an edge worker."
          },
          "zh": {
            "title": "五分钟快速开始（1/4）：安装 SDK",
            "body": "SDK 作为本站的静态文件提供：不需要 npm 账号、不需要 registry 账号、也没有我们的服务端参与。该包是纯 ESM。\n\n只有签名验签器会导入可选 peer 依赖 @msg-chain/sdk（Dilithium-5 原语）。消息构建、请求与响应校验、回调解析与 drop-in 按钮都不依赖任何第三方包，因此也能在边缘函数里运行。"
          },
          "code": {
            "language": "bash",
            "content": "npm install https://aidid.im/download/aidid-login-0.1.0.tgz"
          },
          "links": [
            {
              "href": "/download/",
              "kind": "internal",
              "en": {
                "label": "Downloads"
              },
              "zh": {
                "label": "下载"
              }
            },
            {
              "href": "/download/index.json",
              "kind": "internal",
              "en": {
                "label": "Downloads index"
              },
              "zh": {
                "label": "下载索引"
              }
            }
          ]
        },
        {
          "id": "integrate-2",
          "status": "available",
          "refs": [
            "rpIntegration"
          ],
          "en": {
            "title": "Five-minute quick start (2/4): generate the request on your server",
            "body": "createSignInRequest fills type, version, an https uri default, a single-use nonce (32 CSPRNG bytes) and a 5-minute expirationTime. Keep the TTL at or below 5 minutes.\n\nStore the request yourself: the response only echoes it back, and that echo proves nothing. The web entry point is https://aidid.im/authorize/?payload=... and that URL must not change."
          },
          "zh": {
            "title": "五分钟快速开始（2/4）：在你的服务端生成请求",
            "body": "createSignInRequest 会填好 type、version、默认的 https uri、一次性 nonce（32 字节 CSPRNG）与 5 分钟的 expirationTime。TTL 请保持不超过 5 分钟。\n\n请求必须自己保存：响应只是把它回声回来，而这个回声不能证明任何事。网页入口是 https://aidid.im/authorize/?payload=...，这个 URL 不能改。"
          },
          "code": {
            "language": "js",
            "content": "// server.js — generate a request and keep YOUR OWN copy of it.\nimport { createSignInRequest, encodeSignInDeeplink } from '@aidid/login';\nimport { buildAuthorizeUrl } from '@aidid/login/widget';\n\nconst WALLET_ORIGIN = 'https://aidid.im'; // must be a bare https origin\n\nexport const startSignIn = async (kv, domain) => {\n  const request = createSignInRequest({\n    domain,                                  // e.g. 'rp.example' (a port is allowed)\n    chainId: 'msg-chain-1',                  // the chain the response must be bound to\n    uri: `https://${domain}/aidid/callback`, // https callback on your origin\n    statement: 'Sign in to my site.',\n    resources: ['did:auth'],                 // add 'pay:address' only if you need the payout address\n  });\n\n  // Store YOUR copy (TTL <= 5 minutes). Never trust the echo inside the response.\n  await kv.put(`req:${request.nonce}`, JSON.stringify(request), { ttlSeconds: 300 });\n\n  return {\n    authorizeUrl: buildAuthorizeUrl(request, WALLET_ORIGIN), // same device: redirect here\n    deeplink: encodeSignInDeeplink(request),                 // cross device: turn this into a QR\n  };\n};"
          },
          "links": [
            {
              "href": "/schemas/login/signin-request.schema.json",
              "kind": "internal",
              "en": {
                "label": "Sign-in request schema"
              },
              "zh": {
                "label": "登录请求 schema"
              }
            },
            {
              "href": "/authorize/",
              "kind": "internal",
              "en": {
                "label": "Wallet entry point"
              },
              "zh": {
                "label": "钱包入口"
              }
            }
          ]
        },
        {
          "id": "integrate-3",
          "status": "offline-testable",
          "refs": [
            "rpIntegration",
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Five-minute quick start (3/4): verify the callback, fail-closed",
            "body": "The eleven ordered checks run inside verifySignInResponse. It returns { ok: false, error } and never downgrades to allow.\n\nToday it always fails closed at the resolve step with endpoint_unavailable, because the chain-side resolve endpoint is not authorised. That is the honest result, not a bug in your code. nonces must be a shared store (Redis, D1, a Durable Object), not createMemoryNonceStore, as soon as you run more than one instance."
          },
          "zh": {
            "title": "五分钟快速开始（3/4）：校验回调，失败关闭",
            "body": "11 步有序校验都在 verifySignInResponse 内部执行。它返回 { ok: false, error }，绝不降级放行。\n\n今天它总会在 resolve 这一步以 endpoint_unavailable 失败关闭，因为链侧 resolve endpoint 尚未授权。这是诚实的结果，不是你代码的 bug。只要运行超过一个实例，nonces 就必须是共享存储（Redis、D1、Durable Object），不能用 createMemoryNonceStore。"
          },
          "code": {
            "language": "js",
            "content": "// callback.js — verify fail-closed against the request YOU stored.\nimport { createAgentApiResolver, createCachedResolver, verifySignInResponse } from '@aidid/login';\nimport { createMsgChainVerifier } from '@aidid/login/verifiers/msgChainSdk';\n\nexport const onCallback = async (kv, nonces, response) => {\n  const stored = await kv.get(`req:${response.nonce}`); // look up by the returned nonce\n  if (stored === null) return { ok: false, error: 'nonce_unknown' };\n\n  return verifySignInResponse({\n    request: JSON.parse(stored), // rebuild the canonical message from YOUR copy, never the echo\n    response,\n    resolve: createCachedResolver({\n      resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }),\n    }), // 10 s TTL + single-flight by default; the API has no CORS, so this runs server-side\n    nonces,                      // shared store: consume each nonce exactly once\n    verifySignature: await createMsgChainVerifier(),\n    chainId: 'msg-chain-1',\n  });\n};"
          },
          "links": [
            {
              "href": "/schemas/login/login-error.schema.json",
              "kind": "internal",
              "en": {
                "label": "Login error codes"
              },
              "zh": {
                "label": "登录错误码"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "integrate-4",
          "status": "available",
          "refs": [
            "rpIntegration"
          ],
          "en": {
            "title": "Five-minute quick start (4/4): mount the drop-in button",
            "body": "The widget renders a plain button with an inline SVG and no hosted asset, so there is nothing to style or fetch. On creation it reads #response= once (the wallet returns to request.uri#response=...), clears it from the address bar, then calls onResponse exactly once — or onError with a stable reason (no_fragment is not an error).\n\nIt does not verify anything and never approves silently, and it never puts /authorize in an iframe. Verification stays on your server."
          },
          "zh": {
            "title": "五分钟快速开始（4/4）：挂上 drop-in 按钮",
            "body": "该组件渲染一个带内联 SVG 的普通按钮，不需要任何托管资源，因此没有需要拉取或额外样式化的东西。创建时它读一次 #response=（钱包会回跳到 request.uri#response=...），把它从地址栏清除，然后只调用一次 onResponse；失败时调用 onError 并给出稳定 reason（no_fragment 不算错误）。\n\n它不做任何校验、也绝不静默批准，并且从不把 /authorize 放进 iframe。校验仍然在你的服务端完成。"
          },
          "code": {
            "language": "js",
            "content": "// app.js — the drop-in button (browser, ES module, no framework).\nimport { mountSignInButton } from '@aidid/login/widget';\n\nconst button = mountSignInButton('#login', {\n  request,                        // the request your server generated and stored\n  walletOrigin: 'https://aidid.im',\n  mode: 'auto',                   // 'redirect' same device | 'deeplink' cross device\n  onResponse: (response) => post('/aidid/callback', { response }), // fires once, fail-closed\n  onError: (error) => showError(error.reason),\n  labels: { signIn: 'Sign in with AIDID' }, // override any subset, including Chinese\n});\n// Cross device: render your own QR from button.getDeeplink()."
          },
          "links": [
            {
              "href": "/schemas/login/signin-request.schema.json",
              "kind": "internal",
              "en": {
                "label": "Sign-in request schema"
              },
              "zh": {
                "label": "登录请求 schema"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "integrate-5",
          "status": "available",
          "refs": [
            "rpIntegration"
          ],
          "en": {
            "title": "Two transports: same device (top-level redirect) vs cross device (QR / deeplink)",
            "body": "mode: 'auto' picks redirect when a browser exists and the device looks like the one holding the DID (a mobile user agent, including iPadOS reporting a macOS UA), otherwise deeplink. The auto rule is specified in SPEC-LOGIN-v1 §9.2 (a user-agent / touch-point heuristic), and it stays a heuristic: pass mode explicitly when you know your audience better than the heuristic does.\n\nSame device: a top-level navigation to the wallet. After the user approves, the wallet returns to your callback URL with #response=..., and the widget reads that fragment.\n\nCross device: the widget shows a web+aidid://auth?payload=... string and a copy button; render the QR with your own library, because the SDK ships no QR dependency. The phone that holds the DID approves and lands on your callback page — the browser on the other machine does not see the response by itself. Today the spec defines no server-side handoff, so the callback page must POST the fragment to your server and the originating browser must poll for the resulting session.\n\nNot verified on hardware: whether an iOS PWA (display: standalone) returns to the RP automatically after approval has not been tested on a real device. Test it in your own target environment and offer a fallback (for example a link the user can open manually)."
          },
          "zh": {
            "title": "两种承载：同设备（顶层跳转）与跨设备（二维码 / deeplink）",
            "body": "mode: 'auto' 会在有浏览器、且设备看起来就是持有 DID 的那台时选择 redirect（移动 UA，含以 macOS UA 上报的 iPadOS），否则选择 deeplink。auto 规则已写进 SPEC-LOGIN-v1 §9.2（基于 UA / 触点数的启发式）；它仍然只是启发式，你比它更了解你的用户时就显式传 mode。\n\n同设备：顶层跳转到钱包。用户批准后，钱包带着 #response=... 回跳到你的回调 URL，组件读取该 fragment。\n\n跨设备：组件展示 web+aidid://auth?payload=... 字符串与复制按钮；二维码请用你自己的库渲染，因为 SDK 不引入任何二维码依赖。持有 DID 的手机会批准并落到你的回调页——另一台机器上的浏览器本身不会看到响应。今天规范没有定义服务端交接，所以回调页必须把 fragment POST 回你的服务端，原始浏览器再轮询会话结果。\n\n尚未在真机验证：iOS PWA（display: standalone）批准后能否自动回跳 RP，没有在真机上测过。请在你自己的目标环境里实测，并提供降级入口（例如让用户手动打开的链接）。"
          },
          "code": {
            "language": "js",
            "content": "// 'redirect' — same device: the SDK does a top-level navigation to\n//   https://aidid.im/authorize/?payload=<encoded JSON>\nconst url = button.getAuthorizeUrl();\n\n// 'deeplink' — cross device: put this string in a QR code, or offer copy\nconst deeplink = button.getDeeplink(); // web+aidid://auth?payload=..."
          },
          "links": [
            {
              "href": "/authorize/",
              "kind": "internal",
              "en": {
                "label": "Wallet entry point"
              },
              "zh": {
                "label": "钱包入口"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "integrate-6",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Read the wire contract first",
            "body": "The RP contract is four machine-readable schemas: signin-request (what you generate), signin-message (the canonical text that gets signed), signin-response (what the wallet returns) and login-error (the stable error strings you must handle).\n\nThe prose authority is the login specification, shipped in the specs tarball. Read section 0 (honest limits) and section 7 (the eleven ordered checks) before writing code."
          },
          "zh": {
            "title": "先读 wire 契约",
            "body": "RP 契约是四个机器可读 schema：signin-request（你生成的请求）、signin-message（被签名的规范化文本）、signin-response（钱包返回的响应）与 login-error（你必须处理的稳定错误字符串）。\n\n文字口径的权威是登录规范，随 specs tarball 一起分发。写代码之前先读第 0 节（诚实边界）与第 7 节（11 步有序校验）。"
          },
          "links": [
            {
              "href": "/schemas/index.json",
              "kind": "internal",
              "en": {
                "label": "Schemas index"
              },
              "zh": {
                "label": "Schema 索引"
              }
            },
            {
              "href": "/download/index.json",
              "kind": "internal",
              "en": {
                "label": "Downloads index"
              },
              "zh": {
                "label": "下载索引"
              }
            }
          ]
        },
        {
          "id": "integrate-7",
          "status": "offline-testable",
          "refs": [
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Run the offline end-to-end demo (real signatures, no chain)",
            "body": "The fixture uses a fixed public test seed to derive a Dilithium-5 key pair and signs the canonical message for real. It exercises request, approve, verify and session with no network, no chain and no testnet.\n\nThree runnable paths: the Node flow (examples/node/offline-demo.mjs from the tarball), the browser flow against the SDK's browser bundle, and a server-side example that shows verifySignInResponse plus an HTTP resolver, which fails closed with endpoint_unavailable when no public endpoint exists.\n\nThe demo does not check chain state, because a fixture resolver replaces canonical resolve, so it cannot prove that a DID exists on chain. Its test key must never be used for a real identity."
          },
          "zh": {
            "title": "跑离线端到端 demo（真签名、无链）",
            "body": "测试向量用一个固定的公开测试种子派生 Dilithium-5 密钥对，并对规范化消息做真实签名。它在没有网络、没有链、没有测试网的情况下演练请求、批准、验签与会话。\n\n三条可跑路径：Node 全流程（tarball 里的 examples/node/offline-demo.mjs）、对照 SDK 浏览器 bundle 的浏览器全流程，以及一个服务端示例，展示 verifySignInResponse 加 HTTP resolver——在没有公网 endpoint 时以 endpoint_unavailable 失败关闭。\n\ndemo 不校验链上状态（用 fixture resolver 代替 canonical resolve），所以它不能证明某个 DID 在链上存在。它的测试密钥绝不能用于任何真实身份。"
          },
          "links": [
            {
              "href": "/download/",
              "kind": "internal",
              "en": {
                "label": "Downloads"
              },
              "zh": {
                "label": "下载"
              }
            },
            {
              "href": "/download/index.json",
              "kind": "internal",
              "en": {
                "label": "Downloads index"
              },
              "zh": {
                "label": "下载索引"
              }
            }
          ]
        },
        {
          "id": "integrate-8",
          "status": "offline-testable",
          "refs": [
            "rpIntegration",
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Verify responses in eleven ordered steps, fail-closed",
            "body": "Verify in order and reject on the first failure: parse the response; compare nonce and domain with your stored request; rebuild the canonical message and compare byte-for-byte; check expiry; consume the nonce once; check the chain id; resolve the DID on the server; confirm the verification method is in authentication; allow only the Dilithium-5 proof type; verify the signature; then create the session.\n\nResolve must be server-side, because the agent API returns no CORS headers and a browser cannot call it. There is no built-in rate limit and no Retry-After, so cache with a 5 to 15 second TTL, use single-flight, back off on failure, and do not batch. Consume the nonce before verifying the signature.\n\nThe bridge proof type AididP384BridgeSignature2025 is demo-only; reject it unless you explicitly opt in, and label it non-quantum-safe if you do."
          },
          "zh": {
            "title": "按 11 步有序校验，全程 fail-closed",
            "body": "按顺序校验，任一步失败即拒绝：解析响应；把 nonce 与 domain 同你自己保存的请求比对；重建规范化消息并逐字节比较；检查是否过期；单次消费 nonce；检查 chain id；在服务端 resolve DID；确认验证方法在 authentication 列表中；只允许 Dilithium-5 proof 类型；验签；最后建立会话。\n\nresolve 必须由服务端调用，因为 agent API 不返回 CORS 头，浏览器无法直连。链侧没有内置限流、也不承诺 Retry-After，所以要做 5 到 15 秒的短 TTL 缓存、单飞防击穿、失败退避，且不要批量解析。nonce 必须在验签之前消费。\n\nbridge proof 类型 AididP384BridgeSignature2025 仅用于演示；除非显式选择接受，否则一律拒绝；即便接受，也必须标注它不抗量子。"
          },
          "links": [
            {
              "href": "/schemas/login/login-error.schema.json",
              "kind": "internal",
              "en": {
                "label": "Login error codes"
              },
              "zh": {
                "label": "登录错误码"
              }
            },
            {
              "href": "/schemas/login/signin-message.schema.json",
              "kind": "internal",
              "en": {
                "label": "Canonical message schema"
              },
              "zh": {
                "label": "规范化消息 schema"
              }
            }
          ]
        },
        {
          "id": "integrate-9",
          "status": "offline-testable",
          "refs": [
            "rpIntegration",
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Go-live checklist: do not ship before every line is true",
            "body": "Before production, all of these must hold:\n\n1. The chain-side resolve endpoint is authorised. It is not today (didLoginProduction), so every real verification fails closed with endpoint_unavailable.\n2. The signing user actually holds a DID. Most users cannot create one today, because there is no end-user path to obtain MSG (userCreatesDid).\n3. Your nonce store is shared across instances and consumes a nonce atomically; a per-instance memory store will accept replays.\n4. Resolve runs server-side with a 5 to 15 second TTL cache and single-flight; the API has no CORS and no built-in rate limit.\n5. You handle every stable error from login-error (did_not_found, did_deactivated, nonce_replayed, endpoint_unavailable, and the rest) and never fall back to an anonymous session.\n6. Registering in the directory is optional and is only a declaration that you control your origin. It is not a security endorsement and it is not required to log in.\n\nUntil items 1 and 2 are true, ship nothing that claims to be a working sign-in."
          },
          "zh": {
            "title": "上线前清单：每一条都为真之前不要发布",
            "body": "上生产之前，下列每一条都必须成立：\n\n1. 链侧 resolve endpoint 已授权。今天没有（didLoginProduction），所以每一次真实校验都会以 endpoint_unavailable 失败关闭。\n2. 签名的用户确实持有 DID。今天大多数用户创建不了 DID，因为普通用户没有获取 MSG 的路径（userCreatesDid）。\n3. 你的 nonce store 跨实例共享，并且原子地消费 nonce；每实例内存存储会接受重放。\n4. resolve 在服务端运行，带 5 到 15 秒 TTL 缓存与单飞；该 API 没有 CORS、也没有内置限流。\n5. 你处理 login-error 的每一个稳定错误（did_not_found、did_deactivated、nonce_replayed、endpoint_unavailable 等），并且绝不降级为匿名会话。\n6. 登记进目录是可选的，它只是一句「你控制该 origin」的声明。它不是安全背书，也不是登录的前置条件。\n\n在第 1、2 条为真之前，不要发布任何声称「登录可用」的东西。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            },
            {
              "href": "/schemas/login/login-error.schema.json",
              "kind": "internal",
              "en": {
                "label": "Login error codes"
              },
              "zh": {
                "label": "登录错误码"
              }
            }
          ]
        },
        {
          "id": "integrate-10",
          "status": "blocked",
          "refs": [
            "didLoginProduction"
          ],
          "en": {
            "title": "Production login is blocked (chain-side resolve not authorised)",
            "body": "You can finish and self-test the integration, but you cannot go live: GET {agent_api}/agent/v1/aidid/resolve is not authorised on the public network yet, so an RP cannot resolve any DID in production. This is the unavailable capability didLoginProduction in status.json.\n\nDo not describe AIDID login as production-ready, and do not ship a flow that silently falls back to an anonymous session when resolution fails. The correct behaviour is fail-closed with endpoint_unavailable."
          },
          "zh": {
            "title": "生产登录被阻塞（链侧 resolve 未授权）",
            "body": "你可以把集成写完并自测，但无法上线：GET {agent_api}/agent/v1/aidid/resolve 在公网尚未获得授权，因此 RP 在生产环境解析不了任何 DID。这就是 status.json 里不可用的能力 didLoginProduction。\n\n不要把 AIDID 登录描述为已可用于生产；也不要上线一种在 resolve 失败时静默降级为匿名会话的流程。正确行为是以 endpoint_unavailable 失败关闭。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "integrate-11",
          "status": "available",
          "refs": [],
          "en": {
            "title": "List your site in the registry (proof of origin, nothing more)",
            "body": "Once your integration is written you can publish /.well-known/aidid-rp.txt on your origin and open a pull request against the public registry repository. A GitHub Action fetches your file and compares it with the registered challenge; a pull request that changes only the registry file and matches is merged automatically — including a pull request opened from your own fork, with no invitation or collaborator access needed. Registration is free and explicitly transitional until the on-chain registry exists.\n\nThere is no rebuild to wait for: the directory page reads a first-party cache that is refreshed on demand (a cache older than ten minutes triggers a refresh, rate-limited to once every ten minutes), and falls back to the snapshot built into the deployment when that read fails — so a merged entry normally shows up on your next visit or two.\n\nBeing listed proves only that you control the origin and declare the integration. It is not a security or reputation endorsement by AIDID."
          },
          "zh": {
            "title": "把你的站点登记进目录（只证明域名控制权，仅此而已）",
            "body": "集成写好后，你可以在自己的 origin 上发布 /.well-known/aidid-rp.txt，并向公开 registry 仓库提交一个 pull request。GitHub Action 会抓取你的文件并与登记的 challenge 比对；当一个 PR 只改这一个文件且 challenge 匹配时，会被自动合并——从你自己的 fork 提的 PR 也一样，不需要邀请，也不需要协作者权限。当前登记免费，并明确标注为过渡方案，直到链上 registry 可用。\n\n不需要等重建：目录页读取第一方缓存，该缓存按需刷新（缓存超过十分钟时下一次访问会触发刷新，同一缓存至少间隔十分钟），读取失败时回退到本次部署内置的快照——因此合并后的条目通常在一两次访问内出现。\n\n被列入目录只证明你控制该 origin，并声明接入了 AIDID；不构成 AIDID 对其安全性或信誉的背书。"
          },
          "links": [
            {
              "href": "/registry/",
              "kind": "internal",
              "en": {
                "label": "Third-party registry"
              },
              "zh": {
                "label": "第三方接入目录"
              }
            }
          ]
        }
      ]
    },
    {
      "slug": "pay",
      "order": 4,
      "status": "offline-testable",
      "refs": [
        "rpIntegration",
        "offlineEndToEnd"
      ],
      "en": {
        "title": "Use DID login for payments (payout address)",
        "summary": "After a verified login you can resolve the DID on your server and read the payout address from the canonical record. You can write and self-test that logic offline today; reading a real address in production is blocked with the rest of production login. pay:address is a declaration the user sees and approves, not a permission."
      },
      "zh": {
        "title": "用 DID 登录打通收付款（收款地址）",
        "summary": "登录通过校验后，你可以在服务端 resolve 该 DID，并从 canonical 记录里读出收款地址。这段逻辑今天就能写完并离线自测；在生产环境读取真实地址则随生产登录一起被阻塞。pay:address 是一句用户在批准页看到并同意的声明，不是权限。"
      },
      "steps": [
        {
          "id": "pay-1",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Where the payout address actually comes from",
            "body": "A login response carries a did, a canonical message and a signature — no account address. To get the address you resolve the DID and read the canonical record on the server.\n\nThe record field owner is the 44-character MSG account address (msg + 40 hex + one checksum digit). The document's controller field is different: it lists controller DIDs, not addresses, so never send MSG to a did:msg:... string. Check deactivated before you use the address, and do not cache it longer than your resolve TTL (5 to 15 seconds); a name-to-address mapping can change between DID updates."
          },
          "zh": {
            "title": "收款地址究竟从哪里来",
            "body": "一次登录响应只带 did、规范化消息与签名——没有账户地址。要拿到地址，你必须在服务端 resolve 该 DID 并读取 canonical 记录。\n\n记录里的 owner 就是那个 44 字符的 MSG 账户地址（msg + 40 位十六进制 + 1 位校验）。文档里的 controller 字段不一样：它列的是 controller DID，不是地址，所以绝不要把 MSG 转给 did:msg:... 字符串。使用地址之前先检查 deactivated，并且缓存不要超过你的 resolve TTL（5 到 15 秒）；DID 更新之间，名字到地址的映射是可能变化的。"
          },
          "code": {
            "language": "js",
            "content": "// After a verified login you have `did` (result.did) — never an address.\nimport { createAgentApiResolver, createCachedResolver } from '@aidid/login';\n\nconst resolve = createCachedResolver({\n  resolver: createAgentApiResolver({ agentApiEndpoint: process.env.AGENT_API }),\n});\n\nconst record = await resolve.resolve(result.did); // canonical record, 10 s TTL by default\nif (record.deactivated) throw new Error('did_deactivated'); // refuse a retired identity first\n\nconst payoutAddress = record.owner; // 'msg' + 40 hex + 1 checksum = 44 chars, an ACCOUNT address\n// record.document.controller is a list of controller DIDs — NOT a payment address."
          },
          "links": [
            {
              "href": "/schemas/did/canonical-resolve.schema.json",
              "kind": "internal",
              "en": {
                "label": "Canonical resolve schema"
              },
              "zh": {
                "label": "canonical resolve schema"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "pay-2",
          "status": "available",
          "refs": [],
          "en": {
            "title": "resources: ['pay:address'] is a declaration, not a grant",
            "body": "Adding pay:address to resources puts a statement into the canonical message that the user signs, and the wallet shows it on the approval page: \"this site will obtain your payout address\". It is a declaration, and the protocol says so explicitly.\n\nIt is not a permission, it grants no on-chain right, and it does not mean the user authorised a payment. The response still contains no address, so you must resolve the DID yourself. If you do not request pay:address, do not derive, display or use the address for payment at all."
          },
          "zh": {
            "title": "resources: ['pay:address'] 是一句声明，不是授权",
            "body": "把 pay:address 加进 resources，只是把一句话放进用户签名的规范化消息里，钱包会在批准页显示它：「该站点将获得你的收款地址」。这是一句声明，规范也明确如此定性。\n\n它不是权限、不授予任何链上权利，也不代表用户已完成支付授权。响应里仍然不会有地址，所以你必须自己 resolve 该 DID。如果你没有请求 pay:address，就完全不要推导、展示或把它用于收款。"
          },
          "code": {
            "language": "js",
            "content": "import { createSignInRequest } from '@aidid/login';\n\nconst request = createSignInRequest({\n  domain: 'shop.example',\n  chainId: 'msg-chain-1',\n  uri: 'https://shop.example/aidid/callback',\n  statement: 'Sign in to shop.example.',\n  // A declaration the user sees and approves. It is NOT a permission and the\n  // response carries no address, so you still resolve the DID yourself.\n  resources: ['did:auth', 'pay:address'],\n});\n// result of the callback: { ok: true, did: 'did:msg:...' } — no address field, by design."
          },
          "links": [
            {
              "href": "/schemas/login/signin-request.schema.json",
              "kind": "internal",
              "en": {
                "label": "Sign-in request schema"
              },
              "zh": {
                "label": "登录请求 schema"
              }
            },
            {
              "href": "/authorize/",
              "kind": "internal",
              "en": {
                "label": "Wallet entry point"
              },
              "zh": {
                "label": "钱包入口"
              }
            }
          ]
        },
        {
          "id": "pay-3",
          "status": "offline-testable",
          "refs": [
            "offlineEndToEnd"
          ],
          "en": {
            "title": "Binding the address to the DID: keys, devices and sub-DIDs",
            "body": "The address belongs to the DID, so the questions that matter are about the DID and where it lives.\n\nNew device or restored backup: a restore brings back the same DID document, so the same owner address still applies. A newly created DID gets a new address, so never hard-code an address against a user account.\n\nKey rotation or a DID update: re-resolve before every payout; the record carries a monotonic version and an updated block time, and the owner can change across updates.\n\nSub-DIDs: a sub-DID is its own did:msg with its own resolve record. Pay the sub-DID's owner only when that sub-DID is the identity you accepted, and remember that a sub-DID grant has no on-chain revocation channel today — a revoked grant may still resolve until the document is updated.\n\nDeactivated DIDs: refuse before reading an address (did_deactivated), and keep your own deny list, because the chain has no blacklist."
          },
          "zh": {
            "title": "地址与 DID 的绑定：换设备、换密钥、子 DID",
            "body": "地址属于 DID，所以真正要紧的问题是「DID 本身，以及它此刻在哪里」。\n\n换设备或恢复备份：恢复带回的是同一份 DID 文档，所以同一个 owner 地址依然适用。新建的 DID 会得到新地址，所以绝不要把地址硬编码到用户账号上。\n\n密钥轮换或 DID 更新：每次打款前都重新 resolve；记录带有单调递增的 version 与 updated 区块时间，owner 可能随更新变化。\n\n子 DID：子 DID 是它自己的 did:msg，有自己的 resolve 记录。只有当这个子 DID 就是你接受的身份时，才把款项付给它的 owner；同时记住，子 DID 授权今天没有链上吊销通道——在文档更新之前，已撤销的授权仍可能 resolve 出来。\n\n已停用的 DID：先用 did_deactivated 拒绝，再读地址；并且维护你自己的 deny list，因为链上没有黑名单。"
          },
          "links": [
            {
              "href": "/schemas/did/canonical-resolve.schema.json",
              "kind": "internal",
              "en": {
                "label": "Canonical resolve schema"
              },
              "zh": {
                "label": "canonical resolve schema"
              }
            },
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        },
        {
          "id": "pay-4",
          "status": "blocked",
          "refs": [
            "didLoginProduction"
          ],
          "en": {
            "title": "Today: offline self-test yes, production address no",
            "body": "Everything above can be written and self-tested offline today, with a fixture resolver in place of canonical resolve: the request, the approval declaration, the callback, the resolve step and the address read.\n\nWhat you cannot do today is read a real address in production. Resolve is not authorised on the public network, so the address step fails closed with endpoint_unavailable for every DID. Do not ship a checkout or payout flow that depends on it; the honest status is fail-closed until the chain side authorises the endpoint and ordinary users can actually hold a DID."
          },
          "zh": {
            "title": "今天：可以离线自测，不能在生产取地址",
            "body": "以上这些今天都能写完并离线自测，用 fixture resolver 代替 canonical resolve：请求、批准页声明、回调、resolve 这一步以及读取地址。\n\n今天做不到的是在生产环境读取真实地址。公网上 resolve 未获授权，所以对任何 DID，取地址这一步都会以 endpoint_unavailable 失败关闭。不要上线任何依赖它的结账或打款流程；在链侧授权 endpoint、且普通用户确实能持有 DID 之前，诚实的状态就是失败关闭。"
          },
          "links": [
            {
              "href": "/status.json",
              "kind": "internal",
              "en": {
                "label": "Capability status"
              },
              "zh": {
                "label": "能力状态"
              }
            }
          ]
        }
      ]
    },
    {
      "slug": "backup",
      "order": 5,
      "status": "available",
      "refs": [],
      "en": {
        "title": "Back up and restore your local identity",
        "summary": "Backup and restore work today and are entirely local: the payload is encrypted with a passphrase that never leaves your device. There is no server-side recovery, so the backup and its passphrase are the only way to move an identity to another device."
      },
      "zh": {
        "title": "备份与恢复本机身份",
        "summary": "备份与恢复今天就能用，而且完全在本机：载荷用口令加密，口令永远不会离开你的设备。没有服务端恢复通道，所以备份文件（和口令）是把身份迁到另一台设备的唯一途径。"
      },
      "steps": [
        {
          "id": "backup-1",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Why you must back up: there is no server-side recovery",
            "body": "Your signing key is stored passphrase-encrypted in this browser. AIDID has no backend and holds no copy, so there is no recovery link and no support reset: if you forget the passphrase you must create a new identity.\n\nThat is why the backup is the migration path. Keep the exported carrier and its passphrase somewhere safe. The passphrase is not stored inside the carrier, so losing it means the backup cannot be decrypted by anyone."
          },
          "zh": {
            "title": "为什么必须备份：没有服务端恢复",
            "body": "你的签名密钥以口令加密的形式存储在这个浏览器里。AIDID 没有后端、也不持有任何副本，所以没有找回链接、也没有客服重置：忘记口令就只能新建一个身份。\n\n正因如此，备份才是迁移途径。请把导出的载体和口令一起保存在安全的地方。口令不会存进载体里，所以一旦丢失，任何人都无法解开这份备份。"
          },
          "links": [
            {
              "href": "/",
              "kind": "internal",
              "en": {
                "label": "Home (backup panel)"
              },
              "zh": {
                "label": "首页（备份面板）"
              }
            }
          ]
        },
        {
          "id": "backup-2",
          "status": "available",
          "refs": [],
          "en": {
            "title": "What the backup contains, and what it does not",
            "body": "Every carrier wraps the same payload: your DID document, your Dilithium-5 key material, your credentials, your sub-DID grants and your sub-DID usage receipts.\n\nIt deliberately excludes your local visited-sites footprint and the backup UI preference. Nothing identity-related lives in IndexedDB, sessionStorage or the service-worker cache."
          },
          "zh": {
            "title": "备份包含什么、不包含什么",
            "body": "所有载体包的都是同一个载荷：你的 DID 文档、Dilithium-5 密钥材料、凭证、子 DID 授权记录，以及子 DID 用量回执。\n\n它故意排除本机「登录过的站点」足迹与备份界面偏好。任何与身份有关的数据都不会落在 IndexedDB、sessionStorage 或 Service Worker 缓存里。"
          },
          "links": [
            {
              "href": "/",
              "kind": "internal",
              "en": {
                "label": "Home (backup panel)"
              },
              "zh": {
                "label": "首页（备份面板）"
              }
            },
            {
              "href": "/identities/",
              "kind": "internal",
              "en": {
                "label": "Sub-DID records"
              },
              "zh": {
                "label": "子 DID 记录"
              }
            }
          ]
        },
        {
          "id": "backup-3",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Choose a carrier",
            "body": "Open Backup / Restore on the home page, enter a passphrase of at least 8 characters, then export. Carriers: a CKX text string, a .ckx file, a PNG image, a video carrier, or the legacy .encrypted.json. Restore accepts paste, a file picker and drag-and-drop.\n\nA PNG only survives a byte-for-byte copy: a screenshot, or any chat-app recompression or resize, destroys the hidden payload. A video carrier is appended to a source video you choose (keep it under 64 MB) and the result still plays normally. CKX carriers are optional; the JSON path works on its own."
          },
          "zh": {
            "title": "选择一种载体",
            "body": "在首页打开「备份与恢复」，输入至少 8 位的口令，然后导出。可选载体：CKX 文本、.ckx 文件、PNG 图片、视频载体，或旧的 .encrypted.json。恢复支持粘贴、文件选择与拖拽。\n\nPNG 只有逐字节复制才有效：截图、或聊天软件的任何二次压缩与缩放，都会破坏隐藏的载荷。视频载体尾挂在你选择的一段源视频之后（控制在 64 MB 以内），输出文件仍可正常播放。CKX 载体是可选的；JSON 路径可以独立使用。"
          },
          "links": [
            {
              "href": "/",
              "kind": "internal",
              "en": {
                "label": "Home (backup panel)"
              },
              "zh": {
                "label": "首页（备份面板）"
              }
            }
          ]
        },
        {
          "id": "backup-4",
          "status": "available",
          "refs": [],
          "en": {
            "title": "Restore semantics: an old backup never wipes your sub-DID records",
            "body": "Sub-DID grants and usage receipts are optional members of the payload, and their import semantics are deliberately asymmetric.\n\nIf the member is missing or is not an array, restore leaves your existing local records untouched: an old backup that predates sub-DIDs must not silently delete the only copy of a grant. If the member is an array, including an empty array, it is treated as authoritative and replaces the local table wholesale. Malformed entries are filtered by the same validators the storage layer uses.\n\nRestoring a backup that contains a stale chain mirror can overwrite a newer local cache; the chain copy can always be re-fetched."
          },
          "zh": {
            "title": "恢复语义：旧备份绝不会清空你的子 DID 记录",
            "body": "子 DID 授权记录与用量回执是载荷中的可选成员，它们的导入语义是刻意不对称的。\n\n如果该成员缺失或不是数组，恢复不会触碰本机已有记录：一份早于子 DID 功能的旧备份，绝不能静默删掉某条授权的唯一副本。如果该成员是数组（包括空数组），则视为权威，整表替换本机记录。畸形成员会按存储层同一套校验器被过滤掉。\n\n恢复一份包含过时链上镜像的备份，可能覆盖更新的本地缓存；链上副本随时可以重新取回。"
          },
          "links": [
            {
              "href": "/",
              "kind": "internal",
              "en": {
                "label": "Home (backup panel)"
              },
              "zh": {
                "label": "首页（备份面板）"
              }
            },
            {
              "href": "/identities/",
              "kind": "internal",
              "en": {
                "label": "Sub-DID records"
              },
              "zh": {
                "label": "子 DID 记录"
              }
            }
          ]
        },
        {
          "id": "backup-5",
          "status": "available",
          "refs": [],
          "en": {
            "title": "If a restore fails, read the message",
            "body": "Restore distinguishes four cases before decrypting: not a valid AIDID backup (empty or not a carrier at all), carrier damaged or truncated, unsupported CKX format version, and wrong passphrase or corrupted backup. A structure check runs before the expensive key derivation, so junk input fails fast and nothing is written.\n\nA failed restore is fail-closed: your current local state is left unchanged."
          },
          "zh": {
            "title": "恢复失败时，先看提示文案",
            "body": "恢复在解密之前会区分四种情况：这不是有效的 AIDID 备份（空输入或根本不是载体）、CKX 载体已损坏或被截断、不支持的 CKX 格式版本、以及口令错误或备份已损坏。结构自检在昂贵的密钥派生之前运行，所以垃圾输入会快速失败，且不会写入任何东西。\n\n恢复失败是 fail-closed 的：本机现有状态保持不变。"
          },
          "links": [
            {
              "href": "/",
              "kind": "internal",
              "en": {
                "label": "Home (backup panel)"
              },
              "zh": {
                "label": "首页（备份面板）"
              }
            }
          ]
        }
      ]
    }
  ]
}
