feat: add snjs package
This commit is contained in:
316
packages/snjs/mocha/lib/AppContext.js
Normal file
316
packages/snjs/mocha/lib/AppContext.js
Normal file
@@ -0,0 +1,316 @@
|
||||
import FakeWebCrypto from './fake_web_crypto.js'
|
||||
import * as Applications from './Applications.js'
|
||||
import * as Utils from './Utils.js'
|
||||
import { createNotePayload } from './Items.js'
|
||||
|
||||
UuidGenerator.SetGenerator(new FakeWebCrypto().generateUUID)
|
||||
|
||||
const MaximumSyncOptions = {
|
||||
checkIntegrity: true,
|
||||
awaitAll: true,
|
||||
}
|
||||
|
||||
export class AppContext {
|
||||
constructor({ identifier, crypto, email, password, passcode } = {}) {
|
||||
if (!identifier) {
|
||||
identifier = `${Math.random()}`
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
email = UuidGenerator.GenerateUuid()
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
password = UuidGenerator.GenerateUuid()
|
||||
}
|
||||
|
||||
if (!passcode) {
|
||||
passcode = 'mypasscode'
|
||||
}
|
||||
|
||||
this.identifier = identifier
|
||||
this.crypto = crypto
|
||||
this.email = email
|
||||
this.password = password
|
||||
this.passcode = passcode
|
||||
}
|
||||
|
||||
enableLogging() {
|
||||
const syncService = this.application.syncService
|
||||
const payloadManager = this.application.payloadManager
|
||||
|
||||
syncService.getServiceName = () => {
|
||||
return `${this.identifier}—SyncService`
|
||||
}
|
||||
payloadManager.getServiceName = () => {
|
||||
return `${this.identifier}-PayloadManager`
|
||||
}
|
||||
|
||||
syncService.loggingEnabled = true
|
||||
payloadManager.loggingEnabled = true
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.application = await Applications.createApplication(
|
||||
this.identifier,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
this.crypto || new FakeWebCrypto(),
|
||||
)
|
||||
}
|
||||
|
||||
ignoreChallenges() {
|
||||
this.ignoringChallenges = true
|
||||
}
|
||||
|
||||
resumeChallenges() {
|
||||
this.ignoringChallenges = false
|
||||
}
|
||||
|
||||
disableIntegrityAutoHeal() {
|
||||
this.application.syncService.emitOutOfSyncRemotePayloads = () => {
|
||||
console.warn('Integrity self-healing is disabled for this test')
|
||||
}
|
||||
}
|
||||
|
||||
disableKeyRecovery() {
|
||||
this.application.keyRecoveryService.beginKeyRecovery = () => {
|
||||
console.warn('Key recovery is disabled for this test')
|
||||
}
|
||||
}
|
||||
|
||||
handleChallenge = (challenge) => {
|
||||
if (this.ignoringChallenges) {
|
||||
this.application.challengeService.cancelChallenge(challenge)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const responses = []
|
||||
|
||||
const accountPassword = this.passwordToUseForAccountPasswordChallenge || this.password
|
||||
|
||||
for (const prompt of challenge.prompts) {
|
||||
if (prompt.validation === ChallengeValidation.LocalPasscode) {
|
||||
responses.push(CreateChallengeValue(prompt, this.passcode))
|
||||
} else if (prompt.validation === ChallengeValidation.AccountPassword) {
|
||||
responses.push(CreateChallengeValue(prompt, accountPassword))
|
||||
} else if (prompt.validation === ChallengeValidation.ProtectionSessionDuration) {
|
||||
responses.push(CreateChallengeValue(prompt, 0))
|
||||
} else if (prompt.placeholder === 'Email') {
|
||||
responses.push(CreateChallengeValue(prompt, this.email))
|
||||
} else if (prompt.placeholder === 'Password') {
|
||||
responses.push(CreateChallengeValue(prompt, accountPassword))
|
||||
} else if (challenge.heading.includes('account password')) {
|
||||
responses.push(CreateChallengeValue(prompt, accountPassword))
|
||||
} else {
|
||||
console.log('Unhandled challenge', challenge)
|
||||
throw Error(`Unhandled custom challenge in Factory.createAppContext`)
|
||||
}
|
||||
}
|
||||
|
||||
this.application.submitValuesForChallenge(challenge, responses)
|
||||
}
|
||||
|
||||
signIn() {
|
||||
const strict = false
|
||||
const ephemeral = false
|
||||
const mergeLocal = true
|
||||
const awaitSync = true
|
||||
return this.application.signIn(this.email, this.password, strict, ephemeral, mergeLocal, awaitSync)
|
||||
}
|
||||
|
||||
register() {
|
||||
return this.application.register(this.email, this.password)
|
||||
}
|
||||
|
||||
receiveServerResponse({ retrievedItems }) {
|
||||
const response = new ServerSyncResponse({
|
||||
data: {
|
||||
retrieved_items: retrievedItems,
|
||||
},
|
||||
})
|
||||
|
||||
return this.application.syncService.handleSuccessServerResponse({ payloadsSavedOrSaving: [] }, response)
|
||||
}
|
||||
|
||||
resolveWhenKeyRecovered(uuid) {
|
||||
return new Promise((resolve) => {
|
||||
this.application.keyRecoveryService.addEventObserver((_eventName, keys) => {
|
||||
if (Uuids(keys).includes(uuid)) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async awaitSignInEvent() {
|
||||
return new Promise((resolve) => {
|
||||
this.application.userService.addEventObserver((eventName) => {
|
||||
if (eventName === AccountEvent.SignedInOrRegistered) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async restart() {
|
||||
const id = this.application.identifier
|
||||
await Utils.safeDeinit(this.application)
|
||||
const newApplication = await Applications.createAndInitializeApplication(id)
|
||||
this.application = newApplication
|
||||
return newApplication
|
||||
}
|
||||
|
||||
syncWithIntegrityCheck() {
|
||||
return this.application.sync.sync({ checkIntegrity: true, awaitAll: true })
|
||||
}
|
||||
|
||||
awaitNextSucessfulSync() {
|
||||
return new Promise((resolve) => {
|
||||
const removeObserver = this.application.syncService.addEventObserver((event) => {
|
||||
if (event === SyncEvent.SyncCompletedWithAllItemsUploadedAndDownloaded) {
|
||||
removeObserver()
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
awaitNextSyncEvent(eventName) {
|
||||
return new Promise((resolve) => {
|
||||
const removeObserver = this.application.syncService.addEventObserver((event, data) => {
|
||||
if (event === eventName) {
|
||||
removeObserver()
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async launch({ awaitDatabaseLoad = true, receiveChallenge } = { awaitDatabaseLoad: true }) {
|
||||
await this.application.prepareForLaunch({
|
||||
receiveChallenge: receiveChallenge || this.handleChallenge,
|
||||
})
|
||||
await this.application.launch(awaitDatabaseLoad)
|
||||
}
|
||||
|
||||
async deinit() {
|
||||
await Utils.safeDeinit(this.application)
|
||||
}
|
||||
|
||||
async sync(options) {
|
||||
await this.application.sync.sync(options || { awaitAll: true })
|
||||
}
|
||||
|
||||
async maximumSync() {
|
||||
await this.sync(MaximumSyncOptions)
|
||||
}
|
||||
|
||||
async changePassword(newPassword) {
|
||||
await this.application.changePassword(this.password, newPassword)
|
||||
|
||||
this.password = newPassword
|
||||
}
|
||||
|
||||
findItem(uuid) {
|
||||
return this.application.items.findItem(uuid)
|
||||
}
|
||||
|
||||
findPayload(uuid) {
|
||||
return this.application.payloadManager.findPayload(uuid)
|
||||
}
|
||||
|
||||
get itemsKeys() {
|
||||
return this.application.items.getDisplayableItemsKeys()
|
||||
}
|
||||
|
||||
disableSyncingOfItems(uuids) {
|
||||
const originalImpl = this.application.items.getDirtyItems
|
||||
|
||||
this.application.items.getDirtyItems = function () {
|
||||
const result = originalImpl.apply(this)
|
||||
|
||||
return result.filter((i) => !uuids.includes(i.uuid))
|
||||
}
|
||||
}
|
||||
|
||||
disableKeyRecoveryServerSignIn() {
|
||||
this.application.keyRecoveryService.performServerSignIn = () => {
|
||||
console.warn('application.keyRecoveryService.performServerSignIn has been stubbed with an empty implementation')
|
||||
}
|
||||
}
|
||||
|
||||
preventKeyRecoveryOfKeys(ids) {
|
||||
const originalImpl = this.application.keyRecoveryService.handleUndecryptableItemsKeys
|
||||
|
||||
this.application.keyRecoveryService.handleUndecryptableItemsKeys = function (keys) {
|
||||
const filtered = keys.filter((k) => !ids.includes(k.uuid))
|
||||
|
||||
originalImpl.apply(this, [filtered])
|
||||
}
|
||||
}
|
||||
|
||||
respondToAccountPasswordChallengeWith(password) {
|
||||
this.passwordToUseForAccountPasswordChallenge = password
|
||||
}
|
||||
|
||||
spyOnChangedItems(callback) {
|
||||
this.application.items.addObserver(ContentType.Any, ({ changed, unerrored }) => {
|
||||
callback([...changed, ...unerrored])
|
||||
})
|
||||
}
|
||||
|
||||
async createSyncedNote(title, text) {
|
||||
const payload = createNotePayload(title, text)
|
||||
const item = await this.application.items.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await this.application.items.setItemDirty(item)
|
||||
await this.application.syncService.sync(MaximumSyncOptions)
|
||||
const note = this.application.items.findItem(payload.uuid)
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
async deleteItemAndSync(item) {
|
||||
await this.application.mutator.deleteItem(item)
|
||||
}
|
||||
|
||||
async changeNoteTitle(note, title) {
|
||||
return this.application.items.changeNote(note, (mutator) => {
|
||||
mutator.title = title
|
||||
})
|
||||
}
|
||||
|
||||
async changeNoteTitleAndSync(note, title) {
|
||||
await this.changeNoteTitle(note, title)
|
||||
await this.sync()
|
||||
|
||||
return this.findItem(note.uuid)
|
||||
}
|
||||
|
||||
findNoteByTitle(title) {
|
||||
return this.application.items.getDisplayableNotes().find((note) => note.title === title)
|
||||
}
|
||||
|
||||
get noteCount() {
|
||||
return this.application.items.getDisplayableNotes().length
|
||||
}
|
||||
|
||||
async createConflictedNotes(otherContext) {
|
||||
const note = await this.createSyncedNote()
|
||||
|
||||
await otherContext.sync()
|
||||
|
||||
await this.changeNoteTitleAndSync(note, 'title-1')
|
||||
|
||||
await otherContext.changeNoteTitleAndSync(note, 'title-2')
|
||||
|
||||
await this.sync()
|
||||
|
||||
return {
|
||||
original: note,
|
||||
conflict: this.findNoteByTitle('title-2'),
|
||||
}
|
||||
}
|
||||
}
|
||||
71
packages/snjs/mocha/lib/Applications.js
Normal file
71
packages/snjs/mocha/lib/Applications.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import WebDeviceInterface from './web_device_interface.js'
|
||||
import FakeWebCrypto from './fake_web_crypto.js'
|
||||
import * as Defaults from './Defaults.js'
|
||||
|
||||
export function createApplicationWithOptions({ identifier, environment, platform, host, crypto, device }) {
|
||||
if (!device) {
|
||||
device = new WebDeviceInterface()
|
||||
device.environment = environment
|
||||
}
|
||||
|
||||
return new SNApplication({
|
||||
environment: environment || Environment.Web,
|
||||
platform: platform || Platform.MacWeb,
|
||||
deviceInterface: device,
|
||||
crypto: crypto || new FakeWebCrypto(),
|
||||
alertService: {
|
||||
confirm: async () => true,
|
||||
alert: async () => {},
|
||||
blockingDialog: () => () => {},
|
||||
},
|
||||
identifier: identifier || `${Math.random()}`,
|
||||
defaultHost: host || Defaults.getDefaultHost(),
|
||||
appVersion: Defaults.getAppVersion(),
|
||||
webSocketUrl: Defaults.getDefaultWebSocketUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
export function createApplication(identifier, environment, platform, host, crypto) {
|
||||
return createApplicationWithOptions({ identifier, environment, platform, host, crypto })
|
||||
}
|
||||
|
||||
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
|
||||
return createApplication(identifier, environment, platform, host, new FakeWebCrypto())
|
||||
}
|
||||
|
||||
export function createApplicationWithRealCrypto(identifier, environment, platform, host) {
|
||||
return createApplication(identifier, environment, platform, host, new SNWebCrypto())
|
||||
}
|
||||
|
||||
export async function createAppWithRandNamespace(environment, platform) {
|
||||
const namespace = Math.random().toString(36).substring(2, 15)
|
||||
return createApplication(namespace, environment, platform)
|
||||
}
|
||||
|
||||
export async function createInitAppWithFakeCrypto(environment, platform) {
|
||||
const namespace = Math.random().toString(36).substring(2, 15)
|
||||
return createAndInitializeApplication(namespace, environment, platform, undefined, new FakeWebCrypto())
|
||||
}
|
||||
|
||||
export async function createInitAppWithRealCrypto(environment, platform) {
|
||||
const namespace = Math.random().toString(36).substring(2, 15)
|
||||
return createAndInitializeApplication(namespace, environment, platform, undefined, new SNWebCrypto())
|
||||
}
|
||||
|
||||
export async function createAndInitializeApplication(namespace, environment, platform, host, crypto) {
|
||||
const application = createApplication(namespace, environment, platform, host, crypto)
|
||||
await initializeApplication(application)
|
||||
return application
|
||||
}
|
||||
|
||||
export async function initializeApplication(application) {
|
||||
await application.prepareForLaunch({
|
||||
receiveChallenge: (challenge) => {
|
||||
console.warn('Factory received potentially unhandled challenge', challenge)
|
||||
if (challenge.reason !== ChallengeReason.Custom) {
|
||||
throw Error("Factory application shouldn't have challenges")
|
||||
}
|
||||
},
|
||||
})
|
||||
await application.launch(true)
|
||||
}
|
||||
15
packages/snjs/mocha/lib/Defaults.js
Normal file
15
packages/snjs/mocha/lib/Defaults.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export function getDefaultHost() {
|
||||
return 'http://localhost:3123'
|
||||
}
|
||||
|
||||
export function getDefaultMockedEventServiceUrl() {
|
||||
return 'http://localhost:3124'
|
||||
}
|
||||
|
||||
export function getDefaultWebSocketUrl() {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getAppVersion() {
|
||||
return '1.2.3'
|
||||
}
|
||||
72
packages/snjs/mocha/lib/Items.js
Normal file
72
packages/snjs/mocha/lib/Items.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import * as Utils from './Utils.js'
|
||||
|
||||
const MaximumSyncOptions = {
|
||||
checkIntegrity: true,
|
||||
awaitAll: true,
|
||||
}
|
||||
|
||||
export function createItemParams(contentType) {
|
||||
const params = {
|
||||
uuid: Utils.generateUuid(),
|
||||
content_type: contentType,
|
||||
content: {
|
||||
title: 'hello',
|
||||
text: 'world',
|
||||
},
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export function createNoteParams({ title, text, dirty = true } = {}) {
|
||||
const params = {
|
||||
uuid: Utils.generateUuid(),
|
||||
content_type: ContentType.Note,
|
||||
dirty: dirty,
|
||||
dirtyIndex: dirty ? getIncrementedDirtyIndex() : undefined,
|
||||
content: FillItemContent({
|
||||
title: title || 'hello',
|
||||
text: text || 'world',
|
||||
}),
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export function createTagParams({ title, dirty = true, uuid = undefined } = {}) {
|
||||
const params = {
|
||||
uuid: uuid || Utils.generateUuid(),
|
||||
content_type: ContentType.Tag,
|
||||
dirty: dirty,
|
||||
dirtyIndex: dirty ? getIncrementedDirtyIndex() : undefined,
|
||||
content: FillItemContent({
|
||||
title: title || 'thoughts',
|
||||
}),
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
export function createRelatedNoteTagPairPayload({ noteTitle, noteText, tagTitle, dirty = true } = {}) {
|
||||
const noteParams = createNoteParams({
|
||||
title: noteTitle,
|
||||
text: noteText,
|
||||
dirty,
|
||||
})
|
||||
const tagParams = createTagParams({ title: tagTitle, dirty })
|
||||
tagParams.content.references = [
|
||||
{
|
||||
uuid: noteParams.uuid,
|
||||
content_type: noteParams.content_type,
|
||||
},
|
||||
]
|
||||
noteParams.content.references = []
|
||||
return [new DecryptedPayload(noteParams), new DecryptedPayload(tagParams)]
|
||||
}
|
||||
|
||||
export async function createSyncedNoteWithTag(application) {
|
||||
const payloads = createRelatedNoteTagPairPayload()
|
||||
await application.itemManager.emitItemsFromPayloads(payloads)
|
||||
return application.sync.sync(MaximumSyncOptions)
|
||||
}
|
||||
|
||||
export function createNotePayload(title, text = undefined, dirty = true) {
|
||||
return new DecryptedPayload(createNoteParams({ title, text, dirty }))
|
||||
}
|
||||
34
packages/snjs/mocha/lib/Utils.js
Normal file
34
packages/snjs/mocha/lib/Utils.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import FakeWebCrypto from './fake_web_crypto.js'
|
||||
|
||||
export async function safeDeinit(application) {
|
||||
if (application.dealloced) {
|
||||
console.warn(
|
||||
'Attempting to deinit already deinited application. Check the test case to find where you are double deiniting.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
await application.diskStorageService.awaitPersist()
|
||||
|
||||
/** Limit waiting to 1s */
|
||||
await Promise.race([sleep(1), application.syncService?.awaitCurrentSyncs()])
|
||||
|
||||
await application.prepareForDeinit()
|
||||
|
||||
application.deinit(DeinitMode.Soft, DeinitSource.SignOut)
|
||||
}
|
||||
|
||||
export async function sleep(seconds) {
|
||||
console.warn(`Test sleeping for ${seconds}s`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(function () {
|
||||
resolve()
|
||||
}, seconds * 1000)
|
||||
})
|
||||
}
|
||||
|
||||
export function generateUuid() {
|
||||
const crypto = new FakeWebCrypto()
|
||||
return crypto.generateUUID()
|
||||
}
|
||||
492
packages/snjs/mocha/lib/factory.js
Normal file
492
packages/snjs/mocha/lib/factory.js
Normal file
@@ -0,0 +1,492 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-undef */
|
||||
import FakeWebCrypto from './fake_web_crypto.js'
|
||||
import { AppContext } from './AppContext.js'
|
||||
import * as Applications from './Applications.js'
|
||||
import * as Defaults from './Defaults.js'
|
||||
import * as Utils from './Utils.js'
|
||||
import { createItemParams, createNoteParams, createTagParams } from './Items.js'
|
||||
|
||||
export const TenSecondTimeout = 10_000
|
||||
export const TwentySecondTimeout = 20_000
|
||||
export const ThirtySecondTimeout = 30_000
|
||||
|
||||
export const syncOptions = {
|
||||
checkIntegrity: true,
|
||||
awaitAll: true,
|
||||
}
|
||||
|
||||
export async function createAndInitSimpleAppContext(
|
||||
{ registerUser, environment } = {
|
||||
registerUser: false,
|
||||
environment: Environment.Web,
|
||||
},
|
||||
) {
|
||||
const application = await createInitAppWithFakeCrypto(environment)
|
||||
const email = UuidGenerator.GenerateUuid()
|
||||
const password = UuidGenerator.GenerateUuid()
|
||||
const newPassword = randomString()
|
||||
|
||||
if (registerUser) {
|
||||
await registerUserToApplication({
|
||||
application,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
application,
|
||||
email,
|
||||
password,
|
||||
newPassword,
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAppContextWithFakeCrypto(identifier, email, password) {
|
||||
return createAppContext({ identifier, crypto: new FakeWebCrypto(), email, password })
|
||||
}
|
||||
|
||||
export async function createAppContextWithRealCrypto(identifier) {
|
||||
return createAppContext({ identifier, crypto: new SNWebCrypto() })
|
||||
}
|
||||
|
||||
export async function createAppContext({ identifier, crypto, email, password } = {}) {
|
||||
const context = new AppContext({ identifier, crypto, email, password })
|
||||
await context.initialize()
|
||||
return context
|
||||
}
|
||||
|
||||
export function disableIntegrityAutoHeal(application) {
|
||||
application.syncService.emitOutOfSyncRemotePayloads = () => {
|
||||
console.warn('Integrity self-healing is disabled for this test')
|
||||
}
|
||||
}
|
||||
|
||||
export async function safeDeinit(application) {
|
||||
return Utils.safeDeinit(application)
|
||||
}
|
||||
|
||||
export function getDefaultHost() {
|
||||
return Defaults.getDefaultHost()
|
||||
}
|
||||
|
||||
export async function publishMockedEvent(eventType, eventPayload) {
|
||||
await fetch(`${Defaults.getDefaultMockedEventServiceUrl()}/events`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
eventType,
|
||||
eventPayload,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
export function createApplicationWithFakeCrypto(identifier, environment, platform, host) {
|
||||
return Applications.createApplicationWithFakeCrypto(identifier, environment, platform, host)
|
||||
}
|
||||
|
||||
export function createApplicationWithRealCrypto(identifier, environment, platform, host) {
|
||||
return Applications.createApplicationWithRealCrypto(identifier, environment, platform, host)
|
||||
}
|
||||
|
||||
export async function createAppWithRandNamespace(environment, platform) {
|
||||
return Applications.createAppWithRandNamespace(environment, platform)
|
||||
}
|
||||
|
||||
export async function createInitAppWithFakeCrypto(environment, platform) {
|
||||
return Applications.createInitAppWithFakeCrypto(environment, platform)
|
||||
}
|
||||
|
||||
export async function createInitAppWithFakeCryptoWithOptions({ environment, platform, identifier }) {
|
||||
const application = Applications.createApplicationWithOptions({ identifier, environment, platform })
|
||||
await Applications.initializeApplication(application)
|
||||
return application
|
||||
}
|
||||
|
||||
export async function createInitAppWithRealCrypto(environment, platform) {
|
||||
return Applications.createInitAppWithRealCrypto(environment, platform)
|
||||
}
|
||||
|
||||
export async function createAndInitializeApplication(namespace, environment, platform, host, crypto) {
|
||||
return Applications.createAndInitializeApplication(namespace, environment, platform, host, crypto)
|
||||
}
|
||||
|
||||
export async function initializeApplication(application) {
|
||||
return Applications.initializeApplication(application)
|
||||
}
|
||||
|
||||
export function registerUserToApplication({ application, email, password, ephemeral, mergeLocal = true }) {
|
||||
if (!email) email = Utils.generateUuid()
|
||||
if (!password) password = Utils.generateUuid()
|
||||
return application.register(email, password, ephemeral, mergeLocal)
|
||||
}
|
||||
|
||||
export async function setOldVersionPasscode({ application, passcode, version }) {
|
||||
const identifier = await application.protocolService.crypto.generateUUID()
|
||||
const operator = application.protocolService.operatorManager.operatorForVersion(version)
|
||||
const key = await operator.createRootKey(identifier, passcode, KeyParamsOrigination.PasscodeCreate)
|
||||
await application.protocolService.setNewRootKeyWrapper(key)
|
||||
await application.userService.rewriteItemsKeys()
|
||||
await application.syncService.sync(syncOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Using application.register will always use latest version of protocol.
|
||||
* To use older version, use this method.
|
||||
*/
|
||||
export async function registerOldUser({ application, email, password, version }) {
|
||||
if (!email) email = Utils.generateUuid()
|
||||
if (!password) password = Utils.generateUuid()
|
||||
const operator = application.protocolService.operatorManager.operatorForVersion(version)
|
||||
const accountKey = await operator.createRootKey(email, password, KeyParamsOrigination.Registration)
|
||||
|
||||
const response = await application.userApiService.register(email, accountKey.serverPassword, accountKey.keyParams)
|
||||
/** Mark all existing items as dirty. */
|
||||
await application.itemManager.changeItems(application.itemManager.items, (m) => {
|
||||
m.dirty = true
|
||||
})
|
||||
await application.sessionManager.handleSuccessAuthResponse(response, accountKey)
|
||||
application.notifyEvent(ApplicationEvent.SignedIn)
|
||||
await application.syncService.sync({
|
||||
mode: SyncMode.DownloadFirst,
|
||||
...syncOptions,
|
||||
})
|
||||
await application.protocolService.decryptErroredPayloads()
|
||||
}
|
||||
|
||||
export function createStorageItemPayload(contentType) {
|
||||
return new DecryptedPayload(createItemParams(contentType))
|
||||
}
|
||||
|
||||
export function createNotePayload(title, text = undefined, dirty = true) {
|
||||
return new DecryptedPayload(createNoteParams({ title, text, dirty }))
|
||||
}
|
||||
|
||||
export function createNote(title, text = undefined, dirty = true) {
|
||||
return new SNNote(new DecryptedPayload(createNoteParams({ title, text, dirty })))
|
||||
}
|
||||
|
||||
export function createStorageItemTagPayload(tagParams = {}) {
|
||||
return new DecryptedPayload(createTagParams(tagParams))
|
||||
}
|
||||
|
||||
export function itemToStoragePayload(item) {
|
||||
return new DecryptedPayload(item)
|
||||
}
|
||||
|
||||
export function createMappedNote(application, title, text, dirty = true) {
|
||||
const payload = createNotePayload(title, text, dirty)
|
||||
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
export async function createMappedTag(application, tagParams = {}) {
|
||||
const payload = createStorageItemTagPayload(tagParams)
|
||||
return application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
}
|
||||
|
||||
export async function createSyncedNote(application, title, text) {
|
||||
const payload = createNotePayload(title, text)
|
||||
const item = await application.itemManager.emitItemFromPayload(payload, PayloadEmitSource.LocalChanged)
|
||||
await application.itemManager.setItemDirty(item)
|
||||
await application.syncService.sync(syncOptions)
|
||||
const note = application.items.findItem(payload.uuid)
|
||||
return note
|
||||
}
|
||||
|
||||
export async function getStoragePayloadsOfType(application, type) {
|
||||
const rawPayloads = await application.diskStorageService.getAllRawPayloads()
|
||||
return rawPayloads
|
||||
.filter((rp) => rp.content_type === type)
|
||||
.map((rp) => {
|
||||
return new CreatePayload(rp)
|
||||
})
|
||||
}
|
||||
|
||||
export async function createManyMappedNotes(application, count) {
|
||||
const createdNotes = []
|
||||
for (let i = 0; i < count; i++) {
|
||||
const note = await createMappedNote(application)
|
||||
await application.itemManager.setItemDirty(note)
|
||||
createdNotes.push(note)
|
||||
}
|
||||
return createdNotes
|
||||
}
|
||||
|
||||
export async function loginToApplication({
|
||||
application,
|
||||
email,
|
||||
password,
|
||||
ephemeral,
|
||||
strict = false,
|
||||
mergeLocal = true,
|
||||
awaitSync = true,
|
||||
}) {
|
||||
return application.signIn(email, password, strict, ephemeral, mergeLocal, awaitSync)
|
||||
}
|
||||
|
||||
export async function awaitFunctionInvokation(object, functionName) {
|
||||
return new Promise((resolve) => {
|
||||
const original = object[functionName]
|
||||
object[functionName] = async function () {
|
||||
const result = original.apply(this, arguments)
|
||||
resolve(result)
|
||||
return result
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Signing out of an application deinits it.
|
||||
* A new one must be created.
|
||||
*/
|
||||
export async function signOutApplicationAndReturnNew(application) {
|
||||
const isRealCrypto = application.crypto instanceof SNWebCrypto
|
||||
await application.user.signOut()
|
||||
if (isRealCrypto) {
|
||||
return createInitAppWithRealCrypto()
|
||||
} else {
|
||||
return createInitAppWithFakeCrypto()
|
||||
}
|
||||
}
|
||||
|
||||
export async function signOutAndBackIn(application, email, password) {
|
||||
const isRealCrypto = application.crypto instanceof SNWebCrypto
|
||||
await application.user.signOut()
|
||||
const newApplication = isRealCrypto ? await createInitAppWithRealCrypto() : await createInitAppWithFakeCrypto()
|
||||
await this.loginToApplication({
|
||||
application: newApplication,
|
||||
email,
|
||||
password,
|
||||
})
|
||||
return newApplication
|
||||
}
|
||||
|
||||
export async function restartApplication(application) {
|
||||
const id = application.identifier
|
||||
await safeDeinit(application)
|
||||
const newApplication = await createAndInitializeApplication(id)
|
||||
return newApplication
|
||||
}
|
||||
|
||||
export async function storagePayloadCount(application) {
|
||||
const payloads = await application.diskStorageService.getAllRawPayloads()
|
||||
return payloads.length
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of seconds between changes before a server creates a new revision.
|
||||
* Controlled via docker/syncing-server-js.env
|
||||
*/
|
||||
export const ServerRevisionFrequency = 1.1
|
||||
|
||||
export function yesterday() {
|
||||
return new Date(new Date().setDate(new Date().getDate() - 1))
|
||||
}
|
||||
|
||||
export function dateToMicroseconds(date) {
|
||||
return date.getTime() * 1_000
|
||||
}
|
||||
|
||||
export function tomorrow() {
|
||||
return new Date(new Date().setDate(new Date().getDate() + 1))
|
||||
}
|
||||
|
||||
export async function sleep(seconds) {
|
||||
return Utils.sleep(seconds)
|
||||
}
|
||||
|
||||
export function shuffleArray(a) {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[a[i], a[j]] = [a[j], a[i]]
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
export function randomString(length = 10) {
|
||||
let result = ''
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const charactersLength = characters.length
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function generateUuidish() {
|
||||
return this.randomString(32)
|
||||
}
|
||||
|
||||
export function randomArrayValue(array) {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export async function expectThrowsAsync(method, errorMessage) {
|
||||
let error = null
|
||||
try {
|
||||
await method()
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
const expect = chai.expect
|
||||
expect(error).to.be.an('Error')
|
||||
if (errorMessage) {
|
||||
expect(error.message)
|
||||
.to.be.a('string')
|
||||
.and.satisfy((msg) => msg.startsWith(errorMessage))
|
||||
}
|
||||
}
|
||||
|
||||
export function ignoreChallenges(application) {
|
||||
application.setLaunchCallback({
|
||||
receiveChallenge() {
|
||||
/** no-op */
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function handlePasswordChallenges(application, password) {
|
||||
application.setLaunchCallback({
|
||||
receiveChallenge: (challenge) => {
|
||||
const values = challenge.prompts.map((prompt) =>
|
||||
CreateChallengeValue(
|
||||
prompt,
|
||||
prompt.validation === ChallengeValidation.ProtectionSessionDuration
|
||||
? UnprotectedAccessSecondsDuration.OneMinute
|
||||
: password,
|
||||
),
|
||||
)
|
||||
application.submitValuesForChallenge(challenge, values)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function createTags(application, hierarchy, parent = undefined, resultAccumulator = undefined) {
|
||||
const result = resultAccumulator || {}
|
||||
|
||||
const promises = Object.entries(hierarchy).map(async ([key, value]) => {
|
||||
let tag = await application.mutator.findOrCreateTag(key)
|
||||
|
||||
result[key] = tag
|
||||
|
||||
if (parent) {
|
||||
await application.mutator.setTagParent(parent, tag)
|
||||
}
|
||||
|
||||
if (value === true) {
|
||||
return
|
||||
}
|
||||
|
||||
await createTags(application, value, tag, result)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function pinNote(application, note) {
|
||||
return application.mutator.changeItem(note, (mutator) => {
|
||||
mutator.pinned = true
|
||||
})
|
||||
}
|
||||
|
||||
export async function insertItemWithOverride(application, contentType, content, needsSync = false, errorDecrypting) {
|
||||
const item = await application.itemManager.createItem(contentType, content, needsSync)
|
||||
|
||||
if (errorDecrypting) {
|
||||
const encrypted = new EncryptedPayload({
|
||||
...item.payload.ejected(),
|
||||
content: '004:...',
|
||||
errorDecrypting,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(encrypted)
|
||||
} else {
|
||||
const decrypted = new DecryptedPayload({
|
||||
...item.payload.ejected(),
|
||||
})
|
||||
await application.itemManager.emitItemFromPayload(decrypted)
|
||||
}
|
||||
|
||||
return application.itemManager.findAnyItem(item.uuid)
|
||||
}
|
||||
|
||||
export async function alternateUuidForItem(application, uuid) {
|
||||
const item = application.itemManager.findItem(uuid)
|
||||
const payload = new DecryptedPayload(item)
|
||||
const results = await PayloadsByAlternatingUuid(payload, application.payloadManager.getMasterCollection())
|
||||
await application.payloadManager.emitPayloads(results, PayloadEmitSource.LocalChanged)
|
||||
await application.syncService.persistPayloads(results)
|
||||
return application.itemManager.findItem(results[0].uuid)
|
||||
}
|
||||
|
||||
export async function markDirtyAndSyncItem(application, itemToLookupUuidFor) {
|
||||
const item = application.itemManager.findItem(itemToLookupUuidFor.uuid)
|
||||
if (!item) {
|
||||
throw Error('Attempting to save non-inserted item')
|
||||
}
|
||||
if (!item.dirty) {
|
||||
await application.itemManager.changeItem(item, undefined, MutationType.NoUpdateUserTimestamps)
|
||||
}
|
||||
await application.sync.sync()
|
||||
}
|
||||
|
||||
export async function changePayloadTimeStampAndSync(application, payload, timestamp, contentOverride, syncOptions) {
|
||||
await changePayloadTimeStamp(application, payload, timestamp, contentOverride)
|
||||
|
||||
await application.sync.sync(syncOptions)
|
||||
|
||||
return application.itemManager.findAnyItem(payload.uuid)
|
||||
}
|
||||
|
||||
export async function changePayloadTimeStamp(application, payload, timestamp, contentOverride) {
|
||||
payload = application.payloadManager.collection.find(payload.uuid)
|
||||
const changedPayload = new DecryptedPayload({
|
||||
...payload,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
content: {
|
||||
...payload.content,
|
||||
...contentOverride,
|
||||
},
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
|
||||
return application.itemManager.findAnyItem(payload.uuid)
|
||||
}
|
||||
|
||||
export async function changePayloadUpdatedAt(application, payload, updatedAt) {
|
||||
const latestPayload = application.payloadManager.collection.find(payload.uuid)
|
||||
const changedPayload = new DecryptedPayload({
|
||||
...latestPayload,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
updated_at: updatedAt,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
|
||||
return application.itemManager.findAnyItem(payload.uuid)
|
||||
}
|
||||
|
||||
export async function changePayloadTimeStampDeleteAndSync(application, payload, timestamp, syncOptions) {
|
||||
payload = application.payloadManager.collection.find(payload.uuid)
|
||||
const changedPayload = new DeletedPayload({
|
||||
...payload,
|
||||
content: undefined,
|
||||
dirty: true,
|
||||
dirtyIndex: getIncrementedDirtyIndex(),
|
||||
deleted: true,
|
||||
updated_at_timestamp: timestamp,
|
||||
})
|
||||
|
||||
await application.itemManager.emitItemFromPayload(changedPayload)
|
||||
await application.sync.sync(syncOptions)
|
||||
}
|
||||
138
packages/snjs/mocha/lib/fake_web_crypto.js
Normal file
138
packages/snjs/mocha/lib/fake_web_crypto.js
Normal file
@@ -0,0 +1,138 @@
|
||||
export default class FakeWebCrypto {
|
||||
constructor() {}
|
||||
|
||||
deinit() {}
|
||||
|
||||
initialize() {
|
||||
return
|
||||
}
|
||||
|
||||
randomString(len) {
|
||||
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
let randomString = ''
|
||||
for (let i = 0; i < len; i++) {
|
||||
const randomPoz = Math.floor(Math.random() * charSet.length)
|
||||
randomString += charSet.substring(randomPoz, randomPoz + 1)
|
||||
}
|
||||
return randomString
|
||||
}
|
||||
|
||||
generateUUIDSync = () => {
|
||||
const globalScope = getGlobalScope()
|
||||
const crypto = globalScope.crypto || globalScope.msCrypto
|
||||
if (crypto) {
|
||||
const buf = new Uint32Array(4)
|
||||
crypto.getRandomValues(buf)
|
||||
let idx = -1
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
idx++
|
||||
const r = (buf[idx >> 3] >> ((idx % 8) * 4)) & 15
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
} else {
|
||||
let d = new Date().getTime()
|
||||
if (globalScope.performance && typeof globalScope.performance.now === 'function') {
|
||||
d += performance.now() // use high-precision timer if available
|
||||
}
|
||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
||||
})
|
||||
return uuid
|
||||
}
|
||||
}
|
||||
|
||||
generateUUID = () => {
|
||||
return this.generateUUIDSync()
|
||||
}
|
||||
|
||||
timingSafeEqual(a, b) {
|
||||
return a === b
|
||||
}
|
||||
|
||||
base64Encode(text) {
|
||||
return btoa(text)
|
||||
}
|
||||
|
||||
base64URLEncode(text) {
|
||||
return this.base64Encode(text).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, '')
|
||||
}
|
||||
|
||||
base64Decode(base64String) {
|
||||
return atob(base64String)
|
||||
}
|
||||
|
||||
async pbkdf2(password, salt, iterations, length) {
|
||||
return btoa(password + salt + iterations)
|
||||
}
|
||||
|
||||
generateRandomKey(bits) {
|
||||
const length = bits / 8
|
||||
return this.randomString(length)
|
||||
}
|
||||
|
||||
async aes256CbcEncrypt(plaintext, iv, key) {
|
||||
const data = {
|
||||
plaintext,
|
||||
iv,
|
||||
key,
|
||||
}
|
||||
return btoa(JSON.stringify(data))
|
||||
}
|
||||
|
||||
async aes256CbcDecrypt(ciphertext, iv, key) {
|
||||
const data = JSON.parse(atob(ciphertext))
|
||||
if (data.key !== key || data.iv !== iv) {
|
||||
return undefined
|
||||
}
|
||||
return data.plaintext
|
||||
}
|
||||
|
||||
async hmac256(message, key) {
|
||||
return btoa(message + key)
|
||||
}
|
||||
|
||||
async sha256(text) {
|
||||
return new SNWebCrypto().sha256(text)
|
||||
}
|
||||
|
||||
async hmac1(message, key) {
|
||||
return btoa(message + key)
|
||||
}
|
||||
|
||||
async unsafeSha1(text) {
|
||||
return btoa(text)
|
||||
}
|
||||
|
||||
argon2(password, salt, iterations, bytes, length) {
|
||||
return btoa(password)
|
||||
}
|
||||
|
||||
xchacha20Encrypt(plaintext, nonce, key, assocData) {
|
||||
const data = {
|
||||
plaintext,
|
||||
nonce,
|
||||
key,
|
||||
assocData,
|
||||
}
|
||||
return btoa(JSON.stringify(data))
|
||||
}
|
||||
|
||||
xchacha20Decrypt(ciphertext, nonce, key, assocData) {
|
||||
const data = JSON.parse(atob(ciphertext))
|
||||
if (data.key !== key || data.nonce !== nonce || data.assocData !== assocData) {
|
||||
return undefined
|
||||
}
|
||||
return data.plaintext
|
||||
}
|
||||
|
||||
generateOtpSecret() {
|
||||
return 'WQVV2GFBRQWU3UQZWQFZC37PSNRXKTA6'
|
||||
}
|
||||
|
||||
totpToken(secret, timestamp, tokenLength, step) {
|
||||
return '123456'
|
||||
}
|
||||
}
|
||||
157
packages/snjs/mocha/lib/web_device_interface.js
Normal file
157
packages/snjs/mocha/lib/web_device_interface.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const KEYCHAIN_STORAGE_KEY = 'keychain'
|
||||
|
||||
export default class WebDeviceInterface {
|
||||
environment = Environment.Web
|
||||
|
||||
async getRawStorageValue(key) {
|
||||
return localStorage.getItem(key)
|
||||
}
|
||||
|
||||
async getJsonParsedRawStorageValue(key) {
|
||||
const value = await this.getRawStorageValue(key)
|
||||
if (isNullOrUndefined(value)) {
|
||||
return undefined
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch (e) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
async getAllRawStorageKeyValues() {
|
||||
const results = []
|
||||
for (const key of Object.keys(localStorage)) {
|
||||
results.push({
|
||||
key: key,
|
||||
value: localStorage[key],
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
async setRawStorageValue(key, value) {
|
||||
localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
async removeRawStorageValue(key) {
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
async removeAllRawStorageValues() {
|
||||
localStorage.clear()
|
||||
}
|
||||
|
||||
async openDatabase(_identifier) {
|
||||
return {}
|
||||
}
|
||||
|
||||
_getDatabaseKeyPrefix(identifier) {
|
||||
if (identifier) {
|
||||
return `${identifier}-item-`
|
||||
} else {
|
||||
return 'item-'
|
||||
}
|
||||
}
|
||||
|
||||
_keyForPayloadId(id, identifier) {
|
||||
return `${this._getDatabaseKeyPrefix(identifier)}${id}`
|
||||
}
|
||||
|
||||
async getAllRawDatabasePayloads(identifier) {
|
||||
const models = []
|
||||
for (const key in localStorage) {
|
||||
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
|
||||
models.push(JSON.parse(localStorage[key]))
|
||||
}
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
async saveRawDatabasePayload(payload, identifier) {
|
||||
localStorage.setItem(this._keyForPayloadId(payload.uuid, identifier), JSON.stringify(payload))
|
||||
}
|
||||
|
||||
async saveRawDatabasePayloads(payloads, identifier) {
|
||||
for (const payload of payloads) {
|
||||
await this.saveRawDatabasePayload(payload, identifier)
|
||||
}
|
||||
}
|
||||
|
||||
async removeRawDatabasePayloadWithId(id, identifier) {
|
||||
localStorage.removeItem(this._keyForPayloadId(id, identifier))
|
||||
}
|
||||
|
||||
async removeAllRawDatabasePayloads(identifier) {
|
||||
for (const key in localStorage) {
|
||||
if (key.startsWith(this._getDatabaseKeyPrefix(identifier))) {
|
||||
delete localStorage[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @keychain */
|
||||
async getNamespacedKeychainValue(identifier) {
|
||||
const keychain = await this.getRawKeychainValue(identifier)
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
return keychain[identifier]
|
||||
}
|
||||
|
||||
async setNamespacedKeychainValue(value, identifier) {
|
||||
let keychain = await this.getRawKeychainValue()
|
||||
if (!keychain) {
|
||||
keychain = {}
|
||||
}
|
||||
localStorage.setItem(
|
||||
KEYCHAIN_STORAGE_KEY,
|
||||
JSON.stringify({
|
||||
...keychain,
|
||||
[identifier]: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async clearNamespacedKeychainValue(identifier) {
|
||||
const keychain = await this.getRawKeychainValue()
|
||||
if (!keychain) {
|
||||
return
|
||||
}
|
||||
delete keychain[identifier]
|
||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(keychain))
|
||||
}
|
||||
|
||||
/** Allows unit tests to set legacy keychain structure as it was <= 003 */
|
||||
// eslint-disable-next-line camelcase
|
||||
async setLegacyRawKeychainValue(value) {
|
||||
localStorage.setItem(KEYCHAIN_STORAGE_KEY, JSON.stringify(value))
|
||||
}
|
||||
|
||||
async getRawKeychainValue() {
|
||||
const keychain = localStorage.getItem(KEYCHAIN_STORAGE_KEY)
|
||||
return JSON.parse(keychain)
|
||||
}
|
||||
|
||||
async clearRawKeychainValue() {
|
||||
localStorage.removeItem(KEYCHAIN_STORAGE_KEY)
|
||||
}
|
||||
|
||||
performSoftReset() {
|
||||
|
||||
}
|
||||
|
||||
performHardReset() {
|
||||
|
||||
}
|
||||
|
||||
isDeviceDestroyed() {
|
||||
return false
|
||||
}
|
||||
|
||||
deinit() {
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user