diff --git a/README.md b/README.md index 858cc46..2b94b73 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,11 @@ Created 'example.png' with 8 frames. # Code Examples -- [spliting QRs](bbqr/split.py) -- [joining QRs](bbqr/join.py) -- [binary to internal encoding](bbqr/utils.py) -- [wrapper CLI](bbqr/cli.py) +- Splitting QRs: [Python](python/bbqr/split.py), [JS](js/src/split.ts) +- Joining QRs: [Python](python/bbqr/join.py), [JS](js/src/join.ts) +- Binary to internal encoding: [Python](python/bbqr/utils.py), [JS](js/src/utils.ts) +- Wrapper CLI: [Python](python/bbqr/cli.py) +- [Example of using the JS implementation](https://bbqr.org/js-demo.html) # License diff --git a/js-demo.html b/js-demo.html new file mode 100644 index 0000000..04567c4 --- /dev/null +++ b/js-demo.html @@ -0,0 +1,302 @@ + + + + + + + BBQr JS Demo + + + + + + + + +
+
+BBBBBBBBBBBBBBBBB   BBBBBBBBBBBBBBBBB        QQQQQQQQQ                          
+B::::::::::::::::B  B::::::::::::::::B     QQ:::::::::QQ                        
+B::::::BBBBBB:::::B B::::::BBBBBB:::::B  QQ:::::::::::::QQ                      
+BB:::::B     B:::::BBB:::::B     B:::::BQ:::::::QQQ:::::::Q                     
+  B::::B     B:::::B  B::::B     B:::::BQ::::::O   Q::::::Q rrrrr   rrrrrrrrr   
+  B::::B     B:::::B  B::::B     B:::::BQ:::::O     Q:::::Q r::::rrr:::::::::r  
+  B::::BBBBBB:::::B   B::::BBBBBB:::::B Q:::::O     Q:::::Q r:::::::::::::::::r 
+  B:::::::::::::BB    B:::::::::::::BB  Q:::::O     Q:::::Q rr::::::rrrrr::::::r
+  B::::BBBBBB:::::B   B::::BBBBBB:::::B Q:::::O     Q:::::Q  r:::::r     r:::::r
+  B::::B     B:::::B  B::::B     B:::::BQ:::::O     Q:::::Q  r:::::r     rrrrrrr
+  B::::B     B:::::B  B::::B     B:::::BQ:::::O  QQQQ:::::Q  r:::::r            
+  B::::B     B:::::B  B::::B     B:::::BQ::::::O Q::::::::Q  r:::::r            
+BB:::::BBBBBB::::::BBB:::::BBBBBB::::::BQ:::::::QQ::::::::Q  r:::::r            
+B:::::::::::::::::B B:::::::::::::::::B  QQ::::::::::::::Q   r:::::r            
+B::::::::::::::::B  B::::::::::::::::B     QQ:::::::::::Q    r:::::r            
+BBBBBBBBBBBBBBBBB   BBBBBBBBBBBBBBBBB        QQQQQQQQ::::QQ  rrrrrrr            
+                                                      Q:::::Q                    
+                                                      QQQQQQ
+
+ +
+

+ This page makes use of the + BBQr JavaScript library + to apply BBQr encoding to PSBTs, raw Bitcoin transactions, or other types of data. Visit the + BBQr home page for more information. +

+ +

How to use?

+ + + +

+ Note: PSBTs and Bitcoin transactions are supported in raw binary format as well as + encoded as Base64 or HEX. +

+ + + + +
+

BBQr created

+
+
+ +
+
+ +
+

Error!

+
+
+ +
+
+ +
+

Generating BBQr, please wait...

+ +
+
+
+ +
+

Drop your file here!

+
+ + + + diff --git a/js/.gitignore b/js/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/js/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..a43991f --- /dev/null +++ b/js/README.md @@ -0,0 +1,122 @@ +# BBQr JavaScript / TypeScript implementation + +This is an implementation of [BBQr - Better Bitcoin QR](https://bbqr.org) for the browser, written in TypeScript. + +## Installation + +Install with npm (or your package manager of choice): + +```sh +npm install bbqr +``` + +## Usage + +### Low-level splitting/joining + +```js +import { joinQRs, splitQRs } from 'bbqr'; + +// Create a Uint8Array containing the raw bytes of a PSBT, transaction or other data +const input = new Uint8Array([ + 112, + 115, + 98, + 116, + 255, // ... rest of PSBT bytes +]); + +// ... + +const fileType = 'P'; // 'P' is for PSBT + +const splitResult = splitQRs(input, fileType, { + // these are optional - default values are shown + encoding: 'Z', // Zlib compressed base32 encoding + minSplit: 1, // minimum number of parts to return + maxSplit: 1295, // maximum number of parts to return + minVersion: 5, // minimum QR code version + maxVersion: 40, // maximum QR code version +}); + +// the QR code version chosen for best efficiency +console.log(splitResult.version); + +// the actual encoding used - could be '2' (uncompressed base32) if the 'Z' option didn't provide a smaller result +console.log(splitResult.encoding); + +// the QR code parts +console.log(splitResult.parts); + +// now we do this in reverse and get back the bytes + +const reassembled = joinQRs(splitResult.parts); + +console.log(reassembled.fileType === fileType); // true +console.log(reassembled.encoding === splitResult.encoding); // true +console.log(reassembled.raw.every((byte, i) => byte === input[i])); // true +``` + +### Detecting the File Type + +In the previous example, we provided the raw bytes and specified the File Type to use. You can also +detect the file type by calling `detectFileType` with a `Uint8Array`, a `File` object or a `string` (the contents of +a text file). + +This is especially useful for PSBTs and Bitcoin transactions as `detectFileType` can also detect these +in HEX and Base64 format. + +If a PSBT or transaction is not detected, the file type will be: + +- `J` for a text file that can be successfully parsed as JSON. +- `U` for all other text files. +- `B` for all other binary files. + +For other supported type codes, you should do the conersion to `Uint8Array` not rely on `detectFileType`. + +```js +import { detectFileType } from 'bbqr'; + +// hex representation of a Bitcoin transaction +const contents = + '01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0704ffff001d0104ffffffff0100f2052a0100000043410496b538e853519c726a2c91e61ec11600ae1390813a627c66fb8be7947be63c52da7589379515d4e0a604f8141781e62294721166bf621e73a82cbf2342c858eeac00000000'; + +const detected = await detectFileType(contents); + +// 'T' for transaction +console.log(detected.fileType); + +// Uint8Array of the transaction bytes +console.log(detected.raw); +``` + +### Generating QR Code Images + +The result from `splitQRs` can be passed to `renderQRImage` to generate (animated) PNG images of +the QR codes. The result is an `ArrayBuffer` which can easily be converted to a suitable format. + +```js +import { renderQRImage } from 'bbqr'; + +// get an ArrayBuffer containing the PNG image data +const imgBuffer = await renderQRImage(splitResult.parts, splitResult.version, { + // optional settings - values here are the defaults + frameDelay: 250, + randomizeOrder: false, +}); + +// convert to data URL for display +const base64String = btoa(String.fromCharCode(...new Uint8Array(imgBuffer))); +const dataUrl = `data:image/png;base64,${base64String}`; + +document.body.innerHTML += ``; +``` + +## Developing + +This library is built with [Vite](https://vitejs.dev). + +- The `index.html` and `demo.ts` files are for local development. +- Start the Vite dev server: `npm run dev`. +- Start the Vitest tests in watch mode: `npm run test:watch` +- Building for production: `npm run build` diff --git a/js/demo.ts b/js/demo.ts new file mode 100644 index 0000000..c6e2616 --- /dev/null +++ b/js/demo.ts @@ -0,0 +1,96 @@ +// code only for the demo page, not part of the library + +import { detectFileType, renderQRImage, splitQRs } from './src/main'; + +const resultEl = document.querySelector('#result')!; +const inputEl = document.querySelector('#text-input')!; + +function clearPrevious() { + const existingImgs = resultEl.querySelectorAll('img'); + + existingImgs.forEach((img) => { + // remove references to any old images + URL.revokeObjectURL(img.src); + }); + + resultEl.innerHTML = ''; +} + +let busy = false; + +async function handleFileOrTextInput(input: File | string) { + if (busy) { + return; + } + + busy = true; + + try { + clearPrevious(); + + let resultMsg = ''; + + const { raw, fileType } = await detectFileType(input); + + resultMsg += `Detected file type: ${fileType}
`; + + const { parts, version } = splitQRs(raw, fileType, { encoding: 'Z' }); + + const imgBuf = await renderQRImage(parts, version); + + if (parts.length === 1) { + resultMsg += `A single QR version ${version} will be needed.`; + } else { + resultMsg += `Need ${parts.length} QRs of version ${version}.`; + } + + resultEl.innerHTML = `

${resultMsg}

`; + + const url = URL.createObjectURL(new Blob([imgBuf], { type: 'image/png' })); + + resultEl.innerHTML += `QR codes`; + } finally { + busy = false; + } +} + +document.addEventListener('dragover', (e) => { + // prevent browser from opening the file when dropped + e.preventDefault(); +}); + +document.addEventListener('drop', (e) => { + e.preventDefault(); + + if (!e.dataTransfer) { + return; + } + + const files: File[] = []; + + for (const item of e.dataTransfer.items) { + if (item.kind === 'file') { + const file = item.getAsFile(); + + if (file) { + files.push(file); + } + } + } + + if (files.length > 1) { + throw new Error('Only one file at a time, please.'); + } else if (files.length === 1) { + inputEl.value = ''; + handleFileOrTextInput(files[0]); + } +}); + +// detect paste in textarea +inputEl.addEventListener('paste', (e) => { + const text = e.clipboardData?.getData('text'); + + if (text) { + handleFileOrTextInput(text); + } +}); diff --git a/js/index.html b/js/index.html new file mode 100644 index 0000000..79a8884 --- /dev/null +++ b/js/index.html @@ -0,0 +1,57 @@ + + + + + + BBQr JS + + + + +

+ Drop a file into this window or paste contents below. +

+ + + +
+ + + diff --git a/js/package-lock.json b/js/package-lock.json new file mode 100644 index 0000000..1dab204 --- /dev/null +++ b/js/package-lock.json @@ -0,0 +1,2607 @@ +{ + "name": "bbqr", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "bbqr", + "version": "0.0.1", + "devDependencies": { + "@scure/base": "^1.1.5", + "@types/pako": "^2.0.3", + "@types/qrcode": "^1.5.5", + "@types/upng-js": "^2.1.5", + "pako": "^2.1.0", + "qrcode": "^1.5.3", + "typescript": "^5.3.3", + "upng-js": "^2.1.0", + "vite": "^5.1.3", + "vite-plugin-dts": "^3.7.2", + "vitest": "^1.3.1" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@microsoft/api-extractor": { + "version": "7.39.0", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.39.0.tgz", + "integrity": "sha512-PuXxzadgnvp+wdeZFPonssRAj/EW4Gm4s75TXzPk09h3wJ8RS3x7typf95B4vwZRrPTQBGopdUl+/vHvlPdAcg==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor-model": "7.28.3", + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.62.0", + "@rushstack/rig-package": "0.5.1", + "@rushstack/ts-command-line": "4.17.1", + "colors": "~1.2.1", + "lodash": "~4.17.15", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "source-map": "~0.6.1", + "typescript": "5.3.3" + }, + "bin": { + "api-extractor": "bin/api-extractor" + } + }, + "node_modules/@microsoft/api-extractor-model": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.3.tgz", + "integrity": "sha512-wT/kB2oDbdZXITyDh2SQLzaWwTOFbV326fP0pUwNW00WeliARs0qjmXBWmGWardEzp2U3/axkO3Lboqun6vrig==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "@microsoft/tsdoc-config": "~0.16.1", + "@rushstack/node-core-library": "3.62.0" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", + "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz", + "integrity": "sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.14.2", + "ajv": "~6.12.6", + "jju": "~1.4.0", + "resolve": "~1.19.0" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/resolve": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", + "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", + "dev": true, + "dependencies": { + "is-core-module": "^2.1.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz", + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz", + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz", + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz", + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz", + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz", + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz", + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz", + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz", + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz", + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz", + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz", + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz", + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rushstack/node-core-library": { + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.62.0.tgz", + "integrity": "sha512-88aJn2h8UpSvdwuDXBv1/v1heM6GnBf3RjEy6ZPP7UnzHNCqOHA2Ut+ScYUbXcqIdfew9JlTAe3g+cnX9xQ/Aw==", + "dev": true, + "dependencies": { + "colors": "~1.2.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4", + "z-schema": "~5.0.2" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/rig-package": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", + "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", + "dev": true, + "dependencies": { + "resolve": "~1.22.1", + "strip-json-comments": "~3.1.1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz", + "integrity": "sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==", + "dev": true, + "dependencies": { + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "colors": "~1.2.1", + "string-argv": "~0.3.1" + } + }, + "node_modules/@scure/base": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", + "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "dev": true, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/pako": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz", + "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==", + "dev": true + }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/upng-js": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/upng-js/-/upng-js-2.1.5.tgz", + "integrity": "sha512-CzXg1lcCcWzrmYmke9BLbBPzb2DpdC1bXuXf0BtK3Bygvsozslei8S1bheDI1QUfZzZpMeQI5fywfnMj4CxocQ==", + "dev": true + }, + "node_modules/@vitest/expect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.3.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/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, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz", + "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.19", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.0.2" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz", + "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.4.19", + "@vue/shared": "3.4.19" + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/shared": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz", + "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "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, + "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 + }, + "node_modules/colors": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.2.5.tgz", + "integrity": "sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "dev": true, + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encode-utf8": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-1.0.3.tgz", + "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", + "dev": true + }, + "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, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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 + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "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, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "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, + "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, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "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 + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.30.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", + "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "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 + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.0.tgz", + "integrity": "sha512-YOvg9hfYQmnaB56Yb+KrJE2u0Yzz5zR+sLejEvF4fzwzV1Al6hkf2vyHTwqCRyv0hCi9rVCqVoXpyYevQIRwLQ==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "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, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "dev": true + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "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, + "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, + "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 + }, + "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 + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "dev": true, + "dependencies": { + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz", + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.12.0", + "@rollup/rollup-android-arm64": "4.12.0", + "@rollup/rollup-darwin-arm64": "4.12.0", + "@rollup/rollup-darwin-x64": "4.12.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0", + "@rollup/rollup-linux-arm64-gnu": "4.12.0", + "@rollup/rollup-linux-arm64-musl": "4.12.0", + "@rollup/rollup-linux-riscv64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-gnu": "4.12.0", + "@rollup/rollup-linux-x64-musl": "4.12.0", + "@rollup/rollup-win32-arm64-msvc": "4.12.0", + "@rollup/rollup-win32-ia32-msvc": "4.12.0", + "@rollup/rollup-win32-x64-msvc": "4.12.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "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, + "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, + "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 + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "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 + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", + "dev": true, + "dependencies": { + "js-tokens": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/upng-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/upng-js/-/upng-js-2.1.0.tgz", + "integrity": "sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==", + "dev": true, + "dependencies": { + "pako": "^1.0.5" + } + }, + "node_modules/upng-js/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.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": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "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/vite-plugin-dts": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-3.7.2.tgz", + "integrity": "sha512-kg//1nDA01b8rufJf4TsvYN8LMkdwv0oBYpiQi6nRwpHyue+wTlhrBiqgipdFpMnW1oOYv6ywmzE5B0vg6vSEA==", + "dev": true, + "dependencies": { + "@microsoft/api-extractor": "7.39.0", + "@rollup/pluginutils": "^5.1.0", + "@vue/language-core": "^1.8.26", + "debug": "^4.3.4", + "kolorist": "^1.8.0", + "vue-tsc": "^1.8.26" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "typescript": "*", + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.3.1", + "why-is-node-running": "^2.2.2" + }, + "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": "1.3.1", + "@vitest/ui": "1.3.1", + "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/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "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, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "dev": true + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "dev": true, + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + } + } +} diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..c8e3e9e --- /dev/null +++ b/js/package.json @@ -0,0 +1,42 @@ +{ + "name": "bbqr", + "private": false, + "version": "1.0.0", + "repository": { + "type": "git", + "url": "git://github.com/coinkite/BBQr.git", + "directory": "js" + }, + "type": "module", + "files": [ + "dist" + ], + "module": "./dist/bbqr.js", + "browser": "./dist/bbqr.iife.js", + "types": "./dist/main.d.ts", + "exports": { + ".": { + "import": "./dist/bbqr.js", + "types": "./dist/main.d.ts" + } + }, + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "test": "vitest run", + "test:watch": "vitest" + }, + "devDependencies": { + "@scure/base": "^1.1.5", + "@types/pako": "^2.0.3", + "@types/qrcode": "^1.5.5", + "@types/upng-js": "^2.1.5", + "pako": "^2.1.0", + "qrcode": "^1.5.3", + "typescript": "^5.3.3", + "upng-js": "^2.1.0", + "vite": "^5.1.3", + "vite-plugin-dts": "^3.7.2", + "vitest": "^1.3.1" + } +} diff --git a/js/src/consts.ts b/js/src/consts.ts new file mode 100644 index 0000000..27fffdb --- /dev/null +++ b/js/src/consts.ts @@ -0,0 +1,273 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * Constants and fixed values. + */ + +// Fixed-length header +export const HEADER_LEN = 8; + +export const FILETYPE_NAMES = { + P: 'PSBT', + T: 'Transaction', + J: 'JSON', + U: 'Unicode Text', + X: 'Executable', + B: 'Binary', +} as const; + +export const FILETYPES = new Set(Object.keys(FILETYPE_NAMES)); + +export const ENCODING_NAMES = { + H: 'HEX', + Z: 'Zlib compressed', + '2': 'Base32', +} as const; + +export const ENCODINGS = new Set(Object.keys(ENCODING_NAMES)); + +// taken from: https://github.com/mnooner256/pyqrcode/blob/674a77b5eaf850d063f518bd90c243ee34ad6b5d/pyqrcode/tables.py#L84 +export const QR_DATA_CAPACITY = { + 1: { + L: { 0: 152, 1: 41, 2: 25, 4: 17, 8: 10 }, + M: { 0: 128, 1: 34, 2: 20, 4: 14, 8: 8 }, + Q: { 0: 104, 1: 27, 2: 16, 4: 11, 8: 7 }, + H: { 0: 72, 1: 17, 2: 10, 4: 7, 8: 4 }, + }, + 2: { + L: { 0: 272, 1: 77, 2: 47, 4: 32, 8: 20 }, + M: { 0: 224, 1: 63, 2: 38, 4: 26, 8: 16 }, + Q: { 0: 176, 1: 48, 2: 29, 4: 20, 8: 12 }, + H: { 0: 128, 1: 34, 2: 20, 4: 14, 8: 8 }, + }, + 3: { + L: { 0: 440, 1: 127, 2: 77, 4: 53, 8: 32 }, + M: { 0: 352, 1: 101, 2: 61, 4: 42, 8: 26 }, + Q: { 0: 272, 1: 77, 2: 47, 4: 32, 8: 20 }, + H: { 0: 208, 1: 58, 2: 35, 4: 24, 8: 15 }, + }, + 4: { + L: { 0: 640, 1: 187, 2: 114, 4: 78, 8: 48 }, + M: { 0: 512, 1: 149, 2: 90, 4: 62, 8: 38 }, + Q: { 0: 384, 1: 111, 2: 67, 4: 46, 8: 28 }, + H: { 0: 288, 1: 82, 2: 50, 4: 34, 8: 21 }, + }, + 5: { + L: { 0: 864, 1: 255, 2: 154, 4: 106, 8: 65 }, + M: { 0: 688, 1: 202, 2: 122, 4: 84, 8: 52 }, + Q: { 0: 496, 1: 144, 2: 87, 4: 60, 8: 37 }, + H: { 0: 368, 1: 106, 2: 64, 4: 44, 8: 27 }, + }, + 6: { + L: { 0: 1088, 1: 322, 2: 195, 4: 134, 8: 82 }, + M: { 0: 864, 1: 255, 2: 154, 4: 106, 8: 65 }, + Q: { 0: 608, 1: 178, 2: 108, 4: 74, 8: 45 }, + H: { 0: 480, 1: 139, 2: 84, 4: 58, 8: 36 }, + }, + 7: { + L: { 0: 1248, 1: 370, 2: 224, 4: 154, 8: 95 }, + M: { 0: 992, 1: 293, 2: 178, 4: 122, 8: 75 }, + Q: { 0: 704, 1: 207, 2: 125, 4: 86, 8: 53 }, + H: { 0: 528, 1: 154, 2: 93, 4: 64, 8: 39 }, + }, + 8: { + L: { 0: 1552, 1: 461, 2: 279, 4: 192, 8: 118 }, + M: { 0: 1232, 1: 365, 2: 221, 4: 152, 8: 93 }, + Q: { 0: 880, 1: 259, 2: 157, 4: 108, 8: 66 }, + H: { 0: 688, 1: 202, 2: 122, 4: 84, 8: 52 }, + }, + 9: { + L: { 0: 1856, 1: 552, 2: 335, 4: 230, 8: 141 }, + M: { 0: 1456, 1: 432, 2: 262, 4: 180, 8: 111 }, + Q: { 0: 1056, 1: 312, 2: 189, 4: 130, 8: 80 }, + H: { 0: 800, 1: 235, 2: 143, 4: 98, 8: 60 }, + }, + 10: { + L: { 0: 2192, 1: 652, 2: 395, 4: 271, 8: 167 }, + M: { 0: 1728, 1: 513, 2: 311, 4: 213, 8: 131 }, + Q: { 0: 1232, 1: 364, 2: 221, 4: 151, 8: 93 }, + H: { 0: 976, 1: 288, 2: 174, 4: 119, 8: 74 }, + }, + 11: { + L: { 0: 2592, 1: 772, 2: 468, 4: 321, 8: 198 }, + M: { 0: 2032, 1: 604, 2: 366, 4: 251, 8: 155 }, + Q: { 0: 1440, 1: 427, 2: 259, 4: 177, 8: 109 }, + H: { 0: 1120, 1: 331, 2: 200, 4: 137, 8: 85 }, + }, + 12: { + L: { 0: 2960, 1: 883, 2: 535, 4: 367, 8: 226 }, + M: { 0: 2320, 1: 691, 2: 419, 4: 287, 8: 177 }, + Q: { 0: 1648, 1: 489, 2: 296, 4: 203, 8: 125 }, + H: { 0: 1264, 1: 374, 2: 227, 4: 155, 8: 96 }, + }, + 13: { + L: { 0: 3424, 1: 1022, 2: 619, 4: 425, 8: 262 }, + M: { 0: 2672, 1: 796, 2: 483, 4: 331, 8: 204 }, + Q: { 0: 1952, 1: 580, 2: 352, 4: 241, 8: 149 }, + H: { 0: 1440, 1: 427, 2: 259, 4: 177, 8: 109 }, + }, + 14: { + L: { 0: 3688, 1: 1101, 2: 667, 4: 458, 8: 282 }, + M: { 0: 2920, 1: 871, 2: 528, 4: 362, 8: 223 }, + Q: { 0: 2088, 1: 621, 2: 376, 4: 258, 8: 159 }, + H: { 0: 1576, 1: 468, 2: 283, 4: 194, 8: 120 }, + }, + 15: { + L: { 0: 4184, 1: 1250, 2: 758, 4: 520, 8: 320 }, + M: { 0: 3320, 1: 991, 2: 600, 4: 412, 8: 254 }, + Q: { 0: 2360, 1: 703, 2: 426, 4: 292, 8: 180 }, + H: { 0: 1784, 1: 530, 2: 321, 4: 220, 8: 136 }, + }, + 16: { + L: { 0: 4712, 1: 1408, 2: 854, 4: 586, 8: 361 }, + M: { 0: 3624, 1: 1082, 2: 656, 4: 450, 8: 277 }, + Q: { 0: 2600, 1: 775, 2: 470, 4: 322, 8: 198 }, + H: { 0: 2024, 1: 602, 2: 365, 4: 250, 8: 154 }, + }, + 17: { + L: { 0: 5176, 1: 1548, 2: 938, 4: 644, 8: 397 }, + M: { 0: 4056, 1: 1212, 2: 734, 4: 504, 8: 310 }, + Q: { 0: 2936, 1: 876, 2: 531, 4: 364, 8: 224 }, + H: { 0: 2264, 1: 674, 2: 408, 4: 280, 8: 173 }, + }, + 18: { + L: { 0: 5768, 1: 1725, 2: 1046, 4: 718, 8: 442 }, + M: { 0: 4504, 1: 1346, 2: 816, 4: 560, 8: 345 }, + Q: { 0: 3176, 1: 948, 2: 574, 4: 394, 8: 243 }, + H: { 0: 2504, 1: 746, 2: 452, 4: 310, 8: 191 }, + }, + 19: { + L: { 0: 6360, 1: 1903, 2: 1153, 4: 792, 8: 488 }, + M: { 0: 5016, 1: 1500, 2: 909, 4: 624, 8: 384 }, + Q: { 0: 3560, 1: 1063, 2: 644, 4: 442, 8: 272 }, + H: { 0: 2728, 1: 813, 2: 493, 4: 338, 8: 208 }, + }, + 20: { + L: { 0: 6888, 1: 2061, 2: 1249, 4: 858, 8: 528 }, + M: { 0: 5352, 1: 1600, 2: 970, 4: 666, 8: 410 }, + Q: { 0: 3880, 1: 1159, 2: 702, 4: 482, 8: 297 }, + H: { 0: 3080, 1: 919, 2: 557, 4: 382, 8: 235 }, + }, + 21: { + L: { 0: 7456, 1: 2232, 2: 1352, 4: 929, 8: 572 }, + M: { 0: 5712, 1: 1708, 2: 1035, 4: 711, 8: 438 }, + Q: { 0: 4096, 1: 1224, 2: 742, 4: 509, 8: 314 }, + H: { 0: 3248, 1: 969, 2: 587, 4: 403, 8: 248 }, + }, + 22: { + L: { 0: 8048, 1: 2409, 2: 1460, 4: 1003, 8: 618 }, + M: { 0: 6256, 1: 1872, 2: 1134, 4: 779, 8: 480 }, + Q: { 0: 4544, 1: 1358, 2: 823, 4: 565, 8: 348 }, + H: { 0: 3536, 1: 1056, 2: 640, 4: 439, 8: 270 }, + }, + 23: { + L: { 0: 8752, 1: 2620, 2: 1588, 4: 1091, 8: 672 }, + M: { 0: 6880, 1: 2059, 2: 1248, 4: 857, 8: 528 }, + Q: { 0: 4912, 1: 1468, 2: 890, 4: 611, 8: 376 }, + H: { 0: 3712, 1: 1108, 2: 672, 4: 461, 8: 284 }, + }, + 24: { + L: { 0: 9392, 1: 2812, 2: 1704, 4: 1171, 8: 721 }, + M: { 0: 7312, 1: 2188, 2: 1326, 4: 911, 8: 561 }, + Q: { 0: 5312, 1: 1588, 2: 963, 4: 661, 8: 407 }, + H: { 0: 4112, 1: 1228, 2: 744, 4: 511, 8: 315 }, + }, + 25: { + L: { 0: 10208, 1: 3057, 2: 1853, 4: 1273, 8: 784 }, + M: { 0: 8000, 1: 2395, 2: 1451, 4: 997, 8: 614 }, + Q: { 0: 5744, 1: 1718, 2: 1041, 4: 715, 8: 440 }, + H: { 0: 4304, 1: 1286, 2: 779, 4: 535, 8: 330 }, + }, + 26: { + L: { 0: 10960, 1: 3283, 2: 1990, 4: 1367, 8: 842 }, + M: { 0: 8496, 1: 2544, 2: 1542, 4: 1059, 8: 652 }, + Q: { 0: 6032, 1: 1804, 2: 1094, 4: 751, 8: 462 }, + H: { 0: 4768, 1: 1425, 2: 864, 4: 593, 8: 365 }, + }, + 27: { + L: { 0: 11744, 1: 3514, 2: 2132, 4: 1465, 8: 902 }, + M: { 0: 9024, 1: 2701, 2: 1637, 4: 1125, 8: 692 }, + Q: { 0: 6464, 1: 1933, 2: 1172, 4: 805, 8: 496 }, + H: { 0: 5024, 1: 1501, 2: 910, 4: 625, 8: 385 }, + }, + 28: { + L: { 0: 12248, 1: 3669, 2: 2223, 4: 1528, 8: 940 }, + M: { 0: 9544, 1: 2857, 2: 1732, 4: 1190, 8: 732 }, + Q: { 0: 6968, 1: 2085, 2: 1263, 4: 868, 8: 534 }, + H: { 0: 5288, 1: 1581, 2: 958, 4: 658, 8: 405 }, + }, + 29: { + L: { 0: 13048, 1: 3909, 2: 2369, 4: 1628, 8: 1002 }, + M: { 0: 10136, 1: 3035, 2: 1839, 4: 1264, 8: 778 }, + Q: { 0: 7288, 1: 2181, 2: 1322, 4: 908, 8: 559 }, + H: { 0: 5608, 1: 1677, 2: 1016, 4: 698, 8: 430 }, + }, + 30: { + L: { 0: 13880, 1: 4158, 2: 2520, 4: 1732, 8: 1066 }, + M: { 0: 10984, 1: 3289, 2: 1994, 4: 1370, 8: 843 }, + Q: { 0: 7880, 1: 2358, 2: 1429, 4: 982, 8: 604 }, + H: { 0: 5960, 1: 1782, 2: 1080, 4: 742, 8: 457 }, + }, + 31: { + L: { 0: 14744, 1: 4417, 2: 2677, 4: 1840, 8: 1132 }, + M: { 0: 11640, 1: 3486, 2: 2113, 4: 1452, 8: 894 }, + Q: { 0: 8264, 1: 2473, 2: 1499, 4: 1030, 8: 634 }, + H: { 0: 6344, 1: 1897, 2: 1150, 4: 790, 8: 486 }, + }, + 32: { + L: { 0: 15640, 1: 4686, 2: 2840, 4: 1952, 8: 1201 }, + M: { 0: 12328, 1: 3693, 2: 2238, 4: 1538, 8: 947 }, + Q: { 0: 8920, 1: 2670, 2: 1618, 4: 1112, 8: 684 }, + H: { 0: 6760, 1: 2022, 2: 1226, 4: 842, 8: 518 }, + }, + 33: { + L: { 0: 16568, 1: 4965, 2: 3009, 4: 2068, 8: 1273 }, + M: { 0: 13048, 1: 3909, 2: 2369, 4: 1628, 8: 1002 }, + Q: { 0: 9368, 1: 2805, 2: 1700, 4: 1168, 8: 719 }, + H: { 0: 7208, 1: 2157, 2: 1307, 4: 898, 8: 553 }, + }, + 34: { + L: { 0: 17528, 1: 5253, 2: 3183, 4: 2188, 8: 1347 }, + M: { 0: 13800, 1: 4134, 2: 2506, 4: 1722, 8: 1060 }, + Q: { 0: 9848, 1: 2949, 2: 1787, 4: 1228, 8: 756 }, + H: { 0: 7688, 1: 2301, 2: 1394, 4: 958, 8: 590 }, + }, + 35: { + L: { 0: 18448, 1: 5529, 2: 3351, 4: 2303, 8: 1417 }, + M: { 0: 14496, 1: 4343, 2: 2632, 4: 1809, 8: 1113 }, + Q: { 0: 10288, 1: 3081, 2: 1867, 4: 1283, 8: 790 }, + H: { 0: 7888, 1: 2361, 2: 1431, 4: 983, 8: 605 }, + }, + 36: { + L: { 0: 19472, 1: 5836, 2: 3537, 4: 2431, 8: 1496 }, + M: { 0: 15312, 1: 4588, 2: 2780, 4: 1911, 8: 1176 }, + Q: { 0: 10832, 1: 3244, 2: 1966, 4: 1351, 8: 832 }, + H: { 0: 8432, 1: 2524, 2: 1530, 4: 1051, 8: 647 }, + }, + 37: { + L: { 0: 20528, 1: 6153, 2: 3729, 4: 2563, 8: 1577 }, + M: { 0: 15936, 1: 4775, 2: 2894, 4: 1989, 8: 1224 }, + Q: { 0: 11408, 1: 3417, 2: 2071, 4: 1423, 8: 876 }, + H: { 0: 8768, 1: 2625, 2: 1591, 4: 1093, 8: 673 }, + }, + 38: { + L: { 0: 21616, 1: 6479, 2: 3927, 4: 2699, 8: 1661 }, + M: { 0: 16816, 1: 5039, 2: 3054, 4: 2099, 8: 1292 }, + Q: { 0: 12016, 1: 3599, 2: 2181, 4: 1499, 8: 923 }, + H: { 0: 9136, 1: 2735, 2: 1658, 4: 1139, 8: 701 }, + }, + 39: { + L: { 0: 22496, 1: 6743, 2: 4087, 4: 2809, 8: 1729 }, + M: { 0: 17728, 1: 5313, 2: 3220, 4: 2213, 8: 1362 }, + Q: { 0: 12656, 1: 3791, 2: 2298, 4: 1579, 8: 972 }, + H: { 0: 9776, 1: 2927, 2: 1774, 4: 1219, 8: 750 }, + }, + 40: { + L: { 0: 23648, 1: 7089, 2: 4296, 4: 2953, 8: 1817 }, + M: { 0: 18672, 1: 5596, 2: 3391, 4: 2331, 8: 1435 }, + Q: { 0: 13328, 1: 3993, 2: 2420, 4: 1663, 8: 1024 }, + H: { 0: 10208, 1: 3057, 2: 1852, 4: 1273, 8: 784 }, + }, +} as const; + +// EOF diff --git a/js/src/image.ts b/js/src/image.ts new file mode 100644 index 0000000..a1cee0c --- /dev/null +++ b/js/src/image.ts @@ -0,0 +1,107 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * QR code image rendering. + */ + +import QRCode from 'qrcode'; +import UPNG from 'upng-js'; +import { ImageOptions, Version } from './types'; +import { shuffled } from './utils'; + +/** + * Generates QR code images from the given parts and renders them as (animated) PNG. + * + * @param parts The string parts to encode as QR codes. + * @param version The QR code version to use. + * @param options An optional ImageOptions object. + * @param options.frameDelay The delay between frames in the animated PNG in milliseconds. Defaults to 250. + * @param options.randomizeOrder Whether to randomize the order of the parts. Defaults to false. + * @returns A Promise that resolves to an ArrayBuffer containing the image data; + */ +export async function renderQRImage( + parts: string[], + version: Version, + options: ImageOptions = {} +): Promise { + if (typeof window === 'undefined') { + throw new Error('makeImage is only available in a web browser environment.'); + } + + const frameDelay = options.frameDelay ?? 250; + + if (options.randomizeOrder) { + parts = shuffled(parts); + } + + const frames: ArrayBuffer[] = []; + + let width = 0; + let height = 0; + + // additional space for progress bar, if more than one part + const progressAreaHeight = parts.length > 1 ? 20 : 0; + + // quiet zone + const margin = 4; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + + const dataURL = await QRCode.toDataURL([{ data: part, mode: 'alphanumeric' }], { + errorCorrectionLevel: 'L', + version, + margin, + }); + + // Create an image and draw it onto the canvas to get its dimensions + const img = new Image(); + img.src = dataURL; + await img.decode(); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Could not get 2d context for canvas element.'); + } + + canvas.width = img.width; + canvas.height = img.height + progressAreaHeight; + + ctx.drawImage(img, 0, 0); + + if (progressAreaHeight > 0) { + // fill area between QR and progress bar + ctx.fillStyle = '#fff'; + ctx.fillRect(0, img.height, canvas.width, progressAreaHeight); + + const progressBarHeight = progressAreaHeight / 4; + const progressBarY = img.height + progressAreaHeight / 2; // Position the progress bar below the QR code + const segmentWidth = canvas.width / parts.length; + + ctx.fillStyle = '#ccc'; + ctx.fillRect(0, progressBarY, canvas.width, progressBarHeight); + + ctx.fillStyle = '#000'; + + ctx.fillRect(segmentWidth * i, progressBarY, segmentWidth, progressBarHeight); + } + + if (i === 0) { + width = canvas.width; + height = canvas.height; + } else if (canvas.width !== width || canvas.height !== height) { + throw new Error('QR codes must all be the same size'); + } + + const imgData = ctx.getImageData(0, 0, width, height).data; + frames.push(imgData.buffer); + } + + const delays = parts.map(() => frameDelay); + + return UPNG.encode(frames, width, height, 0, delays); +} + +// EOF diff --git a/js/src/join.ts b/js/src/join.ts new file mode 100644 index 0000000..7efe609 --- /dev/null +++ b/js/src/join.ts @@ -0,0 +1,79 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * QR code decoding/joining. + */ + +import { ENCODINGS, FILETYPES } from './consts'; +import { Encoding, FileType, JoinResult } from './types'; +import { decodeData } from './utils'; + +/** + * Decodes and joins QR code parts back to binary data. + * + * @param parts Array of QR code parts + * @returns Object containing the file type, encoding, and raw binary data. + */ +export function joinQRs(parts: string[]): JoinResult { + const headers = new Set(parts.map((p) => p.slice(0, 6))); + + if (headers.size !== 1) { + throw new Error('conflicting/variable filetype/encodings/sizes'); + } + + const header = [...headers][0]; + + if (header.slice(0, 2) !== 'B$') { + throw new Error('fixed header not found, expected B$'); + } + + if (!ENCODINGS.has(header[2])) { + throw new Error(`bad encoding: ${header[2]}`); + } + if (!FILETYPES.has(header[3])) { + throw new Error(`bad file type: ${header[3]}`); + } + + const encoding = header[2] as Encoding; + const fileType = header[3] as FileType; + + const numParts = parseInt(header.slice(4, 6), 36); + + if (numParts < 1) { + throw new Error('zero parts?'); + } + + const data = new Map(); + + for (const p of parts) { + const idx = parseInt(p.slice(6, 8), 36); + + if (idx >= numParts) { + throw new Error(`got part ${idx} but only expecting ${numParts}`); + } + + if (data.has(idx) && data.get(idx) !== p.slice(8)) { + throw new Error(`Duplicate part 0x${idx.toString(16)} has wrong content`); + } + + data.set(idx, p.slice(8)); + } + + const orderedParts = []; + + for (let i = 0; i < numParts; i++) { + const p = data.get(i); + + if (!p) { + throw new Error(`Part ${i} is missing`); + } + + orderedParts.push(p); + } + + const raw = decodeData(orderedParts, encoding); + + return { fileType, encoding, raw }; +} + +// EOF diff --git a/js/src/main.ts b/js/src/main.ts new file mode 100644 index 0000000..8f7c187 --- /dev/null +++ b/js/src/main.ts @@ -0,0 +1,13 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * Main entry point for the library. + */ + +import { renderQRImage } from './image.ts'; +import { joinQRs } from './join.ts'; +import { detectFileType, splitQRs } from './split.ts'; + +export { detectFileType, joinQRs, renderQRImage, splitQRs }; + +// EOF diff --git a/js/src/split.ts b/js/src/split.ts new file mode 100644 index 0000000..1b8c208 --- /dev/null +++ b/js/src/split.ts @@ -0,0 +1,206 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * Splitting of data and encoding as BBQr QR codes. + */ + +import { FILETYPES, HEADER_LEN } from './consts'; +import { FileType, SplitOptions, SplitResult, Version } from './types'; +import { + base64ToBytes, + encodeData, + fileToBytes, + hexToBytes, + intToBase36, + looksLikePsbt, + validateSplitOptions, + versionToChars, +} from './utils'; + +function numQRNeeded(version: Version, length: number, splitMod: number) { + const baseCap = versionToChars(version) - HEADER_LEN; + + // adjust capacity to be a multiple of splitMod + const adjustedCap = baseCap - (baseCap % splitMod); + + const estimatedCount = Math.ceil(length / adjustedCap); + + if (estimatedCount === 1) { + // if it fits in one QR, we're done + return { count: 1, perEach: length }; + } + + // the total capacity of our estimated count + // all but the last QR need to use adjusted capacity to ensure proper split + const estimatedCap = (estimatedCount - 1) * adjustedCap + baseCap; + + return { + count: estimatedCap >= length ? estimatedCount : estimatedCount + 1, + perEach: adjustedCap, + }; +} + +function findBestVersion(length: number, splitMod: number, opts: Required) { + const options: { version: Version; count: number; perEach: number }[] = []; + + for (let version = opts.minVersion; version <= opts.maxVersion; version++) { + const { count, perEach } = numQRNeeded(version, length, splitMod); + + if (opts.minSplit <= count && count <= opts.maxSplit) { + options.push({ version, count, perEach }); + } + } + + if (!options.length) { + throw new Error('Cannot make it fit'); + } + + // pick smallest number of QR, lowest version + options.sort((a, b) => a.count - b.count || a.version - b.version); + + return options[0]; +} + +/** + * Converts the input bytes into a series of QR codes, ensuring that the most efficient QR code + * version is used. + * + * NOTE: When the 'Z' (Zlib) encoding is selected, it is possible that the actual used encoding + * will be '2' (Base32) in case Zlib compression does not reduce the size of the output. + * + * @param raw The input bytes to split and encode. + * @param fileType The file type to use. Refer to BBQr spec. + * + * @param opts An optional SplitOptions object. + * @param opts.encoding The Encoding to use. Defaults to 'Z'. + * @param opts.minSplit The minimum number of QR codes to use. Defaults to 1. + * @param opts.maxSplit The maximum number of QR codes to use. Defaults to 1295. + * @param opts.minVersion The minimum QR code version to use. Defaults to 5. + * @param opts.maxVersion The maximum QR code version to use. Defaults to 40. + * + * @returns An object containing the version of the QR codes, their string parts, and the actual encoding used. + */ +export function splitQRs( + raw: Uint8Array, + fileType: FileType, + opts: SplitOptions = {} +): SplitResult { + if (!FILETYPES.has(fileType)) { + throw new Error(`Invalid value for fileType: ${fileType}`); + } + + const validatedOpts = validateSplitOptions(opts); + + const { encoding: actualEncoding, encoded, splitMod } = encodeData(raw, validatedOpts.encoding); + + const { version, count, perEach } = findBestVersion(encoded.length, splitMod, validatedOpts); + + const parts: string[] = []; + + for (let n = 0, offset = 0; offset < encoded.length; n++, offset += perEach) { + parts.push( + `B$${actualEncoding}${fileType}` + + intToBase36(count) + + intToBase36(n) + + encoded.slice(offset, offset + perEach) + ); + } + + return { version, parts, encoding: actualEncoding }; +} + +/** + * Takes a given given input (Uint8Array, File, or string) and detects its FileType. + * PSBTs and Bitcoin transactions are supported in raw binary, Base64, or hex format. + * + * @param input - The input to detect the FileType of. + * @returns A Promise that resolves to an object containing the FileType and raw data. + */ +export async function detectFileType( + input: File | Uint8Array | string +): Promise<{ fileType: FileType; raw: Uint8Array }> { + // keep references to both raw and decoded versions of the input to run checks on + let raw: Uint8Array | undefined = undefined; + let decoded: string | undefined = undefined; + + if (input instanceof File) { + // convert a File to Uint8Array so we have access to the raw bytes + input = await fileToBytes(input); + } + + if (input instanceof Uint8Array) { + // we got binary, see if we recognize it + raw = input; + + if (looksLikePsbt(input)) { + console.debug('Detected type "P" from binary input'); + return { fileType: 'P', raw }; + } + + if (raw[0] === 0x01 || raw[0] === 0x02) { + console.debug('Detected type "T" from binary input'); + return { fileType: 'T', raw }; + } + + // otherwise, try to decode as text (could be contents of a file) + try { + decoded = new TextDecoder('utf-8', { fatal: true }).decode(raw); + } catch (err) { + // not text, so fall back to generic binary + + console.debug('Detected type "B" from binary input'); + return { fileType: 'B', raw }; + } + } else if (typeof input === 'string') { + decoded = input; + } else { + throw new Error('Invalid input - must be a File, Uint8Array or string'); + } + + const trimmed = decoded.trim(); + + if (/^70736274ff[0-9A-Fa-f]+$/.test(trimmed)) { + // PSBT in hex format + console.debug('Detected type "P" from hex input'); + + return { fileType: 'P', raw: hexToBytes(trimmed) }; + } + + if (/^0[1,2]000000[0-9A-Fa-f]+$/.test(trimmed)) { + // Transaction in hex format + console.debug('Detected type "T" from hex input'); + + return { fileType: 'T', raw: hexToBytes(trimmed) }; + } + + if (/^[A-Za-z0-9+/=]+$/.test(trimmed)) { + // looks like base64 - could be PSBT or transaction + const bytes = base64ToBytes(decoded); + + if (looksLikePsbt(bytes)) { + console.debug('Detected type "P" from base64 input'); + return { fileType: 'P', raw: bytes }; + } + + if (bytes[0] === 0x01 || bytes[0] === 0x02) { + console.debug('Detected type "T" from base64 input'); + return { fileType: 'T', raw: bytes }; + } + } + + // ensure we have raw bytes for the next step + raw = raw ?? new TextEncoder().encode(decoded); + + try { + JSON.parse(decoded); + console.debug('Detected type "J"'); + return { fileType: 'J', raw }; + } catch (err) { + // not JSON - fall back to generic Unicode + + console.debug('Detected type "U"'); + return { fileType: 'U', raw }; + } +} + +// EOF diff --git a/js/src/types.ts b/js/src/types.ts new file mode 100644 index 0000000..83f5b08 --- /dev/null +++ b/js/src/types.ts @@ -0,0 +1,38 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * Types + */ + +import { ENCODING_NAMES, FILETYPE_NAMES, QR_DATA_CAPACITY } from './consts'; + +export type FileType = keyof typeof FILETYPE_NAMES; +export type Encoding = keyof typeof ENCODING_NAMES; +export type Version = keyof typeof QR_DATA_CAPACITY; + +export type SplitOptions = { + encoding?: Encoding; + minSplit?: number; + maxSplit?: number; + minVersion?: Version; + maxVersion?: Version; +}; + +export type SplitResult = { + version: Version; + parts: string[]; + encoding: Encoding; +}; + +export type JoinResult = { + fileType: FileType; + encoding: Encoding; + raw: Uint8Array; +}; + +export type ImageOptions = { + frameDelay?: number; + randomizeOrder?: boolean; +}; + +// EOF diff --git a/js/src/utils.ts b/js/src/utils.ts new file mode 100644 index 0000000..2e67e14 --- /dev/null +++ b/js/src/utils.ts @@ -0,0 +1,221 @@ +/** + * (c) Copyright 2024 by Coinkite Inc. This file is in the public domain. + * + * Helper/utility functions. + */ + +import { base32 } from '@scure/base'; +import pako from 'pako'; +import { QR_DATA_CAPACITY } from './consts'; +import type { Encoding, SplitOptions, Version } from './types'; + +export function hexToBytes(hex: string) { + // convert a hex string to a Uint8Array + + const match = hex.match(/.{1,2}/g) ?? []; + + return Uint8Array.from(match.map((byte) => parseInt(byte, 16))); +} + +export function base64ToBytes(base64: string) { + // convert a base64 string to a Uint8Array + + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + return bytes; +} + +export function intToBase36(n: number) { + // convert an integer 0-1295 to two digits of base 36 - 00-ZZ + + if (n < 0 || n > 1295 || !Number.isInteger(n)) { + throw new Error('Out of range'); + } + + return n.toString(36).toUpperCase().padStart(2, '0'); +} + +export async function fileToBytes(file: File) { + // read a File's contents and return as a Uint8Array + + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = (e) => { + const result = e.target?.result; + + if (result instanceof ArrayBuffer) { + resolve(new Uint8Array(result)); + } else { + reject(new Error('FileReader result is not an ArrayBuffer')); + } + }; + + reader.readAsArrayBuffer(file); + }); +} + +function joinByteParts(parts: Uint8Array[]) { + // perf-optimized way to join Uint8Arrays + + const length = parts.reduce((acc, bytes) => acc + bytes.length, 0); + + const rv = new Uint8Array(length); + + let offset = 0; + for (const bytes of parts) { + rv.set(bytes, offset); + offset += bytes.length; + } + + return rv; +} + +export function isValidVersion(v: number): v is Version { + // act as a TS type guard but also a runtime check + + return v in QR_DATA_CAPACITY; +} + +export function isValidSplit(s: number) { + return s >= 1 && s <= 1295; +} + +export function validateSplitOptions(opts: SplitOptions) { + // ensure all split options are valid, filling in defaults as needed + + const allOpts = { + minVersion: opts.minVersion ?? 5, + maxVersion: opts.maxVersion ?? 40, + minSplit: opts.minSplit ?? 1, + maxSplit: opts.maxSplit ?? 1295, + encoding: opts.encoding ?? 'Z', + } as const; + + if ( + allOpts.minVersion > allOpts.maxVersion || + !isValidVersion(allOpts.minVersion) || + !isValidVersion(allOpts.maxVersion) + ) { + throw new Error('min/max version out of range'); + } + + if ( + !isValidSplit(allOpts.minSplit) || + !isValidSplit(allOpts.maxSplit) || + allOpts.minSplit > allOpts.maxSplit + ) { + throw new Error('min/max split out of range'); + } + + return allOpts; +} + +export function looksLikePsbt(data: Uint8Array) { + try { + // 'psbt' + 0xff + return new Uint8Array([0x70, 0x73, 0x62, 0x74, 0xff]).every((b, i) => b === data[i]); + } catch (err) { + return false; + } +} + +export function shuffled(arr: T[]): T[] { + // modern Fisher-Yates shuffle (https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#The_modern_algorithm) + + // create a copy so we don't mutate the original + arr = [...arr]; + + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = arr[i]; + arr[i] = arr[j]; + arr[j] = temp; + } + + return arr; +} + +export function versionToChars(v: Version) { + // return number of **chars** that fit into indicated version QR + // - assumes L for ECC + // - assumes alnum encoding + + if (!isValidVersion(v)) { + throw new Error('Invalid version'); + } + + const ecc = 'L'; + const encoding = 2; // alnum + + return QR_DATA_CAPACITY[v][ecc][encoding]; +} + +export function encodeData(raw: Uint8Array, encoding?: Encoding) { + // return new encoding (if we upgraded) and the + // characters after encoding (a string) + // - default is Zlib or if compression doesn't help, base32 + // - returned data can be split, but must be done modX where X provided + + encoding = encoding ?? 'Z'; + + if (encoding === 'H') { + return { + encoding, + encoded: raw + .reduce((acc, byte) => acc + byte.toString(16).padStart(2, '0'), '') + .toUpperCase(), + splitMod: 2, + }; + } + + if (encoding === 'Z') { + // trial compression, but skip if it embiggens the data + + const compressed = pako.deflate(raw, { windowBits: -10 }); + + if (compressed.length >= raw.length) { + encoding = '2'; + } else { + encoding = 'Z'; + raw = compressed; + } + } + + return { + encoding, + // base32 without padding + encoded: base32.encode(raw).replace(/=*$/, ''), + splitMod: 8, + }; +} + +export function decodeData(parts: string[], encoding: Encoding) { + // decode the parts back into a Uint8Array + + if (encoding === 'H') { + return joinByteParts(parts.map((p) => hexToBytes(p))); + } + + const bytes = joinByteParts( + parts.map((p) => { + const padding = (8 - (p.length % 8)) % 8; + + return base32.decode(p + '='.repeat(padding)); + }) + ); + + if (encoding === 'Z') { + return pako.inflate(bytes, { windowBits: -10 }); + } + + return bytes; +} + +// EOF diff --git a/js/tests/join.test.ts b/js/tests/join.test.ts new file mode 100644 index 0000000..3c2983c --- /dev/null +++ b/js/tests/join.test.ts @@ -0,0 +1,21 @@ +import fs from 'fs'; +import path from 'path'; +import { expect, test } from 'vitest'; +import { joinQRs } from '../src/join'; + +test('Test real-scan.txt', async () => { + const lines = ( + await fs.promises.readFile(path.join(__dirname, '../../test_data/real-scan.txt'), 'utf-8') + ) + .split('\n') + .filter((l) => l.trim() !== ''); + + const { fileType, raw } = joinQRs(lines); + + expect(fileType).toBe('U'); + + const decoded = new TextDecoder().decode(raw); + + expect(decoded).toContain('Zlib compressed'); + expect(decoded).toContain('PSBT'); +}); diff --git a/js/tests/loopback.test.ts b/js/tests/loopback.test.ts new file mode 100644 index 0000000..6f1e4bb --- /dev/null +++ b/js/tests/loopback.test.ts @@ -0,0 +1,175 @@ +import QRCode from 'qrcode'; +import { expect, test } from 'vitest'; +import { HEADER_LEN, QR_DATA_CAPACITY } from '../src/consts'; +import { joinQRs } from '../src/join'; +import { splitQRs } from '../src/split'; +import { Encoding, FileType, Version } from '../src/types'; +import { shuffled } from '../src/utils'; + +// helper to create cartesian product of arrays akin to @pytest.mark.parametrize over multiple parameters +// https://stackoverflow.com/a/43053803 +function cartesian(...a: any[][]) { + return a.reduce((a, b) => a.flatMap((d) => b.map((e) => [d, e].flat()))); +} + +// for validating QRs +function producesValidQRs(parts: string[], version: number) { + try { + for (const p of parts) { + QRCode.create([{ data: p, mode: 'alphanumeric' }], { + version, + errorCorrectionLevel: 'L', + }); + } + + return true; + } catch (err) { + return false; + } +} + +const LOOPBACK_CASES: [Encoding | undefined, number, Version, boolean, FileType][] = cartesian( + [undefined, 'H', '2', 'Z'], // encoding + [10, 100, 2000, 10_000, 50_000], // size + [11, 29, 40], // maxVersion + [true, false], // lowEntropy + ['P', 'T'] // fileType +); + +test('Loopback', () => { + for (const [i, tc] of LOOPBACK_CASES.entries()) { + const [encoding, size, maxVersion, lowEntropy, fileType] = tc; + + const data = new Uint8Array(size); + if (lowEntropy) { + data.fill(0x41); + } else { + crypto.getRandomValues(data); + } + + const { version, parts } = splitQRs(data, fileType, { encoding, maxVersion }); + + expect(version).toBeLessThanOrEqual(maxVersion); + + if (encoding && encoding !== parts[0][2]) { + // encoding can only change from 'Z' to '2' if Zlib doesn't provide a smaller result + expect(encoding).toBe('Z'); + expect(parts[0][2]).toBe('2'); + } + + const decoded = joinQRs(parts); + expect(decoded.fileType).toBe(fileType); + expect(decoded.raw).toEqual(data); + + const randomized = shuffled(parts); + const decoded2 = joinQRs(randomized); + expect(decoded2.fileType).toBe(fileType); + expect(decoded2.raw).toEqual(data); + + // try to construct a few QRs. too slow to do for every case + if (i % 50 === 0) { + expect(producesValidQRs(parts, version)).toBe(true); + } + } +}); + +test('Minimum split', () => { + const data = new Uint8Array(10_000); + crypto.getRandomValues(data); + + for (let i = 2; i < 10; i++) { + const { parts, version } = splitQRs(data, 'T', { encoding: '2', minSplit: i }); + + expect(parts.length).toBeGreaterThanOrEqual(i); + + const decoded = joinQRs(parts); + + expect(decoded.fileType).toBe('T'); + expect(decoded.raw).toEqual(data); + + expect(producesValidQRs(parts, version)).toBe(true); + } +}); + +const V27_EDGE_CASES: [Encoding, number, boolean][] = cartesian( + ['H', '2', 'Z'], // encoding + Array.from({ length: 20 }, (_, i) => i + 1060), // size + [true, false] // lowEntropy +); + +test('Version 27 edge cases', () => { + for (const c of V27_EDGE_CASES) { + const [encoding, size, lowEntropy] = c; + + const needVersion = 27; + + const data = new Uint8Array(size); + if (lowEntropy) { + data.fill(0x41); + } else { + crypto.getRandomValues(data); + } + + const { parts, version: actualVersion } = splitQRs(data, 'T', { + encoding, + maxSplit: 2, + minVersion: needVersion, + maxVersion: needVersion, + }); + + expect(actualVersion).toBe(needVersion); + + if (encoding === 'H') { + expect(parts.length).toBe(size <= 1062 ? 1 : 2); + } else if (encoding === 'Z') { + expect(parts.length).toBe(1); + if (lowEntropy) { + expect(parts[0].length).toBeLessThan(100); + } + } else if (encoding === '2') { + expect(parts.length).toBe(1); + } + + const decoded = joinQRs(parts); + + expect(decoded.fileType).toBe('T'); + expect(decoded.raw).toEqual(data); + } +}); + +test.each(['H', '2'] as const)(`Test max size for encoding %s`, (encoding) => { + const capacity = QR_DATA_CAPACITY[40]['L'][2] - HEADER_LEN; + + let pktSize: number; + + if (encoding === 'H') { + pktSize = Math.floor(capacity / 2); + } else if (encoding === '2') { + pktSize = Math.floor((capacity * 5) / 8); + } else { + throw new Error('unreachable'); + } + + const nparts = 1295; + + const data = new Uint8Array(pktSize * nparts); + + // cannot use crypto.getRandomValues(data) - exceeds max size + + for (let i = 0; i < data.length; i++) { + data[i] = Math.floor(Math.random() * 256); + } + + const { parts, version } = splitQRs(data, 'T', { encoding, minVersion: 40 }); + + expect(version).toBe(40); + + expect(parts.length).toBe(nparts); + + const decoded = joinQRs(parts); + expect(decoded.raw).toEqual(data); + + // test a random part + const idx = Math.floor(Math.random() * parts.length); + expect(producesValidQRs([parts[idx]], version)).toBe(true); +}); diff --git a/js/tests/utils.test.ts b/js/tests/utils.test.ts new file mode 100644 index 0000000..b3e6696 --- /dev/null +++ b/js/tests/utils.test.ts @@ -0,0 +1,71 @@ +import fs from 'fs'; +import path from 'path'; +import { expect, test } from 'vitest'; +import { base64ToBytes, decodeData, encodeData, hexToBytes, intToBase36 } from '../src/utils'; + +test('Decode hex string to bytes', () => { + const hex = '48656c6c6f20776f726c64'; // "Hello world" + const expected = new Uint8Array([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + + expect(hexToBytes(hex)).toEqual(expected); +}); + +test('Decode base64 to binary', () => { + const b64 = 'SGVsbG8gd29ybGQ='; // "Hello world" + const expected = new Uint8Array([72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + + expect(base64ToBytes(b64)).toEqual(expected); +}); + +test('Encode integers 0 thru 1295 to base32', () => { + for (let i = 0; i <= 1295; i++) { + const s = intToBase36(i); + + expect(s).toHaveLength(2); + expect(s.toUpperCase()).toBe(s); + + expect(parseInt(s, 36)).toBe(i); + } + + expect(() => intToBase36(-1)).toThrowError(); + expect(() => intToBase36(1296)).toThrowError(); +}); + +const dataFiles = [ + '1in1000out.psbt', + '1in100out.psbt', + '1in10out.psbt', + '1in20out.psbt', + '1in2out.psbt', + 'devils-txn.txn', + 'finalized-by-ckcc.txn', + 'last.txn', + 'nfc-result.txn', + 'signed.txn', +]; + +test.each(dataFiles)('Encode, decode, measure Zlib compression: %s', async (file) => { + // need the conversion to Uint8Array because node's Buffer won't be equal + const raw = new Uint8Array( + await fs.promises.readFile(path.join(__dirname, '../../test_data', file)) + ); + + const { encoded, encoding } = encodeData(raw, 'Z'); + + expect(encoding).toBe('Z'); + + const decoded = decodeData([encoded], 'Z'); + + expect(decoded).toEqual(raw); + + // decode with base32 only and see how much smaller it is + const decodedCompressed = decodeData([encoded], '2'); + + expect(decodedCompressed.length).toBeLessThan(raw.length); + + const ratio = 100 - (decodedCompressed.length * 100) / raw.length; + + console.log( + `${file}: ${raw.length} => ${decodedCompressed.length}, ${ratio.toFixed(1)}% compression` + ); +}); diff --git a/js/tsconfig.json b/js/tsconfig.json new file mode 100644 index 0000000..75abdef --- /dev/null +++ b/js/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/js/vite.config.ts b/js/vite.config.ts new file mode 100644 index 0000000..1375cc3 --- /dev/null +++ b/js/vite.config.ts @@ -0,0 +1,14 @@ +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + build: { + lib: { + name: 'BBQr', + entry: resolve(__dirname, 'src/main.ts'), + formats: ['es', 'iife'], + }, + }, + plugins: [dts()], +}); diff --git a/Makefile b/python/Makefile similarity index 100% rename from Makefile rename to python/Makefile diff --git a/bbqr/__init__.py b/python/bbqr/__init__.py similarity index 100% rename from bbqr/__init__.py rename to python/bbqr/__init__.py diff --git a/bbqr/cli.py b/python/bbqr/cli.py similarity index 100% rename from bbqr/cli.py rename to python/bbqr/cli.py diff --git a/bbqr/consts.py b/python/bbqr/consts.py similarity index 100% rename from bbqr/consts.py rename to python/bbqr/consts.py diff --git a/bbqr/join.py b/python/bbqr/join.py similarity index 100% rename from bbqr/join.py rename to python/bbqr/join.py diff --git a/bbqr/split.py b/python/bbqr/split.py similarity index 100% rename from bbqr/split.py rename to python/bbqr/split.py diff --git a/bbqr/tables.py b/python/bbqr/tables.py similarity index 100% rename from bbqr/tables.py rename to python/bbqr/tables.py diff --git a/bbqr/utils.py b/python/bbqr/utils.py similarity index 100% rename from bbqr/utils.py rename to python/bbqr/utils.py diff --git a/bbqr/version.py b/python/bbqr/version.py similarity index 100% rename from bbqr/version.py rename to python/bbqr/version.py diff --git a/requirements.txt b/python/requirements.txt similarity index 100% rename from requirements.txt rename to python/requirements.txt diff --git a/setup.py b/python/setup.py similarity index 96% rename from setup.py rename to python/setup.py index 8f0a33f..a1674c3 100644 --- a/setup.py +++ b/python/setup.py @@ -21,7 +21,7 @@ 'pytest' ] -with open("README.md", "r") as fh: +with open("../README.md", "r") as fh: long_description = fh.read() # get the version w/o importing anything other parts which might need requirements diff --git a/tests/context.py b/python/tests/context.py similarity index 100% rename from tests/context.py rename to python/tests/context.py diff --git a/tests/test_encoding.py b/python/tests/test_encoding.py similarity index 75% rename from tests/test_encoding.py rename to python/tests/test_encoding.py index c21cec0..e82b5c8 100644 --- a/tests/test_encoding.py +++ b/python/tests/test_encoding.py @@ -6,16 +6,16 @@ import pytest, os, pyqrcode @pytest.mark.parametrize('fname', [ - 'data/1in1000out.psbt', - 'data/1in100out.psbt', - 'data/1in10out.psbt', - 'data/1in20out.psbt', - 'data/1in2out.psbt', - 'data/devils-txn.txn', - 'data/finalized-by-ckcc.txn', - 'data/last.txn', - 'data/nfc-result.txn', - 'data/signed.txn', + '../test_data/1in1000out.psbt', + '../test_data/1in100out.psbt', + '../test_data/1in10out.psbt', + '../test_data/1in20out.psbt', + '../test_data/1in2out.psbt', + '../test_data/devils-txn.txn', + '../test_data/finalized-by-ckcc.txn', + '../test_data/last.txn', + '../test_data/nfc-result.txn', + '../test_data/signed.txn', ]) def test_compression(fname): # more of a measurement than a test... diff --git a/tests/test_join.py b/python/tests/test_join.py similarity index 74% rename from tests/test_join.py rename to python/tests/test_join.py index 4d4a161..11ac5e9 100644 --- a/tests/test_join.py +++ b/python/tests/test_join.py @@ -6,7 +6,7 @@ import pytest, os, pyqrcode def test_real_scan(): - lines = [ln.strip() for ln in open('data/real-scan.txt', 'rt').readlines() if ln.strip()] + lines = [ln.strip() for ln in open('../test_data/real-scan.txt', 'rt').readlines() if ln.strip()] file_type, data = bbqr.join_qrs(lines) diff --git a/tests/test_loopback.py b/python/tests/test_loopback.py similarity index 100% rename from tests/test_loopback.py rename to python/tests/test_loopback.py diff --git a/tests/data/1in1000out.psbt b/test_data/1in1000out.psbt similarity index 100% rename from tests/data/1in1000out.psbt rename to test_data/1in1000out.psbt diff --git a/tests/data/1in100out.psbt b/test_data/1in100out.psbt similarity index 100% rename from tests/data/1in100out.psbt rename to test_data/1in100out.psbt diff --git a/tests/data/1in10out.psbt b/test_data/1in10out.psbt similarity index 100% rename from tests/data/1in10out.psbt rename to test_data/1in10out.psbt diff --git a/tests/data/1in20out.psbt b/test_data/1in20out.psbt similarity index 100% rename from tests/data/1in20out.psbt rename to test_data/1in20out.psbt diff --git a/tests/data/1in2out.psbt b/test_data/1in2out.psbt similarity index 100% rename from tests/data/1in2out.psbt rename to test_data/1in2out.psbt diff --git a/tests/data/devils-txn.txn b/test_data/devils-txn.txn similarity index 100% rename from tests/data/devils-txn.txn rename to test_data/devils-txn.txn diff --git a/tests/data/finalized-by-ckcc.txn b/test_data/finalized-by-ckcc.txn similarity index 100% rename from tests/data/finalized-by-ckcc.txn rename to test_data/finalized-by-ckcc.txn diff --git a/tests/data/last.txn b/test_data/last.txn similarity index 100% rename from tests/data/last.txn rename to test_data/last.txn diff --git a/tests/data/nfc-result.txn b/test_data/nfc-result.txn similarity index 100% rename from tests/data/nfc-result.txn rename to test_data/nfc-result.txn diff --git a/tests/data/real-scan.txt b/test_data/real-scan.txt similarity index 100% rename from tests/data/real-scan.txt rename to test_data/real-scan.txt diff --git a/tests/data/signed.txn b/test_data/signed.txn similarity index 100% rename from tests/data/signed.txn rename to test_data/signed.txn