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
18 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 −172
manifest.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": {

view file

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",

view file

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",

view file

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"

view file

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; }

view file

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-}

view file at the parent

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-}

view file at the parent

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 }

view file

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-}

view file at the parent

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-}

view file at the parent

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-};

view file at the parent

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;

view file at the parent

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 builds

view file at the parent

src/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-}

view file at the parent

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-}

view file at the parent

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 }

view file

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-});

view file at the parent

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-}

view file at the parent