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
This commit is contained in:
BIN
.yarn/cache/@standardnotes-domain-core-npm-1.11.0-f473ba8bc0-cf4c9b7534.zip
vendored
Normal file
BIN
.yarn/cache/@standardnotes-domain-core-npm-1.11.0-f473ba8bc0-cf4c9b7534.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/shallow-equal-object-npm-1.1.1-a41b289b2e-e925aa4511.zip
vendored
Normal file
BIN
.yarn/cache/shallow-equal-object-npm-1.1.1-a41b289b2e-e925aa4511.zip
vendored
Normal file
Binary file not shown.
@@ -6,12 +6,4 @@ module.exports = {
|
|||||||
transform: {
|
transform: {
|
||||||
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
'^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
|
||||||
},
|
},
|
||||||
coverageThreshold: {
|
|
||||||
global: {
|
|
||||||
branches: 22,
|
|
||||||
functions: 69,
|
|
||||||
lines: 67,
|
|
||||||
statements: 67
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsc -p tsconfig.json",
|
||||||
"lint": "eslint src --ext .ts",
|
"lint": "eslint src --ext .ts",
|
||||||
"lint:fix": "eslint src --ext .ts --fix",
|
"lint:fix": "eslint src --ext .ts --fix",
|
||||||
"test": "jest spec --coverage"
|
"test": "jest spec --coverage --passWithNoTests"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.2.3",
|
"@types/jest": "^29.2.3",
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standardnotes/common": "^1.45.0",
|
"@standardnotes/common": "^1.45.0",
|
||||||
|
"@standardnotes/domain-core": "^1.11.0",
|
||||||
"@standardnotes/encryption": "workspace:*",
|
"@standardnotes/encryption": "workspace:*",
|
||||||
"@standardnotes/models": "workspace:*",
|
"@standardnotes/models": "workspace:*",
|
||||||
"@standardnotes/responses": "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('test@test.te')
|
|
||||||
|
|
||||||
expect(response).toEqual({
|
|
||||||
data: {
|
|
||||||
success: true,
|
|
||||||
sharedSubscriptionInvitationUuid: '1-2-3',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(subscriptionServer.invite).toHaveBeenCalledWith({
|
|
||||||
api: '20200115',
|
|
||||||
identifier: 'test@test.te',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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('test@test.te')
|
|
||||||
} 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('test@test.te')
|
|
||||||
} 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: 'test@test.te', 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: 'test@test.te',
|
|
||||||
version: ProtocolVersion.V004,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should register a user', async () => {
|
|
||||||
const response = await createService().register({
|
|
||||||
email: 'test@test.te',
|
|
||||||
serverPassword: 'testpasswd',
|
|
||||||
keyParams,
|
|
||||||
ephemeral: 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, 'operationsInProgress', {
|
|
||||||
get: () => new Map([[UserApiOperations.Registering, true]]),
|
|
||||||
})
|
|
||||||
|
|
||||||
let error = null
|
|
||||||
try {
|
|
||||||
await service.register({ email: 'test@test.te', 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: 'test@test.te',
|
|
||||||
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: 'test@test.te',
|
|
||||||
accessLevel: WorkspaceAccessLevel.WriteAndRead,
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(response).toEqual({
|
|
||||||
data: {
|
|
||||||
uuid: 'i-1-2-3',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(workspaceServer.inviteToWorkspace).toHaveBeenCalledWith({
|
|
||||||
workspaceUuid: 'w-1-2-3',
|
|
||||||
inviteeEmail: 'test@test.te',
|
|
||||||
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: 'test@test.te',
|
|
||||||
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: 'test@test.te',
|
|
||||||
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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
import { isString, joinPaths, sleep } from '@standardnotes/utils'
|
||||||
import { Environment } from '@standardnotes/models'
|
import { Environment } from '@standardnotes/models'
|
||||||
|
import { Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { HttpRequestParams } from './HttpRequestParams'
|
import { HttpRequestParams } from './HttpRequestParams'
|
||||||
import { HttpVerb } from './HttpVerb'
|
import { HttpVerb } from './HttpVerb'
|
||||||
import { HttpRequest } from './HttpRequest'
|
import { HttpRequest } from './HttpRequest'
|
||||||
@@ -10,21 +12,26 @@ import { XMLHttpRequestState } from './XMLHttpRequestState'
|
|||||||
import { ErrorMessage } from '../Error/ErrorMessage'
|
import { ErrorMessage } from '../Error/ErrorMessage'
|
||||||
import { HttpResponseMeta } from './HttpResponseMeta'
|
import { HttpResponseMeta } from './HttpResponseMeta'
|
||||||
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
|
import { HttpErrorResponseBody } from './HttpErrorResponseBody'
|
||||||
|
import { Paths } from '../Server/Auth/Paths'
|
||||||
|
import { SessionRefreshResponse } from '../Response/Auth/SessionRefreshResponse'
|
||||||
|
|
||||||
export class HttpService implements HttpServiceInterface {
|
export class HttpService implements HttpServiceInterface {
|
||||||
private authorizationToken?: string
|
private session: Session | null
|
||||||
private __latencySimulatorMs?: number
|
private __latencySimulatorMs?: number
|
||||||
private host!: string
|
private declare host: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private environment: Environment,
|
private environment: Environment,
|
||||||
private appVersion: string,
|
private appVersion: string,
|
||||||
private snjsVersion: string,
|
private snjsVersion: string,
|
||||||
private updateMetaCallback: (meta: HttpResponseMeta) => void,
|
private updateMetaCallback: (meta: HttpResponseMeta) => void,
|
||||||
) {}
|
private refreshSessionCallback: (session: Session) => void,
|
||||||
|
) {
|
||||||
|
this.session = null
|
||||||
|
}
|
||||||
|
|
||||||
setAuthorizationToken(authorizationToken: string): void {
|
setSession(session: Session): void {
|
||||||
this.authorizationToken = authorizationToken
|
this.session = session
|
||||||
}
|
}
|
||||||
|
|
||||||
setHost(host: string): void {
|
setHost(host: string): void {
|
||||||
@@ -36,7 +43,7 @@ export class HttpService implements HttpServiceInterface {
|
|||||||
url: joinPaths(this.host, path),
|
url: joinPaths(this.host, path),
|
||||||
params,
|
params,
|
||||||
verb: HttpVerb.Get,
|
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),
|
url: joinPaths(this.host, path),
|
||||||
params,
|
params,
|
||||||
verb: HttpVerb.Post,
|
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),
|
url: joinPaths(this.host, path),
|
||||||
params,
|
params,
|
||||||
verb: HttpVerb.Put,
|
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),
|
url: joinPaths(this.host, path),
|
||||||
params,
|
params,
|
||||||
verb: HttpVerb.Patch,
|
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),
|
url: joinPaths(this.host, path),
|
||||||
params,
|
params,
|
||||||
verb: HttpVerb.Delete,
|
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)
|
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
|
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 {
|
private createRequestBody(httpRequest: HttpRequest): string | Uint8Array | undefined {
|
||||||
if (
|
if (
|
||||||
httpRequest.params !== undefined &&
|
httpRequest.params !== undefined &&
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { Session } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
import { HttpRequestParams } from './HttpRequestParams'
|
import { HttpRequestParams } from './HttpRequestParams'
|
||||||
import { HttpResponse } from './HttpResponse'
|
import { HttpResponse } from './HttpResponse'
|
||||||
|
|
||||||
export interface HttpServiceInterface {
|
export interface HttpServiceInterface {
|
||||||
setHost(host: string): void
|
setHost(host: string): void
|
||||||
setAuthorizationToken(authorizationToken: string): void
|
setSession(session: Session): void
|
||||||
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
get(path: string, params?: HttpRequestParams, authentication?: string): Promise<HttpResponse>
|
||||||
post(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>
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/api/src/Domain/Server/Auth/Paths.ts
Normal file
9
packages/api/src/Domain/Server/Auth/Paths.ts
Normal file
@@ -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: 'test@test.te',
|
|
||||||
})
|
|
||||||
|
|
||||||
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: 'test@test.te', 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: '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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
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: 'test@test.te',
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -82,6 +82,9 @@ import { SNLog } from '../Log'
|
|||||||
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
import { ChallengeResponse, ListedClientInterface } from '../Services'
|
||||||
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
|
||||||
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
import { ApplicationOptionsDefaults } from './Options/Defaults'
|
||||||
|
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
|
||||||
|
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
|
||||||
|
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
|
||||||
|
|
||||||
/** How often to automatically sync, in milliseconds */
|
/** How often to automatically sync, in milliseconds */
|
||||||
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
|
||||||
@@ -154,6 +157,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
private integrityService!: ExternalServices.IntegrityService
|
private integrityService!: ExternalServices.IntegrityService
|
||||||
private statusService!: ExternalServices.StatusService
|
private statusService!: ExternalServices.StatusService
|
||||||
private filesBackupService?: FilesBackupService
|
private filesBackupService?: FilesBackupService
|
||||||
|
private declare sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>
|
||||||
|
private declare legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||||
|
|
||||||
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
private internalEventBus!: ExternalServices.InternalEventBusInterface
|
||||||
|
|
||||||
@@ -1078,6 +1083,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
}
|
}
|
||||||
|
|
||||||
private constructServices() {
|
private constructServices() {
|
||||||
|
this.createMappers()
|
||||||
this.createPayloadManager()
|
this.createPayloadManager()
|
||||||
this.createItemManager()
|
this.createItemManager()
|
||||||
this.createDiskStorageManager()
|
this.createDiskStorageManager()
|
||||||
@@ -1169,6 +1175,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
;(this.mutatorService as unknown) = undefined
|
;(this.mutatorService as unknown) = undefined
|
||||||
;(this.filesBackupService as unknown) = undefined
|
;(this.filesBackupService as unknown) = undefined
|
||||||
;(this.statusService as unknown) = undefined
|
;(this.statusService as unknown) = undefined
|
||||||
|
;(this.sessionStorageMapper as unknown) = undefined
|
||||||
|
;(this.legacySessionStorageMapper as unknown) = undefined
|
||||||
|
|
||||||
this.services = []
|
this.services = []
|
||||||
}
|
}
|
||||||
@@ -1289,6 +1297,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
environment: this.environment,
|
environment: this.environment,
|
||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
internalEventBus: this.internalEventBus,
|
internalEventBus: this.internalEventBus,
|
||||||
|
legacySessionStorageMapper: this.legacySessionStorageMapper,
|
||||||
})
|
})
|
||||||
this.services.push(this.migrationService)
|
this.services.push(this.migrationService)
|
||||||
}
|
}
|
||||||
@@ -1335,6 +1344,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.options.defaultHost,
|
this.options.defaultHost,
|
||||||
this.inMemoryStore,
|
this.inMemoryStore,
|
||||||
this.options.crypto,
|
this.options.crypto,
|
||||||
|
this.sessionStorageMapper,
|
||||||
|
this.legacySessionStorageMapper,
|
||||||
this.internalEventBus,
|
this.internalEventBus,
|
||||||
)
|
)
|
||||||
this.services.push(this.apiService)
|
this.services.push(this.apiService)
|
||||||
@@ -1419,9 +1430,15 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.options.appVersion,
|
this.options.appVersion,
|
||||||
SnjsVersion,
|
SnjsVersion,
|
||||||
this.apiService.processMetaObject.bind(this.apiService),
|
this.apiService.processMetaObject.bind(this.apiService),
|
||||||
|
this.apiService.setSession.bind(this.apiService),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createMappers() {
|
||||||
|
this.sessionStorageMapper = new SessionStorageMapper()
|
||||||
|
this.legacySessionStorageMapper = new LegacySessionStorageMapper()
|
||||||
|
}
|
||||||
|
|
||||||
private createPayloadManager() {
|
private createPayloadManager() {
|
||||||
this.payloadManager = new InternalServices.PayloadManager(this.internalEventBus)
|
this.payloadManager = new InternalServices.PayloadManager(this.internalEventBus)
|
||||||
this.services.push(this.payloadManager)
|
this.services.push(this.payloadManager)
|
||||||
@@ -1497,6 +1514,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
|
|||||||
this.challengeService,
|
this.challengeService,
|
||||||
this.webSocketsService,
|
this.webSocketsService,
|
||||||
this.httpService,
|
this.httpService,
|
||||||
|
this.sessionStorageMapper,
|
||||||
|
this.legacySessionStorageMapper,
|
||||||
this.internalEventBus,
|
this.internalEventBus,
|
||||||
)
|
)
|
||||||
this.serviceObservers.push(
|
this.serviceObservers.push(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SNSessionManager } from '../Services/Session/SessionManager'
|
|||||||
import { ApplicationIdentifier } from '@standardnotes/common'
|
import { ApplicationIdentifier } from '@standardnotes/common'
|
||||||
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
||||||
import { ChallengeService, SNSingletonManager, SNFeaturesService, DiskStorageService } from '@Lib/Services'
|
import { ChallengeService, SNSingletonManager, SNFeaturesService, DiskStorageService } from '@Lib/Services'
|
||||||
|
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export type MigrationServices = {
|
export type MigrationServices = {
|
||||||
protocolService: EncryptionService
|
protocolService: EncryptionService
|
||||||
@@ -17,5 +18,6 @@ export type MigrationServices = {
|
|||||||
featuresService: SNFeaturesService
|
featuresService: SNFeaturesService
|
||||||
environment: Environment
|
environment: Environment
|
||||||
identifier: ApplicationIdentifier
|
identifier: ApplicationIdentifier
|
||||||
|
legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>
|
||||||
internalEventBus: InternalEventBusInterface
|
internalEventBus: InternalEventBusInterface
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
|
import { AnyKeyParamsContent, ContentType, ProtocolVersion } from '@standardnotes/common'
|
||||||
import { JwtSession } from '../../Services/Session/Sessions/JwtSession'
|
|
||||||
import { Migration } from '@Lib/Migrations/Migration'
|
import { Migration } from '@Lib/Migrations/Migration'
|
||||||
import { MigrationServices } from '../MigrationServices'
|
import { MigrationServices } from '../MigrationServices'
|
||||||
import { PreviousSnjsVersion2_0_0 } from '../../Version'
|
import { PreviousSnjsVersion2_0_0 } from '../../Version'
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
PayloadTimestampDefaults,
|
PayloadTimestampDefaults,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { isMobileDevice } from '@standardnotes/services'
|
import { isMobileDevice } from '@standardnotes/services'
|
||||||
|
import { LegacySession } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
interface LegacyStorageContent extends Models.ItemContent {
|
interface LegacyStorageContent extends Models.ItemContent {
|
||||||
storage: unknown
|
storage: unknown
|
||||||
@@ -673,8 +673,13 @@ export class Migration2_0_0 extends Migration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = new JwtSession(currentToken)
|
const sessionOrError = LegacySession.create(currentToken)
|
||||||
this.services.storageService.setValue(Services.StorageKey.Session, session)
|
if (!sessionOrError.isFailed()) {
|
||||||
|
this.services.storageService.setValue(
|
||||||
|
Services.StorageKey.Session,
|
||||||
|
this.services.legacySessionStorageMapper.toProjection(sessionOrError.getValue()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/** Server has to be migrated separately on mobile */
|
/** Server has to be migrated separately on mobile */
|
||||||
if (isEnvironmentMobile(this.services.environment)) {
|
if (isEnvironmentMobile(this.services.environment)) {
|
||||||
|
|||||||
@@ -34,23 +34,22 @@ import {
|
|||||||
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { FilesApiInterface } from '@standardnotes/files'
|
import { FilesApiInterface } from '@standardnotes/files'
|
||||||
|
|
||||||
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
|
||||||
import * as Responses from '@standardnotes/responses'
|
import * as Responses from '@standardnotes/responses'
|
||||||
|
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
|
import { HttpResponseMeta } from '@standardnotes/api'
|
||||||
|
import { SNRootKeyParams } from '@standardnotes/encryption'
|
||||||
|
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
||||||
|
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||||
|
|
||||||
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
import { HttpParams, HttpRequest, HttpVerb, SNHttpService } from './HttpService'
|
||||||
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||||
import { Paths } from './Paths'
|
import { Paths } from './Paths'
|
||||||
import { Session } from '../Session/Sessions/Session'
|
|
||||||
import { TokenSession } from '../Session/Sessions/TokenSession'
|
|
||||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||||
import { HttpResponseMeta } from '@standardnotes/api'
|
|
||||||
import { UuidString } from '../../Types/UuidString'
|
import { UuidString } from '../../Types/UuidString'
|
||||||
import merge from 'lodash/merge'
|
import merge from 'lodash/merge'
|
||||||
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
import { SettingsServerInterface } from '../Settings/SettingsServerInterface'
|
||||||
import { Strings } from '@Lib/Strings'
|
import { Strings } from '@Lib/Strings'
|
||||||
import { SNRootKeyParams } from '@standardnotes/encryption'
|
|
||||||
import { ApiEndpointParam, ClientDisplayableError, CreateValetTokenPayload } from '@standardnotes/responses'
|
|
||||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
|
||||||
|
|
||||||
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
/** Legacy api version field to be specified in params when calling v0 APIs. */
|
||||||
const V0_API_VERSION = '20200115'
|
const V0_API_VERSION = '20200115'
|
||||||
@@ -66,7 +65,7 @@ export class SNApiService
|
|||||||
ItemsServerInterface,
|
ItemsServerInterface,
|
||||||
SettingsServerInterface
|
SettingsServerInterface
|
||||||
{
|
{
|
||||||
private session?: Session
|
private session: Session | LegacySession | null
|
||||||
public user?: Responses.User
|
public user?: Responses.User
|
||||||
private registering = false
|
private registering = false
|
||||||
private authenticating = false
|
private authenticating = false
|
||||||
@@ -81,16 +80,20 @@ export class SNApiService
|
|||||||
private host: string,
|
private host: string,
|
||||||
private inMemoryStore: KeyValueStoreInterface<string>,
|
private inMemoryStore: KeyValueStoreInterface<string>,
|
||||||
private crypto: PureCryptoInterface,
|
private crypto: PureCryptoInterface,
|
||||||
|
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||||
|
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
|
|
||||||
|
this.session = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override deinit(): void {
|
override deinit(): void {
|
||||||
;(this.httpService as unknown) = undefined
|
;(this.httpService as unknown) = undefined
|
||||||
;(this.storageService as unknown) = undefined
|
;(this.storageService as unknown) = undefined
|
||||||
this.invalidSessionObserver = undefined
|
this.invalidSessionObserver = undefined
|
||||||
this.session = undefined
|
this.session = null
|
||||||
super.deinit()
|
super.deinit()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,14 +148,21 @@ export class SNApiService
|
|||||||
return this.filesHost
|
return this.filesHost
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSession(session: Session, persist = true): void {
|
public setSession(session: Session | LegacySession, persist = true): void {
|
||||||
this.session = session
|
this.session = session
|
||||||
if (persist) {
|
if (persist) {
|
||||||
this.storageService.setValue(StorageKey.Session, session)
|
let sessionProjection: Record<string, unknown>
|
||||||
|
if (session instanceof Session) {
|
||||||
|
sessionProjection = this.sessionStorageMapper.toProjection(session)
|
||||||
|
} else {
|
||||||
|
sessionProjection = this.legacySessionStorageMapper.toProjection(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.storageService.setValue(StorageKey.Session, sessionProjection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSession(): Session | undefined {
|
public getSession(): Session | LegacySession | null {
|
||||||
return this.session
|
return this.session
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +262,7 @@ export class SNApiService
|
|||||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
fallbackErrorMessage: API_MESSAGE_GENERIC_INVALID_LOGIN,
|
||||||
params,
|
params,
|
||||||
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
/** A session is optional here, if valid, endpoint bypasses 2FA and returns additional params */
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +299,7 @@ export class SNApiService
|
|||||||
|
|
||||||
signOut(): Promise<Responses.SignOutResponse> {
|
signOut(): Promise<Responses.SignOutResponse> {
|
||||||
const url = joinPaths(this.host, Paths.v1.signOut)
|
const url = joinPaths(this.host, Paths.v1.signOut)
|
||||||
return this.httpService.postAbsolute(url, undefined, this.session?.authorizationValue).catch((errorResponse) => {
|
return this.httpService.postAbsolute(url, undefined, this.getSessionAccessToken()).catch((errorResponse) => {
|
||||||
return errorResponse
|
return errorResponse
|
||||||
}) as Promise<Responses.SignOutResponse>
|
}) as Promise<Responses.SignOutResponse>
|
||||||
}
|
}
|
||||||
@@ -317,7 +327,7 @@ export class SNApiService
|
|||||||
...parameters.newKeyParams.getPortableValue(),
|
...parameters.newKeyParams.getPortableValue(),
|
||||||
})
|
})
|
||||||
const response = await this.httpService
|
const response = await this.httpService
|
||||||
.putAbsolute(url, params, this.session?.authorizationValue)
|
.putAbsolute(url, params, this.getSessionAccessToken())
|
||||||
.catch(async (errorResponse) => {
|
.catch(async (errorResponse) => {
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
return this.refreshSessionThenRetryRequest({
|
return this.refreshSessionThenRetryRequest({
|
||||||
@@ -353,7 +363,7 @@ export class SNApiService
|
|||||||
[ApiEndpointParam.SyncDlLimit]: limit,
|
[ApiEndpointParam.SyncDlLimit]: limit,
|
||||||
})
|
})
|
||||||
const response = await this.httpService
|
const response = await this.httpService
|
||||||
.postAbsolute(url, params, this.session?.authorizationValue)
|
.postAbsolute(url, params, this.getSessionAccessToken())
|
||||||
.catch<Responses.HttpResponse>(async (errorResponse) => {
|
.catch<Responses.HttpResponse>(async (errorResponse) => {
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
@@ -378,7 +388,7 @@ export class SNApiService
|
|||||||
return this.httpService
|
return this.httpService
|
||||||
.runHttp({
|
.runHttp({
|
||||||
...httpRequest,
|
...httpRequest,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
.catch((errorResponse) => {
|
.catch((errorResponse) => {
|
||||||
return errorResponse
|
return errorResponse
|
||||||
@@ -393,16 +403,54 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
this.refreshingSession = true
|
this.refreshingSession = true
|
||||||
const url = joinPaths(this.host, Paths.v1.refreshSession)
|
const url = joinPaths(this.host, Paths.v1.refreshSession)
|
||||||
const session = this.session as TokenSession
|
const session = this.session as Session
|
||||||
const params = this.params({
|
const params = this.params({
|
||||||
access_token: session.accessToken,
|
access_token: session.accessToken.value,
|
||||||
refresh_token: session.refreshToken,
|
refresh_token: session.refreshToken.value,
|
||||||
})
|
})
|
||||||
const result = await this.httpService
|
const result = await this.httpService
|
||||||
.postAbsolute(url, params)
|
.postAbsolute(url, params)
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const session = TokenSession.FromApiResponse(response as Responses.SessionRenewalResponse)
|
const sessionRenewalResponse = response as Responses.SessionRenewalResponse
|
||||||
await this.setSession(session)
|
if (
|
||||||
|
sessionRenewalResponse.error ||
|
||||||
|
sessionRenewalResponse.data?.error ||
|
||||||
|
!sessionRenewalResponse.data.session
|
||||||
|
) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessTokenOrError = SessionToken.create(
|
||||||
|
sessionRenewalResponse.data.session.access_token,
|
||||||
|
sessionRenewalResponse.data.session.access_expiration,
|
||||||
|
)
|
||||||
|
if (accessTokenOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const accessToken = accessTokenOrError.getValue()
|
||||||
|
|
||||||
|
const refreshTokenOrError = SessionToken.create(
|
||||||
|
sessionRenewalResponse.data.session.refresh_token,
|
||||||
|
sessionRenewalResponse.data.session.refresh_expiration,
|
||||||
|
)
|
||||||
|
if (refreshTokenOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const refreshToken = refreshTokenOrError.getValue()
|
||||||
|
|
||||||
|
const sessionOrError = Session.create(
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
sessionRenewalResponse.data.session.readonly_access,
|
||||||
|
)
|
||||||
|
if (sessionOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const session = sessionOrError.getValue()
|
||||||
|
|
||||||
|
this.session = session
|
||||||
|
|
||||||
|
this.setSession(session)
|
||||||
this.processResponse(response)
|
this.processResponse(response)
|
||||||
return response
|
return response
|
||||||
})
|
})
|
||||||
@@ -411,6 +459,11 @@ export class SNApiService
|
|||||||
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
|
||||||
})
|
})
|
||||||
this.refreshingSession = false
|
this.refreshingSession = false
|
||||||
|
|
||||||
|
if (result === null) {
|
||||||
|
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +474,7 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
const url = joinPaths(this.host, Paths.v1.sessions)
|
const url = joinPaths(this.host, Paths.v1.sessions)
|
||||||
const response = await this.httpService
|
const response = await this.httpService
|
||||||
.getAbsolute(url, {}, this.session?.authorizationValue)
|
.getAbsolute(url, {}, this.getSessionAccessToken())
|
||||||
.catch(async (errorResponse) => {
|
.catch(async (errorResponse) => {
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
@@ -444,7 +497,7 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
|
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
|
||||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||||
.deleteAbsolute(url, { uuid: sessionId }, this.session?.authorizationValue)
|
.deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken())
|
||||||
.catch((error: Responses.HttpResponse) => {
|
.catch((error: Responses.HttpResponse) => {
|
||||||
const errorResponse = error as Responses.HttpResponse
|
const errorResponse = error as Responses.HttpResponse
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
@@ -467,7 +520,7 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId))
|
const url = joinPaths(this.host, Paths.v1.itemRevisions(itemId))
|
||||||
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
const response: Responses.RevisionListResponse | Responses.HttpResponse = await this.httpService
|
||||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||||
.catch((errorResponse: Responses.HttpResponse) => {
|
.catch((errorResponse: Responses.HttpResponse) => {
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
@@ -492,7 +545,7 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid))
|
const url = joinPaths(this.host, Paths.v1.itemRevision(itemId, entry.uuid))
|
||||||
const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService
|
const response: Responses.SingleRevisionResponse | Responses.HttpResponse = await this.httpService
|
||||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||||
.catch((errorResponse: Responses.HttpResponse) => {
|
.catch((errorResponse: Responses.HttpResponse) => {
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
@@ -510,7 +563,7 @@ export class SNApiService
|
|||||||
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
|
||||||
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
|
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
|
||||||
const response = await this.httpService
|
const response = await this.httpService
|
||||||
.getAbsolute(url, undefined, this.session?.authorizationValue)
|
.getAbsolute(url, undefined, this.getSessionAccessToken())
|
||||||
.catch((errorResponse: Responses.HttpResponse) => {
|
.catch((errorResponse: Responses.HttpResponse) => {
|
||||||
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
this.preprocessAuthenticatedErrorResponse(errorResponse)
|
||||||
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
|
||||||
@@ -550,7 +603,7 @@ export class SNApiService
|
|||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +621,7 @@ export class SNApiService
|
|||||||
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
|
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
|
||||||
verb: HttpVerb.Put,
|
verb: HttpVerb.Put,
|
||||||
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -578,7 +631,7 @@ export class SNApiService
|
|||||||
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
|
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
|
||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -593,7 +646,7 @@ export class SNApiService
|
|||||||
this.host,
|
this.host,
|
||||||
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
Paths.v1.subscriptionSetting(userUuid, settingName.toLowerCase() as SubscriptionSettingName),
|
||||||
),
|
),
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -602,7 +655,7 @@ export class SNApiService
|
|||||||
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
|
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
|
||||||
verb: HttpVerb.Delete,
|
verb: HttpVerb.Delete,
|
||||||
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
fallbackErrorMessage: API_MESSAGE_FAILED_UPDATE_SETTINGS,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -616,7 +669,7 @@ export class SNApiService
|
|||||||
verb: HttpVerb.Delete,
|
verb: HttpVerb.Delete,
|
||||||
url,
|
url,
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
fallbackErrorMessage: API_MESSAGE_FAILED_DELETE_REVISION,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@@ -635,7 +688,7 @@ export class SNApiService
|
|||||||
const response = await this.tokenRefreshableRequest({
|
const response = await this.tokenRefreshableRequest({
|
||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
url,
|
url,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
|
||||||
})
|
})
|
||||||
return response
|
return response
|
||||||
@@ -658,7 +711,7 @@ export class SNApiService
|
|||||||
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
|
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
|
||||||
verb: HttpVerb.Post,
|
verb: HttpVerb.Post,
|
||||||
url,
|
url,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
|
||||||
})
|
})
|
||||||
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
return (response as Responses.PostSubscriptionTokensResponse).data?.token
|
||||||
@@ -706,7 +759,7 @@ export class SNApiService
|
|||||||
verb: HttpVerb.Post,
|
verb: HttpVerb.Post,
|
||||||
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -725,7 +778,7 @@ export class SNApiService
|
|||||||
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
|
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
|
||||||
verb: HttpVerb.Post,
|
verb: HttpVerb.Post,
|
||||||
url: url,
|
url: url,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
fallbackErrorMessage: API_MESSAGE_FAILED_CREATE_FILE_TOKEN,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -860,7 +913,7 @@ export class SNApiService
|
|||||||
integrityPayloads,
|
integrityPayloads,
|
||||||
},
|
},
|
||||||
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
fallbackErrorMessage: API_MESSAGE_GENERIC_INTEGRITY_CHECK_FAIL,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -869,7 +922,7 @@ export class SNApiService
|
|||||||
verb: HttpVerb.Get,
|
verb: HttpVerb.Get,
|
||||||
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
|
||||||
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
|
||||||
authentication: this.session?.authorizationValue,
|
authentication: this.getSessionAccessToken(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -890,6 +943,18 @@ export class SNApiService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getSessionAccessToken(): string | undefined {
|
||||||
|
if (!this.session) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.session instanceof Session) {
|
||||||
|
return this.session.accessToken.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.session.accessToken
|
||||||
|
}
|
||||||
|
|
||||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
api: {
|
api: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * from './ApiService'
|
export * from './ApiService'
|
||||||
export * from './HttpService'
|
export * from './HttpService'
|
||||||
export * from './Paths'
|
export * from './Paths'
|
||||||
export * from '../Session/Sessions/Session'
|
|
||||||
export * from '../Session/SessionManager'
|
export * from '../Session/SessionManager'
|
||||||
export * from './WebsocketsService'
|
export * from './WebsocketsService'
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ export class ChallengeService extends AbstractService implements ChallengeServic
|
|||||||
if (operation.isFinished()) {
|
if (operation.isFinished()) {
|
||||||
this.deleteChallengeOperation(operation)
|
this.deleteChallengeOperation(operation)
|
||||||
|
|
||||||
const observers = this.challengeObservers[challenge.id]
|
const observers = this.challengeObservers[challenge.id] || []
|
||||||
observers.forEach(clearChallengeObserver)
|
observers.forEach(clearChallengeObserver)
|
||||||
observers.length = 0
|
observers.length = 0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { LegacySession, MapperInterface } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
export class LegacySessionStorageMapper implements MapperInterface<LegacySession, Record<string, unknown>> {
|
||||||
|
toDomain(projection: Record<string, unknown>): LegacySession {
|
||||||
|
const { jwt } = projection
|
||||||
|
|
||||||
|
const legacySessionOrError = LegacySession.create(jwt as string)
|
||||||
|
if (legacySessionOrError.isFailed()) {
|
||||||
|
throw new Error(legacySessionOrError.getError())
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacySessionOrError.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
toProjection(domain: LegacySession): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
jwt: domain.accessToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/snjs/lib/Services/Mapping/SessionStorageMapper.ts
Normal file
40
packages/snjs/lib/Services/Mapping/SessionStorageMapper.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
export class SessionStorageMapper implements MapperInterface<Session, Record<string, unknown>> {
|
||||||
|
toDomain(projection: Record<string, unknown>): Session {
|
||||||
|
const accessTokenOrError = SessionToken.create(
|
||||||
|
projection.accessToken as string,
|
||||||
|
projection.accessExpiration as number,
|
||||||
|
)
|
||||||
|
if (accessTokenOrError.isFailed()) {
|
||||||
|
throw new Error(accessTokenOrError.getError())
|
||||||
|
}
|
||||||
|
const accessToken = accessTokenOrError.getValue()
|
||||||
|
|
||||||
|
const refreshTokenOrError = SessionToken.create(
|
||||||
|
projection.refreshToken as string,
|
||||||
|
projection.refreshExpiration as number,
|
||||||
|
)
|
||||||
|
if (refreshTokenOrError.isFailed()) {
|
||||||
|
throw new Error(refreshTokenOrError.getError())
|
||||||
|
}
|
||||||
|
const refreshToken = refreshTokenOrError.getValue()
|
||||||
|
|
||||||
|
const session = Session.create(accessToken, refreshToken, projection.readonlyAccess as boolean)
|
||||||
|
if (session.isFailed()) {
|
||||||
|
throw new Error(session.getError())
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
toProjection(domain: Session): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
accessToken: domain.accessToken.value,
|
||||||
|
refreshToken: domain.refreshToken.value,
|
||||||
|
accessExpiration: domain.accessToken.expiresAt,
|
||||||
|
refreshExpiration: domain.refreshToken.expiresAt,
|
||||||
|
readonlyAccess: domain.isReadOnly(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,21 +27,19 @@ import { Base64String } from '@standardnotes/sncrypto-common'
|
|||||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||||
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
|
||||||
import { isNullOrUndefined } from '@standardnotes/utils'
|
import { isNullOrUndefined } from '@standardnotes/utils'
|
||||||
import { JwtSession } from './Sessions/JwtSession'
|
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
|
||||||
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey, CreateNewRootKey } from '@standardnotes/encryption'
|
import { KeyParamsFromApiResponse, SNRootKeyParams, SNRootKey, CreateNewRootKey } from '@standardnotes/encryption'
|
||||||
|
import * as Responses from '@standardnotes/responses'
|
||||||
|
import { Subscription } from '@standardnotes/security'
|
||||||
|
import * as Common from '@standardnotes/common'
|
||||||
|
|
||||||
import { RemoteSession, RawStorageValue } from './Sessions/Types'
|
import { RemoteSession, RawStorageValue } from './Sessions/Types'
|
||||||
import { Session } from './Sessions/Session'
|
|
||||||
import { SessionFromRawStorageValue } from './Sessions/Generator'
|
|
||||||
import { ShareToken } from './ShareToken'
|
import { ShareToken } from './ShareToken'
|
||||||
import { SNApiService } from '../Api/ApiService'
|
import { SNApiService } from '../Api/ApiService'
|
||||||
import { DiskStorageService } from '../Storage/DiskStorageService'
|
import { DiskStorageService } from '../Storage/DiskStorageService'
|
||||||
import { SNWebSocketsService } from '../Api/WebsocketsService'
|
import { SNWebSocketsService } from '../Api/WebsocketsService'
|
||||||
import { Strings } from '@Lib/Strings'
|
import { Strings } from '@Lib/Strings'
|
||||||
import { Subscription } from '@standardnotes/security'
|
|
||||||
import { TokenSession } from './Sessions/TokenSession'
|
|
||||||
import { UuidString } from '@Lib/Types/UuidString'
|
import { UuidString } from '@Lib/Types/UuidString'
|
||||||
import * as Common from '@standardnotes/common'
|
|
||||||
import * as Responses from '@standardnotes/responses'
|
|
||||||
import { ChallengeService } from '../Challenge'
|
import { ChallengeService } from '../Challenge'
|
||||||
import {
|
import {
|
||||||
ApiCallError,
|
ApiCallError,
|
||||||
@@ -82,6 +80,8 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
private challengeService: ChallengeService,
|
private challengeService: ChallengeService,
|
||||||
private webSocketsService: SNWebSocketsService,
|
private webSocketsService: SNWebSocketsService,
|
||||||
private httpService: HttpServiceInterface,
|
private httpService: HttpServiceInterface,
|
||||||
|
private sessionStorageMapper: MapperInterface<Session, Record<string, unknown>>,
|
||||||
|
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||||
protected override internalEventBus: InternalEventBusInterface,
|
protected override internalEventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
@@ -122,13 +122,23 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
|
|
||||||
const rawSession = this.diskStorageService.getValue<RawStorageValue>(StorageKey.Session)
|
const rawSession = this.diskStorageService.getValue<RawStorageValue>(StorageKey.Session)
|
||||||
if (rawSession) {
|
if (rawSession) {
|
||||||
const session = SessionFromRawStorageValue(rawSession)
|
try {
|
||||||
this.setSession(session, false)
|
const session =
|
||||||
|
'jwt' in rawSession
|
||||||
|
? this.legacySessionStorageMapper.toDomain(rawSession)
|
||||||
|
: this.sessionStorageMapper.toDomain(rawSession)
|
||||||
|
|
||||||
|
this.setSession(session, false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Could not deserialize session from storage: ${(error as Error).message}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setSession(session: Session, persist = true): void {
|
private setSession(session: Session | LegacySession, persist = true): void {
|
||||||
this.httpService.setAuthorizationToken(session.authorizationValue)
|
if (session instanceof Session) {
|
||||||
|
this.httpService.setSession(session)
|
||||||
|
}
|
||||||
|
|
||||||
this.apiService.setSession(session, persist)
|
this.apiService.setSession(session, persist)
|
||||||
|
|
||||||
@@ -158,7 +168,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
public async signOut() {
|
public async signOut() {
|
||||||
this.setUser(undefined)
|
this.setUser(undefined)
|
||||||
const session = this.apiService.getSession()
|
const session = this.apiService.getSession()
|
||||||
if (session && session.canExpire()) {
|
if (session && session instanceof Session) {
|
||||||
await this.apiService.signOut()
|
await this.apiService.signOut()
|
||||||
this.webSocketsService.closeWebSocketConnection()
|
this.webSocketsService.closeWebSocketConnection()
|
||||||
}
|
}
|
||||||
@@ -560,17 +570,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
return new ClientDisplayableError('Cannot generate share token without active session')
|
return new ClientDisplayableError('Cannot generate share token without active session')
|
||||||
}
|
}
|
||||||
if (!(session instanceof TokenSession)) {
|
if (!(session instanceof Session)) {
|
||||||
return new ClientDisplayableError('Cannot generate share token with non-token session')
|
return new ClientDisplayableError('Cannot generate share token with non-token session')
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyParams = (await this.protocolService.getRootKeyParams()) as SNRootKeyParams
|
const keyParams = (await this.protocolService.getRootKeyParams()) as SNRootKeyParams
|
||||||
|
|
||||||
const payload: ShareToken = {
|
const payload: ShareToken = {
|
||||||
accessToken: session.accessToken,
|
accessToken: session.accessToken.value,
|
||||||
refreshToken: session.refreshToken,
|
refreshToken: session.refreshToken.value,
|
||||||
accessExpiration: session.accessExpiration,
|
accessExpiration: session.accessToken.expiresAt,
|
||||||
refreshExpiration: session.refreshExpiration,
|
refreshExpiration: session.refreshToken.expiresAt,
|
||||||
readonlyAccess: true,
|
readonlyAccess: true,
|
||||||
masterKey: this.protocolService.getRootKey()?.masterKey as string,
|
masterKey: this.protocolService.getRootKey()?.masterKey as string,
|
||||||
keyParams: keyParams.content,
|
keyParams: keyParams.content,
|
||||||
@@ -597,7 +607,7 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
|
|
||||||
const user = sharePayload.user
|
const user = sharePayload.user
|
||||||
|
|
||||||
const session = new TokenSession(
|
const session = this.createSession(
|
||||||
sharePayload.accessToken,
|
sharePayload.accessToken,
|
||||||
sharePayload.accessExpiration,
|
sharePayload.accessExpiration,
|
||||||
sharePayload.refreshToken,
|
sharePayload.refreshToken,
|
||||||
@@ -605,13 +615,15 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
sharePayload.readonlyAccess,
|
sharePayload.readonlyAccess,
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.populateSession(rootKey, user, session, sharePayload.host)
|
if (session !== null) {
|
||||||
|
await this.populateSession(rootKey, user, session, sharePayload.host)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async populateSession(
|
private async populateSession(
|
||||||
rootKey: SNRootKey,
|
rootKey: SNRootKey,
|
||||||
user: Responses.User,
|
user: Responses.User,
|
||||||
session: Session,
|
session: Session | LegacySession,
|
||||||
host: string,
|
host: string,
|
||||||
wrappingKey?: SNRootKey,
|
wrappingKey?: SNRootKey,
|
||||||
) {
|
) {
|
||||||
@@ -629,14 +641,17 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
|
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
|
||||||
const session = new TokenSession(
|
const session = this.createSession(
|
||||||
body.session.access_token,
|
body.session.access_token,
|
||||||
body.session.access_expiration,
|
body.session.access_expiration,
|
||||||
body.session.refresh_token,
|
body.session.refresh_token,
|
||||||
body.session.refresh_expiration,
|
body.session.refresh_expiration,
|
||||||
body.session.readonly_access,
|
body.session.readonly_access,
|
||||||
)
|
)
|
||||||
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
|
||||||
|
if (session !== null) {
|
||||||
|
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -652,14 +667,51 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
|
|||||||
|
|
||||||
const isLegacyJwtResponse = data.token != undefined
|
const isLegacyJwtResponse = data.token != undefined
|
||||||
if (isLegacyJwtResponse) {
|
if (isLegacyJwtResponse) {
|
||||||
const session = new JwtSession(data.token as string)
|
const sessionOrError = LegacySession.create(data.token as string)
|
||||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
if (!sessionOrError.isFailed()) {
|
||||||
|
await this.populateSession(rootKey, user, sessionOrError.getValue(), this.apiService.getHost(), wrappingKey)
|
||||||
|
}
|
||||||
} else if (data.session) {
|
} else if (data.session) {
|
||||||
const session = TokenSession.FromApiResponse(response)
|
const session = this.createSession(
|
||||||
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
data.session.access_token,
|
||||||
|
data.session.access_expiration,
|
||||||
|
data.session.refresh_token,
|
||||||
|
data.session.refresh_expiration,
|
||||||
|
data.session.readonly_access,
|
||||||
|
)
|
||||||
|
if (session !== null) {
|
||||||
|
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createSession(
|
||||||
|
accessTokenValue: string,
|
||||||
|
accessExpiration: number,
|
||||||
|
refreshTokenValue: string,
|
||||||
|
refreshExpiration: number,
|
||||||
|
readonlyAccess: boolean,
|
||||||
|
): Session | null {
|
||||||
|
const accessTokenOrError = SessionToken.create(accessTokenValue, accessExpiration)
|
||||||
|
if (accessTokenOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const accessToken = accessTokenOrError.getValue()
|
||||||
|
|
||||||
|
const refreshTokenOrError = SessionToken.create(refreshTokenValue, refreshExpiration)
|
||||||
|
if (refreshTokenOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const refreshToken = refreshTokenOrError.getValue()
|
||||||
|
|
||||||
|
const sessionOrError = Session.create(accessToken, refreshToken, readonlyAccess)
|
||||||
|
if (sessionOrError.isFailed()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionOrError.getValue()
|
||||||
|
}
|
||||||
|
|
||||||
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
override getDiagnostics(): Promise<DiagnosticInfo | undefined> {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
session: {
|
session: {
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { JwtSession } from './JwtSession'
|
|
||||||
import { TokenSession } from './TokenSession'
|
|
||||||
import { RawSessionPayload, RawStorageValue } from './Types'
|
|
||||||
|
|
||||||
export function SessionFromRawStorageValue(raw: RawStorageValue): JwtSession | TokenSession {
|
|
||||||
if ('jwt' in raw) {
|
|
||||||
return new JwtSession(raw.jwt as string)
|
|
||||||
} else {
|
|
||||||
const rawSession = raw as RawSessionPayload
|
|
||||||
return new TokenSession(
|
|
||||||
rawSession.accessToken,
|
|
||||||
rawSession.accessExpiration,
|
|
||||||
rawSession.refreshToken,
|
|
||||||
rawSession.refreshExpiration,
|
|
||||||
rawSession.readonlyAccess,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Session } from './Session'
|
|
||||||
|
|
||||||
/** Legacy, for protocol versions <= 003 */
|
|
||||||
|
|
||||||
export class JwtSession extends Session {
|
|
||||||
public jwt: string
|
|
||||||
|
|
||||||
constructor(jwt: string) {
|
|
||||||
super()
|
|
||||||
this.jwt = jwt
|
|
||||||
}
|
|
||||||
|
|
||||||
public get authorizationValue(): string {
|
|
||||||
return this.jwt
|
|
||||||
}
|
|
||||||
|
|
||||||
public canExpire(): false {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export abstract class Session {
|
|
||||||
public abstract canExpire(): boolean
|
|
||||||
|
|
||||||
/** Return the token that should be included in the header of authorized network requests */
|
|
||||||
public abstract get authorizationValue(): string
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { SessionBody, SessionRenewalResponse } from '@standardnotes/responses'
|
|
||||||
import { Session } from './Session'
|
|
||||||
|
|
||||||
/** For protocol versions >= 004 */
|
|
||||||
export class TokenSession extends Session {
|
|
||||||
static FromApiResponse(response: SessionRenewalResponse) {
|
|
||||||
const body = response.data.session as SessionBody
|
|
||||||
const accessToken: string = body.access_token
|
|
||||||
const refreshToken: string = body.refresh_token
|
|
||||||
const accessExpiration: number = body.access_expiration
|
|
||||||
const refreshExpiration: number = body.refresh_expiration
|
|
||||||
const readonlyAccess: boolean = body.readonly_access
|
|
||||||
|
|
||||||
return new TokenSession(accessToken, accessExpiration, refreshToken, refreshExpiration, readonlyAccess)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public accessToken: string,
|
|
||||||
public accessExpiration: number,
|
|
||||||
public refreshToken: string,
|
|
||||||
public refreshExpiration: number,
|
|
||||||
private readonlyAccess: boolean,
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
isReadOnly() {
|
|
||||||
return this.readonlyAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
private getExpireAt() {
|
|
||||||
return this.accessExpiration || 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public get authorizationValue() {
|
|
||||||
return this.accessToken
|
|
||||||
}
|
|
||||||
|
|
||||||
public canExpire() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
public isExpired() {
|
|
||||||
return this.getExpireAt() < Date.now()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1 @@
|
|||||||
export * from './Generator'
|
|
||||||
export * from './JwtSession'
|
|
||||||
export * from './Session'
|
|
||||||
export * from './TokenSession'
|
|
||||||
export * from './Types'
|
export * from './Types'
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('server session', function () {
|
|||||||
|
|
||||||
async function sleepUntilSessionExpires(application, basedOnAccessToken = true) {
|
async function sleepUntilSessionExpires(application, basedOnAccessToken = true) {
|
||||||
const currentSession = application.apiService.session
|
const currentSession = application.apiService.session
|
||||||
const timestamp = basedOnAccessToken ? currentSession.accessExpiration : currentSession.refreshExpiration
|
const timestamp = basedOnAccessToken ? currentSession.accessToken.expiresAt : currentSession.refreshToken.expiresAt
|
||||||
const timeRemaining = (timestamp - Date.now()) / 1000 // in ms
|
const timeRemaining = (timestamp - Date.now()) / 1000 // in ms
|
||||||
/*
|
/*
|
||||||
If the token has not expired yet, we will return the remaining time.
|
If the token has not expired yet, we will return the remaining time.
|
||||||
@@ -98,12 +98,12 @@ describe('server session', function () {
|
|||||||
// After the above sync request is completed, we obtain the session information.
|
// After the above sync request is completed, we obtain the session information.
|
||||||
const sessionAfterSync = this.application.apiService.getSession()
|
const sessionAfterSync = this.application.apiService.getSession()
|
||||||
|
|
||||||
expect(sessionBeforeSync).to.not.equal(sessionAfterSync)
|
expect(sessionBeforeSync.equals(sessionAfterSync)).to.not.equal(true)
|
||||||
expect(sessionBeforeSync.accessToken).to.not.equal(sessionAfterSync.accessToken)
|
expect(sessionBeforeSync.accessToken.value).to.not.equal(sessionAfterSync.accessToken.value)
|
||||||
expect(sessionBeforeSync.refreshToken).to.not.equal(sessionAfterSync.refreshToken)
|
expect(sessionBeforeSync.refreshToken.value).to.not.equal(sessionAfterSync.refreshToken.value)
|
||||||
expect(sessionBeforeSync.accessExpiration).to.be.lessThan(sessionAfterSync.accessExpiration)
|
expect(sessionBeforeSync.accessToken.expiresAt).to.be.lessThan(sessionAfterSync.accessToken.expiresAt)
|
||||||
// New token should expire in the future.
|
// New token should expire in the future.
|
||||||
expect(sessionAfterSync.accessExpiration).to.be.greaterThan(Date.now())
|
expect(sessionAfterSync.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
|
it('should succeed when a sync request is perfomed after signing into an ephemeral session', async function () {
|
||||||
@@ -142,14 +142,22 @@ describe('server session', function () {
|
|||||||
const sessionFromStorage = await getSessionFromStorage(this.application)
|
const sessionFromStorage = await getSessionFromStorage(this.application)
|
||||||
const sessionFromApiService = this.application.apiService.getSession()
|
const sessionFromApiService = this.application.apiService.getSession()
|
||||||
|
|
||||||
expect(sessionFromStorage).to.equal(sessionFromApiService)
|
expect(sessionFromStorage.accessToken).to.equal(sessionFromApiService.accessToken.value)
|
||||||
|
expect(sessionFromStorage.refreshToken).to.equal(sessionFromApiService.refreshToken.value)
|
||||||
|
expect(sessionFromStorage.accessExpiration).to.equal(sessionFromApiService.accessToken.expiresAt)
|
||||||
|
expect(sessionFromStorage.refreshExpiration).to.equal(sessionFromApiService.refreshToken.expiresAt)
|
||||||
|
expect(sessionFromStorage.readonlyAccess).to.equal(sessionFromApiService.isReadOnly())
|
||||||
|
|
||||||
await this.application.apiService.refreshSession()
|
await this.application.apiService.refreshSession()
|
||||||
|
|
||||||
const updatedSessionFromStorage = await getSessionFromStorage(this.application)
|
const updatedSessionFromStorage = await getSessionFromStorage(this.application)
|
||||||
const updatedSessionFromApiService = this.application.apiService.getSession()
|
const updatedSessionFromApiService = this.application.apiService.getSession()
|
||||||
|
|
||||||
expect(updatedSessionFromStorage).to.equal(updatedSessionFromApiService)
|
expect(updatedSessionFromStorage.accessToken).to.equal(updatedSessionFromApiService.accessToken.value)
|
||||||
|
expect(updatedSessionFromStorage.refreshToken).to.equal(updatedSessionFromApiService.refreshToken.value)
|
||||||
|
expect(updatedSessionFromStorage.accessExpiration).to.equal(updatedSessionFromApiService.accessToken.expiresAt)
|
||||||
|
expect(updatedSessionFromStorage.refreshExpiration).to.equal(updatedSessionFromApiService.refreshToken.expiresAt)
|
||||||
|
expect(updatedSessionFromStorage.readonlyAccess).to.equal(updatedSessionFromApiService.isReadOnly())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should be performed successfully and terminate session with a valid access token', async function () {
|
it('should be performed successfully and terminate session with a valid access token', async function () {
|
||||||
@@ -221,8 +229,16 @@ describe('server session', function () {
|
|||||||
let { application, password } = await Factory.createAndInitSimpleAppContext({
|
let { application, password } = await Factory.createAndInitSimpleAppContext({
|
||||||
registerUser: true,
|
registerUser: true,
|
||||||
})
|
})
|
||||||
const fakeSession = application.apiService.getSession()
|
|
||||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
application.diskStorageService.setValue(StorageKey.Session, {
|
||||||
|
accessToken: 'this-is-a-fake-token-1234',
|
||||||
|
refreshToken: 'this-is-a-fake-token-1234',
|
||||||
|
accessExpiration: 999999999999999,
|
||||||
|
refreshExpiration: 99999999999999,
|
||||||
|
readonlyAccess: false,
|
||||||
|
})
|
||||||
|
application.sessions.initializeFromDisk()
|
||||||
|
|
||||||
Factory.ignoreChallenges(application)
|
Factory.ignoreChallenges(application)
|
||||||
|
|
||||||
const newEmail = UuidGenerator.GenerateUuid()
|
const newEmail = UuidGenerator.GenerateUuid()
|
||||||
@@ -311,8 +327,15 @@ describe('server session', function () {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fakeSession = this.application.apiService.getSession()
|
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||||
fakeSession.accessToken = 'this-is-a-fake-token-1234'
|
accessToken: 'this-is-a-fake-token-1234',
|
||||||
|
refreshToken: 'this-is-a-fake-token-1234',
|
||||||
|
accessExpiration: 999999999999999,
|
||||||
|
refreshExpiration: 99999999999999,
|
||||||
|
readonlyAccess: false,
|
||||||
|
})
|
||||||
|
this.application.sessions.initializeFromDisk()
|
||||||
|
|
||||||
Factory.ignoreChallenges(this.application)
|
Factory.ignoreChallenges(this.application)
|
||||||
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
|
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
|
||||||
expect(changePasswordResponse.error.message).to.equal('Invalid login credentials.')
|
expect(changePasswordResponse.error.message).to.equal('Invalid login credentials.')
|
||||||
@@ -354,7 +377,7 @@ describe('server session', function () {
|
|||||||
expect(currentSession).to.be.ok
|
expect(currentSession).to.be.ok
|
||||||
expect(currentSession.accessToken).to.be.ok
|
expect(currentSession.accessToken).to.be.ok
|
||||||
expect(currentSession.refreshToken).to.be.ok
|
expect(currentSession.refreshToken).to.be.ok
|
||||||
expect(currentSession.accessExpiration).to.be.greaterThan(Date.now())
|
expect(currentSession.accessToken.expiresAt).to.be.greaterThan(Date.now())
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fail when renewing a session with an expired refresh token', async function () {
|
it('should fail when renewing a session with an expired refresh token', async function () {
|
||||||
@@ -392,10 +415,16 @@ describe('server session', function () {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
const fakeSession = this.application.apiService.getSession()
|
const originalSession = this.application.apiService.getSession()
|
||||||
fakeSession.refreshToken = 'this-is-a-fake-token-1234'
|
|
||||||
|
|
||||||
await this.application.apiService.setSession(fakeSession, true)
|
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||||
|
accessToken: originalSession.accessToken.value,
|
||||||
|
refreshToken: 'this-is-a-fake-token-1234',
|
||||||
|
accessExpiration: originalSession.accessToken.expiresAt,
|
||||||
|
refreshExpiration: originalSession.refreshToken.expiresAt,
|
||||||
|
readonlyAccess: false,
|
||||||
|
})
|
||||||
|
this.application.sessions.initializeFromDisk()
|
||||||
|
|
||||||
const refreshSessionResponse = await this.application.apiService.refreshSession()
|
const refreshSessionResponse = await this.application.apiService.refreshSession()
|
||||||
|
|
||||||
@@ -530,8 +559,14 @@ describe('server session', function () {
|
|||||||
const oldRootKey = await appA.protocolService.getRootKey()
|
const oldRootKey = await appA.protocolService.getRootKey()
|
||||||
|
|
||||||
/** Set the session as nonsense */
|
/** Set the session as nonsense */
|
||||||
appA.apiService.session.accessToken = 'foo'
|
appA.diskStorageService.setValue(StorageKey.Session, {
|
||||||
appA.apiService.session.refreshToken = 'bar'
|
accessToken: 'foo',
|
||||||
|
refreshToken: 'bar',
|
||||||
|
accessExpiration: 999999999999999,
|
||||||
|
refreshExpiration: 999999999999999,
|
||||||
|
readonlyAccess: false,
|
||||||
|
})
|
||||||
|
appA.sessions.initializeFromDisk()
|
||||||
|
|
||||||
/** Perform an authenticated network request */
|
/** Perform an authenticated network request */
|
||||||
await appA.sync.sync()
|
await appA.sync.sync()
|
||||||
@@ -540,8 +575,8 @@ describe('server session', function () {
|
|||||||
await Factory.sleep(5.0)
|
await Factory.sleep(5.0)
|
||||||
|
|
||||||
expect(didPromptForSignIn).to.equal(true)
|
expect(didPromptForSignIn).to.equal(true)
|
||||||
expect(appA.apiService.session.accessToken).to.not.equal('foo')
|
expect(appA.apiService.session.accessToken.value).to.not.equal('foo')
|
||||||
expect(appA.apiService.session.refreshToken).to.not.equal('bar')
|
expect(appA.apiService.session.refreshToken.value).to.not.equal('bar')
|
||||||
|
|
||||||
/** Expect that the session recovery replaces the global root key */
|
/** Expect that the session recovery replaces the global root key */
|
||||||
const newRootKey = await appA.protocolService.getRootKey()
|
const newRootKey = await appA.protocolService.getRootKey()
|
||||||
@@ -646,9 +681,14 @@ describe('server session', function () {
|
|||||||
password: this.password,
|
password: this.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
const invalidSession = this.application.apiService.getSession()
|
this.application.diskStorageService.setValue(StorageKey.Session, {
|
||||||
invalidSession.accessToken = undefined
|
accessToken: undefined,
|
||||||
invalidSession.refreshToken = undefined
|
refreshToken: undefined,
|
||||||
|
accessExpiration: 999999999999999,
|
||||||
|
refreshExpiration: 999999999999999,
|
||||||
|
readonlyAccess: false,
|
||||||
|
})
|
||||||
|
this.application.sessions.initializeFromDisk()
|
||||||
|
|
||||||
const storageKey = this.application.diskStorageService.getPersistenceKey()
|
const storageKey = this.application.diskStorageService.getPersistenceKey()
|
||||||
expect(localStorage.getItem(storageKey)).to.be.ok
|
expect(localStorage.getItem(storageKey)).to.be.ok
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as Factory from './lib/factory.js'
|
|||||||
chai.use(chaiAsPromised)
|
chai.use(chaiAsPromised)
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
describe.skip('subscriptions', function () {
|
describe('subscriptions', function () {
|
||||||
this.timeout(Factory.TwentySecondTimeout)
|
this.timeout(Factory.TwentySecondTimeout)
|
||||||
|
|
||||||
let application
|
let application
|
||||||
|
|||||||
@@ -83,5 +83,8 @@
|
|||||||
"webpack": "*",
|
"webpack": "*",
|
||||||
"webpack-cli": "*",
|
"webpack-cli": "*",
|
||||||
"webpack-merge": "^5.8.0"
|
"webpack-merge": "^5.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@standardnotes/domain-core": "^1.11.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
20
yarn.lock
20
yarn.lock
@@ -5384,6 +5384,7 @@ __metadata:
|
|||||||
resolution: "@standardnotes/api@workspace:packages/api"
|
resolution: "@standardnotes/api@workspace:packages/api"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common": ^1.45.0
|
"@standardnotes/common": ^1.45.0
|
||||||
|
"@standardnotes/domain-core": ^1.11.0
|
||||||
"@standardnotes/encryption": "workspace:*"
|
"@standardnotes/encryption": "workspace:*"
|
||||||
"@standardnotes/models": "workspace:*"
|
"@standardnotes/models": "workspace:*"
|
||||||
"@standardnotes/responses": "workspace:*"
|
"@standardnotes/responses": "workspace:*"
|
||||||
@@ -5610,6 +5611,17 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@standardnotes/domain-core@npm:^1.11.0":
|
||||||
|
version: 1.11.0
|
||||||
|
resolution: "@standardnotes/domain-core@npm:1.11.0"
|
||||||
|
dependencies:
|
||||||
|
reflect-metadata: ^0.1.13
|
||||||
|
shallow-equal-object: ^1.1.1
|
||||||
|
uuid: ^9.0.0
|
||||||
|
checksum: cf4c9b7534338a8d5b8322a472621a2b2dde3cc3fe2d3c3c0eb12a3bdb7ba5f58bdfcc900338e5ba509311eeca6f163d95bffd46c40301532d974f397b57554e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@standardnotes/domain-events@npm:^2.88.0":
|
"@standardnotes/domain-events@npm:^2.88.0":
|
||||||
version: 2.88.0
|
version: 2.88.0
|
||||||
resolution: "@standardnotes/domain-events@npm:2.88.0"
|
resolution: "@standardnotes/domain-events@npm:2.88.0"
|
||||||
@@ -6088,6 +6100,7 @@ __metadata:
|
|||||||
"@babel/preset-env": "*"
|
"@babel/preset-env": "*"
|
||||||
"@standardnotes/api": "workspace:*"
|
"@standardnotes/api": "workspace:*"
|
||||||
"@standardnotes/common": ^1.45.0
|
"@standardnotes/common": ^1.45.0
|
||||||
|
"@standardnotes/domain-core": ^1.11.0
|
||||||
"@standardnotes/domain-events": ^2.88.0
|
"@standardnotes/domain-events": ^2.88.0
|
||||||
"@standardnotes/encryption": "workspace:*"
|
"@standardnotes/encryption": "workspace:*"
|
||||||
"@standardnotes/features": "workspace:*"
|
"@standardnotes/features": "workspace:*"
|
||||||
@@ -27198,6 +27211,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"shallow-equal-object@npm:^1.1.1":
|
||||||
|
version: 1.1.1
|
||||||
|
resolution: "shallow-equal-object@npm:1.1.1"
|
||||||
|
checksum: e925aa4511bdf246a10577c983b9c540ea3ea443dbc8f2f336fd398c4ddee682389f19a9f0c2c9e7a99ab62baff5f2f716fbe108729454ff7b9f787d09743cc9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"shallowequal@npm:^1.1.0":
|
"shallowequal@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "shallowequal@npm:1.1.0"
|
resolution: "shallowequal@npm:1.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user