diff --git a/.yarn/cache/@aws-crypto-crc32-npm-3.0.0-10d83e85b0-9fdb3e837f.zip b/.yarn/cache/@aws-crypto-crc32-npm-3.0.0-10d83e85b0-9fdb3e837f.zip new file mode 100644 index 000000000..0b1b7c31f Binary files /dev/null and b/.yarn/cache/@aws-crypto-crc32-npm-3.0.0-10d83e85b0-9fdb3e837f.zip differ diff --git a/.yarn/cache/@aws-crypto-crc32c-npm-3.0.0-79c813b90d-0a116b5d1c.zip b/.yarn/cache/@aws-crypto-crc32c-npm-3.0.0-79c813b90d-0a116b5d1c.zip new file mode 100644 index 000000000..f0be52470 Binary files /dev/null and b/.yarn/cache/@aws-crypto-crc32c-npm-3.0.0-79c813b90d-0a116b5d1c.zip differ diff --git a/.yarn/cache/@aws-crypto-ie11-detection-npm-3.0.0-71f24dcf6a-299b2ddd46.zip b/.yarn/cache/@aws-crypto-ie11-detection-npm-3.0.0-71f24dcf6a-299b2ddd46.zip new file mode 100644 index 000000000..cee7a4220 Binary files /dev/null and b/.yarn/cache/@aws-crypto-ie11-detection-npm-3.0.0-71f24dcf6a-299b2ddd46.zip differ diff --git a/.yarn/cache/@aws-crypto-sha1-browser-npm-3.0.0-f8218a7691-78c379e105.zip b/.yarn/cache/@aws-crypto-sha1-browser-npm-3.0.0-f8218a7691-78c379e105.zip new file mode 100644 index 000000000..c943c768f Binary files /dev/null and b/.yarn/cache/@aws-crypto-sha1-browser-npm-3.0.0-f8218a7691-78c379e105.zip differ diff --git a/.yarn/cache/@aws-crypto-sha256-browser-npm-3.0.0-467f48a447-ca89456bf5.zip b/.yarn/cache/@aws-crypto-sha256-browser-npm-3.0.0-467f48a447-ca89456bf5.zip new file mode 100644 index 000000000..c7ad35f98 Binary files /dev/null and b/.yarn/cache/@aws-crypto-sha256-browser-npm-3.0.0-467f48a447-ca89456bf5.zip differ diff --git a/.yarn/cache/@aws-crypto-sha256-js-npm-3.0.0-2ba1013fd6-644ded32ea.zip b/.yarn/cache/@aws-crypto-sha256-js-npm-3.0.0-2ba1013fd6-644ded32ea.zip new file mode 100644 index 000000000..7524def8f Binary files /dev/null and b/.yarn/cache/@aws-crypto-sha256-js-npm-3.0.0-2ba1013fd6-644ded32ea.zip differ diff --git a/.yarn/cache/@aws-crypto-supports-web-crypto-npm-3.0.0-55222d294a-35479a1558.zip b/.yarn/cache/@aws-crypto-supports-web-crypto-npm-3.0.0-55222d294a-35479a1558.zip new file mode 100644 index 000000000..6ae195fc2 Binary files /dev/null and b/.yarn/cache/@aws-crypto-supports-web-crypto-npm-3.0.0-55222d294a-35479a1558.zip differ diff --git a/.yarn/cache/@aws-crypto-util-npm-3.0.0-6c4b38c78e-d29d554504.zip b/.yarn/cache/@aws-crypto-util-npm-3.0.0-6c4b38c78e-d29d554504.zip new file mode 100644 index 000000000..193275ae3 Binary files /dev/null and b/.yarn/cache/@aws-crypto-util-npm-3.0.0-6c4b38c78e-d29d554504.zip differ diff --git a/.yarn/cache/@aws-sdk-chunked-blob-reader-npm-3.310.0-1a751a969c-4969fe05c6.zip b/.yarn/cache/@aws-sdk-chunked-blob-reader-npm-3.310.0-1a751a969c-4969fe05c6.zip new file mode 100644 index 000000000..57d3ae9ab Binary files /dev/null and b/.yarn/cache/@aws-sdk-chunked-blob-reader-npm-3.310.0-1a751a969c-4969fe05c6.zip differ diff --git a/.yarn/cache/@aws-sdk-client-lambda-npm-3.363.0-3b9faf7cd9-207c2f6619.zip b/.yarn/cache/@aws-sdk-client-lambda-npm-3.363.0-3b9faf7cd9-207c2f6619.zip new file mode 100644 index 000000000..fb1a73fbc Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-lambda-npm-3.363.0-3b9faf7cd9-207c2f6619.zip differ diff --git a/.yarn/cache/@aws-sdk-client-s3-npm-3.363.0-91de8d0e73-35557dd4b9.zip b/.yarn/cache/@aws-sdk-client-s3-npm-3.363.0-91de8d0e73-35557dd4b9.zip new file mode 100644 index 000000000..1afae09d9 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-s3-npm-3.363.0-91de8d0e73-35557dd4b9.zip differ diff --git a/.yarn/cache/@aws-sdk-client-sns-npm-3.363.0-4b1c089d33-768a97ac50.zip b/.yarn/cache/@aws-sdk-client-sns-npm-3.363.0-4b1c089d33-768a97ac50.zip new file mode 100644 index 000000000..9ae957546 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-sns-npm-3.363.0-4b1c089d33-768a97ac50.zip differ diff --git a/.yarn/cache/@aws-sdk-client-sqs-npm-3.363.0-f0b08d1156-aa00d22cbb.zip b/.yarn/cache/@aws-sdk-client-sqs-npm-3.363.0-f0b08d1156-aa00d22cbb.zip new file mode 100644 index 000000000..02a91a63b Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-sqs-npm-3.363.0-f0b08d1156-aa00d22cbb.zip differ diff --git a/.yarn/cache/@aws-sdk-client-sso-npm-3.363.0-83a5ecf01f-938f435f25.zip b/.yarn/cache/@aws-sdk-client-sso-npm-3.363.0-83a5ecf01f-938f435f25.zip new file mode 100644 index 000000000..b02687639 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-sso-npm-3.363.0-83a5ecf01f-938f435f25.zip differ diff --git a/.yarn/cache/@aws-sdk-client-sso-oidc-npm-3.363.0-669fa969da-89c24c9bc0.zip b/.yarn/cache/@aws-sdk-client-sso-oidc-npm-3.363.0-669fa969da-89c24c9bc0.zip new file mode 100644 index 000000000..ee3f6f943 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-sso-oidc-npm-3.363.0-669fa969da-89c24c9bc0.zip differ diff --git a/.yarn/cache/@aws-sdk-client-sts-npm-3.363.0-50cf4722f1-5af409a69d.zip b/.yarn/cache/@aws-sdk-client-sts-npm-3.363.0-50cf4722f1-5af409a69d.zip new file mode 100644 index 000000000..2c9730981 Binary files /dev/null and b/.yarn/cache/@aws-sdk-client-sts-npm-3.363.0-50cf4722f1-5af409a69d.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.363.0-97a5aabae7-760cd1090b.zip b/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.363.0-97a5aabae7-760cd1090b.zip new file mode 100644 index 000000000..e4b990023 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-env-npm-3.363.0-97a5aabae7-760cd1090b.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.363.0-0f9fa76c38-6a23678a4e.zip b/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.363.0-0f9fa76c38-6a23678a4e.zip new file mode 100644 index 000000000..ca52c92be Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-ini-npm-3.363.0-0f9fa76c38-6a23678a4e.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.363.0-b547ee0817-9a50f8ec17.zip b/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.363.0-b547ee0817-9a50f8ec17.zip new file mode 100644 index 000000000..bc7b967a6 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-node-npm-3.363.0-b547ee0817-9a50f8ec17.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.363.0-2a03b152d3-d8f12f30a4.zip b/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.363.0-2a03b152d3-d8f12f30a4.zip new file mode 100644 index 000000000..da65da7cd Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-process-npm-3.363.0-2a03b152d3-d8f12f30a4.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.363.0-87f09b5233-0cf8b29865.zip b/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.363.0-87f09b5233-0cf8b29865.zip new file mode 100644 index 000000000..a0f16808a Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-sso-npm-3.363.0-87f09b5233-0cf8b29865.zip differ diff --git a/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.363.0-951e583258-b0625bf4e5.zip b/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.363.0-951e583258-b0625bf4e5.zip new file mode 100644 index 000000000..3f7262275 Binary files /dev/null and b/.yarn/cache/@aws-sdk-credential-provider-web-identity-npm-3.363.0-951e583258-b0625bf4e5.zip differ diff --git a/.yarn/cache/@aws-sdk-hash-blob-browser-npm-3.357.0-331c069fad-3bdc8b401b.zip b/.yarn/cache/@aws-sdk-hash-blob-browser-npm-3.357.0-331c069fad-3bdc8b401b.zip new file mode 100644 index 000000000..e02a8d00c Binary files /dev/null and b/.yarn/cache/@aws-sdk-hash-blob-browser-npm-3.357.0-331c069fad-3bdc8b401b.zip differ diff --git a/.yarn/cache/@aws-sdk-hash-stream-node-npm-3.357.0-2639648c74-6599d19353.zip b/.yarn/cache/@aws-sdk-hash-stream-node-npm-3.357.0-2639648c74-6599d19353.zip new file mode 100644 index 000000000..6c9bd876a Binary files /dev/null and b/.yarn/cache/@aws-sdk-hash-stream-node-npm-3.357.0-2639648c74-6599d19353.zip differ diff --git a/.yarn/cache/@aws-sdk-is-array-buffer-npm-3.310.0-1a5a04a172-ddd1536ad1.zip b/.yarn/cache/@aws-sdk-is-array-buffer-npm-3.310.0-1a5a04a172-ddd1536ad1.zip new file mode 100644 index 000000000..234b63214 Binary files /dev/null and b/.yarn/cache/@aws-sdk-is-array-buffer-npm-3.310.0-1a5a04a172-ddd1536ad1.zip differ diff --git a/.yarn/cache/@aws-sdk-md5-js-npm-3.357.0-1afc7386a6-7ca7ea50cc.zip b/.yarn/cache/@aws-sdk-md5-js-npm-3.357.0-1afc7386a6-7ca7ea50cc.zip new file mode 100644 index 000000000..ccb81adfa Binary files /dev/null and b/.yarn/cache/@aws-sdk-md5-js-npm-3.357.0-1afc7386a6-7ca7ea50cc.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-bucket-endpoint-npm-3.363.0-a38b7227b4-a087b168b3.zip b/.yarn/cache/@aws-sdk-middleware-bucket-endpoint-npm-3.363.0-a38b7227b4-a087b168b3.zip new file mode 100644 index 000000000..cf0f4527e Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-bucket-endpoint-npm-3.363.0-a38b7227b4-a087b168b3.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-expect-continue-npm-3.363.0-da8e39e415-82ee54c2da.zip b/.yarn/cache/@aws-sdk-middleware-expect-continue-npm-3.363.0-da8e39e415-82ee54c2da.zip new file mode 100644 index 000000000..c5cf63e4c Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-expect-continue-npm-3.363.0-da8e39e415-82ee54c2da.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-flexible-checksums-npm-3.363.0-28d6a6e5d6-273d5d58e5.zip b/.yarn/cache/@aws-sdk-middleware-flexible-checksums-npm-3.363.0-28d6a6e5d6-273d5d58e5.zip new file mode 100644 index 000000000..33ea9858b Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-flexible-checksums-npm-3.363.0-28d6a6e5d6-273d5d58e5.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-host-header-npm-3.363.0-94da61ed18-80f747bacb.zip b/.yarn/cache/@aws-sdk-middleware-host-header-npm-3.363.0-94da61ed18-80f747bacb.zip new file mode 100644 index 000000000..f8b051211 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-host-header-npm-3.363.0-94da61ed18-80f747bacb.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-location-constraint-npm-3.363.0-7e4ba242c2-53af61498e.zip b/.yarn/cache/@aws-sdk-middleware-location-constraint-npm-3.363.0-7e4ba242c2-53af61498e.zip new file mode 100644 index 000000000..a98b5be4b Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-location-constraint-npm-3.363.0-7e4ba242c2-53af61498e.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-logger-npm-3.363.0-90113dc685-13c6dccb41.zip b/.yarn/cache/@aws-sdk-middleware-logger-npm-3.363.0-90113dc685-13c6dccb41.zip new file mode 100644 index 000000000..18923e144 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-logger-npm-3.363.0-90113dc685-13c6dccb41.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.363.0-96c50e2733-7ef55ef6ca.zip b/.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.363.0-96c50e2733-7ef55ef6ca.zip new file mode 100644 index 000000000..c39fe1559 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-recursion-detection-npm-3.363.0-96c50e2733-7ef55ef6ca.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-sdk-s3-npm-3.363.0-73eb8d0845-44f2c91be1.zip b/.yarn/cache/@aws-sdk-middleware-sdk-s3-npm-3.363.0-73eb8d0845-44f2c91be1.zip new file mode 100644 index 000000000..e95959779 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-sdk-s3-npm-3.363.0-73eb8d0845-44f2c91be1.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-sdk-sqs-npm-3.363.0-afed0312e4-eef2e56432.zip b/.yarn/cache/@aws-sdk-middleware-sdk-sqs-npm-3.363.0-afed0312e4-eef2e56432.zip new file mode 100644 index 000000000..bfa809844 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-sdk-sqs-npm-3.363.0-afed0312e4-eef2e56432.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-sdk-sts-npm-3.363.0-7e943a11c6-b5f03018e8.zip b/.yarn/cache/@aws-sdk-middleware-sdk-sts-npm-3.363.0-7e943a11c6-b5f03018e8.zip new file mode 100644 index 000000000..2ca519a5b Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-sdk-sts-npm-3.363.0-7e943a11c6-b5f03018e8.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-signing-npm-3.363.0-40d6a38852-7f4efb241b.zip b/.yarn/cache/@aws-sdk-middleware-signing-npm-3.363.0-40d6a38852-7f4efb241b.zip new file mode 100644 index 000000000..3c27c1557 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-signing-npm-3.363.0-40d6a38852-7f4efb241b.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-ssec-npm-3.363.0-e85ed9bb29-3a4db9cf52.zip b/.yarn/cache/@aws-sdk-middleware-ssec-npm-3.363.0-e85ed9bb29-3a4db9cf52.zip new file mode 100644 index 000000000..86184d9db Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-ssec-npm-3.363.0-e85ed9bb29-3a4db9cf52.zip differ diff --git a/.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.363.0-7528a91dc1-d66f0ec803.zip b/.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.363.0-7528a91dc1-d66f0ec803.zip new file mode 100644 index 000000000..9d0c6ea03 Binary files /dev/null and b/.yarn/cache/@aws-sdk-middleware-user-agent-npm-3.363.0-7528a91dc1-d66f0ec803.zip differ diff --git a/.yarn/cache/@aws-sdk-signature-v4-multi-region-npm-3.363.0-edce795942-920328f0e8.zip b/.yarn/cache/@aws-sdk-signature-v4-multi-region-npm-3.363.0-edce795942-920328f0e8.zip new file mode 100644 index 000000000..1df13463d Binary files /dev/null and b/.yarn/cache/@aws-sdk-signature-v4-multi-region-npm-3.363.0-edce795942-920328f0e8.zip differ diff --git a/.yarn/cache/@aws-sdk-token-providers-npm-3.363.0-d4bc8cc357-ba945164da.zip b/.yarn/cache/@aws-sdk-token-providers-npm-3.363.0-d4bc8cc357-ba945164da.zip new file mode 100644 index 000000000..dcc322ebd Binary files /dev/null and b/.yarn/cache/@aws-sdk-token-providers-npm-3.363.0-d4bc8cc357-ba945164da.zip differ diff --git a/.yarn/cache/@aws-sdk-types-npm-3.357.0-3fecab3f76-41001b0ea7.zip b/.yarn/cache/@aws-sdk-types-npm-3.357.0-3fecab3f76-41001b0ea7.zip new file mode 100644 index 000000000..b351fc056 Binary files /dev/null and b/.yarn/cache/@aws-sdk-types-npm-3.357.0-3fecab3f76-41001b0ea7.zip differ diff --git a/.yarn/cache/@aws-sdk-util-arn-parser-npm-3.310.0-06bb539cba-faac1e10f8.zip b/.yarn/cache/@aws-sdk-util-arn-parser-npm-3.310.0-06bb539cba-faac1e10f8.zip new file mode 100644 index 000000000..a3ee0d6ea Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-arn-parser-npm-3.310.0-06bb539cba-faac1e10f8.zip differ diff --git a/.yarn/cache/@aws-sdk-util-buffer-from-npm-3.310.0-c31752fc9c-9c3bd9c066.zip b/.yarn/cache/@aws-sdk-util-buffer-from-npm-3.310.0-c31752fc9c-9c3bd9c066.zip new file mode 100644 index 000000000..385699f96 Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-buffer-from-npm-3.310.0-c31752fc9c-9c3bd9c066.zip differ diff --git a/.yarn/cache/@aws-sdk-util-endpoints-npm-3.357.0-b40cbd34ed-dcbe4a4ee0.zip b/.yarn/cache/@aws-sdk-util-endpoints-npm-3.357.0-b40cbd34ed-dcbe4a4ee0.zip new file mode 100644 index 000000000..c1cef7415 Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-endpoints-npm-3.357.0-b40cbd34ed-dcbe4a4ee0.zip differ diff --git a/.yarn/cache/@aws-sdk-util-locate-window-npm-3.310.0-0bb775a2bf-d552ce5f0f.zip b/.yarn/cache/@aws-sdk-util-locate-window-npm-3.310.0-0bb775a2bf-d552ce5f0f.zip new file mode 100644 index 000000000..b2e646276 Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-locate-window-npm-3.310.0-0bb775a2bf-d552ce5f0f.zip differ diff --git a/.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.363.0-305f9aad64-efd4319c2c.zip b/.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.363.0-305f9aad64-efd4319c2c.zip new file mode 100644 index 000000000..6c009979a Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-user-agent-browser-npm-3.363.0-305f9aad64-efd4319c2c.zip differ diff --git a/.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.363.0-0a96a5f405-bfa6bdcfdf.zip b/.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.363.0-0a96a5f405-bfa6bdcfdf.zip new file mode 100644 index 000000000..ea77a05d5 Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-user-agent-node-npm-3.363.0-0a96a5f405-bfa6bdcfdf.zip differ diff --git a/.yarn/cache/@aws-sdk-util-utf8-browser-npm-3.259.0-343a1dba08-b6a1e580da.zip b/.yarn/cache/@aws-sdk-util-utf8-browser-npm-3.259.0-343a1dba08-b6a1e580da.zip new file mode 100644 index 000000000..81ac20adf Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-utf8-browser-npm-3.259.0-343a1dba08-b6a1e580da.zip differ diff --git a/.yarn/cache/@aws-sdk-util-utf8-npm-3.310.0-f1575cc359-4045e79b8e.zip b/.yarn/cache/@aws-sdk-util-utf8-npm-3.310.0-f1575cc359-4045e79b8e.zip new file mode 100644 index 000000000..307eddf17 Binary files /dev/null and b/.yarn/cache/@aws-sdk-util-utf8-npm-3.310.0-f1575cc359-4045e79b8e.zip differ diff --git a/.yarn/cache/@aws-sdk-xml-builder-npm-3.310.0-5c3886db44-fc17fd8f68.zip b/.yarn/cache/@aws-sdk-xml-builder-npm-3.310.0-5c3886db44-fc17fd8f68.zip new file mode 100644 index 000000000..6cdae4824 Binary files /dev/null and b/.yarn/cache/@aws-sdk-xml-builder-npm-3.310.0-5c3886db44-fc17fd8f68.zip differ diff --git a/.yarn/cache/@babel-runtime-npm-7.22.5-0a6711d04c-12a50b7de2.zip b/.yarn/cache/@babel-runtime-npm-7.22.5-0a6711d04c-12a50b7de2.zip new file mode 100644 index 000000000..197f91be9 Binary files /dev/null and b/.yarn/cache/@babel-runtime-npm-7.22.5-0a6711d04c-12a50b7de2.zip differ diff --git a/.yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-8.zip b/.yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-8.zip new file mode 100644 index 000000000..bc292a3dd Binary files /dev/null and b/.yarn/cache/@cbor-extract-cbor-extract-darwin-arm64-npm-2.1.1-7f6025512f-8.zip differ diff --git a/.yarn/cache/@cbor-extract-cbor-extract-linux-arm64-npm-2.1.1-23a641c278-62548564f2.zip b/.yarn/cache/@cbor-extract-cbor-extract-linux-arm64-npm-2.1.1-23a641c278-62548564f2.zip new file mode 100644 index 000000000..0cd25f8e4 Binary files /dev/null and b/.yarn/cache/@cbor-extract-cbor-extract-linux-arm64-npm-2.1.1-23a641c278-62548564f2.zip differ diff --git a/.yarn/cache/@cbor-extract-cbor-extract-linux-x64-npm-2.1.1-4471164400-c46d3e3d8c.zip b/.yarn/cache/@cbor-extract-cbor-extract-linux-x64-npm-2.1.1-4471164400-c46d3e3d8c.zip new file mode 100644 index 000000000..e5950bfd4 Binary files /dev/null and b/.yarn/cache/@cbor-extract-cbor-extract-linux-x64-npm-2.1.1-4471164400-c46d3e3d8c.zip differ diff --git a/.yarn/cache/@chevrotain-cst-dts-gen-npm-10.5.0-96bdf6daf3-3ff851d5cb.zip b/.yarn/cache/@chevrotain-cst-dts-gen-npm-10.5.0-96bdf6daf3-3ff851d5cb.zip new file mode 100644 index 000000000..ee28eacbc Binary files /dev/null and b/.yarn/cache/@chevrotain-cst-dts-gen-npm-10.5.0-96bdf6daf3-3ff851d5cb.zip differ diff --git a/.yarn/cache/@chevrotain-gast-npm-10.5.0-c98fd4883a-35183e7067.zip b/.yarn/cache/@chevrotain-gast-npm-10.5.0-c98fd4883a-35183e7067.zip new file mode 100644 index 000000000..256bf1a5c Binary files /dev/null and b/.yarn/cache/@chevrotain-gast-npm-10.5.0-c98fd4883a-35183e7067.zip differ diff --git a/.yarn/cache/@chevrotain-types-npm-10.5.0-29d3508ef3-72f7b48de1.zip b/.yarn/cache/@chevrotain-types-npm-10.5.0-29d3508ef3-72f7b48de1.zip new file mode 100644 index 000000000..92a589acd Binary files /dev/null and b/.yarn/cache/@chevrotain-types-npm-10.5.0-29d3508ef3-72f7b48de1.zip differ diff --git a/.yarn/cache/@chevrotain-utils-npm-10.5.0-1c062e1720-f3ae9e0fea.zip b/.yarn/cache/@chevrotain-utils-npm-10.5.0-1c062e1720-f3ae9e0fea.zip new file mode 100644 index 000000000..d8010dac2 Binary files /dev/null and b/.yarn/cache/@chevrotain-utils-npm-10.5.0-1c062e1720-f3ae9e0fea.zip differ diff --git a/.yarn/cache/@colors-colors-npm-1.5.0-875af3a8b4-d64d5260be.zip b/.yarn/cache/@colors-colors-npm-1.5.0-875af3a8b4-d64d5260be.zip new file mode 100644 index 000000000..c1edd324f Binary files /dev/null and b/.yarn/cache/@colors-colors-npm-1.5.0-875af3a8b4-d64d5260be.zip differ diff --git a/.yarn/cache/@contrast-fn-inspect-npm-3.3.1-3a415085d5-fa3766f3f4.zip b/.yarn/cache/@contrast-fn-inspect-npm-3.3.1-3a415085d5-fa3766f3f4.zip new file mode 100644 index 000000000..6b136bae2 Binary files /dev/null and b/.yarn/cache/@contrast-fn-inspect-npm-3.3.1-3a415085d5-fa3766f3f4.zip differ diff --git a/.yarn/cache/@dabh-diagnostics-npm-2.0.3-0f2cd64f24-4879600c55.zip b/.yarn/cache/@dabh-diagnostics-npm-2.0.3-0f2cd64f24-4879600c55.zip new file mode 100644 index 000000000..43db318eb Binary files /dev/null and b/.yarn/cache/@dabh-diagnostics-npm-2.0.3-0f2cd64f24-4879600c55.zip differ diff --git a/.yarn/cache/@grpc-grpc-js-npm-1.8.17-420a6f3a9d-0ac8cd7342.zip b/.yarn/cache/@grpc-grpc-js-npm-1.8.17-420a6f3a9d-0ac8cd7342.zip new file mode 100644 index 000000000..599fe594d Binary files /dev/null and b/.yarn/cache/@grpc-grpc-js-npm-1.8.17-420a6f3a9d-0ac8cd7342.zip differ diff --git a/.yarn/cache/@grpc-proto-loader-npm-0.7.7-3c0a27e6a6-6015d99d36.zip b/.yarn/cache/@grpc-proto-loader-npm-0.7.7-3c0a27e6a6-6015d99d36.zip new file mode 100644 index 000000000..2a8cc09fc Binary files /dev/null and b/.yarn/cache/@grpc-proto-loader-npm-0.7.7-3c0a27e6a6-6015d99d36.zip differ diff --git a/.yarn/cache/@hexagon-base64-npm-1.1.26-dbfda05df8-596ccc3973.zip b/.yarn/cache/@hexagon-base64-npm-1.1.26-dbfda05df8-596ccc3973.zip new file mode 100644 index 000000000..dc7a28595 Binary files /dev/null and b/.yarn/cache/@hexagon-base64-npm-1.1.26-dbfda05df8-596ccc3973.zip differ diff --git a/.yarn/cache/@ioredis-commands-npm-1.2.0-47541de88b-9b20225ba3.zip b/.yarn/cache/@ioredis-commands-npm-1.2.0-47541de88b-9b20225ba3.zip new file mode 100644 index 000000000..fa54b5d9b Binary files /dev/null and b/.yarn/cache/@ioredis-commands-npm-1.2.0-47541de88b-9b20225ba3.zip differ diff --git a/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.10-1811160047-1a98db05d9.zip b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.10-1811160047-1a98db05d9.zip new file mode 100644 index 000000000..52d1268e9 Binary files /dev/null and b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.10-1811160047-1a98db05d9.zip differ diff --git a/.yarn/cache/@mrleebo-prisma-ast-npm-0.5.2-538c9d793e-ebb04e827b.zip b/.yarn/cache/@mrleebo-prisma-ast-npm-0.5.2-538c9d793e-ebb04e827b.zip new file mode 100644 index 000000000..354112281 Binary files /dev/null and b/.yarn/cache/@mrleebo-prisma-ast-npm-0.5.2-538c9d793e-ebb04e827b.zip differ diff --git a/.yarn/cache/@newrelic-aws-sdk-npm-6.0.0-a6ffb91c7a-e8ff9730dc.zip b/.yarn/cache/@newrelic-aws-sdk-npm-6.0.0-a6ffb91c7a-e8ff9730dc.zip new file mode 100644 index 000000000..9eeb3464b Binary files /dev/null and b/.yarn/cache/@newrelic-aws-sdk-npm-6.0.0-a6ffb91c7a-e8ff9730dc.zip differ diff --git a/.yarn/cache/@newrelic-koa-npm-7.2.0-94fd4c62e5-fb06ea7512.zip b/.yarn/cache/@newrelic-koa-npm-7.2.0-94fd4c62e5-fb06ea7512.zip new file mode 100644 index 000000000..4fe8dd4f8 Binary files /dev/null and b/.yarn/cache/@newrelic-koa-npm-7.2.0-94fd4c62e5-fb06ea7512.zip differ diff --git a/.yarn/cache/@newrelic-native-metrics-npm-9.0.1-0cceba255d-da3218df5b.zip b/.yarn/cache/@newrelic-native-metrics-npm-9.0.1-0cceba255d-da3218df5b.zip new file mode 100644 index 000000000..1fa89ee8d Binary files /dev/null and b/.yarn/cache/@newrelic-native-metrics-npm-9.0.1-0cceba255d-da3218df5b.zip differ diff --git a/.yarn/cache/@newrelic-security-agent-npm-0.1.3-c5175b3aa9-65215516a3.zip b/.yarn/cache/@newrelic-security-agent-npm-0.1.3-c5175b3aa9-65215516a3.zip new file mode 100644 index 000000000..8edda9a05 Binary files /dev/null and b/.yarn/cache/@newrelic-security-agent-npm-0.1.3-c5175b3aa9-65215516a3.zip differ diff --git a/.yarn/cache/@newrelic-superagent-npm-6.0.0-db8b77d0f3-d9e9b20d75.zip b/.yarn/cache/@newrelic-superagent-npm-6.0.0-db8b77d0f3-d9e9b20d75.zip new file mode 100644 index 000000000..fc06a9c47 Binary files /dev/null and b/.yarn/cache/@newrelic-superagent-npm-6.0.0-db8b77d0f3-d9e9b20d75.zip differ diff --git a/.yarn/cache/@newrelic-winston-enricher-npm-4.0.1-ef1230a3ce-e0f70495df.zip b/.yarn/cache/@newrelic-winston-enricher-npm-4.0.1-ef1230a3ce-e0f70495df.zip new file mode 100644 index 000000000..9af8c23a8 Binary files /dev/null and b/.yarn/cache/@newrelic-winston-enricher-npm-4.0.1-ef1230a3ce-e0f70495df.zip differ diff --git a/.yarn/cache/@peculiar-asn1-android-npm-2.3.6-cf68215309-66615ada47.zip b/.yarn/cache/@peculiar-asn1-android-npm-2.3.6-cf68215309-66615ada47.zip new file mode 100644 index 000000000..abc8380c3 Binary files /dev/null and b/.yarn/cache/@peculiar-asn1-android-npm-2.3.6-cf68215309-66615ada47.zip differ diff --git a/.yarn/cache/@peculiar-asn1-ecc-npm-2.3.6-63fb6f966e-4b9a383dd4.zip b/.yarn/cache/@peculiar-asn1-ecc-npm-2.3.6-63fb6f966e-4b9a383dd4.zip new file mode 100644 index 000000000..8d2464acc Binary files /dev/null and b/.yarn/cache/@peculiar-asn1-ecc-npm-2.3.6-63fb6f966e-4b9a383dd4.zip differ diff --git a/.yarn/cache/@peculiar-asn1-rsa-npm-2.3.6-8e964bf596-120dda00af.zip b/.yarn/cache/@peculiar-asn1-rsa-npm-2.3.6-8e964bf596-120dda00af.zip new file mode 100644 index 000000000..e131839fb Binary files /dev/null and b/.yarn/cache/@peculiar-asn1-rsa-npm-2.3.6-8e964bf596-120dda00af.zip differ diff --git a/.yarn/cache/@peculiar-asn1-schema-npm-2.3.6-1020f463b2-fc09387c6e.zip b/.yarn/cache/@peculiar-asn1-schema-npm-2.3.6-1020f463b2-fc09387c6e.zip new file mode 100644 index 000000000..2a3410ac2 Binary files /dev/null and b/.yarn/cache/@peculiar-asn1-schema-npm-2.3.6-1020f463b2-fc09387c6e.zip differ diff --git a/.yarn/cache/@peculiar-asn1-x509-npm-2.3.6-5714dc2041-6e946bd440.zip b/.yarn/cache/@peculiar-asn1-x509-npm-2.3.6-5714dc2041-6e946bd440.zip new file mode 100644 index 000000000..63a7c799f Binary files /dev/null and b/.yarn/cache/@peculiar-asn1-x509-npm-2.3.6-5714dc2041-6e946bd440.zip differ diff --git a/.yarn/cache/@protobufjs-aspromise-npm-1.1.2-71d00b938f-011fe7ef08.zip b/.yarn/cache/@protobufjs-aspromise-npm-1.1.2-71d00b938f-011fe7ef08.zip new file mode 100644 index 000000000..fc9081b9d Binary files /dev/null and b/.yarn/cache/@protobufjs-aspromise-npm-1.1.2-71d00b938f-011fe7ef08.zip differ diff --git a/.yarn/cache/@protobufjs-base64-npm-1.1.2-cd8ca6814a-67173ac34d.zip b/.yarn/cache/@protobufjs-base64-npm-1.1.2-cd8ca6814a-67173ac34d.zip new file mode 100644 index 000000000..cdc42f13f Binary files /dev/null and b/.yarn/cache/@protobufjs-base64-npm-1.1.2-cd8ca6814a-67173ac34d.zip differ diff --git a/.yarn/cache/@protobufjs-codegen-npm-2.0.4-36e188bbe6-59240c850b.zip b/.yarn/cache/@protobufjs-codegen-npm-2.0.4-36e188bbe6-59240c850b.zip new file mode 100644 index 000000000..2217a817e Binary files /dev/null and b/.yarn/cache/@protobufjs-codegen-npm-2.0.4-36e188bbe6-59240c850b.zip differ diff --git a/.yarn/cache/@protobufjs-eventemitter-npm-1.1.0-029cc7d431-0369163a3d.zip b/.yarn/cache/@protobufjs-eventemitter-npm-1.1.0-029cc7d431-0369163a3d.zip new file mode 100644 index 000000000..917298153 Binary files /dev/null and b/.yarn/cache/@protobufjs-eventemitter-npm-1.1.0-029cc7d431-0369163a3d.zip differ diff --git a/.yarn/cache/@protobufjs-fetch-npm-1.1.0-ca857b7df4-3fce7e09eb.zip b/.yarn/cache/@protobufjs-fetch-npm-1.1.0-ca857b7df4-3fce7e09eb.zip new file mode 100644 index 000000000..3f687b0bf Binary files /dev/null and b/.yarn/cache/@protobufjs-fetch-npm-1.1.0-ca857b7df4-3fce7e09eb.zip differ diff --git a/.yarn/cache/@protobufjs-float-npm-1.0.2-5678f64d08-5781e12412.zip b/.yarn/cache/@protobufjs-float-npm-1.0.2-5678f64d08-5781e12412.zip new file mode 100644 index 000000000..d7027a9cf Binary files /dev/null and b/.yarn/cache/@protobufjs-float-npm-1.0.2-5678f64d08-5781e12412.zip differ diff --git a/.yarn/cache/@protobufjs-inquire-npm-1.1.0-3c7759e9ce-ca06f02eaf.zip b/.yarn/cache/@protobufjs-inquire-npm-1.1.0-3c7759e9ce-ca06f02eaf.zip new file mode 100644 index 000000000..c7a6b3dcd Binary files /dev/null and b/.yarn/cache/@protobufjs-inquire-npm-1.1.0-3c7759e9ce-ca06f02eaf.zip differ diff --git a/.yarn/cache/@protobufjs-path-npm-1.1.2-641d08de76-856eeb532b.zip b/.yarn/cache/@protobufjs-path-npm-1.1.2-641d08de76-856eeb532b.zip new file mode 100644 index 000000000..27b166d22 Binary files /dev/null and b/.yarn/cache/@protobufjs-path-npm-1.1.2-641d08de76-856eeb532b.zip differ diff --git a/.yarn/cache/@protobufjs-pool-npm-1.1.0-47a76f96a1-d6a34fbbd2.zip b/.yarn/cache/@protobufjs-pool-npm-1.1.0-47a76f96a1-d6a34fbbd2.zip new file mode 100644 index 000000000..14babc22b Binary files /dev/null and b/.yarn/cache/@protobufjs-pool-npm-1.1.0-47a76f96a1-d6a34fbbd2.zip differ diff --git a/.yarn/cache/@protobufjs-utf8-npm-1.1.0-02c590807c-f9bf3163d1.zip b/.yarn/cache/@protobufjs-utf8-npm-1.1.0-02c590807c-f9bf3163d1.zip new file mode 100644 index 000000000..6e9fdd4c7 Binary files /dev/null and b/.yarn/cache/@protobufjs-utf8-npm-1.1.0-02c590807c-f9bf3163d1.zip differ diff --git a/.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.2.0-db7b12b859-663b3cf8b8.zip b/.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.2.0-db7b12b859-663b3cf8b8.zip new file mode 100644 index 000000000..ec3f9d245 Binary files /dev/null and b/.yarn/cache/@simplewebauthn-iso-webcrypto-npm-7.2.0-db7b12b859-663b3cf8b8.zip differ diff --git a/.yarn/cache/@simplewebauthn-server-npm-7.3.1-dcddc5ba7c-0657cd6a85.zip b/.yarn/cache/@simplewebauthn-server-npm-7.3.1-dcddc5ba7c-0657cd6a85.zip new file mode 100644 index 000000000..595ace451 Binary files /dev/null and b/.yarn/cache/@simplewebauthn-server-npm-7.3.1-dcddc5ba7c-0657cd6a85.zip differ diff --git a/.yarn/cache/@simplewebauthn-typescript-types-npm-7.0.0-cc6ca20415-11e55e7403.zip b/.yarn/cache/@simplewebauthn-typescript-types-npm-7.0.0-cc6ca20415-11e55e7403.zip new file mode 100644 index 000000000..86ae00cc1 Binary files /dev/null and b/.yarn/cache/@simplewebauthn-typescript-types-npm-7.0.0-cc6ca20415-11e55e7403.zip differ diff --git a/.yarn/cache/@smithy-abort-controller-npm-1.0.1-ee1deaca3d-f472dd2434.zip b/.yarn/cache/@smithy-abort-controller-npm-1.0.1-ee1deaca3d-f472dd2434.zip new file mode 100644 index 000000000..13eafbaec Binary files /dev/null and b/.yarn/cache/@smithy-abort-controller-npm-1.0.1-ee1deaca3d-f472dd2434.zip differ diff --git a/.yarn/cache/@smithy-config-resolver-npm-1.0.1-ada2f26d57-c0489bfd52.zip b/.yarn/cache/@smithy-config-resolver-npm-1.0.1-ada2f26d57-c0489bfd52.zip new file mode 100644 index 000000000..c46dadaa6 Binary files /dev/null and b/.yarn/cache/@smithy-config-resolver-npm-1.0.1-ada2f26d57-c0489bfd52.zip differ diff --git a/.yarn/cache/@smithy-credential-provider-imds-npm-1.0.1-4c86bf026d-41bfa3e472.zip b/.yarn/cache/@smithy-credential-provider-imds-npm-1.0.1-4c86bf026d-41bfa3e472.zip new file mode 100644 index 000000000..89597756c Binary files /dev/null and b/.yarn/cache/@smithy-credential-provider-imds-npm-1.0.1-4c86bf026d-41bfa3e472.zip differ diff --git a/.yarn/cache/@smithy-eventstream-codec-npm-1.0.1-c327e1a72d-042e387582.zip b/.yarn/cache/@smithy-eventstream-codec-npm-1.0.1-c327e1a72d-042e387582.zip new file mode 100644 index 000000000..71bee94d1 Binary files /dev/null and b/.yarn/cache/@smithy-eventstream-codec-npm-1.0.1-c327e1a72d-042e387582.zip differ diff --git a/.yarn/cache/@smithy-eventstream-serde-browser-npm-1.0.1-ab1100d3f0-98557e6093.zip b/.yarn/cache/@smithy-eventstream-serde-browser-npm-1.0.1-ab1100d3f0-98557e6093.zip new file mode 100644 index 000000000..afee9affd Binary files /dev/null and b/.yarn/cache/@smithy-eventstream-serde-browser-npm-1.0.1-ab1100d3f0-98557e6093.zip differ diff --git a/.yarn/cache/@smithy-eventstream-serde-config-resolver-npm-1.0.1-2a4902cdd0-68a821a2d8.zip b/.yarn/cache/@smithy-eventstream-serde-config-resolver-npm-1.0.1-2a4902cdd0-68a821a2d8.zip new file mode 100644 index 000000000..28fbdad2b Binary files /dev/null and b/.yarn/cache/@smithy-eventstream-serde-config-resolver-npm-1.0.1-2a4902cdd0-68a821a2d8.zip differ diff --git a/.yarn/cache/@smithy-eventstream-serde-node-npm-1.0.1-46cf5469b2-300d434e2b.zip b/.yarn/cache/@smithy-eventstream-serde-node-npm-1.0.1-46cf5469b2-300d434e2b.zip new file mode 100644 index 000000000..0567d3c7b Binary files /dev/null and b/.yarn/cache/@smithy-eventstream-serde-node-npm-1.0.1-46cf5469b2-300d434e2b.zip differ diff --git a/.yarn/cache/@smithy-eventstream-serde-universal-npm-1.0.1-dac553ff28-bf2d3cdba9.zip b/.yarn/cache/@smithy-eventstream-serde-universal-npm-1.0.1-dac553ff28-bf2d3cdba9.zip new file mode 100644 index 000000000..7bee5d75c Binary files /dev/null and b/.yarn/cache/@smithy-eventstream-serde-universal-npm-1.0.1-dac553ff28-bf2d3cdba9.zip differ diff --git a/.yarn/cache/@smithy-fetch-http-handler-npm-1.0.1-ccac6ba111-ed13d1c9f4.zip b/.yarn/cache/@smithy-fetch-http-handler-npm-1.0.1-ccac6ba111-ed13d1c9f4.zip new file mode 100644 index 000000000..862553baa Binary files /dev/null and b/.yarn/cache/@smithy-fetch-http-handler-npm-1.0.1-ccac6ba111-ed13d1c9f4.zip differ diff --git a/.yarn/cache/@smithy-hash-node-npm-1.0.1-37f36e6496-94caff8828.zip b/.yarn/cache/@smithy-hash-node-npm-1.0.1-37f36e6496-94caff8828.zip new file mode 100644 index 000000000..29fd9d8ab Binary files /dev/null and b/.yarn/cache/@smithy-hash-node-npm-1.0.1-37f36e6496-94caff8828.zip differ diff --git a/.yarn/cache/@smithy-invalid-dependency-npm-1.0.1-32fd07ecd7-9e6d20a53a.zip b/.yarn/cache/@smithy-invalid-dependency-npm-1.0.1-32fd07ecd7-9e6d20a53a.zip new file mode 100644 index 000000000..1c1fd37bc Binary files /dev/null and b/.yarn/cache/@smithy-invalid-dependency-npm-1.0.1-32fd07ecd7-9e6d20a53a.zip differ diff --git a/.yarn/cache/@smithy-is-array-buffer-npm-1.0.1-33d60ff654-1b582c1969.zip b/.yarn/cache/@smithy-is-array-buffer-npm-1.0.1-33d60ff654-1b582c1969.zip new file mode 100644 index 000000000..be99bded1 Binary files /dev/null and b/.yarn/cache/@smithy-is-array-buffer-npm-1.0.1-33d60ff654-1b582c1969.zip differ diff --git a/.yarn/cache/@smithy-md5-js-npm-1.0.1-809e7bad6b-4c2cd45d75.zip b/.yarn/cache/@smithy-md5-js-npm-1.0.1-809e7bad6b-4c2cd45d75.zip new file mode 100644 index 000000000..192606abf Binary files /dev/null and b/.yarn/cache/@smithy-md5-js-npm-1.0.1-809e7bad6b-4c2cd45d75.zip differ diff --git a/.yarn/cache/@smithy-middleware-content-length-npm-1.0.1-90a89e3055-93847fcb22.zip b/.yarn/cache/@smithy-middleware-content-length-npm-1.0.1-90a89e3055-93847fcb22.zip new file mode 100644 index 000000000..dff36f2e6 Binary files /dev/null and b/.yarn/cache/@smithy-middleware-content-length-npm-1.0.1-90a89e3055-93847fcb22.zip differ diff --git a/.yarn/cache/@smithy-middleware-endpoint-npm-1.0.2-7b20292684-6a5182c1c6.zip b/.yarn/cache/@smithy-middleware-endpoint-npm-1.0.2-7b20292684-6a5182c1c6.zip new file mode 100644 index 000000000..5251f0980 Binary files /dev/null and b/.yarn/cache/@smithy-middleware-endpoint-npm-1.0.2-7b20292684-6a5182c1c6.zip differ diff --git a/.yarn/cache/@smithy-middleware-retry-npm-1.0.3-5eb2f3048b-b9d829be9f.zip b/.yarn/cache/@smithy-middleware-retry-npm-1.0.3-5eb2f3048b-b9d829be9f.zip new file mode 100644 index 000000000..5a9dee3e5 Binary files /dev/null and b/.yarn/cache/@smithy-middleware-retry-npm-1.0.3-5eb2f3048b-b9d829be9f.zip differ diff --git a/.yarn/cache/@smithy-middleware-serde-npm-1.0.1-fc0aa888a9-06c783fcf9.zip b/.yarn/cache/@smithy-middleware-serde-npm-1.0.1-fc0aa888a9-06c783fcf9.zip new file mode 100644 index 000000000..d15fc3015 Binary files /dev/null and b/.yarn/cache/@smithy-middleware-serde-npm-1.0.1-fc0aa888a9-06c783fcf9.zip differ diff --git a/.yarn/cache/@smithy-middleware-stack-npm-1.0.1-6f1e59f278-fe428320fe.zip b/.yarn/cache/@smithy-middleware-stack-npm-1.0.1-6f1e59f278-fe428320fe.zip new file mode 100644 index 000000000..34603eb79 Binary files /dev/null and b/.yarn/cache/@smithy-middleware-stack-npm-1.0.1-6f1e59f278-fe428320fe.zip differ diff --git a/.yarn/cache/@smithy-node-config-provider-npm-1.0.1-3b330da955-ac85f8f368.zip b/.yarn/cache/@smithy-node-config-provider-npm-1.0.1-3b330da955-ac85f8f368.zip new file mode 100644 index 000000000..463e99ab2 Binary files /dev/null and b/.yarn/cache/@smithy-node-config-provider-npm-1.0.1-3b330da955-ac85f8f368.zip differ diff --git a/.yarn/cache/@smithy-node-http-handler-npm-1.0.2-bd82dbc0a7-ed0c84000a.zip b/.yarn/cache/@smithy-node-http-handler-npm-1.0.2-bd82dbc0a7-ed0c84000a.zip new file mode 100644 index 000000000..0037fa9af Binary files /dev/null and b/.yarn/cache/@smithy-node-http-handler-npm-1.0.2-bd82dbc0a7-ed0c84000a.zip differ diff --git a/.yarn/cache/@smithy-property-provider-npm-1.0.1-1b18e0bd32-5700a745fe.zip b/.yarn/cache/@smithy-property-provider-npm-1.0.1-1b18e0bd32-5700a745fe.zip new file mode 100644 index 000000000..4233e658b Binary files /dev/null and b/.yarn/cache/@smithy-property-provider-npm-1.0.1-1b18e0bd32-5700a745fe.zip differ diff --git a/.yarn/cache/@smithy-protocol-http-npm-1.1.0-de1eca6ef1-f912e085a4.zip b/.yarn/cache/@smithy-protocol-http-npm-1.1.0-de1eca6ef1-f912e085a4.zip new file mode 100644 index 000000000..77d71dac0 Binary files /dev/null and b/.yarn/cache/@smithy-protocol-http-npm-1.1.0-de1eca6ef1-f912e085a4.zip differ diff --git a/.yarn/cache/@smithy-querystring-builder-npm-1.0.1-9269ef52bf-f8f0a9e008.zip b/.yarn/cache/@smithy-querystring-builder-npm-1.0.1-9269ef52bf-f8f0a9e008.zip new file mode 100644 index 000000000..cc3d86462 Binary files /dev/null and b/.yarn/cache/@smithy-querystring-builder-npm-1.0.1-9269ef52bf-f8f0a9e008.zip differ diff --git a/.yarn/cache/@smithy-querystring-parser-npm-1.0.1-294a8b226e-a62ea8df9b.zip b/.yarn/cache/@smithy-querystring-parser-npm-1.0.1-294a8b226e-a62ea8df9b.zip new file mode 100644 index 000000000..88a860f81 Binary files /dev/null and b/.yarn/cache/@smithy-querystring-parser-npm-1.0.1-294a8b226e-a62ea8df9b.zip differ diff --git a/.yarn/cache/@smithy-service-error-classification-npm-1.0.2-a1ec92a3c6-e6a5298d77.zip b/.yarn/cache/@smithy-service-error-classification-npm-1.0.2-a1ec92a3c6-e6a5298d77.zip new file mode 100644 index 000000000..0b88afe97 Binary files /dev/null and b/.yarn/cache/@smithy-service-error-classification-npm-1.0.2-a1ec92a3c6-e6a5298d77.zip differ diff --git a/.yarn/cache/@smithy-shared-ini-file-loader-npm-1.0.1-8acd3f098f-07b6e85979.zip b/.yarn/cache/@smithy-shared-ini-file-loader-npm-1.0.1-8acd3f098f-07b6e85979.zip new file mode 100644 index 000000000..dae947d88 Binary files /dev/null and b/.yarn/cache/@smithy-shared-ini-file-loader-npm-1.0.1-8acd3f098f-07b6e85979.zip differ diff --git a/.yarn/cache/@smithy-signature-v4-npm-1.0.1-f5d6b4a23b-4696f81c79.zip b/.yarn/cache/@smithy-signature-v4-npm-1.0.1-f5d6b4a23b-4696f81c79.zip new file mode 100644 index 000000000..df9e88787 Binary files /dev/null and b/.yarn/cache/@smithy-signature-v4-npm-1.0.1-f5d6b4a23b-4696f81c79.zip differ diff --git a/.yarn/cache/@smithy-smithy-client-npm-1.0.3-fb91c6b7f3-5b6092cfce.zip b/.yarn/cache/@smithy-smithy-client-npm-1.0.3-fb91c6b7f3-5b6092cfce.zip new file mode 100644 index 000000000..596e2fcfe Binary files /dev/null and b/.yarn/cache/@smithy-smithy-client-npm-1.0.3-fb91c6b7f3-5b6092cfce.zip differ diff --git a/.yarn/cache/@smithy-types-npm-1.1.0-5815164e7e-8c0589fa97.zip b/.yarn/cache/@smithy-types-npm-1.1.0-5815164e7e-8c0589fa97.zip new file mode 100644 index 000000000..5c4475eb5 Binary files /dev/null and b/.yarn/cache/@smithy-types-npm-1.1.0-5815164e7e-8c0589fa97.zip differ diff --git a/.yarn/cache/@smithy-url-parser-npm-1.0.1-baa5a327cd-ced0dba013.zip b/.yarn/cache/@smithy-url-parser-npm-1.0.1-baa5a327cd-ced0dba013.zip new file mode 100644 index 000000000..5fed2395d Binary files /dev/null and b/.yarn/cache/@smithy-url-parser-npm-1.0.1-baa5a327cd-ced0dba013.zip differ diff --git a/.yarn/cache/@smithy-util-base64-npm-1.0.1-ec4fb9e662-8ac7a8562c.zip b/.yarn/cache/@smithy-util-base64-npm-1.0.1-ec4fb9e662-8ac7a8562c.zip new file mode 100644 index 000000000..ba0fcf5fc Binary files /dev/null and b/.yarn/cache/@smithy-util-base64-npm-1.0.1-ec4fb9e662-8ac7a8562c.zip differ diff --git a/.yarn/cache/@smithy-util-body-length-browser-npm-1.0.1-e625f6dbf2-b4afec2825.zip b/.yarn/cache/@smithy-util-body-length-browser-npm-1.0.1-e625f6dbf2-b4afec2825.zip new file mode 100644 index 000000000..7f1bb8e50 Binary files /dev/null and b/.yarn/cache/@smithy-util-body-length-browser-npm-1.0.1-e625f6dbf2-b4afec2825.zip differ diff --git a/.yarn/cache/@smithy-util-body-length-node-npm-1.0.1-6664136d06-a70440e32c.zip b/.yarn/cache/@smithy-util-body-length-node-npm-1.0.1-6664136d06-a70440e32c.zip new file mode 100644 index 000000000..83504f7c3 Binary files /dev/null and b/.yarn/cache/@smithy-util-body-length-node-npm-1.0.1-6664136d06-a70440e32c.zip differ diff --git a/.yarn/cache/@smithy-util-buffer-from-npm-1.0.1-67832654d2-bfb18108d8.zip b/.yarn/cache/@smithy-util-buffer-from-npm-1.0.1-67832654d2-bfb18108d8.zip new file mode 100644 index 000000000..d87e7a2da Binary files /dev/null and b/.yarn/cache/@smithy-util-buffer-from-npm-1.0.1-67832654d2-bfb18108d8.zip differ diff --git a/.yarn/cache/@smithy-util-config-provider-npm-1.0.1-5b93c15424-5365b79d39.zip b/.yarn/cache/@smithy-util-config-provider-npm-1.0.1-5b93c15424-5365b79d39.zip new file mode 100644 index 000000000..f1f06cda5 Binary files /dev/null and b/.yarn/cache/@smithy-util-config-provider-npm-1.0.1-5b93c15424-5365b79d39.zip differ diff --git a/.yarn/cache/@smithy-util-defaults-mode-browser-npm-1.0.1-4af98a2c1e-510cab3dfd.zip b/.yarn/cache/@smithy-util-defaults-mode-browser-npm-1.0.1-4af98a2c1e-510cab3dfd.zip new file mode 100644 index 000000000..0ecc1463b Binary files /dev/null and b/.yarn/cache/@smithy-util-defaults-mode-browser-npm-1.0.1-4af98a2c1e-510cab3dfd.zip differ diff --git a/.yarn/cache/@smithy-util-defaults-mode-node-npm-1.0.1-8a32cc0722-dde9cc0cde.zip b/.yarn/cache/@smithy-util-defaults-mode-node-npm-1.0.1-8a32cc0722-dde9cc0cde.zip new file mode 100644 index 000000000..9692cc15e Binary files /dev/null and b/.yarn/cache/@smithy-util-defaults-mode-node-npm-1.0.1-8a32cc0722-dde9cc0cde.zip differ diff --git a/.yarn/cache/@smithy-util-hex-encoding-npm-1.0.1-bcae7e022a-8a5da7863a.zip b/.yarn/cache/@smithy-util-hex-encoding-npm-1.0.1-bcae7e022a-8a5da7863a.zip new file mode 100644 index 000000000..552c3e562 Binary files /dev/null and b/.yarn/cache/@smithy-util-hex-encoding-npm-1.0.1-bcae7e022a-8a5da7863a.zip differ diff --git a/.yarn/cache/@smithy-util-middleware-npm-1.0.1-daa9b99840-e6c143b172.zip b/.yarn/cache/@smithy-util-middleware-npm-1.0.1-daa9b99840-e6c143b172.zip new file mode 100644 index 000000000..82fa151ec Binary files /dev/null and b/.yarn/cache/@smithy-util-middleware-npm-1.0.1-daa9b99840-e6c143b172.zip differ diff --git a/.yarn/cache/@smithy-util-retry-npm-1.0.3-546c228e3f-ebded11887.zip b/.yarn/cache/@smithy-util-retry-npm-1.0.3-546c228e3f-ebded11887.zip new file mode 100644 index 000000000..990719f16 Binary files /dev/null and b/.yarn/cache/@smithy-util-retry-npm-1.0.3-546c228e3f-ebded11887.zip differ diff --git a/.yarn/cache/@smithy-util-stream-npm-1.0.1-938ce31a6c-faf57c8b2f.zip b/.yarn/cache/@smithy-util-stream-npm-1.0.1-938ce31a6c-faf57c8b2f.zip new file mode 100644 index 000000000..ef504740e Binary files /dev/null and b/.yarn/cache/@smithy-util-stream-npm-1.0.1-938ce31a6c-faf57c8b2f.zip differ diff --git a/.yarn/cache/@smithy-util-uri-escape-npm-1.0.1-11fccae1bd-109a4ab88c.zip b/.yarn/cache/@smithy-util-uri-escape-npm-1.0.1-11fccae1bd-109a4ab88c.zip new file mode 100644 index 000000000..8c8fd9848 Binary files /dev/null and b/.yarn/cache/@smithy-util-uri-escape-npm-1.0.1-11fccae1bd-109a4ab88c.zip differ diff --git a/.yarn/cache/@smithy-util-utf8-npm-1.0.1-c043a08414-ddb841cfe8.zip b/.yarn/cache/@smithy-util-utf8-npm-1.0.1-c043a08414-ddb841cfe8.zip new file mode 100644 index 000000000..789ea251a Binary files /dev/null and b/.yarn/cache/@smithy-util-utf8-npm-1.0.1-c043a08414-ddb841cfe8.zip differ diff --git a/.yarn/cache/@smithy-util-waiter-npm-1.0.1-f5c9f8222f-24342710f6.zip b/.yarn/cache/@smithy-util-waiter-npm-1.0.1-f5c9f8222f-24342710f6.zip new file mode 100644 index 000000000..e4a96470a Binary files /dev/null and b/.yarn/cache/@smithy-util-waiter-npm-1.0.1-f5c9f8222f-24342710f6.zip differ diff --git a/.yarn/cache/@sqltools-formatter-npm-1.2.5-709e7c0ab8-9b8354e715.zip b/.yarn/cache/@sqltools-formatter-npm-1.2.5-709e7c0ab8-9b8354e715.zip new file mode 100644 index 000000000..44e0dbb68 Binary files /dev/null and b/.yarn/cache/@sqltools-formatter-npm-1.2.5-709e7c0ab8-9b8354e715.zip differ diff --git a/.yarn/cache/@standardnotes-api-gateway-npm-1.65.2-f70199e3f8-fd6a4920c8.zip b/.yarn/cache/@standardnotes-api-gateway-npm-1.65.2-f70199e3f8-fd6a4920c8.zip new file mode 100644 index 000000000..8e8996a5d Binary files /dev/null and b/.yarn/cache/@standardnotes-api-gateway-npm-1.65.2-f70199e3f8-fd6a4920c8.zip differ diff --git a/.yarn/cache/@standardnotes-auth-server-npm-1.120.2-ed04893734-019e5afc5d.zip b/.yarn/cache/@standardnotes-auth-server-npm-1.120.2-ed04893734-019e5afc5d.zip new file mode 100644 index 000000000..b33e3562d Binary files /dev/null and b/.yarn/cache/@standardnotes-auth-server-npm-1.120.2-ed04893734-019e5afc5d.zip differ diff --git a/.yarn/cache/@standardnotes-common-npm-1.49.0-05c84db337-064180f442.zip b/.yarn/cache/@standardnotes-common-npm-1.49.0-05c84db337-064180f442.zip new file mode 100644 index 000000000..d03563afa Binary files /dev/null and b/.yarn/cache/@standardnotes-common-npm-1.49.0-05c84db337-064180f442.zip differ diff --git a/.yarn/cache/@standardnotes-domain-core-npm-1.19.0-47cf4e4bba-40d3cad6d6.zip b/.yarn/cache/@standardnotes-domain-core-npm-1.19.0-47cf4e4bba-40d3cad6d6.zip new file mode 100644 index 000000000..6ad7fc569 Binary files /dev/null and b/.yarn/cache/@standardnotes-domain-core-npm-1.19.0-47cf4e4bba-40d3cad6d6.zip differ diff --git a/.yarn/cache/@standardnotes-domain-events-infra-npm-1.12.7-091ea7089a-76c2c21012.zip b/.yarn/cache/@standardnotes-domain-events-infra-npm-1.12.7-091ea7089a-76c2c21012.zip new file mode 100644 index 000000000..5cec7406b Binary files /dev/null and b/.yarn/cache/@standardnotes-domain-events-infra-npm-1.12.7-091ea7089a-76c2c21012.zip differ diff --git a/.yarn/cache/@standardnotes-domain-events-npm-2.112.1-4d55a2f0d9-96c88d3c7f.zip b/.yarn/cache/@standardnotes-domain-events-npm-2.112.1-4d55a2f0d9-96c88d3c7f.zip new file mode 100644 index 000000000..879543395 Binary files /dev/null and b/.yarn/cache/@standardnotes-domain-events-npm-2.112.1-4d55a2f0d9-96c88d3c7f.zip differ diff --git a/.yarn/cache/@standardnotes-files-server-npm-1.19.2-8e44cea492-39d2e3c757.zip b/.yarn/cache/@standardnotes-files-server-npm-1.19.2-8e44cea492-39d2e3c757.zip new file mode 100644 index 000000000..d6ee4a92f Binary files /dev/null and b/.yarn/cache/@standardnotes-files-server-npm-1.19.2-8e44cea492-39d2e3c757.zip differ diff --git a/.yarn/cache/@standardnotes-home-server-npm-1.11.19-fb76684933-01b5f7aee5.zip b/.yarn/cache/@standardnotes-home-server-npm-1.11.19-fb76684933-01b5f7aee5.zip new file mode 100644 index 000000000..b66d514b9 Binary files /dev/null and b/.yarn/cache/@standardnotes-home-server-npm-1.11.19-fb76684933-01b5f7aee5.zip differ diff --git a/.yarn/cache/@standardnotes-predicates-npm-1.6.9-7b42d49f9d-3b931aaccd.zip b/.yarn/cache/@standardnotes-predicates-npm-1.6.9-7b42d49f9d-3b931aaccd.zip new file mode 100644 index 000000000..8da3424d5 Binary files /dev/null and b/.yarn/cache/@standardnotes-predicates-npm-1.6.9-7b42d49f9d-3b931aaccd.zip differ diff --git a/.yarn/cache/@standardnotes-revisions-server-npm-1.23.6-7bef9abf12-a9500e702e.zip b/.yarn/cache/@standardnotes-revisions-server-npm-1.23.6-7bef9abf12-a9500e702e.zip new file mode 100644 index 000000000..60890278e Binary files /dev/null and b/.yarn/cache/@standardnotes-revisions-server-npm-1.23.6-7bef9abf12-a9500e702e.zip differ diff --git a/.yarn/cache/@standardnotes-security-npm-1.8.1-ad9439c6e5-5218a924af.zip b/.yarn/cache/@standardnotes-security-npm-1.8.1-ad9439c6e5-5218a924af.zip new file mode 100644 index 000000000..2851f0d75 Binary files /dev/null and b/.yarn/cache/@standardnotes-security-npm-1.8.1-ad9439c6e5-5218a924af.zip differ diff --git a/.yarn/cache/@standardnotes-settings-npm-1.21.9-beea2ccfe9-0ed9ba7dba.zip b/.yarn/cache/@standardnotes-settings-npm-1.21.9-beea2ccfe9-0ed9ba7dba.zip new file mode 100644 index 000000000..b0619b24a Binary files /dev/null and b/.yarn/cache/@standardnotes-settings-npm-1.21.9-beea2ccfe9-0ed9ba7dba.zip differ diff --git a/.yarn/cache/@standardnotes-sncrypto-node-npm-1.15.2-d57c2e35dd-01771d7369.zip b/.yarn/cache/@standardnotes-sncrypto-node-npm-1.15.2-d57c2e35dd-01771d7369.zip new file mode 100644 index 000000000..d1aa7bc24 Binary files /dev/null and b/.yarn/cache/@standardnotes-sncrypto-node-npm-1.15.2-d57c2e35dd-01771d7369.zip differ diff --git a/.yarn/cache/@standardnotes-syncing-server-npm-1.47.0-fe9f1fdcb4-ba755da3ca.zip b/.yarn/cache/@standardnotes-syncing-server-npm-1.47.0-fe9f1fdcb4-ba755da3ca.zip new file mode 100644 index 000000000..e2856b7e3 Binary files /dev/null and b/.yarn/cache/@standardnotes-syncing-server-npm-1.47.0-fe9f1fdcb4-ba755da3ca.zip differ diff --git a/.yarn/cache/@standardnotes-time-npm-1.14.6-9a887d0e87-facd440306.zip b/.yarn/cache/@standardnotes-time-npm-1.14.6-9a887d0e87-facd440306.zip new file mode 100644 index 000000000..9cb3fc8e0 Binary files /dev/null and b/.yarn/cache/@standardnotes-time-npm-1.14.6-9a887d0e87-facd440306.zip differ diff --git a/.yarn/cache/@types-concat-stream-npm-1.6.1-42cd06b019-7d211e7433.zip b/.yarn/cache/@types-concat-stream-npm-1.6.1-42cd06b019-7d211e7433.zip new file mode 100644 index 000000000..af2a7f6e6 Binary files /dev/null and b/.yarn/cache/@types-concat-stream-npm-1.6.1-42cd06b019-7d211e7433.zip differ diff --git a/.yarn/cache/@types-debug-npm-4.1.8-a04e2ca136-a9a9bb40a1.zip b/.yarn/cache/@types-debug-npm-4.1.8-a04e2ca136-a9a9bb40a1.zip new file mode 100644 index 000000000..62cdd38f2 Binary files /dev/null and b/.yarn/cache/@types-debug-npm-4.1.8-a04e2ca136-a9a9bb40a1.zip differ diff --git a/.yarn/cache/@types-form-data-npm-0.0.33-3cbbcd9710-f0c283fdef.zip b/.yarn/cache/@types-form-data-npm-0.0.33-3cbbcd9710-f0c283fdef.zip new file mode 100644 index 000000000..01403b010 Binary files /dev/null and b/.yarn/cache/@types-form-data-npm-0.0.33-3cbbcd9710-f0c283fdef.zip differ diff --git a/.yarn/cache/@types-fs-extra-npm-11.0.1-f11620397b-3e930346e5.zip b/.yarn/cache/@types-fs-extra-npm-11.0.1-f11620397b-3e930346e5.zip new file mode 100644 index 000000000..e8c09b75d Binary files /dev/null and b/.yarn/cache/@types-fs-extra-npm-11.0.1-f11620397b-3e930346e5.zip differ diff --git a/.yarn/cache/@types-jsonfile-npm-6.1.1-62ad833c58-0f8fe0a922.zip b/.yarn/cache/@types-jsonfile-npm-6.1.1-62ad833c58-0f8fe0a922.zip new file mode 100644 index 000000000..d7a6fb7b5 Binary files /dev/null and b/.yarn/cache/@types-jsonfile-npm-6.1.1-62ad833c58-0f8fe0a922.zip differ diff --git a/.yarn/cache/@types-long-npm-4.0.2-e7bdc00dd4-d16cde7240.zip b/.yarn/cache/@types-long-npm-4.0.2-e7bdc00dd4-d16cde7240.zip new file mode 100644 index 000000000..677817d19 Binary files /dev/null and b/.yarn/cache/@types-long-npm-4.0.2-e7bdc00dd4-d16cde7240.zip differ diff --git a/.yarn/cache/@types-newrelic-npm-9.14.0-4668da51a1-7f7bfc77e0.zip b/.yarn/cache/@types-newrelic-npm-9.14.0-4668da51a1-7f7bfc77e0.zip new file mode 100644 index 000000000..3b13492ed Binary files /dev/null and b/.yarn/cache/@types-newrelic-npm-9.14.0-4668da51a1-7f7bfc77e0.zip differ diff --git a/.yarn/cache/@types-node-npm-10.17.60-63ac1f669f-2cdb3a77d0.zip b/.yarn/cache/@types-node-npm-10.17.60-63ac1f669f-2cdb3a77d0.zip new file mode 100644 index 000000000..3f120443e Binary files /dev/null and b/.yarn/cache/@types-node-npm-10.17.60-63ac1f669f-2cdb3a77d0.zip differ diff --git a/.yarn/cache/@types-node-npm-20.3.3-aac92e0a89-7a0d008004.zip b/.yarn/cache/@types-node-npm-20.3.3-aac92e0a89-7a0d008004.zip new file mode 100644 index 000000000..2cc3f389d Binary files /dev/null and b/.yarn/cache/@types-node-npm-20.3.3-aac92e0a89-7a0d008004.zip differ diff --git a/.yarn/cache/@types-node-npm-8.10.66-b849acaf16-c52039de86.zip b/.yarn/cache/@types-node-npm-8.10.66-b849acaf16-c52039de86.zip new file mode 100644 index 000000000..b3f6e2b3b Binary files /dev/null and b/.yarn/cache/@types-node-npm-8.10.66-b849acaf16-c52039de86.zip differ diff --git a/.yarn/cache/@types-triple-beam-npm-1.3.2-e1699700a8-dd7b4a563f.zip b/.yarn/cache/@types-triple-beam-npm-1.3.2-e1699700a8-dd7b4a563f.zip new file mode 100644 index 000000000..afce33a0b Binary files /dev/null and b/.yarn/cache/@types-triple-beam-npm-1.3.2-e1699700a8-dd7b4a563f.zip differ diff --git a/.yarn/cache/@tyriar-fibonacci-heap-npm-2.0.9-f50901eb42-1781df7370.zip b/.yarn/cache/@tyriar-fibonacci-heap-npm-2.0.9-f50901eb42-1781df7370.zip new file mode 100644 index 000000000..55b2bdd58 Binary files /dev/null and b/.yarn/cache/@tyriar-fibonacci-heap-npm-2.0.9-f50901eb42-1781df7370.zip differ diff --git a/.yarn/cache/app-root-path-npm-3.1.0-9822bb2a96-e3db3957ae.zip b/.yarn/cache/app-root-path-npm-3.1.0-9822bb2a96-e3db3957ae.zip new file mode 100644 index 000000000..1049e5124 Binary files /dev/null and b/.yarn/cache/app-root-path-npm-3.1.0-9822bb2a96-e3db3957ae.zip differ diff --git a/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip new file mode 100644 index 000000000..41d8c663d Binary files /dev/null and b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-6c80b4fd04.zip differ diff --git a/.yarn/cache/asn1js-npm-3.0.5-cf5558af33-3b6af1bbad.zip b/.yarn/cache/asn1js-npm-3.0.5-cf5558af33-3b6af1bbad.zip new file mode 100644 index 000000000..22a8145d7 Binary files /dev/null and b/.yarn/cache/asn1js-npm-3.0.5-cf5558af33-3b6af1bbad.zip differ diff --git a/.yarn/cache/axios-npm-0.21.4-e278873748-44245f24ac.zip b/.yarn/cache/axios-npm-0.21.4-e278873748-44245f24ac.zip new file mode 100644 index 000000000..756d87a54 Binary files /dev/null and b/.yarn/cache/axios-npm-0.21.4-e278873748-44245f24ac.zip differ diff --git a/.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-0e80ed852a.zip b/.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-0e80ed852a.zip new file mode 100644 index 000000000..77c024569 Binary files /dev/null and b/.yarn/cache/bcryptjs-npm-2.4.3-32de4957eb-0e80ed852a.zip differ diff --git a/.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-ad243b7e2f.zip b/.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-ad243b7e2f.zip new file mode 100644 index 000000000..ae328369f Binary files /dev/null and b/.yarn/cache/bignumber.js-npm-9.1.1-5929e8d8dc-ad243b7e2f.zip differ diff --git a/.yarn/cache/bowser-npm-2.11.0-33664d9063-29c3f01f22.zip b/.yarn/cache/bowser-npm-2.11.0-33664d9063-29c3f01f22.zip new file mode 100644 index 000000000..7860319da Binary files /dev/null and b/.yarn/cache/bowser-npm-2.11.0-33664d9063-29c3f01f22.zip differ diff --git a/.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip b/.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip new file mode 100644 index 000000000..ef174b22d Binary files /dev/null and b/.yarn/cache/busboy-npm-1.6.0-ebb5cbb04b-32801e2c01.zip differ diff --git a/.yarn/cache/cbor-extract-npm-2.1.1-bcad1459e1-283d9cdb3c.zip b/.yarn/cache/cbor-extract-npm-2.1.1-bcad1459e1-283d9cdb3c.zip new file mode 100644 index 000000000..b87c024c5 Binary files /dev/null and b/.yarn/cache/cbor-extract-npm-2.1.1-bcad1459e1-283d9cdb3c.zip differ diff --git a/.yarn/cache/cbor-x-npm-1.5.3-1d452dd267-16f0da10e2.zip b/.yarn/cache/cbor-x-npm-1.5.3-1d452dd267-16f0da10e2.zip new file mode 100644 index 000000000..fff2ec319 Binary files /dev/null and b/.yarn/cache/cbor-x-npm-1.5.3-1d452dd267-16f0da10e2.zip differ diff --git a/.yarn/cache/check-disk-space-npm-3.4.0-78a4169ea3-0154c149c3.zip b/.yarn/cache/check-disk-space-npm-3.4.0-78a4169ea3-0154c149c3.zip new file mode 100644 index 000000000..1b8651464 Binary files /dev/null and b/.yarn/cache/check-disk-space-npm-3.4.0-78a4169ea3-0154c149c3.zip differ diff --git a/.yarn/cache/chevrotain-npm-10.5.0-1ff4e87f41-b641f149f6.zip b/.yarn/cache/chevrotain-npm-10.5.0-1ff4e87f41-b641f149f6.zip new file mode 100644 index 000000000..5d3ec363f Binary files /dev/null and b/.yarn/cache/chevrotain-npm-10.5.0-1ff4e87f41-b641f149f6.zip differ diff --git a/.yarn/cache/cli-highlight-npm-2.1.11-569697f73a-0a60e60545.zip b/.yarn/cache/cli-highlight-npm-2.1.11-569697f73a-0a60e60545.zip new file mode 100644 index 000000000..fd378c6a2 Binary files /dev/null and b/.yarn/cache/cli-highlight-npm-2.1.11-569697f73a-0a60e60545.zip differ diff --git a/.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip b/.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip new file mode 100644 index 000000000..d6087b322 Binary files /dev/null and b/.yarn/cache/cluster-key-slot-npm-1.1.2-0571a28825-be0ad2d262.zip differ diff --git a/.yarn/cache/color-npm-3.2.1-568cf1014f-f81220e8b7.zip b/.yarn/cache/color-npm-3.2.1-568cf1014f-f81220e8b7.zip new file mode 100644 index 000000000..6021f3d11 Binary files /dev/null and b/.yarn/cache/color-npm-3.2.1-568cf1014f-f81220e8b7.zip differ diff --git a/.yarn/cache/color-string-npm-1.9.1-dc020e56be-c13fe7cff7.zip b/.yarn/cache/color-string-npm-1.9.1-dc020e56be-c13fe7cff7.zip new file mode 100644 index 000000000..7f3fc7289 Binary files /dev/null and b/.yarn/cache/color-string-npm-1.9.1-dc020e56be-c13fe7cff7.zip differ diff --git a/.yarn/cache/colors-npm-1.4.0-7e2cf12234-98aa2c2418.zip b/.yarn/cache/colors-npm-1.4.0-7e2cf12234-98aa2c2418.zip new file mode 100644 index 000000000..74451b04a Binary files /dev/null and b/.yarn/cache/colors-npm-1.4.0-7e2cf12234-98aa2c2418.zip differ diff --git a/.yarn/cache/colorspace-npm-1.1.4-f01655548a-bb3934ef3c.zip b/.yarn/cache/colorspace-npm-1.1.4-f01655548a-bb3934ef3c.zip new file mode 100644 index 000000000..61c649a0c Binary files /dev/null and b/.yarn/cache/colorspace-npm-1.1.4-f01655548a-bb3934ef3c.zip differ diff --git a/.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip b/.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip new file mode 100644 index 000000000..73550eecf Binary files /dev/null and b/.yarn/cache/connect-busboy-npm-1.0.0-9908d1785d-e4a8cece06.zip differ diff --git a/.yarn/cache/content-type-npm-1.0.5-3e037bf9ab-566271e0a2.zip b/.yarn/cache/content-type-npm-1.0.5-3e037bf9ab-566271e0a2.zip new file mode 100644 index 000000000..728f06fa8 Binary files /dev/null and b/.yarn/cache/content-type-npm-1.0.5-3e037bf9ab-566271e0a2.zip differ diff --git a/.yarn/cache/cors-npm-2.8.5-c9935a2d12-ced838404c.zip b/.yarn/cache/cors-npm-2.8.5-c9935a2d12-ced838404c.zip new file mode 100644 index 000000000..b7ab2c53f Binary files /dev/null and b/.yarn/cache/cors-npm-2.8.5-c9935a2d12-ced838404c.zip differ diff --git a/.yarn/cache/cross-fetch-npm-3.1.8-71c3c05709-78f993fa09.zip b/.yarn/cache/cross-fetch-npm-3.1.8-71c3c05709-78f993fa09.zip new file mode 100644 index 000000000..b5d3394a9 Binary files /dev/null and b/.yarn/cache/cross-fetch-npm-3.1.8-71c3c05709-78f993fa09.zip differ diff --git a/.yarn/cache/date-fns-npm-2.30.0-895c790e0f-f7be015232.zip b/.yarn/cache/date-fns-npm-2.30.0-895c790e0f-f7be015232.zip new file mode 100644 index 000000000..f51ffd3ec Binary files /dev/null and b/.yarn/cache/date-fns-npm-2.30.0-895c790e0f-f7be015232.zip differ diff --git a/.yarn/cache/date-format-npm-4.0.14-50da5e5139-dfe5139df6.zip b/.yarn/cache/date-format-npm-4.0.14-50da5e5139-dfe5139df6.zip new file mode 100644 index 000000000..2b8bcd8f8 Binary files /dev/null and b/.yarn/cache/date-format-npm-4.0.14-50da5e5139-dfe5139df6.zip differ diff --git a/.yarn/cache/dayjs-npm-1.11.9-c47d327b7c-a4844d83dc.zip b/.yarn/cache/dayjs-npm-1.11.9-c47d327b7c-a4844d83dc.zip new file mode 100644 index 000000000..075052cfb Binary files /dev/null and b/.yarn/cache/dayjs-npm-1.11.9-c47d327b7c-a4844d83dc.zip differ diff --git a/.yarn/cache/denque-npm-2.1.0-578d0b6297-1d4ae1d05e.zip b/.yarn/cache/denque-npm-2.1.0-578d0b6297-1d4ae1d05e.zip new file mode 100644 index 000000000..4bef0e90a Binary files /dev/null and b/.yarn/cache/denque-npm-2.1.0-578d0b6297-1d4ae1d05e.zip differ diff --git a/.yarn/cache/dotenv-npm-16.3.1-e6d380a398-15d75e7279.zip b/.yarn/cache/dotenv-npm-16.3.1-e6d380a398-15d75e7279.zip new file mode 100644 index 000000000..7bdd10c0d Binary files /dev/null and b/.yarn/cache/dotenv-npm-16.3.1-e6d380a398-15d75e7279.zip differ diff --git a/.yarn/cache/enabled-npm-2.0.0-bf5d96c9d8-9d256d89f4.zip b/.yarn/cache/enabled-npm-2.0.0-bf5d96c9d8-9d256d89f4.zip new file mode 100644 index 000000000..def462538 Binary files /dev/null and b/.yarn/cache/enabled-npm-2.0.0-bf5d96c9d8-9d256d89f4.zip differ diff --git a/.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-8ee6e2a076.zip b/.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-8ee6e2a076.zip new file mode 100644 index 000000000..33afc41da Binary files /dev/null and b/.yarn/cache/express-robots-txt-npm-1.0.0-dcc8bd8f0a-8ee6e2a076.zip differ diff --git a/.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip b/.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip new file mode 100644 index 000000000..c591797e7 Binary files /dev/null and b/.yarn/cache/express-winston-npm-4.2.0-e4cfb26486-029529107f.zip differ diff --git a/.yarn/cache/fast-safe-stringify-npm-2.1.1-7ce89033ca-a851cbddc4.zip b/.yarn/cache/fast-safe-stringify-npm-2.1.1-7ce89033ca-a851cbddc4.zip new file mode 100644 index 000000000..0de375bb1 Binary files /dev/null and b/.yarn/cache/fast-safe-stringify-npm-2.1.1-7ce89033ca-a851cbddc4.zip differ diff --git a/.yarn/cache/fast-xml-parser-npm-4.2.5-342a3689c5-d32b220055.zip b/.yarn/cache/fast-xml-parser-npm-4.2.5-342a3689c5-d32b220055.zip new file mode 100644 index 000000000..b83440e37 Binary files /dev/null and b/.yarn/cache/fast-xml-parser-npm-4.2.5-342a3689c5-d32b220055.zip differ diff --git a/.yarn/cache/fecha-npm-4.2.3-75248da3fd-f94e2fb3ac.zip b/.yarn/cache/fecha-npm-4.2.3-75248da3fd-f94e2fb3ac.zip new file mode 100644 index 000000000..f716e2f73 Binary files /dev/null and b/.yarn/cache/fecha-npm-4.2.3-75248da3fd-f94e2fb3ac.zip differ diff --git a/.yarn/cache/find-package-json-npm-1.2.0-0adfc8b282-ea3fc597c3.zip b/.yarn/cache/find-package-json-npm-1.2.0-0adfc8b282-ea3fc597c3.zip new file mode 100644 index 000000000..3cd43f3e7 Binary files /dev/null and b/.yarn/cache/find-package-json-npm-1.2.0-0adfc8b282-ea3fc597c3.zip differ diff --git a/.yarn/cache/fn.name-npm-1.1.0-b472333184-e357144f48.zip b/.yarn/cache/fn.name-npm-1.1.0-b472333184-e357144f48.zip new file mode 100644 index 000000000..416b895bf Binary files /dev/null and b/.yarn/cache/fn.name-npm-1.1.0-b472333184-e357144f48.zip differ diff --git a/.yarn/cache/form-data-npm-2.5.1-47256351b5-5134ada56c.zip b/.yarn/cache/form-data-npm-2.5.1-47256351b5-5134ada56c.zip new file mode 100644 index 000000000..ee8c79f4d Binary files /dev/null and b/.yarn/cache/form-data-npm-2.5.1-47256351b5-5134ada56c.zip differ diff --git a/.yarn/cache/fs-extra-npm-11.1.1-ca44602180-fb883c6824.zip b/.yarn/cache/fs-extra-npm-11.1.1-ca44602180-fb883c6824.zip new file mode 100644 index 000000000..a1b88a31b Binary files /dev/null and b/.yarn/cache/fs-extra-npm-11.1.1-ca44602180-fb883c6824.zip differ diff --git a/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip new file mode 100644 index 000000000..92db25139 Binary files /dev/null and b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-81296c00c7.zip differ diff --git a/.yarn/cache/generate-function-npm-2.3.1-c839dc559c-652f083de2.zip b/.yarn/cache/generate-function-npm-2.3.1-c839dc559c-652f083de2.zip new file mode 100644 index 000000000..92980622d Binary files /dev/null and b/.yarn/cache/generate-function-npm-2.3.1-c839dc559c-652f083de2.zip differ diff --git a/.yarn/cache/get-port-npm-3.2.0-4093f8843e-31f5303265.zip b/.yarn/cache/get-port-npm-3.2.0-4093f8843e-31f5303265.zip new file mode 100644 index 000000000..a1d50224f Binary files /dev/null and b/.yarn/cache/get-port-npm-3.2.0-4093f8843e-31f5303265.zip differ diff --git a/.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e350096e65.zip b/.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e350096e65.zip new file mode 100644 index 000000000..8ec9b47f8 Binary files /dev/null and b/.yarn/cache/hash.js-npm-1.1.7-f1ad187358-e350096e65.zip differ diff --git a/.yarn/cache/helmet-npm-6.2.0-43622c54ea-cf01e02424.zip b/.yarn/cache/helmet-npm-6.2.0-43622c54ea-cf01e02424.zip new file mode 100644 index 000000000..6cb9b3c3b Binary files /dev/null and b/.yarn/cache/helmet-npm-6.2.0-43622c54ea-cf01e02424.zip differ diff --git a/.yarn/cache/highlight.js-npm-10.7.3-247e67d5c0-defeafcd54.zip b/.yarn/cache/highlight.js-npm-10.7.3-247e67d5c0-defeafcd54.zip new file mode 100644 index 000000000..ee7a7453f Binary files /dev/null and b/.yarn/cache/highlight.js-npm-10.7.3-247e67d5c0-defeafcd54.zip differ diff --git a/.yarn/cache/html-entities-npm-2.4.0-510164c624-25bea32642.zip b/.yarn/cache/html-entities-npm-2.4.0-510164c624-25bea32642.zip new file mode 100644 index 000000000..06749a1c7 Binary files /dev/null and b/.yarn/cache/html-entities-npm-2.4.0-510164c624-25bea32642.zip differ diff --git a/.yarn/cache/http-basic-npm-8.1.3-ae54b14025-7df5dc4d4b.zip b/.yarn/cache/http-basic-npm-8.1.3-ae54b14025-7df5dc4d4b.zip new file mode 100644 index 000000000..c4797383c Binary files /dev/null and b/.yarn/cache/http-basic-npm-8.1.3-ae54b14025-7df5dc4d4b.zip differ diff --git a/.yarn/cache/http-response-object-npm-3.0.2-cbb68c5487-6cbdcb4ce7.zip b/.yarn/cache/http-response-object-npm-3.0.2-cbb68c5487-6cbdcb4ce7.zip new file mode 100644 index 000000000..bafd0acce Binary files /dev/null and b/.yarn/cache/http-response-object-npm-3.0.2-cbb68c5487-6cbdcb4ce7.zip differ diff --git a/.yarn/cache/http-status-codes-npm-2.2.0-8d45a60399-31e1d73085.zip b/.yarn/cache/http-status-codes-npm-2.2.0-8d45a60399-31e1d73085.zip new file mode 100644 index 000000000..9edb83361 Binary files /dev/null and b/.yarn/cache/http-status-codes-npm-2.2.0-8d45a60399-31e1d73085.zip differ diff --git a/.yarn/cache/ignore-by-default-npm-1.0.1-78ea10bc54-441509147b.zip b/.yarn/cache/ignore-by-default-npm-1.0.1-78ea10bc54-441509147b.zip new file mode 100644 index 000000000..fecc35c21 Binary files /dev/null and b/.yarn/cache/ignore-by-default-npm-1.0.1-78ea10bc54-441509147b.zip differ diff --git a/.yarn/cache/inversify-express-utils-npm-6.4.3-8478048fb7-4aa9a836fe.zip b/.yarn/cache/inversify-express-utils-npm-6.4.3-8478048fb7-4aa9a836fe.zip new file mode 100644 index 000000000..e76c2f7fb Binary files /dev/null and b/.yarn/cache/inversify-express-utils-npm-6.4.3-8478048fb7-4aa9a836fe.zip differ diff --git a/.yarn/cache/inversify-npm-6.0.1-39ef6784da-b6c9b56ef7.zip b/.yarn/cache/inversify-npm-6.0.1-39ef6784da-b6c9b56ef7.zip new file mode 100644 index 000000000..6211f9717 Binary files /dev/null and b/.yarn/cache/inversify-npm-6.0.1-39ef6784da-b6c9b56ef7.zip differ diff --git a/.yarn/cache/ioredis-npm-5.3.2-58471071b1-9a23559133.zip b/.yarn/cache/ioredis-npm-5.3.2-58471071b1-9a23559133.zip new file mode 100644 index 000000000..115473e0a Binary files /dev/null and b/.yarn/cache/ioredis-npm-5.3.2-58471071b1-9a23559133.zip differ diff --git a/.yarn/cache/is-arrayish-npm-0.3.2-f856180f79-977e64f54d.zip b/.yarn/cache/is-arrayish-npm-0.3.2-f856180f79-977e64f54d.zip new file mode 100644 index 000000000..593895a16 Binary files /dev/null and b/.yarn/cache/is-arrayish-npm-0.3.2-f856180f79-977e64f54d.zip differ diff --git a/.yarn/cache/is-invalid-path-npm-1.0.2-5d84629aa0-8776ef093e.zip b/.yarn/cache/is-invalid-path-npm-1.0.2-5d84629aa0-8776ef093e.zip new file mode 100644 index 000000000..137e483d4 Binary files /dev/null and b/.yarn/cache/is-invalid-path-npm-1.0.2-5d84629aa0-8776ef093e.zip differ diff --git a/.yarn/cache/is-property-npm-1.0.2-6eac53b30e-33b661a369.zip b/.yarn/cache/is-property-npm-1.0.2-6eac53b30e-33b661a369.zip new file mode 100644 index 000000000..d30e32d7e Binary files /dev/null and b/.yarn/cache/is-property-npm-1.0.2-6eac53b30e-33b661a369.zip differ diff --git a/.yarn/cache/json-bigint-npm-1.0.0-8e35bcb143-c67bb93ccb.zip b/.yarn/cache/json-bigint-npm-1.0.0-8e35bcb143-c67bb93ccb.zip new file mode 100644 index 000000000..ca6d059da Binary files /dev/null and b/.yarn/cache/json-bigint-npm-1.0.0-8e35bcb143-c67bb93ccb.zip differ diff --git a/.yarn/cache/jsonschema-npm-1.4.1-548ecda9d0-1ef02a6cd9.zip b/.yarn/cache/jsonschema-npm-1.4.1-548ecda9d0-1ef02a6cd9.zip new file mode 100644 index 000000000..9904dda98 Binary files /dev/null and b/.yarn/cache/jsonschema-npm-1.4.1-548ecda9d0-1ef02a6cd9.zip differ diff --git a/.yarn/cache/kuler-npm-2.0.0-19e74c9695-9e10b5a165.zip b/.yarn/cache/kuler-npm-2.0.0-19e74c9695-9e10b5a165.zip new file mode 100644 index 000000000..1c905daa0 Binary files /dev/null and b/.yarn/cache/kuler-npm-2.0.0-19e74c9695-9e10b5a165.zip differ diff --git a/.yarn/cache/lodash.camelcase-npm-4.3.0-bf268e3bf0-cb9227612f.zip b/.yarn/cache/lodash.camelcase-npm-4.3.0-bf268e3bf0-cb9227612f.zip new file mode 100644 index 000000000..2e9ae3fcb Binary files /dev/null and b/.yarn/cache/lodash.camelcase-npm-4.3.0-bf268e3bf0-cb9227612f.zip differ diff --git a/.yarn/cache/lodash.defaults-npm-4.2.0-c5dea025ab-8492325823.zip b/.yarn/cache/lodash.defaults-npm-4.2.0-c5dea025ab-8492325823.zip new file mode 100644 index 000000000..b190e7a3c Binary files /dev/null and b/.yarn/cache/lodash.defaults-npm-4.2.0-c5dea025ab-8492325823.zip differ diff --git a/.yarn/cache/lodash.isarguments-npm-3.1.0-9e74d350b8-ae1526f3eb.zip b/.yarn/cache/lodash.isarguments-npm-3.1.0-9e74d350b8-ae1526f3eb.zip new file mode 100644 index 000000000..328abc0f5 Binary files /dev/null and b/.yarn/cache/lodash.isarguments-npm-3.1.0-9e74d350b8-ae1526f3eb.zip differ diff --git a/.yarn/cache/log4js-npm-6.9.1-b621c90f9f-59d98c37d4.zip b/.yarn/cache/log4js-npm-6.9.1-b621c90f9f-59d98c37d4.zip new file mode 100644 index 000000000..1a2630e99 Binary files /dev/null and b/.yarn/cache/log4js-npm-6.9.1-b621c90f9f-59d98c37d4.zip differ diff --git a/.yarn/cache/logform-npm-2.5.1-06017d630d-08fdf03be5.zip b/.yarn/cache/logform-npm-2.5.1-06017d630d-08fdf03be5.zip new file mode 100644 index 000000000..b75e21c72 Binary files /dev/null and b/.yarn/cache/logform-npm-2.5.1-06017d630d-08fdf03be5.zip differ diff --git a/.yarn/cache/long-npm-4.0.0-ecd96a31ed-16afbe8f74.zip b/.yarn/cache/long-npm-4.0.0-ecd96a31ed-16afbe8f74.zip new file mode 100644 index 000000000..228e6f994 Binary files /dev/null and b/.yarn/cache/long-npm-4.0.0-ecd96a31ed-16afbe8f74.zip differ diff --git a/.yarn/cache/long-npm-5.2.3-61dddb7586-885ede7c3d.zip b/.yarn/cache/long-npm-5.2.3-61dddb7586-885ede7c3d.zip new file mode 100644 index 000000000..b6e2d5d0d Binary files /dev/null and b/.yarn/cache/long-npm-5.2.3-61dddb7586-885ede7c3d.zip differ diff --git a/.yarn/cache/lru-cache-npm-7.18.3-e68be5b11c-e550d77238.zip b/.yarn/cache/lru-cache-npm-7.18.3-e68be5b11c-e550d77238.zip new file mode 100644 index 000000000..49f2621a7 Binary files /dev/null and b/.yarn/cache/lru-cache-npm-7.18.3-e68be5b11c-e550d77238.zip differ diff --git a/.yarn/cache/lru-cache-npm-8.0.5-bb030cd93e-87d72196d8.zip b/.yarn/cache/lru-cache-npm-8.0.5-bb030cd93e-87d72196d8.zip new file mode 100644 index 000000000..32b5fb846 Binary files /dev/null and b/.yarn/cache/lru-cache-npm-8.0.5-bb030cd93e-87d72196d8.zip differ diff --git a/.yarn/cache/microtime-npm-3.1.1-ff5289e8ad-1161571d9c.zip b/.yarn/cache/microtime-npm-3.1.1-ff5289e8ad-1161571d9c.zip new file mode 100644 index 000000000..7c43e4704 Binary files /dev/null and b/.yarn/cache/microtime-npm-3.1.1-ff5289e8ad-1161571d9c.zip differ diff --git a/.yarn/cache/mkdirp-npm-2.1.6-832c38f12a-8a1d09ffac.zip b/.yarn/cache/mkdirp-npm-2.1.6-832c38f12a-8a1d09ffac.zip new file mode 100644 index 000000000..1de27b5d7 Binary files /dev/null and b/.yarn/cache/mkdirp-npm-2.1.6-832c38f12a-8a1d09ffac.zip differ diff --git a/.yarn/cache/mysql2-npm-3.4.3-3b7278a8b4-dab83e683a.zip b/.yarn/cache/mysql2-npm-3.4.3-3b7278a8b4-dab83e683a.zip new file mode 100644 index 000000000..20630c0bf Binary files /dev/null and b/.yarn/cache/mysql2-npm-3.4.3-3b7278a8b4-dab83e683a.zip differ diff --git a/.yarn/cache/named-placeholders-npm-1.1.3-1b385febe5-7834adc91e.zip b/.yarn/cache/named-placeholders-npm-1.1.3-1b385febe5-7834adc91e.zip new file mode 100644 index 000000000..5ae8ba4aa Binary files /dev/null and b/.yarn/cache/named-placeholders-npm-1.1.3-1b385febe5-7834adc91e.zip differ diff --git a/.yarn/cache/newrelic-npm-10.4.0-258cfcc563-adf3d32822.zip b/.yarn/cache/newrelic-npm-10.4.0-258cfcc563-adf3d32822.zip new file mode 100644 index 000000000..2c525aed7 Binary files /dev/null and b/.yarn/cache/newrelic-npm-10.4.0-258cfcc563-adf3d32822.zip differ diff --git a/.yarn/cache/node-addon-api-npm-5.1.0-b50d00f739-2508bd2d29.zip b/.yarn/cache/node-addon-api-npm-5.1.0-b50d00f739-2508bd2d29.zip new file mode 100644 index 000000000..4f67a17c6 Binary files /dev/null and b/.yarn/cache/node-addon-api-npm-5.1.0-b50d00f739-2508bd2d29.zip differ diff --git a/.yarn/cache/node-fetch-npm-2.6.12-48619ce9d6-3bc1655203.zip b/.yarn/cache/node-fetch-npm-2.6.12-48619ce9d6-3bc1655203.zip new file mode 100644 index 000000000..d8efe25f6 Binary files /dev/null and b/.yarn/cache/node-fetch-npm-2.6.12-48619ce9d6-3bc1655203.zip differ diff --git a/.yarn/cache/node-gyp-build-npm-4.6.0-5434aac3e5-25d78c5ef1.zip b/.yarn/cache/node-gyp-build-npm-4.6.0-5434aac3e5-25d78c5ef1.zip new file mode 100644 index 000000000..0084d9fdf Binary files /dev/null and b/.yarn/cache/node-gyp-build-npm-4.6.0-5434aac3e5-25d78c5ef1.zip differ diff --git a/.yarn/cache/node-gyp-build-optional-packages-npm-5.0.3-50b9c76481-be3f023592.zip b/.yarn/cache/node-gyp-build-optional-packages-npm-5.0.3-50b9c76481-be3f023592.zip new file mode 100644 index 000000000..b7d288db9 Binary files /dev/null and b/.yarn/cache/node-gyp-build-optional-packages-npm-5.0.3-50b9c76481-be3f023592.zip differ diff --git a/.yarn/cache/nodemon-npm-2.0.22-2e71ccda0b-9c987e1397.zip b/.yarn/cache/nodemon-npm-2.0.22-2e71ccda0b-9c987e1397.zip new file mode 100644 index 000000000..22f76bafc Binary files /dev/null and b/.yarn/cache/nodemon-npm-2.0.22-2e71ccda0b-9c987e1397.zip differ diff --git a/.yarn/cache/nopt-npm-1.0.10-f3db192976-f62575acea.zip b/.yarn/cache/nopt-npm-1.0.10-f3db192976-f62575acea.zip new file mode 100644 index 000000000..1f5b95d52 Binary files /dev/null and b/.yarn/cache/nopt-npm-1.0.10-f3db192976-f62575acea.zip differ diff --git a/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip new file mode 100644 index 000000000..d2eec072e Binary files /dev/null and b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-516b266302.zip differ diff --git a/.yarn/cache/one-time-npm-1.0.0-aeaad5e524-fd008d7e99.zip b/.yarn/cache/one-time-npm-1.0.0-aeaad5e524-fd008d7e99.zip new file mode 100644 index 000000000..59188f657 Binary files /dev/null and b/.yarn/cache/one-time-npm-1.0.0-aeaad5e524-fd008d7e99.zip differ diff --git a/.yarn/cache/parse-cache-control-npm-1.0.1-81068d3680-5a70868792.zip b/.yarn/cache/parse-cache-control-npm-1.0.1-81068d3680-5a70868792.zip new file mode 100644 index 000000000..a3935a020 Binary files /dev/null and b/.yarn/cache/parse-cache-control-npm-1.0.1-81068d3680-5a70868792.zip differ diff --git a/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-6.0.1-60b4888f75-1848378b35.zip b/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-6.0.1-60b4888f75-1848378b35.zip new file mode 100644 index 000000000..868840bf4 Binary files /dev/null and b/.yarn/cache/parse5-htmlparser2-tree-adapter-npm-6.0.1-60b4888f75-1848378b35.zip differ diff --git a/.yarn/cache/parse5-npm-5.1.1-8e63d82cff-613a714af4.zip b/.yarn/cache/parse5-npm-5.1.1-8e63d82cff-613a714af4.zip new file mode 100644 index 000000000..3d2a50988 Binary files /dev/null and b/.yarn/cache/parse5-npm-5.1.1-8e63d82cff-613a714af4.zip differ diff --git a/.yarn/cache/parse5-npm-6.0.1-70a35a494a-7d569a176c.zip b/.yarn/cache/parse5-npm-6.0.1-70a35a494a-7d569a176c.zip new file mode 100644 index 000000000..f3ba0239e Binary files /dev/null and b/.yarn/cache/parse5-npm-6.0.1-70a35a494a-7d569a176c.zip differ diff --git a/.yarn/cache/pretty-bytes-npm-5.6.0-0061079c9f-9c082500d1.zip b/.yarn/cache/pretty-bytes-npm-5.6.0-0061079c9f-9c082500d1.zip new file mode 100644 index 000000000..767e74fc0 Binary files /dev/null and b/.yarn/cache/pretty-bytes-npm-5.6.0-0061079c9f-9c082500d1.zip differ diff --git a/.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip b/.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip new file mode 100644 index 000000000..fbc4cf55b Binary files /dev/null and b/.yarn/cache/prettyjson-npm-1.2.5-a72b7bf823-e36e8ae4f7.zip differ diff --git a/.yarn/cache/protobufjs-npm-7.2.4-c40bd79e8d-a952cdf2a5.zip b/.yarn/cache/protobufjs-npm-7.2.4-c40bd79e8d-a952cdf2a5.zip new file mode 100644 index 000000000..c13f4c226 Binary files /dev/null and b/.yarn/cache/protobufjs-npm-7.2.4-c40bd79e8d-a952cdf2a5.zip differ diff --git a/.yarn/cache/pstree.remy-npm-1.1.8-2dd5d55de2-5cb53698d6.zip b/.yarn/cache/pstree.remy-npm-1.1.8-2dd5d55de2-5cb53698d6.zip new file mode 100644 index 000000000..dccb458a6 Binary files /dev/null and b/.yarn/cache/pstree.remy-npm-1.1.8-2dd5d55de2-5cb53698d6.zip differ diff --git a/.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-9b81556113.zip b/.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-9b81556113.zip new file mode 100644 index 000000000..5f520889c Binary files /dev/null and b/.yarn/cache/pvtsutils-npm-1.3.2-e1483da905-9b81556113.zip differ diff --git a/.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-2ee26a9e51.zip b/.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-2ee26a9e51.zip new file mode 100644 index 000000000..0df7fa843 Binary files /dev/null and b/.yarn/cache/pvutils-npm-1.1.3-da8b07d6cf-2ee26a9e51.zip differ diff --git a/.yarn/cache/qs-npm-6.11.2-b118bc1c6f-e812f3c590.zip b/.yarn/cache/qs-npm-6.11.2-b118bc1c6f-e812f3c590.zip new file mode 100644 index 000000000..e6e6f34e1 Binary files /dev/null and b/.yarn/cache/qs-npm-6.11.2-b118bc1c6f-e812f3c590.zip differ diff --git a/.yarn/cache/readable-stream-npm-3.6.2-d2a6069158-bdcbe6c22e.zip b/.yarn/cache/readable-stream-npm-3.6.2-d2a6069158-bdcbe6c22e.zip new file mode 100644 index 000000000..0053b6723 Binary files /dev/null and b/.yarn/cache/readable-stream-npm-3.6.2-d2a6069158-bdcbe6c22e.zip differ diff --git a/.yarn/cache/redis-errors-npm-1.2.0-a81fd9b0f1-f28ac26921.zip b/.yarn/cache/redis-errors-npm-1.2.0-a81fd9b0f1-f28ac26921.zip new file mode 100644 index 000000000..97bde6b08 Binary files /dev/null and b/.yarn/cache/redis-errors-npm-1.2.0-a81fd9b0f1-f28ac26921.zip differ diff --git a/.yarn/cache/redis-parser-npm-3.0.0-7ebe40abcb-89290ae530.zip b/.yarn/cache/redis-parser-npm-3.0.0-7ebe40abcb-89290ae530.zip new file mode 100644 index 000000000..8ee1a7bcc Binary files /dev/null and b/.yarn/cache/redis-parser-npm-3.0.0-7ebe40abcb-89290ae530.zip differ diff --git a/.yarn/cache/regexp-to-ast-npm-0.5.0-1e96b9f3a0-72e32f2a12.zip b/.yarn/cache/regexp-to-ast-npm-0.5.0-1e96b9f3a0-72e32f2a12.zip new file mode 100644 index 000000000..15fdfab61 Binary files /dev/null and b/.yarn/cache/regexp-to-ast-npm-0.5.0-1e96b9f3a0-72e32f2a12.zip differ diff --git a/.yarn/cache/request-ip-npm-3.3.0-d0fb01a2ad-9ca26f8142.zip b/.yarn/cache/request-ip-npm-3.3.0-d0fb01a2ad-9ca26f8142.zip new file mode 100644 index 000000000..dd3f42ce1 Binary files /dev/null and b/.yarn/cache/request-ip-npm-3.3.0-d0fb01a2ad-9ca26f8142.zip differ diff --git a/.yarn/cache/ringbufferjs-npm-2.0.0-96f0501584-55727c184c.zip b/.yarn/cache/ringbufferjs-npm-2.0.0-96f0501584-55727c184c.zip new file mode 100644 index 000000000..4070bf721 Binary files /dev/null and b/.yarn/cache/ringbufferjs-npm-2.0.0-96f0501584-55727c184c.zip differ diff --git a/.yarn/cache/semver-npm-7.5.3-275095dbf3-9d58db1652.zip b/.yarn/cache/semver-npm-7.5.3-275095dbf3-9d58db1652.zip new file mode 100644 index 000000000..79b7d4718 Binary files /dev/null and b/.yarn/cache/semver-npm-7.5.3-275095dbf3-9d58db1652.zip differ diff --git a/.yarn/cache/seq-queue-npm-0.0.5-d5064d9793-f8695a6cb6.zip b/.yarn/cache/seq-queue-npm-0.0.5-d5064d9793-f8695a6cb6.zip new file mode 100644 index 000000000..66abe0a38 Binary files /dev/null and b/.yarn/cache/seq-queue-npm-0.0.5-d5064d9793-f8695a6cb6.zip differ diff --git a/.yarn/cache/simple-swizzle-npm-0.2.2-8dee37fad1-a7f3f2ab5c.zip b/.yarn/cache/simple-swizzle-npm-0.2.2-8dee37fad1-a7f3f2ab5c.zip new file mode 100644 index 000000000..8420b563a Binary files /dev/null and b/.yarn/cache/simple-swizzle-npm-0.2.2-8dee37fad1-a7f3f2ab5c.zip differ diff --git a/.yarn/cache/sqlite3-npm-5.1.6-23ff1f329d-ea64062884.zip b/.yarn/cache/sqlite3-npm-5.1.6-23ff1f329d-ea64062884.zip new file mode 100644 index 000000000..2715c3329 Binary files /dev/null and b/.yarn/cache/sqlite3-npm-5.1.6-23ff1f329d-ea64062884.zip differ diff --git a/.yarn/cache/sqlstring-npm-2.3.3-2db6939570-1e7e2d51c3.zip b/.yarn/cache/sqlstring-npm-2.3.3-2db6939570-1e7e2d51c3.zip new file mode 100644 index 000000000..a81b88be5 Binary files /dev/null and b/.yarn/cache/sqlstring-npm-2.3.3-2db6939570-1e7e2d51c3.zip differ diff --git a/.yarn/cache/sqs-consumer-npm-6.2.1-857abd3d30-ffa8b42ddb.zip b/.yarn/cache/sqs-consumer-npm-6.2.1-857abd3d30-ffa8b42ddb.zip new file mode 100644 index 000000000..b4a2c542a Binary files /dev/null and b/.yarn/cache/sqs-consumer-npm-6.2.1-857abd3d30-ffa8b42ddb.zip differ diff --git a/.yarn/cache/stack-trace-npm-0.0.10-9460b173e1-473036ad32.zip b/.yarn/cache/stack-trace-npm-0.0.10-9460b173e1-473036ad32.zip new file mode 100644 index 000000000..6674dc5c4 Binary files /dev/null and b/.yarn/cache/stack-trace-npm-0.0.10-9460b173e1-473036ad32.zip differ diff --git a/.yarn/cache/standard-as-callback-npm-2.1.0-8e47620bd4-88bec83ee2.zip b/.yarn/cache/standard-as-callback-npm-2.1.0-8e47620bd4-88bec83ee2.zip new file mode 100644 index 000000000..f166a786a Binary files /dev/null and b/.yarn/cache/standard-as-callback-npm-2.1.0-8e47620bd4-88bec83ee2.zip differ diff --git a/.yarn/cache/streamroller-npm-3.1.5-2fe0f7e85a-c1df5612b7.zip b/.yarn/cache/streamroller-npm-3.1.5-2fe0f7e85a-c1df5612b7.zip new file mode 100644 index 000000000..f59bad3c0 Binary files /dev/null and b/.yarn/cache/streamroller-npm-3.1.5-2fe0f7e85a-c1df5612b7.zip differ diff --git a/.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip b/.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip new file mode 100644 index 000000000..68383b403 Binary files /dev/null and b/.yarn/cache/streamsearch-npm-1.1.0-fc3ad6536d-1cce16cea8.zip differ diff --git a/.yarn/cache/string.fromcodepoint-npm-0.2.1-84d94c4fb5-6ba80f70c3.zip b/.yarn/cache/string.fromcodepoint-npm-0.2.1-84d94c4fb5-6ba80f70c3.zip new file mode 100644 index 000000000..f4dbcb3c8 Binary files /dev/null and b/.yarn/cache/string.fromcodepoint-npm-0.2.1-84d94c4fb5-6ba80f70c3.zip differ diff --git a/.yarn/cache/sync-request-npm-6.1.0-6ac24ab111-cc8438a674.zip b/.yarn/cache/sync-request-npm-6.1.0-6ac24ab111-cc8438a674.zip new file mode 100644 index 000000000..dda60af74 Binary files /dev/null and b/.yarn/cache/sync-request-npm-6.1.0-6ac24ab111-cc8438a674.zip differ diff --git a/.yarn/cache/sync-rpc-npm-1.3.6-96e5b4b996-4340974fb5.zip b/.yarn/cache/sync-rpc-npm-1.3.6-96e5b4b996-4340974fb5.zip new file mode 100644 index 000000000..352c60070 Binary files /dev/null and b/.yarn/cache/sync-rpc-npm-1.3.6-96e5b4b996-4340974fb5.zip differ diff --git a/.yarn/cache/text-hex-npm-1.0.0-22389e4d56-1138f68adc.zip b/.yarn/cache/text-hex-npm-1.0.0-22389e4d56-1138f68adc.zip new file mode 100644 index 000000000..ce4bf0be7 Binary files /dev/null and b/.yarn/cache/text-hex-npm-1.0.0-22389e4d56-1138f68adc.zip differ diff --git a/.yarn/cache/then-request-npm-6.0.2-d89438d618-a24a4fc95d.zip b/.yarn/cache/then-request-npm-6.0.2-d89438d618-a24a4fc95d.zip new file mode 100644 index 000000000..93fdcb8ff Binary files /dev/null and b/.yarn/cache/then-request-npm-6.0.2-d89438d618-a24a4fc95d.zip differ diff --git a/.yarn/cache/touch-npm-3.1.0-e2eacebbda-e0be589cb5.zip b/.yarn/cache/touch-npm-3.1.0-e2eacebbda-e0be589cb5.zip new file mode 100644 index 000000000..84e3b2380 Binary files /dev/null and b/.yarn/cache/touch-npm-3.1.0-e2eacebbda-e0be589cb5.zip differ diff --git a/.yarn/cache/triple-beam-npm-1.3.0-eda4e2a46c-7d7b77d862.zip b/.yarn/cache/triple-beam-npm-1.3.0-eda4e2a46c-7d7b77d862.zip new file mode 100644 index 000000000..5aab747e4 Binary files /dev/null and b/.yarn/cache/triple-beam-npm-1.3.0-eda4e2a46c-7d7b77d862.zip differ diff --git a/.yarn/cache/tslib-npm-2.6.0-4d336a6824-c01066038f.zip b/.yarn/cache/tslib-npm-2.6.0-4d336a6824-c01066038f.zip new file mode 100644 index 000000000..dc828cd16 Binary files /dev/null and b/.yarn/cache/tslib-npm-2.6.0-4d336a6824-c01066038f.zip differ diff --git a/.yarn/cache/typeorm-npm-0.3.17-f8c2578e7f-71fcb2b2e8.zip b/.yarn/cache/typeorm-npm-0.3.17-f8c2578e7f-71fcb2b2e8.zip new file mode 100644 index 000000000..c3242f299 Binary files /dev/null and b/.yarn/cache/typeorm-npm-0.3.17-f8c2578e7f-71fcb2b2e8.zip differ diff --git a/.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-02370d38a0.zip b/.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-02370d38a0.zip new file mode 100644 index 000000000..455738796 Binary files /dev/null and b/.yarn/cache/ua-parser-js-npm-1.0.35-38ecdb7612-02370d38a0.zip differ diff --git a/.yarn/cache/undefsafe-npm-2.0.5-8c3bbf9354-f42ab3b577.zip b/.yarn/cache/undefsafe-npm-2.0.5-8c3bbf9354-f42ab3b577.zip new file mode 100644 index 000000000..ef05395eb Binary files /dev/null and b/.yarn/cache/undefsafe-npm-2.0.5-8c3bbf9354-f42ab3b577.zip differ diff --git a/.yarn/cache/unescape-js-npm-1.1.4-f41cc6935a-97acf60a8f.zip b/.yarn/cache/unescape-js-npm-1.1.4-f41cc6935a-97acf60a8f.zip new file mode 100644 index 000000000..780e5721a Binary files /dev/null and b/.yarn/cache/unescape-js-npm-1.1.4-f41cc6935a-97acf60a8f.zip differ diff --git a/.yarn/cache/unescape-npm-1.0.1-df37f0a8bb-0d89b0f55e.zip b/.yarn/cache/unescape-npm-1.0.1-df37f0a8bb-0d89b0f55e.zip new file mode 100644 index 000000000..e73b096ee Binary files /dev/null and b/.yarn/cache/unescape-npm-1.0.1-df37f0a8bb-0d89b0f55e.zip differ diff --git a/.yarn/cache/winston-npm-3.9.0-88b81bb8c3-410f82b7a5.zip b/.yarn/cache/winston-npm-3.9.0-88b81bb8c3-410f82b7a5.zip new file mode 100644 index 000000000..26a859032 Binary files /dev/null and b/.yarn/cache/winston-npm-3.9.0-88b81bb8c3-410f82b7a5.zip differ diff --git a/.yarn/cache/winston-transport-npm-4.5.0-e10bfb2169-a56e5678a8.zip b/.yarn/cache/winston-transport-npm-4.5.0-e10bfb2169-a56e5678a8.zip new file mode 100644 index 000000000..6f2b4f793 Binary files /dev/null and b/.yarn/cache/winston-transport-npm-4.5.0-e10bfb2169-a56e5678a8.zip differ diff --git a/.yarn/cache/yargs-npm-17.7.2-80b62638e1-73b572e863.zip b/.yarn/cache/yargs-npm-17.7.2-80b62638e1-73b572e863.zip new file mode 100644 index 000000000..54c49dc9c Binary files /dev/null and b/.yarn/cache/yargs-npm-17.7.2-80b62638e1-73b572e863.zip differ diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore index aad888dee..623368be4 100644 --- a/packages/desktop/.gitignore +++ b/packages/desktop/.gitignore @@ -9,5 +9,7 @@ test/data/tmp/ .idea .env +data/* + codeqldb -yarn-error.log \ No newline at end of file +yarn-error.log diff --git a/packages/desktop/app/javascripts/Main/Directory/DirectoryManager.ts b/packages/desktop/app/javascripts/Main/Directory/DirectoryManager.ts new file mode 100644 index 000000000..4fe134286 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/Directory/DirectoryManager.ts @@ -0,0 +1,60 @@ +import path from 'path' +import { shell } from 'electron' +import { DirectoryManagerInterface } from '@standardnotes/snjs' + +import { FilesManagerInterface } from '../File/FilesManagerInterface' + +export class DirectoryManager implements DirectoryManagerInterface { + private lastErrorMessage: string | undefined + + constructor(private filesManager: FilesManagerInterface) {} + + async presentDirectoryPickerForLocationChangeAndTransferOld( + appendPath: string, + oldLocation?: string, + ): Promise { + try { + this.lastErrorMessage = undefined + + const selectedDirectory = await this.filesManager.openDirectoryPicker('Select') + + if (!selectedDirectory) { + return undefined + } + + const newPath = path.join(selectedDirectory, path.normalize(appendPath)) + + await this.filesManager.ensureDirectoryExists(newPath) + + if (oldLocation) { + const result = await this.filesManager.moveDirectory(path.normalize(oldLocation), newPath) + if (result.isFailed()) { + this.lastErrorMessage = result.getError() + + return undefined + } + + const deletingDirectoryResult = await this.filesManager.deleteDir(path.normalize(oldLocation)) + if (deletingDirectoryResult.isFailed()) { + this.lastErrorMessage = deletingDirectoryResult.getError() + + return undefined + } + } + + return newPath + } catch (error) { + this.lastErrorMessage = (error as Error).message + + return undefined + } + } + + async openLocation(location: string): Promise { + void shell.openPath(location) + } + + async getDirectoryManagerLastErrorMessage(): Promise { + return this.lastErrorMessage + } +} diff --git a/packages/desktop/app/javascripts/Main/ExtensionsServer.ts b/packages/desktop/app/javascripts/Main/ExtensionsServer.ts index 6b4844d6f..13b931213 100644 --- a/packages/desktop/app/javascripts/Main/ExtensionsServer.ts +++ b/packages/desktop/app/javascripts/Main/ExtensionsServer.ts @@ -5,8 +5,8 @@ import path from 'path' import { URL } from 'url' import { extensions as str } from './Strings' import { Paths } from './Types/Paths' -import { FileDoesNotExist } from './Utils/FileUtils' import { app } from 'electron' +import { FileErrorCodes } from './File/FileErrorCodes' const Protocol = 'http' @@ -80,7 +80,7 @@ function onRequestError(error: Error | { code: string }, response: ServerRespons let responseCode: number let message: string - if ('code' in error && error.code === FileDoesNotExist) { + if ('code' in error && error.code === FileErrorCodes.FileDoesNotExist) { responseCode = 404 message = str().missingExtension } else { diff --git a/packages/desktop/app/javascripts/Main/File/FileErrorCodes.ts b/packages/desktop/app/javascripts/Main/File/FileErrorCodes.ts new file mode 100644 index 000000000..8b8a02bf6 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/File/FileErrorCodes.ts @@ -0,0 +1,6 @@ +export enum FileErrorCodes { + FileDoesNotExist = 'ENOENT', + FileAlreadyExists = 'EEXIST', + OperationNotPermitted = 'EPERM', + DeviceIsBusy = 'EBUSY', +} diff --git a/packages/desktop/app/javascripts/Main/File/FilesManager.ts b/packages/desktop/app/javascripts/Main/File/FilesManager.ts new file mode 100644 index 000000000..65e10fb82 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/File/FilesManager.ts @@ -0,0 +1,314 @@ +import { dialog } from 'electron' +import fs, { PathLike } from 'fs' +import fse from 'fs-extra' +import { debounce } from 'lodash' +import path from 'path' +import yauzl from 'yauzl' +import { Result } from '@standardnotes/domain-core' + +import { removeFromArray } from '../Utils/Utils' + +import { FileErrorCodes } from './FileErrorCodes' +import { FilesManagerInterface } from './FilesManagerInterface' + +export class FilesManager implements FilesManagerInterface { + debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void { + let writingToDisk = false + return debounce(async () => { + if (writingToDisk) { + return + } + writingToDisk = true + try { + await this.writeJSONFile(location, data()) + } catch (error) { + console.error(error) + } finally { + writingToDisk = false + } + }, durationMs) + } + + async openDirectoryPicker(buttonLabel?: string): Promise { + const result = await dialog.showOpenDialog({ + properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'], + buttonLabel: buttonLabel, + }) + + return result.filePaths[0] + } + + async readJSONFile(filepath: string): Promise { + try { + const data = await fs.promises.readFile(filepath, 'utf8') + return JSON.parse(data) + } catch (error) { + return undefined + } + } + + readJSONFileSync(filepath: string): T { + const data = fs.readFileSync(filepath, 'utf8') + return JSON.parse(data) + } + + async writeJSONFile(filepath: string, data: unknown): Promise { + await this.ensureDirectoryExists(path.dirname(filepath)) + await fs.promises.writeFile(filepath, JSON.stringify(data, null, 2), 'utf8') + } + + async writeFile(filepath: string, data: string): Promise { + await this.ensureDirectoryExists(path.dirname(filepath)) + await fs.promises.writeFile(filepath, data, 'utf8') + } + + writeJSONFileSync(filepath: string, data: unknown): void { + fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8') + } + + async ensureDirectoryExists(dirPath: string): Promise { + try { + const stat = await fs.promises.lstat(dirPath) + if (!stat.isDirectory()) { + throw new Error('Tried to create a directory where a file of the same ' + `name already exists: ${dirPath}`) + } + } catch (error: any) { + if (error.code === FileErrorCodes.FileDoesNotExist) { + /** + * No directory here. Make sure there is a *parent* directory, and then + * create it. + */ + await this.ensureDirectoryExists(path.dirname(dirPath)) + + /** Now that its parent(s) exist, create the directory */ + try { + await fs.promises.mkdir(dirPath) + } catch (error: any) { + if (error.code === FileErrorCodes.FileAlreadyExists) { + /** + * A concurrent process must have created the directory already. + * Make sure it *is* a directory and not something else. + */ + await this.ensureDirectoryExists(dirPath) + } else { + throw error + } + } + } else { + throw error + } + } + } + + async deleteDir(dirPath: string): Promise> { + try { + fse.removeSync(dirPath) + + return Result.ok('Directory deleted successfully') + } catch (error) { + return Result.fail((error as Error).message) + } + } + + async deleteDirContents(dirPath: string): Promise { + /** + * Scan the directory up to ten times, to handle cases where files are being added while + * the directory's contents are being deleted + */ + for (let i = 1, maxTries = 10; i < maxTries; i++) { + const children = await fs.promises.readdir(dirPath, { + withFileTypes: true, + }) + + if (children.length === 0) { + break + } + + for (const child of children) { + const childPath = path.join(dirPath, child.name) + if (child.isDirectory()) { + await this.deleteDirContents(childPath) + try { + await fs.promises.rmdir(childPath) + } catch (error) { + if (error !== FileErrorCodes.FileDoesNotExist) { + throw error + } + } + } else { + await this.deleteFile(childPath) + } + } + } + } + + isChildOfDir(parent: string, potentialChild: string): boolean { + const relative = path.relative(parent, potentialChild) + return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative) + } + + async moveDirectory(dir: string, destination: string): Promise> { + try { + await fse.move(dir, destination, { overwrite: true }) + + return Result.ok('Directory moved successfully') + } catch (error) { + return Result.fail((error as Error).message) + } + } + + async moveDirContents(srcDir: string, destDir: string): Promise> { + try { + let srcDirectoryContents = await fs.promises.readdir(srcDir) + + await this.ensureDirectoryExists(destDir) + + if (this.isChildOfDir(srcDir, destDir)) { + srcDirectoryContents = srcDirectoryContents.filter((name) => { + return !this.isChildOfDir(destDir, path.join(srcDir, name)) + }) + removeFromArray(srcDirectoryContents, path.basename(destDir)) + } + + const directoryNames = [] + const fileNames = [] + for (const contentName of srcDirectoryContents) { + const stats = await fs.promises.lstat(path.join(srcDir, contentName)) + if (stats.isDirectory()) { + directoryNames.push(contentName) + + continue + } + + fileNames.push(contentName) + } + + for (const directoryName of directoryNames) { + const result = await this.moveDirContents(path.join(srcDir, directoryName), path.join(destDir, directoryName)) + if (result.isFailed()) { + return result + } + } + + await this.moveFiles( + fileNames.map((fileName) => path.join(srcDir, fileName)), + destDir, + ) + + return Result.ok('Directory contents moved successfully') + } catch (error) { + console.error(error) + + return Result.fail(`Could not move directory contentes: ${(error as Error).message}`) + } + } + + async extractZip(source: string, dest: string): Promise { + return new Promise((resolve, reject) => { + yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => { + let cancelled = false + + const tryReject = (err: Error) => { + if (!cancelled) { + cancelled = true + reject(err) + } + } + + if (err) { + return tryReject(err) + } + + if (!zipFile) { + return tryReject(new Error('zipFile === undefined')) + } + + zipFile.readEntry() + + zipFile.on('close', resolve) + + zipFile.on('entry', (entry) => { + if (cancelled) { + return + } + + const isEntryDirectory = entry.fileName.endsWith('/') + if (isEntryDirectory) { + zipFile.readEntry() + return + } + + zipFile.openReadStream(entry, async (err, stream) => { + if (cancelled) { + return + } + + if (err) { + return tryReject(err) + } + + if (!stream) { + return tryReject(new Error('stream === undefined')) + } + + stream.on('error', tryReject) + + const filepath = path.join(dest, entry.fileName) + + try { + await this.ensureDirectoryExists(path.dirname(filepath)) + } catch (error: any) { + return tryReject(error) + } + const writeStream = fs.createWriteStream(filepath).on('error', tryReject).on('error', tryReject) + + stream.pipe(writeStream).on('close', () => { + zipFile.readEntry() + }) + }) + }) + }) + }) + } + + async moveFiles(sources: string[], destDir: string): Promise { + await this.ensureDirectoryExists(destDir) + return Promise.all(sources.map((fileName) => this.moveFile(fileName, path.join(destDir, path.basename(fileName))))) + } + + async moveFile(source: PathLike, destination: PathLike): Promise { + try { + await fs.promises.rename(source, destination) + } catch (_error) { + /** Fall back to copying and then deleting. */ + await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_FICLONE_FORCE) + await fs.promises.unlink(source) + } + } + + async deleteFileIfExists(filePath: PathLike): Promise { + try { + await this.deleteFile(filePath) + } catch { + return + } + } + + async deleteFile(filePath: PathLike): Promise { + for (let i = 1, maxTries = 10; i < maxTries; i++) { + try { + await fs.promises.unlink(filePath) + break + } catch (error: any) { + if (error.code === FileErrorCodes.OperationNotPermitted || error.code === FileErrorCodes.DeviceIsBusy) { + await new Promise((resolve) => setTimeout(resolve, 300)) + continue + } else if (error.code === FileErrorCodes.FileDoesNotExist) { + /** Already deleted */ + break + } + throw error + } + } + } +} diff --git a/packages/desktop/app/javascripts/Main/File/FilesManagerInterface.ts b/packages/desktop/app/javascripts/Main/File/FilesManagerInterface.ts new file mode 100644 index 000000000..78a65af65 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/File/FilesManagerInterface.ts @@ -0,0 +1,23 @@ +import { Result } from '@standardnotes/domain-core' +import { PathLike } from 'fs' + +export interface FilesManagerInterface { + debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void + openDirectoryPicker(buttonLabel?: string): Promise + readJSONFile(filepath: string): Promise + readJSONFileSync(filepath: string): T + writeJSONFile(filepath: string, data: unknown): Promise + writeFile(filepath: string, data: string): Promise + writeJSONFileSync(filepath: string, data: unknown): void + ensureDirectoryExists(dirPath: string): Promise + deleteDir(dirPath: string): Promise> + deleteDirContents(dirPath: string): Promise + isChildOfDir(parent: string, potentialChild: string): boolean + moveDirectory(dir: string, destination: string): Promise> + moveDirContents(srcDir: string, destDir: string): Promise> + extractZip(source: string, dest: string): Promise + moveFiles(sources: string[], destDir: string): Promise + moveFile(source: PathLike, destination: PathLike): Promise + deleteFileIfExists(filePath: PathLike): Promise + deleteFile(filePath: PathLike): Promise +} diff --git a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts index 650525d21..8652621fb 100644 --- a/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts +++ b/packages/desktop/app/javascripts/Main/FileBackups/FileBackupsManager.ts @@ -9,23 +9,14 @@ import { } from '@web/Application/Device/DesktopSnjsExports' import { AppState } from 'app/AppState' import { promises as fs, existsSync } from 'fs' -import { WebContents, shell } from 'electron' +import { WebContents } from 'electron' import { StoreKeys } from '../Store/StoreKeys' import path from 'path' -import { - deleteFileIfExists, - ensureDirectoryExists, - moveDirContents, - moveFile, - openDirectoryPicker, - readJSONFile, - writeFile, - writeJSONFile, -} from '../Utils/FileUtils' import { FileDownloader } from './FileDownloader' import { FileReadOperation } from './FileReadOperation' import { Paths } from '../Types/Paths' import { MessageToWebApp } from '../../Shared/IpcMessages' +import { FilesManagerInterface } from '../File/FilesManagerInterface' const TextBackupFileExtension = '.txt' @@ -39,7 +30,11 @@ export class FilesBackupManager implements FileBackupsDevice { private readOperations: Map = new Map() private plaintextMappingCache?: PlaintextBackupsMapping - constructor(private appState: AppState, private webContents: WebContents) {} + constructor( + private appState: AppState, + private webContents: WebContents, + private filesManager: FilesManagerInterface, + ) {} private async findUuidForPlaintextBackupFileName( backupsDirectory: string, @@ -72,16 +67,16 @@ export class FilesBackupManager implements FileBackupsDevice { return } - await ensureDirectoryExists(newLocation) + await this.filesManager.ensureDirectoryExists(newLocation) const legacyMappingLocation = path.join(legacyLocation, 'info.json') const newMappingLocation = this.getFileBackupsMappingFilePath(newLocation) - await ensureDirectoryExists(path.dirname(newMappingLocation)) + await this.filesManager.ensureDirectoryExists(path.dirname(newMappingLocation)) if (existsSync(legacyMappingLocation)) { - await moveFile(legacyMappingLocation, newMappingLocation) + await this.filesManager.moveFile(legacyMappingLocation, newMappingLocation) } - await moveDirContents(legacyLocation, newLocation) + await this.filesManager.moveDirContents(legacyLocation, newLocation) } public async isLegacyFilesBackupsEnabled(): Promise { @@ -111,33 +106,12 @@ export class FilesBackupManager implements FileBackupsDevice { return path.join(Paths.homeDir, LegacyTextBackupsDirectory) } - public async presentDirectoryPickerForLocationChangeAndTransferOld( - appendPath: string, - oldLocation?: string, - ): Promise { - const selectedDirectory = await openDirectoryPicker('Select') - - if (!selectedDirectory) { - return undefined - } - - const newPath = path.join(selectedDirectory, path.normalize(appendPath)) - - await ensureDirectoryExists(newPath) - - if (oldLocation) { - await moveDirContents(path.normalize(oldLocation), newPath) - } - - return newPath - } - private getFileBackupsMappingFilePath(backupsLocation: string): string { return path.join(backupsLocation, '.settings', 'info.json') } private async getFileBackupsMappingFileFromDisk(backupsLocation: string): Promise { - return readJSONFile(this.getFileBackupsMappingFilePath(backupsLocation)) + return this.filesManager.readJSONFile(this.getFileBackupsMappingFilePath(backupsLocation)) } private defaulFileBackupstMappingFileValue(): FileBackupsMapping { @@ -158,12 +132,8 @@ export class FilesBackupManager implements FileBackupsDevice { return data } - async openLocation(location: string): Promise { - void shell.openPath(location) - } - private async saveFilesBackupsMappingFile(location: string, file: FileBackupsMapping): Promise<'success' | 'failed'> { - await writeJSONFile(this.getFileBackupsMappingFilePath(location), file) + await this.filesManager.writeJSONFile(this.getFileBackupsMappingFilePath(location), file) return 'success' } @@ -182,9 +152,9 @@ export class FilesBackupManager implements FileBackupsDevice { const metaFilePath = path.join(fileDir, FileBackupsConstantsV1.MetadataFileName) const binaryPath = path.join(fileDir, FileBackupsConstantsV1.BinaryFileName) - await ensureDirectoryExists(fileDir) + await this.filesManager.ensureDirectoryExists(fileDir) - await writeFile(metaFilePath, metaFile) + await this.filesManager.writeFile(metaFilePath, metaFile) const downloader = new FileDownloader( downloadRequest.chunkSizes, @@ -247,7 +217,7 @@ export class FilesBackupManager implements FileBackupsDevice { let success: boolean try { - await ensureDirectoryExists(location) + await this.filesManager.ensureDirectoryExists(location) const name = `${new Date().toISOString().replace(/:/g, '-')}${TextBackupFileExtension}` const filePath = path.join(location, name) await fs.writeFile(filePath, data) @@ -262,7 +232,7 @@ export class FilesBackupManager implements FileBackupsDevice { async copyDecryptScript(location: string) { try { - await ensureDirectoryExists(location) + await this.filesManager.ensureDirectoryExists(location) await fs.copyFile(Paths.decryptScript, path.join(location, path.basename(Paths.decryptScript))) } catch (error) { console.error(error) @@ -274,14 +244,14 @@ export class FilesBackupManager implements FileBackupsDevice { } private async getPlaintextMappingFileFromDisk(location: string): Promise { - return readJSONFile(this.getPlaintextMappingFilePath(location)) + return this.filesManager.readJSONFile(this.getPlaintextMappingFilePath(location)) } private async savePlaintextBackupsMappingFile( location: string, file: PlaintextBackupsMapping, ): Promise<'success' | 'failed'> { - await writeJSONFile(this.getPlaintextMappingFilePath(location), file) + await this.filesManager.writeJSONFile(this.getPlaintextMappingFilePath(location), file) return 'success' } @@ -324,7 +294,7 @@ export class FilesBackupManager implements FileBackupsDevice { const records = mapping.files[uuid] for (const record of records) { const filePath = path.join(location, record.path) - await deleteFileIfExists(filePath) + await this.filesManager.deleteFileIfExists(filePath) } mapping.files[uuid] = [] } @@ -337,12 +307,12 @@ export class FilesBackupManager implements FileBackupsDevice { return records.find((record) => record.tag === tag) } - await ensureDirectoryExists(absolutePath) + await this.filesManager.ensureDirectoryExists(absolutePath) const relativePath = forTag ?? '' const filenameWithSlashesEscaped = filename.replace(/\//g, '\u2215') const fileAbsolutePath = path.join(absolutePath, relativePath, filenameWithSlashesEscaped) - await writeFile(fileAbsolutePath, data) + await this.filesManager.writeFile(fileAbsolutePath, data) const existingRecord = findMappingRecord(forTag) if (!existingRecord) { @@ -385,6 +355,10 @@ export class FilesBackupManager implements FileBackupsDevice { const watcher = fs.watch(backupsDirectory, { recursive: true }) for await (const event of watcher) { const { eventType, filename } = event + if (!filename) { + continue + } + if (eventType !== 'change' && eventType !== 'rename') { continue } diff --git a/packages/desktop/app/javascripts/Main/HomeServer/HomeServerConfigurationFile.ts b/packages/desktop/app/javascripts/Main/HomeServer/HomeServerConfigurationFile.ts new file mode 100644 index 000000000..1069a20c0 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/HomeServer/HomeServerConfigurationFile.ts @@ -0,0 +1,7 @@ +import { HomeServerEnvironmentConfiguration } from '@standardnotes/snjs' + +export interface HomeServerConfigurationFile { + version: '1.0.0' + info: Record + configuration: HomeServerEnvironmentConfiguration +} diff --git a/packages/desktop/app/javascripts/Main/HomeServer/HomeServerManager.ts b/packages/desktop/app/javascripts/Main/HomeServer/HomeServerManager.ts new file mode 100644 index 000000000..19cb94891 --- /dev/null +++ b/packages/desktop/app/javascripts/Main/HomeServer/HomeServerManager.ts @@ -0,0 +1,254 @@ +import path from 'path' + +import { + HomeServerManagerInterface, + HomeServerEnvironmentConfiguration, +} from '@web/Application/Device/DesktopSnjsExports' +import { HomeServerInterface } from '@standardnotes/home-server' + +import { WebContents } from 'electron' +import { MessageToWebApp } from '../../Shared/IpcMessages' +import { FilesManagerInterface } from '../File/FilesManagerInterface' +import { HomeServerConfigurationFile } from './HomeServerConfigurationFile' + +const os = require('os') + +export class HomeServerManager implements HomeServerManagerInterface { + private readonly HOME_SERVER_CONFIGURATION_FILE_NAME = 'config.json' + + private homeServerConfiguration: HomeServerEnvironmentConfiguration | undefined + private homeServerDataLocation: string | undefined + private lastErrorMessage: string | undefined + private logs: string[] = [] + + private readonly LOGS_BUFFER_SIZE = 1000 + + constructor( + private homeServer: HomeServerInterface, + private webContents: WebContents, + private filesManager: FilesManagerInterface, + ) {} + + async getHomeServerUrl(): Promise { + const homeServerConfiguration = await this.getHomeServerConfigurationObject() + if (!homeServerConfiguration) { + return undefined + } + + return `http://${this.getLocalIP()}:${homeServerConfiguration.port}` + } + + async isHomeServerRunning(): Promise { + return this.homeServer.isRunning() + } + + async getHomeServerLastErrorMessage(): Promise { + return this.lastErrorMessage + } + + async activatePremiumFeatures(username: string): Promise { + const result = await this.homeServer.activatePremiumFeatures(username) + + if (result.isFailed()) { + return result.getError() + } + } + + async getHomeServerConfiguration(): Promise { + if (this.homeServerConfiguration) { + return JSON.stringify(this.homeServerConfiguration) + } + + if (!this.homeServerDataLocation) { + return undefined + } + + const homeServerConfiguration = await this.filesManager.readJSONFile( + path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME), + ) + if (!homeServerConfiguration) { + return undefined + } + + return JSON.stringify(homeServerConfiguration.configuration) + } + + async setHomeServerConfiguration(configurationJSONString: string): Promise { + try { + if (!this.homeServerDataLocation) { + throw new Error('Home server data location is not set.') + } + + const homeServerConfiguration = JSON.parse(configurationJSONString) as HomeServerEnvironmentConfiguration + + await this.filesManager.ensureDirectoryExists(this.homeServerDataLocation) + + const configurationFile: HomeServerConfigurationFile = { + version: '1.0.0', + info: { + warning: 'Do not edit this file.', + information: + 'The values below are encrypted with a key created by the desktop application after installation. The key is stored in your secure device keychain.', + instructions: + 'Put this file inside your home server data location to restore your home server configuration.', + }, + configuration: homeServerConfiguration, + } + + await this.filesManager.writeJSONFile( + path.join(this.homeServerDataLocation, this.HOME_SERVER_CONFIGURATION_FILE_NAME), + configurationFile, + ) + + this.homeServerConfiguration = homeServerConfiguration + } catch (error) { + console.error(`Could not save server configuration: ${(error as Error).message}`) + } + } + + async setHomeServerDataLocation(location: string): Promise { + this.homeServerDataLocation = location + } + + async stopHomeServer(): Promise { + const result = await this.homeServer.stop() + if (result.isFailed()) { + return result.getError() + } + + return undefined + } + + async startHomeServer(): Promise { + try { + this.lastErrorMessage = undefined + this.logs = [] + + let homeServerConfiguration = await this.getHomeServerConfigurationObject() + if (!homeServerConfiguration) { + homeServerConfiguration = this.generateHomeServerConfiguration() + } + await this.setHomeServerConfiguration(JSON.stringify(homeServerConfiguration)) + + if (!this.homeServerDataLocation) { + this.lastErrorMessage = 'Home server data location is not set.' + + return this.lastErrorMessage + } + + const { + jwtSecret, + authJwtSecret, + encryptionServerKey, + pseudoKeyParamsKey, + valetTokenSecret, + port, + logLevel, + databaseEngine, + mysqlConfiguration, + } = homeServerConfiguration as HomeServerEnvironmentConfiguration + + const environment: { [name: string]: string } = { + JWT_SECRET: jwtSecret, + AUTH_JWT_SECRET: authJwtSecret, + ENCRYPTION_SERVER_KEY: encryptionServerKey, + PSEUDO_KEY_PARAMS_KEY: pseudoKeyParamsKey, + VALET_TOKEN_SECRET: valetTokenSecret, + FILES_SERVER_URL: (await this.getHomeServerUrl()) as string, + LOG_LEVEL: logLevel ?? 'info', + VERSION: 'desktop', + PORT: port.toString(), + DB_TYPE: databaseEngine, + } + + if (mysqlConfiguration !== undefined) { + environment.DB_HOST = mysqlConfiguration.host + if (mysqlConfiguration.port) { + environment.DB_PORT = mysqlConfiguration.port.toString() + } + environment.DB_USERNAME = mysqlConfiguration.username + environment.DB_PASSWORD = mysqlConfiguration.password + environment.DB_DATABASE = mysqlConfiguration.database + } + + const result = await this.homeServer.start({ + dataDirectoryPath: this.homeServerDataLocation, + environment, + logStreamCallback: this.appendLogs.bind(this), + }) + + if (result.isFailed()) { + this.lastErrorMessage = result.getError() + + return this.lastErrorMessage + } + + this.webContents.send(MessageToWebApp.HomeServerStarted, await this.getHomeServerUrl()) + } catch (error) { + return (error as Error).message + } + } + + async getHomeServerLogs(): Promise { + return this.logs + } + + private appendLogs(log: Buffer): void { + this.logs.push(log.toString()) + + if (this.logs.length > this.LOGS_BUFFER_SIZE) { + this.logs.shift() + } + } + + private generateRandomKey(length: number): string { + return require('crypto').randomBytes(length).toString('hex') + } + + private getLocalIP() { + const interfaces = os.networkInterfaces() + for (const interfaceName in interfaces) { + const addresses = interfaces[interfaceName] + for (const address of addresses) { + if (address.family === 'IPv4' && !address.internal) { + return address.address + } + } + } + } + + private async getHomeServerConfigurationObject(): Promise { + try { + const homeServerConfigurationJSON = await this.getHomeServerConfiguration() + if (!homeServerConfigurationJSON) { + return undefined + } + + return JSON.parse(homeServerConfigurationJSON) + } catch (error) { + console.error(`Could not get home server configuration: ${(error as Error).message}`) + } + } + + private generateHomeServerConfiguration(): HomeServerEnvironmentConfiguration { + const jwtSecret = this.generateRandomKey(32) + const authJwtSecret = this.generateRandomKey(32) + const encryptionServerKey = this.generateRandomKey(32) + const pseudoKeyParamsKey = this.generateRandomKey(32) + const valetTokenSecret = this.generateRandomKey(32) + const port = 3127 + + const configuration: HomeServerEnvironmentConfiguration = { + jwtSecret, + authJwtSecret, + encryptionServerKey, + pseudoKeyParamsKey, + valetTokenSecret, + port, + databaseEngine: 'sqlite', + logLevel: 'info', + } + + return configuration + } +} diff --git a/packages/desktop/app/javascripts/Main/Packages/Networking.ts b/packages/desktop/app/javascripts/Main/Packages/Networking.ts index 45c8f53f5..4fc742f42 100644 --- a/packages/desktop/app/javascripts/Main/Packages/Networking.ts +++ b/packages/desktop/app/javascripts/Main/Packages/Networking.ts @@ -4,9 +4,9 @@ import path from 'path' import { pipeline as pipelineFn } from 'stream' import { promisify } from 'util' import { MessageType } from '../../../../test/TestIpcMessage' -import { ensureDirectoryExists } from '../Utils/FileUtils' import { handleTestMessage } from '../Utils/Testing' import { isTesting } from '../Utils/Utils' +import { FilesManager } from '../File/FilesManager' const pipeline = promisify(pipelineFn) @@ -21,7 +21,7 @@ if (isTesting()) { * not exist) */ export async function downloadFile(url: string, filePath: string): Promise { - await ensureDirectoryExists(path.dirname(filePath)) + await new FilesManager().ensureDirectoryExists(path.dirname(filePath)) const response = await get(url) await pipeline( /** diff --git a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts index 73a928ea3..639ea3c47 100644 --- a/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts +++ b/packages/desktop/app/javascripts/Main/Packages/PackageManager.ts @@ -5,19 +5,12 @@ import path from 'path' import { MessageToWebApp } from '../../Shared/IpcMessages' import { AppName } from '../Strings' import { Paths } from '../Types/Paths' -import { - debouncedJSONDiskWriter, - deleteDir, - deleteDirContents, - ensureDirectoryExists, - extractZip, - FileDoesNotExist, - moveDirContents, - readJSONFile, -} from '../Utils/FileUtils' import { timeout } from '../Utils/Utils' import { downloadFile, getJSON } from './Networking' import { Component, MappingFile, PackageInfo, PackageManagerInterface, SyncTask } from './PackageManagerInterface' +import { FilesManagerInterface } from '../File/FilesManagerInterface' +import { FilesManager } from '../File/FilesManager' +import { FileErrorCodes } from '../File/FileErrorCodes' function logMessage(...message: any) { log.info('PackageManager[Info]:', ...message) @@ -33,26 +26,27 @@ function logError(...message: any) { class MappingFileHandler { static async create() { let mapping: MappingFile + const filesManager = new FilesManager() try { - const result = await readJSONFile(Paths.extensionsMappingJson) + const result = await filesManager.readJSONFile(Paths.extensionsMappingJson) mapping = result || {} } catch (error: any) { /** * Mapping file might be absent (first start, corrupted data) */ - if (error.code === FileDoesNotExist) { - await ensureDirectoryExists(path.dirname(Paths.extensionsMappingJson)) + if (error.code === FileErrorCodes.FileDoesNotExist) { + await filesManager.ensureDirectoryExists(path.dirname(Paths.extensionsMappingJson)) } else { logError(error) } mapping = {} } - return new MappingFileHandler(mapping) + return new MappingFileHandler(mapping, filesManager) } - constructor(private mapping: MappingFile) {} + constructor(private mapping: MappingFile, private filesManager: FilesManagerInterface) {} get = (componendId: string) => { return this.mapping[componendId] @@ -63,12 +57,14 @@ class MappingFileHandler { location, version, } - this.writeToDisk() + + this.filesManager.debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping) } remove = (componentId: string) => { delete this.mapping[componentId] - this.writeToDisk() + + this.filesManager.debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping) } getInstalledVersionForComponent = async (component: Component): Promise => { @@ -83,15 +79,13 @@ class MappingFileHandler { */ const paths = pathsForComponent(component) const packagePath = path.join(paths.absolutePath, 'package.json') - const response = await readJSONFile<{ version: string }>(packagePath) + const response = await this.filesManager.readJSONFile<{ version: string }>(packagePath) if (!response) { return '' } this.set(component.uuid, paths.relativePath, response.version) return response.version } - - private writeToDisk = debouncedJSONDiskWriter(100, Paths.extensionsMappingJson, () => this.mapping) } export async function initializePackageManager(webContents: Electron.WebContents): Promise { @@ -230,7 +224,7 @@ async function syncComponents(webContents: Electron.WebContents, mapping: Mappin await checkForUpdate(webContents, mapping, component) } } catch (error: any) { - if (error.code === FileDoesNotExist) { + if (error.code === FileErrorCodes.FileDoesNotExist) { /** We have a component but no content. Install the component */ await installComponent(webContents, mapping, component, component.content.package_info, version) } else { @@ -289,7 +283,7 @@ async function unnestLegacyStructure(dir: string) { const sourceDir = path.join(dir, fileNames[0]) const destDir = dir - await moveDirContents(sourceDir, destDir) + await new FilesManager().moveDirContents(sourceDir, destDir) } async function installComponent( @@ -331,13 +325,15 @@ async function installComponent( downloadFile(downloadUrl, paths.downloadPath), (async () => { /** Clear the component's directory before extracting the zip. */ - await ensureDirectoryExists(paths.absolutePath) - await deleteDirContents(paths.absolutePath) + const filesManager = new FilesManager() + await filesManager.ensureDirectoryExists(paths.absolutePath) + await filesManager.deleteDirContents(paths.absolutePath) })(), ]) logMessage('Extracting', paths.downloadPath, 'to', paths.absolutePath) - await extractZip(paths.downloadPath, paths.absolutePath) + const filesManager = new FilesManager() + await filesManager.extractZip(paths.downloadPath, paths.absolutePath) const legacyStructure = await usesLegacyNestedFolderStructure(paths.absolutePath) if (legacyStructure) { @@ -348,7 +344,7 @@ async function installComponent( try { /** Try to read 'sn.main' field from 'package.json' file */ const packageJsonPath = path.join(paths.absolutePath, 'package.json') - const packageJson = await readJSONFile<{ + const packageJson = await filesManager.readJSONFile<{ sn?: { main?: string } version?: string }>(packageJsonPath) @@ -403,6 +399,8 @@ async function uninstallComponent(mapping: MappingFileHandler, uuid: string) { /** No mapping for component */ return } - await deleteDir(path.join(Paths.userDataDir, componentMapping.location)) - mapping.remove(uuid) + const result = await new FilesManager().deleteDir(path.join(Paths.userDataDir, componentMapping.location)) + if (!result.isFailed()) { + mapping.remove(uuid) + } } diff --git a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts index a60af3fcf..7114ebf27 100644 --- a/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts +++ b/packages/desktop/app/javascripts/Main/Remote/RemoteBridge.ts @@ -10,7 +10,9 @@ import { FileBackupsMapping, FileBackupReadToken, FileBackupReadChunkResponse, + HomeServerManagerInterface, PlaintextBackupsMapping, + DirectoryManagerInterface, } from '@web/Application/Device/DesktopSnjsExports' import { app, BrowserWindow } from 'electron' import { KeychainInterface } from '../Keychain/KeychainInterface' @@ -34,6 +36,8 @@ export class RemoteBridge implements CrossProcessBridge { private menus: MenuManagerInterface, private fileBackups: FileBackupsDevice, private media: MediaManagerInterface, + private homeServerManager: HomeServerManagerInterface, + private directoryManager: DirectoryManagerInterface, ) {} get exposableValue(): CrossProcessBridge { @@ -63,6 +67,8 @@ export class RemoteBridge implements CrossProcessBridge { getFileBackupReadToken: this.getFileBackupReadToken.bind(this), readNextChunk: this.readNextChunk.bind(this), askForMediaAccess: this.askForMediaAccess.bind(this), + startHomeServer: this.startHomeServer.bind(this), + stopHomeServer: this.stopHomeServer.bind(this), wasLegacyTextBackupsExplicitlyDisabled: this.wasLegacyTextBackupsExplicitlyDisabled.bind(this), getLegacyTextBackupsLocation: this.getLegacyTextBackupsLocation.bind(this), saveTextBackupData: this.saveTextBackupData.bind(this), @@ -70,6 +76,7 @@ export class RemoteBridge implements CrossProcessBridge { openLocation: this.openLocation.bind(this), presentDirectoryPickerForLocationChangeAndTransferOld: this.presentDirectoryPickerForLocationChangeAndTransferOld.bind(this), + getDirectoryManagerLastErrorMessage: this.getDirectoryManagerLastErrorMessage.bind(this), getPlaintextBackupsMappingFile: this.getPlaintextBackupsMappingFile.bind(this), persistPlaintextBackupsMappingFile: this.persistPlaintextBackupsMappingFile.bind(this), getTextBackupsCount: this.getTextBackupsCount.bind(this), @@ -77,6 +84,14 @@ export class RemoteBridge implements CrossProcessBridge { getUserDocumentsDirectory: this.getUserDocumentsDirectory.bind(this), monitorPlaintextBackupsLocationForChanges: this.monitorPlaintextBackupsLocationForChanges.bind(this), joinPaths: this.joinPaths.bind(this), + setHomeServerConfiguration: this.setHomeServerConfiguration.bind(this), + getHomeServerConfiguration: this.getHomeServerConfiguration.bind(this), + setHomeServerDataLocation: this.setHomeServerDataLocation.bind(this), + activatePremiumFeatures: this.activatePremiumFeatures.bind(this), + isHomeServerRunning: this.isHomeServerRunning.bind(this), + getHomeServerLogs: this.getHomeServerLogs.bind(this), + getHomeServerUrl: this.getHomeServerUrl.bind(this), + getHomeServerLastErrorMessage: this.getHomeServerLastErrorMessage.bind(this), } } @@ -201,15 +216,19 @@ export class RemoteBridge implements CrossProcessBridge { return this.fileBackups.savePlaintextNoteBackup(location, uuid, name, tags, data) } - openLocation(path: string): Promise { - return this.fileBackups.openLocation(path) + async openLocation(path: string): Promise { + return this.directoryManager.openLocation(path) } - presentDirectoryPickerForLocationChangeAndTransferOld( + async presentDirectoryPickerForLocationChangeAndTransferOld( appendPath: string, oldLocation?: string | undefined, ): Promise { - return this.fileBackups.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation) + return this.directoryManager.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation) + } + + async getDirectoryManagerLastErrorMessage(): Promise { + return this.directoryManager.getDirectoryManagerLastErrorMessage() } getPlaintextBackupsMappingFile(location: string): Promise { @@ -243,4 +262,44 @@ export class RemoteBridge implements CrossProcessBridge { askForMediaAccess(type: 'camera' | 'microphone'): Promise { return this.media.askForMediaAccess(type) } + + async startHomeServer(): Promise { + return this.homeServerManager.startHomeServer() + } + + async stopHomeServer(): Promise { + return this.homeServerManager.stopHomeServer() + } + + async setHomeServerConfiguration(configurationJSONString: string): Promise { + return this.homeServerManager.setHomeServerConfiguration(configurationJSONString) + } + + async getHomeServerConfiguration(): Promise { + return this.homeServerManager.getHomeServerConfiguration() + } + + async setHomeServerDataLocation(location: string): Promise { + return this.homeServerManager.setHomeServerDataLocation(location) + } + + async activatePremiumFeatures(username: string): Promise { + return this.homeServerManager.activatePremiumFeatures(username) + } + + async isHomeServerRunning(): Promise { + return this.homeServerManager.isHomeServerRunning() + } + + async getHomeServerLogs(): Promise { + return this.homeServerManager.getHomeServerLogs() + } + + async getHomeServerUrl(): Promise { + return this.homeServerManager.getHomeServerUrl() + } + + async getHomeServerLastErrorMessage(): Promise { + return this.homeServerManager.getHomeServerLastErrorMessage() + } } diff --git a/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts b/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts index 9d4ab8dd7..bca55f7a5 100644 --- a/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts +++ b/packages/desktop/app/javascripts/Main/Store/StoreKeys.ts @@ -11,6 +11,8 @@ export enum StoreKeys { UseNativeKeychain = 'useNativeKeychain', LastRunVersion = 'LastRunVersion', + HomeServerDataLocation = 'HomeServerDataLocation', + LegacyTextBackupsLocation = 'backupsLocation', LegacyTextBackupsDisabled = 'backupsDisabled', diff --git a/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts b/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts index 6b59e9856..bc4506c35 100644 --- a/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts +++ b/packages/desktop/app/javascripts/Main/Store/createSanitizedStoreData.ts @@ -1,9 +1,9 @@ import fs from 'fs' import { Language } from '../SpellcheckerManager' -import { FileDoesNotExist } from '../Utils/FileUtils' import { ensureIsBoolean, isBoolean } from '../Utils/Utils' import { StoreData, StoreKeys } from './StoreKeys' import { logError } from './Store' +import { FileErrorCodes } from '../File/FileErrorCodes' export function createSanitizedStoreData(data: any = {}): StoreData { return { @@ -69,7 +69,7 @@ export function parseDataFile(filePath: string) { return createSanitizedStoreData(userData) } catch (error: any) { console.log('Error reading store file', error) - if (error.code !== FileDoesNotExist) { + if (error.code !== FileErrorCodes.FileDoesNotExist) { logError(error) } diff --git a/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts b/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts deleted file mode 100644 index 9bb0ff121..000000000 --- a/packages/desktop/app/javascripts/Main/Utils/FileUtils.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { dialog } from 'electron' -import fs, { PathLike } from 'fs' -import { debounce } from 'lodash' -import path from 'path' -import yauzl from 'yauzl' -import { removeFromArray } from '../Utils/Utils' - -export const FileDoesNotExist = 'ENOENT' -export const FileAlreadyExists = 'EEXIST' -const OperationNotPermitted = 'EPERM' -const DeviceIsBusy = 'EBUSY' - -export function debouncedJSONDiskWriter(durationMs: number, location: string, data: () => unknown): () => void { - let writingToDisk = false - return debounce(async () => { - if (writingToDisk) { - return - } - writingToDisk = true - try { - await writeJSONFile(location, data()) - } catch (error) { - console.error(error) - } finally { - writingToDisk = false - } - }, durationMs) -} - -export async function openDirectoryPicker(buttonLabel?: string): Promise { - const result = await dialog.showOpenDialog({ - properties: ['openDirectory', 'showHiddenFiles', 'createDirectory'], - buttonLabel: buttonLabel, - }) - - return result.filePaths[0] -} - -export async function readJSONFile(filepath: string): Promise { - try { - const data = await fs.promises.readFile(filepath, 'utf8') - return JSON.parse(data) - } catch (error) { - return undefined - } -} - -export function readJSONFileSync(filepath: string): T { - const data = fs.readFileSync(filepath, 'utf8') - return JSON.parse(data) -} - -export async function writeJSONFile(filepath: string, data: unknown): Promise { - await ensureDirectoryExists(path.dirname(filepath)) - await fs.promises.writeFile(filepath, JSON.stringify(data, null, 2), 'utf8') -} - -export async function writeFile(filepath: string, data: string): Promise { - await ensureDirectoryExists(path.dirname(filepath)) - await fs.promises.writeFile(filepath, data, 'utf8') -} - -export function writeJSONFileSync(filepath: string, data: unknown): void { - fs.writeFileSync(filepath, JSON.stringify(data, null, 2), 'utf8') -} - -/** Creates the directory if it doesn't exist. */ -export async function ensureDirectoryExists(dirPath: string): Promise { - try { - const stat = await fs.promises.lstat(dirPath) - if (!stat.isDirectory()) { - throw new Error('Tried to create a directory where a file of the same ' + `name already exists: ${dirPath}`) - } - } catch (error: any) { - if (error.code === FileDoesNotExist) { - /** - * No directory here. Make sure there is a *parent* directory, and then - * create it. - */ - await ensureDirectoryExists(path.dirname(dirPath)) - - /** Now that its parent(s) exist, create the directory */ - try { - await fs.promises.mkdir(dirPath) - } catch (error: any) { - if (error.code === FileAlreadyExists) { - /** - * A concurrent process must have created the directory already. - * Make sure it *is* a directory and not something else. - */ - await ensureDirectoryExists(dirPath) - } else { - throw error - } - } - } else { - throw error - } - } -} - -/** - * Deletes a directory (handling recursion.) - * @param {string} dirPath the path of the directory - */ -export async function deleteDir(dirPath: string): Promise { - try { - await deleteDirContents(dirPath) - } catch (error: any) { - if (error.code === FileDoesNotExist) { - /** Directory has already been deleted. */ - return - } - throw error - } - await fs.promises.rmdir(dirPath) -} - -export async function deleteDirContents(dirPath: string): Promise { - /** - * Scan the directory up to ten times, to handle cases where files are being added while - * the directory's contents are being deleted - */ - for (let i = 1, maxTries = 10; i < maxTries; i++) { - const children = await fs.promises.readdir(dirPath, { - withFileTypes: true, - }) - - if (children.length === 0) { - break - } - - for (const child of children) { - const childPath = path.join(dirPath, child.name) - if (child.isDirectory()) { - await deleteDirContents(childPath) - try { - await fs.promises.rmdir(childPath) - } catch (error) { - if (error !== FileDoesNotExist) { - throw error - } - } - } else { - await deleteFile(childPath) - } - } - } -} - -function isChildOfDir(parent: string, potentialChild: string) { - const relative = path.relative(parent, potentialChild) - return relative && !relative.startsWith('..') && !path.isAbsolute(relative) -} - -export async function moveDirContents(srcDir: string, destDir: string): Promise { - let fileNames: string[] - try { - fileNames = await fs.promises.readdir(srcDir) - } catch (error) { - console.error(error) - return - } - await ensureDirectoryExists(destDir) - - if (isChildOfDir(srcDir, destDir)) { - fileNames = fileNames.filter((name) => { - return !isChildOfDir(destDir, path.join(srcDir, name)) - }) - removeFromArray(fileNames, path.basename(destDir)) - } - - try { - await moveFiles( - fileNames.map((fileName) => path.join(srcDir, fileName)), - destDir, - ) - } catch (error) { - console.error(error) - } -} - -export async function extractZip(source: string, dest: string): Promise { - return new Promise((resolve, reject) => { - yauzl.open(source, { lazyEntries: true, autoClose: true }, (err, zipFile) => { - let cancelled = false - - const tryReject = (err: Error) => { - if (!cancelled) { - cancelled = true - reject(err) - } - } - - if (err) { - return tryReject(err) - } - - if (!zipFile) { - return tryReject(new Error('zipFile === undefined')) - } - - zipFile.readEntry() - - zipFile.on('close', resolve) - - zipFile.on('entry', (entry) => { - if (cancelled) { - return - } - - const isEntryDirectory = entry.fileName.endsWith('/') - if (isEntryDirectory) { - zipFile.readEntry() - return - } - - zipFile.openReadStream(entry, async (err, stream) => { - if (cancelled) { - return - } - - if (err) { - return tryReject(err) - } - - if (!stream) { - return tryReject(new Error('stream === undefined')) - } - - stream.on('error', tryReject) - - const filepath = path.join(dest, entry.fileName) - - try { - await ensureDirectoryExists(path.dirname(filepath)) - } catch (error: any) { - return tryReject(error) - } - const writeStream = fs.createWriteStream(filepath).on('error', tryReject).on('error', tryReject) - - stream.pipe(writeStream).on('close', () => { - zipFile.readEntry() - }) - }) - }) - }) - }) -} - -export async function moveFiles(sources: string[], destDir: string): Promise { - await ensureDirectoryExists(destDir) - return Promise.all(sources.map((fileName) => moveFile(fileName, path.join(destDir, path.basename(fileName))))) -} - -export async function moveFile(source: PathLike, destination: PathLike) { - try { - await fs.promises.rename(source, destination) - } catch (_error) { - /** Fall back to copying and then deleting. */ - await fs.promises.copyFile(source, destination, fs.constants.COPYFILE_FICLONE_FORCE) - await fs.promises.unlink(source) - } -} - -export async function deleteFileIfExists(filePath: PathLike): Promise { - try { - await deleteFile(filePath) - } catch { - return - } -} - -/** Deletes a file, handling EPERM and EBUSY errors on Windows. */ -export async function deleteFile(filePath: PathLike): Promise { - for (let i = 1, maxTries = 10; i < maxTries; i++) { - try { - await fs.promises.unlink(filePath) - break - } catch (error: any) { - if (error.code === OperationNotPermitted || error.code === DeviceIsBusy) { - await new Promise((resolve) => setTimeout(resolve, 300)) - continue - } else if (error.code === FileDoesNotExist) { - /** Already deleted */ - break - } - throw error - } - } -} diff --git a/packages/desktop/app/javascripts/Main/Window.ts b/packages/desktop/app/javascripts/Main/Window.ts index e63ca3a5a..cdebff94d 100644 --- a/packages/desktop/app/javascripts/Main/Window.ts +++ b/packages/desktop/app/javascripts/Main/Window.ts @@ -24,6 +24,10 @@ import { checkForUpdate, setupUpdates } from './UpdateManager' import { handleTestMessage, send } from './Utils/Testing' import { isTesting, lowercaseDriveLetter } from './Utils/Utils' import { initializeZoomManager } from './ZoomManager' +import { HomeServerManager } from './HomeServer/HomeServerManager' +import { HomeServer } from '@standardnotes/home-server' +import { FilesManager } from './File/FilesManager' +import { DirectoryManager } from './Directory/DirectoryManager' const WINDOW_DEFAULT_WIDTH = 1100 const WINDOW_DEFAULT_HEIGHT = 800 @@ -72,6 +76,8 @@ export async function createWindowState({ services.menuManager, services.fileBackupsManager, services.mediaManager, + services.homeServerManager, + services.directoryManager, ) const shouldOpenUrl = (url: string) => url.startsWith('http') || url.startsWith('mailto') @@ -200,6 +206,11 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap const trayManager = createTrayManager(window, appState.store) const spellcheckerManager = createSpellcheckerManager(appState.store, window.webContents, appLocale) const mediaManager = new MediaManager() + const homeServer = new HomeServer() + const filesManager = new FilesManager() + const directoryManager = new DirectoryManager(filesManager) + + const homeServerManager = new HomeServerManager(homeServer, window.webContents, filesManager) if (isTesting()) { handleTestMessage(MessageType.SpellCheckerManager, () => spellcheckerManager) @@ -213,7 +224,7 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap spellcheckerManager, }) - const fileBackupsManager = new FilesBackupManager(appState, window.webContents) + const fileBackupsManager = new FilesBackupManager(appState, window.webContents, filesManager) return { updateManager, @@ -224,6 +235,8 @@ async function createWindowServices(window: Electron.BrowserWindow, appState: Ap searchManager, fileBackupsManager, mediaManager, + homeServerManager, + directoryManager, } } diff --git a/packages/desktop/app/javascripts/Renderer/CrossProcessBridge.ts b/packages/desktop/app/javascripts/Renderer/CrossProcessBridge.ts index d07cfb50d..e9e4df933 100644 --- a/packages/desktop/app/javascripts/Renderer/CrossProcessBridge.ts +++ b/packages/desktop/app/javascripts/Renderer/CrossProcessBridge.ts @@ -1,7 +1,11 @@ -import { FileBackupsDevice } from '@web/Application/Device/DesktopSnjsExports' +import { + DirectoryManagerInterface, + FileBackupsDevice, + HomeServerManagerInterface, +} from '@web/Application/Device/DesktopSnjsExports' import { Component } from '../Main/Packages/PackageManagerInterface' -export interface CrossProcessBridge extends FileBackupsDevice { +export interface CrossProcessBridge extends FileBackupsDevice, DirectoryManagerInterface, HomeServerManagerInterface { get extServerHost(): string get useNativeKeychain(): boolean get rendererPath(): string diff --git a/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts b/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts index 4408535b5..efb0279da 100644 --- a/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts +++ b/packages/desktop/app/javascripts/Renderer/DesktopDevice.ts @@ -25,6 +25,46 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn super(appVersion) } + async getHomeServerUrl(): Promise { + return this.remoteBridge.getHomeServerUrl() + } + + async getHomeServerLastErrorMessage(): Promise { + return this.remoteBridge.getHomeServerLastErrorMessage() + } + + async isHomeServerRunning(): Promise { + return this.remoteBridge.isHomeServerRunning() + } + + async activatePremiumFeatures(username: string): Promise { + return this.remoteBridge.activatePremiumFeatures(username) + } + + async setHomeServerConfiguration(configurationJSONString: string): Promise { + return this.remoteBridge.setHomeServerConfiguration(configurationJSONString) + } + + async getHomeServerConfiguration(): Promise { + return this.remoteBridge.getHomeServerConfiguration() + } + + async setHomeServerDataLocation(location: string): Promise { + return this.remoteBridge.setHomeServerDataLocation(location) + } + + startHomeServer(): Promise { + return this.remoteBridge.startHomeServer() + } + + stopHomeServer(): Promise { + return this.remoteBridge.stopHomeServer() + } + + getHomeServerLogs(): Promise { + return this.remoteBridge.getHomeServerLogs() + } + openLocation(path: string): Promise { return this.remoteBridge.openLocation(path) } @@ -36,6 +76,10 @@ export class DesktopDevice extends WebOrDesktopDevice implements DesktopDeviceIn return this.remoteBridge.presentDirectoryPickerForLocationChangeAndTransferOld(appendPath, oldLocation) } + getDirectoryManagerLastErrorMessage(): Promise { + return this.remoteBridge.getDirectoryManagerLastErrorMessage() + } + getFilesBackupsMappingFile(location: string): Promise { return this.remoteBridge.getFilesBackupsMappingFile(location) } diff --git a/packages/desktop/app/javascripts/Renderer/Preload.ts b/packages/desktop/app/javascripts/Renderer/Preload.ts index 86fdb3c1b..285436d33 100644 --- a/packages/desktop/app/javascripts/Renderer/Preload.ts +++ b/packages/desktop/app/javascripts/Renderer/Preload.ts @@ -19,6 +19,9 @@ process.once('loaded', function () { setInstallComponentCompleteHandler: (handler: MainEventHandler) => ipcRenderer.on(MessageToWebApp.InstallComponentComplete, handler), + + setHomeServerStartedHandler: (handler: MainEventHandler) => + ipcRenderer.on(MessageToWebApp.HomeServerStarted, handler), } contextBridge.exposeInMainWorld('electronMainEvents', mainEvents) diff --git a/packages/desktop/app/javascripts/Renderer/Renderer.ts b/packages/desktop/app/javascripts/Renderer/Renderer.ts index 33415124f..a0b37f6bd 100644 --- a/packages/desktop/app/javascripts/Renderer/Renderer.ts +++ b/packages/desktop/app/javascripts/Renderer/Renderer.ts @@ -54,6 +54,12 @@ window.onload = () => { void loadAndStartApplication() } +window.onunload = () => { + if (window.device) { + void window.device.stopHomeServer() + } +} + /** @returns whether the keychain structure is up to date or not */ async function migrateKeychain(remoteBridge: CrossProcessBridge): Promise { if (!remoteBridge.useNativeKeychain) { @@ -151,3 +157,7 @@ window.electronMainEvents.setInstallComponentCompleteHandler((_: IpcRendererEven window.electronMainEvents.setWatchedDirectoriesChangeHandler((_: IpcRendererEvent, changes: unknown) => { void window.webClient.handleWatchedDirectoriesChanges(changes as DesktopWatchedDirectoriesChanges) }) + +window.electronMainEvents.setHomeServerStartedHandler((_: IpcRendererEvent, serverUrl: unknown) => { + void window.webClient.handleHomeServerStarted(serverUrl as string) +}) diff --git a/packages/desktop/app/javascripts/Shared/ElectronMainEvents.ts b/packages/desktop/app/javascripts/Shared/ElectronMainEvents.ts index fcc4cffcd..ef27b6452 100644 --- a/packages/desktop/app/javascripts/Shared/ElectronMainEvents.ts +++ b/packages/desktop/app/javascripts/Shared/ElectronMainEvents.ts @@ -8,4 +8,5 @@ export interface ElectronMainEvents { setWindowFocusedHandler(handler: MainEventHandler): void setInstallComponentCompleteHandler(handler: MainEventHandler): void setWatchedDirectoriesChangeHandler(handler: MainEventHandler): void + setHomeServerStartedHandler(handler: MainEventHandler): void } diff --git a/packages/desktop/app/javascripts/Shared/IpcMessages.ts b/packages/desktop/app/javascripts/Shared/IpcMessages.ts index 5b2a48a2e..8ae722c19 100644 --- a/packages/desktop/app/javascripts/Shared/IpcMessages.ts +++ b/packages/desktop/app/javascripts/Shared/IpcMessages.ts @@ -4,6 +4,7 @@ export enum MessageToWebApp { WindowFocused = 'window-focused', InstallComponentComplete = 'install-component-complete', WatchedDirectoriesChanges = 'watched-directories-changes', + HomeServerStarted = 'home-server-started', } export enum MessageToMainProcess { diff --git a/packages/desktop/app/package.json b/packages/desktop/app/package.json index 6635450cd..50d41a995 100644 --- a/packages/desktop/app/package.json +++ b/packages/desktop/app/package.json @@ -9,6 +9,7 @@ "selfReferences": true }, "dependencies": { + "@standardnotes/home-server": "^1.11.14", "keytar": "^7.9.0" } } diff --git a/packages/desktop/desktop.webpack.common.js b/packages/desktop/desktop.webpack.common.js index 051126d2f..7e8c880e0 100644 --- a/packages/desktop/desktop.webpack.common.js +++ b/packages/desktop/desktop.webpack.common.js @@ -67,6 +67,7 @@ module.exports = function ({ onlyTranspileTypescript = false, experimentalFeatur module: moduleConfig, externals: { keytar: 'commonjs keytar', + "@standardnotes/home-server": "commonjs @standardnotes/home-server", }, optimization: { minimizer: [ diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 3251cfb0a..69e0b6693 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@electron/remote": "^2.0.9", + "@standardnotes/domain-core": "^1.18.0", "@standardnotes/electron-clear-data": "1.1.1", "@standardnotes/web": "workspace:*", "axios": "^1.1.3", @@ -44,7 +45,7 @@ "electron": "22.1.0", "electron-log": "^4.4.8", "electron-updater": "^5.3.0", - "fs-extra": "^10.1.0", + "fs-extra": "^11.1.1", "lodash": "^4.17.21", "mime-types": "^2.1.35", "mobx": "^6.7.0" @@ -52,6 +53,7 @@ "devDependencies": { "@babel/core": "*", "@babel/preset-env": "*", + "@types/fs-extra": "^11.0.1", "@types/lodash": "^4.14.189", "@types/mime-types": "^2.1.1", "@types/node": "18", @@ -107,7 +109,8 @@ "NSCameraUsageDescription": "Standard Notes requires access to your camera to enable the Moments feature." }, "asarUnpack": [ - "node_modules/keytar" + "node_modules/keytar", + "node_modules/@standardnotes/home-server" ], "target": [ "dmg", diff --git a/packages/files/src/Domain/Device/FileBackupsDevice.ts b/packages/files/src/Domain/Device/FileBackupsDevice.ts index 70b4eaf80..a66e73962 100644 --- a/packages/files/src/Domain/Device/FileBackupsDevice.ts +++ b/packages/files/src/Domain/Device/FileBackupsDevice.ts @@ -17,21 +17,8 @@ export interface FileBackupsDevice LegacyBackupsMethods, PlaintextBackupsMethods, TextBackupsMethods { - openLocation(path: string): Promise - joinPaths(...paths: string[]): Promise - /** - * The reason we combine presenting a directory picker and transfering old files to the new location - * in one function is so we don't have to expose a general `transferDirectories` function to the web app, - * which would give it too much power. - * @param appendPath The path to append to the selected directory. - */ - presentDirectoryPickerForLocationChangeAndTransferOld( - appendPath: string, - oldLocation?: string, - ): Promise - monitorPlaintextBackupsLocationForChanges(backupsDirectory: string): Promise } diff --git a/packages/files/src/Domain/Directory/DirectoryManagerInterface.ts b/packages/files/src/Domain/Directory/DirectoryManagerInterface.ts new file mode 100644 index 000000000..7874bfe06 --- /dev/null +++ b/packages/files/src/Domain/Directory/DirectoryManagerInterface.ts @@ -0,0 +1,17 @@ +export interface DirectoryManagerInterface { + /** + * The reason we combine presenting a directory picker and transfering old files to the new location + * in one function is so we don't have to expose a general `transferDirectories` function to the web app, + * which would give it too much power. + * + * @param appendPath The path to append to the selected directory. + */ + presentDirectoryPickerForLocationChangeAndTransferOld( + appendPath: string, + oldLocation?: string, + ): Promise + + openLocation(path: string): Promise + + getDirectoryManagerLastErrorMessage(): Promise +} diff --git a/packages/files/src/Domain/index.ts b/packages/files/src/Domain/index.ts index 07f8bcce6..f47457ac6 100644 --- a/packages/files/src/Domain/index.ts +++ b/packages/files/src/Domain/index.ts @@ -17,6 +17,7 @@ export * from './Device/FileBackupMetadataFile' export * from './Device/FileBackupsConstantsV1' export * from './Device/FileBackupsDevice' export * from './Device/FileBackupsMapping' +export * from './Directory/DirectoryManagerInterface' export * from './Operations/DownloadAndDecrypt' export * from './Operations/EncryptAndUpload' export * from './Service/BackupServiceInterface' diff --git a/packages/mobile/src/Lib/MobileDevice.ts b/packages/mobile/src/Lib/MobileDevice.ts index 679b3b096..c7f4cdca7 100644 --- a/packages/mobile/src/Lib/MobileDevice.ts +++ b/packages/mobile/src/Lib/MobileDevice.ts @@ -3,13 +3,16 @@ import { AppleIAPProductId, AppleIAPReceipt, ApplicationIdentifier, + ApplicationInterface, DatabaseKeysLoadChunkResponse, DatabaseLoadOptions, Environment, MobileDeviceInterface, + namespacedKey, NamespacedRootKeyInKeychain, Platform as SNPlatform, RawKeychainValue, + RawStorageKey, removeFromArray, TransferPayload, UuidString, @@ -72,6 +75,19 @@ export class MobileDevice implements MobileDeviceInterface { private colorSchemeService?: ColorSchemeObserverService, ) {} + async removeRawStorageValuesForIdentifier(identifier: string): Promise { + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.SnjsVersion)) + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.StorageObject)) + } + + setApplication(_application: ApplicationInterface): void { + throw new Error('Method not implemented.') + } + + removeApplication(_application: ApplicationInterface): void { + throw new Error('Method not implemented.') + } + async authenticateWithU2F(authenticationOptionsJSONString: string): Promise | null> { const { Fido2ApiModule } = NativeModules diff --git a/packages/services/package.json b/packages/services/package.json index 3b9291f21..283055a01 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@types/jest": "^29.2.3", + "@types/node": "^20.2.5", "@typescript-eslint/eslint-plugin": "*", "@typescript-eslint/parser": "*", "eslint": "^8.27.0", diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts index 35b83e41b..9cc18aeef 100644 --- a/packages/services/src/Domain/Application/ApplicationInterface.ts +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -30,6 +30,7 @@ import { DeinitMode } from './DeinitMode' import { DeinitSource } from './DeinitSource' import { UserClientInterface } from '../User/UserClientInterface' import { SessionsClientInterface } from '../Session/SessionsClientInterface' +import { HomeServerServiceInterface } from '../HomeServer/HomeServerServiceInterface' import { User } from '@standardnotes/responses' export interface ApplicationInterface { @@ -61,6 +62,9 @@ export interface ApplicationInterface { getUser(): User | undefined hasAccount(): boolean + setCustomHost(host: string): Promise + isThirdPartyHostUsed(): boolean + isUsingHomeServer(): Promise importData(data: BackupFile, awaitSync?: boolean): Promise /** @@ -94,6 +98,7 @@ export interface ApplicationInterface { get subscriptions(): SubscriptionClientInterface get fileBackups(): BackupServiceInterface | undefined get sessions(): SessionsClientInterface + get homeServer(): HomeServerServiceInterface | undefined get vaults(): VaultServiceInterface get challenges(): ChallengeServiceInterface get alerts(): AlertService diff --git a/packages/services/src/Domain/Backups/BackupService.spec.ts b/packages/services/src/Domain/Backups/BackupService.spec.ts index 892742077..c9ef4ee3e 100644 --- a/packages/services/src/Domain/Backups/BackupService.spec.ts +++ b/packages/services/src/Domain/Backups/BackupService.spec.ts @@ -11,7 +11,7 @@ import { InternalEventBusInterface } from '..' import { AlertService } from '../Alert/AlertService' import { ApiServiceInterface } from '../Api/ApiServiceInterface' import { SyncServiceInterface } from '../Sync/SyncServiceInterface' -import { FileBackupsDevice } from '@standardnotes/files' +import { DirectoryManagerInterface, FileBackupsDevice } from '@standardnotes/files' describe('backup service', () => { let apiService: ApiServiceInterface @@ -23,7 +23,7 @@ describe('backup service', () => { let encryptor: EncryptionProviderInterface let internalEventBus: InternalEventBusInterface let backupService: FilesBackupService - let device: FileBackupsDevice + let device: FileBackupsDevice & DirectoryManagerInterface let session: SessionsClientInterface let storage: StorageServiceInterface let payloads: PayloadManagerInterface @@ -42,7 +42,7 @@ describe('backup service', () => { status = {} as jest.Mocked - device = {} as jest.Mocked + device = {} as jest.Mocked device.getFileBackupReadToken = jest.fn() device.readNextChunk = jest.fn() device.joinPaths = jest.fn() @@ -80,6 +80,7 @@ describe('backup service', () => { session, payloads, history, + device, internalEventBus, ) backupService.getFilesBackupsLocation = jest.fn().mockReturnValue('/') diff --git a/packages/services/src/Domain/Backups/BackupService.ts b/packages/services/src/Domain/Backups/BackupService.ts index a19695159..80ae57da9 100644 --- a/packages/services/src/Domain/Backups/BackupService.ts +++ b/packages/services/src/Domain/Backups/BackupService.ts @@ -22,6 +22,7 @@ import { BackupServiceInterface, DesktopWatchedDirectoriesChanges, SuperConverterServiceInterface, + DirectoryManagerInterface, } from '@standardnotes/files' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ItemManagerInterface } from '../Item/ItemManagerInterface' @@ -59,6 +60,7 @@ export class FilesBackupService extends AbstractService implements BackupService private session: SessionsClientInterface, private payloads: PayloadManagerInterface, private history: HistoryServiceInterface, + private directory: DirectoryManagerInterface, protected override internalEventBus: InternalEventBusInterface, ) { super(internalEventBus) @@ -161,15 +163,15 @@ export class FilesBackupService extends AbstractService implements BackupService const textBackupsLocation = this.getTextBackupsLocation() if (fileBackupsLocation) { - void this.device.openLocation(fileBackupsLocation) + void this.directory.openLocation(fileBackupsLocation) } if (plaintextBackupsLocation) { - void this.device.openLocation(plaintextBackupsLocation) + void this.directory.openLocation(plaintextBackupsLocation) } if (textBackupsLocation) { - void this.device.openLocation(textBackupsLocation) + void this.directory.openLocation(textBackupsLocation) } } @@ -194,7 +196,7 @@ export class FilesBackupService extends AbstractService implements BackupService async enableTextBackups(): Promise { let location = this.getTextBackupsLocation() if (!location) { - location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + location = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(TextBackupsDirectoryName), ) if (!location) { @@ -217,13 +219,13 @@ export class FilesBackupService extends AbstractService implements BackupService async openTextBackupsLocation(): Promise { const location = this.getTextBackupsLocation() if (location) { - void this.device.openLocation(location) + void this.directory.openLocation(location) } } async changeTextBackupsLocation(): Promise { const oldLocation = this.getTextBackupsLocation() - const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + const newLocation = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(TextBackupsDirectoryName), oldLocation, ) @@ -253,7 +255,7 @@ export class FilesBackupService extends AbstractService implements BackupService public async enablePlaintextBackups(): Promise { let location = this.getPlaintextBackupsLocation() if (!location) { - location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + location = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName), ) if (!location) { @@ -279,13 +281,13 @@ export class FilesBackupService extends AbstractService implements BackupService async openPlaintextBackupsLocation(): Promise { const location = this.getPlaintextBackupsLocation() if (location) { - void this.device.openLocation(location) + void this.directory.openLocation(location) } } async changePlaintextBackupsLocation(): Promise { const oldLocation = this.getPlaintextBackupsLocation() - const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + const newLocation = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(PlaintextBackupsDirectoryName), oldLocation, ) @@ -302,7 +304,7 @@ export class FilesBackupService extends AbstractService implements BackupService public async enableFilesBackups(): Promise { let location = this.getFilesBackupsLocation() if (!location) { - location = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + location = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(FileBackupsDirectoryName), ) if (!location) { @@ -328,7 +330,7 @@ export class FilesBackupService extends AbstractService implements BackupService public async changeFilesBackupsLocation(): Promise { const oldLocation = this.getFilesBackupsLocation() - const newLocation = await this.device.presentDirectoryPickerForLocationChangeAndTransferOld( + const newLocation = await this.directory.presentDirectoryPickerForLocationChangeAndTransferOld( await this.prependWorkspacePathForPath(FileBackupsDirectoryName), oldLocation, ) @@ -344,7 +346,7 @@ export class FilesBackupService extends AbstractService implements BackupService public async openFilesBackupsLocation(): Promise { const location = this.getFilesBackupsLocation() if (location) { - void this.device.openLocation(location) + void this.directory.openLocation(location) } } @@ -389,7 +391,7 @@ export class FilesBackupService extends AbstractService implements BackupService public async openFileBackup(record: FileBackupRecord): Promise { const location = await this.getFileBackupAbsolutePath(record) - await this.device.openLocation(location) + await this.directory.openLocation(location) } private async handleChangedFiles(files: FileItem[]): Promise { diff --git a/packages/services/src/Domain/Challenge/Challenge.ts b/packages/services/src/Domain/Challenge/Challenge.ts index 31dd2d5fd..2a1f840c5 100644 --- a/packages/services/src/Domain/Challenge/Challenge.ts +++ b/packages/services/src/Domain/Challenge/Challenge.ts @@ -4,6 +4,7 @@ import { ChallengeInterface } from './ChallengeInterface' import { ChallengePrompt } from './Prompt/ChallengePrompt' import { ChallengeReason } from './Types/ChallengeReason' import { ChallengeValidation } from './Types/ChallengeValidation' +import { ChallengeValue } from './Types/ChallengeValue' /** * A challenge is a stateless description of what the client needs to provide @@ -11,6 +12,7 @@ import { ChallengeValidation } from './Types/ChallengeValidation' */ export class Challenge implements ChallengeInterface { public readonly id = Math.random() + customHandler?: (challenge: ChallengeInterface, values: ChallengeValue[]) => Promise constructor( public readonly prompts: ChallengePrompt[], @@ -18,9 +20,7 @@ export class Challenge implements ChallengeInterface { public readonly cancelable: boolean, public readonly _heading?: string, public readonly _subheading?: string, - ) { - Object.freeze(this) - } + ) {} /** Outside of the modal, this is the title of the modal itself */ get modalTitle(): string { diff --git a/packages/services/src/Domain/Challenge/ChallengeInterface.ts b/packages/services/src/Domain/Challenge/ChallengeInterface.ts index 29205d6b8..4763575da 100644 --- a/packages/services/src/Domain/Challenge/ChallengeInterface.ts +++ b/packages/services/src/Domain/Challenge/ChallengeInterface.ts @@ -1,6 +1,7 @@ import { ChallengePromptInterface } from './Prompt/ChallengePromptInterface' import { ChallengeReason } from './Types/ChallengeReason' import { ChallengeValidation } from './Types/ChallengeValidation' +import { ChallengeValue } from './Types/ChallengeValue' export interface ChallengeInterface { readonly id: number @@ -8,6 +9,8 @@ export interface ChallengeInterface { readonly reason: ChallengeReason readonly cancelable: boolean + customHandler?: (challenge: ChallengeInterface, values: ChallengeValue[]) => Promise + /** Outside of the modal, this is the title of the modal itself */ get modalTitle(): string diff --git a/packages/services/src/Domain/Device/DesktopDeviceInterface.ts b/packages/services/src/Domain/Device/DesktopDeviceInterface.ts index 17b6b0315..45dc4d484 100644 --- a/packages/services/src/Domain/Device/DesktopDeviceInterface.ts +++ b/packages/services/src/Domain/Device/DesktopDeviceInterface.ts @@ -1,5 +1,7 @@ import { Environment } from '@standardnotes/models' +import { HomeServerManagerInterface } from '../HomeServer/HomeServerManagerInterface' + import { WebClientRequiresDesktopMethods } from './DesktopWebCommunication' import { DeviceInterface } from './DeviceInterface' import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface' @@ -10,6 +12,9 @@ export function isDesktopDevice(x: DeviceInterface): x is DesktopDeviceInterface return x.environment === Environment.Desktop } -export interface DesktopDeviceInterface extends WebOrDesktopDeviceInterface, WebClientRequiresDesktopMethods { +export interface DesktopDeviceInterface + extends WebOrDesktopDeviceInterface, + WebClientRequiresDesktopMethods, + HomeServerManagerInterface { environment: Environment.Desktop } diff --git a/packages/services/src/Domain/Device/DesktopWebCommunication.ts b/packages/services/src/Domain/Device/DesktopWebCommunication.ts index 23176b55a..97348b8c0 100644 --- a/packages/services/src/Domain/Device/DesktopWebCommunication.ts +++ b/packages/services/src/Domain/Device/DesktopWebCommunication.ts @@ -1,7 +1,7 @@ import { DecryptedTransferPayload } from '@standardnotes/models' -import { DesktopWatchedDirectoriesChanges, FileBackupsDevice } from '@standardnotes/files' +import { DesktopWatchedDirectoriesChanges, DirectoryManagerInterface, FileBackupsDevice } from '@standardnotes/files' -export interface WebClientRequiresDesktopMethods extends FileBackupsDevice { +export interface WebClientRequiresDesktopMethods extends FileBackupsDevice, DirectoryManagerInterface { syncComponents(payloads: unknown[]): void onSearch(text?: string): void @@ -21,4 +21,6 @@ export interface DesktopClientRequiresWebMethods { onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise handleWatchedDirectoriesChanges(changes: DesktopWatchedDirectoriesChanges): Promise + + handleHomeServerStarted(serverUrl: string): Promise } diff --git a/packages/services/src/Domain/Device/DeviceInterface.ts b/packages/services/src/Domain/Device/DeviceInterface.ts index f4ef66e26..aa5037e61 100644 --- a/packages/services/src/Domain/Device/DeviceInterface.ts +++ b/packages/services/src/Domain/Device/DeviceInterface.ts @@ -1,3 +1,4 @@ +import { ApplicationInterface } from './../Application/ApplicationInterface' import { ApplicationIdentifier } from '@standardnotes/common' import { FullyFormedTransferPayload, @@ -31,6 +32,11 @@ export interface DeviceInterface { removeAllRawStorageValues(): Promise + removeRawStorageValuesForIdentifier(identifier: ApplicationIdentifier): Promise + + setApplication(application: ApplicationInterface): void + removeApplication(application: ApplicationInterface): void + /** * On web platforms, databased created may be new. * New databases can be because of new sessions, or if the browser deleted it. diff --git a/packages/services/src/Domain/HomeServer/HomeServerEnvironmentConfiguration.ts b/packages/services/src/Domain/HomeServer/HomeServerEnvironmentConfiguration.ts new file mode 100644 index 000000000..1a3254f2b --- /dev/null +++ b/packages/services/src/Domain/HomeServer/HomeServerEnvironmentConfiguration.ts @@ -0,0 +1,17 @@ +export interface HomeServerEnvironmentConfiguration { + jwtSecret: string + authJwtSecret: string + encryptionServerKey: string + pseudoKeyParamsKey: string + valetTokenSecret: string + port: number + logLevel?: string + databaseEngine: 'sqlite' | 'mysql' + mysqlConfiguration?: { + host: string + port: number + username: string + password: string + database: string + } +} diff --git a/packages/services/src/Domain/HomeServer/HomeServerManagerInterface.ts b/packages/services/src/Domain/HomeServer/HomeServerManagerInterface.ts new file mode 100644 index 000000000..77193e8d5 --- /dev/null +++ b/packages/services/src/Domain/HomeServer/HomeServerManagerInterface.ts @@ -0,0 +1,12 @@ +export interface HomeServerManagerInterface { + startHomeServer(): Promise + setHomeServerConfiguration(configurationJSONString: string): Promise + getHomeServerConfiguration(): Promise + setHomeServerDataLocation(location: string): Promise + stopHomeServer(): Promise + activatePremiumFeatures(username: string): Promise + getHomeServerLogs(): Promise + isHomeServerRunning(): Promise + getHomeServerUrl(): Promise + getHomeServerLastErrorMessage(): Promise +} diff --git a/packages/services/src/Domain/HomeServer/HomeServerService.ts b/packages/services/src/Domain/HomeServer/HomeServerService.ts new file mode 100644 index 000000000..aa99028e5 --- /dev/null +++ b/packages/services/src/Domain/HomeServer/HomeServerService.ts @@ -0,0 +1,171 @@ +import { Result } from '@standardnotes/domain-core' + +import { ApplicationStage } from '../Application/ApplicationStage' +import { DesktopDeviceInterface } from '../Device/DesktopDeviceInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { AbstractService } from '../Service/AbstractService' +import { RawStorageKey } from '../Storage/StorageKeys' + +import { HomeServerServiceInterface } from './HomeServerServiceInterface' +import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfiguration' +import { HomeServerStatus } from './HomeServerStatus' + +export class HomeServerService extends AbstractService implements HomeServerServiceInterface { + private readonly HOME_SERVER_DATA_DIRECTORY_NAME = '.homeserver' + + constructor( + private desktopDevice: DesktopDeviceInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { + super(internalEventBus) + } + + override deinit() { + ;(this.desktopDevice as unknown) = undefined + super.deinit() + } + + override async handleApplicationStage(stage: ApplicationStage) { + await super.handleApplicationStage(stage) + + switch (stage) { + case ApplicationStage.StorageDecrypted_09: { + await this.setHomeServerDataLocationOnDevice() + break + } + case ApplicationStage.Launched_10: { + await this.startHomeServerIfItIsEnabled() + break + } + } + } + + async getHomeServerStatus(): Promise { + const isHomeServerRunning = await this.desktopDevice.isHomeServerRunning() + + if (!isHomeServerRunning) { + return { status: 'off', errorMessage: await this.desktopDevice.getHomeServerLastErrorMessage() } + } + + return { + status: 'on', + url: await this.getHomeServerUrl(), + } + } + + async getHomeServerLogs(): Promise { + return this.desktopDevice.getHomeServerLogs() + } + + async getHomeServerUrl(): Promise { + return this.desktopDevice.getHomeServerUrl() + } + + async startHomeServer(): Promise { + return this.desktopDevice.startHomeServer() + } + + async stopHomeServer(): Promise { + return this.desktopDevice.stopHomeServer() + } + + async isHomeServerRunning(): Promise { + return this.desktopDevice.isHomeServerRunning() + } + + async activatePremiumFeatures(username: string): Promise> { + const result = await this.desktopDevice.activatePremiumFeatures(username) + + if (result !== undefined) { + return Result.fail(result) + } + + return Result.ok('Premium features activated') + } + + async setHomeServerConfiguration(config: HomeServerEnvironmentConfiguration): Promise { + await this.desktopDevice.setHomeServerConfiguration(JSON.stringify(config)) + } + + async getHomeServerConfiguration(): Promise { + const configurationJSONString = await this.desktopDevice.getHomeServerConfiguration() + if (!configurationJSONString) { + return undefined + } + + return JSON.parse(configurationJSONString) as HomeServerEnvironmentConfiguration + } + + async enableHomeServer(): Promise { + await this.desktopDevice.setRawStorageValue(RawStorageKey.HomeServerEnabled, 'true') + + await this.startHomeServer() + } + + async isHomeServerEnabled(): Promise { + const value = await this.desktopDevice.getRawStorageValue(RawStorageKey.HomeServerEnabled) + + return value === 'true' + } + + async getHomeServerDataLocation(): Promise { + return this.desktopDevice.getRawStorageValue(RawStorageKey.HomeServerDataLocation) + } + + async disableHomeServer(): Promise> { + await this.desktopDevice.setRawStorageValue(RawStorageKey.HomeServerEnabled, 'false') + + const result = await this.stopHomeServer() + if (result !== undefined) { + return Result.fail(result) + } + + return Result.ok('Home server disabled') + } + + async changeHomeServerDataLocation(): Promise> { + const oldLocation = await this.getHomeServerDataLocation() + const newLocation = await this.desktopDevice.presentDirectoryPickerForLocationChangeAndTransferOld( + this.HOME_SERVER_DATA_DIRECTORY_NAME, + oldLocation, + ) + + if (!newLocation) { + const lastErrorMessage = await this.desktopDevice.getDirectoryManagerLastErrorMessage() + + return Result.fail(lastErrorMessage ?? 'No location selected') + } + + await this.desktopDevice.setRawStorageValue(RawStorageKey.HomeServerDataLocation, newLocation) + + await this.desktopDevice.setHomeServerDataLocation(newLocation) + + return Result.ok(newLocation) + } + + async openHomeServerDataLocation(): Promise { + const location = await this.getHomeServerDataLocation() + if (location) { + void this.desktopDevice.openLocation(location) + } + } + + private async startHomeServerIfItIsEnabled(): Promise { + const homeServerIsEnabled = await this.isHomeServerEnabled() + if (homeServerIsEnabled) { + await this.startHomeServer() + } + } + + private async setHomeServerDataLocationOnDevice(): Promise { + let location = await this.getHomeServerDataLocation() + if (!location) { + const documentsDirectory = await this.desktopDevice.getUserDocumentsDirectory() + location = `${documentsDirectory}/${this.HOME_SERVER_DATA_DIRECTORY_NAME}` + } + + await this.desktopDevice.setRawStorageValue(RawStorageKey.HomeServerDataLocation, location) + + await this.desktopDevice.setHomeServerDataLocation(location) + } +} diff --git a/packages/services/src/Domain/HomeServer/HomeServerServiceInterface.ts b/packages/services/src/Domain/HomeServer/HomeServerServiceInterface.ts new file mode 100644 index 000000000..69618840f --- /dev/null +++ b/packages/services/src/Domain/HomeServer/HomeServerServiceInterface.ts @@ -0,0 +1,22 @@ +import { Result } from '@standardnotes/domain-core' + +import { HomeServerEnvironmentConfiguration } from './HomeServerEnvironmentConfiguration' +import { HomeServerStatus } from './HomeServerStatus' + +export interface HomeServerServiceInterface { + activatePremiumFeatures(username: string): Promise> + isHomeServerRunning(): Promise + isHomeServerEnabled(): Promise + getHomeServerDataLocation(): Promise + enableHomeServer(): Promise + disableHomeServer(): Promise> + startHomeServer(): Promise + stopHomeServer(): Promise + changeHomeServerDataLocation(): Promise> + openHomeServerDataLocation(): Promise + getHomeServerConfiguration(): Promise + setHomeServerConfiguration(config: HomeServerEnvironmentConfiguration): Promise + getHomeServerUrl(): Promise + getHomeServerStatus(): Promise + getHomeServerLogs(): Promise +} diff --git a/packages/services/src/Domain/HomeServer/HomeServerStatus.ts b/packages/services/src/Domain/HomeServer/HomeServerStatus.ts new file mode 100644 index 000000000..366e43bdb --- /dev/null +++ b/packages/services/src/Domain/HomeServer/HomeServerStatus.ts @@ -0,0 +1,5 @@ +export type HomeServerStatus = { + status: 'on' | 'off' + url?: string + errorMessage?: string +} diff --git a/packages/services/src/Domain/Service/AbstractService.ts b/packages/services/src/Domain/Service/AbstractService.ts index a6c0aa7f3..266cf9b6b 100644 --- a/packages/services/src/Domain/Service/AbstractService.ts +++ b/packages/services/src/Domain/Service/AbstractService.ts @@ -2,14 +2,14 @@ import { log, removeFromArray } from '@standardnotes/utils' import { EventObserver } from '../Event/EventObserver' -import { ServiceInterface } from './ServiceInterface' +import { ApplicationServiceInterface } from './ApplicationServiceInterface' import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' import { ApplicationStage } from '../Application/ApplicationStage' import { InternalEventPublishStrategy } from '../Internal/InternalEventPublishStrategy' import { DiagnosticInfo } from '../Diagnostics/ServiceDiagnostics' export abstract class AbstractService - implements ServiceInterface + implements ApplicationServiceInterface { private eventObservers: EventObserver[] = [] public loggingEnabled = false diff --git a/packages/services/src/Domain/Service/ServiceInterface.ts b/packages/services/src/Domain/Service/ApplicationServiceInterface.ts similarity index 84% rename from packages/services/src/Domain/Service/ServiceInterface.ts rename to packages/services/src/Domain/Service/ApplicationServiceInterface.ts index 20fac94ba..91f4658ea 100644 --- a/packages/services/src/Domain/Service/ServiceInterface.ts +++ b/packages/services/src/Domain/Service/ApplicationServiceInterface.ts @@ -2,7 +2,7 @@ import { ApplicationStage } from '../Application/ApplicationStage' import { ServiceDiagnostics } from '../Diagnostics/ServiceDiagnostics' import { EventObserver } from '../Event/EventObserver' -export interface ServiceInterface extends ServiceDiagnostics { +export interface ApplicationServiceInterface extends ServiceDiagnostics { loggingEnabled: boolean addEventObserver(observer: EventObserver): () => void blockDeinit(): Promise diff --git a/packages/services/src/Domain/Session/SessionsClientInterface.ts b/packages/services/src/Domain/Session/SessionsClientInterface.ts index fb289bb6f..06b8bde83 100644 --- a/packages/services/src/Domain/Session/SessionsClientInterface.ts +++ b/packages/services/src/Domain/Session/SessionsClientInterface.ts @@ -12,6 +12,7 @@ export interface SessionsClientInterface { populateSessionFromDemoShareToken(token: Base64String): Promise getUser(): User | undefined + isSignedIn(): boolean get userUuid(): string getSureUser(): User @@ -24,7 +25,6 @@ export interface SessionsClientInterface { ephemeral: boolean, minAllowedVersion?: ProtocolVersion, ): Promise - isSignedIn(): boolean bypassChecksAndSignInWithRootKey( email: string, rootKey: RootKeyInterface, diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts index a064d1f03..a923aaa10 100644 --- a/packages/services/src/Domain/Storage/StorageKeys.ts +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -6,6 +6,8 @@ export enum RawStorageKey { StorageObject = 'storage', DescriptorRecord = 'descriptors', SnjsVersion = 'snjs_version', + HomeServerEnabled = 'home_server_enabled', + HomeServerDataLocation = 'home_serve_data_location', } /** diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts index 325fa4bd7..d27275a21 100644 --- a/packages/services/src/Domain/index.ts +++ b/packages/services/src/Domain/index.ts @@ -51,7 +51,9 @@ export * from './Device/DeviceInterface' export * from './Device/MobileDeviceInterface' export * from './Device/TypeCheck' export * from './Device/WebOrDesktopDeviceInterface' - +export * from './Device/DatabaseLoadOptions' +export * from './Device/DatabaseItemMetadata' +export * from './Device/DatabaseLoadSorter' export * from './Diagnostics/ServiceDiagnostics' export * from './Encryption/DecryptBackupFileUseCase' @@ -77,7 +79,11 @@ export * from './Feature/SetOfflineFeaturesFunctionResponse' export * from './Files/FileService' export * from './History/HistoryServiceInterface' - +export * from './HomeServer/HomeServerEnvironmentConfiguration' +export * from './HomeServer/HomeServerManagerInterface' +export * from './HomeServer/HomeServerService' +export * from './HomeServer/HomeServerServiceInterface' +export * from './HomeServer/HomeServerStatus' export * from './Integrity/IntegrityApiInterface' export * from './Integrity/IntegrityEvent' export * from './Integrity/IntegrityEventPayload' @@ -114,8 +120,7 @@ export * from './Revision/RevisionClientInterface' export * from './Revision/RevisionManager' export * from './Service/AbstractService' -export * from './Service/ServiceInterface' - +export * from './Service/ApplicationServiceInterface' export * from './Session/SessionManagerResponse' export * from './Session/SessionsClientInterface' export * from './Session/SessionEvent' diff --git a/packages/snjs/lib/Application/Application.ts b/packages/snjs/lib/Application/Application.ts index c8f4dcda0..b957cfd38 100644 --- a/packages/snjs/lib/Application/Application.ts +++ b/packages/snjs/lib/Application/Application.ts @@ -70,7 +70,12 @@ import { RevisionManager, ApiServiceEvent, } from '@standardnotes/services' -import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files' +import { + BackupServiceInterface, + DirectoryManagerInterface, + FileBackupsDevice, + FilesClientInterface, +} from '@standardnotes/files' import { ComputePrivateUsername } from '@standardnotes/encryption' import { useBoolean } from '@standardnotes/utils' import { @@ -133,14 +138,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli */ private deprecatedHttpService!: InternalServices.DeprecatedHttpService private declare httpService: HttpServiceInterface - private payloadManager!: InternalServices.PayloadManager + public payloadManager!: InternalServices.PayloadManager public protocolService!: EncryptionService private diskStorageService!: InternalServices.DiskStorageService private inMemoryStore!: ExternalServices.KeyValueStoreInterface /** * @deprecated will be fully replaced by @standardnotes/api services */ - private apiService!: InternalServices.SNApiService + public apiService!: InternalServices.SNApiService private declare userApiService: UserApiServiceInterface private declare userServer: UserServerInterface private declare userRequestServer: UserRequestServerInterface @@ -152,7 +157,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private sessionManager!: InternalServices.SNSessionManager private syncService!: InternalServices.SNSyncService - private challengeService!: InternalServices.ChallengeService + public challengeService!: InternalServices.ChallengeService public singletonManager!: InternalServices.SNSingletonManager public componentManagerService!: InternalServices.SNComponentManager public protectionService!: InternalServices.SNProtectionService @@ -184,6 +189,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private declare authenticatorManager: AuthenticatorClientInterface private declare authManager: AuthClientInterface private declare revisionManager: RevisionClientInterface + private homeServerService?: ExternalServices.HomeServerService private declare _signInWithRecoveryCodes: SignInWithRecoveryCodes private declare _getRecoveryCodes: GetRecoveryCodes @@ -200,7 +206,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli private eventHandlers: ApplicationObserver[] = [] // eslint-disable-next-line @typescript-eslint/no-explicit-any - private services: ExternalServices.ServiceInterface[] = [] + private services: ExternalServices.ApplicationServiceInterface[] = [] private streamRemovers: ObserverRemover[] = [] private serviceObservers: ObserverRemover[] = [] private managedSubscribers: ObserverRemover[] = [] @@ -381,6 +387,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.challengeService } + get homeServer(): ExternalServices.HomeServerServiceInterface | undefined { + return this.homeServerService + } + public get vaults(): ExternalServices.VaultServiceInterface { return this.vaultService } @@ -712,7 +722,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli await this.apiService.setHost(host) } - public getHost(): string | undefined { + public getHost(): string { return this.apiService.getHost() } @@ -1221,6 +1231,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli return this.apiService.isThirdPartyHostUsed() } + async isUsingHomeServer(): Promise { + if (!this.homeServerService) { + return false + } + + return this.getHost() === (await this.homeServerService.getHomeServerUrl()) + } + private constructServices() { this.createMappers() this.createPayloadManager() @@ -1264,6 +1282,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.createStatusService() if (isDesktopDevice(this.deviceInterface)) { this.createFilesBackupService(this.deviceInterface) + this.createHomeServerService(this.deviceInterface) } this.createMigrationService() this.createFileService() @@ -1328,6 +1347,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli ;(this.authenticatorManager as unknown) = undefined ;(this.authManager as unknown) = undefined ;(this.revisionManager as unknown) = undefined + ;(this.homeServerService as unknown) = undefined ;(this._signInWithRecoveryCodes as unknown) = undefined ;(this._getRecoveryCodes as unknown) = undefined ;(this._addAuthenticator as unknown) = undefined @@ -1689,6 +1709,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.services.push(this.diskStorageService) } + private createHomeServerService(device: ExternalServices.DesktopDeviceInterface) { + this.homeServerService = new ExternalServices.HomeServerService(device, this.internalEventBus) + + this.services.push(this.homeServerService) + } + private createInMemoryStorageManager() { this.inMemoryStore = new ExternalServices.InMemoryStore() } @@ -1928,13 +1954,14 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli this.itemManager, this.apiService, this.protocolService, - device, + device as FileBackupsDevice, this.statusService, this.options.crypto, this.storage, this.sessions, this.payloadManager, this.historyManager, + device as DirectoryManagerInterface, this.internalEventBus, ) this.services.push(this.filesBackupService) diff --git a/packages/snjs/lib/Logging.ts b/packages/snjs/lib/Logging.ts index b19375a09..0d01fd914 100644 --- a/packages/snjs/lib/Logging.ts +++ b/packages/snjs/lib/Logging.ts @@ -5,11 +5,13 @@ export const isDev = true export enum LoggingDomain { DatabaseLoad, Sync, + AccountMigration, } const LoggingStatus: Record = { [LoggingDomain.DatabaseLoad]: false, [LoggingDomain.Sync]: false, + [LoggingDomain.AccountMigration]: true, } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/snjs/lib/Services/Challenge/ChallengeService.ts b/packages/snjs/lib/Services/Challenge/ChallengeService.ts index 45f841f4d..675a39270 100644 --- a/packages/snjs/lib/Services/Challenge/ChallengeService.ts +++ b/packages/snjs/lib/Services/Challenge/ChallengeService.ts @@ -254,6 +254,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic private deleteChallengeOperation(operation: ChallengeOperation) { const challenge = operation.challenge + challenge.customHandler = undefined operation.deinit() delete this.challengeOperations[challenge.id] diff --git a/packages/snjs/lib/Services/Settings/SettingsList.ts b/packages/snjs/lib/Services/Settings/SettingsList.ts index 69c9062cc..94ddef44a 100644 --- a/packages/snjs/lib/Services/Settings/SettingsList.ts +++ b/packages/snjs/lib/Services/Settings/SettingsList.ts @@ -1,7 +1,6 @@ import { SettingData } from '@standardnotes/responses' import { MuteSignInEmailsOption, - MuteFailedCloudBackupsEmailsOption, MuteFailedBackupsEmailsOption, EmailBackupFrequency, ListedAuthorSecretsData, @@ -15,7 +14,6 @@ type SettingType = | ListedAuthorSecretsData | LogSessionUserAgentOption | MuteFailedBackupsEmailsOption - | MuteFailedCloudBackupsEmailsOption | MuteSignInEmailsOption | MuteMarketingEmailsOption diff --git a/packages/snjs/lib/Services/Storage/DiskStorageService.ts b/packages/snjs/lib/Services/Storage/DiskStorageService.ts index 8d386cdb9..02e413dd3 100644 --- a/packages/snjs/lib/Services/Storage/DiskStorageService.ts +++ b/packages/snjs/lib/Services/Storage/DiskStorageService.ts @@ -75,7 +75,9 @@ export class DiskStorageService extends Services.AbstractService implements Serv this.persistencePolicy = persistencePolicy if (this.persistencePolicy === Services.StoragePersistencePolicies.Ephemeral) { - await this.deviceInterface.removeAllRawStorageValues() + await this.deviceInterface.clearNamespacedKeychainValue(this.identifier) + await this.deviceInterface.removeAllDatabaseEntries(this.identifier) + await this.deviceInterface.removeRawStorageValuesForIdentifier(this.identifier) await this.clearAllPayloads() } } diff --git a/packages/snjs/lib/Services/Sync/SyncClientInterface.ts b/packages/snjs/lib/Services/Sync/SyncClientInterface.ts index cc5515eae..775b3cfed 100644 --- a/packages/snjs/lib/Services/Sync/SyncClientInterface.ts +++ b/packages/snjs/lib/Services/Sync/SyncClientInterface.ts @@ -1,16 +1,14 @@ import { SyncOpStatus } from './SyncOpStatus' -import { SyncOptions } from '@standardnotes/services' +import { AbstractService, SyncEvent, SyncOptions } from '@standardnotes/services' -export interface SyncClientInterface { +export interface SyncClientInterface extends AbstractService { setLaunchPriorityUuids(launchPriorityUuids: string[]): void - sync(options?: Partial): Promise - isOutOfSync(): boolean - getLastSyncDate(): Date | undefined - getSyncStatus(): SyncOpStatus + lockSyncing(): void + unlockSyncing(): void completedOnlineDownloadFirstSync: boolean } diff --git a/packages/snjs/mocha/lib/web_device_interface.js b/packages/snjs/mocha/lib/web_device_interface.js index 9be392c8d..077e7fb19 100644 --- a/packages/snjs/mocha/lib/web_device_interface.js +++ b/packages/snjs/mocha/lib/web_device_interface.js @@ -172,6 +172,11 @@ export default class WebDeviceInterface { localStorage.removeItem(KEYCHAIN_STORAGE_KEY) } + async removeRawStorageValuesForIdentifier(identifier) { + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.SnjsVersion)) + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.StorageObject)) + } + performSoftReset() {} performHardReset() {} diff --git a/packages/snjs/mocha/storage.test.js b/packages/snjs/mocha/storage.test.js index 9cd3b091d..1a02ea5b3 100644 --- a/packages/snjs/mocha/storage.test.js +++ b/packages/snjs/mocha/storage.test.js @@ -89,7 +89,10 @@ describe('storage manager', function () { const key = 'foo' const value = 'bar' await this.application.diskStorageService.setValueAndAwaitPersist(key, value) - expect(Object.keys(localStorage).length).to.equal(0) + + const expectedKeys = ['keychain'] + expect(Object.keys(localStorage).length).to.equal(expectedKeys.length) + const retrievedValue = await this.application.diskStorageService.getValue(key) expect(retrievedValue).to.equal(value) }) diff --git a/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts b/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts index 4f97f56d7..1a486bdec 100644 --- a/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts +++ b/packages/ui-services/src/Abstract/AbstractUIServiceInterface.ts @@ -1,7 +1,7 @@ -import { ApplicationEvent, ServiceInterface } from '@standardnotes/services' +import { ApplicationEvent, ApplicationServiceInterface } from '@standardnotes/services' export interface AbstractUIServiceInterface - extends ServiceInterface { + extends ApplicationServiceInterface { onAppStart(): Promise onAppEvent(event: ApplicationEvent): Promise } diff --git a/packages/ui-services/src/Preferences/PreferenceId.ts b/packages/ui-services/src/Preferences/PreferenceId.ts index 07933f60a..11ad04a22 100644 --- a/packages/ui-services/src/Preferences/PreferenceId.ts +++ b/packages/ui-services/src/Preferences/PreferenceId.ts @@ -2,6 +2,7 @@ const PREFERENCE_IDS = [ 'general', 'account', 'security', + 'home-server', 'vaults', 'appearance', 'backups', diff --git a/packages/web/src/javascripts/Application/Device/DesktopManager.ts b/packages/web/src/javascripts/Application/Device/DesktopManager.ts index d4027c099..66e92e471 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopManager.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopManager.ts @@ -48,6 +48,13 @@ export class DesktopManager void this.backups.importWatchedDirectoryChanges(changes) } + async handleHomeServerStarted(serverUrl: string): Promise { + const userIsSignedIn = this.application.sessions.isSignedIn() + if (!userIsSignedIn) { + await this.application.setCustomHost(serverUrl) + } + } + beginTextBackupsTimer() { if (this.textBackupsInterval) { clearInterval(this.textBackupsInterval) diff --git a/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts b/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts index a290d8c3c..0fcf0c15f 100644 --- a/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts +++ b/packages/web/src/javascripts/Application/Device/DesktopSnjsExports.ts @@ -10,7 +10,11 @@ export { FileBackupReadToken, FileBackupReadChunkResponse, FileDownloadProgress, + HomeServerManagerInterface, + HomeServerStatus, PlaintextBackupsMapping, DesktopWatchedDirectoriesChanges, DesktopWatchedDirectoriesChange, + HomeServerEnvironmentConfiguration, + DirectoryManagerInterface, } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts index dc1699f88..47aa4f6c9 100644 --- a/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts +++ b/packages/web/src/javascripts/Application/Device/WebOrDesktopDevice.ts @@ -12,6 +12,9 @@ import { GetSortedPayloadsByPriority, DatabaseFullEntryLoadChunk, DatabaseFullEntryLoadChunkResponse, + ApplicationInterface, + namespacedKey, + RawStorageKey, } from '@standardnotes/snjs' import { Database } from '../Database' @@ -30,6 +33,12 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface this.databases.push(database) } + removeApplication(application: ApplicationInterface): void { + const database = this.databaseForIdentifier(application.identifier) + database.deinit() + this.databases = this.databases.filter((db) => db !== database) + } + public async getJsonParsedRawStorageValue(key: string): Promise { const value = await this.getRawStorageValue(key) if (value == undefined) { @@ -87,6 +96,11 @@ export abstract class WebOrDesktopDevice implements WebOrDesktopDeviceInterface localStorage.clear() } + async removeRawStorageValuesForIdentifier(identifier: ApplicationIdentifier) { + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.SnjsVersion)) + await this.removeRawStorageValue(namespacedKey(identifier, RawStorageKey.StorageObject)) + } + async openDatabase(identifier: ApplicationIdentifier) { this.databaseForIdentifier(identifier).unlock() return new Promise((resolve, reject) => { diff --git a/packages/web/src/javascripts/Application/WebApplication.spec.ts b/packages/web/src/javascripts/Application/WebApplication.spec.ts index a42c18041..97913ac14 100644 --- a/packages/web/src/javascripts/Application/WebApplication.spec.ts +++ b/packages/web/src/javascripts/Application/WebApplication.spec.ts @@ -24,7 +24,7 @@ describe('web application', () => { SNLog.onLog = console.log SNLog.onError = console.error - beforeEach(() => { + beforeEach(async () => { const identifier = '123' window.matchMedia = jest.fn().mockReturnValue({ matches: false, addListener: jest.fn() }) @@ -34,7 +34,7 @@ describe('web application', () => { appVersion: '1.2.3', setApplication: jest.fn(), openDatabase: jest.fn().mockReturnValue(Promise.resolve()), - getRawStorageValue: jest.fn().mockImplementation((key) => { + getRawStorageValue: jest.fn().mockImplementation(async (key) => { if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) { return '10.0.0' } @@ -49,7 +49,7 @@ describe('web application', () => { componentManager.legacyGetDefaultEditor = jest.fn() Object.defineProperty(application, 'componentManager', { value: componentManager }) - application.prepareForLaunch({ receiveChallenge: jest.fn() }) + await application.prepareForLaunch({ receiveChallenge: jest.fn() }) }) describe('geDefaultEditorIdentifier', () => { diff --git a/packages/web/src/javascripts/Application/WebApplication.ts b/packages/web/src/javascripts/Application/WebApplication.ts index 2a9d7e5e4..555bdb4c8 100644 --- a/packages/web/src/javascripts/Application/WebApplication.ts +++ b/packages/web/src/javascripts/Application/WebApplication.ts @@ -90,8 +90,12 @@ export class WebApplication extends SNApplication implements WebApplicationInter deviceInterface.environment === Environment.Mobile ? 250 : ApplicationOptionsDefaults.sleepBetweenBatches, allowMultipleSelection: deviceInterface.environment !== Environment.Mobile, allowNoteSelectionStatePersistence: deviceInterface.environment !== Environment.Mobile, - u2fAuthenticatorRegistrationPromptFunction: startRegistration, - u2fAuthenticatorVerificationPromptFunction: startAuthentication, + u2fAuthenticatorRegistrationPromptFunction: startRegistration as unknown as ( + registrationOptions: Record, + ) => Promise>, + u2fAuthenticatorVerificationPromptFunction: startAuthentication as unknown as ( + authenticationOptions: Record, + ) => Promise>, }) if (isDev) { diff --git a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx index 033a8eddc..34b3ef7a6 100644 --- a/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx +++ b/packages/web/src/javascripts/Components/ChallengeModal/ChallengeModal.tsx @@ -106,7 +106,11 @@ const ChallengeModal: FunctionComponent = ({ */ setTimeout(() => { if (valuesToProcess.length > 0) { - application.submitValuesForChallenge(challenge, valuesToProcess).catch(console.error) + if (challenge.customHandler) { + void challenge.customHandler(challenge, valuesToProcess) + } else { + application.submitValuesForChallenge(challenge, valuesToProcess).catch(console.error) + } } else { setIsProcessing(false) } diff --git a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx index 38f2fc2c2..dc3da5647 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteView.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteView.tsx @@ -34,7 +34,6 @@ import LinkedItemsButton from '../LinkedItems/LinkedItemsButton' import MobileItemsListButton from '../NoteGroupView/MobileItemsListButton' import EditingDisabledBanner from './EditingDisabledBanner' import { reloadFont } from './FontFunctions' -import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator' import NoteViewFileDropTarget from './NoteViewFileDropTarget' import { NoteViewProps } from './NoteViewProps' import { @@ -45,6 +44,7 @@ import { SuperEditorContentId } from '../SuperEditor/Constants' import { NoteViewController } from './Controller/NoteViewController' import { PlainEditor, PlainEditorInterface } from './PlainEditor/PlainEditor' import { EditorMargins, EditorMaxWidths } from '../EditorWidthSelectionModal/EditorWidths' +import NoteStatusIndicator, { NoteStatus } from './NoteStatusIndicator' import CollaborationInfoHUD from './CollaborationInfoHUD' import Button from '../Button/Button' import ModalOverlay from '../Modal/ModalOverlay' diff --git a/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx b/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx index d57a531cc..5870012f4 100644 --- a/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx +++ b/packages/web/src/javascripts/Components/Preferences/PaneSelector.tsx @@ -10,6 +10,7 @@ import Listed from './Panes/Listed/Listed' import HelpAndFeedback from './Panes/HelpFeedback' import { PreferencesProps } from './PreferencesProps' import WhatsNew from './Panes/WhatsNew/WhatsNew' +import HomeServer from './Panes/HomeServer/HomeServer' import Vaults from './Panes/Vaults/Vaults' const PaneSelector: FunctionComponent = ({ @@ -32,6 +33,8 @@ const PaneSelector: FunctionComponent case 'appearance': return + case 'home-server': + return case 'security': return ( ( - - {!application.hasAccount() ? ( - - ) : ( - <> - - - - )} - - - {application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && ( - - )} - {application.hasAccount() && } - - - -) +const AccountPreferences = ({ application, viewControllerManager }: Props) => { + const isUsingThirdPartyServer = application.isThirdPartyHostUsed() + + return ( + + {!application.hasAccount() ? ( + + ) : ( + <> + + + + )} + + + {application.hasAccount() && viewControllerManager.featuresController.entitledToFiles && ( + + )} + {application.hasAccount() && !isUsingThirdPartyServer && } + + + + ) +} export default observer(AccountPreferences) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Files.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Files.tsx index d7d102d90..a32083503 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/Account/Files.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/Account/Files.tsx @@ -21,15 +21,18 @@ const FilesSection: FunctionComponent = ({ application }) => { const filesQuotaUsed = await application.settings.getSubscriptionSetting( SettingName.create(SettingName.NAMES.FileUploadBytesUsed).getValue(), ) - const filesQuotaTotal = await application.settings.getSubscriptionSetting( - SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), - ) - if (filesQuotaUsed) { setFilesQuotaUsed(parseFloat(filesQuotaUsed)) } - if (filesQuotaTotal) { - setFilesQuotaTotal(parseFloat(filesQuotaTotal)) + + if (!application.isThirdPartyHostUsed()) { + const filesQuotaTotal = await application.settings.getSubscriptionSetting( + SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(), + ) + + if (filesQuotaTotal) { + setFilesQuotaTotal(parseFloat(filesQuotaTotal)) + } } setIsLoading(false) @@ -51,7 +54,7 @@ const FilesSection: FunctionComponent = ({ application }) => { <>
{formatSizeToReadableString(filesQuotaUsed)} of{' '} - {formatSizeToReadableString(filesQuotaTotal)} used + {application.isThirdPartyHostUsed() ? '∞' : formatSizeToReadableString(filesQuotaTotal)} used
= ({ application, viewControllerManager }) => { + const isUsingThirdPartyServer = application.isThirdPartyHostUsed() + return ( - + {!isUsingThirdPartyServer && } ) } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/OfflineSubscription.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/OfflineSubscription.tsx index 87287b2b4..31507faec 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/OfflineSubscription.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Advanced/OfflineSubscription.tsx @@ -12,9 +12,10 @@ import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' type Props = { application: WebApplication viewControllerManager: ViewControllerManager + onSuccess?: () => void } -const OfflineSubscription: FunctionComponent = ({ application }) => { +const OfflineSubscription: FunctionComponent = ({ application, onSuccess }) => { const [activationCode, setActivationCode] = useState('') const [isSuccessfullyActivated, setIsSuccessfullyActivated] = useState(false) const [isSuccessfullyRemoved, setIsSuccessfullyRemoved] = useState(false) @@ -33,14 +34,44 @@ const OfflineSubscription: FunctionComponent = ({ application }) => { const handleSubscriptionCodeSubmit = async (event: React.FormEvent) => { event.preventDefault() + const homeServer = application.homeServer + + const homeServerEnabled = homeServer && homeServer.isHomeServerEnabled() + const homeServerIsRunning = homeServerEnabled && (await homeServer.isHomeServerRunning()) + + if (homeServerEnabled) { + if (!homeServerIsRunning) { + await application.alertService.alert('Please start your home server before activating offline features.') + + return + } + + const signedInUser = application.getUser() + if (!signedInUser) { + return + } + + const serverActivationResult = await homeServer.activatePremiumFeatures(signedInUser.email) + if (serverActivationResult.isFailed()) { + await application.alertService.alert(serverActivationResult.getError()) + + return + } + } + const result = await application.features.setOfflineFeaturesCode(activationCode) if (result instanceof ClientDisplayableError) { await application.alertService.alert(result.text) - } else { - setIsSuccessfullyActivated(true) - setHasUserPreviouslyStoredCode(true) - setIsSuccessfullyRemoved(false) + + return + } + + setIsSuccessfullyActivated(true) + setHasUserPreviouslyStoredCode(true) + setIsSuccessfullyRemoved(false) + if (onSuccess) { + onSuccess() } } diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServer.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServer.tsx new file mode 100644 index 000000000..bbae01ff2 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServer.tsx @@ -0,0 +1,62 @@ +import { Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' +import PreferencesPane from '../../PreferencesComponents/PreferencesPane' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import HomeServerSettings from './HomeServerSettings' + +const HomeServer = () => { + return ( + + + + + + + + + Remote access + Accessing your home server while on the go is easy and secure with Tailscale. +
    +
  1. + Register on{' '} + + Tailscale.com + {' '} + for free. +
  2. +
  3. + Download Tailscale on this computer and complete the Tailscale setup wizard until you are presented with the + IP address of your computer. It should start with something like 100.xxx... +
  4. +
  5. Download Tailscale on your mobile device and sign into your Tailscale account.
  6. +
  7. Activate the Tailscale VPN on your mobile device.
  8. +
  9. + Open Standard Notes on your mobile device and sign into your home server by specifying the sync server URL + during sign in. The URL will be the Tailscale-based IP address of this computer, followed by the port number + of your home server. For example, if your computer Tailscale IP address is 100.112.45.106 and your home + server is running on port 3127, your sync server URL will be http://100.112.45.106:3127. +
  10. +
+
+ + + Backing up your data + + For automatic backups, you can place your server's data inside of a synced cloud folder, like Dropbox, + Tresorit, or iCloud Drive. + +
    +
  1. Change your server's data location by selecting "Change Location" in the Home Server section above.
  2. +
  3. Select a cloud drive to store your server's data in.
  4. +
  5. Restart your home server.
  6. +
+ + Your Standard Notes data is always end-to-end encrypted on disk, so your cloud provider will not be able to + read your notes or files. + +
+
+ ) +} + +export default HomeServer diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServerSettings.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServerSettings.tsx new file mode 100644 index 000000000..6adafb218 --- /dev/null +++ b/packages/web/src/javascripts/Components/Preferences/Panes/HomeServer/HomeServerSettings.tsx @@ -0,0 +1,413 @@ +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' +import Button from '@/Components/Button/Button' +import { Pill, Subtitle, Text, Title } from '@/Components/Preferences/PreferencesComponents/Content' +import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' +import { useApplication } from '@/Components/ApplicationProvider' +import EncryptionStatusItem from '../Security/EncryptionStatusItem' +import Icon from '@/Components/Icon/Icon' +import OfflineSubscription from '../General/Advanced/OfflineSubscription' +import EnvironmentConfiguration from './Settings/EnvironmentConfiguration' +import DatabaseConfiguration from './Settings/DatabaseConfiguration' +import { HomeServerEnvironmentConfiguration, HomeServerServiceInterface, classNames, sleep } from '@standardnotes/snjs' +import StatusIndicator from './Status/StatusIndicator' +import { Status } from './Status/Status' +import { PremiumFeatureIconClass, PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon' +import Switch from '@/Components/Switch/Switch' +import AccordionItem from '@/Components/Shared/AccordionItem' +import PreferencesSegment from '../../PreferencesComponents/PreferencesSegment' +import PreferencesGroup from '../../PreferencesComponents/PreferencesGroup' + +const HomeServerSettings = () => { + const SERVER_SYNTHEIC_CHANGE_DELAY = 1500 + const LOGS_REFRESH_INTERVAL = 5000 + + const application = useApplication() + const homeServerService = application.homeServer as HomeServerServiceInterface + const featuresService = application.features + const sessionsService = application.sessions + const viewControllerManager = application.getViewControllerManager() + + const logsTextarea = useRef(null) + + const [isAtBottom, setIsAtBottom] = useState(true) + const [showLogs, setShowLogs] = useState(false) + const [logs, setLogs] = useState([]) + const [status, setStatus] = useState() + const [homeServerDataLocation, setHomeServerDataLocation] = useState('') + const [isAPremiumUser, setIsAPremiumUser] = useState(false) + const [isSignedIn, setIsSignedIn] = useState(false) + const [showOfflineSubscriptionActivation, setShowOfflineSubscriptionActivation] = useState(false) + const [logsIntervalRef, setLogsIntervalRef] = useState(null) + const [homeServerConfiguration, setHomeServerConfiguration] = useState( + null, + ) + const [homeServerEnabled, setHomeServerEnabled] = useState(false) + + const refreshStatus = useCallback(async () => { + const result = await homeServerService.getHomeServerStatus() + setStatus({ + state: result.status === 'on' ? 'online' : result.errorMessage ? 'error' : 'offline', + message: result.status === 'on' ? 'Online' : result.errorMessage ? 'Offline' : 'Starting...', + description: + result.status === 'on' ? ( + <> + Accessible on local network at{' '} + + {result.url} + + + ) : ( + result.errorMessage ?? 'Your home server is offline.' + ), + }) + }, [homeServerService, setStatus]) + + const initialyLoadHomeServerConfiguration = useCallback(async () => { + if (!homeServerConfiguration) { + const homeServerConfiguration = await homeServerService.getHomeServerConfiguration() + if (homeServerConfiguration) { + setHomeServerConfiguration(homeServerConfiguration) + } + } + }, [homeServerConfiguration, homeServerService]) + + const toggleHomeServer = useCallback(async () => { + if (status?.state === 'restarting') { + return + } + + if (homeServerEnabled) { + setStatus({ state: 'restarting', message: 'Shutting down...' }) + + const result = await homeServerService.disableHomeServer() + + await sleep(SERVER_SYNTHEIC_CHANGE_DELAY) + + if (result.isFailed() && (await homeServerService.isHomeServerRunning())) { + setStatus({ state: 'error', message: result.getError() }) + + return + } + + setHomeServerEnabled(await homeServerService.isHomeServerEnabled()) + + await refreshStatus() + } else { + setStatus({ state: 'restarting', message: 'Starting...' }) + + await homeServerService.enableHomeServer() + + setHomeServerEnabled(await homeServerService.isHomeServerEnabled()) + + await sleep(SERVER_SYNTHEIC_CHANGE_DELAY) + + await refreshStatus() + + void initialyLoadHomeServerConfiguration() + } + }, [homeServerEnabled, homeServerService, status, refreshStatus, initialyLoadHomeServerConfiguration]) + + const clearLogs = useCallback( + (hideLogs = false) => { + if (logsIntervalRef !== null) { + clearInterval(logsIntervalRef) + } + if (hideLogs) { + setShowLogs(false) + } + setLogs([]) + }, + [setLogs, logsIntervalRef], + ) + + const setupLogsRefresh = useCallback(async () => { + clearLogs() + + setLogs(await homeServerService.getHomeServerLogs()) + + const interval = setInterval(async () => { + setLogs(await homeServerService.getHomeServerLogs()) + }, LOGS_REFRESH_INTERVAL) + setLogsIntervalRef(interval) + }, [homeServerService, clearLogs]) + + useEffect(() => { + async function updateHomeServerDataLocation() { + const location = await homeServerService.getHomeServerDataLocation() + if (location) { + setHomeServerDataLocation(location) + } + } + + void updateHomeServerDataLocation() + + async function updateHomeServerEnabled() { + setHomeServerEnabled(await homeServerService.isHomeServerEnabled()) + } + + void updateHomeServerEnabled() + + setIsAPremiumUser(featuresService.hasOfflineRepo()) + + setIsSignedIn(sessionsService.isSignedIn()) + + void initialyLoadHomeServerConfiguration() + + void refreshStatus() + }, [featuresService, sessionsService, homeServerService, refreshStatus, initialyLoadHomeServerConfiguration]) + + const handleHomeServerConfigurationChange = useCallback( + async (changedServerConfiguration: HomeServerEnvironmentConfiguration) => { + try { + setStatus({ state: 'restarting', message: 'Applying changes and restarting...' }) + + setHomeServerConfiguration(changedServerConfiguration) + + await homeServerService.stopHomeServer() + + await sleep(SERVER_SYNTHEIC_CHANGE_DELAY) + + await homeServerService.setHomeServerConfiguration(changedServerConfiguration) + + clearLogs(true) + + const result = await homeServerService.startHomeServer() + if (result !== undefined) { + setStatus({ state: 'error', message: result }) + } + + void refreshStatus() + } catch (error) { + setStatus({ state: 'error', message: (error as Error).message }) + } + }, + [homeServerService, setStatus, setHomeServerConfiguration, refreshStatus, clearLogs], + ) + + const changeHomeServerDataLocation = useCallback( + async (location?: string) => { + try { + await homeServerService.stopHomeServer() + + if (location === undefined) { + const oldLocation = await homeServerService.getHomeServerDataLocation() + const newLocationOrError = await homeServerService.changeHomeServerDataLocation() + if (newLocationOrError.isFailed()) { + setStatus({ + state: 'error', + message: `${newLocationOrError.getError()}. Restoring to initial location in a moment...`, + }) + + await sleep(2 * SERVER_SYNTHEIC_CHANGE_DELAY) + + await changeHomeServerDataLocation(oldLocation) + + return + } + location = newLocationOrError.getValue() + } + + setStatus({ state: 'restarting', message: 'Applying changes and restarting...' }) + + await sleep(SERVER_SYNTHEIC_CHANGE_DELAY) + + setHomeServerDataLocation(location) + + clearLogs(true) + + const result = await homeServerService.startHomeServer() + if (result !== undefined) { + setStatus({ state: 'error', message: result }) + } + + void refreshStatus() + } catch (error) { + setStatus({ state: 'error', message: (error as Error).message }) + } + }, + [homeServerService, setStatus, setHomeServerDataLocation, refreshStatus, clearLogs], + ) + + const openHomeServerDataLocation = useCallback(async () => { + try { + await homeServerService.openHomeServerDataLocation() + } catch (error) { + setStatus({ state: 'error', message: (error as Error).message }) + } + }, [homeServerService]) + + const handleShowLogs = () => { + const newValueForShowingLogs = !showLogs + + if (newValueForShowingLogs) { + void setupLogsRefresh() + } else { + if (logsIntervalRef) { + clearInterval(logsIntervalRef) + setLogsIntervalRef(null) + } + } + + setShowLogs(newValueForShowingLogs) + } + + function isTextareaScrolledToBottom(textarea: HTMLTextAreaElement) { + const { scrollHeight, scrollTop, clientHeight } = textarea + const scrolledToBottom = scrollTop + clientHeight >= scrollHeight + return scrolledToBottom + } + + useLayoutEffect(() => { + if (logsTextarea.current) { + setIsAtBottom(isTextareaScrolledToBottom(logsTextarea.current)) + } + }, [logs]) + + useEffect(() => { + const handleScroll = () => { + if (logsTextarea.current) { + setIsAtBottom(isTextareaScrolledToBottom(logsTextarea.current)) + } + } + + const textArea = logsTextarea.current + + if (textArea) { + textArea.addEventListener('scroll', handleScroll) + } + + return () => { + if (textArea) { + textArea.removeEventListener('scroll', handleScroll) + } + if (logsIntervalRef !== null) { + clearInterval(logsIntervalRef) + } + } + }, [logsIntervalRef]) + + useEffect(() => { + if (logsTextarea.current && isAtBottom) { + logsTextarea.current.scrollTop = logsTextarea.current.scrollHeight + } + }, [logs, isAtBottom]) + + return ( + <> +
+
+ Home Server + Labs +
+
+
+
+ Sync your data on a private cloud running on your home computer. +
+ +
+ {homeServerEnabled && ( +
+ + + {status?.state !== 'restarting' && ( + <> + + <> + Home server is enabled. All data is stored at: + + } + checkmark={false} + /> + +
+
+ + + + + +
+
+