fix
All checks were successful
Deploy Encrypted Todo App / build-and-push (push) Successful in 2m36s

This commit is contained in:
2025-06-16 10:03:41 -06:00
parent a87a0bf6f8
commit 82b4892fda
4 changed files with 573 additions and 311 deletions

View File

@ -3,6 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@libsql/client": "^0.15.9",
"@signalapp/libsignal-client": "^0.74.1", "@signalapp/libsignal-client": "^0.74.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",

232
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@libsql/client':
specifier: ^0.15.9
version: 0.15.9
'@signalapp/libsignal-client': '@signalapp/libsignal-client':
specifier: ^0.74.1 specifier: ^0.74.1
version: 0.74.1 version: 0.74.1
@ -26,9 +29,79 @@ importers:
packages: packages:
'@libsql/client@0.15.9':
resolution: {integrity: sha512-VT3do0a0vwYVaNcp/y05ikkKS3OrFR5UeEf5SUuYZVgKVl1Nc1k9ajoYSsOid8AD/vlhLDB5yFQaV4HmT/OB9w==}
'@libsql/core@0.15.9':
resolution: {integrity: sha512-4OVdeAmuaCUq5hYT8NNn0nxlO9AcA/eTjXfUZ+QK8MT3Dz7Z76m73x7KxjU6I64WyXX98dauVH2b9XM+d84npw==}
'@libsql/darwin-arm64@0.5.13':
resolution: {integrity: sha512-ASz/EAMLDLx3oq9PVvZ4zBXXHbz2TxtxUwX2xpTRFR4V4uSHAN07+jpLu3aK5HUBLuv58z7+GjaL5w/cyjR28Q==}
cpu: [arm64]
os: [darwin]
'@libsql/darwin-x64@0.5.13':
resolution: {integrity: sha512-kzglniv1difkq8opusSXM7u9H0WoEPeKxw0ixIfcGfvlCVMJ+t9UNtXmyNHW68ljdllje6a4C6c94iPmIYafYA==}
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-arm-gnueabihf@0.5.13':
resolution: {integrity: sha512-UEW+VZN2r0mFkfztKOS7cqfS8IemuekbjUXbXCwULHtusww2QNCXvM5KU9eJCNE419SZCb0qaEWYytcfka8qeA==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm-musleabihf@0.5.13':
resolution: {integrity: sha512-NMDgLqryYBv4Sr3WoO/m++XDjR5KLlw9r/JK4Ym6A1XBv2bxQQNhH0Lxx3bjLW8qqhBD4+0xfms4d2cOlexPyA==}
cpu: [arm]
os: [linux]
'@libsql/linux-arm64-gnu@0.5.13':
resolution: {integrity: sha512-/wCxVdrwl1ee6D6LEjwl+w4SxuLm5UL9Kb1LD5n0bBGs0q+49ChdPPh7tp175iRgkcrTgl23emymvt1yj3KxVQ==}
cpu: [arm64]
os: [linux]
'@libsql/linux-arm64-musl@0.5.13':
resolution: {integrity: sha512-xnVAbZIanUgX57XqeI5sNaDnVilp0Di5syCLSEo+bRyBobe/1IAeehNZpyVbCy91U2N6rH1C/mZU7jicVI9x+A==}
cpu: [arm64]
os: [linux]
'@libsql/linux-x64-gnu@0.5.13':
resolution: {integrity: sha512-/mfMRxcQAI9f8t7tU3QZyh25lXgXKzgin9B9TOSnchD73PWtsVhlyfA6qOCfjQl5kr4sHscdXD5Yb3KIoUgrpQ==}
cpu: [x64]
os: [linux]
'@libsql/linux-x64-musl@0.5.13':
resolution: {integrity: sha512-rdefPTpQCVwUjIQYbDLMv3qpd5MdrT0IeD0UZPGqhT9AWU8nJSQoj2lfyIDAWEz7PPOVCY4jHuEn7FS2sw9kRA==}
cpu: [x64]
os: [linux]
'@libsql/win32-x64-msvc@0.5.13':
resolution: {integrity: sha512-aNcmDrD1Ws+dNZIv9ECbxBQumqB9MlSVEykwfXJpqv/593nABb8Ttg5nAGUPtnADyaGDTrGvPPP81d/KsKho4Q==}
cpu: [x64]
os: [win32]
'@neon-rs/load@0.0.4':
resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
'@signalapp/libsignal-client@0.74.1': '@signalapp/libsignal-client@0.74.1':
resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==} resolution: {integrity: sha512-PEJou0yrBvxaAGg7JjONlRNM/t3PCBuY96wu7W6+57e38/7Mibo9kAMfE5B8DgVv+DUNMW9AgJhx5McCoIXYew==}
'@types/node@24.0.3':
resolution: {integrity: sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
accepts@2.0.0: accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -65,6 +138,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
data-uri-to-buffer@4.0.1:
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
engines: {node: '>= 12'}
debug@4.4.1: debug@4.4.1:
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -78,6 +155,10 @@ packages:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
dotenv@16.5.0: dotenv@16.5.0:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'} engines: {node: '>=12'}
@ -116,10 +197,18 @@ packages:
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
fetch-blob@3.2.0:
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
engines: {node: ^12.20 || >= 14.13}
finalhandler@2.1.0: finalhandler@2.1.0:
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
formdata-polyfill@4.0.10:
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
engines: {node: '>=12.20.0'}
forwarded@0.2.0: forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -169,6 +258,14 @@ packages:
is-promise@4.0.0: is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
js-base64@3.7.7:
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
libsql@0.5.13:
resolution: {integrity: sha512-5Bwoa/CqzgkTwySgqHA5TsaUDRrdLIbdM4egdPcaAnqO3aC+qAgS6BwdzuZwARA5digXwiskogZ8H7Yy4XfdOg==}
cpu: [x64, arm64, wasm32, arm]
os: [darwin, linux, win32]
math-intrinsics@1.1.0: math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -196,6 +293,15 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'} 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}
node-gyp-build@4.8.4: node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true hasBin: true
@ -219,6 +325,9 @@ packages:
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
promise-limit@2.7.0:
resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
proxy-addr@2.0.7: proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
@ -292,6 +401,9 @@ packages:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
unpipe@1.0.0: unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@ -304,6 +416,10 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
web-streams-polyfill@3.3.3:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'}
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
@ -321,12 +437,84 @@ packages:
snapshots: snapshots:
'@libsql/client@0.15.9':
dependencies:
'@libsql/core': 0.15.9
'@libsql/hrana-client': 0.7.0
js-base64: 3.7.7
libsql: 0.5.13
promise-limit: 2.7.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/core@0.15.9':
dependencies:
js-base64: 3.7.7
'@libsql/darwin-arm64@0.5.13':
optional: true
'@libsql/darwin-x64@0.5.13':
optional: true
'@libsql/hrana-client@0.7.0':
dependencies:
'@libsql/isomorphic-fetch': 0.3.1
'@libsql/isomorphic-ws': 0.1.5
js-base64: 3.7.7
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.18.2
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@libsql/linux-arm-gnueabihf@0.5.13':
optional: true
'@libsql/linux-arm-musleabihf@0.5.13':
optional: true
'@libsql/linux-arm64-gnu@0.5.13':
optional: true
'@libsql/linux-arm64-musl@0.5.13':
optional: true
'@libsql/linux-x64-gnu@0.5.13':
optional: true
'@libsql/linux-x64-musl@0.5.13':
optional: true
'@libsql/win32-x64-msvc@0.5.13':
optional: true
'@neon-rs/load@0.0.4': {}
'@signalapp/libsignal-client@0.74.1': '@signalapp/libsignal-client@0.74.1':
dependencies: dependencies:
node-gyp-build: 4.8.4 node-gyp-build: 4.8.4
type-fest: 4.41.0 type-fest: 4.41.0
uuid: 11.1.0 uuid: 11.1.0
'@types/node@24.0.3':
dependencies:
undici-types: 7.8.0
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.0.3
accepts@2.0.0: accepts@2.0.0:
dependencies: dependencies:
mime-types: 3.0.1 mime-types: 3.0.1
@ -368,12 +556,16 @@ snapshots:
cookie@0.7.2: {} cookie@0.7.2: {}
data-uri-to-buffer@4.0.1: {}
debug@4.4.1: debug@4.4.1:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
depd@2.0.0: {} depd@2.0.0: {}
detect-libc@2.0.2: {}
dotenv@16.5.0: {} dotenv@16.5.0: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
@ -430,6 +622,11 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
web-streams-polyfill: 3.3.3
finalhandler@2.1.0: finalhandler@2.1.0:
dependencies: dependencies:
debug: 4.4.1 debug: 4.4.1
@ -441,6 +638,10 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
formdata-polyfill@4.0.10:
dependencies:
fetch-blob: 3.2.0
forwarded@0.2.0: {} forwarded@0.2.0: {}
fresh@2.0.0: {} fresh@2.0.0: {}
@ -491,6 +692,23 @@ snapshots:
is-promise@4.0.0: {} is-promise@4.0.0: {}
js-base64@3.7.7: {}
libsql@0.5.13:
dependencies:
'@neon-rs/load': 0.0.4
detect-libc: 2.0.2
optionalDependencies:
'@libsql/darwin-arm64': 0.5.13
'@libsql/darwin-x64': 0.5.13
'@libsql/linux-arm-gnueabihf': 0.5.13
'@libsql/linux-arm-musleabihf': 0.5.13
'@libsql/linux-arm64-gnu': 0.5.13
'@libsql/linux-arm64-musl': 0.5.13
'@libsql/linux-x64-gnu': 0.5.13
'@libsql/linux-x64-musl': 0.5.13
'@libsql/win32-x64-msvc': 0.5.13
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
media-typer@1.1.0: {} media-typer@1.1.0: {}
@ -507,6 +725,14 @@ snapshots:
negotiator@1.0.0: {} negotiator@1.0.0: {}
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
node-gyp-build@4.8.4: {} node-gyp-build@4.8.4: {}
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
@ -523,6 +749,8 @@ snapshots:
path-to-regexp@8.2.0: {} path-to-regexp@8.2.0: {}
promise-limit@2.7.0: {}
proxy-addr@2.0.7: proxy-addr@2.0.7:
dependencies: dependencies:
forwarded: 0.2.0 forwarded: 0.2.0
@ -624,12 +852,16 @@ snapshots:
media-typer: 1.1.0 media-typer: 1.1.0
mime-types: 3.0.1 mime-types: 3.0.1
undici-types@7.8.0: {}
unpipe@1.0.0: {} unpipe@1.0.0: {}
uuid@11.1.0: {} uuid@11.1.0: {}
vary@1.1.2: {} vary@1.1.2: {}
web-streams-polyfill@3.3.3: {}
wrappy@1.0.2: {} wrappy@1.0.2: {}
ws@8.18.2: {} ws@8.18.2: {}

257
server.js
View File

@ -31,130 +31,129 @@ wss.on("connection", (ws, req) => {
connections.delete(userId); connections.delete(userId);
console.log(`User ${userId} disconnected`); console.log(`User ${userId} disconnected`);
}); });
ws.on("error", (error) => {
console.error(`WebSocket error for user ${userId}:`, error);
connections.delete(userId);
});
} }
}); });
// Broadcast to all connected clients // Helper function to broadcast to all connected users
function broadcast(message) { function broadcast(message) {
const messageStr = JSON.stringify(message); const messageStr = JSON.stringify(message);
connections.forEach((ws) => { connections.forEach((ws, userId) => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.send(messageStr); ws.send(messageStr);
} }
}); });
} }
// Broadcast to specific user // Helper function to broadcast to a specific user
function broadcastToUser(userId, message) { function broadcastToUser(userId, message) {
const ws = connections.get(userId); const ws = connections.get(userId);
console.log(
`broadcastToUser called for ${userId}, connection exists: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`,
);
if (ws && ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
console.log(
`✓ Sending WebSocket message to ${userId}:`,
message.type,
JSON.stringify(message.data),
);
ws.send(JSON.stringify(message)); ws.send(JSON.stringify(message));
} else {
console.log(
`✗ WebSocket not available for user ${userId} - connection: ${!!ws}, readyState: ${ws ? ws.readyState : "N/A"}`,
);
} }
} }
app.use(express.json()); app.use(express.json());
app.use(express.static("public")); app.use(express.static("public"));
// Create user // Middleware to handle async errors
app.post("/api/users", (req, res) => { const asyncHandler = (fn) => (req, res, next) => {
try { Promise.resolve(fn(req, res, next)).catch(next);
const { userId } = req.body; };
const user = todoService.createUser(userId);
broadcast({ type: "users", data: todoService.getAllUsers() }); // Error handling middleware
res.json(user); app.use((err, req, res, next) => {
} catch (error) { console.error("Error:", err);
res.status(400).json({ error: error.message }); res.status(500).json({ error: err.message || "Internal server error" });
}
}); });
// Create a new user
app.post(
"/api/users",
asyncHandler(async (req, res) => {
const { userId } = req.body;
const user = await todoService.createUser(userId);
const users = await todoService.getAllUsers();
broadcast({ type: "users", data: users });
res.json(user);
}),
);
// Get all users // Get all users
app.get("/api/users", (req, res) => { app.get(
try { "/api/users",
const users = todoService.getAllUsers(); asyncHandler(async (req, res) => {
const users = await todoService.getAllUsers();
res.json(users); res.json(users);
} catch (error) { }),
res.status(400).json({ error: error.message }); );
}
});
// Add todo // Add a new todo
app.post("/api/users/:userId/todos", (req, res) => { app.post(
try { "/api/users/:userId/todos",
asyncHandler(async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const { text } = req.body; const { text } = req.body;
const todoGroupId = todoService.addTodo(userId, text); const todoGroupId = await todoService.addTodo(userId, text);
const todo = todoService.getTodo(userId, todoGroupId); const todo = await todoService.getTodo(userId, todoGroupId);
broadcastToUser(userId, { type: "todo_added", data: todo }); broadcastToUser(userId, { type: "todo_added", data: todo });
res.json({ id: todoGroupId }); res.json({ id: todoGroupId });
} catch (error) { }),
res.status(400).json({ error: error.message }); );
}
});
// Get todos // Get todos for a user
app.get("/api/users/:userId/todos", (req, res) => { app.get(
try { "/api/users/:userId/todos",
asyncHandler(async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const todos = todoService.getTodos(userId); const todos = await todoService.getTodos(userId);
res.json(todos); res.json(todos);
} catch (error) { }),
res.status(400).json({ error: error.message }); );
}
});
// Get encrypted todos // Get encrypted todos (for debugging)
app.get("/api/users/:userId/todos/encrypted", (req, res) => { app.get(
try { "/api/users/:userId/todos/encrypted",
asyncHandler(async (req, res) => {
const { userId } = req.params; const { userId } = req.params;
const todos = todoService.getTodos(userId); const todos = await todoService.getTodos(userId);
const encryptedTodos = todos.map((todo) => { const encryptedTodos = [];
const row = todoService.db
.prepare(
"SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?",
)
.get(todo.id, userId);
if (!row) { for (const todo of todos) {
const row = await todoService.db.execute(
"SELECT encrypted, createdAt FROM todos WHERE todoGroupId = ? AND userId = ?",
[todo.id, userId],
);
if (row.rows.length === 0) {
console.warn( console.warn(
`No encrypted data found for todo ${todo.id} and user ${userId}`, `No encrypted data found for todo ${todo.id} and user ${userId}`,
); );
return { continue;
id: todo.id,
encrypted: { body: "Encryption data not available" },
createdAt: todo.createdAt,
};
} }
return { encryptedTodos.push({
id: todo.id, id: todo.id,
encrypted: JSON.parse(row.encrypted), encrypted: row.rows[0].encrypted,
createdAt: row.createdAt, createdAt: row.rows[0].createdAt,
}; participants: todo.participants,
}); });
res.json(encryptedTodos); }
} catch (error) {
console.error("Error in encrypted todos endpoint:", error);
res.status(400).json({ error: error.message });
}
});
// Delete todo res.json(encryptedTodos);
app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => { }),
try { );
// Delete a todo
app.delete(
"/api/users/:userId/todos/:todoGroupId",
asyncHandler(async (req, res) => {
const { userId, todoGroupId } = req.params; const { userId, todoGroupId } = req.params;
const { deletedIds, affectedUsers } = todoService.deleteTodo( const { deletedIds, affectedUsers } = await todoService.deleteTodo(
userId, userId,
todoGroupId, todoGroupId,
); );
@ -163,9 +162,6 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`); console.log(`Broadcasting deletion to users: ${affectedUsers.join(", ")}`);
affectedUsers.forEach((affectedUserId) => { affectedUsers.forEach((affectedUserId) => {
deletedIds.forEach((deletedId) => { deletedIds.forEach((deletedId) => {
console.log(
`Sending todo_deleted event for todo ${deletedId} to user ${affectedUserId}`,
);
broadcastToUser(affectedUserId, { broadcastToUser(affectedUserId, {
type: "todo_deleted", type: "todo_deleted",
data: { id: deletedId, deletedBy: userId }, data: { id: deletedId, deletedBy: userId },
@ -173,43 +169,41 @@ app.delete("/api/users/:userId/todos/:todoGroupId", (req, res) => {
}); });
}); });
res.json({ success: true }); res.json({ deletedIds, affectedUsers });
} catch (error) { }),
res.status(400).json({ error: error.message }); );
}
});
// Share todo // Share a todo with another user
app.post("/api/users/:userId/todos/:todoGroupId/share", (req, res) => { app.post(
try { "/api/users/:userId/todos/:todoGroupId/share",
asyncHandler(async (req, res) => {
const { userId, todoGroupId } = req.params; const { userId, todoGroupId } = req.params;
const { recipientId } = req.body; const { recipientId } = req.body;
if (!recipientId) { if (!recipientId) {
return res.status(400).json({ error: "Recipient ID is required" }); return res.status(400).json({ error: "Recipient ID is required" });
} }
const sharedTodoGroupId = todoService.shareTodo( const sharedTodoGroupId = await todoService.shareTodo(
userId, userId,
todoGroupId, todoGroupId,
recipientId, recipientId,
); );
const sharedTodo = todoService.getTodo(recipientId, sharedTodoGroupId); const sharedTodo = await todoService.getTodo(
recipientId,
sharedTodoGroupId,
);
broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo }); broadcastToUser(recipientId, { type: "todo_shared", data: sharedTodo });
// Also notify the original user that the todo was shared res.json({
broadcastToUser(userId, { sharedTodoGroupId,
type: "todo_share_confirmed", message: `Todo shared with ${recipientId}`,
data: { todoGroupId, sharedWith: recipientId },
}); });
}),
res.json({ success: true }); );
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// Update todo completion status // Update todo completion status
app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => { app.patch(
try { "/api/users/:userId/todos/:todoGroupId",
asyncHandler(async (req, res) => {
const { userId, todoGroupId } = req.params; const { userId, todoGroupId } = req.params;
const { completed } = req.body; const { completed } = req.body;
@ -217,41 +211,38 @@ app.patch("/api/users/:userId/todos/:todoGroupId", (req, res) => {
`PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`, `PATCH request: user ${userId} updating todo ${todoGroupId} to completed=${completed}`,
); );
const affectedUsers = todoService.updateTodoStatus( const affectedUsers = await todoService.updateTodoStatus(
userId, userId,
todoGroupId, todoGroupId,
completed, completed,
); );
console.log(`updateTodoStatus returned affected users:`, affectedUsers); console.log(`Broadcasting update to users: ${affectedUsers.join(", ")}`);
// Broadcast update to all affected users // Broadcast the update to all affected users
console.log( for (const affectedUserId of affectedUsers) {
`Broadcasting todo update to users: ${affectedUsers.join(", ")}`, try {
); const todo = await todoService.getTodo(affectedUserId, todoGroupId);
affectedUsers.forEach((affectedUserId) => { const message = {
console.log( type: "todo_updated",
`Sending todo_updated event for todo ${todoGroupId} to user ${affectedUserId}`, data: { id: todoGroupId, completed, updatedBy: userId },
); };
const message = { broadcastToUser(affectedUserId, message);
type: "todo_updated", console.log(`Sent update to user ${affectedUserId}:`, message);
data: { id: todoGroupId, completed, updatedBy: userId }, } catch (error) {
}; console.error(
console.log( `Error getting todo for user ${affectedUserId}:`,
`Message being sent to ${affectedUserId}:`, error.message,
JSON.stringify(message), );
); }
broadcastToUser(affectedUserId, message); }
});
res.json({ success: true }); res.json({ message: "Todo updated successfully", affectedUsers });
} catch (error) { }),
console.error(`Error updating todo status:`, error); );
res.status(400).json({ error: error.message });
} const PORT = process.env.APP_PORT || 3000;
});
const PORT = 3000;
server.listen(PORT, () => { server.listen(PORT, () => {
console.log(`Server running on port 3000`); console.log(`Server running on port ${PORT}`);
}); });

View File

@ -1,18 +1,28 @@
import { DatabaseSync } from "node:sqlite"; import { createClient } from "@libsql/client";
import "dotenv/config"; import "dotenv/config";
import { SignalClient } from "./signal-crypto.js"; import { SignalClient } from "./signal-crypto.js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
export class EncryptedTodoService { export class EncryptedTodoService {
constructor() { constructor() {
this.db = new DatabaseSync(process.env.SQLITE_DB_PATH); this.db = createClient({
url: `file:${process.env.SQLITE_DB_PATH}`,
});
this.users = new Map(); this.users = new Map();
// Initialize database and migrate
this.initializeDatabase();
}
async initializeDatabase() {
// Create tables with new schema // Create tables with new schema
this.db.exec(` await this.db.execute(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
userId TEXT PRIMARY KEY userId TEXT PRIMARY KEY
); );
`);
await this.db.execute(`
CREATE TABLE IF NOT EXISTS todos ( CREATE TABLE IF NOT EXISTS todos (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
todoGroupId TEXT, todoGroupId TEXT,
@ -25,12 +35,18 @@ export class EncryptedTodoService {
originalTodoId TEXT, originalTodoId TEXT,
FOREIGN KEY(userId) REFERENCES users(userId) FOREIGN KEY(userId) REFERENCES users(userId)
); );
`);
await this.db.execute(`
CREATE TABLE IF NOT EXISTS todo_groups ( CREATE TABLE IF NOT EXISTS todo_groups (
todoGroupId TEXT PRIMARY KEY, todoGroupId TEXT PRIMARY KEY,
createdBy TEXT, createdBy TEXT,
createdAt TEXT, createdAt TEXT,
FOREIGN KEY(createdBy) REFERENCES users(userId) FOREIGN KEY(createdBy) REFERENCES users(userId)
); );
`);
await this.db.execute(`
CREATE TABLE IF NOT EXISTS todo_participants ( CREATE TABLE IF NOT EXISTS todo_participants (
todoGroupId TEXT, todoGroupId TEXT,
userId TEXT, userId TEXT,
@ -39,6 +55,9 @@ export class EncryptedTodoService {
FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId), FOREIGN KEY(todoGroupId) REFERENCES todo_groups(todoGroupId),
FOREIGN KEY(userId) REFERENCES users(userId) FOREIGN KEY(userId) REFERENCES users(userId)
); );
`);
await this.db.execute(`
CREATE TABLE IF NOT EXISTS todo_shares ( CREATE TABLE IF NOT EXISTS todo_shares (
todoId TEXT, todoId TEXT,
sharedWith TEXT, sharedWith TEXT,
@ -48,84 +67,90 @@ export class EncryptedTodoService {
`); `);
// Migrate existing data to new schema // Migrate existing data to new schema
this.migrateToNewSchema(); await this.migrateToNewSchema();
// Load existing users from database on startup // Load existing users from database on startup
this.loadExistingUsers(); await this.loadExistingUsers();
} }
migrateToNewSchema() { async migrateToNewSchema() {
// Check if we need to migrate by looking for todos without todoGroupId // Check if we need to migrate by looking for todos without todoGroupId
const oldTodos = this.db const oldTodos = await this.db.execute(
.prepare("SELECT * FROM todos WHERE todoGroupId IS NULL") "SELECT * FROM todos WHERE todoGroupId IS NULL",
.all(); );
if (oldTodos.length > 0) { if (oldTodos.rows.length > 0) {
console.log(`Migrating ${oldTodos.length} todos to new schema...`); console.log(`Migrating ${oldTodos.rows.length} todos to new schema...`);
for (const todo of oldTodos) { for (const todo of oldTodos.rows) {
const todoGroupId = uuidv4(); const todoGroupId = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
// If this is an original todo (not shared) // If this is an original todo (not shared)
if (!todo.sharedBy && !todo.originalTodoId) { if (!todo.sharedBy && !todo.originalTodoId) {
// Create todo group // Create todo group
this.db await this.db.execute(
.prepare( "INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", [todoGroupId, todo.userId, todo.createdAt || now],
) );
.run(todoGroupId, todo.userId, todo.createdAt || now);
// Add creator as participant // Add creator as participant
this.db await this.db.execute(
.prepare( "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", [todoGroupId, todo.userId, todo.createdAt || now],
) );
.run(todoGroupId, todo.userId, todo.createdAt || now);
// Update the todo with todoGroupId // Update the todo with todoGroupId
this.db await this.db.execute(
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") "UPDATE todos SET todoGroupId = ? WHERE id = ?",
.run(todoGroupId, todo.id); [todoGroupId, todo.id],
);
// Find and migrate shared copies // Find and migrate shared copies
const sharedCopies = this.db const sharedCopies = await this.db.execute(
.prepare("SELECT * FROM todos WHERE originalTodoId = ?") "SELECT * FROM todos WHERE originalTodoId = ?",
.all(todo.id); [todo.id],
for (const sharedTodo of sharedCopies) { );
for (const sharedTodo of sharedCopies.rows) {
// Add shared user as participant // Add shared user as participant
this.db await this.db.execute(
.prepare( "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", [todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now],
) );
.run(todoGroupId, sharedTodo.userId, sharedTodo.createdAt || now);
// Update shared todo with same todoGroupId // Update shared todo with same todoGroupId
this.db await this.db.execute(
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") "UPDATE todos SET todoGroupId = ? WHERE id = ?",
.run(todoGroupId, sharedTodo.id); [todoGroupId, sharedTodo.id],
);
} }
} }
// If this is a shared todo without a group (shouldn't happen after above, but just in case) // If this is a shared todo without a group (shouldn't happen after above, but just in case)
else if (todo.originalTodoId && !todo.todoGroupId) { else if (todo.originalTodoId && !todo.todoGroupId) {
const originalTodo = this.db const originalTodo = await this.db.execute(
.prepare("SELECT todoGroupId FROM todos WHERE id = ?") "SELECT todoGroupId FROM todos WHERE id = ?",
.get(todo.originalTodoId); [todo.originalTodoId],
if (originalTodo && originalTodo.todoGroupId) { );
this.db
.prepare("UPDATE todos SET todoGroupId = ? WHERE id = ?") if (
.run(originalTodo.todoGroupId, todo.id); originalTodo.rows.length > 0 &&
originalTodo.rows[0].todoGroupId
) {
await this.db.execute(
"UPDATE todos SET todoGroupId = ? WHERE id = ?",
[originalTodo.rows[0].todoGroupId, todo.id],
);
// Add as participant if not already // Add as participant if not already
this.db await this.db.execute(
.prepare( "INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
"INSERT OR IGNORE INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", [
) originalTodo.rows[0].todoGroupId,
.run(
originalTodo.todoGroupId,
todo.userId, todo.userId,
todo.createdAt || now, todo.createdAt || now,
); ],
);
} }
} }
} }
@ -134,9 +159,9 @@ export class EncryptedTodoService {
} }
} }
loadExistingUsers() { async loadExistingUsers() {
const existingUsers = this.db.prepare("SELECT userId FROM users").all(); const existingUsers = await this.db.execute("SELECT userId FROM users");
for (const user of existingUsers) { for (const user of existingUsers.rows) {
if (!this.users.has(user.userId)) { if (!this.users.has(user.userId)) {
this.users.set(user.userId, SignalClient.create(user.userId)); this.users.set(user.userId, SignalClient.create(user.userId));
console.log(`Loaded existing user: ${user.userId}`); console.log(`Loaded existing user: ${user.userId}`);
@ -144,24 +169,22 @@ export class EncryptedTodoService {
} }
} }
createUser(userId) { async createUser(userId) {
this.db await this.db.execute("INSERT OR IGNORE INTO users (userId) VALUES (?)", [
.prepare("INSERT OR IGNORE INTO users (userId) VALUES (?)") userId,
.run(userId); ]);
if (!this.users.has(userId)) { if (!this.users.has(userId)) {
this.users.set(userId, SignalClient.create(userId)); this.users.set(userId, SignalClient.create(userId));
} }
return { userId }; return { userId };
} }
getAllUsers() { async getAllUsers() {
return this.db const result = await this.db.execute("SELECT userId FROM users");
.prepare("SELECT userId FROM users") return result.rows.map((u) => u.userId);
.all()
.map((u) => u.userId);
} }
addTodo(userId, todoText) { async addTodo(userId, todoText) {
const todoGroupId = uuidv4(); const todoGroupId = uuidv4();
const client = this.users.get(userId); const client = this.users.get(userId);
const todoId = uuidv4(); const todoId = uuidv4();
@ -169,25 +192,21 @@ export class EncryptedTodoService {
const now = new Date().toISOString(); const now = new Date().toISOString();
// Create the todo group // Create the todo group
this.db await this.db.execute(
.prepare( "INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)",
"INSERT INTO todo_groups (todoGroupId, createdBy, createdAt) VALUES (?, ?, ?)", [todoGroupId, userId, now],
) );
.run(todoGroupId, userId, now);
// Add the creator as a participant // Add the creator as a participant
this.db await this.db.execute(
.prepare( "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", [todoGroupId, userId, now],
) );
.run(todoGroupId, userId, now);
// Create the todo entry for the creator // Create the todo entry for the creator
this.db await this.db.execute(
.prepare( "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", [
)
.run(
todoId, todoId,
todoGroupId, todoGroupId,
userId, userId,
@ -195,24 +214,25 @@ export class EncryptedTodoService {
now, now,
todoText, todoText,
0, 0,
); ],
);
return todoGroupId; return todoGroupId;
} }
getTodos(userId) { async getTodos(userId) {
const client = this.users.get(userId); const client = this.users.get(userId);
if (!client) { if (!client) {
console.error(`No client found for user ${userId}`); console.error(`No client found for user ${userId}`);
return []; return [];
} }
const rows = this.db const rows = await this.db.execute("SELECT * FROM todos WHERE userId = ?", [
.prepare("SELECT * FROM todos WHERE userId = ?") userId,
.all(userId); ]);
const todos = []; const todos = [];
for (const row of rows) { for (const row of rows.rows) {
try { try {
// Skip todos without todoGroupId (migration didn't work) // Skip todos without todoGroupId (migration didn't work)
if (!row.todoGroupId) { if (!row.todoGroupId) {
@ -223,24 +243,28 @@ export class EncryptedTodoService {
const decryptedText = client.decrypt(JSON.parse(row.encrypted)); const decryptedText = client.decrypt(JSON.parse(row.encrypted));
// Get all participants in this todo group // Get all participants in this todo group
const participants = this.db const participants = await this.db.execute(
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") "SELECT userId FROM todo_participants WHERE todoGroupId = ?",
.all(row.todoGroupId) [row.todoGroupId],
.map((p) => p.userId); );
// Get group creator // Get group creator
const groupInfo = this.db const groupInfo = await this.db.execute(
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
.get(row.todoGroupId); [row.todoGroupId],
);
todos.push({ todos.push({
id: row.todoGroupId, // Use todoGroupId as the primary identifier id: row.todoGroupId, // Use todoGroupId as the primary identifier
text: decryptedText, text: decryptedText,
createdAt: row.createdAt, createdAt: row.createdAt,
completed: !!row.completed, completed: !!row.completed,
participants: participants.length > 0 ? participants : [userId], // Fallback to current user participants:
createdBy: groupInfo?.createdBy || userId, // Fallback to current user participants.rows.length > 0
isCreator: (groupInfo?.createdBy || userId) === userId, ? participants.rows.map((p) => p.userId)
: [userId], // Fallback to current user
createdBy: groupInfo.rows[0]?.createdBy || userId, // Fallback to current user
isCreator: (groupInfo.rows[0]?.createdBy || userId) === userId,
}); });
} catch (e) { } catch (e) {
console.error(`Error processing todo ${row.id}:`, e); console.error(`Error processing todo ${row.id}:`, e);
@ -249,183 +273,197 @@ export class EncryptedTodoService {
return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); return todos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
} }
getTodo(userId, todoGroupId) { async getTodo(userId, todoGroupId) {
const client = this.users.get(userId); const client = this.users.get(userId);
const row = this.db const row = await this.db.execute(
.prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?") "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?",
.get(todoGroupId, userId); [todoGroupId, userId],
if (!row) throw new Error("Todo not found"); );
const decryptedText = client.decrypt(JSON.parse(row.encrypted)); if (row.rows.length === 0) throw new Error("Todo not found");
const todoRow = row.rows[0];
const decryptedText = client.decrypt(JSON.parse(todoRow.encrypted));
// Get all participants in this todo group // Get all participants in this todo group
const participants = this.db const participants = await this.db.execute(
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") "SELECT userId FROM todo_participants WHERE todoGroupId = ?",
.all(todoGroupId) [todoGroupId],
.map((p) => p.userId); );
// Get group creator // Get group creator
const groupInfo = this.db const groupInfo = await this.db.execute(
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
.get(todoGroupId); [todoGroupId],
);
return { return {
id: todoGroupId, id: todoGroupId,
text: decryptedText, text: decryptedText,
createdAt: row.createdAt, createdAt: todoRow.createdAt,
completed: !!row.completed, completed: !!todoRow.completed,
participants: participants, participants: participants.rows.map((p) => p.userId),
createdBy: groupInfo?.createdBy, createdBy: groupInfo.rows[0]?.createdBy,
isCreator: groupInfo?.createdBy === userId, isCreator: groupInfo.rows[0]?.createdBy === userId,
}; };
} }
updateTodoStatus(userId, todoGroupId, completed) { async updateTodoStatus(userId, todoGroupId, completed) {
console.log( console.log(
`updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`, `updateTodoStatus called: userId=${userId}, todoGroupId=${todoGroupId}, completed=${completed}`,
); );
// Get all participants in this todo group // Get all participants in this todo group
const participants = this.db const participants = await this.db.execute(
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") "SELECT userId FROM todo_participants WHERE todoGroupId = ?",
.all(todoGroupId) [todoGroupId],
.map((p) => p.userId); );
if (!participants.includes(userId)) { const participantIds = participants.rows.map((p) => p.userId);
if (!participantIds.includes(userId)) {
throw new Error("User is not a participant in this todo group"); throw new Error("User is not a participant in this todo group");
} }
console.log(`Updating todo for all participants:`, participants); console.log(`Updating todo for all participants:`, participantIds);
// Update todos for all participants // Update todos for all participants
for (const participantId of participants) { for (const participantId of participantIds) {
this.db await this.db.execute(
.prepare( "UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?",
"UPDATE todos SET completed = ? WHERE todoGroupId = ? AND userId = ?", [completed ? 1 : 0, todoGroupId, participantId],
) );
.run(completed ? 1 : 0, todoGroupId, participantId);
} }
console.log(`Affected users for todo update:`, participants); console.log(`Affected users for todo update:`, participantIds);
return participants; return participantIds;
} }
deleteTodo(userId, todoGroupId) { async deleteTodo(userId, todoGroupId) {
// Get all participants in this todo group // Get all participants in this todo group
const participants = this.db const participants = await this.db.execute(
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") "SELECT userId FROM todo_participants WHERE todoGroupId = ?",
.all(todoGroupId) [todoGroupId],
.map((p) => p.userId); );
if (!participants.includes(userId)) { const participantIds = participants.rows.map((p) => p.userId);
if (!participantIds.includes(userId)) {
throw new Error("User is not a participant in this todo group"); throw new Error("User is not a participant in this todo group");
} }
// Check if user is the creator of this todo group // Check if user is the creator of this todo group
const groupInfo = this.db const groupInfo = await this.db.execute(
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
.get(todoGroupId); [todoGroupId],
);
if (!groupInfo) { if (groupInfo.rows.length === 0) {
throw new Error("Todo group not found"); throw new Error("Todo group not found");
} }
if (groupInfo.createdBy !== userId) { if (groupInfo.rows[0].createdBy !== userId) {
throw new Error("Only the creator can delete this todo"); throw new Error("Only the creator can delete this todo");
} }
console.log( console.log(
`Deleting todo group ${todoGroupId} for all participants:`, `Deleting todo group ${todoGroupId} for all participants:`,
participants, participantIds,
); );
// Delete all todo entries for this group // Delete all todo entries for this group
this.db.prepare("DELETE FROM todos WHERE todoGroupId = ?").run(todoGroupId); await this.db.execute("DELETE FROM todos WHERE todoGroupId = ?", [
todoGroupId,
]);
// Delete all participants // Delete all participants
this.db await this.db.execute(
.prepare("DELETE FROM todo_participants WHERE todoGroupId = ?") "DELETE FROM todo_participants WHERE todoGroupId = ?",
.run(todoGroupId); [todoGroupId],
);
// Delete the group itself // Delete the group itself
this.db await this.db.execute("DELETE FROM todo_groups WHERE todoGroupId = ?", [
.prepare("DELETE FROM todo_groups WHERE todoGroupId = ?") todoGroupId,
.run(todoGroupId); ]);
return { return {
deletedIds: [todoGroupId], deletedIds: [todoGroupId],
affectedUsers: participants, affectedUsers: participantIds,
}; };
} }
shareTodo(userId, todoGroupId, recipientId) { async shareTodo(userId, todoGroupId, recipientId) {
// Ensure recipient user exists // Ensure recipient user exists
if (!this.users.has(recipientId)) { if (!this.users.has(recipientId)) {
this.users.set(recipientId, SignalClient.create(recipientId)); this.users.set(recipientId, SignalClient.create(recipientId));
} }
// Check if user is a participant in this todo group // Check if user is a participant in this todo group
const participants = this.db const participants = await this.db.execute(
.prepare("SELECT userId FROM todo_participants WHERE todoGroupId = ?") "SELECT userId FROM todo_participants WHERE todoGroupId = ?",
.all(todoGroupId) [todoGroupId],
.map((p) => p.userId); );
if (!participants.includes(userId)) { const participantIds = participants.rows.map((p) => p.userId);
if (!participantIds.includes(userId)) {
throw new Error("User is not a participant in this todo group"); throw new Error("User is not a participant in this todo group");
} }
// Check if user is the creator of this todo group // Check if user is the creator of this todo group
const groupInfo = this.db const groupInfo = await this.db.execute(
.prepare("SELECT createdBy FROM todo_groups WHERE todoGroupId = ?") "SELECT createdBy FROM todo_groups WHERE todoGroupId = ?",
.get(todoGroupId); [todoGroupId],
);
if (!groupInfo) { if (groupInfo.rows.length === 0) {
throw new Error("Todo group not found"); throw new Error("Todo group not found");
} }
if (groupInfo.createdBy !== userId) { if (groupInfo.rows[0].createdBy !== userId) {
throw new Error("Only the creator can add participants to this todo"); throw new Error("Only the creator can add participants to this todo");
} }
if (participants.includes(recipientId)) { if (participantIds.includes(recipientId)) {
throw new Error("User is already a participant in this todo group"); throw new Error("User is already a participant in this todo group");
} }
// Get the original text from the sender's todo // Get the original text from the sender's todo
const senderTodo = this.db const senderTodo = await this.db.execute(
.prepare("SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?") "SELECT * FROM todos WHERE todoGroupId = ? AND userId = ?",
.get(todoGroupId, userId); [todoGroupId, userId],
);
if (!senderTodo) throw new Error("Todo not found"); if (senderTodo.rows.length === 0) throw new Error("Todo not found");
const senderTodoRow = senderTodo.rows[0];
const recipientClient = this.users.get(recipientId); const recipientClient = this.users.get(recipientId);
const encryptedForRecipient = recipientClient.encrypt( const encryptedForRecipient = recipientClient.encrypt(
senderTodo.originalText, senderTodoRow.originalText,
); );
const recipientTodoId = uuidv4(); const recipientTodoId = uuidv4();
const now = new Date().toISOString(); const now = new Date().toISOString();
// Add recipient as a participant // Add recipient as a participant
this.db await this.db.execute(
.prepare( "INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)",
"INSERT INTO todo_participants (todoGroupId, userId, joinedAt) VALUES (?, ?, ?)", [todoGroupId, recipientId, now],
) );
.run(todoGroupId, recipientId, now);
// Create todo entry for the recipient // Create todo entry for the recipient
this.db await this.db.execute(
.prepare( "INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO todos (id, todoGroupId, userId, encrypted, createdAt, originalText, completed) VALUES (?, ?, ?, ?, ?, ?, ?)", [
)
.run(
recipientTodoId, recipientTodoId,
todoGroupId, todoGroupId,
recipientId, recipientId,
JSON.stringify(encryptedForRecipient), JSON.stringify(encryptedForRecipient),
now, now,
senderTodo.originalText, senderTodoRow.originalText,
senderTodo.completed, senderTodoRow.completed,
); ],
);
return todoGroupId; return todoGroupId;
} }