Skip to content

Commit

Permalink
chore: remove calling payments server for subscriptions if using thir…
Browse files Browse the repository at this point in the history
…d party api hosts
  • Loading branch information
karolsojko committed Aug 9, 2023
1 parent 970238a commit bd898b5
Show file tree
Hide file tree
Showing 24 changed files with 233 additions and 89 deletions.
8 changes: 4 additions & 4 deletions packages/services/src/Domain/Api/LegacyApiServiceInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import { AnyFeatureDescription } from '@standardnotes/features'
export interface LegacyApiServiceInterface
extends AbstractService<ApiServiceEvent, ApiServiceEventData>,
FilesApiInterface {
isThirdPartyHostUsed(): boolean
setHost(host: string): Promise<void>
getHost(): string

downloadOfflineFeaturesFromRepo(
repo: SNFeatureRepo,
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>
downloadOfflineFeaturesFromRepo(dto: {
repo: SNFeatureRepo
trustedFeatureHosts: string[]
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError>

downloadFeatureUrl(url: string): Promise<HttpResponse>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ export interface ApplicationInterface {

hasAccount(): boolean
setCustomHost(host: string): Promise<void>
isThirdPartyHostUsed(): boolean
isUsingHomeServer(): Promise<boolean>

importData(data: BackupFile, awaitSync?: boolean): Promise<ImportDataReturnType>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { SubscriptionApiServiceInterface } from '@standardnotes/api'
import { Invitation } from '@standardnotes/models'
import { InternalEventBusInterface } from '..'
import { SubscriptionManager } from './SubscriptionManager'
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'
import { Result } from '@standardnotes/domain-core'

describe('SubscriptionManager', () => {
let subscriptionApiService: SubscriptionApiServiceInterface
let internalEventBus: InternalEventBusInterface
let sessions: SessionsClientInterface
let storage: StorageServiceInterface
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost

const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, internalEventBus)
const createManager = () => new SubscriptionManager(subscriptionApiService, sessions, storage, isApplicationUsingThirdPartyHostUseCase, internalEventBus)

beforeEach(() => {
subscriptionApiService = {} as jest.Mocked<SubscriptionApiServiceInterface>
Expand All @@ -31,6 +34,9 @@ describe('SubscriptionManager', () => {
internalEventBus = {} as jest.Mocked<InternalEventBusInterface>
internalEventBus.addEventHandler = jest.fn()
internalEventBus.publish = jest.fn()

isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))
})

describe('event handling', () => {
Expand Down
12 changes: 11 additions & 1 deletion packages/services/src/Domain/Subscription/SubscriptionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
} from '@standardnotes/responses'
import { SubscriptionManagerEvent } from './SubscriptionManagerEvent'
import { ApplicationStageChangedEventPayload } from '../Event/ApplicationStageChangedEventPayload'
import { IsApplicationUsingThirdPartyHost } from '../UseCase/IsApplicationUsingThirdPartyHost'

export class SubscriptionManager
extends AbstractService<SubscriptionManagerEvent>
Expand All @@ -34,6 +35,7 @@ export class SubscriptionManager
private subscriptionApiService: SubscriptionApiServiceInterface,
private sessions: SessionsClientInterface,
private storage: StorageServiceInterface,
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
protected override internalEventBus: InternalEventBusInterface,
) {
super(internalEventBus)
Expand All @@ -43,7 +45,15 @@ export class SubscriptionManager
switch (event.type) {
case ApplicationEvent.Launched: {
void this.fetchOnlineSubscription()
void this.fetchAvailableSubscriptions()

const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
if (isThirdPartyHostUsedOrError.isFailed()) {
break
}
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
if (!isThirdPartyHostUsed) {
void this.fetchAvailableSubscriptions()
}
break
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Result } from '@standardnotes/domain-core'

import { GetHost } from '../..'
import { IsApplicationUsingThirdPartyHost } from './IsApplicationUsingThirdPartyHost'

describe('IsApplicationUsingThirdPartyHost', () => {
let getHostUseCase: GetHost

const createUseCase = () => new IsApplicationUsingThirdPartyHost(getHostUseCase)

beforeEach(() => {
getHostUseCase = {} as jest.Mocked<GetHost>
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://api.standardnotes.com'))
})

it('returns true if host is localhost', () => {
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('http://localhost:3000'))

const useCase = createUseCase()
const result = useCase.execute()

expect(result.getValue()).toBe(true)
})

it('returns false if host is api.standardnotes.com', () => {
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://api.standardnotes.com'))

const useCase = createUseCase()
const result = useCase.execute()

expect(result.getValue()).toBe(false)
})

it('returns false if host is sync.standardnotes.org', () => {
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://sync.standardnotes.org'))

const useCase = createUseCase()
const result = useCase.execute()

expect(result.getValue()).toBe(false)
})

it('returns false if host is files.standardnotes.com', () => {
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://files.standardnotes.com'))

const useCase = createUseCase()
const result = useCase.execute()

expect(result.getValue()).toBe(false)
})

it('returns true if host is not first party', () => {
getHostUseCase.execute = jest.fn().mockReturnValue(Result.ok('https://example.com'))

const useCase = createUseCase()
const result = useCase.execute()

expect(result.getValue()).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'

import { GetHost } from './GetHost'

export class IsApplicationUsingThirdPartyHost implements SyncUseCaseInterface<boolean> {
private readonly APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']

private readonly FILES_DEFAULT_HOSTS = ['files.standardnotes.com']

constructor(private getHostUseCase: GetHost) {}

execute(): Result<boolean> {
const result = this.getHostUseCase.execute()
if (result.isFailed()) {
return Result.fail(result.getError())
}

const host = result.getValue()

return Result.ok(!this.isUrlFirstParty(host))
}

private isUrlFirstParty(url: string): boolean {
try {
const { host } = new URL(url)
return this.APPLICATION_DEFAULT_HOSTS.includes(host) || this.FILES_DEFAULT_HOSTS.includes(host)
} catch (error) {
return false
}
}
}
1 change: 1 addition & 0 deletions packages/services/src/Domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ export * from './UseCase/ChangeAndSaveItem'
export * from './UseCase/DiscardItemsLocally'
export * from './UseCase/GenerateUuid'
export * from './UseCase/GetHost'
export * from './UseCase/IsApplicationUsingThirdPartyHost'
export * from './UseCase/SetHost'
export * from './User/AccountEvent'
export * from './User/AccountEventData'
Expand Down
4 changes: 0 additions & 4 deletions packages/snjs/lib/Application/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,10 +962,6 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
}
}

public isThirdPartyHostUsed(): boolean {
return this.legacyApi.isThirdPartyHostUsed()
}

async isUsingHomeServer(): Promise<boolean> {
const homeServerService = this.dependencies.get<HomeServerServiceInterface>(TYPES.HomeServerService)

Expand Down
8 changes: 8 additions & 0 deletions packages/snjs/lib/Application/Dependencies/Dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ import {
GenerateUuid,
GetVaultItems,
ValidateVaultPassword,
IsApplicationUsingThirdPartyHost,
} from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
Expand Down Expand Up @@ -243,6 +244,10 @@ export class Dependencies {
return new GetHost(this.get<LegacyApiService>(TYPES.LegacyApiService))
})

this.factory.set(TYPES.IsApplicationUsingThirdPartyHost, () => {
return new IsApplicationUsingThirdPartyHost(this.get<GetHost>(TYPES.GetHost))
})

this.factory.set(TYPES.SetHost, () => {
return new SetHost(this.get<HttpService>(TYPES.HttpService), this.get<LegacyApiService>(TYPES.LegacyApiService))
})
Expand Down Expand Up @@ -1159,6 +1164,7 @@ export class Dependencies {
this.get<SessionManager>(TYPES.SessionManager),
this.get<PureCryptoInterface>(TYPES.Crypto),
this.get<Logger>(TYPES.Logger),
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
this.get<InternalEventBus>(TYPES.InternalEventBus),
)
})
Expand Down Expand Up @@ -1267,6 +1273,7 @@ export class Dependencies {
this.get<SubscriptionApiService>(TYPES.SubscriptionApiService),
this.get<SessionManager>(TYPES.SessionManager),
this.get<DiskStorageService>(TYPES.DiskStorageService),
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
this.get<InternalEventBus>(TYPES.InternalEventBus),
)
})
Expand All @@ -1286,6 +1293,7 @@ export class Dependencies {
this.get<LegacySessionStorageMapper>(TYPES.LegacySessionStorageMapper),
this.options.identifier,
this.get<GetKeyPairs>(TYPES.GetKeyPairs),
this.get<IsApplicationUsingThirdPartyHost>(TYPES.IsApplicationUsingThirdPartyHost),
this.get<InternalEventBus>(TYPES.InternalEventBus),
)
})
Expand Down
1 change: 1 addition & 0 deletions packages/snjs/lib/Application/Dependencies/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export const TYPES = {
ChangeVaultStorageMode: Symbol.for('ChangeVaultStorageMode'),
ChangeAndSaveItem: Symbol.for('ChangeAndSaveItem'),
GetHost: Symbol.for('GetHost'),
IsApplicationUsingThirdPartyHost: Symbol.for('IsApplicationUsingThirdPartyHost'),
SetHost: Symbol.for('SetHost'),
GenerateUuid: Symbol.for('GenerateUuid'),
GetVaultItems: Symbol.for('GetVaultItems'),
Expand Down
32 changes: 0 additions & 32 deletions packages/snjs/lib/Hosts.ts

This file was deleted.

19 changes: 7 additions & 12 deletions packages/snjs/lib/Services/Api/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ import { LegacySession, MapperInterface, Session, SessionToken } from '@standard
import { HttpServiceInterface } from '@standardnotes/api'
import { SNRootKeyParams } from '@standardnotes/encryption'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import { isUrlFirstParty, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
import { Paths } from './Paths'
import { DiskStorageService } from '../Storage/DiskStorageService'
import { UuidString } from '../../Types/UuidString'
Expand Down Expand Up @@ -157,11 +156,6 @@ export class LegacyApiService
return this.host
}

public isThirdPartyHostUsed(): boolean {
const applicationHost = this.getHost() || ''
return !isUrlFirstParty(applicationHost)
}

public getFilesHost(): string {
if (!this.filesHost) {
throw Error('Attempting to access undefined filesHost')
Expand Down Expand Up @@ -620,19 +614,20 @@ export class LegacyApiService
return response.data.token
}

public async downloadOfflineFeaturesFromRepo(
repo: SNFeatureRepo,
): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
public async downloadOfflineFeaturesFromRepo(dto: {
repo: SNFeatureRepo
trustedFeatureHosts: string[]
}): Promise<{ features: AnyFeatureDescription[]; roles: string[] } | ClientDisplayableError> {
try {
const featuresUrl = repo.offlineFeaturesUrl
const extensionKey = repo.offlineKey
const featuresUrl = dto.repo.offlineFeaturesUrl
const extensionKey = dto.repo.offlineKey
if (!featuresUrl || !extensionKey) {
throw Error('Cannot download offline repo without url and offlineKEy')
}

const { hostname } = new URL(featuresUrl)

if (!TRUSTED_FEATURE_HOSTS.includes(hostname)) {
if (!dto.trustedFeatureHosts.includes(hostname)) {
return new ClientDisplayableError(`The offline features host ${hostname} is not in the trusted allowlist.`)
}

Expand Down
10 changes: 8 additions & 2 deletions packages/snjs/lib/Services/Features/FeaturesService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ItemInterface, SNFeatureRepo } from '@standardnotes/models'
import { SyncService } from '../Sync/SyncService'
import { SettingName } from '@standardnotes/settings'
import { FeaturesService } from '@Lib/Services/Features'
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
import { RoleName, ContentType, Uuid, Result } from '@standardnotes/domain-core'
import { NativeFeatureIdentifier, GetFeatures } from '@standardnotes/features'
import { WebSocketsService } from '../Api/WebsocketsService'
import { SettingsService } from '../Settings'
Expand All @@ -22,6 +22,7 @@ import {
SyncServiceInterface,
UserServiceInterface,
UserService,
IsApplicationUsingThirdPartyHost,
} from '@standardnotes/services'
import { LegacyApiService, SessionManager } from '../Api'
import { ItemManager } from '../Items'
Expand All @@ -47,6 +48,7 @@ describe('FeaturesService', () => {
let internalEventBus: InternalEventBusInterface
let featureService: FeaturesService
let logger: LoggerInterface
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost

beforeEach(() => {
logger = {} as jest.Mocked<LoggerInterface>
Expand All @@ -62,7 +64,6 @@ describe('FeaturesService', () => {

apiService = {} as jest.Mocked<LegacyApiService>
apiService.addEventObserver = jest.fn()
apiService.isThirdPartyHostUsed = jest.fn().mockReturnValue(false)

itemManager = {} as jest.Mocked<ItemManager>
itemManager.getItems = jest.fn().mockReturnValue(items)
Expand Down Expand Up @@ -107,6 +108,9 @@ describe('FeaturesService', () => {
internalEventBus.publish = jest.fn()
internalEventBus.addEventHandler = jest.fn()

isApplicationUsingThirdPartyHostUseCase = {} as jest.Mocked<IsApplicationUsingThirdPartyHost>
isApplicationUsingThirdPartyHostUseCase.execute = jest.fn().mockReturnValue(Result.ok(false))

featureService = new FeaturesService(
storageService,
itemManager,
Expand All @@ -121,6 +125,7 @@ describe('FeaturesService', () => {
sessionManager,
crypto,
logger,
isApplicationUsingThirdPartyHostUseCase,
internalEventBus,
)
})
Expand Down Expand Up @@ -202,6 +207,7 @@ describe('FeaturesService', () => {
sessionManager,
crypto,
logger,
isApplicationUsingThirdPartyHostUseCase,
internalEventBus,
)
}
Expand Down

0 comments on commit bd898b5

Please sign in to comment.