@funnycaptcha/rotate 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -22,11 +22,12 @@ function proofInput(c) {
22
22
 
23
23
  // src/render.ts
24
24
  var STR = {
25
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9" },
26
- en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up" }
25
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u5C06\u56FE\u5F62\u8F6C\u6B63", success: "\u9A8C\u8BC1\u6210\u529F", hint: "\u8BA9\u7BAD\u5934\u6307\u5411\u4E0A\u65B9", refresh: "\u5237\u65B0" },
26
+ en: { title: "Drag to rotate the figure upright", success: "Verified", hint: "Point the arrow up", refresh: "Refresh" }
27
27
  };
28
28
  function createRotateInstance(container, config) {
29
29
  const t = STR[config.locale];
30
+ const theme = config.theme ?? "light";
30
31
  let current;
31
32
  let listeners = [];
32
33
  let startTime = Date.now();
@@ -37,31 +38,42 @@ function createRotateInstance(container, config) {
37
38
  done = false;
38
39
  container.innerHTML = `
39
40
  <style>
40
- .fc-rotate{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
41
- .fc-rotate-title{font-size:14px;color:#333;text-align:center}
42
- .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed #ccc;border-radius:50%}
41
+ .fc-rotate{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}
42
+ .fc-rotate[data-theme="light"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}
43
+ .fc-rotate[data-theme="dark"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}
44
+ .fc-rotate{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
45
+ .fc-rotate-title{font-size:14px;color:var(--fc-text);text-align:center}
46
+ .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed var(--fc-border);border-radius:50%}
43
47
  .fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}
44
- .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid #e74c3c}
45
- .fc-rotate-hint{font-size:12px;color:#999}
46
- .fc-rotate-range{width:240px;accent-color:#4a90d9}
47
- .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:#2e7d32}
48
+ .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid var(--fc-danger)}
49
+ .fc-rotate-hint{font-size:12px;color:var(--fc-text-soft)}
50
+ .fc-rotate-range{width:240px;accent-color:var(--fc-accent)}
51
+ .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:var(--fc-success)}
52
+ .fc-rotate-row{display:flex;justify-content:center}
53
+ .fc-rotate-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}
54
+ .fc-rotate-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
48
55
  </style>
49
- <div class="fc-rotate">
56
+ <div class="fc-rotate" data-theme="${theme}">
50
57
  <div class="fc-rotate-title">${t.title}</div>
51
58
  <div class="fc-rotate-stage">
52
59
  <div class="fc-rotate-figure" style="transform: rotate(${current.angle}deg)">
53
- <svg viewBox="0 0 100 100" width="100" height="100"><polygon points="50,12 90,85 10,85" fill="#4a90d9"/></svg>
60
+ <svg viewBox="0 0 100 100" width="100" height="100"><path d="M 50 8 L 82 42 L 62 42 L 62 92 L 38 92 L 38 42 Z" fill="var(--fc-accent)"/></svg>
54
61
  </div>
55
62
  <div class="fc-rotate-marker"></div>
56
63
  </div>
57
64
  <div class="fc-rotate-hint">${t.hint}</div>
58
65
  <input class="fc-rotate-range" type="range" min="0" max="360" step="1" value="0" />
59
66
  <div class="fc-rotate-msg"></div>
67
+ <div class="fc-rotate-row">
68
+ <button class="fc-rotate-refresh">${t.refresh}</button>
69
+ </div>
60
70
  </div>
61
71
  `;
62
72
  const figure = container.querySelector(".fc-rotate-figure");
63
73
  const range = container.querySelector(".fc-rotate-range");
64
74
  const msg = container.querySelector(".fc-rotate-msg");
75
+ const refreshBtn = container.querySelector(".fc-rotate-refresh");
76
+ refreshBtn.addEventListener("click", render);
65
77
  const update = async () => {
66
78
  if (done) return;
67
79
  const v = Number(range.value);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createRotateInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const rotatePlugin: CaptchaPlugin = {\n id: 'rotate',\n category: 'interactive',\n create: createRotateInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '旋转对齐' : 'Rotate Align',\n description: locale === 'zh'\n ? '拖动滑块将偏转的图形转正,考验空间感'\n : 'Rotate the tilted figure back upright. Tests spatial sense.',\n tags: ['rotate', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(rotatePlugin);\n\nexport function mountRotate(container: HTMLElement, config: CaptchaConfig) {\n return createRotateInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyAlignment, type RotateChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块将图形转正', success: '验证成功', hint: '让箭头指向上方' },\n en: { title: 'Drag to rotate the figure upright', success: 'Verified', hint: 'Point the arrow up' },\n};\n\nexport function createRotateInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: RotateChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n container.innerHTML = `\n <style>\n .fc-rotate{font-family:-apple-system,system-ui,sans-serif;width:300px;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}\n .fc-rotate-title{font-size:14px;color:#333;text-align:center}\n .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed #ccc;border-radius:50%}\n .fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}\n .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid #e74c3c}\n .fc-rotate-hint{font-size:12px;color:#999}\n .fc-rotate-range{width:240px;accent-color:#4a90d9}\n .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:#2e7d32}\n </style>\n <div class=\"fc-rotate\">\n <div class=\"fc-rotate-title\">${t.title}</div>\n <div class=\"fc-rotate-stage\">\n <div class=\"fc-rotate-figure\" style=\"transform: rotate(${current.angle}deg)\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\"><polygon points=\"50,12 90,85 10,85\" fill=\"#4a90d9\"/></svg>\n </div>\n <div class=\"fc-rotate-marker\"></div>\n </div>\n <div class=\"fc-rotate-hint\">${t.hint}</div>\n <input class=\"fc-rotate-range\" type=\"range\" min=\"0\" max=\"360\" step=\"1\" value=\"0\" />\n <div class=\"fc-rotate-msg\"></div>\n </div>\n `;\n const figure = container.querySelector('.fc-rotate-figure') as HTMLDivElement;\n const range = container.querySelector('.fc-rotate-range') as HTMLInputElement;\n const msg = container.querySelector('.fc-rotate-msg') as HTMLDivElement;\n\n const update = async () => {\n if (done) return;\n const v = Number(range.value);\n const final = (((current.angle + v) % 360) + 360) % 360;\n figure.style.transform = `rotate(${final}deg)`;\n if (verifyAlignment(current, v)) {\n done = true;\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n range.disabled = true;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n }\n };\n\n range.addEventListener('input', update);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 旋转对齐题目:图形被旋转了 angle 度,用户拖动滑块校正使其回正(误差 < 5°)\nexport interface RotateChallenge {\n // 目标校正角度(0-359),即图形初始的偏转量;用户需反向校正使其回正\n angle: number;\n}\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// 生成 30-330 之间的角度(避免太接近 0/360 导致一眼即过)\nexport function generateChallenge(): RotateChallenge {\n return { angle: randInt(30, 330) };\n}\n\n// 校验对齐:最终旋转 = angle + userAngle,回正即 ≡ 0 (mod 360),误差 <= tolerance\nexport function verifyAlignment(c: RotateChallenge, userAngle: number, tolerance = 5): boolean {\n const final = (((c.angle + userAngle) % 360) + 360) % 360;\n // 取到 0/360 的最短距离\n const diff = Math.min(final, 360 - final);\n return diff <= tolerance;\n}\n\n// 构造 proof 输入串:angle + ':aligned'\nexport function proofInput(c: RotateChallenge): string {\n return `${c.angle}:aligned`;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type RotateChallenge } from './challenge.js';\n\nexport function issueRotateChallenge(): { challenge: RotateChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { angle: challenge.angle } };\n}\n\nexport async function verifyRotateProof(\n challenge: RotateChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(proofInput(challenge));\n return expected === proof;\n}\n"],"mappings":";AAAA,SAAS,qBAA6D;;;ACCtE,SAAS,iBAAiB;;;ACK1B,SAAS,QAAQ,KAAa,KAAqB;AACjD,SAAO,KAAK,MAAM,KAAK,OAAO,KAAK,MAAM,MAAM,EAAE,IAAI;AACvD;AAGO,SAAS,oBAAqC;AACnD,SAAO,EAAE,OAAO,QAAQ,IAAI,GAAG,EAAE;AACnC;AAGO,SAAS,gBAAgB,GAAoB,WAAmB,YAAY,GAAY;AAC7F,QAAM,UAAW,EAAE,QAAQ,aAAa,MAAO,OAAO;AAEtD,QAAM,OAAO,KAAK,IAAI,OAAO,MAAM,KAAK;AACxC,SAAO,QAAQ;AACjB;AAGO,SAAS,WAAW,GAA4B;AACrD,SAAO,GAAG,EAAE,KAAK;AACnB;;;ADtBA,IAAM,MAAM;AAAA,EACV,IAAI,EAAE,OAAO,0DAAa,SAAS,4BAAQ,MAAM,6CAAU;AAAA,EAC3D,IAAI,EAAE,OAAO,qCAAqC,SAAS,YAAY,MAAM,qBAAqB;AACpG;AAEO,SAAS,qBACd,WACA,QACiB;AACjB,QAAM,IAAI,IAAI,OAAO,MAAM;AAC3B,MAAI;AACJ,MAAI,YAA4C,CAAC;AACjD,MAAI,YAAY,KAAK,IAAI;AACzB,MAAI,OAAO;AAEX,WAAS,SAAS;AAChB,cAAU,kBAAkB;AAC5B,gBAAY,KAAK,IAAI;AACrB,WAAO;AACP,cAAU,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,uCAYa,EAAE,KAAK;AAAA;AAAA,mEAEqB,QAAQ,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,sCAK1C,EAAE,IAAI;AAAA;AAAA;AAAA;AAAA;AAKxC,UAAM,SAAS,UAAU,cAAc,mBAAmB;AAC1D,UAAM,QAAQ,UAAU,cAAc,kBAAkB;AACxD,UAAM,MAAM,UAAU,cAAc,gBAAgB;AAEpD,UAAM,SAAS,YAAY;AACzB,UAAI,KAAM;AACV,YAAM,IAAI,OAAO,MAAM,KAAK;AAC5B,YAAM,UAAW,QAAQ,QAAQ,KAAK,MAAO,OAAO;AACpD,aAAO,MAAM,YAAY,UAAU,KAAK;AACxC,UAAI,gBAAgB,SAAS,CAAC,GAAG;AAC/B,eAAO;AACP,cAAM,SAAwB;AAAA,UAC5B,SAAS;AAAA,UACT,OAAO,MAAM,UAAU,WAAW,OAAO,CAAC;AAAA,UAC1C,UAAU,KAAK,IAAI,IAAI;AAAA,QACzB;AACA,YAAI,cAAc,EAAE;AACpB,cAAM,WAAW;AACjB,eAAO,WAAW,MAAM;AACxB,kBAAU,QAAQ,QAAM,GAAG,MAAM,CAAC;AAAA,MACpC;AAAA,IACF;AAEA,UAAM,iBAAiB,SAAS,MAAM;AAAA,EACxC;AAEA,SAAO;AAAA,IACL,OAAO,MAAM,OAAO;AAAA,IACpB,OAAO,MAAM,OAAO;AAAA,IACpB,SAAS,MAAM;AAAE,gBAAU,YAAY;AAAI,kBAAY,CAAC;AAAA,IAAG;AAAA,IAC3D,UAAU,QAAM,UAAU,KAAK,EAAE;AAAA,EACnC;AACF;;;AE/EA,SAAS,aAAAA,kBAAiB;AAGnB,SAAS,uBAAyE;AACvF,QAAM,YAAY,kBAAkB;AACpC,SAAO,EAAE,WAAW,SAAS,EAAE,OAAO,UAAU,MAAM,EAAE;AAC1D;AAEA,eAAsB,kBACpB,WACA,OACkB;AAClB,QAAM,WAAW,MAAMC,WAAU,WAAW,SAAS,CAAC;AACtD,SAAO,aAAa;AACtB;;;AHRO,IAAM,eAA8B;AAAA,EACzC,IAAI;AAAA,EACJ,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU,CAAC,YAAY;AAAA,IACrB,MAAM,WAAW,OAAO,6BAAS;AAAA,IACjC,aAAa,WAAW,OACpB,iHACA;AAAA,IACJ,MAAM,CAAC,UAAU,QAAQ,aAAa;AAAA,EACxC;AACF;AAEA,cAAc,YAAY;AAEnB,SAAS,YAAY,WAAwB,QAAuB;AACzE,SAAO,qBAAqB,WAAW,MAAM;AAC/C;","names":["hashProof","hashProof"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/render.ts","../src/challenge.ts","../src/verifier.ts"],"sourcesContent":["import { defineCaptcha, type CaptchaConfig, type CaptchaPlugin } from '@funnycaptcha/core';\nimport { createRotateInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const rotatePlugin: CaptchaPlugin = {\n id: 'rotate',\n category: 'interactive',\n create: createRotateInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '旋转对齐' : 'Rotate Align',\n description: locale === 'zh'\n ? '拖动滑块将偏转的图形转正,考验空间感'\n : 'Rotate the tilted figure back upright. Tests spatial sense.',\n tags: ['rotate', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(rotatePlugin);\n\nexport function mountRotate(container: HTMLElement, config: CaptchaConfig) {\n return createRotateInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyAlignment, type RotateChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块将图形转正', success: '验证成功', hint: '让箭头指向上方', refresh: '刷新' },\n en: { title: 'Drag to rotate the figure upright', success: 'Verified', hint: 'Point the arrow up', refresh: 'Refresh' },\n};\n\nexport function createRotateInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: RotateChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n container.innerHTML = `\n <style>\n .fc-rotate{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box;display:flex;flex-direction:column;align-items:center;gap:12px}\n .fc-rotate[data-theme=\"light\"]{--fc-bg:#ffffff;--fc-surface:#f6f7f9;--fc-text:#0f172a;--fc-text-soft:#64748b;--fc-border:#e2e8f0;--fc-accent:#6366f1;--fc-accent-soft:#eef2ff;--fc-success:#16a34a;--fc-danger:#dc2626}\n .fc-rotate[data-theme=\"dark\"]{--fc-bg:#1e2544;--fc-surface:#171c36;--fc-text:#e5e9f0;--fc-text-soft:#94a3b8;--fc-border:#2a3358;--fc-accent:#818cf8;--fc-accent-soft:#252b5c;--fc-success:#4ade80;--fc-danger:#f87171}\n .fc-rotate{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-rotate-title{font-size:14px;color:var(--fc-text);text-align:center}\n .fc-rotate-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center;border:2px dashed var(--fc-border);border-radius:50%}\n .fc-rotate-figure{width:100px;height:100px;transform-origin:center;transition:transform .05s}\n .fc-rotate-marker{position:absolute;top:-6px;left:50%;transform:translateX(-50%);width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-top:12px solid var(--fc-danger)}\n .fc-rotate-hint{font-size:12px;color:var(--fc-text-soft)}\n .fc-rotate-range{width:240px;accent-color:var(--fc-accent)}\n .fc-rotate-msg{font-size:13px;min-height:18px;text-align:center;color:var(--fc-success)}\n .fc-rotate-row{display:flex;justify-content:center}\n .fc-rotate-refresh{padding:4px 12px;font-size:12px;border:1px solid var(--fc-border);background:var(--fc-surface);color:var(--fc-text-soft);border-radius:6px;cursor:pointer}\n .fc-rotate-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n </style>\n <div class=\"fc-rotate\" data-theme=\"${theme}\">\n <div class=\"fc-rotate-title\">${t.title}</div>\n <div class=\"fc-rotate-stage\">\n <div class=\"fc-rotate-figure\" style=\"transform: rotate(${current.angle}deg)\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\"><path d=\"M 50 8 L 82 42 L 62 42 L 62 92 L 38 92 L 38 42 Z\" fill=\"var(--fc-accent)\"/></svg>\n </div>\n <div class=\"fc-rotate-marker\"></div>\n </div>\n <div class=\"fc-rotate-hint\">${t.hint}</div>\n <input class=\"fc-rotate-range\" type=\"range\" min=\"0\" max=\"360\" step=\"1\" value=\"0\" />\n <div class=\"fc-rotate-msg\"></div>\n <div class=\"fc-rotate-row\">\n <button class=\"fc-rotate-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const figure = container.querySelector('.fc-rotate-figure') as HTMLDivElement;\n const range = container.querySelector('.fc-rotate-range') as HTMLInputElement;\n const msg = container.querySelector('.fc-rotate-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-rotate-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n const update = async () => {\n if (done) return;\n const v = Number(range.value);\n const final = (((current.angle + v) % 360) + 360) % 360;\n figure.style.transform = `rotate(${final}deg)`;\n if (verifyAlignment(current, v)) {\n done = true;\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n };\n msg.textContent = t.success;\n range.disabled = true;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n }\n };\n\n range.addEventListener('input', update);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 旋转对齐题目:图形被旋转了 angle 度,用户拖动滑块校正使其回正(误差 < 5°)\nexport interface RotateChallenge {\n // 目标校正角度(0-359),即图形初始的偏转量;用户需反向校正使其回正\n angle: number;\n}\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// 生成 30-330 之间的角度(避免太接近 0/360 导致一眼即过)\nexport function generateChallenge(): RotateChallenge {\n return { angle: randInt(30, 330) };\n}\n\n// 校验对齐:最终旋转 = angle + userAngle,回正即 ≡ 0 (mod 360),误差 <= tolerance\nexport function verifyAlignment(c: RotateChallenge, userAngle: number, tolerance = 5): boolean {\n const final = (((c.angle + userAngle) % 360) + 360) % 360;\n // 取到 0/360 的最短距离\n const diff = Math.min(final, 360 - final);\n return diff <= tolerance;\n}\n\n// 构造 proof 输入串:angle + ':aligned'\nexport function proofInput(c: RotateChallenge): string {\n return `${c.angle}:aligned`;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type RotateChallenge } from './challenge.js';\n\nexport function issueRotateChallenge(): { challenge: RotateChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { angle: challenge.angle } };\n}\n\nexport async function verifyRotateProof(\n challenge: RotateChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(proofInput(challenge));\n return expected === proof;\n}\n"],"mappings":"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","names":["hashProof","hashProof"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnycaptcha/rotate",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Rotate captcha — FunnyChapter",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "dist"
16
16
  ],
17
17
  "dependencies": {
18
- "@funnycaptcha/core": "0.2.0"
18
+ "@funnycaptcha/core": "0.3.0"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public",