diff --git a/.gitignore b/.gitignore index 411b3e349..798bf5409 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ packages/features/dist packages/encryption/dist packages/files/dist packages/models/dist +packages/services/dist **/.pnp.* **/.yarn/* diff --git a/.yarn/cache/@standardnotes-services-npm-1.13.22-e649cbd9ce-e84f4e43d4.zip b/.yarn/cache/@standardnotes-services-npm-1.13.22-e649cbd9ce-e84f4e43d4.zip deleted file mode 100644 index a4b1c8418..000000000 Binary files a/.yarn/cache/@standardnotes-services-npm-1.13.22-e649cbd9ce-e84f4e43d4.zip and /dev/null differ diff --git a/.yarn/cache/@standardnotes-services-npm-1.13.23-c7085fb4e1-7e67af13c4.zip b/.yarn/cache/@standardnotes-services-npm-1.13.23-c7085fb4e1-7e67af13c4.zip deleted file mode 100644 index c06bd562b..000000000 Binary files a/.yarn/cache/@standardnotes-services-npm-1.13.23-c7085fb4e1-7e67af13c4.zip and /dev/null differ diff --git a/packages/encryption/package.json b/packages/encryption/package.json index a88b8c82b..df4eb25ae 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -41,7 +41,7 @@ "@standardnotes/common": "^1.23.1", "@standardnotes/models": "workspace:*", "@standardnotes/responses": "^1.6.39", - "@standardnotes/services": "^1.13.23", + "@standardnotes/services": "workspace:*", "@standardnotes/sncrypto-common": "^1.9.0", "@standardnotes/utils": "^1.6.12", "reflect-metadata": "^0.1.13" diff --git a/packages/filepicker/package.json b/packages/filepicker/package.json index af189a9d8..2683c5e88 100644 --- a/packages/filepicker/package.json +++ b/packages/filepicker/package.json @@ -35,7 +35,7 @@ }, "dependencies": { "@standardnotes/common": "^1.23.1", - "@standardnotes/services": "^1.13.23", + "@standardnotes/services": "workspace:*", "@standardnotes/utils": "^1.6.12", "reflect-metadata": "^0.1.13" } diff --git a/packages/files/package.json b/packages/files/package.json index 152b1017a..662a133ea 100644 --- a/packages/files/package.json +++ b/packages/files/package.json @@ -37,7 +37,7 @@ "@standardnotes/filepicker": "workspace:*", "@standardnotes/models": "workspace:*", "@standardnotes/responses": "^1.6.39", - "@standardnotes/services": "^1.13.23", + "@standardnotes/services": "workspace:*", "@standardnotes/sncrypto-common": "^1.9.0", "@standardnotes/utils": "^1.6.12", "reflect-metadata": "^0.1.13" diff --git a/packages/services/.eslintignore b/packages/services/.eslintignore new file mode 100644 index 000000000..5a19e8ace --- /dev/null +++ b/packages/services/.eslintignore @@ -0,0 +1,3 @@ +node_modules +dist +coverage \ No newline at end of file diff --git a/packages/services/.eslintrc b/packages/services/.eslintrc new file mode 100644 index 000000000..42e723b15 --- /dev/null +++ b/packages/services/.eslintrc @@ -0,0 +1,9 @@ +{ + "extends": "../../.eslintrc", + "parserOptions": { + "project": "./linter.tsconfig.json" + }, + "rules": { + "@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }] + } +} diff --git a/packages/services/CHANGELOG.md b/packages/services/CHANGELOG.md new file mode 100644 index 000000000..8159b084c --- /dev/null +++ b/packages/services/CHANGELOG.md @@ -0,0 +1,500 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [1.13.25](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.24...@standardnotes/services@1.13.25) (2022-07-05) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.24](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.23...@standardnotes/services@1.13.24) (2022-07-04) + +### Bug Fixes + +* add missing reflect-metadata package to all packages ([ce3a5bb](https://github.com/standardnotes/snjs/commit/ce3a5bbf3f1d2276ac4abc3eec3c6a44c8c3ba9b)) + +## [1.13.23](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.22...@standardnotes/services@1.13.23) (2022-06-29) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.22](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.21...@standardnotes/services@1.13.22) (2022-06-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.21](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.20...@standardnotes/services@1.13.21) (2022-06-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.20](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.19...@standardnotes/services@1.13.20) (2022-06-22) + +### Bug Fixes + +* mobile keychain types ([#769](https://github.com/standardnotes/snjs/issues/769)) ([1fa6fb5](https://github.com/standardnotes/snjs/commit/1fa6fb57e398e60c27041b826540b6a1a6de5e91)) + +## [1.13.19](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.18...@standardnotes/services@1.13.19) (2022-06-20) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.18](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.17...@standardnotes/services@1.13.18) (2022-06-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.17](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.16...@standardnotes/services@1.13.17) (2022-06-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.16](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.15...@standardnotes/services@1.13.16) (2022-06-15) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.15](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.14...@standardnotes/services@1.13.15) (2022-06-10) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.14](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.13...@standardnotes/services@1.13.14) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.13](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.12...@standardnotes/services@1.13.13) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.12](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.11...@standardnotes/services@1.13.12) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.11](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.10...@standardnotes/services@1.13.11) (2022-06-06) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.10](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.9...@standardnotes/services@1.13.10) (2022-06-03) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.8...@standardnotes/services@1.13.9) (2022-06-02) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.7...@standardnotes/services@1.13.8) (2022-06-02) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.6...@standardnotes/services@1.13.7) (2022-06-01) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.5...@standardnotes/services@1.13.6) (2022-05-30) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.4...@standardnotes/services@1.13.5) (2022-05-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.3...@standardnotes/services@1.13.4) (2022-05-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.2...@standardnotes/services@1.13.3) (2022-05-24) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.1...@standardnotes/services@1.13.2) (2022-05-24) + +**Note:** Version bump only for package @standardnotes/services + +## [1.13.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.13.0...@standardnotes/services@1.13.1) (2022-05-22) + +**Note:** Version bump only for package @standardnotes/services + +# [1.13.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.12.2...@standardnotes/services@1.13.0) (2022-05-21) + +### Features + +* display controllers ([#743](https://github.com/standardnotes/snjs/issues/743)) ([5fadce3](https://github.com/standardnotes/snjs/commit/5fadce37f1b3f2f51b8a90c257bc666ac7710074)) + +## [1.12.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.12.1...@standardnotes/services@1.12.2) (2022-05-20) + +**Note:** Version bump only for package @standardnotes/services + +## [1.12.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.12.0...@standardnotes/services@1.12.1) (2022-05-20) + +**Note:** Version bump only for package @standardnotes/services + +# [1.12.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.9...@standardnotes/services@1.12.0) (2022-05-20) + +### Features + +* authentication with PKCE mechanism ([#719](https://github.com/standardnotes/snjs/issues/719)) ([1bc19b7](https://github.com/standardnotes/snjs/commit/1bc19b79decf83a563d1cf095ee2e56f738152d1)) + +## [1.11.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.8...@standardnotes/services@1.11.9) (2022-05-18) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.7...@standardnotes/services@1.11.8) (2022-05-17) + +### Bug Fixes + +* workspace signout all ([0ac4501](https://github.com/standardnotes/snjs/commit/0ac45019428946016ef02384b07b8190378008fc)) + +## [1.11.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.6...@standardnotes/services@1.11.7) (2022-05-17) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.5...@standardnotes/services@1.11.6) (2022-05-17) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.4...@standardnotes/services@1.11.5) (2022-05-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.3...@standardnotes/services@1.11.4) (2022-05-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.2...@standardnotes/services@1.11.3) (2022-05-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.1...@standardnotes/services@1.11.2) (2022-05-13) + +**Note:** Version bump only for package @standardnotes/services + +## [1.11.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.11.0...@standardnotes/services@1.11.1) (2022-05-12) + +**Note:** Version bump only for package @standardnotes/services + +# [1.11.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.11...@standardnotes/services@1.11.0) (2022-05-12) + +### Features + +* file desktop backups ([#731](https://github.com/standardnotes/snjs/issues/731)) ([0dbce7d](https://github.com/standardnotes/snjs/commit/0dbce7dc9712fde848445b951079c81479c8bc11)) + +## [1.10.11](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.10...@standardnotes/services@1.10.11) (2022-05-12) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.10](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.9...@standardnotes/services@1.10.10) (2022-05-12) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.8...@standardnotes/services@1.10.9) (2022-05-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.7...@standardnotes/services@1.10.8) (2022-05-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.6...@standardnotes/services@1.10.7) (2022-05-06) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.5...@standardnotes/services@1.10.6) (2022-05-06) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.4...@standardnotes/services@1.10.5) (2022-05-05) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.2...@standardnotes/services@1.10.4) (2022-05-04) + +### Bug Fixes + +* config package missing dependencies ([3dec12f](https://github.com/standardnotes/snjs/commit/3dec12fa4a83a8aed8419819eafb7c34795cb09f)) + +## [1.10.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.2...@standardnotes/services@1.10.3) (2022-05-04) + +### Bug Fixes + +* config package missing dependencies ([3dec12f](https://github.com/standardnotes/snjs/commit/3dec12fa4a83a8aed8419819eafb7c34795cb09f)) + +## [1.10.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.1...@standardnotes/services@1.10.2) (2022-05-03) + +**Note:** Version bump only for package @standardnotes/services + +## [1.10.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.10.0...@standardnotes/services@1.10.1) (2022-05-02) + +**Note:** Version bump only for package @standardnotes/services + +# [1.10.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.15...@standardnotes/services@1.10.0) (2022-04-29) + +### Features + +* service diagnostics ([#718](https://github.com/standardnotes/snjs/issues/718)) ([17cf40f](https://github.com/standardnotes/snjs/commit/17cf40f4489c8f1915b19c0318d252cf83bc050d)) + +## [1.9.15](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.14...@standardnotes/services@1.9.15) (2022-04-28) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.14](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.13...@standardnotes/services@1.9.14) (2022-04-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.13](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.12...@standardnotes/services@1.9.13) (2022-04-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.12](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.11...@standardnotes/services@1.9.12) (2022-04-27) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.11](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.10...@standardnotes/services@1.9.11) (2022-04-25) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.10](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.9...@standardnotes/services@1.9.10) (2022-04-22) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.8...@standardnotes/services@1.9.9) (2022-04-22) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.7...@standardnotes/services@1.9.8) (2022-04-21) + +### Bug Fixes + +* abort key recovery after aborted challenge ([#703](https://github.com/standardnotes/snjs/issues/703)) ([a67fb7e](https://github.com/standardnotes/snjs/commit/a67fb7e8cde41a5c9fadf545933e35d525faeaf0)) + +## [1.9.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.6...@standardnotes/services@1.9.7) (2022-04-20) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.5...@standardnotes/services@1.9.6) (2022-04-20) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.4...@standardnotes/services@1.9.5) (2022-04-19) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.3...@standardnotes/services@1.9.4) (2022-04-19) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.2...@standardnotes/services@1.9.3) (2022-04-19) + +**Note:** Version bump only for package @standardnotes/services + +## [1.9.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.1...@standardnotes/services@1.9.2) (2022-04-18) + +### Bug Fixes + +* make timestamps required in payload construction ([#695](https://github.com/standardnotes/snjs/issues/695)) ([b3326c0](https://github.com/standardnotes/snjs/commit/b3326c0a998cd9d44a76afc377f182885ef48275)) + +## [1.9.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.9.0...@standardnotes/services@1.9.1) (2022-04-15) + +**Note:** Version bump only for package @standardnotes/services + +# [1.9.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.6...@standardnotes/services@1.9.0) (2022-04-15) + +### Features + +* no merge payloads in payload manager ([#693](https://github.com/standardnotes/snjs/issues/693)) ([68a577c](https://github.com/standardnotes/snjs/commit/68a577cb887fd2d5556dc9ddec461f6ae665fcb6)) + +## [1.8.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.5...@standardnotes/services@1.8.6) (2022-04-15) + +**Note:** Version bump only for package @standardnotes/services + +## [1.8.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.4...@standardnotes/services@1.8.5) (2022-04-14) + +**Note:** Version bump only for package @standardnotes/services + +## [1.8.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.3...@standardnotes/services@1.8.4) (2022-04-13) + +**Note:** Version bump only for package @standardnotes/services + +## [1.8.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.2...@standardnotes/services@1.8.3) (2022-04-12) + +**Note:** Version bump only for package @standardnotes/services + +## [1.8.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.1...@standardnotes/services@1.8.2) (2022-04-11) + +**Note:** Version bump only for package @standardnotes/services + +## [1.8.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.8.0...@standardnotes/services@1.8.1) (2022-04-01) + +**Note:** Version bump only for package @standardnotes/services + +# [1.8.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.7.2...@standardnotes/services@1.8.0) (2022-04-01) + +### Features + +* content interfaces and model type strictness ([#685](https://github.com/standardnotes/snjs/issues/685)) ([e2450c5](https://github.com/standardnotes/snjs/commit/e2450c59e8309d7080efaa03905b2abc728d9403)) + +## [1.7.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.7.1...@standardnotes/services@1.7.2) (2022-04-01) + +**Note:** Version bump only for package @standardnotes/services + +## [1.7.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.7.0...@standardnotes/services@1.7.1) (2022-03-31) + +**Note:** Version bump only for package @standardnotes/services + +# [1.7.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.14...@standardnotes/services@1.7.0) (2022-03-31) + +### Features + +* encryption and models packages ([#679](https://github.com/standardnotes/snjs/issues/679)) ([5e03d48](https://github.com/standardnotes/snjs/commit/5e03d48aba7e3dd266117201139ab869b1f70cc9)) + +## [1.6.14](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.13...@standardnotes/services@1.6.14) (2022-03-31) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.13](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.12...@standardnotes/services@1.6.13) (2022-03-30) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.12](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.11...@standardnotes/services@1.6.12) (2022-03-25) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.11](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.10...@standardnotes/services@1.6.11) (2022-03-24) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.10](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.9...@standardnotes/services@1.6.10) (2022-03-23) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.8...@standardnotes/services@1.6.9) (2022-03-23) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.7...@standardnotes/services@1.6.8) (2022-03-22) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.6...@standardnotes/services@1.6.7) (2022-03-21) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.5...@standardnotes/services@1.6.6) (2022-03-21) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.4...@standardnotes/services@1.6.5) (2022-03-21) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.3...@standardnotes/services@1.6.4) (2022-03-18) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.2...@standardnotes/services@1.6.3) (2022-03-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.0...@standardnotes/services@1.6.2) (2022-03-16) + +**Note:** Version bump only for package @standardnotes/services + +## [1.6.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.6.0...@standardnotes/services@1.6.1) (2022-03-16) + +**Note:** Version bump only for package @standardnotes/services + +# [1.6.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.12...@standardnotes/services@1.6.0) (2022-03-14) + +### Features + +* move vault into applications package ([#653](https://github.com/standardnotes/snjs/issues/653)) ([3d320eb](https://github.com/standardnotes/snjs/commit/3d320eb51ac74729ab8864f1c4c4f24d8fb794d5)) + +## [1.5.12](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.11...@standardnotes/services@1.5.12) (2022-03-11) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.11](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.10...@standardnotes/services@1.5.11) (2022-03-11) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.10](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.9...@standardnotes/services@1.5.10) (2022-03-11) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.9](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.8...@standardnotes/services@1.5.9) (2022-03-10) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.8](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.7...@standardnotes/services@1.5.8) (2022-03-10) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.7](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.6...@standardnotes/services@1.5.7) (2022-03-10) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.6](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.5...@standardnotes/services@1.5.6) (2022-03-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.5](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.4...@standardnotes/services@1.5.5) (2022-03-09) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.4](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.3...@standardnotes/services@1.5.4) (2022-03-08) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.2...@standardnotes/services@1.5.3) (2022-03-08) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.1...@standardnotes/services@1.5.2) (2022-03-08) + +**Note:** Version bump only for package @standardnotes/services + +## [1.5.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.5.0...@standardnotes/services@1.5.1) (2022-03-07) + +**Note:** Version bump only for package @standardnotes/services + +# [1.5.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.4.0...@standardnotes/services@1.5.0) (2022-03-07) + +### Features + +* integrity service ([#626](https://github.com/standardnotes/snjs/issues/626)) ([c5854fb](https://github.com/standardnotes/snjs/commit/c5854fb912dbe585516eeac3dde73573586c4e67)) + +# [1.4.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.3.0...@standardnotes/services@1.4.0) (2022-03-02) + +### Features + +* inject internal event bus to services for seamless event publishing ([#624](https://github.com/standardnotes/snjs/issues/624)) ([24b1e5c](https://github.com/standardnotes/snjs/commit/24b1e5c3e5ffe3c8ff228b97e91b83cb6c4077a5)) + +# [1.3.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.2.3...@standardnotes/services@1.3.0) (2022-03-02) + +### Features + +* add internal events handling between services ([#620](https://github.com/standardnotes/snjs/issues/620)) ([d982e36](https://github.com/standardnotes/snjs/commit/d982e365eda5268b6df339e9e0fe926a4808d86f)) + +## [1.2.3](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.2.1...@standardnotes/services@1.2.3) (2022-02-28) + +### Bug Fixes + +* add pseudo change to get lerna to trigger ([41e6817](https://github.com/standardnotes/snjs/commit/41e6817bbf726b0932cdf16f58622328b9e42803)) + +## [1.2.2](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.2.1...@standardnotes/services@1.2.2) (2022-02-28) + +### Bug Fixes + +* add pseudo change to get lerna to trigger ([41e6817](https://github.com/standardnotes/snjs/commit/41e6817bbf726b0932cdf16f58622328b9e42803)) + +## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.2.0...@standardnotes/services@1.2.1) (2022-02-27) + +**Note:** Version bump only for package @standardnotes/services + +# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.1.1...@standardnotes/services@1.2.0) (2022-02-25) + +### Features + +* extract core functionalities to separate packages ([#610](https://github.com/standardnotes/snjs/issues/610)) ([801547a](https://github.com/standardnotes/snjs/commit/801547a71614ad51a92fb249eaa184ed46a44aac)) + +## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/services@1.1.0...@standardnotes/services@1.1.1) (2022-02-24) + +**Note:** Version bump only for package @standardnotes/services + +# 1.1.0 (2022-02-22) + +### Features + +* extract services package ([#605](https://github.com/standardnotes/snjs/issues/605)) ([3966b10](https://github.com/standardnotes/snjs/commit/3966b10745c10ef5bb92871abb13ceb4ea631362)) diff --git a/packages/services/jest.config.js b/packages/services/jest.config.js new file mode 100644 index 000000000..ad1ceabb0 --- /dev/null +++ b/packages/services/jest.config.js @@ -0,0 +1,11 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const base = require('../../node_modules/@standardnotes/config/src/jest.json'); + +module.exports = { + ...base, + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + } +}; diff --git a/packages/services/linter.tsconfig.json b/packages/services/linter.tsconfig.json new file mode 100644 index 000000000..c1a7d22c5 --- /dev/null +++ b/packages/services/linter.tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["dist"] +} diff --git a/packages/services/package.json b/packages/services/package.json new file mode 100644 index 000000000..659db7d68 --- /dev/null +++ b/packages/services/package.json @@ -0,0 +1,43 @@ +{ + "name": "@standardnotes/services", + "version": "1.14.0", + "engines": { + "node": ">=16.0.0 <17.0.0" + }, + "description": "Services for Standard Notes SNJS library", + "main": "dist/index.js", + "author": "Standard Notes", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "license": "AGPL-3.0-or-later", + "scripts": { + "clean": "rm -fr dist", + "prestart": "yarn clean", + "start": "tsc -p tsconfig.json --watch", + "prebuild": "yarn clean", + "build": "tsc -p tsconfig.json", + "lint": "eslint . --ext .ts", + "test:unit": "jest spec --coverage" + }, + "dependencies": { + "@standardnotes/auth": "^3.19.4", + "@standardnotes/common": "^1.23.1", + "@standardnotes/models": "workspace:*", + "@standardnotes/responses": "^1.6.39", + "@standardnotes/utils": "^1.6.12", + "reflect-metadata": "^0.1.13" + }, + "devDependencies": { + "@types/jest": "^27.4.1", + "@typescript-eslint/eslint-plugin": "^5.30.0", + "@typescript-eslint/parser": "^5.12.1", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" + } +} diff --git a/packages/services/src/Domain/Alert/AlertService.ts b/packages/services/src/Domain/Alert/AlertService.ts new file mode 100644 index 000000000..ec4b4bcdf --- /dev/null +++ b/packages/services/src/Domain/Alert/AlertService.ts @@ -0,0 +1,28 @@ +import { ClientDisplayableError } from '@standardnotes/responses' + +/* istanbul ignore file */ + +export enum ButtonType { + Info = 0, + Danger = 1, +} + +export type DismissBlockingDialog = () => void + +export abstract class AlertService { + abstract confirm( + text: string, + title?: string, + confirmButtonText?: string, + confirmButtonType?: ButtonType, + cancelButtonText?: string, + ): Promise + + abstract alert(text: string, title?: string, closeButtonText?: string): Promise + + abstract blockingDialog(text: string, title?: string): DismissBlockingDialog | Promise + + showErrorAlert(error: ClientDisplayableError): Promise { + return this.alert(error.text, error.title) + } +} diff --git a/packages/services/src/Domain/Api/ApiServiceInterface.ts b/packages/services/src/Domain/Api/ApiServiceInterface.ts new file mode 100644 index 000000000..d60e3227b --- /dev/null +++ b/packages/services/src/Domain/Api/ApiServiceInterface.ts @@ -0,0 +1,19 @@ +import { AbstractService } from '../Service/AbstractService' +import { Uuid } from '@standardnotes/common' +import { Role } from '@standardnotes/auth' +import { FilesApiInterface } from '../Files/FilesApiInterface' + +/* istanbul ignore file */ + +export enum ApiServiceEvent { + MetaReceived = 'MetaReceived', +} + +export type MetaReceivedData = { + userUuid: Uuid + userRoles: Role[] +} + +export interface ApiServiceInterface + extends AbstractService, + FilesApiInterface {} diff --git a/packages/services/src/Domain/Application/ApplicationInterface.ts b/packages/services/src/Domain/Application/ApplicationInterface.ts new file mode 100644 index 000000000..6f7875484 --- /dev/null +++ b/packages/services/src/Domain/Application/ApplicationInterface.ts @@ -0,0 +1,22 @@ +import { ApplicationIdentifier } from '@standardnotes/common' + +import { DeinitCallback } from './DeinitCallback' +import { DeinitMode } from './DeinitMode' +import { DeinitSource } from './DeinitSource' +import { UserClientInterface } from './UserClientInterface' + +export interface ApplicationInterface { + deinit(mode: DeinitMode, source: DeinitSource): void + + getDeinitMode(): DeinitMode + + get user(): UserClientInterface + + readonly identifier: ApplicationIdentifier +} + +export interface AppGroupManagedApplication extends ApplicationInterface { + onDeinit: DeinitCallback + + setOnDeinit(onDeinit: DeinitCallback): void +} diff --git a/packages/services/src/Domain/Application/ApplicationStage.ts b/packages/services/src/Domain/Application/ApplicationStage.ts new file mode 100644 index 000000000..d36a71e8e --- /dev/null +++ b/packages/services/src/Domain/Application/ApplicationStage.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ +export enum ApplicationStage { + PreparingForLaunch_0 = 0.0, + ReadyForLaunch_05 = 0.5, + StorageDecrypted_09 = 0.9, + Launched_10 = 1.0, + LoadingDatabase_11 = 1.1, + LoadedDatabase_12 = 1.2, + FullSyncCompleted_13 = 1.3, + SignedIn_30 = 3.0, +} diff --git a/packages/services/src/Domain/Application/DeinitCallback.ts b/packages/services/src/Domain/Application/DeinitCallback.ts new file mode 100644 index 000000000..b7a72abc0 --- /dev/null +++ b/packages/services/src/Domain/Application/DeinitCallback.ts @@ -0,0 +1,5 @@ +import { DeinitSource } from './DeinitSource' +import { DeinitMode } from './DeinitMode' +import { AppGroupManagedApplication } from './ApplicationInterface' + +export type DeinitCallback = (application: AppGroupManagedApplication, mode: DeinitMode, source: DeinitSource) => void diff --git a/packages/services/src/Domain/Application/DeinitMode.ts b/packages/services/src/Domain/Application/DeinitMode.ts new file mode 100644 index 000000000..7c1d7dd99 --- /dev/null +++ b/packages/services/src/Domain/Application/DeinitMode.ts @@ -0,0 +1,6 @@ +/* istanbul ignore file */ + +export enum DeinitMode { + Soft = 'Soft', + Hard = 'Hard', +} diff --git a/packages/services/src/Domain/Application/DeinitSource.ts b/packages/services/src/Domain/Application/DeinitSource.ts new file mode 100644 index 000000000..ec6acf522 --- /dev/null +++ b/packages/services/src/Domain/Application/DeinitSource.ts @@ -0,0 +1,8 @@ +/* istanbul ignore file */ + +export enum DeinitSource { + SignOut = 1, + Lock, + SwitchWorkspace, + SignOutAll, +} diff --git a/packages/services/src/Domain/Application/UserClientInterface.ts b/packages/services/src/Domain/Application/UserClientInterface.ts new file mode 100644 index 000000000..0533679c9 --- /dev/null +++ b/packages/services/src/Domain/Application/UserClientInterface.ts @@ -0,0 +1,9 @@ +import { DeinitSource } from './DeinitSource' +export interface UserClientInterface { + deleteAccount(): Promise<{ + error: boolean + message?: string + }> + + signOut(force?: boolean, source?: DeinitSource): Promise +} diff --git a/packages/services/src/Domain/Challenge/ChallengeInterface.ts b/packages/services/src/Domain/Challenge/ChallengeInterface.ts new file mode 100644 index 000000000..29205d6b8 --- /dev/null +++ b/packages/services/src/Domain/Challenge/ChallengeInterface.ts @@ -0,0 +1,21 @@ +import { ChallengePromptInterface } from './Prompt/ChallengePromptInterface' +import { ChallengeReason } from './Types/ChallengeReason' +import { ChallengeValidation } from './Types/ChallengeValidation' + +export interface ChallengeInterface { + readonly id: number + readonly prompts: ChallengePromptInterface[] + readonly reason: ChallengeReason + readonly cancelable: boolean + + /** Outside of the modal, this is the title of the modal itself */ + get modalTitle(): string + + /** Inside of the modal, this is the H1 */ + get heading(): string | undefined + + /** Inside of the modal, this is the H2 */ + get subheading(): string | undefined + + hasPromptForValidationType(type: ChallengeValidation): boolean +} diff --git a/packages/services/src/Domain/Challenge/ChallengeResponseInterface.ts b/packages/services/src/Domain/Challenge/ChallengeResponseInterface.ts new file mode 100644 index 000000000..d5793e464 --- /dev/null +++ b/packages/services/src/Domain/Challenge/ChallengeResponseInterface.ts @@ -0,0 +1,13 @@ +import { ChallengeInterface } from './ChallengeInterface' +import { ChallengeArtifacts } from './Types/ChallengeArtifacts' +import { ChallengeValidation } from './Types/ChallengeValidation' +import { ChallengeValue } from './Types/ChallengeValue' + +export interface ChallengeResponseInterface { + readonly challenge: ChallengeInterface + readonly values: ChallengeValue[] + readonly artifacts?: ChallengeArtifacts + + getValueForType(type: ChallengeValidation): ChallengeValue + getDefaultValue(): ChallengeValue +} diff --git a/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts new file mode 100644 index 000000000..6f5f5b269 --- /dev/null +++ b/packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts @@ -0,0 +1,23 @@ +import { AbstractService } from '../Service/AbstractService' +import { ChallengeInterface } from './ChallengeInterface' +import { ChallengePromptInterface } from './Prompt/ChallengePromptInterface' +import { ChallengeResponseInterface } from './ChallengeResponseInterface' +import { ChallengeReason } from './Types/ChallengeReason' + +export interface ChallengeServiceInterface extends AbstractService { + /** + * Resolves when the challenge has been completed. + * For non-validated challenges, will resolve when the first value is submitted. + */ + promptForChallengeResponse(challenge: ChallengeInterface): Promise + + createChallenge( + prompts: ChallengePromptInterface[], + reason: ChallengeReason, + cancelable: boolean, + heading?: string, + subheading?: string, + ): ChallengeInterface + + completeChallenge(challenge: ChallengeInterface): void +} diff --git a/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts b/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts new file mode 100644 index 000000000..801dbffbe --- /dev/null +++ b/packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts @@ -0,0 +1,55 @@ +import { assertUnreachable } from '@standardnotes/utils' +import { ChallengeKeyboardType } from '../Types/ChallengeKeyboardType' +import { ChallengeRawValue } from '../Types/ChallengeRawValue' +import { ChallengeValidation } from '../Types/ChallengeValidation' +import { ChallengePromptInterface } from './ChallengePromptInterface' +import { ChallengePromptTitle } from './PromptTitles' + +/* istanbul ignore file */ + +export class ChallengePrompt implements ChallengePromptInterface { + public readonly id = Math.random() + public readonly placeholder: string + public readonly title: string + public readonly validates: boolean + + constructor( + public readonly validation: ChallengeValidation, + title?: string, + placeholder?: string, + public readonly secureTextEntry = true, + public readonly keyboardType?: ChallengeKeyboardType, + public readonly initialValue?: ChallengeRawValue, + ) { + switch (this.validation) { + case ChallengeValidation.AccountPassword: + this.title = title ?? ChallengePromptTitle.AccountPassword + this.placeholder = placeholder ?? ChallengePromptTitle.AccountPassword + this.validates = true + break + case ChallengeValidation.LocalPasscode: + this.title = title ?? ChallengePromptTitle.LocalPasscode + this.placeholder = placeholder ?? ChallengePromptTitle.LocalPasscode + this.validates = true + break + case ChallengeValidation.Biometric: + this.title = title ?? ChallengePromptTitle.Biometrics + this.placeholder = placeholder ?? '' + this.validates = true + break + case ChallengeValidation.ProtectionSessionDuration: + this.title = title ?? ChallengePromptTitle.RememberFor + this.placeholder = placeholder ?? '' + this.validates = true + break + case ChallengeValidation.None: + this.title = title ?? '' + this.placeholder = placeholder ?? '' + this.validates = false + break + default: + assertUnreachable(this.validation) + } + Object.freeze(this) + } +} diff --git a/packages/services/src/Domain/Challenge/Prompt/ChallengePromptInterface.ts b/packages/services/src/Domain/Challenge/Prompt/ChallengePromptInterface.ts new file mode 100644 index 000000000..1474febdf --- /dev/null +++ b/packages/services/src/Domain/Challenge/Prompt/ChallengePromptInterface.ts @@ -0,0 +1,19 @@ +import { ChallengeKeyboardType } from '../Types/ChallengeKeyboardType' +import { ChallengeRawValue } from '../Types/ChallengeRawValue' +import { ChallengeValidation } from '../Types/ChallengeValidation' + +/** + * A Challenge can have many prompts. Each prompt represents a unique input, + * such as a text field, or biometric scanner. + */ +export interface ChallengePromptInterface { + readonly id: number + readonly placeholder: string + readonly title: string + readonly validates: boolean + + readonly validation: ChallengeValidation + readonly secureTextEntry: boolean + readonly keyboardType?: ChallengeKeyboardType + readonly initialValue?: ChallengeRawValue +} diff --git a/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts b/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts new file mode 100644 index 000000000..5dd7f0cf4 --- /dev/null +++ b/packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ + +export const ChallengePromptTitle = { + AccountPassword: 'Account Password', + LocalPasscode: 'Application Passcode', + Biometrics: 'Biometrics', + RememberFor: 'Remember For', + Mfa: 'Two-factor Authentication Code', +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeArtifacts.ts b/packages/services/src/Domain/Challenge/Types/ChallengeArtifacts.ts new file mode 100644 index 000000000..27538a9e6 --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeArtifacts.ts @@ -0,0 +1,8 @@ +import { RootKeyInterface } from '@standardnotes/models' + +/* istanbul ignore file */ + +export type ChallengeArtifacts = { + wrappingKey?: RootKeyInterface + rootKey?: RootKeyInterface +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeKeyboardType.ts b/packages/services/src/Domain/Challenge/Types/ChallengeKeyboardType.ts new file mode 100644 index 000000000..3e561f3dc --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeKeyboardType.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +/** For mobile */ +export enum ChallengeKeyboardType { + Alphanumeric = 'default', + Numeric = 'numeric', +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts b/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts new file mode 100644 index 000000000..b34c6606a --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts @@ -0,0 +1,3 @@ +/* istanbul ignore file */ + +export type ChallengeRawValue = number | string | boolean diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeReason.ts b/packages/services/src/Domain/Challenge/Types/ChallengeReason.ts new file mode 100644 index 000000000..2db623e51 --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeReason.ts @@ -0,0 +1,27 @@ +/* istanbul ignore file */ + +export enum ChallengeReason { + AccessProtectedFile, + AccessProtectedNote, + AddPasscode, + ApplicationUnlock, + ChangeAutolockInterval, + ChangePasscode, + CreateDecryptedBackupWithProtectedItems, + Custom, + DecryptEncryptedFile, + DisableBiometrics, + DisableMfa, + ExportBackup, + ImportFile, + Migration, + ProtocolUpgrade, + RemovePasscode, + ResaveRootKey, + RevokeSession, + SearchProtectedNotesText, + SelectProtectedNote, + UnprotectFile, + UnprotectNote, + DeleteAccount, +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts b/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts new file mode 100644 index 000000000..3f8f32b5a --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts @@ -0,0 +1,9 @@ +/* istanbul ignore file */ + +export enum ChallengeValidation { + None = 0, + LocalPasscode = 1, + AccountPassword = 2, + Biometric = 3, + ProtectionSessionDuration = 4, +} diff --git a/packages/services/src/Domain/Challenge/Types/ChallengeValue.ts b/packages/services/src/Domain/Challenge/Types/ChallengeValue.ts new file mode 100644 index 000000000..701b32a89 --- /dev/null +++ b/packages/services/src/Domain/Challenge/Types/ChallengeValue.ts @@ -0,0 +1,13 @@ +import { ChallengePromptInterface } from '../Prompt/ChallengePromptInterface' +import { ChallengeRawValue } from './ChallengeRawValue' + +export interface ChallengeValue { + readonly prompt: ChallengePromptInterface + readonly value: ChallengeRawValue +} + +/* istanbul ignore file */ + +export function CreateChallengeValue(prompt: ChallengePromptInterface, value: ChallengeRawValue): ChallengeValue { + return { prompt, value } +} diff --git a/packages/services/src/Domain/Challenge/index.ts b/packages/services/src/Domain/Challenge/index.ts new file mode 100644 index 000000000..283e0d1a2 --- /dev/null +++ b/packages/services/src/Domain/Challenge/index.ts @@ -0,0 +1,12 @@ +export * from './ChallengeInterface' +export * from './ChallengeResponseInterface' +export * from './ChallengeServiceInterface' +export * from './Prompt/ChallengePrompt' +export * from './Prompt/ChallengePromptInterface' +export * from './Prompt/PromptTitles' +export * from './Types/ChallengeArtifacts' +export * from './Types/ChallengeKeyboardType' +export * from './Types/ChallengeRawValue' +export * from './Types/ChallengeReason' +export * from './Types/ChallengeValidation' +export * from './Types/ChallengeValue' diff --git a/packages/services/src/Domain/Device/DesktopDeviceInterface.ts b/packages/services/src/Domain/Device/DesktopDeviceInterface.ts new file mode 100644 index 000000000..315954b88 --- /dev/null +++ b/packages/services/src/Domain/Device/DesktopDeviceInterface.ts @@ -0,0 +1,14 @@ +import { WebClientRequiresDesktopMethods } from './DesktopWebCommunication' +import { DeviceInterface } from './DeviceInterface' +import { Environment } from './Environments' +import { WebOrDesktopDeviceInterface } from './WebOrDesktopDeviceInterface' + +/* istanbul ignore file */ + +export function isDesktopDevice(x: DeviceInterface): x is DesktopDeviceInterface { + return x.environment === Environment.Desktop +} + +export interface DesktopDeviceInterface extends WebOrDesktopDeviceInterface, WebClientRequiresDesktopMethods { + environment: Environment.Desktop +} diff --git a/packages/services/src/Domain/Device/DesktopWebCommunication.ts b/packages/services/src/Domain/Device/DesktopWebCommunication.ts new file mode 100644 index 000000000..b3447c20d --- /dev/null +++ b/packages/services/src/Domain/Device/DesktopWebCommunication.ts @@ -0,0 +1,38 @@ +import { DecryptedTransferPayload } from '@standardnotes/models' +import { FileBackupsDevice } from './FileBackupsDevice' + +export interface WebClientRequiresDesktopMethods extends FileBackupsDevice { + localBackupsCount(): Promise + + viewlocalBackups(): void + + deleteLocalBackups(): Promise + + syncComponents(payloads: unknown[]): void + + onMajorDataChange(): void + + onInitialDataLoad(): void + + onSearch(text?: string): void + + downloadBackup(): void | Promise + + get extensionsServerHost(): string +} + +export interface DesktopClientRequiresWebMethods { + updateAvailable(): void + + windowGainedFocus(): void + + windowLostFocus(): void + + onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise + + requestBackupFile(): Promise + + didBeginBackup(): void + + didFinishBackup(success: boolean): void +} diff --git a/packages/services/src/Domain/Device/DeviceInterface.ts b/packages/services/src/Domain/Device/DeviceInterface.ts new file mode 100644 index 000000000..b80e29a1f --- /dev/null +++ b/packages/services/src/Domain/Device/DeviceInterface.ts @@ -0,0 +1,84 @@ +import { Environment } from './Environments' +import { ApplicationIdentifier } from '@standardnotes/common' +import { + FullyFormedTransferPayload, + TransferPayload, + LegacyRawKeychainValue, + NamespacedRootKeyInKeychain, +} from '@standardnotes/models' + +/** + * Platforms must override this class to provide platform specific utilities + * and access to the migration service, such as exposing an interface to read + * raw values from the database or value storage. + */ +export interface DeviceInterface { + environment: Environment + + deinit(): void + + getRawStorageValue(key: string): Promise + + getJsonParsedRawStorageValue(key: string): Promise + + getAllRawStorageKeyValues(): Promise<{ key: string; value: unknown }[]> + + setRawStorageValue(key: string, value: string): Promise + + removeRawStorageValue(key: string): Promise + + removeAllRawStorageValues(): Promise + + /** + * On web platforms, databased created may be new. + * New databases can be because of new sessions, or if the browser deleted it. + * In this case, callers should orchestrate with the server to redownload all items + * from scratch. + * @returns { isNewDatabase } - True if the database was newly created + */ + openDatabase(identifier: ApplicationIdentifier): Promise<{ isNewDatabase?: boolean } | undefined> + + /** + * In a key/value database, this function returns just the keys. + */ + getDatabaseKeys(): Promise + + /** + * Remove all keychain and database data from device. + * @param workspaceIdentifiers An array of identifiers present during time of function call. Used in case + * caller needs to reference the identifiers. This param should not be used to selectively clear workspaces. + * @returns true for killsApplication if the clear data operation kills the application process completely. + * This tends to be the case for the desktop application. + */ + clearAllDataFromDevice(workspaceIdentifiers: ApplicationIdentifier[]): Promise<{ killsApplication: boolean }> + + getAllRawDatabasePayloads( + identifier: ApplicationIdentifier, + ): Promise + + saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise + + saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise + + removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise + + removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise + + getNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise + + setNamespacedKeychainValue(value: NamespacedRootKeyInKeychain, identifier: ApplicationIdentifier): Promise + + clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise + + setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise + + clearRawKeychainValue(): Promise + + openUrl(url: string): void + + performSoftReset(): void + + performHardReset(): void + + isDeviceDestroyed(): boolean +} diff --git a/packages/services/src/Domain/Device/Environments.ts b/packages/services/src/Domain/Device/Environments.ts new file mode 100644 index 000000000..eab8bded8 --- /dev/null +++ b/packages/services/src/Domain/Device/Environments.ts @@ -0,0 +1,16 @@ +export enum Environment { + Web = 1, + Desktop = 2, + Mobile = 3, +} + +export enum Platform { + Ios = 1, + Android = 2, + MacWeb = 3, + MacDesktop = 4, + WindowsWeb = 5, + WindowsDesktop = 6, + LinuxWeb = 7, + LinuxDesktop = 8, +} diff --git a/packages/services/src/Domain/Device/FileBackupsDevice.ts b/packages/services/src/Domain/Device/FileBackupsDevice.ts new file mode 100644 index 000000000..ab0c969cb --- /dev/null +++ b/packages/services/src/Domain/Device/FileBackupsDevice.ts @@ -0,0 +1,51 @@ +import { Uuid } from '@standardnotes/common' +import { BackupFileEncryptedContextualPayload } from '@standardnotes/models' + +/* istanbul ignore file */ + +export const FileBackupsConstantsV1 = { + Version: '1.0.0', + MetadataFileName: 'metadata.sn.json', + BinaryFileName: 'file.encrypted', +} + +export interface FileBackupMetadataFile { + info: Record + file: BackupFileEncryptedContextualPayload + itemsKey: BackupFileEncryptedContextualPayload + version: '1.0.0' +} + +export interface FileBackupsMapping { + version: typeof FileBackupsConstantsV1.Version + files: Record< + Uuid, + { + backedUpOn: Date + absolutePath: string + relativePath: string + metadataFileName: typeof FileBackupsConstantsV1.MetadataFileName + binaryFileName: typeof FileBackupsConstantsV1.BinaryFileName + version: typeof FileBackupsConstantsV1.Version + } + > +} + +export interface FileBackupsDevice { + getFilesBackupsMappingFile(): Promise + saveFilesBackupsFile( + uuid: Uuid, + metaFile: string, + downloadRequest: { + chunkSizes: number[] + valetToken: string + url: string + }, + ): Promise<'success' | 'failed'> + isFilesBackupsEnabled(): Promise + enableFilesBackups(): Promise + disableFilesBackups(): Promise + changeFilesBackupsLocation(): Promise + getFilesBackupsLocation(): Promise + openFilesBackupsLocation(): Promise +} diff --git a/packages/services/src/Domain/Device/MobileDeviceInterface.ts b/packages/services/src/Domain/Device/MobileDeviceInterface.ts new file mode 100644 index 000000000..fe26acb99 --- /dev/null +++ b/packages/services/src/Domain/Device/MobileDeviceInterface.ts @@ -0,0 +1,9 @@ +import { DeviceInterface } from './DeviceInterface' +import { Environment } from './Environments' +import { RawKeychainValue } from '@standardnotes/models' + +export interface MobileDeviceInterface extends DeviceInterface { + environment: Environment.Mobile + + getRawKeychainValue(): Promise +} diff --git a/packages/services/src/Domain/Device/TypeCheck.spec.ts b/packages/services/src/Domain/Device/TypeCheck.spec.ts new file mode 100644 index 000000000..c66c7a1be --- /dev/null +++ b/packages/services/src/Domain/Device/TypeCheck.spec.ts @@ -0,0 +1,18 @@ +import { DeviceInterface } from './DeviceInterface' +import { Environment } from './Environments' +import { MobileDeviceInterface } from './MobileDeviceInterface' +import { isMobileDevice } from './TypeCheck' + +describe('device type check', () => { + it('should return true for mobile devices', () => { + const device = { environment: Environment.Mobile } as jest.Mocked + + expect(isMobileDevice(device)).toBeTruthy() + }) + + it('should return false for non mobile devices', () => { + const device = { environment: Environment.Web } as jest.Mocked + + expect(isMobileDevice(device)).toBeFalsy() + }) +}) diff --git a/packages/services/src/Domain/Device/TypeCheck.ts b/packages/services/src/Domain/Device/TypeCheck.ts new file mode 100644 index 000000000..a67302f6e --- /dev/null +++ b/packages/services/src/Domain/Device/TypeCheck.ts @@ -0,0 +1,9 @@ +import { Environment } from './Environments' +import { MobileDeviceInterface } from './MobileDeviceInterface' +import { DeviceInterface } from './DeviceInterface' + +/* istanbul ignore file */ + +export function isMobileDevice(x: DeviceInterface): x is MobileDeviceInterface { + return x.environment === Environment.Mobile +} diff --git a/packages/services/src/Domain/Device/WebOrDesktopDeviceInterface.ts b/packages/services/src/Domain/Device/WebOrDesktopDeviceInterface.ts new file mode 100644 index 000000000..63ed04ec1 --- /dev/null +++ b/packages/services/src/Domain/Device/WebOrDesktopDeviceInterface.ts @@ -0,0 +1,10 @@ +import { DeviceInterface } from './DeviceInterface' +import { RawKeychainValue } from '@standardnotes/models' + +export interface WebOrDesktopDeviceInterface extends DeviceInterface { + readonly appVersion: string + + getKeychainValue(): Promise + + setKeychainValue(value: RawKeychainValue): Promise +} diff --git a/packages/services/src/Domain/Diagnostics/ServiceDiagnostics.ts b/packages/services/src/Domain/Diagnostics/ServiceDiagnostics.ts new file mode 100644 index 000000000..b2f30d287 --- /dev/null +++ b/packages/services/src/Domain/Diagnostics/ServiceDiagnostics.ts @@ -0,0 +1,17 @@ +type DiagnosticValue = + | string + | number + | Date + | boolean + | null + | undefined + | DiagnosticValue[] + | { [key: string]: DiagnosticValue } + +export type DiagnosticInfo = { + [key: string]: Record +} + +export interface ServiceDiagnostics { + getDiagnostics(): Promise +} diff --git a/packages/services/src/Domain/Event/EventObserver.ts b/packages/services/src/Domain/Event/EventObserver.ts new file mode 100644 index 000000000..0fa92b3ec --- /dev/null +++ b/packages/services/src/Domain/Event/EventObserver.ts @@ -0,0 +1 @@ +export type EventObserver = (eventName: E, data?: D) => Promise | void diff --git a/packages/services/src/Domain/Event/SyncEvent.ts b/packages/services/src/Domain/Event/SyncEvent.ts new file mode 100644 index 000000000..6e6ff3dc4 --- /dev/null +++ b/packages/services/src/Domain/Event/SyncEvent.ts @@ -0,0 +1,25 @@ +/* istanbul ignore file */ +export enum SyncEvent { + /** + * A potentially multi-round trip that keeps syncing until all items have been uploaded. + * However, this event will still trigger if there are more items waiting to be downloaded on the + * server + */ + SyncCompletedWithAllItemsUploaded = 'SyncCompletedWithAllItemsUploaded', + SyncCompletedWithAllItemsUploadedAndDownloaded = 'SyncCompletedWithAllItemsUploadedAndDownloaded', + SingleRoundTripSyncCompleted = 'SingleRoundTripSyncCompleted', + SyncWillBegin = 'sync:will-begin', + DownloadFirstSyncCompleted = 'sync:download-first-completed', + SyncTakingTooLong = 'sync:taking-too-long', + SyncError = 'sync:error', + InvalidSession = 'sync:invalid-session', + MajorDataChange = 'major-data-change', + LocalDataIncrementalLoad = 'local-data-incremental-load', + LocalDataLoaded = 'local-data-loaded', + EnterOutOfSync = 'enter-out-of-sync', + ExitOutOfSync = 'exit-out-of-sync', + StatusChanged = 'status-changed', + DatabaseWriteError = 'database-write-error', + DatabaseReadError = 'database-read-error', + SyncRequestsIntegrityCheck = 'sync:requests-integrity-check', +} diff --git a/packages/services/src/Domain/Event/SyncEventReceiver.ts b/packages/services/src/Domain/Event/SyncEventReceiver.ts new file mode 100644 index 000000000..ba7e3327d --- /dev/null +++ b/packages/services/src/Domain/Event/SyncEventReceiver.ts @@ -0,0 +1,3 @@ +import { SyncEvent } from './SyncEvent' + +export type SyncEventReceiver = (event: SyncEvent) => void diff --git a/packages/services/src/Domain/FileSystem/FileSystemApi.ts b/packages/services/src/Domain/FileSystem/FileSystemApi.ts new file mode 100644 index 000000000..882461f7a --- /dev/null +++ b/packages/services/src/Domain/FileSystem/FileSystemApi.ts @@ -0,0 +1,27 @@ +export interface DirectoryHandle { + nativeHandle: unknown +} +export interface FileHandleReadWrite { + nativeHandle: unknown + writableStream: unknown +} +export interface FileHandleRead { + nativeHandle: unknown +} + +export type FileSystemResult = 'aborted' | 'success' | 'failed' +export type FileSystemNoSelection = 'aborted' | 'failed' + +export interface FileSystemApi { + selectDirectory(): Promise + selectFile(): Promise + readFile( + file: FileHandleRead, + onBytes: (bytes: Uint8Array, isLast: boolean) => Promise, + ): Promise + createDirectory(parentDirectory: DirectoryHandle, name: string): Promise + createFile(directory: DirectoryHandle, name: string): Promise + saveBytes(file: FileHandleReadWrite, bytes: Uint8Array): Promise<'success' | 'failed'> + saveString(file: FileHandleReadWrite, contents: string): Promise<'success' | 'failed'> + closeFileWriteStream(file: FileHandleReadWrite): Promise<'success' | 'failed'> +} diff --git a/packages/services/src/Domain/Files/FilesApiInterface.ts b/packages/services/src/Domain/Files/FilesApiInterface.ts new file mode 100644 index 000000000..9060043b3 --- /dev/null +++ b/packages/services/src/Domain/Files/FilesApiInterface.ts @@ -0,0 +1,28 @@ +import { StartUploadSessionResponse, MinimalHttpResponse, ClientDisplayableError } from '@standardnotes/responses' +import { FileContent } from '@standardnotes/models' + +export interface FilesApiInterface { + startUploadSession(apiToken: string): Promise + + uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise + + closeUploadSession(apiToken: string): Promise + + downloadFile( + file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] }, + chunkIndex: number, + apiToken: string, + contentRangeStart: number, + onBytesReceived: (bytes: Uint8Array) => Promise, + ): Promise + + deleteFile(apiToken: string): Promise + + createFileValetToken( + remoteIdentifier: string, + operation: 'write' | 'read' | 'delete', + unencryptedFileSize?: number, + ): Promise + + getFilesDownloadUrl(): string +} diff --git a/packages/services/src/Domain/Integrity/IntegrityApiInterface.ts b/packages/services/src/Domain/Integrity/IntegrityApiInterface.ts new file mode 100644 index 000000000..3b1f46854 --- /dev/null +++ b/packages/services/src/Domain/Integrity/IntegrityApiInterface.ts @@ -0,0 +1,5 @@ +import { CheckIntegrityResponse, IntegrityPayload } from '@standardnotes/responses' + +export interface IntegrityApiInterface { + checkIntegrity(integrityPayloads: IntegrityPayload[]): Promise +} diff --git a/packages/services/src/Domain/Integrity/IntegrityEvent.ts b/packages/services/src/Domain/Integrity/IntegrityEvent.ts new file mode 100644 index 000000000..1ec64d67b --- /dev/null +++ b/packages/services/src/Domain/Integrity/IntegrityEvent.ts @@ -0,0 +1,4 @@ +/* istanbul ignore file */ +export enum IntegrityEvent { + IntegrityCheckCompleted = 'IntegrityCheckCompleted', +} diff --git a/packages/services/src/Domain/Integrity/IntegrityEventPayload.ts b/packages/services/src/Domain/Integrity/IntegrityEventPayload.ts new file mode 100644 index 000000000..059882a9d --- /dev/null +++ b/packages/services/src/Domain/Integrity/IntegrityEventPayload.ts @@ -0,0 +1,7 @@ +import { ServerItemResponse } from '@standardnotes/responses' +import { SyncSource } from '../Sync/SyncSource' + +export type IntegrityEventPayload = { + rawPayloads: ServerItemResponse[] + source: SyncSource +} diff --git a/packages/services/src/Domain/Integrity/IntegrityService.spec.ts b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts new file mode 100644 index 000000000..525da03bb --- /dev/null +++ b/packages/services/src/Domain/Integrity/IntegrityService.spec.ts @@ -0,0 +1,160 @@ +import { TransferPayload } from '@standardnotes/models' +import { SyncEvent } from '../Event/SyncEvent' + +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { ItemsServerInterface } from '../Item/ItemsServerInterface' +import { SyncSource } from '../Sync/SyncSource' +import { IntegrityApiInterface } from './IntegrityApiInterface' +import { IntegrityService } from './IntegrityService' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' +import { IntegrityPayload } from '@standardnotes/responses' + +describe('IntegrityService', () => { + let integrityApi: IntegrityApiInterface + let itemApi: ItemsServerInterface + let payloadManager: PayloadManagerInterface + let internalEventBus: InternalEventBusInterface + + const createService = () => new IntegrityService(integrityApi, itemApi, payloadManager, internalEventBus) + + beforeEach(() => { + integrityApi = {} as jest.Mocked + integrityApi.checkIntegrity = jest.fn() + + itemApi = {} as jest.Mocked + itemApi.getSingleItem = jest.fn() + + payloadManager = {} as jest.Mocked + payloadManager.integrityPayloads = [] + + internalEventBus = {} as jest.Mocked + internalEventBus.publishSync = jest.fn() + }) + + it('should check integrity of payloads and publish mismatches', async () => { + integrityApi.checkIntegrity = jest.fn().mockReturnValue({ + data: { + mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload], + }, + }) + itemApi.getSingleItem = jest.fn().mockReturnValue({ + data: { + item: { + uuid: '1-2-3', + content: 'foobar', + } as Partial, + }, + }) + + await createService().handleEvent({ + type: SyncEvent.SyncRequestsIntegrityCheck, + payload: { + integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload], + source: SyncSource.AfterDownloadFirst, + }, + }) + + expect(internalEventBus.publishSync).toHaveBeenCalledWith( + { + payload: { + rawPayloads: [ + { + content: 'foobar', + uuid: '1-2-3', + }, + ], + source: 5, + }, + type: 'IntegrityCheckCompleted', + }, + 'SEQUENCE', + ) + }) + + it('should publish empty mismatches if everything is in sync', async () => { + integrityApi.checkIntegrity = jest.fn().mockReturnValue({ + data: { + mismatches: [], + }, + }) + + await createService().handleEvent({ + type: SyncEvent.SyncRequestsIntegrityCheck, + payload: { + integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload], + source: SyncSource.AfterDownloadFirst, + }, + }) + + expect(internalEventBus.publishSync).toHaveBeenCalledWith( + { + payload: { + rawPayloads: [], + source: 5, + }, + type: 'IntegrityCheckCompleted', + }, + 'SEQUENCE', + ) + }) + + it('should not publish mismatches if checking integrity fails', async () => { + integrityApi.checkIntegrity = jest.fn().mockReturnValue({ + error: 'Ooops', + }) + + await createService().handleEvent({ + type: SyncEvent.SyncRequestsIntegrityCheck, + payload: { + integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload], + source: SyncSource.AfterDownloadFirst, + }, + }) + + expect(internalEventBus.publishSync).not.toHaveBeenCalled() + }) + + it('should publish empty mismatches if fetching items fails', async () => { + integrityApi.checkIntegrity = jest.fn().mockReturnValue({ + data: { + mismatches: [{ uuid: '1-2-3', updated_at_timestamp: 234 } as IntegrityPayload], + }, + }) + itemApi.getSingleItem = jest.fn().mockReturnValue({ + error: 'Ooops', + }) + + await createService().handleEvent({ + type: SyncEvent.SyncRequestsIntegrityCheck, + payload: { + integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload], + source: SyncSource.AfterDownloadFirst, + }, + }) + + expect(internalEventBus.publishSync).toHaveBeenCalledWith( + { + payload: { + rawPayloads: [], + source: 5, + }, + type: 'IntegrityCheckCompleted', + }, + 'SEQUENCE', + ) + }) + + it('should not handle different event types', async () => { + await createService().handleEvent({ + type: SyncEvent.SyncCompletedWithAllItemsUploaded, + payload: { + integrityPayloads: [{ uuid: '1-2-3', updated_at_timestamp: 123 } as IntegrityPayload], + source: SyncSource.AfterDownloadFirst, + }, + }) + + expect(integrityApi.checkIntegrity).not.toHaveBeenCalled() + expect(itemApi.getSingleItem).not.toHaveBeenCalled() + expect(internalEventBus.publishSync).not.toHaveBeenCalled() + }) +}) diff --git a/packages/services/src/Domain/Integrity/IntegrityService.ts b/packages/services/src/Domain/Integrity/IntegrityService.ts new file mode 100644 index 000000000..9705019b3 --- /dev/null +++ b/packages/services/src/Domain/Integrity/IntegrityService.ts @@ -0,0 +1,62 @@ +import { IntegrityEvent } from './IntegrityEvent' +import { AbstractService } from '../Service/AbstractService' +import { ItemsServerInterface } from '../Item/ItemsServerInterface' +import { IntegrityApiInterface } from './IntegrityApiInterface' +import { GetSingleItemResponse } from '@standardnotes/responses' +import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface' +import { InternalEventInterface } from '../Internal/InternalEventInterface' +import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface' +import { SyncEvent } from '../Event/SyncEvent' +import { IntegrityEventPayload } from './IntegrityEventPayload' +import { SyncSource } from '../Sync/SyncSource' +import { PayloadManagerInterface } from '../Payloads/PayloadManagerInterface' + +export class IntegrityService + extends AbstractService + implements InternalEventHandlerInterface +{ + constructor( + private integrityApi: IntegrityApiInterface, + private itemApi: ItemsServerInterface, + private payloadManager: PayloadManagerInterface, + protected override internalEventBus: InternalEventBusInterface, + ) { + super(internalEventBus) + } + + async handleEvent(event: InternalEventInterface): Promise { + if (event.type !== SyncEvent.SyncRequestsIntegrityCheck) { + return + } + + const integrityCheckResponse = await this.integrityApi.checkIntegrity(this.payloadManager.integrityPayloads) + if (integrityCheckResponse.error !== undefined) { + this.log(`Could not obtain integrity check: ${integrityCheckResponse.error}`) + + return + } + + const serverItemResponsePromises: Promise[] = [] + for (const mismatch of integrityCheckResponse.data.mismatches) { + serverItemResponsePromises.push(this.itemApi.getSingleItem(mismatch.uuid)) + } + + const serverItemResponses = await Promise.all(serverItemResponsePromises) + + const rawPayloads = [] + for (const serverItemResponse of serverItemResponses) { + if (serverItemResponse.data === undefined || serverItemResponse.error || !('item' in serverItemResponse.data)) { + this.log(`Could not obtain item for integrity adjustments: ${serverItemResponse.error}`) + + continue + } + + rawPayloads.push(serverItemResponse.data.item) + } + + await this.notifyEventSync(IntegrityEvent.IntegrityCheckCompleted, { + rawPayloads: rawPayloads, + source: (event.payload as { source: SyncSource }).source, + }) + } +} diff --git a/packages/services/src/Domain/Internal/InternalEventBus.spec.ts b/packages/services/src/Domain/Internal/InternalEventBus.spec.ts new file mode 100644 index 000000000..150fa0745 --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventBus.spec.ts @@ -0,0 +1,117 @@ +import { InternalEventHandlerInterface } from './InternalEventHandlerInterface' +import { InternalEventBus } from './InternalEventBus' +import { InternalEventPublishStrategy } from './InternalEventPublishStrategy' + +describe('InternalEventBus', () => { + let eventHandler1: InternalEventHandlerInterface + let eventHandler2: InternalEventHandlerInterface + let eventHandler3: InternalEventHandlerInterface + + const createEventBus = () => new InternalEventBus() + + beforeEach(() => { + eventHandler1 = {} as jest.Mocked + eventHandler1.handleEvent = jest.fn() + + eventHandler2 = {} as jest.Mocked + eventHandler2.handleEvent = jest.fn() + + eventHandler3 = {} as jest.Mocked + eventHandler3.handleEvent = jest.fn() + }) + + it('should trigger appropriate event handlers upon event publishing', () => { + const eventBus = createEventBus() + eventBus.addEventHandler(eventHandler1, 'test_event_1') + eventBus.addEventHandler(eventHandler2, 'test_event_2') + eventBus.addEventHandler(eventHandler1, 'test_event_3') + eventBus.addEventHandler(eventHandler3, 'test_event_2') + + eventBus.publish({ type: 'test_event_2', payload: { foo: 'bar' } }) + + expect(eventHandler1.handleEvent).not.toHaveBeenCalled() + expect(eventHandler2.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + expect(eventHandler3.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + }) + + it('should do nothing if there are no appropriate event handlers', () => { + const eventBus = createEventBus() + eventBus.addEventHandler(eventHandler1, 'test_event_1') + eventBus.addEventHandler(eventHandler2, 'test_event_2') + eventBus.addEventHandler(eventHandler1, 'test_event_3') + eventBus.addEventHandler(eventHandler3, 'test_event_2') + + eventBus.publish({ type: 'test_event_4', payload: { foo: 'bar' } }) + + expect(eventHandler1.handleEvent).not.toHaveBeenCalled() + expect(eventHandler2.handleEvent).not.toHaveBeenCalled() + expect(eventHandler3.handleEvent).not.toHaveBeenCalled() + }) + + it('should handle event synchronously in a sequential order', async () => { + const eventBus = createEventBus() + eventBus.addEventHandler(eventHandler1, 'test_event_1') + eventBus.addEventHandler(eventHandler2, 'test_event_2') + eventBus.addEventHandler(eventHandler1, 'test_event_3') + eventBus.addEventHandler(eventHandler3, 'test_event_2') + + await eventBus.publishSync({ type: 'test_event_2', payload: { foo: 'bar' } }, InternalEventPublishStrategy.SEQUENCE) + + expect(eventHandler1.handleEvent).not.toHaveBeenCalled() + expect(eventHandler2.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + expect(eventHandler3.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + }) + + it('should handle event synchronously in a random order', async () => { + const eventBus = createEventBus() + eventBus.addEventHandler(eventHandler1, 'test_event_1') + eventBus.addEventHandler(eventHandler2, 'test_event_2') + eventBus.addEventHandler(eventHandler1, 'test_event_3') + eventBus.addEventHandler(eventHandler3, 'test_event_2') + + await eventBus.publishSync({ type: 'test_event_2', payload: { foo: 'bar' } }, InternalEventPublishStrategy.ASYNC) + + expect(eventHandler1.handleEvent).not.toHaveBeenCalled() + expect(eventHandler2.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + expect(eventHandler3.handleEvent).toHaveBeenCalledWith({ + type: 'test_event_2', + payload: { foo: 'bar' }, + }) + }) + + it('should do nothing if there are no appropriate event handlers for synchronous handling', async () => { + const eventBus = createEventBus() + eventBus.addEventHandler(eventHandler1, 'test_event_1') + eventBus.addEventHandler(eventHandler2, 'test_event_2') + eventBus.addEventHandler(eventHandler1, 'test_event_3') + eventBus.addEventHandler(eventHandler3, 'test_event_2') + + await eventBus.publishSync({ type: 'test_event_4', payload: { foo: 'bar' } }, InternalEventPublishStrategy.ASYNC) + + expect(eventHandler1.handleEvent).not.toHaveBeenCalled() + expect(eventHandler2.handleEvent).not.toHaveBeenCalled() + expect(eventHandler3.handleEvent).not.toHaveBeenCalled() + }) + + it('should clear event observers on deinit', async () => { + const eventBus = createEventBus() + eventBus.deinit() + + expect(eventBus['eventHandlers']).toBeUndefined + }) +}) diff --git a/packages/services/src/Domain/Internal/InternalEventBus.ts b/packages/services/src/Domain/Internal/InternalEventBus.ts new file mode 100644 index 000000000..40c46e2a1 --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventBus.ts @@ -0,0 +1,61 @@ +import { InternalEventBusInterface } from './InternalEventBusInterface' +import { InternalEventHandlerInterface } from './InternalEventHandlerInterface' +import { InternalEventInterface } from './InternalEventInterface' +import { InternalEventPublishStrategy } from './InternalEventPublishStrategy' +import { InternalEventType } from './InternalEventType' + +export class InternalEventBus implements InternalEventBusInterface { + private eventHandlers: Map + + constructor() { + this.eventHandlers = new Map() + } + + deinit(): void { + ;(this.eventHandlers as unknown) = undefined + } + + addEventHandler(handler: InternalEventHandlerInterface, eventType: string): void { + let handlersForEventType = this.eventHandlers.get(eventType) + if (handlersForEventType === undefined) { + handlersForEventType = [] + } + + handlersForEventType.push(handler) + + this.eventHandlers.set(eventType, handlersForEventType) + } + + publish(event: InternalEventInterface): void { + const handlersForEventType = this.eventHandlers.get(event.type) + if (handlersForEventType === undefined) { + return + } + + for (const handlerForEventType of handlersForEventType) { + void handlerForEventType.handleEvent(event) + } + } + + async publishSync(event: InternalEventInterface, strategy: InternalEventPublishStrategy): Promise { + const handlersForEventType = this.eventHandlers.get(event.type) + if (handlersForEventType === undefined) { + return + } + + if (strategy === InternalEventPublishStrategy.SEQUENCE) { + for (const handlerForEventType of handlersForEventType) { + await handlerForEventType.handleEvent(event) + } + } + + if (strategy === InternalEventPublishStrategy.ASYNC) { + const handlerPromises = [] + for (const handlerForEventType of handlersForEventType) { + handlerPromises.push(handlerForEventType.handleEvent(event)) + } + + await Promise.all(handlerPromises) + } + } +} diff --git a/packages/services/src/Domain/Internal/InternalEventBusInterface.ts b/packages/services/src/Domain/Internal/InternalEventBusInterface.ts new file mode 100644 index 000000000..285efa4a6 --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventBusInterface.ts @@ -0,0 +1,28 @@ +import { InternalEventInterface } from './InternalEventInterface' +import { InternalEventType } from './InternalEventType' +import { InternalEventHandlerInterface } from './InternalEventHandlerInterface' +import { InternalEventPublishStrategy } from '..' + +export interface InternalEventBusInterface { + /** + * Associate an event handler with a certain event type + * @param handler event handler instance + * @param eventType event type to associate with + */ + addEventHandler(handler: InternalEventHandlerInterface, eventType: InternalEventType): void + /** + * Asynchronously publish an event for handling + * @param event internal event object + */ + publish(event: InternalEventInterface): void + /** + * Synchronously publish an event for handling. + * This will await for all handlers to finish processing the event. + * @param event internal event object + * @param strategy strategy with which the handlers will process the event. + * Either all handlers will start at once or they will do it sequentially. + */ + publishSync(event: InternalEventInterface, strategy: InternalEventPublishStrategy): Promise + + deinit(): void +} diff --git a/packages/services/src/Domain/Internal/InternalEventHandlerInterface.ts b/packages/services/src/Domain/Internal/InternalEventHandlerInterface.ts new file mode 100644 index 000000000..83037c2e9 --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventHandlerInterface.ts @@ -0,0 +1,5 @@ +import { InternalEventInterface } from './InternalEventInterface' + +export interface InternalEventHandlerInterface { + handleEvent(event: InternalEventInterface): Promise +} diff --git a/packages/services/src/Domain/Internal/InternalEventInterface.ts b/packages/services/src/Domain/Internal/InternalEventInterface.ts new file mode 100644 index 000000000..091b0353c --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventInterface.ts @@ -0,0 +1,6 @@ +import { InternalEventType } from './InternalEventType' + +export interface InternalEventInterface { + type: InternalEventType + payload: unknown +} diff --git a/packages/services/src/Domain/Internal/InternalEventPublishStrategy.ts b/packages/services/src/Domain/Internal/InternalEventPublishStrategy.ts new file mode 100644 index 000000000..216357b34 --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventPublishStrategy.ts @@ -0,0 +1,4 @@ +export enum InternalEventPublishStrategy { + ASYNC = 'ASYNC', + SEQUENCE = 'SEQUENCE', +} diff --git a/packages/services/src/Domain/Internal/InternalEventType.ts b/packages/services/src/Domain/Internal/InternalEventType.ts new file mode 100644 index 000000000..1d67636cd --- /dev/null +++ b/packages/services/src/Domain/Internal/InternalEventType.ts @@ -0,0 +1 @@ +export type InternalEventType = string diff --git a/packages/services/src/Domain/Item/ItemManagerInterface.ts b/packages/services/src/Domain/Item/ItemManagerInterface.ts new file mode 100644 index 000000000..a67637563 --- /dev/null +++ b/packages/services/src/Domain/Item/ItemManagerInterface.ts @@ -0,0 +1,133 @@ +import { ContentType } from '@standardnotes/common' +import { + MutationType, + ItemsKeyInterface, + ItemsKeyMutatorInterface, + DecryptedItemInterface, + DecryptedItemMutator, + DecryptedPayloadInterface, + PayloadEmitSource, + EncryptedItemInterface, + DeletedItemInterface, + ItemContent, + PredicateInterface, +} from '@standardnotes/models' +import { AbstractService } from '../Service/AbstractService' + +export type ItemManagerChangeData = { + /** The items are pre-existing but have been changed */ + changed: I[] + + /** The items have been newly inserted */ + inserted: I[] + + /** The items should no longer be displayed in the interface, either due to being deleted, or becoming error-encrypted */ + removed: (EncryptedItemInterface | DeletedItemInterface)[] + + /** Items for which encrypted overwrite protection is enabled and enacted */ + ignored: EncryptedItemInterface[] + + /** Items which were previously error decrypting but now successfully decrypted */ + unerrored: I[] + + source: PayloadEmitSource + sourceKey?: string +} + +export type ItemManagerChangeObserverCallback = ( + data: ItemManagerChangeData, +) => void + +export interface ItemManagerInterface extends AbstractService { + addObserver( + contentType: ContentType | ContentType[], + callback: ItemManagerChangeObserverCallback, + ): () => void + + /** + * Marks the item as deleted and needing sync. + */ + setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise + + setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise + + setItemsDirty( + itemsToLookupUuidsFor: DecryptedItemInterface[], + isUserModified?: boolean, + ): Promise + + get items(): DecryptedItemInterface[] + + /** + * Inserts the item as-is by reading its payload value. This function will not + * modify item in any way (such as marking it as dirty). It is up to the caller + * to pass in a dirtied item if that is their intention. + */ + insertItem(item: DecryptedItemInterface): Promise + + emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise + + getItems(contentType: ContentType | ContentType[]): T[] + + /** + * Returns all non-deleted items keys + */ + getDisplayableItemsKeys(): ItemsKeyInterface[] + + /** + * Creates an item and conditionally maps it and marks it as dirty. + * @param needsSync - Whether to mark the item as needing sync + */ + createItem( + contentType: ContentType, + content: C, + needsSync?: boolean, + ): Promise + + /** + * Create an unmanaged item that can later be inserted via `insertItem` + */ + createTemplateItem< + C extends ItemContent = ItemContent, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + contentType: ContentType, + content?: C, + ): I + + /** + * Consumers wanting to modify an item should run it through this block, + * so that data is properly mapped through our function, and latest state + * is properly reconciled. + */ + changeItem< + M extends DecryptedItemMutator = DecryptedItemMutator, + I extends DecryptedItemInterface = DecryptedItemInterface, + >( + itemToLookupUuidFor: I, + mutate?: (mutator: M) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise + + changeItemsKey( + itemToLookupUuidFor: ItemsKeyInterface, + mutate: (mutator: ItemsKeyMutatorInterface) => void, + mutationType?: MutationType, + emitSource?: PayloadEmitSource, + payloadSourceKey?: string, + ): Promise + + itemsMatchingPredicate( + contentType: ContentType, + predicate: PredicateInterface, + ): T[] + + itemsMatchingPredicates( + contentType: ContentType, + predicates: PredicateInterface[], + ): T[] + + subItemsMatchingPredicates(items: T[], predicates: PredicateInterface[]): T[] +} diff --git a/packages/services/src/Domain/Item/ItemsServerInterface.ts b/packages/services/src/Domain/Item/ItemsServerInterface.ts new file mode 100644 index 000000000..973d69902 --- /dev/null +++ b/packages/services/src/Domain/Item/ItemsServerInterface.ts @@ -0,0 +1,6 @@ +import { Uuid } from '@standardnotes/common' +import { GetSingleItemResponse } from '@standardnotes/responses' + +export interface ItemsServerInterface { + getSingleItem(itemUuid: Uuid): Promise +} diff --git a/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts new file mode 100644 index 000000000..c619829e6 --- /dev/null +++ b/packages/services/src/Domain/Payloads/PayloadManagerInterface.ts @@ -0,0 +1,24 @@ +import { + PayloadInterface, + EncryptedPayloadInterface, + FullyFormedPayloadInterface, + PayloadEmitSource, +} from '@standardnotes/models' +import { IntegrityPayload } from '@standardnotes/responses' + +export interface PayloadManagerInterface { + emitPayloads( + payloads: PayloadInterface[], + emitSource: PayloadEmitSource, + sourceKey?: string, + ): Promise + + integrityPayloads: IntegrityPayload[] + + get invalidPayloads(): EncryptedPayloadInterface[] + + /** + * Returns a detached array of all items which are not deleted + */ + get nonDeletedItems(): FullyFormedPayloadInterface[] +} diff --git a/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts new file mode 100644 index 000000000..744d05fd9 --- /dev/null +++ b/packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts @@ -0,0 +1,15 @@ +import { PrefKey, PrefValue } from '@standardnotes/models' +import { AbstractService } from '../Service/AbstractService' + +/* istanbul ignore file */ + +export enum PreferencesServiceEvent { + PreferencesChanged = 'PreferencesChanged', +} + +export interface PreferenceServiceInterface extends AbstractService { + getValue(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined + getValue(key: K, defaultValue: PrefValue[K]): PrefValue[K] + getValue(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined + setValue(key: K, value: PrefValue[K]): Promise +} diff --git a/packages/services/src/Domain/Service/AbstractService.ts b/packages/services/src/Domain/Service/AbstractService.ts new file mode 100644 index 000000000..47448a0f3 --- /dev/null +++ b/packages/services/src/Domain/Service/AbstractService.ts @@ -0,0 +1,108 @@ +/* istanbul ignore file */ + +import { log, removeFromArray } from '@standardnotes/utils' +import { EventObserver } from '../Event/EventObserver' +import { ServiceInterface } from './ServiceInterface' +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 +{ + private eventObservers: EventObserver[] = [] + public loggingEnabled = false + private criticalPromises: Promise[] = [] + + constructor(protected internalEventBus: InternalEventBusInterface) {} + + public addEventObserver(observer: EventObserver): () => void { + this.eventObservers.push(observer) + + const thislessEventObservers = this.eventObservers + return () => { + removeFromArray(thislessEventObservers, observer) + } + } + + protected async notifyEvent(eventName: EventName, data?: EventData): Promise { + for (const observer of this.eventObservers) { + await observer(eventName, data) + } + + this.internalEventBus?.publish({ + type: eventName as unknown as string, + payload: data, + }) + } + + protected async notifyEventSync(eventName: EventName, data?: EventData): Promise { + for (const observer of this.eventObservers) { + await observer(eventName, data) + } + + await this.internalEventBus?.publishSync( + { + type: eventName as unknown as string, + payload: data, + }, + InternalEventPublishStrategy.SEQUENCE, + ) + } + + getDiagnostics(): Promise { + return Promise.resolve(undefined) + } + + /** + * Called by application to allow services to momentarily block deinit until + * sensitive operations complete. + */ + public async blockDeinit(): Promise { + await Promise.all(this.criticalPromises) + } + + /** + * Called by application before restart. + * Subclasses should deregister any observers/timers + */ + public deinit(): void { + this.eventObservers.length = 0 + ;(this.internalEventBus as unknown) = undefined + ;(this.criticalPromises as unknown) = undefined + } + + /** + * A critical function is one that should block signing out or destroying application + * session until the crticial function has completed. For example, persisting keys to + * disk is a critical operation, and should be wrapped in this function call. The + * parent application instance will await all criticial functions via the `blockDeinit` + * function before signing out and deiniting. + */ + protected async executeCriticalFunction(func: () => Promise): Promise { + const promise = func() + this.criticalPromises.push(promise) + return promise + } + + /** + * Application instances will call this function directly when they arrive + * at a certain migratory state. + */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + public async handleApplicationStage(_stage: ApplicationStage): Promise { + // optional override + } + + getServiceName(): string { + return this.constructor.name + } + + log(..._args: unknown[]): void { + if (this.loggingEnabled) { + // eslint-disable-next-line prefer-rest-params + log(this.getServiceName(), ...arguments) + } + } +} diff --git a/packages/services/src/Domain/Service/ServiceInterface.ts b/packages/services/src/Domain/Service/ServiceInterface.ts new file mode 100644 index 000000000..20fac94ba --- /dev/null +++ b/packages/services/src/Domain/Service/ServiceInterface.ts @@ -0,0 +1,12 @@ +import { ApplicationStage } from '../Application/ApplicationStage' +import { ServiceDiagnostics } from '../Diagnostics/ServiceDiagnostics' +import { EventObserver } from '../Event/EventObserver' + +export interface ServiceInterface extends ServiceDiagnostics { + loggingEnabled: boolean + addEventObserver(observer: EventObserver): () => void + blockDeinit(): Promise + deinit(): void + handleApplicationStage(stage: ApplicationStage): Promise + log(message: string, ...args: unknown[]): void +} diff --git a/packages/services/src/Domain/Status/StatusService.ts b/packages/services/src/Domain/Status/StatusService.ts new file mode 100644 index 000000000..077636251 --- /dev/null +++ b/packages/services/src/Domain/Status/StatusService.ts @@ -0,0 +1,62 @@ +import { removeFromArray } from '@standardnotes/utils' +import { AbstractService } from '../Service/AbstractService' +import { StatusServiceEvent, StatusServiceInterface, StatusMessageIdentifier } from './StatusServiceInterface' + +/* istanbul ignore file */ + +export class StatusService extends AbstractService implements StatusServiceInterface { + private _message = '' + private directSetMessage?: string + private dynamicMessages: string[] = [] + + get message(): string { + return this._message + } + + setMessage(message: string | undefined): void { + this.directSetMessage = message + this.recomputeMessage() + } + + addMessage(message: string): StatusMessageIdentifier { + this.dynamicMessages.push(message) + + this.recomputeMessage() + + return message + } + + removeMessage(message: StatusMessageIdentifier): void { + removeFromArray(this.dynamicMessages, message) + + this.recomputeMessage() + } + + private recomputeMessage(): void { + const messages = [...this.dynamicMessages] + + if (this.directSetMessage) { + messages.unshift(this.directSetMessage) + } + + this._message = this.messageFromArray(messages) + + void this.notifyEvent(StatusServiceEvent.MessageChanged, this._message) + } + + private messageFromArray(messages: string[]): string { + let message = '' + + messages.forEach((value, index) => { + const isLast = index === messages.length - 1 + + message += value + + if (!isLast) { + message += ', ' + } + }) + + return message + } +} diff --git a/packages/services/src/Domain/Status/StatusServiceInterface.ts b/packages/services/src/Domain/Status/StatusServiceInterface.ts new file mode 100644 index 000000000..dc347f717 --- /dev/null +++ b/packages/services/src/Domain/Status/StatusServiceInterface.ts @@ -0,0 +1,16 @@ +import { AbstractService } from '../Service/AbstractService' + +/* istanbul ignore file */ + +export enum StatusServiceEvent { + MessageChanged = 'MessageChanged', +} + +export type StatusMessageIdentifier = string + +export interface StatusServiceInterface extends AbstractService { + get message(): string + setMessage(message: string | undefined): void + addMessage(message: string): StatusMessageIdentifier + removeMessage(message: StatusMessageIdentifier): void +} diff --git a/packages/services/src/Domain/Storage/InMemoryStore.spec.ts b/packages/services/src/Domain/Storage/InMemoryStore.spec.ts new file mode 100644 index 000000000..556117a0c --- /dev/null +++ b/packages/services/src/Domain/Storage/InMemoryStore.spec.ts @@ -0,0 +1,24 @@ +import { InMemoryStore } from './InMemoryStore' +import { StorageKey } from './StorageKeys' + +describe('InMemoryStore', () => { + const createStore = () => new InMemoryStore() + + it('should set and retrieve a value', () => { + const store = createStore() + + store.setValue(StorageKey.CodeVerifier, 'test') + + expect(store.getValue(StorageKey.CodeVerifier)).toEqual('test') + }) + + it('should remove a value', () => { + const store = createStore() + + store.setValue(StorageKey.CodeVerifier, 'test') + + store.removeValue(StorageKey.CodeVerifier) + + expect(store.getValue(StorageKey.CodeVerifier)).toBeUndefined() + }) +}) diff --git a/packages/services/src/Domain/Storage/InMemoryStore.ts b/packages/services/src/Domain/Storage/InMemoryStore.ts new file mode 100644 index 000000000..106cf912f --- /dev/null +++ b/packages/services/src/Domain/Storage/InMemoryStore.ts @@ -0,0 +1,22 @@ +import { KeyValueStoreInterface } from './KeyValueStoreInterface' +import { StorageKey } from './StorageKeys' + +export class InMemoryStore implements KeyValueStoreInterface { + private values: Map + + constructor() { + this.values = new Map() + } + + setValue(key: StorageKey, value: string): void { + this.values.set(key, value) + } + + getValue(key: StorageKey): string | undefined { + return this.values.get(key) + } + + removeValue(key: StorageKey): void { + this.values.delete(key) + } +} diff --git a/packages/services/src/Domain/Storage/KeyValueStoreInterface.ts b/packages/services/src/Domain/Storage/KeyValueStoreInterface.ts new file mode 100644 index 000000000..4b871e335 --- /dev/null +++ b/packages/services/src/Domain/Storage/KeyValueStoreInterface.ts @@ -0,0 +1,7 @@ +import { StorageKey } from './StorageKeys' + +export interface KeyValueStoreInterface { + setValue(key: StorageKey, value: T): void + getValue(key: StorageKey): T | undefined + removeValue(key: StorageKey): void +} diff --git a/packages/services/src/Domain/Storage/StorageKeys.spec.ts b/packages/services/src/Domain/Storage/StorageKeys.spec.ts new file mode 100644 index 000000000..bb89bfb48 --- /dev/null +++ b/packages/services/src/Domain/Storage/StorageKeys.spec.ts @@ -0,0 +1,7 @@ +import { namespacedKey } from './StorageKeys' + +describe('StorageKeys', () => { + it('namespacedKey', () => { + expect(namespacedKey('namespace', 'key')).toEqual('namespace-key') + }) +}) diff --git a/packages/services/src/Domain/Storage/StorageKeys.ts b/packages/services/src/Domain/Storage/StorageKeys.ts new file mode 100644 index 000000000..d402354eb --- /dev/null +++ b/packages/services/src/Domain/Storage/StorageKeys.ts @@ -0,0 +1,66 @@ +/** + * Unmanaged keys stored in root storage. + * Raw storage keys exist outside of StorageManager domain + */ +export enum RawStorageKey { + StorageObject = 'storage', + DescriptorRecord = 'descriptors', + SnjsVersion = 'snjs_version', +} + +/** + * Keys used for retrieving and saving simple key/value pairs. + * These keys are managed and are embedded inside RawStorageKey.StorageObject + */ +export enum StorageKey { + RootKeyParams = 'ROOT_KEY_PARAMS', + WrappedRootKey = 'WRAPPED_ROOT_KEY', + RootKeyWrapperKeyParams = 'ROOT_KEY_WRAPPER_KEY_PARAMS', + Session = 'session', + User = 'user', + ServerHost = 'server', + LegacyUuid = 'uuid', + LastSyncToken = 'syncToken', + PaginationToken = 'cursorToken', + BiometricsState = 'biometrics_state', + MobilePasscodeTiming = 'passcode_timing', + MobileBiometricsTiming = 'biometrics_timing', + MobilePasscodeKeyboardType = 'passcodeKeyboardType', + MobilePreferences = 'preferences', + MobileScreenshotPrivacyEnabled = 'screenshotPrivacy_enabled', + ProtectionExpirey = 'SessionExpiresAtKey', + ProtectionSessionLength = 'SessionLengthKey', + KeyRecoveryUndecryptableItems = 'key_recovery_undecryptable', + StorageEncryptionPolicy = 'storage_policy', + WebSocketUrl = 'webSocket_url', + UserRoles = 'user_roles', + UserFeatures = 'user_features', + ExperimentalFeatures = 'experimental_features', + DeinitMode = 'deinit_mode', + CodeVerifier = 'code_verifier', +} + +export enum NonwrappedStorageKey { + MobileFirstRun = 'first_run', +} + +export function namespacedKey(namespace: string, key: string) { + return `${namespace}-${key}` +} + +export const LegacyKeys1_0_0 = { + WebPasscodeParamsKey: 'offlineParams', + MobilePasscodeParamsKey: 'pc_params', + AllAccountKeyParamsKey: 'auth_params', + WebEncryptedStorageKey: 'encryptedStorage', + MobileWrappedRootKeyKey: 'encrypted_account_keys', + MobileBiometricsPrefs: 'biometrics_prefs', + AllMigrations: 'migrations', + MobileThemesCache: 'ThemePreferencesKey', + MobileLightTheme: 'lightTheme', + MobileDarkTheme: 'darkTheme', + MobileLastExportDate: 'LastExportDateKey', + MobileDoNotWarnUnsupportedEditors: 'DoNotShowAgainUnsupportedEditorsKey', + MobileOptionsState: 'options', + MobilePasscodeKeyboardType: 'passcodeKeyboardType', +} diff --git a/packages/services/src/Domain/Storage/StorageServiceInterface.ts b/packages/services/src/Domain/Storage/StorageServiceInterface.ts new file mode 100644 index 000000000..2e814bf50 --- /dev/null +++ b/packages/services/src/Domain/Storage/StorageServiceInterface.ts @@ -0,0 +1,16 @@ +import { PayloadInterface, RootKeyInterface } from '@standardnotes/models' +import { StorageValueModes } from './StorageTypes' + +export interface StorageServiceInterface { + getValue(key: string, mode?: StorageValueModes, defaultValue?: T): T + + canDecryptWithKey(key: RootKeyInterface): Promise + + savePayload(payload: PayloadInterface): Promise + + savePayloads(decryptedPayloads: PayloadInterface[]): Promise + + setValue(key: string, value: unknown, mode?: StorageValueModes): void + + removeValue(key: string, mode?: StorageValueModes): Promise +} diff --git a/packages/services/src/Domain/Storage/StorageTypes.ts b/packages/services/src/Domain/Storage/StorageTypes.ts new file mode 100644 index 000000000..a28807144 --- /dev/null +++ b/packages/services/src/Domain/Storage/StorageTypes.ts @@ -0,0 +1,39 @@ +import { LocalStorageEncryptedContextualPayload, LocalStorageDecryptedContextualPayload } from '@standardnotes/models' + +/* istanbul ignore file */ + +export enum StoragePersistencePolicies { + Default = 1, + Ephemeral = 2, +} + +export enum StorageEncryptionPolicy { + Default = 1, + Disabled = 2, +} + +export enum StorageValueModes { + /** Stored inside wrapped encrpyed storage object */ + Default = 1, + /** Stored outside storage object, unencrypted */ + Nonwrapped = 2, +} + +export enum ValueModesKeys { + /* Is encrypted */ + Wrapped = 'wrapped', + /* Is decrypted */ + Unwrapped = 'unwrapped', + /* Lives outside of wrapped/unwrapped */ + Nonwrapped = 'nonwrapped', +} + +export type ValuesObjectRecord = Record + +export type WrappedStorageValue = LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload + +export type StorageValuesObject = { + [ValueModesKeys.Wrapped]: WrappedStorageValue + [ValueModesKeys.Unwrapped]: ValuesObjectRecord + [ValueModesKeys.Nonwrapped]: ValuesObjectRecord +} diff --git a/packages/services/src/Domain/Sync/SyncMode.ts b/packages/services/src/Domain/Sync/SyncMode.ts new file mode 100644 index 000000000..a8a51fbec --- /dev/null +++ b/packages/services/src/Domain/Sync/SyncMode.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +export enum SyncMode { + /** + * Performs a standard sync, uploading any dirty items and retrieving items. + */ + Default = 1, + /** + * The first sync for an account, where we first want to download all remote items first + * before uploading any dirty items. This allows a consumer, for example, to download + * all data to see if user has an items key, and if not, only then create a new one. + */ + DownloadFirst = 2, +} diff --git a/packages/services/src/Domain/Sync/SyncOptions.ts b/packages/services/src/Domain/Sync/SyncOptions.ts new file mode 100644 index 000000000..abc44c983 --- /dev/null +++ b/packages/services/src/Domain/Sync/SyncOptions.ts @@ -0,0 +1,21 @@ +/* istanbul ignore file */ + +import { SyncMode } from './SyncMode' +import { SyncQueueStrategy } from './SyncQueueStrategy' +import { SyncSource } from './SyncSource' + +export type SyncOptions = { + queueStrategy?: SyncQueueStrategy + mode?: SyncMode + /** Whether the server should compute and return an integrity hash. */ + checkIntegrity?: boolean + /** Internally used to keep track of how sync requests were spawned. */ + source: SyncSource + /** Whether to await any sync requests that may be queued from this call. */ + awaitAll?: boolean + /** + * A callback that is triggered after pre-sync save completes, + * and before the sync request is network dispatched + */ + onPresyncSave?: () => void +} diff --git a/packages/services/src/Domain/Sync/SyncQueueStrategy.ts b/packages/services/src/Domain/Sync/SyncQueueStrategy.ts new file mode 100644 index 000000000..8d233ac04 --- /dev/null +++ b/packages/services/src/Domain/Sync/SyncQueueStrategy.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ + +export enum SyncQueueStrategy { + /** + * Promise will be resolved on the next sync request after the current one completes. + * If there is no scheduled sync request, one will be scheduled. + */ + ResolveOnNext = 1, + /** + * A new sync request is guarenteed to be generated for your request, no matter how long it takes. + * Promise will be resolved whenever this sync request is processed in the serial queue. + */ + ForceSpawnNew = 2, +} diff --git a/packages/services/src/Domain/Sync/SyncServiceInterface.ts b/packages/services/src/Domain/Sync/SyncServiceInterface.ts new file mode 100644 index 000000000..f332fe3ae --- /dev/null +++ b/packages/services/src/Domain/Sync/SyncServiceInterface.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ + +import { SyncOptions } from './SyncOptions' + +export interface SyncServiceInterface { + sync(options?: Partial): Promise +} diff --git a/packages/services/src/Domain/Sync/SyncSource.ts b/packages/services/src/Domain/Sync/SyncSource.ts new file mode 100644 index 000000000..646157c45 --- /dev/null +++ b/packages/services/src/Domain/Sync/SyncSource.ts @@ -0,0 +1,11 @@ +/* istanbul ignore file */ + +export enum SyncSource { + External = 1, + SpawnQueue = 2, + ResolveQueue = 3, + MoreDirtyItems = 4, + AfterDownloadFirst = 5, + IntegrityCheck = 6, + ResolveOutOfSync = 7, +} diff --git a/packages/services/src/Domain/index.ts b/packages/services/src/Domain/index.ts new file mode 100644 index 000000000..241e7624d --- /dev/null +++ b/packages/services/src/Domain/index.ts @@ -0,0 +1,51 @@ +export * from './Alert/AlertService' +export * from './Api/ApiServiceInterface' +export * from './Application/ApplicationStage' +export * from './Application/DeinitCallback' +export * from './Application/DeinitSource' +export * from './Application/DeinitMode' +export * from './Application/UserClientInterface' +export * from './Application/ApplicationInterface' +export * from './Challenge' +export * from './Device/DesktopDeviceInterface' +export * from './Device/DesktopWebCommunication' +export * from './Device/DeviceInterface' +export * from './Device/Environments' +export * from './Device/FileBackupsDevice' +export * from './Device/MobileDeviceInterface' +export * from './Device/TypeCheck' +export * from './Device/WebOrDesktopDeviceInterface' +export * from './Diagnostics/ServiceDiagnostics' +export * from './Event/EventObserver' +export * from './Event/SyncEvent' +export * from './Event/SyncEventReceiver' +export * from './Files/FilesApiInterface' +export * from './FileSystem/FileSystemApi' +export * from './Integrity/IntegrityApiInterface' +export * from './Integrity/IntegrityEvent' +export * from './Integrity/IntegrityEventPayload' +export * from './Integrity/IntegrityService' +export * from './Internal/InternalEventBus' +export * from './Internal/InternalEventBusInterface' +export * from './Internal/InternalEventHandlerInterface' +export * from './Internal/InternalEventInterface' +export * from './Internal/InternalEventPublishStrategy' +export * from './Internal/InternalEventType' +export * from './Item/ItemManagerInterface' +export * from './Item/ItemsServerInterface' +export * from './Payloads/PayloadManagerInterface' +export * from './Preferences/PreferenceServiceInterface' +export * from './Service/AbstractService' +export * from './Service/ServiceInterface' +export * from './Status/StatusService' +export * from './Status/StatusServiceInterface' +export * from './Storage/StorageKeys' +export * from './Storage/InMemoryStore' +export * from './Storage/KeyValueStoreInterface' +export * from './Storage/StorageServiceInterface' +export * from './Storage/StorageTypes' +export * from './Sync/SyncMode' +export * from './Sync/SyncOptions' +export * from './Sync/SyncQueueStrategy' +export * from './Sync/SyncServiceInterface' +export * from './Sync/SyncSource' diff --git a/packages/services/src/index.ts b/packages/services/src/index.ts new file mode 100644 index 000000000..920deacdb --- /dev/null +++ b/packages/services/src/index.ts @@ -0,0 +1 @@ +export * from './Domain' diff --git a/packages/services/tsconfig.json b/packages/services/tsconfig.json new file mode 100644 index 000000000..f3dac14ef --- /dev/null +++ b/packages/services/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../node_modules/@standardnotes/config/src/tsconfig.json", + "compilerOptions": { + "skipLibCheck": true, + "rootDir": "./src", + "outDir": "./dist", + }, + "include": [ + "src/**/*" + ], + "references": [], + "exclude": ["**/*.spec.ts", "dist", "node_modules"] +} diff --git a/packages/web/package.json b/packages/web/package.json index 95fe7d99a..d6edd3189 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -71,7 +71,7 @@ "@standardnotes/encryption": "workspace:*", "@standardnotes/filepicker": "workspace:*", "@standardnotes/icons": "workspace:*", - "@standardnotes/services": "^1.13.23", + "@standardnotes/services": "workspace:*", "@standardnotes/sncrypto-web": "1.10.1", "@standardnotes/snjs": "^2.118.3", "@standardnotes/styles": "workspace:*", diff --git a/yarn.lock b/yarn.lock index 76ed598bd..a6d34aabc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6549,7 +6549,7 @@ __metadata: "@standardnotes/config": 2.4.3 "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 - "@standardnotes/services": ^1.13.23 + "@standardnotes/services": "workspace:*" "@standardnotes/sncrypto-common": ^1.9.0 "@standardnotes/utils": ^1.6.12 "@types/jest": ^27.4.1 @@ -6604,7 +6604,7 @@ __metadata: resolution: "@standardnotes/filepicker@workspace:packages/filepicker" dependencies: "@standardnotes/common": ^1.23.1 - "@standardnotes/services": ^1.13.23 + "@standardnotes/services": "workspace:*" "@standardnotes/utils": ^1.6.12 "@types/jest": ^27.4.1 "@types/wicg-file-system-access": ^2020.9.5 @@ -6626,7 +6626,7 @@ __metadata: "@standardnotes/filepicker": "workspace:*" "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 - "@standardnotes/services": ^1.13.23 + "@standardnotes/services": "workspace:*" "@standardnotes/sncrypto-common": ^1.9.0 "@standardnotes/utils": ^1.6.12 "@types/jest": ^27.4.1 @@ -7122,31 +7122,24 @@ __metadata: languageName: node linkType: hard -"@standardnotes/services@npm:^1.13.22": - version: 1.13.22 - resolution: "@standardnotes/services@npm:1.13.22" +"@standardnotes/services@^1.13.22, @standardnotes/services@^1.13.23, @standardnotes/services@workspace:*, @standardnotes/services@workspace:packages/services": + version: 0.0.0-use.local + resolution: "@standardnotes/services@workspace:packages/services" dependencies: "@standardnotes/auth": ^3.19.4 "@standardnotes/common": ^1.23.1 - "@standardnotes/models": ^1.11.12 - "@standardnotes/responses": ^1.6.38 - "@standardnotes/utils": ^1.6.12 - checksum: e84f4e43d49c42b1f99b4e54380f1539ca3ff3451290b7f290fc1e480f2207f8567035015f8788c7b9f961f88eb81b43a4cca591328390dbd0622c9e4891063b - languageName: node - linkType: hard - -"@standardnotes/services@npm:^1.13.23": - version: 1.13.23 - resolution: "@standardnotes/services@npm:1.13.23" - dependencies: - "@standardnotes/auth": ^3.19.4 - "@standardnotes/common": ^1.23.1 - "@standardnotes/models": ^1.11.13 + "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 "@standardnotes/utils": ^1.6.12 - checksum: 7e67af13c4eb845c6bcbbac46897b94fe4754a728dba04605ccfbd96da49a0b305299fd3db9778a29488c5b29cf1ac4db08c28844e3ed95a2a872595376e1dc6 - languageName: node - linkType: hard + "@types/jest": ^27.4.1 + "@typescript-eslint/eslint-plugin": ^5.30.0 + "@typescript-eslint/parser": ^5.12.1 + eslint-plugin-prettier: ^4.2.1 + jest: ^27.5.1 + reflect-metadata: ^0.1.13 + ts-jest: ^27.1.3 + languageName: unknown + linkType: soft "@standardnotes/settings@npm:^1.15.0": version: 1.15.0 @@ -7419,7 +7412,7 @@ __metadata: "@standardnotes/encryption": "workspace:*" "@standardnotes/filepicker": "workspace:*" "@standardnotes/icons": "workspace:*" - "@standardnotes/services": ^1.13.23 + "@standardnotes/services": "workspace:*" "@standardnotes/sncrypto-web": 1.10.1 "@standardnotes/snjs": ^2.118.3 "@standardnotes/styles": "workspace:*"