From 9d7d18e7f20b3023aac078017efb16bbbc3da226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20S=C3=B3jko?= Date: Wed, 6 Jul 2022 11:53:39 +0200 Subject: [PATCH] feat: add api package --- .gitignore | 1 + ...s-api-npm-1.1.19-6a6d650ec9-cca168245a.zip | Bin 36928 -> 0 bytes ...curity-npm-1.1.0-9d90b8c189-2098584cd3.zip | Bin 0 -> 26393 bytes packages/api/.eslintignore | 2 + packages/api/.eslintrc | 6 + packages/api/CHANGELOG.md | 118 +++++++++++ packages/api/jest.config.js | 19 ++ packages/api/linter.tsconfig.json | 4 + packages/api/package.json | 44 ++++ packages/api/src/Domain/Api/ApiVersion.ts | 3 + packages/api/src/Domain/Api/index.ts | 1 + .../Domain/Client/User/UserApiService.spec.ts | 77 +++++++ .../src/Domain/Client/User/UserApiService.ts | 45 +++++ .../Client/User/UserApiServiceInterface.ts | 11 + packages/api/src/Domain/Client/index.ts | 2 + packages/api/src/Domain/Error/ApiCallError.ts | 6 + packages/api/src/Domain/Error/ErrorMessage.ts | 7 + packages/api/src/Domain/Error/index.ts | 2 + packages/api/src/Domain/Http/ErrorTag.ts | 11 + .../src/Domain/Http/HttpErrorResponseBody.ts | 8 + packages/api/src/Domain/Http/HttpHeaders.ts | 1 + packages/api/src/Domain/Http/HttpRequest.ts | 13 ++ .../api/src/Domain/Http/HttpRequestParams.ts | 1 + packages/api/src/Domain/Http/HttpResponse.ts | 12 ++ .../api/src/Domain/Http/HttpResponseBody.ts | 1 + .../api/src/Domain/Http/HttpResponseMeta.ts | 12 ++ .../api/src/Domain/Http/HttpService.spec.ts | 27 +++ packages/api/src/Domain/Http/HttpService.ts | 190 ++++++++++++++++++ .../src/Domain/Http/HttpServiceInterface.ts | 11 + .../api/src/Domain/Http/HttpStatusCode.ts | 9 + packages/api/src/Domain/Http/HttpVerb.ts | 7 + .../src/Domain/Http/XMLHttpRequestState.ts | 3 + packages/api/src/Domain/Http/index.ts | 13 ++ .../src/Domain/Request/ApiEndpointParam.ts | 7 + .../User/UserRegistrationRequestParams.ts | 11 + packages/api/src/Domain/Request/index.ts | 2 + .../Response/User/UserRegistrationResponse.ts | 7 + .../User/UserRegistrationResponseBody.ts | 11 + packages/api/src/Domain/Response/index.ts | 2 + packages/api/src/Domain/Server/User/Paths.ts | 9 + .../src/Domain/Server/User/UserServer.spec.ts | 39 ++++ .../api/src/Domain/Server/User/UserServer.ts | 15 ++ .../Domain/Server/User/UserServerInterface.ts | 6 + packages/api/src/Domain/Server/index.ts | 3 + packages/api/src/Domain/index.ts | 7 + packages/api/src/index.ts | 1 + packages/api/tsconfig.json | 13 ++ yarn.lock | 37 +++- 48 files changed, 827 insertions(+), 10 deletions(-) delete mode 100644 .yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip create mode 100644 .yarn/cache/@standardnotes-security-npm-1.1.0-9d90b8c189-2098584cd3.zip create mode 100644 packages/api/.eslintignore create mode 100644 packages/api/.eslintrc create mode 100644 packages/api/CHANGELOG.md create mode 100644 packages/api/jest.config.js create mode 100644 packages/api/linter.tsconfig.json create mode 100644 packages/api/package.json create mode 100644 packages/api/src/Domain/Api/ApiVersion.ts create mode 100644 packages/api/src/Domain/Api/index.ts create mode 100644 packages/api/src/Domain/Client/User/UserApiService.spec.ts create mode 100644 packages/api/src/Domain/Client/User/UserApiService.ts create mode 100644 packages/api/src/Domain/Client/User/UserApiServiceInterface.ts create mode 100644 packages/api/src/Domain/Client/index.ts create mode 100644 packages/api/src/Domain/Error/ApiCallError.ts create mode 100644 packages/api/src/Domain/Error/ErrorMessage.ts create mode 100644 packages/api/src/Domain/Error/index.ts create mode 100644 packages/api/src/Domain/Http/ErrorTag.ts create mode 100644 packages/api/src/Domain/Http/HttpErrorResponseBody.ts create mode 100644 packages/api/src/Domain/Http/HttpHeaders.ts create mode 100644 packages/api/src/Domain/Http/HttpRequest.ts create mode 100644 packages/api/src/Domain/Http/HttpRequestParams.ts create mode 100644 packages/api/src/Domain/Http/HttpResponse.ts create mode 100644 packages/api/src/Domain/Http/HttpResponseBody.ts create mode 100644 packages/api/src/Domain/Http/HttpResponseMeta.ts create mode 100644 packages/api/src/Domain/Http/HttpService.spec.ts create mode 100644 packages/api/src/Domain/Http/HttpService.ts create mode 100644 packages/api/src/Domain/Http/HttpServiceInterface.ts create mode 100644 packages/api/src/Domain/Http/HttpStatusCode.ts create mode 100644 packages/api/src/Domain/Http/HttpVerb.ts create mode 100644 packages/api/src/Domain/Http/XMLHttpRequestState.ts create mode 100644 packages/api/src/Domain/Http/index.ts create mode 100644 packages/api/src/Domain/Request/ApiEndpointParam.ts create mode 100644 packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts create mode 100644 packages/api/src/Domain/Request/index.ts create mode 100644 packages/api/src/Domain/Response/User/UserRegistrationResponse.ts create mode 100644 packages/api/src/Domain/Response/User/UserRegistrationResponseBody.ts create mode 100644 packages/api/src/Domain/Response/index.ts create mode 100644 packages/api/src/Domain/Server/User/Paths.ts create mode 100644 packages/api/src/Domain/Server/User/UserServer.spec.ts create mode 100644 packages/api/src/Domain/Server/User/UserServer.ts create mode 100644 packages/api/src/Domain/Server/User/UserServerInterface.ts create mode 100644 packages/api/src/Domain/Server/index.ts create mode 100644 packages/api/src/Domain/index.ts create mode 100644 packages/api/src/index.ts create mode 100644 packages/api/tsconfig.json diff --git a/.gitignore b/.gitignore index ad60fd8c1..cc8873c68 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ packages/files/dist packages/models/dist packages/services/dist packages/utils/dist +packages/api/dist **/.pnp.* **/.yarn/* diff --git a/.yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip b/.yarn/cache/@standardnotes-api-npm-1.1.19-6a6d650ec9-cca168245a.zip deleted file mode 100644 index dd20ae1a29522f02a48b22e6600097d7a2ffb68b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36928 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

JwCmx8PQS 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 LoMbZP`tqB93&(je literal 0 HcmV?d00001 diff --git a/packages/api/.eslintignore b/packages/api/.eslintignore new file mode 100644 index 000000000..f06235c46 --- /dev/null +++ b/packages/api/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/packages/api/.eslintrc b/packages/api/.eslintrc new file mode 100644 index 000000000..cb7136174 --- /dev/null +++ b/packages/api/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "../../.eslintrc", + "parserOptions": { + "project": "./linter.tsconfig.json" + } +} diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md new file mode 100644 index 000000000..b23bc8e16 --- /dev/null +++ b/packages/api/CHANGELOG.md @@ -0,0 +1,118 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [1.3.0](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.2.1...@standardnotes/api@1.3.0) (2022-07-06) + +### Features + +* remove filepicker, features, services and models packages in favor of standardnotes/app repository ([27f474e](https://github.com/standardnotes/snjs/commit/27f474e859701c5713c8b6ed27cd1a4d5e4392bb)) + +## [1.2.1](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.2.0...@standardnotes/api@1.2.1) (2022-07-05) + +**Note:** Version bump only for package @standardnotes/api + +# [1.2.0](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.20...@standardnotes/api@1.2.0) (2022-07-05) + +### Features + +* remove encryption package in favor of standardnotes/app repository ([f6d1c9e](https://github.com/standardnotes/snjs/commit/f6d1c9ee538bb59ee7ac28c0d49ca682d4eb4d38)) + +## [1.1.20](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.19...@standardnotes/api@1.1.20) (2022-07-04) + +### Bug Fixes + +* add missing reflect-metadata package to all packages ([ce3a5bb](https://github.com/standardnotes/snjs/commit/ce3a5bbf3f1d2276ac4abc3eec3c6a44c8c3ba9b)) + +## [1.1.19](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.18...@standardnotes/api@1.1.19) (2022-06-29) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.18](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.17...@standardnotes/api@1.1.18) (2022-06-28) + +### Bug Fixes + +* setting custom host ([#773](https://github.com/standardnotes/snjs/issues/773)) ([2fe27b0](https://github.com/standardnotes/snjs/commit/2fe27b0324486fad915a91096142579e649995b8)) + +## [1.1.17](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.16...@standardnotes/api@1.1.17) (2022-06-27) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.16](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.15...@standardnotes/api@1.1.16) (2022-06-27) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.15](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.14...@standardnotes/api@1.1.15) (2022-06-23) + +### Bug Fixes + +* set host on user server ([#771](https://github.com/standardnotes/snjs/issues/771)) ([62d2c60](https://github.com/standardnotes/snjs/commit/62d2c60834b20b386b8c60f0ee172aec3e57ec05)) + +## [1.1.14](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.13...@standardnotes/api@1.1.14) (2022-06-22) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.13](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.12...@standardnotes/api@1.1.13) (2022-06-22) + +### Bug Fixes + +* missing dependency ([b748571](https://github.com/standardnotes/snjs/commit/b748571c3288ecec3a2c0aed333ceaaf718832cd)) + +## [1.1.12](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.11...@standardnotes/api@1.1.12) (2022-06-20) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.11](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.10...@standardnotes/api@1.1.11) (2022-06-16) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.10](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.9...@standardnotes/api@1.1.10) (2022-06-16) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.9](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.8...@standardnotes/api@1.1.9) (2022-06-15) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.8](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.7...@standardnotes/api@1.1.8) (2022-06-10) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.7](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.6...@standardnotes/api@1.1.7) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.6](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.5...@standardnotes/api@1.1.6) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.5](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.4...@standardnotes/api@1.1.5) (2022-06-09) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.4](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.3...@standardnotes/api@1.1.4) (2022-06-06) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.3](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.2...@standardnotes/api@1.1.3) (2022-06-03) + +**Note:** Version bump only for package @standardnotes/api + +## [1.1.2](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.1...@standardnotes/api@1.1.2) (2022-06-03) + +### Bug Fixes + +* add option to define additional params to the user registration request if needed ([78f169f](https://github.com/standardnotes/snjs/commit/78f169f993cb0d1950e553fe251f6bc903dd6da8)) + +## [1.1.1](https://github.com/standardnotes/snjs/compare/@standardnotes/api@1.1.0...@standardnotes/api@1.1.1) (2022-06-03) + +### Bug Fixes + +* make response headers optional for ease of defining on the server side ([#755](https://github.com/standardnotes/snjs/issues/755)) ([b5c2092](https://github.com/standardnotes/snjs/commit/b5c2092fc33dbd389fa67c122b3907956ca2eafa)) + +# 1.1.0 (2022-06-03) + +### Features + +* api service refactor -extract registration ([#733](https://github.com/standardnotes/snjs/issues/733)) ([1d7fac8](https://github.com/standardnotes/snjs/commit/1d7fac8c9dbb0fdb78a88743965a33c6d6a7d7d3)) diff --git a/packages/api/jest.config.js b/packages/api/jest.config.js new file mode 100644 index 000000000..aabae3703 --- /dev/null +++ b/packages/api/jest.config.js @@ -0,0 +1,19 @@ +// 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', + }, + }, + coverageThreshold: { + global: { + branches: 17, + functions: 43, + lines: 46, + statements: 46 + } + } +}; diff --git a/packages/api/linter.tsconfig.json b/packages/api/linter.tsconfig.json new file mode 100644 index 000000000..c1a7d22c5 --- /dev/null +++ b/packages/api/linter.tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["dist"] +} diff --git a/packages/api/package.json b/packages/api/package.json new file mode 100644 index 000000000..a9328c5c1 --- /dev/null +++ b/packages/api/package.json @@ -0,0 +1,44 @@ +{ + "name": "@standardnotes/api", + "version": "1.3.0", + "engines": { + "node": ">=16.0.0 <17.0.0" + }, + "description": "Interfaces for dealing with SN server-side API", + "main": "dist/src/index.js", + "author": "Standard Notes", + "types": "dist/src/index.d.ts", + "files": [ + "dist/src" + ], + "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" + }, + "devDependencies": { + "@types/jest": "^27.4.1", + "@types/lodash": "^4.14.182", + "@typescript-eslint/eslint-plugin": "^5.30.0", + "eslint-plugin-prettier": "^4.2.1", + "jest": "^27.5.1", + "ts-jest": "^27.1.3" + }, + "dependencies": { + "@standardnotes/common": "^1.23.1", + "@standardnotes/encryption": "workspace:*", + "@standardnotes/models": "workspace:*", + "@standardnotes/responses": "^1.6.39", + "@standardnotes/security": "^1.1.0", + "@standardnotes/services": "workspace:*", + "reflect-metadata": "^0.1.13" + } +} diff --git a/packages/api/src/Domain/Api/ApiVersion.ts b/packages/api/src/Domain/Api/ApiVersion.ts new file mode 100644 index 000000000..b69ba8f1a --- /dev/null +++ b/packages/api/src/Domain/Api/ApiVersion.ts @@ -0,0 +1,3 @@ +export enum ApiVersion { + v0 = '20200115', +} diff --git a/packages/api/src/Domain/Api/index.ts b/packages/api/src/Domain/Api/index.ts new file mode 100644 index 000000000..0a84a596b --- /dev/null +++ b/packages/api/src/Domain/Api/index.ts @@ -0,0 +1 @@ +export * from './ApiVersion' diff --git a/packages/api/src/Domain/Client/User/UserApiService.spec.ts b/packages/api/src/Domain/Client/User/UserApiService.spec.ts new file mode 100644 index 000000000..f9737d7b6 --- /dev/null +++ b/packages/api/src/Domain/Client/User/UserApiService.spec.ts @@ -0,0 +1,77 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { RootKeyParamsInterface } from '@standardnotes/models' +import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' +import { UserServerInterface } from '../../Server' +import { UserApiService } from './UserApiService' + +describe('UserApiService', () => { + let userServer: UserServerInterface + let keyParams: RootKeyParamsInterface + + const createService = () => new UserApiService(userServer) + + beforeEach(() => { + userServer = {} as jest.Mocked + userServer.register = jest.fn().mockReturnValue({ + data: { user: { email: 'test@test.te', uuid: '1-2-3' } }, + } as jest.Mocked) + + keyParams = {} as jest.Mocked + keyParams.getPortableValue = jest.fn().mockReturnValue({ + identifier: 'test@test.te', + version: ProtocolVersion.V004, + }) + }) + + it('should register a user', async () => { + const response = await createService().register('test@test.te', 'testpasswd', keyParams, false) + + expect(response).toEqual({ + data: { + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + }) + expect(userServer.register).toHaveBeenCalledWith({ + api: '20200115', + email: 'test@test.te', + ephemeral: false, + identifier: 'test@test.te', + password: 'testpasswd', + version: '004', + }) + }) + + it('should not register a user if it is already registering', async () => { + const service = createService() + Object.defineProperty(service, 'registering', { + get: () => true, + }) + + let error = null + try { + await service.register('test@test.te', 'testpasswd', keyParams, false) + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) + + it('should not register a user if the server fails', async () => { + userServer.register = jest.fn().mockImplementation(() => { + throw new Error('Oops') + }) + + let error = null + try { + await createService().register('test@test.te', 'testpasswd', keyParams, false) + } catch (caughtError) { + error = caughtError + } + + expect(error).not.toBeNull() + }) +}) diff --git a/packages/api/src/Domain/Client/User/UserApiService.ts b/packages/api/src/Domain/Client/User/UserApiService.ts new file mode 100644 index 000000000..b4a261b1c --- /dev/null +++ b/packages/api/src/Domain/Client/User/UserApiService.ts @@ -0,0 +1,45 @@ +import { RootKeyParamsInterface } from '@standardnotes/models' + +import { ErrorMessage } from '../../Error/ErrorMessage' +import { ApiCallError } from '../../Error/ApiCallError' +import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' +import { UserServerInterface } from '../../Server/User/UserServerInterface' +import { ApiVersion } from '../../Api/ApiVersion' +import { ApiEndpointParam } from '../../Request/ApiEndpointParam' +import { UserApiServiceInterface } from './UserApiServiceInterface' + +export class UserApiService implements UserApiServiceInterface { + private registering: boolean + + constructor(private userServer: UserServerInterface) { + this.registering = false + } + + async register( + email: string, + serverPassword: string, + keyParams: RootKeyParamsInterface, + ephemeral: boolean, + ): Promise { + if (this.registering) { + throw new ApiCallError(ErrorMessage.RegistrationInProgress) + } + this.registering = true + + try { + const response = await this.userServer.register({ + [ApiEndpointParam.ApiVersion]: ApiVersion.v0, + password: serverPassword, + email, + ephemeral, + ...keyParams.getPortableValue(), + }) + + this.registering = false + + return response + } catch (error) { + throw new ApiCallError(ErrorMessage.GenericRegistrationFail) + } + } +} diff --git a/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts new file mode 100644 index 000000000..35952b73b --- /dev/null +++ b/packages/api/src/Domain/Client/User/UserApiServiceInterface.ts @@ -0,0 +1,11 @@ +import { RootKeyParamsInterface } from '@standardnotes/models' +import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' + +export interface UserApiServiceInterface { + register( + email: string, + serverPassword: string, + keyParams: RootKeyParamsInterface, + ephemeral: boolean, + ): Promise +} diff --git a/packages/api/src/Domain/Client/index.ts b/packages/api/src/Domain/Client/index.ts new file mode 100644 index 000000000..8b0797259 --- /dev/null +++ b/packages/api/src/Domain/Client/index.ts @@ -0,0 +1,2 @@ +export * from './User/UserApiService' +export * from './User/UserApiServiceInterface' diff --git a/packages/api/src/Domain/Error/ApiCallError.ts b/packages/api/src/Domain/Error/ApiCallError.ts new file mode 100644 index 000000000..5726f5961 --- /dev/null +++ b/packages/api/src/Domain/Error/ApiCallError.ts @@ -0,0 +1,6 @@ +export class ApiCallError extends Error { + constructor(message: string) { + super(message) + Object.setPrototypeOf(this, ApiCallError.prototype) + } +} diff --git a/packages/api/src/Domain/Error/ErrorMessage.ts b/packages/api/src/Domain/Error/ErrorMessage.ts new file mode 100644 index 000000000..292e98d2b --- /dev/null +++ b/packages/api/src/Domain/Error/ErrorMessage.ts @@ -0,0 +1,7 @@ +export enum ErrorMessage { + RegistrationInProgress = 'An existing registration request is already in progress.', + GenericRegistrationFail = 'A server error occurred while trying to register. Please try again.', + RateLimited = 'Too many successive server requests. Please wait a few minutes and try again.', + InsufficientPasswordMessage = 'Your password must be at least %LENGTH% characters in length. For your security, please choose a longer password or, ideally, a passphrase, and try again.', + PasscodeRequired = 'Your passcode is required in order to register for an account.', +} diff --git a/packages/api/src/Domain/Error/index.ts b/packages/api/src/Domain/Error/index.ts new file mode 100644 index 000000000..f7961bf34 --- /dev/null +++ b/packages/api/src/Domain/Error/index.ts @@ -0,0 +1,2 @@ +export * from './ApiCallError' +export * from './ErrorMessage' diff --git a/packages/api/src/Domain/Http/ErrorTag.ts b/packages/api/src/Domain/Http/ErrorTag.ts new file mode 100644 index 000000000..1ddc90d31 --- /dev/null +++ b/packages/api/src/Domain/Http/ErrorTag.ts @@ -0,0 +1,11 @@ +export enum ErrorTag { + MfaInvalid = 'mfa-invalid', + MfaRequired = 'mfa-required', + RefreshTokenInvalid = 'invalid-refresh-token', + RefreshTokenExpired = 'expired-refresh-token', + AccessTokenExpired = 'expired-access-token', + ParametersInvalid = 'invalid-parameters', + RevokedSession = 'revoked-session', + AuthInvalid = 'invalid-auth', + ReadOnlyAccess = 'read-only-access', +} diff --git a/packages/api/src/Domain/Http/HttpErrorResponseBody.ts b/packages/api/src/Domain/Http/HttpErrorResponseBody.ts new file mode 100644 index 000000000..788a8d46e --- /dev/null +++ b/packages/api/src/Domain/Http/HttpErrorResponseBody.ts @@ -0,0 +1,8 @@ +import { ErrorTag } from './ErrorTag' + +export type HttpErrorResponseBody = { + error: { + message: string + tag?: ErrorTag + } +} diff --git a/packages/api/src/Domain/Http/HttpHeaders.ts b/packages/api/src/Domain/Http/HttpHeaders.ts new file mode 100644 index 000000000..e93603ae0 --- /dev/null +++ b/packages/api/src/Domain/Http/HttpHeaders.ts @@ -0,0 +1 @@ +export type HttpHeaders = Map diff --git a/packages/api/src/Domain/Http/HttpRequest.ts b/packages/api/src/Domain/Http/HttpRequest.ts new file mode 100644 index 000000000..1f191b69d --- /dev/null +++ b/packages/api/src/Domain/Http/HttpRequest.ts @@ -0,0 +1,13 @@ +import { HttpRequestParams } from './HttpRequestParams' +import { HttpVerb } from './HttpVerb' + +export type HttpRequest = { + url: string + params?: HttpRequestParams + rawBytes?: Uint8Array + verb: HttpVerb + authentication?: string + customHeaders?: Record[] + responseType?: XMLHttpRequestResponseType + external?: boolean +} diff --git a/packages/api/src/Domain/Http/HttpRequestParams.ts b/packages/api/src/Domain/Http/HttpRequestParams.ts new file mode 100644 index 000000000..f0b6807cf --- /dev/null +++ b/packages/api/src/Domain/Http/HttpRequestParams.ts @@ -0,0 +1 @@ +export type HttpRequestParams = Record diff --git a/packages/api/src/Domain/Http/HttpResponse.ts b/packages/api/src/Domain/Http/HttpResponse.ts new file mode 100644 index 000000000..4fa6a6a92 --- /dev/null +++ b/packages/api/src/Domain/Http/HttpResponse.ts @@ -0,0 +1,12 @@ +import { HttpStatusCode } from './HttpStatusCode' +import { HttpResponseBody } from './HttpResponseBody' +import { HttpErrorResponseBody } from './HttpErrorResponseBody' +import { HttpResponseMeta } from './HttpResponseMeta' +import { HttpHeaders } from './HttpHeaders' + +export interface HttpResponse { + status: HttpStatusCode + data?: HttpResponseBody | HttpErrorResponseBody + meta?: HttpResponseMeta + headers?: HttpHeaders +} diff --git a/packages/api/src/Domain/Http/HttpResponseBody.ts b/packages/api/src/Domain/Http/HttpResponseBody.ts new file mode 100644 index 000000000..cc2dcc5de --- /dev/null +++ b/packages/api/src/Domain/Http/HttpResponseBody.ts @@ -0,0 +1 @@ +export type HttpResponseBody = Record diff --git a/packages/api/src/Domain/Http/HttpResponseMeta.ts b/packages/api/src/Domain/Http/HttpResponseMeta.ts new file mode 100644 index 000000000..baebf9db6 --- /dev/null +++ b/packages/api/src/Domain/Http/HttpResponseMeta.ts @@ -0,0 +1,12 @@ +import { Role } from '@standardnotes/security' +import { Uuid } from '@standardnotes/common' + +export type HttpResponseMeta = { + auth: { + userUuid?: Uuid + roles?: Role[] + } + server: { + filesServerUrl?: string + } +} diff --git a/packages/api/src/Domain/Http/HttpService.spec.ts b/packages/api/src/Domain/Http/HttpService.spec.ts new file mode 100644 index 000000000..c0d07669e --- /dev/null +++ b/packages/api/src/Domain/Http/HttpService.spec.ts @@ -0,0 +1,27 @@ +import { Environment } from '@standardnotes/services' +import { HttpResponseMeta } from './HttpResponseMeta' +import { HttpService } from './HttpService' + +describe('HttpService', () => { + const environment = Environment.Web + const appVersion = '1.2.3' + const snjsVersion = '2.3.4' + const host = 'http://bar' + let updateMetaCallback: (meta: HttpResponseMeta) => void + + const createService = () => new HttpService(environment, appVersion, snjsVersion, host, updateMetaCallback) + + beforeEach(() => { + updateMetaCallback = jest.fn() + }) + + it('should set host', () => { + const service = createService() + + expect(service['host']).toEqual('http://bar') + + service.setHost('http://foo') + + expect(service['host']).toEqual('http://foo') + }) +}) diff --git a/packages/api/src/Domain/Http/HttpService.ts b/packages/api/src/Domain/Http/HttpService.ts new file mode 100644 index 000000000..f2dd9bd73 --- /dev/null +++ b/packages/api/src/Domain/Http/HttpService.ts @@ -0,0 +1,190 @@ +import { isString, joinPaths } from '@standardnotes/utils' +import { Environment } from '@standardnotes/services' +import { HttpRequestParams } from './HttpRequestParams' +import { HttpVerb } from './HttpVerb' +import { HttpRequest } from './HttpRequest' +import { HttpResponse } from './HttpResponse' +import { HttpServiceInterface } from './HttpServiceInterface' +import { HttpStatusCode } from './HttpStatusCode' +import { XMLHttpRequestState } from './XMLHttpRequestState' +import { ErrorMessage } from '../Error/ErrorMessage' +import { HttpResponseMeta } from './HttpResponseMeta' +import { HttpErrorResponseBody } from './HttpErrorResponseBody' + +export class HttpService implements HttpServiceInterface { + constructor( + private environment: Environment, + private appVersion: string, + private snjsVersion: string, + private host: string, + private updateMetaCallback: (meta: HttpResponseMeta) => void, + ) {} + + setHost(host: string): void { + this.host = host + } + + async get(path: string, params?: HttpRequestParams, authentication?: string): Promise { + return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Get, authentication }) + } + + async post(path: string, params?: HttpRequestParams, authentication?: string): Promise { + return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Post, authentication }) + } + + async put(path: string, params?: HttpRequestParams, authentication?: string): Promise { + return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Put, authentication }) + } + + async patch(path: string, params: HttpRequestParams, authentication?: string): Promise { + return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Patch, authentication }) + } + + async delete(path: string, params?: HttpRequestParams, authentication?: string): Promise { + return this.runHttp({ url: joinPaths(this.host, path), params, verb: HttpVerb.Delete, authentication }) + } + + private async runHttp(httpRequest: HttpRequest): Promise { + const request = this.createXmlRequest(httpRequest) + + const response = await this.runRequest(request, this.createRequestBody(httpRequest)) + + if (response.meta) { + this.updateMetaCallback(response.meta) + } + + return response + } + + private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined { + if ( + httpRequest.params !== undefined && + [HttpVerb.Post, HttpVerb.Put, HttpVerb.Patch, HttpVerb.Delete].includes(httpRequest.verb) + ) { + return JSON.stringify(httpRequest.params) + } + + return httpRequest.rawBytes + } + + private createXmlRequest(httpRequest: HttpRequest) { + const request = new XMLHttpRequest() + if (httpRequest.params && httpRequest.verb === HttpVerb.Get && Object.keys(httpRequest.params).length > 0) { + httpRequest.url = this.urlForUrlAndParams(httpRequest.url, httpRequest.params) + } + request.open(httpRequest.verb, httpRequest.url, true) + request.responseType = httpRequest.responseType ?? '' + + if (!httpRequest.external) { + request.setRequestHeader('X-SNJS-Version', this.snjsVersion) + + const appVersionHeaderValue = `${Environment[this.environment]}-${this.appVersion}` + request.setRequestHeader('X-Application-Version', appVersionHeaderValue) + + if (httpRequest.authentication) { + request.setRequestHeader('Authorization', 'Bearer ' + httpRequest.authentication) + } + } + + let contenTypeIsSet = false + if (httpRequest.customHeaders && httpRequest.customHeaders.length > 0) { + httpRequest.customHeaders.forEach(({ key, value }) => { + request.setRequestHeader(key, value) + if (key === 'Content-Type') { + contenTypeIsSet = true + } + }) + } + if (!contenTypeIsSet && !httpRequest.external) { + request.setRequestHeader('Content-Type', 'application/json') + } + + return request + } + + private async runRequest(request: XMLHttpRequest, body?: string | Uint8Array): Promise { + return new Promise((resolve, reject) => { + request.onreadystatechange = () => { + this.stateChangeHandlerForRequest(request, resolve, reject) + } + request.send(body) + }) + } + + private stateChangeHandlerForRequest( + request: XMLHttpRequest, + resolve: (response: HttpResponse) => void, + reject: (response: HttpResponse) => void, + ) { + if (request.readyState !== XMLHttpRequestState.Completed) { + return + } + const httpStatus = request.status + const response: HttpResponse = { + status: httpStatus, + headers: new Map(), + } + + const responseHeaderLines = request + .getAllResponseHeaders() + ?.trim() + .split(/[\r\n]+/) + responseHeaderLines?.forEach((responseHeaderLine) => { + const parts = responseHeaderLine.split(': ') + const name = parts.shift() as string + const value = parts.join(': ') + + ;(>response.headers).set(name, value) + }) + + try { + if (httpStatus !== HttpStatusCode.NoContent) { + let body + + const contentTypeHeader = response.headers?.get('content-type') || response.headers?.get('Content-Type') + + if (contentTypeHeader?.includes('application/json')) { + body = JSON.parse(request.responseText) + } else { + body = request.response + } + /** + * v0 APIs do not have a `data` top-level object. In such cases, mimic + * the newer response body style by putting all the top-level + * properties inside a `data` object. + */ + if (!body.data) { + response.data = body + } + if (!isString(body)) { + Object.assign(response, body) + } + } + } catch (error) { + console.error(error) + } + if (httpStatus >= HttpStatusCode.Success && httpStatus < HttpStatusCode.MultipleChoices) { + resolve(response) + } else { + if (httpStatus === HttpStatusCode.Forbidden && response.data && response.data.error !== undefined) { + ;(response.data as HttpErrorResponseBody).error.message = ErrorMessage.RateLimited + } + + reject(response) + } + } + + private urlForUrlAndParams(url: string, params: HttpRequestParams) { + const keyValueString = Object.keys(params) + .map((key) => { + return key + '=' + encodeURIComponent(params[key] as string) + }) + .join('&') + + if (url.includes('?')) { + return url + '&' + keyValueString + } else { + return url + '?' + keyValueString + } + } +} diff --git a/packages/api/src/Domain/Http/HttpServiceInterface.ts b/packages/api/src/Domain/Http/HttpServiceInterface.ts new file mode 100644 index 000000000..281aacd81 --- /dev/null +++ b/packages/api/src/Domain/Http/HttpServiceInterface.ts @@ -0,0 +1,11 @@ +import { HttpRequestParams } from './HttpRequestParams' +import { HttpResponse } from './HttpResponse' + +export interface HttpServiceInterface { + setHost(host: string): void + get(path: string, params?: HttpRequestParams, authentication?: string): Promise + post(path: string, params?: HttpRequestParams, authentication?: string): Promise + put(path: string, params?: HttpRequestParams, authentication?: string): Promise + patch(path: string, params: HttpRequestParams, authentication?: string): Promise + delete(path: string, params?: HttpRequestParams, authentication?: string): Promise +} diff --git a/packages/api/src/Domain/Http/HttpStatusCode.ts b/packages/api/src/Domain/Http/HttpStatusCode.ts new file mode 100644 index 000000000..3a8e7c54f --- /dev/null +++ b/packages/api/src/Domain/Http/HttpStatusCode.ts @@ -0,0 +1,9 @@ +export enum HttpStatusCode { + Success = 200, + NoContent = 204, + MultipleChoices = 300, + BadRequest = 400, + Unauthorized = 401, + Forbidden = 403, + ExpiredAccessToken = 498, +} diff --git a/packages/api/src/Domain/Http/HttpVerb.ts b/packages/api/src/Domain/Http/HttpVerb.ts new file mode 100644 index 000000000..2593b47bd --- /dev/null +++ b/packages/api/src/Domain/Http/HttpVerb.ts @@ -0,0 +1,7 @@ +export enum HttpVerb { + Get = 'GET', + Post = 'POST', + Put = 'PUT', + Patch = 'PATCH', + Delete = 'DELETE', +} diff --git a/packages/api/src/Domain/Http/XMLHttpRequestState.ts b/packages/api/src/Domain/Http/XMLHttpRequestState.ts new file mode 100644 index 000000000..89aeefa5f --- /dev/null +++ b/packages/api/src/Domain/Http/XMLHttpRequestState.ts @@ -0,0 +1,3 @@ +export enum XMLHttpRequestState { + Completed = 4, +} diff --git a/packages/api/src/Domain/Http/index.ts b/packages/api/src/Domain/Http/index.ts new file mode 100644 index 000000000..9b4c93dda --- /dev/null +++ b/packages/api/src/Domain/Http/index.ts @@ -0,0 +1,13 @@ +export * from './ErrorTag' +export * from './HttpErrorResponseBody' +export * from './HttpHeaders' +export * from './HttpRequest' +export * from './HttpRequestParams' +export * from './HttpResponse' +export * from './HttpResponseBody' +export * from './HttpResponseMeta' +export * from './HttpService' +export * from './HttpServiceInterface' +export * from './HttpStatusCode' +export * from './HttpVerb' +export * from './XMLHttpRequestState' diff --git a/packages/api/src/Domain/Request/ApiEndpointParam.ts b/packages/api/src/Domain/Request/ApiEndpointParam.ts new file mode 100644 index 000000000..007ded326 --- /dev/null +++ b/packages/api/src/Domain/Request/ApiEndpointParam.ts @@ -0,0 +1,7 @@ +export enum ApiEndpointParam { + LastSyncToken = 'sync_token', + PaginationToken = 'cursor_token', + SyncDlLimit = 'limit', + SyncPayloads = 'items', + ApiVersion = 'api', +} diff --git a/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts b/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts new file mode 100644 index 000000000..eee43049e --- /dev/null +++ b/packages/api/src/Domain/Request/User/UserRegistrationRequestParams.ts @@ -0,0 +1,11 @@ +import { AnyKeyParamsContent } from '@standardnotes/common' +import { ApiEndpointParam } from '../ApiEndpointParam' +import { ApiVersion } from '../../Api/ApiVersion' + +export type UserRegistrationRequestParams = AnyKeyParamsContent & { + [ApiEndpointParam.ApiVersion]: ApiVersion.v0 + password: string + email: string + ephemeral: boolean + [additionalParam: string]: unknown +} diff --git a/packages/api/src/Domain/Request/index.ts b/packages/api/src/Domain/Request/index.ts new file mode 100644 index 000000000..c3324f422 --- /dev/null +++ b/packages/api/src/Domain/Request/index.ts @@ -0,0 +1,2 @@ +export * from './ApiEndpointParam' +export * from './User/UserRegistrationRequestParams' diff --git a/packages/api/src/Domain/Response/User/UserRegistrationResponse.ts b/packages/api/src/Domain/Response/User/UserRegistrationResponse.ts new file mode 100644 index 000000000..9f83b2960 --- /dev/null +++ b/packages/api/src/Domain/Response/User/UserRegistrationResponse.ts @@ -0,0 +1,7 @@ +import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody' +import { HttpResponse } from '../../Http/HttpResponse' +import { UserRegistrationResponseBody } from './UserRegistrationResponseBody' + +export interface UserRegistrationResponse extends HttpResponse { + data: UserRegistrationResponseBody | HttpErrorResponseBody +} diff --git a/packages/api/src/Domain/Response/User/UserRegistrationResponseBody.ts b/packages/api/src/Domain/Response/User/UserRegistrationResponseBody.ts new file mode 100644 index 000000000..57cbd340f --- /dev/null +++ b/packages/api/src/Domain/Response/User/UserRegistrationResponseBody.ts @@ -0,0 +1,11 @@ +import { Uuid } from '@standardnotes/common' +import { KeyParamsData, SessionBody } from '@standardnotes/responses' + +export type UserRegistrationResponseBody = { + session: SessionBody + key_params: KeyParamsData + user: { + uuid: Uuid + email: string + } +} diff --git a/packages/api/src/Domain/Response/index.ts b/packages/api/src/Domain/Response/index.ts new file mode 100644 index 000000000..a2ed09315 --- /dev/null +++ b/packages/api/src/Domain/Response/index.ts @@ -0,0 +1,2 @@ +export * from './User/UserRegistrationResponse' +export * from './User/UserRegistrationResponseBody' diff --git a/packages/api/src/Domain/Server/User/Paths.ts b/packages/api/src/Domain/Server/User/Paths.ts new file mode 100644 index 000000000..5b9a09fc5 --- /dev/null +++ b/packages/api/src/Domain/Server/User/Paths.ts @@ -0,0 +1,9 @@ +const UserPaths = { + register: '/v1/users', +} + +export const Paths = { + v1: { + ...UserPaths, + }, +} diff --git a/packages/api/src/Domain/Server/User/UserServer.spec.ts b/packages/api/src/Domain/Server/User/UserServer.spec.ts new file mode 100644 index 000000000..43251cae7 --- /dev/null +++ b/packages/api/src/Domain/Server/User/UserServer.spec.ts @@ -0,0 +1,39 @@ +import { ProtocolVersion } from '@standardnotes/common' +import { ApiVersion } from '../../Api' +import { HttpServiceInterface } from '../../Http' +import { UserRegistrationResponse } from '../../Response' +import { UserServer } from './UserServer' + +describe('UserServer', () => { + let httpService: HttpServiceInterface + + const createServer = () => new UserServer(httpService) + + beforeEach(() => { + httpService = {} as jest.Mocked + httpService.post = jest.fn().mockReturnValue({ + data: { user: { email: 'test@test.te', uuid: '1-2-3' } }, + } as jest.Mocked) + }) + + it('should register a user', async () => { + const response = await createServer().register({ + password: 'test', + api: ApiVersion.v0, + email: 'test@test.te', + ephemeral: false, + version: ProtocolVersion.V004, + pw_nonce: 'test', + identifier: 'test@test.te', + }) + + expect(response).toEqual({ + data: { + user: { + email: 'test@test.te', + uuid: '1-2-3', + }, + }, + }) + }) +}) diff --git a/packages/api/src/Domain/Server/User/UserServer.ts b/packages/api/src/Domain/Server/User/UserServer.ts new file mode 100644 index 000000000..ca8f796f7 --- /dev/null +++ b/packages/api/src/Domain/Server/User/UserServer.ts @@ -0,0 +1,15 @@ +import { HttpServiceInterface } from '../../Http/HttpServiceInterface' +import { UserRegistrationRequestParams } from '../../Request/User/UserRegistrationRequestParams' +import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' +import { Paths } from './Paths' +import { UserServerInterface } from './UserServerInterface' + +export class UserServer implements UserServerInterface { + constructor(private httpService: HttpServiceInterface) {} + + async register(params: UserRegistrationRequestParams): Promise { + const response = await this.httpService.post(Paths.v1.register, params) + + return response as UserRegistrationResponse + } +} diff --git a/packages/api/src/Domain/Server/User/UserServerInterface.ts b/packages/api/src/Domain/Server/User/UserServerInterface.ts new file mode 100644 index 000000000..e531f38d9 --- /dev/null +++ b/packages/api/src/Domain/Server/User/UserServerInterface.ts @@ -0,0 +1,6 @@ +import { UserRegistrationRequestParams } from '../../Request/User/UserRegistrationRequestParams' +import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse' + +export interface UserServerInterface { + register(params: UserRegistrationRequestParams): Promise +} diff --git a/packages/api/src/Domain/Server/index.ts b/packages/api/src/Domain/Server/index.ts new file mode 100644 index 000000000..a2af11233 --- /dev/null +++ b/packages/api/src/Domain/Server/index.ts @@ -0,0 +1,3 @@ +export * from './User/Paths' +export * from './User/UserServer' +export * from './User/UserServerInterface' diff --git a/packages/api/src/Domain/index.ts b/packages/api/src/Domain/index.ts new file mode 100644 index 000000000..9fb72a2d0 --- /dev/null +++ b/packages/api/src/Domain/index.ts @@ -0,0 +1,7 @@ +export * from './Api' +export * from './Client' +export * from './Error' +export * from './Http' +export * from './Request' +export * from './Response' +export * from './Server' diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts new file mode 100644 index 000000000..920deacdb --- /dev/null +++ b/packages/api/src/index.ts @@ -0,0 +1 @@ +export * from './Domain' diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json new file mode 100644 index 000000000..f3dac14ef --- /dev/null +++ b/packages/api/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/yarn.lock b/yarn.lock index 730b82bb3..6b9b2d58b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6167,19 +6167,25 @@ __metadata: languageName: unknown linkType: soft -"@standardnotes/api@npm:^1.1.19": - version: 1.1.19 - resolution: "@standardnotes/api@npm:1.1.19" +"@standardnotes/api@^1.1.19, @standardnotes/api@workspace:packages/api": + version: 0.0.0-use.local + resolution: "@standardnotes/api@workspace:packages/api" dependencies: - "@standardnotes/auth": ^3.19.4 "@standardnotes/common": ^1.23.1 - "@standardnotes/encryption": ^1.8.23 + "@standardnotes/encryption": "workspace:*" + "@standardnotes/models": "workspace:*" "@standardnotes/responses": ^1.6.39 - "@standardnotes/services": ^1.13.23 - "@standardnotes/utils": ^1.6.12 - checksum: cca168245a80d333ca6433799a7cbe4a233956cace92b9e9ec45b3f67e4e907ef4f08a9573008bdf2b11a09100dc0381cff820ee5bea384407c2107c494913ba - languageName: node - linkType: hard + "@standardnotes/security": ^1.1.0 + "@standardnotes/services": "workspace:*" + "@types/jest": ^27.4.1 + "@types/lodash": ^4.14.182 + "@typescript-eslint/eslint-plugin": ^5.30.0 + 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/app-monorepo@workspace:.": version: 0.0.0-use.local @@ -7081,6 +7087,17 @@ __metadata: languageName: node linkType: hard +"@standardnotes/security@npm:^1.1.0": + version: 1.1.0 + resolution: "@standardnotes/security@npm:1.1.0" + dependencies: + "@standardnotes/common": ^1.23.1 + jsonwebtoken: ^8.5.1 + reflect-metadata: ^0.1.13 + checksum: 2098584cd3fae7b89c13aba6b110a7717dbcce7121162a29478b0c2e754e37d349d1e869f6d0040b709377ec83d0ab0eeef668e234dedba1652fb760ffdf57cc + languageName: node + linkType: hard + "@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"