diff --git a/apps/pwa/package.json b/apps/pwa/package.json index 1d92de2c..f4a64932 100644 --- a/apps/pwa/package.json +++ b/apps/pwa/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@libsql/client": "^0.14.0", + "@profullstack/synconfig": "^0.1.1", "@simplewebauthn/browser": "^13.3.0", "@simplewebauthn/server": "^13.1.0", "@xterm/addon-fit": "^0.11.0", diff --git a/apps/pwa/pnpm-lock.yaml b/apps/pwa/pnpm-lock.yaml new file mode 100644 index 00000000..60ba0a4c --- /dev/null +++ b/apps/pwa/pnpm-lock.yaml @@ -0,0 +1,1211 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@libsql/client': + specifier: ^0.14.0 + version: 0.14.0 + '@profullstack/synconfig': + specifier: ^0.1.1 + version: 0.1.1 + '@simplewebauthn/browser': + specifier: ^13.3.0 + version: 13.3.0 + '@simplewebauthn/server': + specifier: ^13.1.0 + version: 13.3.3 + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 + cookie-parser: + specifier: ^1.4.7 + version: 1.4.7 + express: + specifier: ^4.21.2 + version: 4.22.2 + web-push: + specifier: ^3.6.7 + version: 3.6.7 + devDependencies: + '@moshcoder/moshpit-name': + specifier: ^0.4.0 + version: 0.4.0 + +packages: + + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + + '@libsql/client@0.14.0': + resolution: {integrity: sha512-/9HEKfn6fwXB5aTEEoMeFh4CtG0ZzbncBb1e++OCdVpgKZ/xyMsIVYXm0w7Pv4RUel803vE6LwniB3PqD72R0Q==} + + '@libsql/core@0.14.0': + resolution: {integrity: sha512-nhbuXf7GP3PSZgdCY2Ecj8vz187ptHlZQ0VRc751oB2C1W8jQUXKKklvt7t1LJiUTQBVJuadF628eUk+3cRi4Q==} + + '@libsql/darwin-arm64@0.4.7': + resolution: {integrity: sha512-yOL742IfWUlUevnI5PdnIT4fryY3LYTdLm56bnY0wXBw7dhFcnjuA7jrH3oSVz2mjZTHujxoITgAE7V6Z+eAbg==} + cpu: [arm64] + os: [darwin] + + '@libsql/darwin-x64@0.4.7': + resolution: {integrity: sha512-ezc7V75+eoyyH07BO9tIyJdqXXcRfZMbKcLCeF8+qWK5nP8wWuMcfOVywecsXGRbT99zc5eNra4NEx6z5PkSsA==} + cpu: [x64] + os: [darwin] + + '@libsql/hrana-client@0.7.0': + resolution: {integrity: sha512-OF8fFQSkbL7vJY9rfuegK1R7sPgQ6kFMkDamiEccNUvieQ+3urzfDFI616oPl8V7T9zRmnTkSjMOImYCAVRVuw==} + + '@libsql/isomorphic-fetch@0.3.1': + resolution: {integrity: sha512-6kK3SUK5Uu56zPq/Las620n5aS9xJq+jMBcNSOmjhNf/MUvdyji4vrMTqD7ptY7/4/CAVEAYDeotUz60LNQHtw==} + engines: {node: '>=18.0.0'} + + '@libsql/isomorphic-ws@0.1.5': + resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} + + '@libsql/linux-arm64-gnu@0.4.7': + resolution: {integrity: sha512-WlX2VYB5diM4kFfNaYcyhw5y+UJAI3xcMkEUJZPtRDEIu85SsSFrQ+gvoKfcVh76B//ztSeEX2wl9yrjF7BBCA==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-arm64-musl@0.4.7': + resolution: {integrity: sha512-6kK9xAArVRlTCpWeqnNMCoXW1pe7WITI378n4NpvU5EJ0Ok3aNTIC2nRPRjhro90QcnmLL1jPcrVwO4WD1U0xw==} + cpu: [arm64] + os: [linux] + + '@libsql/linux-x64-gnu@0.4.7': + resolution: {integrity: sha512-CMnNRCmlWQqqzlTw6NeaZXzLWI8bydaXDke63JTUCvu8R+fj/ENsLrVBtPDlxQ0wGsYdXGlrUCH8Qi9gJep0yQ==} + cpu: [x64] + os: [linux] + + '@libsql/linux-x64-musl@0.4.7': + resolution: {integrity: sha512-nI6tpS1t6WzGAt1Kx1n1HsvtBbZ+jHn0m7ogNNT6pQHZQj7AFFTIMeDQw/i/Nt5H38np1GVRNsFe99eSIMs9XA==} + cpu: [x64] + os: [linux] + + '@libsql/win32-x64-msvc@0.4.7': + resolution: {integrity: sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw==} + cpu: [x64] + os: [win32] + + '@moshcoder/moshpit-name@0.4.0': + resolution: {integrity: sha512-9qRHakrYNMCmIBJ6qIHnXwzN471Mq36mHYRHkuy2Ixpha4A9HzYC73mLOuNqcOkp7UsxUhnl0mPZfbEjejlJvw==} + engines: {node: '>=18'} + hasBin: true + + '@neon-rs/load@0.0.4': + resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} + + '@peculiar/asn1-android@2.9.4': + resolution: {integrity: sha512-SYHm4SoWSI0nRCoos6jpGusIqhPH9bbGBqv7ohlZ+H6BunrDzzQPk2ePgDuEUzV82OdvbLgtW4twUDwhU9P3YQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-cms@2.9.4': + resolution: {integrity: sha512-cben7oxmQsUGZqotus7yt0srYdncOT6RNWcTQ77T2RFOXejYVYkXadrfePdRcrVpO9K95IRLKKglG2k38jKXuw==} + engines: {node: '>=14'} + + '@peculiar/asn1-csr@2.9.4': + resolution: {integrity: sha512-xd4YN4vpRjkDAQWVfZZkeu12IEND7DOpkqaHSIHxZl1uggUNa9Ju0QxY2jHvDAS9pP0zhRBytg8ifsnGo3V0jw==} + engines: {node: '>=14'} + + '@peculiar/asn1-ecc@2.9.4': + resolution: {integrity: sha512-JJXefFshRAuVAjWQo/39bkg1ywc1VaiO44S8RRC+Ykvf/u2KDmYffoDb0ZBPCR5uJy4AGKQhl8mX+Q8ShcWaXQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-pfx@2.9.4': + resolution: {integrity: sha512-khuGzHTzNzk4GDlIBEILyIs6Lce0yn0ZBdoI9v93kmNncfZRhD+AQ5ODFqdhvoE8cMJF/JMTQ8yA+t1D14kqCw==} + engines: {node: '>=14'} + + '@peculiar/asn1-pkcs8@2.9.4': + resolution: {integrity: sha512-duRdotlUx9eDZe6QrQpQKl61RbWykCHBCkKayP8V8XdEFwlKHZ8qGGDMyS6Pye7OX7nLFttTTpRkJeet78ckwQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-pkcs9@2.9.4': + resolution: {integrity: sha512-kaL4cNxBpdQE2dKlyZBqz4ygCrwffO+8wfoxTEqM1Z8RadvCeELBRzcv0dzM8aY9azHMwODO5nxU65zXmhToOQ==} + engines: {node: '>=14'} + + '@peculiar/asn1-rsa@2.9.4': + resolution: {integrity: sha512-pZ96eD1PptovcWQ/GSmuNFXd/7EQJNlKfDaNCyE2rx3W0v6QFelkzquVqRSRyyDXXCYD69ZXJDzZ8GhIiQzKoA==} + engines: {node: '>=14'} + + '@peculiar/asn1-schema@2.9.4': + resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + engines: {node: '>=14'} + + '@peculiar/asn1-x509-attr@2.9.4': + resolution: {integrity: sha512-ehQXbpQaQYycgu8OrvigwSPTFfVRcu0ECNYCWw+yzBp02Lw5paRqzzhUpfOgO2K38+WfFZuEz/0RPtam5g0OMg==} + engines: {node: '>=14'} + + '@peculiar/asn1-x509@2.9.4': + resolution: {integrity: sha512-CxhBo/RdEbMMob7T31ZdQjGuoyRFLVwrDzTn25bihzBasRg9kRm/0IxIPvhgQtcK/9dNcO1XQL2fuPugwELL0Q==} + engines: {node: '>=14'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/x509@1.14.3': + resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} + engines: {node: '>=20.0.0'} + + '@profullstack/synconfig@0.1.1': + resolution: {integrity: sha512-gX/tZ0j0QShtAf2/Mpr40Fev0h7HVoh5jl3tVbFVrSfuQHalthiwVgZ8lQKzhv+0kpxvVex4qkT8xMFHSopklA==} + engines: {node: '>=22'} + + '@simplewebauthn/browser@13.3.0': + resolution: {integrity: sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==} + + '@simplewebauthn/server@13.3.3': + resolution: {integrity: sha512-LelX/lcy5cjc15A86i/aNxHhB5eU7dd20QsbP0VLAf9e38+SLlsnqCCyecx3xqfGofhmX05h1J9fKRYWxw+luA==} + engines: {node: '>=20.0.0'} + + '@types/node@22.20.2': + resolution: {integrity: sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + + bn.js@4.12.5: + resolution: {integrity: sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==} + + body-parser@1.20.8: + resolution: {integrity: sha512-JNcyFQ64OiijEkPzUBTCe+hyPXUD/3LEldGQ6iF5LR1w00mx9o7xtDWHXBY2iItjdCFGoilOLNQbH943ut7pHA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-parser@1.4.7: + resolution: {integrity: sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==} + engines: {node: '>= 0.8.0'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.22.2: + resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} + engines: {node: '>= 0.10.0'} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + js-base64@3.9.3: + resolution: {integrity: sha512-uwYQp+VJ38FVvtim6qNbit6e9uT6dwWQ4Y1+H9TxhW5hcHjpHwoxlR0nMpqUmIFOmu4VqMxwdJA88gIVuZJQ/g==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + libsql@0.4.7: + resolution: {integrity: sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw==} + cpu: [x64, arm64, wasm32] + os: [darwin, linux, win32] + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.13: + resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + + promise-limit@2.7.0: + resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.2.0: + resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} + engines: {node: '>=16.0.0'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsyringe@4.10.0: + resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} + engines: {node: '>= 6.0.0'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + +snapshots: + + '@hexagon/base64@1.1.28': {} + + '@levischuck/tiny-cbor@0.2.11': {} + + '@libsql/client@0.14.0': + dependencies: + '@libsql/core': 0.14.0 + '@libsql/hrana-client': 0.7.0 + js-base64: 3.9.3 + libsql: 0.4.7 + promise-limit: 2.7.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/core@0.14.0': + dependencies: + js-base64: 3.9.3 + + '@libsql/darwin-arm64@0.4.7': + optional: true + + '@libsql/darwin-x64@0.4.7': + optional: true + + '@libsql/hrana-client@0.7.0': + dependencies: + '@libsql/isomorphic-fetch': 0.3.1 + '@libsql/isomorphic-ws': 0.1.5 + js-base64: 3.9.3 + node-fetch: 3.3.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/isomorphic-fetch@0.3.1': {} + + '@libsql/isomorphic-ws@0.1.5': + dependencies: + '@types/ws': 8.18.1 + ws: 8.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@libsql/linux-arm64-gnu@0.4.7': + optional: true + + '@libsql/linux-arm64-musl@0.4.7': + optional: true + + '@libsql/linux-x64-gnu@0.4.7': + optional: true + + '@libsql/linux-x64-musl@0.4.7': + optional: true + + '@libsql/win32-x64-msvc@0.4.7': + optional: true + + '@moshcoder/moshpit-name@0.4.0': {} + + '@neon-rs/load@0.0.4': {} + + '@peculiar/asn1-android@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-cms@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-x509-attr': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-csr@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pfx@2.9.4': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-pkcs8': 2.9.4 + '@peculiar/asn1-rsa': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs8@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-pkcs9@2.9.4': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-pfx': 2.9.4 + '@peculiar/asn1-pkcs8': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/asn1-x509-attr': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.9.4': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509-attr@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.9.4': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/x509@1.14.3': + dependencies: + '@peculiar/asn1-cms': 2.9.4 + '@peculiar/asn1-csr': 2.9.4 + '@peculiar/asn1-ecc': 2.9.4 + '@peculiar/asn1-pkcs9': 2.9.4 + '@peculiar/asn1-rsa': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + pvtsutils: 1.3.6 + reflect-metadata: 0.2.2 + tslib: 2.8.1 + tsyringe: 4.10.0 + + '@profullstack/synconfig@0.1.1': {} + + '@simplewebauthn/browser@13.3.0': {} + + '@simplewebauthn/server@13.3.3': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.9.4 + '@peculiar/asn1-ecc': 2.9.4 + '@peculiar/asn1-rsa': 2.9.4 + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/asn1-x509': 2.9.4 + '@peculiar/x509': 1.14.3 + + '@types/node@22.20.2': + dependencies: + undici-types: 6.21.0 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.20.2 + + '@xterm/addon-fit@0.11.0': {} + + '@xterm/xterm@6.0.0': {} + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + agent-base@7.1.4: {} + + array-flatten@1.1.1: {} + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.5 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + bn.js@4.12.5: {} + + body-parser@1.20.8: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.16.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-parser@1.4.7: + dependencies: + cookie: 0.7.2 + cookie-signature: 1.0.6 + + cookie-signature@1.0.6: {} + + cookie-signature@1.0.7: {} + + cookie@0.7.2: {} + + data-uri-to-buffer@4.0.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + detect-libc@2.0.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.22.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.8 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.13 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http_ece@1.2.0: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + js-base64@3.9.3: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + libsql@0.4.7: + dependencies: + '@neon-rs/load': 0.0.4 + detect-libc: 2.0.2 + optionalDependencies: + '@libsql/darwin-arm64': 0.4.7 + '@libsql/darwin-x64': 0.4.7 + '@libsql/linux-arm64-gnu': 0.4.7 + '@libsql/linux-arm64-musl': 0.4.7 + '@libsql/linux-x64-gnu': 0.4.7 + '@libsql/linux-x64-musl': 0.4.7 + '@libsql/win32-x64-msvc': 0.4.7 + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + minimalistic-assert@1.0.1: {} + + minimist@1.2.8: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + node-domexception@1.0.0: {} + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.13: {} + + promise-limit@2.7.0: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.2.0: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + qs@6.16.0: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + reflect-metadata@0.2.2: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsyringe@4.10.0: + dependencies: + tslib: 1.14.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + undici-types@6.21.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + web-push@3.6.7: + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.6 + jws: 4.0.1 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color + + web-streams-polyfill@3.3.3: {} + + ws@8.21.3: {} diff --git a/apps/pwa/src/routes/settings-sync.mjs b/apps/pwa/src/routes/settings-sync.mjs index 0047eb82..70f223da 100644 --- a/apps/pwa/src/routes/settings-sync.mjs +++ b/apps/pwa/src/routes/settings-sync.mjs @@ -15,6 +15,14 @@ // no name in it is a path traversal, and that two machines saving at once cannot // silently overwrite each other. import { Router } from "express"; +import { + KEEP_REVISIONS as PACKAGE_KEEP_REVISIONS, + digestSnapshot as digestPackageSnapshot, + handleGet, + handlePut, + handleRevisions, + snapshotProblem as packageSnapshotProblem, +} from "@profullstack/synconfig/server"; import { get, all, run } from "../db.mjs"; import { id, sha256 } from "../lib/crypto.mjs"; import { bearer, userForApiKey } from "../lib/apikey.mjs"; @@ -31,57 +39,33 @@ export const settingsSyncRouter = Router(); * the page and pressing a button, few enough that a scripted save loop can't * grow one account's row count without bound. */ -export const KEEP_REVISIONS = 10; +export const KEEP_REVISIONS = PACKAGE_KEEP_REVISIONS; /** Total snapshot size, and how many files one may carry. */ export const MAX_SNAPSHOT_BYTES = 256 * 1024; export const MAX_FILES = 32; +/** The caps as @profullstack/synconfig takes them. Per-file matches the CLI's MAX_FILE_BYTES. */ +const LIMITS = { maxFileBytes: 64 * 1024, maxTotalBytes: MAX_SNAPSHOT_BYTES, maxFiles: MAX_FILES }; + /** * Is this shaped like a snapshot? Returns null when it is, else the reason. * - * Structural only — see the header. The one substantive rule is on names: a - * snapshot is applied by writing its keys as paths under ~/.moshcode, so a name - * carrying `..`, a leading slash, a backslash or a NUL has no honest reading and - * is refused at the door rather than stored for a client to refuse later. + * Structural only, and the package's: the body is small, it is shaped like a + * snapshot, no name in it is a path traversal. moshcode's own list of which + * files sync stays in the CLI, so a release that syncs one more file needs + * no deploy here. */ export function snapshotProblem(snapshot, { maxBytes = MAX_SNAPSHOT_BYTES } = {}) { - if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return "not a snapshot"; - const files = snapshot.files; - if (!files || typeof files !== "object" || Array.isArray(files)) return "no files in the snapshot"; - const names = Object.keys(files); - if (!names.length) return "no files in the snapshot"; - if (names.length > MAX_FILES) return `too many files (${names.length}, the cap is ${MAX_FILES})`; - for (const name of names) { - if (!name || name.length > 200) return "a file name is empty or absurdly long"; - if (name.startsWith("/") || name.includes("..") || name.includes("\\") || name.includes("\0")) { - return `"${name}" is not a settings path`; - } - if (typeof files[name]?.content !== "string") return `"${name}" has no contents`; - } - const bytes = Buffer.byteLength(JSON.stringify(snapshot)); - if (bytes > maxBytes) return `snapshot is ${bytes} bytes — the cap is ${maxBytes}`; - return null; + return packageSnapshotProblem(snapshot, { ...LIMITS, maxTotalBytes: maxBytes }); } /** * The digest the CLI computes, recomputed here so the stored one is ours. - * - * Byte-for-byte the same construction as `digestFiles` in - * src/settings-sync.mjs: names sorted, each field framed by a NUL and preceded - * by its byte length. Two implementations of one hash is a drift risk, so both - * sides pin the digest of a fixed input in their tests — this one in - * apps/pwa/test/settings-sync.test.mjs, the CLI's in test/settings-sync.test.mjs - * — and any change to either framing fails both. + * Both sides are the package now; the pinned fixture in both suites stays. */ export function digestSnapshot(snapshot) { - const files = snapshot.files; - let material = ""; - for (const name of Object.keys(files).sort()) { - const content = String(files[name].content); - material += `${name}\0${Buffer.byteLength(content)}\0${content}\0`; - } - return sha256(material); + return digestPackageSnapshot(snapshot); } async function cliAuth(req, res, next) { @@ -109,154 +93,116 @@ export async function latestSnapshotMeta(userId) { : null; } -/** An older body promoted to a new revision, or a fresh save — one code path. */ +const shape = (row) => ({ + revision: Number(row.revision), + digest: row.digest, + host: row.host, + version: row.version, + size: Number(row.size), + body: JSON.parse(row.body), + savedAt: Number(row.created_at), +}); + +/** + * The package's SnapshotStore over this app's database. + * + * The revision is chosen inside the INSERT, and `ifRevision` is checked there + * too, by a HAVING on the same aggregate. Reading MAX(revision) first and then + * inserting is not enough against a network database: both requests see the + * same maximum and one save silently replaces the other. Here the second one + * either trips the HAVING (no row inserted — a conflict we can report) or the + * unique index (an error we retry as a conflict). + * + * GROUP BY user_id is not decoration. A bare HAVING on an implicit + * single-group aggregate is accepted by the SQLite that backs a `file:` + * database and *rejected by Turso's parser*: + * + * SQL string could not be parsed: near HAVING, "None": syntax error + * + * So the unconditional insert takes the branch with no HAVING at all. + */ +export const snapshotStore = { + async latest(userId) { + const row = await latest(userId); + return row ? shape(row) : null; + }, + + async insert(userId, entry, ifRevision) { + const body = JSON.stringify(entry.body); + const row = { id: id(), created_at: Date.now() }; + const sql = ifRevision === null + ? `INSERT INTO settings_snapshots (id,user_id,revision,digest,host,version,size,body,created_at) + SELECT ?, ?, COALESCE(MAX(revision),0) + 1, ?, ?, ?, ?, ?, ? + FROM settings_snapshots WHERE user_id = ? + RETURNING revision` + : `INSERT INTO settings_snapshots (id,user_id,revision,digest,host,version,size,body,created_at) + SELECT ?, ?, COALESCE(MAX(revision),0) + 1, ?, ?, ?, ?, ?, ? + FROM settings_snapshots WHERE user_id = ? + GROUP BY user_id + HAVING COALESCE(MAX(revision),0) = ? + RETURNING revision`; + const args = [row.id, userId, entry.digest, entry.host, entry.version, entry.size, body, row.created_at, userId]; + if (ifRevision !== null) args.push(ifRevision); + + let inserted; + try { inserted = await get(sql, args); } + catch (e) { + if (/UNIQUE|constraint/i.test(String(e?.message || e))) inserted = null; + else throw e; + } + if (!inserted) { + const current = await latest(userId); + return { conflict: true, revision: current ? Number(current.revision) : 0 }; + } + const revision = Number(inserted.revision); + // Prune by revision rather than by count: the index makes this one range + // delete, and it cannot race with a concurrent save the way "delete all but + // the newest N" can. + await run(`DELETE FROM settings_snapshots WHERE user_id = ? AND revision <= ?`, + [userId, revision - KEEP_REVISIONS]); + return { revision, savedAt: row.created_at }; + }, + + async list(userId, limit) { + const rows = await all( + `SELECT revision, digest, host, version, size, created_at FROM settings_snapshots + WHERE user_id = ? ORDER BY revision DESC LIMIT ?`, + [userId, limit] + ); + return rows.map((r) => ({ + revision: Number(r.revision), + digest: r.digest, + host: r.host, + version: r.version, + size: Number(r.size), + savedAt: Number(r.created_at), + })); + }, +}; + +/** An older body promoted to a new revision: the store's insert, unconditional. */ async function insertRevision({ userId, body, digest, host, version, ifRevision }) { - const size = Buffer.byteLength(body); - const row = { id: id(), created_at: Date.now() }; - - // The revision is chosen inside the INSERT, and `ifRevision` is checked there - // too, by a HAVING on the same aggregate. Reading MAX(revision) first and then - // inserting is not enough against a network database: both requests see the - // same maximum and one save silently replaces the other. Here the second one - // either trips the HAVING (no row inserted — a conflict we can report) or the - // unique index (an error we retry as a conflict). - const sql = ifRevision === null - ? `INSERT INTO settings_snapshots (id,user_id,revision,digest,host,version,size,body,created_at) - SELECT ?, ?, COALESCE(MAX(revision),0) + 1, ?, ?, ?, ?, ?, ? - FROM settings_snapshots WHERE user_id = ? - RETURNING revision` - // GROUP BY user_id is not decoration. A bare HAVING on an implicit - // single-group aggregate is accepted by the SQLite that backs a `file:` - // database and *rejected by Turso's parser*: - // - // SQL string could not be parsed: near HAVING, "None": syntax error - // - // So this statement worked in every test and threw on every deployment, - // which is what `/save` returning 502 actually was. `--force` sends - // ifRevision: null and takes the branch above, which is why forcing was the - // only way to save. The rows always exist here — the caller sets ifRevision - // to null when the account has no current revision — so grouping by the - // user cannot lose the row the HAVING is meant to test. - : `INSERT INTO settings_snapshots (id,user_id,revision,digest,host,version,size,body,created_at) - SELECT ?, ?, COALESCE(MAX(revision),0) + 1, ?, ?, ?, ?, ?, ? - FROM settings_snapshots WHERE user_id = ? - GROUP BY user_id - HAVING COALESCE(MAX(revision),0) = ? - RETURNING revision`; - const args = [row.id, userId, digest, host, version, size, body, row.created_at, userId]; - if (ifRevision !== null) args.push(ifRevision); - - let inserted; - try { inserted = await get(sql, args); } - catch (e) { - // The unique index fired: another save took this revision between our - // aggregate and our insert. Same answer as a failed HAVING. - if (/UNIQUE|constraint/i.test(String(e?.message || e))) return { conflict: true }; - throw e; - } - if (!inserted) return { conflict: true }; - - const revision = Number(inserted.revision); - // Prune by revision rather than by count: the index makes this one range - // delete, and it cannot race with a concurrent save the way "delete all but - // the newest N" can. - await run(`DELETE FROM settings_snapshots WHERE user_id = ? AND revision <= ?`, - [userId, revision - KEEP_REVISIONS]); - return { revision, digest, savedAt: row.created_at, size }; + return snapshotStore.insert(userId, { digest, host, version, size: Buffer.byteLength(body), body: JSON.parse(body) }, ifRevision); } +/** Where moshcode keeps its version in a snapshot; `app` is what newer clients also send. */ +const versionOf = (snapshot) => (snapshot.moshcode ? String(snapshot.moshcode).slice(0, 20) : snapshot.app ? String(snapshot.app).slice(0, 20) : null); + /* --------------------------------------------------------------- CLI (Bearer) */ settingsSyncRouter.put("/api/settings", cliAuth, async (req, res) => { - const snapshot = req.body?.snapshot; - const problem = snapshotProblem(snapshot); - if (problem) return res.status(400).json({ error: problem }); - - // `ifRevision` absent or null means "save regardless" — `/save --force`, or a - // machine that has never synced. A number means "only if the account is still - // where I left it". - const asked = req.body?.ifRevision; - let ifRevision = asked === null || asked === undefined ? null : Number(asked); - if (ifRevision !== null && !Number.isSafeInteger(ifRevision)) { - return res.status(400).json({ error: "ifRevision must be an integer or null" }); - } - - const digest = digestSnapshot(snapshot); - const current = await latest(req.apiUser.id); - - // Byte-identical to what is already current: answer with that revision and - // insert nothing. The check lives here rather than in the CLI because only the - // account knows what it holds — a CLI that skipped the request on the strength - // of its own marker reported "already saved" to someone who had just deleted - // everything from the web, and left them stuck behind a --force. - if (current && current.digest === digest) { - return res.json({ - revision: Number(current.revision), - digest, - savedAt: Number(current.created_at), - unchanged: true, - }); - } - - // A precondition against an account with nothing in it cannot be protecting - // anything: the revisions it names are gone (forgotten from the web), so there - // is no other machine's save to lose. Refusing here would strand every machine - // behind --force after a perfectly deliberate delete. - if (!current) ifRevision = null; - - const result = await insertRevision({ - userId: req.apiUser.id, - body: JSON.stringify(snapshot), - digest, - host: snapshot.host ? String(snapshot.host).slice(0, 60) : null, - version: snapshot.moshcode ? String(snapshot.moshcode).slice(0, 20) : null, - ifRevision, - }); - - if (result.conflict) { - const current = await latest(req.apiUser.id); - return res.status(409).json({ - error: "the account has been saved from another machine since then", - revision: current ? Number(current.revision) : 0, - }); - } - res.json({ revision: result.revision, digest: result.digest, savedAt: result.savedAt }); + const reply = await handlePut(snapshotStore, req.apiUser.id, req.body, { limits: LIMITS, versionOf }); + res.status(reply.status).json(reply.body); }); settingsSyncRouter.get("/api/settings", cliAuth, async (req, res) => { - const row = await latest(req.apiUser.id); - if (!row) return res.status(404).json({ error: "nothing saved yet" }); - let snapshot; - // Stored as the CLI sent it, so a body that will not parse is a bug on this - // side of the wire — say so rather than handing the CLI a 200 it can't use. - try { snapshot = JSON.parse(row.body); } - catch { return res.status(500).json({ error: "the stored snapshot is unreadable" }); } - res.json({ - revision: Number(row.revision), - digest: row.digest, - savedAt: Number(row.created_at), - host: row.host, - version: row.version, - snapshot, - }); + const reply = await handleGet(snapshotStore, req.apiUser.id); + res.status(reply.status).json(reply.body); }); settingsSyncRouter.get("/api/settings/revisions", cliAuth, async (req, res) => { - const rows = await all( - `SELECT revision, digest, host, version, size, created_at FROM settings_snapshots - WHERE user_id = ? ORDER BY revision DESC`, - [req.apiUser.id] - ); - res.json({ - revisions: rows.map((r) => ({ - revision: Number(r.revision), - digest: r.digest, - host: r.host, - version: r.version, - size: Number(r.size), - savedAt: Number(r.created_at), - })), - }); + const reply = await handleRevisions(snapshotStore, req.apiUser.id, KEEP_REVISIONS); + res.status(reply.status).json(reply.body); }); /* ------------------------------------------------------------ human (cookies) */ diff --git a/package.json b/package.json index a3846bad..acddf577 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "moshcode", - "version": "0.95.0", + "version": "0.96.0", "type": "module", - "description": "moshcode \u2014 a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript", + "description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript", "repository": { "type": "git", "url": "git+https://github.com/moshcoder/moshcode.git" @@ -34,5 +34,8 @@ "packageManager": "pnpm@10.32.1", "devDependencies": { "@moshcoder/moshpit-dns": "^0.5.0" + }, + "dependencies": { + "@profullstack/synconfig": "^0.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96767ea6..dfb0b66c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,10 @@ settings: importers: .: + dependencies: + '@profullstack/synconfig': + specifier: ^0.1.1 + version: 0.1.1 devDependencies: '@moshcoder/moshpit-dns': specifier: ^0.5.0 @@ -19,6 +23,12 @@ packages: engines: {node: '>=20'} hasBin: true + '@profullstack/synconfig@0.1.1': + resolution: {integrity: sha512-gX/tZ0j0QShtAf2/Mpr40Fev0h7HVoh5jl3tVbFVrSfuQHalthiwVgZ8lQKzhv+0kpxvVex4qkT8xMFHSopklA==} + engines: {node: '>=22'} + snapshots: '@moshcoder/moshpit-dns@0.5.0': {} + + '@profullstack/synconfig@0.1.1': {} diff --git a/src/settings-sync.mjs b/src/settings-sync.mjs index 24a1bd63..199f7393 100644 --- a/src/settings-sync.mjs +++ b/src/settings-sync.mjs @@ -27,13 +27,30 @@ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { + SNAPSHOT_VERSION as PACKAGE_SNAPSHOT_VERSION, + applyFiles as applySnapshotFiles, + collectSnapshot as collectPolicyFiles, + digestFiles as digestPolicyFiles, + isSyncable as policyAllows, + planApply as planPolicyApply, + validateSnapshot as validatePolicySnapshot, +} from "@profullstack/synconfig"; import { loadCreds } from "./auth.mjs"; import { engineStatus } from "./engines.mjs"; import { toolStatus } from "./tools.mjs"; import { ash, moshcodeVersion } from "./ui.mjs"; -/** The snapshot shape this build writes and is willing to read. */ -export const SNAPSHOT_VERSION = 1; +/** + * The snapshot shape this build writes and is willing to read. + * + * Since 0.96 the primitives here (the allowlist check, collecting, validating, + * planning, applying, the digest) are @profullstack/synconfig, the package + * this file was extracted into so myna and the rest could sync the same way. + * What stays here is moshcode's own: which files, the marker, the transport, + * and the two verbs with their messages. + */ +export const SNAPSHOT_VERSION = PACKAGE_SNAPSHOT_VERSION; /** Owner-only, like everything else moshcode keeps under ~/.moshcode. */ const FILE_MODE = 0o600; @@ -164,13 +181,24 @@ export const NEVER_SYNCED_PREFIXES = [ */ export const NEVER_SYNCED_SUFFIXES = [".transcript", ".stdin", ".exit", ".sock", ".pid", ".log"]; +/** + * The three lists above as the policy @profullstack/synconfig applies, on the + * way out and on the way in. The server keeps its own copy of the size caps + * (apps/pwa/src/routes/settings-sync.mjs) and never this list. + */ +export const SYNC_POLICY = { + files: SYNCED_FILES.map(({ path: file, json, label }) => ({ path: file, json, label })), + never: NEVER_SYNCED, + neverPrefixes: NEVER_SYNCED_PREFIXES.map((prefix) => prefix.replace(/\/+$/, "")), + neverSuffixes: NEVER_SYNCED_SUFFIXES, + maxFileBytes: MAX_FILE_BYTES, + maxTotalBytes: MAX_TOTAL_BYTES, + maxFiles: 32, +}; + /** True for a path this build is willing to read or write. */ export function isSyncable(relative) { - const name = String(relative ?? ""); - if (NEVER_SYNCED.includes(name)) return false; - if (NEVER_SYNCED_PREFIXES.some((prefix) => name.startsWith(prefix))) return false; - if (NEVER_SYNCED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return false; - return SYNCED_FILES.some((f) => f.path === name); + return policyAllows(SYNC_POLICY, String(relative ?? "")); } export function moshcodeDir(home = os.homedir()) { @@ -208,26 +236,12 @@ export function saveMarker(marker, home = os.homedir()) { /** * The digest of a set of files, over their names and contents. * - * Canonical by construction — names sorted, every field framed by a NUL and - * preceded by its byte length — so the same files digest the same on every - * machine regardless of the order they were read in, and no content can be - * arranged to look like a different file list. NUL rather than a space because a - * space appears in file contents and a NUL does not appear in text config at - * all. - * - * The app computes the same digest over the same bytes - * (apps/pwa/src/routes/settings-sync.mjs). Both sides pin the value for a fixed - * input in their tests, because two implementations of one hash that quietly - * disagree is a comparison that silently stops meaning anything. + * @profullstack/synconfig's construction: names sorted, every field framed by + * a NUL and preceded by its byte length. The app computes the same through + * the package's server half, and both suites still pin the value for a fixed + * input, because that is the catch that found the drift the first time. */ -export function digestFiles(files) { - const hash = crypto.createHash("sha256"); - for (const name of Object.keys(files).sort()) { - const content = String(files[name]?.content ?? ""); - hash.update(`${name}\0${Buffer.byteLength(content)}\0${content}\0`); - } - return hash.digest("hex"); -} +export const digestFiles = digestPolicyFiles; /** Engines and tools this machine has, by name. Informational, never applied. */ function installedHere() { @@ -251,41 +265,23 @@ export function collectSnapshot({ version = moshcodeVersion(), installed = installedHere(), } = {}) { - const dir = moshcodeDir(home); - const files = {}; - const included = []; - const skipped = []; - let total = 0; - - for (const entry of SYNCED_FILES) { - const file = path.join(dir, entry.path); - let content; - try { content = fs.readFileSync(file, "utf8"); } - catch { continue; } // not here — nothing to say about it - const bytes = Buffer.byteLength(content); - if (bytes > MAX_FILE_BYTES) { - skipped.push({ path: entry.path, reason: `${bytes} bytes — the cap is ${MAX_FILE_BYTES}` }); - continue; - } - if (entry.json) { - try { JSON.parse(content); } - catch { skipped.push({ path: entry.path, reason: "not valid JSON — fix it locally first" }); continue; } - } - if (total + bytes > MAX_TOTAL_BYTES) { - skipped.push({ path: entry.path, reason: "the snapshot is already at its size cap" }); - continue; - } - total += bytes; - files[entry.path] = { content }; - included.push({ path: entry.path, bytes, label: entry.label }); - } - + const host = String(hostname || "").slice(0, 60); + const { snapshot: collected, skipped } = collectPolicyFiles(moshcodeDir(home), SYNC_POLICY, { + host, + app: version ? `moshcode ${version}` : "moshcode", + }); + const included = Object.keys(collected.files).map((name) => ({ + path: name, + bytes: Buffer.byteLength(collected.files[name].content), + label: SYNCED_FILES.find((f) => f.path === name)?.label, + })); const snapshot = { version: SNAPSHOT_VERSION, - host: String(hostname || "").slice(0, 60) || null, + host: host || null, moshcode: version || null, + app: collected.app, installed, - files, + files: collected.files, }; return { snapshot, included, skipped }; } @@ -302,45 +298,19 @@ export function validateSnapshot(snapshot) { if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) { return { ok: false, error: "the saved settings are not a snapshot", files: {}, rejected: [] }; } - if (Number(snapshot.version) > SNAPSHOT_VERSION) { + const { files, rejected, newer } = validatePolicySnapshot(snapshot, SYNC_POLICY); + if (newer) { return { ok: false, files: {}, rejected: [], - error: `these settings were saved by a newer moshcode (snapshot v${snapshot.version}) — run \`moshcode upgrade\` first`, + error: `these settings were saved by a newer moshcode (snapshot v${newer}) — run \`moshcode upgrade\` first`, }; } const raw = snapshot.files; if (!raw || typeof raw !== "object" || Array.isArray(raw)) { return { ok: false, error: "the snapshot carries no files", files: {}, rejected: [] }; } - - const files = {}; - const rejected = []; - let total = 0; - for (const [name, value] of Object.entries(raw)) { - // Every reason a name can be refused, in one place. `isSyncable` is the - // allowlist; the checks around it catch the shapes that never reach it — - // an absolute path, a traversal, a non-string body. - if (typeof name !== "string" || !name || name !== path.posix.normalize(name) - || path.posix.isAbsolute(name) || name.includes("..") || name.includes("\\")) { - rejected.push({ path: String(name), reason: "not a settings path" }); - continue; - } - if (!isSyncable(name)) { rejected.push({ path: name, reason: "this moshcode does not sync that file" }); continue; } - const content = value?.content; - if (typeof content !== "string") { rejected.push({ path: name, reason: "no contents" }); continue; } - const bytes = Buffer.byteLength(content); - if (bytes > MAX_FILE_BYTES) { rejected.push({ path: name, reason: `${bytes} bytes — the cap is ${MAX_FILE_BYTES}` }); continue; } - if (total + bytes > MAX_TOTAL_BYTES) { rejected.push({ path: name, reason: "past the snapshot size cap" }); continue; } - const entry = SYNCED_FILES.find((f) => f.path === name); - if (entry?.json) { - try { JSON.parse(content); } - catch { rejected.push({ path: name, reason: "not valid JSON — refusing to write it" }); continue; } - } - total += bytes; - files[name] = { content }; - } return { ok: true, error: null, files, rejected }; } @@ -351,34 +321,25 @@ export function validateSnapshot(snapshot) { * same plan, and so "nothing to do" is an answer rather than four no-op writes. */ export function planApply(files, { home = os.homedir() } = {}) { - const dir = moshcodeDir(home); - return Object.keys(files).sort().map((name) => { - let current = null; - try { current = fs.readFileSync(path.join(dir, name), "utf8"); } catch { /* absent */ } - const content = files[name].content; - return { - path: name, - action: current === null ? "new" : current === content ? "same" : "changed", - bytes: Buffer.byteLength(content), - }; - }); + return planPolicyApply(moshcodeDir(home), files).map((entry) => ({ + path: entry.path, + action: entry.status, + bytes: Buffer.byteLength(files[entry.path].content), + })); } /** Write the snapshot's files. Returns the plan, with `written` marked. */ export function applyFiles(files, { home = os.homedir() } = {}) { - const dir = moshcodeDir(home); const plan = planApply(files, { home }); + const written = new Set( + applySnapshotFiles( + moshcodeDir(home), + files, + plan.map((item) => ({ path: item.path, status: item.action })), + ), + ); for (const item of plan) { - if (item.action === "same") continue; - const file = path.join(dir, item.path); - fs.mkdirSync(path.dirname(file), { recursive: true, mode: DIR_MODE }); - // Written beside the target and renamed over it: a settings file truncated - // by a full disk halfway through a write is a prompt that no longer starts. - const temp = `${file}.${process.pid}.tmp`; - fs.writeFileSync(temp, files[item.path].content, { mode: FILE_MODE }); - fs.renameSync(temp, file); - try { fs.chmodSync(file, FILE_MODE); } catch { /* best effort */ } - item.written = true; + if (written.has(item.path)) item.written = true; } return plan; }