refactor: http service (#2233)

This commit is contained in:
Mo
2023-02-28 20:43:25 -06:00
committed by GitHub
parent 6e7618b258
commit e7f1d35341
142 changed files with 1116 additions and 1307 deletions

View File

@@ -83,7 +83,7 @@ import {
ItemStream,
Platform,
} from '@standardnotes/models'
import { ClientDisplayableError } from '@standardnotes/responses'
import { ClientDisplayableError, SessionListEntry } from '@standardnotes/responses'
import { SnjsVersion } from './../Version'
import { SNLog } from '../Log'
@@ -133,7 +133,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
/**
* @deprecated will be fully replaced by @standardnotes/api::HttpService
*/
private deprecatedHttpService!: InternalServices.SNHttpService
private deprecatedHttpService!: InternalServices.DeprecatedHttpService
private declare httpService: HttpServiceInterface
private payloadManager!: InternalServices.PayloadManager
public protocolService!: EncryptionService
@@ -599,13 +599,13 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return this.syncService.isDatabaseLoaded()
}
public getSessions(): Promise<
(Responses.HttpResponse & { data: InternalServices.RemoteSession[] }) | Responses.HttpResponse
> {
public getSessions(): Promise<Responses.HttpResponse<SessionListEntry[]>> {
return this.sessionManager.getSessionsList()
}
public async revokeSession(sessionId: UuidString): Promise<Responses.HttpResponse | undefined> {
public async revokeSession(
sessionId: UuidString,
): Promise<Responses.HttpResponse<Responses.SessionListResponse> | undefined> {
if (await this.protectionService.authorizeSessionRevoking()) {
return this.sessionManager.revokeSession(sessionId)
}
@@ -627,7 +627,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
return Common.compareVersions(userVersion, Common.ProtocolVersion.V004) >= 0
}
public async getUserSubscription(): Promise<Subscription | Responses.ClientDisplayableError> {
public async getUserSubscription(): Promise<Subscription | Responses.ClientDisplayableError | undefined> {
return this.sessionManager.getSubscription()
}
@@ -897,6 +897,9 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
service.deinit()
}
this.httpService.deinit()
;(this.httpService as unknown) = undefined
this.options.crypto.deinit()
;(this.options as unknown) = undefined
@@ -939,7 +942,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
ephemeral = false,
mergeLocal = true,
awaitSync = false,
): Promise<Responses.HttpResponse | Responses.SignInResponse> {
): Promise<Responses.HttpResponse<Responses.SignInResponse>> {
return this.userService.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
}
@@ -1161,8 +1164,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.diskStorageService.provideEncryptionProvider(this.protocolService)
this.createChallengeService()
this.createLegacyHttpManager()
this.createApiService()
this.createHttpService()
this.createHttpServiceAndApiService()
this.createUserServer()
this.createUserRequestServer()
this.createUserApiService()
@@ -1419,20 +1421,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.services.push(this.userService)
}
private createApiService() {
this.apiService = new InternalServices.SNApiService(
this.deprecatedHttpService,
this.diskStorageService,
this.options.defaultHost,
this.inMemoryStore,
this.options.crypto,
this.sessionStorageMapper,
this.legacySessionStorageMapper,
this.internalEventBus,
)
this.services.push(this.apiService)
}
private createUserApiService() {
this.userApiService = new UserApiService(this.userServer, this.userRequestServer)
}
@@ -1486,7 +1474,7 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}
private createLegacyHttpManager() {
this.deprecatedHttpService = new InternalServices.SNHttpService(
this.deprecatedHttpService = new InternalServices.DeprecatedHttpService(
this.environment,
this.options.appVersion,
this.internalEventBus,
@@ -1494,11 +1482,22 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
this.services.push(this.deprecatedHttpService)
}
private createHttpService() {
this.httpService = new HttpService(
this.environment,
this.options.appVersion,
SnjsVersion,
private createHttpServiceAndApiService() {
this.httpService = new HttpService(this.environment, this.options.appVersion, SnjsVersion)
this.apiService = new InternalServices.SNApiService(
this.httpService,
this.diskStorageService,
this.options.defaultHost,
this.inMemoryStore,
this.options.crypto,
this.sessionStorageMapper,
this.legacySessionStorageMapper,
this.internalEventBus,
)
this.services.push(this.apiService)
this.httpService.setCallbacks(
this.apiService.processMetaObject.bind(this.apiService),
this.apiService.setSession.bind(this.apiService),
)

View File

@@ -2,7 +2,7 @@ import { removeFromArray } from '@standardnotes/utils'
import { SNRootKey } from '@standardnotes/encryption'
import { ChallengeService } from '../Challenge'
import { ListedService } from '../Listed/ListedService'
import { ActionResponse, HttpResponse } from '@standardnotes/responses'
import { ActionResponse, DeprecatedHttpResponse } from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
import { ItemManager } from '@Lib/Services/Items/ItemManager'
import {
@@ -24,7 +24,7 @@ import {
} from '@standardnotes/models'
import { SNSyncService } from '../Sync/SyncService'
import { PayloadManager } from '../Payloads/PayloadManager'
import { SNHttpService } from '../Api/HttpService'
import { DeprecatedHttpService } from '../Api/DeprecatedHttpService'
import {
AbstractService,
DeviceInterface,
@@ -61,7 +61,7 @@ export class SNActionsService extends AbstractService {
private itemManager: ItemManager,
private alertService: AlertService,
public deviceInterface: DeviceInterface,
private httpService: SNHttpService,
private httpService: DeprecatedHttpService,
private payloadManager: PayloadManager,
private protocolService: EncryptionService,
private syncService: SNSyncService,
@@ -185,7 +185,7 @@ export class SNActionsService extends AbstractService {
message: 'An issue occurred while processing this action. Please try again.',
}
void this.alertService.alert(error.message)
return { error } as HttpResponse
return { error } as DeprecatedHttpResponse
})
return response as ActionResponse

View File

@@ -1,7 +1,6 @@
import { FeatureDescription } from '@standardnotes/features'
import { isNullOrUndefined, joinPaths } from '@standardnotes/utils'
import { joinPaths } from '@standardnotes/utils'
import { SettingName, SubscriptionSettingName } from '@standardnotes/settings'
import { ErrorTag } from '@standardnotes/common'
import {
AbstractService,
ApiServiceInterface,
@@ -34,14 +33,53 @@ import {
} from '@standardnotes/services'
import { FilesApiInterface } from '@standardnotes/files'
import { ServerSyncPushContextualPayload, SNFeatureRepo, FileContent } from '@standardnotes/models'
import * as Responses from '@standardnotes/responses'
import {
User,
HttpStatusCode,
KeyParamsResponse,
SignInResponse,
SignOutResponse,
ChangeCredentialsResponse,
RawSyncResponse,
SessionRenewalResponse,
SessionListResponse,
UserFeaturesResponse,
ListSettingsResponse,
UpdateSettingResponse,
GetSettingResponse,
DeleteSettingResponse,
GetSubscriptionResponse,
GetAvailableSubscriptionsResponse,
PostSubscriptionTokensResponse,
GetOfflineFeaturesResponse,
ListedRegistrationResponse,
CreateValetTokenResponse,
StartUploadSessionResponse,
UploadFileChunkResponse,
CloseUploadSessionResponse,
DownloadFileChunkResponse,
IntegrityPayload,
CheckIntegrityResponse,
GetSingleItemResponse,
HttpResponse,
HttpResponseMeta,
ErrorTag,
HttpRequestParams,
HttpRequest,
HttpVerb,
ApiEndpointParam,
ClientDisplayableError,
CreateValetTokenPayload,
HttpErrorResponse,
HttpSuccessResponse,
isErrorResponse,
} from '@standardnotes/responses'
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
import { HttpResponseMeta } from '@standardnotes/api'
import { HttpServiceInterface } 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 { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
import { Paths } from './Paths'
import { DiskStorageService } from '../Storage/DiskStorageService'
@@ -65,7 +103,7 @@ export class SNApiService
SettingsServerInterface
{
private session: Session | LegacySession | null
public user?: Responses.User
public user?: User
private registering = false
private authenticating = false
private changing = false
@@ -74,7 +112,7 @@ export class SNApiService
private filesHost?: string
constructor(
private httpService: SNHttpService,
private httpService: HttpServiceInterface,
private storageService: DiskStorageService,
private host: string,
private inMemoryStore: KeyValueStoreInterface<string>,
@@ -96,7 +134,7 @@ export class SNApiService
super.deinit()
}
public setUser(user?: Responses.User): void {
public setUser(user?: User): void {
this.user = user
}
@@ -161,25 +199,22 @@ export class SNApiService
return V0_API_VERSION
}
private params(inParams: Record<string | number | symbol, unknown>): HttpParams {
private params(inParams: Record<string | number | symbol, unknown>): HttpRequestParams {
const params = merge(inParams, {
[ApiEndpointParam.ApiVersion]: this.apiVersion,
})
return params
}
public createErrorResponse(message: string, status?: Responses.StatusCode): Responses.HttpResponse {
return { error: { message, status } } as Responses.HttpResponse
public createErrorResponse(message: string, status?: HttpStatusCode, tag?: ErrorTag): HttpErrorResponse {
return { data: { error: { message, tag } }, status: status ?? HttpStatusCode.BadRequest }
}
private errorResponseWithFallbackMessage(response: Responses.HttpResponse, message: string) {
if (!response.error?.message) {
response.error = {
...response.error,
status: response.error?.status ?? Responses.StatusCode.UnknownError,
message,
}
private errorResponseWithFallbackMessage(response: HttpErrorResponse, message: string): HttpErrorResponse {
if (!response.data.error.message) {
response.data.error.message = message
}
return response
}
@@ -196,44 +231,44 @@ export class SNApiService
}
}
private processResponse(response: Responses.HttpResponse) {
private processSuccessResponseForMetaBody<T>(response: HttpSuccessResponse<T>) {
if (response.meta) {
this.processMetaObject(response.meta)
}
}
private async request(params: {
private async request<T>(params: {
verb: HttpVerb
url: string
fallbackErrorMessage: string
params?: HttpParams
params?: HttpRequestParams
rawBytes?: Uint8Array
authentication?: string
customHeaders?: Record<string, string>[]
responseType?: XMLHttpRequestResponseType
external?: boolean
}) {
}): Promise<HttpResponse<T>> {
try {
const response = await this.httpService.runHttp(params)
this.processResponse(response)
return response
const response = await this.httpService.runHttp<T>(params)
if (isErrorResponse(response)) {
return this.errorResponseWithFallbackMessage(response, params.fallbackErrorMessage)
} else {
this.processSuccessResponseForMetaBody(response)
return response
}
} catch (errorResponse) {
return this.errorResponseWithFallbackMessage(errorResponse as Responses.HttpResponse, params.fallbackErrorMessage)
return this.errorResponseWithFallbackMessage(errorResponse as HttpErrorResponse, params.fallbackErrorMessage)
}
}
/**
* @param mfaKeyPath The params path the server expects for authentication against
* a particular mfa challenge. A value of foo would mean the server
* would receive parameters as params['foo'] with value equal to mfaCode.
* @param mfaCode The mfa challenge response value.
*/
async getAccountKeyParams(dto: {
email: string
mfaKeyPath?: string
mfaCode?: string
authenticatorResponse?: Record<string, unknown>
}): Promise<Responses.KeyParamsResponse | Responses.HttpResponse> {
}): Promise<HttpResponse<KeyParamsResponse>> {
const codeVerifier = this.crypto.generateRandomKey(256)
this.inMemoryStore.setValue(StorageKey.CodeVerifier, codeVerifier)
@@ -242,10 +277,10 @@ export class SNApiService
const params = this.params({
email: dto.email,
code_challenge: codeChallenge,
})
}) as Record<string, unknown>
if (dto.mfaKeyPath !== undefined && dto.mfaCode !== undefined) {
params[dto.mfaKeyPath] = dto.mfaCode
if (dto.mfaCode !== undefined) {
params['mfa_code'] = dto.mfaCode
}
if (dto.authenticatorResponse) {
@@ -266,9 +301,9 @@ export class SNApiService
email: string
serverPassword: string
ephemeral: boolean
}): Promise<Responses.SignInResponse | Responses.HttpResponse> {
}): Promise<HttpResponse<SignInResponse>> {
if (this.authenticating) {
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS) as Responses.SignInResponse
return this.createErrorResponse(API_MESSAGE_LOGIN_IN_PROGRESS, HttpStatusCode.BadRequest)
}
this.authenticating = true
const url = joinPaths(this.host, Paths.v2.signIn)
@@ -279,7 +314,7 @@ export class SNApiService
code_verifier: this.inMemoryStore.getValue(StorageKey.CodeVerifier) as string,
})
const response = await this.request({
const response = await this.request<SignInResponse>({
verb: HttpVerb.Post,
url,
params,
@@ -293,11 +328,8 @@ export class SNApiService
return response
}
signOut(): Promise<Responses.SignOutResponse> {
const url = joinPaths(this.host, Paths.v1.signOut)
return this.httpService.postAbsolute(url, undefined, this.getSessionAccessToken()).catch((errorResponse) => {
return errorResponse
}) as Promise<Responses.SignOutResponse>
signOut(): Promise<HttpResponse<SignOutResponse>> {
return this.httpService.post<SignOutResponse>(Paths.v1.signOut, undefined, this.getSessionAccessToken())
}
async changeCredentials(parameters: {
@@ -306,38 +338,33 @@ export class SNApiService
newServerPassword: string
newKeyParams: SNRootKeyParams
newEmail?: string
}): Promise<Responses.ChangeCredentialsResponse | Responses.HttpResponse> {
}): Promise<HttpResponse<ChangeCredentialsResponse>> {
if (this.changing) {
return this.createErrorResponse(API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS)
return this.createErrorResponse(API_MESSAGE_CHANGE_CREDENTIALS_IN_PROGRESS, HttpStatusCode.BadRequest)
}
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
}
this.changing = true
const url = joinPaths(this.host, Paths.v1.changeCredentials(parameters.userUuid) as string)
const path = Paths.v1.changeCredentials(parameters.userUuid)
const params = this.params({
current_password: parameters.currentServerPassword,
new_password: parameters.newServerPassword,
new_email: parameters.newEmail,
...parameters.newKeyParams.getPortableValue(),
})
const response = await this.httpService
.putAbsolute(url, params, this.getSessionAccessToken())
.catch(async (errorResponse) => {
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest({
verb: HttpVerb.Put,
url,
params,
})
}
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL)
})
this.processResponse(response)
const response = await this.httpService.put<ChangeCredentialsResponse>(path, params, this.getSessionAccessToken())
this.changing = false
if (isErrorResponse(response)) {
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_CHANGE_CREDENTIALS_FAIL)
}
this.processSuccessResponseForMetaBody(response)
return response
}
@@ -346,79 +373,54 @@ export class SNApiService
lastSyncToken: string,
paginationToken: string,
limit: number,
): Promise<Responses.RawSyncResponse | Responses.HttpResponse> {
): Promise<HttpResponse<RawSyncResponse>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
}
const url = joinPaths(this.host, Paths.v1.sync)
const path = Paths.v1.sync
const params = this.params({
[ApiEndpointParam.SyncPayloads]: payloads,
[ApiEndpointParam.LastSyncToken]: lastSyncToken,
[ApiEndpointParam.PaginationToken]: paginationToken,
[ApiEndpointParam.SyncDlLimit]: limit,
})
const response = await this.httpService
.postAbsolute(url, params, this.getSessionAccessToken())
.catch<Responses.HttpResponse>(async (errorResponse) => {
this.preprocessAuthenticatedErrorResponse(errorResponse)
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest({
verb: HttpVerb.Post,
url,
params,
})
}
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
})
this.processResponse(response)
const response = await this.httpService.post<RawSyncResponse>(path, params, this.getSessionAccessToken())
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_SYNC_FAIL)
}
this.processSuccessResponseForMetaBody(response)
return response
}
private async refreshSessionThenRetryRequest(httpRequest: HttpRequest): Promise<Responses.HttpResponse> {
const sessionResponse = await this.refreshSession()
if (sessionResponse.error || isNullOrUndefined(sessionResponse.data)) {
return sessionResponse
} else {
return this.httpService
.runHttp({
...httpRequest,
authentication: this.getSessionAccessToken(),
})
.catch((errorResponse) => {
return errorResponse
})
}
}
async refreshSession(): Promise<Responses.SessionRenewalResponse | Responses.HttpResponse> {
async refreshSession(): Promise<HttpResponse<SessionRenewalResponse>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
}
this.refreshingSession = true
const url = joinPaths(this.host, Paths.v1.refreshSession)
const session = this.session as Session
const params = this.params({
access_token: session.accessToken.value,
refresh_token: session.refreshToken.value,
})
const result = await this.httpService
.postAbsolute(url, params)
const response = await this.httpService
.post<SessionRenewalResponse>(Paths.v1.refreshSession, params)
.then(async (response) => {
const sessionRenewalResponse = response as Responses.SessionRenewalResponse
if (
sessionRenewalResponse.error ||
sessionRenewalResponse.data?.error ||
!sessionRenewalResponse.data.session
) {
return null
if (isErrorResponse(response) || !response.data.session) {
return response
}
const accessTokenOrError = SessionToken.create(
sessionRenewalResponse.data.session.access_token,
sessionRenewalResponse.data.session.access_expiration,
response.data.session.access_token,
response.data.session.access_expiration,
)
if (accessTokenOrError.isFailed()) {
return null
@@ -426,19 +428,15 @@ export class SNApiService
const accessToken = accessTokenOrError.getValue()
const refreshTokenOrError = SessionToken.create(
sessionRenewalResponse.data.session.refresh_token,
sessionRenewalResponse.data.session.refresh_expiration,
response.data.session.refresh_token,
response.data.session.refresh_expiration,
)
if (refreshTokenOrError.isFailed()) {
return null
}
const refreshToken = refreshTokenOrError.getValue()
const sessionOrError = Session.create(
accessToken,
refreshToken,
sessionRenewalResponse.data.session.readonly_access,
)
const sessionOrError = Session.create(accessToken, refreshToken, response.data.session.readonly_access)
if (sessionOrError.isFailed()) {
return null
}
@@ -447,7 +445,7 @@ export class SNApiService
this.session = session
this.setSession(session)
this.processResponse(response)
this.processSuccessResponseForMetaBody(response)
await this.notifyEventSync(ApiServiceEvent.SessionRefreshed, {
session,
@@ -455,105 +453,94 @@ export class SNApiService
return response
})
.catch((errorResponse) => {
this.preprocessAuthenticatedErrorResponse(errorResponse)
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
})
this.refreshingSession = false
if (result === null) {
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
if (response === null) {
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION, HttpStatusCode.BadRequest)
}
return result
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_TOKEN_REFRESH_FAIL)
}
return response
}
async getSessionsList(): Promise<Responses.SessionListResponse | Responses.HttpResponse> {
async getSessionsList(): Promise<HttpResponse<SessionListResponse>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
}
const url = joinPaths(this.host, Paths.v1.sessions)
const response = await this.httpService
.getAbsolute(url, {}, this.getSessionAccessToken())
.catch(async (errorResponse) => {
this.preprocessAuthenticatedErrorResponse(errorResponse)
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest({
verb: HttpVerb.Get,
url,
})
}
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
})
this.processResponse(response)
const path = Paths.v1.sessions
const response = await this.httpService.get<SessionListResponse>(path, {}, this.getSessionAccessToken())
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_SYNC_FAIL)
}
this.processSuccessResponseForMetaBody(response)
return response
}
async deleteSession(sessionId: UuidString): Promise<Responses.HttpResponse> {
async deleteSession(sessionId: UuidString): Promise<HttpResponse<SessionListResponse>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError
}
const url = joinPaths(this.host, <string>Paths.v1.session(sessionId))
const response: Responses.SessionListResponse | Responses.HttpResponse = await this.httpService
.deleteAbsolute(url, { uuid: sessionId }, this.getSessionAccessToken())
.catch((error: Responses.HttpResponse) => {
const errorResponse = error as Responses.HttpResponse
this.preprocessAuthenticatedErrorResponse(errorResponse)
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest({
verb: HttpVerb.Delete,
url,
})
}
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
})
this.processResponse(response)
const path = Paths.v1.session(sessionId)
const response = await this.httpService.delete<SessionListResponse>(
path,
{ uuid: sessionId },
this.getSessionAccessToken(),
)
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_SYNC_FAIL)
}
this.processSuccessResponseForMetaBody(response)
return response
}
async getUserFeatures(userUuid: UuidString): Promise<Responses.HttpResponse | Responses.UserFeaturesResponse> {
const url = joinPaths(this.host, Paths.v1.userFeatures(userUuid))
const response = await this.httpService
.getAbsolute(url, undefined, this.getSessionAccessToken())
.catch((errorResponse: Responses.HttpResponse) => {
this.preprocessAuthenticatedErrorResponse(errorResponse)
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest({
verb: HttpVerb.Get,
url,
})
}
return this.errorResponseWithFallbackMessage(errorResponse, API_MESSAGE_GENERIC_SYNC_FAIL)
})
this.processResponse(response)
async getUserFeatures(userUuid: UuidString): Promise<HttpResponse<UserFeaturesResponse>> {
const path = Paths.v1.userFeatures(userUuid)
const response = await this.httpService.get<UserFeaturesResponse>(path, undefined, this.getSessionAccessToken())
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, API_MESSAGE_GENERIC_SYNC_FAIL)
}
this.processSuccessResponseForMetaBody(response)
return response
}
private async tokenRefreshableRequest<T extends Responses.MinimalHttpResponse>(
private async tokenRefreshableRequest<T>(
params: HttpRequest & { fallbackErrorMessage: string },
): Promise<T> {
): Promise<HttpResponse<T>> {
const preprocessingError = this.preprocessingError()
if (preprocessingError) {
return preprocessingError as T
return preprocessingError
}
const response: T | Responses.HttpResponse = await this.httpService
.runHttp(params)
.catch((errorResponse: Responses.HttpResponse) => {
this.preprocessAuthenticatedErrorResponse(errorResponse)
if (Responses.isErrorResponseExpiredToken(errorResponse)) {
return this.refreshSessionThenRetryRequest(params)
}
return this.errorResponseWithFallbackMessage(errorResponse, params.fallbackErrorMessage)
})
this.processResponse(response)
return response as T
const response = await this.httpService.runHttp<T>(params)
if (isErrorResponse(response)) {
this.preprocessAuthenticatedErrorResponse(response)
return this.errorResponseWithFallbackMessage(response, params.fallbackErrorMessage)
}
this.processSuccessResponseForMetaBody(response)
return response
}
async listSettings(userUuid: UuidString): Promise<Responses.ListSettingsResponse> {
return await this.tokenRefreshableRequest<Responses.ListSettingsResponse>({
async listSettings(userUuid: UuidString): Promise<HttpResponse<ListSettingsResponse>> {
return await this.tokenRefreshableRequest<ListSettingsResponse>({
verb: HttpVerb.Get,
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
fallbackErrorMessage: API_MESSAGE_FAILED_GET_SETTINGS,
@@ -566,13 +553,13 @@ export class SNApiService
settingName: string,
settingValue: string | null,
sensitive: boolean,
): Promise<Responses.UpdateSettingResponse> {
): Promise<HttpResponse<UpdateSettingResponse>> {
const params = {
name: settingName,
value: settingValue,
sensitive: sensitive,
}
return this.tokenRefreshableRequest<Responses.UpdateSettingResponse>({
return this.tokenRefreshableRequest<UpdateSettingResponse>({
verb: HttpVerb.Put,
url: joinPaths(this.host, Paths.v1.settings(userUuid)),
authentication: this.getSessionAccessToken(),
@@ -581,8 +568,8 @@ export class SNApiService
})
}
async getSetting(userUuid: UuidString, settingName: SettingName): Promise<Responses.GetSettingResponse> {
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
async getSetting(userUuid: UuidString, settingName: SettingName): Promise<HttpResponse<GetSettingResponse>> {
return await this.tokenRefreshableRequest<GetSettingResponse>({
verb: HttpVerb.Get,
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName.toLowerCase() as SettingName)),
authentication: this.getSessionAccessToken(),
@@ -593,8 +580,8 @@ export class SNApiService
async getSubscriptionSetting(
userUuid: UuidString,
settingName: SubscriptionSettingName,
): Promise<Responses.GetSettingResponse> {
return await this.tokenRefreshableRequest<Responses.GetSettingResponse>({
): Promise<HttpResponse<GetSettingResponse>> {
return await this.tokenRefreshableRequest<GetSettingResponse>({
verb: HttpVerb.Get,
url: joinPaths(
this.host,
@@ -605,8 +592,8 @@ export class SNApiService
})
}
async deleteSetting(userUuid: UuidString, settingName: SettingName): Promise<Responses.DeleteSettingResponse> {
return this.tokenRefreshableRequest<Responses.DeleteSettingResponse>({
async deleteSetting(userUuid: UuidString, settingName: SettingName): Promise<HttpResponse<DeleteSettingResponse>> {
return this.tokenRefreshableRequest<DeleteSettingResponse>({
verb: HttpVerb.Delete,
url: joinPaths(this.host, Paths.v1.setting(userUuid, settingName)),
authentication: this.getSessionAccessToken(),
@@ -614,7 +601,7 @@ export class SNApiService
})
}
public downloadFeatureUrl(url: string): Promise<Responses.HttpResponse> {
public downloadFeatureUrl(url: string): Promise<HttpResponse> {
return this.request({
verb: HttpVerb.Get,
url,
@@ -623,38 +610,39 @@ export class SNApiService
})
}
public async getSubscription(userUuid: string): Promise<Responses.HttpResponse | Responses.GetSubscriptionResponse> {
public async getSubscription(userUuid: string): Promise<HttpResponse<GetSubscriptionResponse>> {
const url = joinPaths(this.host, Paths.v1.subscription(userUuid))
const response = await this.tokenRefreshableRequest({
return this.tokenRefreshableRequest({
verb: HttpVerb.Get,
url,
authentication: this.getSessionAccessToken(),
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
})
return response
}
public async getAvailableSubscriptions(): Promise<
Responses.HttpResponse | Responses.GetAvailableSubscriptionsResponse
> {
public async getAvailableSubscriptions(): Promise<HttpResponse<GetAvailableSubscriptionsResponse>> {
const url = joinPaths(this.host, Paths.v2.subscriptions)
const response = await this.request({
return this.request({
verb: HttpVerb.Get,
url,
fallbackErrorMessage: API_MESSAGE_FAILED_SUBSCRIPTION_INFO,
})
return response
}
public async getNewSubscriptionToken(): Promise<string | undefined> {
const url = joinPaths(this.host, Paths.v1.subscriptionTokens)
const response: Responses.HttpResponse | Responses.PostSubscriptionTokensResponse = await this.request({
const response = await this.request<PostSubscriptionTokensResponse>({
verb: HttpVerb.Post,
url,
authentication: this.getSessionAccessToken(),
fallbackErrorMessage: API_MESSAGE_FAILED_ACCESS_PURCHASE,
})
return (response as Responses.PostSubscriptionTokensResponse).data?.token
if (isErrorResponse(response)) {
return undefined
}
return response.data.token
}
public async downloadOfflineFeaturesFromRepo(
@@ -673,17 +661,17 @@ export class SNApiService
return new ClientDisplayableError('This offline features host is not in the trusted allowlist.')
}
const response: Responses.HttpResponse | Responses.GetOfflineFeaturesResponse = await this.request({
const response = await this.request<GetOfflineFeaturesResponse>({
verb: HttpVerb.Get,
url: featuresUrl,
fallbackErrorMessage: API_MESSAGE_FAILED_OFFLINE_FEATURES,
customHeaders: [{ key: 'x-offline-token', value: extensionKey }],
})
if (response.error) {
return ClientDisplayableError.FromError(response.error)
if (isErrorResponse(response)) {
return ClientDisplayableError.FromError(response.data.error)
}
const data = (response as Responses.GetOfflineFeaturesResponse).data
const data = response.data
return {
features: data?.features || [],
roles: data?.roles || [],
@@ -693,11 +681,11 @@ export class SNApiService
}
}
public async registerForListedAccount(): Promise<Responses.ListedRegistrationResponse> {
public async registerForListedAccount(): Promise<HttpResponse<ListedRegistrationResponse>> {
if (!this.user) {
throw Error('Cannot register for Listed without user account.')
}
return await this.tokenRefreshableRequest<Responses.ListedRegistrationResponse>({
return this.tokenRefreshableRequest<ListedRegistrationResponse>({
verb: HttpVerb.Post,
url: joinPaths(this.host, Paths.v1.listedRegistration(this.user.uuid)),
fallbackErrorMessage: API_MESSAGE_FAILED_LISTED_REGISTRATION,
@@ -717,7 +705,7 @@ export class SNApiService
resources: [{ remoteIdentifier, unencryptedFileSize: unencryptedFileSize || 0 }],
}
const response = await this.tokenRefreshableRequest<Responses.CreateValetTokenResponse>({
const response = await this.tokenRefreshableRequest<CreateValetTokenResponse>({
verb: HttpVerb.Post,
url: url,
authentication: this.getSessionAccessToken(),
@@ -725,6 +713,10 @@ export class SNApiService
params,
})
if (isErrorResponse(response)) {
return new ClientDisplayableError(response.data?.error?.message as string)
}
if (!response.data?.success) {
return new ClientDisplayableError(response.data?.reason as string, undefined, response.data?.reason as string)
}
@@ -732,30 +724,26 @@ export class SNApiService
return response.data?.valetToken
}
public async startUploadSession(apiToken: string): Promise<Responses.StartUploadSessionResponse> {
public async startUploadSession(apiToken: string): Promise<HttpResponse<StartUploadSessionResponse>> {
const url = joinPaths(this.getFilesHost(), Paths.v1.startUploadSession)
const response: Responses.HttpResponse | Responses.StartUploadSessionResponse = await this.tokenRefreshableRequest({
return this.tokenRefreshableRequest({
verb: HttpVerb.Post,
url,
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
fallbackErrorMessage: Strings.Network.Files.FailedStartUploadSession,
})
return response as Responses.StartUploadSessionResponse
}
public async deleteFile(apiToken: string): Promise<Responses.MinimalHttpResponse> {
public async deleteFile(apiToken: string): Promise<HttpResponse<StartUploadSessionResponse>> {
const url = joinPaths(this.getFilesHost(), Paths.v1.deleteFile)
const response: Responses.HttpResponse | Responses.StartUploadSessionResponse = await this.tokenRefreshableRequest({
return this.tokenRefreshableRequest({
verb: HttpVerb.Delete,
url,
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
fallbackErrorMessage: Strings.Network.Files.FailedDeleteFile,
})
return response as Responses.MinimalHttpResponse
}
public async uploadFileBytes(apiToken: string, chunkId: number, encryptedBytes: Uint8Array): Promise<boolean> {
@@ -764,7 +752,7 @@ export class SNApiService
}
const url = joinPaths(this.getFilesHost(), Paths.v1.uploadFileChunk)
const response: Responses.HttpResponse | Responses.UploadFileChunkResponse = await this.tokenRefreshableRequest({
const response = await this.tokenRefreshableRequest<UploadFileChunkResponse>({
verb: HttpVerb.Post,
url,
rawBytes: encryptedBytes,
@@ -776,20 +764,28 @@ export class SNApiService
fallbackErrorMessage: Strings.Network.Files.FailedUploadFileChunk,
})
return (response as Responses.UploadFileChunkResponse).success
if (isErrorResponse(response)) {
return false
}
return response.data.success
}
public async closeUploadSession(apiToken: string): Promise<boolean> {
const url = joinPaths(this.getFilesHost(), Paths.v1.closeUploadSession)
const response: Responses.HttpResponse | Responses.CloseUploadSessionResponse = await this.tokenRefreshableRequest({
const response = await this.tokenRefreshableRequest<CloseUploadSessionResponse>({
verb: HttpVerb.Post,
url,
customHeaders: [{ key: 'x-valet-token', value: apiToken }],
fallbackErrorMessage: Strings.Network.Files.FailedCloseUploadSession,
})
return (response as Responses.CloseUploadSessionResponse).success
if (isErrorResponse(response)) {
return false
}
return response.data.success
}
public getFilesDownloadUrl(): string {
@@ -806,21 +802,24 @@ export class SNApiService
const url = this.getFilesDownloadUrl()
const pullChunkSize = file.encryptedChunkSizes[chunkIndex]
const response: Responses.HttpResponse | Responses.DownloadFileChunkResponse =
await this.tokenRefreshableRequest<Responses.DownloadFileChunkResponse>({
verb: HttpVerb.Get,
url,
customHeaders: [
{ key: 'x-valet-token', value: apiToken },
{
key: 'x-chunk-size',
value: pullChunkSize.toString(),
},
{ key: 'range', value: `bytes=${contentRangeStart}-` },
],
fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk,
responseType: 'arraybuffer',
})
const response = await this.tokenRefreshableRequest<DownloadFileChunkResponse>({
verb: HttpVerb.Get,
url,
customHeaders: [
{ key: 'x-valet-token', value: apiToken },
{
key: 'x-chunk-size',
value: pullChunkSize.toString(),
},
{ key: 'range', value: `bytes=${contentRangeStart}-` },
],
fallbackErrorMessage: Strings.Network.Files.FailedDownloadFileChunk,
responseType: 'arraybuffer',
})
if (isErrorResponse(response)) {
return new ClientDisplayableError(response.data?.error?.message as string)
}
const contentRangeHeader = (<Map<string, string | null>>response.headers).get('content-range')
if (!contentRangeHeader) {
@@ -836,7 +835,7 @@ export class SNApiService
const rangeEnd = +matches[3]
const totalSize = +matches[4]
const bytesReceived = new Uint8Array(response.data as ArrayBuffer)
const bytesReceived = new Uint8Array(response.data)
await onBytesReceived(bytesReceived)
@@ -847,8 +846,8 @@ export class SNApiService
return undefined
}
async checkIntegrity(integrityPayloads: Responses.IntegrityPayload[]): Promise<Responses.CheckIntegrityResponse> {
return await this.tokenRefreshableRequest<Responses.CheckIntegrityResponse>({
async checkIntegrity(integrityPayloads: IntegrityPayload[]): Promise<HttpResponse<CheckIntegrityResponse>> {
return this.tokenRefreshableRequest<CheckIntegrityResponse>({
verb: HttpVerb.Post,
url: joinPaths(this.host, Paths.v1.checkIntegrity),
params: {
@@ -859,8 +858,8 @@ export class SNApiService
})
}
async getSingleItem(itemUuid: string): Promise<Responses.GetSingleItemResponse> {
return await this.tokenRefreshableRequest<Responses.GetSingleItemResponse>({
async getSingleItem(itemUuid: string): Promise<HttpResponse<GetSingleItemResponse>> {
return this.tokenRefreshableRequest<GetSingleItemResponse>({
verb: HttpVerb.Get,
url: joinPaths(this.host, Paths.v1.getSingleItem(itemUuid)),
fallbackErrorMessage: API_MESSAGE_GENERIC_SINGLE_ITEM_SYNC_FAIL,
@@ -870,18 +869,19 @@ export class SNApiService
private preprocessingError() {
if (this.refreshingSession) {
return this.createErrorResponse(API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS)
return this.createErrorResponse(API_MESSAGE_TOKEN_REFRESH_IN_PROGRESS, HttpStatusCode.BadRequest)
}
if (!this.session) {
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION)
return this.createErrorResponse(API_MESSAGE_INVALID_SESSION, HttpStatusCode.BadRequest)
}
return undefined
}
/** Handle errored responses to authenticated requests */
private preprocessAuthenticatedErrorResponse(response: Responses.HttpResponse) {
if (response.status === Responses.StatusCode.HttpStatusInvalidSession && this.session) {
this.invalidSessionObserver?.(response.error?.tag === ErrorTag.RevokedSession)
private preprocessAuthenticatedErrorResponse(response: HttpResponse) {
if (response.status === HttpStatusCode.Unauthorized && this.session) {
this.invalidSessionObserver?.(response.data.error?.tag === ErrorTag.RevokedSession)
}
}

View File

@@ -1,4 +1,10 @@
import { HttpResponse, StatusCode } from '@standardnotes/responses'
import {
DeprecatedHttpResponse,
DeprecatedStatusCode,
HttpRequestParams,
HttpVerb,
HttpRequest,
} from '@standardnotes/responses'
import { isString } from '@standardnotes/utils'
import { SnjsVersion } from '@Lib/Version'
import {
@@ -9,33 +15,12 @@ import {
} from '@standardnotes/services'
import { Environment } from '@standardnotes/models'
export enum HttpVerb {
Get = 'GET',
Post = 'POST',
Put = 'PUT',
Patch = 'PATCH',
Delete = 'DELETE',
}
const REQUEST_READY_STATE_COMPLETED = 4
export type HttpParams = Record<string, unknown>
export type HttpRequest = {
url: string
params?: HttpParams
rawBytes?: Uint8Array
verb: HttpVerb
authentication?: string
customHeaders?: Record<string, string>[]
responseType?: XMLHttpRequestResponseType
external?: boolean
}
/**
* A non-SNJS specific wrapper for XMLHttpRequests
*/
export class SNHttpService extends AbstractService {
export class DeprecatedHttpService extends AbstractService {
constructor(
private readonly environment: Environment,
private readonly appVersion: string,
@@ -44,27 +29,47 @@ export class SNHttpService extends AbstractService {
super(internalEventBus)
}
public async getAbsolute(url: string, params?: HttpParams, authentication?: string): Promise<HttpResponse> {
public async getAbsolute(
url: string,
params?: HttpRequestParams,
authentication?: string,
): Promise<DeprecatedHttpResponse> {
return this.runHttp({ url, params, verb: HttpVerb.Get, authentication })
}
public async postAbsolute(url: string, params?: HttpParams, authentication?: string): Promise<HttpResponse> {
public async postAbsolute(
url: string,
params?: HttpRequestParams,
authentication?: string,
): Promise<DeprecatedHttpResponse> {
return this.runHttp({ url, params, verb: HttpVerb.Post, authentication })
}
public async putAbsolute(url: string, params?: HttpParams, authentication?: string): Promise<HttpResponse> {
public async putAbsolute(
url: string,
params?: HttpRequestParams,
authentication?: string,
): Promise<DeprecatedHttpResponse> {
return this.runHttp({ url, params, verb: HttpVerb.Put, authentication })
}
public async patchAbsolute(url: string, params: HttpParams, authentication?: string): Promise<HttpResponse> {
public async patchAbsolute(
url: string,
params: HttpRequestParams,
authentication?: string,
): Promise<DeprecatedHttpResponse> {
return this.runHttp({ url, params, verb: HttpVerb.Patch, authentication })
}
public async deleteAbsolute(url: string, params?: HttpParams, authentication?: string): Promise<HttpResponse> {
public async deleteAbsolute(
url: string,
params?: HttpRequestParams,
authentication?: string,
): Promise<DeprecatedHttpResponse> {
return this.runHttp({ url, params, verb: HttpVerb.Delete, authentication })
}
public async runHttp(httpRequest: HttpRequest): Promise<HttpResponse> {
public async runHttp(httpRequest: HttpRequest): Promise<DeprecatedHttpResponse> {
const request = this.createXmlRequest(httpRequest)
return this.runRequest(request, this.createRequestBody(httpRequest))
@@ -84,7 +89,7 @@ export class SNHttpService extends AbstractService {
private createXmlRequest(httpRequest: HttpRequest) {
const request = new XMLHttpRequest()
if (httpRequest.params && httpRequest.verb === HttpVerb.Get && Object.keys(httpRequest.params).length > 0) {
httpRequest.url = this.urlForUrlAndParams(httpRequest.url, httpRequest.params)
httpRequest.url = this.urlForUrlAndParams(httpRequest.url, httpRequest.params as Record<string, unknown>)
}
request.open(httpRequest.verb, httpRequest.url, true)
request.responseType = httpRequest.responseType ?? ''
@@ -116,7 +121,7 @@ export class SNHttpService extends AbstractService {
return request
}
private async runRequest(request: XMLHttpRequest, body?: string | Uint8Array): Promise<HttpResponse> {
private async runRequest(request: XMLHttpRequest, body?: string | Uint8Array): Promise<DeprecatedHttpResponse> {
return new Promise((resolve, reject) => {
request.onreadystatechange = () => {
this.stateChangeHandlerForRequest(request, resolve, reject)
@@ -127,14 +132,14 @@ export class SNHttpService extends AbstractService {
private stateChangeHandlerForRequest(
request: XMLHttpRequest,
resolve: (response: HttpResponse) => void,
reject: (response: HttpResponse) => void,
resolve: (response: DeprecatedHttpResponse) => void,
reject: (response: DeprecatedHttpResponse) => void,
) {
if (request.readyState !== REQUEST_READY_STATE_COMPLETED) {
return
}
const httpStatus = request.status
const response: HttpResponse = {
const response: DeprecatedHttpResponse = {
status: httpStatus,
headers: new Map<string, string | null>(),
}
@@ -152,7 +157,7 @@ export class SNHttpService extends AbstractService {
})
try {
if (httpStatus !== StatusCode.HttpStatusNoContent) {
if (httpStatus !== DeprecatedStatusCode.HttpStatusNoContent) {
let body
const contentTypeHeader = response.headers?.get('content-type') || response.headers?.get('Content-Type')
@@ -177,10 +182,13 @@ export class SNHttpService extends AbstractService {
} catch (error) {
console.error(error)
}
if (httpStatus >= StatusCode.HttpStatusMinSuccess && httpStatus <= StatusCode.HttpStatusMaxSuccess) {
if (
httpStatus >= DeprecatedStatusCode.HttpStatusMinSuccess &&
httpStatus <= DeprecatedStatusCode.HttpStatusMaxSuccess
) {
resolve(response)
} else {
if (httpStatus === StatusCode.HttpStatusForbidden) {
if (httpStatus === DeprecatedStatusCode.HttpStatusForbidden) {
response.error = {
message: API_MESSAGE_RATE_LIMITED,
status: httpStatus,
@@ -200,7 +208,7 @@ export class SNHttpService extends AbstractService {
}
}
private urlForUrlAndParams(url: string, params: HttpParams) {
private urlForUrlAndParams(url: string, params: Record<string, unknown>) {
const keyValueString = Object.keys(params)
.map((key) => {
return key + '=' + encodeURIComponent(params[key] as string)

View File

@@ -1,3 +1,4 @@
import { isErrorResponse } from '@standardnotes/responses'
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
import { AbstractService, InternalEventBusInterface, StorageKey } from '@standardnotes/services'
import { WebSocketApiServiceInterface } from '@standardnotes/api'
@@ -72,7 +73,7 @@ export class SNWebSocketsService extends AbstractService<WebSocketsServiceEvent,
private async createWebSocketConnectionToken(): Promise<string | undefined> {
try {
const response = await this.webSocketApiService.createConnectionToken()
if (response.data.error) {
if (isErrorResponse(response)) {
console.error(response.data.error)
return undefined

View File

@@ -1,5 +1,5 @@
export * from './ApiService'
export * from './HttpService'
export * from './DeprecatedHttpService'
export * from './Paths'
export * from '../Session/SessionManager'
export * from './WebsocketsService'

View File

@@ -7,7 +7,7 @@ import {
lastElement,
isString,
} from '@standardnotes/utils'
import { ClientDisplayableError, UserFeaturesResponse } from '@standardnotes/responses'
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import { ContentType } from '@standardnotes/common'
import { RoleName } from '@standardnotes/domain-core'
import { FillItemContent, PayloadEmitSource } from '@standardnotes/models'
@@ -417,8 +417,8 @@ export class SNFeaturesService
if (shouldDownloadRoleBasedFeatures) {
const featuresResponse = await this.apiService.getUserFeatures(userUuid)
if (!featuresResponse.error && featuresResponse.data && !this.deinited) {
const features = (featuresResponse as UserFeaturesResponse).data.features
if (!isErrorResponse(featuresResponse) && !this.deinited) {
const features = featuresResponse.data.features
await this.didDownloadFeatures(features)
}
}
@@ -747,7 +747,7 @@ export class SNFeaturesService
private async performDownloadExternalFeature(url: string): Promise<Models.SNComponent | undefined> {
const response = await this.apiService.downloadFeatureUrl(url)
if (response.error) {
if (response.data?.error) {
await this.alertService.alert(API_MESSAGE_FAILED_DOWNLOADING_EXTENSION)
return undefined
}

View File

@@ -18,7 +18,7 @@ import { SNApiService } from '@Lib/Services/Api/ApiService'
import { ContentType } from '@standardnotes/common'
import { ItemManager } from '../Items/ItemManager'
import { removeFromArray, Uuids } from '@standardnotes/utils'
import { ClientDisplayableError, KeyParamsResponse } from '@standardnotes/responses'
import { ClientDisplayableError, isErrorResponse } from '@standardnotes/responses'
import {
AlertService,
AbstractService,
@@ -283,7 +283,7 @@ export class SNKeyRecoveryService extends AbstractService<KeyRecoveryEvent, Decr
const signInResponse = await this.userService.correctiveSignIn(rootKey)
if (!signInResponse.error) {
if (!isErrorResponse(signInResponse)) {
void this.alertService.alert(KeyRecoveryStrings.KeyRecoveryRootKeyReplaced)
return rootKey
@@ -335,8 +335,8 @@ export class SNKeyRecoveryService extends AbstractService<KeyRecoveryEvent, Decr
email: identifier,
})
if (!paramsResponse.error && paramsResponse.data) {
return KeyParamsFromApiResponse(paramsResponse as KeyParamsResponse)
if (!isErrorResponse(paramsResponse)) {
return KeyParamsFromApiResponse(paramsResponse.data)
} else {
return undefined
}

View File

@@ -2,12 +2,12 @@ import { isString, lastElement, sleep } from '@standardnotes/utils'
import { UuidString } from '@Lib/Types/UuidString'
import { ContentType } from '@standardnotes/common'
import { ItemManager } from '@Lib/Services/Items/ItemManager'
import { SNHttpService } from '../Api/HttpService'
import { DeprecatedHttpService } from '../Api/DeprecatedHttpService'
import { SettingName } from '@standardnotes/settings'
import { SNSettingsService } from '../Settings/SNSettingsService'
import { ListedClientInterface } from './ListedClientInterface'
import { SNApiService } from '../Api/ApiService'
import { ListedAccount, ListedAccountInfo, ListedAccountInfoResponse } from '@standardnotes/responses'
import { isErrorResponse, ListedAccount, ListedAccountInfo, ListedAccountInfoResponse } from '@standardnotes/responses'
import { NoteMutator, SNActionsExtension, SNNote } from '@standardnotes/models'
import { AbstractService, InternalEventBusInterface, MutatorClientInterface } from '@standardnotes/services'
import { SNProtectionService } from '../Protection'
@@ -17,7 +17,7 @@ export class ListedService extends AbstractService implements ListedClientInterf
private apiService: SNApiService,
private itemManager: ItemManager,
private settingsService: SNSettingsService,
private httpSerivce: SNHttpService,
private httpSerivce: DeprecatedHttpService,
private protectionService: SNProtectionService,
private mutatorService: MutatorClientInterface,
protected override internalEventBus: InternalEventBusInterface,
@@ -63,7 +63,7 @@ export class ListedService extends AbstractService implements ListedClientInterf
public async requestNewListedAccount(): Promise<ListedAccount | undefined> {
const accountsBeforeRequest = await this.getSettingsBasedListedAccounts()
const response = await this.apiService.registerForListedAccount()
if (response.error) {
if (isErrorResponse(response)) {
return undefined
}
const MaxAttempts = 4
@@ -99,11 +99,12 @@ export class ListedService extends AbstractService implements ListedClientInterf
const response = (await this.httpSerivce.getAbsolute(url).catch((error) => {
console.error(error)
})) as ListedAccountInfoResponse
if (!response || response.error || !response.data || isString(response.data)) {
if (!response || response.data?.error || !response.data || isString(response.data)) {
return undefined
}
return response.data
return response
}
private async getSettingsBasedListedAccounts(): Promise<ListedAccount[]> {

View File

@@ -28,16 +28,28 @@ import {
SessionRefreshedData,
} from '@standardnotes/services'
import { Base64String } from '@standardnotes/sncrypto-common'
import { ClientDisplayableError, SessionBody } from '@standardnotes/responses'
import {
ClientDisplayableError,
SessionBody,
ErrorTag,
HttpResponse,
isErrorResponse,
SessionListEntry,
User,
AvailableSubscriptions,
KeyParamsResponse,
SignInResponse,
ChangeCredentialsResponse,
SessionListResponse,
HttpSuccessResponse,
} from '@standardnotes/responses'
import { CopyPayloadWithContentOverride } from '@standardnotes/models'
import { isNullOrUndefined } from '@standardnotes/utils'
import { LegacySession, MapperInterface, Session, SessionToken } from '@standardnotes/domain-core'
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 { RawStorageValue } from './Sessions/Types'
import { ShareToken } from './ShareToken'
import { SNApiService } from '../Api/ApiService'
import { DiskStorageService } from '../Storage/DiskStorageService'
@@ -48,8 +60,6 @@ import { ChallengeService } from '../Challenge'
import {
ApiCallError,
ErrorMessage,
ErrorTag,
HttpErrorResponseBody,
HttpServiceInterface,
UserApiServiceInterface,
UserRegistrationResponseBody,
@@ -76,7 +86,7 @@ export class SNSessionManager
extends AbstractService<SessionEvent>
implements SessionsClientInterface, InternalEventHandlerInterface
{
private user?: Responses.User
private user?: User
private isSessionRenewChallengePresented = false
private session?: Session | LegacySession
@@ -120,7 +130,7 @@ export class SNSessionManager
super.deinit()
}
private setUser(user?: Responses.User) {
private setUser(user?: User) {
this.user = user
this.apiService.setUser(user)
}
@@ -167,10 +177,10 @@ export class SNSessionManager
}
public offline() {
return isNullOrUndefined(this.apiService.getSession())
return this.apiService.getSession() == undefined
}
public getUser(): Responses.User | undefined {
public getUser(): User | undefined {
return this.user
}
@@ -187,7 +197,7 @@ export class SNSessionManager
}
public getSureUser() {
return this.user as Responses.User
return this.user as User
}
public getSession() {
@@ -213,7 +223,7 @@ export class SNSessionManager
public async reauthenticateInvalidSession(
cancelable = true,
onResponse?: (response: Responses.HttpResponse) => void,
onResponse?: (response: HttpResponse) => void,
): Promise<void> {
if (this.isSessionRenewChallengePresented) {
return
@@ -241,16 +251,16 @@ export class SNSessionManager
const email = challengeResponse.values[0].value as string
const password = challengeResponse.values[1].value as string
const currentKeyParams = this.protocolService.getAccountKeyParams()
const signInResult = await this.signIn(
const { response } = await this.signIn(
email,
password,
false,
this.diskStorageService.isEphemeralSession(),
currentKeyParams?.version,
)
if (signInResult.response.error) {
if (isErrorResponse(response)) {
this.challengeService.setValidationStatusForChallenge(challenge, challengeResponse!.values[1], false)
onResponse?.(signInResult.response)
onResponse?.(response)
} else {
resolve()
this.challengeService.completeChallenge(challenge)
@@ -263,26 +273,26 @@ export class SNSessionManager
})
}
public async getSubscription(): Promise<ClientDisplayableError | Subscription> {
public async getSubscription(): Promise<ClientDisplayableError | Subscription | undefined> {
const result = await this.apiService.getSubscription(this.getSureUser().uuid)
if (result.error) {
return ClientDisplayableError.FromError(result.error)
if (isErrorResponse(result)) {
return ClientDisplayableError.FromError(result.data?.error)
}
const subscription = (result as Responses.GetSubscriptionResponse).data!.subscription!
const subscription = result.data.subscription
return subscription
}
public async getAvailableSubscriptions(): Promise<Responses.AvailableSubscriptions | ClientDisplayableError> {
public async getAvailableSubscriptions(): Promise<AvailableSubscriptions | ClientDisplayableError> {
const response = await this.apiService.getAvailableSubscriptions()
if (response.error) {
return ClientDisplayableError.FromError(response.error)
if (isErrorResponse(response)) {
return ClientDisplayableError.FromError(response.data.error)
}
return (response as Responses.GetAvailableSubscriptionsResponse).data!
return response.data
}
private async promptForU2FVerification(username: string): Promise<Record<string, unknown> | undefined> {
@@ -361,7 +371,7 @@ export class SNSessionManager
const registerResponse = await this.userApiService.register({ email, serverPassword, keyParams, ephemeral })
if ('error' in registerResponse.data) {
throw new ApiCallError((registerResponse.data as HttpErrorResponseBody).error.message)
throw new ApiCallError(registerResponse.data.error.message)
}
await this.handleAuthentication({
@@ -376,37 +386,37 @@ export class SNSessionManager
private async retrieveKeyParams(dto: {
email: string
mfaKeyPath?: string
mfaCode?: string
authenticatorResponse?: Record<string, unknown>
}): Promise<{
keyParams?: SNRootKeyParams
response: Responses.KeyParamsResponse | Responses.HttpResponse
mfaKeyPath?: string
response: HttpResponse<KeyParamsResponse>
mfaCode?: string
}> {
const response = await this.apiService.getAccountKeyParams(dto)
if (response.error || isNullOrUndefined(response.data)) {
if (isErrorResponse(response) || !response.data) {
if (dto.mfaCode) {
await this.alertService.alert(SignInStrings.IncorrectMfa)
}
if ([ErrorTag.U2FRequired, ErrorTag.MfaRequired].includes(response.error?.tag as ErrorTag)) {
const isU2FRequired = response.error?.tag === ErrorTag.U2FRequired
const error = isErrorResponse(response) ? response.data.error : undefined
if (response.data && [ErrorTag.U2FRequired, ErrorTag.MfaRequired].includes(error?.tag as ErrorTag)) {
const isU2FRequired = error?.tag === ErrorTag.U2FRequired
const result = isU2FRequired ? await this.promptForU2FVerification(dto.email) : await this.promptForMfaValue()
if (!result) {
return {
response: this.apiService.createErrorResponse(
SignInStrings.SignInCanceledMissingMfa,
Responses.StatusCode.CanceledMfa,
undefined,
ErrorTag.ClientCanceledMfa,
),
}
}
return this.retrieveKeyParams({
email: dto.email,
mfaKeyPath: isU2FRequired ? undefined : response.error?.payload?.mfa_key,
mfaCode: isU2FRequired ? undefined : (result as string),
authenticatorResponse: isU2FRequired ? (result as Record<string, unknown>) : undefined,
})
@@ -415,13 +425,13 @@ export class SNSessionManager
}
}
/** Make sure to use client value for identifier/email */
const keyParams = KeyParamsFromApiResponse(response as Responses.KeyParamsResponse, dto.email)
const keyParams = KeyParamsFromApiResponse(response.data, dto.email)
if (!keyParams || !keyParams.version) {
return {
response: this.apiService.createErrorResponse(API_MESSAGE_FALLBACK_LOGIN_FAIL),
}
}
return { keyParams, response, mfaKeyPath: dto.mfaKeyPath, mfaCode: dto.mfaCode }
return { keyParams, response, mfaCode: dto.mfaCode }
}
public async signIn(
@@ -433,9 +443,9 @@ export class SNSessionManager
): Promise<SessionManagerResponse> {
const result = await this.performSignIn(email, password, strict, ephemeral, minAllowedVersion)
if (
result.response.error &&
result.response.error.status !== Responses.StatusCode.LocalValidationError &&
result.response.error.status !== Responses.StatusCode.CanceledMfa
isErrorResponse(result.response) &&
result.response.data.error.tag !== ErrorTag.ClientValidationError &&
result.response.data.error.tag !== ErrorTag.ClientCanceledMfa
) {
const cleanedEmail = cleanedEmailString(email)
if (cleanedEmail !== email) {
@@ -461,7 +471,7 @@ export class SNSessionManager
const paramsResult = await this.retrieveKeyParams({
email,
})
if (paramsResult.response.error) {
if (isErrorResponse(paramsResult.response)) {
return {
response: paramsResult.response,
}
@@ -512,7 +522,7 @@ export class SNSessionManager
minAllowedVersion = this.protocolService.getLatestVersion()
}
if (!isNullOrUndefined(minAllowedVersion)) {
if (minAllowedVersion != undefined) {
if (!Common.leftVersionGreaterThanOrEqualToRight(keyParams.version, minAllowedVersion)) {
return {
response: this.apiService.createErrorResponse(StrictSignInFailed(keyParams.version, minAllowedVersion)),
@@ -521,6 +531,7 @@ export class SNSessionManager
}
const rootKey = await this.protocolService.computeRootKey(password, keyParams)
const signInResponse = await this.bypassChecksAndSignInWithRootKey(email, rootKey, ephemeral)
return {
response: signInResponse,
}
@@ -530,13 +541,14 @@ export class SNSessionManager
email: string,
rootKey: SNRootKey,
ephemeral = false,
): Promise<Responses.SignInResponse | Responses.HttpResponse> {
): Promise<HttpResponse<SignInResponse>> {
const { wrappingKey, canceled } = await this.challengeService.getWrappingKeyIfApplicable()
if (canceled) {
return this.apiService.createErrorResponse(
SignInStrings.PasscodeRequired,
Responses.StatusCode.LocalValidationError,
undefined,
ErrorTag.ClientValidationError,
)
}
@@ -546,18 +558,18 @@ export class SNSessionManager
ephemeral,
})
if (signInResponse.error || !signInResponse.data) {
if (!signInResponse.data || isErrorResponse(signInResponse)) {
return signInResponse
}
const updatedKeyParams = (signInResponse as Responses.SignInResponse).data.key_params
const updatedKeyParams = signInResponse.data.key_params
const expandedRootKey = new SNRootKey(
CopyPayloadWithContentOverride(rootKey.payload, {
keyParams: updatedKeyParams || rootKey.keyParams.getPortableValue(),
}),
)
await this.handleSuccessAuthResponse(signInResponse as Responses.SignInResponse, expandedRootKey, wrappingKey)
await this.handleSuccessAuthResponse(signInResponse, expandedRootKey, wrappingKey)
return signInResponse
}
@@ -577,59 +589,54 @@ export class SNSessionManager
newEmail: parameters.newEmail,
})
return this.processChangeCredentialsResponse(
response as Responses.ChangeCredentialsResponse,
parameters.newRootKey,
parameters.wrappingKey,
)
return this.processChangeCredentialsResponse(response, parameters.newRootKey, parameters.wrappingKey)
}
public async getSessionsList(): Promise<
(Responses.HttpResponse & { data: RemoteSession[] }) | Responses.HttpResponse
> {
public async getSessionsList(): Promise<HttpResponse<SessionListEntry[]>> {
const response = await this.apiService.getSessionsList()
if (response.error || isNullOrUndefined(response.data)) {
if (isErrorResponse(response)) {
return response
}
;(
response as Responses.HttpResponse & {
data: RemoteSession[]
}
).data = (response as Responses.SessionListResponse).data
.map<RemoteSession>((session) => ({
...session,
updated_at: new Date(session.updated_at),
}))
.sort((s1: RemoteSession, s2: RemoteSession) => (s1.updated_at < s2.updated_at ? 1 : -1))
response.data = response.data.sort((s1: SessionListEntry, s2: SessionListEntry) => {
return new Date(s1.updated_at) < new Date(s2.updated_at) ? 1 : -1
})
return response
}
public async revokeSession(sessionId: UuidString): Promise<Responses.HttpResponse> {
const response = await this.apiService.deleteSession(sessionId)
return response
public async revokeSession(sessionId: UuidString): Promise<HttpResponse<SessionListResponse>> {
return this.apiService.deleteSession(sessionId)
}
public async revokeAllOtherSessions(): Promise<void> {
const response = await this.getSessionsList()
if (response.error != undefined || response.data == undefined) {
throw new Error(response.error?.message ?? API_MESSAGE_GENERIC_SYNC_FAIL)
if (isErrorResponse(response) || !response.data) {
const error = isErrorResponse(response) ? response.data?.error : undefined
throw new Error(error?.message ?? API_MESSAGE_GENERIC_SYNC_FAIL)
}
const sessions = response.data as RemoteSession[]
const otherSessions = sessions.filter((session) => !session.current)
const otherSessions = response.data.filter((session) => !session.current)
await Promise.all(otherSessions.map((session) => this.revokeSession(session.uuid)))
}
private async processChangeCredentialsResponse(
response: Responses.ChangeCredentialsResponse,
response: HttpResponse<ChangeCredentialsResponse>,
newRootKey: SNRootKey,
wrappingKey?: SNRootKey,
): Promise<SessionManagerResponse> {
if (!response.error && response.data) {
await this.handleSuccessAuthResponse(response as Responses.ChangeCredentialsResponse, newRootKey, wrappingKey)
if (isErrorResponse(response)) {
return {
response: response,
}
}
await this.handleSuccessAuthResponse(response, newRootKey, wrappingKey)
return {
response: response,
keyParams: (response as Responses.ChangeCredentialsResponse).data?.key_params,
keyParams: response.data?.key_params,
}
}
@@ -690,7 +697,7 @@ export class SNSessionManager
private async populateSession(
rootKey: SNRootKey,
user: Responses.User,
user: User,
session: Session | LegacySession,
host: string,
wrappingKey?: SNRootKey,
@@ -734,17 +741,17 @@ export class SNSessionManager
* @deprecated use handleAuthentication instead
*/
private async handleSuccessAuthResponse(
response: Responses.SignInResponse | Responses.ChangeCredentialsResponse,
response: HttpSuccessResponse<SignInResponse | ChangeCredentialsResponse>,
rootKey: SNRootKey,
wrappingKey?: SNRootKey,
) {
const { data } = response
const user = data.user as Responses.User
const user = data.user
const isLegacyJwtResponse = data.token != undefined
if (isLegacyJwtResponse) {
const sessionOrError = LegacySession.create(data.token as string)
if (!sessionOrError.isFailed()) {
if (!sessionOrError.isFailed() && user) {
await this.populateSession(rootKey, user, sessionOrError.getValue(), this.apiService.getHost(), wrappingKey)
}
} else if (data.session) {
@@ -755,7 +762,7 @@ export class SNSessionManager
data.session.refresh_expiration,
data.session.readonly_access,
)
if (session !== null) {
if (session !== null && user) {
await this.populateSession(rootKey, user, session, this.apiService.getHost(), wrappingKey)
}
}

View File

@@ -11,10 +11,3 @@ export type RawSessionPayload = {
}
export type RawStorageValue = RawJwtPayload | RawSessionPayload
export type RemoteSession = {
uuid: string
updated_at: Date
device_info: string
current: boolean
}

View File

@@ -1,7 +1,7 @@
import { SettingsList } from './SettingsList'
import { SettingName, SensitiveSettingName, SubscriptionSettingName } from '@standardnotes/settings'
import { API_MESSAGE_INVALID_SESSION } from '@standardnotes/services'
import { StatusCode, User } from '@standardnotes/responses'
import { HttpStatusCode, isErrorResponse, User } from '@standardnotes/responses'
import { SettingsServerInterface } from './SettingsServerInterface'
/**
@@ -31,30 +31,29 @@ export class SettingsGateway {
}
async listSettings() {
const { error, data } = await this.settingsApi.listSettings(this.userUuid)
const response = await this.settingsApi.listSettings(this.userUuid)
if (error != undefined) {
throw new Error(error.message)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
if (data == undefined || data.settings == undefined) {
if (response.data == undefined || response.data.settings == undefined) {
return new SettingsList([])
}
const settings: SettingsList = new SettingsList(data.settings)
const settings: SettingsList = new SettingsList(response.data.settings)
return settings
}
async getSetting(name: SettingName): Promise<string | undefined> {
const response = await this.settingsApi.getSetting(this.userUuid, name)
// Backend responds with 400 when setting doesn't exist
if (response.status === StatusCode.HttpBadRequest) {
if (response.status === HttpStatusCode.BadRequest) {
return undefined
}
if (response.error != undefined) {
throw new Error(response.error.message)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
return response?.data?.setting?.value ?? undefined
@@ -63,12 +62,12 @@ export class SettingsGateway {
async getSubscriptionSetting(name: SubscriptionSettingName): Promise<string | undefined> {
const response = await this.settingsApi.getSubscriptionSetting(this.userUuid, name)
if (response.status === StatusCode.HttpBadRequest) {
if (response.status === HttpStatusCode.BadRequest) {
return undefined
}
if (response.error != undefined) {
throw new Error(response.error.message)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
return response?.data?.setting?.value ?? undefined
@@ -77,29 +76,28 @@ export class SettingsGateway {
async getDoesSensitiveSettingExist(name: SensitiveSettingName): Promise<boolean> {
const response = await this.settingsApi.getSetting(this.userUuid, name)
// Backend responds with 400 when setting doesn't exist
if (response.status === StatusCode.HttpBadRequest) {
if (response.status === HttpStatusCode.BadRequest) {
return false
}
if (response.error != undefined) {
throw new Error(response.error.message)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
return response.data?.success ?? false
}
async updateSetting(name: SettingName, payload: string, sensitive: boolean): Promise<void> {
const { error } = await this.settingsApi.updateSetting(this.userUuid, name, payload, sensitive)
if (error != undefined) {
throw new Error(error.message)
const response = await this.settingsApi.updateSetting(this.userUuid, name, payload, sensitive)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
}
async deleteSetting(name: SettingName): Promise<void> {
const { error } = await this.settingsApi.deleteSetting(this.userUuid, name)
if (error != undefined) {
throw new Error(error.message)
const response = await this.settingsApi.deleteSetting(this.userUuid, name)
if (isErrorResponse(response)) {
throw new Error(response.data?.error.message)
}
}

View File

@@ -1,24 +1,25 @@
import {
DeleteSettingResponse,
GetSettingResponse,
HttpResponse,
ListSettingsResponse,
UpdateSettingResponse,
} from '@standardnotes/responses'
import { UuidString } from '@Lib/Types/UuidString'
export interface SettingsServerInterface {
listSettings(userUuid: UuidString): Promise<ListSettingsResponse>
listSettings(userUuid: UuidString): Promise<HttpResponse<ListSettingsResponse>>
updateSetting(
userUuid: UuidString,
settingName: string,
settingValue: string,
sensitive: boolean,
): Promise<UpdateSettingResponse>
): Promise<HttpResponse<UpdateSettingResponse>>
getSetting(userUuid: UuidString, settingName: string): Promise<GetSettingResponse>
getSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<GetSettingResponse>
getSubscriptionSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<GetSettingResponse>>
deleteSetting(userUuid: UuidString, settingName: string): Promise<DeleteSettingResponse>
deleteSetting(userUuid: UuidString, settingName: string): Promise<HttpResponse<DeleteSettingResponse>>
}

View File

@@ -3,7 +3,6 @@ import { arrayByDifference, nonSecureRandomIdentifier, subtractFromArray } from
import { ServerSyncResponse } from '@Lib/Services/Sync/Account/Response'
import { ResponseSignalReceiver, SyncSignal } from '@Lib/Services/Sync/Signals'
import { SNApiService } from '../../Api/ApiService'
import { RawSyncResponse } from '@standardnotes/responses'
export const SyncUpDownLimit = 150
@@ -56,12 +55,7 @@ export class AccountSyncOperation {
})
const payloads = this.popPayloads(this.upLimit)
const rawResponse = (await this.apiService.sync(
payloads,
this.lastSyncToken,
this.paginationToken,
this.downLimit,
)) as RawSyncResponse
const rawResponse = await this.apiService.sync(payloads, this.lastSyncToken, this.paginationToken, this.downLimit)
const response = new ServerSyncResponse(rawResponse)
this.responses.push(response)

View File

@@ -2,7 +2,9 @@ import {
ApiEndpointParam,
ConflictParams,
ConflictType,
Error,
HttpError,
HttpResponse,
isErrorResponse,
RawSyncResponse,
ServerItemResponse,
} from '@standardnotes/responses'
@@ -12,24 +14,31 @@ import {
ServerSyncSavedContextualPayload,
FilteredServerItem,
} from '@standardnotes/models'
import { deepFreeze, isNullOrUndefined } from '@standardnotes/utils'
import { deepFreeze } from '@standardnotes/utils'
export class ServerSyncResponse {
public readonly rawResponse: RawSyncResponse
public readonly savedPayloads: ServerSyncSavedContextualPayload[]
public readonly retrievedPayloads: FilteredServerItem[]
public readonly uuidConflictPayloads: FilteredServerItem[]
public readonly dataConflictPayloads: FilteredServerItem[]
public readonly rejectedPayloads: FilteredServerItem[]
constructor(rawResponse: RawSyncResponse) {
private successResponseData: RawSyncResponse | undefined
constructor(public rawResponse: HttpResponse<RawSyncResponse>) {
this.rawResponse = rawResponse
this.savedPayloads = FilterDisallowedRemotePayloadsAndMap(rawResponse.data?.saved_items || []).map((rawItem) => {
return CreateServerSyncSavedPayload(rawItem)
})
if (!isErrorResponse(rawResponse)) {
this.successResponseData = rawResponse.data
}
this.retrievedPayloads = FilterDisallowedRemotePayloadsAndMap(rawResponse.data?.retrieved_items || [])
this.savedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.saved_items || []).map(
(rawItem) => {
return CreateServerSyncSavedPayload(rawItem)
},
)
this.retrievedPayloads = FilterDisallowedRemotePayloadsAndMap(this.successResponseData?.retrieved_items || [])
this.dataConflictPayloads = FilterDisallowedRemotePayloadsAndMap(this.rawDataConflictItems)
@@ -40,8 +49,8 @@ export class ServerSyncResponse {
deepFreeze(this)
}
public get error(): Error | undefined {
return this.rawResponse.error || this.rawResponse.data?.error
public get error(): HttpError | undefined {
return isErrorResponse(this.rawResponse) ? this.rawResponse.data?.error : undefined
}
public get status(): number {
@@ -49,11 +58,11 @@ export class ServerSyncResponse {
}
public get lastSyncToken(): string | undefined {
return this.rawResponse.data?.[ApiEndpointParam.LastSyncToken]
return this.successResponseData?.[ApiEndpointParam.LastSyncToken]
}
public get paginationToken(): string | undefined {
return this.rawResponse.data?.[ApiEndpointParam.PaginationToken]
return this.successResponseData?.[ApiEndpointParam.PaginationToken]
}
public get numberOfItemsInvolved(): number {
@@ -75,7 +84,7 @@ export class ServerSyncResponse {
return conflict.type === ConflictType.UuidConflict
})
.map((conflict) => {
return conflict.unsaved_item || (conflict.item as ServerItemResponse)
return conflict.unsaved_item || conflict.item!
})
}
@@ -85,7 +94,7 @@ export class ServerSyncResponse {
return conflict.type === ConflictType.ConflictingData
})
.map((conflict) => {
return conflict.server_item || (conflict.item as ServerItemResponse)
return conflict.server_item || conflict.item!
})
}
@@ -99,17 +108,17 @@ export class ServerSyncResponse {
)
})
.map((conflict) => {
return conflict.unsaved_item as ServerItemResponse
return conflict.unsaved_item!
})
}
private get rawConflictObjects(): ConflictParams[] {
const conflicts = this.rawResponse.data?.conflicts || []
const legacyConflicts = this.rawResponse.data?.unsaved || []
const conflicts = this.successResponseData?.conflicts || []
const legacyConflicts = this.successResponseData?.unsaved || []
return conflicts.concat(legacyConflicts)
}
public get hasError(): boolean {
return !isNullOrUndefined(this.rawResponse.error)
return this.error != undefined
}
}

View File

@@ -13,7 +13,10 @@ export * from '@standardnotes/encryption'
export * from '@standardnotes/features'
export * from '@standardnotes/files'
export * from '@standardnotes/models'
export * from '@standardnotes/responses'
export { ErrorTag } from '@standardnotes/responses'
export * from '@standardnotes/services'
export * from '@standardnotes/settings'
export * from '@standardnotes/utils'

View File

@@ -68,7 +68,7 @@ describe('basic auth', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const response = await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.not.be.ok
expect(response.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
}).timeout(20000)
@@ -78,7 +78,7 @@ describe('basic auth', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const response = await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.not.be.ok
expect(response.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
let error
@@ -110,7 +110,7 @@ describe('basic auth', function () {
(async () => {
const response = await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.not.be.ok
expect(response.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
})(),
(async () => {
@@ -157,12 +157,12 @@ describe('basic auth', function () {
let response = await this.application.signIn(this.email, 'wrong password', undefined, undefined, undefined, true)
expect(response).to.have.property('status', 401)
expect(response.error).to.be.ok
expect(response.data.error).to.be.ok
response = await this.application.signIn(this.email, this.password, undefined, undefined, undefined, true)
expect(response.status).to.equal(200)
expect(response).to.not.haveOwnProperty('error')
expect(response.data).to.not.haveOwnProperty('error')
}).timeout(20000)
it('server retrieved key params should use our client inputted value for identifier', async function () {
@@ -203,7 +203,7 @@ describe('basic auth', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const response = await this.application.signIn(uppercase, this.password, undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.not.be.ok
expect(response.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
}).timeout(20000)
@@ -219,7 +219,7 @@ describe('basic auth', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const response = await this.application.signIn(withspace, this.password, undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.not.be.ok
expect(response.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
}).timeout(20000)
@@ -228,7 +228,7 @@ describe('basic auth', function () {
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const response = await this.application.signIn(this.email, 'wrongpassword', undefined, undefined, undefined, true)
expect(response).to.be.ok
expect(response.error).to.be.ok
expect(response.data.error).to.be.ok
expect(await this.application.protocolService.getRootKey()).to.not.be.ok
}).timeout(20000)
@@ -337,7 +337,7 @@ describe('basic auth', function () {
const signinResponse = await this.application.signIn(this.email, newPassword, undefined, undefined, undefined, true)
expect(signinResponse).to.be.ok
expect(signinResponse.error).to.not.be.ok
expect(signinResponse.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
@@ -420,7 +420,7 @@ describe('basic auth', function () {
)
expect(signinResponse).to.be.ok
expect(signinResponse.error).to.not.be.ok
expect(signinResponse.data.error).to.not.be.ok
expect(await this.application.protocolService.getRootKey()).to.be.ok
}
}).timeout(80000)

View File

@@ -172,8 +172,8 @@ describe('server session', function () {
Factory.ignoreChallenges(this.application)
const syncResponse = await this.application.apiService.sync([])
expect(syncResponse.status).to.equal(401)
expect(syncResponse.error.tag).to.equal('invalid-auth')
expect(syncResponse.error.message).to.equal('Invalid login credentials.')
expect(syncResponse.data.error.tag).to.equal('invalid-auth')
expect(syncResponse.data.error.message).to.equal('Invalid login credentials.')
})
it('sign out request should be performed successfully and terminate session with expired access token', async function () {
@@ -194,8 +194,8 @@ describe('server session', function () {
Factory.ignoreChallenges(this.application)
const syncResponse = await this.application.apiService.sync([])
expect(syncResponse.status).to.equal(401)
expect(syncResponse.error.tag).to.equal('invalid-auth')
expect(syncResponse.error.message).to.equal('Invalid login credentials.')
expect(syncResponse.data.error.tag).to.equal('invalid-auth')
expect(syncResponse.data.error.message).to.equal('Invalid login credentials.')
})
it('change email request should be successful with a valid access token', async function () {
@@ -207,8 +207,7 @@ describe('server session', function () {
const newEmail = UuidGenerator.GenerateUuid()
const changeEmailResponse = await application.changeEmail(newEmail, password)
expect(changeEmailResponse.status).to.equal(200)
expect(changeEmailResponse.data.user).to.be.ok
expect(changeEmailResponse.error).to.not.be.ok
application = await Factory.signOutApplicationAndReturnNew(application)
const loginResponse = await Factory.loginToApplication({
@@ -277,8 +276,7 @@ describe('server session', function () {
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
expect(changePasswordResponse.status).to.equal(200)
expect(changePasswordResponse.data.user).to.be.ok
expect(changePasswordResponse.error).to.not.be.ok
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const loginResponse = await Factory.loginToApplication({
@@ -305,8 +303,7 @@ describe('server session', function () {
const changePasswordResponse = await this.application.changePassword(this.password, this.newPassword)
expect(changePasswordResponse).to.be.ok
expect(changePasswordResponse.status).to.equal(200)
expect(changePasswordResponse.error).to.not.be.ok
this.application = await Factory.signOutApplicationAndReturnNew(this.application)
const loginResponse = await Factory.loginToApplication({
@@ -393,8 +390,8 @@ describe('server session', function () {
const refreshSessionResponse = await this.application.apiService.refreshSession()
expect(refreshSessionResponse.status).to.equal(400)
expect(refreshSessionResponse.error.tag).to.equal('expired-refresh-token')
expect(refreshSessionResponse.error.message).to.equal('The refresh token has expired.')
expect(refreshSessionResponse.data.error.tag).to.equal('expired-refresh-token')
expect(refreshSessionResponse.data.error.message).to.equal('The refresh token has expired.')
/*
The access token and refresh token should be expired up to this point.
@@ -403,8 +400,8 @@ describe('server session', function () {
Factory.ignoreChallenges(this.application)
const syncResponse = await this.application.apiService.sync([])
expect(syncResponse.status).to.equal(401)
expect(syncResponse.error.tag).to.equal('invalid-auth')
expect(syncResponse.error.message).to.equal('Invalid login credentials.')
expect(syncResponse.data.error.tag).to.equal('invalid-auth')
expect(syncResponse.data.error.message).to.equal('Invalid login credentials.')
})
it('should fail when renewing a session with an invalid refresh token', async function () {
@@ -428,8 +425,8 @@ describe('server session', function () {
const refreshSessionResponse = await this.application.apiService.refreshSession()
expect(refreshSessionResponse.status).to.equal(400)
expect(refreshSessionResponse.error.tag).to.equal('invalid-refresh-token')
expect(refreshSessionResponse.error.message).to.equal('The refresh token is not valid.')
expect(refreshSessionResponse.data.error.tag).to.equal('invalid-refresh-token')
expect(refreshSessionResponse.data.error.message).to.equal('The refresh token is not valid.')
// Access token should remain valid.
const syncResponse = await this.application.apiService.sync([])
@@ -446,10 +443,10 @@ describe('server session', function () {
const refreshPromise = this.application.apiService.refreshSession()
const syncResponse = await this.application.apiService.sync([])
expect(syncResponse.error).to.be.ok
expect(syncResponse.data.error).to.be.ok
const errorMessage = 'Your account session is being renewed with the server. Please try your request again.'
expect(syncResponse.error.message).to.be.equal(errorMessage)
expect(syncResponse.data.error.message).to.be.equal(errorMessage)
/** Wait for finish so that test cleans up properly */
await refreshPromise
})