1882919c9260
remove all Android/Capacitor code, native sync host, and mobile-only dead code — browser extension only (v2.30.0)
- time
- 2026-05-04 23:44 UTC · 5 months ago
- machine
- koh2
- parent
- cba62ca5b081
↓ peach-browser-1882919c.tar.gzatlas at this save
files changed18 files
manifest.firefox.json+1 −1manifest.json+1 −1package-lock.json+30 −753package.json+3 −1src/popup/styles/popup.css+2 −245src/shared/android-sync-identity.ts+0 −90src/shared/biometric.ts+0 −22src/shared/bridge.ts+1 −15src/shared/capacitor-pairing-handshake.ts+0 −96src/shared/capacitor-pairing-server.ts+0 −332src/shared/chrome-mobile.ts+0 −60src/shared/chrome-shim.ts+0 −147src/shared/chrome.ts+0 −61src/shared/fold.ts+0 −51src/shared/in-memory-vault.ts+0 −32src/shared/storage.ts+5 −24src/shared/sync-bridge.test.ts+0 −44src/shared/sync-bridge.ts+0 −172manifest.firefox.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.29.3",4+ "version": "2.30.0",55 "description": "Zero-knowledge password manager",66 "browser_specific_settings": {77 "gecko": {manifest.json+1 −1
@@ -1,7 +1,7 @@11 {22 "manifest_version": 3,33 "name": "Peach",4- "version": "2.29.3",4+ "version": "2.30.0",55 "description": "Zero-knowledge password manager",66 "action": {77 "default_popup": "src/popup/index.html",package-lock.json+30 −753
@@ -1,22 +1,14 @@11 {22 "name": "peach",3- "version": "2.14.4",3+ "version": "2.29.3",44 "lockfileVersion": 3,55 "requires": true,66 "packages": {77 "": {88 "name": "peach",9- "version": "2.14.4",9+ "version": "2.29.3",1010 "dependencies": {1111 "@bokuweb/zstd-wasm": "^0.0.27",12- "@capacitor/android": "^8.3.1",13- "@capacitor/app": "^8.1.0",14- "@capacitor/cli": "^8.3.1",15- "@capacitor/core": "^8.3.1",16- "@capacitor/haptics": "^8.0.2",17- "@capacitor/keyboard": "^8.0.3",18- "@capacitor/preferences": "^8.0.1",19- "@capacitor/status-bar": "^8.0.2",2012 "@noble/ciphers": "^2.2.0",2113 "@noble/curves": "^2.2.0",2214 "@noble/hashes": "^2.2.0",@@ -27,6 +19,7 @@2719 "@stablelib/random": "^2.0.1",2820 "@stablelib/sha256": "^2.0.1",2921 "@stablelib/x25519": "^2.0.1",22+ "@types/qrcode": "^1.5.6",3023 "eff-diceware-passphrase": "^3.0.0",3124 "hash-wasm": "^4.12.0",3225 "onnxruntime-web": "^1.24.3",@@ -43,7 +36,6 @@4336 "@types/chrome": "^0.1.40",4437 "@types/node": "^22.0.0",4538 "@types/papaparse": "^5.5.2",46- "@types/qrcode": "^1.5.6",4739 "@types/zxcvbn": "^4.4.5",4840 "@vitest/ui": "^4.1.4",4941 "jsdom": "^29.0.2",@@ -407,234 +399,6 @@407399 "specificity": "bin/cli.js"408400 }409401 },410- "node_modules/@capacitor/android": {411- "version": "8.3.1",412- "resolved": "https://registry.npmjs.org/@capacitor/android/-/android-8.3.1.tgz",413- "integrity": "sha512-hjskIG8YcBEh3X4yaTXvE9gcqpdcxunTgFruSKnuPxtMxAUzEK4Oq25x0Z1g3cz+MQPc+lRG09R7Ovc+ydKsNw==",414- "license": "MIT",415- "peerDependencies": {416- "@capacitor/core": "^8.3.0"417- }418- },419- "node_modules/@capacitor/app": {420- "version": "8.1.0",421- "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-8.1.0.tgz",422- "integrity": "sha512-MlmttTOWHDedr/G4SrhNRxsXMqY+R75S4MM4eIgzsgCzOYhb/MpCkA5Q3nuOCfL1oHm26xjUzqZ5aupbOwdfYg==",423- "license": "MIT",424- "peerDependencies": {425- "@capacitor/core": ">=8.0.0"426- }427- },428- "node_modules/@capacitor/cli": {429- "version": "8.3.1",430- "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.3.1.tgz",431- "integrity": "sha512-1sPGW4THTDfR6YjXwZ0jM7oAfAtciPOHN00qs/3sNAQx1kKrrEYSfDPwCm1/xlAgi0OeL69SiRfw314Ans+1sw==",432- "license": "MIT",433- "dependencies": {434- "@ionic/cli-framework-output": "^2.2.8",435- "@ionic/utils-subprocess": "^3.0.1",436- "@ionic/utils-terminal": "^2.3.5",437- "commander": "^12.1.0",438- "debug": "^4.4.0",439- "env-paths": "^2.2.0",440- "fs-extra": "^11.2.0",441- "kleur": "^4.1.5",442- "native-run": "^2.0.3",443- "open": "^8.4.0",444- "plist": "^3.1.0",445- "prompts": "^2.4.2",446- "rimraf": "^6.0.1",447- "semver": "^7.6.3",448- "tar": "^7.5.3",449- "tslib": "^2.8.1",450- "xml2js": "^0.6.2"451- },452- "bin": {453- "cap": "bin/capacitor",454- "capacitor": "bin/capacitor"455- },456- "engines": {457- "node": ">=22.0.0"458- }459- },460- "node_modules/@capacitor/cli/node_modules/balanced-match": {461- "version": "4.0.4",462- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",463- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",464- "license": "MIT",465- "engines": {466- "node": "18 || 20 || >=22"467- }468- },469- "node_modules/@capacitor/cli/node_modules/brace-expansion": {470- "version": "5.0.5",471- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",472- "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",473- "license": "MIT",474- "dependencies": {475- "balanced-match": "^4.0.2"476- },477- "engines": {478- "node": "18 || 20 || >=22"479- }480- },481- "node_modules/@capacitor/cli/node_modules/commander": {482- "version": "12.1.0",483- "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",484- "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",485- "license": "MIT",486- "engines": {487- "node": ">=18"488- }489- },490- "node_modules/@capacitor/cli/node_modules/define-lazy-prop": {491- "version": "2.0.0",492- "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",493- "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",494- "license": "MIT",495- "engines": {496- "node": ">=8"497- }498- },499- "node_modules/@capacitor/cli/node_modules/fs-extra": {500- "version": "11.3.4",501- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz",502- "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==",503- "license": "MIT",504- "dependencies": {505- "graceful-fs": "^4.2.0",506- "jsonfile": "^6.0.1",507- "universalify": "^2.0.0"508- },509- "engines": {510- "node": ">=14.14"511- }512- },513- "node_modules/@capacitor/cli/node_modules/glob": {514- "version": "13.0.6",515- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",516- "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",517- "license": "BlueOak-1.0.0",518- "dependencies": {519- "minimatch": "^10.2.2",520- "minipass": "^7.1.3",521- "path-scurry": "^2.0.2"522- },523- "engines": {524- "node": "18 || 20 || >=22"525- },526- "funding": {527- "url": "https://github.com/sponsors/isaacs"528- }529- },530- "node_modules/@capacitor/cli/node_modules/minimatch": {531- "version": "10.2.5",532- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",533- "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",534- "license": "BlueOak-1.0.0",535- "dependencies": {536- "brace-expansion": "^5.0.5"537- },538- "engines": {539- "node": "18 || 20 || >=22"540- },541- "funding": {542- "url": "https://github.com/sponsors/isaacs"543- }544- },545- "node_modules/@capacitor/cli/node_modules/open": {546- "version": "8.4.2",547- "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",548- "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",549- "license": "MIT",550- "dependencies": {551- "define-lazy-prop": "^2.0.0",552- "is-docker": "^2.1.1",553- "is-wsl": "^2.2.0"554- },555- "engines": {556- "node": ">=12"557- },558- "funding": {559- "url": "https://github.com/sponsors/sindresorhus"560- }561- },562- "node_modules/@capacitor/cli/node_modules/rimraf": {563- "version": "6.1.3",564- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",565- "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",566- "license": "BlueOak-1.0.0",567- "dependencies": {568- "glob": "^13.0.3",569- "package-json-from-dist": "^1.0.1"570- },571- "bin": {572- "rimraf": "dist/esm/bin.mjs"573- },574- "engines": {575- "node": "20 || >=22"576- },577- "funding": {578- "url": "https://github.com/sponsors/isaacs"579- }580- },581- "node_modules/@capacitor/cli/node_modules/semver": {582- "version": "7.7.4",583- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",584- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",585- "license": "ISC",586- "bin": {587- "semver": "bin/semver.js"588- },589- "engines": {590- "node": ">=10"591- }592- },593- "node_modules/@capacitor/core": {594- "version": "8.3.1",595- "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.3.1.tgz",596- "integrity": "sha512-UF8ItlHguU1Z6GXfPTeT2gakf+ctNI8pAS1kwSBQlsJMlfD4OPoto/SmKnOxKCQvnF4WRcdWeg6C0zREUNaAQg==",597- "license": "MIT",598- "dependencies": {599- "tslib": "^2.1.0"600- }601- },602- "node_modules/@capacitor/haptics": {603- "version": "8.0.2",604- "resolved": "https://registry.npmjs.org/@capacitor/haptics/-/haptics-8.0.2.tgz",605- "integrity": "sha512-c2hZzRR5Fk1tbTvhG1jhh2XBAf3EhnIerMIb2sl7Mt41Gxx1fhBJFDa0/BI1IbY4loVepyyuqNC9820/GZuoWQ==",606- "license": "MIT",607- "peerDependencies": {608- "@capacitor/core": ">=8.0.0"609- }610- },611- "node_modules/@capacitor/keyboard": {612- "version": "8.0.3",613- "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-8.0.3.tgz",614- "integrity": "sha512-27Bv5/2w1Ss2njguBgTS98O0Bb8DRJhAARyzXYib0JlT/n6BrJw/EZ0CokM4C8GFUjFDjJnEKF1Ie01buTMEXQ==",615- "license": "MIT",616- "peerDependencies": {617- "@capacitor/core": ">=8.0.0"618- }619- },620- "node_modules/@capacitor/preferences": {621- "version": "8.0.1",622- "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-8.0.1.tgz",623- "integrity": "sha512-T6no3ebi79XJCk91U3Jp/liJUwgBdvHR+s6vhvPkPxSuch7z3zx5Rv1bdWM6sWruNx+pViuEGqZvbfCdyBvcHQ==",624- "license": "MIT",625- "peerDependencies": {626- "@capacitor/core": ">=8.0.0"627- }628- },629- "node_modules/@capacitor/status-bar": {630- "version": "8.0.2",631- "resolved": "https://registry.npmjs.org/@capacitor/status-bar/-/status-bar-8.0.2.tgz",632- "integrity": "sha512-WXs8YB8B9eEaPZz+bcdY6t2nForF1FLoj/JU0Dl9RRgQnddnS98FEEyDooQhaY7wivr000j4+SC1FyeJkrFO7A==",633- "license": "MIT",634- "peerDependencies": {635- "@capacitor/core": ">=8.0.0"636- }637- },638402 "node_modules/@crxjs/vite-plugin": {639403 "version": "2.4.0",640404 "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.4.0.tgz",@@ -1118,174 +882,6 @@1118882 "dev": true,1119883 "license": "BSD-3-Clause"1120884 },1121- "node_modules/@ionic/cli-framework-output": {1122- "version": "2.2.8",1123- "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",1124- "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==",1125- "license": "MIT",1126- "dependencies": {1127- "@ionic/utils-terminal": "2.3.5",1128- "debug": "^4.0.0",1129- "tslib": "^2.0.1"1130- },1131- "engines": {1132- "node": ">=16.0.0"1133- }1134- },1135- "node_modules/@ionic/utils-array": {1136- "version": "2.1.6",1137- "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",1138- "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==",1139- "license": "MIT",1140- "dependencies": {1141- "debug": "^4.0.0",1142- "tslib": "^2.0.1"1143- },1144- "engines": {1145- "node": ">=16.0.0"1146- }1147- },1148- "node_modules/@ionic/utils-fs": {1149- "version": "3.1.7",1150- "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz",1151- "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==",1152- "license": "MIT",1153- "dependencies": {1154- "@types/fs-extra": "^8.0.0",1155- "debug": "^4.0.0",1156- "fs-extra": "^9.0.0",1157- "tslib": "^2.0.1"1158- },1159- "engines": {1160- "node": ">=16.0.0"1161- }1162- },1163- "node_modules/@ionic/utils-fs/node_modules/fs-extra": {1164- "version": "9.1.0",1165- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",1166- "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",1167- "license": "MIT",1168- "dependencies": {1169- "at-least-node": "^1.0.0",1170- "graceful-fs": "^4.2.0",1171- "jsonfile": "^6.0.1",1172- "universalify": "^2.0.0"1173- },1174- "engines": {1175- "node": ">=10"1176- }1177- },1178- "node_modules/@ionic/utils-object": {1179- "version": "2.1.6",1180- "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz",1181- "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==",1182- "license": "MIT",1183- "dependencies": {1184- "debug": "^4.0.0",1185- "tslib": "^2.0.1"1186- },1187- "engines": {1188- "node": ">=16.0.0"1189- }1190- },1191- "node_modules/@ionic/utils-process": {1192- "version": "2.1.12",1193- "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz",1194- "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==",1195- "license": "MIT",1196- "dependencies": {1197- "@ionic/utils-object": "2.1.6",1198- "@ionic/utils-terminal": "2.3.5",1199- "debug": "^4.0.0",1200- "signal-exit": "^3.0.3",1201- "tree-kill": "^1.2.2",1202- "tslib": "^2.0.1"1203- },1204- "engines": {1205- "node": ">=16.0.0"1206- }1207- },1208- "node_modules/@ionic/utils-stream": {1209- "version": "3.1.7",1210- "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz",1211- "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==",1212- "license": "MIT",1213- "dependencies": {1214- "debug": "^4.0.0",1215- "tslib": "^2.0.1"1216- },1217- "engines": {1218- "node": ">=16.0.0"1219- }1220- },1221- "node_modules/@ionic/utils-subprocess": {1222- "version": "3.0.1",1223- "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz",1224- "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==",1225- "license": "MIT",1226- "dependencies": {1227- "@ionic/utils-array": "2.1.6",1228- "@ionic/utils-fs": "3.1.7",1229- "@ionic/utils-process": "2.1.12",1230- "@ionic/utils-stream": "3.1.7",1231- "@ionic/utils-terminal": "2.3.5",1232- "cross-spawn": "^7.0.3",1233- "debug": "^4.0.0",1234- "tslib": "^2.0.1"1235- },1236- "engines": {1237- "node": ">=16.0.0"1238- }1239- },1240- "node_modules/@ionic/utils-terminal": {1241- "version": "2.3.5",1242- "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz",1243- "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==",1244- "license": "MIT",1245- "dependencies": {1246- "@types/slice-ansi": "^4.0.0",1247- "debug": "^4.0.0",1248- "signal-exit": "^3.0.3",1249- "slice-ansi": "^4.0.0",1250- "string-width": "^4.1.0",1251- "strip-ansi": "^6.0.0",1252- "tslib": "^2.0.1",1253- "untildify": "^4.0.0",1254- "wrap-ansi": "^7.0.0"1255- },1256- "engines": {1257- "node": ">=16.0.0"1258- }1259- },1260- "node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": {1261- "version": "7.0.0",1262- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",1263- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",1264- "license": "MIT",1265- "dependencies": {1266- "ansi-styles": "^4.0.0",1267- "string-width": "^4.1.0",1268- "strip-ansi": "^6.0.0"1269- },1270- "engines": {1271- "node": ">=10"1272- },1273- "funding": {1274- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"1275- }1276- },1277- "node_modules/@isaacs/fs-minipass": {1278- "version": "4.0.1",1279- "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",1280- "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",1281- "license": "ISC",1282- "dependencies": {1283- "minipass": "^7.0.4"1284- },1285- "engines": {1286- "node": ">=18.0.0"1287- }1288- },1289885 "node_modules/@jridgewell/gen-mapping": {1290886 "version": "0.3.13",1291887 "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",@@ -2128,15 +1724,6 @@21281724 "dev": true,21291725 "license": "MIT"21301726 },2131- "node_modules/@types/fs-extra": {2132- "version": "8.1.5",2133- "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",2134- "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",2135- "license": "MIT",2136- "dependencies": {2137- "@types/node": "*"2138- }2139- },21401727 "node_modules/@types/har-format": {21411728 "version": "1.2.16",21421729 "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz",@@ -2174,18 +1761,11 @@21741761 "version": "1.5.6",21751762 "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz",21761763 "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==",2177- "dev": true,21781764 "license": "MIT",21791765 "dependencies": {21801766 "@types/node": "*"21811767 }21821768 },2183- "node_modules/@types/slice-ansi": {2184- "version": "4.0.0",2185- "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",2186- "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",2187- "license": "MIT"2188- },21891769 "node_modules/@types/yauzl": {21901770 "version": "2.10.3",21911771 "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",@@ -2369,15 +1949,6 @@23691949 "dev": true,23701950 "license": "BSD-3-Clause"23711951 },2372- "node_modules/@xmldom/xmldom": {2373- "version": "0.9.10",2374- "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz",2375- "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==",2376- "license": "MIT",2377- "engines": {2378- "node": ">=14.6"2379- }2380- },23811952 "node_modules/acorn": {23821953 "version": "8.16.0",23831954 "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",@@ -2695,15 +2266,6 @@26952266 "node": ">=12"26962267 }26972268 },2698- "node_modules/astral-regex": {2699- "version": "2.0.0",2700- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",2701- "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",2702- "license": "MIT",2703- "engines": {2704- "node": ">=8"2705- }2706- },27072269 "node_modules/async": {27082270 "version": "3.2.6",27092271 "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",@@ -2711,15 +2273,6 @@27112273 "dev": true,27122274 "license": "MIT"27132275 },2714- "node_modules/at-least-node": {2715- "version": "1.0.0",2716- "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",2717- "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",2718- "license": "ISC",2719- "engines": {2720- "node": ">= 4.0.0"2721- }2722- },27232276 "node_modules/atomic-sleep": {27242277 "version": "1.0.0",27252278 "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",@@ -2797,26 +2350,6 @@27972350 "dev": true,27982351 "license": "MIT"27992352 },2800- "node_modules/base64-js": {2801- "version": "1.5.1",2802- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",2803- "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",2804- "funding": [2805- {2806- "type": "github",2807- "url": "https://github.com/sponsors/feross"2808- },2809- {2810- "type": "patreon",2811- "url": "https://www.patreon.com/feross"2812- },2813- {2814- "type": "consulting",2815- "url": "https://feross.org/support"2816- }2817- ],2818- "license": "MIT"2819- },28202353 "node_modules/baseline-browser-mapping": {28212354 "version": "2.10.19",28222355 "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz",@@ -2840,15 +2373,6 @@28402373 "require-from-string": "^2.0.2"28412374 }28422375 },2843- "node_modules/big-integer": {2844- "version": "1.6.52",2845- "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",2846- "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",2847- "license": "Unlicense",2848- "engines": {2849- "node": ">=0.6"2850- }2851- },28522376 "node_modules/binary-search-bounds": {28532377 "version": "2.0.5",28542378 "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz",@@ -3016,18 +2540,6 @@30162540 "url": "https://github.com/chalk/wrap-ansi?sponsor=1"30172541 }30182542 },3019- "node_modules/bplist-parser": {3020- "version": "0.3.2",3021- "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",3022- "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",3023- "license": "MIT",3024- "dependencies": {3025- "big-integer": "1.6.x"3026- },3027- "engines": {3028- "node": ">= 5.10.0"3029- }3030- },30312543 "node_modules/brace-expansion": {30322544 "version": "1.1.14",30332545 "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",@@ -3090,6 +2602,7 @@30902602 "version": "0.2.13",30912603 "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",30922604 "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",2605+ "dev": true,30932606 "license": "MIT",30942607 "engines": {30952608 "node": "*"@@ -3225,15 +2738,6 @@32252738 "url": "https://github.com/sponsors/fb55"32262739 }32272740 },3228- "node_modules/chownr": {3229- "version": "3.0.0",3230- "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",3231- "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",3232- "license": "BlueOak-1.0.0",3233- "engines": {3234- "node": ">=18"3235- }3236- },32372741 "node_modules/chrome-launcher": {32382742 "version": "1.2.0",32392743 "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.0.tgz",@@ -3417,6 +2921,7 @@34172921 "version": "7.0.6",34182922 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",34192923 "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",2924+ "dev": true,34202925 "license": "MIT",34212926 "dependencies": {34222927 "path-key": "^3.1.0",@@ -3504,6 +3009,7 @@35043009 "version": "4.4.3",35053010 "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",35063011 "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",3012+ "dev": true,35073013 "license": "MIT",35083014 "dependencies": {35093015 "ms": "^2.1.3"@@ -3752,24 +3258,6 @@37523258 "dev": true,37533259 "license": "ISC"37543260 },3755- "node_modules/elementtree": {3756- "version": "0.1.7",3757- "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz",3758- "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==",3759- "license": "Apache-2.0",3760- "dependencies": {3761- "sax": "1.1.4"3762- },3763- "engines": {3764- "node": ">= 0.4.0"3765- }3766- },3767- "node_modules/elementtree/node_modules/sax": {3768- "version": "1.1.4",3769- "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz",3770- "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==",3771- "license": "ISC"3772- },37733261 "node_modules/emoji-regex": {37743262 "version": "8.0.0",37753263 "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",@@ -3789,15 +3277,6 @@37893277 "url": "https://github.com/fb55/entities?sponsor=1"37903278 }37913279 },3792- "node_modules/env-paths": {3793- "version": "2.2.1",3794- "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",3795- "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",3796- "license": "MIT",3797- "engines": {3798- "node": ">=6"3799- }3800- },38013280 "node_modules/error-ex": {38023281 "version": "1.3.4",38033282 "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",@@ -4269,6 +3748,7 @@42693748 "version": "1.1.0",42703749 "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",42713750 "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",3751+ "dev": true,42723752 "license": "MIT",42733753 "dependencies": {42743754 "pend": "~1.2.0"@@ -4602,6 +4082,7 @@46024082 "version": "4.2.11",46034083 "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",46044084 "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",4085+ "dev": true,46054086 "license": "ISC"46064087 },46074088 "node_modules/graceful-readlink": {@@ -4784,12 +4265,14 @@47844265 "version": "2.0.4",47854266 "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",47864267 "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",4268+ "dev": true,47874269 "license": "ISC"47884270 },47894271 "node_modules/ini": {47904272 "version": "4.1.3",47914273 "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",47924274 "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",4275+ "dev": true,47934276 "license": "ISC",47944277 "engines": {47954278 "node": "^14.17.0 || ^16.13.0 || >=18.0.0"@@ -4819,6 +4302,7 @@48194302 "version": "2.2.1",48204303 "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",48214304 "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",4305+ "dev": true,48224306 "license": "MIT",48234307 "bin": {48244308 "is-docker": "cli.js"@@ -5016,6 +4500,7 @@50164500 "version": "2.2.0",50174501 "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",50184502 "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",4503+ "dev": true,50194504 "license": "MIT",50204505 "dependencies": {50214506 "is-docker": "^2.0.0"@@ -5035,6 +4520,7 @@50354520 "version": "2.0.0",50364521 "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",50374522 "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",4523+ "dev": true,50384524 "license": "ISC"50394525 },50404526 "node_modules/jose": {@@ -5215,6 +4701,7 @@52154701 "version": "6.2.0",52164702 "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",52174703 "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",4704+ "dev": true,52184705 "license": "MIT",52194706 "dependencies": {52204707 "universalify": "^2.0.0"@@ -5246,15 +4733,6 @@52464733 "json-buffer": "3.0.1"52474734 }52484735 },5249- "node_modules/kleur": {5250- "version": "4.1.5",5251- "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",5252- "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",5253- "license": "MIT",5254- "engines": {5255- "node": ">=6"5256- }5257- },52584736 "node_modules/ky": {52594737 "version": "1.14.3",52604738 "resolved": "https://registry.npmjs.org/ky/-/ky-1.14.3.tgz",@@ -5719,27 +5197,6 @@57195197 "url": "https://github.com/sponsors/ljharb"57205198 }57215199 },5722- "node_modules/minipass": {5723- "version": "7.1.3",5724- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",5725- "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",5726- "license": "BlueOak-1.0.0",5727- "engines": {5728- "node": ">=16 || 14 >=14.17"5729- }5730- },5731- "node_modules/minizlib": {5732- "version": "3.1.0",5733- "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",5734- "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",5735- "license": "MIT",5736- "dependencies": {5737- "minipass": "^7.1.2"5738- },5739- "engines": {5740- "node": ">= 18"5741- }5742- },57435200 "node_modules/mrmime": {57445201 "version": "2.0.1",57455202 "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",@@ -5754,6 +5211,7 @@57545211 "version": "2.1.3",57555212 "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",57565213 "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",5214+ "dev": true,57575215 "license": "MIT"57585216 },57595217 "node_modules/multimatch": {@@ -5800,31 +5258,6 @@58005258 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"58015259 }58025260 },5803- "node_modules/native-run": {5804- "version": "2.0.3",5805- "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz",5806- "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==",5807- "license": "MIT",5808- "dependencies": {5809- "@ionic/utils-fs": "^3.1.7",5810- "@ionic/utils-terminal": "^2.3.4",5811- "bplist-parser": "^0.3.2",5812- "debug": "^4.3.4",5813- "elementtree": "^0.1.7",5814- "ini": "^4.1.1",5815- "plist": "^3.1.0",5816- "split2": "^4.2.0",5817- "through2": "^4.0.2",5818- "tslib": "^2.6.2",5819- "yauzl": "^2.10.0"5820- },5821- "bin": {5822- "native-run": "bin/native-run"5823- },5824- "engines": {5825- "node": ">=16.0.0"5826- }5827- },58285261 "node_modules/natural-compare": {58295262 "version": "1.4.0",58305263 "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",@@ -6088,12 +5521,6 @@60885521 "url": "https://github.com/sponsors/sindresorhus"60895522 }60905523 },6091- "node_modules/package-json-from-dist": {6092- "version": "1.0.1",6093- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",6094- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",6095- "license": "BlueOak-1.0.0"6096- },60975524 "node_modules/package-json/node_modules/semver": {60985525 "version": "7.7.4",60995526 "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",@@ -6229,36 +5656,12 @@62295656 "version": "3.1.1",62305657 "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",62315658 "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",5659+ "dev": true,62325660 "license": "MIT",62335661 "engines": {62345662 "node": ">=8"62355663 }62365664 },6237- "node_modules/path-scurry": {6238- "version": "2.0.2",6239- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",6240- "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",6241- "license": "BlueOak-1.0.0",6242- "dependencies": {6243- "lru-cache": "^11.0.0",6244- "minipass": "^7.1.2"6245- },6246- "engines": {6247- "node": "18 || 20 || >=22"6248- },6249- "funding": {6250- "url": "https://github.com/sponsors/isaacs"6251- }6252- },6253- "node_modules/path-scurry/node_modules/lru-cache": {6254- "version": "11.3.5",6255- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",6256- "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",6257- "license": "BlueOak-1.0.0",6258- "engines": {6259- "node": "20 || >=22"6260- }6261- },62625665 "node_modules/pathe": {62635666 "version": "2.0.3",62645667 "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",@@ -6270,6 +5673,7 @@62705673 "version": "1.2.0",62715674 "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",62725675 "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",5676+ "dev": true,62735677 "license": "MIT"62745678 },62755679 "node_modules/picocolors": {@@ -6385,29 +5789,6 @@63855789 "node": "^8.16.0 || ^10.6.0 || >=11.0.0"63865790 }63875791 },6388- "node_modules/plist": {6389- "version": "3.1.1",6390- "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",6391- "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==",6392- "license": "MIT",6393- "dependencies": {6394- "@xmldom/xmldom": "^0.9.10",6395- "base64-js": "^1.5.1",6396- "xmlbuilder": "^15.1.1"6397- },6398- "engines": {6399- "node": ">=10.4.0"6400- }6401- },6402- "node_modules/plist/node_modules/xmlbuilder": {6403- "version": "15.1.1",6404- "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",6405- "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",6406- "license": "MIT",6407- "engines": {6408- "node": ">=8.0"6409- }6410- },64115792 "node_modules/pngjs": {64125793 "version": "5.0.0",64135794 "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",@@ -6493,28 +5874,6 @@64935874 "node": ">=6"64945875 }64955876 },6496- "node_modules/prompts": {6497- "version": "2.4.2",6498- "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",6499- "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",6500- "license": "MIT",6501- "dependencies": {6502- "kleur": "^3.0.3",6503- "sisteransi": "^1.0.5"6504- },6505- "engines": {6506- "node": ">= 6"6507- }6508- },6509- "node_modules/prompts/node_modules/kleur": {6510- "version": "3.0.3",6511- "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",6512- "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",6513- "license": "MIT",6514- "engines": {6515- "node": ">=6"6516- }6517- },65185877 "node_modules/proto-list": {65195878 "version": "1.2.4",65205879 "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",@@ -6925,6 +6284,7 @@69256284 "version": "1.6.0",69266285 "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",69276286 "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",6287+ "dev": true,69286288 "license": "BlueOak-1.0.0",69296289 "engines": {69306290 "node": ">=11.0.0"@@ -7031,6 +6391,7 @@70316391 "version": "2.0.0",70326392 "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",70336393 "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",6394+ "dev": true,70346395 "license": "MIT",70356396 "dependencies": {70366397 "shebang-regex": "^3.0.0"@@ -7043,6 +6404,7 @@70436404 "version": "3.0.0",70446405 "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",70456406 "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",6407+ "dev": true,70466408 "license": "MIT",70476409 "engines": {70486410 "node": ">=8"@@ -7069,12 +6431,6 @@70696431 "dev": true,70706432 "license": "ISC"70716433 },7072- "node_modules/signal-exit": {7073- "version": "3.0.7",7074- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",7075- "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",7076- "license": "ISC"7077- },70786434 "node_modules/sirv": {70796435 "version": "3.0.2",70806436 "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",@@ -7090,29 +6446,6 @@70906446 "node": ">=18"70916447 }70926448 },7093- "node_modules/sisteransi": {7094- "version": "1.0.5",7095- "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",7096- "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",7097- "license": "MIT"7098- },7099- "node_modules/slice-ansi": {7100- "version": "4.0.0",7101- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",7102- "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",7103- "license": "MIT",7104- "dependencies": {7105- "ansi-styles": "^4.0.0",7106- "astral-regex": "^2.0.0",7107- "is-fullwidth-code-point": "^3.0.0"7108- },7109- "engines": {7110- "node": ">=10"7111- },7112- "funding": {7113- "url": "https://github.com/chalk/slice-ansi?sponsor=1"7114- }7115- },71166449 "node_modules/sodium-native": {71176450 "version": "3.4.1",71186451 "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-3.4.1.tgz",@@ -7221,6 +6554,7 @@72216554 "version": "4.2.0",72226555 "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",72236556 "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",6557+ "dev": true,72246558 "license": "ISC",72256559 "engines": {72266560 "node": ">= 10.x"@@ -7244,6 +6578,7 @@72446578 "version": "1.1.1",72456579 "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",72466580 "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",6581+ "dev": true,72476582 "license": "MIT",72486583 "dependencies": {72496584 "safe-buffer": "~5.1.0"@@ -7253,6 +6588,7 @@72536588 "version": "5.1.2",72546589 "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",72556590 "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",6591+ "dev": true,72566592 "license": "MIT"72576593 },72586594 "node_modules/string-width": {@@ -7371,31 +6707,6 @@73716707 "dev": true,73726708 "license": "MIT"73736709 },7374- "node_modules/tar": {7375- "version": "7.5.13",7376- "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz",7377- "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==",7378- "license": "BlueOak-1.0.0",7379- "dependencies": {7380- "@isaacs/fs-minipass": "^4.0.0",7381- "chownr": "^3.0.0",7382- "minipass": "^7.1.2",7383- "minizlib": "^3.1.0",7384- "yallist": "^5.0.0"7385- },7386- "engines": {7387- "node": ">=18"7388- }7389- },7390- "node_modules/tar/node_modules/yallist": {7391- "version": "5.0.0",7392- "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",7393- "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",7394- "license": "BlueOak-1.0.0",7395- "engines": {7396- "node": ">=18"7397- }7398- },73996710 "node_modules/text-table": {74006711 "version": "0.2.0",74016712 "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",@@ -7420,29 +6731,6 @@74206731 "dev": true,74216732 "license": "MIT"74226733 },7423- "node_modules/through2": {7424- "version": "4.0.2",7425- "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",7426- "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",7427- "license": "MIT",7428- "dependencies": {7429- "readable-stream": "3"7430- }7431- },7432- "node_modules/through2/node_modules/readable-stream": {7433- "version": "3.6.2",7434- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",7435- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",7436- "license": "MIT",7437- "dependencies": {7438- "inherits": "^2.0.3",7439- "string_decoder": "^1.1.1",7440- "util-deprecate": "^1.0.1"7441- },7442- "engines": {7443- "node": ">= 6"7444- }7445- },74466734 "node_modules/tinybench": {74476735 "version": "2.9.0",74486736 "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",@@ -7595,19 +6883,11 @@75956883 "node": ">=20"75966884 }75976885 },7598- "node_modules/tree-kill": {7599- "version": "1.2.2",7600- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",7601- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",7602- "license": "MIT",7603- "bin": {7604- "tree-kill": "cli.js"7605- }7606- },76076886 "node_modules/tslib": {76086887 "version": "2.8.1",76096888 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",76106889 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",6890+ "dev": true,76116891 "license": "0BSD"76126892 },76136893 "node_modules/type-check": {@@ -7677,20 +6957,12 @@76776957 "version": "2.0.1",76786958 "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",76796959 "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",6960+ "dev": true,76806961 "license": "MIT",76816962 "engines": {76826963 "node": ">= 10.0.0"76836964 }76846965 },7685- "node_modules/untildify": {7686- "version": "4.0.0",7687- "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",7688- "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",7689- "license": "MIT",7690- "engines": {7691- "node": ">=8"7692- }7693- },76946966 "node_modules/upath": {76956967 "version": "2.0.1",76966968 "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz",@@ -7798,6 +7070,7 @@77987070 "version": "1.0.2",77997071 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",78007072 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",7073+ "dev": true,78017074 "license": "MIT"78027075 },78037076 "node_modules/uuid": {@@ -8293,6 +7566,7 @@82937566 "version": "2.0.2",82947567 "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",82957568 "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",7569+ "dev": true,82967570 "license": "ISC",82977571 "dependencies": {82987572 "isexe": "^2.0.0"@@ -8494,6 +7768,7 @@84947768 "version": "0.6.2",84957769 "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",84967770 "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",7771+ "dev": true,84977772 "license": "MIT",84987773 "dependencies": {84997774 "sax": ">=0.6.0",@@ -8507,6 +7782,7 @@85077782 "version": "11.0.1",85087783 "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",85097784 "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",7785+ "dev": true,85107786 "license": "MIT",85117787 "engines": {85127788 "node": ">=4.0"@@ -8571,6 +7847,7 @@85717847 "version": "2.10.0",85727848 "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",85737849 "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",7850+ "dev": true,85747851 "license": "MIT",85757852 "dependencies": {85767853 "buffer-crc32": "~0.2.3",package.json+3 −1
@@ -1,6 +1,6 @@11 {22 "name": "peach",3- "version": "2.29.3",3+ "version": "2.30.0",44 "description": "Zero-knowledge password manager",55 "type": "module",66 "scripts": {@@ -27,11 +27,13 @@2727 "@stablelib/random": "^2.0.1",2828 "@stablelib/sha256": "^2.0.1",2929 "@stablelib/x25519": "^2.0.1",30+ "@types/qrcode": "^1.5.6",3031 "eff-diceware-passphrase": "^3.0.0",3132 "hash-wasm": "^4.12.0",3233 "onnxruntime-web": "^1.24.3",3334 "otpauth": "^9.5.0",3435 "papaparse": "^5.5.3",36+ "qrcode": "^1.5.4",3537 "tldts": "^7.0.28",3638 "webextension-polyfill": "^0.12.0",3739 "zxcvbn": "^4.4.2"src/popup/styles/popup.css+2 −245
@@ -9,17 +9,6 @@99 margin: 0;1010 }1111 12-.peach-mobile-root,13-body.peach-mobile,14-.peach-mobile #app,15-.peach-mobile #pre-lock,16-.peach-mobile .popup-root {17- width: 100vw;18- min-height: 100dvh;19- height: 100dvh;20- margin: 0;21-}22-2312 body {2413 overflow: hidden;2514 background: var(--bg);@@ -29,13 +18,6 @@2918 position: relative;3019 }3120 32-body.peach-mobile {33- min-height: 100dvh;34- height: 100dvh;35- overflow: hidden;36- transition: padding-bottom 180ms ease;37-}38-3921 * {4022 box-sizing: border-box;4123 }@@ -66,15 +48,6 @@6648 overflow: hidden;6749 }6850 69-body.peach-mobile .popup-root {70- padding:71- env(safe-area-inset-top)72- env(safe-area-inset-right)73- env(safe-area-inset-bottom)74- env(safe-area-inset-left);75- overflow: hidden;76-}77-7851 .peach-theme {7952 color: var(--peach-t0);8053 font-family: var(--font-ui);@@ -132,26 +105,6 @@132105 overflow: hidden;133106 }134107 135-body.peach-mobile .peach-shell[data-surface="extension"],136-body.peach-mobile .peach-screen {137- width: 100%;138- height: 100%;139- max-height: none;140- min-height: 100%;141- border: 0;142- box-shadow: none;143-}144-145-body.peach-mobile .peach-shell[data-surface="extension"] {146- grid-template-rows: auto minmax(0, 1fr);147-}148-149-body.peach-mobile .peach-stage,150-body.peach-mobile .peach-overlay-view,151-body.peach-mobile .peach-settings-content {152- overscroll-behavior: contain;153-}154-155108 /* ── Lock screen – full-window lock/create stage ── */156109 .lock-screen--vault-stage {157110 display: flex;@@ -7090,18 +7043,6 @@70907043 animation: none;70917044 }70927045 7093-.peach-mobile-row {7094- display: flex;7095- align-items: center;7096- justify-content: space-between;7097- padding: 8px 0;7098-}7099-7100-.peach-mobile-status {7101- color: #9a8b7a;7102- font-size: 0.875rem;7103-}7104-71057046 /* Vault History — Settings → Vault sub-view */71067047 71077048 .peach-history-list {@@ -7171,117 +7112,7 @@71717112 padding: 2px 0 0;71727113 }71737114 7174-/* ── Adaptive layout — Window Size Classes ───────────────────────────7175- Compact (< 600dp) — phone, foldable cover, narrow split-screen7176- Medium (600–839) — Z Fold inner display, small tablet7177- Expanded (840+) — tablet, DeX window, desktop7178- Body class is set from App.tsx onMount + window resize. */7179-7180-/* Compact stays edge-to-edge (existing behavior). */7181-7182-body.peach-mobile.layout-medium .popup-root,7183-body.peach-mobile.layout-expanded .popup-root {7184- max-width: 840px;7185- margin: 0 auto;7186-}7187-7188-body.peach-mobile.layout-medium .peach-shell[data-surface="extension"],7189-body.peach-mobile.layout-expanded .peach-shell[data-surface="extension"] {7190- max-width: 840px;7191- margin: 0 auto;7192-}7193-7194-/* Two-pane: vault list on the left, entry detail on the right.7195- Activates on medium+ when both are present. App-side wires the7196- .peach-vault-twopane wrapper class on these widths. */7197-body.peach-mobile.layout-medium .peach-vault-twopane,7198-body.peach-mobile.layout-expanded .peach-vault-twopane {7199- display: grid;7200- grid-template-columns: 320px minmax(0, 1fr);7201- gap: 0;7202- height: 100%;7203-}7204-7205-body.peach-mobile.layout-medium .peach-vault-twopane > .peach-vault-list-pane,7206-body.peach-mobile.layout-expanded .peach-vault-twopane > .peach-vault-list-pane {7207- border-right: 1px solid var(--peach-border, var(--peach-t3));7208- overflow-y: auto;7209- scrollbar-width: none;7210-}7211-7212-body.peach-mobile.layout-medium .peach-vault-twopane > .peach-vault-detail-pane,7213-body.peach-mobile.layout-expanded .peach-vault-twopane > .peach-vault-detail-pane {7214- overflow-y: auto;7215- scrollbar-width: none;7216-}7217-7218-/* Tabletop posture — pad content away from the hinge crease. */7219-body.peach-mobile[data-posture="tabletop"] .peach-vault-twopane,7220-body.peach-mobile[data-posture="tabletop"] .peach-stage,7221-body.peach-mobile[data-posture="tabletop"] .peach-overlay-view {7222- padding-bottom: var(--peach-hinge-height, 0px);7223-}7224-7225-/* Landscape phone — short viewport, reduce vertical padding so content fits. */7226-@media (orientation: landscape) and (max-height: 500px) {7227- body.peach-mobile .popup-root {7228- padding-top: env(safe-area-inset-top);7229- }7230- body.peach-mobile .peach-shell[data-surface="extension"] {7231- grid-template-rows: auto minmax(0, 1fr);7232- }7233-}7234-7235-/* Narrow split-screen (down to 280dp) — keep content readable. */7236-@media (max-width: 320px) {7237- body.peach-mobile .peach-chrome,7238- body.peach-mobile .peach-stage,7239- body.peach-mobile .peach-overlay-view {7240- padding-left: 8px;7241- padding-right: 8px;7242- }7243-}7244-7245-/* ── Two-pane on medium+ ───────────────────────────────────────────────7246- The vault list is normally a full-screen overlay. On Z Fold inner /7247- tablet / DeX widths, scope it to a 320px left column so the entry7248- detail behind it stays visible on the right. handleSelectEntry keeps7249- view===\"list\" on these widths so the list never animates away. */7250-body.peach-mobile.layout-medium .peach-overlay-view,7251-body.peach-mobile.layout-expanded .peach-overlay-view {7252- right: auto;7253- width: 320px;7254- border-right: 1px solid var(--peach-t3);7255- animation: none;7256- z-index: 2;7257-}7258-7259-/* Push the entry-frame content past the sidebar so it isn't hidden. */7260-body.peach-mobile.layout-medium .peach-content-frame,7261-body.peach-mobile.layout-expanded .peach-content-frame {7262- padding-left: 320px;7263-}7264-7265-/* Search bar is part of the shell — when the list sidebar is mounted on7266- medium+, the list owns search. Keep the shell search bar visible only7267- when no overlay is active. */7268-body.peach-mobile.layout-medium .peach-search-wrap,7269-body.peach-mobile.layout-expanded .peach-search-wrap {7270- padding-left: 320px;7271-}7272-7273-/* On expanded (840+), leave more breathing room and cap detail width. */7274-body.peach-mobile.layout-expanded .peach-content-frame {7275- padding-left: 360px;7276-}7277-body.peach-mobile.layout-expanded .peach-overlay-view {7278- width: 360px;7279-}7280-body.peach-mobile.layout-expanded .peach-search-wrap {7281- padding-left: 360px;7282-}7283-7284-/* ── Long-press context menu + swipe-copy flash ─────────────────────── */7115+/* ── Context menu + swipe-copy flash ─────────────────────── */72857116 .peach-context-menu-backdrop {72867117 position: fixed;72877118 inset: 0;@@ -7334,76 +7165,7 @@73347165 border-radius: 0 0 10px 10px;73357166 }73367167 7337-/* ── Mobile wordmark → settings tap target ── */7338-.peach-wordmark-btn {7339- background: none;7340- padding: 0;7341- cursor: pointer;7342- -webkit-tap-highlight-color: transparent;7343- display: flex;7344- align-items: center;7345-}7346-7347-/* ── Gesture hints welcome card ── */7348-.peach-gesture-hints-overlay {7349- position: fixed;7350- inset: 0;7351- z-index: 200;7352- background: rgba(0, 0, 0, 0.55);7353- display: flex;7354- align-items: center;7355- justify-content: center;7356- padding: 32px 24px;7357- animation: peach-fade-in 200ms ease both;7358-}7359-@keyframes peach-fade-in {7360- from { opacity: 0; }7361- to { opacity: 1; }7362-}7363-.peach-gesture-hints-card {7364- background: var(--bg-1);7365- border: 1px solid var(--peach-t3);7366- border-radius: 16px;7367- padding: 28px 24px 24px;7368- width: 100%;7369- max-width: 320px;7370- display: flex;7371- flex-direction: column;7372- gap: 20px;7373- animation: ov-enter-up 280ms var(--peach-curve-arrival) both;7374-}7375-.peach-gesture-hints-title {7376- font-family: var(--font-display);7377- font-style: var(--font-display-style);7378- font-size: 22px;7379- color: var(--peach-t0);7380- margin: 0;7381-}7382-.peach-gesture-hints-list {7383- list-style: none;7384- margin: 0;7385- padding: 0;7386- display: flex;7387- flex-direction: column;7388- gap: 14px;7389-}7390-.peach-gesture-hints-list li {7391- display: flex;7392- align-items: center;7393- gap: 14px;7394- font-size: 14px;7395- color: var(--peach-t1);7396-}7397-.peach-gesture-glyph {7398- width: 36px;7399- flex-shrink: 0;7400- font-size: 14px;7401- color: var(--peach-accent);7402- text-align: center;7403- letter-spacing: 0.02em;7404-}7405-7406-/* Swipe-to-copy amber flash */7168+/* Swipe-to-copy flash */74077169 .peach-entry-row.is-copy-flash {74087170 animation: peach-copy-flash 600ms ease both;74097171 }@@ -7413,9 +7175,4 @@74137175 100% { background: transparent; }74147176 }74157177 7416-/* Font scaling baseline. 16px is the WebView default; setting it explicitly7417- anchors all the rem-based body-text sizes that depend on it. On Android,7418- MainActivity overrides WebSettings.textZoom so system font scale flows7419- through to rem-sized text. Display sizes (>=18px) still use px to keep7420- tight layout control around headlines and the lock-screen hero. */74217178 html { font-size: 16px; }src/shared/android-sync-identity.ts+0 −90
@@ -1,90 +0,0 @@1-/**2- * Android sync identity — persistent X25519 keypair for the Capacitor side.3- *4- * The Kotlin SyncKey plugin stores the keypair in encrypted SharedPreferences5- * keyed by hardware-bound master key. JS handles the X25519 math; the plugin6- * is a simple persisted blob store. On first invocation we generate, persist,7- * and return; subsequent calls return the cached pair.8- *9- * Non-Android contexts (extension, dev WebView) throw — this identity is10- * Android-only. Desktop sync uses the native messaging host's identity11- * (`SyncIdentity` in the Rust host) which has its own key file.12- */13-14-import { Capacitor, registerPlugin } from "@capacitor/core";15-import { generateKeyPair } from "@stablelib/x25519";16-17-interface SyncKeyPlugin {18- /**19- * Returns the stored keypair if one exists, else { privateKeyHex: '',20- * publicKeyHex: '' } so the JS side knows to generate + persist.21- */22- loadKeypair(): Promise<{ privateKeyHex: string; publicKeyHex: string }>;23- /** Persists a fresh keypair generated on the JS side. */24- saveKeypair(keys: { privateKeyHex: string; publicKeyHex: string }): Promise<void>;25- /** Wipes the persisted keypair. The JS cache must be invalidated separately. */26- clearKeypair(): Promise<void>;27-}28-29-const SyncKey = registerPlugin<SyncKeyPlugin>("SyncKey");30-31-let cached: { privateKey: Uint8Array; publicKey: Uint8Array } | null = null;32-33-function bytesToHex(bytes: Uint8Array): string {34- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");35-}36-37-function hexToBytes(hex: string): Uint8Array {38- const out = new Uint8Array(hex.length / 2);39- for (let i = 0; i < hex.length; i += 2) {40- out[i / 2] = parseInt(hex.slice(i, i + 2), 16);41- }42- return out;43-}44-45-export async function getAndroidSyncIdentity(): Promise<{46- privateKey: Uint8Array;47- publicKey: Uint8Array;48-}> {49- if (cached) return cached;50- if (!Capacitor.isNativePlatform()) {51- throw new Error("Android sync identity requested on non-native platform");52- }53-54- const stored = await SyncKey.loadKeypair();55- if (stored.privateKeyHex && stored.publicKeyHex) {56- cached = {57- privateKey: hexToBytes(stored.privateKeyHex),58- publicKey: hexToBytes(stored.publicKeyHex),59- };60- return cached;61- }62-63- const pair = generateKeyPair();64- const privateKeyHex = bytesToHex(pair.secretKey);65- const publicKeyHex = bytesToHex(pair.publicKey);66- await SyncKey.saveKeypair({ privateKeyHex, publicKeyHex });67- cached = { privateKey: pair.secretKey, publicKey: pair.publicKey };68- return cached;69-}70-71-/**72- * Wipe the persisted keypair and the in-memory cache. The next call to73- * getAndroidSyncIdentity() will generate and persist a fresh pair.74- *75- * No-op on non-native platforms — desktop doesn't use this identity.76- */77-export async function clearAndroidSyncIdentity(): Promise<void> {78- cached = null;79- if (!Capacitor.isNativePlatform()) return;80- try {81- await SyncKey.clearKeypair();82- } catch {83- // Plugin may be missing on older builds — best-effort.84- }85-}86-87-/** Test-only — wipe the in-memory cache. */88-export function __resetForTests(): void {89- cached = null;90-}src/shared/biometric.ts+0 −22
@@ -1,22 +0,0 @@1-import { registerPlugin, Capacitor } from "@capacitor/core";2-3-const Biometric = registerPlugin<{4- authenticate(): Promise<{ verified: boolean }>;5- isAvailable(): Promise<{ available: boolean }>;6-}>("Biometric");7-8-export async function biometricUnlock(): Promise<boolean> {9- if (!Capacitor.isNativePlatform()) return false;10- try {11- const result = await Biometric.authenticate();12- return result.verified;13- } catch {14- return false;15- }16-}17-18-export async function biometricAvailable(): Promise<boolean> {19- if (!Capacitor.isNativePlatform()) return false;20- const result = await Biometric.isAvailable();21- return result.available;22-}src/shared/bridge.ts+1 −15
@@ -1,4 +1,3 @@1-import { Capacitor } from "@capacitor/core";21 import type { Message, MessageResponse } from "./types";32 43 const MESSAGE_TIMEOUT_MS = 5000;@@ -11,7 +10,7 @@1110 return error;1211 }1312 14-async function sendExtensionMessage(message: Message): Promise<MessageResponse> {13+export async function sendMessage(message: Message): Promise<MessageResponse> {1514 return new Promise((resolve) => {1615 let settled = false;1716 const timeoutId = window.setTimeout(() => {@@ -54,17 +53,4 @@5453 });5554 }5655 });57-}58-59-export async function sendMessage(message: Message): Promise<MessageResponse> {60- if (!Capacitor.isNativePlatform()) {61- return sendExtensionMessage(message);62- }63-64- const { handleMessage } = await import("../background/messages");65- const response = await handleMessage(message, {});66- if (response.ok === false) {67- response.error = humanizeError(response.error);68- }69- return response;7056 }src/shared/capacitor-pairing-handshake.ts+0 −96
@@ -1,96 +0,0 @@1-/**2- * Capacitor pairing handshake — WebSocket variant of the desktop3- * INITIATE_PAIR_WITH_PEER flow. Used by the Android entering-side path4- * after `Sync.resolvePeerByCode` returns the desktop's connection info.5- *6- * Bytes-on-wire match the desktop TCP path: Noise NK + auth proof, then a7- * single PAIR_REQUEST frame and a single PAIR_ACCEPT/PAIR_REJECT frame.8- * The transport returned by `createCapacitorSyncTransport` already wraps9- * Noise + framing; this module just sends/receives the two pairing frames.10- */11-12-import { createCapacitorSyncTransport } from "./capacitor-sync-transport";13-import { encodeSyncMessage, decodeSyncMessage } from "./sync-wire";14-import type { DiscoveredPeer, SyncMessage } from "./sync-types";15-16-const PAIR_RESPONSE_TIMEOUT_MS = 30_000;17-18-export interface CapacitorPairingResult {19- publicKeyHex: string;20- deviceName: string;21-}22-23-export async function runCapacitorPairingHandshake(24- peer: DiscoveredPeer,25- ourPrivateKey: Uint8Array,26- ourPublicKey: Uint8Array,27- ourPublicKeyHex: string,28- ourDeviceName: string,29-): Promise<CapacitorPairingResult> {30- const transport = await createCapacitorSyncTransport(31- peer,32- ourPrivateKey,33- ourPublicKey,34- );35-36- try {37- const responsePromise = new Promise<SyncMessage>((resolve, reject) => {38- const timer = setTimeout(() => {39- cleanup();40- reject(new Error("pairing response timeout"));41- }, PAIR_RESPONSE_TIMEOUT_MS);42-43- const offFrame = transport.onFrame((frame) => {44- try {45- const message = decodeSyncMessage(frame);46- cleanup();47- resolve(message);48- } catch (err) {49- cleanup();50- reject(err instanceof Error ? err : new Error(String(err)));51- }52- });53- const offError = transport.onError((message) => {54- cleanup();55- reject(new Error(message));56- });57- const offClose = transport.onClose(() => {58- cleanup();59- reject(new Error("connection closed before pairing response"));60- });61-62- function cleanup() {63- clearTimeout(timer);64- offFrame();65- offError();66- offClose();67- }68- });69-70- await transport.send(71- encodeSyncMessage({72- type: "PAIR_REQUEST",73- public_key_hex: ourPublicKeyHex,74- device_name: ourDeviceName,75- }),76- );77-78- const response = await responsePromise;79- if (response.type === "PAIR_ACCEPT") {80- return {81- publicKeyHex: response.public_key_hex,82- deviceName: response.device_name,83- };84- }85- if (response.type === "PAIR_REJECT") {86- throw new Error(`pair_rejected:${response.reason}`);87- }88- throw new Error(`unexpected_response:${response.type}`);89- } finally {90- try {91- transport.close();92- } catch {93- // best-effort94- }95- }96-}src/shared/capacitor-pairing-server.ts+0 −332
@@ -1,332 +0,0 @@1-/**2- * Capacitor pairing server — JS runtime for Android show-mode pairing.3- *4- * Kotlin (`SyncPlugin.startPairingTcpServer`) is the byte pump: it accepts5- * one TCP connection and pumps inbound bytes to JS as `pairingChunk` events6- * (base64). Outbound bytes go back via `sendPairingChunk`. Bytes are not7- * framed on the Kotlin side — chunks may split or coalesce frames.8- *9- * This module reassembles u32-BE length-prefixed frames (matching10- * `peach-sync-host/src/frame.rs`), drives the Noise NK responder, runs the11- * auth challenge/proof round, and processes a single PAIR_REQUEST. On12- * accept it sends PAIR_ACCEPT, persists the paired device to13- * `peach_paired_devices`, and writes a `RecentPairing` so the popup can14- * surface a "Paired with X" toast on next open.15- */16-17-import { registerPlugin, type PluginListenerHandle } from "@capacitor/core";18-import {19- noiseNKResponder,20- performResponderAuth,21- type NoiseNKSession,22-} from "./noise-js";23-import { encodeSyncMessage, decodeSyncMessage } from "./sync-wire";24-25-const MAX_FRAME_BYTES = 1024 * 1024;26-const PAIR_REQUEST_TIMEOUT_MS = 5 * 60 * 1000;27-28-interface SyncPluginPairing {29- startPairingTcpServer(): Promise<{ tcpPort: number }>;30- startPairingBeacon(opts: {31- token: string;32- tcpPort: number;33- x25519PublicKey: string;34- deviceId: string;35- name: string;36- }): Promise<void>;37- stopPairingServer(): Promise<void>;38- sendPairingChunk(opts: { dataBase64: string }): Promise<void>;39- addListener(40- eventName: "pairingChunk",41- listener: (event: { dataBase64: string }) => void,42- ): Promise<PluginListenerHandle>;43- addListener(44- eventName: "pairingConnected",45- listener: (event: { peerAddr: string }) => void,46- ): Promise<PluginListenerHandle>;47- addListener(48- eventName: "pairingEnded",49- listener: () => void,50- ): Promise<PluginListenerHandle>;51-}52-53-const Sync = registerPlugin<SyncPluginPairing>("Sync");54-55-export interface CapacitorPairingDeps {56- /** Our static x25519 keypair. Persisted by `getAndroidSyncIdentity()`. */57- privateKey: Uint8Array;58- publicKey: Uint8Array;59- /** Local device name shown to the peer in PAIR_ACCEPT. */60- deviceName: string;61- /** base64url-encoded random device id (not the public key). */62- deviceId: string;63- /** Hex-encoded public key, for the UDP beacon's `x25519_public_key`. */64- publicKeyHex: string;65- /** 6-digit pairing code. */66- token: string;67- /** Persists the new paired device on accept. */68- onPaired: (paired: {69- deviceId: string;70- displayName: string;71- publicKeyHex: string;72- }) => void | Promise<void>;73- /**74- * Optional notification fired BEFORE persisting — popup uses it to flip to75- * the success card while the SW is still finishing storage writes.76- */77- onPairRequest?: (deviceName: string) => void;78-}79-80-export interface CapacitorPairingHandle {81- /** Tear down the server, beacon, and any in-flight handshake. */82- stop: () => Promise<void>;83-}84-85-function base64ToBytes(b64: string): Uint8Array {86- const binary = atob(b64);87- const out = new Uint8Array(binary.length);88- for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);89- return out;90-}91-92-function bytesToBase64(bytes: Uint8Array): string {93- let binary = "";94- for (const b of bytes) binary += String.fromCharCode(b);95- return btoa(binary);96-}97-98-/**99- * Start the show-mode pairing server: bind TCP listener via Kotlin, start100- * the UDP beacon, then drive Noise NK responder + auth + PAIR_REQUEST as101- * soon as a peer connects.102- *103- * The returned handle's `stop()` is idempotent — call it from popup104- * cleanup, vault-lock, and post-pair so the kotlin sockets always close.105- */106-export async function startCapacitorPairingServer(107- deps: CapacitorPairingDeps,108-): Promise<CapacitorPairingHandle> {109- const { tcpPort } = await Sync.startPairingTcpServer();110- await Sync.startPairingBeacon({111- token: deps.token,112- tcpPort,113- x25519PublicKey: deps.publicKeyHex,114- deviceId: deps.deviceId,115- name: deps.deviceName,116- });117-118- // Inbound byte buffer — Kotlin sends raw socket reads; we reassemble119- // u32-BE length-prefixed frames ourselves.120- let inboundBuf = new Uint8Array(0);121- const frameWaiters: Array<{122- resolve: (frame: Uint8Array) => void;123- reject: (err: Error) => void;124- }> = [];125- let connectionClosed = false;126- let closeReason: Error | null = null;127- let stopped = false;128-129- function appendInbound(chunk: Uint8Array) {130- const next = new Uint8Array(inboundBuf.length + chunk.length);131- next.set(inboundBuf, 0);132- next.set(chunk, inboundBuf.length);133- inboundBuf = next;134- drainFrameWaiters();135- }136-137- function tryReadFrame(): Uint8Array | null {138- if (inboundBuf.length < 4) return null;139- const view = new DataView(inboundBuf.buffer, inboundBuf.byteOffset, 4);140- const len = view.getUint32(0, false); // big-endian, matches frame.rs141- if (len > MAX_FRAME_BYTES) {142- throw new Error(`incoming frame too large: ${len}`);143- }144- if (inboundBuf.length < 4 + len) return null;145- const payload = inboundBuf.slice(4, 4 + len);146- inboundBuf = inboundBuf.slice(4 + len);147- return payload;148- }149-150- function drainFrameWaiters() {151- while (frameWaiters.length > 0) {152- let frame: Uint8Array | null;153- try {154- frame = tryReadFrame();155- } catch (err) {156- const w = frameWaiters.shift();157- w?.reject(err instanceof Error ? err : new Error(String(err)));158- continue;159- }160- if (!frame) return;161- const w = frameWaiters.shift()!;162- w.resolve(frame);163- }164- }165-166- function failPendingWaiters(err: Error) {167- while (frameWaiters.length > 0) {168- const w = frameWaiters.shift()!;169- w.reject(err);170- }171- }172-173- function receiveFrame(): Promise<Uint8Array> {174- if (connectionClosed && inboundBuf.length < 4) {175- return Promise.reject(closeReason ?? new Error("connection closed"));176- }177- return new Promise<Uint8Array>((resolve, reject) => {178- frameWaiters.push({ resolve, reject });179- drainFrameWaiters();180- });181- }182-183- async function sendFrame(payload: Uint8Array): Promise<void> {184- if (connectionClosed) {185- throw closeReason ?? new Error("connection closed");186- }187- const len = payload.length;188- const framed = new Uint8Array(4 + len);189- new DataView(framed.buffer).setUint32(0, len, false);190- framed.set(payload, 4);191- await Sync.sendPairingChunk({ dataBase64: bytesToBase64(framed) });192- }193-194- const chunkHandle = await Sync.addListener("pairingChunk", (event) => {195- if (stopped) return;196- try {197- appendInbound(base64ToBytes(event.dataBase64));198- } catch (err) {199- failPendingWaiters(err instanceof Error ? err : new Error(String(err)));200- }201- });202-203- const endHandle = await Sync.addListener("pairingEnded", () => {204- connectionClosed = true;205- closeReason = closeReason ?? new Error("connection closed");206- failPendingWaiters(closeReason);207- });208-209- const connectedHandle = await Sync.addListener("pairingConnected", () => {210- void runHandshake();211- });212-213- async function runHandshake(): Promise<void> {214- let session: NoiseNKSession;215- try {216- session = await noiseNKResponder(217- deps.privateKey,218- deps.publicKey,219- sendFrame,220- receiveFrame,221- );222- } catch (err) {223- console.warn("[peach-pair] noise responder failed:", err);224- void stop();225- return;226- }227-228- const sendEnc = async (data: Uint8Array) => sendFrame(session.encrypt(data));229- const recvEnc = async () => session.decrypt(await receiveFrame());230-231- let initiatorPublicKey: Uint8Array;232- try {233- const auth = await performResponderAuth(234- session,235- deps.privateKey,236- deps.publicKey,237- sendEnc,238- recvEnc,239- );240- initiatorPublicKey = auth.initiatorPublicKey;241- } catch (err) {242- console.warn("[peach-pair] auth failed:", err);243- void stop();244- return;245- }246-247- // Read exactly one PAIR_REQUEST. Anything else on a code-pairing window248- // is an attempted bypass — drop the connection.249- let pairRequest;250- try {251- const frame = await Promise.race([252- recvEnc(),253- new Promise<Uint8Array>((_, reject) =>254- setTimeout(() => reject(new Error("PAIR_REQUEST timeout")), PAIR_REQUEST_TIMEOUT_MS),255- ),256- ]);257- const text = new TextDecoder().decode(frame);258- const message = decodeSyncMessage(text);259- if (message.type !== "PAIR_REQUEST") {260- throw new Error(`unexpected frame: ${message.type}`);261- }262- pairRequest = message;263- } catch (err) {264- console.warn("[peach-pair] PAIR_REQUEST read failed:", err);265- void stop();266- return;267- }268-269- // The 6-digit token IS the user's consent — auto-accept on the showing270- // side. The Noise+auth round above already proved the initiator owns271- // the public key in PAIR_REQUEST; cross-check it with auth here.272- const claimedHex = pairRequest.public_key_hex.toLowerCase();273- const authedHex = bytesToHex(initiatorPublicKey);274- if (claimedHex !== authedHex) {275- console.warn(276- "[peach-pair] PAIR_REQUEST public_key_hex did not match authenticated key",277- );278- void stop();279- return;280- }281-282- deps.onPairRequest?.(pairRequest.device_name);283-284- try {285- await sendEnc(286- new TextEncoder().encode(287- encodeSyncMessage({288- type: "PAIR_ACCEPT",289- public_key_hex: bytesToHex(deps.publicKey),290- device_name: deps.deviceName,291- }),292- ),293- );294- } catch (err) {295- console.warn("[peach-pair] PAIR_ACCEPT send failed:", err);296- void stop();297- return;298- }299-300- try {301- await deps.onPaired({302- deviceId: pairRequest.public_key_hex,303- displayName: pairRequest.device_name,304- publicKeyHex: pairRequest.public_key_hex,305- });306- } catch (err) {307- console.warn("[peach-pair] onPaired callback failed:", err);308- }309-310- // One code = one pairing. Drop the connection so a second peer can't311- // sneak in on the same window.312- void stop();313- }314-315- async function stop(): Promise<void> {316- if (stopped) return;317- stopped = true;318- try { await chunkHandle.remove(); } catch { /* best-effort */ }319- try { await endHandle.remove(); } catch { /* best-effort */ }320- try { await connectedHandle.remove(); } catch { /* best-effort */ }321- try { await Sync.stopPairingServer(); } catch { /* best-effort */ }322- connectionClosed = true;323- closeReason = closeReason ?? new Error("pairing server stopped");324- failPendingWaiters(closeReason);325- }326-327- return { stop };328-}329-330-function bytesToHex(bytes: Uint8Array): string {331- return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");332-}src/shared/chrome-mobile.ts+0 −60
@@ -1,60 +0,0 @@1-// Capacitor-backed chrome.storage.local shim.2-// Used on mobile builds where there is no service worker / Chrome extension API.3-// Redirects all chrome.storage.local calls to @capacitor/preferences.4-import { Preferences } from "@capacitor/preferences";5-6-export const chrome = {7- storage: {8- local: {9- async get(keys: string | string[] | Record<string, unknown>): Promise<Record<string, unknown>> {10- const keyArray = Array.isArray(keys) ? keys : typeof keys === "string" ? [keys] : Object.keys(keys);11- const result: Record<string, unknown> = {};12- for (const k of keyArray) {13- const { value } = await Preferences.get({ key: k });14- if (value !== undefined) result[k] = value;15- }16- return result;17- },18- async set(items: Record<string, unknown>): Promise<void> {19- for (const [k, v] of Object.entries(items)) {20- await Preferences.set({ key: k, value: String(v) });21- }22- },23- async remove(keys: string | string[]): Promise<void> {24- const keyArray = Array.isArray(keys) ? keys : [keys];25- for (const k of keyArray) {26- await Preferences.remove({ key: k });27- }28- },29- async clear(): Promise<void> {30- await Preferences.clear();31- },32- },33- session: {34- // Session storage on mobile: use a plain Map (not persisted)35- _session: new Map<string, unknown>(),36- async get(keys: string | string[] | Record<string, unknown>): Promise<Record<string, unknown>> {37- const keyArray = Array.isArray(keys) ? keys : typeof keys === "string" ? [keys] : Object.keys(keys);38- const result: Record<string, unknown> = {};39- for (const k of keyArray) {40- if (this._session.has(k)) result[k] = this._session.get(k);41- }42- return result;43- },44- async set(items: Record<string, unknown>): Promise<void> {45- for (const [k, v] of Object.entries(items)) this._session.set(k, v);46- },47- async remove(keys: string | string[]): Promise<void> {48- const keyArray = Array.isArray(keys) ? keys : [keys];49- for (const k of keyArray) this._session.delete(k);50- },51- },52- onChanged: { addListener: () => {}, removeListener: () => {} },53- },54- runtime: {55- sendMessage: (_msg: unknown, cb?: (r: unknown) => void) => { if (cb) cb({ ok: false, error: "NO_SERVICE_WORKER" }); },56- onMessage: { addListener: () => {}, removeListener: () => {} },57- connect: () => ({ disconnect: () => {} }),58- lastError: null,59- },60-};src/shared/chrome-shim.ts+0 −147
@@ -1,147 +0,0 @@1-// Capacitor chrome shim. The mobile foreground imports background modules that2-// were written for MV3, so this supplies the small chrome.* surface they touch.3-4-import { Preferences } from "@capacitor/preferences";5-6-declare const __APP_VERSION__: string;7-8-type Listener<T extends (...args: any[]) => void> = {9- addListener: (_listener: T) => void;10- removeListener: (_listener: T) => void;11-};12-13-const sessionStore = new Map<string, unknown>();14-15-function noopListener<T extends (...args: any[]) => void>(): Listener<T> {16- return {17- addListener: () => {},18- removeListener: () => {},19- };20-}21-22-async function keysFor(input: string | string[] | Record<string, unknown> | null): Promise<string[]> {23- if (input === null) {24- const result = await Preferences.keys();25- return result.keys;26- }27- if (Array.isArray(input)) return input;28- if (typeof input === "string") return [input];29- return Object.keys(input);30-}31-32-async function localGet(keys: string | string[] | Record<string, unknown> | null): Promise<Record<string, unknown>> {33- const result: Record<string, unknown> = {};34- for (const key of await keysFor(keys)) {35- const { value } = await Preferences.get({ key });36- if (value === null) continue;37- try {38- result[key] = JSON.parse(value);39- } catch {40- result[key] = value;41- }42- }43- return result;44-}45-46-async function localSet(items: Record<string, unknown>): Promise<void> {47- for (const [key, value] of Object.entries(items)) {48- await Preferences.set({ key, value: JSON.stringify(value) });49- }50-}51-52-async function localRemove(keys: string | string[]): Promise<void> {53- const keyArray = Array.isArray(keys) ? keys : [keys];54- for (const key of keyArray) {55- await Preferences.remove({ key });56- }57-}58-59-function sessionKeys(input: string | string[] | Record<string, unknown> | null): string[] {60- if (input === null) return [...sessionStore.keys()];61- if (Array.isArray(input)) return input;62- if (typeof input === "string") return [input];63- return Object.keys(input);64-}65-66-const chromeShim = {67- runtime: {68- id: "peach-mobile",69- getManifest: () => ({70- name: "Peach",71- version: typeof __APP_VERSION__ !== "undefined" ? __APP_VERSION__ : "0.0.0",72- manifest_version: 3,73- }),74- sendMessage: (_msg: unknown, cb?: (response: unknown) => void) => {75- const response = { ok: false, error: "NO_SERVICE_WORKER" };76- cb?.(response);77- return Promise.resolve(response);78- },79- onMessage: noopListener<(message: unknown) => void>(),80- connect: () => ({81- disconnect: () => {},82- onMessage: noopListener<(message: unknown) => void>(),83- onDisconnect: noopListener<() => void>(),84- postMessage: () => {},85- }),86- lastError: null,87- },88- storage: {89- local: {90- get: localGet,91- set: localSet,92- remove: localRemove,93- clear: () => Preferences.clear(),94- },95- session: {96- get: async (keys: string | string[] | Record<string, unknown> | null) => {97- const result: Record<string, unknown> = {};98- for (const key of sessionKeys(keys)) {99- if (sessionStore.has(key)) result[key] = sessionStore.get(key);100- }101- return result;102- },103- set: async (items: Record<string, unknown>) => {104- for (const [key, value] of Object.entries(items)) {105- sessionStore.set(key, value);106- }107- },108- remove: async (keys: string | string[]) => {109- const keyArray = Array.isArray(keys) ? keys : [keys];110- for (const key of keyArray) sessionStore.delete(key);111- },112- clear: async () => {113- sessionStore.clear();114- },115- },116- onChanged: noopListener<(changes: Record<string, unknown>, areaName: string) => void>(),117- },118- alarms: {119- clear: () => {},120- create: () => {},121- onAlarm: noopListener<(alarm: { name: string }) => void>(),122- },123- idle: {124- setDetectionInterval: () => {},125- onStateChanged: noopListener<(state: string) => void>(),126- },127- tabs: {128- query: (_query: unknown, cb?: (tabs: any[]) => void) => {129- cb?.([]);130- return Promise.resolve([]);131- },132- sendMessage: () => Promise.resolve(undefined),133- create: () => Promise.resolve(undefined),134- onActivated: noopListener<() => void>(),135- onUpdated: noopListener<() => void>(),136- },137- action: {138- openPopup: () => Promise.resolve(),139- setBadgeText: () => Promise.resolve(),140- setBadgeBackgroundColor: () => Promise.resolve(),141- },142- scripting: {143- executeScript: () => Promise.resolve(),144- },145-};146-147-(window as any).chrome = chromeShim;src/shared/chrome.ts+0 −61
@@ -1,61 +0,0 @@1-// Shared chrome shim — uses real Chrome APIs on extension builds,2-// Capacitor-backed shim on mobile builds.3-// Background/vault code all imports from here so storage calls work on both platforms.4-import { Capacitor } from "@capacitor/core";5-6-export const PeachChrome = Capacitor.isNativePlatform()7- ? {8- storage: {9- local: {10- async get(keys: string | string[] | Record<string, unknown>): Promise<Record<string, unknown>> {11- const { Preferences } = await import("@capacitor/preferences");12- const keyArray = Array.isArray(keys) ? keys : typeof keys === "string" ? [keys] : Object.keys(keys);13- const result: Record<string, unknown> = {};14- for (const k of keyArray) {15- const { value } = await Preferences.get({ key: k });16- if (value !== undefined) result[k] = value;17- }18- return result;19- },20- async set(items: Record<string, unknown>): Promise<void> {21- const { Preferences } = await import("@capacitor/preferences");22- for (const [k, v] of Object.entries(items)) {23- await Preferences.set({ key: k, value: String(v) });24- }25- },26- async remove(keys: string | string[]): Promise<void> {27- const { Preferences } = await import("@capacitor/preferences");28- const keyArray = Array.isArray(keys) ? keys : [keys];29- for (const k of keyArray) await Preferences.remove({ key: k });30- },31- async clear(): Promise<void> {32- const { Preferences } = await import("@capacitor/preferences");33- await Preferences.clear();34- },35- },36- session: {37- _s: new Map<string, unknown>(),38- async get(keys: string | string[] | Record<string, unknown>): Promise<Record<string, unknown>> {39- const keyArray = Array.isArray(keys) ? keys : typeof keys === "string" ? [keys] : Object.keys(keys);40- const result: Record<string, unknown> = {};41- for (const k of keyArray) { if (this._s.has(k)) result[k] = this._s.get(k); }42- return result;43- },44- async set(items: Record<string, unknown>): Promise<void> {45- for (const [k, v] of Object.entries(items)) this._s.set(k, v);46- },47- async remove(keys: string | string[]): Promise<void> {48- const keyArray = Array.isArray(keys) ? keys : [keys];49- for (const k of keyArray) this._s.delete(k);50- },51- },52- onChanged: { addListener: () => {}, removeListener: () => {} },53- },54- runtime: {55- sendMessage: (_msg: unknown, cb?: (r: unknown) => void) => { if (cb) cb({ ok: false, error: "NO_SERVICE_WORKER" }); },56- onMessage: { addListener: () => {}, removeListener: () => {} },57- connect: () => ({ disconnect: () => {} }),58- lastError: null,59- },60- }61- : undefined; // Use real chrome on extension buildssrc/shared/fold.ts+0 −51
@@ -1,51 +0,0 @@1-import { Capacitor, registerPlugin, type PluginListenerHandle } from "@capacitor/core";2-3-export type FoldPosture = "flat" | "tabletop" | "book" | "unknown";4-5-export interface PostureChangedEvent {6- posture: FoldPosture;7- hingeTop: number;8- hingeBottom: number;9- hingeLeft: number;10- hingeRight: number;11- hingeHeight: number;12-}13-14-interface FoldPlugin {15- startMonitoring(): Promise<{ posture: FoldPosture }>;16- stopMonitoring(): Promise<void>;17- addListener(18- event: "postureChanged",19- fn: (e: PostureChangedEvent) => void,20- ): Promise<PluginListenerHandle> | PluginListenerHandle;21-}22-23-const Fold = registerPlugin<FoldPlugin>("Fold");24-25-export async function startFoldMonitoring(26- onChange: (e: PostureChangedEvent) => void,27-): Promise<() => void> {28- if (!Capacitor.isNativePlatform() || Capacitor.getPlatform() !== "android") {29- return () => {};30- }31- let initial: FoldPosture = "flat";32- try {33- const r = await Fold.startMonitoring();34- initial = r.posture;35- } catch {36- return () => {};37- }38- onChange({39- posture: initial,40- hingeTop: 0,41- hingeBottom: 0,42- hingeLeft: 0,43- hingeRight: 0,44- hingeHeight: 0,45- });46- const handle = await Fold.addListener("postureChanged", onChange);47- return () => {48- void handle.remove();49- void Fold.stopMonitoring().catch(() => {});50- };51-}src/shared/in-memory-vault.ts+0 −32
@@ -1,32 +0,0 @@1-import { Capacitor, registerPlugin } from "@capacitor/core";2-import type { VaultEntry } from "./types";3-4-const InMemoryVault = registerPlugin<{5- setEntries(options: { entries: string }): Promise<void>;6- clearEntries(): Promise<void>;7-}>("InMemoryVault");8-9-function credentialEntries(entries: VaultEntry[]) {10- return entries11- .filter((entry) => entry.type === "login" && !entry.deletedAt && entry.password)12- .map((entry) => ({13- id: entry.id,14- type: entry.type,15- title: entry.title,16- username: entry.username ?? "",17- password: entry.password ?? "",18- url: entry.url ?? "",19- }));20-}21-22-export async function publishInMemoryVault(entries: VaultEntry[]): Promise<void> {23- if (!Capacitor.isNativePlatform()) return;24- await InMemoryVault.setEntries({25- entries: JSON.stringify(credentialEntries(entries)),26- });27-}28-29-export async function clearInMemoryVault(): Promise<void> {30- if (!Capacitor.isNativePlatform()) return;31- await InMemoryVault.clearEntries();32-}src/shared/storage.ts+5 −24
@@ -1,35 +1,16 @@1-import { Capacitor } from "@capacitor/core";2-import { Preferences } from "@capacitor/preferences";3-41 export async function storageGet(key: string): Promise<string | null> {5- if (!Capacitor.isNativePlatform()) {6- const result = await chrome.storage.local.get(key);7- return (result as Record<string, string | undefined>)[key] ?? null;8- }9- const { value } = await Preferences.get({ key });10- return value;2+ const result = await chrome.storage.local.get(key);3+ return (result as Record<string, string | undefined>)[key] ?? null;114 }125 136 export async function storageSet(key: string, value: string): Promise<void> {14- if (!Capacitor.isNativePlatform()) {15- await chrome.storage.local.set({ [key]: value });16- return;17- }18- await Preferences.set({ key, value });7+ await chrome.storage.local.set({ [key]: value });198 }209 2110 export async function storageRemove(key: string): Promise<void> {22- if (!Capacitor.isNativePlatform()) {23- await chrome.storage.local.remove(key);24- return;25- }26- await Preferences.remove({ key });11+ await chrome.storage.local.remove(key);2712 }2813 2914 export async function storageClear(): Promise<void> {30- if (!Capacitor.isNativePlatform()) {31- await chrome.storage.local.clear();32- return;33- }34- await Preferences.clear();15+ await chrome.storage.local.clear();3516 }src/shared/sync-bridge.test.ts+0 −44
@@ -1,44 +0,0 @@1-import { describe, expect, it } from "vitest";2-import { peerWebSocketUrl, toDiscoveredPeer } from "./sync-bridge";3-4-describe("sync-bridge helpers", () => {5- const basePeer = {6- deviceId: "p1",7- deviceName: "iPhone",8- host: "192.168.1.10",9- port: 44100,10- x25519PublicKey: "abc",11- pairing: false,12- };13-14- it("toDiscoveredPeer carries wsPort and alreadyPaired", () => {15- const peer = toDiscoveredPeer({ ...basePeer, wsPort: 44101 }, true);16- expect(peer).toEqual({17- deviceId: "p1",18- deviceName: "iPhone",19- host: "192.168.1.10",20- port: 44100,21- wsPort: 44101,22- x25519PublicKey: "abc",23- alreadyPaired: true,24- });25- });26-27- it("toDiscoveredPeer omits wsPort when not advertised", () => {28- const peer = toDiscoveredPeer(basePeer, false);29- expect(peer.wsPort).toBeUndefined();30- expect(peer.alreadyPaired).toBe(false);31- });32-33- it("peerWebSocketUrl builds ws:// URL using wsPort", () => {34- expect(peerWebSocketUrl({ ...basePeer, wsPort: 44101 })).toBe(35- "ws://192.168.1.10:44101/"36- );37- });38-39- it("peerWebSocketUrl throws when wsPort is missing", () => {40- expect(() => peerWebSocketUrl(basePeer)).toThrow(41- "peer did not advertise a WebSocket port"42- );43- });44-});src/shared/sync-bridge.ts+0 −172
@@ -1,172 +0,0 @@1-/**2- * WebSocket sync bridge (Capacitor / Android).3- *4- * Discovers peers via NsdManager (the Kotlin `Sync` plugin) and5- * connects through the WebView's WebSocket API to the peer's `wsport`.6- *7- * The shape is intentionally narrow: peer discovery + a single bidirectional8- * encrypted-frame channel keyed by connection id. Higher layers9- * (`sync-coordinator.ts`) compose `SyncSessionTransport` on top.10- */11-12-import { registerPlugin } from "@capacitor/core";13-import type { PluginListenerHandle } from "@capacitor/core";14-import type { DiscoveredPeer } from "./sync-types";15-import type { SyncSessionResult } from "../background/sync-coordinator";16-17-export interface SyncBridgePeer {18- deviceId: string;19- deviceName: string;20- host: string;21- port: number;22- wsPort?: number;23- x25519PublicKey: string;24- pairing: boolean;25-}26-27-export interface SyncBridge {28- /** Begin discovering `_peach-sync._tcp` peers on the local network. */29- startDiscovery(): Promise<void>;30- stopDiscovery(): Promise<void>;31- onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void;32- onPeerLost(handler: (deviceId: string) => void): () => void;33- /**34- * Sync the full vault blob with a discovered peer. On Capacitor, this opens35- * a WebSocket to the peer's `wsport`, runs Noise NK + auth, and runs a blob36- * sync session as initiator. On the native-messaging path this method is37- * not used — TRIGGER_SYNC drives the equivalent via sync-coordinator.38- */39- syncWithPeer?(peer: DiscoveredPeer): Promise<SyncSessionResult>;40-}41-42-interface SyncPluginShape {43- startDiscovery(): Promise<{ alreadyDiscovering?: boolean } | void>;44- stopDiscovery(): Promise<void>;45- getDiscoveredPeers(): Promise<{ peers: SyncBridgePeer[] }>;46- addListener(47- eventName: "peerDiscovered",48- listener: (peer: SyncBridgePeer) => void49- ): Promise<PluginListenerHandle>;50- addListener(51- eventName: "peerLost",52- listener: (event: { deviceId: string }) => void53- ): Promise<PluginListenerHandle>;54- addListener(55- eventName: "syncError",56- listener: (event: { code: string; errorCode?: number }) => void57- ): Promise<PluginListenerHandle>;58-}59-60-class CapacitorSyncBridge implements SyncBridge {61- private plugin = registerPlugin<SyncPluginShape>("Sync");62- private peerDiscoveredHandlers: Array<(peer: SyncBridgePeer) => void> = [];63- private peerLostHandlers: Array<(deviceId: string) => void> = [];64- private listenerHandles: PluginListenerHandle[] = [];65-66- async startDiscovery(): Promise<void> {67- await this.plugin.startDiscovery();68- if (this.listenerHandles.length === 0) {69- const found = await this.plugin.addListener("peerDiscovered", (peer) => {70- for (const h of this.peerDiscoveredHandlers) h(peer);71- });72- const lost = await this.plugin.addListener("peerLost", (event) => {73- for (const h of this.peerLostHandlers) h(event.deviceId);74- });75- this.listenerHandles.push(found, lost);76- }77- }78-79- async stopDiscovery(): Promise<void> {80- for (const handle of this.listenerHandles) {81- try {82- await handle.remove();83- } catch {84- // listener may already be detached85- }86- }87- this.listenerHandles = [];88- await this.plugin.stopDiscovery();89- }90-91- onPeerDiscovered(handler: (peer: SyncBridgePeer) => void): () => void {92- this.peerDiscoveredHandlers.push(handler);93- return () => {94- const i = this.peerDiscoveredHandlers.indexOf(handler);95- if (i >= 0) this.peerDiscoveredHandlers.splice(i, 1);96- };97- }98-99- onPeerLost(handler: (deviceId: string) => void): () => void {100- this.peerLostHandlers.push(handler);101- return () => {102- const i = this.peerLostHandlers.indexOf(handler);103- if (i >= 0) this.peerLostHandlers.splice(i, 1);104- };105- }106-107- async syncWithPeer(peer: DiscoveredPeer): Promise<SyncSessionResult> {108- const [{ getAndroidSyncIdentity }, { createCapacitorSyncTransport }, { runBlobSyncSession }, { storageGet }] =109- await Promise.all([110- import("./android-sync-identity"),111- import("./capacitor-sync-transport"),112- import("../background/sync-coordinator"),113- import("./storage"),114- ]);115-116- const identity = await getAndroidSyncIdentity();117- const transport = await createCapacitorSyncTransport(118- peer,119- identity.privateKey,120- identity.publicKey,121- );122-123- return runBlobSyncSession("initiator", transport, {124- // Pro gating + unlock state on Android live in the in-memory vault since125- // there is no separate background; the caller may have already gated126- // via UI. Trust the caller — if the blobs are empty the responder will127- // ignore due to sent_at semantics anyway.128- isPro: async () => true,129- isUnlocked: () => true,130- getVaultBlobs: async () => ({131- vault_a: (await storageGet("peach_vault_a")) ?? "",132- vault_b: (await storageGet("peach_vault_b")) ?? "",133- }),134- });135- }136-}137-138-let cached: SyncBridge | null = null;139-140-export function getSyncBridge(): SyncBridge {141- if (cached) return cached;142- cached = new CapacitorSyncBridge();143- return cached;144-}145-146-/** Project a `SyncBridgePeer` into the popup's `DiscoveredPeer` shape. */147-export function toDiscoveredPeer(148- peer: SyncBridgePeer,149- alreadyPaired: boolean150-): DiscoveredPeer {151- return {152- deviceId: peer.deviceId,153- deviceName: peer.deviceName,154- host: peer.host,155- port: peer.port,156- wsPort: peer.wsPort,157- x25519PublicKey: peer.x25519PublicKey,158- alreadyPaired,159- };160-}161-162-/**163- * Build the WebSocket URL that the Capacitor side connects to. Throws when164- * the peer did not advertise a `wsport` — Android cannot fall back to raw165- * TCP from a WebView.166- */167-export function peerWebSocketUrl(peer: SyncBridgePeer): string {168- if (peer.wsPort === undefined) {169- throw new Error("peer did not advertise a WebSocket port");170- }171- return `ws://${peer.host}:${peer.wsPort}/`;172-}