Compare commits

...
Author SHA1 Message Date
StandardNotes CI 343c39e873 chore(release): publish
- @standardnotes/[email protected].1
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
2022-12-19 17:43:16 +00:00
Mo 889f245ab9 chore: upgrade web deps and lexical (#2094) 2022-12-19 11:11:32 -06:00
Tony e8ada146e1 docs: update edit link (#2104) 2022-12-19 11:09:16 -06:00
StandardNotes CI c9c5e915ce chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].4
2022-12-19 15:22:56 +00:00
Karol Sójko a1ec13d0b0 fix(snjs): skip subscriptions test suite until fixed 2022-12-19 15:52:32 +01:00
Aman Harwara 31bb03943d refactor: de-couple linking controller from active item (#2108) 2022-12-19 20:08:57 +05:30
StandardNotes CI 2b84c242f7 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].3
2022-12-19 13:49:02 +00:00
Karol Sójko b8bf8ca112 fix(snjs): mark flaky subscription test as skipped until fixed 2022-12-19 14:23:34 +01:00
StandardNotes CI c914b88e2d chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].7
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
2022-12-19 10:06:43 +00:00
Aman Harwara 0169224bf2 chore(ci): trigger publish (skip e2e) 2022-12-19 15:22:08 +05:30
Aman Harwara 4049b19170 Merge branch 'main' of github.com:standardnotes/app 2022-12-19 14:44:46 +05:30
Aman Harwara df4148f824 fix: Fixed issue where the first item in item link autcomplete would not be correctly focused (skip e2e) 2022-12-19 14:44:34 +05:30
Aman Harwara 91f3295cff fix: Fixed white screen issue on mobile 2022-12-19 13:36:16 +05:30
StandardNotes CI c9078f374b chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].1
2022-12-19 07:53:04 +00:00
Karol Sójko bb4f1ff099 fix(snjs): refreshing sessions (#2106)
* fix(snjs): refreshing sessions

* fix(snjs): bring back all tests

* fix(snjs): passing session tokens values

* fix(api): remove redundant specs

* fix(snjs): add projecting sessions to storage values

* fix(snjs): deps tree

* fix(snjs): bring back subscription tests

* fix(snjs): remove only tag for migration tests

* fix(snjs): session specs
2022-12-19 08:28:10 +01:00
StandardNotes CI abdaec89b7 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-12-16 20:09:12 +00:00
Aman Harwara f559442a67 feat: Added "Whats New" indicator to Preferences button (#2107) 2022-12-17 00:35:25 +05:30
StandardNotes CI 51aed0a518 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
2022-12-15 16:01:35 +00:00
Mo 35bb671e31 build: only unpack keytar for macOS to avoid Windows runtime issue (skip e2e) 2022-12-15 09:47:17 -06:00
StandardNotes CI d76b190aba chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-12-15 09:54:12 +00:00
Aman Harwara d6fd85e451 fix(desktop): Fixed issue where the "Record video" modal would error after stopping recording 2022-12-15 14:55:31 +05:30
StandardNotes CI c90087462f chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2022-12-14 16:57:03 +00:00
Mo 58838a44cc ci: trigger desktop build 2022-12-14 10:24:20 -06:00
StandardNotes CI 0223800fdf chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
2022-12-14 15:55:51 +00:00
Mo 318b1fdb0f build: remove entitlements no longer required by newer versions of Electron 2022-12-14 08:54:56 -06:00
StandardNotes CI fb517a91b0 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].0
2022-12-13 17:15:10 +00:00
Mo c7b158eb64 ci(temp): skip subscription tests 2022-12-13 10:50:44 -06:00
Aman Harwara d4b63e4ea6 feat: Ability to record videos and capture photos directly in app by selecting + in Files view (#2095) 2022-12-13 21:41:41 +05:30
Karol Sójko cd8596b14e Revert "fix(snjs): add sleep intervals for subscription sharing tests"
This reverts commit 4e576d7526.
2022-12-13 06:39:23 +01:00
Karol Sójko 6c11766051 chore: upgrade trigger-workflow-and-wait procedure 2022-12-12 13:25:14 +01:00
Karol Sójko 7f41564328 fix(snjs): remove @newrelic/native-metrics by upgrading @standardnotes/settings 2022-12-12 13:23:20 +01:00
Karol Sójko 4e576d7526 fix(snjs): add sleep intervals for subscription sharing tests 2022-12-12 13:22:53 +01:00
StandardNotes CI 317c6555d2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-12-08 19:53:54 +00:00
Aman Harwara 220f62665e feat: Added "Open" button to file upload success notification (#2091) 2022-12-09 00:58:32 +05:30
StandardNotes CI 1dc9086257 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].1
 - @standardnotes/[email protected].6
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
2022-12-08 17:37:43 +00:00
Mo d149b500ee feat: Add current Writing Streak to Daily Notebooks (#2093) 2022-12-08 11:11:13 -06:00
StandardNotes CI ad70bea368 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].5
 - @standardnotes/[email protected].1
2022-12-08 16:09:41 +00:00
Aman Harwara 3bcd969f83 chore: remove unnecessary conditions 2022-12-08 21:12:45 +05:30
Aman Harwara d4af612fea fix: Fixed issue where passcode lock setting was not visible on mobile 2022-12-08 21:00:54 +05:30
Aman Harwara b068b814f2 refactor: allow opting out of toast pause on window blur behavior 2022-12-08 20:49:13 +05:30
StandardNotes CI 51de012236 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-12-08 03:35:46 +00:00
Mo 2694c2f1a9 refactor: class-based photo recorder for Moments 2022-12-07 21:09:23 -06:00
Mo d3609853c6 fix: alignment of Loading text in What's New screen 2022-12-07 20:37:53 -06:00
Mo a7ac715499 feat: Moments will now create a link with the current open note, so you can look back on what you were working on during a moment 2022-12-07 20:34:17 -06:00
StandardNotes CI 5ac93a6bd8 chore(release): publish
- @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].0
2022-12-07 17:40:15 +00:00
Mo be4cc4e605 feat: add ability to convert selection in Super to bulleted or numbered list 2022-12-07 11:14:03 -06:00
StandardNotes CI 2fa3fec9ab chore(release): publish
- @standardnotes/[email protected]
2022-12-07 15:08:32 +00:00
Mo 059ee9ea36 chore: update documentation 2022-12-07 08:39:07 -06:00
Mo 66d08895fe chore: update documentation 2022-12-07 08:35:47 -06:00
StandardNotes CI 518643dc71 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].1
2022-12-07 13:03:02 +00:00
Aman Harwara 5a0c005e8c fix: Fixed issue about note UUID not being selectable in menu 2022-12-07 18:01:07 +05:30
Aman Harwara 45cdab6f6c fix: Fixed workspace menu item rename issue 2022-12-07 17:46:49 +05:30
StandardNotes CI 46245b5569 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
2022-12-07 04:27:17 +00:00
Mo 8b103307cb fix: improve variance between Super header font sizes and weights 2022-12-06 22:01:44 -06:00
Mo a70d1009a3 feat: add Indent/Outdent options in Super when on mobile 2022-12-06 21:56:05 -06:00
Mo a5b1ff9595 fix: Fixed issue where entering a space or punctuation in Super autocomplete would dismiss menu 2022-12-06 21:43:45 -06:00
Mo d691d7a3b3 fix: Fixed issue where Dark theme could be called 'Focus' in Appearance preferences 2022-12-06 21:13:04 -06:00
StandardNotes CI 71675dc6a2 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2022-12-06 19:39:07 +00:00
Aman Harwara 6a1b6cef1b fix: Fixed error when trying to upload file 2022-12-07 00:42:03 +05:30
Karol Sójko 3d8c12a789 fix: invalidate cache on web deploy for both .com and .org domains 2022-12-06 09:34:45 +01:00
StandardNotes CI 370aaecfb8 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
 - @standardnotes/[email protected].2
2022-12-06 01:52:37 +00:00
Mo 68bfb2ff68 chore: fix test 2022-12-05 19:25:46 -06:00
Mo a66bb08c3b fix: fixed issue where popover menus would open with incorrect positioning on desktop 2022-12-05 17:34:41 -06:00
Mo ccde207de8 chore: add deprecation notice on dot org site 2022-12-05 17:20:47 -06:00
Mo d3262b0f01 chore: fix permission 2022-12-05 17:05:34 -06:00
Mo ffbc984c58 fix: fixed issue where camera was not accessible for Moments on Android 2022-12-05 17:01:04 -06:00
StandardNotes CI 979163bae7 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].1
2022-12-05 22:15:54 +00:00
Mo c928f8a281 fix: fixes issue where add button would not be visible at times on iOS (#2087)
* fix: fixes issue where add button would not be visible at times on iOS

* fix: fixes issue where screens could not be navigated with certain window sizes
2022-12-05 15:50:19 -06:00
212 changed files with 3350 additions and 2585 deletions
+1 -1
View File
@@ -64,7 +64,7 @@ jobs:
- name: Run E2E test suite
if: "${{ contains(github.event.head_commit.message, 'skip e2e') == false }}"
uses: convictional/trigger-workflow-and-wait@v1.6.1
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: e2e
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
docker tag standardnotes/snjs:${{ github.sha }} standardnotes/snjs:test
docker push standardnotes/snjs:test
- name: Run E2E test suite
uses: convictional/trigger-workflow-and-wait@v1.6.1
uses: convictional/trigger-workflow-and-wait@master
with:
owner: standardnotes
repo: e2e
+10 -2
View File
@@ -45,10 +45,18 @@ jobs:
aws-region: us-east-1
- name: Deploy static site to S3 bucket
run: aws s3 sync packages/web/dist/ s3://app.standardnotes.com --delete
- name: Invalidate CloudFront Cache
- name: Invalidate CloudFront Cache on .COM
uses: chetan/invalidate-cloudfront-action@master
env:
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_DISTRIBUTION_ID }}
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_COM_DISTRIBUTION_ID }}
PATHS: '/*'
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Invalidate CloudFront Cache on .ORG
uses: chetan/invalidate-cloudfront-action@master
env:
DISTRIBUTION: ${{ secrets.WEBAPP_CLOUDFRONT_ORG_DISTRIBUTION_ID }}
PATHS: '/*'
AWS_REGION: 'us-east-1'
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.20.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/api
## [1.20.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
### Bug Fixes
* **snjs:** refreshing sessions ([#2106](https://github.com/standardnotes/app/issues/2106)) ([bb4f1ff](https://github.com/standardnotes/app/commit/bb4f1ff0990eb5a6f64e76a699fb95540cffdbb4))
## [1.20.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/api
## [1.20.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
**Note:** Version bump only for package @standardnotes/api
-8
View File
@@ -6,12 +6,4 @@ module.exports = {
transform: {
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
},
coverageThreshold: {
global: {
branches: 22,
functions: 69,
lines: 67,
statements: 67
}
}
};
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.20.18",
"version": "1.20.21",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -23,7 +23,7 @@
"build": "tsc -p tsconfig.json",
"lint": "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
"test": "jest spec --coverage"
"test": "jest spec --coverage --passWithNoTests"
},
"devDependencies": {
"@types/jest": "^29.2.3",
@@ -37,6 +37,7 @@
},
"dependencies": {
"@standardnotes/common": "^1.45.0",
"@standardnotes/domain-core": "^1.11.0",
"@standardnotes/encryption": "workspace:*",
"@standardnotes/models": "workspace:*",
"@standardnotes/responses": "workspace:*",
@@ -1,211 +0,0 @@
import { type Invitation } from '@standardnotes/models'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionInviteResponse } from '../../Response/Subscription/SubscriptionInviteResponse'
import { SubscriptionServerInterface } from '../../Server/Subscription/SubscriptionServerInterface'
import { SubscriptionApiOperations } from './SubscriptionApiOperations'
import { SubscriptionApiService } from './SubscriptionApiService'
describe('SubscriptionApiService', () => {
let subscriptionServer: SubscriptionServerInterface
const createService = () => new SubscriptionApiService(subscriptionServer)
beforeEach(() => {
subscriptionServer = {} as jest.Mocked<SubscriptionServerInterface>
subscriptionServer.invite = jest.fn().mockReturnValue({
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
} as jest.Mocked<SubscriptionInviteResponse>)
subscriptionServer.cancelInvite = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteCancelResponse>)
subscriptionServer.listInvites = jest.fn().mockReturnValue({
data: { invitations: [{} as jest.Mocked<Invitation>] },
} as jest.Mocked<SubscriptionInviteListResponse>)
subscriptionServer.acceptInvite = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
})
it('should invite a user', async () => {
const response = await createService().invite('[email protected]')
expect(response).toEqual({
data: {
success: true,
sharedSubscriptionInvitationUuid: '1-2-3',
},
})
expect(subscriptionServer.invite).toHaveBeenCalledWith({
api: '20200115',
identifier: '[email protected]',
})
})
it('should not invite a user if it is already inviting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[SubscriptionApiOperations.Inviting, true]]),
})
let error = null
try {
await service.invite('[email protected]')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not invite a user if the server fails', async () => {
subscriptionServer.invite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().invite('[email protected]')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should cancel an invite', async () => {
const response = await createService().cancelInvite('1-2-3')
expect(response).toEqual({
data: {
success: true,
},
})
expect(subscriptionServer.cancelInvite).toHaveBeenCalledWith({
api: '20200115',
inviteUuid: '1-2-3',
})
})
it('should not cancel an invite if it is already canceling', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[SubscriptionApiOperations.CancelingInvite, true]]),
})
let error = null
try {
await service.cancelInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not cancel an invite if the server fails', async () => {
subscriptionServer.cancelInvite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().cancelInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should list invites', async () => {
const response = await createService().listInvites()
expect(response).toEqual({
data: {
invitations: [{} as jest.Mocked<Invitation>],
},
})
expect(subscriptionServer.listInvites).toHaveBeenCalledWith({
api: '20200115',
})
})
it('should not list invitations if it is already listing', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[SubscriptionApiOperations.ListingInvites, true]]),
})
let error = null
try {
await service.listInvites()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not list invites if the server fails', async () => {
subscriptionServer.listInvites = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().listInvites()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should accept an invite', async () => {
const response = await createService().acceptInvite('1-2-3')
expect(response).toEqual({
data: {
success: true,
},
})
expect(subscriptionServer.acceptInvite).toHaveBeenCalledWith({
inviteUuid: '1-2-3',
})
})
it('should not accept an invite if it is already accepting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[SubscriptionApiOperations.AcceptingInvite, true]]),
})
let error = null
try {
await service.acceptInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not accept an invite if the server fails', async () => {
subscriptionServer.acceptInvite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().acceptInvite('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})
@@ -1,197 +0,0 @@
import { ProtocolVersion, UserRequestType } from '@standardnotes/common'
import { type RootKeyParamsInterface } from '@standardnotes/models'
import { UserDeletionResponse } from '../../Response/User/UserDeletionResponse'
import { UserRegistrationResponse } from '../../Response/User/UserRegistrationResponse'
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
import { UserServerInterface } from '../../Server'
import { UserRequestServerInterface } from '../../Server/UserRequest/UserRequestServerInterface'
import { UserApiOperations } from './UserApiOperations'
import { UserApiService } from './UserApiService'
describe('UserApiService', () => {
let userServer: UserServerInterface
let userRequestServer: UserRequestServerInterface
let keyParams: RootKeyParamsInterface
const createService = () => new UserApiService(userServer, userRequestServer)
beforeEach(() => {
userServer = {} as jest.Mocked<UserServerInterface>
userServer.register = jest.fn().mockReturnValue({
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
} as jest.Mocked<UserRegistrationResponse>)
userServer.deleteAccount = jest.fn().mockReturnValue({
data: { message: 'Success' },
} as jest.Mocked<UserDeletionResponse>)
userRequestServer = {} as jest.Mocked<UserRequestServerInterface>
userRequestServer.submitUserRequest = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<UserRequestResponse>)
keyParams = {} as jest.Mocked<RootKeyParamsInterface>
keyParams.getPortableValue = jest.fn().mockReturnValue({
identifier: '[email protected]',
version: ProtocolVersion.V004,
})
})
it('should register a user', async () => {
const response = await createService().register({
email: '[email protected]',
serverPassword: 'testpasswd',
keyParams,
ephemeral: false,
})
expect(response).toEqual({
data: {
user: {
email: '[email protected]',
uuid: '1-2-3',
},
},
})
expect(userServer.register).toHaveBeenCalledWith({
api: '20200115',
email: '[email protected]',
ephemeral: false,
identifier: '[email protected]',
password: 'testpasswd',
version: '004',
})
})
it('should not register a user if it is already registering', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[UserApiOperations.Registering, true]]),
})
let error = null
try {
await service.register({ email: '[email protected]', serverPassword: 'testpasswd', keyParams, ephemeral: 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({
email: '[email protected]',
serverPassword: 'testpasswd',
keyParams,
ephemeral: false,
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should submit a user request', async () => {
const response = await createService().submitUserRequest({
userUuid: '1-2-3',
requestType: UserRequestType.ExitDiscount,
})
expect(response).toEqual({
data: {
success: true,
},
})
expect(userRequestServer.submitUserRequest).toHaveBeenCalledWith({
userUuid: '1-2-3',
requestType: UserRequestType.ExitDiscount,
})
})
it('should not submit a user request if it is already submitting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[UserApiOperations.SubmittingRequest, true]]),
})
let error = null
try {
await service.submitUserRequest({ userUuid: '1-2-3', requestType: UserRequestType.ExitDiscount })
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not submit a user request if the server fails', async () => {
userRequestServer.submitUserRequest = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().submitUserRequest({
userUuid: '1-2-3',
requestType: UserRequestType.ExitDiscount,
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should delete a user', async () => {
const response = await createService().deleteAccount('1-2-3')
expect(response).toEqual({
data: {
message: 'Success',
},
})
expect(userServer.deleteAccount).toHaveBeenCalledWith({
userUuid: '1-2-3',
})
})
it('should not delete a user if it is already deleting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[UserApiOperations.DeletingAccount, true]]),
})
let error = null
try {
await service.deleteAccount('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not delete a user if the server fails', async () => {
userServer.deleteAccount = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().deleteAccount('1-2-3')
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})
@@ -1,61 +0,0 @@
import { WebSocketConnectionTokenResponse } from '../../Response'
import { WebSocketServerInterface } from '../../Server/WebSocket/WebSocketServerInterface'
import { WebSocketApiOperations } from './WebSocketApiOperations'
import { WebSocketApiService } from './WebSocketApiService'
describe('WebSocketApiService', () => {
let webSocketServer: WebSocketServerInterface
const createService = () => new WebSocketApiService(webSocketServer)
beforeEach(() => {
webSocketServer = {} as jest.Mocked<WebSocketServerInterface>
webSocketServer.createConnectionToken = jest.fn().mockReturnValue({
data: { token: 'foobar' },
} as jest.Mocked<WebSocketConnectionTokenResponse>)
})
it('should create a websocket connection token', async () => {
const response = await createService().createConnectionToken()
expect(response).toEqual({
data: {
token: 'foobar',
},
})
expect(webSocketServer.createConnectionToken).toHaveBeenCalledWith({})
})
it('should not create a token if it is already creating', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WebSocketApiOperations.CreatingConnectionToken, true]]),
})
let error = null
try {
await service.createConnectionToken()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not create a token if the server fails', async () => {
webSocketServer.createConnectionToken = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().createConnectionToken()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})
@@ -1,368 +0,0 @@
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
import { HttpStatusCode } from '../../Http'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
import { WorkspaceServerInterface } from '../../Server/Workspace/WorkspaceServerInterface'
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
import { WorkspaceApiOperations } from './WorkspaceApiOperations'
import { WorkspaceApiService } from './WorkspaceApiService'
describe('WorkspaceApiService', () => {
let workspaceServer: WorkspaceServerInterface
const createService = () => new WorkspaceApiService(workspaceServer)
beforeEach(() => {
workspaceServer = {} as jest.Mocked<WorkspaceServerInterface>
workspaceServer.createWorkspace = jest.fn().mockReturnValue({
data: { uuid: '1-2-3' },
} as jest.Mocked<WorkspaceCreationResponse>)
workspaceServer.inviteToWorkspace = jest.fn().mockReturnValue({
data: { uuid: 'i-1-2-3' },
} as jest.Mocked<WorkspaceInvitationResponse>)
workspaceServer.acceptInvite = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
workspaceServer.listWorkspaces = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
} as jest.Mocked<WorkspaceListResponse>)
workspaceServer.listWorkspaceUsers = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { users: [] },
} as jest.Mocked<WorkspaceUserListResponse>)
workspaceServer.initiateKeyshare = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { success: true },
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
})
it('should create a workspace', async () => {
const response = await createService().createWorkspace({
workspaceType: WorkspaceType.Private,
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
expect(response).toEqual({
data: {
uuid: '1-2-3',
},
})
expect(workspaceServer.createWorkspace).toHaveBeenCalledWith({
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
workspaceType: 'private',
})
})
it('should not create a workspace if it is already creating', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.Creating, true]]),
})
let error = null
try {
await service.createWorkspace({
workspaceType: WorkspaceType.Private,
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not create a workspace if the server fails', async () => {
workspaceServer.createWorkspace = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().createWorkspace({
workspaceType: WorkspaceType.Private,
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should invite to a workspace', async () => {
const response = await createService().inviteToWorkspace({
workspaceUuid: 'w-1-2-3',
inviteeEmail: '[email protected]',
accessLevel: WorkspaceAccessLevel.WriteAndRead,
})
expect(response).toEqual({
data: {
uuid: 'i-1-2-3',
},
})
expect(workspaceServer.inviteToWorkspace).toHaveBeenCalledWith({
workspaceUuid: 'w-1-2-3',
inviteeEmail: '[email protected]',
accessLevel: 'write-and-read',
})
})
it('should not invite to a workspace if it is already inviting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.Inviting, true]]),
})
let error = null
try {
await service.inviteToWorkspace({
workspaceUuid: 'w-1-2-3',
inviteeEmail: '[email protected]',
accessLevel: WorkspaceAccessLevel.WriteAndRead,
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not invite to a workspace if the server fails', async () => {
workspaceServer.inviteToWorkspace = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().inviteToWorkspace({
workspaceUuid: 'w-1-2-3',
inviteeEmail: '[email protected]',
accessLevel: WorkspaceAccessLevel.WriteAndRead,
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should accept invite to a workspace', async () => {
const response = await createService().acceptInvite({
userUuid: 'u-1-2-3',
inviteUuid: 'i-1-2-3',
publicKey: 'foo',
encryptedPrivateKey: 'bar',
})
expect(response).toEqual({
data: {
success: true,
},
})
expect(workspaceServer.acceptInvite).toHaveBeenCalledWith({
userUuid: 'u-1-2-3',
inviteUuid: 'i-1-2-3',
publicKey: 'foo',
encryptedPrivateKey: 'bar',
})
})
it('should not accept invite to a workspace if it is already accepting', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.Accepting, true]]),
})
let error = null
try {
await service.acceptInvite({
userUuid: 'u-1-2-3',
inviteUuid: 'i-1-2-3',
publicKey: 'foo',
encryptedPrivateKey: 'bar',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not accept invite to a workspace if the server fails', async () => {
workspaceServer.acceptInvite = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().acceptInvite({
userUuid: 'u-1-2-3',
inviteUuid: 'i-1-2-3',
publicKey: 'foo',
encryptedPrivateKey: 'bar',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should list workspaces', async () => {
const response = await createService().listWorkspaces()
expect(response).toEqual({
status: 200,
data: {
ownedWorkspaces: [],
joinedWorkspaces: [],
},
})
expect(workspaceServer.listWorkspaces).toHaveBeenCalled()
})
it('should not list workspaces if it is already listing them', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaces, true]]),
})
let error = null
try {
await service.listWorkspaces()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not list workspaces if the server fails', async () => {
workspaceServer.listWorkspaces = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().listWorkspaces()
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should list workspace users', async () => {
const response = await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
expect(response).toEqual({
status: 200,
data: {
users: [],
},
})
expect(workspaceServer.listWorkspaceUsers).toHaveBeenCalledWith({ workspaceUuid: 'w-1-2-3' })
})
it('should not list workspace users if it is already listing them', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.ListingWorkspaceUsers, true]]),
})
let error = null
try {
await service.listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not list workspace users if the server fails', async () => {
workspaceServer.listWorkspaceUsers = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should initiate keyshare in workspace for user', async () => {
const response = await createService().initiateKeyshare({
workspaceUuid: 'w-1-2-3',
userUuid: 'u-1-2-3',
encryptedWorkspaceKey: 'foobar',
})
expect(response).toEqual({
status: 200,
data: {
success: true,
},
})
expect(workspaceServer.initiateKeyshare).toHaveBeenCalledWith({
workspaceUuid: 'w-1-2-3',
userUuid: 'u-1-2-3',
encryptedWorkspaceKey: 'foobar',
})
})
it('should not initiate keyshare in workspace if it is already initiating', async () => {
const service = createService()
Object.defineProperty(service, 'operationsInProgress', {
get: () => new Map([[WorkspaceApiOperations.InitiatingKeyshare, true]]),
})
let error = null
try {
await service.initiateKeyshare({ workspaceUuid: 'w-1-2-3', userUuid: 'u-1-2-3', encryptedWorkspaceKey: 'foobar' })
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
it('should not initiate keyshare in workspace if the server fails', async () => {
workspaceServer.initiateKeyshare = jest.fn().mockImplementation(() => {
throw new Error('Oops')
})
let error = null
try {
await createService().initiateKeyshare({
workspaceUuid: 'w-1-2-3',
userUuid: 'u-1-2-3',
encryptedWorkspaceKey: 'foobar',
})
} catch (caughtError) {
error = caughtError
}
expect(error).not.toBeNull()
})
})
@@ -1,42 +0,0 @@
import { Environment } from '@standardnotes/models'
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 = () => {
const service = new HttpService(environment, appVersion, snjsVersion, updateMetaCallback)
service.setHost(host)
return service
}
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')
})
it('should set and use the authorization token', () => {
const service = createService()
expect(service['authorizationToken']).toBeUndefined()
service.setAuthorizationToken('foo-bar')
expect(service['authorizationToken']).toEqual('foo-bar')
})
})
+76 -10
View File
@@ -1,5 +1,7 @@
import { isString, joinPaths, sleep } from '@standardnotes/utils'
import { Environment } from '@standardnotes/models'
import { Session, SessionToken } from '@standardnotes/domain-core'
import { HttpRequestParams } from './HttpRequestParams'
import { HttpVerb } from './HttpVerb'
import { HttpRequest } from './HttpRequest'
@@ -10,21 +12,26 @@ import { XMLHttpRequestState } from './XMLHttpRequestState'
import { ErrorMessage } from '../Error/ErrorMessage'
import { HttpResponseMeta } from './HttpResponseMeta'
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
import { Paths } from '../Server/Auth/Paths'
import { SessionRefreshResponse } from '../Response/Auth/SessionRefreshResponse'
export class HttpService implements HttpServiceInterface {
private authorizationToken?: string
private session: Session | null
private __latencySimulatorMs?: number
private host!: string
private declare host: string
constructor(
private environment: Environment,
private appVersion: string,
private snjsVersion: string,
private updateMetaCallback: (meta: HttpResponseMeta) => void,
) {}
private refreshSessionCallback: (session: Session) => void,
) {
this.session = null
}
setAuthorizationToken(authorizationToken: string): void {
this.authorizationToken = authorizationToken
setSession(session: Session): void {
this.session = session
}
setHost(host: string): void {
@@ -36,7 +43,7 @@ export class HttpService implements HttpServiceInterface {
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Get,
authentication: authentication ?? this.authorizationToken,
authentication: authentication ?? this.session?.accessToken.value,
})
}
@@ -45,7 +52,7 @@ export class HttpService implements HttpServiceInterface {
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Post,
authentication: authentication ?? this.authorizationToken,
authentication: authentication ?? this.session?.accessToken.value,
})
}
@@ -54,7 +61,7 @@ export class HttpService implements HttpServiceInterface {
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Put,
authentication: authentication ?? this.authorizationToken,
authentication: authentication ?? this.session?.accessToken.value,
})
}
@@ -63,7 +70,7 @@ export class HttpService implements HttpServiceInterface {
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Patch,
authentication: authentication ?? this.authorizationToken,
authentication: authentication ?? this.session?.accessToken.value,
})
}
@@ -72,7 +79,7 @@ export class HttpService implements HttpServiceInterface {
url: joinPaths(this.host, path),
params,
verb: HttpVerb.Delete,
authentication: authentication ?? this.authorizationToken,
authentication: authentication ?? this.session?.accessToken.value,
})
}
@@ -89,9 +96,68 @@ export class HttpService implements HttpServiceInterface {
this.updateMetaCallback(response.meta)
}
if (response.status === HttpStatusCode.ExpiredAccessToken) {
const isSessionRefreshed = await this.refreshSession()
if (!isSessionRefreshed) {
return response
}
httpRequest.authentication = this.session?.accessToken.value
return this.runHttp(httpRequest)
}
return response
}
private async refreshSession(): Promise<boolean> {
if (this.session === null) {
return false
}
const response = (await this.post(joinPaths(this.host, Paths.v1.refreshSession), {
access_token: this.session.accessToken.value,
refresh_token: this.session.refreshToken.value,
})) as SessionRefreshResponse
if (response.data.error) {
return false
}
if (response.meta) {
this.updateMetaCallback(response.meta)
}
const accessTokenOrError = SessionToken.create(
response.data.session.access_token,
response.data.session.access_expiration,
)
if (accessTokenOrError.isFailed()) {
return false
}
const accessToken = accessTokenOrError.getValue()
const refreshTokenOrError = SessionToken.create(
response.data.session.refresh_token,
response.data.session.refresh_expiration,
)
if (refreshTokenOrError.isFailed()) {
return false
}
const refreshToken = refreshTokenOrError.getValue()
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
if (sessionOrError.isFailed()) {
return false
}
this.setSession(sessionOrError.getValue())
this.refreshSessionCallback(this.session)
return true
}
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
if (
httpRequest.params !== undefined &&
@@ -1,9 +1,11 @@
import { Session } from '@standardnotes/domain-core'
import { HttpRequestParams } from './HttpRequestParams'
import { HttpResponse } from './HttpResponse'
export interface HttpServiceInterface {
setHost(host: string): void
setAuthorizationToken(authorizationToken: string): void
setSession(session: Session): void
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
post(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
put(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
@@ -0,0 +1,9 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SessionRefreshResponseBody } from './SessionRefreshResponseBody'
export interface SessionRefreshResponse extends HttpResponse {
data: Either<SessionRefreshResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1,9 @@
export type SessionRefreshResponseBody = {
session: {
access_token: string
refresh_token: string
access_expiration: number
refresh_expiration: number
readonly_access: boolean
}
}
@@ -0,0 +1,9 @@
const SessionPaths = {
refreshSession: '/v1/sessions/refresh',
}
export const Paths = {
v1: {
...SessionPaths,
},
}
@@ -1,106 +0,0 @@
import { type Invitation } from '@standardnotes/models'
import { ApiVersion } from '../../Api'
import { HttpServiceInterface } from '../../Http'
import { SubscriptionInviteResponse } from '../../Response'
import { SubscriptionInviteAcceptResponse } from '../../Response/Subscription/SubscriptionInviteAcceptResponse'
import { SubscriptionInviteCancelResponse } from '../../Response/Subscription/SubscriptionInviteCancelResponse'
import { SubscriptionInviteDeclineResponse } from '../../Response/Subscription/SubscriptionInviteDeclineResponse'
import { SubscriptionInviteListResponse } from '../../Response/Subscription/SubscriptionInviteListResponse'
import { SubscriptionServer } from './SubscriptionServer'
describe('SubscriptionServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new SubscriptionServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
})
it('should invite a user to a shared subscription', async () => {
httpService.post = jest.fn().mockReturnValue({
data: { success: true, sharedSubscriptionInvitationUuid: '1-2-3' },
} as jest.Mocked<SubscriptionInviteResponse>)
const response = await createServer().invite({
api: ApiVersion.v0,
identifier: '[email protected]',
})
expect(response).toEqual({
data: {
success: true,
sharedSubscriptionInvitationUuid: '1-2-3',
},
})
})
it('should accept an invite to a shared subscription', async () => {
httpService.post = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteAcceptResponse>)
const response = await createServer().acceptInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should decline an invite to a shared subscription', async () => {
httpService.get = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteDeclineResponse>)
const response = await createServer().declineInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should cancel an invite to a shared subscription', async () => {
httpService.delete = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<SubscriptionInviteCancelResponse>)
const response = await createServer().cancelInvite({
api: ApiVersion.v0,
inviteUuid: '1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should list invitations to a shared subscription', async () => {
httpService.get = jest.fn().mockReturnValue({
data: { invitations: [{} as jest.Mocked<Invitation>] },
} as jest.Mocked<SubscriptionInviteListResponse>)
const response = await createServer().listInvites({
api: ApiVersion.v0,
})
expect(response).toEqual({
data: {
invitations: [{} as jest.Mocked<Invitation>],
},
})
})
})
@@ -1,54 +0,0 @@
import { ProtocolVersion } from '@standardnotes/common'
import { ApiVersion } from '../../Api'
import { HttpServiceInterface } from '../../Http'
import { UserDeletionResponse, UserRegistrationResponse } from '../../Response'
import { UserServer } from './UserServer'
describe('UserServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new UserServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
httpService.post = jest.fn().mockReturnValue({
data: { user: { email: '[email protected]', uuid: '1-2-3' } },
} as jest.Mocked<UserRegistrationResponse>)
httpService.delete = jest.fn().mockReturnValue({
data: { message: 'Success' },
} as jest.Mocked<UserDeletionResponse>)
})
it('should register a user', async () => {
const response = await createServer().register({
password: 'test',
api: ApiVersion.v0,
email: '[email protected]',
ephemeral: false,
version: ProtocolVersion.V004,
pw_nonce: 'test',
identifier: '[email protected]',
})
expect(response).toEqual({
data: {
user: {
email: '[email protected]',
uuid: '1-2-3',
},
},
})
})
it('should delete a user', async () => {
const response = await createServer().deleteAccount({
userUuid: '1-2-3',
})
expect(response).toEqual({
data: {
message: 'Success',
},
})
})
})
@@ -1,32 +0,0 @@
import { UserRequestType } from '@standardnotes/common'
import { HttpServiceInterface } from '../../Http'
import { UserRequestResponse } from '../../Response/UserRequest/UserRequestResponse'
import { UserRequestServer } from './UserRequestServer'
describe('UserRequestServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new UserRequestServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
httpService.post = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<UserRequestResponse>)
})
it('should submit a user request', async () => {
const response = await createServer().submitUserRequest({
userUuid: '1-2-3',
requestType: UserRequestType.ExitDiscount,
})
expect(response).toEqual({
data: {
success: true,
},
})
})
})
@@ -1,27 +0,0 @@
import { HttpServiceInterface } from '../../Http'
import { WebSocketConnectionTokenResponse } from '../../Response'
import { WebSocketServer } from './WebSocketServer'
describe('WebSocketServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new WebSocketServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
httpService.post = jest.fn().mockReturnValue({
data: { token: 'foobar' },
} as jest.Mocked<WebSocketConnectionTokenResponse>)
})
it('should create a websocket connection token', async () => {
const response = await createServer().createConnectionToken({})
expect(response).toEqual({
data: {
token: 'foobar',
},
})
})
})
@@ -1,124 +0,0 @@
import { WorkspaceAccessLevel, WorkspaceType } from '@standardnotes/common'
import { HttpServiceInterface, HttpStatusCode } from '../../Http'
import { WorkspaceCreationResponse } from '../../Response/Workspace/WorkspaceCreationResponse'
import { WorkspaceInvitationAcceptingResponse } from '../../Response/Workspace/WorkspaceInvitationAcceptingResponse'
import { WorkspaceInvitationResponse } from '../../Response/Workspace/WorkspaceInvitationResponse'
import { WorkspaceKeyshareInitiatingResponse } from '../../Response/Workspace/WorkspaceKeyshareInitiatingResponse'
import { WorkspaceListResponse } from '../../Response/Workspace/WorkspaceListResponse'
import { WorkspaceUserListResponse } from '../../Response/Workspace/WorkspaceUserListResponse'
import { WorkspaceServer } from './WorkspaceServer'
describe('WorkspaceServer', () => {
let httpService: HttpServiceInterface
const createServer = () => new WorkspaceServer(httpService)
beforeEach(() => {
httpService = {} as jest.Mocked<HttpServiceInterface>
httpService.post = jest.fn().mockReturnValue({
data: { uuid: '1-2-3' },
} as jest.Mocked<WorkspaceCreationResponse>)
})
it('should create a workspace', async () => {
const response = await createServer().createWorkspace({
workspaceType: WorkspaceType.Private,
encryptedPrivateKey: 'foo',
encryptedWorkspaceKey: 'bar',
publicKey: 'buzz',
})
expect(response).toEqual({
data: {
uuid: '1-2-3',
},
})
})
it('should inivte to a workspace', async () => {
httpService.post = jest.fn().mockReturnValue({
data: { uuid: 'i-1-2-3' },
} as jest.Mocked<WorkspaceInvitationResponse>)
const response = await createServer().inviteToWorkspace({
inviteeEmail: '[email protected]',
workspaceUuid: 'w-1-2-3',
accessLevel: WorkspaceAccessLevel.WriteAndRead,
})
expect(response).toEqual({
data: {
uuid: 'i-1-2-3',
},
})
})
it('should accept invitation to a workspace', async () => {
httpService.post = jest.fn().mockReturnValue({
data: { success: true },
} as jest.Mocked<WorkspaceInvitationAcceptingResponse>)
const response = await createServer().acceptInvite({
encryptedPrivateKey: 'foo',
inviteUuid: 'i-1-2-3',
publicKey: 'bar',
userUuid: 'u-1-2-3',
})
expect(response).toEqual({
data: {
success: true,
},
})
})
it('should list workspaces', async () => {
httpService.get = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
} as jest.Mocked<WorkspaceListResponse>)
const response = await createServer().listWorkspaces({})
expect(response).toEqual({
status: 200,
data: { ownedWorkspaces: [], joinedWorkspaces: [] },
})
})
it('should list workspace users', async () => {
httpService.get = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { users: [] },
} as jest.Mocked<WorkspaceUserListResponse>)
const response = await createServer().listWorkspaceUsers({ workspaceUuid: 'w-1-2-3' })
expect(response).toEqual({
status: 200,
data: { users: [] },
})
})
it('should initiate keyshare for user in a workspace', async () => {
httpService.post = jest.fn().mockReturnValue({
status: HttpStatusCode.Success,
data: { success: true },
} as jest.Mocked<WorkspaceKeyshareInitiatingResponse>)
const response = await createServer().initiateKeyshare({
workspaceUuid: 'w-1-2-3',
userUuid: 'u-1-2-3',
encryptedWorkspaceKey: 'foobar',
})
expect(response).toEqual({
status: 200,
data: {
success: true,
},
})
})
})
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.10.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/blocks-editor
# [1.10.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
### Features
* add ability to convert selection in Super to bulleted or numbered list ([be4cc4e](https://github.com/standardnotes/app/commit/be4cc4e605100e0a86de214e548e9ed10e2568aa))
## [1.9.7](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
### Bug Fixes
* improve variance between Super header font sizes and weights ([8b10330](https://github.com/standardnotes/app/commit/8b103307cba41855713b8627cf5d66a6fef6ff52))
## [1.9.6](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-30)
### Bug Fixes
+4 -4
View File
@@ -1,17 +1,17 @@
{
"name": "@standardnotes/blocks-editor",
"version": "1.9.6",
"version": "1.10.1",
"private": true,
"main": "./src/index.ts",
"scripts": {
"tsc": "tsc -p tsconfig.json"
},
"dependencies": {
"@lexical/react": "^0.6.0",
"@lexical/react": "^0.6.5",
"@standardnotes/icons": "workspace:*",
"@types/react": "^18.0.25",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9",
"lexical": "^0.6.0",
"lexical": "^0.6.5",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -11,7 +11,11 @@ import './index.css';
import {$isCodeHighlightNode} from '@lexical/code';
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
import {mergeRegister} from '@lexical/utils';
import {
mergeRegister,
$findMatchingParent,
$getNearestNodeOfType,
} from '@lexical/utils';
import {
$getSelection,
$isRangeSelection,
@@ -20,7 +24,17 @@ import {
FORMAT_TEXT_COMMAND,
LexicalEditor,
SELECTION_CHANGE_COMMAND,
$isRootOrShadowRoot,
COMMAND_PRIORITY_CRITICAL,
} from 'lexical';
import {$isHeadingNode} from '@lexical/rich-text';
import {
INSERT_UNORDERED_LIST_COMMAND,
REMOVE_LIST_COMMAND,
$isListNode,
ListNode,
INSERT_ORDERED_LIST_COMMAND,
} from '@lexical/list';
import {useCallback, useEffect, useRef, useState} from 'react';
import {createPortal} from 'react-dom';
@@ -36,9 +50,26 @@ import {
LinkIcon,
SuperscriptIcon,
SubscriptIcon,
ListBulleted,
ListNumbered,
} from '@standardnotes/icons';
import {IconComponent} from '../../Theme/IconComponent';
const blockTypeToBlockName = {
bullet: 'Bulleted List',
check: 'Check List',
code: 'Code Block',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
number: 'Numbered List',
paragraph: 'Normal',
quote: 'Quote',
};
const IconSize = 15;
function TextFormatFloatingToolbar({
@@ -52,6 +83,8 @@ function TextFormatFloatingToolbar({
isStrikethrough,
isSubscript,
isSuperscript,
isBulletedList,
isNumberedList,
}: {
editor: LexicalEditor;
anchorElem: HTMLElement;
@@ -63,6 +96,8 @@ function TextFormatFloatingToolbar({
isSubscript: boolean;
isSuperscript: boolean;
isUnderline: boolean;
isBulletedList: boolean;
isNumberedList: boolean;
}): JSX.Element {
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
@@ -74,6 +109,22 @@ function TextFormatFloatingToolbar({
}
}, [editor, isLink]);
const formatBulletList = useCallback(() => {
if (!isBulletedList) {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
}, [isBulletedList]);
const formatNumberedList = useCallback(() => {
if (!isNumberedList) {
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
} else {
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
}
}, [isNumberedList]);
const updateTextFormatFloatingToolbar = useCallback(() => {
const selection = $getSelection();
@@ -226,6 +277,22 @@ function TextFormatFloatingToolbar({
<LinkIcon />
</IconComponent>
</button>
<button
onClick={formatBulletList}
className={'popup-item spaced ' + (isBulletedList ? 'active' : '')}
aria-label="Insert bulleted list">
<IconComponent size={IconSize}>
<ListBulleted />
</IconComponent>
</button>
<button
onClick={formatNumberedList}
className={'popup-item spaced ' + (isNumberedList ? 'active' : '')}
aria-label="Insert numbered list">
<IconComponent size={IconSize}>
<ListNumbered />
</IconComponent>
</button>
</>
)}
</div>
@@ -236,6 +303,7 @@ function useFloatingTextFormatToolbar(
editor: LexicalEditor,
anchorElem: HTMLElement,
): JSX.Element | null {
const [activeEditor, setActiveEditor] = useState(editor);
const [isText, setIsText] = useState(false);
const [isLink, setIsLink] = useState(false);
const [isBold, setIsBold] = useState(false);
@@ -245,6 +313,8 @@ function useFloatingTextFormatToolbar(
const [isSubscript, setIsSubscript] = useState(false);
const [isSuperscript, setIsSuperscript] = useState(false);
const [isCode, setIsCode] = useState(false);
const [blockType, setBlockType] =
useState<keyof typeof blockTypeToBlockName>('paragraph');
const updatePopup = useCallback(() => {
editor.getEditorState().read(() => {
@@ -270,6 +340,42 @@ function useFloatingTextFormatToolbar(
return;
}
const anchorNode = selection.anchor.getNode();
let element =
anchorNode.getKey() === 'root'
? anchorNode
: $findMatchingParent(anchorNode, (e) => {
const parent = e.getParent();
return parent !== null && $isRootOrShadowRoot(parent);
});
if (element === null) {
element = anchorNode.getTopLevelElementOrThrow();
}
const elementKey = element.getKey();
const elementDOM = activeEditor.getElementByKey(elementKey);
if (elementDOM !== null) {
if ($isListNode(element)) {
const parentList = $getNearestNodeOfType<ListNode>(
anchorNode,
ListNode,
);
const type = parentList
? parentList.getListType()
: element.getListType();
setBlockType(type);
} else {
const type = $isHeadingNode(element)
? element.getTag()
: element.getType();
if (type in blockTypeToBlockName) {
setBlockType(type as keyof typeof blockTypeToBlockName);
}
}
}
const node = getSelectedNode(selection);
// Update text format
@@ -298,14 +404,19 @@ function useFloatingTextFormatToolbar(
setIsText(false);
}
});
}, [editor]);
}, [editor, activeEditor]);
useEffect(() => {
document.addEventListener('selectionchange', updatePopup);
return () => {
document.removeEventListener('selectionchange', updatePopup);
};
}, [updatePopup]);
return editor.registerCommand(
SELECTION_CHANGE_COMMAND,
(_payload, newEditor) => {
setActiveEditor(newEditor);
updatePopup();
return false;
},
COMMAND_PRIORITY_CRITICAL,
);
}, [editor, updatePopup]);
useEffect(() => {
return mergeRegister(
@@ -336,6 +447,8 @@ function useFloatingTextFormatToolbar(
isSuperscript={isSuperscript}
isUnderline={isUnderline}
isCode={isCode}
isBulletedList={blockType === 'bullet'}
isNumberedList={blockType === 'number'}
/>,
anchorElem,
);
@@ -20,19 +20,20 @@
padding-left: 16px;
}
.Lexical__h1 {
font-size: 24px;
font-size: 26px;
color: var(--sn-stylekit-editor-foreground-color);
font-weight: 400;
font-weight: 700;
margin: 0;
}
.Lexical__h2 {
font-size: 15px;
font-size: 22px;
color: var(--sn-stylekit-editor-foreground-color);
font-weight: 700;
margin: 0;
}
.Lexical__h3 {
font-size: 12px;
font-size: 19px;
font-weight: 700;
margin: 0;
}
.Lexical__textBold {
+86
View File
@@ -3,6 +3,92 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.104.31](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.30](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.29](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.28](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-16)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.25](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.24](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-15)
### Bug Fixes
* **desktop:** Fixed issue where the "Record video" modal would error after stopping recording ([d6fd85e](https://github.com/standardnotes/app/commit/d6fd85e451962e539bada9e207aa0ec65a92f511))
## [3.104.23](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.22](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-14)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.21](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-13)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.20](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.19](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.18](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.17](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.16](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.15](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.14](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -12,7 +12,7 @@
default-src 'self' blob:;
script-src 'self' 'unsafe-eval';
worker-src blob:;
connect-src * data:;
connect-src * data: blob:;
style-src 'unsafe-inline' 'self' http://localhost:* http://127.0.0.1:45653;
frame-src * blob:;
"
@@ -4,12 +4,6 @@
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
+4 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.104.10",
"version": "3.104.31",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
@@ -107,6 +107,9 @@
"extendInfo": {
"NSCameraUsageDescription": "Standard Notes requires access to your camera to enable the Moments feature."
},
"asarUnpack": [
"node_modules/keytar"
],
"target": [
"dmg",
"zip"
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-19)
**Note:** Version bump only for package @standardnotes/docs
## [0.2.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-07)
**Note:** Version bump only for package @standardnotes/docs
## [0.2.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-11-15)
**Note:** Version bump only for package @standardnotes/docs
+49 -2
View File
@@ -90,7 +90,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
--sn-stylekit-sans-serif-font: -apple-system, BlinkMacSystemFont, 'Segoe UI',
'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
'Helvetica Neue', sans-serif;
--sn-stylekit-grey-1: #72767e;
--sn-stylekit-grey-2: #bbbec4;
--sn-stylekit-grey-3: #dfe1e4;
@@ -98,7 +98,7 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
--sn-stylekit-grey-4-opacity-variant: #bbbec43d;
--sn-stylekit-grey-5: #f4f5f7;
--sn-stylekit-grey-6: #e5e5e5;
--sn-stylekit-accessory-tint-color-1: #086dd6;
--sn-stylekit-accessory-tint-color-2: #ea6595;
--sn-stylekit-accessory-tint-color-3: #ebad00;
@@ -108,6 +108,53 @@ _Note that font and font sizes do not apply to mobile; only desktop/web._
}
```
### Optional Overrides
```css
--modal-background-color: var(--sn-stylekit-background-color);
--editor-header-bar-background-color: var(--sn-stylekit-background-color);
--editor-background-color: var(--sn-stylekit-editor-background-color);
--editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
--editor-title-bar-border-bottom-color: var(--sn-stylekit-border-color);
--editor-title-input-color: var(--sn-stylekit-editor-foreground-color);
--editor-pane-background-color: var(--sn-stylekit-background-color);
--editor-pane-editor-background-color: var(--sn-stylekit-editor-background-color);
--editor-pane-editor-foreground-color: var(--sn-stylekit-editor-foreground-color);
--editor-pane-component-stack-item-background-color: var(--sn-stylekit-background-color);
--text-selection-color: var(--sn-stylekit-info-contrast-color);
--text-selection-background-color: var(--sn-stylekit-info-color);
--note-preview-progress-color: var(--sn-stylekit-info-color);
--note-preview-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
--note-preview-selected-progress-color: var(--sn-stylekit-secondary-background-color);
--note-preview-selected-progress-background-color: var(--sn-stylekit-passive-color-4-opacity-variant);
--items-column-background-color: var(--sn-stylekit-background-color);
--items-column-items-background-color: var(--sn-stylekit-background-color);
--items-column-border-left-color: var(--sn-stylekit-border-color);
--items-column-border-right-color: var(--sn-stylekit-border-color);
--items-column-search-background-color: var(--sn-stylekit-contrast-background-color);
--item-cell-selected-background-color: var(--sn-stylekit-contrast-background-color);
--item-cell-selected-border-left-color: var(--sn-stylekit-info-color);
--navigation-column-background-color: var(--sn-stylekit-secondary-background-color);
--navigation-section-title-color: var(--sn-stylekit-secondary-foreground-color);
--navigation-item-text-color: var(--sn-stylekit-secondary-foreground-color);
--navigation-item-count-color: var(--sn-stylekit-neutral-color);
--navigation-item-selected-background-color: rgb(253, 253, 253);
--preferences-navigation-icon-color: var(--sn-stylekit-neutral-color);
--preferences-navigation-selected-background-color: var(--sn-stylekit-info-backdrop-color);
--dropdown-menu-radio-button-inactive-color: var(--sn-stylekit-passive-color-1);
--panel-resizer-background-color: var(--sn-stylekit-secondary-contrast-background-color);
--link-element-color: var(--sn-stylekit-info-color);
```
In order to get SN to display a dock icon for your theme (a circle in the lower right corner of the app that allows you to quickly toggle themes), add the following payload into the your ext.json file when [publishing your theme](/extensions/publishing):
```json
-303
View File
@@ -1,303 +0,0 @@
---
id: auth
title: Auth API
sidebar_label: Auth
description: Authentication portion of the Standard Notes API.
keywords:
- standard notes
- docs
- notes app
- end-to-end encryption
- sync
image: /img/logo.png
hide_title: false
hide_table_of_contents: false
---
### POST /auth
**Registers a new user**
| | | |
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
| created | `string` | Integer timestamp representing the date the client generated the account's encryption keys. Example "1622494310383" |
| email | `string` | The account email. |
| ephemeral | `boolean` | Whether the initial session created for this account is ephemeral ("Stay signed in" unchecked). |
| identifier | `string` | Should equal the account email. |
| origination | `string` | Should equal "registration" |
| password | `string` | The server password generated by the client from the user's master password. |
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
#### Response
```JSON
{
"session": {
"access_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:WwKgTEDUoEhxECu6",
"refresh_token": "1:457b1f4b-88c2-4328-bf32-fe7dd9431d62:G-qEoZsFtoj~RLL-",
"access_expiration": 1627678312000,
"refresh_expiration": 1654051238000
},
"key_params": {
"created": "1622494310383",
"identifier": "[email protected]",
"origination": "registration",
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
"version": "004"
},
"user": {
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
"email": "[email protected]"
}
}
```
### POST /auth/sign_in
**Authenticates a user and returns a session.**
| | | |
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
| email | `string` | The account email. |
| ephemeral | `boolean` | Whether the session created for this account is ephemeral ("Stay signed in" unchecked). |
| password | `string` | The server password generated by the client from the user's master password. |
#### Response
```JSON
{
"session": {
"access_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Q~-UoIpDhdtkii_t",
"refresh_token": "1:ee03808d-dd05-417c-9c87-d471e49bcc49:Jn8UWzeHx2H5nZI5",
"access_expiration": 1627678779000,
"refresh_expiration": 1654051705000
},
"key_params": {
"created": "1622494310383",
"identifier": "[email protected]",
"origination": "registration",
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
"version": "004"
},
"user": {
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
"email": "[email protected]"
}
}
```
### GET /auth/params
**Queries the parameters used for key generation for an email address. Queried before signing into an account.**
| | | |
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
| email | `string` | The account email. |
#### Response
```JSON
{
"identifier": "[email protected]",
"pw_nonce": "d97ed41c581fe8c3e0dce7d2ee72afcb63f9f461ae875bae66e30ecf3d952900",
"version": "004"
}
```
### POST /auth/change_pw
**Changes a user's password.**
| | | |
|-------------|---------|---------------------------------------------------------------------------------------------------------------------|
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115" |
| created | `string` | Integer timestamp representing the date the client generated the account's new encryption keys. Example "1622494310383" |
| identifier | `string` | The account email. |
| origination | `string` | Should equal "password-change" |
| current_password | `string` | The old server password generated by the client from the user's master password. |
| new_password | `string` | The new server password generated by the client from the user's master password. |
| pw_nonce | `string` | The nonce generated by the client for the user's encryption key. |
| version | `string` | The protocol version the client used to generate the user's encryption key. Latest is "004". |
#### Response
```JSON
{
"session": {
"access_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:HMgmZwV5k5ePt0vj",
"refresh_token": "1:27d4fd8f-b730-4e0a-afd3-1600fb466aaa:wHuTeOB-qWGP3AE3",
"access_expiration": 1627679129000,
"refresh_expiration": 1654052055000
},
"key_params": {
"created": "1622494310383",
"identifier": "[email protected]",
"origination": "password-change",
"pw_nonce": "be1974ff6fb1c541aa8c71fd3c66851b6492cf224b661c72daf44e0bef3096bb",
"version": "004"
},
"user": {
"uuid": "682f9deb-b75f-4d97-91fa-6fd82a482db1",
"email": "[email protected]"
}
}
```
## Sessions
### Session Model
| Field | Type | Description |
|----------------|-----------|---------------------------------------------------|
| uuid | `string` | Unique identifier of the session |
| user_uuid | `string` | Unique identifier of the user |
| user_agent | `string` | The user agent used to create the session |
| api_version | `string` | The server API version used to create the session |
| access_token | `string` | The access token used to authenticate requests |
| refresh_token | `string` | The refresh token used to extend tokens |
| access_expiration | `datetime` | The expiration time of the access token |
| refresh_expiration | `datetime` | The expiration time of the refresh token |
| created_at | `datetime` | Date and time of creation of the session |
| updated_at | `datetime` | Last updated date and time of the session |
- Each `session` includes the API version they were created with. This way we can deny sessions for a given API version if we detect a vulnerability with that version in the future
- Sessions are created in the following cases:
- When a user signs in
- When a user registers a new account
---
### Authenticated requests
The `Authorization` request header field is used by clients to make authenticated request. `Bearer` is the only authentication scheme allowed.
The client must send the `access` token generated by the session, in the `Authorization` header. For example:
```
GET /sessions HTTP/1.1
Host: sync.standardnotes.org
Authorization: Bearer <access token>
```
---
Below is a list of endpoints related to session management:
| Method | URL | Params | Description | Successful response code |
|-----------|--------------------------|---------------------|--------------------------------------------------------|--------------------------|
|`POST` | /auth/sign_out | *None* | Terminates the current session | `204` |
|`DELETE` | /session | **uuid** | Terminates the specified session by UUID | `204` |
|`DELETE` | /sessions | *None* | Terminates all sessions, except the current one | `204` |
|`GET` | /sessions | *None* | Lists all sessions active sessions | `200` |
|`POST` | /session/token/refresh | **refresh_token** | Obtains new pair of `access_token` and `refresh_token` | `200` |
A successful request to `GET /sessions` returns the following JSON response:
```json
{
"sessions": [
{
"uuid": "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
"user_agent": "<product> / <product-version> <comment>",
"api_version": "xxxxyyzz",
"current": "<boolean>",
"created_at": "2020-01-01T00:00:00.000Z"
}
...
]
}
```
#### Obtaining tokens
Tokens can be obtained every time the user performs any of the following actions:
1. When a user signs in
1. When a user registers an account
1. When the tokens are refreshed
#### Refreshing tokens
When an expired `access_token` is provided in the `Authorization` HTTP header, the following JSON response is returned:
HTTP Status Code: `498 Expired Access Token`
```json
{
"error": {
"tag": "expired-access-token",
"message": "The provided access token has expired."
}
}
```
To continue accessing resources, the `access_token` must be refreshed. That is, the current `access_token` is replaced with a new one with an extended expiration date.
To refresh an `access_token`, a valid `refresh_token` is needed. This `refresh_token` must meet the following requirements:
- It should belong to the session of the `access_token`
- It should not be expired
Since the `refresh_token` is of single-use, a new `refresh_token` is obtained when the `access_token` is refreshed.
Refreshing tokens is a process that is transparent to the user, meaning that the client will perform the requests to keep valid tokens without user intervention.
Here's how to refresh tokens:
1. Send a `POST` request to `/session/token/refresh`. The body should contain a JSON paylaod with the `refresh_token`:
```
POST /session/token/refresh HTTP/1.1
Host: sync.standardnotes.org
Authorization: Bearer <access token>
{
"refresh_token": "R_xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx"
}
```
1. The `refresh_token` is validated on the server. Depending of the circumstances, there should be two outcomes:
1. The provided `refresh_token` is valid. If so, a new pair of tokens is generated and the following JSON response is returned:
```json
{
"token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
"session": {
"refresh_expiration": 1583020800,
"refresh_token": "xxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx",
}
}
```
1. The provided `refresh_token` is expired. If so, the following JSON response is returned:
HTTP Status Code: `400 Bad Request`
```json
{
"error": {
"tag": "expired-refresh-token",
"message": "The refresh token has expired."
}
}
```
User must start a new session by re-entering their credentials.
### Expiration
#### Sessions
Sessions should be terminated after a period of inactivity. This is for best security practices.
`Long-lived sessions` are a good choice for our use case, because it can build a better user experience than expiring sessions for a short idle-timeout.
- A `session` that remains inactive for `1 year` will be terminated, along with all `tokens`
#### Tokens
| Name | Type | Expiration |
|-----------|-------------|---------------|
| `access` | Short-lived | `60 days` |
| `refresh` | Long-lived | `1 year` |
- A `refresh` token is of single-use, while an `access` token can be used as long as it is not expired
-118
View File
@@ -1,118 +0,0 @@
---
id: sync
title: Sync API
sidebar_label: Sync
description: Specification for the Standard Notes Sync API.
keywords:
- standard notes
- docs
- notes app
- end-to-end encryption
- sync
image: /img/logo.png
hide_title: false
hide_table_of_contents: false
---
This document outlines the client-server communication of the Standard Notes ecosystem.
## Item
An `Item` is largely the only model that both clients and server are concerned wtih. The `content` field stores an encrypted object that can be anything the client needs to operate.
Relationships between items are handled by the client, and not the server, and are stored encrypted in the `content` field as `references`.
Items have the following properties:
| name | type | description |
| ------------ | ----------------------------------- | -------------------------------------------------------------------------------------------- |
| uuid | `string` (or uuid for some databases) | The unique identifier for this item. |
| content | `text` | An encrypted string generated by the client. |
| content_type | `string` | The type of data contained in the `content` field. (i.e Note, Tag, SN|Component, etc.) |
| enc_item_key | `text` | The key used to encrypt `content`, itself also encrypted. |
| deleted | `boolean` | Whether the model has been deleted. |
| created_at_timestamp | `integer` | Timestamp representing when the item was created, with microsecond precision. |
| updated_at_timestamp | `integer` | Timestamp representing when the item was updated _by the server_, with microsecond precision. |
## Content
Data generated by the client is stored in the `content` field of the `Item`. An item's `content` is "sticky" and can have any property the client chooses, as well as the following:
| name | type | description |
| ---------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| appData | `dictionary` | A domain based dictionary whose content must be preserved between platforms. `appData[domain]` contains data the client stores in the item. |
| references | `array` | A metadata array of other `Item` uuids this item is related to and their respective `content_type`. See sample below. |
`references` array structure:
```JSON
[
{uuid: xxxx, content_type: "Tag"},
{uuid: xxxxx, content_type: "Tag"},
]
```
## REST API
General:
1. All requests after initial authentication should be authenticated with a session token in the `Authorization` header:
```
Authorization: Bearer <access token>
```
### POST items/sync
| | | |
|-------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| api | `string` | The API version to use. Must not be null, otherwise oldest API version used. Latest value is "20200115". |
| compute_integrity | `boolean` | Should be false in most cases. Pass true if you plan to read the `integrity_hash` result (integrity can be optionally checked once on app launch). |
| items | `array` | An array of Items. |
| limit | `integer` | The maximum number of items to return per request. Official clients use 150. |
| sync_token | `string` | An opaque token returned by the server from a previous sync request. This lets the server know to pull in only new changes. |
| cursor_token | `string` | An opaque pagination token returned by the server from a previous multi-page sync request. Send this value back only if the server returned this value from a previous request. |
#### Response
```JSON
{
"retrieved_items": Array<Item>,
"saved_items": Array<Item>,
"conflicts": Array<Conflict>,
"sync_token": string,
"cursor_token": string?,
"integrity_hash": string?,
}
```
| | |
|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| retrieved_items | Items that are new to the client or have been modified since last sync and should be merged with local values or created locally. |
| saved_items | Items which have been successfully saved. Clients should merge *only* the resulting `updated_at_timestamp` with local values. Do not update local value of `content` from remote `saved_items` value. |
| conflicts | Items which have not been saved by the server and returned to client. |
| sync_token | An opaque token which should be sent back to server in subsequent sync request. |
| cursor_token | An opaque token which should be sent back to server in subsequent sync request (if present). |
#### Conflicts
```typescript
type Conflict = {
server_item?: Item
unsaved_item?: Item
type: 'sync_conflict' | 'uuid_conflict'
}
```
A `sync_conflict` occurs when the item the client is attempting to save has a newer change on the server. The server is able to determine this by reading the `updated_at_timestamp` value from both the incoming payload and the server payload. If the incoming payload's `updated_at_timestamp` != to the server's `updated_at_timestamp` for that item, the save is rejected and returned to client as a `sync_conflict`. These types of conflicts are not uncommon.
To resolve a `sync_conflict`, the client must choose a winner between server_item and unsaved_item then re-sync. The winning item should have its updated_at_timestamp value set to the server's value.
A `uuid_conflict` occurs when the UUID the client has choosen for an item for X user already belongs to an item for Y user. This should mostly only occur when a user registers a new account and imports a data backup from another account.
To resolve a `uuid_conflict`, change the UUID of the item attempting to be saved to a new UUID, then re-sync.
#### Deletion
To delete an item permanently, set `deleted` to `true` on the `Item` and sync. When receiving an item that is `deleted`, remove it from the local database immediately.
-2
View File
@@ -23,8 +23,6 @@ This documentation explains how to get started with using Standard Notes, publis
- [Self-hosting a Syncing Server](/self-hosting/getting-started)
- [Developing Extensions](/extensions/intro)
- [Client Encryption API](/specification/encryption)
- [Sync API](/specification/sync)
- [Auth API](/specification/auth)
## Staying informed
+1 -6
View File
@@ -43,11 +43,6 @@ module.exports = {
label: 'Client Encryption API',
position: 'left',
},
{
to: '/specification/sync',
label: 'Server API',
position: 'left',
},
{
to: '/extensions/intro',
label: 'Build an Extension',
@@ -167,7 +162,7 @@ module.exports = {
{
docs: {
sidebarPath: require.resolve('./sidebars.js'),
editUrl: 'https://github.com/standardnotes/docs/edit/main/',
editUrl: 'https://github.com/standardnotes/app/blob/main/packages/docs/',
routeBasePath: '/',
remarkPlugins: [math],
rehypePlugins: [katex],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/docs",
"version": "0.2.2",
"version": "0.2.4",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+1 -5
View File
@@ -14,11 +14,7 @@ module.exports = {
],
},
{
API: [
'specification/encryption',
'specification/sync',
'specification/auth',
],
API: ['specification/encryption'],
},
{
Extensions: [
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.19.27](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-08)
**Note:** Version bump only for package @standardnotes/encryption
## [1.19.26](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2022-12-05)
**Note:** Version bump only for package @standardnotes/encryption

Some files were not shown because too many files have changed in this diff Show More