Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: remove calling payments server for subscriptions if using third party api hosts #2398

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions packages/services/src/Domain/Api/LegacyApiServiceInterface.ts
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
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
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
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
@@ -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)
})
})
@@ -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
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
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
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
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
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
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