feat: add snjs package

This commit is contained in:
Karol Sójko
2022-07-06 14:04:18 +02:00
parent 321a055bae
commit 0e40469e2f
296 changed files with 46109 additions and 187 deletions

View File

@@ -0,0 +1,151 @@
import { SNLog } from './../Log'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
import {
AlertService,
DeviceInterface,
Environment,
namespacedKey,
Platform,
RawStorageKey,
} from '@standardnotes/services'
import { SNApplication } from './Application'
describe('application', () => {
// eslint-disable-next-line no-console
SNLog.onLog = console.log
SNLog.onError = console.error
let application: SNApplication
let device: DeviceInterface
let crypto: PureCryptoInterface
beforeEach(async () => {
const identifier = '123'
crypto = {} as jest.Mocked<PureCryptoInterface>
crypto.initialize = jest.fn()
device = {} as jest.Mocked<DeviceInterface>
device.openDatabase = jest.fn().mockResolvedValue(true)
device.getAllRawDatabasePayloads = jest.fn().mockReturnValue([])
device.setRawStorageValue = jest.fn()
device.getRawStorageValue = jest.fn().mockImplementation((key) => {
if (key === namespacedKey(identifier, RawStorageKey.SnjsVersion)) {
return '10.0.0'
}
return undefined
})
device.getDatabaseKeys = async () => {
return Promise.resolve(['1', '2', '3'])
}
application = new SNApplication({
environment: Environment.Mobile,
platform: Platform.Ios,
deviceInterface: device,
crypto: crypto,
alertService: {} as jest.Mocked<AlertService>,
identifier: identifier,
defaultHost: 'localhost',
appVersion: '1.0',
})
await application.prepareForLaunch({ receiveChallenge: jest.fn() })
})
it('diagnostics', async () => {
const diagnostics = await application.getDiagnostics()
expect(diagnostics).toEqual(
expect.objectContaining({
application: expect.objectContaining({
appVersion: '1.0',
environment: 3,
platform: 1,
}),
payloads: {
integrityPayloads: [],
nonDeletedItemCount: 0,
invalidPayloadsCount: 0,
},
items: { allIds: [] },
storage: {
storagePersistable: false,
persistencePolicy: 'Default',
encryptionPolicy: 'Default',
needsPersist: false,
currentPersistPromise: false,
isStorageWrapped: false,
allRawPayloadsCount: 0,
databaseKeys: ['1', '2', '3'],
},
encryption: expect.objectContaining({
getLatestVersion: '004',
hasAccount: false,
getUserVersion: undefined,
upgradeAvailable: false,
accountUpgradeAvailable: false,
passcodeUpgradeAvailable: false,
hasPasscode: false,
isPasscodeLocked: false,
itemsEncryption: expect.objectContaining({
itemsKeysIds: [],
}),
rootKeyEncryption: expect.objectContaining({
hasRootKey: false,
keyMode: 'RootKeyNone',
hasRootKeyWrapper: false,
hasAccount: false,
hasPasscode: false,
}),
}),
api: {
hasSession: false,
user: undefined,
registering: false,
authenticating: false,
changing: false,
refreshingSession: false,
filesHost: undefined,
host: 'localhost',
},
session: {
isSessionRenewChallengePresented: false,
online: false,
offline: true,
isSignedIn: false,
isSignedIntoFirstPartyServer: false,
},
sync: {
syncToken: undefined,
cursorToken: undefined,
lastSyncDate: undefined,
outOfSync: false,
completedOnlineDownloadFirstSync: false,
clientLocked: false,
databaseLoaded: false,
syncLock: false,
dealloced: false,
itemsNeedingSync: [],
itemsNeedingSyncCount: 0,
pendingRequestCount: 0,
},
protections: expect.objectContaining({
getLastSessionLength: undefined,
hasProtectionSources: false,
hasUnprotectedAccessSession: true,
hasBiometricsEnabled: false,
}),
keyRecovery: { queueLength: 0, isProcessingQueue: false },
features: {
roles: [],
features: [],
enabledExperimentalFeatures: [],
needsInitialFeaturesUpdate: true,
completedSuccessfulFeaturesRetrieval: false,
},
migrations: { activeMigrations: [] },
}),
)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
import { SyncEvent } from '@standardnotes/services'
export { SyncEvent }
export enum ApplicationEvent {
SignedIn = 2,
SignedOut = 3,
/** When a full, potentially multi-page sync completes */
CompletedFullSync = 5,
FailedSync = 6,
HighLatencySync = 7,
EnteredOutOfSync = 8,
ExitedOutOfSync = 9,
/**
* The application has finished it `prepareForLaunch` state and is now ready for unlock
* Called when the application has initialized and is ready for launch, but before
* the application has been unlocked, if applicable. Use this to do pre-launch
* configuration, but do not attempt to access user data like notes or tags.
*/
Started = 10,
/**
* The applicaiton is fully unlocked and ready for i/o
* Called when the application has been fully decrypted and unlocked. Use this to
* to begin streaming data like notes and tags.
*/
Launched = 11,
LocalDataLoaded = 12,
/**
* When the root key or root key wrapper changes. Includes events like account state
* changes (registering, signing in, changing pw, logging out) and passcode state
* changes (adding, removing, changing).
*/
KeyStatusChanged = 13,
MajorDataChange = 14,
CompletedRestart = 15,
LocalDataIncrementalLoad = 16,
SyncStatusChanged = 17,
WillSync = 18,
InvalidSyncSession = 19,
LocalDatabaseReadError = 20,
LocalDatabaseWriteError = 21,
/** When a single roundtrip completes with sync, in a potentially multi-page sync request.
* If just a single roundtrip, this event will be triggered, along with CompletedFullSync */
CompletedIncrementalSync = 22,
/**
* The application has loaded all pending migrations (but not run any, except for the base one),
* and consumers may now call `hasPendingMigrations`
*/
MigrationsLoaded = 23,
/** When StorageService is ready to start servicing read/write requests */
StorageReady = 24,
PreferencesChanged = 25,
UnprotectedSessionBegan = 26,
UserRolesChanged = 27,
FeaturesUpdated = 28,
UnprotectedSessionExpired = 29,
/** Called when the app first launches and after first sync request made after sign in */
CompletedInitialSync = 30,
}
export function applicationEventForSyncEvent(syncEvent: SyncEvent) {
return (
{
[SyncEvent.SyncCompletedWithAllItemsUploaded]: ApplicationEvent.CompletedFullSync,
[SyncEvent.SingleRoundTripSyncCompleted]: ApplicationEvent.CompletedIncrementalSync,
[SyncEvent.SyncError]: ApplicationEvent.FailedSync,
[SyncEvent.SyncTakingTooLong]: ApplicationEvent.HighLatencySync,
[SyncEvent.EnterOutOfSync]: ApplicationEvent.EnteredOutOfSync,
[SyncEvent.ExitOutOfSync]: ApplicationEvent.ExitedOutOfSync,
[SyncEvent.LocalDataLoaded]: ApplicationEvent.LocalDataLoaded,
[SyncEvent.MajorDataChange]: ApplicationEvent.MajorDataChange,
[SyncEvent.LocalDataIncrementalLoad]: ApplicationEvent.LocalDataIncrementalLoad,
[SyncEvent.StatusChanged]: ApplicationEvent.SyncStatusChanged,
[SyncEvent.SyncWillBegin]: ApplicationEvent.WillSync,
[SyncEvent.InvalidSession]: ApplicationEvent.InvalidSyncSession,
[SyncEvent.DatabaseReadError]: ApplicationEvent.LocalDatabaseReadError,
[SyncEvent.DatabaseWriteError]: ApplicationEvent.LocalDatabaseWriteError,
[SyncEvent.DownloadFirstSyncCompleted]: ApplicationEvent.CompletedInitialSync,
} as any
)[syncEvent]
}

View File

@@ -0,0 +1,34 @@
import { DecryptedItemInterface } from '@standardnotes/models'
import { SNApplication } from './Application'
/** Keeps an item reference up to date with changes */
export class LiveItem<T extends DecryptedItemInterface> {
public item: T
private removeObserver: () => void
constructor(uuid: string, application: SNApplication, onChange?: (item: T) => void) {
this.item = application.items.findSureItem(uuid)
onChange && onChange(this.item)
this.removeObserver = application.streamItems(this.item.content_type, ({ changed, inserted }) => {
const matchingItem = [...changed, ...inserted].find((item) => {
return item.uuid === uuid
})
if (matchingItem) {
this.item = matchingItem as T
onChange && onChange(this.item)
}
})
}
public deinit() {
if (!this.removeObserver) {
console.error('A LiveItem is attempting to be deinited more than once.')
} else {
this.removeObserver()
;(this.removeObserver as unknown) = undefined
}
}
}

View File

@@ -0,0 +1,16 @@
import { ApplicationOptionsWhichHaveDefaults } from './Defaults'
import {
ApplicationDisplayOptions,
ApplicationOptionalConfiguratioOptions,
ApplicationSyncOptions,
} from './OptionalOptions'
import { RequiredApplicationOptions } from './RequiredOptions'
export type ApplicationConstructorOptions = RequiredApplicationOptions &
Partial<ApplicationSyncOptions & ApplicationDisplayOptions & ApplicationOptionalConfiguratioOptions>
export type FullyResolvedApplicationOptions = RequiredApplicationOptions &
ApplicationSyncOptions &
ApplicationDisplayOptions &
ApplicationOptionalConfiguratioOptions &
ApplicationOptionsWhichHaveDefaults

View File

@@ -0,0 +1,11 @@
import { ApplicationDisplayOptions, ApplicationSyncOptions } from './OptionalOptions'
export interface ApplicationOptionsWhichHaveDefaults {
loadBatchSize: ApplicationSyncOptions['loadBatchSize']
supportsFileNavigation: ApplicationDisplayOptions['supportsFileNavigation']
}
export const ApplicationOptionsDefaults: ApplicationOptionsWhichHaveDefaults = {
loadBatchSize: 700,
supportsFileNavigation: false,
}

View File

@@ -0,0 +1,25 @@
export interface ApplicationSyncOptions {
/**
* The size of the item batch to decrypt and render upon application load.
*/
loadBatchSize: number
}
export interface ApplicationDisplayOptions {
supportsFileNavigation: boolean
}
export interface ApplicationOptionalConfiguratioOptions {
/**
* Gives consumers the ability to provide their own custom
* subclass for a service. swapClasses should be an array of key/value pairs
* consisting of keys 'swap' and 'with'. 'swap' is the base class you wish to replace,
* and 'with' is the custom subclass to use.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
swapClasses?: { swap: any; with: any }[]
/**
* URL for WebSocket providing permissions and roles information.
*/
webSocketUrl?: string
}

View File

@@ -0,0 +1,42 @@
import { ApplicationIdentifier } from '@standardnotes/common'
import { AlertService, DeviceInterface, Environment, Platform } from '@standardnotes/services'
import { PureCryptoInterface } from '@standardnotes/sncrypto-common'
export interface RequiredApplicationOptions {
/**
* The Environment that identifies your application.
*/
environment: Environment
/**
* The Platform that identifies your application.
*/
platform: Platform
/**
* The device interface that provides platform specific
* utilities that are used to read/write raw values from/to the database or value storage.
*/
deviceInterface: DeviceInterface
/**
* The platform-dependent implementation of SNPureCrypto to use.
* Web uses SNWebCrypto, mobile uses SNReactNativeCrypto.
*/
crypto: PureCryptoInterface
/**
* The platform-dependent implementation of alert service.
*/
alertService: AlertService
/**
* A unique persistent identifier to namespace storage and other
* persistent properties. For an ephemeral runtime identifier, use ephemeralIdentifier.
*/
identifier: ApplicationIdentifier
/**
* Default host to use in ApiService.
*/
defaultHost: string
/**
* Version of client application.
*/
appVersion: string
}

View File

@@ -0,0 +1,55 @@
import { Environment, Platform } from '@standardnotes/services'
export function platformFromString(string: string) {
const map: Record<string, Platform> = {
'mac-web': Platform.MacWeb,
'mac-desktop': Platform.MacDesktop,
'linux-web': Platform.LinuxWeb,
'linux-desktop': Platform.LinuxDesktop,
'windows-web': Platform.WindowsWeb,
'windows-desktop': Platform.WindowsDesktop,
ios: Platform.Ios,
android: Platform.Android,
}
return map[string]
}
export function platformToString(platform: Platform) {
const map = {
[Platform.MacWeb]: 'mac-web',
[Platform.MacDesktop]: 'mac-desktop',
[Platform.LinuxWeb]: 'linux-web',
[Platform.LinuxDesktop]: 'linux-desktop',
[Platform.WindowsWeb]: 'windows-web',
[Platform.WindowsDesktop]: 'windows-desktop',
[Platform.Ios]: 'ios',
[Platform.Android]: 'android',
}
return map[platform]
}
export function environmentFromString(string: string) {
const map: Record<string, Environment> = {
web: Environment.Web,
desktop: Environment.Desktop,
mobile: Environment.Mobile,
}
return map[string]
}
export function environmentToString(environment: Environment) {
const map = {
[Environment.Web]: 'web',
[Environment.Desktop]: 'desktop',
[Environment.Mobile]: 'mobile',
}
return map[environment]
}
export function isEnvironmentWebOrDesktop(environment: Environment) {
return environment === Environment.Web || environment === Environment.Desktop
}
export function isEnvironmentMobile(environment: Environment) {
return environment === Environment.Mobile
}

View File

@@ -0,0 +1,4 @@
export * from './Application'
export * from './Event'
export * from './LiveItem'
export * from './Platforms'