{"_id":"@bluesyoung/playcaptcha","name":"@bluesyoung/playcaptcha","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bluesyoung/playcaptcha","version":"1.0.0","description":"Mahjong winning-tile captcha web component with local and server-issued verification","license":"MIT","repository":{"type":"git","url":"git+https://github.com/BluesYoung-web/playcaptcha.git"},"type":"module","main":"./dist/index.mjs","module":"./dist/index.mjs","types":"./dist/index.d.mts","exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"},"./assets/*":"./assets/*"},"publishConfig":{"access":"public","provenance":true},"scripts":{"dev":"vp dev","check":"vp check","test":"vp test --run && node --test scripts/verify-umd-build.test.mjs && pnpm run test:workspace-contract","test:workspace-contract":"node --test scripts/verify-workspace.test.mjs","test:playgrounds-contract":"node --test scripts/verify-playgrounds.test.mjs","test:playgrounds":"pnpm run playgrounds:check && node scripts/verify-playgrounds.mjs","test:umd":"node --test scripts/verify-umd-build.test.mjs && node scripts/verify-umd-build.mjs","test:consumer":"node scripts/verify-package-consumer.mjs","test:consumer-contract":"node --test scripts/verify-package-consumer.test.mjs","build:consumer":"node scripts/verify-package-consumer.mjs","playground:vanilla":"pnpm build && vp run @playcaptcha/playground-vanilla#dev","playground:react":"pnpm build && vp run @playcaptcha/playground-react#dev","playground:vue":"pnpm build && vp run @playcaptcha/playground-vue#dev","playground:server":"pnpm build && vp run @playcaptcha/playground-server#dev","playgrounds:check":"pnpm build && vp run --filter '@playcaptcha/playground-*' --fail-if-no-match check","playgrounds:build":"pnpm build && vp run --filter '@playcaptcha/playground-*' --fail-if-no-match build","build":"vp pack && pnpm run test:umd","prepublishOnly":"pnpm build && pnpm check && pnpm test","postinstall":"simple-git-hooks"},"dependencies":{"lit":"^3.3.3"},"devDependencies":{"@voidzero-dev/vite-plus-core":"^0.2.6","happy-dom":"^20.11.1","simple-git-hooks":"^2.13.1","typescript":"^5.9.3","vite":"npm:@voidzero-dev/vite-plus-core@0.2.6","vite-plus":"^0.2.6","vitest":"4.1.10"},"simple-git-hooks":{"pre-commit":"pnpm build && pnpm check"},"packageManager":"pnpm@10.33.0","_id":"@bluesyoung/playcaptcha@1.0.0","gitHead":"6c262be868fd7a64c567df969a753a30f42922e6","bugs":{"url":"https://github.com/BluesYoung-web/playcaptcha/issues"},"homepage":"https://github.com/BluesYoung-web/playcaptcha#readme","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-K9dyl5M1GUas5qmfmQkaBSMoqgWqhTMzm+2BVKpOiCq1y8YhuaUGZPJyJdadz+khuHa15xKV6y2eydL5OTrHfg==","shasum":"daf6702cf629375d2e37572b7bf915bb59e9408c","tarball":"https://registry.npmjs.org/@bluesyoung/playcaptcha/-/playcaptcha-1.0.0.tgz","fileCount":48,"unpackedSize":655206,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bluesyoung%2fplaycaptcha@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDb74/wW2eoInyNIUNrCITACgfLA4XX8lHvQ50G/c8x1wIgRXFlhlrUyJRMw7dt4JQALxKYSzbasm+2v9r0QjNqU3w="}]},"_npmUser":{"name":"bluesyoung","email":"15171255945@163.com"},"directories":{},"maintainers":[{"name":"bluesyoung","email":"15171255945@163.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/playcaptcha_1.0.0_1785901958372_0.5836889565276424"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-05T03:52:38.192Z","1.0.0":"2026-08-05T03:52:38.534Z","modified":"2026-08-05T03:52:38.904Z"},"maintainers":[{"name":"bluesyoung","email":"15171255945@163.com"}],"description":"Mahjong winning-tile captcha web component with local and server-issued verification","homepage":"https://github.com/BluesYoung-web/playcaptcha#readme","repository":{"type":"git","url":"git+https://github.com/BluesYoung-web/playcaptcha.git"},"bugs":{"url":"https://github.com/BluesYoung-web/playcaptcha/issues"},"license":"MIT","readme":"# PlayCaptcha\n\n麻将胡牌真人验证 Web Component。组件只提供麻将玩法，保留两条验证路径：\n\n- **本地纯前端**：浏览器生成与服务端一致的 7 张等价待胡牌和 12 张候选牌，本地判断结果。\n- **服务端签发 v3**：浏览器只获得受保护的手牌 raster、12 张候选 raster 和不透明 candidate ID，服务端原子消费并判题。\n\n## 安装\n\n```bash\npnpm add @bluesyoung/playcaptcha\n```\n\n```ts\nimport '@bluesyoung/playcaptcha'\n```\n\n## 本地纯前端\n\n不配置验证端点或 transport 时，组件直接运行本地麻将挑战：\n\n```html\n<play-captcha locale=\"zh-CN\"></play-captcha>\n\n<script type=\"module\">\n  import '@bluesyoung/playcaptcha'\n\n  document.querySelector('play-captcha').addEventListener('verify', (event) => {\n    console.log(event.detail) // { mode: 'mahjong', source: 'local' }\n  })\n</script>\n```\n\n`show-answer` 只影响本地提示；默认关闭。错误选择不会发送 `verify`，正确选择发送一个 bubbling、composed 的事件。\n\n## 服务端签发 v3\n\n```html\n<play-captcha\n  verify-endpoint=\"/api/captcha\"\n  verify-timeout=\"10000\"\n  verification-data='{\"scene\":\"login\"}'\n></play-captcha>\n```\n\n宿主也可以设置 `verificationTransport`。它优先于 `verify-endpoint`，接收 `{ request, signal }`，必须遵守相同 v3 语义。\n\n### 创建请求\n\n```json\n{\n  \"version\": 3,\n  \"action\": \"create\",\n  \"mode\": \"mahjong\",\n  \"context\": { \"scene\": \"login\" }\n}\n```\n\n### 签发响应\n\n```json\n{\n  \"version\": 3,\n  \"challengeId\": \"random-opaque-id\",\n  \"mode\": \"mahjong\",\n  \"challenge\": {\n    \"handImage\": \"/captcha/assets/random-hand-token\",\n    \"candidates\": [{ \"id\": \"random-candidate-id\", \"image\": \"/captcha/assets/random-image-token\" }]\n  },\n  \"expiresAt\": \"2026-08-04T12:00:00Z\"\n}\n```\n\n`candidates` 必须恰好 12 项，所有 `id` 和 `image` 唯一。响应不得暴露麻将牌 ID、素材编号、答案或映射。\n\n### 验证请求\n\n```json\n{\n  \"version\": 3,\n  \"action\": \"verify\",\n  \"challengeId\": \"random-opaque-id\",\n  \"completion\": { \"selected\": \"random-candidate-id\" },\n  \"context\": { \"scene\": \"login\" }\n}\n```\n\n### 验证响应\n\n```json\n{\n  \"verified\": true,\n  \"token\": \"business-bound-result-token\",\n  \"expiresAt\": \"2026-08-04T12:02:00Z\"\n}\n```\n\n失败响应：\n\n```json\n{ \"verified\": false, \"message\": \"Challenge unavailable\" }\n```\n\n## 服务端安全与实现清单\n\n协议与语言、框架、存储无关。Go、Node、Java、Python、Rust 或其他实现同等有效；不要求 Node runtime、Sharp 或任何特定库。\n\n- 严格验证 v3 HTTP schema；远端 create 只接受 `mode: \"mahjong\"`。\n- 使用高熵 challenge/candidate/asset ID；不得在 JSON、DOM、URL 或日志暴露牌义。\n- 将 challenge 绑定到 HttpOnly session、账号或其他认证主体，并保存 canonical context digest。\n- 手牌合成为新的 raster；候选重新编码并使用每轮唯一的受保护资源 token。\n- raster 只允许未过期、未消费且主体匹配的 challenge 读取；返回 `private, no-store` 和 `nosniff`。\n- 第一次格式合法的 verify 必须通过比较并交换原子消费，无论答案对错；随后撤销全部资源。\n- 按主体、IP、账号和业务对象限制 create/verify，记录异常速度、失败率和重复创建。\n- 成功 token 必须短期、一次性，绑定业务动作、主体、context digest 和 challenge ID；业务接口必须再次验证。\n- 分布式生产部署使用共享数据库/缓存及受保护对象存储，不能依赖单进程内存。\n\n完整规范：[`docs/compose/specs/2026-07-28-opaque-mahjong-v3-backend-design.md`](docs/compose/specs/2026-07-28-opaque-mahjong-v3-backend-design.md)。\n\n仓库保留一个可运行的非规范教学适配器：\n\n```bash\npnpm run playground:server\n```\n\n它选择 Node、Sharp/WebP、Vite middleware、端口 `4186` 和进程内 `Map`，只用于演示协议。生产实现仍负责共享状态、原子消费、context digest、限速和业务 token 绑定。\n\n## 属性与 JavaScript 配置\n\n| 属性                | Property           | 说明                                 |\n| ------------------- | ------------------ | ------------------------------------ |\n| `locale`            | `locale`           | `zh-CN` 或 `en`；区域值归一化        |\n| `title`             | `title`            | 自定义标题；移除后恢复当前语言默认值 |\n| `show-answer`       | `showAnswer`       | 仅本地挑战显示胡牌答案               |\n| `asset-base`        | `assetBase`        | 麻将素材目录，HTTP(S) hierarchy URL  |\n| `verify-endpoint`   | `verifyEndpoint`   | v3 JSON endpoint；留空启用本地验证   |\n| `verify-timeout`    | `verifyTimeout`    | 1000–60000ms，默认 10000ms           |\n| `verification-data` | `verificationData` | 作为 `context` 发送的 JSON 字符串    |\n\nJavaScript-only 配置：\n\n```ts\nconst captcha = document.querySelector('play-captcha')\n\ncaptcha.verificationHeaders = { Authorization: 'Bearer …' }\ncaptcha.verificationCredentials = 'include'\ncaptcha.verificationTransport = async ({ request, signal }) => {\n  const response = await fetch('/api/captcha', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(request),\n    signal,\n  })\n  return response.json()\n}\n```\n\nTransport 会收到 `AbortSignal`；刷新、配置变化、超时和断开连接会取消过期请求。已签发挑战到期时组件自动创建新题。服务端拒绝、HTTP、网络、超时、配置和响应 schema 错误通过 `verification-error` 报告。\n\n## 事件\n\n### `verify`\n\n```ts\ntype PlayCaptchaVerifyEventDetail = {\n  mode: 'mahjong'\n  source: 'local' | 'remote'\n  token?: string\n  expiresAt?: string\n}\n```\n\n仅本地正确答案或服务端明确返回 `{ \"verified\": true }` 后发送。\n\n### `verification-error`\n\n```ts\ntype PlayCaptchaVerificationErrorDetail = {\n  kind: 'config' | 'rejected' | 'http' | 'network' | 'timeout' | 'response'\n  message: string\n  status?: number\n}\n```\n\n两个事件都 bubbling 且 composed。\n\n## 麻将 API\n\n包入口导出：\n\n```ts\nimport {\n  PlayCaptcha,\n  PLAY_CAPTCHA_LOCALES,\n  DEFAULT_PLAY_CAPTCHA_LOCALE,\n  normalizeLocale,\n  MAHJONG_TILE_IDS,\n  MAHJONG_TILE_META,\n  createMahjongChallenge,\n  isMahjongTileId,\n  isStandardMahjongWin,\n  sortMahjongTiles,\n  winningTilesForHand,\n  parseIssuedChallenge,\n  parseVerificationResponse,\n  type MahjongChallenge,\n  type MahjongTileId,\n  type MahjongWaitType,\n  type PlayCaptchaLocale,\n  type PlayCaptchaVerifyEventDetail,\n  type PlayCaptchaVerificationTransport,\n} from '@bluesyoung/playcaptcha'\n```\n\n组件公开只读状态：\n\n- `currentMahjongTarget`：本地题的胡牌答案；远端题为 `null`。\n- `mahjongHand`：本地题用于界面展示的 7 张等价待胡手牌；远端题为空数组。\n\n## 素材与自托管\n\n默认资源由 bundler 解析。自托管时，`asset-base` 表示 **`majiang_ui` 目录本身**：\n\n```html\n<play-captcha asset-base=\"/assets/majiang_ui/\"></play-captcha>\n```\n\n组件从该目录读取 `<assetValue>.webp`。只接受 HTTP(S) hierarchy URL；query/hash 会被移除，缺失尾部 `/` 会自动补全。\n\nnpm 包包含：\n\n- `assets/majiang_ui/` 下 42 张最新 WebP：标准牌 34 张，加 `46.webp`–`53.webp` 八张花/季节牌。\n\n当前规则只引用 34 张标准牌，因此消费者 bundle 的运行时资源精确为 34 张 WebP。八张花/季节牌仅作为完整最新源素材打包，不进入当前挑战 bundle。\n\nClassic script：\n\n```html\n<script src=\"https://cdn.example/playcaptcha/dist/playcaptcha.umd.js\"></script>\n<play-captcha></play-captcha>\n```\n\n保持 `dist/playcaptcha.umd.js` 到包根 `assets/` 的相对布局；UMD 同步注册一个组件，不创建匿名 AMD module，不暴露受支持的全局 API。浏览器边界为 Chrome 80、Safari 13；不支持 IE11。\n\n## 交互与无障碍\n\n- 本地与服务端都展示 7 张等价待胡手牌、12 张候选牌；候选按 5/4/3 三排排列且每张横向可达。\n- 摇杆或 `←` / `→` 移动，红色按钮或 Space/Enter 抓取和投放。\n- 190px compact 与 440px 标准布局保留同一逻辑坐标和可达性。\n- 帮助层非 modal，可由 Escape 关闭并恢复焦点。\n- 支持 `prefers-reduced-motion`，刷新会清除 carry、反馈和成功状态。\n\n## 本地开发\n\n```bash\npnpm install\npnpm run dev\npnpm run check\npnpm run test\npnpm run build\npnpm run test:consumer\n```\n\n仓库使用 pnpm 管理 workspace。consumer gate 内部用 `npm pack` 和 `npm install` 模拟 npm 消费者，验证真实 tarball、UMD/native fixture 及 bundle 资源；这不是仓库依赖安装命令。\n\n## Playground 与验证命令\n\n- 根页面：本地纯前端麻将。\n- Vanilla：DOM property、原生事件和宽度切换。\n- React：ref、原生事件订阅和宽度切换。\n- Vue：ref、mount/unmount、事件订阅和宽度切换。\n- Server：远端 v3；非规范 Node 教学适配器。\n\n```bash\npnpm check\npnpm test\npnpm build\npnpm run test:consumer-contract\nnode scripts/verify-package-consumer.mjs --skip-build\npnpm run playgrounds:check\npnpm run playgrounds:build\npnpm run test:playgrounds-contract\nnode scripts/verify-playgrounds.mjs --skip-build\n```\n\n`pnpm run playgrounds:check` 先构建一次根包，再直接检查四个 playground。`prepublishOnly` 串联根 check/test/build、consumer contract、四个 playground 的直接 check/build、playground contract 和最终产物验证。\n\n## 来源与致谢\n\n本仓库最初的 PlayCaptcha 概念与实现来源归功于 [mortspace/playcaptcha](https://github.com/mortspace/playcaptcha)。\n\n## 许可证\n\nMIT。\n","readmeFilename":"README.md","_rev":"1-823db7e10aaf046f68e1b6b60d5238d7"}