feat: add services package

This commit is contained in:
Karol Sójko
2022-07-05 20:51:42 +02:00
parent b614c71e79
commit fbfed0a05c
85 changed files with 2418 additions and 28 deletions

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ packages/features/dist
packages/encryption/dist
packages/files/dist
packages/models/dist
packages/services/dist
**/.pnp.*
**/.yarn/*

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
node_modules
dist
coverage

View File

@@ -0,0 +1,9 @@
{
"extends": "../../.eslintrc",
"parserOptions": {
"project": "./linter.tsconfig.json"
},
"rules": {
"@typescript-eslint/no-explicit-any": ["warn", { "ignoreRestArgs": true }]
}
}

View File

@@ -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))

View File

@@ -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',
},
}
};

View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["dist"]
}

View File

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

View File

@@ -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<boolean>
abstract alert(text: string, title?: string, closeButtonText?: string): Promise<void>
abstract blockingDialog(text: string, title?: string): DismissBlockingDialog | Promise<DismissBlockingDialog>
showErrorAlert(error: ClientDisplayableError): Promise<void> {
return this.alert(error.text, error.title)
}
}

View File

@@ -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<ApiServiceEvent.MetaReceived, MetaReceivedData>,
FilesApiInterface {}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
/* istanbul ignore file */
export enum DeinitMode {
Soft = 'Soft',
Hard = 'Hard',
}

View File

@@ -0,0 +1,8 @@
/* istanbul ignore file */
export enum DeinitSource {
SignOut = 1,
Lock,
SwitchWorkspace,
SignOutAll,
}

View File

@@ -0,0 +1,9 @@
import { DeinitSource } from './DeinitSource'
export interface UserClientInterface {
deleteAccount(): Promise<{
error: boolean
message?: string
}>
signOut(force?: boolean, source?: DeinitSource): Promise<void>
}

View File

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

View File

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

View File

@@ -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<ChallengeResponseInterface | undefined>
createChallenge(
prompts: ChallengePromptInterface[],
reason: ChallengeReason,
cancelable: boolean,
heading?: string,
subheading?: string,
): ChallengeInterface
completeChallenge(challenge: ChallengeInterface): void
}

View File

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

View File

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

View File

@@ -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',
}

View File

@@ -0,0 +1,8 @@
import { RootKeyInterface } from '@standardnotes/models'
/* istanbul ignore file */
export type ChallengeArtifacts = {
wrappingKey?: RootKeyInterface
rootKey?: RootKeyInterface
}

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
/** For mobile */
export enum ChallengeKeyboardType {
Alphanumeric = 'default',
Numeric = 'numeric',
}

View File

@@ -0,0 +1,3 @@
/* istanbul ignore file */
export type ChallengeRawValue = number | string | boolean

View File

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

View File

@@ -0,0 +1,9 @@
/* istanbul ignore file */
export enum ChallengeValidation {
None = 0,
LocalPasscode = 1,
AccountPassword = 2,
Biometric = 3,
ProtectionSessionDuration = 4,
}

View File

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

View File

@@ -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'

View File

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

View File

@@ -0,0 +1,38 @@
import { DecryptedTransferPayload } from '@standardnotes/models'
import { FileBackupsDevice } from './FileBackupsDevice'
export interface WebClientRequiresDesktopMethods extends FileBackupsDevice {
localBackupsCount(): Promise<number>
viewlocalBackups(): void
deleteLocalBackups(): Promise<void>
syncComponents(payloads: unknown[]): void
onMajorDataChange(): void
onInitialDataLoad(): void
onSearch(text?: string): void
downloadBackup(): void | Promise<void>
get extensionsServerHost(): string
}
export interface DesktopClientRequiresWebMethods {
updateAvailable(): void
windowGainedFocus(): void
windowLostFocus(): void
onComponentInstallationComplete(componentData: DecryptedTransferPayload, error: unknown): Promise<void>
requestBackupFile(): Promise<string | undefined>
didBeginBackup(): void
didFinishBackup(success: boolean): void
}

View File

@@ -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<string | undefined>
getJsonParsedRawStorageValue(key: string): Promise<unknown | undefined>
getAllRawStorageKeyValues(): Promise<{ key: string; value: unknown }[]>
setRawStorageValue(key: string, value: string): Promise<void>
removeRawStorageValue(key: string): Promise<void>
removeAllRawStorageValues(): Promise<void>
/**
* 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<string[]>
/**
* 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<T extends FullyFormedTransferPayload = FullyFormedTransferPayload>(
identifier: ApplicationIdentifier,
): Promise<T[]>
saveRawDatabasePayload(payload: TransferPayload, identifier: ApplicationIdentifier): Promise<void>
saveRawDatabasePayloads(payloads: TransferPayload[], identifier: ApplicationIdentifier): Promise<void>
removeRawDatabasePayloadWithId(id: string, identifier: ApplicationIdentifier): Promise<void>
removeAllRawDatabasePayloads(identifier: ApplicationIdentifier): Promise<void>
getNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise<NamespacedRootKeyInKeychain | undefined>
setNamespacedKeychainValue(value: NamespacedRootKeyInKeychain, identifier: ApplicationIdentifier): Promise<void>
clearNamespacedKeychainValue(identifier: ApplicationIdentifier): Promise<void>
setLegacyRawKeychainValue(value: LegacyRawKeychainValue): Promise<void>
clearRawKeychainValue(): Promise<void>
openUrl(url: string): void
performSoftReset(): void
performHardReset(): void
isDeviceDestroyed(): boolean
}

View File

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

View File

@@ -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<string, string>
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<FileBackupsMapping>
saveFilesBackupsFile(
uuid: Uuid,
metaFile: string,
downloadRequest: {
chunkSizes: number[]
valetToken: string
url: string
},
): Promise<'success' | 'failed'>
isFilesBackupsEnabled(): Promise<boolean>
enableFilesBackups(): Promise<void>
disableFilesBackups(): Promise<void>
changeFilesBackupsLocation(): Promise<string | undefined>
getFilesBackupsLocation(): Promise<string>
openFilesBackupsLocation(): Promise<void>
}

View File

@@ -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<RawKeychainValue | undefined>
}

View File

@@ -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<MobileDeviceInterface>
expect(isMobileDevice(device)).toBeTruthy()
})
it('should return false for non mobile devices', () => {
const device = { environment: Environment.Web } as jest.Mocked<DeviceInterface>
expect(isMobileDevice(device)).toBeFalsy()
})
})

View File

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

View File

@@ -0,0 +1,10 @@
import { DeviceInterface } from './DeviceInterface'
import { RawKeychainValue } from '@standardnotes/models'
export interface WebOrDesktopDeviceInterface extends DeviceInterface {
readonly appVersion: string
getKeychainValue(): Promise<RawKeychainValue>
setKeychainValue(value: RawKeychainValue): Promise<void>
}

View File

@@ -0,0 +1,17 @@
type DiagnosticValue =
| string
| number
| Date
| boolean
| null
| undefined
| DiagnosticValue[]
| { [key: string]: DiagnosticValue }
export type DiagnosticInfo = {
[key: string]: Record<string, DiagnosticValue>
}
export interface ServiceDiagnostics {
getDiagnostics(): Promise<DiagnosticInfo | undefined>
}

View File

@@ -0,0 +1 @@
export type EventObserver<E, D> = (eventName: E, data?: D) => Promise<void> | void

View File

@@ -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',
}

View File

@@ -0,0 +1,3 @@
import { SyncEvent } from './SyncEvent'
export type SyncEventReceiver = (event: SyncEvent) => void

View File

@@ -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<DirectoryHandle | FileSystemNoSelection>
selectFile(): Promise<FileHandleRead | FileSystemNoSelection>
readFile(
file: FileHandleRead,
onBytes: (bytes: Uint8Array, isLast: boolean) => Promise<void>,
): Promise<FileSystemResult>
createDirectory(parentDirectory: DirectoryHandle, name: string): Promise<DirectoryHandle | FileSystemNoSelection>
createFile(directory: DirectoryHandle, name: string): Promise<FileHandleReadWrite | FileSystemNoSelection>
saveBytes(file: FileHandleReadWrite, bytes: Uint8Array): Promise<'success' | 'failed'>
saveString(file: FileHandleReadWrite, contents: string): Promise<'success' | 'failed'>
closeFileWriteStream(file: FileHandleReadWrite): Promise<'success' | 'failed'>
}

View File

@@ -0,0 +1,28 @@
import { StartUploadSessionResponse, MinimalHttpResponse, ClientDisplayableError } from '@standardnotes/responses'
import { FileContent } from '@standardnotes/models'
export interface FilesApiInterface {
startUploadSession(apiToken: string): Promise<StartUploadSessionResponse>
uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise<boolean>
closeUploadSession(apiToken: string): Promise<boolean>
downloadFile(
file: { encryptedChunkSizes: FileContent['encryptedChunkSizes'] },
chunkIndex: number,
apiToken: string,
contentRangeStart: number,
onBytesReceived: (bytes: Uint8Array) => Promise<void>,
): Promise<ClientDisplayableError | undefined>
deleteFile(apiToken: string): Promise<MinimalHttpResponse>
createFileValetToken(
remoteIdentifier: string,
operation: 'write' | 'read' | 'delete',
unencryptedFileSize?: number,
): Promise<string | ClientDisplayableError>
getFilesDownloadUrl(): string
}

View File

@@ -0,0 +1,5 @@
import { CheckIntegrityResponse, IntegrityPayload } from '@standardnotes/responses'
export interface IntegrityApiInterface {
checkIntegrity(integrityPayloads: IntegrityPayload[]): Promise<CheckIntegrityResponse>
}

View File

@@ -0,0 +1,4 @@
/* istanbul ignore file */
export enum IntegrityEvent {
IntegrityCheckCompleted = 'IntegrityCheckCompleted',
}

View File

@@ -0,0 +1,7 @@
import { ServerItemResponse } from '@standardnotes/responses'
import { SyncSource } from '../Sync/SyncSource'
export type IntegrityEventPayload = {
rawPayloads: ServerItemResponse[]
source: SyncSource
}

View File

@@ -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<IntegrityApiInterface>
integrityApi.checkIntegrity = jest.fn()
itemApi = {} as jest.Mocked<ItemsServerInterface>
itemApi.getSingleItem = jest.fn()
payloadManager = {} as jest.Mocked<PayloadManagerInterface>
payloadManager.integrityPayloads = []
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
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<TransferPayload>,
},
})
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()
})
})

View File

@@ -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<IntegrityEvent, IntegrityEventPayload>
implements InternalEventHandlerInterface
{
constructor(
private integrityApi: IntegrityApiInterface,
private itemApi: ItemsServerInterface,
private payloadManager: PayloadManagerInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
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<GetSingleItemResponse>[] = []
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,
})
}
}

View File

@@ -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<InternalEventHandlerInterface>
eventHandler1.handleEvent = jest.fn()
eventHandler2 = {} as jest.Mocked<InternalEventHandlerInterface>
eventHandler2.handleEvent = jest.fn()
eventHandler3 = {} as jest.Mocked<InternalEventHandlerInterface>
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
})
})

View File

@@ -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<InternalEventType, InternalEventHandlerInterface[]>
constructor() {
this.eventHandlers = new Map<InternalEventType, InternalEventHandlerInterface[]>()
}
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<void> {
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)
}
}
}

View File

@@ -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<void>
deinit(): void
}

View File

@@ -0,0 +1,5 @@
import { InternalEventInterface } from './InternalEventInterface'
export interface InternalEventHandlerInterface {
handleEvent(event: InternalEventInterface): Promise<void>
}

View File

@@ -0,0 +1,6 @@
import { InternalEventType } from './InternalEventType'
export interface InternalEventInterface {
type: InternalEventType
payload: unknown
}

View File

@@ -0,0 +1,4 @@
export enum InternalEventPublishStrategy {
ASYNC = 'ASYNC',
SEQUENCE = 'SEQUENCE',
}

View File

@@ -0,0 +1 @@
export type InternalEventType = string

View File

@@ -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<I extends DecryptedItemInterface = DecryptedItemInterface> = {
/** 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<I extends DecryptedItemInterface = DecryptedItemInterface> = (
data: ItemManagerChangeData<I>,
) => void
export interface ItemManagerInterface extends AbstractService {
addObserver<I extends DecryptedItemInterface = DecryptedItemInterface>(
contentType: ContentType | ContentType[],
callback: ItemManagerChangeObserverCallback<I>,
): () => void
/**
* Marks the item as deleted and needing sync.
*/
setItemToBeDeleted(itemToLookupUuidFor: DecryptedItemInterface, source?: PayloadEmitSource): Promise<void>
setItemsToBeDeleted(itemsToLookupUuidsFor: DecryptedItemInterface[]): Promise<void>
setItemsDirty(
itemsToLookupUuidsFor: DecryptedItemInterface[],
isUserModified?: boolean,
): Promise<DecryptedItemInterface[]>
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<DecryptedItemInterface>
emitItemFromPayload(payload: DecryptedPayloadInterface, source: PayloadEmitSource): Promise<DecryptedItemInterface>
getItems<T extends DecryptedItemInterface>(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<T extends DecryptedItemInterface, C extends ItemContent = ItemContent>(
contentType: ContentType,
content: C,
needsSync?: boolean,
): Promise<T>
/**
* Create an unmanaged item that can later be inserted via `insertItem`
*/
createTemplateItem<
C extends ItemContent = ItemContent,
I extends DecryptedItemInterface<C> = DecryptedItemInterface<C>,
>(
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<I>
changeItemsKey(
itemToLookupUuidFor: ItemsKeyInterface,
mutate: (mutator: ItemsKeyMutatorInterface) => void,
mutationType?: MutationType,
emitSource?: PayloadEmitSource,
payloadSourceKey?: string,
): Promise<ItemsKeyInterface>
itemsMatchingPredicate<T extends DecryptedItemInterface>(
contentType: ContentType,
predicate: PredicateInterface<T>,
): T[]
itemsMatchingPredicates<T extends DecryptedItemInterface>(
contentType: ContentType,
predicates: PredicateInterface<T>[],
): T[]
subItemsMatchingPredicates<T extends DecryptedItemInterface>(items: T[], predicates: PredicateInterface<T>[]): T[]
}

View File

@@ -0,0 +1,6 @@
import { Uuid } from '@standardnotes/common'
import { GetSingleItemResponse } from '@standardnotes/responses'
export interface ItemsServerInterface {
getSingleItem(itemUuid: Uuid): Promise<GetSingleItemResponse>
}

View File

@@ -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<PayloadInterface[]>
integrityPayloads: IntegrityPayload[]
get invalidPayloads(): EncryptedPayloadInterface[]
/**
* Returns a detached array of all items which are not deleted
*/
get nonDeletedItems(): FullyFormedPayloadInterface[]
}

View File

@@ -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<PreferencesServiceEvent> {
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K]
getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined
setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
}

View File

@@ -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<EventName = string, EventData = undefined>
implements ServiceInterface<EventName, EventData>
{
private eventObservers: EventObserver<EventName, EventData>[] = []
public loggingEnabled = false
private criticalPromises: Promise<unknown>[] = []
constructor(protected internalEventBus: InternalEventBusInterface) {}
public addEventObserver(observer: EventObserver<EventName, EventData>): () => void {
this.eventObservers.push(observer)
const thislessEventObservers = this.eventObservers
return () => {
removeFromArray(thislessEventObservers, observer)
}
}
protected async notifyEvent(eventName: EventName, data?: EventData): Promise<void> {
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<void> {
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<DiagnosticInfo | undefined> {
return Promise.resolve(undefined)
}
/**
* Called by application to allow services to momentarily block deinit until
* sensitive operations complete.
*/
public async blockDeinit(): Promise<void> {
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<T = void>(func: () => Promise<T>): Promise<T> {
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<void> {
// 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)
}
}
}

View File

@@ -0,0 +1,12 @@
import { ApplicationStage } from '../Application/ApplicationStage'
import { ServiceDiagnostics } from '../Diagnostics/ServiceDiagnostics'
import { EventObserver } from '../Event/EventObserver'
export interface ServiceInterface<E, D> extends ServiceDiagnostics {
loggingEnabled: boolean
addEventObserver(observer: EventObserver<E, D>): () => void
blockDeinit(): Promise<void>
deinit(): void
handleApplicationStage(stage: ApplicationStage): Promise<void>
log(message: string, ...args: unknown[]): void
}

View File

@@ -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<StatusServiceEvent, string> 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
}
}

View File

@@ -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<StatusServiceEvent, string> {
get message(): string
setMessage(message: string | undefined): void
addMessage(message: string): StatusMessageIdentifier
removeMessage(message: StatusMessageIdentifier): void
}

View File

@@ -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()
})
})

View File

@@ -0,0 +1,22 @@
import { KeyValueStoreInterface } from './KeyValueStoreInterface'
import { StorageKey } from './StorageKeys'
export class InMemoryStore implements KeyValueStoreInterface<string> {
private values: Map<StorageKey, string>
constructor() {
this.values = new Map<StorageKey, string>()
}
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)
}
}

View File

@@ -0,0 +1,7 @@
import { StorageKey } from './StorageKeys'
export interface KeyValueStoreInterface<T> {
setValue(key: StorageKey, value: T): void
getValue(key: StorageKey): T | undefined
removeValue(key: StorageKey): void
}

View File

@@ -0,0 +1,7 @@
import { namespacedKey } from './StorageKeys'
describe('StorageKeys', () => {
it('namespacedKey', () => {
expect(namespacedKey('namespace', 'key')).toEqual('namespace-key')
})
})

View File

@@ -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',
}

View File

@@ -0,0 +1,16 @@
import { PayloadInterface, RootKeyInterface } from '@standardnotes/models'
import { StorageValueModes } from './StorageTypes'
export interface StorageServiceInterface {
getValue<T>(key: string, mode?: StorageValueModes, defaultValue?: T): T
canDecryptWithKey(key: RootKeyInterface): Promise<boolean>
savePayload(payload: PayloadInterface): Promise<void>
savePayloads(decryptedPayloads: PayloadInterface[]): Promise<void>
setValue(key: string, value: unknown, mode?: StorageValueModes): void
removeValue(key: string, mode?: StorageValueModes): Promise<void>
}

View File

@@ -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<string, unknown>
export type WrappedStorageValue = LocalStorageEncryptedContextualPayload | LocalStorageDecryptedContextualPayload
export type StorageValuesObject = {
[ValueModesKeys.Wrapped]: WrappedStorageValue
[ValueModesKeys.Unwrapped]: ValuesObjectRecord
[ValueModesKeys.Nonwrapped]: ValuesObjectRecord
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
import { SyncOptions } from './SyncOptions'
export interface SyncServiceInterface {
sync(options?: Partial<SyncOptions>): Promise<unknown>
}

View File

@@ -0,0 +1,11 @@
/* istanbul ignore file */
export enum SyncSource {
External = 1,
SpawnQueue = 2,
ResolveQueue = 3,
MoreDirtyItems = 4,
AfterDownloadFirst = 5,
IntegrityCheck = 6,
ResolveOutOfSync = 7,
}

View File

@@ -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'

View File

@@ -0,0 +1 @@
export * from './Domain'

View File

@@ -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"]
}

View File

@@ -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:*",

View File

@@ -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:*"