diff --git a/README.md b/README.md index c616fa0..75868d3 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,50 @@ # sharing -Instantly share files, directories, and clipboard content from your terminal to any device with a browser — no apps required. +**Instantly move files, folders, and clipboard text between your computer and any phone — no app, no account, no cloud. Just run a command and scan the QR code.** ![Sharing screenshot](/doc/sharing-banner.svg?raw=true "Sharing a directory") -### Features +```sh +npx easy-sharing ~/Photos # share a folder — scan the QR on your phone — done +``` + +That's the whole idea. Your phone opens a normal web page over your own Wi-Fi. Nothing to install on the other device, nothing leaves your network. + +## Why `sharing`? + +If you've ever tried to get a file from your laptop to your phone, you know the options are all a little painful: cloud uploads are slow and nosy, AirDrop is Apple-only, and "real" tools want an app installed on *both* ends. `sharing` takes the simplest path that always works: **a tiny web server and a QR code.** Any phone with a browser can use it. -- Share files and directories over your local network -- Share clipboard content -- Receive files from other devices -- Protect shares with basic authentication -- HTTPS support via custom SSL certificates -- Expose shares over the internet with tunnel services +| | **sharing** | `python -m http.server` | `npx serve` | qrcp | AirDrop / LocalSend | +|---|:---:|:---:|:---:|:---:|:---:| +| No app on the phone | ✅ | ✅ | ✅ | ✅ | ❌ (app both ends) | +| QR code to connect | ✅ | ❌ | ❌ | ✅ | ❌ | +| **Receive** files from the phone | ✅ | ❌ | ❌ | ✅ | ✅ | +| Receive **multiple** files / drag-drop | ✅ | ❌ | ❌ | ➖ | ✅ | +| Download a whole folder as **.zip** | ✅ | ❌ | ❌ | ✅ | ➖ | +| Share **clipboard** text *and* receive text back | ✅ | ❌ | ❌ | ➖ | ✅ | +| Built-in auth **and** HTTPS | ✅ | ❌ | ➖ | ➖ | ✅ | +| One-flag private share (`--secure`) | ✅ | ❌ | ❌ | ❌ | ✅ | +| Picks the **right** network address automatically | ✅ | ❌ | ➖ | ✅ | ✅ | + +`sharing` is the one that does **all** of it from a single command, in a browser, with nothing to install on the device in your hand. ## Getting Started -**Requirements:** Node.js v14 or later +**Requirements:** Node.js v14.17 or later. + +### Try it without installing + +```sh +npx easy-sharing /path/to/file-or-directory +``` -### Install +### Install globally ```sh npm install -g easy-sharing ``` -> **macOS users:** use the `easy-sharing` command instead of `sharing`. +> **macOS users:** macOS already ships a built-in `/usr/sbin/sharing` command, so use **`easy-sharing`** instead of `sharing`. > Example: `easy-sharing /path/to/file` ### Quick Start @@ -37,12 +58,67 @@ sharing -c # Receive files from another device sharing /destination/directory --receive + +# Share privately — secret link + password + HTTPS, in one flag +sharing /path/to/file-or-directory --secure ``` -Scan the QR code displayed in your terminal with your phone to access the shared content. Both devices must be on the same network, or you can use the `--ip` flag to specify a public IP address: +Scan the QR code shown in your terminal with your phone. Both devices just need to be on the same Wi-Fi. + +**QR code won't scan?** (Some Windows terminals and unicode paths can't draw it.) Open the link the terminal prints, or run with `--open` to pop the QR up in a browser window on your computer — then scan that. + +## Features + +### 📤 Share anything + +- **Files and directories** over your local network, with a clean browsable listing. +- **Download a whole folder as a single `.zip`** — one tap on the phone instead of saving files one by one. +- **Clipboard text** (`-c`) opens on the phone with a one-tap **Copy** button. + +### 📥 Receive just as easily + +- Turn your machine into a drop target with `--receive`. +- **Multiple files at once**, with **drag-and-drop** and a live progress bar. +- **Send a note or link back** from the phone straight to your terminal (and your clipboard). + +### 🔒 Private when you need it + +- `--secure` — the easy button: a secret unguessable link **+** an auto-generated password **+** HTTPS, all at once. +- Or mix and match: `-U`/`-P` for a password, `--token` for a secret link, `-S` for HTTPS. +- **Auto HTTPS:** `-S` now generates a certificate for you — no more fiddling with OpenSSL. (Bring your own with `-C`/`-K` if you prefer.) + +### ⏱️ Ephemeral by choice + +- `--once` — stop sharing automatically after the first transfer. +- `--timeout 10m` — auto-stop after a set time (`30s`, `10m`, `1h`). + +### 🎯 It just works + +- **Smart network detection:** `sharing` advertises your real Wi-Fi address and skips Docker/VPN/WSL adapters — the #1 reason "the QR scans but the page won't load." Pin one explicitly with `--interface en0` or `--ip`. +- **QR fallback:** `--open` shows the QR as an image in a browser for terminals that can't render it. +- **Internet sharing:** `--tunnel` walks you through exposing a share beyond your LAN. + +## Usage examples ```sh -sharing --ip /path/to/file-or-directory +# Receive a batch of photos from your phone (multi-file + drag & drop) +sharing ~/Downloads --receive + +# Share a folder and let the recipient grab it all as one zip +sharing ~/project # the listing shows a "Download as .zip" button + +# Copy a snippet to your phone, or send a link from the phone to your terminal +sharing -c # then use the Copy button on the page +sharing ~/x --receive # the upload page also has a "Send text" box + +# A private, self-destructing share +sharing report.pdf --secure --once + +# Pick a specific network interface (multi-homed / VPN machines) +sharing ~/x --interface en0 + +# Share over HTTPS with your own certificate +sharing ~/x -S -C cert.pem -K key.pem ``` ## Options @@ -67,45 +143,62 @@ Examples: Share with basic authentication $ sharing /path/to/file-or-directory -U user -P password + Share privately (secret link + password + HTTPS) + $ sharing /path/to/file-or-directory --secure + Share over HTTPS $ sharing /path/to/file-or-directory -S -C cert.pem -K key.pem Options: --version Show version number [boolean] - --debug Enable debug logging - [boolean] [default: false] - -p, --port Set the server port (default: auto-assigned) - [number] - --ip Specify your machine's public IP address - [string] + --debug Enable debug logging [boolean] [default: false] + -p, --port Set the server port (default: auto-assigned) [number] + --ip Specify your machine's public IP address [string] + -i, --interface Network interface/adapter name to advertise + (e.g. en0, eth0) [string] -c, --clipboard Share clipboard content [boolean] - -t, --tmpdir Set temporary directory for clipboard files - [string] - -w, --on-windows-native-terminal Enable QR code rendering in Windows native - terminal [boolean] + -w, --on-windows-native-terminal Enable QR code rendering in Windows native terminal [boolean] + --open Open the QR code in a browser window on this computer [boolean] -r, --receive Receive files from another device [boolean] -q, --receive-port Set the port for receiving files [number] -U, --username Set username for basic authentication [string] [default: "user"] - -P, --password Set password for basic authentication - [string] - -S, --ssl Enable HTTPS [boolean] + -P, --password Set password for basic authentication [string] + -S, --ssl Enable HTTPS (auto self-signed cert when + -C/-K are not given) [boolean] -C, --cert Path to SSL certificate file [string] -K, --key Path to SSL private key file [string] + --token Add a secret token to the share URL so it is + unguessable [boolean] + --secure Private share preset: secret link + + generated password + HTTPS [boolean] + --once Stop sharing after the first completed transfer [boolean] + --timeout Auto-stop the share after a duration (e.g. + 30s, 10m, 1h) [string] --tunnel Show guide for sharing over the internet via tunnel services [boolean] --help Show help [boolean] ``` +## A note on security + +By default a share is open to everyone on your Wi-Fi — perfect for your own devices at home, less so on a café or office network. `sharing` reminds you of this on startup and gives you one-flag protection: + +```sh +sharing ~/private --secure +``` + +This generates a **secret link** (so the share isn't browsable by IP alone), a **random password**, and turns on **HTTPS** — printed for you when the server starts. + ## Sharing Over the Internet (Tunneling) -If you want to share files with someone who is **not** on your local network, you can use a tunnel service to make your share accessible over the internet — no public IP address required. +To share with someone who is **not** on your local network, pair `sharing` with a tunnel service — no public IP required. -Run `sharing --tunnel` for a quick setup guide, or follow these steps: +Run `sharing --tunnel` for a quick setup guide, or: 1. Start sharing as usual: `sharing /path/to/files` 2. In a separate terminal, run one of the tunnel commands below -3. Share the public URL provided by the tunnel service +3. Share the public URL the tunnel gives you | Service | Command | Documentation | |---|---|---| @@ -116,6 +209,12 @@ Run `sharing --tunnel` for a quick setup guide, or follow these steps: > Replace `7478` with the port shown when you start sharing. +## Development + +```sh +npm test # runs the test suite (no external test framework) +``` + ## License [MIT](LICENSE) diff --git a/bin/app.js b/bin/app.js index 943a010..3524537 100644 --- a/bin/app.js +++ b/bin/app.js @@ -42,9 +42,91 @@ const fixListingLinks = (html, mountPath) => return 'href="' + mountPath + encodePathSegments(decodedPath) + '"'; }); -const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onStart, postUploadRedirectUrl, shareAddress }) => { +// Escape a string for safe interpolation into HTML text/attributes. +const escapeHtml = (s) => + String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + +// Strip terminal control characters (except tab/newline/carriage-return) so that +// text submitted from a phone cannot inject escape sequences into the terminal. +// eslint-disable-next-line no-control-regex +const stripControlChars = (s) => + String(s).replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); + +// Resolve a destination for an uploaded file using only its basename (any path +// components in the supplied name are discarded, so a crafted "../" name cannot +// escape the share directory). Never overwrites an existing file, never reuses a +// name already reserved in this request (so two same-named files in one upload +// don't clobber each other), and never writes through a symlink (a dangling or +// real symlink at the target would otherwise let a write escape the share dir). +const destForUpload = (root, rawName, reserved) => { + const base = path.basename(String(rawName).replace(/\\/g, '/')); + if (!base || base === '.' || base === '..') return null; + const isSymlink = (p) => { + try { return fs.lstatSync(p).isSymbolicLink(); } + catch (e) { return false; } // ENOENT -> no entry, safe + }; + const taken = (p) => reserved.has(p) || fs.existsSync(p) || isSymlink(p); + let dest = path.join(root, base); + if (!taken(dest)) { + reserved.add(dest); + return dest; + } + const ext = path.extname(base); + const stem = path.basename(base, ext); + let i = 1; + do { + dest = path.join(root, stem + ' (' + i + ')' + ext); + i++; + } while (taken(dest)); + reserved.add(dest); + return dest; +}; + +// Recursively add a directory's *regular files* to an archive, skipping symlinks +// (which serve-handler also refuses to follow) so their outside-the-share targets +// are never disclosed or followed. +const addDirToArchive = (archive, dir, base) => { + let names; + try { names = fs.readdirSync(dir); } catch (e) { return; } + for (const name of names) { + const abs = path.join(dir, name); + let st; + try { st = fs.lstatSync(abs); } catch (e) { continue; } + if (st.isSymbolicLink()) continue; + const rel = base ? base + '/' + name : name; + if (st.isDirectory()) addDirToArchive(archive, abs, rel); + else if (st.isFile()) archive.file(abs, { name: rel }); + } +}; + +const start = ({ + port, + sharePath, + receive, + clipboard, + updateClipboardData, + onStart, + postUploadRedirectUrl, + shareAddress, + // Optional capabilities (default off -> behaviour identical to before): + token, // capability token; when set the share is mounted under /share/ + allowZip, // expose the zip route and inject a "Download as .zip" link into listings + clipboardText, // serve the clipboard copy page instead of a file download + getClipboardData, // () => { isPath, text } — re-read live on each request + once, // stop the server after the first completed transfer + onFinish, // called when --once completes a transfer (the caller owns process exit) +} = {}) => { const app = express(); + const sharePrefix = '/share' + (token ? '/' + token : ''); + const clipboardPrefix = '/clipboard' + (token ? '/' + token : ''); + const zipPrefix = '/zip' + (token ? '/' + token : ''); + // Basic Auth if (config.auth.username && config.auth.password) { app.use(basicAuth({ @@ -54,6 +136,55 @@ const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onSta })); } + let server; + let onceDone = false; + const finishOnce = (reason) => { + if (!once || onceDone) return; + onceDone = true; + console.log('\nTransfer complete (' + reason + '). Stopping share.'); + // app.js stays a pure server module: it closes the listener and hands control + // back to the caller via onFinish (which owns whether to exit the process). + let done = false; + const finish = () => { if (done) return; done = true; if (onFinish) onFinish(reason); }; + if (server) server.close(finish); else finish(); + // Safety net in case a keep-alive socket keeps the server open. + setTimeout(finish, 1500).unref(); + }; + + // QR fallback page: renders the share (and upload) URL as a scannable image, + // for terminals that cannot draw the QR (Windows native terminal, unicode paths). + app.get('/qr', async (req, res) => { + let QRCode; + try { + QRCode = require('qrcode'); + } catch (e) { + return res.status(501).type('text').send('QR image support is not installed (npm install qrcode).'); + } + const targets = []; + if (receive) targets.push({ label: 'Scan to upload a file', url: postUploadRedirectUrl }); + if (shareAddress) targets.push({ label: clipboard ? 'Scan to open the clipboard' : 'Scan to open the share', url: shareAddress }); + try { + const blocks = await Promise.all(targets.map(async (t) => { + const dataUrl = await QRCode.toDataURL(t.url, { margin: 2, width: 320 }); + return '

' + escapeHtml(t.label) + '

' + + 'QR code' + + '

' + escapeHtml(t.url) + '

'; + })); + res.type('html').send( + '' + + '' + + 'sharing — QR' + + '

📷 Scan with your phone

' + blocks.join('') + '' + ); + } catch (err) { + res.status(500).type('text').send('Could not render QR: ' + (err.message || String(err))); + } + }); + // Routing if (receive) { app.use(fileUpload()); @@ -71,23 +202,134 @@ const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onSta return res.status(400).send('No files were received.'); } - const selectedFile = req.files.selected; - const selectedFileName = Buffer.from(selectedFile.name, 'ascii').toString('utf8'); - const uploadPath = path.join(path.resolve(sharePath), selectedFileName); - utils.debugLog('upload path: ' + uploadPath); + // express-fileupload returns a single object for one file and an array + // when several files share the field name ("selected"); normalise to an array. + const files = [].concat(req.files.selected).filter(Boolean); + if (files.length === 0) { + return res.status(400).send('No files were received.'); + } - selectedFile.mv(uploadPath) + const root = path.resolve(sharePath); + const saved = []; + // Reserve each destination synchronously as we iterate, so several files + // sharing a basename in one request each get a distinct, fresh name. + const reserved = new Set(); + const tasks = files.map((file) => { + const decodedName = Buffer.from(file.name, 'ascii').toString('utf8'); + const dest = destForUpload(root, decodedName, reserved); + if (!dest) return Promise.reject(new Error('Invalid file name: ' + file.name)); + utils.debugLog('upload path: ' + dest); + return file.mv(dest).then(() => { saved.push(dest); }); + }); + + Promise.all(tasks) .then(() => { - console.log('File received: ' + uploadPath); - res.type('text').send('File shared successfully at ' + uploadPath); + saved.forEach((p) => console.log('File received: ' + p)); + res.type('text').send( + saved.length === 1 + ? 'File shared successfully at ' + saved[0] + : saved.length + ' files shared successfully.' + ); + finishOnce('upload'); }) .catch((err) => { res.status(500).send(err.message || String(err)); }); }); + + // Receive a short text/URL snippet from the phone: print it (sanitised) and + // best-effort copy it to the host clipboard. + app.post('/text', express.json({ limit: '5mb' }), express.urlencoded({ extended: false, limit: '5mb' }), (req, res) => { + const text = (req.body && (req.body.text != null ? req.body.text : '')) || ''; + if (!String(text).length) return res.status(400).type('text').send('No text received.'); + const clean = stripControlChars(text); + console.log('\nText received from device:\n' + clean + '\n'); + // Best-effort copy to the host clipboard (skipped under tests so the + // suite never clobbers the developer's clipboard). + if (!process.env.SHARING_TEST) { + try { + const clipboardy = require('clipboardy'); + clipboardy.writeSync(String(text)); + console.log('(copied to this computer\'s clipboard)'); + } catch (e) { /* best-effort only */ } + } + res.type('text').send('Text received.'); + finishOnce('text'); + }); + } + + // Clipboard share: present the text on a styled page with a one-tap Copy button + // instead of forcing a file download. + if (clipboardText) { + const clipboardPageHtml = fs.readFileSync(path.join(__dirname, 'clipboard-page.html'), 'utf8'); + const currentText = () => { + if (getClipboardData) { + const d = getClipboardData(); + return (d && d.text) || ''; + } + return ''; + }; + // When a token is set the page lives only at the secret path; otherwise it is + // reachable at both '/' and '/clipboard' for convenience. + const pagePaths = token ? [clipboardPrefix] : ['/', '/clipboard']; + const txtPath = clipboardPrefix + '.txt'; + app.get(pagePaths, (req, res) => { + res.type('html').send( + clipboardPageHtml + .replace(/\{clipboardText\}/g, escapeHtml(currentText())) + .replace(/\{downloadUrl\}/g, txtPath) + ); + }); + app.get(txtPath, (req, res) => { + res.on('finish', () => { if (res.statusCode === 200) finishOnce('download'); }); + res.attachment('clipboard.txt'); + res.type('text/plain').send(currentText()); + }); } - app.use('/share', (req, res) => { + // Download an entire shared directory (or a sub-folder via ?path=) as a zip. + // The capability token (when set) is carried in the path (zipPrefix), matching + // the /share design, so it is not exposed in the query string / Referer. + if (allowZip) { + app.get(zipPrefix, (req, res) => { + const root = path.resolve(sharePath); + let target = root; + if (req.query.path) { + const rel = String(req.query.path).replace(/^[/\\]+/, ''); + const resolved = path.resolve(root, rel); + if (resolved !== root && !resolved.startsWith(root + path.sep)) { + return res.status(400).type('text').send('Invalid path.'); + } + target = resolved; + } + if (!fs.existsSync(target) || !fs.lstatSync(target).isDirectory()) { + return res.status(404).type('text').send('Not a directory.'); + } + let archiver; + try { + archiver = require('archiver'); + } catch (e) { + return res.status(501).type('text').send('Zip support is not installed (npm install archiver).'); + } + res.attachment((path.basename(target) || 'share') + '.zip'); + const archive = archiver('zip', { zlib: { level: 6 } }); + archive.on('error', (err) => { + // Once headers/body are out, we can't switch to an error page without + // producing a corrupt-but-200 zip; abort the connection instead. + if (!res.headersSent) res.status(500).type('text').end(String(err && err.message || err)); + else res.destroy(err); + }); + res.on('finish', () => finishOnce('download')); + archive.pipe(res); + addDirToArchive(archive, target, ''); + archive.finalize(); + }); + } + + // In clipboard-text mode there is no real shared directory (sharePath points at + // the working directory only to satisfy the API), so mounting serve-handler here + // would leak the cwd. Skip the share mount entirely in that mode. + if (!clipboardText) app.use(sharePrefix, (req, res) => { if (clipboard && updateClipboardData) { updateClipboardData(); } @@ -98,24 +340,62 @@ const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onSta // (e.g. "/file.txt", "/subdir/", "/"). Browsing or downloading by // clicking those links then navigates to a path Express does not route, // producing a 404. The mount prefix is req.baseUrl ('/share'). - const mountPath = req.baseUrl || '/share'; + const mountPath = req.baseUrl || sharePrefix; const originalUrl = req.url; - req.url = req.url.replace(/^\/share/, '') || '/'; + // Express has already stripped the mount prefix; just normalise the root. + // (A manual /^\/share/ strip would mangle a real file/dir literally named + // "share..." at the top level.) + req.url = req.url || '/'; + // The directory currently being listed, relative to sharePath — used to + // point the injected "Download as .zip" link at this exact folder. + const listingDir = decodeURIComponent((req.url.split('?')[0]) || '/'); + + // Whether this response was the directory listing (vs. an actual file + // download). Used by --once so browsing a folder doesn't count as a transfer. + let wasListing = false; // Rewrite the generated directory-listing links before sending them so - // they are valid URLs that route back through /share (see fixListingLinks). - // Gated on serve-handler's listing signature ('id="files"') so it never - // touches the contents of an actual shared .html file. + // they are valid URLs that route back through /share (see fixListingLinks), + // and inject a "Download as .zip" link when zip support is enabled. Gated on + // serve-handler's listing signature ('id="files"') so it never touches the + // contents of an actual shared .html file. const originalEnd = res.end.bind(res); res.end = function (body, ...rest) { const contentType = res.getHeader('content-type'); const isHtml = contentType && String(contentType).includes('text/html'); if (typeof body === 'string' && isHtml && body.includes('id="files"')) { + wasListing = true; body = fixListingLinks(body, mountPath); + if (allowZip) { + const zipHref = zipPrefix + '?path=' + encodeURIComponent(listingDir); + const bar = '
' + + '' + + '📦 Download this folder as .zip
'; + body = body.replace(/(]*>)/i, '$1' + bar); + } } return originalEnd(body, ...rest); }; + // --once: a GET that served an actual file (not the listing) counts as the + // transfer that ends the share. A 200 is a whole-file download; a 206 only + // counts once the final byte has been delivered, so range-requesting media + // players (which probe with small ranges) don't end the share prematurely. + if (once) { + res.on('finish', () => { + if (req.method !== 'GET' || wasListing) return; + if (res.statusCode === 200) { + finishOnce('download'); + } else if (res.statusCode === 206) { + const cr = String(res.getHeader('content-range') || ''); + const m = cr.match(/bytes\s+\d+-(\d+)\/(\d+)/); + if (m && (parseInt(m[1], 10) + 1) >= parseInt(m[2], 10)) { + finishOnce('download'); + } + } + }); + } + // cleanUrls defaults to true in serve-handler, which makes it answer a // request for an .html file with a 301 to the extension-less path. That // redirect target is built from the prefix-stripped URL, so it both @@ -126,7 +406,7 @@ const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onSta }); // Listen - const server = config.ssl.protocolModule.createServer(config.ssl.option, app).listen(port, onStart); + server = config.ssl.protocolModule.createServer(config.ssl.option, app).listen(port, onStart); return server; }; diff --git a/bin/clipboard-page.html b/bin/clipboard-page.html new file mode 100644 index 0000000..ec32e91 --- /dev/null +++ b/bin/clipboard-page.html @@ -0,0 +1,89 @@ + + + + + clipboard + + + + + + +
+ +
+ + Download as file 💾 +
+
+ + + + diff --git a/bin/index.js b/bin/index.js index 58e5d6f..29fba82 100755 --- a/bin/index.js +++ b/bin/index.js @@ -2,6 +2,7 @@ const fs = require('fs'); const https = require('https'); +const crypto = require('crypto'); const path = require('path'); const yargs = require('yargs'); const qrcode = require('qrcode-terminal'); @@ -31,10 +32,25 @@ const usage = [ ' Share with basic authentication', ' $ sharing /path/to/file-or-directory -U user -P password', '', + ' Share privately (secret link + password + HTTPS)', + ' $ sharing /path/to/file-or-directory --secure', + '', ' Share over HTTPS', ' $ sharing /path/to/file-or-directory -S -C cert.pem -K key.pem', ].join('\n'); +// Open a URL in the host machine's default browser (best effort). +const openBrowser = (url) => { + const { spawn } = require('child_process'); + let cmd; + let args; + if (process.platform === 'darwin') { cmd = 'open'; args = [url]; } + else if (process.platform === 'win32') { cmd = 'cmd'; args = ['/c', 'start', '""', url]; } + else { cmd = 'xdg-open'; args = [url]; } + try { spawn(cmd, args, { stdio: 'ignore', detached: true }).unref(); } + catch (e) { /* ignore */ } +}; + // Main (async () => { const options = yargs @@ -42,16 +58,21 @@ const usage = [ .option('debug', { describe: 'Enable debug logging', type: 'boolean', default: false }) .option('p', { alias: 'port', describe: 'Set the server port (default: auto-assigned)', type: 'number' }) .option('ip', { describe: 'Specify your machine\'s public IP address', type: 'string' }) + .option('i', { alias: 'interface', describe: 'Network interface/adapter name to advertise (e.g. en0, eth0)', type: 'string' }) .option('c', { alias: 'clipboard', describe: 'Share clipboard content', type: 'boolean' }) - .option('t', { alias: 'tmpdir', describe: 'Set temporary directory for clipboard files', type: 'string' }) .option('w', { alias: 'on-windows-native-terminal', describe: 'Enable QR code rendering in Windows native terminal', type: 'boolean' }) + .option('open', { describe: 'Open the QR code in a browser window on this computer', type: 'boolean' }) .option('r', { alias: 'receive', describe: 'Receive files from another device', type: 'boolean' }) .option('q', { alias: 'receive-port', describe: 'Set the port for receiving files', type: 'number' }) .option('U', { alias: 'username', describe: 'Set username for basic authentication', type: 'string', default: 'user' }) .option('P', { alias: 'password', describe: 'Set password for basic authentication', type: 'string' }) - .option('S', { alias: 'ssl', describe: 'Enable HTTPS', type: 'boolean' }) + .option('S', { alias: 'ssl', describe: 'Enable HTTPS (auto self-signed cert when -C/-K are not given)', type: 'boolean' }) .option('C', { alias: 'cert', describe: 'Path to SSL certificate file', type: 'string' }) .option('K', { alias: 'key', describe: 'Path to SSL private key file', type: 'string' }) + .option('token', { describe: 'Add a secret token to the share URL so it is unguessable', type: 'boolean' }) + .option('secure', { describe: 'Private share preset: secret link + generated password + HTTPS', type: 'boolean' }) + .option('once', { describe: 'Stop sharing after the first completed transfer', type: 'boolean' }) + .option('timeout', { describe: 'Auto-stop the share after a duration (e.g. 30s, 10m, 1h)', type: 'string' }) .option('tunnel', { describe: 'Show guide for sharing over the internet via tunnel services', type: 'boolean' }) .help(true) .argv; @@ -100,34 +121,14 @@ const usage = [ process.exit(0); } - if (options.username && options.password) { - config.auth.username = options.username; - config.auth.password = options.password; - } - let sharePath; let fileName; + let clipboardText = false; - if (options.ssl) { - if (!options.cert) { - console.log('Specify the cert path.'); - return; - } - if (!options.key) { - console.log('Specify the key path.'); - return; - } - config.ssl = { - protocolModule: https, - protocol: 'https', - option: { - key: fs.readFileSync(path.resolve(process.cwd(), options.key)), - cert: fs.readFileSync(path.resolve(process.cwd(), options.cert)), - }, - }; - } - - const updateClipboardData = () => { + // Read the clipboard and classify it: either an existing filesystem path to + // share directly, or raw text to present on the clipboard page. Re-reads live + // so each request reflects the current clipboard contents. + const getClipboardData = () => { let clipboard; try { clipboard = require('clipboardy'); @@ -146,18 +147,22 @@ const usage = [ } utils.debugLog('clipboard file path:\n ' + filePath); - if (fs.existsSync(filePath)) { - utils.debugLog('clipboard file ' + filePath + ' found'); - sharePath = filePath; - } else { - const outPath = options.tmpdir ? path.join(options.tmpdir, '.clipboard-tmp') : '.clipboard-tmp'; - fs.writeFileSync(outPath, data); - sharePath = path.resolve(outPath); + if (filePath && fs.existsSync(filePath)) { + return { isPath: true, path: filePath, text: null }; } + return { isPath: false, path: null, text: data }; }; if (options.clipboard) { - updateClipboardData(); + const cb = getClipboardData(); + if (cb.isPath) { + sharePath = cb.path; + } else { + clipboardText = true; + // Not served (the /share mount is skipped in clipboard-text mode); this + // only needs to be an existing directory to satisfy validation below. + sharePath = process.cwd(); + } } else { sharePath = options._[0]; } @@ -177,22 +182,98 @@ const usage = [ process.exit(1); } - if (fs.lstatSync(sharePath).isFile()) { + if (!clipboardText && fs.lstatSync(sharePath).isFile()) { fileName = path.basename(sharePath); sharePath = path.dirname(sharePath); } + // A directory share (not a single file, not clipboard text) can be zipped. + const allowZip = !fileName && !clipboardText; + if (!options.port) { options.port = await portfinder.getPortPromise(config.portfinder); } - const host = options.ip || utils.getNetworkAddress(); + const interfaceCandidates = utils.getNetworkInterfaces(); + const host = options.ip || utils.getNetworkAddress(options.interface); + + // HTTPS: explicit (-S) or implied by --secure. Use the supplied cert/key when + // both are given, otherwise generate a self-signed certificate for the host. + const wantHttps = options.ssl || options.secure; + const usingProvidedCert = Boolean(options.cert && options.key); + if (wantHttps) { + if (usingProvidedCert) { + config.ssl = { + protocolModule: https, + protocol: 'https', + option: { + key: fs.readFileSync(path.resolve(process.cwd(), options.key)), + cert: fs.readFileSync(path.resolve(process.cwd(), options.cert)), + }, + }; + } else if (options.cert || options.key) { + console.log('For custom HTTPS, pass both --cert and --key. Omit both to use an auto self-signed certificate.'); + process.exit(1); + } else { + let selfsigned; + try { + selfsigned = require('selfsigned'); + } catch (e) { + console.error('Auto HTTPS is not available. Install selfsigned, or pass -C/-K.'); + process.exit(1); + } + // An IP altName (type 7) must be a literal IP; a hostname in --ip would + // make selfsigned throw, so emit it as a DNS altName (type 2) instead. + const isIp = /^(\d{1,3}\.){3}\d{1,3}$/.test(host) || host.indexOf(':') !== -1; + const altNames = [{ type: 2, value: 'localhost' }]; + altNames.unshift(isIp ? { type: 7, ip: host } : { type: 2, value: host }); + let pems; + try { + pems = selfsigned.generate( + [{ name: 'commonName', value: host }], + { days: 365, keySize: 2048, algorithm: 'sha256', extensions: [{ name: 'subjectAltName', altNames: altNames }] } + ); + } catch (e) { + console.error('Could not create a self-signed certificate; pass -C/-K instead.'); + process.exit(1); + } + config.ssl = { protocolModule: https, protocol: 'https', option: { key: pems.private, cert: pems.cert } }; + } + } + + // Secret capability token in the share URL (explicit --token or via --secure). + const useToken = options.token || options.secure; + const token = useToken ? crypto.randomBytes(8).toString('hex') : ''; + + // Basic auth: explicit password, or a generated one when --secure is used. + let password = options.password; + if (options.secure && !password) { + // Fixed length over a fixed alphabet -> deterministic ~95 bits of entropy + // (and no dependency on Buffer base64url, which is only on Node >= 14.18). + const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + password = Array.from(crypto.randomBytes(16), (b) => ALPHA[b % ALPHA.length]).join(''); + } + const username = options.username || 'user'; + if (password) { + config.auth.username = username; + config.auth.password = password; + } + const protocol = config.ssl.protocol; const baseUrl = protocol + '://' + host + ':' + options.port; + const sharePrefix = '/share' + (token ? '/' + token : ''); + const clipboardPrefix = '/clipboard' + (token ? '/' + token : ''); const uploadAddress = baseUrl + '/receive'; const file = fileName ? encodeURIComponent(fileName) : ''; - const shareAddress = baseUrl + '/share/' + file; + const shareAddress = clipboardText ? (baseUrl + clipboardPrefix) : (baseUrl + sharePrefix + '/' + file); + const qrPageUrl = baseUrl + '/qr'; + + const timeoutMs = utils.parseDuration(options.timeout); + if (options.timeout != null && timeoutMs == null) { + console.error("Could not parse --timeout '" + options.timeout + "'; expected forms like 30s, 10m, 1h."); + process.exit(1); + } const onStart = () => { // Handle receive @@ -215,17 +296,76 @@ const usage = [ console.log(usageMessage); qrcode.generate(shareAddress, config.qrcode); console.log('access link: ' + shareAddress); + + // QR fallback for terminals that can't render it (Windows native, unicode). + console.log("\nCan't scan the QR-Code? Open this in a browser on this computer:\n " + qrPageUrl); + + // If several network addresses exist and the user didn't pin one, surface + // them so a wrong-interface guess is easy to correct. + if (!options.ip && !options.interface && interfaceCandidates.length > 1) { + const others = interfaceCandidates + .filter((c) => c.address !== host) + .map((c) => c.name + ' (' + c.address + ')') + .join(', '); + if (others) { + console.log('\nAdvertising ' + host + '. Other addresses: ' + others); + console.log(' (wrong one? pick with --interface or --ip )'); + } + } + + if (token) { + console.log('\nThis is a secret link — only people you send the exact URL to can open it.'); + } + if (options.secure && password) { + console.log('Login — username: ' + username + ' password: ' + password); + } + if (wantHttps && !usingProvidedCert) { + console.log('Using a self-signed HTTPS certificate; your browser shows a one-time warning — that is expected.'); + } + if (!config.auth.password && !token) { + console.log('\n⚠ Anyone on your network can open this share. Restrict it with -U -P , or use --secure.'); + } + + // Connectivity hint — the #1 reason a scan "works" but the page won't load. + console.log('\nNot loading on your phone? Check that both devices are on the same Wi-Fi,'); + console.log('and that your firewall allows port ' + options.port + '.'); + + if (timeoutMs) { + console.log('\nThis share will stop automatically in ' + options.timeout + '.'); + } + if (options.once) { + console.log('This share will stop after the first transfer (--once).'); + } + console.log('\nPress ctrl+c to stop sharing\n'); + + if (options.open) { + openBrowser(protocol + '://localhost:' + options.port + '/qr'); + } }; - app.start({ + const server = app.start({ port: options.port, sharePath: sharePath, receive: options.receive, clipboard: options.clipboard, - updateClipboardData: updateClipboardData, + updateClipboardData: (options.clipboard && !clipboardText) ? getClipboardData : undefined, onStart: onStart, postUploadRedirectUrl: uploadAddress, shareAddress: shareAddress, + token: token, + allowZip: allowZip, + clipboardText: clipboardText, + getClipboardData: clipboardText ? getClipboardData : undefined, + once: options.once, + onFinish: () => process.exit(0), }); + + if (timeoutMs) { + setTimeout(() => { + console.log('\nShare expired (' + options.timeout + '). Stopping.'); + server.close(() => process.exit(0)); + setTimeout(() => process.exit(0), 1500).unref(); + }, timeoutMs).unref(); + } })(); diff --git a/bin/receive-form.html b/bin/receive-form.html index f6ea593..0a6d442 100644 --- a/bin/receive-form.html +++ b/bin/receive-form.html @@ -16,8 +16,10 @@ display: flex; align-items: center; justify-content: center; - height: 100vh; + min-height: 100vh; flex-direction: column; + padding: 24px 0; + box-sizing: border-box; } #share-container { @@ -31,8 +33,15 @@ font-weight: bolder; text-align: center; color: #181818; + cursor: pointer; + } + + #share-container.dragover { + background-color: #b388ff; + outline: 4px dashed #181818; + outline-offset: 6px; } - + a { font-size: 32px; font-size: 5vw; @@ -44,6 +53,14 @@ margin-bottom: 20px; } + #hint { + font-family: monospace; + font-weight: bold; + color: #181818; + margin-bottom: 16px; + text-align: center; + } + #progress-container { display: none; width: 80%; @@ -73,19 +90,58 @@ margin-top: 8px; color: #181818; } + + #text-panel { + width: 80%; + max-width: 500px; + margin-top: 28px; + display: flex; + flex-direction: column; + gap: 10px; + } + + #text-input { + width: 100%; + box-sizing: border-box; + min-height: 80px; + padding: 10px; + font-family: monospace; + font-size: 16px; + border: 3px solid #181818; + border-radius: 6px; + resize: vertical; + } + + #send-text { + font-family: monospace; + font-weight: bolder; + font-size: 18px; + color: #181818; + background-color: #fff; + border: 3px solid #181818; + border-radius: 6px; + padding: 10px; + cursor: pointer; + } -
- +
+
Tap to choose, or drag & drop files here
View directory📁 - + + +
+ + +
@@ -96,24 +152,26 @@ diff --git a/bin/utils.js b/bin/utils.js index dad2fc0..9020fc8 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -1,18 +1,71 @@ const os = require('os'); const config = require('./config'); -const getNetworkAddress = () => { +// Interface-name fragments that indicate a virtual / non-LAN adapter (Docker, +// VPNs, WSL/Hyper-V, VirtualBox, Tailscale/ZeroTier, Apple AWDL, etc.). These +// are the real cause of the category's most common failure — "the QR scans but +// the page won't load" — because a naive "first non-internal IPv4" pick often +// lands on one of these adapters, whose address a phone on the Wi-Fi cannot reach. +const VIRTUAL_NAME_PATTERN = /(docker|veth|virbr|vmnet|vboxnet|vethernet|hyper-?v|wsl|tailscale|^zt|utun|^tun|^tap|ppp|llw|awdl|bridge|^br-)/i; + +// Collect every non-internal IPv4 address together with its interface name. +const getNetworkInterfaces = () => { const interfaces = os.networkInterfaces(); + const result = []; for (const name of Object.keys(interfaces)) { const details = interfaces[name]; if (!details) continue; for (const detail of details) { - if (detail.family === 'IPv4' && !detail.internal) { - return detail.address; + // Node >= 18 reports family as the string 'IPv4'; older as the number 4. + const isIPv4 = detail.family === 'IPv4' || detail.family === 4; + if (isIPv4 && !detail.internal) { + result.push({ name: name, address: detail.address }); } } } - return '127.0.0.1'; + return result; +}; + +// Score a candidate so the most-likely-reachable LAN address wins (higher is +// better). Private LAN ranges are preferred; 172.16/12 ranks lower because +// Docker's default bridge lives there, and virtual adapter names are penalised. +const scoreInterface = ({ name, address }) => { + let score = 0; + if (VIRTUAL_NAME_PATTERN.test(name)) score -= 100; + if (/^192\.168\./.test(address)) score += 50; + else if (/^10\./.test(address)) score += 40; + else if (/^172\.(1[6-9]|2\d|3[01])\./.test(address)) score += 20; + else if (/^169\.254\./.test(address)) score -= 50; // link-local / APIPA: not useful + else score += 10; // routable / public or otherwise + return score; +}; + +// Return the best LAN address. If `preferred` (an interface name) is given, +// return that interface's IPv4 address when present. Falls back to 127.0.0.1. +const getNetworkAddress = (preferred) => { + const candidates = getNetworkInterfaces(); + if (preferred) { + const match = candidates.find((c) => c.name === preferred); + if (match) return match.address; + // Preferred interface not found — fall through to best-effort selection. + } + if (candidates.length === 0) return '127.0.0.1'; + const ranked = candidates + .map((c) => ({ c: c, s: scoreInterface(c) })) + .sort((a, b) => b.s - a.s); + return ranked[0].c.address; +}; + +// Parse a short human duration ("30s", "10m", "1h", "500ms", or a bare number +// of seconds) into milliseconds. Returns null when the input is not parseable. +const parseDuration = (str) => { + if (str == null) return null; + const m = String(str).trim().match(/^(\d+)\s*(ms|s|m|h)?$/i); + if (!m) return null; + const n = parseInt(m[1], 10); + const unit = (m[2] || 's').toLowerCase(); + const mult = unit === 'ms' ? 1 : unit === 's' ? 1000 : unit === 'm' ? 60000 : 3600000; + return n * mult; }; const debugLog = (log) => { @@ -23,5 +76,8 @@ const debugLog = (log) => { module.exports = { getNetworkAddress, + getNetworkInterfaces, + scoreInterface, + parseDuration, debugLog, -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index aecf777..fdebd01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,20 +1,23 @@ { "name": "easy-sharing", - "version": "1.3.2", + "version": "1.4.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "easy-sharing", - "version": "1.3.2", + "version": "1.4.0", "license": "MIT", "dependencies": { + "archiver": "^7.0.1", "clipboardy": "^2.3.0", "express": "^4.21.2", "express-basic-auth": "^1.2.1", "express-fileupload": "^1.4.0", "portfinder": "^1.0.32", + "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", + "selfsigned": "^2.4.1", "serve-handler": "^6.1.6", "yargs": "^17.6.0" }, @@ -26,6 +29,142 @@ "node": ">=14.0.0" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -80,6 +219,60 @@ ], "license": "MIT" }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/archiver/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -93,12 +286,138 @@ "lodash": "^4.17.14" } }, + "node_modules/b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "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==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -153,6 +472,39 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -202,6 +554,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/clipboardy": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", @@ -245,6 +606,34 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -283,6 +672,37 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -308,6 +728,15 @@ "ms": "2.0.0" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -327,6 +756,12 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "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==", + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -341,6 +776,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -423,6 +864,33 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -550,6 +1018,12 @@ } ] }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -568,6 +1042,106 @@ "node": ">= 0.8" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/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==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/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==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -651,6 +1225,51 @@ "node": ">=6" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -663,6 +1282,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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==", + "license": "ISC" + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -719,6 +1344,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -777,18 +1422,93 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -872,6 +1592,15 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -903,6 +1632,24 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "license": "MIT" }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -957,6 +1704,48 @@ "node": ">=4" } }, + "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==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -966,6 +1755,15 @@ "node": ">= 0.8" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", @@ -981,12 +1779,37 @@ "node": ">=4" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", "license": "MIT" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -1013,6 +1836,21 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1035,6 +1873,23 @@ "once": "^1.3.1" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qrcode-terminal": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", @@ -1043,6 +1898,72 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/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==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/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==", + "license": "ISC" + }, + "node_modules/qrcode/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==", + "license": "MIT", + "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/qrcode/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==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -1091,6 +2012,52 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1099,6 +2066,12 @@ "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==", + "license": "ISC" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1110,6 +2083,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -1209,6 +2195,12 @@ "node": ">= 0.8.0" } }, + "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==", + "license": "ISC" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -1331,6 +2323,46 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -1344,6 +2376,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "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==", + "license": "MIT", + "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", @@ -1355,6 +2402,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -1364,6 +2424,36 @@ "node": ">=0.10.0" } }, + "node_modules/tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1386,6 +2476,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -1395,6 +2491,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1423,6 +2525,12 @@ "which": "bin/which" } }, + "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==", + "license": "ISC" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -1439,6 +2547,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1470,16 +2596,118 @@ "node": ">=12" } }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + } + }, + "dependencies": { + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==" + }, + "ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==" + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "requires": { + "ansi-regex": "^6.2.2" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true + }, + "@types/node": { + "version": "25.9.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.3.tgz", + "integrity": "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg==", + "requires": { + "undici-types": ">=7.24.0 <7.24.7" } - } - }, - "dependencies": { + }, + "@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "requires": { + "@types/node": "*" + } + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1507,6 +2735,48 @@ "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==" }, + "archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "requires": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "dependencies": { + "async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + } + } + }, + "archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "requires": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + } + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -1520,11 +2790,71 @@ "lodash": "^4.17.14" } }, + "b4a": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", + "requires": {} + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bare-events": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", + "requires": {} + }, + "bare-fs": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", + "requires": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + } + }, + "bare-os": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==" + }, + "bare-path": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", + "requires": { + "bare-os": "^3.0.1" + } + }, + "bare-stream": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", + "requires": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + } + }, + "bare-url": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", + "requires": { + "bare-path": "^3.0.0" + } + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -1568,6 +2898,20 @@ "concat-map": "0.0.1" } }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, "busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1599,6 +2943,11 @@ "get-intrinsic": "^1.3.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, "clipboardy": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz", @@ -1632,6 +2981,25 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "requires": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" + } + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1657,6 +3025,25 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" + }, + "crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "requires": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + } + }, "cross-spawn": { "version": "6.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", @@ -1677,6 +3064,11 @@ "ms": "2.0.0" } }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" + }, "depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1687,6 +3079,11 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1697,6 +3094,11 @@ "gopd": "^1.2.0" } }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1753,6 +3155,24 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "requires": { + "bare-events": "^2.7.0" + } + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -1846,6 +3266,11 @@ "busboy": "^1.6.0" } }, + "fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -1860,6 +3285,67 @@ "unpipe": "~1.0.0" } }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + }, + "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==", + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==" + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -1914,11 +3400,47 @@ "pump": "^3.0.0" } }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, "gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, + "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==" + }, "has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -1952,6 +3474,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1985,16 +3512,75 @@ "is-docker": "^2.0.0" } }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "requires": { + "readable-stream": "^2.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, "lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, "math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2046,6 +3632,11 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==" + }, "mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -2069,6 +3660,16 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -2103,11 +3704,42 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==" }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "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==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "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==" + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, + "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==" + }, "path-is-inside": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", @@ -2118,11 +3750,25 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==" }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + } + }, "path-to-regexp": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, + "pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + }, "portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -2148,6 +3794,16 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2166,6 +3822,70 @@ "once": "^1.3.1" } }, + "qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "requires": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "dependencies": { + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "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==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "requires": { + "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" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, "qrcode-terminal": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", @@ -2202,11 +3922,54 @@ } } }, + "readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "requires": { + "minimatch": "^5.1.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "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==" }, + "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==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -2217,6 +3980,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, "semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", @@ -2294,6 +4066,11 @@ "send": "~0.19.1" } }, + "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==" + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2371,6 +4148,31 @@ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" }, + "streamx": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", + "requires": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2381,6 +4183,16 @@ "strip-ansi": "^6.0.1" } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2389,11 +4201,46 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==" }, + "tar-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", + "requires": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "requires": { + "streamx": "^2.12.5" + } + }, + "text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "requires": { + "b4a": "^1.6.4" + } + }, "toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -2408,11 +4255,21 @@ "mime-types": "~2.1.24" } }, + "undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==" + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2431,6 +4288,11 @@ "isexe": "^2.0.0" } }, + "which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2441,6 +4303,16 @@ "strip-ansi": "^6.0.0" } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -2469,6 +4341,16 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "requires": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + } } } } diff --git a/package.json b/package.json index 3997612..88b967b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "easy-sharing", - "version": "1.3.2", + "version": "1.4.0", "description": "Sharing is a command-line tool to share directories and files from the CLI to iOS and Android devices without the need of an extra client app", "main": "bin/index.js", "scripts": { @@ -8,7 +8,7 @@ "start": "node bin/index.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=14.17.0" }, "keywords": [ "file sharing", @@ -42,12 +42,15 @@ }, "homepage": "https://github.com/parvardegr/sharing#readme", "dependencies": { + "archiver": "^7.0.1", "clipboardy": "^2.3.0", "express": "^4.21.2", "express-basic-auth": "^1.2.1", "express-fileupload": "^1.4.0", "portfinder": "^1.0.32", + "qrcode": "^1.5.4", "qrcode-terminal": "^0.12.0", + "selfsigned": "^2.4.1", "serve-handler": "^6.1.6", "yargs": "^17.6.0" } diff --git a/test/test.js b/test/test.js index 388d5fc..8f34448 100644 --- a/test/test.js +++ b/test/test.js @@ -9,6 +9,10 @@ const fs = require('fs'); const path = require('path'); const { execFile } = require('child_process'); +// Signal to the app that we are running under tests (e.g. so the /text route +// does not write to the developer's real clipboard). +process.env.SHARING_TEST = '1'; + const utils = require('../bin/utils'); const config = require('../bin/config'); const app = require('../bin/app'); @@ -72,6 +76,37 @@ test('debugLog does not throw when debug is true', () => { config.debug = false; }); +test('scoreInterface prefers a LAN address over a docker/virtual one', () => { + const lan = utils.scoreInterface({ name: 'en0', address: '192.168.1.20' }); + const docker = utils.scoreInterface({ name: 'docker0', address: '172.17.0.1' }); + const vpn = utils.scoreInterface({ name: 'utun3', address: '10.8.0.2' }); + assert.ok(lan > docker, 'LAN interface should outrank docker'); + assert.ok(lan > vpn, 'LAN interface should outrank a VPN tunnel'); +}); + +test('getNetworkInterfaces returns an array of {name,address}', () => { + const list = utils.getNetworkInterfaces(); + assert.ok(Array.isArray(list)); + list.forEach((c) => { + assert.strictEqual(typeof c.name, 'string'); + assert.strictEqual(typeof c.address, 'string'); + }); +}); + +test('getNetworkAddress falls back when a missing interface is requested', () => { + assert.strictEqual(typeof utils.getNetworkAddress('definitely-not-an-iface'), 'string'); +}); + +test('parseDuration parses human durations', () => { + assert.strictEqual(utils.parseDuration('30s'), 30000); + assert.strictEqual(utils.parseDuration('10m'), 600000); + assert.strictEqual(utils.parseDuration('1h'), 3600000); + assert.strictEqual(utils.parseDuration('500ms'), 500); + assert.strictEqual(utils.parseDuration('45'), 45000); + assert.strictEqual(utils.parseDuration('nope'), null); + assert.strictEqual(utils.parseDuration(null), null); +}); + // ---------- config tests ---------- console.log('\nconfig.js'); @@ -105,6 +140,62 @@ function request(url) { }); } +// Collect a (possibly binary) response as a Buffer. +function requestRaw(url) { + return new Promise((resolve, reject) => { + http.get(url, (res) => { + const chunks = []; + res.on('data', (chunk) => { chunks.push(chunk); }); + res.on('end', () => resolve({ status: res.statusCode, buf: Buffer.concat(chunks), headers: res.headers })); + }).on('error', reject); + }); +} + +// POST several files in a single multipart/form-data request, all under the same field. +function postMultipart(port, urlPath, parts) { + return new Promise((resolve, reject) => { + const boundary = '----testboundary' + Date.now(); + const pieces = []; + parts.forEach((p) => { + pieces.push('--' + boundary + '\r\n'); + pieces.push('Content-Disposition: form-data; name="' + p.field + '"; filename="' + p.filename + '"\r\n'); + pieces.push('Content-Type: application/octet-stream\r\n\r\n'); + pieces.push(p.content); + pieces.push('\r\n'); + }); + pieces.push('--' + boundary + '--\r\n'); + const body = Buffer.from(pieces.join(''), 'utf8'); + const req = http.request({ + hostname: '127.0.0.1', port: port, path: urlPath, method: 'POST', + headers: { 'Content-Type': 'multipart/form-data; boundary=' + boundary, 'Content-Length': body.length }, + }, (res) => { + let data = ''; + res.on('data', (c) => { data += c; }); + res.on('end', () => resolve({ status: res.statusCode, data: data })); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function postJson(port, urlPath, obj) { + return new Promise((resolve, reject) => { + const body = Buffer.from(JSON.stringify(obj), 'utf8'); + const req = http.request({ + hostname: '127.0.0.1', port: port, path: urlPath, method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': body.length }, + }, (res) => { + let data = ''; + res.on('data', (c) => { data += c; }); + res.on('end', () => resolve({ status: res.statusCode, data: data })); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + async function integrationTests() { // Create a temp directory with test files const tmpDir = path.join(__dirname, '.tmp-test-dir'); @@ -451,17 +542,264 @@ async function integrationTests() { }); }); + await asyncTest('qr route renders a scannable image page', async () => { + const p = port + 22; + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: tmpDir, receive: false, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', + shareAddress: 'http://127.0.0.1:' + p + '/share/', + onStart: async () => { + try { + const res = await request('http://127.0.0.1:' + p + '/qr'); + assert.strictEqual(res.status, 200); + assert.ok(res.data.indexOf('data:image') !== -1, 'should embed a QR image'); + assert.ok(res.data.indexOf('/share/') !== -1, 'should show the share link'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('uploads multiple files in one request, never overwriting or escaping the dir', async () => { + const p = port + 20; + const recvDir = path.join(tmpDir, 'recv'); + if (!fs.existsSync(recvDir)) fs.mkdirSync(recvDir); + fs.writeFileSync(path.join(recvDir, 'a.txt'), 'pre-existing'); + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: recvDir, receive: true, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await postMultipart(p, '/upload', [ + { field: 'selected', filename: 'a.txt', content: 'AAA' }, + { field: 'selected', filename: 'b.txt', content: 'BBB' }, + { field: 'selected', filename: '../escape.txt', content: 'CCC' }, + ]); + assert.strictEqual(res.status, 200); + assert.strictEqual(fs.readFileSync(path.join(recvDir, 'a.txt'), 'utf8'), 'pre-existing'); + assert.ok(fs.existsSync(path.join(recvDir, 'a (1).txt')), 'collision-safe rename'); + assert.ok(fs.existsSync(path.join(recvDir, 'b.txt')), 'b.txt saved'); + assert.ok(fs.existsSync(path.join(recvDir, 'escape.txt')), 'escape saved as basename'); + assert.ok(!fs.existsSync(path.join(tmpDir, 'escape.txt')), 'must not escape the share dir'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('two same-named files in one request are both kept (no clobber)', async () => { + const p = port + 25; + const d = path.join(tmpDir, 'recv2'); + if (!fs.existsSync(d)) fs.mkdirSync(d); + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: d, receive: true, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await postMultipart(p, '/upload', [ + { field: 'selected', filename: 'dup.txt', content: 'FIRST' }, + { field: 'selected', filename: 'dup.txt', content: 'SECOND' }, + ]); + assert.strictEqual(res.status, 200); + assert.ok(fs.existsSync(path.join(d, 'dup.txt')), 'first kept'); + assert.ok(fs.existsSync(path.join(d, 'dup (1).txt')), 'second kept under a fresh name'); + const c1 = fs.readFileSync(path.join(d, 'dup.txt'), 'utf8'); + const c2 = fs.readFileSync(path.join(d, 'dup (1).txt'), 'utf8'); + assert.ok((c1 === 'FIRST' && c2 === 'SECOND') || (c1 === 'SECOND' && c2 === 'FIRST'), 'both contents preserved'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('upload never writes through a symlink to escape the share dir', async () => { + const p = port + 26; + const d = path.join(tmpDir, 'recv3'); + if (!fs.existsSync(d)) fs.mkdirSync(d); + const outside = path.join(tmpDir, 'OUTSIDE-secret.txt'); + try { fs.unlinkSync(outside); } catch (e) { /* ignore */ } + let symlinked = true; + try { fs.symlinkSync(outside, path.join(d, 'evil.txt')); } catch (e) { symlinked = false; } + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: d, receive: true, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + await postMultipart(p, '/upload', [{ field: 'selected', filename: 'evil.txt', content: 'PWNED' }]); + assert.ok(!fs.existsSync(outside), 'must not write through the symlink to an outside path'); + if (symlinked) { + assert.ok(fs.existsSync(path.join(d, 'evil (1).txt')), 'should land under a safe, non-symlink name'); + } + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('zip route streams a zip of the shared directory and rejects traversal', async () => { + const p = port + 21; + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: tmpDir, receive: false, clipboard: false, allowZip: true, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await requestRaw('http://127.0.0.1:' + p + '/zip'); + assert.strictEqual(res.status, 200); + assert.ok(res.buf.length > 0, 'zip should not be empty'); + assert.strictEqual(res.buf.slice(0, 2).toString('latin1'), 'PK', 'response should be a zip'); + const bad = await request('http://127.0.0.1:' + p + '/zip?path=../../etc'); + assert.ok(bad.status === 400 || bad.status === 404, 'traversal must be rejected, got ' + bad.status); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('zip skips symlinks (does not disclose their targets)', async () => { + const p = port + 29; + const d = path.join(tmpDir, 'ziptest'); + if (!fs.existsSync(d)) fs.mkdirSync(d); + fs.writeFileSync(path.join(d, 'real.txt'), 'real'); + const linkName = 'ZZLINKZZ'; + try { fs.symlinkSync('/etc/hosts', path.join(d, linkName)); } catch (e) { /* ignore */ } + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: d, receive: false, clipboard: false, allowZip: true, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await requestRaw('http://127.0.0.1:' + p + '/zip'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.buf.slice(0, 2).toString('latin1'), 'PK', 'should be a zip'); + assert.ok(!res.buf.includes(Buffer.from(linkName)), 'symlink name must not appear in the zip'); + assert.ok(res.buf.includes(Buffer.from('real.txt')), 'real file should be in the zip'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('clipboard page shows text with a copy button and does not serve the cwd', async () => { + const p = port + 24; + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: process.cwd(), receive: false, clipboard: true, + clipboardText: true, getClipboardData: () => ({ isPath: false, text: 'secret clip text' }), + updateClipboardData: null, postUploadRedirectUrl: '', + shareAddress: 'http://127.0.0.1:' + p + '/clipboard', + onStart: async () => { + try { + const res = await request('http://127.0.0.1:' + p + '/clipboard'); + assert.strictEqual(res.status, 200); + assert.ok(res.data.indexOf('secret clip text') !== -1, 'shows clipboard text'); + assert.ok(res.data.toLowerCase().indexOf('copy') !== -1, 'has a copy button'); + const dl = await request('http://127.0.0.1:' + p + '/clipboard.txt'); + assert.strictEqual(dl.status, 200); + assert.strictEqual(dl.data, 'secret clip text'); + const share = await request('http://127.0.0.1:' + p + '/share/'); + assert.strictEqual(share.status, 404, 'cwd must not be served'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('text route accepts a snippet and rejects empty input', async () => { + const p = port + 23; + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: tmpDir, receive: true, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await postJson(p, '/text', { text: 'hello from a test (not your clipboard)' }); + assert.strictEqual(res.status, 200); + const empty = await postJson(p, '/text', { text: '' }); + assert.strictEqual(empty.status, 400); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('a top-level path whose name starts with "share" is reachable', async () => { + const p = port + 27; + fs.writeFileSync(path.join(tmpDir, 'sharething.txt'), 'share-prefixed'); + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: tmpDir, receive: false, clipboard: false, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const res = await request('http://127.0.0.1:' + p + '/share/sharething.txt'); + assert.strictEqual(res.status, 200); + assert.strictEqual(res.data, 'share-prefixed'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + + await asyncTest('capability token gates the share and the zip', async () => { + const p = port + 28; + const tok = 'testtoken123'; + await new Promise((resolve, reject) => { + const server = app.start({ + port: p, sharePath: tmpDir, receive: false, clipboard: false, allowZip: true, token: tok, + updateClipboardData: null, postUploadRedirectUrl: '', shareAddress: '', + onStart: async () => { + try { + const base = 'http://127.0.0.1:' + p; + assert.strictEqual((await request(base + '/share/' + tok + '/')).status, 200, 'tokened listing loads'); + assert.strictEqual((await request(base + '/share/')).status, 404, 'untokened share is 404'); + assert.strictEqual((await requestRaw(base + '/zip/' + tok)).status, 200, 'tokened zip works'); + assert.strictEqual((await request(base + '/zip')).status, 404, 'untokened zip is 404'); + resolve(); + } catch (e) { reject(e); } + }, + }); + servers.push(server); + }); + }); + // Cleanup closeServers(); try { - fs.unlinkSync(path.join(tmpDir, 'hello.txt')); - fs.unlinkSync(path.join(tmpDir, 'File #1.txt')); - fs.unlinkSync(path.join(tmpDir, 'page.html')); - trickyNames.forEach((n) => fs.unlinkSync(path.join(tmpDir, n))); - fs.unlinkSync(path.join(subDir, 'nested.txt')); - fs.rmdirSync(subDir); - fs.rmdirSync(tmpDir); - } catch (e) { /* ignore */ } + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch (e) { + try { + fs.unlinkSync(path.join(tmpDir, 'hello.txt')); + fs.unlinkSync(path.join(tmpDir, 'File #1.txt')); + fs.unlinkSync(path.join(tmpDir, 'page.html')); + trickyNames.forEach((n) => fs.unlinkSync(path.join(tmpDir, n))); + fs.unlinkSync(path.join(subDir, 'nested.txt')); + fs.rmdirSync(subDir); + fs.rmdirSync(tmpDir); + } catch (e2) { /* ignore */ } + } } integrationTests().then(() => {