chore: remove calling payments server for subscriptions if using third party api hosts (#2398)
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ import {
|
||||
GenerateUuid,
|
||||
GetVaultItems,
|
||||
ValidateVaultPassword,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { ItemManager } from '../../Services/Items/ItemManager'
|
||||
import { PayloadManager } from '../../Services/Payloads/PayloadManager'
|
||||
@@ -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))
|
||||
})
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
export const APPLICATION_DEFAULT_HOSTS = ['api.standardnotes.com', 'sync.standardnotes.org']
|
||||
|
||||
export const FILES_DEFAULT_HOSTS = ['files.standardnotes.com']
|
||||
|
||||
export const TRUSTED_FEATURE_HOSTS = [
|
||||
'api.standardnotes.com',
|
||||
'extensions.standardnotes.com',
|
||||
'extensions.standardnotes.org',
|
||||
'features.standardnotes.com',
|
||||
'localhost',
|
||||
]
|
||||
|
||||
export enum ExtensionsServerURL {
|
||||
Prod = 'https://extensions.standardnotes.org',
|
||||
}
|
||||
|
||||
const LocalHost = 'localhost'
|
||||
|
||||
export function isUrlFirstParty(url: string): boolean {
|
||||
try {
|
||||
const { host } = new URL(url)
|
||||
return host.startsWith(LocalHost) || APPLICATION_DEFAULT_HOSTS.includes(host) || FILES_DEFAULT_HOSTS.includes(host)
|
||||
} catch (_err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
||||
|
||||
export const LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
||||
|
||||
export const TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
||||
@@ -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'
|
||||
@@ -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')
|
||||
@@ -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.`)
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
SyncServiceInterface,
|
||||
UserServiceInterface,
|
||||
UserService,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { LegacyApiService, SessionManager } from '../Api'
|
||||
import { ItemManager } from '../Items'
|
||||
@@ -47,6 +48,7 @@ describe('FeaturesService', () => {
|
||||
let internalEventBus: InternalEventBusInterface
|
||||
let featureService: FeaturesService
|
||||
let logger: LoggerInterface
|
||||
let isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost
|
||||
|
||||
beforeEach(() => {
|
||||
logger = {} as jest.Mocked<LoggerInterface>
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
@@ -121,6 +125,7 @@ describe('FeaturesService', () => {
|
||||
sessionManager,
|
||||
crypto,
|
||||
logger,
|
||||
isApplicationUsingThirdPartyHostUseCase,
|
||||
internalEventBus,
|
||||
)
|
||||
})
|
||||
@@ -202,6 +207,7 @@ describe('FeaturesService', () => {
|
||||
sessionManager,
|
||||
crypto,
|
||||
logger,
|
||||
isApplicationUsingThirdPartyHostUseCase,
|
||||
internalEventBus,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ import { MigrateFeatureRepoToUserSettingUseCase } from './UseCase/MigrateFeature
|
||||
import { arraysEqual, removeFromArray, lastElement, LoggerInterface } from '@standardnotes/utils'
|
||||
import { ClientDisplayableError } from '@standardnotes/responses'
|
||||
import { RoleName, ContentType, Uuid } from '@standardnotes/domain-core'
|
||||
import { PROD_OFFLINE_FEATURES_URL } from '../../Hosts'
|
||||
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import { WebSocketsService } from '../Api/WebsocketsService'
|
||||
import { WebSocketsServiceEvent } from '../Api/WebSocketsServiceEvent'
|
||||
import { TRUSTED_CUSTOM_EXTENSIONS_HOSTS, TRUSTED_FEATURE_HOSTS } from '@Lib/Hosts'
|
||||
import { UserRolesChangedEvent } from '@standardnotes/domain-events'
|
||||
import { ExperimentalFeatures, FindNativeFeature, NativeFeatureIdentifier } from '@standardnotes/features'
|
||||
import {
|
||||
@@ -48,6 +46,7 @@ import {
|
||||
SubscriptionManagerEvent,
|
||||
ApplicationEvent,
|
||||
ApplicationStageChangedEventPayload,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
|
||||
import { DownloadRemoteThirdPartyFeatureUseCase } from './UseCase/DownloadRemoteThirdPartyFeature'
|
||||
@@ -67,6 +66,18 @@ export class FeaturesService
|
||||
|
||||
private getFeatureStatusUseCase = new GetFeatureStatusUseCase(this.items)
|
||||
|
||||
private readonly TRUSTED_FEATURE_HOSTS = [
|
||||
'api.standardnotes.com',
|
||||
'extensions.standardnotes.com',
|
||||
'extensions.standardnotes.org',
|
||||
'features.standardnotes.com',
|
||||
'localhost',
|
||||
]
|
||||
|
||||
private readonly TRUSTED_CUSTOM_EXTENSIONS_HOSTS = ['listed.to']
|
||||
|
||||
private readonly PROD_OFFLINE_FEATURES_URL = 'https://api.standardnotes.com/v1/offline/features'
|
||||
|
||||
constructor(
|
||||
private storage: StorageServiceInterface,
|
||||
private items: ItemManagerInterface,
|
||||
@@ -81,6 +92,7 @@ export class FeaturesService
|
||||
private sessions: SessionsClientInterface,
|
||||
private crypto: PureCryptoInterface,
|
||||
private logger: LoggerInterface,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -128,7 +140,12 @@ export class FeaturesService
|
||||
if (eventName === AccountEvent.SignedInOrRegistered) {
|
||||
const featureRepos = this.items.getItems(ContentType.TYPES.ExtensionRepo) as SNFeatureRepo[]
|
||||
|
||||
if (!this.api.isThirdPartyHostUsed()) {
|
||||
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||
return
|
||||
}
|
||||
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||
if (!isThirdPartyHostUsed) {
|
||||
void this.migrateFeatureRepoToUserSetting(featureRepos)
|
||||
}
|
||||
}
|
||||
@@ -285,7 +302,10 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
private async downloadOfflineRoles(repo: SNFeatureRepo): Promise<SetOfflineFeaturesFunctionResponse> {
|
||||
const result = await this.api.downloadOfflineFeaturesFromRepo(repo)
|
||||
const result = await this.api.downloadOfflineFeaturesFromRepo({
|
||||
repo,
|
||||
trustedFeatureHosts: this.TRUSTED_FEATURE_HOSTS,
|
||||
})
|
||||
|
||||
if (result instanceof ClientDisplayableError) {
|
||||
return result
|
||||
@@ -301,7 +321,7 @@ export class FeaturesService
|
||||
|
||||
public async migrateFeatureRepoToOfflineEntitlements(featureRepos: SNFeatureRepo[] = []): Promise<void> {
|
||||
const usecase = new MigrateFeatureRepoToOfflineEntitlementsUseCase(this.mutator)
|
||||
const updatedRepos = await usecase.execute(featureRepos)
|
||||
const updatedRepos = await usecase.execute({ featureRepos, prodOfflineFeaturesUrl: this.PROD_OFFLINE_FEATURES_URL })
|
||||
|
||||
if (updatedRepos.length > 0) {
|
||||
await this.downloadOfflineRoles(updatedRepos[0])
|
||||
@@ -322,7 +342,7 @@ export class FeaturesService
|
||||
return false
|
||||
}
|
||||
|
||||
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === PROD_OFFLINE_FEATURES_URL
|
||||
const hasFirstPartyOfflineSubscription = offlineRepo.content.offlineFeaturesUrl === this.PROD_OFFLINE_FEATURES_URL
|
||||
return hasFirstPartyOfflineSubscription || new URL(offlineRepo.content.offlineFeaturesUrl).hostname === 'localhost'
|
||||
}
|
||||
|
||||
@@ -427,7 +447,7 @@ export class FeaturesService
|
||||
}
|
||||
|
||||
try {
|
||||
const trustedCustomExtensionsUrls = [...TRUSTED_FEATURE_HOSTS, ...TRUSTED_CUSTOM_EXTENSIONS_HOSTS]
|
||||
const trustedCustomExtensionsUrls = [...this.TRUSTED_FEATURE_HOSTS, ...this.TRUSTED_CUSTOM_EXTENSIONS_HOSTS]
|
||||
const { host } = new URL(url)
|
||||
|
||||
const usecase = new DownloadRemoteThirdPartyFeatureUseCase(this.api, this.items, this.alerts)
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { LEGACY_PROD_EXT_ORIGIN, PROD_OFFLINE_FEATURES_URL } from '@Lib/Hosts'
|
||||
import { SNFeatureRepo } from '@standardnotes/models'
|
||||
import { MutatorClientInterface } from '@standardnotes/services'
|
||||
|
||||
export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
private readonly LEGACY_PROD_EXT_ORIGIN = 'https://extensions.standardnotes.org'
|
||||
|
||||
constructor(private mutator: MutatorClientInterface) {}
|
||||
|
||||
async execute(featureRepos: SNFeatureRepo[] = []): Promise<SNFeatureRepo[]> {
|
||||
async execute(dto: { featureRepos: SNFeatureRepo[]; prodOfflineFeaturesUrl: string }): Promise<SNFeatureRepo[]> {
|
||||
const updatedRepos: SNFeatureRepo[] = []
|
||||
for (const item of featureRepos) {
|
||||
for (const item of dto.featureRepos) {
|
||||
if (item.migratedToOfflineEntitlements) {
|
||||
continue
|
||||
}
|
||||
@@ -19,7 +20,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
const repoUrl = item.onlineUrl
|
||||
const { origin } = new URL(repoUrl)
|
||||
|
||||
if (!origin.includes(LEGACY_PROD_EXT_ORIGIN)) {
|
||||
if (!origin.includes(this.LEGACY_PROD_EXT_ORIGIN)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -28,7 +29,7 @@ export class MigrateFeatureRepoToOfflineEntitlementsUseCase {
|
||||
const userKey = userKeyMatch[0]
|
||||
|
||||
const updatedRepo = await this.mutator.changeFeatureRepo(item, (m) => {
|
||||
m.offlineFeaturesUrl = PROD_OFFLINE_FEATURES_URL
|
||||
m.offlineFeaturesUrl = dto.prodOfflineFeaturesUrl
|
||||
m.offlineKey = userKey
|
||||
m.migratedToOfflineEntitlements = true
|
||||
})
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
ApplicationStageChangedEventPayload,
|
||||
ApplicationStage,
|
||||
GetKeyPairs,
|
||||
IsApplicationUsingThirdPartyHost,
|
||||
} from '@standardnotes/services'
|
||||
import { Base64String, PureCryptoInterface } from '@standardnotes/sncrypto-common'
|
||||
import {
|
||||
@@ -105,6 +106,7 @@ export class SessionManager
|
||||
private legacySessionStorageMapper: MapperInterface<LegacySession, Record<string, unknown>>,
|
||||
private workspaceIdentifier: string,
|
||||
private _getKeyPairs: GetKeyPairs,
|
||||
private isApplicationUsingThirdPartyHostUseCase: IsApplicationUsingThirdPartyHost,
|
||||
protected override internalEventBus: InternalEventBusInterface,
|
||||
) {
|
||||
super(internalEventBus)
|
||||
@@ -274,7 +276,13 @@ export class SessionManager
|
||||
}
|
||||
|
||||
public isSignedIntoFirstPartyServer(): boolean {
|
||||
return this.isSignedIn() && !this.apiService.isThirdPartyHostUsed()
|
||||
const isThirdPartyHostUsedOrError = this.isApplicationUsingThirdPartyHostUseCase.execute()
|
||||
if (isThirdPartyHostUsedOrError.isFailed()) {
|
||||
return false
|
||||
}
|
||||
const isThirdPartyHostUsed = isThirdPartyHostUsedOrError.getValue()
|
||||
|
||||
return this.isSignedIn() && !isThirdPartyHostUsed
|
||||
}
|
||||
|
||||
public async reauthenticateInvalidSession(
|
||||
|
||||
@@ -47,8 +47,8 @@ describe('features', () => {
|
||||
|
||||
describe('extension repo items observer', () => {
|
||||
it('should migrate to user setting when extension repo is added', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
|
||||
expect(
|
||||
@@ -74,8 +74,8 @@ describe('features', () => {
|
||||
})
|
||||
|
||||
it('signing into account with ext repo should migrate it', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
/** Attach an ExtensionRepo object to an account, but prevent it from being migrated.
|
||||
* Then sign out, sign back in, and ensure the item is migrated. */
|
||||
@@ -96,8 +96,8 @@ describe('features', () => {
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
|
||||
sinon.restore()
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const promise = new Promise((resolve) => {
|
||||
sinon.stub(application.features, 'migrateFeatureRepoToUserSetting').callsFake(resolve)
|
||||
@@ -112,8 +112,8 @@ describe('features', () => {
|
||||
|
||||
it('having an ext repo with no account, then signing into account, should migrate it', async () => {
|
||||
application = await Factory.signOutApplicationAndReturnNew(application)
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const extensionKey = UuidGenerator.GenerateUuid().split('-').join('')
|
||||
await application.mutator.createItem(
|
||||
@@ -137,8 +137,8 @@ describe('features', () => {
|
||||
})
|
||||
|
||||
it('migrated ext repo should have property indicating it was migrated', async () => {
|
||||
sinon.stub(application.legacyApi, 'isThirdPartyHostUsed').callsFake(() => {
|
||||
return false
|
||||
sinon.stub(application.features.isApplicationUsingThirdPartyHostUseCase, 'execute').callsFake(() => {
|
||||
return Result.ok(false)
|
||||
})
|
||||
const setting = SettingName.create(SettingName.NAMES.ExtensionKey).getValue()
|
||||
expect(await application.settings.getDoesSensitiveSettingExist(setting)).to.equal(false)
|
||||
|
||||
Reference in New Issue
Block a user