From fbfed0a05cd5eb6ec4d5579012971e4495e04a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Tue, 5 Jul 2022 20:51:42 +0200 Subject: [PATCH] feat: add services package --- .gitignore | 1 + ...ices-npm-1.13.22-e649cbd9ce-e84f4e43d4.zip | Bin 76352 -> 0 bytes ...ices-npm-1.13.23-c7085fb4e1-7e67af13c4.zip | Bin 76365 -> 0 bytes packages/encryption/package.json | 2 +- packages/filepicker/package.json | 2 +- packages/files/package.json | 2 +- packages/services/.eslintignore | 3 + packages/services/.eslintrc | 9 + packages/services/CHANGELOG.md | 500 ++++++++++++++++++ packages/services/jest.config.js | 11 + packages/services/linter.tsconfig.json | 4 + packages/services/package.json | 43 ++ .../services/src/Domain/Alert/AlertService.ts | 28 + .../src/Domain/Api/ApiServiceInterface.ts | 19 + .../Application/ApplicationInterface.ts | 22 + .../Domain/Application/ApplicationStage.ts | 11 + .../src/Domain/Application/DeinitCallback.ts | 5 + .../src/Domain/Application/DeinitMode.ts | 6 + .../src/Domain/Application/DeinitSource.ts | 8 + .../Domain/Application/UserClientInterface.ts | 9 + .../Domain/Challenge/ChallengeInterface.ts | 21 + .../Challenge/ChallengeResponseInterface.ts | 13 + .../Challenge/ChallengeServiceInterface.ts | 23 + .../Challenge/Prompt/ChallengePrompt.ts | 55 ++ .../Prompt/ChallengePromptInterface.ts | 19 + .../Domain/Challenge/Prompt/PromptTitles.ts | 9 + .../Challenge/Types/ChallengeArtifacts.ts | 8 + .../Challenge/Types/ChallengeKeyboardType.ts | 7 + .../Challenge/Types/ChallengeRawValue.ts | 3 + .../Domain/Challenge/Types/ChallengeReason.ts | 27 + .../Challenge/Types/ChallengeValidation.ts | 9 + .../Domain/Challenge/Types/ChallengeValue.ts | 13 + .../services/src/Domain/Challenge/index.ts | 12 + .../Domain/Device/DesktopDeviceInterface.ts | 14 + .../Domain/Device/DesktopWebCommunication.ts | 38 ++ .../src/Domain/Device/DeviceInterface.ts | 84 +++ .../src/Domain/Device/Environments.ts | 16 + .../src/Domain/Device/FileBackupsDevice.ts | 51 ++ .../Domain/Device/MobileDeviceInterface.ts | 9 + .../src/Domain/Device/TypeCheck.spec.ts | 18 + .../services/src/Domain/Device/TypeCheck.ts | 9 + .../Device/WebOrDesktopDeviceInterface.ts | 10 + .../Domain/Diagnostics/ServiceDiagnostics.ts | 17 + .../src/Domain/Event/EventObserver.ts | 1 + .../services/src/Domain/Event/SyncEvent.ts | 25 + .../src/Domain/Event/SyncEventReceiver.ts | 3 + .../src/Domain/FileSystem/FileSystemApi.ts | 27 + .../src/Domain/Files/FilesApiInterface.ts | 28 + .../Domain/Integrity/IntegrityApiInterface.ts | 5 + .../src/Domain/Integrity/IntegrityEvent.ts | 4 + .../Domain/Integrity/IntegrityEventPayload.ts | 7 + .../Domain/Integrity/IntegrityService.spec.ts | 160 ++++++ .../src/Domain/Integrity/IntegrityService.ts | 62 +++ .../Domain/Internal/InternalEventBus.spec.ts | 117 ++++ .../src/Domain/Internal/InternalEventBus.ts | 61 +++ .../Internal/InternalEventBusInterface.ts | 28 + .../Internal/InternalEventHandlerInterface.ts | 5 + .../Domain/Internal/InternalEventInterface.ts | 6 + .../Internal/InternalEventPublishStrategy.ts | 4 + .../src/Domain/Internal/InternalEventType.ts | 1 + .../src/Domain/Item/ItemManagerInterface.ts | 133 +++++ .../src/Domain/Item/ItemsServerInterface.ts | 6 + .../Payloads/PayloadManagerInterface.ts | 24 + .../Preferences/PreferenceServiceInterface.ts | 15 + .../src/Domain/Service/AbstractService.ts | 108 ++++ .../src/Domain/Service/ServiceInterface.ts | 12 + .../src/Domain/Status/StatusService.ts | 62 +++ .../Domain/Status/StatusServiceInterface.ts | 16 + .../src/Domain/Storage/InMemoryStore.spec.ts | 24 + .../src/Domain/Storage/InMemoryStore.ts | 22 + .../Domain/Storage/KeyValueStoreInterface.ts | 7 + .../src/Domain/Storage/StorageKeys.spec.ts | 7 + .../src/Domain/Storage/StorageKeys.ts | 66 +++ .../Domain/Storage/StorageServiceInterface.ts | 16 + .../src/Domain/Storage/StorageTypes.ts | 39 ++ packages/services/src/Domain/Sync/SyncMode.ts | 14 + .../services/src/Domain/Sync/SyncOptions.ts | 21 + .../src/Domain/Sync/SyncQueueStrategy.ts | 14 + .../src/Domain/Sync/SyncServiceInterface.ts | 7 + .../services/src/Domain/Sync/SyncSource.ts | 11 + packages/services/src/Domain/index.ts | 51 ++ packages/services/src/index.ts | 1 + packages/services/tsconfig.json | 13 + packages/web/package.json | 2 +- yarn.lock | 41 +- 85 files changed, 2418 insertions(+), 28 deletions(-) delete mode 100644 .yarn/cache/@standardnotes-services-npm-1.13.22-e649cbd9ce-e84f4e43d4.zip delete mode 100644 .yarn/cache/@standardnotes-services-npm-1.13.23-c7085fb4e1-7e67af13c4.zip create mode 100644 packages/services/.eslintignore create mode 100644 packages/services/.eslintrc create mode 100644 packages/services/CHANGELOG.md create mode 100644 packages/services/jest.config.js create mode 100644 packages/services/linter.tsconfig.json create mode 100644 packages/services/package.json create mode 100644 packages/services/src/Domain/Alert/AlertService.ts create mode 100644 packages/services/src/Domain/Api/ApiServiceInterface.ts create mode 100644 packages/services/src/Domain/Application/ApplicationInterface.ts create mode 100644 packages/services/src/Domain/Application/ApplicationStage.ts create mode 100644 packages/services/src/Domain/Application/DeinitCallback.ts create mode 100644 packages/services/src/Domain/Application/DeinitMode.ts create mode 100644 packages/services/src/Domain/Application/DeinitSource.ts create mode 100644 packages/services/src/Domain/Application/UserClientInterface.ts create mode 100644 packages/services/src/Domain/Challenge/ChallengeInterface.ts create mode 100644 packages/services/src/Domain/Challenge/ChallengeResponseInterface.ts create mode 100644 packages/services/src/Domain/Challenge/ChallengeServiceInterface.ts create mode 100644 packages/services/src/Domain/Challenge/Prompt/ChallengePrompt.ts create mode 100644 packages/services/src/Domain/Challenge/Prompt/ChallengePromptInterface.ts create mode 100644 packages/services/src/Domain/Challenge/Prompt/PromptTitles.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeArtifacts.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeKeyboardType.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeRawValue.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeReason.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeValidation.ts create mode 100644 packages/services/src/Domain/Challenge/Types/ChallengeValue.ts create mode 100644 packages/services/src/Domain/Challenge/index.ts create mode 100644 packages/services/src/Domain/Device/DesktopDeviceInterface.ts create mode 100644 packages/services/src/Domain/Device/DesktopWebCommunication.ts create mode 100644 packages/services/src/Domain/Device/DeviceInterface.ts create mode 100644 packages/services/src/Domain/Device/Environments.ts create mode 100644 packages/services/src/Domain/Device/FileBackupsDevice.ts create mode 100644 packages/services/src/Domain/Device/MobileDeviceInterface.ts create mode 100644 packages/services/src/Domain/Device/TypeCheck.spec.ts create mode 100644 packages/services/src/Domain/Device/TypeCheck.ts create mode 100644 packages/services/src/Domain/Device/WebOrDesktopDeviceInterface.ts create mode 100644 packages/services/src/Domain/Diagnostics/ServiceDiagnostics.ts create mode 100644 packages/services/src/Domain/Event/EventObserver.ts create mode 100644 packages/services/src/Domain/Event/SyncEvent.ts create mode 100644 packages/services/src/Domain/Event/SyncEventReceiver.ts create mode 100644 packages/services/src/Domain/FileSystem/FileSystemApi.ts create mode 100644 packages/services/src/Domain/Files/FilesApiInterface.ts create mode 100644 packages/services/src/Domain/Integrity/IntegrityApiInterface.ts create mode 100644 packages/services/src/Domain/Integrity/IntegrityEvent.ts create mode 100644 packages/services/src/Domain/Integrity/IntegrityEventPayload.ts create mode 100644 packages/services/src/Domain/Integrity/IntegrityService.spec.ts create mode 100644 packages/services/src/Domain/Integrity/IntegrityService.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventBus.spec.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventBus.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventBusInterface.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventHandlerInterface.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventInterface.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventPublishStrategy.ts create mode 100644 packages/services/src/Domain/Internal/InternalEventType.ts create mode 100644 packages/services/src/Domain/Item/ItemManagerInterface.ts create mode 100644 packages/services/src/Domain/Item/ItemsServerInterface.ts create mode 100644 packages/services/src/Domain/Payloads/PayloadManagerInterface.ts create mode 100644 packages/services/src/Domain/Preferences/PreferenceServiceInterface.ts create mode 100644 packages/services/src/Domain/Service/AbstractService.ts create mode 100644 packages/services/src/Domain/Service/ServiceInterface.ts create mode 100644 packages/services/src/Domain/Status/StatusService.ts create mode 100644 packages/services/src/Domain/Status/StatusServiceInterface.ts create mode 100644 packages/services/src/Domain/Storage/InMemoryStore.spec.ts create mode 100644 packages/services/src/Domain/Storage/InMemoryStore.ts create mode 100644 packages/services/src/Domain/Storage/KeyValueStoreInterface.ts create mode 100644 packages/services/src/Domain/Storage/StorageKeys.spec.ts create mode 100644 packages/services/src/Domain/Storage/StorageKeys.ts create mode 100644 packages/services/src/Domain/Storage/StorageServiceInterface.ts create mode 100644 packages/services/src/Domain/Storage/StorageTypes.ts create mode 100644 packages/services/src/Domain/Sync/SyncMode.ts create mode 100644 packages/services/src/Domain/Sync/SyncOptions.ts create mode 100644 packages/services/src/Domain/Sync/SyncQueueStrategy.ts create mode 100644 packages/services/src/Domain/Sync/SyncServiceInterface.ts create mode 100644 packages/services/src/Domain/Sync/SyncSource.ts create mode 100644 packages/services/src/Domain/index.ts create mode 100644 packages/services/src/index.ts create mode 100644 packages/services/tsconfig.json 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 a4b1c8418f6507f3933bc0a6985b5a467340c004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76352 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

Wtb~sbSp@N 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 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 c06bd562b94dcf026e54c982ad69d0a424440c8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 76365 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

Wtb~sbSp@N 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

n$E)d=^8vn+z__Bmb~-hwMA)_0TXk;*oky_N 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 M0;VS0m(c(I2d~Gb5&!@I 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:*"