feat: add snjs package
This commit is contained in:
151
packages/snjs/lib/Application/Application.spec.ts
Normal file
151
packages/snjs/lib/Application/Application.spec.ts
Normal 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: [] },
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
1568
packages/snjs/lib/Application/Application.ts
Normal file
1568
packages/snjs/lib/Application/Application.ts
Normal file
File diff suppressed because it is too large
Load Diff
90
packages/snjs/lib/Application/Event.ts
Normal file
90
packages/snjs/lib/Application/Event.ts
Normal 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]
|
||||
}
|
||||
34
packages/snjs/lib/Application/LiveItem.ts
Normal file
34
packages/snjs/lib/Application/LiveItem.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/snjs/lib/Application/Options/ApplicationOptions.ts
Normal file
16
packages/snjs/lib/Application/Options/ApplicationOptions.ts
Normal 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
|
||||
11
packages/snjs/lib/Application/Options/Defaults.ts
Normal file
11
packages/snjs/lib/Application/Options/Defaults.ts
Normal 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,
|
||||
}
|
||||
25
packages/snjs/lib/Application/Options/OptionalOptions.ts
Normal file
25
packages/snjs/lib/Application/Options/OptionalOptions.ts
Normal 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
|
||||
}
|
||||
42
packages/snjs/lib/Application/Options/RequiredOptions.ts
Normal file
42
packages/snjs/lib/Application/Options/RequiredOptions.ts
Normal 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
|
||||
}
|
||||
55
packages/snjs/lib/Application/Platforms.ts
Normal file
55
packages/snjs/lib/Application/Platforms.ts
Normal 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
|
||||
}
|
||||
4
packages/snjs/lib/Application/index.ts
Normal file
4
packages/snjs/lib/Application/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './Application'
|
||||
export * from './Event'
|
||||
export * from './LiveItem'
|
||||
export * from './Platforms'
|
||||
Reference in New Issue
Block a user