From 6eeb292fd08b942320abb3b3ff5506710600da78 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 16:53:58 -0400 Subject: [PATCH 01/19] ext(affordances): seed shared/form-affordances/ + barrel test --- .../src/shared/form-affordances/__tests__/index.test.ts | 8 ++++++++ extension/src/shared/form-affordances/index.ts | 5 +++++ 2 files changed, 13 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/index.test.ts create mode 100644 extension/src/shared/form-affordances/index.ts diff --git a/extension/src/shared/form-affordances/__tests__/index.test.ts b/extension/src/shared/form-affordances/__tests__/index.test.ts new file mode 100644 index 0000000..297e359 --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/index.test.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest'; +import * as affordances from '../index'; + +describe('form-affordances barrel', () => { + it('exports nothing yet but the module loads', () => { + expect(typeof affordances).toBe('object'); + }); +}); diff --git a/extension/src/shared/form-affordances/index.ts b/extension/src/shared/form-affordances/index.ts new file mode 100644 index 0000000..ab37fc7 --- /dev/null +++ b/extension/src/shared/form-affordances/index.ts @@ -0,0 +1,5 @@ +/// Shared form affordance modules. Each named export wires one family of +/// smart-input behavior (url, group, password, totp, notes) into a mounted +/// form element. Wired by `popup/components/types/login.ts` after the form +/// HTML is rendered. +export {}; From f872ab5183807d38a3d042cde7de7fa9500423d8 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 16:57:18 -0400 Subject: [PATCH 02/19] ext(sw): add get_active_tab_url popup handler --- .../router/__tests__/router.test.ts | 33 +++++++++++++++++++ .../src/service-worker/router/popup-only.ts | 13 ++++++++ extension/src/shared/messages.ts | 3 +- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/extension/src/service-worker/router/__tests__/router.test.ts b/extension/src/service-worker/router/__tests__/router.test.ts index b00b0e1..f503b61 100644 --- a/extension/src/service-worker/router/__tests__/router.test.ts +++ b/extension/src/service-worker/router/__tests__/router.test.ts @@ -921,3 +921,36 @@ describe('parse_lastpass_csv / import_lastpass_commit sender check', () => { expect(result).toEqual({ ok: false, error: 'unauthorized_sender' }); }); }); + +// --- get_active_tab_url --- + +describe('get_active_tab_url', () => { + it('get_active_tab_url returns active tab url + title', async () => { + // happy-dom does not provide chrome.tabs; stub it. + (globalThis as any).chrome = { + ...((globalThis as any).chrome ?? {}), + tabs: { + query: (q: any, cb: (tabs: any[]) => void) => { + cb([{ url: 'https://github.com/login', title: 'Sign in to GitHub' }]); + }, + }, + }; + const resp = await route({ type: 'get_active_tab_url' } as any, makeState(), makePopupSender()); + expect(resp.ok).toBe(true); + expect(resp.data).toEqual({ url: 'https://github.com/login', title: 'Sign in to GitHub' }); + }); + + it('get_active_tab_url returns null for chrome:// pages', async () => { + (globalThis as any).chrome = { + ...((globalThis as any).chrome ?? {}), + tabs: { + query: (q: any, cb: (tabs: any[]) => void) => { + cb([{ url: 'chrome://newtab/', title: 'New Tab' }]); + }, + }, + }; + const resp = await route({ type: 'get_active_tab_url' } as any, makeState(), makePopupSender()); + expect(resp.ok).toBe(true); + expect(resp.data).toBeNull(); + }); +}); diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index 905e949..c289be6 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -146,6 +146,19 @@ export async function handle( case 'rate_passphrase': return { ok: true, data: state.wasm.rate_passphrase(msg.passphrase) }; + case 'get_active_tab_url': { + const tabs = await new Promise((resolve) => { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, (t) => resolve(t)); + }); + const tab = tabs[0]; + if (!tab?.url) return { ok: true, data: null }; + // Filter out chrome:// and extension URLs — autofill doesn't apply. + if (/^(chrome|chrome-extension|moz-extension|edge|about|file):/i.test(tab.url)) { + return { ok: true, data: null }; + } + return { ok: true, data: { url: tab.url, title: tab.title ?? '' } }; + } + case 'generate_password': { const password = state.wasm.generate_password(JSON.stringify(msg.request)); return { ok: true, data: { password } }; diff --git a/extension/src/shared/messages.ts b/extension/src/shared/messages.ts index f5a73a7..ecbcafe 100644 --- a/extension/src/shared/messages.ts +++ b/extension/src/shared/messages.ts @@ -36,6 +36,7 @@ export type PopupMessage = | { type: 'update_vault_settings'; settings: VaultSettings } | { type: 'get_blacklist' } | { type: 'remove_blacklist'; hostname: string } + | { type: 'get_active_tab_url' } | { type: 'upload_attachment'; itemId: string; filename: string; mimeType: string; bytes: ArrayBuffer } | { type: 'download_attachment'; itemId: string; attachmentId: string } | { type: 'list_devices' } @@ -157,7 +158,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet = new Set([ 'fill_credentials', 'ack_autofill_origin', 'get_settings', 'update_settings', 'get_vault_settings', 'update_vault_settings', 'get_blacklist', - 'remove_blacklist', 'upload_attachment', 'download_attachment', + 'remove_blacklist', 'get_active_tab_url', 'upload_attachment', 'download_attachment', 'list_devices', 'add_device', 'register_this_device', 'revoke_device', 'list_trashed', 'restore_item', 'purge_item', 'purge_all_trash', 'get_field_history', From 918fdef5197e8f959d903e748ca9d003e9a8ec08 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 17:01:36 -0400 Subject: [PATCH 03/19] ext(sw): expand active-tab URL filter; isolate chrome stub in tests Expand get_active_tab_url protocol filter regex to include view-source:, data:, devtools:, and other browser-internal/extension contexts that would misbehave if autofilled. Add third regression test for view-source: URLs. Wrap get_active_tab_url tests in dedicated describe block with beforeEach/ afterEach to snapshot/restore globalThis.chrome, preventing stub leakage between tests. Co-Authored-By: Claude Opus 4.7 --- .../router/__tests__/router.test.ts | 20 ++++++++++++++++++- .../src/service-worker/router/popup-only.ts | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/extension/src/service-worker/router/__tests__/router.test.ts b/extension/src/service-worker/router/__tests__/router.test.ts index f503b61..0a41a39 100644 --- a/extension/src/service-worker/router/__tests__/router.test.ts +++ b/extension/src/service-worker/router/__tests__/router.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; // --- Mocks (must be declared before `route` is imported so the router's // `import * as vault` / `import * as session` resolve to these doubles) --- @@ -925,6 +925,10 @@ describe('parse_lastpass_csv / import_lastpass_commit sender check', () => { // --- get_active_tab_url --- describe('get_active_tab_url', () => { + let originalChrome: any; + beforeEach(() => { originalChrome = (globalThis as any).chrome; }); + afterEach(() => { (globalThis as any).chrome = originalChrome; }); + it('get_active_tab_url returns active tab url + title', async () => { // happy-dom does not provide chrome.tabs; stub it. (globalThis as any).chrome = { @@ -953,4 +957,18 @@ describe('get_active_tab_url', () => { expect(resp.ok).toBe(true); expect(resp.data).toBeNull(); }); + + it('get_active_tab_url returns null for view-source: URLs', async () => { + (globalThis as any).chrome = { + ...((globalThis as any).chrome ?? {}), + tabs: { + query: (q: any, cb: (tabs: any[]) => void) => { + cb([{ url: 'view-source:https://github.com/login', title: 'View Source' }]); + }, + }, + }; + const resp = await route({ type: 'get_active_tab_url' } as any, makeState(), makePopupSender()); + expect(resp.ok).toBe(true); + expect(resp.data).toBeNull(); + }); }); diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index c289be6..f1217e2 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -153,7 +153,7 @@ export async function handle( const tab = tabs[0]; if (!tab?.url) return { ok: true, data: null }; // Filter out chrome:// and extension URLs — autofill doesn't apply. - if (/^(chrome|chrome-extension|moz-extension|edge|about|file):/i.test(tab.url)) { + if (/^(chrome|chrome-extension|chrome-search|moz-extension|edge|edge-extension|about|file|view-source|data|devtools|javascript):/i.test(tab.url)) { return { ok: true, data: null }; } return { ok: true, data: { url: tab.url, title: tab.title ?? '' } }; From 4be0bcff83e8d9a9636bbd5ed77e3257a69d9c45 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 17:04:17 -0400 Subject: [PATCH 04/19] ext(affordances): wireFillFromTab + .glyph-btn CSS --- extension/src/popup/styles.css | 30 ++++++++++++ .../__tests__/url-tools.test.ts | 46 +++++++++++++++++++ .../src/shared/form-affordances/url-tools.ts | 24 ++++++++++ extension/src/vault/vault.css | 30 ++++++++++++ 4 files changed, 130 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/url-tools.test.ts create mode 100644 extension/src/shared/form-affordances/url-tools.ts diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 146e72a..6a35990 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -10,6 +10,7 @@ --bg-page: #0d1117; --bg-pane: #161b22; --bg-elevated: #21262d; + --bg-input: #161b22; --border-subtle: #30363d; /* Text */ @@ -1332,3 +1333,32 @@ textarea { padding: 1px 5px; border-radius: 3px; } + +/* Glyph button used by smart-input affordances. Sits inline with an input. */ +.glyph-btn { + min-width: 28px; + height: 28px; + padding: 0 6px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 3px; + color: var(--text-muted); + font-family: inherit; + font-size: 14px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.glyph-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} +.glyph-btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +.glyph-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} diff --git a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts new file mode 100644 index 0000000..7794f84 --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts @@ -0,0 +1,46 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireFillFromTab } from '../url-tools'; + +describe('wireFillFromTab', () => { + let form: HTMLElement; + let sendMessage: ReturnType; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + +
+ + +
+ `; + document.body.appendChild(form); + sendMessage = vi.fn(); + }); + + it('fills url + title from active tab on click', async () => { + sendMessage.mockResolvedValue({ ok: true, data: { url: 'https://github.com/login', title: 'GitHub' } }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#f-url') as HTMLInputElement).value).toBe('https://github.com/login'); + expect((form.querySelector('#f-title') as HTMLInputElement).value).toBe('GitHub'); + }); + + it('does not overwrite a non-empty title', async () => { + (form.querySelector('#f-title') as HTMLInputElement).value = 'My GitHub'; + sendMessage.mockResolvedValue({ ok: true, data: { url: 'https://github.com/login', title: 'GitHub' } }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#f-title') as HTMLInputElement).value).toBe('My GitHub'); + }); + + it('disables the button if SW returns null', async () => { + sendMessage.mockResolvedValue({ ok: true, data: null }); + wireFillFromTab(form, { sendMessage }); + (form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).click(); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).disabled).toBe(true); + }); +}); diff --git a/extension/src/shared/form-affordances/url-tools.ts b/extension/src/shared/form-affordances/url-tools.ts new file mode 100644 index 0000000..93e8d13 --- /dev/null +++ b/extension/src/shared/form-affordances/url-tools.ts @@ -0,0 +1,24 @@ +import { GLYPH_FILL_FROM_TAB } from '../glyphs'; + +export interface FillFromTabOpts { + sendMessage: (msg: { type: 'get_active_tab_url' }) => Promise<{ ok: boolean; data?: { url: string; title: string } | null }>; +} + +export function wireFillFromTab(form: HTMLElement, opts: FillFromTabOpts): void { + const btn = form.querySelector('#fill-from-tab-btn'); + if (!btn) return; + btn.addEventListener('click', async () => { + const resp = await opts.sendMessage({ type: 'get_active_tab_url' }); + if (!resp.ok || !resp.data) { + btn.disabled = true; + btn.title = 'no active tab'; + return; + } + const urlEl = form.querySelector('#f-url'); + const titleEl = form.querySelector('#f-title'); + if (urlEl) urlEl.value = resp.data.url; + if (titleEl && !titleEl.value.trim()) titleEl.value = resp.data.title; + }); +} + +export const FILL_FROM_TAB_BTN_HTML = ``; diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 5340a73..02763eb 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -10,6 +10,7 @@ --bg-page: #0d1117; --bg-pane: #161b22; --bg-elevated: #21262d; + --bg-input: #161b22; --border-subtle: #30363d; /* Text */ @@ -1362,3 +1363,32 @@ textarea { .vault-lock-screen__form input { text-align: center; } + +/* Glyph button used by smart-input affordances. Sits inline with an input. */ +.glyph-btn { + min-width: 28px; + height: 28px; + padding: 0 6px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 3px; + color: var(--text-muted); + font-family: inherit; + font-size: 14px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; +} +.glyph-btn:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} +.glyph-btn:focus-visible { + outline: none; + box-shadow: var(--focus-ring); +} +.glyph-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} From 8eff96da9dab079a3ad4b5b3f9fb02bf2a7e6794 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 17:54:57 -0400 Subject: [PATCH 05/19] ext(affordances): tighten FillFromTabOpts.sendMessage return type --- extension/src/shared/form-affordances/url-tools.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/src/shared/form-affordances/url-tools.ts b/extension/src/shared/form-affordances/url-tools.ts index 93e8d13..5f71646 100644 --- a/extension/src/shared/form-affordances/url-tools.ts +++ b/extension/src/shared/form-affordances/url-tools.ts @@ -1,7 +1,7 @@ import { GLYPH_FILL_FROM_TAB } from '../glyphs'; export interface FillFromTabOpts { - sendMessage: (msg: { type: 'get_active_tab_url' }) => Promise<{ ok: boolean; data?: { url: string; title: string } | null }>; + sendMessage: (msg: { type: 'get_active_tab_url' }) => Promise<{ ok: boolean; data: { url: string; title: string } | null }>; } export function wireFillFromTab(form: HTMLElement, opts: FillFromTabOpts): void { From 61dbb4d3a3102f54848de21c82af3aff92f7eb30 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 18:06:15 -0400 Subject: [PATCH 06/19] ext(affordances): wireHostnameChip with debounced URL parse --- extension/src/popup/styles.css | 23 ++++++++ .../__tests__/url-tools.test.ts | 59 ++++++++++++++++++- .../src/shared/form-affordances/url-tools.ts | 55 +++++++++++++++++ extension/src/vault/vault.css | 23 ++++++++ 4 files changed, 159 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 6a35990..5e5dcb6 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1362,3 +1362,26 @@ textarea { opacity: 0.4; cursor: not-allowed; } + +.hostname-chip-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 11px; + color: var(--text-muted); +} +.hostname-chip { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + color: #0c1118; +} +.hostname-text { + font-family: ui-monospace, monospace; +} diff --git a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts index 7794f84..0373ca8 100644 --- a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts +++ b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { wireFillFromTab } from '../url-tools'; +import { wireFillFromTab, wireHostnameChip } from '../url-tools'; describe('wireFillFromTab', () => { let form: HTMLElement; @@ -44,3 +44,60 @@ describe('wireFillFromTab', () => { expect((form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).disabled).toBe(true); }); }); + +describe('wireHostnameChip', () => { + let form: HTMLElement; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` +
+ + +
+ `; + document.body.appendChild(form); + vi.useFakeTimers(); + }); + + it('renders chip + hostname on valid URL after debounce', () => { + wireHostnameChip(form); + const input = form.querySelector('#f-url') as HTMLInputElement; + input.value = 'https://github.com/login'; + input.dispatchEvent(new Event('input')); + vi.advanceTimersByTime(250); + const row = form.querySelector('#hostname-chip-row') as HTMLElement; + expect(row.hidden).toBe(false); + expect(row.textContent).toContain('github.com'); + expect(row.querySelector('.hostname-chip')?.textContent).toBe('G'); + }); + + it('hides chip if URL is empty', () => { + wireHostnameChip(form); + const input = form.querySelector('#f-url') as HTMLInputElement; + input.value = ''; + input.dispatchEvent(new Event('input')); + vi.advanceTimersByTime(250); + expect((form.querySelector('#hostname-chip-row') as HTMLElement).hidden).toBe(true); + }); + + it('hides chip if URL does not parse', () => { + wireHostnameChip(form); + const input = form.querySelector('#f-url') as HTMLInputElement; + input.value = '!!!not-a-url'; + input.dispatchEvent(new Event('input')); + vi.advanceTimersByTime(250); + expect((form.querySelector('#hostname-chip-row') as HTMLElement).hidden).toBe(true); + }); + + it('treats scheme-less host as https://', () => { + wireHostnameChip(form); + const input = form.querySelector('#f-url') as HTMLInputElement; + input.value = 'gitlab.com/users/sign_in'; + input.dispatchEvent(new Event('input')); + vi.advanceTimersByTime(250); + const row = form.querySelector('#hostname-chip-row') as HTMLElement; + expect(row.hidden).toBe(false); + expect(row.textContent).toContain('gitlab.com'); + }); +}); diff --git a/extension/src/shared/form-affordances/url-tools.ts b/extension/src/shared/form-affordances/url-tools.ts index 5f71646..5b6b0ae 100644 --- a/extension/src/shared/form-affordances/url-tools.ts +++ b/extension/src/shared/form-affordances/url-tools.ts @@ -22,3 +22,58 @@ export function wireFillFromTab(form: HTMLElement, opts: FillFromTabOpts): void } export const FILL_FROM_TAB_BTN_HTML = ``; + +const CHIP_HUES = [ + '#5ea0c4', '#c47e5e', '#5ec47a', '#c45e9c', + '#a3c45e', '#7e5ec4', '#c4b75e', '#5ec4c4', +]; + +function hostnameHue(host: string): string { + let h = 0; + for (let i = 0; i < host.length; i++) h = (h * 31 + host.charCodeAt(i)) | 0; + return CHIP_HUES[Math.abs(h) % CHIP_HUES.length]; +} + +function tryParseHost(raw: string): string | null { + const trimmed = raw.trim(); + if (!trimmed) return null; + const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`; + try { + const u = new URL(candidate); + const host = u.host || null; + if (!host) return null; + // Validate hostname contains only valid characters + if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(host)) { + return null; + } + return host; + } catch { + return null; + } +} + +export function wireHostnameChip(form: HTMLElement): void { + const input = form.querySelector('#f-url'); + const row = form.querySelector('#hostname-chip-row'); + if (!input || !row) return; + let timer: ReturnType | null = null; + + const update = () => { + const host = tryParseHost(input.value); + if (!host) { + row.hidden = true; + row.innerHTML = ''; + return; + } + const initial = host[0]?.toUpperCase() ?? '?'; + const hue = hostnameHue(host); + row.hidden = false; + row.innerHTML = `${initial}${host}`; + }; + + input.addEventListener('input', () => { + if (timer !== null) clearTimeout(timer); + timer = setTimeout(() => { timer = null; update(); }, 200); + }); + update(); // initial render for prefilled values +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 02763eb..bb808e8 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1392,3 +1392,26 @@ textarea { opacity: 0.4; cursor: not-allowed; } + +.hostname-chip-row { + display: flex; + align-items: center; + gap: 6px; + margin-top: 4px; + font-size: 11px; + color: var(--text-muted); +} +.hostname-chip { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + color: #0c1118; +} +.hostname-text { + font-family: ui-monospace, monospace; +} From 5fbdd30a19b9327924fac18e2a6238cfedc8fbf8 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 18:08:34 -0400 Subject: [PATCH 07/19] ext(sw): add list_groups popup handler --- .../router/__tests__/router.test.ts | 28 +++++++++++++++++++ .../src/service-worker/router/popup-only.ts | 10 +++++++ extension/src/shared/messages.ts | 3 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/extension/src/service-worker/router/__tests__/router.test.ts b/extension/src/service-worker/router/__tests__/router.test.ts index 0a41a39..f4b4cf3 100644 --- a/extension/src/service-worker/router/__tests__/router.test.ts +++ b/extension/src/service-worker/router/__tests__/router.test.ts @@ -972,3 +972,31 @@ describe('get_active_tab_url', () => { expect(resp.data).toBeNull(); }); }); + +// --- list_groups --- + +describe('list_groups', () => { + it('list_groups returns deduplicated sorted groups from manifest', async () => { + const state = makeState(); + state.manifest = { + schema_version: 2, + items: { + a: { id: 'a', title: 't1', type: 'login', group: 'work', tags: [], modified: 0, created: 0, favorite: false, attachment_summaries: [] }, + b: { id: 'b', title: 't2', type: 'login', group: 'personal', tags: [], modified: 0, created: 0, favorite: false, attachment_summaries: [] }, + c: { id: 'c', title: 't3', type: 'login', group: 'work', tags: [], modified: 0, created: 0, favorite: false, attachment_summaries: [] }, + d: { id: 'd', title: 't4', type: 'login', tags: [], modified: 0, created: 0, favorite: false, attachment_summaries: [] }, // no group + }, + }; + const resp = await route({ type: 'list_groups' } as any, state, makePopupSender()); + expect(resp.ok).toBe(true); + expect(resp.data).toEqual({ groups: ['personal', 'work'] }); + }); + + it('list_groups returns empty array when manifest is null', async () => { + const state = makeState(); + state.manifest = null; + const resp = await route({ type: 'list_groups' } as any, state, makePopupSender()); + expect(resp.ok).toBe(true); + expect(resp.data).toEqual({ groups: [] }); + }); +}); diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index f1217e2..55842db 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -159,6 +159,16 @@ export async function handle( return { ok: true, data: { url: tab.url, title: tab.title ?? '' } }; } + case 'list_groups': { + if (!state.manifest) return { ok: true, data: { groups: [] } }; + const set = new Set(); + for (const id in state.manifest.items) { + const g = state.manifest.items[id].group; + if (g) set.add(g); + } + return { ok: true, data: { groups: Array.from(set).sort() } }; + } + case 'generate_password': { const password = state.wasm.generate_password(JSON.stringify(msg.request)); return { ok: true, data: { password } }; diff --git a/extension/src/shared/messages.ts b/extension/src/shared/messages.ts index ecbcafe..e68de37 100644 --- a/extension/src/shared/messages.ts +++ b/extension/src/shared/messages.ts @@ -37,6 +37,7 @@ export type PopupMessage = | { type: 'get_blacklist' } | { type: 'remove_blacklist'; hostname: string } | { type: 'get_active_tab_url' } + | { type: 'list_groups' } | { type: 'upload_attachment'; itemId: string; filename: string; mimeType: string; bytes: ArrayBuffer } | { type: 'download_attachment'; itemId: string; attachmentId: string } | { type: 'list_devices' } @@ -158,7 +159,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet = new Set([ 'fill_credentials', 'ack_autofill_origin', 'get_settings', 'update_settings', 'get_vault_settings', 'update_vault_settings', 'get_blacklist', - 'remove_blacklist', 'get_active_tab_url', 'upload_attachment', 'download_attachment', + 'remove_blacklist', 'get_active_tab_url', 'list_groups', 'upload_attachment', 'download_attachment', 'list_devices', 'add_device', 'register_this_device', 'revoke_device', 'list_trashed', 'restore_item', 'purge_item', 'purge_all_trash', 'get_field_history', From e452d8df02f8ef85decf74f1cdc974d3a01bb198 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 18:09:33 -0400 Subject: [PATCH 08/19] ext(affordances): wireGroupAutocomplete via --- .../__tests__/group-autocomplete.test.ts | 35 +++++++++++++++++++ .../form-affordances/group-autocomplete.ts | 23 ++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts create mode 100644 extension/src/shared/form-affordances/group-autocomplete.ts diff --git a/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts new file mode 100644 index 0000000..da6b0ed --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireGroupAutocomplete } from '../group-autocomplete'; + +describe('wireGroupAutocomplete', () => { + let form: HTMLElement; + + beforeEach(() => { + // Clean up any datalist from a prior test + document.getElementById('groups-datalist')?.remove(); + form = document.createElement('div'); + form.innerHTML = ``; + document.body.appendChild(form); + }); + + it('attaches datalist with all groups', async () => { + const sendMessage = vi.fn().mockResolvedValue({ + ok: true, + data: { groups: ['personal', 'work', 'finance'] }, + }); + await wireGroupAutocomplete(form, { sendMessage }); + const list = document.getElementById('groups-datalist') as HTMLDataListElement | null; + expect(list).not.toBeNull(); + const opts = Array.from(list!.querySelectorAll('option')).map((o) => o.value); + expect(opts).toEqual(['personal', 'work', 'finance']); + const input = form.querySelector('#f-group') as HTMLInputElement; + expect(input.getAttribute('list')).toBe('groups-datalist'); + }); + + it('is a no-op if SW returns error', async () => { + const sendMessage = vi.fn().mockResolvedValue({ ok: false, error: 'vault_locked' }); + await wireGroupAutocomplete(form, { sendMessage }); + const input = form.querySelector('#f-group') as HTMLInputElement; + expect(input.getAttribute('list')).toBeNull(); + }); +}); diff --git a/extension/src/shared/form-affordances/group-autocomplete.ts b/extension/src/shared/form-affordances/group-autocomplete.ts new file mode 100644 index 0000000..60a4c89 --- /dev/null +++ b/extension/src/shared/form-affordances/group-autocomplete.ts @@ -0,0 +1,23 @@ +export interface GroupAutocompleteOpts { + sendMessage: (msg: { type: 'list_groups' }) => Promise<{ ok: boolean; data?: { groups: string[] }; error?: string }>; +} + +const DATALIST_ID = 'groups-datalist'; + +export async function wireGroupAutocomplete(form: HTMLElement, opts: GroupAutocompleteOpts): Promise { + const input = form.querySelector('#f-group'); + if (!input) return; + const resp = await opts.sendMessage({ type: 'list_groups' }); + if (!resp.ok || !resp.data) return; + + // Datalists must live in the document, not nested inside an input. Reuse if + // we've already mounted one this session. + let list = document.getElementById(DATALIST_ID) as HTMLDataListElement | null; + if (!list) { + list = document.createElement('datalist'); + list.id = DATALIST_ID; + document.body.appendChild(list); + } + list.innerHTML = resp.data.groups.map((g) => ``).join(''); + input.setAttribute('list', DATALIST_ID); +} From 6cbd0117055462654cfbecc6517efed67702c02b Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 18:13:17 -0400 Subject: [PATCH 09/19] cli: add 'completions ' subcommand via clap_complete --- Cargo.lock | 10 ++++++++ crates/relicario-cli/Cargo.toml | 1 + crates/relicario-cli/src/main.rs | 15 ++++++++++- crates/relicario-cli/tests/smart_inputs.rs | 30 ++++++++++++++++++++++ 4 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 crates/relicario-cli/tests/smart_inputs.rs diff --git a/Cargo.lock b/Cargo.lock index 19c769d..c1ef94a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.6.0" @@ -1604,6 +1613,7 @@ dependencies = [ "assert_cmd", "chrono", "clap", + "clap_complete", "data-encoding", "dirs", "ed25519-dalek", diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml index e56ab64..f07da01 100644 --- a/crates/relicario-cli/Cargo.toml +++ b/crates/relicario-cli/Cargo.toml @@ -25,6 +25,7 @@ zeroize = "1" url = "2" data-encoding = "2" tar = { version = "0.4", default-features = false } +clap_complete = "4" [dev-dependencies] assert_cmd = "2" diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 6153420..1989d45 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -8,7 +8,8 @@ mod session; use std::path::PathBuf; use anyhow::{bail, Context, Result}; -use clap::{Parser, Subcommand}; +use clap::{CommandFactory, Parser, Subcommand}; +use clap_complete::{generate, Shell}; #[derive(Parser)] #[command( @@ -163,6 +164,13 @@ enum Commands { /// Lock the vault (no-op in CLI; present for UX parity with the extension). Lock, + + /// Emit a shell completion script for the given shell. + /// Pipe to your shell's completion file (e.g. `> /etc/bash_completion.d/relicario`). + Completions { + #[arg(value_enum)] + shell: Shell, + }, } #[derive(Subcommand)] @@ -354,6 +362,11 @@ fn main() -> Result<()> { Commands::Status => cmd_status(), Commands::Device { action } => cmd_device(action), Commands::Lock => { eprintln!("no cached session to lock"); Ok(()) } + Commands::Completions { shell } => { + let mut cmd = Cli::command(); + generate(shell, &mut cmd, "relicario", &mut std::io::stdout()); + Ok(()) + } } } diff --git a/crates/relicario-cli/tests/smart_inputs.rs b/crates/relicario-cli/tests/smart_inputs.rs new file mode 100644 index 0000000..c5c60bb --- /dev/null +++ b/crates/relicario-cli/tests/smart_inputs.rs @@ -0,0 +1,30 @@ +use assert_cmd::Command; +use predicates::str::contains; + +#[test] +fn completions_bash_emits_script() { + Command::cargo_bin("relicario").unwrap() + .args(["completions", "bash"]) + .assert() + .success() + .stdout(contains("_relicario")) + .stdout(contains("complete -F")); +} + +#[test] +fn completions_zsh_emits_script() { + Command::cargo_bin("relicario").unwrap() + .args(["completions", "zsh"]) + .assert() + .success() + .stdout(contains("#compdef relicario")); +} + +#[test] +fn completions_fish_emits_script() { + Command::cargo_bin("relicario").unwrap() + .args(["completions", "fish"]) + .assert() + .success() + .stdout(contains("complete -c relicario")); +} From f7e245d6b06b3bfce4092acbdf7af082c4118701 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 18:19:53 -0400 Subject: [PATCH 10/19] cli: write groups.cache for shell-completion --group enumeration Co-Authored-By: Claude Sonnet 4.6 --- crates/relicario-cli/src/helpers.rs | 32 +++++++++ crates/relicario-cli/src/main.rs | 35 +++++++++- crates/relicario-cli/tests/smart_inputs.rs | 75 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/crates/relicario-cli/src/helpers.rs b/crates/relicario-cli/src/helpers.rs index 2e7b43b..2d09b5c 100644 --- a/crates/relicario-cli/src/helpers.rs +++ b/crates/relicario-cli/src/helpers.rs @@ -83,6 +83,38 @@ pub fn humanize_age(seconds: i64) -> String { fn plural(n: i64) -> &'static str { if n == 1 { "" } else { "s" } } +/// Path to the plaintext `groups.cache` file used by shell completion to +/// enumerate `--group ` candidates without unlocking the vault. +/// +/// **Plaintext leak:** group names land on disk in cleartext alongside the +/// vault directory. This is intentional — the file feeds shell completion, +/// which cannot prompt for a passphrase. Set `RELICARIO_NO_GROUPS_CACHE=1` +/// to suppress the write. +pub fn groups_cache_path(vault_dir: &Path) -> PathBuf { + vault_dir.join(".relicario").join("groups.cache") +} + +/// Write the sorted set of group names to `/.relicario/groups.cache`, +/// one name per line. A no-op if `RELICARIO_NO_GROUPS_CACHE` is set. +pub fn write_groups_cache( + vault_dir: &Path, + groups: &std::collections::BTreeSet, +) -> std::io::Result<()> { + if std::env::var_os("RELICARIO_NO_GROUPS_CACHE").is_some() { + return Ok(()); + } + let path = groups_cache_path(vault_dir); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let mut body = String::new(); + for g in groups { + body.push_str(g); + body.push('\n'); + } + std::fs::write(path, body) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 1989d45..1846557 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -166,7 +166,15 @@ enum Commands { Lock, /// Emit a shell completion script for the given shell. - /// Pipe to your shell's completion file (e.g. `> /etc/bash_completion.d/relicario`). + /// + /// For `--group ` autocomplete, the bash/zsh/fish scripts read + /// the plaintext `${RELICARIO_VAULT}/.relicario/groups.cache` file, + /// which the CLI refreshes on every manifest read. Set + /// `RELICARIO_NO_GROUPS_CACHE=1` to opt out of the cache (completion + /// will fall back to no value enumeration). + /// + /// Pipe stdout to your shell's completion location (e.g. + /// `relicario completions bash > /etc/bash_completion.d/relicario`). Completions { #[arg(value_enum)] shell: Shell, @@ -370,6 +378,24 @@ fn main() -> Result<()> { } } +/// Collect all non-empty group names from the manifest and write them to the +/// plaintext `groups.cache` file so shell completion can enumerate `--group` +/// candidates without prompting for the vault passphrase. +/// +/// Failures are silently swallowed — a missing cache is merely a UX degradation, +/// not a correctness problem. +fn refresh_groups_cache(vault_dir: &std::path::Path, manifest: &relicario_core::Manifest) { + let mut set = std::collections::BTreeSet::::new(); + for entry in manifest.items.values() { + if let Some(g) = entry.group.as_ref() { + if !g.is_empty() { + set.insert(g.clone()); + } + } + } + let _ = helpers::write_groups_cache(vault_dir, &set); +} + /// `rpassword::prompt_password` wrapper that honours `RELICARIO_TEST_ITEM_SECRET` /// for integration-test use (rpassword reads /dev/tty by default, which is /// unavailable in assert_cmd-spawned children). @@ -508,6 +534,7 @@ fn cmd_add(kind: AddKind) -> Result<()> { vault.save_item(&item)?; manifest.upsert(&item); vault.save_manifest(&manifest)?; + refresh_groups_cache(vault.root(), &manifest); let mut paths: Vec = vec![ format!("items/{}.enc", item.id.as_str()), @@ -848,6 +875,7 @@ fn cmd_get(query: String, show: bool, copy: bool) -> Result<()> { let vault = crate::session::UnlockedVault::unlock_interactive()?; let manifest = vault.load_manifest()?; + refresh_groups_cache(vault.root(), &manifest); let entry = resolve_query(&manifest, &query)?; let item = vault.load_item(&entry.id)?; @@ -965,6 +993,7 @@ fn cmd_list( let vault = crate::session::UnlockedVault::unlock_interactive()?; let manifest = vault.load_manifest()?; + refresh_groups_cache(vault.root(), &manifest); let parsed_type: Option = match type_filter.as_deref() { None => None, @@ -1038,6 +1067,7 @@ fn cmd_edit(query: String) -> Result<()> { vault.save_item(&item)?; manifest.upsert(&item); vault.save_manifest(&manifest)?; + refresh_groups_cache(vault.root(), &manifest); commit_paths(&vault, &format!("edit: {} ({})", item.title, item.id.as_str()), &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; eprintln!("Updated {}", item.id.as_str()); @@ -1237,6 +1267,7 @@ fn cmd_rm(query: String) -> Result<()> { vault.save_item(&item)?; manifest.upsert(&item); vault.save_manifest(&manifest)?; + refresh_groups_cache(vault.root(), &manifest); commit_paths(&vault, &format!("trash: {} ({})", item.title, item.id.as_str()), &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; eprintln!("Moved to trash: {}", item.title); @@ -1254,6 +1285,7 @@ fn cmd_restore(query: String) -> Result<()> { vault.save_item(&item)?; manifest.upsert(&item); vault.save_manifest(&manifest)?; + refresh_groups_cache(vault.root(), &manifest); commit_paths(&vault, &format!("restore: {} ({})", item.title, item.id.as_str()), &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; eprintln!("Restored: {}", item.title); @@ -1295,6 +1327,7 @@ fn cmd_purge(query: String) -> Result<()> { purge_item(&vault, &mut manifest, &id, &title)?; vault.save_manifest(&manifest)?; + refresh_groups_cache(vault.root(), &manifest); let status = crate::helpers::git_command(vault.root(), &["add", "manifest.enc"]).status()?; if !status.success() { anyhow::bail!("git add manifest.enc failed"); } diff --git a/crates/relicario-cli/tests/smart_inputs.rs b/crates/relicario-cli/tests/smart_inputs.rs index c5c60bb..897f2f4 100644 --- a/crates/relicario-cli/tests/smart_inputs.rs +++ b/crates/relicario-cli/tests/smart_inputs.rs @@ -1,3 +1,5 @@ +mod common; + use assert_cmd::Command; use predicates::str::contains; @@ -28,3 +30,76 @@ fn completions_fish_emits_script() { .success() .stdout(contains("complete -c relicario")); } + +#[test] +fn list_command_refreshes_groups_cache() { + let v = common::TestVault::init(); + + let out = v.run(&[ + "add", "login", + "--title", "T", + "--username", "u", + "--group", "work", + "--password", "hunter2", + ]); + assert!(out.status.success(), "add failed: {:?}", out); + + let out = v.run(&["list"]); + assert!(out.status.success(), "list failed: {:?}", out); + + let cache_path = v.path().join(".relicario/groups.cache"); + let cache = std::fs::read_to_string(&cache_path) + .unwrap_or_else(|e| panic!("groups.cache not found at {}: {e}", cache_path.display())); + assert!( + cache.lines().any(|l| l == "work"), + "expected 'work' in groups.cache, got: {cache:?}" + ); +} + +#[test] +fn no_groups_cache_env_var_suppresses_write() { + use std::process::{Command as StdCommand, Stdio}; + use assert_cmd::cargo::CommandCargoExt as _; + + let v = common::TestVault::init(); + + // Add with the env var set so no cache is created by add either. + let out = StdCommand::cargo_bin("relicario").unwrap() + .current_dir(v.path()) + .env("RELICARIO_IMAGE", &v.reference_image) + .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase) + .env("RELICARIO_NO_GROUPS_CACHE", "1") + .args([ + "add", "login", + "--title", "T2", + "--username", "u", + "--group", "personal", + "--password", "hunter2", + ]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + assert!(out.status.success(), "add failed: {:?}", out); + + // Run list with RELICARIO_NO_GROUPS_CACHE=1 — cache must NOT be written. + let out = StdCommand::cargo_bin("relicario").unwrap() + .current_dir(v.path()) + .env("RELICARIO_IMAGE", &v.reference_image) + .env("RELICARIO_TEST_PASSPHRASE", &v.passphrase) + .env("RELICARIO_NO_GROUPS_CACHE", "1") + .args(["list"]) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + assert!(out.status.success(), "list failed: {:?}", out); + + let cache_path = v.path().join(".relicario/groups.cache"); + assert!( + !cache_path.exists(), + "groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1" + ); +} From 026b94092e9e052924aaecedff52cfb0d49be121 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:48:32 -0400 Subject: [PATCH 11/19] ext(affordances): wirePasswordReveal toggle --- .../__tests__/password-tools.test.ts | 49 +++++++++++++++++++ .../shared/form-affordances/password-tools.ts | 28 +++++++++++ 2 files changed, 77 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/password-tools.test.ts create mode 100644 extension/src/shared/form-affordances/password-tools.ts diff --git a/extension/src/shared/form-affordances/__tests__/password-tools.test.ts b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts new file mode 100644 index 0000000..168acc4 --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { wirePasswordReveal } from '../password-tools'; + +describe('wirePasswordReveal', () => { + let form: HTMLElement; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + + + `; + document.body.appendChild(form); + }); + + afterEach(() => { + document.body.removeChild(form); + }); + + it('flips input.type and glyph on click', () => { + wirePasswordReveal(form); + const btn = form.querySelector('#reveal-password-btn') as HTMLButtonElement; + const input = form.querySelector('#f-password') as HTMLInputElement; + expect(input.type).toBe('password'); + expect(btn.textContent).toBe('⊙'); + + btn.click(); + expect(input.type).toBe('text'); + expect(btn.textContent).toBe('⊘'); + expect(btn.title).toBe('hide'); + + btn.click(); + expect(input.type).toBe('password'); + expect(btn.textContent).toBe('⊙'); + expect(btn.title).toBe('reveal'); + }); + + it('teardown returned by wirePasswordReveal resets to password', () => { + const teardown = wirePasswordReveal(form); + const btn = form.querySelector('#reveal-password-btn') as HTMLButtonElement; + const input = form.querySelector('#f-password') as HTMLInputElement; + btn.click(); // now revealed + expect(input.type).toBe('text'); + teardown(); + expect(input.type).toBe('password'); + expect(btn.textContent).toBe('⊙'); + expect(btn.title).toBe('reveal'); + }); +}); diff --git a/extension/src/shared/form-affordances/password-tools.ts b/extension/src/shared/form-affordances/password-tools.ts new file mode 100644 index 0000000..dacf023 --- /dev/null +++ b/extension/src/shared/form-affordances/password-tools.ts @@ -0,0 +1,28 @@ +import { GLYPH_REVEAL, GLYPH_HIDE } from '../glyphs'; + +/// Returns a teardown fn the caller must invoke on unmount. +export function wirePasswordReveal(form: HTMLElement): () => void { + const btn = form.querySelector('#reveal-password-btn'); + const input = form.querySelector('#f-password'); + if (!btn || !input) return () => {}; + + const handler = () => { + if (input.type === 'password') { + input.type = 'text'; + btn.textContent = GLYPH_HIDE; + btn.title = 'hide'; + } else { + input.type = 'password'; + btn.textContent = GLYPH_REVEAL; + btn.title = 'reveal'; + } + }; + btn.addEventListener('click', handler); + + return () => { + btn.removeEventListener('click', handler); + input.type = 'password'; + btn.textContent = GLYPH_REVEAL; + btn.title = 'reveal'; + }; +} From 7bd1a9dd7dea8771b0f7be5bec72f7d194981142 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:50:18 -0400 Subject: [PATCH 12/19] ext(affordances): wirePasswordStrength via scheduleRate Co-Authored-By: Claude Opus 4.7 --- extension/src/popup/styles.css | 26 ++++++++++ .../__tests__/password-tools.test.ts | 49 ++++++++++++++++++- .../shared/form-affordances/password-tools.ts | 39 +++++++++++++++ extension/src/vault/vault.css | 26 ++++++++++ 4 files changed, 138 insertions(+), 2 deletions(-) diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 5e5dcb6..3f278fd 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1385,3 +1385,29 @@ textarea { .hostname-text { font-family: ui-monospace, monospace; } +.strength-bar-row { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} +.strength-bar { + display: flex; + gap: 3px; + height: 4px; +} +.strength-bar > span { + flex: 1; + background: var(--border-subtle); + border-radius: 2px; +} +.strength-bar.s-very-weak > span.lit { background: #c75a4f; } +.strength-bar.s-weak > span.lit { background: #c75a4f; } +.strength-bar.s-fair > span.lit { background: #d49b3a; } +.strength-bar.s-good > span.lit { background: #d49b3a; } +.strength-bar.s-strong > span.lit { background: #6cb37a; } +.strength-label { + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} diff --git a/extension/src/shared/form-affordances/__tests__/password-tools.test.ts b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts index 168acc4..7e16210 100644 --- a/extension/src/shared/form-affordances/__tests__/password-tools.test.ts +++ b/extension/src/shared/form-affordances/__tests__/password-tools.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { wirePasswordReveal } from '../password-tools'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { wirePasswordReveal, wirePasswordStrength } from '../password-tools'; describe('wirePasswordReveal', () => { let form: HTMLElement; @@ -47,3 +47,48 @@ describe('wirePasswordReveal', () => { expect(btn.title).toBe('reveal'); }); }); + +describe('wirePasswordStrength', () => { + let form: HTMLElement; + let scheduleRate: ReturnType; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + + + `; + document.body.appendChild(form); + scheduleRate = vi.fn(); + }); + + afterEach(() => { + document.body.removeChild(form); + }); + + it('shows bar with score class on input', () => { + scheduleRate.mockImplementation((_pw, cb) => cb({ score: 3, guessesLog10: 11.4 })); + wirePasswordStrength(form, { scheduleRate }); + const input = form.querySelector('#f-password') as HTMLInputElement; + input.value = 'CorrectHorseBatteryStaple'; + input.dispatchEvent(new Event('input')); + const row = form.querySelector('#strength-bar-row') as HTMLElement; + expect(row.hidden).toBe(false); + expect(row.querySelector('.strength-bar')?.className).toContain('s-good'); + expect(row.querySelector('.strength-label')?.textContent).toContain('good'); + expect(row.querySelector('.strength-label')?.textContent).toContain('10^11'); + }); + + it('hides bar when input is empty', () => { + scheduleRate.mockImplementation((_pw, cb) => cb({ score: -1, guessesLog10: -1 })); + wirePasswordStrength(form, { scheduleRate }); + const input = form.querySelector('#f-password') as HTMLInputElement; + input.value = ''; + input.dispatchEvent(new Event('input')); + const row = form.querySelector('#strength-bar-row') as HTMLElement; + expect(row.hidden).toBe(true); + }); +}); diff --git a/extension/src/shared/form-affordances/password-tools.ts b/extension/src/shared/form-affordances/password-tools.ts index dacf023..f1061e0 100644 --- a/extension/src/shared/form-affordances/password-tools.ts +++ b/extension/src/shared/form-affordances/password-tools.ts @@ -1,4 +1,5 @@ import { GLYPH_REVEAL, GLYPH_HIDE } from '../glyphs'; +import { STRENGTH_LABELS, entropyText, type Strength } from '../../setup/setup-helpers'; /// Returns a teardown fn the caller must invoke on unmount. export function wirePasswordReveal(form: HTMLElement): () => void { @@ -26,3 +27,41 @@ export function wirePasswordReveal(form: HTMLElement): () => void { btn.title = 'reveal'; }; } + +export interface PasswordStrengthOpts { + scheduleRate: (passphrase: string, cb: (s: Strength) => void) => void; +} + +export function wirePasswordStrength(form: HTMLElement, opts: PasswordStrengthOpts): void { + const input = form.querySelector('#f-password'); + const row = form.querySelector('#strength-bar-row'); + if (!input || !row) return; + const bar = row.querySelector('.strength-bar'); + const label = row.querySelector('.strength-label'); + if (!bar || !label) return; + + const update = () => { + const v = input.value; + if (!v) { + row.hidden = true; + return; + } + opts.scheduleRate(v, (s) => { + if (s.score < 0) { row.hidden = true; return; } + row.hidden = false; + // Reset score classes, then add the current one to the bar element. + bar.className = 'strength-bar'; + const cls = STRENGTH_LABELS[s.score]?.cls ?? 's-very-weak'; + bar.classList.add(cls); + // Light up segments 0..score (5-segment bar). + Array.from(bar.children).forEach((seg, i) => { + (seg as HTMLElement).classList.toggle('lit', i <= s.score); + }); + const text = STRENGTH_LABELS[s.score]?.text ?? '?'; + label.textContent = `${text} · ${entropyText(s.guessesLog10)}`; + }); + }; + + input.addEventListener('input', update); + update(); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index bb808e8..b7099c8 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1415,3 +1415,29 @@ textarea { .hostname-text { font-family: ui-monospace, monospace; } +.strength-bar-row { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 4px; +} +.strength-bar { + display: flex; + gap: 3px; + height: 4px; +} +.strength-bar > span { + flex: 1; + background: var(--border-subtle); + border-radius: 2px; +} +.strength-bar.s-very-weak > span.lit { background: #c75a4f; } +.strength-bar.s-weak > span.lit { background: #c75a4f; } +.strength-bar.s-fair > span.lit { background: #d49b3a; } +.strength-bar.s-good > span.lit { background: #d49b3a; } +.strength-bar.s-strong > span.lit { background: #6cb37a; } +.strength-label { + font-size: 11px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} From ed2d299a92e18740d096447f0999ebb5c2652794 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:53:29 -0400 Subject: [PATCH 13/19] cli: add 'rate ' subcommand (zxcvbn) --- crates/relicario-cli/src/main.rs | 36 ++++++++++++++++++++++ crates/relicario-cli/tests/smart_inputs.rs | 33 ++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 1846557..98d4022 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -179,6 +179,16 @@ enum Commands { #[arg(value_enum)] shell: Shell, }, + + /// Rate a passphrase with zxcvbn — prints score (0-4) and estimated + /// guesses. Informational only; does not gate vault operations. + /// + /// Pass `-` as the argument to read one line from stdin instead, which + /// keeps the passphrase out of shell history. + Rate { + /// Passphrase to score, or `-` to read from stdin. + passphrase: String, + }, } #[derive(Subcommand)] @@ -375,6 +385,7 @@ fn main() -> Result<()> { generate(shell, &mut cmd, "relicario", &mut std::io::stdout()); Ok(()) } + Commands::Rate { passphrase } => cmd_rate(passphrase), } } @@ -2180,3 +2191,28 @@ struct ParamsKdf { argon2_t: u32, argon2_p: u32, } + +fn cmd_rate(passphrase: String) -> Result<()> { + let pw: String = if passphrase == "-" { + use std::io::BufRead; + let stdin = std::io::stdin(); + let mut line = String::new(); + stdin.lock().read_line(&mut line)?; + line.trim_end_matches(&['\r', '\n'][..]).to_string() + } else { + passphrase + }; + let est = relicario_core::generators::rate_passphrase(&pw); + let label = match est.score { + 0 => "very weak", + 1 => "weak", + 2 => "fair", + 3 => "good", + 4 => "strong", + _ => "?", + }; + println!("score: {}/4 ({})", est.score, label); + println!("guesses: ~10^{:.1}", est.guesses_log10); + println!("note: init requires score ≥ 3 (see `relicario init`)"); + Ok(()) +} diff --git a/crates/relicario-cli/tests/smart_inputs.rs b/crates/relicario-cli/tests/smart_inputs.rs index 897f2f4..5795980 100644 --- a/crates/relicario-cli/tests/smart_inputs.rs +++ b/crates/relicario-cli/tests/smart_inputs.rs @@ -1,6 +1,7 @@ mod common; use assert_cmd::Command; +use predicates::prelude::PredicateBooleanExt; use predicates::str::contains; #[test] @@ -103,3 +104,35 @@ fn no_groups_cache_env_var_suppresses_write() { "groups.cache should not exist when RELICARIO_NO_GROUPS_CACHE=1" ); } + +#[test] +fn rate_strong_passphrase_prints_score_and_guesses() { + Command::cargo_bin("relicario").unwrap() + .args(["rate", "correct horse battery staple table cocoa rocket spirit ferment"]) + .assert() + .success() + .stdout(contains("score:")) + .stdout(contains("guesses:")) + .stdout(contains("strong")); +} + +#[test] +fn rate_weak_passphrase_exits_zero_with_weak_label() { + // `rate` is informational — does NOT exit nonzero on weak input. + // The hard gate lives at `init` (Plan 2B Task 10). + Command::cargo_bin("relicario").unwrap() + .args(["rate", "password"]) + .assert() + .success() + .stdout(contains("very weak").or(contains("weak"))); +} + +#[test] +fn rate_reads_from_stdin_when_arg_is_dash() { + Command::cargo_bin("relicario").unwrap() + .args(["rate", "-"]) + .write_stdin("correcthorsebatterystaple\n") + .assert() + .success() + .stdout(contains("score:")); +} From bb8b86f0d5528a9a0429768764c5ac7bc89dc3e8 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:55:24 -0400 Subject: [PATCH 14/19] ext(sw): add preview_totp_from_secret popup handler --- .../router/__tests__/router.test.ts | 38 +++++++++++++++++++ .../src/service-worker/router/popup-only.ts | 24 ++++++++++++ extension/src/shared/messages.ts | 4 +- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/extension/src/service-worker/router/__tests__/router.test.ts b/extension/src/service-worker/router/__tests__/router.test.ts index f4b4cf3..117610f 100644 --- a/extension/src/service-worker/router/__tests__/router.test.ts +++ b/extension/src/service-worker/router/__tests__/router.test.ts @@ -1000,3 +1000,41 @@ describe('list_groups', () => { expect(resp.data).toEqual({ groups: [] }); }); }); + +// --- preview_totp_from_secret --- + +describe('preview_totp_from_secret', () => { + let originalChrome: any; + beforeEach(() => { originalChrome = (globalThis as any).chrome; }); + afterEach(() => { (globalThis as any).chrome = originalChrome; }); + + it('returns code for valid base32', async () => { + const state = makeState(); + state.wasm = { + totp_compute: vi.fn().mockReturnValue({ code: '123456', expires_at: 9_999_999_999 }), + }; + const resp = await route( + { type: 'preview_totp_from_secret', secret_b32: 'JBSWY3DPEHPK3PXP' } as any, + state, makePopupSender(), + ); + expect(resp.ok).toBe(true); + expect(resp.data).toEqual({ code: '123456', expires_at: 9_999_999_999 }); + // Verify a transient TotpConfig was passed (sha1, 6 digits, 30s) + const cfgArg = JSON.parse(state.wasm.totp_compute.mock.calls[0][0]); + expect(cfgArg.algorithm).toBe('sha1'); + expect(cfgArg.digits).toBe(6); + expect(cfgArg.period_seconds).toBe(30); + }); + + it('rejects invalid base32', async () => { + const state = makeState(); + state.wasm = { totp_compute: vi.fn() }; + const resp = await route( + { type: 'preview_totp_from_secret', secret_b32: 'too-short!!!' } as any, + state, makePopupSender(), + ); + expect(resp.ok).toBe(false); + expect(resp.error).toMatch(/invalid/i); + expect(state.wasm.totp_compute).not.toHaveBeenCalled(); + }); +}); diff --git a/extension/src/service-worker/router/popup-only.ts b/extension/src/service-worker/router/popup-only.ts index 55842db..9b62f3b 100644 --- a/extension/src/service-worker/router/popup-only.ts +++ b/extension/src/service-worker/router/popup-only.ts @@ -6,6 +6,7 @@ import type { PopupMessage, Response } from '../../shared/messages'; import type { Item, ItemId, Manifest, VaultConfig, SetupState, DeviceSettings, TotpConfig, AttachmentRef } from '../../shared/types'; import { DEFAULT_DEVICE_SETTINGS } from '../../shared/types'; +import { base32Decode } from '../../shared/base32'; import type { GitHost } from '../git-host'; import { createGitHost, base64ToUint8Array } from '../git-host'; import * as vault from '../vault'; @@ -169,6 +170,29 @@ export async function handle( return { ok: true, data: { groups: Array.from(set).sort() } }; } + case 'preview_totp_from_secret': { + const cleaned = msg.secret_b32.toUpperCase().replace(/\s+/g, '').replace(/=+$/, ''); + if (cleaned.length < 16 || !/^[A-Z2-7]+$/.test(cleaned)) { + return { ok: false, error: 'invalid base32 secret' }; + } + let secretBytes: Uint8Array; + try { + secretBytes = base32Decode(cleaned); + } catch (e) { + return { ok: false, error: `invalid base32: ${e instanceof Error ? e.message : String(e)}` }; + } + const cfg = { + secret: Array.from(secretBytes), + algorithm: 'sha1', + digits: 6, + period_seconds: 30, + kind: 'totp', + }; + const now = Math.floor(Date.now() / 1000); + const result = state.wasm.totp_compute(JSON.stringify(cfg), BigInt(now)); + return { ok: true, data: { code: result.code, expires_at: result.expires_at } }; + } + case 'generate_password': { const password = state.wasm.generate_password(JSON.stringify(msg.request)); return { ok: true, data: { password } }; diff --git a/extension/src/shared/messages.ts b/extension/src/shared/messages.ts index e68de37..7408124 100644 --- a/extension/src/shared/messages.ts +++ b/extension/src/shared/messages.ts @@ -59,7 +59,8 @@ export type PopupMessage = newRemote: { hostType: 'gitea' | 'github'; hostUrl: string; repoPath: string; apiToken: string }; } | { type: 'parse_lastpass_csv'; bytes: ArrayBuffer } - | { type: 'import_lastpass_commit'; items: Item[] }; + | { type: 'import_lastpass_commit'; items: Item[] } + | { type: 'preview_totp_from_secret'; secret_b32: string }; // --- Messages a content script may send --- @@ -166,6 +167,7 @@ export const POPUP_ONLY_TYPES: ReadonlySet = new Set([ 'get_session_config', 'update_session_config', 'export_backup', 'restore_backup', 'parse_lastpass_csv', 'import_lastpass_commit', + 'preview_totp_from_secret', ] as PopupMessage['type'][]); export interface ExportBackupResponse extends Extract { From c91b31a7ca273d10bf7ccc030f2bc9779444d153 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 19:56:55 -0400 Subject: [PATCH 15/19] ext(affordances): wireTotpPreview live ticker --- extension/src/popup/styles.css | 21 +++++++ .../__tests__/totp-tools.test.ts | 60 +++++++++++++++++++ .../src/shared/form-affordances/totp-tools.ts | 53 ++++++++++++++++ extension/src/vault/vault.css | 21 +++++++ 4 files changed, 155 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/totp-tools.test.ts create mode 100644 extension/src/shared/form-affordances/totp-tools.ts diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 3f278fd..8e78353 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1411,3 +1411,24 @@ textarea { color: var(--text-muted); font-variant-numeric: tabular-nums; } + +.totp-preview { + margin-top: 6px; + padding: 6px 10px; + border: 1px dashed var(--border-subtle); + border-radius: 3px; + display: flex; + justify-content: space-between; + align-items: center; + font-variant-numeric: tabular-nums; + color: var(--text-muted); +} +.totp-code { + font-size: 14px; + font-weight: 600; + letter-spacing: 1px; + color: var(--accent); +} +.totp-countdown { + font-size: 11px; +} diff --git a/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts b/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts new file mode 100644 index 0000000..0d0ff17 --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireTotpPreview } from '../totp-tools'; + +describe('wireTotpPreview', () => { + let form: HTMLElement; + let sendMessage: ReturnType; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + + + `; + document.body.appendChild(form); + sendMessage = vi.fn(); + vi.useFakeTimers(); + }); + + it('shows preview when secret is valid base32', async () => { + sendMessage.mockResolvedValue({ ok: true, data: { code: '492837', expires_at: Math.floor(Date.now() / 1000) + 23 } }); + const teardown = wireTotpPreview(form, { sendMessage }); + const input = form.querySelector('#f-totp') as HTMLInputElement; + input.value = 'JBSWY3DPEHPK3PXP'; + input.dispatchEvent(new Event('input')); + await vi.advanceTimersByTimeAsync(50); + const row = form.querySelector('#totp-preview-row') as HTMLElement; + expect(row.hidden).toBe(false); + expect(row.querySelector('.totp-code')?.textContent).toBe('492 837'); + expect(row.querySelector('.totp-countdown')?.textContent).toMatch(/\d+s/); + teardown(); + }); + + it('hides preview when secret is too short', async () => { + const teardown = wireTotpPreview(form, { sendMessage }); + const input = form.querySelector('#f-totp') as HTMLInputElement; + input.value = 'TOOSHORT'; + input.dispatchEvent(new Event('input')); + await vi.advanceTimersByTimeAsync(50); + const row = form.querySelector('#totp-preview-row') as HTMLElement; + expect(row.hidden).toBe(true); + expect(sendMessage).not.toHaveBeenCalled(); + teardown(); + }); + + it('teardown stops the interval', async () => { + sendMessage.mockResolvedValue({ ok: true, data: { code: '111111', expires_at: Math.floor(Date.now() / 1000) + 30 } }); + const teardown = wireTotpPreview(form, { sendMessage }); + const input = form.querySelector('#f-totp') as HTMLInputElement; + input.value = 'JBSWY3DPEHPK3PXP'; + input.dispatchEvent(new Event('input')); + await vi.advanceTimersByTimeAsync(50); + const callsBefore = sendMessage.mock.calls.length; + teardown(); + await vi.advanceTimersByTimeAsync(2000); + expect(sendMessage.mock.calls.length).toBe(callsBefore); + }); +}); diff --git a/extension/src/shared/form-affordances/totp-tools.ts b/extension/src/shared/form-affordances/totp-tools.ts new file mode 100644 index 0000000..c1232ab --- /dev/null +++ b/extension/src/shared/form-affordances/totp-tools.ts @@ -0,0 +1,53 @@ +export interface TotpPreviewOpts { + sendMessage: (msg: { type: 'preview_totp_from_secret'; secret_b32: string }) => + Promise<{ ok: boolean; data?: { code: string; expires_at: number }; error?: string }>; +} + +const VALID_B32 = /^[A-Z2-7]{16,}=*$/; + +export function wireTotpPreview(form: HTMLElement, opts: TotpPreviewOpts): () => void { + const input = form.querySelector('#f-totp'); + const row = form.querySelector('#totp-preview-row'); + if (!input || !row) return () => {}; + const codeEl = row.querySelector('.totp-code'); + const cdEl = row.querySelector('.totp-countdown'); + if (!codeEl || !cdEl) return () => {}; + + let interval: ReturnType | null = null; + let lastSecret = ''; + + const tick = async () => { + const cleaned = lastSecret.toUpperCase().replace(/\s+/g, '').replace(/=+$/, ''); + if (!VALID_B32.test(cleaned)) { + row.hidden = true; + return; + } + const resp = await opts.sendMessage({ type: 'preview_totp_from_secret', secret_b32: cleaned }); + if (!resp.ok || !resp.data) { + row.hidden = true; + return; + } + row.hidden = false; + // Format "492837" → "492 837" for legibility. + codeEl.textContent = resp.data.code.length === 6 + ? `${resp.data.code.slice(0, 3)} ${resp.data.code.slice(3)}` + : resp.data.code; + const remaining = Math.max(0, resp.data.expires_at - Math.floor(Date.now() / 1000)); + cdEl.textContent = `${remaining}s`; + }; + + const onInput = () => { + lastSecret = input.value; + void tick(); + }; + input.addEventListener('input', onInput); + if (interval === null) { + interval = setInterval(() => { void tick(); }, 1000); + } + + return () => { + input.removeEventListener('input', onInput); + if (interval !== null) { clearInterval(interval); interval = null; } + row.hidden = true; + }; +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index b7099c8..6ae5155 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1441,3 +1441,24 @@ textarea { color: var(--text-muted); font-variant-numeric: tabular-nums; } + +.totp-preview { + margin-top: 6px; + padding: 6px 10px; + border: 1px dashed var(--border-subtle); + border-radius: 3px; + display: flex; + justify-content: space-between; + align-items: center; + font-variant-numeric: tabular-nums; + color: var(--text-muted); +} +.totp-code { + font-size: 14px; + font-weight: 600; + letter-spacing: 1px; + color: var(--accent); +} +.totp-countdown { + font-size: 11px; +} From bd8102c9add61c4e11914c28d0fc1a00927ce5ec Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 22:14:05 -0400 Subject: [PATCH 16/19] ext(affordances): wireTotpQr (jsqr lazy-load) for QR -> otpauth:// fill Co-Authored-By: Claude Sonnet 4.6 --- extension/package-lock.json | 3528 +++++++++++++++++ extension/package.json | 3 + extension/src/popup/styles.css | 17 + .../__tests__/totp-tools.test.ts | 67 +- .../src/shared/form-affordances/totp-tools.ts | 89 + extension/src/vault/vault.css | 17 + 6 files changed, 3720 insertions(+), 1 deletion(-) create mode 100644 extension/package-lock.json diff --git a/extension/package-lock.json b/extension/package-lock.json new file mode 100644 index 0000000..1e1383b --- /dev/null +++ b/extension/package-lock.json @@ -0,0 +1,3528 @@ +{ + "name": "relicario-extension", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "relicario-extension", + "version": "0.2.0", + "dependencies": { + "jsqr": "^1.4.0" + }, + "devDependencies": { + "@types/chrome": "^0.1.40", + "copy-webpack-plugin": "^12.0", + "happy-dom": "^15", + "ts-loader": "^9.5", + "typescript": "^5.4", + "vitest": "^2.0", + "webpack": "^5.90", + "webpack-cli": "^5.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/chrome": { + "version": "0.1.40", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.40.tgz", + "integrity": "sha512-UnfyRAe8ORu9HSuTH0EqyOEUin3JrWW9Nl/gDXezNfTUrfIoxw+WRZgKOxGz0t5BnjbfXBnS2eCYfW2PxH1wcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.25", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.25.tgz", + "integrity": "sha512-QO/VHsXCQdnzADMfmkeOPvHdIAkoB7i0/rGjINPJEetLx75hNttVWGQ/jycHUDP9zZ9rupbm60WRxcwViB0MiA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.348", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.348.tgz", + "integrity": "sha512-QC2X59nRlycQQMc4ZXjSVBX+tSgJfgRtcrYHbIZLgOV2dCvefoQGegLR7lLXKgpPpSuVmJU19LMzGrSa2C7k3Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.0.tgz", + "integrity": "sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/happy-dom": { + "version": "15.11.7", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz", + "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0", + "webidl-conversions": "^7.0.0", + "whatwg-mimetype": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsqr": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz", + "integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==", + "license": "Apache-2.0" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", + "integrity": "sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", + "integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", + "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.5.0.tgz", + "integrity": "sha512-UYhptBwhWvfIjKd/UuFo6D8uq9xpGLDK+z8EDsj/zWhrTaH34cKEbrkMKfV5YWqGBvAYA3tlzZbs2R+qYrbQJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.7", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.7.tgz", + "integrity": "sha512-/ZNrKgA3K3PtpMYOC71EeMWIloGw3IYEa5/t1cyz2r5/PyUwTXGzYJvcD3kfUvmhlfpz1rhV8B2O6IVTQ0avsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/webpack": { + "version": "5.106.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", + "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "loader-runner": "^4.3.1", + "mime-db": "^1.54.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.4.1.tgz", + "integrity": "sha512-eACpxRN02yaawnt+uUNIF7Qje6A9zArxBbcAJjK1PK3S9Ycg5jIuJ8pW4q8EMnwNZCEGltcjkRx1QzOxOkKD8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/extension/package.json b/extension/package.json index 3c9a132..2ae8967 100644 --- a/extension/package.json +++ b/extension/package.json @@ -12,6 +12,9 @@ "test": "vitest run", "test:watch": "vitest" }, + "dependencies": { + "jsqr": "^1.4.0" + }, "devDependencies": { "@types/chrome": "^0.1.40", "copy-webpack-plugin": "^12.0", diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index 8e78353..c0afa53 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1432,3 +1432,20 @@ textarea { .totp-countdown { font-size: 11px; } +.totp-qr-panel { + margin-top: 6px; + padding: 10px; + border: 1px dashed var(--border-subtle); + border-radius: 3px; + background: var(--bg-input); +} +.totp-qr-panel input[type="file"] { + display: block; + font-family: inherit; + color: var(--text-muted); +} +.totp-qr-error { + margin-top: 6px; + font-size: 11px; + color: var(--danger, #c75a4f); +} diff --git a/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts b/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts index 0d0ff17..1cf4227 100644 --- a/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts +++ b/extension/src/shared/form-affordances/__tests__/totp-tools.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { wireTotpPreview } from '../totp-tools'; +import { wireTotpPreview, wireTotpQr } from '../totp-tools'; describe('wireTotpPreview', () => { let form: HTMLElement; @@ -58,3 +58,68 @@ describe('wireTotpPreview', () => { expect(sendMessage.mock.calls.length).toBe(callsBefore); }); }); + +describe('wireTotpQr', () => { + let form: HTMLElement; + let decodeQrFromBlob: ReturnType; + + beforeEach(() => { + vi.useRealTimers(); + form = document.createElement('div'); + form.innerHTML = ` + + + + `; + document.body.appendChild(form); + decodeQrFromBlob = vi.fn(); + }); + + it('toggles the panel on button click', () => { + wireTotpQr(form, { decodeQrFromBlob }); + const btn = form.querySelector('#totp-qr-btn') as HTMLButtonElement; + const panel = form.querySelector('#totp-qr-panel') as HTMLElement; + expect(panel.hidden).toBe(true); + btn.click(); + expect(panel.hidden).toBe(false); + btn.click(); + expect(panel.hidden).toBe(true); + }); + + it('fills f-totp on successful decode of otpauth:// URI', async () => { + decodeQrFromBlob.mockResolvedValue('otpauth://totp/Example:alice@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example'); + wireTotpQr(form, { decodeQrFromBlob }); + const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement; + const fakeFile = new File(['x'], 'qr.png', { type: 'image/png' }); + Object.defineProperty(fileInput, 'files', { value: [fakeFile] }); + fileInput.dispatchEvent(new Event('change')); + await Promise.resolve(); await Promise.resolve(); + expect((form.querySelector('#f-totp') as HTMLInputElement).value).toBe('JBSWY3DPEHPK3PXP'); + }); + + it('shows error when QR decodes but is not otpauth://', async () => { + decodeQrFromBlob.mockResolvedValue('https://example.com/'); + wireTotpQr(form, { decodeQrFromBlob }); + const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement; + Object.defineProperty(fileInput, 'files', { value: [new File(['x'], 'x.png', { type: 'image/png' })] }); + fileInput.dispatchEvent(new Event('change')); + await Promise.resolve(); await Promise.resolve(); + const err = form.querySelector('#totp-qr-error') as HTMLElement; + expect(err.textContent).toMatch(/not a totp uri/i); + expect((form.querySelector('#f-totp') as HTMLInputElement).value).toBe(''); + }); + + it('shows error when decode returns null (no QR found)', async () => { + decodeQrFromBlob.mockResolvedValue(null); + wireTotpQr(form, { decodeQrFromBlob }); + const fileInput = form.querySelector('#totp-qr-file') as HTMLInputElement; + Object.defineProperty(fileInput, 'files', { value: [new File(['x'], 'x.png', { type: 'image/png' })] }); + fileInput.dispatchEvent(new Event('change')); + await Promise.resolve(); await Promise.resolve(); + const err = form.querySelector('#totp-qr-error') as HTMLElement; + expect(err.textContent).toMatch(/no qr found/i); + }); +}); diff --git a/extension/src/shared/form-affordances/totp-tools.ts b/extension/src/shared/form-affordances/totp-tools.ts index c1232ab..5210c7d 100644 --- a/extension/src/shared/form-affordances/totp-tools.ts +++ b/extension/src/shared/form-affordances/totp-tools.ts @@ -51,3 +51,92 @@ export function wireTotpPreview(form: HTMLElement, opts: TotpPreviewOpts): () => row.hidden = true; }; } + +/// Lazy-load jsqr and decode a QR from a Blob/File. Returns the decoded +/// string, or null if no QR was found. +async function defaultDecodeQrFromBlob(blob: Blob): Promise { + const [{ default: jsQR }] = await Promise.all([import('jsqr')]); + const bitmap = await createImageBitmap(blob); + const canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + const ctx = canvas.getContext('2d'); + if (!ctx) return null; + ctx.drawImage(bitmap, 0, 0); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const result = jsQR(imageData.data, imageData.width, imageData.height); + return result?.data ?? null; +} + +export interface TotpQrOpts { + /// Inject a stub in tests where canvas + imports aren't available. + decodeQrFromBlob?: (blob: Blob) => Promise; +} + +export function wireTotpQr(form: HTMLElement, opts: TotpQrOpts = {}): void { + const btn = form.querySelector('#totp-qr-btn'); + const panel = form.querySelector('#totp-qr-panel'); + const fileInput = form.querySelector('#totp-qr-file'); + const errEl = form.querySelector('#totp-qr-error'); + const totpInput = form.querySelector('#f-totp'); + if (!btn || !panel || !fileInput || !errEl || !totpInput) return; + + const decode = opts.decodeQrFromBlob ?? defaultDecodeQrFromBlob; + + btn.addEventListener('click', () => { + panel.hidden = !panel.hidden; + errEl.textContent = ''; + }); + + const handleBlob = async (blob: Blob) => { + errEl.textContent = ''; + let decoded: string | null; + try { + decoded = await decode(blob); + } catch (e) { + errEl.textContent = `decode failed: ${e instanceof Error ? e.message : String(e)}`; + return; + } + if (!decoded) { + errEl.textContent = 'no QR found in image'; + return; + } + if (!decoded.startsWith('otpauth://')) { + errEl.textContent = 'not a TOTP URI (expected otpauth://...)'; + return; + } + try { + const u = new URL(decoded); + const secret = u.searchParams.get('secret'); + if (!secret) { + errEl.textContent = 'TOTP URI missing secret'; + return; + } + totpInput.value = secret; + totpInput.dispatchEvent(new Event('input', { bubbles: true })); // trigger preview + panel.hidden = true; + } catch { + errEl.textContent = 'TOTP URI did not parse'; + } + }; + + fileInput.addEventListener('change', () => { + const f = fileInput.files?.[0]; + if (f) void handleBlob(f); + }); + + panel.addEventListener('paste', (e) => { + const item = Array.from((e as ClipboardEvent).clipboardData?.items ?? []).find((i) => i.type.startsWith('image/')); + if (item) { + const blob = item.getAsFile(); + if (blob) void handleBlob(blob); + } + }); + + panel.addEventListener('dragover', (e) => { e.preventDefault(); }); + panel.addEventListener('drop', (e) => { + e.preventDefault(); + const f = (e as DragEvent).dataTransfer?.files?.[0]; + if (f) void handleBlob(f); + }); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index 6ae5155..a26e72d 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1462,3 +1462,20 @@ textarea { .totp-countdown { font-size: 11px; } +.totp-qr-panel { + margin-top: 6px; + padding: 10px; + border: 1px dashed var(--border-subtle); + border-radius: 3px; + background: var(--bg-input); +} +.totp-qr-panel input[type="file"] { + display: block; + font-family: inherit; + color: var(--text-muted); +} +.totp-qr-error { + margin-top: 6px; + font-size: 11px; + color: var(--danger, #c75a4f); +} From 8855078179b8280397806b27df4af0f8366197ad Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 22:22:20 -0400 Subject: [PATCH 17/19] cli: --totp-qr flag on add login + edit (rqrr decode) Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 67 ++++++++++++++++++++ crates/relicario-cli/Cargo.toml | 4 +- crates/relicario-cli/src/helpers.rs | 28 +++++++++ crates/relicario-cli/src/main.rs | 63 ++++++++++++++++--- crates/relicario-cli/tests/smart_inputs.rs | 72 ++++++++++++++++++++++ 5 files changed, 224 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c1ef94a..29546f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -758,6 +764,34 @@ dependencies = [ "slab", ] +[[package]] +name = "g2gen" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5a7e0eb46f83a20260b850117d204366674e85d3a908d90865c78df9a6b1dfc" +dependencies = [ + "g2poly", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "g2p" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "539e2644c030d3bf4cd208cb842d2ce2f80e82e6e8472390bcef83ceba0d80ad" +dependencies = [ + "g2gen", + "g2poly", +] + +[[package]] +name = "g2poly" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312d2295c7302019c395cfb90dacd00a82a2eabd700429bba9c7a3f38dbbe11b" + [[package]] name = "generic-array" version = "0.14.7" @@ -833,6 +867,8 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", ] @@ -1148,6 +1184,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1489,6 +1534,15 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image", +] + [[package]] name = "quick-error" version = "2.0.1" @@ -1620,9 +1674,11 @@ dependencies = [ "hex", "image", "predicates", + "qrcode", "rand", "relicario-core", "rpassword", + "rqrr", "serde", "serde_json", "tar", @@ -1690,6 +1746,17 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rqrr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0cd0432e6beb2f86aa4c8af1bb5edcf3c9bcb9d4836facc048664205458575" +dependencies = [ + "g2p", + "image", + "lru", +] + [[package]] name = "rtoolbox" version = "0.0.5" diff --git a/crates/relicario-cli/Cargo.toml b/crates/relicario-cli/Cargo.toml index f07da01..321d03b 100644 --- a/crates/relicario-cli/Cargo.toml +++ b/crates/relicario-cli/Cargo.toml @@ -26,10 +26,12 @@ url = "2" data-encoding = "2" tar = { version = "0.4", default-features = false } clap_complete = "4" +image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } +rqrr = "0.7" [dev-dependencies] assert_cmd = "2" predicates = "3" tempfile = "3" -image = { version = "0.25", default-features = false, features = ["jpeg"] } +qrcode = "0.14" serde_json = "1" diff --git a/crates/relicario-cli/src/helpers.rs b/crates/relicario-cli/src/helpers.rs index 2d09b5c..f794baf 100644 --- a/crates/relicario-cli/src/helpers.rs +++ b/crates/relicario-cli/src/helpers.rs @@ -115,6 +115,34 @@ pub fn write_groups_cache( std::fs::write(path, body) } +/// Decode a QR image at `path`. Returns the otpauth secret (base32) if the +/// QR decodes to an `otpauth://...` URI with a `secret` query param. +pub fn decode_totp_qr(path: &std::path::Path) -> anyhow::Result { + let img = image::open(path) + .map_err(|e| anyhow::anyhow!("failed to read image: {e}"))? + .to_luma8(); + let mut prepared = rqrr::PreparedImage::prepare(img); + let grids = prepared.detect_grids(); + let grid = grids + .into_iter() + .next() + .ok_or_else(|| anyhow::anyhow!("no QR code found in image"))?; + let (_meta, content) = grid + .decode() + .map_err(|e| anyhow::anyhow!("QR decode failed: {e}"))?; + if !content.starts_with("otpauth://") { + return Err(anyhow::anyhow!("not a TOTP URI (expected otpauth://...)")); + } + let parsed = + url::Url::parse(&content).map_err(|e| anyhow::anyhow!("invalid otpauth URI: {e}"))?; + let secret = parsed + .query_pairs() + .find(|(k, _)| k == "secret") + .map(|(_, v)| v.to_string()) + .ok_or_else(|| anyhow::anyhow!("otpauth URI missing `secret` parameter"))?; + Ok(secret) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 98d4022..0d3f52c 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -66,7 +66,12 @@ enum Commands { }, /// Edit an item interactively. - Edit { query: String }, + Edit { + query: String, + /// Decode an `otpauth://` QR image to set the TOTP secret (login items only). + #[arg(long, value_name = "PATH")] + totp_qr: Option, + }, /// View captured field history for an item. Values are masked by /// default; pass `--show` to reveal them. @@ -203,6 +208,8 @@ enum AddKind { #[arg(long)] group: Option, #[arg(long, value_delimiter = ',')] tags: Vec, #[arg(long)] favorite: bool, + /// Decode an `otpauth://` QR image to fill the TOTP secret. + #[arg(long, value_name = "PATH")] totp_qr: Option, }, SecureNote { #[arg(long)] title: Option, @@ -360,7 +367,7 @@ fn main() -> Result<()> { Commands::Add { kind } => cmd_add(kind), Commands::Get { query, show, copy } => cmd_get(query, show, copy), Commands::List { r#type, group, tag, trashed } => cmd_list(r#type, group, tag, trashed), - Commands::Edit { query } => cmd_edit(query), + Commands::Edit { query, totp_qr } => cmd_edit(query, totp_qr), Commands::History { query, show, field } => cmd_history(query, show, field), Commands::Rm { query } => cmd_rm(query), Commands::Restore { query } => cmd_restore(query), @@ -526,8 +533,8 @@ fn cmd_add(kind: AddKind) -> Result<()> { let mut manifest = vault.load_manifest()?; let item = match kind { - AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite } => - build_login_item(title, username, url, password_prompt, password, group, tags, favorite)?, + AddKind::Login { title, username, url, password_prompt, password, group, tags, favorite, totp_qr } => + build_login_item(title, username, url, password_prompt, password, group, tags, favorite, totp_qr)?, AddKind::SecureNote { title, body_prompt, group, tags } => build_secure_note_item(title, body_prompt, group, tags)?, AddKind::Identity { title, full_name, email, phone, date_of_birth, group, tags } => @@ -576,8 +583,9 @@ fn build_login_item( group: Option, tags: Vec, favorite: bool, + totp_qr: Option, ) -> Result { - use relicario_core::item_types::LoginCore; + use relicario_core::item_types::{LoginCore, TotpAlgorithm, TotpConfig, TotpKind}; use relicario_core::{Item, ItemCore}; use zeroize::Zeroizing; @@ -595,8 +603,21 @@ fn build_login_item( } else { None }; + let totp = if let Some(path) = totp_qr { + let secret_b32 = crate::helpers::decode_totp_qr(&path)?; + let secret_bytes = base32_decode_lenient(&secret_b32)?; + Some(TotpConfig { + secret: Zeroizing::new(secret_bytes), + algorithm: TotpAlgorithm::Sha1, + digits: 6, + period_seconds: 30, + kind: TotpKind::Totp, + }) + } else { + None + }; let mut item = Item::new(title, ItemCore::Login(LoginCore { - username, password, url: parsed_url, totp: None, + username, password, url: parsed_url, totp, })); item.group = group; item.tags = tags; @@ -904,6 +925,13 @@ fn cmd_get(query: String, show: bool, copy: bool) -> Result<()> { ItemCore::Login(l) => { if let Some(u) = &l.username { println!("Username: {u}"); } if let Some(u) = &l.url { println!("URL: {u}"); } + if let Some(t) = &l.totp { + if show { + println!("TOTP: {}", data_encoding::BASE32.encode(&*t.secret)); + } else { + println!("TOTP: **** (use --show to reveal)"); + } + } if let Some(p) = &l.password { Some(p.clone()) } else { None } } ItemCore::SecureNote(n) => { @@ -1043,7 +1071,7 @@ fn cmd_list( } Ok(()) } -fn cmd_edit(query: String) -> Result<()> { +fn cmd_edit(query: String, totp_qr: Option) -> Result<()> { use relicario_core::time::now_unix; use relicario_core::ItemCore; @@ -1065,7 +1093,7 @@ fn cmd_edit(query: String) -> Result<()> { let history = &mut item.field_history; match &mut item.core { - ItemCore::Login(l) => edit_login(l, history)?, + ItemCore::Login(l) => edit_login(l, history, totp_qr)?, ItemCore::SecureNote(n) => edit_secure_note(n, history)?, ItemCore::Identity(i) => edit_identity(i)?, ItemCore::Card(c) => edit_card(c, history)?, @@ -1094,7 +1122,12 @@ type FieldHistory = std::collections::HashMap< Vec, >; -fn edit_login(l: &mut relicario_core::item_types::LoginCore, history: &mut FieldHistory) -> Result<()> { +fn edit_login( + l: &mut relicario_core::item_types::LoginCore, + history: &mut FieldHistory, + totp_qr: Option, +) -> Result<()> { + use relicario_core::item_types::{TotpAlgorithm, TotpConfig, TotpKind}; use zeroize::Zeroizing; if let Some(v) = prompt_keep_opt("Username", l.username.as_deref())? { l.username = Some(v); } if let Some(v) = prompt_keep_opt("URL", l.url.as_ref().map(|u| u.as_str()))? { @@ -1107,6 +1140,18 @@ fn edit_login(l: &mut relicario_core::item_types::LoginCore, history: &mut Field push_history(history, "login_password", Zeroizing::new(old_pw.as_str().to_string())); } } + if let Some(path) = totp_qr { + let secret_b32 = crate::helpers::decode_totp_qr(&path)?; + let secret_bytes = base32_decode_lenient(&secret_b32)?; + l.totp = Some(TotpConfig { + secret: Zeroizing::new(secret_bytes), + algorithm: TotpAlgorithm::Sha1, + digits: 6, + period_seconds: 30, + kind: TotpKind::Totp, + }); + eprintln!("TOTP secret set from QR image."); + } Ok(()) } diff --git a/crates/relicario-cli/tests/smart_inputs.rs b/crates/relicario-cli/tests/smart_inputs.rs index 5795980..2938ee9 100644 --- a/crates/relicario-cli/tests/smart_inputs.rs +++ b/crates/relicario-cli/tests/smart_inputs.rs @@ -136,3 +136,75 @@ fn rate_reads_from_stdin_when_arg_is_dash() { .success() .stdout(contains("score:")); } + +fn make_test_qr(uri: &str, dest: &std::path::Path) { + use image::{ImageBuffer, Luma}; + let code = qrcode::QrCode::new(uri).expect("QR encode failed"); + let img: ImageBuffer, Vec> = code + .render::>() + .module_dimensions(8, 8) + .build(); + img.save(dest).expect("save QR PNG"); +} + +#[test] +fn add_login_totp_qr_decodes_otpauth_uri() { + use tempfile::TempDir; + let tmp = TempDir::new().unwrap(); + let qr_path = tmp.path().join("test.png"); + make_test_qr( + "otpauth://totp/Example:alice?secret=JBSWY3DPEHPK3PXP&issuer=Example", + &qr_path, + ); + + let v = common::TestVault::init(); + + let out = v.run(&[ + "add", "login", + "--title", "TotpTest", + "--password", "hunter2", + "--totp-qr", qr_path.to_str().unwrap(), + ]); + assert!(out.status.success(), "add failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr)); + + let out = v.run(&["get", "TotpTest", "--show"]); + assert!(out.status.success(), "get failed:\nstdout: {}\nstderr: {}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr)); + let stdout = String::from_utf8_lossy(&out.stdout); + // BASE32.encode(BASE32.decode("JBSWY3DPEHPK3PXP")) should round-trip. + // The secret bytes from JBSWY3DPEHPK3PXP decode to specific bytes, + // then re-encode to JBSWY3DPEHPK3PXP====; we check for the core chars. + assert!( + stdout.contains("JBSWY3DPEHPK3PXP"), + "expected TOTP secret in get output, got:\n{stdout}" + ); +} + +#[test] +fn add_login_totp_qr_errors_on_non_otpauth_qr() { + use tempfile::TempDir; + let tmp = TempDir::new().unwrap(); + let qr_path = tmp.path().join("nottotp.png"); + make_test_qr("https://example.com", &qr_path); + + let v = common::TestVault::init(); + + let out = v.run(&[ + "add", "login", + "--title", "BadQR", + "--password", "hunter2", + "--totp-qr", qr_path.to_str().unwrap(), + ]); + assert!( + !out.status.success(), + "expected nonzero exit for non-otpauth QR, but command succeeded" + ); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("not a TOTP URI"), + "expected 'not a TOTP URI' in stderr, got:\n{stderr}" + ); +} From e6eb698c4c743dd1a3081a2dbea30eee1beaa64d Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 22:23:56 -0400 Subject: [PATCH 18/19] ext(affordances): wireNotesMonoToggle with chrome.storage.local persistence --- extension/src/popup/styles.css | 8 ++++ .../__tests__/notes-tools.test.ts | 38 +++++++++++++++++++ .../shared/form-affordances/notes-tools.ts | 23 +++++++++++ extension/src/vault/vault.css | 8 ++++ 4 files changed, 77 insertions(+) create mode 100644 extension/src/shared/form-affordances/__tests__/notes-tools.test.ts create mode 100644 extension/src/shared/form-affordances/notes-tools.ts diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css index c0afa53..161e809 100644 --- a/extension/src/popup/styles.css +++ b/extension/src/popup/styles.css @@ -1449,3 +1449,11 @@ textarea { font-size: 11px; color: var(--danger, #c75a4f); } +.notes-with-toggle { + display: flex; + align-items: center; + gap: 8px; +} +.f-notes--mono { + font-family: ui-monospace, "JetBrains Mono", "SF Mono", monospace !important; +} diff --git a/extension/src/shared/form-affordances/__tests__/notes-tools.test.ts b/extension/src/shared/form-affordances/__tests__/notes-tools.test.ts new file mode 100644 index 0000000..af4a9ef --- /dev/null +++ b/extension/src/shared/form-affordances/__tests__/notes-tools.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { wireNotesMonoToggle } from '../notes-tools'; + +describe('wireNotesMonoToggle', () => { + let form: HTMLElement; + let storage: { get: ReturnType; set: ReturnType }; + + beforeEach(() => { + form = document.createElement('div'); + form.innerHTML = ` + + + `; + document.body.appendChild(form); + storage = { + get: vi.fn().mockImplementation((_keys, cb) => cb({})), + set: vi.fn().mockImplementation((_obj, cb) => cb && cb()), + }; + (globalThis as any).chrome = { storage: { local: storage } }; + }); + + it('toggles class on click and persists', async () => { + await wireNotesMonoToggle(form, { itemId: 'abc123' }); + const btn = form.querySelector('#notes-mono-btn') as HTMLButtonElement; + const ta = form.querySelector('#f-notes') as HTMLTextAreaElement; + expect(ta.classList.contains('f-notes--mono')).toBe(false); + btn.click(); + expect(ta.classList.contains('f-notes--mono')).toBe(true); + expect(storage.set).toHaveBeenCalledWith({ 'notesMono.abc123': true }, expect.any(Function)); + }); + + it('restores prior state on mount', async () => { + storage.get.mockImplementation((_keys, cb) => cb({ 'notesMono.abc123': true })); + await wireNotesMonoToggle(form, { itemId: 'abc123' }); + const ta = form.querySelector('#f-notes') as HTMLTextAreaElement; + expect(ta.classList.contains('f-notes--mono')).toBe(true); + }); +}); diff --git a/extension/src/shared/form-affordances/notes-tools.ts b/extension/src/shared/form-affordances/notes-tools.ts new file mode 100644 index 0000000..548973e --- /dev/null +++ b/extension/src/shared/form-affordances/notes-tools.ts @@ -0,0 +1,23 @@ +export interface NotesMonoOpts { + /// Item ID for persistence — pass empty string for "add new" forms (state + /// is then session-scoped under the key 'notesMono.__new__'). + itemId: string; +} + +export async function wireNotesMonoToggle(form: HTMLElement, opts: NotesMonoOpts): Promise { + const btn = form.querySelector('#notes-mono-btn'); + const ta = form.querySelector('#f-notes'); + if (!btn || !ta) return; + + const key = `notesMono.${opts.itemId || '__new__'}`; + const stored = await new Promise((resolve) => { + chrome.storage.local.get([key], (result) => resolve(!!result[key])); + }); + if (stored) ta.classList.add('f-notes--mono'); + + btn.addEventListener('click', () => { + const next = !ta.classList.contains('f-notes--mono'); + ta.classList.toggle('f-notes--mono', next); + chrome.storage.local.set({ [key]: next }, () => { /* fire and forget */ }); + }); +} diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css index a26e72d..f0c1a10 100644 --- a/extension/src/vault/vault.css +++ b/extension/src/vault/vault.css @@ -1479,3 +1479,11 @@ textarea { font-size: 11px; color: var(--danger, #c75a4f); } +.notes-with-toggle { + display: flex; + align-items: center; + gap: 8px; +} +.f-notes--mono { + font-family: ui-monospace, "JetBrains Mono", "SF Mono", monospace !important; +} From b450ecd1cce43b8f8a9b499c3198c97d6986800f Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Fri, 1 May 2026 22:32:14 -0400 Subject: [PATCH 19/19] ext(login): wire 8 smart-input affordances into renderForm() Co-Authored-By: Claude Sonnet 4.6 --- .../components/types/__tests__/login.test.ts | 122 ++++++++++++++++++ extension/src/popup/components/types/login.ts | 89 +++++++++++-- .../form-affordances/group-autocomplete.ts | 9 +- .../shared/form-affordances/notes-tools.ts | 16 ++- 4 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 extension/src/popup/components/types/__tests__/login.test.ts diff --git a/extension/src/popup/components/types/__tests__/login.test.ts b/extension/src/popup/components/types/__tests__/login.test.ts new file mode 100644 index 0000000..50400bb --- /dev/null +++ b/extension/src/popup/components/types/__tests__/login.test.ts @@ -0,0 +1,122 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('../../../../shared/state', async () => { + const navigate = vi.fn(); + const setState = vi.fn(); + const sendMessage = vi.fn(); + const getState = vi.fn(() => ({ + view: 'add', entries: [], selectedId: null, selectedItem: null, selectedIndex: 0, + searchQuery: '', activeGroup: null, error: null, loading: false, + capturedTabId: null, capturedUrl: '', newType: 'login', + generatorDefaults: null, + })); + const escapeHtml = (s: string) => s + .replace(/&/g, '&').replace(//g, '>') + .replace(/"/g, '"').replace(/'/g, '''); + return { + navigate, setState, sendMessage, getState, escapeHtml, + popOutToTab: vi.fn(), isInTab: vi.fn(() => false), openVaultTab: vi.fn(), + }; +}); + +// Mock setup-helpers (scheduleRate used by wirePasswordStrength) +vi.mock('../../../../setup/setup-helpers', () => ({ + scheduleRate: vi.fn(), + STRENGTH_LABELS: {}, + entropyText: vi.fn(() => ''), +})); + +import { renderForm } from '../login'; +import { sendMessage } from '../../../../shared/state'; + +describe('login form smart inputs', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + // chrome.storage.local stub (needed by wireNotesMonoToggle) + (globalThis as any).chrome = { + storage: { + local: { + get: vi.fn().mockImplementation((_keys: any, cb: any) => cb({})), + set: vi.fn().mockImplementation((_obj: any, cb: any) => cb && cb()), + }, + }, + runtime: { + sendMessage: vi.fn(), + }, + }; + vi.mocked(sendMessage).mockReset(); + vi.mocked(sendMessage).mockResolvedValue({ ok: true, data: { groups: [] } }); + }); + + it('renders all 6 smart-input slots in the form', async () => { + const app = document.getElementById('app')!; + renderForm(app, 'add', null); + + expect(document.querySelector('#fill-from-tab-btn')).not.toBeNull(); + expect(document.querySelector('#hostname-chip-row')).not.toBeNull(); + expect(document.querySelector('#reveal-password-btn')).not.toBeNull(); + expect(document.querySelector('#strength-bar-row')).not.toBeNull(); + expect(document.querySelector('#totp-preview-row')).not.toBeNull(); + expect(document.querySelector('#totp-qr-btn')).not.toBeNull(); + expect(document.querySelector('#totp-qr-panel')).not.toBeNull(); + expect(document.querySelector('#notes-mono-btn')).not.toBeNull(); + }); +}); + +describe('Login save shape', () => { + beforeEach(() => { + document.body.innerHTML = '
'; + (globalThis as any).chrome = { + storage: { + local: { + get: vi.fn().mockImplementation((_keys: any, cb: any) => cb({})), + set: vi.fn().mockImplementation((_obj: any, cb: any) => cb && cb()), + }, + }, + runtime: { sendMessage: vi.fn() }, + }; + vi.mocked(sendMessage).mockReset(); + vi.mocked(sendMessage).mockImplementation(async (msg: any) => { + if (msg.type === 'list_groups') return { ok: true, data: { groups: [] } }; + if (msg.type === 'preview_totp_from_secret') return { ok: false }; + return { ok: true, data: { id: 'fakeid0000000000', items: [] } }; + }); + }); + + it('saves a login item with url, username, and password', async () => { + const app = document.getElementById('app')!; + renderForm(app, 'add', null); + + (document.getElementById('f-title') as HTMLInputElement).value = 'GitHub'; + (document.getElementById('f-url') as HTMLInputElement).value = 'https://github.com/login'; + (document.getElementById('f-username') as HTMLInputElement).value = 'alice'; + (document.getElementById('f-password') as HTMLInputElement).value = 'hunter2'; + + document.getElementById('save-btn')!.click(); + await new Promise(r => setTimeout(r, 5)); + + const calls = vi.mocked(sendMessage).mock.calls; + const addCall = calls.find(([msg]) => (msg as { type: string }).type === 'add_item'); + expect(addCall).toBeDefined(); + const msg = addCall![0] as { type: 'add_item'; item: any }; + expect(msg.item.type).toBe('login'); + expect(msg.item.core).toMatchObject({ + type: 'login', + username: 'alice', + password: 'hunter2', + url: 'https://github.com/login', + }); + }); + + it('rejects save when title is empty', async () => { + const app = document.getElementById('app')!; + renderForm(app, 'add', null); + + document.getElementById('save-btn')!.click(); + await new Promise(r => setTimeout(r, 5)); + + const calls = vi.mocked(sendMessage).mock.calls; + const addCall = calls.find(([msg]) => (msg as { type: string }).type === 'add_item'); + expect(addCall).toBeUndefined(); + }); +}); diff --git a/extension/src/popup/components/types/login.ts b/extension/src/popup/components/types/login.ts index 16f2308..aff7dff 100644 --- a/extension/src/popup/components/types/login.ts +++ b/extension/src/popup/components/types/login.ts @@ -22,10 +22,20 @@ import { wireAttachmentsDisclosure, teardownAttachmentsDisclosure, } from '../attachments-disclosure'; +import { wireFillFromTab, wireHostnameChip } from '../../../shared/form-affordances/url-tools'; +import { wireGroupAutocomplete } from '../../../shared/form-affordances/group-autocomplete'; +import { wirePasswordReveal, wirePasswordStrength } from '../../../shared/form-affordances/password-tools'; +import { wireTotpPreview, wireTotpQr } from '../../../shared/form-affordances/totp-tools'; +import { wireNotesMonoToggle } from '../../../shared/form-affordances/notes-tools'; +import { scheduleRate } from '../../../setup/setup-helpers'; /// Called by the dispatcher before each render. Stops any in-flight /// tickers / intervals / listeners the previous view may have attached. export function teardown(): void { + for (const fn of pendingAffordanceTeardowns) { + try { fn(); } catch { /* best effort */ } + } + pendingAffordanceTeardowns = []; teardownAttachmentsDisclosure(); stopTotpTicker(); if (activeKeyHandler) { @@ -202,6 +212,7 @@ let totpTickerId: ReturnType | null = null; let activeKeyHandler: ((e: KeyboardEvent) => void) | null = null; let activeFormEscHandler: ((e: KeyboardEvent) => void) | null = null; let sectionsExpanded = false; +let pendingAffordanceTeardowns: Array<() => void> = []; function stopTotpTicker(): void { if (totpTickerId !== null) { clearInterval(totpTickerId); totpTickerId = null; } } @@ -247,23 +258,63 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite
${renderFormHeader({ titleText: mode === 'add' ? 'new login' : 'edit login' })} ${state.error ? `
${escapeHtml(state.error)}
` : ''} +
-
-
+ +
+ +
+ + +
+ +
+
-
+ +
+
- -
-
-
+ + +
+ +
+ +
+ +
+ + +
+ + +
+
-
-
+ +
+
+ + +
+ +
+ ${renderSectionsEditor(sectionsDraft, sectionsExpanded)} ${isInTab() ? renderAttachmentsDisclosure({ itemId: existing?.id ?? '', attachments: attachmentsDraft, mode: 'edit' }) : ''}
@@ -305,6 +356,26 @@ export function renderForm(app: HTMLElement, mode: 'add' | 'edit', existing: Ite wireDisclosure(); } + // ---- Smart input affordances ------------------------------------------ + // Each wireXxx call attaches event listeners to the just-rendered form. + // Affordances that hold timers/intervals return a teardown fn we collect + // here and run from the form's existing teardown() entry point. + const affordanceTeardowns: Array<() => void> = []; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sm = sendMessage as any; + wireFillFromTab(app, { sendMessage: sm }); + wireHostnameChip(app); + void wireGroupAutocomplete(app, { sendMessage: sm }); + affordanceTeardowns.push(wirePasswordReveal(app)); + wirePasswordStrength(app, { scheduleRate }); + affordanceTeardowns.push(wireTotpPreview(app, { sendMessage: sm })); + wireTotpQr(app); + void wireNotesMonoToggle(app, { itemId: existing?.id ?? '' }); + + // Stash teardown-runner so the existing `teardown()` calls it. + pendingAffordanceTeardowns = affordanceTeardowns; + document.getElementById('gen-btn')?.addEventListener('click', (e) => { const trigger = e.currentTarget as HTMLElement; if (isGeneratorPanelOpen()) { diff --git a/extension/src/shared/form-affordances/group-autocomplete.ts b/extension/src/shared/form-affordances/group-autocomplete.ts index 60a4c89..fd3d64f 100644 --- a/extension/src/shared/form-affordances/group-autocomplete.ts +++ b/extension/src/shared/form-affordances/group-autocomplete.ts @@ -7,8 +7,13 @@ const DATALIST_ID = 'groups-datalist'; export async function wireGroupAutocomplete(form: HTMLElement, opts: GroupAutocompleteOpts): Promise { const input = form.querySelector('#f-group'); if (!input) return; - const resp = await opts.sendMessage({ type: 'list_groups' }); - if (!resp.ok || !resp.data) return; + let resp: Awaited>; + try { + resp = await opts.sendMessage({ type: 'list_groups' }); + } catch { + return; + } + if (!resp?.ok || !resp.data?.groups) return; // Datalists must live in the document, not nested inside an input. Reuse if // we've already mounted one this session. diff --git a/extension/src/shared/form-affordances/notes-tools.ts b/extension/src/shared/form-affordances/notes-tools.ts index 548973e..ac9ef47 100644 --- a/extension/src/shared/form-affordances/notes-tools.ts +++ b/extension/src/shared/form-affordances/notes-tools.ts @@ -10,14 +10,20 @@ export async function wireNotesMonoToggle(form: HTMLElement, opts: NotesMonoOpts if (!btn || !ta) return; const key = `notesMono.${opts.itemId || '__new__'}`; - const stored = await new Promise((resolve) => { - chrome.storage.local.get([key], (result) => resolve(!!result[key])); - }); - if (stored) ta.classList.add('f-notes--mono'); + + // chrome.storage may be absent in test environments — guard gracefully. + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + const stored = await new Promise((resolve) => { + chrome.storage.local.get([key], (result) => resolve(!!result[key])); + }); + if (stored) ta.classList.add('f-notes--mono'); + } btn.addEventListener('click', () => { const next = !ta.classList.contains('f-notes--mono'); ta.classList.toggle('f-notes--mono', next); - chrome.storage.local.set({ [key]: next }, () => { /* fire and forget */ }); + if (typeof chrome !== 'undefined' && chrome.storage?.local) { + chrome.storage.local.set({ [key]: next }, () => { /* fire and forget */ }); + } }); }