{"_id":"@adi291/mfa-totp-client","name":"@adi291/mfa-totp-client","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@adi291/mfa-totp-client","version":"1.0.0","description":"原生 Web Components 封装的 TOTP 双因素认证组件库。提供 <mfa-totp> 验证组件与 <mfa-totp-bind> 首次绑定组件。","license":"Apache-2.0","type":"module","keywords":["web-components","mfa","totp","2fa","two-factor"],"sideEffects":["**/*.css"],"main":"./dist/mfa-totp-client.js","module":"./dist/mfa-totp-client.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/mfa-totp-client.js"},"./mfa-totp":{"types":"./dist/mfa-totp/index.d.ts","import":"./dist/mfa-totp.js"},"./mfa-totp-bind":{"types":"./dist/mfa-totp-bind/index.d.ts","import":"./dist/mfa-totp-bind.js"},"./helpers/otpauth":{"types":"./dist/helpers/otpauth.d.ts","import":"./dist/helpers/otpauth.js"},"./helpers/qrcode":{"types":"./dist/helpers/qrcode.d.ts","import":"./dist/helpers/qrcode.js"},"./i18n":{"types":"./dist/i18n/index.d.ts","import":"./dist/i18n/index.js"},"./package.json":"./package.json"},"peerDependencies":{"qrcode":"^1.5.0"},"peerDependenciesMeta":{"qrcode":{"optional":true}},"devDependencies":{"@types/node":"^24.13.3","@types/qrcode":"^1.5.5","typescript":"^6.0.3","vite":"^7.3.6","vite-plugin-dts":"^4.5.4"},"scripts":{"dev":"vite playground --config vite.playground.config.ts","build":"node build-lib.cjs","build:playground":"tsc -p tsconfig.playground.json --noEmit && vite build --config vite.playground.config.ts","typecheck":"tsc --noEmit","typecheck:playground":"tsc -p tsconfig.playground.json --noEmit","preview":"vite preview --config vite.playground.config.ts"},"_nodeVersion":"24.18.0","_id":"@adi291/mfa-totp-client@1.0.0","dist":{"integrity":"sha512-sWR1/NGYrwEoMpdZFjUwKv9iWafwB1o2y+jN7EmxU3LFVrJfObTe0TIs/47/PFT/K98Qk3OjjLQnSd0kZQ7gaA==","shasum":"f7d8f4d0073c4c170b167f01179ea766208b1f90","tarball":"https://registry.npmjs.org/@adi291/mfa-totp-client/-/mfa-totp-client-1.0.0.tgz","fileCount":49,"unpackedSize":218337,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCfSRmyJYL4/QuUb8o8UA4gMTSB2WGZIFxN25l0gM1J0QIgCjC/hwhhhfVs/UdZXlJacrvMdW8Fq1nPyUBAY8vEKzA="}]},"_npmUser":{"name":"adi291","email":"adix291@163.com"},"directories":{},"maintainers":[{"name":"adi291","email":"adix291@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/mfa-totp-client_1.0.0_1786203561962_0.4575013279686626"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-08T15:39:21.822Z","1.0.0":"2026-08-08T15:39:22.117Z","modified":"2026-08-08T15:39:22.329Z"},"maintainers":[{"name":"adi291","email":"adix291@163.com"}],"description":"原生 Web Components 封装的 TOTP 双因素认证组件库。提供 <mfa-totp> 验证组件与 <mfa-totp-bind> 首次绑定组件。","keywords":["web-components","mfa","totp","2fa","two-factor"],"license":"Apache-2.0","readme":"# @adi291/mfa-totp-client\r\n\r\n> 原生 Web Components 封装的 TOTP 双因素认证组件库。\r\n\r\n提供两个零依赖、易嵌入、可主题化的自定义元素：\r\n\r\n- **`<mfa-totp>`** —— 验证码输入与验证组件（登录后的二次校验、敏感操作前的拦截）\r\n- **`<mfa-totp-bind>`** —— 首次绑定 TOTP 组件（展示二维码、密钥、确认流程）\r\n\r\n## 目录\r\n\r\n- [特性](#特性)\r\n- [安装](#安装)\r\n- [快速上手](#快速上手)\r\n  - [1. 验证场景 `<mfa-totp>`](#1-验证场景-mfa-totp)\r\n  - [2. 绑定场景 `<mfa-totp-bind>`](#2-绑定场景-mfa-totp-bind)\r\n- [三种模式](#三种模式)\r\n  - [`inline` 内联](#inline-内联)\r\n  - [`modal` 弹窗](#modal-弹窗)\r\n  - [`page` 全屏](#page-全屏)\r\n- [`<mfa-totp>` API](#mfa-totp-api)\r\n  - [属性](#属性)\r\n  - [JS 属性（Property）](#js-属性property)\r\n  - [公共方法](#公共方法)\r\n  - [事件](#事件)\r\n- [`<mfa-totp-bind>` API](#mfa-totp-bind-api)\r\n  - [属性](#属性-1)\r\n  - [JS 属性（Property）](#js-属性property-1)\r\n  - [事件](#事件-1)\r\n- [主题与样式定制](#主题与样式定制)\r\n  - [CSS 变量](#css-变量)\r\n  - [CSS Parts](#css-parts)\r\n- [国际化 i18n](#国际化-i18n)\r\n- [Helpers](#helpers)\r\n- [工作原理](#工作原理)\r\n- [本地开发](#本地开发)\r\n- [构建与发布](#构建与发布)\r\n- [License](#license)\r\n\r\n## 特性\r\n\r\n- **纯原生 Web Components**：无 React、Vue 等框架依赖；用 `<script type=\"module\">` 引入即可在任何页面工作。\r\n- **职责单一**：组件只负责**收集**用户输入并**提交**给后端，**不生成、不存储 TOTP**。6 位验证码由用户的身份验证器 APP 计算。\r\n- **三种使用模式**：`inline` 内联、`modal` 弹窗、`page` 全屏，覆盖登录后 MFA、敏感操作二次校验等典型场景。\r\n- **可选 API 自动调用**：传 `api-url` 组件自动 `fetch` 验证；不传则只派发 `submit` 事件，由宿主自行处理。\r\n- **支持恢复码**：与 Gitee 等主流站点一致，可切换 `TOTP ↔ Recovery Code` 两种验证方式。\r\n- **首次绑定流程齐全**：QR 码 / 密钥展示 / 复制密钥 / 输入确认一站式。\r\n- **可主题化**：暴露一整套 CSS 变量与 `::part()` 选择器，定制皮肤无需修改源文件。\r\n- **零运行时硬依赖**：`qrcode` 是可选 peerDependency，仅 `<mfa-totp-bind>` 使用，按需安装。\r\n- **TypeScript 严格模式 + 完整类型导出**：所有事件 `detail`、属性、可选值都有类型。\r\n- **Shadow DOM 隔离样式**：组件内部样式不会污染宿主页面，宿主页面也进不去组件内部。\r\n\r\n## 安装\r\n\r\n```bash\r\npnpm add @adi291/mfa-totp-client\r\n# 只用 <mfa-totp> 不需要 qrcode\r\npnpm add qrcode          # 仅 <mfa-totp-bind> 需要\r\n```\r\n\r\n## 快速上手\r\n\r\n### 1. 验证场景 `<mfa-totp>`\r\n\r\n`<mfa-totp>` 接收 6 位验证码，**不**做任何本地计算，只把它交给后端校验或派发事件。\r\n\r\n**方式 A：组件自调用 API（推荐 80% 场景）**\r\n\r\n后端只需一个接受 `{ code, method }` JSON 的接口：\r\n\r\n```html\r\n<script type=\"module\">\r\n  import \"@adi291/mfa-totp-client/mfa-totp\";\r\n</script>\r\n\r\n<mfa-totp\r\n  mode=\"modal\"\r\n  api-url=\"/api/mfa/verify\"\r\n  api-method=\"POST\"\r\n  language=\"zh-CN\"\r\n  theme=\"gitee\"\r\n></mfa-totp>\r\n\r\n<script>\r\n  const el = document.querySelector(\"mfa-totp\");\r\n\r\n  // 想弹窗时显示\r\n  el.show();\r\n\r\n  // 成功后通常要跳页 / 跳转，这里监听 ready → close\r\n  el.addEventListener(\"success\", (e) => {\r\n    console.log(\"verify ok:\", e.detail.data);\r\n    setTimeout(() => el.hide(), 800);\r\n  });\r\n  el.addEventListener(\"error\", (e) => {\r\n    console.warn(\"verify failed:\", e.detail.message);\r\n  });\r\n  el.addEventListener(\"cancel\", () => el.hide());\r\n</script>\r\n```\r\n\r\n后端响应格式约定（满足任一形式都接受）：\r\n\r\n```json\r\n// 成功\r\n{ \"ok\": true, \"data\": { \"userId\": \"u_123\" } }\r\n\r\n// 失败\r\n{ \"ok\": false, \"message\": \"验证码已过期\", \"code\": \"TOTP_EXPIRED\" }\r\n```\r\n\r\n也可以直接返回非 `{ok:false}` 的 2xx 响应，会被视为成功。\r\n\r\n**方式 B：不带 API，宿主自行处理 submit 事件**\r\n\r\n适合「提交后还有额外逻辑」的场景（例如先校验凭证再二次校验）：\r\n\r\n```html\r\n<mfa-totp mode=\"inline\" language=\"zh-CN\"></mfa-totp>\r\n\r\n<script type=\"module\">\r\n  import \"@adi291/mfa-totp-client/mfa-totp\";\r\n\r\n  document.querySelector(\"mfa-totp\").addEventListener(\"submit\", async (e) => {\r\n    const { code, method } = e.detail;\r\n    try {\r\n      const r = await fetch(\"/api/mfa/verify\", {\r\n        method: \"POST\",\r\n        headers: { \"Content-Type\": \"application/json\" },\r\n        body: JSON.stringify({ code, method }),\r\n      });\r\n      if (!r.ok) throw new Error(\"invalid\");\r\n      location.href = \"/home\";\r\n    } catch (err) {\r\n      // 主动设置错误，组件会抖动提示\r\n      document.querySelector(\"mfa-totp\").setError(\"验证码不正确\");\r\n    }\r\n  });\r\n</script>\r\n```\r\n\r\n### 2. 绑定场景 `<mfa-totp-bind>`\r\n\r\n```html\r\n<script type=\"module\">\r\n  import \"@adi291/mfa-totp-client/mfa-totp-bind\"; // 自动包含 <mfa-totp>\r\n  import QRCode from \"qrcode\";                    // 用户预装，本库不强依赖\r\n</script>\r\n\r\n<mfa-totp-bind\r\n  mode=\"page\"\r\n  secret=\"JBSWY3DPEHPK3PXP\"\r\n  account=\"user@example.com\"\r\n  issuer=\"MyApp\"\r\n  api-url=\"/api/mfa/bind\"\r\n  api-method=\"POST\"\r\n  language=\"zh-CN\"\r\n></mfa-totp-bind>\r\n\r\n<script>\r\n  const el = document.querySelector(\"mfa-totp-bind\");\r\n  el.addEventListener(\"bound\", (e) => {\r\n    console.log(\"bound ok:\", e.detail.data);\r\n    setTimeout(() => el.remove(), 1200);\r\n  });\r\n  el.addEventListener(\"bind-error\", (e) => {\r\n    console.warn(\"bind failed:\", e.detail.message);\r\n  });\r\n  el.addEventListener(\"cancel\", () => el.remove());\r\n</script>\r\n```\r\n\r\n`secret` 一般**从后端一次性下发**，不能写在前端常量里——它应该随用户登录态由服务端返回。\r\n\r\n## 三种模式\r\n\r\n通过 `mode` 属性切换：\r\n\r\n| Mode      | 适用场景                  | 视觉                                |\r\n|-----------|--------------------------|-------------------------------------|\r\n| `inline`  | 业务页面内二次校验        | 嵌入流程，无遮罩、无固定定位        |\r\n| `modal`   | 任意场景的临时验证        | 居中卡片 + 半透明遮罩，点遮罩关闭   |\r\n| `page`    | 登录后独立 MFA 验证页     | 铺满视口的灰底页面                  |\r\n\r\n### `inline` 内联\r\n\r\n```html\r\n<section style=\"padding: 24px;\">\r\n  <h2>敏感操作</h2>\r\n  <p>请输入验证码以确认删除操作：</p>\r\n  <mfa-totp mode=\"inline\" api-url=\"/api/mfa/verify\"></mfa-totp>\r\n</section>\r\n```\r\n\r\n### `modal` 弹窗\r\n\r\n```html\r\n<mfa-totp id=\"mfa\" mode=\"modal\" api-url=\"/api/mfa/verify\" hidden></mfa-totp>\r\n<button onclick=\"document.getElementById('mfa').show()\">开启二次验证</button>\r\n```\r\n\r\n- 宿主调用 `.show()` / `.hide()`（实际是设置/移除 `hidden` 属性）。\r\n- 点遮罩关闭（仅 `modal` 模式）；`page` 模式常驻。\r\n- ESC 关闭：`modal` 模式下默认不监听 ESC，由宿主按需扩展。\r\n\r\n### `page` 全屏\r\n\r\n```html\r\n<mfa-totp\r\n  mode=\"page\"\r\n  api-url=\"/api/mfa/verify\"\r\n  language=\"zh-CN\"\r\n></mfa-totp>\r\n```\r\n\r\n`page` 模式下 host 元素通过 `position: fixed; inset: 0; z-index: 100` 铺满视口，**避免被父容器的 `overflow` / `transform` 影响**。\r\n\r\n## `<mfa-totp>` API\r\n\r\n### 属性\r\n\r\n| 属性                    | 类型 / 取值                          | 默认     | 说明 |\r\n|-------------------------|--------------------------------------|----------|------|\r\n| `mode`                  | `\"inline\" \\| \"page\" \\| \"modal\"`      | `inline` | 视觉模式 |\r\n| `theme`                 | `\"gitee\" \\| \"minimal\"`               | `gitee`  | logo 样式 |\r\n| `language`              | `\"zh-CN\" \\| \"en-US\"`                 | `zh-CN`  | 国际化语言 |\r\n| `length`                | 数字（≥4）                           | `6`      | 验证码位数 |\r\n| `api-url`               | 字符串 \\| 删除表示不调用             | —        | 验证接口地址；不设置则只派发事件 |\r\n| `api-method`            | HTTP 方法字符串                       | `POST`   | 调用方法 |\r\n| `title`                 | 字符串                                | `i18n`   | 顶部标题 |\r\n| `subtitle`              | 字符串                                | `i18n`   | 副标题（留空则隐藏） |\r\n| `description`           | 字符串                                | `i18n`   | 描述提示框（留空则隐藏） |\r\n| `logo-src`              | 字符串                                | —        | 自定义 logo 图 URL |\r\n| `<slot name=\"logo\">`    | Light DOM 节点                       | —        | 用 slot 自定义 logo |\r\n| `show-recovery`         | `\"true\" \\| \"false\"` \\| 布尔属性       | `true`   | 是否显示「使用恢复代码」入口 |\r\n| `recovery-placeholder`  | 字符串                                | `i18n`   | 恢复码输入框 placeholder |\r\n| `show-cancel`           | `\"true\" \\| \"false\"` \\| 布尔属性       | `auto`   | 是否显示取消按钮；不设置时，`modal` / `inline` 默认显示，`page` 不显示 |\r\n| `show-submit`           | `\"true\" \\| \"false\"` \\| 布尔属性       | `auto`   | 是否显示提交按钮；不设置时，`auto-submit` 开启则隐藏 |\r\n| `auto-submit`           | `\"true\" \\| \"false\"` \\| 布尔属性       | `true`   | 输满自动提交（防抖 120ms） |\r\n| `auto-focus`            | `\"true\" \\| \"false\"` \\| 布尔属性       | `true`   | 挂载后自动聚焦到第一个输入框 |\r\n| `verify-on-enter`       | `\"true\" \\| \"false\"` \\| 布尔属性       | `true`   | 在输入框按 Enter 触发验证 |\r\n| `loading`               | 布尔属性                              | `false`  | 显示菊花，禁用按钮 |\r\n| `disabled`              | 布尔属性                              | `false`  | 全卡禁用，半透明 + 不可交互 |\r\n| `error-message`         | 字符串                                | —        | 错误文案（自动清空需调用 `clearError()`） |\r\n| `hidden`                | 布尔属性                              | `false`  | 隐藏整个组件（见下） |\r\n\r\n**关于 `hidden`**：组件内部对 Shadow DOM 内部每个带 `hidden` 属性的节点都显式声明了 `[hidden] { display: none !important }`，因为 UA 样式表里的 `[hidden]` 规则不会自动渗入 Shadow Root。\r\n\r\n### JS 属性（Property）\r\n\r\n```ts\r\nel.mode;            // → MFAMode\r\nel.theme;           // → MFATheme\r\nel.language;        // → Language\r\nel.length;          // → number\r\nel.apiUrl;          // → string | null\r\nel.apiMethod;       // → string\r\nel.title;           // → string\r\nel.subtitle;        // → string\r\nel.description;     // → string\r\nel.logoSrc;         // → string\r\nel.showRecovery;    // → boolean\r\nel.showCancel;      // → boolean\r\nel.showSubmit;      // → boolean\r\nel.autoSubmit;      // → boolean\r\nel.autoFocus;       // → boolean\r\nel.verifyOnEnter;   // → boolean\r\nel.disabled;        // → boolean\r\nel.loading;         // → boolean\r\nel.errorMessage;    // → string\r\nel.i18n;            // → I18nStrings（当前语言下的完整文案）\r\n```\r\n\r\n**设置一个属性（property）会自动同步到 attribute**（除了只读 getter）。\r\n\r\n### 公共方法\r\n\r\n| 方法                                                       | 说明                                |\r\n|------------------------------------------------------------|-------------------------------------|\r\n| `verify(): Promise<MfaVerifyResponse \\| undefined>`        | 手动触发验证，返回后端响应数据       |\r\n| `reset(): void`                                            | 清空所有输入、错误、成功状态        |\r\n| `show(): void`                                             | 移除 `hidden`，显示组件             |\r\n| `hide(): void`                                             | 设置 `hidden`，隐藏组件             |\r\n| `focus(): void`                                            | 聚焦到当前方法的第一个输入          |\r\n| `setError(message: string): void`                          | 设置错误并抖动（外部触发）          |\r\n| `clearError(): void`                                       | 清除错误                            |\r\n| `setLoading(loading: boolean): void`                       | 切换加载态                          |\r\n| `setMethod(method: \"totp\" \\| \"recovery\"): void`            | 切换 TOTP / 恢复码                  |\r\n| `shake(): void`                                            | 手动触发卡片抖动                    |\r\n\r\n### 事件\r\n\r\n所有事件都是 `bubbles: true, composed: true`，可以穿透 Shadow Root。\r\n\r\n| 事件             | `detail` 类型                                              | 触发时机 |\r\n|------------------|------------------------------------------------------------|----------|\r\n| `submit`         | `{ code: string; method: \"totp\" \\| \"recovery\" }`           | 用户完成输入（含自动 / 手动） |\r\n| `code-change`    | `{ code: string; method: \"totp\" \\| \"recovery\" }`           | 每次输入变化（用户首次操作后才触发） |\r\n| `success`        | `MfaSuccessDetail<T>`                                      | 后端校验通过或 `verify()` 解析到成功 |\r\n| `error`          | `{ message: string; code?: string\\|number; status?: number }` | 校验失败 / 网络异常 |\r\n| `change-method`  | `{ method: \"totp\" \\| \"recovery\" }`                         | 切换 TOTP / 恢复码 |\r\n| `cancel`         | `void`                                                     | 用户点击取消；modal / page 模式同时会 `hide()` |\r\n\r\n## `<mfa-totp-bind>` API\r\n\r\n### 属性\r\n\r\n| 属性                  | 类型 / 取值                       | 默认     | 说明 |\r\n|-----------------------|-----------------------------------|----------|------|\r\n| `mode`                | `\"inline\" \\| \"page\" \\| \"modal\"`   | `inline` | 视觉模式 |\r\n| `language`            | `\"zh-CN\" \\| \"en-US\"`              | `zh-CN`  | 国际化语言 |\r\n| `secret`              | base32 字符串                     | —        | **必填**，从后端获取的共享密钥 |\r\n| `account`             | 字符串                            | —        | 用户标识（邮箱、用户名）写入 otpauth label |\r\n| `issuer`              | 字符串                            | —        | 发行方（公司名）写入 otpauth label |\r\n| `digits`              | 数字                              | `6`      | 验证码位数（同步给内嵌 mfa-totp） |\r\n| `period`              | 数字                              | `30`     | TOTP 周期（仅影响 otpauth URL） |\r\n| `algorithm`           | `\"SHA-1\" \\| \"SHA-256\" \\| \"SHA-512\"` | `SHA-1` | 算法（仅影响 otpauth URL） |\r\n| `api-url`             | 字符串                            | —        | 确认接口；与 `<mfa-totp>` 同义 |\r\n| `api-method`          | HTTP 方法                         | `POST`   | 调用方法 |\r\n| `title`               | 字符串                            | `i18n`   | 顶部标题 |\r\n| `description`         | 字符串                            | `i18n`   | 描述 |\r\n| `verify-title`        | 字符串                            | —        | 覆盖内嵌 mfa-totp 的标题 |\r\n| `verify-description`  | 字符串                            | —        | 覆盖内嵌 mfa-totp 的描述 |\r\n| `qrcode-size`         | CSS 像素                          | `200`    | QR 码画布尺寸 |\r\n| `show-secret`         | `\"true\" \\| \"false\"`              | `true`   | 是否显示密钥明文 + 复制按钮 |\r\n| `show-cancel`         | `\"true\" \\| \"false\"`              | `false`  | 是否显示取消按钮 |\r\n| `auto-focus`          | `\"true\" \\| \"false\"`              | `true`   | 挂载后自动聚焦内嵌 mfa-totp |\r\n\r\n### JS 属性（Property）\r\n\r\n```ts\r\nel.language;       // → Language\r\nel.mode;           // → MFAMode\r\nel.secret;         // → string\r\nel.account;        // → string\r\nel.issuer;         // → string\r\nel.digits;         // → number\r\nel.period;         // → number\r\nel.algorithm;      // → TOTPAlgorithm\r\nel.apiUrl;         // → string | null\r\nel.apiMethod;      // → string\r\nel.title;          // → string\r\nel.description;    // → string\r\nel.qrcodeSize;     // → number\r\nel.showSecret;     // → boolean\r\nel.showCancel;     // → boolean\r\nel.autoFocus;      // → boolean\r\n```\r\n\r\n### 公共方法\r\n\r\n| 方法                                | 说明                       |\r\n|-------------------------------------|----------------------------|\r\n| `reset(): void`                     | 复位到初始状态             |\r\n| `setError(msg: string): void`       | 透传错误到内嵌 mfa-totp    |\r\n\r\n### 事件\r\n\r\n| 事件          | `detail` 类型                                              | 触发时机 |\r\n|---------------|------------------------------------------------------------|----------|\r\n| `bound`       | `{ data: unknown; raw: Response }`                         | 绑定成功（内嵌 mfa-totp 校验通过） |\r\n| `bind-error`  | `{ message: string; code?: string\\|number; status?: number }` | 绑定失败 |\r\n| `cancel`      | `void`                                                     | 点击取消 |\r\n\r\n> **注意**：内嵌 `<mfa-totp>` 自己派发的 `submit` / `success` / `error` 事件**已被外层 `stopPropagation`**，所以宿主只会收到 `bound` / `bind-error` 一次，不会被穿透。\r\n\r\n## 主题与样式定制\r\n\r\n组件的样式全部封装在 Shadow DOM 内，宿主页面样式不会污染组件，组件样式也不会污染宿主页面。要定制外观，使用下面两种方式之一：\r\n\r\n### CSS 变量\r\n\r\n通过宿主 CSS 给组件传 CSS 变量：\r\n\r\n```css\r\nmfa-totp {\r\n  --mfa-primary: #0d86ff;\r\n  --mfa-primary-hover: #0a6fd9;\r\n  --mfa-primary-soft: rgba(13, 134, 255, 0.12);\r\n  --mfa-text: #303133;\r\n  --mfa-text-light: #606266;\r\n  --mfa-text-muted: #909399;\r\n  --mfa-border: #dcdfe6;\r\n  --mfa-border-hover: #c0c4cc;\r\n  --mfa-bg: #ffffff;\r\n  --mfa-bg-page: #f5f7fa;\r\n  --mfa-bg-info: #fdf6ec;\r\n  --mfa-border-info: #e6a23c;\r\n  --mfa-text-info: #8a6d3b;\r\n  --mfa-error: #f56c6c;\r\n  --mfa-success: #67c23a;\r\n  --mfa-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);\r\n  --mfa-shadow-modal: 0 12px 32px 0 rgba(0, 0, 0, 0.18);\r\n  --mfa-radius: 8px;\r\n  --mfa-input-size: 44px;\r\n  --mfa-input-gap: 10px;\r\n  --mfa-font: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"PingFang SC\", sans-serif;\r\n}\r\n```\r\n\r\n`<mfa-totp-bind>` 暴露的是子集：\r\n\r\n```css\r\nmfa-totp-bind {\r\n  --mfa-primary / --mfa-primary-hover / --mfa-primary-soft\r\n  --mfa-text / --mfa-text-light / --mfa-text-muted\r\n  --mfa-border\r\n  --mfa-bg / --mfa-error / --mfa-success\r\n  --mfa-shadow / --mfa-radius / --mfa-font\r\n}\r\n```\r\n\r\n### CSS Parts\r\n\r\n如果想在 light DOM 用祖先选择器穿透修改组件内部节点：\r\n\r\n```css\r\n/* 通过 ::part 暴露出来的节点 */\r\nmfa-totp::part(card)         { ... }\r\nmfa-totp::part(title)        { ... }\r\nmfa-totp::part(submit)       { ... }\r\nmfa-totp-bind::part(secret-text) { ... }\r\n```\r\n\r\n支持的 `part` 名称：\r\n\r\n- `<mfa-totp>`: `backdrop` `card` `header` `logo` `title` `subtitle` `info` `inputs` `error` `submit` `footer` `recovery-toggle` `cancel`\r\n- `<mfa-totp-bind>`: `backdrop` `card` `header` `title` `description` `qr-section` `qr-code` `secret-section` `secret-text` `copy-secret` `verify-hint` `verify-step` `success-state` `footer` `cancel`\r\n\r\n## 国际化 i18n\r\n\r\n新增 / 修改文案：\r\n\r\n1. 编辑 `src/i18n/i18n-zh-CN.ts` / `src/i18n-en-US.ts`（或新建语言文件）。\r\n2. 在 `src/i18n/index.ts` 的 `i18n` 对象里注册。\r\n3. 把语言加到 `src/mfa-totp/types.ts` 的 `Language` 联合类型。\r\n\r\n运行时切换：\r\n\r\n```ts\r\nimport { i18n, setLanguage } from \"@adi291/mfa-totp-client/i18n\";\r\n// 或者通过属性：\r\ndocument.querySelector(\"mfa-totp\").language = \"en-US\";\r\n```\r\n\r\n`digitLabel(index)` 是函数式字段，处理「第 N 位」/ `Digit N` 这类动态文案。\r\n\r\n## Helpers\r\n\r\n```ts\r\nimport { buildOtpauthUrl, formatSecret } from \"@adi291/mfa-totp-client/helpers/otpauth\";\r\nimport { renderQrCode, generateQrCodeDataUrl } from \"@adi291/mfa-totp-client/helpers/qrcode\";\r\n```\r\n\r\n### `buildOtpauthUrl(options)`\r\n\r\n按 Google Authenticator 规范生成 `otpauth://totp/...` URL，APP 扫描后即可识别。\r\n\r\n```ts\r\nbuildOtpauthUrl({\r\n  secret: \"JBSWY3DPEHPK3PXP\",\r\n  account: \"user@example.com\",\r\n  issuer: \"MyApp\",\r\n  // digits? = 6, period? = 30, algorithm? = \"SHA-1\"\r\n});\r\n// → \"otpauth://totp/MyApp:user%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&algorithm=SHA1&digits=6&period=30\"\r\n```\r\n\r\n### `formatSecret(secret, groupSize?)`\r\n\r\n把 `JBSWY3DPEHPK3PXP` 这种密钥展示成 `JBSW Y3DP EHPK 3PXP` 的易读分组（默认每 4 字符一组）。**只用于展示，不参与计算**。\r\n\r\n### `renderQrCode(canvas, text, options?)`\r\n\r\n把文本渲染成 QR 码到指定的 `<canvas>` 元素。\r\n\r\n```ts\r\nawait renderQrCode(canvas, otpauthUrl, {\r\n  width: 200,\r\n  margin: 2,\r\n  errorCorrectionLevel: \"M\",\r\n  color: { dark: \"#000\", light: \"#fff\" },\r\n});\r\n```\r\n\r\n### `generateQrCodeDataUrl(text, options?)`\r\n\r\n返回 PNG 的 DataURL，可直接用于 `<img src=\"...\">`。**依赖 `qrcode` 包**。\r\n\r\n## 工作原理\r\n\r\n本库**不**做 TOTP 计算。TOTP（RFC 6238）需要共享密钥 + 当前时间才能生成验证码，这个动作完全在用户手机上的 APP（Google Authenticator、Microsoft Authenticator、1Password…）里发生，前端无论如何都拿不到密钥明文。\r\n\r\n本组件的职责被严格限制为：\r\n\r\n1. **收集**：提供 6 位 / N 位输入框、恢复码输入、键盘导航、自动跳格、自动提交、粘贴支持。\r\n2. **展示**：接管 UI 状态（loading / error / success）、多模式布局（inline / modal / page）、i18n 文案。\r\n3. **转发**：通过 `api-url` 自动 fetch 验证，或通过 `submit` 事件把 `{code, method}` 交给宿主处理。\r\n4. **回流**：对子组件的错误、成功事件统一再派发（`<mfa-totp-bind>` 的 `bound` / `bind-error`）。\r\n\r\n密钥与服务端校验责任属于业务后端，**本库不持有任何密钥、不会签名、不会预计算**。这种设计让组件可以：\r\n\r\n- 在任何部署了 TOTP 校验的后端**直接复用**；\r\n- 不引入 `hash-wasm` / `crypto.subtle` 等需要密钥计算的能力，体积小巧；\r\n- 杜绝前端误存密钥的安全风险。\r\n\r\n## 本地开发\r\n\r\n```bash\r\npnpm install\r\npnpm dev                                # 启动 playground\r\npnpm typecheck                          # 类型检查（仅 src）\r\npnpm typecheck:playground               # 类型检查（仅 playground）\r\npnpm build                              # 产出 dist/ 给 npm\r\npnpm build:playground                   # 构建 playground 静态站点\r\npnpm preview                            # 预览构建后的 playground\r\n```\r\n\r\nplayground 演示包含四种场景：\r\n\r\n- `verify-inline` —— `<mfa-totp mode=\"inline\">`\r\n- `verify-modal` —— `<mfa-totp mode=\"modal\">` 弹窗\r\n- `verify-page`  —— `<mfa-totp mode=\"page\">` 全屏\r\n- `bind`         —— `<mfa-totp-bind>`\r\n\r\nmock 规则（仅 playground）：\r\n\r\n- `/api/mock/verify` 和 `/api/mock/bind`：以 `123` 开头的验证码视为成功；`000000` 视为网络错误；其他视为失败。\r\n\r\n## 构建与发布\r\n\r\n```bash\r\npnpm build       # 输出到 dist/\r\npnpm publish --access public   # pre-publish hook 会自动跑 pnpm build\r\n```\r\n\r\n构建产物：\r\n\r\n```\r\ndist/\r\n├── mfa-totp-client.js              # 统一入口\r\n├── mfa-totp.js                     # 单组件入口\r\n├── mfa-totp-bind.js\r\n├── helpers/otpauth.js\r\n├── helpers/qrcode.js\r\n├── i18n/index.js\r\n└── *.d.ts                          # 类型定义\r\n```\r\n\r\n`exports` 字段：\r\n\r\n```json\r\n{\r\n  \".\":             \"...\",\r\n  \"./mfa-totp\":    \"...\",\r\n  \"./mfa-totp-bind\":\"...\",\r\n  \"./helpers/otpauth\": \"...\",\r\n  \"./helpers/qrcode\": \"...\",\r\n  \"./i18n\":            \"...\"\r\n}\r\n```\r\n\r\n`qrcode` 是 `optional` peerDependency——只用 `<mfa-totp>` 时无需安装。\r\n\r\n## License\r\n\r\n[Apache-2.0](./LICENSE)\r\n","readmeFilename":"","_rev":"1-9b3e145369d46958cd187c178c8e3fe9"}