Compare commits

...
Author SHA1 Message Date
StandardNotes CI 6b774db222 chore(release): publish
- @standardnotes/[email protected].0
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].0
 - @standardnotes/[email protected].4
 - @standardnotes/[email protected].4
2023-01-09 06:23:12 +00:00
Karol SójkoandMo be028ff87b feat(snjs): add sign in with recovery codes use case (#2130)
* feat(snjs): add sign in with recovery codes use case

* fix(snjs): code review adjustments

* fix(snjs): remove unnecessary exposed getter

* fix(services): waiting for event handling

* fix: preferences test

Co-authored-by: Mo <[email protected]>
2023-01-09 06:52:56 +01:00
StandardNotes CI 5f09fc74da chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
2023-01-07 09:52:22 +00:00
Aman Harwara 340f4f7f09 fix: Fixed issue where search button would not show up in Files view 2023-01-07 14:50:06 +05:30
Aman Harwara 371a47d838 refactor: new note default editor options 2023-01-07 14:47:17 +05:30
StandardNotes CI b839ebb58a chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].2
2023-01-06 20:16:01 +00:00
Aman Harwara 482c4d1ca4 fix: Fixed issue where the "Attached to" in table view would report an extra number of links 2023-01-07 01:18:50 +05:30
StandardNotes CI 5f96281948 chore(release): publish
- @standardnotes/[email protected]
 - @standardnotes/[email protected].9
 - @standardnotes/[email protected]
 - @standardnotes/[email protected].3
 - @standardnotes/[email protected].1
2023-01-06 16:02:46 +00:00
Aman Harwara 2e79fc412c fix: Fixed issue where the importer would not correctly parse Google Keep JSON notes 2023-01-06 20:57:18 +05:30
63 changed files with 1060 additions and 138 deletions
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.22.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
### Features
* **snjs:** add sign in with recovery codes use case ([#2130](https://github.com/standardnotes/app/issues/2130)) ([be028ff](https://github.com/standardnotes/app/commit/be028ff87bb5306fc266b70567cbea8a13547e7e))
## [1.21.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/api
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api",
"version": "1.21.4",
"version": "1.22.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -0,0 +1,5 @@
export enum AuthApiOperations {
GenerateRecoveryCodes,
GetRecoveryKeyParams,
SignInWithRecoveryCodes,
}
@@ -0,0 +1,89 @@
import { ApiVersion } from '../../Api'
import { ApiCallError } from '../../Error/ApiCallError'
import { ErrorMessage } from '../../Error/ErrorMessage'
import {
GenerateRecoveryCodesResponse,
RecoveryKeyParamsResponse,
SignInWithRecoveryCodesResponse,
} from '../../Response'
import { AuthServerInterface } from '../../Server'
import { AuthApiOperations } from './AuthApiOperations'
import { AuthApiServiceInterface } from './AuthApiServiceInterface'
export class AuthApiService implements AuthApiServiceInterface {
private operationsInProgress: Map<AuthApiOperations, boolean>
constructor(private authServer: AuthServerInterface) {
this.operationsInProgress = new Map()
}
async generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse> {
if (this.operationsInProgress.get(AuthApiOperations.GenerateRecoveryCodes)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, true)
try {
const response = await this.authServer.generateRecoveryCodes()
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(AuthApiOperations.GenerateRecoveryCodes, false)
}
}
async recoveryKeyParams(dto: {
username: string
codeChallenge: string
recoveryCodes: string
}): Promise<RecoveryKeyParamsResponse> {
if (this.operationsInProgress.get(AuthApiOperations.GetRecoveryKeyParams)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(AuthApiOperations.GetRecoveryKeyParams, true)
try {
const response = await this.authServer.recoveryKeyParams({
apiVersion: ApiVersion.v0,
...dto,
})
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(AuthApiOperations.GetRecoveryKeyParams, false)
}
}
async signInWithRecoveryCodes(dto: {
username: string
password: string
codeVerifier: string
recoveryCodes: string
}): Promise<SignInWithRecoveryCodesResponse> {
if (this.operationsInProgress.get(AuthApiOperations.SignInWithRecoveryCodes)) {
throw new ApiCallError(ErrorMessage.GenericInProgress)
}
this.operationsInProgress.set(AuthApiOperations.SignInWithRecoveryCodes, true)
try {
const response = await this.authServer.signInWithRecoveryCodes({
apiVersion: ApiVersion.v0,
...dto,
})
return response
} catch (error) {
throw new ApiCallError(ErrorMessage.GenericFail)
} finally {
this.operationsInProgress.set(AuthApiOperations.SignInWithRecoveryCodes, false)
}
}
}
@@ -0,0 +1,20 @@
import {
GenerateRecoveryCodesResponse,
RecoveryKeyParamsResponse,
SignInWithRecoveryCodesResponse,
} from '../../Response'
export interface AuthApiServiceInterface {
generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse>
recoveryKeyParams(dto: {
username: string
codeChallenge: string
recoveryCodes: string
}): Promise<RecoveryKeyParamsResponse>
signInWithRecoveryCodes(dto: {
username: string
password: string
codeVerifier: string
recoveryCodes: string
}): Promise<SignInWithRecoveryCodesResponse>
}
+3
View File
@@ -1,3 +1,6 @@
export * from './Auth/AuthApiOperations'
export * from './Auth/AuthApiService'
export * from './Auth/AuthApiServiceInterface'
export * from './Authenticator/AuthenticatorApiOperations'
export * from './Authenticator/AuthenticatorApiService'
export * from './Authenticator/AuthenticatorApiServiceInterface'
@@ -1 +1 @@
export type HttpRequestParams = Record<string, unknown>
export type HttpRequestParams = unknown
+2 -2
View File
@@ -276,9 +276,9 @@ export class HttpService implements HttpServiceInterface {
}
private urlForUrlAndParams(url: string, params: HttpRequestParams) {
const keyValueString = Object.keys(params)
const keyValueString = Object.keys(params as Record<string, unknown>)
.map((key) => {
return key + '=' + encodeURIComponent(params[key] as string)
return key + '=' + encodeURIComponent((params as Record<string, unknown>)[key] as string)
})
.join('&')
@@ -0,0 +1,6 @@
export interface RecoveryKeyParamsRequestParams {
apiVersion: string
username: string
codeChallenge: string
recoveryCodes: string
}
@@ -0,0 +1,7 @@
export interface SignInWithRecoveryCodesRequestParams {
apiVersion: string
username: string
password: string
codeVerifier: string
recoveryCodes: string
}
+2
View File
@@ -5,6 +5,8 @@ export * from './Authenticator/GenerateAuthenticatorRegistrationOptionsRequestPa
export * from './Authenticator/ListAuthenticatorsRequestParams'
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseRequestParams'
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseRequestParams'
export * from './Recovery/RecoveryKeyParamsRequestParams'
export * from './Recovery/SignInWithRecoveryCodesRequestParams'
export * from './Subscription/AppleIAPConfirmRequestParams'
export * from './Subscription/SubscriptionInviteAcceptRequestParams'
export * from './Subscription/SubscriptionInviteCancelRequestParams'
@@ -0,0 +1,10 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { GenerateRecoveryCodesResponseBody } from './GenerateRecoveryCodesResponseBody'
export interface GenerateRecoveryCodesResponse extends HttpResponse {
data: Either<GenerateRecoveryCodesResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1,3 @@
export interface GenerateRecoveryCodesResponseBody {
recoveryCodes: string
}
@@ -0,0 +1,10 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { RecoveryKeyParamsResponseBody } from './RecoveryKeyParamsResponseBody'
export interface RecoveryKeyParamsResponse extends HttpResponse {
data: Either<RecoveryKeyParamsResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1,5 @@
import { KeyParamsData } from '@standardnotes/responses'
export interface RecoveryKeyParamsResponseBody {
keyParams: KeyParamsData
}
@@ -0,0 +1,10 @@
import { Either } from '@standardnotes/common'
import { HttpErrorResponseBody } from '../../Http/HttpErrorResponseBody'
import { HttpResponse } from '../../Http/HttpResponse'
import { SignInWithRecoveryCodesResponseBody } from './SignInWithRecoveryCodesResponseBody'
export interface SignInWithRecoveryCodesResponse extends HttpResponse {
data: Either<SignInWithRecoveryCodesResponseBody, HttpErrorResponseBody>
}
@@ -0,0 +1,11 @@
import { KeyParamsData, SessionBody } from '@standardnotes/responses'
export interface SignInWithRecoveryCodesResponseBody {
session: SessionBody
key_params: KeyParamsData
user: {
uuid: string
email: string
protocolVersion: string
}
}
@@ -12,6 +12,12 @@ export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponse
export * from './Authenticator/VerifyAuthenticatorAuthenticationResponseResponseBody'
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponse'
export * from './Authenticator/VerifyAuthenticatorRegistrationResponseResponseBody'
export * from './Recovery/GenerateRecoveryCodesResponse'
export * from './Recovery/GenerateRecoveryCodesResponseBody'
export * from './Recovery/RecoveryKeyParamsResponse'
export * from './Recovery/RecoveryKeyParamsResponseBody'
export * from './Recovery/SignInWithRecoveryCodesResponse'
export * from './Recovery/SignInWithRecoveryCodesResponseBody'
export * from './Subscription/AppleIAPConfirmResponse'
export * from './Subscription/AppleIAPConfirmResponseBody'
export * from './Subscription/SubscriptionInviteAcceptResponse'
@@ -0,0 +1,33 @@
import { HttpServiceInterface } from '../../Http/HttpServiceInterface'
import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request'
import {
GenerateRecoveryCodesResponse,
RecoveryKeyParamsResponse,
SignInWithRecoveryCodesResponse,
} from '../../Response'
import { AuthServerInterface } from './AuthServerInterface'
import { Paths } from './Paths'
export class AuthServer implements AuthServerInterface {
constructor(private httpService: HttpServiceInterface) {}
async generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse> {
const response = await this.httpService.post(Paths.v1.generateRecoveryCodes)
return response as GenerateRecoveryCodesResponse
}
async recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse> {
const response = await this.httpService.post(Paths.v1.recoveryKeyParams, params)
return response as RecoveryKeyParamsResponse
}
async signInWithRecoveryCodes(
params: SignInWithRecoveryCodesRequestParams,
): Promise<SignInWithRecoveryCodesResponse> {
const response = await this.httpService.post(Paths.v1.signInWithRecoveryCodes, params)
return response as SignInWithRecoveryCodesResponse
}
}
@@ -0,0 +1,12 @@
import { RecoveryKeyParamsRequestParams, SignInWithRecoveryCodesRequestParams } from '../../Request'
import {
GenerateRecoveryCodesResponse,
RecoveryKeyParamsResponse,
SignInWithRecoveryCodesResponse,
} from '../../Response'
export interface AuthServerInterface {
generateRecoveryCodes(): Promise<GenerateRecoveryCodesResponse>
recoveryKeyParams(params: RecoveryKeyParamsRequestParams): Promise<RecoveryKeyParamsResponse>
signInWithRecoveryCodes(params: SignInWithRecoveryCodesRequestParams): Promise<SignInWithRecoveryCodesResponse>
}
@@ -2,8 +2,15 @@ const SessionPaths = {
refreshSession: '/v1/sessions/refresh',
}
const RecoveryPaths = {
generateRecoveryCodes: '/v1/auth/recovery/codes',
recoveryKeyParams: '/v1/auth/recovery/login-params',
signInWithRecoveryCodes: '/v1/auth/recovery/login',
}
export const Paths = {
v1: {
...SessionPaths,
...RecoveryPaths,
},
}
+2
View File
@@ -1,3 +1,5 @@
export * from './Auth/AuthServer'
export * from './Auth/AuthServerInterface'
export * from './Authenticator/AuthenticatorServer'
export * from './Authenticator/AuthenticatorServerInterface'
export * from './Subscription/SubscriptionServer'
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.104.76](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.75](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-07)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.74](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.73](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/desktop
## [3.104.72](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/desktop
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@standardnotes/desktop",
"main": "./app/dist/index.js",
"version": "3.104.72",
"version": "3.104.76",
"license": "AGPL-3.0-or-later",
"author": "Standard Notes.",
"private": true,
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.20.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
### Features
* **snjs:** add sign in with recovery codes use case ([#2130](https://github.com/standardnotes/app/issues/2130)) ([be028ff](https://github.com/standardnotes/app/commit/be028ff87bb5306fc266b70567cbea8a13547e7e))
## [1.19.35](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/encryption
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/encryption",
"version": "1.19.35",
"version": "1.20.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -32,6 +32,9 @@ export interface EncryptionProviderInterface {
getKeyEmbeddedKeyParams(key: EncryptedPayloadInterface): SNRootKeyParams | undefined
computeRootKey(password: string, keyParams: SNRootKeyParams): Promise<RootKeyInterface>
supportedVersions(): ProtocolVersion[]
isVersionNewerThanLibraryVersion(version: ProtocolVersion): boolean
platformSupportsKeyDerivation(keyParams: SNRootKeyParams): boolean
computeWrappingKey(passcode: string): Promise<RootKeyInterface>
getUserVersion(): ProtocolVersion | undefined
decryptBackupFile(
file: BackupFile,
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.28.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/filepicker
## [1.28.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/filepicker
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/filepicker",
"version": "1.28.2",
"version": "1.28.3",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.14.13](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/files
## [1.14.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/files
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files",
"version": "1.14.12",
"version": "1.14.13",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.50.12](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.11](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-07)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.10](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.9](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/mobile
## [3.50.8](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/mobile
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/mobile",
"version": "3.50.8",
"version": "3.50.12",
"author": "Standard Notes.",
"private": true,
"license": "AGPL-3.0-or-later",
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.99](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.98](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-07)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.97](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.96](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/releases
## [1.4.95](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/releases",
"version": "1.4.95",
"version": "1.4.99",
"license": "AGPL-3.0-or-later",
"main": "dist/releases.json",
"types": "dist/index.d.ts",
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.53.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
### Features
* **snjs:** add sign in with recovery codes use case ([#2130](https://github.com/standardnotes/app/issues/2130)) ([be028ff](https://github.com/standardnotes/app/commit/be028ff87bb5306fc266b70567cbea8a13547e7e))
## [1.52.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/services
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/services",
"version": "1.52.2",
"version": "1.53.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -19,6 +19,7 @@
"@standardnotes/api": "workspace:^",
"@standardnotes/auth": "^3.19.4",
"@standardnotes/common": "^1.45.0",
"@standardnotes/domain-core": "^1.11.0",
"@standardnotes/encryption": "workspace:^",
"@standardnotes/files": "workspace:^",
"@standardnotes/models": "workspace:^",
@@ -1,6 +1,7 @@
import { ApplicationIdentifier, ContentType } from '@standardnotes/common'
import { BackupFile, DecryptedItemInterface, ItemStream, Platform, PrefKey, PrefValue } from '@standardnotes/models'
import { FilesClientInterface } from '@standardnotes/files'
import { AlertService } from '../Alert/AlertService'
import { ComponentManagerInterface } from '../Component/ComponentManagerInterface'
import { ApplicationEvent } from '../Event/ApplicationEvent'
@@ -0,0 +1,28 @@
import { AnyKeyParamsContent } from '@standardnotes/common'
import { SessionBody } from '@standardnotes/responses'
export interface AuthClientInterface {
generateRecoveryCodes(): Promise<string | false>
recoveryKeyParams(dto: {
username: string
codeChallenge: string
recoveryCodes: string
}): Promise<AnyKeyParamsContent | false>
signInWithRecoveryCodes(dto: {
username: string
password: string
codeVerifier: string
recoveryCodes: string
}): Promise<
| {
keyParams: AnyKeyParamsContent
session: SessionBody
user: {
uuid: string
email: string
protocolVersion: string
}
}
| false
>
}
@@ -0,0 +1,82 @@
import { AuthApiServiceInterface } from '@standardnotes/api'
import { AnyKeyParamsContent } from '@standardnotes/common'
import { SessionBody } from '@standardnotes/responses'
import { InternalEventBusInterface } from '../Internal/InternalEventBusInterface'
import { AbstractService } from '../Service/AbstractService'
import { AuthClientInterface } from './AuthClientInterface'
export class AuthManager extends AbstractService implements AuthClientInterface {
constructor(
private authApiService: AuthApiServiceInterface,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
}
async generateRecoveryCodes(): Promise<string | false> {
try {
const result = await this.authApiService.generateRecoveryCodes()
if (result.data.error) {
return false
}
return result.data.recoveryCodes
} catch (error) {
return false
}
}
async recoveryKeyParams(dto: {
username: string
codeChallenge: string
recoveryCodes: string
}): Promise<AnyKeyParamsContent | false> {
try {
const result = await this.authApiService.recoveryKeyParams(dto)
if (result.data.error) {
return false
}
return result.data.keyParams as AnyKeyParamsContent
} catch (error) {
return false
}
}
async signInWithRecoveryCodes(dto: {
username: string
password: string
codeVerifier: string
recoveryCodes: string
}): Promise<
| {
keyParams: AnyKeyParamsContent
session: SessionBody
user: {
uuid: string
email: string
protocolVersion: string
}
}
| false
> {
try {
const result = await this.authApiService.signInWithRecoveryCodes(dto)
if (result.data.error) {
return false
}
return {
keyParams: result.data.key_params as AnyKeyParamsContent,
session: result.data.session,
user: result.data.user,
}
} catch (error) {
return false
}
}
}
@@ -1,7 +1,8 @@
import { UserRegistrationResponseBody } from '@standardnotes/api'
import { ProtocolVersion } from '@standardnotes/common'
import { SNRootKey } from '@standardnotes/encryption'
import { RootKeyInterface } from '@standardnotes/models'
import { ClientDisplayableError, HttpResponse, SignInResponse, User } from '@standardnotes/responses'
import { ClientDisplayableError, HttpResponse, SessionBody, SignInResponse, User } from '@standardnotes/responses'
import { Base64String } from '@standardnotes/sncrypto-common'
import { SessionManagerResponse } from './SessionManagerResponse'
@@ -31,4 +32,13 @@ export interface SessionsClientInterface {
wrappingKey?: RootKeyInterface
newEmail?: string
}): Promise<SessionManagerResponse>
handleAuthentication(dto: {
session: SessionBody
user: {
uuid: string
email: string
}
rootKey: SNRootKey
wrappingKey?: SNRootKey
}): Promise<void>
}
@@ -1,7 +1,7 @@
import { Base64String } from '@standardnotes/sncrypto-common'
import { EncryptionProviderInterface, SNRootKey, SNRootKeyParams } from '@standardnotes/encryption'
import { HttpResponse, SignInResponse, User } from '@standardnotes/responses'
import { KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
import { Either, KeyParamsOrigination, UserRequestType } from '@standardnotes/common'
import { UuidGenerator } from '@standardnotes/utils'
import { UserApiServiceInterface, UserRegistrationResponseBody } from '@standardnotes/api'
@@ -25,6 +25,8 @@ import { DeinitSource } from '../Application/DeinitSource'
import { StoragePersistencePolicies } from '../Storage/StorageTypes'
import { SessionsClientInterface } from '../Session/SessionsClientInterface'
import { ProtectionsClientInterface } from '../Protection/ProtectionClientInterface'
import { InternalEventHandlerInterface } from '../Internal/InternalEventHandlerInterface'
import { InternalEventInterface } from '../Internal/InternalEventInterface'
export type CredentialsChangeFunctionResponse = { error?: { message: string } }
export type AccountServiceResponse = HttpResponse
@@ -34,11 +36,25 @@ export enum AccountEvent {
SignedOut = 'SignedOut',
}
type AccountEventData = {
export interface SignedInOrRegisteredEventPayload {
ephemeral: boolean
mergeLocal: boolean
awaitSync: boolean
checkIntegrity: boolean
}
export interface SignedOutEventPayload {
source: DeinitSource
}
export class UserService extends AbstractService<AccountEvent, AccountEventData> implements UserClientInterface {
export interface AccountEventData {
payload: Either<SignedInOrRegisteredEventPayload, SignedOutEventPayload>
}
export class UserService
extends AbstractService<AccountEvent, AccountEventData>
implements UserClientInterface, InternalEventHandlerInterface
{
private signingIn = false
private registering = false
@@ -60,6 +76,43 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
super(internalEventBus)
}
async handleEvent(event: InternalEventInterface): Promise<void> {
if (event.type === AccountEvent.SignedInOrRegistered) {
const payload = (event.payload as AccountEventData).payload as SignedInOrRegisteredEventPayload
this.syncService.resetSyncState()
await this.storageService.setPersistencePolicy(
payload.ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
)
if (payload.mergeLocal) {
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
} else {
void this.itemManager.removeAllItemsFromMemory()
await this.clearDatabase()
}
this.unlockSyncing()
const syncPromise = this.syncService
.downloadFirstSync(1_000, {
checkIntegrity: payload.checkIntegrity,
awaitAll: payload.awaitSync,
})
.then(() => {
if (!payload.awaitSync) {
void this.protocolService.decryptErroredPayloads()
}
})
if (payload.awaitSync) {
await syncPromise
await this.protocolService.decryptErroredPayloads()
}
}
}
public override deinit(): void {
super.deinit()
;(this.sessionManager as unknown) = undefined
@@ -97,27 +150,17 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
this.lockSyncing()
const response = await this.sessionManager.register(email, password, ephemeral)
this.syncService.resetSyncState()
await this.notifyEventSync(AccountEvent.SignedInOrRegistered, {
payload: {
ephemeral,
mergeLocal,
awaitSync: true,
checkIntegrity: false,
},
})
await this.storageService.setPersistencePolicy(
ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
)
if (mergeLocal) {
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
} else {
await this.itemManager.removeAllItemsFromMemory()
await this.clearDatabase()
}
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
this.unlockSyncing()
this.registering = false
await this.syncService.downloadFirstSync(300)
void this.protocolService.decryptErroredPayloads()
return response
} catch (error) {
this.unlockSyncing()
@@ -156,39 +199,15 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
const result = await this.sessionManager.signIn(email, password, strict, ephemeral)
if (!result.response.error) {
this.syncService.resetSyncState()
await this.storageService.setPersistencePolicy(
ephemeral ? StoragePersistencePolicies.Ephemeral : StoragePersistencePolicies.Default,
)
if (mergeLocal) {
await this.syncService.markAllItemsAsNeedingSyncAndPersist()
} else {
void this.itemManager.removeAllItemsFromMemory()
await this.clearDatabase()
}
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
this.unlockSyncing()
const syncPromise = this.syncService
.downloadFirstSync(1_000, {
const notifyingFunction = awaitSync ? this.notifyEventSync.bind(this) : this.notifyEvent.bind(this)
await notifyingFunction(AccountEvent.SignedInOrRegistered, {
payload: {
mergeLocal,
awaitSync,
ephemeral,
checkIntegrity: true,
awaitAll: awaitSync,
})
.then(() => {
if (!awaitSync) {
void this.protocolService.decryptErroredPayloads()
}
})
if (awaitSync) {
await syncPromise
await this.protocolService.decryptErroredPayloads()
}
},
})
} else {
this.unlockSyncing()
}
@@ -267,15 +286,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
)
if (!response.error) {
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
this.unlockSyncing()
void this.syncService.downloadFirstSync(1_000, {
checkIntegrity: true,
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
payload: {
mergeLocal: true,
awaitSync: true,
ephemeral: false,
checkIntegrity: true,
},
})
void this.protocolService.decryptErroredPayloads()
}
this.unlockSyncing()
@@ -310,7 +328,7 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
await this.sessionManager.signOut()
await this.protocolService.deleteWorkspaceSpecificKeyStateFromDevice()
await this.storageService.clearAllData()
await this.notifyEvent(AccountEvent.SignedOut, { source })
await this.notifyEvent(AccountEvent.SignedOut, { payload: { source } })
}
if (force) {
@@ -473,7 +491,14 @@ export class UserService extends AbstractService<AccountEvent, AccountEventData>
public async populateSessionFromDemoShareToken(token: Base64String): Promise<void> {
await this.sessionManager.populateSessionFromDemoShareToken(token)
await this.notifyEvent(AccountEvent.SignedInOrRegistered)
await this.notifyEvent(AccountEvent.SignedInOrRegistered, {
payload: {
ephemeral: false,
mergeLocal: false,
checkIntegrity: false,
awaitSync: true,
},
})
}
private async setPasscodeWithoutWarning(passcode: string, origination: KeyParamsOrigination) {
+2
View File
@@ -6,6 +6,8 @@ export * from './Application/ApplicationStage'
export * from './Application/DeinitCallback'
export * from './Application/DeinitSource'
export * from './Application/DeinitMode'
export * from './Auth/AuthClientInterface'
export * from './Auth/AuthManager'
export * from './Authenticator/AuthenticatorClientInterface'
export * from './Authenticator/AuthenticatorManager'
export * from './User/UserClientInterface'
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.159.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
### Features
* **snjs:** add sign in with recovery codes use case ([#2130](https://github.com/standardnotes/app/issues/2130)) ([be028ff](https://github.com/standardnotes/app/commit/be028ff87bb5306fc266b70567cbea8a13547e7e))
## [2.158.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/snjs
+41 -3
View File
@@ -1,8 +1,10 @@
import {
AuthApiService,
AuthenticatorApiService,
AuthenticatorApiServiceInterface,
AuthenticatorServer,
AuthenticatorServerInterface,
AuthServer,
HttpService,
HttpServiceInterface,
SubscriptionApiService,
@@ -69,6 +71,8 @@ import {
AccountEvent,
AuthenticatorClientInterface,
AuthenticatorManager,
AuthClientInterface,
AuthManager,
} from '@standardnotes/services'
import { BackupServiceInterface, FilesClientInterface } from '@standardnotes/files'
import { ComputePrivateUsername } from '@standardnotes/encryption'
@@ -88,9 +92,11 @@ import { SNLog } from '../Log'
import { ChallengeResponse, ListedClientInterface } from '../Services'
import { ApplicationConstructorOptions, FullyResolvedApplicationOptions } from './Options/ApplicationOptions'
import { ApplicationOptionsDefaults } from './Options/Defaults'
import { LegacySession, MapperInterface, Session } from '@standardnotes/domain-core'
import { LegacySession, MapperInterface, Session, UseCaseInterface } from '@standardnotes/domain-core'
import { SessionStorageMapper } from '@Lib/Services/Mapping/SessionStorageMapper'
import { LegacySessionStorageMapper } from '@Lib/Services/Mapping/LegacySessionStorageMapper'
import { SignInWithRecoveryCodes } from '@Lib/Domain/UseCase/SignInWithRecoveryCodes/SignInWithRecoveryCodes'
import { UseCaseContainerInterface } from '@Lib/Domain/UseCase/UseCaseContainerInterface'
/** How often to automatically sync, in milliseconds */
const DEFAULT_AUTO_SYNC_INTERVAL = 30_000
@@ -106,7 +112,7 @@ type ApplicationObserver = {
type ObserverRemover = () => void
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication {
export class SNApplication implements ApplicationInterface, AppGroupManagedApplication, UseCaseContainerInterface {
onDeinit!: ExternalServices.DeinitCallback
/**
@@ -168,6 +174,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private declare authenticatorApiService: AuthenticatorApiServiceInterface
private declare authenticatorServer: AuthenticatorServerInterface
private declare authenticatorManager: AuthenticatorClientInterface
private declare authManager: AuthClientInterface
private declare _signInWithRecoveryCodes: SignInWithRecoveryCodes
private internalEventBus!: ExternalServices.InternalEventBusInterface
@@ -250,6 +259,10 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.workspaceManager
}
get signInWithRecoveryCodes(): UseCaseInterface<void> {
return this._signInWithRecoveryCodes
}
public get files(): FilesClientInterface {
return this.fileService
}
@@ -1150,6 +1163,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.createAuthenticatorServer()
this.createAuthenticatorApiService()
this.createAuthenticatorManager()
this.createAuthManager()
this.createUseCases()
}
private clearServices() {
@@ -1200,6 +1216,8 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
;(this.authenticatorApiService as unknown) = undefined
;(this.authenticatorServer as unknown) = undefined
;(this.authenticatorManager as unknown) = undefined
;(this.authManager as unknown) = undefined
;(this._signInWithRecoveryCodes as unknown) = undefined
this.services = []
}
@@ -1212,6 +1230,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.internalEventBus.addEventHandler(this.featuresService, ExternalServices.ApiServiceEvent.MetaReceived)
this.internalEventBus.addEventHandler(this.integrityService, ExternalServices.SyncEvent.SyncRequestsIntegrityCheck)
this.internalEventBus.addEventHandler(this.syncService, ExternalServices.IntegrityEvent.IntegrityCheckCompleted)
this.internalEventBus.addEventHandler(this.userService, AccountEvent.SignedInOrRegistered)
}
private clearInternalEventBus(): void {
@@ -1348,7 +1367,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
case AccountEvent.SignedOut: {
await this.notifyEvent(ApplicationEvent.SignedOut)
await this.prepareForDeinit()
this.deinit(this.getDeinitMode(), data?.source || DeinitSource.SignOut)
this.deinit(this.getDeinitMode(), data?.payload.source || DeinitSource.SignOut)
break
}
default: {
@@ -1739,4 +1758,23 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
private createAuthenticatorManager() {
this.authenticatorManager = new AuthenticatorManager(this.authenticatorApiService, this.internalEventBus)
}
private createAuthManager() {
const authServer = new AuthServer(this.httpService)
const authApiService = new AuthApiService(authServer)
this.authManager = new AuthManager(authApiService, this.internalEventBus)
}
private createUseCases() {
this._signInWithRecoveryCodes = new SignInWithRecoveryCodes(
this.authManager,
this.protocolService,
this.inMemoryStore,
this.options.crypto,
this.sessionManager,
this.internalEventBus,
)
}
}
@@ -0,0 +1,179 @@
import {
AuthClientInterface,
InternalEventBusInterface,
KeyValueStoreInterface,
SessionsClientInterface,
} from '@standardnotes/services'
import { EncryptionProviderInterface } from '@standardnotes/encryption'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { AnyKeyParamsContent } from '@standardnotes/common'
import { DecryptedPayloadInterface, RootKeyContent, RootKeyInterface } from '@standardnotes/models'
import { SessionBody } from '@standardnotes/responses'
import { SignInWithRecoveryCodes } from './SignInWithRecoveryCodes'
describe('SignInWithRecoveryCodes', () => {
let authManager: AuthClientInterface
let protocolService: EncryptionProviderInterface
let inMemoryStore: KeyValueStoreInterface<string>
let crypto: PureCryptoInterface
let sessionManager: SessionsClientInterface
let internalEventBus: InternalEventBusInterface
const createUseCase = () => new SignInWithRecoveryCodes(
authManager,
protocolService,
inMemoryStore,
crypto,
sessionManager,
internalEventBus,
)
beforeEach(() => {
authManager = {} as jest.Mocked<AuthClientInterface>
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
identifier: '[email protected]',
pw_nonce: 'pw_nonce',
created: new Date().toISOString(),
/** The event that lead to the creation of these params */
origination: 'register',
version: '004',
})
authManager.signInWithRecoveryCodes = jest.fn()
const rootKey = {
serverPassword: 'foobar',
} as jest.Mocked<RootKeyInterface>
const payload = {} as jest.Mocked<DecryptedPayloadInterface<RootKeyContent>>
payload.ejected = jest.fn().mockReturnValue({
uuid: 'uuid',
})
rootKey.payload = payload
protocolService = {} as jest.Mocked<EncryptionProviderInterface>
protocolService.hasAccount = jest.fn()
protocolService.computeRootKey = jest.fn().mockReturnValue(rootKey)
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(true)
protocolService.supportedVersions = jest.fn().mockReturnValue([
'001',
'002',
'003',
'004',
])
protocolService.isVersionNewerThanLibraryVersion = jest.fn()
inMemoryStore = {} as jest.Mocked<KeyValueStoreInterface<string>>
inMemoryStore.setValue = jest.fn()
inMemoryStore.removeValue = jest.fn()
crypto = {} as jest.Mocked<PureCryptoInterface>
crypto.generateRandomKey = jest.fn()
crypto.base64URLEncode = jest.fn()
crypto.sha256 = jest.fn()
sessionManager = {} as jest.Mocked<SessionsClientInterface>
sessionManager.handleAuthentication = jest.fn()
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.publishSync = jest.fn()
})
it('should fail if an account already exists', async () => {
protocolService.hasAccount = jest.fn().mockReturnValue(true)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Tried to sign in when an account already exists.')
})
it('should fail if recovery key params could not be retrieved', async () => {
authManager.recoveryKeyParams = jest.fn().mockReturnValue(false)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Could not retrieve recovery key params')
})
it('should fail if key params has unsupported deriviation', async () => {
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Your account was created on a platform with higher security capabilities than this browser supports. If we attempted to generate your login keys here, it would take hours. Please use a browser with more up to date security capabilities, like Google Chrome or Firefox, to log in.')
})
it('should fail if key params has unsupported version', async () => {
protocolService.isVersionNewerThanLibraryVersion = jest.fn().mockReturnValue(true)
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
identifier: '[email protected]',
pw_nonce: 'pw_nonce',
created: new Date().toISOString(),
/** The event that lead to the creation of these params */
origination: 'register',
version: '006',
})
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('This version of the application does not support your newer account type. Please upgrade to the latest version of Standard Notes to sign in.')
})
it('should fail if key params has expired version', async () => {
protocolService.isVersionNewerThanLibraryVersion = jest.fn().mockReturnValue(false)
authManager.recoveryKeyParams = jest.fn().mockReturnValue({
identifier: '[email protected]',
pw_nonce: 'pw_nonce',
created: new Date().toISOString(),
/** The event that lead to the creation of these params */
origination: 'register',
version: '006',
})
protocolService.platformSupportsKeyDerivation = jest.fn().mockReturnValue(false)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('The protocol version associated with your account is outdated and no longer supported by this application. Please visit standardnotes.com/help/security for more information.')
})
it('should fail if the sign in with recovery codes fails', async () => {
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue(false)
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(true)
expect(result.getError()).toEqual('Could not sign in with recovery codes')
})
it('should sign in with recovery codes', async () => {
authManager.signInWithRecoveryCodes = jest.fn().mockReturnValue({
keyParams: {} as AnyKeyParamsContent,
session: {} as SessionBody,
user: {
uuid: '1-2-3',
email: '[email protected]',
protocolVersion: '004',
}
})
const useCase = createUseCase()
const result = await useCase.execute({ recoveryCodes: 'recovery-codes', password: 'foobar', username: '[email protected]' })
expect(result.isFailed()).toBe(false)
})
})
@@ -0,0 +1,107 @@
import { Result, UseCaseInterface } from '@standardnotes/domain-core'
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
import {
AccountEvent,
AuthClientInterface,
EXPIRED_PROTOCOL_VERSION,
InternalEventBusInterface,
InternalEventPublishStrategy,
KeyValueStoreInterface,
SessionsClientInterface,
StorageKey,
UNSUPPORTED_KEY_DERIVATION,
UNSUPPORTED_PROTOCOL_VERSION,
} from '@standardnotes/services'
import { CreateAnyKeyParams, EncryptionProviderInterface, SNRootKey } from '@standardnotes/encryption'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { SignInWithRecoveryCodesDTO } from './SignInWithRecoveryCodesDTO'
export class SignInWithRecoveryCodes implements UseCaseInterface<void> {
constructor(
private authManager: AuthClientInterface,
private protocolService: EncryptionProviderInterface,
private inMemoryStore: KeyValueStoreInterface<string>,
private crypto: PureCryptoInterface,
private sessionManager: SessionsClientInterface,
private internalEventBus: InternalEventBusInterface,
) {}
async execute(dto: SignInWithRecoveryCodesDTO): Promise<Result<void>> {
if (this.protocolService.hasAccount()) {
return Result.fail('Tried to sign in when an account already exists.')
}
const codeVerifier = this.crypto.generateRandomKey(256)
this.inMemoryStore.setValue(StorageKey.CodeVerifier, codeVerifier)
const codeChallenge = this.crypto.base64URLEncode(await this.crypto.sha256(codeVerifier))
const recoveryKeyParams = await this.authManager.recoveryKeyParams({
codeChallenge,
...dto,
})
if (recoveryKeyParams === false) {
return Result.fail('Could not retrieve recovery key params')
}
const rootKeyParams = CreateAnyKeyParams(recoveryKeyParams)
if (!this.protocolService.supportedVersions().includes(rootKeyParams.version)) {
if (this.protocolService.isVersionNewerThanLibraryVersion(rootKeyParams.version)) {
return Result.fail(UNSUPPORTED_PROTOCOL_VERSION)
}
return Result.fail(EXPIRED_PROTOCOL_VERSION)
}
if (!this.protocolService.platformSupportsKeyDerivation(rootKeyParams)) {
return Result.fail(UNSUPPORTED_KEY_DERIVATION)
}
const rootKey = await this.protocolService.computeRootKey(dto.password, rootKeyParams)
const signInResult = await this.authManager.signInWithRecoveryCodes({
codeVerifier,
recoveryCodes: dto.recoveryCodes,
username: dto.username,
password: rootKey.serverPassword as string,
})
if (signInResult === false) {
return Result.fail('Could not sign in with recovery codes')
}
this.inMemoryStore.removeValue(StorageKey.CodeVerifier)
const expandedRootKey = new SNRootKey(
CopyPayloadWithContentOverride(rootKey.payload, {
keyParams: signInResult.keyParams,
}),
)
await this.sessionManager.handleAuthentication({
session: signInResult.session,
user: signInResult.user,
rootKey: expandedRootKey,
})
await this.internalEventBus.publishSync(
{
type: AccountEvent.SignedInOrRegistered,
payload: {
payload: {
ephemeral: false,
mergeLocal: false,
awaitSync: true,
checkIntegrity: false,
},
},
},
InternalEventPublishStrategy.SEQUENCE,
)
return Result.ok()
}
}
@@ -0,0 +1,5 @@
export interface SignInWithRecoveryCodesDTO {
recoveryCodes: string
username: string
password: string
}
@@ -0,0 +1,5 @@
import { UseCaseInterface } from '@standardnotes/domain-core'
export interface UseCaseContainerInterface {
get signInWithRecoveryCodes(): UseCaseInterface<void>
}
@@ -24,7 +24,7 @@ import {
Challenge,
} from '@standardnotes/services'
import { Base64String } from '@standardnotes/sncrypto-common'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ClientDisplayableError, SessionBody } from '@standardnotes/responses'
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
import { isNullOrUndefined } from '@standardnotes/utils'
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
@@ -306,7 +306,12 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
throw new ApiCallError((registerResponse.data as HttpErrorResponseBody).error.message)
}
await this.handleAuthResponse(registerResponse.data, rootKey, wrappingKey)
await this.handleAuthentication({
rootKey,
wrappingKey,
session: registerResponse.data.session,
user: registerResponse.data.user,
})
return registerResponse.data
}
@@ -640,22 +645,30 @@ export class SNSessionManager extends AbstractService<SessionEvent> implements S
this.setSession(session)
}
private async handleAuthResponse(body: UserRegistrationResponseBody, rootKey: SNRootKey, wrappingKey?: SNRootKey) {
async handleAuthentication(dto: {
session: SessionBody
user: {
uuid: string
email: string
}
rootKey: SNRootKey
wrappingKey?: SNRootKey
}): Promise<void> {
const session = this.createSession(
body.session.access_token,
body.session.access_expiration,
body.session.refresh_token,
body.session.refresh_expiration,
body.session.readonly_access,
dto.session.access_token,
dto.session.access_expiration,
dto.session.refresh_token,
dto.session.refresh_expiration,
dto.session.readonly_access,
)
if (session !== null) {
await this.populateSession(rootKey, body.user, session, this.apiService.getHost(), wrappingKey)
await this.populateSession(dto.rootKey, dto.user, session, this.apiService.getHost(), dto.wrappingKey)
}
}
/**
* @deprecated use handleAuthResponse instead
* @deprecated use handleAuthentication instead
*/
private async handleSuccessAuthResponse(
response: Responses.SignInResponse | Responses.ChangeCredentialsResponse,
+10
View File
@@ -220,6 +220,16 @@ export class AppContext {
})
}
awaitUserPrefsSingletonResolution() {
return new Promise((resolve) => {
this.application.preferencesService.addEventObserver((eventName) => {
if (eventName === PreferencesServiceEvent.PreferencesChanged) {
resolve()
}
})
})
}
async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) {
await this.application.prepareForLaunch({
receiveChallenge: receiveChallenge || this.handleChallenge,
+7 -1
View File
@@ -74,10 +74,16 @@ describe('preferences', function () {
await register.call(this)
await this.application.setPreference('editorLeft', 300)
await this.application.sync.sync()
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
this.application = await this.context.signout()
await this.application.setPreference('editorLeft', 200)
await this.application.signIn(this.email, this.password)
const promise = this.context.awaitUserPrefsSingletonResolution()
await this.application.sync.sync({ awaitAll: true })
await promise
const editorLeft = this.application.getPreference('editorLeft')
expect(editorLeft).to.equal(300)
})
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/snjs",
"version": "2.158.2",
"version": "2.159.0",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.23.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/ui-services
## [1.23.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Bug Fixes
* Fixed issue where the importer would not correctly parse Google Keep JSON notes ([2e79fc4](https://github.com/standardnotes/app/commit/2e79fc412c8b57b175381c4f59593a03906d89a1))
## [1.23.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/ui-services
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/ui-services",
"version": "1.23.2",
"version": "1.23.4",
"engines": {
"node": ">=16.0.0 <17.0.0"
},
@@ -98,13 +98,13 @@ export class GoogleKeepConverter {
static isValidGoogleKeepJson(json: any): boolean {
return (
json.title &&
json.textContent &&
json.userEditedTimestampUsec &&
typeof json.title === 'string' &&
typeof json.textContent === 'string' &&
typeof json.userEditedTimestampUsec === 'number' &&
typeof json.isArchived === 'boolean' &&
typeof json.isTrashed === 'boolean' &&
typeof json.isPinned === 'boolean' &&
json.color
typeof json.color === 'string'
)
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [3.136.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)
**Note:** Version bump only for package @standardnotes/web
## [3.136.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-07)
### Bug Fixes
* Fixed issue where search button would not show up in Files view ([340f4f7](https://github.com/standardnotes/app/commit/340f4f7f09c28f7818cb4a7f1ccaa387ef0283c9))
## [3.136.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Bug Fixes
* Fixed issue where the "Attached to" in table view would report an extra number of links ([482c4d1](https://github.com/standardnotes/app/commit/482c4d1ca4499ae4e80f592176648ee2c03f16b8))
## [3.136.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
**Note:** Version bump only for package @standardnotes/web
# [3.136.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)
### Features
+50
View File
@@ -1,5 +1,55 @@
{
"versions": [
{
"version": "3.136.4",
"title": "[3.136.4](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-09)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.136.3",
"title": "[3.136.3](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-07)",
"date": null,
"body": "### Bug Fixes\n\n* Fixed issue where search button would not show up in Files view ([340f4f7](https://github.com/standardnotes/app/commit/340f4f7f09c28f7818cb4a7f1ccaa387ef0283c9))",
"parsed": {
"_": [
"Fixed issue where search button would not show up in Files view (340f4f7)"
],
"Bug Fixes": [
"Fixed issue where search button would not show up in Files view (340f4f7)"
]
}
},
{
"version": "3.136.2",
"title": "[3.136.2](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)",
"date": null,
"body": "### Bug Fixes\n\n* Fixed issue where the \"Attached to\" in table view would report an extra number of links ([482c4d1](https://github.com/standardnotes/app/commit/482c4d1ca4499ae4e80f592176648ee2c03f16b8))",
"parsed": {
"_": [
"Fixed issue where the \"Attached to\" in table view would report an extra number of links (482c4d1)"
],
"Bug Fixes": [
"Fixed issue where the \"Attached to\" in table view would report an extra number of links (482c4d1)"
]
}
},
{
"version": "3.136.1",
"title": "[3.136.1](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)",
"date": null,
"body": "**Note:** Version bump only for package @standardnotes/web",
"parsed": {
"_": [
"Note: Version bump only for package @standardnotes/web"
]
}
},
{
"version": "3.136.0",
"title": "[3.136.0](https://github.com/standardnotes/app/compare/@standardnotes/[email protected]...@standardnotes/[email protected]) (2023-01-06)",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/web",
"version": "3.136.0",
"version": "3.136.4",
"license": "AGPL-3.0-or-later",
"main": "dist/app.js",
"author": "Standard Notes.",
@@ -321,7 +321,7 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
addButtonLabel={addButtonLabel}
addNewItem={addNewItem}
isFilesSmartView={isFilesSmartView}
isTableViewEnabled={isTableViewEnabled}
isTableViewEnabled={isTableViewEnabled || isFilesSmartView}
optionsSubtitle={optionsSubtitle}
selectedTag={selectedTag}
filesController={filesController}
@@ -9,7 +9,6 @@ import {
SortableItem,
PrefKey,
ApplicationEvent,
naturalSort,
FileBackupRecord,
SystemViewId,
DecryptedItemInterface,
@@ -25,7 +24,6 @@ import { useTable } from '../Table/useTable'
import Menu from '../Menu/Menu'
import FileMenuOptions from '../FileContextMenu/FileMenuOptions'
import Icon from '../Icon/Icon'
import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem'
import LinkedItemBubble from '../LinkedItems/LinkedItemBubble'
import LinkedItemsPanel from '../LinkedItems/LinkedItemsPanel'
import { LinkingController } from '@/Controllers/LinkingController'
@@ -37,6 +35,8 @@ import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintFor
import NotesOptions from '../NotesOptions/NotesOptions'
import { NotesController } from '@/Controllers/NotesController/NotesController'
import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController'
import { useItemLinks } from '@/Hooks/useItemLinks'
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
const ContextMenuCell = ({
items,
@@ -218,6 +218,38 @@ const ItemNameCell = ({ item }: { item: DecryptedItemInterface }) => {
)
}
const AttachedToCell = ({ item }: { item: DecryptedItemInterface }) => {
const { notesLinkedToItem, notesLinkingToItem, filesLinkedToItem, filesLinkingToItem, tagsLinkedToItem } =
useItemLinks(item)
const application = useApplication()
const allLinks: ItemLink[] = (notesLinkedToItem as ItemLink[]).concat(
notesLinkingToItem,
filesLinkedToItem,
filesLinkingToItem,
tagsLinkedToItem,
)
if (!allLinks.length) {
return null
}
return (
<div className="flex items-center gap-2 overflow-hidden">
<LinkedItemBubble
className="overflow-hidden border border-transparent hover:border-border focus:border-info focus:shadow-none"
link={allLinks[0]}
key={allLinks[0].id}
unlinkItem={async (itemToUnlink) => {
void application.items.unlinkItems(item, itemToUnlink)
}}
isBidirectional={false}
/>
{allLinks.length - 1 >= 1 && <span>and {allLinks.length - 1} more...</span>}
</div>
)
}
type Props = {
application: WebApplication
items: DecryptedItemInterface[]
@@ -318,39 +350,10 @@ const ContentTableView = ({
{
name: 'Attached to',
hidden: isSmallBreakpoint || isMediumBreakpoint || isLargeBreakpoint,
cell: (item) => {
const links = [
...naturalSort(application.items.referencesForItem(item), 'title').map((item) =>
createLinkFromItem(item, 'linked'),
),
...naturalSort(application.items.itemsReferencingItem(item), 'title').map((item) =>
createLinkFromItem(item, 'linked-by'),
),
...application.items.getSortedTagsForItem(item).map((item) => createLinkFromItem(item, 'linked')),
]
if (!links.length) {
return null
}
return (
<div className="flex items-center gap-2 overflow-hidden">
<LinkedItemBubble
className="overflow-hidden border border-transparent hover:border-border focus:border-info focus:shadow-none"
link={links[0]}
key={links[0].id}
unlinkItem={async (itemToUnlink) => {
void application.items.unlinkItems(item, itemToUnlink)
}}
isBidirectional={false}
/>
{links.length - 1 > 1 && <span>and {links.length - 1} more...</span>}
</div>
)
},
cell: (item) => <AttachedToCell item={item} />,
},
],
[application.items, isLargeBreakpoint, isMediumBreakpoint, isSmallBreakpoint, listHasFiles],
[isLargeBreakpoint, isMediumBreakpoint, isSmallBreakpoint, listHasFiles],
)
const getRowId = useCallback((item: DecryptedItemInterface) => item.uuid, [])
@@ -1,4 +1,4 @@
import { FeatureIdentifier } from '@standardnotes/snjs'
import { FeatureIdentifier, FeatureStatus } from '@standardnotes/snjs'
import { ComponentArea, NoteType } from '@standardnotes/features'
import { WebApplication } from '@/Application/Application'
import { PlainEditorMetadata, SuperEditorMetadata } from '@/Constants/Constants'
@@ -46,13 +46,15 @@ export function getDropdownItemsForAllEditors(application: WebApplication): Edit
options.push(plaintextOption)
options.push({
icon: SuperEditorMetadata.icon,
iconClassName: SuperEditorMetadata.iconClassName,
label: SuperEditorMetadata.name,
value: FeatureIdentifier.SuperEditor,
isLabs: true,
})
if (application.features.getFeatureStatus(FeatureIdentifier.SuperEditor) === FeatureStatus.Entitled) {
options.push({
icon: SuperEditorMetadata.icon,
iconClassName: SuperEditorMetadata.iconClassName,
label: SuperEditorMetadata.name,
value: FeatureIdentifier.SuperEditor,
isLabs: true,
})
}
options.sort((a, b) => {
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
+1
View File
@@ -6005,6 +6005,7 @@ __metadata:
"@standardnotes/api": "workspace:^"
"@standardnotes/auth": ^3.19.4
"@standardnotes/common": ^1.45.0
"@standardnotes/domain-core": ^1.11.0
"@standardnotes/encryption": "workspace:^"
"@standardnotes/files": "workspace:^"
"@standardnotes/models": "workspace:^"