@funnycaptcha/puzzle 0.3.0 → 0.6.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
@@ -2,15 +2,15 @@
2
2
  import { defineCaptcha } from "@funnycaptcha/core";
3
3
 
4
4
  // src/render.ts
5
- import { hashProof } from "@funnycaptcha/core";
5
+ import { hashProof, TrackRecorder, analyzeTrack } from "@funnycaptcha/core";
6
6
 
7
7
  // src/challenge.ts
8
- var TOLERANCE = 5;
8
+ var TOLERANCE = 8;
9
9
  function randInt(min, max) {
10
10
  return Math.floor(Math.random() * (max - min + 1)) + min;
11
11
  }
12
12
  function generateChallenge() {
13
- return { gapPosition: randInt(10, 90) };
13
+ return { gapPosition: randInt(25, 58) };
14
14
  }
15
15
  function proofInput(c) {
16
16
  return `${c.gapPosition}:completed`;
@@ -21,71 +21,232 @@ function verifyPosition(c, userPos) {
21
21
 
22
22
  // src/render.ts
23
23
  var STR = {
24
- zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", refresh: "\u5237\u65B0" },
25
- en: { title: "Drag the slider to fit the puzzle", success: "Verified", refresh: "Refresh" }
24
+ zh: { title: "\u62D6\u52A8\u6ED1\u5757\u628A\u62FC\u56FE\u5757\u79FB\u5230\u7F3A\u53E3", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u672A\u5BF9\u9F50\u7F3A\u53E3", botFail: "\u68C0\u6D4B\u5230\u5F02\u5E38\u64CD\u4F5C", refresh: "\u5237\u65B0" },
25
+ en: { title: "Drag the slider to fit the puzzle", success: "Verified", fail: "Not aligned", botFail: "Bot-like behavior", refresh: "Refresh" }
26
26
  };
27
27
  var STAGE_W = 300;
28
+ var STAGE_H = 200;
28
29
  var PIECE_W = 50;
29
30
  var MAX_OFFSET = STAGE_W - PIECE_W;
31
+ var TRACK_W = 300;
32
+ var TRACK_H = 100;
33
+ var HANDLE_W = 40;
34
+ var HANDLE_H = 40;
35
+ function zigzagPoints() {
36
+ const pts = [];
37
+ const margin = HANDLE_W / 2;
38
+ const w = TRACK_W - margin * 2;
39
+ const h = TRACK_H - margin * 2;
40
+ const seg1Steps = 30;
41
+ for (let i = 0; i <= seg1Steps; i++) {
42
+ pts.push({ x: margin + w / seg1Steps * i, y: margin });
43
+ }
44
+ const seg2Steps = 10;
45
+ const midY = margin + h / 4;
46
+ for (let i = 1; i <= seg2Steps; i++) {
47
+ pts.push({ x: TRACK_W - margin, y: margin + (midY - margin) / seg2Steps * i });
48
+ }
49
+ for (let i = 1; i <= seg1Steps; i++) {
50
+ pts.push({ x: TRACK_W - margin - w / seg1Steps * i, y: midY });
51
+ }
52
+ const midY2 = margin + h * 3 / 4;
53
+ for (let i = 1; i <= seg2Steps; i++) {
54
+ pts.push({ x: margin, y: midY + (midY2 - midY) / seg2Steps * i });
55
+ }
56
+ for (let i = 1; i <= seg1Steps; i++) {
57
+ pts.push({ x: margin + w / seg1Steps * i, y: midY2 });
58
+ }
59
+ for (let i = 1; i <= seg2Steps; i++) {
60
+ pts.push({ x: TRACK_W - margin, y: midY2 + (TRACK_H - margin - midY2) / seg2Steps * i });
61
+ }
62
+ return pts;
63
+ }
64
+ function zigzagPath(pts) {
65
+ return pts.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(" ");
66
+ }
67
+ function pointAtProgress(pts, progress) {
68
+ const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));
69
+ return pts[idx];
70
+ }
71
+ function pieceCirclePos(progress) {
72
+ const cx = STAGE_W / 2;
73
+ const cy = STAGE_H / 2;
74
+ const radius = Math.min(STAGE_W, STAGE_H) / 2 - PIECE_W / 2 - 8;
75
+ const angle = -progress * Math.PI * 2;
76
+ return {
77
+ x: cx + radius * Math.cos(angle) - PIECE_W / 2,
78
+ y: cy + radius * Math.sin(angle) - PIECE_W / 2
79
+ };
80
+ }
30
81
  function createPuzzleInstance(container, config) {
31
82
  const t = STR[config.locale];
83
+ const theme = config.theme ?? "light";
84
+ const recorder = new TrackRecorder();
32
85
  let current;
33
86
  let listeners = [];
34
87
  let startTime = Date.now();
35
88
  let done = false;
89
+ let locked = false;
36
90
  function render() {
37
91
  current = generateChallenge();
38
92
  startTime = Date.now();
39
93
  done = false;
94
+ locked = false;
95
+ recorder.clear();
40
96
  const gapLeft = current.gapPosition / 100 * MAX_OFFSET;
97
+ const pts = zigzagPoints();
98
+ const pathD = zigzagPath(pts);
41
99
  container.innerHTML = `
42
100
  <style>
43
101
  .fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
44
102
  .fc-puzzle[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}
45
103
  .fc-puzzle[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}
104
+ .fc-puzzle{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
46
105
  .fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}
47
- .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:160px;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}
48
- .fc-puzzle-gap{position:absolute;top:55px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15)}
49
- .fc-puzzle-piece{position:absolute;top:55px;left:0;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25)}
50
- .fc-puzzle-range{width:${STAGE_W}px;margin-top:14px;accent-color:var(--fc-accent)}
106
+ .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:${STAGE_H}px;margin:0 auto;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}
107
+ .fc-puzzle-gap{position:absolute;top:${(STAGE_H - PIECE_W) / 2}px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15);transition:left .15s}
108
+ .fc-puzzle-piece{position:absolute;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25);transition:background .15s,border-color .15s}
109
+ .fc-puzzle-track{position:relative;width:${TRACK_W}px;height:${TRACK_H}px;margin:14px auto 0;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:8px;overflow:hidden}
110
+ .fc-puzzle-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}
111
+ .fc-puzzle-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
112
+ .fc-puzzle-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;transition:stroke .15s}
113
+ .fc-puzzle-handle{position:absolute;width:${HANDLE_W}px;height:${HANDLE_H}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}
114
+ .fc-puzzle-handle:active{cursor:grabbing}
115
+ .fc-puzzle-handle::after{content:'\\2192';color:var(--fc-accent);font-size:16px;font-weight:bold;transition:color .15s}
51
116
  .fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}
52
117
  .fc-puzzle-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;margin-top:8px;display:block;margin-left:auto;margin-right:auto}
53
118
  .fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
119
+ .fc-puzzle-done .fc-puzzle-piece{background:linear-gradient(135deg,var(--fc-success),var(--fc-accent-soft));border-color:var(--fc-success)}
120
+ .fc-puzzle-done .fc-puzzle-handle{border-color:var(--fc-success)}
121
+ .fc-puzzle-done .fc-puzzle-handle::after{color:var(--fc-success)}
122
+ .fc-puzzle-done .fc-puzzle-path-fg{stroke:var(--fc-success)}
54
123
  </style>
55
- <div class="fc-puzzle" data-theme="${config.theme}">
124
+ <div class="fc-puzzle" data-theme="${theme}">
56
125
  <div class="fc-puzzle-title">${t.title}</div>
57
126
  <div class="fc-puzzle-stage">
58
127
  <div class="fc-puzzle-gap" style="left:${gapLeft}px"></div>
59
- <div class="fc-puzzle-piece" style="left:0"></div>
128
+ <div class="fc-puzzle-piece" style="left:0;top:${(STAGE_H - PIECE_W) / 2}px"></div>
129
+ </div>
130
+ <div class="fc-puzzle-track">
131
+ <svg class="fc-puzzle-svg" viewBox="0 0 ${TRACK_W} ${TRACK_H}">
132
+ <path class="fc-puzzle-path-bg" d="${pathD}"></path>
133
+ <path class="fc-puzzle-path-fg" d="${pathD}"></path>
134
+ </svg>
135
+ <div class="fc-puzzle-handle" role="slider" tabindex="0"></div>
60
136
  </div>
61
- <input class="fc-puzzle-range" type="range" min="0" max="100" step="1" value="0" />
62
137
  <div class="fc-puzzle-msg"></div>
63
138
  <button class="fc-puzzle-refresh">${t.refresh}</button>
64
139
  </div>
65
140
  `;
141
+ const root = container.querySelector(".fc-puzzle");
66
142
  const piece = container.querySelector(".fc-puzzle-piece");
67
- const range = container.querySelector(".fc-puzzle-range");
143
+ const track = container.querySelector(".fc-puzzle-track");
144
+ const handle = container.querySelector(".fc-puzzle-handle");
68
145
  const msg = container.querySelector(".fc-puzzle-msg");
69
146
  const refreshBtn = container.querySelector(".fc-puzzle-refresh");
147
+ const fgPath = container.querySelector(".fc-puzzle-path-fg");
70
148
  refreshBtn.addEventListener("click", () => render());
71
- range.addEventListener("input", async () => {
72
- if (done) return;
73
- const v = Number(range.value);
74
- piece.style.left = `${v / 100 * MAX_OFFSET}px`;
75
- if (verifyPosition(current, v)) {
76
- done = true;
77
- range.disabled = true;
78
- piece.style.left = `${gapLeft}px`;
79
- const result = {
80
- success: true,
81
- proof: await hashProof(proofInput(current)),
82
- duration: Date.now() - startTime
83
- };
84
- msg.textContent = t.success;
85
- config.onVerify?.(result);
86
- listeners.forEach((cb) => cb(result));
149
+ const totalLen = fgPath.getTotalLength();
150
+ fgPath.style.strokeDasharray = `${totalLen}`;
151
+ fgPath.style.strokeDashoffset = `${totalLen}`;
152
+ let dragging = false;
153
+ let startPointer = { x: 0, y: 0 };
154
+ let startProgress = 0;
155
+ let currentProgress = 0;
156
+ function applyProgress(p) {
157
+ currentProgress = Math.max(0, Math.min(1, p));
158
+ const pt = pointAtProgress(pts, currentProgress);
159
+ handle.style.left = `${pt.x / TRACK_W * 100}%`;
160
+ handle.style.top = `${pt.y / TRACK_H * 100}%`;
161
+ fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;
162
+ const piecePos = pieceCirclePos(currentProgress);
163
+ piece.style.left = `${piecePos.x}px`;
164
+ piece.style.top = `${piecePos.y}px`;
165
+ }
166
+ function bounceBack() {
167
+ handle.style.transition = "left .35s ease, top .35s ease, border-color .15s";
168
+ piece.style.transition = "left .35s ease, top .35s ease, background .15s, border-color .15s";
169
+ fgPath.style.transition = "stroke-dashoffset .35s ease, stroke .15s";
170
+ applyProgress(0);
171
+ setTimeout(() => {
172
+ handle.style.transition = "border-color .15s";
173
+ piece.style.transition = "background .15s, border-color .15s";
174
+ fgPath.style.transition = "stroke .15s";
175
+ }, 360);
176
+ }
177
+ applyProgress(0);
178
+ handle.addEventListener("pointerdown", (e) => {
179
+ if (done || locked) return;
180
+ dragging = true;
181
+ startPointer = { x: e.clientX, y: e.clientY };
182
+ startProgress = currentProgress;
183
+ recorder.start();
184
+ try {
185
+ handle.setPointerCapture(e.pointerId);
186
+ } catch {
87
187
  }
188
+ e.preventDefault();
88
189
  });
190
+ handle.addEventListener("pointermove", (e) => {
191
+ if (!dragging) return;
192
+ const rect = track.getBoundingClientRect();
193
+ const sx = rect.width / TRACK_W;
194
+ const sy = rect.height / TRACK_H;
195
+ const svgX = (e.clientX - rect.left) / sx;
196
+ const svgY = (e.clientY - rect.top) / sy;
197
+ let bestLen = 0;
198
+ let bestDist = Infinity;
199
+ const steps = 100;
200
+ for (let i = 0; i <= steps; i++) {
201
+ const len = i / steps * totalLen;
202
+ const p = fgPath.getPointAtLength(len);
203
+ const d = (p.x - svgX) * (p.x - svgX) + (p.y - svgY) * (p.y - svgY);
204
+ if (d < bestDist) {
205
+ bestDist = d;
206
+ bestLen = len;
207
+ }
208
+ }
209
+ applyProgress(bestLen / totalLen);
210
+ recorder.record(e.clientX, e.clientY);
211
+ });
212
+ const finish = async () => {
213
+ if (!dragging) return;
214
+ dragging = false;
215
+ const trackPoints = recorder.stop();
216
+ const piecePos = pieceCirclePos(currentProgress);
217
+ const pieceLeftPct = piecePos.x / STAGE_W * 100;
218
+ if (!verifyPosition(current, pieceLeftPct)) {
219
+ bounceBack();
220
+ msg.style.color = "var(--fc-danger)";
221
+ msg.textContent = t.fail;
222
+ return;
223
+ }
224
+ const analysis = analyzeTrack(trackPoints);
225
+ if (analysis.isBot) {
226
+ locked = true;
227
+ msg.style.color = "var(--fc-danger)";
228
+ msg.textContent = config.locale === "zh" ? `\u7591\u4F3C\u673A\u5668\u4EBA\u64CD\u4F5C\uFF08${analysis.reasons.join("\u3001")}\uFF09` : `${t.botFail} detected`;
229
+ bounceBack();
230
+ setTimeout(() => {
231
+ if (!done) render();
232
+ }, 2e3);
233
+ return;
234
+ }
235
+ done = true;
236
+ root.classList.add("fc-puzzle-done");
237
+ const result = {
238
+ success: true,
239
+ proof: await hashProof(proofInput(current)),
240
+ duration: Date.now() - startTime,
241
+ metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons }
242
+ };
243
+ msg.style.color = "var(--fc-success)";
244
+ msg.textContent = t.success;
245
+ config.onVerify?.(result);
246
+ listeners.forEach((cb) => cb(result));
247
+ };
248
+ handle.addEventListener("pointerup", finish);
249
+ handle.addEventListener("pointercancel", finish);
89
250
  }
90
251
  return {
91
252
  mount: () => render(),
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 { createPuzzleInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const puzzlePlugin: CaptchaPlugin = {\n id: 'puzzle',\n category: 'interactive',\n create: createPuzzleInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '拼图缺口' : 'Puzzle Gap',\n description: locale === 'zh'\n ? '拖动滑块把拼图块移到缺口位置'\n : 'Drag the slider to fit the puzzle piece into the gap.',\n tags: ['puzzle', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(puzzlePlugin);\n\nexport function mountPuzzle(container: HTMLElement, config: CaptchaConfig) {\n return createPuzzleInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyPosition, type PuzzleChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块把拼图块移到缺口', success: '验证成功', refresh: '刷新' },\n en: { title: 'Drag the slider to fit the puzzle', success: 'Verified', refresh: 'Refresh' },\n};\n\n// 舞台与拼图块尺寸\nconst STAGE_W = 300;\nconst PIECE_W = 50;\nconst MAX_OFFSET = STAGE_W - PIECE_W; // 拼图块可水平移动的最大距离\n\nexport function createPuzzleInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: PuzzleChallenge;\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 const gapLeft = (current.gapPosition / 100) * MAX_OFFSET;\n container.innerHTML = `\n <style>\n .fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .fc-puzzle[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-puzzle[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-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:160px;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}\n .fc-puzzle-gap{position:absolute;top:55px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15)}\n .fc-puzzle-piece{position:absolute;top:55px;left:0;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25)}\n .fc-puzzle-range{width:${STAGE_W}px;margin-top:14px;accent-color:var(--fc-accent)}\n .fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n .fc-puzzle-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;margin-top:8px;display:block;margin-left:auto;margin-right:auto}\n .fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n </style>\n <div class=\"fc-puzzle\" data-theme=\"${config.theme}\">\n <div class=\"fc-puzzle-title\">${t.title}</div>\n <div class=\"fc-puzzle-stage\">\n <div class=\"fc-puzzle-gap\" style=\"left:${gapLeft}px\"></div>\n <div class=\"fc-puzzle-piece\" style=\"left:0\"></div>\n </div>\n <input class=\"fc-puzzle-range\" type=\"range\" min=\"0\" max=\"100\" step=\"1\" value=\"0\" />\n <div class=\"fc-puzzle-msg\"></div>\n <button class=\"fc-puzzle-refresh\">${t.refresh}</button>\n </div>\n `;\n const piece = container.querySelector('.fc-puzzle-piece') as HTMLDivElement;\n const range = container.querySelector('.fc-puzzle-range') as HTMLInputElement;\n const msg = container.querySelector('.fc-puzzle-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-puzzle-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', () => render());\n\n range.addEventListener('input', async () => {\n if (done) return;\n const v = Number(range.value);\n piece.style.left = `${(v / 100) * MAX_OFFSET}px`;\n if (verifyPosition(current, v)) {\n done = true;\n range.disabled = true;\n piece.style.left = `${gapLeft}px`;\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 config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n }\n });\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 拼图缺口题目:随机缺口位置(0-100%),容差 ±5%\nexport interface PuzzleChallenge {\n // 缺口位置百分比 0-100\n gapPosition: number;\n}\n\nconst TOLERANCE = 5; // ±5%\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// 生成题目:缺口位置在 10-90 之间(避免贴边导致题目退化)\nexport function generateChallenge(): PuzzleChallenge {\n return { gapPosition: randInt(10, 90) };\n}\n\n// 构造 proof 输入串:gapPosition + ':completed'\nexport function proofInput(c: PuzzleChallenge): string {\n return `${c.gapPosition}:completed`;\n}\n\n// 校验用户拖动位置是否在容差范围内\nexport function verifyPosition(c: PuzzleChallenge, userPos: number): boolean {\n return Math.abs(userPos - c.gapPosition) <= TOLERANCE;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type PuzzleChallenge } from './challenge.js';\n\nexport function issuePuzzleChallenge(): { challenge: PuzzleChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { gapPosition: challenge.gapPosition } };\n}\n\nexport async function verifyPuzzleProof(\n challenge: PuzzleChallenge,\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"]}
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 { createPuzzleInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const puzzlePlugin: CaptchaPlugin = {\n id: 'puzzle',\n category: 'interactive',\n create: createPuzzleInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '拼图缺口' : 'Puzzle Gap',\n description: locale === 'zh'\n ? '拖动滑块把拼图块移到缺口位置'\n : 'Drag the slider to fit the puzzle piece into the gap.',\n tags: ['puzzle', 'drag', 'interactive'],\n }),\n};\n\ndefineCaptcha(puzzlePlugin);\n\nexport function mountPuzzle(container: HTMLElement, config: CaptchaConfig) {\n return createPuzzleInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof, TrackRecorder, analyzeTrack } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, verifyPosition, type PuzzleChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '拖动滑块把拼图块移到缺口', success: '验证成功', fail: '未对齐缺口', botFail: '检测到异常操作', refresh: '刷新' },\n en: { title: 'Drag the slider to fit the puzzle', success: 'Verified', fail: 'Not aligned', botFail: 'Bot-like behavior', refresh: 'Refresh' },\n};\n\n// 舞台与拼图块尺寸\nconst STAGE_W = 300;\nconst STAGE_H = 200;\nconst PIECE_W = 50;\nconst MAX_OFFSET = STAGE_W - PIECE_W; // 拼图块可水平移动的最大距离\n\n// 直角转弯轨道尺寸(S 形折返路径)\nconst TRACK_W = 300;\nconst TRACK_H = 100;\nconst HANDLE_W = 40;\nconst HANDLE_H = 40;\n\n// S 形直角转弯路径点(3 段:右 -> 下 -> 左 -> 下 -> 右)\n// 这是一个\"回\"字形折返路径,从左上角出发,到右下角终点\nfunction zigzagPoints(): { x: number; y: number }[] {\n const pts: { x: number; y: number }[] = [];\n const margin = HANDLE_W / 2;\n const w = TRACK_W - margin * 2;\n const h = TRACK_H - margin * 2;\n // 段 1: 从左到右 (y = margin)\n const seg1Steps = 30;\n for (let i = 0; i <= seg1Steps; i++) {\n pts.push({ x: margin + (w / seg1Steps) * i, y: margin });\n }\n // 段 2: 从右到下 (x = TRACK_W - margin, y 增长到中间)\n const seg2Steps = 10;\n const midY = margin + h / 4;\n for (let i = 1; i <= seg2Steps; i++) {\n pts.push({ x: TRACK_W - margin, y: margin + (midY - margin) / seg2Steps * i });\n }\n // 段 3: 从右到左 (y = midY)\n for (let i = 1; i <= seg1Steps; i++) {\n pts.push({ x: TRACK_W - margin - (w / seg1Steps) * i, y: midY });\n }\n // 段 4: 从左到下 (x = margin, y 增长到 3/4)\n const midY2 = margin + (h * 3) / 4;\n for (let i = 1; i <= seg2Steps; i++) {\n pts.push({ x: margin, y: midY + (midY2 - midY) / seg2Steps * i });\n }\n // 段 5: 从左到右 (y = midY2)\n for (let i = 1; i <= seg1Steps; i++) {\n pts.push({ x: margin + (w / seg1Steps) * i, y: midY2 });\n }\n // 段 6: 从右到下终点 (x = TRACK_W - margin, y 到底)\n for (let i = 1; i <= seg2Steps; i++) {\n pts.push({ x: TRACK_W - margin, y: midY2 + (TRACK_H - margin - midY2) / seg2Steps * i });\n }\n return pts;\n}\n\nfunction zigzagPath(pts: { x: number; y: number }[]): string {\n return pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(' ');\n}\n\nfunction pointAtProgress(pts: { x: number; y: number }[], progress: number): { x: number; y: number } {\n const idx = Math.min(pts.length - 1, Math.max(0, Math.round(progress * (pts.length - 1))));\n return pts[idx]!;\n}\n\n// piece 的运动轨迹:圆形(与 handle 进度反向)\n// progress 0 -> 1 时,piece 沿圆周从 0° 转到 -360°(逆时针)\nfunction pieceCirclePos(progress: number): { x: number; y: number } {\n const cx = STAGE_W / 2;\n const cy = STAGE_H / 2;\n const radius = Math.min(STAGE_W, STAGE_H) / 2 - PIECE_W / 2 - 8;\n // piece 反向运动:进度增加时角度减小\n const angle = -progress * Math.PI * 2;\n return {\n x: cx + radius * Math.cos(angle) - PIECE_W / 2,\n y: cy + radius * Math.sin(angle) - PIECE_W / 2,\n };\n}\n\nexport function createPuzzleInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n const recorder = new TrackRecorder();\n let current: PuzzleChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let done = false;\n let locked = false;\n\n function render() {\n current = generateChallenge();\n startTime = Date.now();\n done = false;\n locked = false;\n recorder.clear();\n const gapLeft = (current.gapPosition / 100) * MAX_OFFSET;\n const pts = zigzagPoints();\n const pathD = zigzagPath(pts);\n container.innerHTML = `\n <style>\n .fc-puzzle{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .fc-puzzle[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-puzzle[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-puzzle{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-puzzle-title{font-size:14px;color:var(--fc-text);margin-bottom:12px;text-align:center}\n .fc-puzzle-stage{position:relative;width:${STAGE_W}px;height:${STAGE_H}px;margin:0 auto;border-radius:8px;overflow:hidden;border:1px solid var(--fc-border);background:linear-gradient(135deg,var(--fc-accent-soft),var(--fc-surface) 60%,var(--fc-accent-soft))}\n .fc-puzzle-gap{position:absolute;top:${(STAGE_H - PIECE_W) / 2}px;width:${PIECE_W}px;height:${PIECE_W}px;background:var(--fc-bg);border:2px dashed var(--fc-text-soft);border-radius:6px;box-shadow:inset 0 0 8px rgba(0,0,0,.15);transition:left .15s}\n .fc-puzzle-piece{position:absolute;width:${PIECE_W}px;height:${PIECE_W}px;border-radius:6px;background:linear-gradient(135deg,var(--fc-accent),var(--fc-accent-soft));border:2px solid var(--fc-bg);box-shadow:0 2px 6px rgba(0,0,0,.25);transition:background .15s,border-color .15s}\n .fc-puzzle-track{position:relative;width:${TRACK_W}px;height:${TRACK_H}px;margin:14px auto 0;background:var(--fc-surface);border:1px solid var(--fc-border);border-radius:8px;overflow:hidden}\n .fc-puzzle-svg{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}\n .fc-puzzle-path-bg{fill:none;stroke:var(--fc-border);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}\n .fc-puzzle-path-fg{fill:none;stroke:var(--fc-accent);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;transition:stroke .15s}\n .fc-puzzle-handle{position:absolute;width:${HANDLE_W}px;height:${HANDLE_H}px;background:var(--fc-bg);border:2px solid var(--fc-accent);border-radius:50%;cursor:grab;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(0,0,0,.2);user-select:none;touch-action:none;transition:border-color .15s;transform:translate(-50%,-50%);z-index:2}\n .fc-puzzle-handle:active{cursor:grabbing}\n .fc-puzzle-handle::after{content:'\\\\2192';color:var(--fc-accent);font-size:16px;font-weight:bold;transition:color .15s}\n .fc-puzzle-msg{font-size:13px;min-height:18px;text-align:center;margin-top:10px;color:var(--fc-success)}\n .fc-puzzle-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;margin-top:8px;display:block;margin-left:auto;margin-right:auto}\n .fc-puzzle-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n .fc-puzzle-done .fc-puzzle-piece{background:linear-gradient(135deg,var(--fc-success),var(--fc-accent-soft));border-color:var(--fc-success)}\n .fc-puzzle-done .fc-puzzle-handle{border-color:var(--fc-success)}\n .fc-puzzle-done .fc-puzzle-handle::after{color:var(--fc-success)}\n .fc-puzzle-done .fc-puzzle-path-fg{stroke:var(--fc-success)}\n </style>\n <div class=\"fc-puzzle\" data-theme=\"${theme}\">\n <div class=\"fc-puzzle-title\">${t.title}</div>\n <div class=\"fc-puzzle-stage\">\n <div class=\"fc-puzzle-gap\" style=\"left:${gapLeft}px\"></div>\n <div class=\"fc-puzzle-piece\" style=\"left:0;top:${(STAGE_H - PIECE_W) / 2}px\"></div>\n </div>\n <div class=\"fc-puzzle-track\">\n <svg class=\"fc-puzzle-svg\" viewBox=\"0 0 ${TRACK_W} ${TRACK_H}\">\n <path class=\"fc-puzzle-path-bg\" d=\"${pathD}\"></path>\n <path class=\"fc-puzzle-path-fg\" d=\"${pathD}\"></path>\n </svg>\n <div class=\"fc-puzzle-handle\" role=\"slider\" tabindex=\"0\"></div>\n </div>\n <div class=\"fc-puzzle-msg\"></div>\n <button class=\"fc-puzzle-refresh\">${t.refresh}</button>\n </div>\n `;\n const root = container.querySelector('.fc-puzzle') as HTMLDivElement;\n const piece = container.querySelector('.fc-puzzle-piece') as HTMLDivElement;\n const track = container.querySelector('.fc-puzzle-track') as HTMLDivElement;\n const handle = container.querySelector('.fc-puzzle-handle') as HTMLDivElement;\n const msg = container.querySelector('.fc-puzzle-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-puzzle-refresh') as HTMLButtonElement;\n const fgPath = container.querySelector('.fc-puzzle-path-fg') as SVGPathElement;\n refreshBtn.addEventListener('click', () => render());\n\n const totalLen = fgPath.getTotalLength();\n fgPath.style.strokeDasharray = `${totalLen}`;\n fgPath.style.strokeDashoffset = `${totalLen}`;\n\n let dragging = false;\n let startPointer = { x: 0, y: 0 };\n let startProgress = 0;\n let currentProgress = 0;\n\n function applyProgress(p: number) {\n currentProgress = Math.max(0, Math.min(1, p));\n // handle 沿直角转弯轨道移动\n const pt = pointAtProgress(pts, currentProgress);\n handle.style.left = `${(pt.x / TRACK_W) * 100}%`;\n handle.style.top = `${(pt.y / TRACK_H) * 100}%`;\n fgPath.style.strokeDashoffset = `${totalLen * (1 - currentProgress)}`;\n // piece 沿圆形路径运动(与进度反向)\n const piecePos = pieceCirclePos(currentProgress);\n piece.style.left = `${piecePos.x}px`;\n piece.style.top = `${piecePos.y}px`;\n }\n\n function bounceBack() {\n handle.style.transition = 'left .35s ease, top .35s ease, border-color .15s';\n piece.style.transition = 'left .35s ease, top .35s ease, background .15s, border-color .15s';\n fgPath.style.transition = 'stroke-dashoffset .35s ease, stroke .15s';\n applyProgress(0);\n setTimeout(() => {\n handle.style.transition = 'border-color .15s';\n piece.style.transition = 'background .15s, border-color .15s';\n fgPath.style.transition = 'stroke .15s';\n }, 360);\n }\n\n applyProgress(0);\n\n handle.addEventListener('pointerdown', (e) => {\n if (done || locked) return;\n dragging = true;\n startPointer = { x: e.clientX, y: e.clientY };\n startProgress = currentProgress;\n recorder.start();\n try { handle.setPointerCapture(e.pointerId); } catch { /* ignore */ }\n e.preventDefault();\n });\n\n handle.addEventListener('pointermove', (e) => {\n if (!dragging) return;\n // S 形路径不自交叉,全路径最近点搜索即可稳定跟手\n // 实时获取 track 坐标(避免页面滚动后坐标过时)\n const rect = track.getBoundingClientRect();\n const sx = rect.width / TRACK_W;\n const sy = rect.height / TRACK_H;\n const svgX = (e.clientX - rect.left) / sx;\n const svgY = (e.clientY - rect.top) / sy;\n let bestLen = 0;\n let bestDist = Infinity;\n const steps = 100;\n for (let i = 0; i <= steps; i++) {\n const len = (i / steps) * totalLen;\n const p = fgPath.getPointAtLength(len);\n const d = (p.x - svgX) * (p.x - svgX) + (p.y - svgY) * (p.y - svgY);\n if (d < bestDist) { bestDist = d; bestLen = len; }\n }\n applyProgress(bestLen / totalLen);\n recorder.record(e.clientX, e.clientY);\n });\n\n const finish = async () => {\n if (!dragging) return;\n dragging = false;\n const trackPoints = recorder.stop();\n\n // piece 当前的水平百分比位置(用于校验是否对齐缺口)\n // piece 走圆形,其对齐缺口的位置是圆形上 x 最接近 gapLeft 的点\n // 简化:当 progress 对应的 piece x 位置在缺口容差内即通过\n const piecePos = pieceCirclePos(currentProgress);\n const pieceLeftPct = (piecePos.x / STAGE_W) * 100;\n if (!verifyPosition(current, pieceLeftPct)) {\n bounceBack();\n msg.style.color = 'var(--fc-danger)';\n msg.textContent = t.fail;\n return;\n }\n\n const analysis = analyzeTrack(trackPoints);\n\n if (analysis.isBot) {\n locked = true;\n msg.style.color = 'var(--fc-danger)';\n msg.textContent = config.locale === 'zh'\n ? `疑似机器人操作(${analysis.reasons.join('、')})`\n : `${t.botFail} detected`;\n bounceBack();\n setTimeout(() => { if (!done) render(); }, 2000);\n return;\n }\n\n done = true;\n root.classList.add('fc-puzzle-done');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(proofInput(current)),\n duration: Date.now() - startTime,\n metadata: { humanScore: analysis.humanScore, reasons: analysis.reasons },\n };\n msg.style.color = 'var(--fc-success)';\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n };\n\n handle.addEventListener('pointerup', finish);\n handle.addEventListener('pointercancel', finish);\n }\n\n return {\n mount: () => render(),\n reset: () => render(),\n destroy: () => { container.innerHTML = ''; listeners = []; },\n onResult: cb => listeners.push(cb),\n };\n}\n","// 拼图缺口题目:随机缺口位置(0-100%),容差 ±5%\nexport interface PuzzleChallenge {\n // 缺口位置百分比 0-100\n gapPosition: number;\n}\n\nconst TOLERANCE = 8; // ±8%(圆形路径更难精确对齐)\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// 生成题目:缺口位置限制在 piece 圆形路径可达范围内\n// piece x = cx + radius*cos(angle) - PIECE_W/2,百分比范围约 19%-64%\n// gap 限制在 25-58 确保 piece 一定能对齐\nexport function generateChallenge(): PuzzleChallenge {\n return { gapPosition: randInt(25, 58) };\n}\n\n// 构造 proof 输入串:gapPosition + ':completed'\nexport function proofInput(c: PuzzleChallenge): string {\n return `${c.gapPosition}:completed`;\n}\n\n// 校验用户拖动位置是否在容差范围内\nexport function verifyPosition(c: PuzzleChallenge, userPos: number): boolean {\n return Math.abs(userPos - c.gapPosition) <= TOLERANCE;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, proofInput, type PuzzleChallenge } from './challenge.js';\n\nexport function issuePuzzleChallenge(): { challenge: PuzzleChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { gapPosition: challenge.gapPosition } };\n}\n\nexport async function verifyPuzzleProof(\n challenge: PuzzleChallenge,\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/puzzle",
3
- "version": "0.3.0",
3
+ "version": "0.6.0",
4
4
  "description": "Puzzle 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.3.0"
18
+ "@funnycaptcha/core": "0.6.0"
19
19
  },
20
20
  "publishConfig": {
21
21
  "access": "public",