@funnycaptcha/meme-quiz 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
@@ -39,11 +39,12 @@ function verifyAnswer(c, userPick) {
39
39
 
40
40
  // src/render.ts
41
41
  var STR = {
42
- zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898" },
43
- en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again" }
42
+ zh: { title: "\u6897\u56FE\u95EE\u7B54", hint: "\u9009\u51FA\u6B63\u786E\u7684\u6897\u540D", success: "\u9A8C\u8BC1\u6210\u529F", fail: "\u7B54\u9519\u4E86\uFF0C\u6362\u4E00\u9898", refresh: "\u5237\u65B0" },
43
+ en: { title: "Meme Quiz", hint: "Pick the correct meme name", success: "Verified", fail: "Wrong, try again", refresh: "Refresh" }
44
44
  };
45
45
  function createMemeQuizInstance(container, config) {
46
46
  const t = STR[config.locale];
47
+ const theme = config.theme ?? "light";
47
48
  let current;
48
49
  let listeners = [];
49
50
  let startTime = Date.now();
@@ -57,26 +58,38 @@ function createMemeQuizInstance(container, config) {
57
58
  ).join("");
58
59
  container.innerHTML = `
59
60
  <style>
60
- .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}
61
- .fc-meme-quiz-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}
62
- .fc-meme-quiz-hint{font-size:12px;color:#888;margin-bottom:10px;text-align:center}
63
- .fc-meme-quiz-q{font-size:24px;color:#4a90d9;margin-bottom:14px;text-align:center;letter-spacing:2px}
61
+ .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}
62
+ .fc-meme-quiz[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}
63
+ .fc-meme-quiz[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}
64
+ .fc-meme-quiz{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}
65
+ .fc-meme-quiz-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}
66
+ .fc-meme-quiz-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}
67
+ .fc-meme-quiz-q{font-size:24px;color:var(--fc-accent);margin-bottom:14px;text-align:center;letter-spacing:2px}
64
68
  .fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}
65
- .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid #e0e0e0;background:#fff;border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:#333}
66
- .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:#4a90d9}
67
- .fc-meme-quiz-opt-wrong{border-color:#e53935;background:#fdecea}
68
- .fc-meme-quiz-opt-correct{border-color:#2e7d32;background:#e8f5e9}
69
- .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}
69
+ .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:var(--fc-text)}
70
+ .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:var(--fc-accent)}
71
+ .fc-meme-quiz-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}
72
+ .fc-meme-quiz-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}
73
+ .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}
74
+ .fc-meme-quiz-msg-ok{color:var(--fc-success)}
75
+ .fc-meme-quiz-row{display:flex;justify-content:center;margin-top:8px}
76
+ .fc-meme-quiz-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}
77
+ .fc-meme-quiz-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}
70
78
  </style>
71
- <div class="fc-meme-quiz">
79
+ <div class="fc-meme-quiz" data-theme="${theme}">
72
80
  <div class="fc-meme-quiz-title">${t.title}</div>
73
81
  <div class="fc-meme-quiz-hint">${t.hint}</div>
74
82
  <div class="fc-meme-quiz-q">${current.question}</div>
75
83
  <div class="fc-meme-quiz-opts">${buttons}</div>
76
84
  <div class="fc-meme-quiz-msg"></div>
85
+ <div class="fc-meme-quiz-row">
86
+ <button class="fc-meme-quiz-refresh">${t.refresh}</button>
87
+ </div>
77
88
  </div>
78
89
  `;
79
90
  const msg = container.querySelector(".fc-meme-quiz-msg");
91
+ const refreshBtn = container.querySelector(".fc-meme-quiz-refresh");
92
+ refreshBtn.addEventListener("click", render);
80
93
  container.querySelectorAll(".fc-meme-quiz-opt").forEach((el) => {
81
94
  el.addEventListener("click", async () => {
82
95
  if (finished) return;
@@ -90,7 +103,7 @@ function createMemeQuizInstance(container, config) {
90
103
  proof: await hashProof(current.correct),
91
104
  duration: Date.now() - startTime
92
105
  };
93
- msg.style.color = "#2e7d32";
106
+ msg.classList.add("fc-meme-quiz-msg-ok");
94
107
  msg.textContent = t.success;
95
108
  config.onVerify?.(result);
96
109
  listeners.forEach((cb) => cb(result));
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 { createMemeQuizInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const memeQuizPlugin: CaptchaPlugin = {\n id: 'meme-quiz',\n category: 'creative',\n create: createMemeQuizInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '梗图问答' : 'Meme Quiz',\n description: locale === 'zh'\n ? '看 emoji 梗描述,选出正确的梗名'\n : 'Read the emoji meme and pick the correct name.',\n tags: ['meme', 'quiz', 'creative'],\n }),\n};\n\ndefineCaptcha(memeQuizPlugin);\n\nexport function mountMemeQuiz(container: HTMLElement, config: CaptchaConfig) {\n return createMemeQuizInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, verifyAnswer, type MemeQuizChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '梗图问答', hint: '选出正确的梗名', success: '验证成功', fail: '答错了,换一题' },\n en: { title: 'Meme Quiz', hint: 'Pick the correct meme name', success: 'Verified', fail: 'Wrong, try again' },\n};\n\nexport function createMemeQuizInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n let current: MemeQuizChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let finished = false;\n\n function render() {\n current = generateChallenge();\n finished = false;\n startTime = Date.now();\n const buttons = current.options.map(\n (o, i) => `<button class=\"fc-meme-quiz-opt\" data-idx=\"${i}\">${o}</button>`,\n ).join('');\n container.innerHTML = `\n <style>\n .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;width:320px;padding:16px;box-sizing:border-box}\n .fc-meme-quiz-title{font-size:14px;color:#333;margin-bottom:6px;text-align:center;font-weight:600}\n .fc-meme-quiz-hint{font-size:12px;color:#888;margin-bottom:10px;text-align:center}\n .fc-meme-quiz-q{font-size:24px;color:#4a90d9;margin-bottom:14px;text-align:center;letter-spacing:2px}\n .fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid #e0e0e0;background:#fff;border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:#333}\n .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:#4a90d9}\n .fc-meme-quiz-opt-wrong{border-color:#e53935;background:#fdecea}\n .fc-meme-quiz-opt-correct{border-color:#2e7d32;background:#e8f5e9}\n .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:#e53935}\n </style>\n <div class=\"fc-meme-quiz\">\n <div class=\"fc-meme-quiz-title\">${t.title}</div>\n <div class=\"fc-meme-quiz-hint\">${t.hint}</div>\n <div class=\"fc-meme-quiz-q\">${current.question}</div>\n <div class=\"fc-meme-quiz-opts\">${buttons}</div>\n <div class=\"fc-meme-quiz-msg\"></div>\n </div>\n `;\n const msg = container.querySelector('.fc-meme-quiz-msg') as HTMLDivElement;\n\n container.querySelectorAll('.fc-meme-quiz-opt').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const btn = el as HTMLButtonElement;\n const pick = btn.textContent ?? '';\n if (verifyAnswer(current, pick)) {\n finished = true;\n btn.classList.add('fc-meme-quiz-opt-correct');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(current.correct),\n duration: Date.now() - startTime,\n };\n msg.style.color = '#2e7d32';\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 答错:高亮错误后换题\n finished = true;\n btn.classList.add('fc-meme-quiz-opt-wrong');\n msg.textContent = t.fail;\n setTimeout(render, 800);\n }\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","// 梗图问答题目:根据 emoji 梗描述从 4 个选项中选出正确的梗名\nexport interface MemeQuizChallenge {\n // 问题(emoji 梗描述)\n question: string;\n // 正确答案\n correct: string;\n // 4 个备选项(含正确答案)\n options: string[];\n}\n\n// 预设题库(中文梗,每题 4 选项)\ninterface MemeItem {\n question: string;\n correct: string;\n options: string[];\n}\n\nconst BANK: MemeItem[] = [\n { question: '🐱+🍞 = ?', correct: '猫面包', options: ['猫面包', '狗饼干', '鸟蛋糕', '鱼馒头'] },\n { question: '🐊+🦛 = ?', correct: '鳄马', options: ['鳄马', '鲸象', '蛇鹿', '鲨牛'] },\n { question: '🐶+🌭 = ?', correct: '狗热狗', options: ['狗热狗', '猫汉堡', '鸟披萨', '鱼寿司'] },\n { question: '🦆+🧅 = ?', correct: '鸭葱', options: ['鸭葱', '鸡蒜', '鹅姜', '鹅韭'] },\n { question: '🐍+📱 = ?', correct: '蛇机', options: ['蛇机', '龙脑', '虎屏', '猫鼠'] },\n];\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:从题库随机选一题,选项打乱顺序\nexport function generateChallenge(): MemeQuizChallenge {\n const item = BANK[randInt(0, BANK.length - 1)]!;\n return {\n question: item.question,\n correct: item.correct,\n options: shuffle(item.options),\n };\n}\n\n// 校验用户选择是否正确\nexport function verifyAnswer(c: MemeQuizChallenge, userPick: string): boolean {\n return c.correct === userPick;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, type MemeQuizChallenge } from './challenge.js';\n\n// 服务端签发题目\nexport function issueMemeQuizChallenge(): { challenge: MemeQuizChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { question: challenge.question, options: challenge.options } };\n}\n\n// 服务端校验 proof\nexport async function verifyMemeQuizProof(\n challenge: MemeQuizChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(challenge.correct);\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 { createMemeQuizInstance } from './render.js';\n\nexport * from './challenge.js';\nexport * from './verifier.js';\n\nexport const memeQuizPlugin: CaptchaPlugin = {\n id: 'meme-quiz',\n category: 'creative',\n create: createMemeQuizInstance,\n describe: (locale) => ({\n name: locale === 'zh' ? '梗图问答' : 'Meme Quiz',\n description: locale === 'zh'\n ? '看 emoji 梗描述,选出正确的梗名'\n : 'Read the emoji meme and pick the correct name.',\n tags: ['meme', 'quiz', 'creative'],\n }),\n};\n\ndefineCaptcha(memeQuizPlugin);\n\nexport function mountMemeQuiz(container: HTMLElement, config: CaptchaConfig) {\n return createMemeQuizInstance(container, config);\n}\n","import type { CaptchaConfig, CaptchaInstance, CaptchaResult } from '@funnycaptcha/core';\nimport { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, verifyAnswer, type MemeQuizChallenge } from './challenge.js';\n\nconst STR = {\n zh: { title: '梗图问答', hint: '选出正确的梗名', success: '验证成功', fail: '答错了,换一题', refresh: '刷新' },\n en: { title: 'Meme Quiz', hint: 'Pick the correct meme name', success: 'Verified', fail: 'Wrong, try again', refresh: 'Refresh' },\n};\n\nexport function createMemeQuizInstance(\n container: HTMLElement,\n config: CaptchaConfig,\n): CaptchaInstance {\n const t = STR[config.locale];\n const theme = config.theme ?? 'light';\n let current: MemeQuizChallenge;\n let listeners: ((r: CaptchaResult) => void)[] = [];\n let startTime = Date.now();\n let finished = false;\n\n function render() {\n current = generateChallenge();\n finished = false;\n startTime = Date.now();\n const buttons = current.options.map(\n (o, i) => `<button class=\"fc-meme-quiz-opt\" data-idx=\"${i}\">${o}</button>`,\n ).join('');\n container.innerHTML = `\n <style>\n .fc-meme-quiz{font-family:-apple-system,system-ui,sans-serif;max-width:360px;width:100%;padding:16px;box-sizing:border-box}\n .fc-meme-quiz[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-meme-quiz[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-meme-quiz{background:var(--fc-bg);border:1px solid var(--fc-border);border-radius:10px;color:var(--fc-text)}\n .fc-meme-quiz-title{font-size:14px;color:var(--fc-text);margin-bottom:6px;text-align:center;font-weight:600}\n .fc-meme-quiz-hint{font-size:12px;color:var(--fc-text-soft);margin-bottom:10px;text-align:center}\n .fc-meme-quiz-q{font-size:24px;color:var(--fc-accent);margin-bottom:14px;text-align:center;letter-spacing:2px}\n .fc-meme-quiz-opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n .fc-meme-quiz-opt{font-size:15px;height:48px;border:2px solid var(--fc-border);background:var(--fc-bg);border-radius:8px;cursor:pointer;transition:transform .12s,border-color .12s,background .12s;color:var(--fc-text)}\n .fc-meme-quiz-opt:hover{transform:scale(1.03);border-color:var(--fc-accent)}\n .fc-meme-quiz-opt-wrong{border-color:var(--fc-danger);background:var(--fc-danger)}\n .fc-meme-quiz-opt-correct{border-color:var(--fc-success);background:var(--fc-accent-soft)}\n .fc-meme-quiz-msg{font-size:13px;min-height:18px;text-align:center;margin-top:12px;color:var(--fc-danger)}\n .fc-meme-quiz-msg-ok{color:var(--fc-success)}\n .fc-meme-quiz-row{display:flex;justify-content:center;margin-top:8px}\n .fc-meme-quiz-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-meme-quiz-refresh:hover{border-color:var(--fc-accent);color:var(--fc-accent)}\n </style>\n <div class=\"fc-meme-quiz\" data-theme=\"${theme}\">\n <div class=\"fc-meme-quiz-title\">${t.title}</div>\n <div class=\"fc-meme-quiz-hint\">${t.hint}</div>\n <div class=\"fc-meme-quiz-q\">${current.question}</div>\n <div class=\"fc-meme-quiz-opts\">${buttons}</div>\n <div class=\"fc-meme-quiz-msg\"></div>\n <div class=\"fc-meme-quiz-row\">\n <button class=\"fc-meme-quiz-refresh\">${t.refresh}</button>\n </div>\n </div>\n `;\n const msg = container.querySelector('.fc-meme-quiz-msg') as HTMLDivElement;\n const refreshBtn = container.querySelector('.fc-meme-quiz-refresh') as HTMLButtonElement;\n refreshBtn.addEventListener('click', render);\n\n container.querySelectorAll('.fc-meme-quiz-opt').forEach(el => {\n el.addEventListener('click', async () => {\n if (finished) return;\n const btn = el as HTMLButtonElement;\n const pick = btn.textContent ?? '';\n if (verifyAnswer(current, pick)) {\n finished = true;\n btn.classList.add('fc-meme-quiz-opt-correct');\n const result: CaptchaResult = {\n success: true,\n proof: await hashProof(current.correct),\n duration: Date.now() - startTime,\n };\n msg.classList.add('fc-meme-quiz-msg-ok');\n msg.textContent = t.success;\n config.onVerify?.(result);\n listeners.forEach(cb => cb(result));\n } else {\n // 答错:高亮错误后换题\n finished = true;\n btn.classList.add('fc-meme-quiz-opt-wrong');\n msg.textContent = t.fail;\n setTimeout(render, 800);\n }\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","// 梗图问答题目:根据 emoji 梗描述从 4 个选项中选出正确的梗名\nexport interface MemeQuizChallenge {\n // 问题(emoji 梗描述)\n question: string;\n // 正确答案\n correct: string;\n // 4 个备选项(含正确答案)\n options: string[];\n}\n\n// 预设题库(中文梗,每题 4 选项)\ninterface MemeItem {\n question: string;\n correct: string;\n options: string[];\n}\n\nconst BANK: MemeItem[] = [\n { question: '🐱+🍞 = ?', correct: '猫面包', options: ['猫面包', '狗饼干', '鸟蛋糕', '鱼馒头'] },\n { question: '🐊+🦛 = ?', correct: '鳄马', options: ['鳄马', '鲸象', '蛇鹿', '鲨牛'] },\n { question: '🐶+🌭 = ?', correct: '狗热狗', options: ['狗热狗', '猫汉堡', '鸟披萨', '鱼寿司'] },\n { question: '🦆+🧅 = ?', correct: '鸭葱', options: ['鸭葱', '鸡蒜', '鹅姜', '鹅韭'] },\n { question: '🐍+📱 = ?', correct: '蛇机', options: ['蛇机', '龙脑', '虎屏', '猫鼠'] },\n];\n\nfunction randInt(min: number, max: number): number {\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\n// Fisher-Yates 洗牌\nfunction shuffle<T>(arr: readonly T[]): T[] {\n const a = arr.slice();\n for (let i = a.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = a[i]!;\n a[i] = a[j]!;\n a[j] = tmp;\n }\n return a;\n}\n\n// 生成题目:从题库随机选一题,选项打乱顺序\nexport function generateChallenge(): MemeQuizChallenge {\n const item = BANK[randInt(0, BANK.length - 1)]!;\n return {\n question: item.question,\n correct: item.correct,\n options: shuffle(item.options),\n };\n}\n\n// 校验用户选择是否正确\nexport function verifyAnswer(c: MemeQuizChallenge, userPick: string): boolean {\n return c.correct === userPick;\n}\n","import { hashProof } from '@funnycaptcha/core';\nimport { generateChallenge, type MemeQuizChallenge } from './challenge.js';\n\n// 服务端签发题目\nexport function issueMemeQuizChallenge(): { challenge: MemeQuizChallenge; payload: unknown } {\n const challenge = generateChallenge();\n return { challenge, payload: { question: challenge.question, options: challenge.options } };\n}\n\n// 服务端校验 proof\nexport async function verifyMemeQuizProof(\n challenge: MemeQuizChallenge,\n proof: string,\n): Promise<boolean> {\n const expected = await hashProof(challenge.correct);\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/meme-quiz",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Meme-quiz 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",