From c11d7846b4e1d9a505dfa778dc363146b1a58c5e Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 14:50:11 +0800 Subject: [PATCH 01/13] chore(deps): add qrcode, archiver, selfsigned; require Node >=14.17 --- package-lock.json | 1900 ++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- 2 files changed, 1895 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index aecf777..834d3d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,15 @@ "version": "1.3.2", "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..4ab44f7 100644 --- a/package.json +++ b/package.json @@ -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" } From e67f2e3e33d09a4a5e56bae195de902d89e41eef Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 14:54:57 +0800 Subject: [PATCH 02/13] feat(network): smart interface detection + --interface --- bin/index.js | 18 +++++++++++++++++- bin/utils.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++----- test/test.js | 21 +++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) diff --git a/bin/index.js b/bin/index.js index 58e5d6f..c0c882d 100755 --- a/bin/index.js +++ b/bin/index.js @@ -42,6 +42,7 @@ 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' }) @@ -186,7 +187,8 @@ const usage = [ 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); const protocol = config.ssl.protocol; const baseUrl = protocol + '://' + host + ':' + options.port; @@ -215,6 +217,20 @@ const usage = [ console.log(usageMessage); qrcode.generate(shareAddress, config.qrcode); console.log('access link: ' + shareAddress); + + // 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 )'); + } + } + console.log('\nPress ctrl+c to stop sharing\n'); }; diff --git a/bin/utils.js b/bin/utils.js index dad2fc0..9488bdb 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -1,18 +1,59 @@ 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; }; const debugLog = (log) => { @@ -23,5 +64,7 @@ const debugLog = (log) => { module.exports = { getNetworkAddress, + getNetworkInterfaces, + scoreInterface, debugLog, -}; \ No newline at end of file +}; diff --git a/test/test.js b/test/test.js index 388d5fc..e3f3b20 100644 --- a/test/test.js +++ b/test/test.js @@ -72,6 +72,27 @@ 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'); +}); + // ---------- config tests ---------- console.log('\nconfig.js'); From 5a75c0a2b11c82d408f2d16730392bab87111469 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 14:56:15 +0800 Subject: [PATCH 03/13] feat(qr): browser QR fallback page and --open --- bin/app.js | 43 +++++++++++++++++++++++++++++++++++++++++++ bin/index.js | 21 +++++++++++++++++++++ test/test.js | 21 +++++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/bin/app.js b/bin/app.js index 943a010..6120205 100644 --- a/bin/app.js +++ b/bin/app.js @@ -42,6 +42,15 @@ const fixListingLinks = (html, mountPath) => return 'href="' + mountPath + encodePathSegments(decodedPath) + '"'; }); +// Escape a string for safe interpolation into HTML text/attributes. +const escapeHtml = (s) => + String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onStart, postUploadRedirectUrl, shareAddress }) => { const app = express(); @@ -54,6 +63,40 @@ const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onSta })); } + // 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()); diff --git a/bin/index.js b/bin/index.js index c0c882d..9aa684c 100755 --- a/bin/index.js +++ b/bin/index.js @@ -35,6 +35,18 @@ const usage = [ ' $ 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 @@ -46,6 +58,7 @@ const usage = [ .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' }) @@ -195,6 +208,7 @@ const usage = [ const uploadAddress = baseUrl + '/receive'; const file = fileName ? encodeURIComponent(fileName) : ''; const shareAddress = baseUrl + '/share/' + file; + const qrPageUrl = baseUrl + '/qr'; const onStart = () => { // Handle receive @@ -218,6 +232,9 @@ const usage = [ 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) { @@ -232,6 +249,10 @@ const usage = [ } console.log('\nPress ctrl+c to stop sharing\n'); + + if (options.open) { + openBrowser(protocol + '://localhost:' + options.port + '/qr'); + } }; app.start({ diff --git a/test/test.js b/test/test.js index e3f3b20..4cc70e8 100644 --- a/test/test.js +++ b/test/test.js @@ -472,6 +472,27 @@ 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); + }); + }); + // Cleanup closeServers(); try { From b77577b5339530608b8bbaf3106e46297e5cde2c Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 14:58:25 +0800 Subject: [PATCH 04/13] feat(receive): multi-file and drag-and-drop uploads --- bin/app.js | 95 +++++++++++++++++++++++++++--- bin/receive-form.html | 91 +++++++++++++++++++++-------- test/test.js | 133 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 279 insertions(+), 40 deletions(-) diff --git a/bin/app.js b/bin/app.js index 6120205..1f4f78f 100644 --- a/bin/app.js +++ b/bin/app.js @@ -51,7 +51,49 @@ const escapeHtml = (s) => .replace(/"/g, '"') .replace(/'/g, '''); -const start = ({ port, sharePath, receive, clipboard, updateClipboardData, onStart, postUploadRedirectUrl, shareAddress }) => { +// 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; +}; + +const start = ({ + port, + sharePath, + receive, + clipboard, + updateClipboardData, + onStart, + postUploadRedirectUrl, + shareAddress, + // Optional capabilities (default off -> behaviour identical to before): + once, // stop the server after the first completed transfer + onFinish, // called when --once completes a transfer (the caller owns process exit) +} = {}) => { const app = express(); // Basic Auth @@ -63,6 +105,21 @@ 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) => { @@ -114,15 +171,35 @@ 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)); @@ -169,7 +246,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/receive-form.html b/bin/receive-form.html index f6ea593..cbc2310 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%; @@ -76,16 +93,17 @@ -
- +
+
Tap to choose, or drag & drop files here
View directory📁 - +
@@ -96,24 +114,26 @@ diff --git a/test/test.js b/test/test.js index 4cc70e8..373aaef 100644 --- a/test/test.js +++ b/test/test.js @@ -126,6 +126,34 @@ function request(url) { }); } +// 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(); + }); +} + async function integrationTests() { // Create a temp directory with test files const tmpDir = path.join(__dirname, '.tmp-test-dir'); @@ -493,17 +521,106 @@ async function integrationTests() { }); }); + 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); + }); + }); + // 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(() => { From e85f148a0ab7c788f2df756e7cd6f606a3600713 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:00:05 +0800 Subject: [PATCH 05/13] feat(zip): download a shared folder as a single .zip --- bin/app.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++--- bin/index.js | 4 +++ test/test.js | 59 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+), 3 deletions(-) diff --git a/bin/app.js b/bin/app.js index 1f4f78f..fdb5a7d 100644 --- a/bin/app.js +++ b/bin/app.js @@ -81,6 +81,23 @@ const destForUpload = (root, rawName, reserved) => { 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, @@ -91,6 +108,7 @@ const start = ({ postUploadRedirectUrl, shareAddress, // Optional capabilities (default off -> behaviour identical to before): + allowZip, // expose the zip route and inject a "Download as .zip" link into listings once, // stop the server after the first completed transfer onFinish, // called when --once completes a transfer (the caller owns process exit) } = {}) => { @@ -207,6 +225,43 @@ const start = ({ }); } + // Download an entire shared directory (or a sub-folder via ?path=) as a zip. + if (allowZip) { + app.get('/zip', (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(); + }); + } + app.use('/share', (req, res) => { if (clipboard && updateClipboardData) { updateClipboardData(); @@ -221,17 +276,28 @@ const start = ({ const mountPath = req.baseUrl || '/share'; const originalUrl = req.url; req.url = req.url.replace(/^\/share/, '') || '/'; + // 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]) || '/'); // 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"')) { body = fixListingLinks(body, mountPath); + if (allowZip) { + const zipHref = '/zip?path=' + encodeURIComponent(listingDir); + const bar = ''; + body = body.replace(/(]*>)/i, '$1' + bar); + } } return originalEnd(body, ...rest); }; diff --git a/bin/index.js b/bin/index.js index 9aa684c..eff3bd5 100755 --- a/bin/index.js +++ b/bin/index.js @@ -196,6 +196,9 @@ const openBrowser = (url) => { sharePath = path.dirname(sharePath); } + // A directory share (not a single file) can be downloaded as a zip. + const allowZip = !fileName; + if (!options.port) { options.port = await portfinder.getPortPromise(config.portfinder); } @@ -264,5 +267,6 @@ const openBrowser = (url) => { onStart: onStart, postUploadRedirectUrl: uploadAddress, shareAddress: shareAddress, + allowZip: allowZip, }); })(); diff --git a/test/test.js b/test/test.js index 373aaef..329f8a7 100644 --- a/test/test.js +++ b/test/test.js @@ -126,6 +126,17 @@ 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) => { @@ -606,6 +617,54 @@ async function integrationTests() { }); }); + 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); + }); + }); + // Cleanup closeServers(); try { From cd7d49b30bf0b25daa0b02fd2e38ae51426dc3b5 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:02:36 +0800 Subject: [PATCH 06/13] feat(clipboard): browser copy page for shared clipboard text --- bin/app.js | 32 ++++++++++++++- bin/clipboard-page.html | 89 +++++++++++++++++++++++++++++++++++++++++ bin/index.js | 39 +++++++++++------- test/test.js | 27 +++++++++++++ 4 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 bin/clipboard-page.html diff --git a/bin/app.js b/bin/app.js index fdb5a7d..1e58890 100644 --- a/bin/app.js +++ b/bin/app.js @@ -109,6 +109,8 @@ const start = ({ shareAddress, // Optional capabilities (default off -> behaviour identical to before): 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) } = {}) => { @@ -225,6 +227,31 @@ const start = ({ }); } + // 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 ''; + }; + app.get(['/', '/clipboard'], (req, res) => { + res.type('html').send( + clipboardPageHtml + .replace(/\{clipboardText\}/g, escapeHtml(currentText())) + .replace(/\{downloadUrl\}/g, '/clipboard.txt') + ); + }); + app.get('/clipboard.txt', (req, res) => { + res.on('finish', () => { if (res.statusCode === 200) finishOnce('download'); }); + res.attachment('clipboard.txt'); + res.type('text/plain').send(currentText()); + }); + } + // Download an entire shared directory (or a sub-folder via ?path=) as a zip. if (allowZip) { app.get('/zip', (req, res) => { @@ -262,7 +289,10 @@ const start = ({ }); } - app.use('/share', (req, res) => { + // 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('/share', (req, res) => { if (clipboard && updateClipboardData) { updateClipboardData(); } 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 eff3bd5..d11cc47 100755 --- a/bin/index.js +++ b/bin/index.js @@ -56,7 +56,6 @@ const openBrowser = (url) => { .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' }) @@ -121,6 +120,7 @@ const openBrowser = (url) => { let sharePath; let fileName; + let clipboardText = false; if (options.ssl) { if (!options.cert) { @@ -141,7 +141,10 @@ const openBrowser = (url) => { }; } - 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'); @@ -160,18 +163,22 @@ const openBrowser = (url) => { } 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]; } @@ -191,13 +198,13 @@ const openBrowser = (url) => { 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) can be downloaded as a zip. - const allowZip = !fileName; + // 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); @@ -210,7 +217,7 @@ const openBrowser = (url) => { const uploadAddress = baseUrl + '/receive'; const file = fileName ? encodeURIComponent(fileName) : ''; - const shareAddress = baseUrl + '/share/' + file; + const shareAddress = clipboardText ? (baseUrl + '/clipboard') : (baseUrl + '/share/' + file); const qrPageUrl = baseUrl + '/qr'; const onStart = () => { @@ -263,10 +270,12 @@ const openBrowser = (url) => { sharePath: sharePath, receive: options.receive, clipboard: options.clipboard, - updateClipboardData: updateClipboardData, + updateClipboardData: (options.clipboard && !clipboardText) ? getClipboardData : undefined, onStart: onStart, postUploadRedirectUrl: uploadAddress, shareAddress: shareAddress, allowZip: allowZip, + clipboardText: clipboardText, + getClipboardData: clipboardText ? getClipboardData : undefined, }); })(); diff --git a/test/test.js b/test/test.js index 329f8a7..317f284 100644 --- a/test/test.js +++ b/test/test.js @@ -665,6 +665,33 @@ async function integrationTests() { }); }); + 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); + }); + }); + // Cleanup closeServers(); try { From e02a2d59d4e164db9b734b58b23d2f484e6f73f3 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:03:44 +0800 Subject: [PATCH 07/13] feat(text): send a text snippet from the phone to the terminal --- bin/app.js | 26 ++++++++++++++++++ bin/receive-form.html | 63 +++++++++++++++++++++++++++++++++++++++++++ test/test.js | 41 ++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/bin/app.js b/bin/app.js index 1e58890..d9467b2 100644 --- a/bin/app.js +++ b/bin/app.js @@ -51,6 +51,12 @@ const escapeHtml = (s) => .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 @@ -225,6 +231,26 @@ const start = ({ 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 diff --git a/bin/receive-form.html b/bin/receive-form.html index cbc2310..0a6d442 100644 --- a/bin/receive-form.html +++ b/bin/receive-form.html @@ -90,6 +90,39 @@ 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; + } @@ -104,6 +137,11 @@
Tap to choose, or drag & drop files here
View directory📁 + +
+ + +
@@ -188,6 +226,31 @@ uploadFiles(e.dataTransfer.files); } }); + + // Send a text snippet back to the terminal. + var textInput = document.getElementById("text-input"); + var sendText = document.getElementById("send-text"); + sendText.onclick = function () { + var text = textInput.value; + if (!text) return; + sendText.innerText = "Sending…"; + fetch("/text", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: text }) + }).then(function (r) { + if (r.ok) { + sendText.innerText = "Sent! ✅"; + textInput.value = ""; + } else { + sendText.innerText = "Failed ❌"; + } + setTimeout(function () { sendText.innerText = "Send text ✉️"; }, 1500); + }).catch(function () { + sendText.innerText = "Failed ❌"; + setTimeout(function () { sendText.innerText = "Send text ✉️"; }, 1500); + }); + }; diff --git a/test/test.js b/test/test.js index 317f284..f361e58 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'); @@ -165,6 +169,23 @@ function postMultipart(port, urlPath, parts) { }); } +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'); @@ -692,6 +713,26 @@ async function integrationTests() { }); }); + 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); + }); + }); + // Cleanup closeServers(); try { From 8adebc6eaf86c9a25d1a6a59741f2579e59eff97 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:04:55 +0800 Subject: [PATCH 08/13] feat(https): auto-generate a self-signed certificate for -S --- bin/index.js | 70 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/bin/index.js b/bin/index.js index d11cc47..89751a8 100755 --- a/bin/index.js +++ b/bin/index.js @@ -62,7 +62,7 @@ const openBrowser = (url) => { .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('tunnel', { describe: 'Show guide for sharing over the internet via tunnel services', type: 'boolean' }) @@ -122,25 +122,6 @@ const openBrowser = (url) => { 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)), - }, - }; - } - // 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. @@ -212,6 +193,51 @@ const openBrowser = (url) => { const interfaceCandidates = utils.getNetworkInterfaces(); const host = options.ip || utils.getNetworkAddress(options.interface); + + // HTTPS (-S): use the supplied cert/key when both are given, otherwise generate + // a self-signed certificate on the fly for the resolved host. + const wantHttps = options.ssl; + 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 } }; + } + } + const protocol = config.ssl.protocol; const baseUrl = protocol + '://' + host + ':' + options.port; @@ -258,6 +284,10 @@ const openBrowser = (url) => { } } + if (wantHttps && !usingProvidedCert) { + console.log('Using a self-signed HTTPS certificate; your browser shows a one-time warning — that is expected.'); + } + console.log('\nPress ctrl+c to stop sharing\n'); if (options.open) { From 4fc5a8e5b619465b7b3984008ee1b8a027b6da33 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:06:37 +0800 Subject: [PATCH 09/13] feat(ephemeral): --once and --timeout --- bin/app.js | 24 ++++++++++++++++++++++++ bin/index.js | 27 ++++++++++++++++++++++++++- bin/utils.js | 13 +++++++++++++ test/test.js | 10 ++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/bin/app.js b/bin/app.js index d9467b2..dd8fdfa 100644 --- a/bin/app.js +++ b/bin/app.js @@ -336,6 +336,10 @@ const start = ({ // 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), // and inject a "Download as .zip" link when zip support is enabled. Gated on @@ -346,6 +350,7 @@ const start = ({ 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 = '/zip?path=' + encodeURIComponent(listingDir); @@ -358,6 +363,25 @@ const start = ({ 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 diff --git a/bin/index.js b/bin/index.js index 89751a8..c628f8f 100755 --- a/bin/index.js +++ b/bin/index.js @@ -65,6 +65,8 @@ const openBrowser = (url) => { .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('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; @@ -246,6 +248,12 @@ const openBrowser = (url) => { const shareAddress = clipboardText ? (baseUrl + '/clipboard') : (baseUrl + '/share/' + 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 if (options.receive) { @@ -288,6 +296,13 @@ const openBrowser = (url) => { console.log('Using a self-signed HTTPS certificate; your browser shows a one-time warning — that is expected.'); } + 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) { @@ -295,7 +310,7 @@ const openBrowser = (url) => { } }; - app.start({ + const server = app.start({ port: options.port, sharePath: sharePath, receive: options.receive, @@ -307,5 +322,15 @@ const openBrowser = (url) => { 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/utils.js b/bin/utils.js index 9488bdb..9020fc8 100644 --- a/bin/utils.js +++ b/bin/utils.js @@ -56,6 +56,18 @@ const getNetworkAddress = (preferred) => { 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) => { if (config.debug) { console.log(log); @@ -66,5 +78,6 @@ module.exports = { getNetworkAddress, getNetworkInterfaces, scoreInterface, + parseDuration, debugLog, }; diff --git a/test/test.js b/test/test.js index f361e58..7aebf1d 100644 --- a/test/test.js +++ b/test/test.js @@ -97,6 +97,16 @@ 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'); From fc2115c999216bb20f29a55113e362e23d4e7dd1 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:10:05 +0800 Subject: [PATCH 10/13] feat(secure): capability-token share links and --secure preset --- bin/app.js | 30 ++++++++++++++++++++++-------- bin/index.js | 49 ++++++++++++++++++++++++++++++++++++++++--------- test/test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 17 deletions(-) diff --git a/bin/app.js b/bin/app.js index dd8fdfa..3524537 100644 --- a/bin/app.js +++ b/bin/app.js @@ -114,6 +114,7 @@ const start = ({ 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 @@ -122,6 +123,10 @@ const start = ({ } = {}) => { 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({ @@ -264,14 +269,18 @@ const start = ({ } return ''; }; - app.get(['/', '/clipboard'], (req, res) => { + // 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, '/clipboard.txt') + .replace(/\{downloadUrl\}/g, txtPath) ); }); - app.get('/clipboard.txt', (req, res) => { + app.get(txtPath, (req, res) => { res.on('finish', () => { if (res.statusCode === 200) finishOnce('download'); }); res.attachment('clipboard.txt'); res.type('text/plain').send(currentText()); @@ -279,8 +288,10 @@ const start = ({ } // 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('/zip', (req, res) => { + app.get(zipPrefix, (req, res) => { const root = path.resolve(sharePath); let target = root; if (req.query.path) { @@ -318,7 +329,7 @@ const start = ({ // 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('/share', (req, res) => { + if (!clipboardText) app.use(sharePrefix, (req, res) => { if (clipboard && updateClipboardData) { updateClipboardData(); } @@ -329,9 +340,12 @@ const start = ({ // (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]) || '/'); @@ -353,7 +367,7 @@ const start = ({ wasListing = true; body = fixListingLinks(body, mountPath); if (allowZip) { - const zipHref = '/zip?path=' + encodeURIComponent(listingDir); + const zipHref = zipPrefix + '?path=' + encodeURIComponent(listingDir); const bar = ''; diff --git a/bin/index.js b/bin/index.js index c628f8f..9ce5053 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,6 +32,9 @@ 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'); @@ -65,6 +69,8 @@ const openBrowser = (url) => { .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' }) @@ -115,11 +121,6 @@ const openBrowser = (url) => { 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; @@ -196,9 +197,9 @@ const openBrowser = (url) => { const interfaceCandidates = utils.getNetworkInterfaces(); const host = options.ip || utils.getNetworkAddress(options.interface); - // HTTPS (-S): use the supplied cert/key when both are given, otherwise generate - // a self-signed certificate on the fly for the resolved host. - const wantHttps = options.ssl; + // 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) { @@ -240,12 +241,32 @@ const openBrowser = (url) => { } } + // 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 = clipboardText ? (baseUrl + '/clipboard') : (baseUrl + '/share/' + file); + const shareAddress = clipboardText ? (baseUrl + clipboardPrefix) : (baseUrl + sharePrefix + '/' + file); const qrPageUrl = baseUrl + '/qr'; const timeoutMs = utils.parseDuration(options.timeout); @@ -292,9 +313,18 @@ const openBrowser = (url) => { } } + 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.'); + } if (timeoutMs) { console.log('\nThis share will stop automatically in ' + options.timeout + '.'); @@ -319,6 +349,7 @@ const openBrowser = (url) => { onStart: onStart, postUploadRedirectUrl: uploadAddress, shareAddress: shareAddress, + token: token, allowZip: allowZip, clipboardText: clipboardText, getClipboardData: clipboardText ? getClipboardData : undefined, diff --git a/test/test.js b/test/test.js index 7aebf1d..8f34448 100644 --- a/test/test.js +++ b/test/test.js @@ -743,6 +743,48 @@ async function integrationTests() { }); }); + 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 { From 03fd3c81bbce4e5356faf8fc8bb936dfb6a0b2fb Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:10:34 +0800 Subject: [PATCH 11/13] feat(network): startup hint for same-Wi-Fi / firewall connectivity --- bin/index.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bin/index.js b/bin/index.js index 9ce5053..29fba82 100755 --- a/bin/index.js +++ b/bin/index.js @@ -326,6 +326,10 @@ const openBrowser = (url) => { 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 + '.'); } From bf962b1d75642beb003de3120cff41df162cc640 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Wed, 17 Jun 2026 15:11:13 +0800 Subject: [PATCH 12/13] docs: rewrite README around the new capabilities; add ROADMAP --- README.md | 157 +++++++++++++++++++++++++++++++++++++++++++---------- ROADMAP.md | 115 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 243 insertions(+), 29 deletions(-) create mode 100644 ROADMAP.md 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/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..7967a8f --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,115 @@ +# easy-sharing — Product Roadmap + +> Instantly move files between your computer and your phone — zero setup, no app, just a QR code. + +## Where we are + +`easy-sharing` does one thing beautifully: run a command, scan a QR, and a folder, file, or your +clipboard is on your phone — over your own Wi-Fi, with no app to install on either side. It has +earned **1,834 GitHub stars** on that promise. + +Yet it sees only **~577 npm installs/year**. The idea resonates; daily reach-for-it adoption +hasn't followed. Our research (27 competing tools, this repo's issue/PR history, and community +threads) says the gap is **not missing breadth** — it's *reliability* on the first run and a few +*table-stakes* transfer capabilities. The fix is sharpening what we already are, not bolting on a +file manager. + +## The one insight that orders everything + +The magic moment is the **first scan**. When it fails, the user never comes back. The most common +failure across this entire category isn't a missing feature — it's **"the QR scans but the page +won't load."** It happens because the tool advertises the wrong network address (a Docker/VPN/WSL +virtual adapter). We have exactly this bug today: `getNetworkAddress()` returns the *first* +non-internal IPv4 it finds. **Reliability is therefore Priority Zero.** Everything else is built on +the assumption that the first scan always works. + +## Principles (what keeps this product *itself*) + +The maintainer's stated philosophy (issue #28) is our north star, and the research validates it: + +1. **Terminal-first.** The CLI is the product. The browser pages stay minimal — no SPA, no framework. +2. **Zero-setup, no app.** Our wedge vs. LocalSend/PairDrop/croc is "nothing to install on the phone." +3. **It just works.** We optimize the 10-second path: command → scan → done. +4. **Minimal dependencies.** Every new dep must earn its place. We are not a NAS web UI. +5. **Safe by default, without friction.** Security should be a default, not a chore. + +--- + +## The roadmap — five themes + +Each item shows **impact / effort** (S = hours, M = ~a day, L = a few days) and is grounded in +evidence (a competitor that has it and/or a request in our own tracker). + +### Theme 1 — "It just works": every scan reaches the page *(Reliability — Priority Zero)* + +| Bet | Impact / Effort | Why & evidence | Where it lives | +|---|---|---|---| +| **Smart network-interface auto-detect + `--interface` flag** | **High / S** | The #1 category failure ("QR scans, page unreachable"). Today we pick the *first* non-internal IPv4 → loses to Docker/VPN/WSL/Hyper-V NICs. qrcp's most-praised differentiator is exactly interface selection (#164, #196, #206). | `bin/utils.js`: rank candidates, prefer RFC-1918 LAN ranges, de-prioritize `docker/veth/vEthernet/wsl/utun/tun/tap`; add `getNetworkInterfaces()`. `bin/index.js`: add `--interface`; on ambiguity print the candidate list (non-interactive). `--ip` remains the override. | +| **Robust QR + browser-QR fallback** | **High / M** | Terminal QR is unscannable on Windows native terminals and with unicode/long paths (#17, #20, #46, #37). qrcp's `--open` sidesteps this. | `bin/index.js`: optional `--qr-image` / auto-open. `bin/app.js`: `GET /qr` serving a tiny page with the QR as an `` data URI — reuse the `receiveFormHtml` read+template+serve pattern. Keep terminal QR the default. | +| **Firewall pre-flight hint** | Med / M | Host firewall silently blocks the random port; users blame the tool. qrcp guides warn about this explicitly. | `bin/index.js` startup: detect likely-blocked bind and print a one-line, OS-aware hint. | + +### Theme 2 — "Move a pile, not a file": batch transfer, both directions *(Core capability)* + +| Bet | Impact / Effort | Why & evidence | Where it lives | +|---|---|---|---| +| **Multi-file (and folder) receive** — finish PR #45 | **High / M** | Our **oldest open request** (PR #45, untouched since 2023) and qrcp #131. Receiving a batch of phone photos is the dominant real-world receive job. | `bin/receive-form.html`: add `multiple` to the input, loop `this.files` into the *existing* XHR + progress bar. `bin/app.js`: normalize `req.files.selected` to an array (`express-fileupload` already returns an array for a repeated field) and `Promise.all(.mv())`. **Add a basename guard** (reject `../`) before `mv` — multi-file amplifies the existing path-traversal exposure. | +| **Download a shared folder as one `.zip`** | **High / M** | Universal across rivals (miniserve `--enable-zip`, dufs `--allow-archive`, qrcp `--zip`, woof). One tap to grab everything on a phone, vs. saving files one by one. | `bin/app.js`: new `GET /zip` route streaming via `archiver` (one focused, streaming dep — justified) with `Content-Disposition: attachment`; confine strictly to `sharePath` (traversal/symlink guard). Inject a "Download all as .zip" link by extending the existing `res.end` + `fixListingLinks` shim. Reuses basic-auth ordering for free. | + +### Theme 3 — "Close the loop": clipboard & text, both ways *(Delight)* + +| Bet | Impact / Effort | Why & evidence | Where it lives | +|---|---|---|---| +| **Clipboard share page with one-tap "Copy"** | Med / M | The job of `-c` is to land text on the *other* device's clipboard, yet today we only serve a `.clipboard-tmp` download. croc/KDE Connect/PairDrop all do one-tap copy. | New `bin/clipboard-page.html` (styled like the receive form), read once in `bin/app.js`; when `clipboard` is set, render it with the text injected (HTML-escaped) and a `navigator.clipboard.writeText` button + textarea fallback. Note: one-tap copy needs a secure context → pairs naturally with auto-HTTPS below. | +| **Send text/paste from the phone → terminal** | Med / S | A loved croc/KDE Connect/PairDrop pattern; we share clipboard *to* the phone but have no reverse path. | `bin/receive-form.html`: small textarea + Send (reuse the XHR pattern). `bin/app.js` inside the `receive` block: `POST /text` that prints it and optionally `clipboardy.writeSync()` (already a dep). Sanitize control chars before `console.log`. | + +### Theme 4 — "Safe without friction": trust by default *(Security)* + +| Bet | Impact / Effort | Why & evidence | Where it lives | +|---|---|---|---| +| **Auto-generated self-signed HTTPS** (one flag) | Med / S | Today `-S` requires a user-supplied cert/key. A `--https` that mints a self-signed cert removes the chore — and unlocks one-tap clipboard copy on LAN. | `bin/index.js` / `bin/config.js`: generate an in-memory cert when no `-C/-K` given. | +| **Ephemeral shares: `--once` and `--timeout`** | Med / S | We run until ctrl-C — no serve-once, no expiry. woof self-destructs after N downloads; qrcp exits after one transfer by default. | `bin/app.js`: close the server after first successful download / after a TTL. | +| **Capability-URL token + `--secure` preset** | Low–Med / S | Path is the predictable `/share/`; anyone on the Wi-Fi can browse. A random URL token (and a `--secure` flag bundling token + password + HTTPS) makes the share unguessable. Warn when binding `0.0.0.0`. | `bin/app.js`: mount under `/share//`; `bin/index.js`: `--secure` preset + a one-line open-by-default warning. | + +### Theme 5 — "Easy to reach for": adoption *(Growth)* + +| Bet | Impact / Effort | Why & evidence | Where it lives | +|---|---|---|---| +| **Promote `npx easy-sharing ` in the README** | Low / S — *quick win* | Removes the global-install commitment (and macOS EACCES pain) for the try-it-once user. Already works today via the `bin` map. | `README.md`: add a "Try without installing" snippet above `npm install -g`. Use `easy-sharing` (not `sharing`) to dodge the macOS `/usr/sbin/sharing` collision. | +| **Homebrew tap and/or single binary** | Med / M | Stars-vs-installs gap is partly install friction; rivals ship single binaries (qrcp, croc) and `brew`. | New release tooling (Node SEA / `pkg` / `bun compile`) on GitHub Releases; a Homebrew tap. No source changes to `bin/`. | + +--- + +## Sequenced phases (recommended order) + +- **Phase 1 — Priority Zero + quick win:** smart interface detect + `--interface`; promote `npx` in README. *Highest ROI; turns silent first-run failures into successes.* +- **Phase 2 — Core batch:** multi-file/folder receive (finish PR #45); folder-as-zip download. +- **Phase 3 — Robust QR + delight:** browser-QR fallback; clipboard copy page + reverse text. +- **Phase 4 — Trust:** auto self-signed HTTPS; `--once`/`--timeout`; capability-URL token + `--secure`. +- **Phase 5 — Growth:** Homebrew tap / single-binary distribution. + +## What we will **not** build (and why) + +Saying no protects the product. The research surfaced these; we decline them to stay terminal-first +and minimal: + +- **Full in-browser file manager** — delete / rename / mkdir / move (dufs, filebrowser). That's a NAS UI; we're a transfer tool. +- **In-browser previews / thumbnails, search, column sort, breadcrumbs** beyond the bare listing (Copyparty, filebrowser). Out of scope for "move it, don't manage it." +- **Cross-network P2P / relay** with codephrase pairing (croc, magic-wormhole). Different product; our wedge is LAN + browser, no phone app. We already point to tunnels via `--tunnel`. +- **Config files / named profiles**, resumable Range transfers, per-file checkbox-select zips. Complexity without matching demand for *this* tool. +- **Renaming the primary command.** Already mitigated by the `easy-sharing` alias; a rename churns existing users. + +--- + +## How we'll validate each shipped bet + +When bets are greenlit and built, validate end-to-end: + +1. **Existing tests:** `node test/test.js` (the repo's dependency-free suite) must stay green; extend it for new `bin/utils.js` interface-ranking logic and the `/zip`, `/text`, multi-file `/upload` routes. +2. **Reliability bet (manual, the important one):** on a machine with Docker/VPN active, run `sharing ` and confirm the QR points to the real LAN IP (not `172.x`/VPN); scan from a phone and confirm the page loads. Verify `--interface` override and the printed candidate list. +3. **Batch transfer:** receive several files + a folder in one go from a phone; confirm all land in `sharePath` with correct (unicode) names and that `../`-crafted names are rejected. Click "Download all as .zip" and verify the archive opens and matches the tree. +4. **Delight/trust:** `-c` shows text with a working Copy button (over `--https`); phone→terminal text prints/copies; `--once` exits after one download; `--timeout` auto-shuts; tokened URL is required. +5. **Cross-platform smoke:** Windows native terminal (QR fallback), macOS, Linux. + +--- + +*This roadmap is a proposal for review. Greenlight the bets you want and we'll scope implementation per phase.* From f2cf2ea15d3ebf7c488131e29ada85d792045b25 Mon Sep 17 00:00:00 2001 From: Amin Rostami Date: Thu, 18 Jun 2026 00:45:38 +0800 Subject: [PATCH 13/13] Bump version from 1.3.2 to 1.4.0 --- ROADMAP.md | 115 ---------------------------------------------- package-lock.json | 4 +- package.json | 2 +- 3 files changed, 3 insertions(+), 118 deletions(-) delete mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 7967a8f..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,115 +0,0 @@ -# easy-sharing — Product Roadmap - -> Instantly move files between your computer and your phone — zero setup, no app, just a QR code. - -## Where we are - -`easy-sharing` does one thing beautifully: run a command, scan a QR, and a folder, file, or your -clipboard is on your phone — over your own Wi-Fi, with no app to install on either side. It has -earned **1,834 GitHub stars** on that promise. - -Yet it sees only **~577 npm installs/year**. The idea resonates; daily reach-for-it adoption -hasn't followed. Our research (27 competing tools, this repo's issue/PR history, and community -threads) says the gap is **not missing breadth** — it's *reliability* on the first run and a few -*table-stakes* transfer capabilities. The fix is sharpening what we already are, not bolting on a -file manager. - -## The one insight that orders everything - -The magic moment is the **first scan**. When it fails, the user never comes back. The most common -failure across this entire category isn't a missing feature — it's **"the QR scans but the page -won't load."** It happens because the tool advertises the wrong network address (a Docker/VPN/WSL -virtual adapter). We have exactly this bug today: `getNetworkAddress()` returns the *first* -non-internal IPv4 it finds. **Reliability is therefore Priority Zero.** Everything else is built on -the assumption that the first scan always works. - -## Principles (what keeps this product *itself*) - -The maintainer's stated philosophy (issue #28) is our north star, and the research validates it: - -1. **Terminal-first.** The CLI is the product. The browser pages stay minimal — no SPA, no framework. -2. **Zero-setup, no app.** Our wedge vs. LocalSend/PairDrop/croc is "nothing to install on the phone." -3. **It just works.** We optimize the 10-second path: command → scan → done. -4. **Minimal dependencies.** Every new dep must earn its place. We are not a NAS web UI. -5. **Safe by default, without friction.** Security should be a default, not a chore. - ---- - -## The roadmap — five themes - -Each item shows **impact / effort** (S = hours, M = ~a day, L = a few days) and is grounded in -evidence (a competitor that has it and/or a request in our own tracker). - -### Theme 1 — "It just works": every scan reaches the page *(Reliability — Priority Zero)* - -| Bet | Impact / Effort | Why & evidence | Where it lives | -|---|---|---|---| -| **Smart network-interface auto-detect + `--interface` flag** | **High / S** | The #1 category failure ("QR scans, page unreachable"). Today we pick the *first* non-internal IPv4 → loses to Docker/VPN/WSL/Hyper-V NICs. qrcp's most-praised differentiator is exactly interface selection (#164, #196, #206). | `bin/utils.js`: rank candidates, prefer RFC-1918 LAN ranges, de-prioritize `docker/veth/vEthernet/wsl/utun/tun/tap`; add `getNetworkInterfaces()`. `bin/index.js`: add `--interface`; on ambiguity print the candidate list (non-interactive). `--ip` remains the override. | -| **Robust QR + browser-QR fallback** | **High / M** | Terminal QR is unscannable on Windows native terminals and with unicode/long paths (#17, #20, #46, #37). qrcp's `--open` sidesteps this. | `bin/index.js`: optional `--qr-image` / auto-open. `bin/app.js`: `GET /qr` serving a tiny page with the QR as an `` data URI — reuse the `receiveFormHtml` read+template+serve pattern. Keep terminal QR the default. | -| **Firewall pre-flight hint** | Med / M | Host firewall silently blocks the random port; users blame the tool. qrcp guides warn about this explicitly. | `bin/index.js` startup: detect likely-blocked bind and print a one-line, OS-aware hint. | - -### Theme 2 — "Move a pile, not a file": batch transfer, both directions *(Core capability)* - -| Bet | Impact / Effort | Why & evidence | Where it lives | -|---|---|---|---| -| **Multi-file (and folder) receive** — finish PR #45 | **High / M** | Our **oldest open request** (PR #45, untouched since 2023) and qrcp #131. Receiving a batch of phone photos is the dominant real-world receive job. | `bin/receive-form.html`: add `multiple` to the input, loop `this.files` into the *existing* XHR + progress bar. `bin/app.js`: normalize `req.files.selected` to an array (`express-fileupload` already returns an array for a repeated field) and `Promise.all(.mv())`. **Add a basename guard** (reject `../`) before `mv` — multi-file amplifies the existing path-traversal exposure. | -| **Download a shared folder as one `.zip`** | **High / M** | Universal across rivals (miniserve `--enable-zip`, dufs `--allow-archive`, qrcp `--zip`, woof). One tap to grab everything on a phone, vs. saving files one by one. | `bin/app.js`: new `GET /zip` route streaming via `archiver` (one focused, streaming dep — justified) with `Content-Disposition: attachment`; confine strictly to `sharePath` (traversal/symlink guard). Inject a "Download all as .zip" link by extending the existing `res.end` + `fixListingLinks` shim. Reuses basic-auth ordering for free. | - -### Theme 3 — "Close the loop": clipboard & text, both ways *(Delight)* - -| Bet | Impact / Effort | Why & evidence | Where it lives | -|---|---|---|---| -| **Clipboard share page with one-tap "Copy"** | Med / M | The job of `-c` is to land text on the *other* device's clipboard, yet today we only serve a `.clipboard-tmp` download. croc/KDE Connect/PairDrop all do one-tap copy. | New `bin/clipboard-page.html` (styled like the receive form), read once in `bin/app.js`; when `clipboard` is set, render it with the text injected (HTML-escaped) and a `navigator.clipboard.writeText` button + textarea fallback. Note: one-tap copy needs a secure context → pairs naturally with auto-HTTPS below. | -| **Send text/paste from the phone → terminal** | Med / S | A loved croc/KDE Connect/PairDrop pattern; we share clipboard *to* the phone but have no reverse path. | `bin/receive-form.html`: small textarea + Send (reuse the XHR pattern). `bin/app.js` inside the `receive` block: `POST /text` that prints it and optionally `clipboardy.writeSync()` (already a dep). Sanitize control chars before `console.log`. | - -### Theme 4 — "Safe without friction": trust by default *(Security)* - -| Bet | Impact / Effort | Why & evidence | Where it lives | -|---|---|---|---| -| **Auto-generated self-signed HTTPS** (one flag) | Med / S | Today `-S` requires a user-supplied cert/key. A `--https` that mints a self-signed cert removes the chore — and unlocks one-tap clipboard copy on LAN. | `bin/index.js` / `bin/config.js`: generate an in-memory cert when no `-C/-K` given. | -| **Ephemeral shares: `--once` and `--timeout`** | Med / S | We run until ctrl-C — no serve-once, no expiry. woof self-destructs after N downloads; qrcp exits after one transfer by default. | `bin/app.js`: close the server after first successful download / after a TTL. | -| **Capability-URL token + `--secure` preset** | Low–Med / S | Path is the predictable `/share/`; anyone on the Wi-Fi can browse. A random URL token (and a `--secure` flag bundling token + password + HTTPS) makes the share unguessable. Warn when binding `0.0.0.0`. | `bin/app.js`: mount under `/share//`; `bin/index.js`: `--secure` preset + a one-line open-by-default warning. | - -### Theme 5 — "Easy to reach for": adoption *(Growth)* - -| Bet | Impact / Effort | Why & evidence | Where it lives | -|---|---|---|---| -| **Promote `npx easy-sharing ` in the README** | Low / S — *quick win* | Removes the global-install commitment (and macOS EACCES pain) for the try-it-once user. Already works today via the `bin` map. | `README.md`: add a "Try without installing" snippet above `npm install -g`. Use `easy-sharing` (not `sharing`) to dodge the macOS `/usr/sbin/sharing` collision. | -| **Homebrew tap and/or single binary** | Med / M | Stars-vs-installs gap is partly install friction; rivals ship single binaries (qrcp, croc) and `brew`. | New release tooling (Node SEA / `pkg` / `bun compile`) on GitHub Releases; a Homebrew tap. No source changes to `bin/`. | - ---- - -## Sequenced phases (recommended order) - -- **Phase 1 — Priority Zero + quick win:** smart interface detect + `--interface`; promote `npx` in README. *Highest ROI; turns silent first-run failures into successes.* -- **Phase 2 — Core batch:** multi-file/folder receive (finish PR #45); folder-as-zip download. -- **Phase 3 — Robust QR + delight:** browser-QR fallback; clipboard copy page + reverse text. -- **Phase 4 — Trust:** auto self-signed HTTPS; `--once`/`--timeout`; capability-URL token + `--secure`. -- **Phase 5 — Growth:** Homebrew tap / single-binary distribution. - -## What we will **not** build (and why) - -Saying no protects the product. The research surfaced these; we decline them to stay terminal-first -and minimal: - -- **Full in-browser file manager** — delete / rename / mkdir / move (dufs, filebrowser). That's a NAS UI; we're a transfer tool. -- **In-browser previews / thumbnails, search, column sort, breadcrumbs** beyond the bare listing (Copyparty, filebrowser). Out of scope for "move it, don't manage it." -- **Cross-network P2P / relay** with codephrase pairing (croc, magic-wormhole). Different product; our wedge is LAN + browser, no phone app. We already point to tunnels via `--tunnel`. -- **Config files / named profiles**, resumable Range transfers, per-file checkbox-select zips. Complexity without matching demand for *this* tool. -- **Renaming the primary command.** Already mitigated by the `easy-sharing` alias; a rename churns existing users. - ---- - -## How we'll validate each shipped bet - -When bets are greenlit and built, validate end-to-end: - -1. **Existing tests:** `node test/test.js` (the repo's dependency-free suite) must stay green; extend it for new `bin/utils.js` interface-ranking logic and the `/zip`, `/text`, multi-file `/upload` routes. -2. **Reliability bet (manual, the important one):** on a machine with Docker/VPN active, run `sharing ` and confirm the QR points to the real LAN IP (not `172.x`/VPN); scan from a phone and confirm the page loads. Verify `--interface` override and the printed candidate list. -3. **Batch transfer:** receive several files + a folder in one go from a phone; confirm all land in `sharePath` with correct (unicode) names and that `../`-crafted names are rejected. Click "Download all as .zip" and verify the archive opens and matches the tree. -4. **Delight/trust:** `-c` shows text with a working Copy button (over `--https`); phone→terminal text prints/copies; `--once` exits after one download; `--timeout` auto-shuts; tokened URL is required. -5. **Cross-platform smoke:** Windows native terminal (QR fallback), macOS, Linux. - ---- - -*This roadmap is a proposal for review. Greenlight the bets you want and we'll scope implementation per phase.* diff --git a/package-lock.json b/package-lock.json index 834d3d9..fdebd01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "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", diff --git a/package.json b/package.json index 4ab44f7..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": {