From ebe6ee0c1cf6759f9e4a03a184c7e9806a9c8aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Sun, 1 May 2022 17:08:24 +0200 Subject: [PATCH 001/411] Signatures and encryption: Preliminary work --- .flowconfig | 2 +- backend/database.js | 2 +- frontend/controller/actions/chatroom.js | 1 + frontend/controller/actions/group.js | 1 + frontend/controller/actions/identity.js | 112 +++++++- frontend/controller/actions/mailbox.js | 3 +- frontend/controller/actions/types.js | 12 +- frontend/controller/actions/utils.js | 6 +- frontend/utils/crypto.js | 333 +++++++++++++++++++++--- frontend/utils/crypto.test.js | 123 +++++++++ shared/domains/chelonia/GIMessage.js | 63 +++-- shared/domains/chelonia/chelonia.js | 77 +++++- shared/domains/chelonia/db.js | 2 +- shared/domains/chelonia/internals.js | 55 +++- 14 files changed, 706 insertions(+), 86 deletions(-) create mode 100644 frontend/utils/crypto.test.js diff --git a/.flowconfig b/.flowconfig index c58898f162..193370d984 100644 --- a/.flowconfig +++ b/.flowconfig @@ -11,7 +11,7 @@ .*/frontend/assets/.* .*/frontend/controller/service-worker.js .*/frontend/utils/blockies.js -.*/frontend/utils/crypto.js +#.*/frontend/utils/crypto.js .*/frontend/utils/flowTyper.js .*/frontend/utils/vuexQueue.js .*/historical/.* diff --git a/backend/database.js b/backend/database.js index 67236b4624..38774fe428 100644 --- a/backend/database.js +++ b/backend/database.js @@ -37,7 +37,7 @@ export default (sbp('sbp/selectors/register', { const json = `"${strToB64(entry.serialize())}"` if (currentHEAD !== hash) { this.push(prefix + json) - currentHEAD = entry.message().previousHEAD + currentHEAD = entry.head().previousHEAD prefix = ',' } else { this.push(prefix + json + ']') diff --git a/frontend/controller/actions/chatroom.js b/frontend/controller/actions/chatroom.js index 962120dacd..895577f567 100644 --- a/frontend/controller/actions/chatroom.js +++ b/frontend/controller/actions/chatroom.js @@ -11,6 +11,7 @@ export default (sbp('sbp/selectors/register', { try { return await sbp('chelonia/out/registerContract', { ...omit(params, ['options']), // any 'options' are for this action, not for Chelonia + keys: [], contractName: 'gi.contracts/chatroom' }) } catch (e) { diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 2fa4a7ad7a..993d0d1164 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -93,6 +93,7 @@ export default (sbp('sbp/selectors/register', { const message = await sbp('chelonia/out/registerContract', { contractName: 'gi.contracts/group', publishOptions, + keys: [], data: { invites: { [initialInvite.inviteSecret]: initialInvite diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index 0516ee5118..2301a51aa9 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -1,14 +1,23 @@ 'use strict' import sbp from '@sbp/sbp' +import { keyId, keygen, deriveKeyFromPassword, serializeKey, encrypt } from '@utils/crypto.js' import { GIErrorUIRuntimeError } from '@model/errors.js' import L, { LError } from '@view-utils/translations.js' import { imageUpload } from '@utils/image.js' import './mailbox.js' import { encryptedAction } from './utils.js' +import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' + +// eslint-disable-next-line camelcase +const salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC = 'SALT CHANGEME' export default (sbp('sbp/selectors/register', { + 'gi.actions/identity/retrieveSalt': async (username: string, password: string) => { + // TODO RETRIEVE FROM SERVER + return await Promise.resolve(salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC) + }, 'gi.actions/identity/create': async function ({ data: { username, email, password, picture }, options: { sync = true } = {}, @@ -39,22 +48,106 @@ export default (sbp('sbp/selectors/register', { // and do this outside of a try block so that if it throws the error just gets passed up const mailbox = await sbp('gi.actions/mailbox/create', { options: { sync: true } }) const mailboxID = mailbox.contractID() + + // Create the necessary keys to initialise the contract + // TODO: The salt needs to be dynamically generated + // eslint-disable-next-line camelcase + const salt = salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC + const IPK = await deriveKeyFromPassword('edwards25519sha512batch', password, salt) + const IEK = await deriveKeyFromPassword('curve25519xsalsa20poly1305', password, salt) + const CSK = keygen('edwards25519sha512batch') + const CEK = keygen('curve25519xsalsa20poly1305') + + // Key IDs + const IPKid = keyId(IPK) + const IEKid = keyId(IEK) + const CSKid = keyId(CSK) + const CEKid = keyId(CEK) + + // Public keys to be stored in the contract + const IPKp = serializeKey(IPK, false) + const IEKp = serializeKey(IEK, false) + const CSKp = serializeKey(CSK, false) + const CEKp = serializeKey(CEK, false) + + // Secret keys to be stored encrypted in the contract + const CSKs = encrypt(IEK, serializeKey(CSK, true)) + const CEKs = encrypt(IEK, serializeKey(CEK, true)) + let userID // next create the identity contract itself and associate it with the mailbox try { - const user = await sbp('chelonia/out/registerContract', { + const user = await sbp('chelonia/with-env', '', { + additionalKeys: { + [IPKid]: IPK, + [CSKid]: CSK, + [CEKid]: CEK + } + }, ['chelonia/out/registerContract', { contractName: 'gi.contracts/identity', publishOptions, + signingKeyId: IPKid, + actionSigningKeyId: CSKid, + actionEncryptionKeyId: CEKid, data: { attributes: { username, email, picture: finalPicture } - } - }) + }, + keys: [ + { + id: IPKid, + type: IPK.type, + data: IPKp, + perm: [GIMessage.OP_CONTRACT, GIMessage.OP_KEY_ADD, GIMessage.OP_KEY_DEL], + meta: { + type: 'ipk' + } + }, + { + id: IEKid, + type: IEK.type, + data: IEKp, + perm: ['gi.contracts/identity/keymeta'], + meta: { + type: 'iek' + } + }, + { + id: CSKid, + type: CSK.type, + data: CSKp, + perm: [GIMessage.OP_ACTION_UNENCRYPTED, GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ATOMIC, GIMessage.OP_CONTRACT_AUTH, GIMessage.OP_CONTRACT_DEAUTH], + meta: { + type: 'csk', + private: { + keyId: IEKid, + content: CSKs + } + } + }, + { + id: CEKid, + type: CEK.type, + data: CEKp, + perm: [GIMessage.OP_ACTION_ENCRYPTED], + meta: { + type: 'cek', + private: { + keyId: IEKid, + content: CEKs + } + } + } + ] + }]) userID = user.contractID() if (sync) { - await sbp('chelonia/contract/sync', userID) + await sbp('chelonia/with-env', userID, { additionalKeys: { [IEKid]: IEK } }, ['chelonia/contract/sync', userID]) } await sbp('gi.actions/identity/setAttributes', { - contractID: userID, data: { mailbox: mailboxID } + contractID: userID, + data: { mailbox: mailboxID }, + signingKeyId: CSKid, + encryptionKeyId: CEKid }) } catch (e) { console.error('gi.actions/identity/create failed!', e) @@ -98,17 +191,22 @@ export default (sbp('sbp/selectors/register', { }) { // TODO: Insert cryptography here const userId = await sbp('namespace/lookup', username) + if (!userId) { throw new GIErrorUIRuntimeError(L('Invalid username or password')) } + const salt = await sbp('gi.actions/identity/retrieveSalt', username, password) + const IEK = await deriveKeyFromPassword('curve25519xsalsa20poly1305', password, salt) + const IEKid = keyId(IEK) + try { console.debug(`Retrieved identity ${userId}`) // TODO: move the login vuex action code into this function (see #804) - await sbp('state/vuex/dispatch', 'login', { username, identityContractID: userId }) + await sbp('chelonia/with-env', userId, { additionalKeys: { [IEKid]: IEK } }, ['state/vuex/dispatch', 'login', { username, identityContractID: userId }]) if (sync) { - await sbp('chelonia/contract/sync', userId) + await sbp('chelonia/with-env', userId, { additionalKeys: { [IEKid]: IEK } }, ['chelonia/contract/sync', userId]) } return userId diff --git a/frontend/controller/actions/mailbox.js b/frontend/controller/actions/mailbox.js index 8783f631ee..796d74627e 100644 --- a/frontend/controller/actions/mailbox.js +++ b/frontend/controller/actions/mailbox.js @@ -14,8 +14,9 @@ export default (sbp('sbp/selectors/register', { }): Promise { try { const mailbox = await sbp('chelonia/out/registerContract', { - contractName: 'gi.contracts/mailbox', publishOptions, data + contractName: 'gi.contracts/mailbox', publishOptions, keys: [], data }) + console.log('gi.actions/mailbox/create', { mailbox }) if (sync) { await sbp('chelonia/contract/sync', mailbox.contractID()) } diff --git a/frontend/controller/actions/types.js b/frontend/controller/actions/types.js index d624b3b3ab..5d38b93e14 100644 --- a/frontend/controller/actions/types.js +++ b/frontend/controller/actions/types.js @@ -11,9 +11,15 @@ export type GIRegParams = { // keep in sync with ChelActionParams export type GIActionParams = { + action: string; contractID: string; data: Object; - options?: Object; // these are options for the action wrapper - hooks?: Object; - publishOptions?: Object + signingKeyId: string; + encryptionKeyId: ?string; + hooks?: { + prepublishContract?: (Object) => void; + prepublish?: (Object) => void; + postpublish?: (Object) => void; + }; + publishOptions?: { maxAttempts: number }; } diff --git a/frontend/controller/actions/utils.js b/frontend/controller/actions/utils.js index 2ddbd7999d..7c80fd5db2 100644 --- a/frontend/controller/actions/utils.js +++ b/frontend/controller/actions/utils.js @@ -9,8 +9,12 @@ export function encryptedAction (action: string, humanError: string | Function): return { [action]: async function (params: GIActionParams) { try { + const state = await sbp('chelonia/latestContractState', params.contractID) return await sbp('chelonia/out/actionEncrypted', { - ...params, action: action.replace('gi.actions', 'gi.contracts') + signingKeyId: (state?._vm?.authorizedKeys?.find((k) => k.meta?.type === 'csk')?.id: ?string), + encryptionKeyId: (state?._vm?.authorizedKeys?.find((k) => k.meta?.type === 'cek')?.id: ?string), + ...params, + action: action.replace('gi.actions', 'gi.contracts') }) } catch (e) { console.error(`${action} failed!`, e) diff --git a/frontend/utils/crypto.js b/frontend/utils/crypto.js index 7ca84479d0..f4dd828eb5 100644 --- a/frontend/utils/crypto.js +++ b/frontend/utils/crypto.js @@ -1,51 +1,324 @@ 'use strict' +import nacl from 'tweetnacl' + +import { blake32Hash, bytesToB64, b64ToBuf, strToBuf } from '~/shared/functions.js' + import scrypt from 'scrypt-async' -export class Key { - constructor (privKey, pubKey, salt) { - this.privKey = privKey - this.pubKey = pubKey // optional - this.salt = salt // optional +export type Key = { + type: string; + secretKey?: any; + publicKey?: any; +} + +export const keygen = (type: string): Key => { + if (type === 'edwards25519sha512batch') { + const key = nacl.sign.keyPair() + + const res: Key = { + type: type, + publicKey: key.publicKey + } + + Object.defineProperty(res, 'secretKey', { value: key.secretKey }) + + return res + } else if (type === 'curve25519xsalsa20poly1305') { + const key = nacl.box.keyPair() + + const res: Key = { + type: type, + publicKey: key.publicKey + } + + Object.defineProperty(res, 'secretKey', { value: key.secretKey }) + + return res + } else if (type === 'xsalsa20poly1305') { + const res: Key = { + type: type + } + + Object.defineProperty(res, 'secretKey', { value: nacl.randomBytes(nacl.secretbox.keyLength) }) + + return res + } + + throw new Error('Unsupported key type') +} +export const generateSalt = (): string => { + return bytesToB64(nacl.randomBytes(18)) +} +export const deriveKeyFromPassword = (type: string, password: string, salt: string): Promise => { + if (!['edwards25519sha512batch', 'curve25519xsalsa20poly1305', 'xsalsa20poly1305'].includes(type)) { + return Promise.reject(new Error('Unsupported type')) } - encrypt (data) {} + return new Promise((resolve) => { + scrypt(password, salt, { + N: 16384, + r: 8, + p: 1, + dkLen: type === 'edwards25519sha512batch' ? nacl.sign.keyLength : type === 'curve25519xsalsa20poly1305' ? nacl.box.keyLength : type === 'xsalsa20poly1305' ? nacl.secretbox.keyLength : 0, + encoding: 'binary' + }, (derivedKey) => { + if (type === 'edwards25519sha512batch') { + const key = nacl.sign.keyPair.fromSeed(derivedKey) + + resolve({ + type: type, + secretKey: key.secretKey, + publicKey: key.publicKey + }) + } else if (type === 'curve25519xsalsa20poly1305') { + const key = nacl.box.keyPair.fromSecretKey(derivedKey) + + resolve({ + type: type, + secretKey: key.secretKey, + publicKey: key.publicKey + }) + } else if (type === 'xsalsa20poly1305') { + resolve({ + type: type, + secretKey: derivedKey + }) + } + }) + }) +} +export const serializeKey = (key: Key, savePrivKey: boolean): string => { + if (key.type === 'edwards25519sha512batch' || key.type === 'curve25519xsalsa20poly1305') { + if (!savePrivKey) { + if (!key.publicKey) { + throw new Error('Unsupported operation: no public key to export') + } + + return JSON.stringify({ + type: key.type, + publicKey: bytesToB64(key.publicKey) + }) + } - decrypt (data) {} + if (!key.secretKey) { + throw new Error('Unsupported operation: no secret key to export') + } - signMessage (msg) {} + return JSON.stringify({ + type: key.type, + secretKey: bytesToB64(key.secretKey) + }) + } else if (key.type === 'xsalsa20poly1305') { + if (!savePrivKey) { + throw new Error('Unsupported operation: no public key to export') + } - verifySignature (msg, sig) {} + if (!key.secretKey) { + throw new Error('Unsupported operation: no secret key to export') + } - // serialization - serialize (savePrivKey = false) { + return JSON.stringify({ + type: key.type, + secretKey: bytesToB64(key.secretKey) + }) } + + throw new Error('Unsupported key type') } +export const deserializeKey = (data: string): Key => { + const keyData = JSON.parse(data) -// To store user's private key: -// var keys = Crypto.randomKeypair() -// var passKey = Crypto.keyFromPassword(password) -// var encryptedKeys = passKey.encrypt(keys.serialize(true)) -export class Crypto { - // TODO: make sure to NEVER store private key to the log. - static randomKeypair () { - // return randomly generated asymettric keypair via new Key() + if (!keyData || !keyData.type) { + throw new Error('Invalid key object') } - static randomKey () { - // return randomly generated symmetric key via new Key() + if (keyData.type === 'edwards25519sha512batch') { + if (keyData.secretKey) { + const key = nacl.sign.keyPair.fromSecretKey(b64ToBuf(keyData.secretKey)) + + const res: Key = { + type: keyData.type, + publicKey: key.publicKey + } + + Object.defineProperty(res, 'secretKey', { value: key.secretKey }) + + return res + } else if (keyData.publicKey) { + return { + type: keyData.type, + publicKey: new Uint8Array(b64ToBuf(keyData.publicKey)) + } + } + + throw new Error('Missing secret or public key') + } else if (keyData.type === 'curve25519xsalsa20poly1305') { + if (keyData.secretKey) { + const key = nacl.box.keyPair.fromSecretKey(b64ToBuf(keyData.secretKey)) + + const res: Key = { + type: keyData.type, + publicKey: key.publicKey + } + + Object.defineProperty(res, 'secretKey', { value: key.secretKey }) + + return res + } else if (keyData.publicKey) { + return { + type: keyData.type, + publicKey: new Uint8Array(b64ToBuf(keyData.publicKey)) + } + } + + throw new Error('Missing secret or public key') + } else if (keyData.type === 'xsalsa20poly1305') { + if (!keyData.secretKey) { + throw new Error('Secret key missing') + } + + const res: Key = { + type: keyData.type + } + + Object.defineProperty(res, 'secretKey', { value: new Uint8Array(b64ToBuf(keyData.secretKey)) }) + + return res } - static randomSalt () { - // return random salt + throw new Error('Unsupported key type') +} +export const keyId = (inKey: Key | string): string => { + const key = (Object(inKey) instanceof String) ? deserializeKey(((inKey: any): string)) : ((inKey: any): Key) + + const serializedKey = serializeKey(key, !key.publicKey) + return blake32Hash(serializedKey) +} +export const sign = (inKey: Key | string, data: string): string => { + const key = (Object(inKey) instanceof String) ? deserializeKey(((inKey: any): string)) : ((inKey: any): Key) + + if (key.type !== 'edwards25519sha512batch') { + throw new Error('Unsupported algorithm') } - // we use dchest/scrypt-async-js in browser - // TODO: use barrysteyn/node-scrypt in node/electrum - static keyFromPassword (password) { - const salt = Crypto.randomSalt() - // TODO: use proper parameters. https://github.com/dchest/scrypt-async-js - const opts = { N: 16384, r: 8, p: 1 } - return new Promise(resolve => scrypt(password, salt, opts, resolve)) + if (!key.secretKey) { + throw new Error('Secret key missing') } + + const messageUint8 = strToBuf(data) + const signature = nacl.sign.detached(messageUint8, key.secretKey) + const base64Signature = bytesToB64(signature) + + return base64Signature +} +export const verifySignature = (inKey: Key | string, data: string, signature: string): void => { + const key = (Object(inKey) instanceof String) ? deserializeKey(((inKey: any): string)) : ((inKey: any): Key) + + if (key.type !== 'edwards25519sha512batch') { + throw new Error('Unsupported algorithm') + } + + if (!key.publicKey) { + throw new Error('Public key missing') + } + + const decodedSignature = b64ToBuf(signature) + const messageUint8 = strToBuf(data) + + const result = nacl.sign.detached.verify(messageUint8, decodedSignature, key.publicKey) + + if (!result) { + throw new Error('Invalid signature') + } +} +export const encrypt = (inKey: Key | string, data: string): string => { + const key = (Object(inKey) instanceof String) ? deserializeKey(((inKey: any): string)) : ((inKey: any): Key) + + if (key.type === 'xsalsa20poly1305') { + if (!key.secretKey) { + throw new Error('Secret key missing') + } + + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + + const messageUint8 = strToBuf(data) + const box = nacl.secretbox(messageUint8, nonce, key.secretKey) + + const fullMessage = new Uint8Array(nonce.length + box.length) + + fullMessage.set(nonce) + fullMessage.set(box, nonce.length) + + const base64FullMessage = bytesToB64(fullMessage) + + return base64FullMessage + } else if (key.type === 'curve25519xsalsa20poly1305') { + if (!key.secretKey || !key.publicKey) { + throw new Error('Keypair missing') + } + + const nonce = nacl.randomBytes(nacl.box.nonceLength) + + const messageUint8 = strToBuf(data) + const box = nacl.box(messageUint8, nonce, key.publicKey, key.secretKey) + + const fullMessage = new Uint8Array(nonce.length + box.length) + + fullMessage.set(nonce) + fullMessage.set(box, nonce.length) + + const base64FullMessage = bytesToB64(fullMessage) + + return base64FullMessage + } + + throw new Error('Unsupported algorithm') +} +export const decrypt = (inKey: Key | string, data: string): string => { + const key = (Object(inKey) instanceof String) ? deserializeKey(((inKey: any): string)) : ((inKey: any): Key) + + if (key.type === 'xsalsa20poly1305') { + if (!key.secretKey) { + throw new Error('Secret key missing') + } + + const messageWithNonceAsUint8Array = b64ToBuf(data) + + const nonce = messageWithNonceAsUint8Array.slice(0, nacl.secretbox.nonceLength) + const message = messageWithNonceAsUint8Array.slice( + nacl.secretbox.nonceLength, + messageWithNonceAsUint8Array.length + ) + + const decrypted = nacl.secretbox.open(message, nonce, key.secretKey) + + if (!decrypted) { + throw new Error('Could not decrypt message') + } + + return Buffer.from(decrypted).toString('utf-8') + } else if (key.type === 'curve25519xsalsa20poly1305') { + if (!key.secretKey || !key.publicKey) { + throw new Error('Keypair missing') + } + + const messageWithNonceAsUint8Array = b64ToBuf(data) + + const nonce = messageWithNonceAsUint8Array.slice(0, nacl.box.nonceLength) + const message = messageWithNonceAsUint8Array.slice( + nacl.box.nonceLength, + messageWithNonceAsUint8Array.length + ) + + const decrypted = nacl.box.open(message, nonce, key.publicKey, key.secretKey) + + if (!decrypted) { + throw new Error('Could not decrypt message') + } + + return Buffer.from(decrypted).toString('utf-8') + } + + throw new Error('Unsupported algorithm') } diff --git a/frontend/utils/crypto.test.js b/frontend/utils/crypto.test.js new file mode 100644 index 0000000000..b7ad1b4508 --- /dev/null +++ b/frontend/utils/crypto.test.js @@ -0,0 +1,123 @@ +/* eslint-env mocha */ + +import should from 'should' +import 'should-sinon' +import { keygen, deriveKeyFromPassword, generateSalt, serializeKey, deserializeKey, encrypt, decrypt, sign, verifySignature } from './crypto.js' + +describe('Crypto suite', () => { + it('should deserialize to the same contents as when serializing', () => { + for (const type of ['edwards25519sha512batch', 'curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const key = keygen(type) + const serializedKey = serializeKey(key, true) + const deserializedKey = deserializeKey(serializedKey) + should(key).deepEqual(deserializedKey) + } + }) + + it('should deserialize to the same contents as when serializing (public)', () => { + for (const type of ['edwards25519sha512batch', 'curve25519xsalsa20poly1305']) { + const key = keygen(type) + const serializedKey = serializeKey(key, false) + delete key.secretKey + const deserializedKey = deserializeKey(serializedKey) + should(key).deepEqual(deserializedKey) + } + }) + + it('should derive the same key for the same password/salt combination', async () => { + for (const type of ['edwards25519sha512batch', 'curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const salt = generateSalt() + const invocation1 = await deriveKeyFromPassword(type, 'password123', salt) + const invocation2 = await deriveKeyFromPassword(type, 'password123', salt) + + should(invocation1).deepEqual(invocation2) + } + }) + + it('should derive different keys for the different password/salt combination', async () => { + const salt1 = 'salt1' + const salt2 = 'salt2' + + for (const type of ['edwards25519sha512batch', 'curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const invocation1 = await deriveKeyFromPassword(type, 'password123', salt1) + const invocation2 = await deriveKeyFromPassword(type, 'password123', salt2) + const invocation3 = await deriveKeyFromPassword(type, 'p4ssw0rd321', salt1) + + should(invocation1).not.deepEqual(invocation2) + should(invocation2).not.deepEqual(invocation3) + should(invocation1).not.deepEqual(invocation3) + } + }) + + it('should correctly sign and verify messages', () => { + const key = keygen('edwards25519sha512batch') + const data = 'data' + + const signature = sign(key, data) + + should(() => verifySignature(key, data, signature)).not.throw() + }) + + it('should not verify signatures made with a different key', () => { + const key1 = keygen('edwards25519sha512batch') + const key2 = keygen('edwards25519sha512batch') + const data = 'data' + + const signature = sign(key1, data) + + should(() => verifySignature(key2, data, signature)).throw() + }) + + it('should not verify signatures made with different data', () => { + const key = keygen('edwards25519sha512batch') + const data1 = 'data1' + const data2 = 'data2' + + const signature = sign(key, data1) + + should(() => verifySignature(key, data2, signature)).throw() + }) + + it('should not verify invalid signatures', () => { + const key = keygen('edwards25519sha512batch') + const data = 'data' + + should(() => verifySignature(key, data, 'INVALID SIGNATURE')).throw() + }) + + it('should correctly encrypt and decrypt messages', () => { + const data = 'data' + + for (const type of ['curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const key = keygen(type) + const encryptedMessage = encrypt(key, data) + + should(encryptedMessage).not.equal(data) + + const result = decrypt(key, encryptedMessage) + + should(result).equal(data) + } + }) + + it('should not decrypt messages encrypted with a different key', () => { + const data = 'data' + + for (const type of ['curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const key1 = keygen(type) + const key2 = keygen(type) + const encryptedMessage = encrypt(key1, data) + + should(encryptedMessage).not.equal(data) + + should(() => decrypt(key2, encryptedMessage)).throw() + } + }) + + it('should not decrypt invalid messages', () => { + for (const type of ['curve25519xsalsa20poly1305', 'xsalsa20poly1305']) { + const key = keygen(type) + should(() => decrypt(key, 'Invalid message')).throw() + } + }) +}) diff --git a/shared/domains/chelonia/GIMessage.js b/shared/domains/chelonia/GIMessage.js index a2d5940509..022e7be57f 100644 --- a/shared/domains/chelonia/GIMessage.js +++ b/shared/domains/chelonia/GIMessage.js @@ -5,16 +5,18 @@ import { blake32Hash } from '~/shared/functions.js' import type { JSONType, JSONObject } from '~/shared/types.js' -export type GIKeyType = '' +export type GIKeyType = 'edwards25519sha512batch' | 'curve25519xsalsa20poly1305' | 'xsalsa20poly1305' export type GIKey = { + id: string; type: GIKeyType; - data: Object; // based on GIKeyType this will change + data: string; + perm: string[]; meta: Object; } // Allows server to check if the user is allowed to register this type of contract // TODO: rename 'type' to 'contractName': -export type GIOpContract = { type: string; keyJSON: string, parentContract?: string } +export type GIOpContract = { type: string; keys: GIKey[], parentContract?: string } export type GIOpActionEncrypted = string // encrypted version of GIOpActionUnencrypted export type GIOpActionUnencrypted = { action: string; data: JSONType; meta: JSONObject } export type GIOpKeyAdd = { keyHash: string, keyJSON: ?string, context: string } @@ -28,7 +30,10 @@ export class GIMessage { // flow type annotations to make flow happy _decrypted: GIOpValue _mapping: Object + _head: Object _message: Object + _signature: string + _signedPayload: string static OP_CONTRACT: 'c' = 'c' static OP_ACTION_ENCRYPTED: 'ae' = 'ae' // e2e-encrypted action @@ -38,6 +43,9 @@ export class GIMessage { static OP_PROTOCOL_UPGRADE: 'pu' = 'pu' static OP_PROP_SET: 'ps' = 'ps' // set a public key/value pair static OP_PROP_DEL: 'pd' = 'pd' // delete a public key/value pair + static OP_CONTRACT_AUTH: 'ca' = 'ca' // authorize a contract + static OP_CONTRACT_DEAUTH: 'cd' = 'cd' // deauthorize a contract + static OP_ATOMIC: 'at' = 'at' // atomic op // eslint-disable-next-line camelcase static createV1_0 ( @@ -46,44 +54,57 @@ export class GIMessage { op: GIOp, signatureFn?: Function = defaultSignatureFn ): this { - const message = { + const head = { version: '1.0.0', previousHEAD, contractID, - op, - // the nonce makes it difficult to predict message contents - // and makes it easier to prevent conflicts during development - nonce: Math.random() + op: op[0] } + console.log('createV1_0', { op, head }) + const message = op[1] // NOTE: the JSON strings generated here must be preserved forever. // do not ever regenerate this message using the contructor. // instead store it using serialize() and restore it using // deserialize(). + const headJSON = JSON.stringify(head) const messageJSON = JSON.stringify(message) + const signedPayload = blake32Hash(`${blake32Hash(headJSON)}${blake32Hash(messageJSON)}`) + const signature = signatureFn(signedPayload) const value = JSON.stringify({ + head: headJSON, message: messageJSON, - sig: signatureFn(messageJSON) + sig: signature }) return new this({ mapping: { key: blake32Hash(value), value }, - message + head, + message, + signature, + signedPayload }) } // TODO: we need signature verification upon decryption somewhere... static deserialize (value: string): this { if (!value) throw new Error(`deserialize bad value: ${value}`) + const parsedValue = JSON.parse(value) return new this({ mapping: { key: blake32Hash(value), value }, - message: JSON.parse(JSON.parse(value).message) + head: JSON.parse(parsedValue.head), + message: JSON.parse(parsedValue.message), + signature: parsedValue.sig, + signedPayload: blake32Hash(`${blake32Hash(parsedValue.head)}${blake32Hash(parsedValue.message)}`) }) } - constructor ({ mapping, message }: { mapping: Object, message: Object }) { + constructor ({ mapping, head, message, signature, signedPayload }: { mapping: Object, head: Object, message: Object, signature: string, signedPayload: string }) { this._mapping = mapping + this._head = head this._message = message + this._signature = signature + this._signedPayload = signedPayload // perform basic sanity check - const [type] = this.message().op + const type = this.opType() switch (type) { case GIMessage.OP_CONTRACT: if (!this.isFirstMessage()) throw new Error('OP_CONTRACT: must be first message') @@ -107,13 +128,19 @@ export class GIMessage { return this._decrypted } + head (): Object { return this._head } + message (): Object { return this._message } - op (): GIOp { return this.message().op } + op (): GIOp { return [this.head().op, this.message()] } + + opType (): GIOpType { return this.head().op } + + opValue (): GIOpValue { return this.message() } - opType (): GIOpType { return this.op()[0] } + signature (): Object { return this._signature } - opValue (): GIOpValue { return this.op()[1] } + signedPayload (): string { return this._signedPayload } description (): string { const type = this.opType() @@ -132,9 +159,9 @@ export class GIMessage { return `${desc}|${this.hash()} of ${this.contractID()}>` } - isFirstMessage (): boolean { return !this.message().previousHEAD } + isFirstMessage (): boolean { return !this.head().previousHEAD } - contractID (): string { return this.message().contractID || this.hash() } + contractID (): string { return this.head().contractID || this.hash() } serialize (): string { return this._mapping.value } diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index c1389c49c4..a23d9999f9 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -10,13 +10,18 @@ import { merge, cloneDeep, randomHexString, intersection, difference } from '~/f // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.js' import { ChelErrorUnrecoverable } from './errors.js' -import type { GIOpContract, GIOpActionUnencrypted } from './GIMessage.js' +import type { GIKey, GIOpContract, GIOpActionUnencrypted } from './GIMessage.js' +import { keyId, sign, encrypt, decrypt } from '@utils/crypto.js' // TODO: define ChelContractType for /defineContract export type ChelRegParams = { contractName: string; data: Object; + signingKeyId: string; + actionSigningKeyId: string; + actionEncryptionKeyId: ?string; + keys: GIKey[]; hooks?: { prepublishContract?: (GIMessage) => void; prepublish?: (GIMessage) => void; @@ -29,6 +34,8 @@ export type ChelActionParams = { action: string; contractID: string; data: Object; + signingKeyId: string; + encryptionKeyId: ?string; hooks?: { prepublishContract?: (GIMessage) => void; prepublish?: (GIMessage) => void; @@ -48,13 +55,43 @@ export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?) // 4 => 'group' // 5 => 'payment' +const signatureFnBuilder = (key) => { + return (data) => { + return { + type: key.type, + keyId: keyId(key), + data: sign(key, data) + } + } +} + sbp('sbp/selectors/register', { // https://www.wordnik.com/words/chelonia // https://gitlab.okturtles.org/okturtles/group-income/-/wikis/E2E-Protocol/Framework.md#alt-names 'chelonia/_init': function () { this.config = { - decryptFn: JSON.parse, // override! - encryptFn: JSON.stringify, // override! + decryptFn: function (message: Object, state: ?Object) { + if (Object(message) instanceof String) { + return JSON.parse(message) + } + + const keyId = message.keyId + const key = this.env.additionalKeys?.[keyId] || state?._volatile?.keys?.[keyId] + + return JSON.parse(decrypt(key, message.content)) + }, + encryptFn: function (message: Object, eKeyId: string, state: ?Object) { + const key = this.env.additionalKeys?.[eKeyId] || state?._volatile?.keys?.[eKeyId] + + if (!key) { + return JSON.stringify(message) + } + + return { + keyId: keyId(key), + content: encrypt(key, JSON.stringify(message)) + } + }, stateSelector: 'chelonia/private/state', // override to integrate with, for example, vuex whitelisted: (action: string): boolean => !!this.whitelistedActions[action], reactiveSet: (obj, key, value) => { obj[key] = value; return value }, // example: set to Vue.set @@ -83,6 +120,7 @@ sbp('sbp/selectors/register', { this.contracts = {} this.whitelistedActions = {} this.sideEffectStacks = {} // [contractID]: Array<*> + this.env = {} this.sideEffectStack = (contractID: string): Array<*> => { let stack = this.sideEffectStacks[contractID] if (!stack) { @@ -91,6 +129,13 @@ sbp('sbp/selectors/register', { return stack } }, + 'chelonia/with-env': async function (contractID: string, env: Object, sbpInvocation: Array<*>) { + const savedEnv = this.env + this.env = env + const res = await sbp('okTurtles.eventQueue/queueEvent', `chelonia/env/${contractID}`, sbpInvocation) + this.env = savedEnv + return res + }, 'chelonia/configure': function (config: Object) { merge(this.config, config) // merge will strip the hooks off of config.hooks when merging from the root of the object @@ -224,7 +269,7 @@ sbp('sbp/selectors/register', { // but after it's finished. This is used in tandem with // queuing the 'chelonia/private/in/handleEvent' selector, defined below. // This prevents handleEvent getting called with the wrong previousHEAD for an event. - return sbp('okTurtles.eventQueue/queueEvent', contractID, [ + return sbp('okTurtles.eventQueue/queueEvent', `chelonia/${contractID}`, [ 'chelonia/private/in/syncContract', contractID ]) })) @@ -233,7 +278,7 @@ sbp('sbp/selectors/register', { 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise<*> { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { - return sbp('okTurtles.eventQueue/queueEvent', contractID, [ + return sbp('okTurtles.eventQueue/queueEvent', `chelonia/${contractID}`, [ 'chelonia/contract/removeImmediately', contractID ]) })) @@ -273,22 +318,27 @@ sbp('sbp/selectors/register', { }, // 'chelonia/out' - selectors that send data out to the server 'chelonia/out/registerContract': async function (params: ChelRegParams) { - const { contractName, hooks, publishOptions } = params + const { contractName, keys, hooks, publishOptions, signingKeyId, actionSigningKeyId, actionEncryptionKeyId } = params const contract = this.contracts[contractName] if (!contract) throw new Error(`contract not defined: ${contractName}`) + const signingKey = this.env.additionalKeys?.[signingKeyId] + const signingFn = signingKey ? signatureFnBuilder(signingKey) : undefined const contractMsg = GIMessage.createV1_0(null, null, [ GIMessage.OP_CONTRACT, ({ type: contractName, - keyJSON: 'TODO: add group public key here' + keys: keys }: GIOpContract) - ]) + ], signingFn) hooks && hooks.prepublishContract && hooks.prepublishContract(contractMsg) - await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions) + await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions, signingFn) + const contractID = contractMsg.hash() const msg = await sbp('chelonia/out/actionEncrypted', { action: contractName, - contractID: contractMsg.hash(), + contractID, data: params.data, + signingKeyId: actionSigningKeyId, + encryptionKeyId: actionEncryptionKeyId, hooks, publishOptions }) @@ -339,11 +389,12 @@ async function outEncryptedOrUnencryptedAction ( contract.metadata.validate(meta, { state, ...gProxy, contractID }) contract.actions[action].validate(data, { state, ...gProxy, meta, contractID }) const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) + const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] + const payload = opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn.call(this, unencMessage, params.encryptionKeyId, state) const message = GIMessage.createV1_0(contractID, previousHEAD, [ opType, - opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn(unencMessage) - ] - // TODO: add the signature function here to sign the message whether encrypted or not + payload + ], signingKey ? signatureFnBuilder(signingKey) : undefined ) hooks && hooks.prepublish && hooks.prepublish(message) await sbp('chelonia/private/out/publishEvent', message, publishOptions) diff --git a/shared/domains/chelonia/db.js b/shared/domains/chelonia/db.js index f2d9a3f225..e8fc2ceb0e 100644 --- a/shared/domains/chelonia/db.js +++ b/shared/domains/chelonia/db.js @@ -55,7 +55,7 @@ export default (sbp('sbp/selectors/register', { }, 'chelonia/db/addEntry': async function (entry: GIMessage): Promise { try { - const { previousHEAD } = entry.message() + const { previousHEAD } = entry.head() const contractID: string = entry.contractID() if (await sbp('chelonia/db/get', entry.hash())) { console.warn(`[chelonia.db] entry exists: ${entry.hash()}`) diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index fc821ad848..abd4d823f9 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -8,6 +8,7 @@ import { b64ToStr } from '~/shared/functions.js' import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/utils/giLodash.js' import { ChelErrorDBBadPreviousHEAD, ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.js' import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.js' +import { decrypt, verifySignature } from '@utils/crypto.js' import type { GIOpContract, GIOpType, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpPropSet, GIOpKeyAdd } from './GIMessage.js' @@ -16,7 +17,7 @@ sbp('sbp/selectors/register', { 'chelonia/private/state': function () { return this.state }, - 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 2 } = {}) { + 'chelonia/private/out/publishEvent': async function (entry: GIMessage, { maxAttempts = 2 } = {}, signatureFn?: Function) { const contractID = entry.contractID() let attempt = 1 // auto resend after short random delay @@ -46,7 +47,7 @@ sbp('sbp/selectors/register', { // if this isn't OP_CONTRACT, get latestHash, recreate and resend message if (!entry.isFirstMessage()) { const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) - entry = GIMessage.createV1_0(contractID, previousHEAD, entry.op()) + entry = GIMessage.createV1_0(contractID, previousHEAD, entry.op(), signatureFn) } } else { const message = (await r.json())?.message @@ -74,17 +75,37 @@ sbp('sbp/selectors/register', { const hash = message.hash() const contractID = message.contractID() const config = this.config + const contracts = this.contracts + const signature = message.signature() + const signedPayload = message.signedPayload() + const env = this.env + const self = this if (!state._vm) state._vm = {} const opFns: { [GIOpType]: (any) => void } = { [GIMessage.OP_CONTRACT] (v: GIOpContract) { - // TODO: shouldn't each contract have its own set of authorized keys? - if (!state._vm.authorizedKeys) state._vm.authorizedKeys = [] - // TODO: we probably want to be pushing the de-JSON-ified key here - state._vm.authorizedKeys.push({ key: v.keyJSON, context: 'owner' }) + const keys = { ...env.additionalKeys, ...state._volatile?.keys } + const { type } = v + if (!contracts[type]) { + throw new Error(`chelonia: contract not recognized: '${type}'`) + } + state._vm.authorizedKeys = v.keys + + for (const key of v.keys) { + if (key.meta?.private) { + if (key.id && key.meta.private.keyId in keys && key.meta.private.content) { + if (!state._volatile) state._volatile = { keys: {} } + try { + state._volatile.keys[key.id] = decrypt(keys[key.meta.private.keyId], key.meta.private.content) + } catch (e) { + console.error('Decryption error', e) + } + } + } + } }, [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { if (!config.skipActionProcessing) { - const decrypted = message.decryptedValue(config.decryptFn) + const decrypted = config.decryptFn.call(self, message.opValue(), state) opFns[GIMessage.OP_ACTION_UNENCRYPTED](decrypted) } }, @@ -115,6 +136,20 @@ sbp('sbp/selectors/register', { if (config.preOp) { processOp = config.preOp(message, state) !== false && processOp } + + // Signature verification + // TODO: Temporary. Skip verifying default signatures + if (signature.type !== 'default') { + const authorizedKeys = opT === GIMessage.OP_CONTRACT ? ((opV: any): GIOpContract).keys : state._vm.authorizedKeys + const signingKey = authorizedKeys?.find((k) => k.id === signature.keyId && Array.isArray(k.perm) && k.perm.includes(opT)) + + if (!si gningKey) { + throw new Error('No matching signing key was defined') + } + + verifySignature(signingKey.data, signedPayload, signature.data) + } + if (config[`preOp_${opT}`]) { processOp = config[`preOp_${opT}`](message, state) !== false && processOp } @@ -216,7 +251,7 @@ sbp('sbp/selectors/register', { if (!processingErrored) { try { if (!this.config.skipActionProcessing && !this.config.skipSideEffects) { - await handleEvent.processSideEffects.call(this, message) + await handleEvent.processSideEffects.call(this, message, state[contractID]) } postHandleEvent && await postHandleEvent(message) sbp('okTurtles.events/emit', hash, contractID, message) @@ -287,11 +322,11 @@ const handleEvent = { await Promise.resolve() // TODO: load any unloaded contract code sbp('chelonia/private/in/processMessage', message, state[contractID]) }, - async processSideEffects (message: GIMessage) { + async processSideEffects (message: GIMessage, state: Object) { if ([GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ACTION_UNENCRYPTED].includes(message.opType())) { const contractID = message.contractID() const hash = message.hash() - const { action, data, meta } = message.decryptedValue() + const { action, data, meta } = this.config.decryptFn.call(this, message.opValue(), state) const mutation = { data, meta, hash, contractID } await sbp(`${action}/sideEffect`, mutation) } From 05976eb2551b5d433ad32385e6646d6c6fe03392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Sun, 1 May 2022 17:57:07 +0200 Subject: [PATCH 002/411] Fix typo --- shared/domains/chelonia/internals.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index abd4d823f9..bdb8a5a330 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -143,7 +143,7 @@ sbp('sbp/selectors/register', { const authorizedKeys = opT === GIMessage.OP_CONTRACT ? ((opV: any): GIOpContract).keys : state._vm.authorizedKeys const signingKey = authorizedKeys?.find((k) => k.id === signature.keyId && Array.isArray(k.perm) && k.perm.includes(opT)) - if (!si gningKey) { + if (!signingKey) { throw new Error('No matching signing key was defined') } From 93274b3c3ec559d4ef2a6bbe3437f06eff2ece31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Sun, 5 Jun 2022 21:52:36 +0200 Subject: [PATCH 003/411] New OP_KEYSHARE --- frontend/controller/actions/chatroom.js | 5 + frontend/controller/actions/group.js | 7 +- frontend/controller/actions/identity.js | 42 +++++++- frontend/controller/actions/mailbox.js | 8 +- package-lock.json | 114 +++++++++++++-------- shared/domains/chelonia/GIMessage.js | 27 +++-- shared/domains/chelonia/chelonia.js | 126 +++++++++++++++++++----- shared/domains/chelonia/internals.js | 65 ++++++++++-- 8 files changed, 298 insertions(+), 96 deletions(-) diff --git a/frontend/controller/actions/chatroom.js b/frontend/controller/actions/chatroom.js index 624799c88e..51a442d4db 100644 --- a/frontend/controller/actions/chatroom.js +++ b/frontend/controller/actions/chatroom.js @@ -28,6 +28,8 @@ export default (sbp('sbp/selectors/register', { const CSKs = encrypt(CEK, serializeKey(CSK, true)) const CEKs = encrypt(CEK, serializeKey(CEK, true)) + const rootState = sbp('state/vuex/state') + const chatroom = await sbp('chelonia/with-env', '', { additionalKeys: { [CSKid]: CSK, @@ -73,6 +75,9 @@ export default (sbp('sbp/selectors/register', { await sbp('chelonia/with-env', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) + const userID = rootState.loggedIn.identityContractID + await sbp('gi.actions/identity/shareKeysWithSelf', { userID, contractID }) + return chatroom } catch (e) { console.error('gi.actions/chatroom/register failed!', e) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index fa221e4dcb..95f50a0756 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -98,6 +98,8 @@ export default (sbp('sbp/selectors/register', { const CSKs = encrypt(CEK, serializeKey(CSK, true)) const CEKs = encrypt(CEK, serializeKey(CEK, true)) + const rootState = sbp('state/vuex/state') + try { const initialInvite = createInvite({ quantity: 60, creator: INVITE_INITIAL_CREATOR }) const proposalSettings = { @@ -144,7 +146,7 @@ export default (sbp('sbp/selectors/register', { id: CEKid, type: CEK.type, data: CEKp, - permissions: [GIMessage.OP_ACTION_ENCRYPTED], + permissions: [GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_KEYSHARE], meta: { type: 'cek', private: { @@ -213,6 +215,9 @@ export default (sbp('sbp/selectors/register', { encryptionKeyId: CEKid }]) + const userID = rootState.loggedIn.identityContractID + await sbp('gi.actions/identity/shareKeysWithSelf', { userID, contractID }) + return message } catch (e) { console.error('gi.actions/group/create failed!', e) diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index 94fabba978..715f9e4373 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -1,7 +1,7 @@ 'use strict' import sbp from '@sbp/sbp' -import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, deriveKeyFromPassword, serializeKey, encrypt } from '~/shared/domains/chelonia/crypto.js' +import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, deriveKeyFromPassword, deserializeKey, serializeKey, encrypt } from '~/shared/domains/chelonia/crypto.js' import { GIErrorUIRuntimeError } from '@model/errors.js' import L, { LError } from '@view-utils/translations.js' import { imageUpload } from '@utils/image.js' @@ -13,6 +13,7 @@ import './mailbox.js' import { encryptedAction } from './utils.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' +import type { GIKey } from '~/shared/domains/chelonia/GIMessage.js' // eslint-disable-next-line camelcase const salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC = 'SALT CHANGEME' @@ -134,7 +135,7 @@ export default (sbp('sbp/selectors/register', { id: CSKid, type: CSK.type, data: CSKp, - permissions: [GIMessage.OP_ACTION_UNENCRYPTED, GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ATOMIC, GIMessage.OP_CONTRACT_AUTH, GIMessage.OP_CONTRACT_DEAUTH], + permissions: [GIMessage.OP_ACTION_UNENCRYPTED, GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ATOMIC, GIMessage.OP_CONTRACT_AUTH, GIMessage.OP_CONTRACT_DEAUTH, GIMessage.OP_KEYSHARE], meta: { type: 'csk', private: { @@ -147,7 +148,7 @@ export default (sbp('sbp/selectors/register', { id: CEKid, type: CEK.type, data: CEKp, - permissions: [GIMessage.OP_ACTION_ENCRYPTED], + permissions: [GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_KEYSHARE], meta: { type: 'cek', private: { @@ -168,12 +169,47 @@ export default (sbp('sbp/selectors/register', { signingKeyId: CSKid, encryptionKeyId: CEKid }]) + + await sbp('gi.actions/identity/shareKeysWithSelf', { userID, contractID: mailboxID }) } catch (e) { console.error('gi.actions/identity/create failed!', e) throw new GIErrorUIRuntimeError(L('Failed to create user identity: {reportError}', LError(e))) } return [userID, mailboxID] }, + 'gi.actions/identity/shareKeysWithSelf': async function ({ userID, contractID }) { + if (userID === contractID) { + return + } + + const contractState = await sbp('chelonia/latestContractState', contractID) + + if (contractState?._volatile?.keys) { + const state = await sbp('chelonia/latestContractState', userID) + + const CEKid = (((Object.values(Object(state?._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'cek')?.id: ?string) + const CSKid = (((Object.values(Object(state?._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?string) + const CEK = deserializeKey(state?._volatile?.keys?.[CEKid]) + + await sbp('chelonia/out/keyShare', { + destinationContractID: userID, + destinationContractName: 'gi.contracts/identity', + data: { + contractID: contractID, + keys: Object.entries(contractState._volatile.keys).map(([keyId, key]: [string, mixed]) => ({ + id: keyId, + meta: { + private: { + keyId: CEKid, + content: encrypt(CEK, (key: any)) + } + } + })) + }, + signingKeyId: CSKid + }) + } + }, 'gi.actions/identity/signup': async function ({ username, email, password }, publishOptions) { try { const randomAvatar = sbp('gi.utils/avatar/create') diff --git a/frontend/controller/actions/mailbox.js b/frontend/controller/actions/mailbox.js index 15d0ff1abf..b67c7c1161 100644 --- a/frontend/controller/actions/mailbox.js +++ b/frontend/controller/actions/mailbox.js @@ -8,7 +8,7 @@ import { encryptedAction } from './utils.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' export default (sbp('sbp/selectors/register', { - '_gi.actions/mailbox/create': async function ({ + 'gi.actions/mailbox/create': async function ({ data = {}, options: { sync = true } = {}, publishOptions }): Promise { try { @@ -81,11 +81,5 @@ export default (sbp('sbp/selectors/register', { throw new GIErrorUIRuntimeError(L('Failed to create mailbox: {reportError}', LError(e))) } }, - get 'gi.actions/mailbox/create' () { - return this['_gi.actions/mailbox/create'] - }, - set 'gi.actions/mailbox/create' (value) { - this['_gi.actions/mailbox/create'] = value - }, ...encryptedAction('gi.actions/mailbox/postMessage', L('Failed to post message to mailbox.')) }): string[]) diff --git a/package-lock.json b/package-lock.json index d20ffd89a5..331c77d642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10199,29 +10199,6 @@ "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true }, - "node_modules/htmlparser2/node_modules/readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/htmlparser2/node_modules/string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", @@ -14359,6 +14336,20 @@ "node": ">=8" } }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -15562,6 +15553,35 @@ "node": ">= 0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", @@ -26000,26 +26020,6 @@ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", "dev": true - }, - "readable-stream": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", - "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } } } }, @@ -29248,6 +29248,17 @@ } } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, "readdirp": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", @@ -30236,6 +30247,23 @@ "limiter": "^1.0.5" } }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, "string-hash": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", diff --git a/shared/domains/chelonia/GIMessage.js b/shared/domains/chelonia/GIMessage.js index 065114f630..57b505cab0 100644 --- a/shared/domains/chelonia/GIMessage.js +++ b/shared/domains/chelonia/GIMessage.js @@ -23,9 +23,10 @@ export type GIOpActionUnencrypted = { action: string; data: JSONType; meta: JSON export type GIOpKeyAdd = GIKey[] export type GIOpKeyDel = string[] export type GIOpPropSet = { key: string, value: JSONType } +export type GIOpKeyShare = { contractID: string, keys: GIKey[] } -export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' -export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpKeyDel | GIOpPropSet +export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' | 'ks' +export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpKeyDel | GIOpPropSet | GIOpKeyShare export type GIOp = [GIOpType, GIOpValue] export class GIMessage { @@ -48,18 +49,29 @@ export class GIMessage { static OP_CONTRACT_AUTH: 'ca' = 'ca' // authorize a contract static OP_CONTRACT_DEAUTH: 'cd' = 'cd' // deauthorize a contract static OP_ATOMIC: 'at' = 'at' // atomic op + static OP_KEYSHARE: 'ks' = 'ks' // key share // eslint-disable-next-line camelcase static createV1_0 ( - contractID: string | null = null, - previousHEAD: string | null = null, - op: GIOp, - signatureFn?: Function = defaultSignatureFn + { + contractID, + originatingContractID, + previousHEAD = null, + op, + signatureFn = defaultSignatureFn + }: { + contractID: string | null, + originatingContractID?: string, + previousHEAD?: string | null, + op: GIOp, + signatureFn?: Function + } ): this { const head = { version: '1.0.0', previousHEAD, contractID, + originatingContractID, op: op[0] } console.log('createV1_0', { op, head }) @@ -111,6 +123,7 @@ export class GIMessage { case GIMessage.OP_CONTRACT: if (!this.isFirstMessage()) throw new Error('OP_CONTRACT: must be first message') break + case GIMessage.OP_KEYSHARE: case GIMessage.OP_ACTION_ENCRYPTED: // nothing for now break @@ -165,6 +178,8 @@ export class GIMessage { contractID (): string { return this.head().contractID || this.hash() } + originatingContractID (): string { return this.head().originatingContractID || this.contractID() } + serialize (): string { return this._mapping.value } hash (): string { return this._mapping.key } diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index f084d479bb..0168551c79 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -10,7 +10,7 @@ import { merge, cloneDeep, randomHexString, intersection, difference } from '~/f // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.js' import { ChelErrorUnrecoverable } from './errors.js' -import type { GIKey, GIOpContract, GIOpActionUnencrypted, GIOpKeyAdd, GIOpKeyDel } from './GIMessage.js' +import type { GIKey, GIOpContract, GIOpActionUnencrypted, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare } from './GIMessage.js' import { keyId, sign, encrypt, decrypt, generateSalt } from './crypto.js' // TODO: define ChelContractType for /defineContract @@ -70,6 +70,21 @@ export type ChelKeyDelParams = { publishOptions?: { maxAttempts: number }; } +export type ChelKeyShareParams = { + originatingContractID?: string; + originatingContractName?: string; + destinationContractID: string; + destinationContractName: string; + data: GIOpKeyShare; + signingKeyId: string; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + export { GIMessage } export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ @@ -373,17 +388,22 @@ sbp('sbp/selectors/register', { const contract = this.contracts[contractName] if (!contract) throw new Error(`contract not defined: ${contractName}`) const signingKey = this.env.additionalKeys?.[signingKeyId] - const signingFn = signingKey ? signatureFnBuilder(signingKey) : undefined - const contractMsg = GIMessage.createV1_0(null, null, [ - GIMessage.OP_CONTRACT, - ({ - type: contractName, - keys: keys, - nonce: generateSalt() - }: GIOpContract) - ], signingFn) + const signatureFn = signingKey ? signatureFnBuilder(signingKey) : undefined + const contractMsg = GIMessage.createV1_0({ + contractID: null, + previousHEAD: null, + op: [ + GIMessage.OP_CONTRACT, + ({ + type: contractName, + keys: keys, + nonce: generateSalt() + }: GIOpContract) + ], + signatureFn + }) hooks && hooks.prepublishContract && hooks.prepublishContract(contractMsg) - await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions, signingFn) + await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions, signatureFn) const contractID = contractMsg.hash() const msg = await sbp('chelonia/out/actionEncrypted', { action: contractName, @@ -404,6 +424,48 @@ sbp('sbp/selectors/register', { 'chelonia/out/actionUnencrypted': function (params: ChelActionParams): Promise { return outEncryptedOrUnencryptedAction.call(this, GIMessage.OP_ACTION_UNENCRYPTED, params) }, + 'chelonia/out/keyShare': async function (params: ChelKeyShareParams): Promise { + const { originatingContractName, originatingContractID, destinationContractName, destinationContractID, data, hooks, publishOptions } = params + const originatingContract = originatingContractID ? this.contracts[originatingContractName] : undefined + const destinationContract = this.contracts[destinationContractName] + let originatingState + + if ((originatingContractID && !originatingContract) || !destinationContract) { + throw new Error('Contract name not found') + } + + if (originatingContractID && originatingContract) { + originatingState = originatingContract.state(originatingContractID) + const originatingGProxy = gettersProxy(originatingState, originatingContract.getters) + const originatingMeta = originatingContract.metadata.create() + originatingContract.metadata.validate(originatingMeta, { state: originatingState, ...originatingGProxy, originatingContractID }) + } + + const destinationState = destinationContract.state(destinationContractID) + const previousHEAD = await sbp('chelonia/private/out/latestHash', destinationContractID) + + const destinationGProxy = gettersProxy(destinationState, destinationContract.getters) + const destinationMeta = destinationContract.metadata.create() + destinationContract.metadata.validate(destinationMeta, { state: destinationState, ...destinationGProxy, destinationContractID }) + const payload = (data: GIOpKeyShare) + + const signingKey = this.env.additionalKeys?.[params.signingKeyId] || ((originatingContractID ? originatingState : destinationState)?._volatile?.keys[params.signingKeyId]) + + const msg = GIMessage.createV1_0({ + contractID: destinationContractID, + originatingContractID, + previousHEAD, + op: [ + GIMessage.OP_KEYSHARE, + payload + ], + signatureFn: signingKey ? signatureFnBuilder(signingKey) : undefined + }) + hooks && hooks.prepublish && hooks.prepublish(msg) + await sbp('chelonia/private/out/publishEvent', msg, publishOptions) + hooks && hooks.postpublish && hooks.postpublish(msg) + return msg + }, 'chelonia/out/keyAdd': async function (params: ChelKeyAddParams): Promise { const { contractID, contractName, data, hooks, publishOptions } = params const contract = this.contracts[contractName] @@ -417,11 +479,15 @@ sbp('sbp/selectors/register', { contract.metadata.validate(meta, { state, ...gProxy, contractID }) const payload = (data: GIOpKeyAdd) const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] - const msg = GIMessage.createV1_0(contractID, previousHEAD, [ - GIMessage.OP_KEY_ADD, - payload - ], signingKey ? signatureFnBuilder(signingKey) : undefined - ) + const msg = GIMessage.createV1_0({ + contractID, + previousHEAD, + op: [ + GIMessage.OP_KEY_ADD, + payload + ], + signatureFn: signingKey ? signatureFnBuilder(signingKey) : undefined + }) hooks && hooks.prepublish && hooks.prepublish(msg) await sbp('chelonia/private/out/publishEvent', msg, publishOptions) hooks && hooks.postpublish && hooks.postpublish(msg) @@ -440,11 +506,15 @@ sbp('sbp/selectors/register', { contract.metadata.validate(meta, { state, ...gProxy, contractID }) const payload = (data: GIOpKeyDel) const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] - const msg = GIMessage.createV1_0(contractID, previousHEAD, [ - GIMessage.OP_KEY_DEL, - payload - ], signingKey ? signatureFnBuilder(signingKey) : undefined - ) + const msg = GIMessage.createV1_0({ + contractID, + previousHEAD, + op: [ + GIMessage.OP_KEY_DEL, + payload + ], + signatureFn: signingKey ? signatureFnBuilder(signingKey) : undefined + }) hooks && hooks.prepublish && hooks.prepublish(msg) await sbp('chelonia/private/out/publishEvent', msg, publishOptions) hooks && hooks.postpublish && hooks.postpublish(msg) @@ -483,11 +553,15 @@ async function outEncryptedOrUnencryptedAction ( const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] const payload = opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn.call(this, unencMessage, params.encryptionKeyId, state) - const message = GIMessage.createV1_0(contractID, previousHEAD, [ - opType, - payload - ], signingKey ? signatureFnBuilder(signingKey) : undefined - ) + const message = GIMessage.createV1_0({ + contractID, + previousHEAD, + op: [ + opType, + payload + ], + signatureFn: signingKey ? signatureFnBuilder(signingKey) : undefined + }) hooks && hooks.prepublish && hooks.prepublish(message) await sbp('chelonia/private/out/publishEvent', message, publishOptions) hooks && hooks.postpublish && hooks.postpublish(message) diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index 40185c1ec0..cbcb3b888f 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -1,16 +1,15 @@ 'use strict' import sbp from '@sbp/sbp' -import './db.js' -import { GIMessage } from './GIMessage.js' import { handleFetchResult } from '~/frontend/controller/utils/misc.js' +import { cloneDeep, debounce, delay, pick, randomIntFromRange } from '~/frontend/utils/giLodash.js' import { b64ToStr } from '~/shared/functions.js' -import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/utils/giLodash.js' -import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.js' -import { CONTRACT_IS_SYNCING, CONTRACTS_MODIFIED, EVENT_HANDLED } from './events.js' import { decrypt, verifySignature } from './crypto.js' - -import type { GIKey, GIOpContract, GIOpType, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpPropSet, GIOpKeyAdd, GIOpKeyDel } from './GIMessage.js' +import './db.js' +import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.js' +import { CONTRACTS_MODIFIED, CONTRACT_IS_SYNCING, EVENT_HANDLED } from './events.js' +import type { GIKey, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpContract, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare, GIOpPropSet, GIOpType } from './GIMessage.js' +import { GIMessage } from './GIMessage.js' const keysToMap = (keys: GIKey[]): Object => { return Object.fromEntries(keys.map(key => [key.id, key])) @@ -53,7 +52,7 @@ sbp('sbp/selectors/register', { // if this isn't OP_CONTRACT, get latestHash, recreate and resend message if (!entry.isFirstMessage()) { const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) - entry = GIMessage.createV1_0(contractID, previousHEAD, entry.op(), signatureFn) + entry = GIMessage.createV1_0({ contractID, previousHEAD, op: entry.op(), signatureFn }) } } else { const message = (await r.json())?.message @@ -77,7 +76,7 @@ sbp('sbp/selectors/register', { return events.reverse().map(b64ToStr) } }, - 'chelonia/private/in/processMessage': function (message: GIMessage, state: Object) { + 'chelonia/private/in/processMessage': async function (message: GIMessage, state: Object) { const [opT, opV] = message.op() const hash = message.hash() const contractID = message.contractID() @@ -125,6 +124,33 @@ sbp('sbp/selectors/register', { sbp(`${action}/process`, { data, meta, hash, contractID }, state) } }, + [GIMessage.OP_KEYSHARE] (v: GIOpKeyShare) { + if (message.originatingContractID() !== contractID && v.contractID !== message.originatingContractID()) { + throw new Error('External contracts can only set keys for themselves') + } + + const cheloniaState = sbp(self.config.stateSelector) + + if (!cheloniaState[v.contractID]) { + cheloniaState[v.contractID] = Object.create(null) + } + const targetState = cheloniaState[v.contractID] + + const keys = { ...env.additionalKeys, ...state._volatile?.keys } + + for (const key of v.keys) { + if (key.meta?.private) { + if (key.id && key.meta.private.keyId in keys && key.meta.private.content) { + if (!targetState._volatile) targetState._volatile = { keys: {} } + try { + targetState._volatile.keys[key.id] = decrypt(keys[key.meta.private.keyId], key.meta.private.content) + } catch (e) { + console.error('Decryption error', e) + } + } + } + } + }, [GIMessage.OP_PROP_DEL]: notImplemented, [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { if (!state._vm.props) state._vm.props = {} @@ -166,7 +192,23 @@ sbp('sbp/selectors/register', { // Signature verification // TODO: Temporary. Skip verifying default signatures if (signature.type !== 'default') { - const authorizedKeys = opT === GIMessage.OP_CONTRACT ? keysToMap(((opV: any): GIOpContract).keys) : state._vm.authorizedKeys + // This sync code has potential issues + // The first issue is that it can deadlock if there are circular references + // The second issue is that it doesn't handle key rotation. If the key used for signing is invalidated / removed from the originating contract, we won't have it in the state + // Both of these issues can be resolved by introducing a parameter with the message ID the state is based on. This requires implementing a separate, ephemeral, state container for operations that refer to a different contract. + // The difficulty of this is how to securely determine the message ID to use. + // The server can assist with this. + if (message.originatingContractID() !== message.contractID()) { + await sbp('okTurtles.eventQueue/queueEvent', `chelonia/${message.originatingContractID()}`, [ + 'chelonia/private/in/syncContract', message.originatingContractID() + ]) + } + + const contractState = message.originatingContractID() === message.contractID() + ? state + : sbp(this.config.stateSelector).contracts[message.originatingContractID()] + + const authorizedKeys = opT === GIMessage.OP_CONTRACT ? keysToMap(((opV: any): GIOpContract).keys) : contractState._vm.authorizedKeys const signingKey = authorizedKeys?.[signature.keyId] if (!signingKey || !Array.isArray(signingKey.permissions) || !signingKey.permissions.includes(opT)) { @@ -273,6 +315,9 @@ sbp('sbp/selectors/register', { } // whether or not there was an exception, we proceed ahead with updating the head // you can prevent this by throwing an exception in the processError hook + if (!state.contracts[contractID]) { + state.contracts[contractID] = Object.create(null) + } state.contracts[contractID].HEAD = hash // process any side-effects (these must never result in any mutation to the contract state!) if (!processingErrored) { From 0e366f724cb743f6b7ad09e8faf512679af857e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Sun, 19 Jun 2022 20:21:05 +0200 Subject: [PATCH 004/411] Broken changes --- frontend/controller/actions/chatroom.js | 36 +++++++++++++++++++++++++ frontend/controller/actions/utils.js | 2 +- frontend/model/contracts/identity.js | 8 +++--- shared/domains/chelonia/chelonia.js | 21 ++++++++++++--- shared/domains/chelonia/crypto.js | 27 ++++++++++--------- shared/domains/chelonia/internals.js | 16 +++++++---- 6 files changed, 85 insertions(+), 25 deletions(-) diff --git a/frontend/controller/actions/chatroom.js b/frontend/controller/actions/chatroom.js index c71024501e..a5c118ae46 100644 --- a/frontend/controller/actions/chatroom.js +++ b/frontend/controller/actions/chatroom.js @@ -30,6 +30,42 @@ export default (sbp('sbp/selectors/register', { const rootState = sbp('state/vuex/state') + console.log('Chatroom create', { + ...omit(params, ['options']), // any 'options' are for this action, not for Chelonia + signingKeyId: CSKid, + actionSigningKeyId: CSKid, + actionEncryptionKeyId: CEKid, + keys: [ + { + id: CSKid, + type: CSK.type, + data: CSKp, + permissions: [GIMessage.OP_CONTRACT, GIMessage.OP_KEY_ADD, GIMessage.OP_KEY_DEL, GIMessage.OP_ACTION_UNENCRYPTED, GIMessage.OP_ACTION_ENCRYPTED, GIMessage.OP_ATOMIC, GIMessage.OP_CONTRACT_AUTH, GIMessage.OP_CONTRACT_DEAUTH], + meta: { + type: 'csk', + private: { + keyId: CEKid, + content: CSKs + } + } + }, + { + id: CEKid, + type: CEK.type, + data: CEKp, + permissions: [GIMessage.OP_ACTION_ENCRYPTED], + meta: { + type: 'cek', + private: { + keyId: CEKid, + content: CEKs + } + } + } + ], + contractName: 'gi.contracts/chatroom' + }) + const chatroom = await sbp('chelonia/with-env', '', { additionalKeys: { [CSKid]: CSK, diff --git a/frontend/controller/actions/utils.js b/frontend/controller/actions/utils.js index 91dd4b9c48..e6d55c4629 100644 --- a/frontend/controller/actions/utils.js +++ b/frontend/controller/actions/utils.js @@ -12,9 +12,9 @@ export function encryptedAction (action: string, humanError: string | Function): try { const state = await sbp('chelonia/latestContractState', params.contractID) return await sbp('chelonia/out/actionEncrypted', { + ...params, signingKeyId: (((Object.values(Object(state?._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?string), encryptionKeyId: (((Object.values(Object(state?._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'cek')?.id: ?string), - ...params, action: action.replace('gi.actions', 'gi.contracts') }) } catch (e) { diff --git a/frontend/model/contracts/identity.js b/frontend/model/contracts/identity.js index c51d01a328..af90fd0626 100644 --- a/frontend/model/contracts/identity.js +++ b/frontend/model/contracts/identity.js @@ -6,7 +6,7 @@ import Vue from 'vue' import '~/shared/domains/chelonia/chelonia.js' import { objectOf, objectMaybeOf, arrayOf, string, object } from '~/frontend/utils/flowTyper.js' import { merge } from '~/frontend/utils/giLodash.js' -import L from '~/frontend/views/utils/translations.js' +// import L from '~/frontend/views/utils/translations.js' sbp('chelonia/defineContract', { name: 'gi.contracts/identity', @@ -69,8 +69,8 @@ sbp('chelonia/defineContract', { Vue.set(state, 'loginState', data) }, async sideEffect () { - try { - await sbp('gi.actions/identity/updateLoginStateUponLogin') + /* try { + sbp('okTurtles.eventQueue/queueEvent', , ['gi.actions/identity/updateLoginStateUponLogin']) } catch (e) { sbp('gi.notifications/emit', 'ERROR', { message: L("Failed to join groups we're part of on another device. Not catastrophic, but could lead to problems. {errName}: '{errMsg}'", { @@ -78,7 +78,7 @@ sbp('chelonia/defineContract', { errMsg: e.message || '?' }) }) - } + } */ } } } diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index 0672a0c65f..875cf7f85f 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -130,6 +130,7 @@ const decryptFn = function (message: Object, state: ?Object) { const key = this.env.additionalKeys?.[keyId] || state?._volatile?.keys?.[keyId] if (!key) { + console.log({ message, state, keyId, env: this.env }) throw new Error(`Key ${keyId} not found`) } @@ -184,7 +185,7 @@ sbp('sbp/selectors/register', { const savedEnv = this.env this.env = env try { - return await sbp('okTurtles.eventQueue/queueEvent', `chelonia/env/${contractID}`, sbpInvocation) + return await sbp('okTurtles.eventQueue/queueEvent', `chelonia/with-env/${contractID}`, sbpInvocation) } finally { this.env = savedEnv } @@ -332,7 +333,7 @@ sbp('sbp/selectors/register', { // but after it's finished. This is used in tandem with // queuing the 'chelonia/private/in/handleEvent' selector, defined below. // This prevents handleEvent getting called with the wrong previousHEAD for an event. - return sbp('okTurtles.eventQueue/queueEvent', `chelonia/${contractID}`, [ + return sbp('okTurtles.eventQueue/queueEvent', contractID, [ 'chelonia/private/in/syncContract', contractID ]).catch((err) => { console.error(`[chelonia] failed to sync ${contractID}:`, err) @@ -345,7 +346,7 @@ sbp('sbp/selectors/register', { 'chelonia/contract/remove': function (contractIDs: string | string[]): Promise<*> { const listOfIds = typeof contractIDs === 'string' ? [contractIDs] : contractIDs return Promise.all(listOfIds.map(contractID => { - return sbp('okTurtles.eventQueue/queueEvent', `chelonia/${contractID}`, [ + return sbp('okTurtles.eventQueue/queueEvent', contractID, [ 'chelonia/contract/removeImmediately', contractID ]) })) @@ -413,6 +414,7 @@ sbp('sbp/selectors/register', { }, // 'chelonia/out' - selectors that send data out to the server 'chelonia/out/registerContract': async function (params: ChelRegParams) { + console.log('Register contract', { params }) const { contractName, keys, hooks, publishOptions, signingKeyId, actionSigningKeyId, actionEncryptionKeyId } = params const contract = this.contracts[contractName] if (!contract) throw new Error(`contract not defined: ${contractName}`) @@ -434,6 +436,18 @@ sbp('sbp/selectors/register', { hooks && hooks.prepublishContract && hooks.prepublishContract(contractMsg) await sbp('chelonia/private/out/publishEvent', contractMsg, publishOptions, signatureFn) const contractID = contractMsg.hash() + console.log('Register contract, sednig action', { + params, + xx: { + action: contractName, + contractID, + data: params.data, + signingKeyId: actionSigningKeyId, + encryptionKeyId: actionEncryptionKeyId, + hooks, + publishOptions + } + }) const msg = await sbp('chelonia/out/actionEncrypted', { action: contractName, contractID, @@ -582,6 +596,7 @@ async function outEncryptedOrUnencryptedAction ( const unencMessage = ({ action, data, meta }: GIOpActionUnencrypted) const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] const payload = opType === GIMessage.OP_ACTION_UNENCRYPTED ? unencMessage : this.config.encryptFn.call(this, unencMessage, params.encryptionKeyId, state) + console.log({ unencMessage, ekid: params.encryptionKeyId, state, payload }) const message = GIMessage.createV1_0({ contractID, previousHEAD, diff --git a/shared/domains/chelonia/crypto.js b/shared/domains/chelonia/crypto.js index 7fc7766422..2a7bea80d1 100644 --- a/shared/domains/chelonia/crypto.js +++ b/shared/domains/chelonia/crypto.js @@ -258,19 +258,22 @@ export const encrypt = (inKey: Key | string, data: string): string => { return base64FullMessage } else if (key.type === CURVE25519XSALSA20POLY1305) { - if (!key.secretKey || !key.publicKey) { - throw new Error('Keypair missing') + if (!key.publicKey) { + throw new Error('Public key missing') } const nonce = nacl.randomBytes(nacl.box.nonceLength) const messageUint8 = strToBuf(data) - const box = nacl.box(messageUint8, nonce, key.publicKey, key.secretKey) + const ephemeralKey = nacl.box.keyPair() + const box = nacl.box(messageUint8, nonce, key.publicKey, ephemeralKey.secretKey) + ephemeralKey.secretKey.fill(0) - const fullMessage = new Uint8Array(nonce.length + box.length) + const fullMessage = new Uint8Array(nacl.box.publicKeyLength + nonce.length + box.length) - fullMessage.set(nonce) - fullMessage.set(box, nonce.length) + fullMessage.set(ephemeralKey.publicKey) + fullMessage.set(nonce, nacl.box.publicKeyLength) + fullMessage.set(box, nacl.box.publicKeyLength + nonce.length) const base64FullMessage = bytesToB64(fullMessage) @@ -303,19 +306,19 @@ export const decrypt = (inKey: Key | string, data: string): string => { return Buffer.from(decrypted).toString('utf-8') } else if (key.type === CURVE25519XSALSA20POLY1305) { - if (!key.secretKey || !key.publicKey) { - throw new Error('Keypair missing') + if (!key.secretKey) { + throw new Error('Secret key missing') } const messageWithNonceAsUint8Array = b64ToBuf(data) - const nonce = messageWithNonceAsUint8Array.slice(0, nacl.box.nonceLength) + const ephemeralPublicKey = messageWithNonceAsUint8Array.slice(0, nacl.box.publicKeyLength) + const nonce = messageWithNonceAsUint8Array.slice(nacl.box.publicKeyLength, nacl.box.publicKeyLength + nacl.box.nonceLength) const message = messageWithNonceAsUint8Array.slice( - nacl.box.nonceLength, - messageWithNonceAsUint8Array.length + nacl.box.publicKeyLength + nacl.box.nonceLength ) - const decrypted = nacl.box.open(message, nonce, key.publicKey, key.secretKey) + const decrypted = nacl.box.open(message, nonce, ephemeralPublicKey, key.secretKey) if (!decrypted) { throw new Error('Could not decrypt message') diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index 8b7927cbd4..bf5bb8f7ae 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -143,7 +143,11 @@ sbp('sbp/selectors/register', { if (key.id && key.meta.private.keyId in keys && key.meta.private.content) { if (!targetState._volatile) targetState._volatile = { keys: {} } try { - targetState._volatile.keys[key.id] = decrypt(keys[key.meta.private.keyId], key.meta.private.content) + const decrypted = decrypt(keys[key.meta.private.keyId], key.meta.private.content) + targetState._volatile.keys[key.id] = decrypted + if (env.additionalKeys) { + env.additionalKeys[key.id] = decrypted + } } catch (e) { console.error('Decryption error', e) } @@ -191,7 +195,7 @@ sbp('sbp/selectors/register', { // Signature verification // TODO: Temporary. Skip verifying default signatures - if (signature.type !== 'default') { + if (isNaN(1) && signature.type !== 'default') { // This sync code has potential issues // The first issue is that it can deadlock if there are circular references // The second issue is that it doesn't handle key rotation. If the key used for signing is invalidated / removed from the originating contract, we won't have it in the state @@ -199,7 +203,7 @@ sbp('sbp/selectors/register', { // The difficulty of this is how to securely determine the message ID to use. // The server can assist with this. if (message.originatingContractID() !== message.contractID()) { - await sbp('okTurtles.eventQueue/queueEvent', `chelonia/${message.originatingContractID()}`, [ + await sbp('okTurtles.eventQueue/queueEvent', message.originatingContractID(), [ 'chelonia/private/in/syncContract', message.originatingContractID() ]) } @@ -315,10 +319,12 @@ sbp('sbp/selectors/register', { } // whether or not there was an exception, we proceed ahead with updating the head // you can prevent this by throwing an exception in the processError hook - if (!state.contracts[contractID]) { + /* if (!state.contracts[contractID]) { state.contracts[contractID] = Object.create(null) + } */ + if (state.contracts[contractID]) { + state.contracts[contractID].HEAD = hash } - state.contracts[contractID].HEAD = hash // process any side-effects (these must never result in any mutation to the contract state!) if (!processingErrored) { try { From 94a76011e68674af2782f4a5d609c0a843bbf078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Thu, 28 Jul 2022 10:21:29 +0200 Subject: [PATCH 005/411] ZKPP password salt --- backend/routes.js | 95 +++++++++++++++ backend/zkppSalt.js | 253 +++++++++++++++++++++++++++++++++++++++ backend/zkppSalt.test.js | 108 +++++++++++++++++ 3 files changed, 456 insertions(+) create mode 100644 backend/zkppSalt.js create mode 100644 backend/zkppSalt.test.js diff --git a/backend/routes.js b/backend/routes.js index b030e29696..8683deb431 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -9,6 +9,7 @@ import { SERVER_INSTANCE } from './instance-keys.js' import path from 'path' import chalk from 'chalk' import './database.js' +import { registrationKey, register, getChallenge, getContractSalt, update } from './zkppSalt.js' const Boom = require('@hapi/boom') const Joi = require('@hapi/joi') @@ -230,3 +231,97 @@ route.GET('/app/{path*}', {}, { route.GET('/', {}, function (req, h) { return h.redirect('/app/') }) + +route.POST('/zkpp/{contract}', { + validate: { + payload: Joi.alternatives([ + { + b: Joi.string().required() + }, + { + r: Joi.string().required(), + s: Joi.string().required(), + sig: Joi.string().required(), + Eh: Joi.string().required() + } + ]) + } +}, async function (req, h) { + if (req.payload['b']) { + const result = await registrationKey(req.params['contract'], req.payload['b']) + + if (!result) { + return Boom.internal('internal error') + } + + return result + } else { + const result = await register(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['Eh']) + + if (!result) { + return Boom.internal('internal error') + } + + return result + } +}) + +route.GET('/zkpp/{contract}/auth_hash', {}, async function (req, h) { + if (!req.query['b']) { + return Boom.badRequest('b query param required') + } + + const challenge = await getChallenge(req.params['contract'], req.query['b']) + + if (!challenge) { + return Boom.internal('internal error') + } + + return challenge +}) + +route.GET('/zkpp/{contract}/contract_hash', {}, async function (req, h) { + if (!req.query['r']) { + return Boom.badRequest('r query param required') + } + + if (!req.query['s']) { + return Boom.badRequest('s query param required') + } + + if (!req.query['sig']) { + return Boom.badRequest('sig query param required') + } + + if (!req.query['hc']) { + return Boom.badRequest('hc query param required') + } + + const salt = await getContractSalt(req.params['contract'], req.query['r'], req.query['s'], req.query['sig'], req.query['hc']) + + if (!salt) { + return Boom.internal('internal error') + } + + return salt +}) + +route.PUT('/zkpp/{contract}', { + validate: { + payload: Joi.object({ + r: Joi.string().required(), + s: Joi.string().required(), + sig: Joi.string().required(), + hc: Joi.string().required(), + Ea: Joi.string().required() + }) + } +}, async function (req, h) { + const result = await update(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['hc'], req.payload['Ea']) + + if (!result) { + return Boom.internal('internal error') + } + + return result +}) diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js new file mode 100644 index 0000000000..107b2e0a11 --- /dev/null +++ b/backend/zkppSalt.js @@ -0,0 +1,253 @@ +import { blake32Hash } from '~/shared/functions.js' +import { timingSafeEqual } from 'crypto' +import nacl from 'tweetnacl' +import sbp from '@sbp/sbp' + +// TODO HARDCODED VALUES +const recordPepper = 'pepper' +const recordMasterKey = 'masterKey' +const challengeSecret = 'secret' +const registrationSecret = 'secret' +const maxAge = 30 + +const hashStringArray = (...args: Array) => { + return nacl.hash(Buffer.concat(args.map((s) => nacl.hash(Buffer.from(s))))) +} + +const hashRawStringArray = (...args: Array) => { + return nacl.hash(Buffer.concat(args.map((s) => Buffer.from(s)))) +} + +const getZkppSaltRecord = async (contract: string) => { + const recordId = blake32Hash(hashStringArray('RID', contract, recordPepper)) + const record = await sbp('chelonia/db/get', recordId) + + if (record) { + const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength) + + const recordBuf = Buffer.from(record, 'base64url') + const nonce = recordBuf.slice(0, nacl.secretbox.nonceLength) + const recordCiphertext = recordBuf.slice(nacl.secretbox.nonceLength) + const recordPlaintext = nacl.secretbox.open(recordCiphertext, nonce, encryptionKey) + + if (!recordPlaintext) { + return null + } + + const recordString = Buffer.from(recordPlaintext).toString('utf-8') + + try { + const recordObj = JSON.parse(recordString) + + if (!Array.isArray(recordObj) || recordObj.length !== 3 || !recordObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { + return null + } + + const [hashedPassword, authSalt, contractSalt] = recordObj + + return { + hashedPassword, + authSalt, + contractSalt + } + } catch { + // empty + } + } + + return null +} + +const setZkppSaltRecord = async (contract: string, hashedPassword: string, authSalt: string, contractSalt: string) => { + const recordId = blake32Hash(hashStringArray('RID', contract, recordPepper)) + const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + const recordPlaintext = JSON.stringify([hashedPassword, authSalt, contractSalt]) + const recordCiphertext = nacl.secretbox(Buffer.from(recordPlaintext), nonce, encryptionKey) + const recordBuf = Buffer.concat([nonce, recordCiphertext]) + const record = recordBuf.toString('base64url') + await sbp('chelonia/db/set', recordId, record) +} + +export const getChallenge = async (contract: string, b: string): Promise => { + const record = await getZkppSaltRecord(contract) + if (!record) { + return false + } + const { authSalt } = record + const s = Buffer.from(nacl.randomBytes(12)).toString('base64url') + const now = (Date.now() / 1000 | 0).toString(16) + const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64url')].join(',') + + return { + authSalt, + s, + sig + } +} + +const verifyChallenge = (contract: string, r: string, s: string, userSig: string): boolean => { + // Check sig has the right format + if (!/^[a-fA-F0-9]{1,11},[a-zA-Z0-9_-]{86}(?:==)?$/.test(userSig)) { + return false + } + + const [then, mac] = userSig.split(',') + const now = Date.now() / 1000 | 0 + const iThen = Number.parseInt(then, 16) + + // Check that sig is no older than Xs + if (!(iThen <= now) || !(iThen >= (now - maxAge))) { + return false + } + + const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url') + const sig = hashStringArray(contract, b, s, then, challengeSecret) + const macBuf = Buffer.from(mac, 'base64url') + + return sig.byteLength === macBuf.byteLength && timingSafeEqual(sig, macBuf) +} + +export const registrationKey = async (contract: string, b: string): Promise => { + const record = await getZkppSaltRecord(contract) + if (record) { + return false + } + + const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + const keyPair = nacl.box.keyPair() + const s = Buffer.concat([nonce, nacl.secretbox(keyPair.secretKey, nonce, encryptionKey)]).toString('base64url') + const now = (Date.now() / 1000 | 0).toString(16) + const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64url')].join(',') + + return { + s, + p: Buffer.from(keyPair.publicKey).toString('base64url'), + sig + } +} + +export const register = async (contract: string, clientPublicKey: string, encryptedSecretKey: string, userSig: string, encryptedHashedPassword: string): Promise => { + if (!verifyChallenge(contract, clientPublicKey, encryptedSecretKey, userSig)) { + return false + } + + const record = await getZkppSaltRecord(contract) + + if (record) { + return false + } + + const clientPublicKeyBuf = Buffer.from(clientPublicKey, 'base64url') + const encryptedSecretKeyBuf = Buffer.from(encryptedSecretKey, 'base64url') + const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength) + const secretKeyBuf = nacl.secretbox.open(encryptedSecretKeyBuf.slice(nacl.secretbox.nonceLength), encryptedSecretKeyBuf.slice(0, nacl.secretbox.nonceLength), encryptionKey) + + if (clientPublicKeyBuf.byteLength !== nacl.box.publicKeyLength || !secretKeyBuf || secretKeyBuf.byteLength !== nacl.box.secretKeyLength) { + return false + } + + const dhKey = nacl.box.before(clientPublicKeyBuf, secretKeyBuf) + + const encryptedHashedPasswordBuf = Buffer.from(encryptedHashedPassword, 'base64url') + + const hashedPasswordBuf = nacl.box.open.after(encryptedHashedPasswordBuf.slice(nacl.box.nonceLength), encryptedHashedPasswordBuf.slice(0, nacl.box.nonceLength), dhKey) + + if (!hashedPasswordBuf) { + return false + } + + const authSalt = Buffer.from(hashStringArray('AUTHSALT', dhKey)).slice(0, 18).toString('base64url') + const contractSalt = Buffer.from(hashStringArray('CONTRACTSALT', dhKey)).slice(0, 18).toString('base64url') + + await setZkppSaltRecord(contract, Buffer.from(hashedPasswordBuf).toString(), authSalt, contractSalt) + + return true +} + +const contractSaltVerifyC = (h: string, r: string, s: string, userHc: string) => { + const ħ = hashStringArray(r, s) + const c = hashStringArray(h, ħ) + const hc = nacl.hash(c) + const userHcBuf = Buffer.from(userHc, 'base64url') + + if (hc.byteLength === userHcBuf.byteLength && timingSafeEqual(hc, userHcBuf)) { + return c + } + + return false +} + +export const getContractSalt = async (contract: string, r: string, s: string, sig: string, hc: string): Promise => { + if (!verifyChallenge(contract, r, s, sig)) { + return false + } + + const record = await getZkppSaltRecord(contract) + if (!record) { + return false + } + + const { hashedPassword, contractSalt } = record + + const c = contractSaltVerifyC(hashedPassword, r, s, hc) + + if (!c) { + return false + } + + const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + + const encryptedContractSalt = nacl.secretbox(Buffer.from(contractSalt), nonce, encryptionKey) + + return Buffer.concat([nonce, encryptedContractSalt]).toString('base64url') +} + +export const update = async (contract: string, r: string, s: string, sig: string, hc: string, encryptedArgs: string): Promise => { + if (!verifyChallenge(contract, r, s, sig)) { + return false + } + + const record = await getZkppSaltRecord(contract) + if (!record) { + return false + } + const { hashedPassword } = record + + const c = contractSaltVerifyC(hashedPassword, r, s, hc) + + if (!c) { + return false + } + + const encryptionKey = hashRawStringArray('SU', c).slice(0, nacl.secretbox.keyLength) + const encryptedArgsBuf = Buffer.from(encryptedArgs, 'base64url') + const nonce = encryptedArgsBuf.slice(0, nacl.secretbox.nonceLength) + const encrytedArgsCiphertext = encryptedArgsBuf.slice(nacl.secretbox.nonceLength) + + const args = nacl.secretbox.open(encrytedArgsCiphertext, nonce, encryptionKey) + + if (!args) { + return false + } + + try { + const argsObj = JSON.parse(Buffer.from(args).toString()) + + if (!Array.isArray(argsObj) || argsObj.length !== 3 || !argsObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { + return false + } + + const [hashedPassword, authSalt, contractSalt] = argsObj + + await setZkppSaltRecord(contract, hashedPassword, authSalt, contractSalt) + + return true + } catch { + // empty + } + + return false +} diff --git a/backend/zkppSalt.test.js b/backend/zkppSalt.test.js new file mode 100644 index 0000000000..42a53ed3d4 --- /dev/null +++ b/backend/zkppSalt.test.js @@ -0,0 +1,108 @@ +/* eslint-env mocha */ + +import nacl from 'tweetnacl' +import should from 'should' +import 'should-sinon' + +import { registrationKey, register, getChallenge, getContractSalt, update } from './zkppSalt.js' + +describe('ZKPP Salt functions', () => { + it('register', async () => { + const keyPair = nacl.box.keyPair() + const nonce = nacl.randomBytes(nacl.box.nonceLength) + const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') + const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') + + const regKeyAlice1 = await registrationKey('alice', publicKeyHash) + const regKeyAlice2 = await registrationKey('alice', publicKeyHash) + should(regKeyAlice1).be.of.type('object') + should(regKeyAlice2).be.of.type('object') + const encryptedHashedPasswordAlice1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice1.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const res1 = await register('alice', publicKey, regKeyAlice1.s, regKeyAlice1.sig, encryptedHashedPasswordAlice1) + should(res1).equal(true, 'register should allow new entry (alice)') + + const encryptedHashedPasswordAlice2 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice2.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const res2 = await register('alice', publicKey, regKeyAlice2.s, regKeyAlice2.sig, encryptedHashedPasswordAlice2) + should(res2).equal(false, 'register should not overwrite entry (alice)') + + const regKeyBob1 = await registrationKey('bob', publicKeyHash) + should(regKeyBob1).be.of.type('object') + const encryptedHashedPasswordBob1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyBob1.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const res3 = await register('bob', publicKey, regKeyBob1.s, regKeyBob1.sig, encryptedHashedPasswordBob1) + should(res3).equal(true, 'register should allow new entry (bob)') + }) + + it('getContractSalt', async () => { + const keyPair = nacl.box.keyPair() + const eNonce = nacl.randomBytes(nacl.box.nonceLength) + const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') + const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') + + const [contract, hash, r] = ['getContractSalt', 'hash', 'r'] + const regKey = await registrationKey(contract, publicKeyHash) + should(regKey).be.of.type('object') + + const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword) + should(res).equal(true, 'register should allow new entry (' + contract + ')') + + const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)) + const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url') + const contractSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('CONTRACTSALT')), dhKey]))).slice(0, 18).toString('base64url') + + const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url') + const challenge = await getChallenge(contract, b) + should(challenge).be.of.type('object', 'challenge should be object') + should(challenge.authSalt).equal(authSalt, 'mismatched authSalt') + + const ħ = nacl.hash(Buffer.concat([nacl.hash(Buffer.from(r)), nacl.hash(Buffer.from(challenge.s))])) + const c = nacl.hash(Buffer.concat([nacl.hash(Buffer.from(hash)), nacl.hash(ħ)])) + const hc = nacl.hash(c) + + const salt = await getContractSalt(contract, r, challenge.s, challenge.sig, Buffer.from(hc).toString('base64url')) + should(salt).be.of.type('string', 'salt response should be string') + + const saltBuf = Buffer.from(salt, 'base64url') + const nonce = saltBuf.slice(0, nacl.secretbox.nonceLength) + const encryptionKey = nacl.hash(Buffer.concat([Buffer.from('CS'), c])).slice(0, nacl.secretbox.keyLength) + const retrievedContractSalt = Buffer.from(nacl.secretbox.open(saltBuf.slice(nacl.secretbox.nonceLength), nonce, encryptionKey)).toString() + should(retrievedContractSalt).equal(contractSalt, 'mismatched contractSalt') + }) + + it('update', async () => { + const keyPair = nacl.box.keyPair() + const eNonce = nacl.randomBytes(nacl.box.nonceLength) + const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') + const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') + + const [contract, hash, r] = ['update', 'hash', 'r'] + const regKey = await registrationKey(contract, publicKeyHash) + should(regKey).be.of.type('object') + + const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword) + should(res).equal(true, 'register should allow new entry (' + contract + ')') + + const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)) + const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url') + + const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url') + const challenge = await getChallenge(contract, b) + should(challenge).be.of.type('object', 'challenge should be object') + should(challenge.authSalt).equal(authSalt, 'mismatched authSalt') + + const ħ = nacl.hash(Buffer.concat([nacl.hash(Buffer.from(r)), nacl.hash(Buffer.from(challenge.s))])) + const c = nacl.hash(Buffer.concat([nacl.hash(Buffer.from(hash)), nacl.hash(ħ)])) + const hc = nacl.hash(c) + + const encryptionKey = nacl.hash(Buffer.concat([Buffer.from('SU'), c])).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + + const encryptedArgsCiphertext = nacl.secretbox(Buffer.from(JSON.stringify(['a', 'b', 'c'])), nonce, encryptionKey) + + const encryptedArgs = Buffer.concat([nonce, encryptedArgsCiphertext]).toString('base64url') + + const updateRes = await update(contract, r, challenge.s, challenge.sig, Buffer.from(hc).toString('base64url'), encryptedArgs) + should(updateRes).equal(true, 'update should be successful') + }) +}) From e9e3851c71817cd155477bf01c38cefb91d6ffab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Tue, 9 Aug 2022 01:18:57 +0200 Subject: [PATCH 006/411] ZKPP: Frontend implementation --- backend/zkppSalt.js | 62 ++++--------- backend/zkppSalt.test.js | 33 +++---- frontend/controller/actions/identity.js | 66 +++++++++++--- shared/zkpp.js | 111 ++++++++++++++++++++++++ 4 files changed, 204 insertions(+), 68 deletions(-) create mode 100644 shared/zkpp.js diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js index 107b2e0a11..c19965126b 100644 --- a/backend/zkppSalt.js +++ b/backend/zkppSalt.js @@ -2,6 +2,7 @@ import { blake32Hash } from '~/shared/functions.js' import { timingSafeEqual } from 'crypto' import nacl from 'tweetnacl' import sbp from '@sbp/sbp' +import { boxKeyPair, encryptContractSalt, hashStringArray, hashRawStringArray, hash, parseRegisterSalt, randomNonce, computeCAndHc } from '~/shared/zkpp.js' // TODO HARDCODED VALUES const recordPepper = 'pepper' @@ -10,14 +11,6 @@ const challengeSecret = 'secret' const registrationSecret = 'secret' const maxAge = 30 -const hashStringArray = (...args: Array) => { - return nacl.hash(Buffer.concat(args.map((s) => nacl.hash(Buffer.from(s))))) -} - -const hashRawStringArray = (...args: Array) => { - return nacl.hash(Buffer.concat(args.map((s) => Buffer.from(s)))) -} - const getZkppSaltRecord = async (contract: string) => { const recordId = blake32Hash(hashStringArray('RID', contract, recordPepper)) const record = await sbp('chelonia/db/get', recordId) @@ -25,7 +18,7 @@ const getZkppSaltRecord = async (contract: string) => { if (record) { const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength) - const recordBuf = Buffer.from(record, 'base64url') + const recordBuf = Buffer.from(record.replace(/_/g, '/').replace(/-/g, '+'), 'base64') const nonce = recordBuf.slice(0, nacl.secretbox.nonceLength) const recordCiphertext = recordBuf.slice(nacl.secretbox.nonceLength) const recordPlaintext = nacl.secretbox.open(recordCiphertext, nonce, encryptionKey) @@ -65,7 +58,7 @@ const setZkppSaltRecord = async (contract: string, hashedPassword: string, authS const recordPlaintext = JSON.stringify([hashedPassword, authSalt, contractSalt]) const recordCiphertext = nacl.secretbox(Buffer.from(recordPlaintext), nonce, encryptionKey) const recordBuf = Buffer.concat([nonce, recordCiphertext]) - const record = recordBuf.toString('base64url') + const record = recordBuf.toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') await sbp('chelonia/db/set', recordId, record) } @@ -75,9 +68,9 @@ export const getChallenge = async (contract: string, b: string): Promise { - const ħ = hashStringArray(r, s) - const c = hashStringArray(h, ħ) - const hc = nacl.hash(c) - const userHcBuf = Buffer.from(userHc, 'base64url') + const [c, hc] = computeCAndHc(r, s, h) + const userHcBuf = Buffer.from(userHc.replace(/_/g, '/').replace(/-/g, '+'), 'base64') if (hc.byteLength === userHcBuf.byteLength && timingSafeEqual(hc, userHcBuf)) { return c @@ -197,12 +178,7 @@ export const getContractSalt = async (contract: string, r: string, s: string, si return false } - const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength) - const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) - - const encryptedContractSalt = nacl.secretbox(Buffer.from(contractSalt), nonce, encryptionKey) - - return Buffer.concat([nonce, encryptedContractSalt]).toString('base64url') + return encryptContractSalt(c, contractSalt) } export const update = async (contract: string, r: string, s: string, sig: string, hc: string, encryptedArgs: string): Promise => { @@ -223,7 +199,7 @@ export const update = async (contract: string, r: string, s: string, sig: string } const encryptionKey = hashRawStringArray('SU', c).slice(0, nacl.secretbox.keyLength) - const encryptedArgsBuf = Buffer.from(encryptedArgs, 'base64url') + const encryptedArgsBuf = Buffer.from(encryptedArgs.replace(/_/g, '/').replace(/-/g, '+'), 'base64') const nonce = encryptedArgsBuf.slice(0, nacl.secretbox.nonceLength) const encrytedArgsCiphertext = encryptedArgsBuf.slice(nacl.secretbox.nonceLength) diff --git a/backend/zkppSalt.test.js b/backend/zkppSalt.test.js index 42a53ed3d4..3a13c7dbf9 100644 --- a/backend/zkppSalt.test.js +++ b/backend/zkppSalt.test.js @@ -6,10 +6,20 @@ import 'should-sinon' import { registrationKey, register, getChallenge, getContractSalt, update } from './zkppSalt.js' +const saltsAndEncryptedHashedPassword = (p: string, secretKey: Uint8Array, hash: string) => { + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + const dhKey = nacl.hash(nacl.box.before(Buffer.from(p, 'base64url'), secretKey)) + const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64') + const contractSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('CONTRACTSALT')), dhKey]))).slice(0, 18).toString('base64') + const encryptionKey = nacl.hash(Buffer.from(authSalt + contractSalt)).slice(0, nacl.secretbox.keyLength) + const encryptedHashedPassword = Buffer.concat([nonce, nacl.secretbox(Buffer.from(hash), nonce, encryptionKey)]).toString('base64url') + + return [authSalt, contractSalt, encryptedHashedPassword] +} + describe('ZKPP Salt functions', () => { it('register', async () => { const keyPair = nacl.box.keyPair() - const nonce = nacl.randomBytes(nacl.box.nonceLength) const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') @@ -17,24 +27,23 @@ describe('ZKPP Salt functions', () => { const regKeyAlice2 = await registrationKey('alice', publicKeyHash) should(regKeyAlice1).be.of.type('object') should(regKeyAlice2).be.of.type('object') - const encryptedHashedPasswordAlice1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice1.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const [, , encryptedHashedPasswordAlice1] = saltsAndEncryptedHashedPassword(regKeyAlice1.p, keyPair.secretKey, 'hash') const res1 = await register('alice', publicKey, regKeyAlice1.s, regKeyAlice1.sig, encryptedHashedPasswordAlice1) should(res1).equal(true, 'register should allow new entry (alice)') - const encryptedHashedPasswordAlice2 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyAlice2.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const [, , encryptedHashedPasswordAlice2] = saltsAndEncryptedHashedPassword(regKeyAlice1.p, keyPair.secretKey, 'hash') const res2 = await register('alice', publicKey, regKeyAlice2.s, regKeyAlice2.sig, encryptedHashedPasswordAlice2) should(res2).equal(false, 'register should not overwrite entry (alice)') const regKeyBob1 = await registrationKey('bob', publicKeyHash) should(regKeyBob1).be.of.type('object') - const encryptedHashedPasswordBob1 = Buffer.concat([nonce, nacl.box(Buffer.from('hash'), nonce, Buffer.from(regKeyBob1.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const [, , encryptedHashedPasswordBob1] = saltsAndEncryptedHashedPassword(regKeyBob1.p, keyPair.secretKey, 'hash') const res3 = await register('bob', publicKey, regKeyBob1.s, regKeyBob1.sig, encryptedHashedPasswordBob1) should(res3).equal(true, 'register should allow new entry (bob)') }) it('getContractSalt', async () => { const keyPair = nacl.box.keyPair() - const eNonce = nacl.randomBytes(nacl.box.nonceLength) const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') @@ -42,14 +51,11 @@ describe('ZKPP Salt functions', () => { const regKey = await registrationKey(contract, publicKeyHash) should(regKey).be.of.type('object') - const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const [authSalt, contractSalt, encryptedHashedPassword] = saltsAndEncryptedHashedPassword(regKey.p, keyPair.secretKey, hash) + const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword) should(res).equal(true, 'register should allow new entry (' + contract + ')') - const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)) - const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url') - const contractSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('CONTRACTSALT')), dhKey]))).slice(0, 18).toString('base64url') - const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url') const challenge = await getChallenge(contract, b) should(challenge).be.of.type('object', 'challenge should be object') @@ -71,7 +77,6 @@ describe('ZKPP Salt functions', () => { it('update', async () => { const keyPair = nacl.box.keyPair() - const eNonce = nacl.randomBytes(nacl.box.nonceLength) const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') @@ -79,13 +84,11 @@ describe('ZKPP Salt functions', () => { const regKey = await registrationKey(contract, publicKeyHash) should(regKey).be.of.type('object') - const encryptedHashedPassword = Buffer.concat([eNonce, nacl.box(Buffer.from('hash'), eNonce, Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)]).toString('base64url') + const [authSalt, , encryptedHashedPassword] = saltsAndEncryptedHashedPassword(regKey.p, keyPair.secretKey, hash) + const res = await register(contract, publicKey, regKey.s, regKey.sig, encryptedHashedPassword) should(res).equal(true, 'register should allow new entry (' + contract + ')') - const dhKey = nacl.hash(nacl.box.before(Buffer.from(regKey.p, 'base64url'), keyPair.secretKey)) - const authSalt = Buffer.from(nacl.hash(Buffer.concat([nacl.hash(Buffer.from('AUTHSALT')), dhKey]))).slice(0, 18).toString('base64url') - const b = Buffer.from(nacl.hash(Buffer.from(r))).toString('base64url') const challenge = await getChallenge(contract, b) should(challenge).be.of.type('object', 'challenge should be object') diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index 715f9e4373..a1056289d7 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -12,11 +12,10 @@ import { LOGIN, LOGOUT } from '~/frontend/utils/events.js' import './mailbox.js' import { encryptedAction } from './utils.js' +import { handleFetchResult } from '../utils/misc.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' import type { GIKey } from '~/shared/domains/chelonia/GIMessage.js' - -// eslint-disable-next-line camelcase -const salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC = 'SALT CHANGEME' +import { boxKeyPair, buildRegisterSaltRequest, computeCAndHc, decryptContractSalt, hash, hashPassword, randomNonce } from '~/shared/zkpp.js' function generatedLoginState () { const { contracts } = sbp('state/vuex/state') @@ -36,8 +35,25 @@ function diffLoginStates (s1: ?Object, s2: ?Object) { export default (sbp('sbp/selectors/register', { 'gi.actions/identity/retrieveSalt': async (username: string, password: string) => { - // TODO RETRIEVE FROM SERVER - return await Promise.resolve(salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC) + const r = randomNonce() + const b = hash(r) + const authHash = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}/auth_hash?b=${encodeURIComponent(b)}`) + .then(handleFetchResult('json')) + + const { authSalt, s, sig } = authHash + + const h = await hashPassword(password, authSalt) + + const [c, hc] = computeCAndHc(r, s, h) + + const contractHash = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}/contract_hash?${(new URLSearchParams({ + 'r': r, + 's': s, + 'sig': sig, + 'hc': Buffer.from(hc).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') + })).toString()}`).then(handleFetchResult('text')) + + return decryptContractSalt(c, contractHash) }, 'gi.actions/identity/create': async function ({ data: { username, email, password, picture }, @@ -69,12 +85,42 @@ export default (sbp('sbp/selectors/register', { const mailbox = await sbp('gi.actions/mailbox/create', { options: { sync: true } }) const mailboxID = mailbox.contractID() + const keyPair = boxKeyPair() + const r = Buffer.from(keyPair.publicKey).toString('base64').replace(/\//g, '_').replace(/\+/g, '-') + const b = hash(r) + const registrationRes = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: `b=${encodeURIComponent(b)}` + }) + .then(handleFetchResult('json')) + + const { p, s, sig } = registrationRes + + const [contractSalt, Eh] = await buildRegisterSaltRequest(p, keyPair.secretKey, password) + + const res = await fetch(`${sbp('okTurtles.data/get', 'API_URL')}/zkpp/user=${encodeURIComponent(username)}`, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + 'r': r, + 's': s, + 'sig': sig, + 'Eh': Eh + }) + }) + + if (!res.ok) { + throw new Error('Unable to register hash') + } + // Create the necessary keys to initialise the contract - // TODO: The salt needs to be dynamically generated - // eslint-disable-next-line camelcase - const salt = salt_TODO_CHANGEME_NEEDS_TO_BE_DYNAMIC - const IPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, password, salt) - const IEK = await deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, password, salt) + const IPK = await deriveKeyFromPassword(EDWARDS25519SHA512BATCH, password, contractSalt) + const IEK = await deriveKeyFromPassword(CURVE25519XSALSA20POLY1305, password, contractSalt) const CSK = keygen(EDWARDS25519SHA512BATCH) const CEK = keygen(CURVE25519XSALSA20POLY1305) diff --git a/shared/zkpp.js b/shared/zkpp.js new file mode 100644 index 0000000000..e41a6bd761 --- /dev/null +++ b/shared/zkpp.js @@ -0,0 +1,111 @@ +import nacl from 'tweetnacl' +import scrypt from 'scrypt-async' + +export const hashStringArray = (...args: Array): Uint8Array => { + return nacl.hash(Buffer.concat(args.map((s) => nacl.hash(Buffer.from(s))))) +} + +export const hashRawStringArray = (...args: Array): Uint8Array => { + return nacl.hash(Buffer.concat(args.map((s) => Buffer.from(s)))) +} + +export const randomNonce = (): string => { + return Buffer.from(nacl.randomBytes(12)).toString('base64').replace(/\//g, '_').replace(/\+/g, '-') +} + +export const hash = (v: string): string => { + return Buffer.from(nacl.hash(Buffer.from(v))).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') +} + +export const computeCAndHc = (r: string, s: string, h: string): [Uint8Array, Uint8Array] => { + const ħ = hashStringArray(r, s) + const c = hashStringArray(h, ħ) + const hc = nacl.hash(c) + + return [c, hc] +} + +export const encryptContractSalt = (c: Uint8Array, contractSalt: string): string => { + const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength) + const nonce = nacl.randomBytes(nacl.secretbox.nonceLength) + + const encryptedContractSalt = nacl.secretbox(Buffer.from(contractSalt), nonce, encryptionKey) + + return Buffer.concat([nonce, encryptedContractSalt]).toString('base64').replace(/\//g, '_').replace(/\+/g, '-') +} + +export const decryptContractSalt = (c: Uint8Array, encryptedContractSaltBox: string): string => { + const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength) + const encryptedContractSaltBoxBuf = Buffer.from(encryptedContractSaltBox.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + + const nonce = encryptedContractSaltBoxBuf.slice(0, nacl.secretbox.nonceLength) + const encryptedContractSalt = encryptedContractSaltBoxBuf.slice(nacl.secretbox.nonceLength) + + return Buffer.from(nacl.secretbox.open(encryptedContractSalt, nonce, encryptionKey)).toString() +} + +export const hashPassword = (password: string, salt: string): Promise => { + return new Promise(resolve => scrypt(password, salt, { + N: 16384, + r: 8, + p: 1, + dkLen: 32, + encoding: 'hex' + }, resolve)) +} + +export const boxKeyPair = (): any => { + return nacl.box.keyPair() +} + +export const saltAgreement = (publicKey: string, secretKey: Uint8Array): false | [string, string] => { + const publicKeyBuf = Buffer.from(publicKey.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const dhKey = nacl.box.before(publicKeyBuf, secretKey) + + if (!publicKeyBuf || publicKeyBuf.byteLength !== nacl.box.publicKeyLength) { + return false + } + + const authSalt = Buffer.from(hashStringArray('AUTHSALT', dhKey)).slice(0, 18).toString('base64') + const contractSalt = Buffer.from(hashStringArray('CONTRACTSALT', dhKey)).slice(0, 18).toString('base64') + + return [authSalt, contractSalt] +} + +export const parseRegisterSalt = (publicKey: string, secretKey: Uint8Array, encryptedHashedPassword: string): false | [string, string, Uint8Array] => { + const saltAgreementRes = saltAgreement(publicKey, secretKey) + if (!saltAgreementRes) { + return false + } + + const [authSalt, contractSalt] = saltAgreementRes + + const encryptionKey = nacl.hash(Buffer.from(authSalt + contractSalt)).slice(0, nacl.secretbox.keyLength) + const encryptedHashedPasswordBuf = Buffer.from(encryptedHashedPassword.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + + const hashedPasswordBuf = nacl.secretbox.open(encryptedHashedPasswordBuf.slice(nacl.box.nonceLength), encryptedHashedPasswordBuf.slice(0, nacl.box.nonceLength), encryptionKey) + + if (!hashedPasswordBuf) { + return false + } + + return [authSalt, contractSalt, hashedPasswordBuf] +} + +export const buildRegisterSaltRequest = async (publicKey: string, secretKey: Uint8Array, password: string): Promise<[string, string]> => { + const saltAgreementRes = saltAgreement(publicKey, secretKey) + if (!saltAgreementRes) { + throw new Error('Invalid public or secret key') + } + + const [authSalt, contractSalt] = saltAgreementRes + + const hashedPassword = await hashPassword(password, authSalt) + + const nonce = nacl.randomBytes(nacl.box.nonceLength) + const encryptionKey = nacl.hash(Buffer.from(authSalt + contractSalt)).slice(0, nacl.secretbox.keyLength) + + const encryptedHashedPasswordBuf = nacl.secretbox(Buffer.from(hashedPassword), nonce, encryptionKey) + + return [contractSalt, Buffer.concat([nonce, encryptedHashedPasswordBuf]).toString('base64').replace(/\//g, '_').replace(/\+/g, '-')] +} From 1051639e64ea3292fddb56ffcdc1e03d3c804175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Mon, 15 Aug 2022 14:18:27 +0200 Subject: [PATCH 007/411] Prevent Grunt from running --- Gruntfile.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 40eed34676..0eb58308e8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,7 @@ 'use strict' +if (process.env['CI']) process.exit(1) + // ======================= // Entry point. // From e86df63a015a3fc3e56fe98fc9063f62f9f55644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Mon, 22 Aug 2022 15:48:17 +0200 Subject: [PATCH 008/411] OP_KEY_REQUEST --- frontend/controller/actions/group.js | 8 +++ frontend/model/contracts/chatroom.js | 1 + shared/domains/chelonia/GIMessage.js | 12 +++- shared/domains/chelonia/chelonia.js | 44 ++++++++++++- shared/domains/chelonia/internals.js | 97 ++++++++++++++++++++++++++-- 5 files changed, 155 insertions(+), 7 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 63c169250b..4f601f771d 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -25,6 +25,7 @@ import { dateToPeriodStamp, addTimeToDate, DAYS_MILLIS } from '@model/contracts/ import { encryptedAction } from './utils.js' import { GIMessage } from '~/shared/domains/chelonia/chelonia.js' import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js' +import type { GIKey } from '~/shared/domains/chelonia/GIMessage.js' import type { GIActionParams } from './types.js' export async function leaveAllChatRooms (groupContractID: string, member: string) { @@ -227,6 +228,13 @@ export default (sbp('sbp/selectors/register', { throw new GIErrorUIRuntimeError(L('Failed to create the group: {reportError}', LError(e))) } }, + 'gi.contracts/group/getShareableKeys': async function (contractID) { + const state = await sbp('chelonia/latestContractState', contractID) + return { + signingKeyId: (((Object.values(Object(state?._vm?.authorizedKeys)): any): GIKey[]).find((k) => k?.meta?.type === 'csk')?.id: ?string), + keys: state._volatile.keys + } + }, 'gi.actions/group/createAndSwitch': async function (params: GIActionParams) { const message = await sbp('gi.actions/group/create', params) sbp('gi.actions/group/switch', message.contractID()) diff --git a/frontend/model/contracts/chatroom.js b/frontend/model/contracts/chatroom.js index 9e936577d9..fbbd04c56f 100644 --- a/frontend/model/contracts/chatroom.js +++ b/frontend/model/contracts/chatroom.js @@ -20,6 +20,7 @@ import { objectOf, string, optional } from '~/frontend/model/contracts/misc/flow function createNotificationData ( notificationType: string, + moreParams: Object = {} ): Object { return { diff --git a/shared/domains/chelonia/GIMessage.js b/shared/domains/chelonia/GIMessage.js index 3af012f29f..0706a52696 100644 --- a/shared/domains/chelonia/GIMessage.js +++ b/shared/domains/chelonia/GIMessage.js @@ -24,9 +24,15 @@ export type GIOpKeyAdd = GIKey[] export type GIOpKeyDel = string[] export type GIOpPropSet = { key: string, value: JSONType } export type GIOpKeyShare = { contractID: string, keys: GIKey[] } +export type GIOpKeyRequest = { + keyId: string; + encryptionKeyId: string; + data: string; +} +export type GIOpKeyRequestResponse = string -export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' | 'ks' -export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpKeyDel | GIOpPropSet | GIOpKeyShare +export type GIOpType = 'c' | 'ae' | 'au' | 'ka' | 'kd' | 'pu' | 'ps' | 'pd' | 'ks' | 'kr' | 'krr' +export type GIOpValue = GIOpContract | GIOpActionEncrypted | GIOpActionUnencrypted | GIOpKeyAdd | GIOpKeyDel | GIOpPropSet | GIOpKeyShare | GIOpKeyRequest | GIOpKeyRequestResponse export type GIOp = [GIOpType, GIOpValue] export class GIMessage { @@ -50,6 +56,8 @@ export class GIMessage { static OP_CONTRACT_DEAUTH: 'cd' = 'cd' // deauthorize a contract static OP_ATOMIC: 'at' = 'at' // atomic op static OP_KEYSHARE: 'ks' = 'ks' // key share + static OP_KEY_REQUEST: 'kr' = 'kr' // key request + static OP_KEY_REQUEST_RESPONSE: 'krr' = 'krr' // key request response // eslint-disable-next-line camelcase static createV1_0 ( diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index 810d034eb2..8f00bda302 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -12,7 +12,7 @@ import { handleFetchResult } from '~/frontend/controller/utils/misc.js' // TODO: rename this to ChelMessage import { GIMessage } from './GIMessage.js' import { ChelErrorUnrecoverable } from './errors.js' -import type { GIKey, GIOpContract, GIOpActionUnencrypted, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare } from './GIMessage.js' +import type { GIKey, GIOpContract, GIOpActionUnencrypted, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare, GIOpKeyRequestResponse } from './GIMessage.js' import { keyId, sign, encrypt, decrypt, generateSalt } from './crypto.js' // TODO: define ChelContractType for /defineContract @@ -89,6 +89,19 @@ export type ChelKeyShareParams = { publishOptions?: { maxAttempts: number }; } +export type ChelKeyRequestResponseParams = { + contractName: string; + contractID: string; + data: GIOpKeyRequestResponse; + signingKeyId: string; + hooks?: { + prepublishContract?: (GIMessage) => void; + prepublish?: (GIMessage) => void; + postpublish?: (GIMessage) => void; + }; + publishOptions?: { maxAttempts: number }; +} + export { GIMessage } export const ACTION_REGEX: RegExp = /^((([\w.]+)\/([^/]+))(?:\/(?:([^/]+)\/)?)?)\w*/ @@ -621,6 +634,35 @@ export default (sbp('sbp/selectors/register', { hooks && hooks.postpublish && hooks.postpublish(msg) return msg }, + 'chelonia/out/keyRequestResponse': async function (params: ChelKeyRequestResponseParams): Promise { + const { contractID, contractName, data, hooks, publishOptions } = params + const manifestHash = this.config.contracts.manifests[contractName] + const contract = this.manifestToContract[manifestHash]?.contract + if (!contract) { + throw new Error('Contract name not found') + } + const state = contract.state(contractID) + const previousHEAD = await sbp('chelonia/private/out/latestHash', contractID) + const meta = contract.metadata.create() + const gProxy = gettersProxy(state, contract.getters) + contract.metadata.validate(meta, { state, ...gProxy, contractID }) + const payload = (data: GIOpKeyRequestResponse) + const signingKey = this.env.additionalKeys?.[params.signingKeyId] || state?._volatile?.keys[params.signingKeyId] + const msg = GIMessage.createV1_0({ + contractID, + previousHEAD, + op: [ + GIMessage.OP_KEY_REQUEST_RESPONSE, + payload + ], + manifest: manifestHash, + signatureFn: signingKey ? signatureFnBuilder(signingKey) : undefined + }) + hooks && hooks.prepublish && hooks.prepublish(msg) + await sbp('chelonia/private/out/publishEvent', msg, publishOptions) + hooks && hooks.postpublish && hooks.postpublish(msg) + return msg + }, 'chelonia/out/protocolUpgrade': async function () { }, diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index ba776b1e13..e139864ca8 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -2,8 +2,8 @@ import sbp, { domainFromSelector } from '@sbp/sbp' import './db.js' -import { decrypt, verifySignature } from './crypto.js' -import type { GIKey, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpContract, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare, GIOpPropSet, GIOpType } from './GIMessage.js' +import { encrypt, decrypt, verifySignature } from './crypto.js' +import type { GIKey, GIOpActionEncrypted, GIOpActionUnencrypted, GIOpContract, GIOpKeyAdd, GIOpKeyDel, GIOpKeyShare, GIOpPropSet, GIOpType, GIOpKeyRequest, GIOpKeyRequestResponse } from './GIMessage.js' import { GIMessage } from './GIMessage.js' import { randomIntFromRange, delay, cloneDeep, debounce, pick } from '~/frontend/model/contracts/shared/giLodash.js' import { ChelErrorUnexpected, ChelErrorUnrecoverable } from './errors.js' @@ -198,6 +198,7 @@ export default (sbp('sbp/selectors/register', { } }, [GIMessage.OP_KEYSHARE] (v: GIOpKeyShare) { + // TODO: Prompt to user if contract not in pending if (message.originatingContractID() !== contractID && v.contractID !== message.originatingContractID()) { throw new Error('External contracts can only set keys for themselves') } @@ -228,6 +229,19 @@ export default (sbp('sbp/selectors/register', { } } }, + [GIMessage.OP_KEY_REQUEST] (v: GIOpKeyRequest) { + if (!state._vm.pending_key_requests) state._vm.pending_key_requests = Object.create(null) + state._vm.pending_key_requests[message.hash()] = [ + message.originatingContractID(), + message.head().previousHEAD, + v + ] + }, + [GIMessage.OP_KEY_REQUEST_RESPONSE] (v: GIOpKeyRequestResponse) { + if (state._vm.pending_key_requests && v in state._vm.pending_key_requests) { + delete state._vm.pending_key_requests[v] + } + }, [GIMessage.OP_PROP_DEL]: notImplemented, [GIMessage.OP_PROP_SET] (v: GIOpPropSet) { if (!state._vm.props) state._vm.props = {} @@ -346,6 +360,7 @@ export default (sbp('sbp/selectors/register', { console.debug(`[chelonia] contract ${contractID} was already synchronized`) } sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) + await sbp('chelonia/private/respondToKeyRequests', contractID) } catch (e) { console.error(`[chelonia] syncContract error: ${e.message}`, e) sbp('okTurtles.events/emit', CONTRACT_IS_SYNCING, contractID, false) @@ -353,6 +368,80 @@ export default (sbp('sbp/selectors/register', { throw e } }, + 'chelonia/private/respondToKeyRequests': async function (contractID: string) { + const state = sbp(this.config.stateSelector) + const contractState = state.contracts[contractID] ?? {} + + if (!contractState._vm || !contractState._vm.pending_key_requests) { + return + } + + const pending = contractState._vm.pending_key_requests + + delete contractState._vm.pending_key_requests + + await Promise.all(Object.entries(pending).map(async ([hash, entry]) => { + if (!Array.isArray(entry) || entry.length !== 3) { + return + } + + const [originatingContractID, previousHEAD, v] = ((entry: any): [string, string, GIOpKeyRequest]) + + // 1. Sync (originating) identity contract + await sbp('okTurtles.eventQueue/queueEvent', originatingContractID, [ + 'chelonia/private/in/syncContract', originatingContractID + ]) + + const contractName = this.state.contracts[contractID].type + const recipientContractName = this.state.contracts[originatingContractID].type + + try { + // 2. Verify 'data' + const { data, keyId, encryptionKeyId } = v + + const originatingState = sbp(self.config.stateSelector)[originatingContractID] + + const signingKey = originatingState._vm.authorizedKeys[keyId] + + if (!signingKey) { + throw new Error('Unable to find signing key') + } + + // sign(originatingContractID + GIMessage.OP_KEY_REQUEST + contractID + HEAD) + verifySignature(signingKey, [originatingContractID, GIMessage.OP_KEY_REQUEST, contractID, previousHEAD].map(encodeURIComponent).join('|'), data) + + const encryptionKey = originatingState._vm.authorizedKeys[encryptionKeyId] + + if (!encryptionKey) { + throw new Error('Unable to find encryption key') + } + + const { keys, signingKeyId } = sbp(`${contractName}/getShareableKeys`, contractID) + + // 3. Send OP_KEYSHARE to identity contract + await sbp('chelonia/out/keyShare', { + destinationContractID: originatingContractID, + destinationContractName: recipientContractName, + data: { + contractID: contractID, + keys: Object.entries(keys).map(([keyId, key]: [string, mixed]) => ({ + id: keyId, + meta: { + private: { + keyId: encryptionKeyId, + content: encrypt(encryptionKey, (key: any)) + } + } + })) + }, + signingKeyId + }) + } finally { + // 4. Update contract with information + await sbp('chelonia/out/keyRequestResponse', { contractID, contractName, data: hash }) + } + })) + }, 'chelonia/private/in/handleEvent': async function (message: GIMessage) { const state = sbp(this.config.stateSelector) const contractID = message.contractID() @@ -535,11 +624,11 @@ function loadScript (file, source, hash) { // script.type = 'application/javascript' script.type = 'module' // problem with this is that scripts will step on each other's feet - script.innerHTML = source + script.text = source // NOTE: this will work if the file route adds .header('Content-Type', 'application/javascript') // script.src = `${this.config.connectionURL}/file/${hash}` // this results in: "SyntaxError: import declarations may only appear at top level of a module" - // script.innerHTML = `(function () { + // script.text = `(function () { // ${source} // })()` script.onload = () => resolve(script) From 158b35df6e3a6de155ba2e766fb18af2d74f4165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Mon, 12 Sep 2022 16:54:43 +0200 Subject: [PATCH 009/411] Feedback on ZKPP and started moving invites into Chelonia --- backend/zkppSalt.js | 47 ++++++++++++++----- backend/zkppSalt.test.js | 6 +-- frontend/controller/actions/chatroom.js | 4 +- frontend/controller/actions/group.js | 38 ++++++++++----- frontend/controller/actions/identity.js | 8 ++-- frontend/controller/actions/mailbox.js | 4 +- frontend/model/contracts/group.js | 8 ++-- frontend/model/contracts/manifests.json | 4 +- frontend/model/contracts/shared/functions.js | 27 +---------- .../group-settings/InvitationLinkModal.vue | 12 ++++- .../group-settings/InvitationsTable.vue | 2 +- .../proposals/ProposalVoteOptions.vue | 5 +- frontend/views/pages/Join.vue | 4 +- shared/domains/chelonia/chelonia.js | 9 ++-- shared/domains/chelonia/internals.js | 20 +++++++- shared/zkpp.js | 18 ++++--- test/backend.test.js | 10 ++-- 17 files changed, 131 insertions(+), 95 deletions(-) diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js index c19965126b..8d53207e2f 100644 --- a/backend/zkppSalt.js +++ b/backend/zkppSalt.js @@ -2,9 +2,10 @@ import { blake32Hash } from '~/shared/functions.js' import { timingSafeEqual } from 'crypto' import nacl from 'tweetnacl' import sbp from '@sbp/sbp' -import { boxKeyPair, encryptContractSalt, hashStringArray, hashRawStringArray, hash, parseRegisterSalt, randomNonce, computeCAndHc } from '~/shared/zkpp.js' +import { boxKeyPair, encryptContractSalt, hashStringArray, hashRawStringArray, hash, parseRegisterSalt, randomNonce, computeCAndHc, base64urlToBase64, base64ToBase64url } from '~/shared/zkpp.js' // TODO HARDCODED VALUES +// These values will eventually come from server the configuration const recordPepper = 'pepper' const recordMasterKey = 'masterKey' const challengeSecret = 'secret' @@ -18,7 +19,7 @@ const getZkppSaltRecord = async (contract: string) => { if (record) { const encryptionKey = hashStringArray('REK', contract, recordMasterKey).slice(0, nacl.secretbox.keyLength) - const recordBuf = Buffer.from(record.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const recordBuf = Buffer.from(base64urlToBase64(record), 'base64') const nonce = recordBuf.slice(0, nacl.secretbox.nonceLength) const recordCiphertext = recordBuf.slice(nacl.secretbox.nonceLength) const recordPlaintext = nacl.secretbox.open(recordCiphertext, nonce, encryptionKey) @@ -33,6 +34,7 @@ const getZkppSaltRecord = async (contract: string) => { const recordObj = JSON.parse(recordString) if (!Array.isArray(recordObj) || recordObj.length !== 3 || !recordObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { + console.log('Error validating encryped JSON object ' + recordId) return null } @@ -44,6 +46,7 @@ const getZkppSaltRecord = async (contract: string) => { contractSalt } } catch { + console.log('Error parsing encrypted JSON object ' + recordId) // empty } } @@ -58,19 +61,20 @@ const setZkppSaltRecord = async (contract: string, hashedPassword: string, authS const recordPlaintext = JSON.stringify([hashedPassword, authSalt, contractSalt]) const recordCiphertext = nacl.secretbox(Buffer.from(recordPlaintext), nonce, encryptionKey) const recordBuf = Buffer.concat([nonce, recordCiphertext]) - const record = recordBuf.toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') + const record = base64ToBase64url(recordBuf.toString('base64')) await sbp('chelonia/db/set', recordId, record) } export const getChallenge = async (contract: string, b: string): Promise => { const record = await getZkppSaltRecord(contract) if (!record) { + console.debug('getChallenge: Error obtaining ZKPP salt record for contract ID ' + contract) return false } const { authSalt } = record const s = randomNonce() const now = (Date.now() / 1000 | 0).toString(16) - const sig = [now, Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '')].join(',') + const sig = [now, base64ToBase64url(Buffer.from(hashStringArray(contract, b, s, now, challengeSecret)).toString('base64'))].join(',') return { authSalt, @@ -96,7 +100,7 @@ const verifyChallenge = (contract: string, r: string, s: string, userSig: string const b = hash(r) const sig = hashStringArray(contract, b, s, then, challengeSecret) - const macBuf = Buffer.from(mac.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const macBuf = Buffer.from(base64urlToBase64(mac), 'base64') return sig.byteLength === macBuf.byteLength && timingSafeEqual(sig, macBuf) } @@ -110,35 +114,43 @@ export const registrationKey = async (contract: string, b: string): Promise => { if (!verifyChallenge(contract, clientPublicKey, encryptedSecretKey, userSig)) { + console.debug('register: Error validating challenge: ' + JSON.stringify({ contract, clientPublicKey, userSig })) return false } const record = await getZkppSaltRecord(contract) if (record) { + console.debug('register: Error obtaining ZKPP salt record for contract ID ' + contract) return false } - const encryptedSecretKeyBuf = Buffer.from(encryptedSecretKey.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const encryptedSecretKeyBuf = Buffer.from(base64urlToBase64(encryptedSecretKey), 'base64') const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength) const secretKeyBuf = nacl.secretbox.open(encryptedSecretKeyBuf.slice(nacl.secretbox.nonceLength), encryptedSecretKeyBuf.slice(0, nacl.secretbox.nonceLength), encryptionKey) + if (!secretKeyBuf) { + console.debug(`register: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`) + return false + } + const parseRegisterSaltRes = parseRegisterSalt(clientPublicKey, secretKeyBuf, encryptedHashedPassword) if (!parseRegisterSaltRes) { + console.debug(`register: Error parsing registration salt for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`) return false } @@ -151,7 +163,7 @@ export const register = async (contract: string, clientPublicKey: string, encryp const contractSaltVerifyC = (h: string, r: string, s: string, userHc: string) => { const [c, hc] = computeCAndHc(r, s, h) - const userHcBuf = Buffer.from(userHc.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const userHcBuf = Buffer.from(base64urlToBase64(userHc), 'base64') if (hc.byteLength === userHcBuf.byteLength && timingSafeEqual(hc, userHcBuf)) { return c @@ -162,11 +174,13 @@ const contractSaltVerifyC = (h: string, r: string, s: string, userHc: string) => export const getContractSalt = async (contract: string, r: string, s: string, sig: string, hc: string): Promise => { if (!verifyChallenge(contract, r, s, sig)) { + console.debug('getContractSalt: Error validating challenge: ' + JSON.stringify({ contract, r, s, sig })) return false } const record = await getZkppSaltRecord(contract) if (!record) { + console.debug('getContractSalt: Error obtaining ZKPP salt record for contract ID ' + contract) return false } @@ -175,6 +189,7 @@ export const getContractSalt = async (contract: string, r: string, s: string, si const c = contractSaltVerifyC(hashedPassword, r, s, hc) if (!c) { + console.debug(`getContractSalt: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } @@ -183,11 +198,13 @@ export const getContractSalt = async (contract: string, r: string, s: string, si export const update = async (contract: string, r: string, s: string, sig: string, hc: string, encryptedArgs: string): Promise => { if (!verifyChallenge(contract, r, s, sig)) { + console.debug('update: Error validating challenge: ' + JSON.stringify({ contract, r, s, sig })) return false } const record = await getZkppSaltRecord(contract) if (!record) { + console.debug('update: Error obtaining ZKPP salt record for contract ID ' + contract) return false } const { hashedPassword } = record @@ -195,17 +212,19 @@ export const update = async (contract: string, r: string, s: string, sig: string const c = contractSaltVerifyC(hashedPassword, r, s, hc) if (!c) { + console.debug(`update: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } const encryptionKey = hashRawStringArray('SU', c).slice(0, nacl.secretbox.keyLength) - const encryptedArgsBuf = Buffer.from(encryptedArgs.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const encryptedArgsBuf = Buffer.from(base64urlToBase64(encryptedArgs), 'base64') const nonce = encryptedArgsBuf.slice(0, nacl.secretbox.nonceLength) - const encrytedArgsCiphertext = encryptedArgsBuf.slice(nacl.secretbox.nonceLength) + const encryptedArgsCiphertext = encryptedArgsBuf.slice(nacl.secretbox.nonceLength) - const args = nacl.secretbox.open(encrytedArgsCiphertext, nonce, encryptionKey) + const args = nacl.secretbox.open(encryptedArgsCiphertext, nonce, encryptionKey) if (!args) { + console.debug(`update: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } @@ -213,6 +232,7 @@ export const update = async (contract: string, r: string, s: string, sig: string const argsObj = JSON.parse(Buffer.from(args).toString()) if (!Array.isArray(argsObj) || argsObj.length !== 3 || !argsObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { + console.debug(`update: Error validating the encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } @@ -222,6 +242,7 @@ export const update = async (contract: string, r: string, s: string, sig: string return true } catch { + console.debug(`update: Error parsing encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) // empty } diff --git a/backend/zkppSalt.test.js b/backend/zkppSalt.test.js index 3a13c7dbf9..12e1cac75d 100644 --- a/backend/zkppSalt.test.js +++ b/backend/zkppSalt.test.js @@ -18,7 +18,7 @@ const saltsAndEncryptedHashedPassword = (p: string, secretKey: Uint8Array, hash: } describe('ZKPP Salt functions', () => { - it('register', async () => { + it('register() conforms to the API to register a new salt', async () => { const keyPair = nacl.box.keyPair() const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') @@ -42,7 +42,7 @@ describe('ZKPP Salt functions', () => { should(res3).equal(true, 'register should allow new entry (bob)') }) - it('getContractSalt', async () => { + it('getContractSalt() conforms to the API to obtain salt', async () => { const keyPair = nacl.box.keyPair() const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') @@ -75,7 +75,7 @@ describe('ZKPP Salt functions', () => { should(retrievedContractSalt).equal(contractSalt, 'mismatched contractSalt') }) - it('update', async () => { + it('update() conforms to the API to update salt', async () => { const keyPair = nacl.box.keyPair() const publicKey = Buffer.from(keyPair.publicKey).toString('base64url') const publicKeyHash = Buffer.from(nacl.hash(Buffer.from(publicKey))).toString('base64url') diff --git a/frontend/controller/actions/chatroom.js b/frontend/controller/actions/chatroom.js index f564d0cc30..699e3fe0a8 100644 --- a/frontend/controller/actions/chatroom.js +++ b/frontend/controller/actions/chatroom.js @@ -67,7 +67,7 @@ export default (sbp('sbp/selectors/register', { contractName: 'gi.contracts/chatroom' }) - const chatroom = await sbp('chelonia/with-env', '', { + const chatroom = await sbp('chelonia/withEnv', '', { additionalKeys: { [CSKid]: CSK, [CEKid]: CEK @@ -110,7 +110,7 @@ export default (sbp('sbp/selectors/register', { const contractID = chatroom.contractID() - await sbp('chelonia/with-env', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) + await sbp('chelonia/withEnv', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) const userID = rootState.loggedIn.identityContractID await sbp('gi.actions/identity/shareKeysWithSelf', { userID, contractID }) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 4f601f771d..75ef781aac 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -3,7 +3,6 @@ import sbp from '@sbp/sbp' // Using relative path to crypto.js instead of ~-path to workaround some esbuild bug import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, serializeKey, encrypt } from '../../../shared/domains/chelonia/crypto.js' -import { createInvite } from '@model/contracts/shared/functions.js' import { GIErrorUIRuntimeError, L, LError } from '@common/common.js' import { INVITE_INITIAL_CREATOR, @@ -85,27 +84,26 @@ export default (sbp('sbp/selectors/register', { // eslint-disable-next-line camelcase const CSK = keygen(EDWARDS25519SHA512BATCH) const CEK = keygen(CURVE25519XSALSA20POLY1305) + const inviteKey = keygen(EDWARDS25519SHA512BATCH) // Key IDs const CSKid = keyId(CSK) const CEKid = keyId(CEK) + const inviteKeyId = keyId(inviteKey) // Public keys to be stored in the contract const CSKp = serializeKey(CSK, false) const CEKp = serializeKey(CEK, false) + const inviteKeyP = serializeKey(inviteKey, false) // Secret keys to be stored encrypted in the contract const CSKs = encrypt(CEK, serializeKey(CSK, true)) const CEKs = encrypt(CEK, serializeKey(CEK, true)) + const inviteKeyS = encrypt(CEK, serializeKey(inviteKey, true)) const rootState = sbp('state/vuex/state') try { - const initialInvite = createInvite({ - quantity: 60, - creator: INVITE_INITIAL_CREATOR, - expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING - }) const proposalSettings = { rule: ruleName, ruleSettings: { @@ -121,10 +119,11 @@ export default (sbp('sbp/selectors/register', { // handle Flowtype annotations, even though our .babelrc should make it work. distributionDate = dateToPeriodStamp(addTimeToDate(new Date(), 3 * DAYS_MILLIS)) } - const message = await sbp('chelonia/with-env', '', { + const message = await sbp('chelonia/withEnv', '', { additionalKeys: { [CSKid]: CSK, - [CEKid]: CEK + [CEKid]: CEK, + [inviteKeyId]: inviteKey } }, ['chelonia/out/registerContract', { contractName: 'gi.contracts/group', @@ -158,12 +157,25 @@ export default (sbp('sbp/selectors/register', { content: CEKs } } + }, + { + id: inviteKeyId, + type: inviteKey.type, + data: inviteKeyP, + permissions: [GIMessage.OP_KEY_REQUEST], + meta: { + type: 'inviteKey', + quantity: 60, + creator: INVITE_INITIAL_CREATOR, + expires: Date.now() + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING, + private: { + keyId: CEKid, + content: inviteKeyS + } + } } ], data: { - invites: { - [initialInvite.inviteSecret]: initialInvite - }, settings: { // authorizations: [contracts.CanModifyAuths.dummyAuth()], // TODO: this groupName: name, @@ -201,11 +213,11 @@ export default (sbp('sbp/selectors/register', { const contractID = message.contractID() - await sbp('chelonia/with-env', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) + await sbp('chelonia/withEnv', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) saveLoginState('creating', contractID) // create a 'General' chatroom contract and let the creator join - await sbp('chelonia/with-env', contractID, { additionalKeys: { [CEKid]: CEK } }, ['gi.actions/group/addAndJoinChatRoom', { + await sbp('chelonia/withEnv', contractID, { additionalKeys: { [CEKid]: CEK } }, ['gi.actions/group/addAndJoinChatRoom', { contractID, data: { attributes: { diff --git a/frontend/controller/actions/identity.js b/frontend/controller/actions/identity.js index de19fbfe22..568e3dbb0f 100644 --- a/frontend/controller/actions/identity.js +++ b/frontend/controller/actions/identity.js @@ -140,7 +140,7 @@ export default (sbp('sbp/selectors/register', { let userID // next create the identity contract itself and associate it with the mailbox try { - const user = await sbp('chelonia/with-env', '', { + const user = await sbp('chelonia/withEnv', '', { additionalKeys: { [IPKid]: IPK, [CSKid]: CSK, @@ -205,8 +205,8 @@ export default (sbp('sbp/selectors/register', { userID = user.contractID() - await sbp('chelonia/with-env', userID, { additionalKeys: { [IEKid]: IEK } }, ['chelonia/contract/sync', userID]) - await sbp('chelonia/with-env', userID, { additionalKeys: { [IEKid]: IEK } }, ['gi.actions/identity/setAttributes', { + await sbp('chelonia/withEnv', userID, { additionalKeys: { [IEKid]: IEK } }, ['chelonia/contract/sync', userID]) + await sbp('chelonia/withEnv', userID, { additionalKeys: { [IEKid]: IEK } }, ['gi.actions/identity/setAttributes', { contractID: userID, data: { mailbox: mailboxID }, signingKeyId: CSKid, @@ -378,7 +378,7 @@ export default (sbp('sbp/selectors/register', { sbp('state/vuex/commit', 'login', { username, identityContractID }) // IMPORTANT: we avoid using 'await' on the syncs so that Vue.js can proceed // loading the website instead of stalling out. - sbp('chelonia/with-env', identityContractID, { additionalKeys }, ['chelonia/contract/sync', contractIDs]).then(async function () { + sbp('chelonia/withEnv', identityContractID, { additionalKeys }, ['chelonia/contract/sync', contractIDs]).then(async function () { // contract sync might've triggered an async call to /remove, so wait before proceeding await sbp('chelonia/contract/wait', contractIDs) // similarly, since removeMember may have triggered saveOurLoginState asynchronously, diff --git a/frontend/controller/actions/mailbox.js b/frontend/controller/actions/mailbox.js index 328e31d388..9f06e490cc 100644 --- a/frontend/controller/actions/mailbox.js +++ b/frontend/controller/actions/mailbox.js @@ -29,7 +29,7 @@ export default (sbp('sbp/selectors/register', { const CSKs = encrypt(CEK, serializeKey(CSK, true)) const CEKs = encrypt(CEK, serializeKey(CEK, true)) - const mailbox = await sbp('chelonia/with-env', '', { + const mailbox = await sbp('chelonia/withEnv', '', { additionalKeys: { [CSKid]: CSK, [CEKid]: CEK @@ -73,7 +73,7 @@ export default (sbp('sbp/selectors/register', { console.log('gi.actions/mailbox/create', { mailbox }) const contractID = mailbox.contractID() if (sync) { - await sbp('chelonia/with-env', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) + await sbp('chelonia/withEnv', contractID, { additionalKeys: { [CEKid]: CEK } }, ['chelonia/contract/sync', contractID]) } return mailbox } catch (e) { diff --git a/frontend/model/contracts/group.js b/frontend/model/contracts/group.js index 5cd9056683..329c27daaf 100644 --- a/frontend/model/contracts/group.js +++ b/frontend/model/contracts/group.js @@ -14,7 +14,7 @@ import { addTimeToDate, dateToPeriodStamp, dateFromPeriodStamp, isPeriodStamp, c import { unadjustedDistribution, adjustedDistribution } from './shared/distribution/distribution.js' import currencies, { saferFloat } from './shared/currencies.js' import { inviteType, chatRoomAttributesType } from './shared/types.js' -import { arrayOf, mapOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js' +import { arrayOf, objectOf, objectMaybeOf, optional, string, number, boolean, object, unionOf, tupleOf } from '~/frontend/model/contracts/misc/flowTyper.js' function vueFetchInitKV (obj: Object, key: string, initialValue: any): any { let value = obj[key] @@ -269,7 +269,7 @@ sbp('chelonia/defineContract', { return getters.groupMembersByUsername.length }, groupMembersPending (state, getters) { - const invites = getters.currentGroupState.invites + const invites = getters.currentGroupState._vm.invites const pendingMembers = {} for (const inviteId in invites) { const invite = invites[inviteId] @@ -389,7 +389,6 @@ sbp('chelonia/defineContract', { // this is the constructor 'gi.contracts/group': { validate: objectMaybeOf({ - invites: mapOf(string, inviteType), settings: objectMaybeOf({ // TODO: add 'groupPubkey' groupName: string, @@ -414,7 +413,6 @@ sbp('chelonia/defineContract', { const initialState = merge({ payments: {}, paymentsByPeriod: {}, - invites: {}, proposals: {}, // hashes => {} TODO: this, see related TODOs in GroupProposal settings: { groupCreator: meta.username, @@ -772,7 +770,7 @@ sbp('chelonia/defineContract', { inviteSecret: string // NOTE: simulate the OP_KEY_* stuff for now })(data) - if (!state.invites[data.inviteSecret]) { + if (!state._vm.invites[data.inviteSecret]) { throw new TypeError(L('The link does not exist.')) } }, diff --git a/frontend/model/contracts/manifests.json b/frontend/model/contracts/manifests.json index 26a30cd0d1..9d6b407ec3 100644 --- a/frontend/model/contracts/manifests.json +++ b/frontend/model/contracts/manifests.json @@ -1,7 +1,7 @@ { "manifests": { - "gi.contracts/chatroom": "21XWnNRhSzQ6PmBC6KKmz52xXAv3s5WEopuCoF9GxWnV2A3rwe", - "gi.contracts/group": "21XWnNJkGh4JEBXhbEz1QZWfi5CsnD3HkU3x3ZcBZeRvN82SQF", + "gi.contracts/chatroom": "21XWnNMKa4YUogqvc2A5kfHFC39742U1KtQUzYKmcC2nVRvXfP", + "gi.contracts/group": "21XWnNGyZotAKkMzfYQ6RwKJtRcNExFQ71g1cdeXUogMyToCoM", "gi.contracts/identity": "21XWnNPSaj9HtLJmg7DMGc3cAHdqcmReuJfCvuM8CroxYd2bxx", "gi.contracts/mailbox": "21XWnNKoSm7e2vnGJvoFSRB7heFTeg24FwDCkb6JAazkzhpNdY" } diff --git a/frontend/model/contracts/shared/functions.js b/frontend/model/contracts/shared/functions.js index d62288ad49..8a0d2bc30f 100644 --- a/frontend/model/contracts/shared/functions.js +++ b/frontend/model/contracts/shared/functions.js @@ -1,8 +1,7 @@ 'use strict' import sbp from '@sbp/sbp' -import { INVITE_STATUS, MESSAGE_TYPES } from './constants.js' -import { DAYS_MILLIS } from './time.js' +import { MESSAGE_TYPES } from './constants.js' import { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js' // !!!!!!!!!!!!!!! @@ -20,30 +19,6 @@ import { logExceptNavigationDuplicated } from '~/frontend/views/utils/misc.js' // DIRECTLY IN YOUR CONTRACT DEFINITION FILE. THEN YOU CAN MODIFY // THEM AS MUCH AS YOU LIKE (and generate new contract versions out of them). -// group.js related - -export function createInvite ({ quantity = 1, creator, expires, invitee }: { - quantity: number, creator: string, expires: number, invitee?: string -}): {| - creator: string, - expires: number, - inviteSecret: string, - invitee: void | string, - quantity: number, - responses: {...}, - status: string, -|} { - return { - inviteSecret: `${parseInt(Math.random() * 10000)}`, // TODO: this - quantity, - creator, - invitee, - status: INVITE_STATUS.VALID, - responses: {}, // { bob: true } list of usernames that accepted the invite. - expires: Date.now() + DAYS_MILLIS * expires - } -} - // chatroom.js related export function createMessage ({ meta, data, hash, state }: { diff --git a/frontend/views/containers/group-settings/InvitationLinkModal.vue b/frontend/views/containers/group-settings/InvitationLinkModal.vue index 145b08e1ee..da26741eca 100644 --- a/frontend/views/containers/group-settings/InvitationLinkModal.vue +++ b/frontend/views/containers/group-settings/InvitationLinkModal.vue @@ -19,6 +19,7 @@ import ModalTemplate from '@components/modal/ModalTemplate.vue' import LinkToCopy from '@components/LinkToCopy.vue' import { INVITE_INITIAL_CREATOR } from '@model/contracts/shared/constants.js' import { buildInvitationUrl } from '@model/contracts/shared/voting/proposals.js' +import { serializeKey } from '../../../../shared/domains/chelonia/crypto.js' export default ({ name: 'InvitationLinkModal', @@ -31,8 +32,15 @@ export default ({ 'currentGroupState' ]), welcomeInviteSecret () { - const invites = this.currentGroupState.invites - return Object.keys(invites).find(invite => invites[invite].creator === INVITE_INITIAL_CREATOR) + const invites = this.currentGroupState._vm.invites + console.log({ invites }) + const initialInvite = Object.keys(invites).find(invite => invites[invite].creator === INVITE_INITIAL_CREATOR) + console.log({ initialInvite }) + const key = this.currentGroupState._volatile.keys[initialInvite] + console.log({ key }) + if (key) { + return serializeKey(key, true) + } }, link () { return buildInvitationUrl(this.$store.state.currentGroupId, this.welcomeInviteSecret) diff --git a/frontend/views/containers/group-settings/InvitationsTable.vue b/frontend/views/containers/group-settings/InvitationsTable.vue index f9f43e2efb..43ee9280d7 100644 --- a/frontend/views/containers/group-settings/InvitationsTable.vue +++ b/frontend/views/containers/group-settings/InvitationsTable.vue @@ -152,7 +152,7 @@ export default ({ 'currentGroupId' ]), invitesToShow () { - const { invites } = this.currentGroupState + const { invites } = this.currentGroupState._vm if (!invites) { return [] } diff --git a/frontend/views/containers/proposals/ProposalVoteOptions.vue b/frontend/views/containers/proposals/ProposalVoteOptions.vue index 7f536c53dd..b8c8c09d45 100644 --- a/frontend/views/containers/proposals/ProposalVoteOptions.vue +++ b/frontend/views/containers/proposals/ProposalVoteOptions.vue @@ -29,7 +29,6 @@ import { L } from '@common/common.js' import { VOTE_FOR, VOTE_AGAINST } from '@model/contracts/shared/voting/rules.js' import { oneVoteToPass } from '@model/contracts/shared/voting/proposals.js' import { PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER } from '@model/contracts/shared/constants.js' -import { createInvite } from '@model/contracts/shared/functions.js' import ButtonSubmit from '@components/ButtonSubmit.vue' import { leaveAllChatRooms } from '@controller/actions/group.js' @@ -107,11 +106,11 @@ export default ({ if (oneVoteToPass(proposalHash)) { if (this.type === PROPOSAL_INVITE_MEMBER) { - passPayload = createInvite({ + /* passPayload = createInvite({ invitee: this.proposal.data.proposalData.member, creator: this.proposal.meta.username, expires: this.currentGroupState.settings.inviteExpiryProposal - }) + }) */ } else if (this.type === PROPOSAL_REMOVE_MEMBER) { passPayload = { secret: `${parseInt(Math.random() * 10000)}` // TODO: this diff --git a/frontend/views/pages/Join.vue b/frontend/views/pages/Join.vue index 4ad31dba15..2c69aad84e 100644 --- a/frontend/views/pages/Join.vue +++ b/frontend/views/pages/Join.vue @@ -115,7 +115,9 @@ export default ({ return } const state = await sbp('chelonia/latestContractState', this.ephemeral.query.groupId) - const invite = state.invites[this.ephemeral.query.secret] + // TODO: Derive public key from secret + const publicKey = this.ephemeral.query.secret + const invite = state._vm.invites[publicKey] if (!invite || invite.status !== INVITE_STATUS.VALID) { console.error('Join.vue error: Link is not valid.') this.ephemeral.errorMsg = L('You should ask for a new one. Sorry about that!') diff --git a/shared/domains/chelonia/chelonia.js b/shared/domains/chelonia/chelonia.js index 8f00bda302..0076432bd6 100644 --- a/shared/domains/chelonia/chelonia.js +++ b/shared/domains/chelonia/chelonia.js @@ -209,11 +209,11 @@ export default (sbp('sbp/selectors/register', { return stack } }, - 'chelonia/with-env': async function (contractID: string, env: Object, sbpInvocation: Array<*>) { + 'chelonia/withEnv': async function (contractID: string, env: Object, sbpInvocation: Array<*>) { const savedEnv = this.env this.env = env try { - return await sbp('okTurtles.eventQueue/queueEvent', `chelonia/with-env/${contractID}`, sbpInvocation) + return await sbp('okTurtles.eventQueue/queueEvent', `chelonia/withEnv/${contractID}`, sbpInvocation) } finally { this.env = savedEnv } @@ -450,7 +450,10 @@ export default (sbp('sbp/selectors/register', { }, 'chelonia/latestContractState': async function (contractID: string) { const events = await sbp('chelonia/private/out/eventsSince', contractID, contractID) - let state = cloneDeep(sbp(this.config.stateSelector)[contractID] || Object.create(null)) + if (sbp(this.config.stateSelector)[contractID]) { + return cloneDeep(sbp(this.config.stateSelector)[contractID]) + } + let state = Object.create(null) // fast-path try { for (const event of events) { diff --git a/shared/domains/chelonia/internals.js b/shared/domains/chelonia/internals.js index e139864ca8..33e4515ce2 100644 --- a/shared/domains/chelonia/internals.js +++ b/shared/domains/chelonia/internals.js @@ -163,7 +163,7 @@ export default (sbp('sbp/selectors/register', { const signedPayload = message.signedPayload() const env = this.env const self = this - if (!state._vm) state._vm = {} + if (!state._vm) state._vm = Object.create(null) const opFns: { [GIOpType]: (any) => void } = { [GIMessage.OP_CONTRACT] (v: GIOpContract) { const keys = { ...env.additionalKeys, ...state._volatile?.keys } @@ -180,6 +180,14 @@ export default (sbp('sbp/selectors/register', { } } } + if (key.meta?.type === 'inviteKey') { + if (!state._vm.invites) state._vm.invites = Object.create(null) + state._vm.invites[key.id] = { + creator: key.meta.creator, + quantity: key.meta.quantity, + expires: key.meta.expires + } + } } }, [GIMessage.OP_ACTION_ENCRYPTED] (v: GIOpActionEncrypted) { @@ -236,6 +244,7 @@ export default (sbp('sbp/selectors/register', { message.head().previousHEAD, v ] + // TODO: Update count on _vm.invites }, [GIMessage.OP_KEY_REQUEST_RESPONSE] (v: GIOpKeyRequestResponse) { if (state._vm.pending_key_requests && v in state._vm.pending_key_requests) { @@ -264,6 +273,14 @@ export default (sbp('sbp/selectors/register', { } } } + if (key.meta?.type === 'inviteKey') { + if (state._vm.invites) state._vm.invites = Object.create(null) + state._vm.invites[key.id] = { + creator: key.meta.creator, + quantity: key.meta.quantity, + expires: key.meta.expires + } + } } }, [GIMessage.OP_KEY_DEL] (v: GIOpKeyDel) { @@ -272,6 +289,7 @@ export default (sbp('sbp/selectors/register', { delete state._vm.authorizedKeys[v] if (state._volatile?.keys) { delete state._volatile.keys[v] } }) + // TODO: Revoke invite keys if (key.meta?.type === 'inviteKey') }, [GIMessage.OP_PROTOCOL_UPGRADE]: notImplemented } diff --git a/shared/zkpp.js b/shared/zkpp.js index e41a6bd761..324274e596 100644 --- a/shared/zkpp.js +++ b/shared/zkpp.js @@ -1,6 +1,10 @@ import nacl from 'tweetnacl' import scrypt from 'scrypt-async' +export const base64ToBase64url = (s: string): string => s.replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') + +export const base64urlToBase64 = (s: string): string => s.replace(/_/g, '/').replace(/-/g, '+') + '='.repeat((4 - s.length % 4) % 4) + export const hashStringArray = (...args: Array): Uint8Array => { return nacl.hash(Buffer.concat(args.map((s) => nacl.hash(Buffer.from(s))))) } @@ -10,11 +14,11 @@ export const hashRawStringArray = (...args: Array): Uint8Ar } export const randomNonce = (): string => { - return Buffer.from(nacl.randomBytes(12)).toString('base64').replace(/\//g, '_').replace(/\+/g, '-') + return base64ToBase64url(Buffer.from(nacl.randomBytes(12)).toString('base64')) } export const hash = (v: string): string => { - return Buffer.from(nacl.hash(Buffer.from(v))).toString('base64').replace(/\//g, '_').replace(/\+/g, '-').replace(/=*$/, '') + return base64ToBase64url(Buffer.from(nacl.hash(Buffer.from(v))).toString('base64')) } export const computeCAndHc = (r: string, s: string, h: string): [Uint8Array, Uint8Array] => { @@ -31,12 +35,12 @@ export const encryptContractSalt = (c: Uint8Array, contractSalt: string): string const encryptedContractSalt = nacl.secretbox(Buffer.from(contractSalt), nonce, encryptionKey) - return Buffer.concat([nonce, encryptedContractSalt]).toString('base64').replace(/\//g, '_').replace(/\+/g, '-') + return base64ToBase64url(Buffer.concat([nonce, encryptedContractSalt]).toString('base64')) } export const decryptContractSalt = (c: Uint8Array, encryptedContractSaltBox: string): string => { const encryptionKey = hashRawStringArray('CS', c).slice(0, nacl.secretbox.keyLength) - const encryptedContractSaltBoxBuf = Buffer.from(encryptedContractSaltBox.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const encryptedContractSaltBoxBuf = Buffer.from(base64urlToBase64(encryptedContractSaltBox), 'base64') const nonce = encryptedContractSaltBoxBuf.slice(0, nacl.secretbox.nonceLength) const encryptedContractSalt = encryptedContractSaltBoxBuf.slice(nacl.secretbox.nonceLength) @@ -59,7 +63,7 @@ export const boxKeyPair = (): any => { } export const saltAgreement = (publicKey: string, secretKey: Uint8Array): false | [string, string] => { - const publicKeyBuf = Buffer.from(publicKey.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const publicKeyBuf = Buffer.from(base64urlToBase64(publicKey), 'base64') const dhKey = nacl.box.before(publicKeyBuf, secretKey) if (!publicKeyBuf || publicKeyBuf.byteLength !== nacl.box.publicKeyLength) { @@ -81,7 +85,7 @@ export const parseRegisterSalt = (publicKey: string, secretKey: Uint8Array, encr const [authSalt, contractSalt] = saltAgreementRes const encryptionKey = nacl.hash(Buffer.from(authSalt + contractSalt)).slice(0, nacl.secretbox.keyLength) - const encryptedHashedPasswordBuf = Buffer.from(encryptedHashedPassword.replace(/_/g, '/').replace(/-/g, '+'), 'base64') + const encryptedHashedPasswordBuf = Buffer.from(base64urlToBase64(encryptedHashedPassword), 'base64') const hashedPasswordBuf = nacl.secretbox.open(encryptedHashedPasswordBuf.slice(nacl.box.nonceLength), encryptedHashedPasswordBuf.slice(0, nacl.box.nonceLength), encryptionKey) @@ -107,5 +111,5 @@ export const buildRegisterSaltRequest = async (publicKey: string, secretKey: Uin const encryptedHashedPasswordBuf = nacl.secretbox(Buffer.from(hashedPassword), nonce, encryptionKey) - return [contractSalt, Buffer.concat([nonce, encryptedHashedPasswordBuf]).toString('base64').replace(/\//g, '_').replace(/\+/g, '-')] + return [contractSalt, base64ToBase64url(Buffer.concat([nonce, encryptedHashedPasswordBuf]).toString('base64'))] } diff --git a/test/backend.test.js b/test/backend.test.js index eaf5b23f43..393bdfaac1 100644 --- a/test/backend.test.js +++ b/test/backend.test.js @@ -10,8 +10,7 @@ import { blake32Hash } from '~/shared/functions.js' import * as Common from '@common/common.js' import proposals from '~/frontend/model/contracts/shared/voting/proposals.js' import { PAYMENT_PENDING, PAYMENT_TYPE_MANUAL } from '~/frontend/model/contracts/shared/payments/index.js' -import { INVITE_INITIAL_CREATOR, INVITE_EXPIRES_IN_DAYS, MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '~/frontend/model/contracts/shared/constants.js' -import { createInvite } from '~/frontend/model/contracts/shared/functions.js' +import { MAIL_TYPE_MESSAGE, PROPOSAL_INVITE_MEMBER, PROPOSAL_REMOVE_MEMBER, PROPOSAL_GROUP_SETTING_CHANGE, PROPOSAL_PROPOSAL_SETTING_CHANGE, PROPOSAL_GENERIC } from '~/frontend/model/contracts/shared/constants.js' import '~/frontend/controller/namespace.js' import chalk from 'chalk' import { THEME_LIGHT } from '~/frontend/utils/themes.js' @@ -138,18 +137,15 @@ describe('Full walkthrough', function () { return msg } function createGroup (name: string, hooks: Object = {}): Promise { - const initialInvite = createInvite({ + /* const initialInvite = createInvite({ quantity: 60, creator: INVITE_INITIAL_CREATOR, expires: INVITE_EXPIRES_IN_DAYS.ON_BOARDING - }) + }) */ return sbp('chelonia/out/registerContract', { contractName: 'gi.contracts/group', keys: [], data: { - invites: { - [initialInvite.inviteSecret]: initialInvite - }, settings: { // authorizations: [Events.CanModifyAuths.dummyAuth(name)], groupName: name, From c6b0dfb4105fa499f3105dd186d7667b8f1d314c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Mon, 19 Sep 2022 15:28:55 +0200 Subject: [PATCH 010/411] Error handling --- backend/routes.js | 68 ++++++++++++++++++++++++++++----------------- backend/zkppSalt.js | 34 +++++++++++++---------- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/backend/routes.js b/backend/routes.js index 8845d152cd..5e54a76045 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -264,23 +264,26 @@ route.POST('/zkpp/{contract}', { ]) } }, async function (req, h) { - if (req.payload['b']) { - const result = await registrationKey(req.params['contract'], req.payload['b']) - - if (!result) { - return Boom.internal('internal error') - } + try { + if (req.payload['b']) { + const result = await registrationKey(req.params['contract'], req.payload['b']) - return result - } else { - const result = await register(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['Eh']) + if (result) { + return result + } + } else { + const result = await register(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['Eh']) - if (!result) { - return Boom.internal('internal error') + if (result) { + return result + } } - - return result + } catch (e) { + const ip = req.info.remoteAddress + console.error(e.message, { ip }) } + + return Boom.internal('internal error') }) route.GET('/zkpp/{contract}/auth_hash', {}, async function (req, h) { @@ -288,13 +291,18 @@ route.GET('/zkpp/{contract}/auth_hash', {}, async function (req, h) { return Boom.badRequest('b query param required') } - const challenge = await getChallenge(req.params['contract'], req.query['b']) + try { + const challenge = await getChallenge(req.params['contract'], req.query['b']) - if (!challenge) { - return Boom.internal('internal error') + if (challenge) { + return challenge + } + } catch (e) { + const ip = req.info.remoteAddress + console.error(e.message, { ip }) } - return challenge + return Boom.internal('internal error') }) route.GET('/zkpp/{contract}/contract_hash', {}, async function (req, h) { @@ -314,13 +322,18 @@ route.GET('/zkpp/{contract}/contract_hash', {}, async function (req, h) { return Boom.badRequest('hc query param required') } - const salt = await getContractSalt(req.params['contract'], req.query['r'], req.query['s'], req.query['sig'], req.query['hc']) + try { + const salt = await getContractSalt(req.params['contract'], req.query['r'], req.query['s'], req.query['sig'], req.query['hc']) - if (!salt) { - return Boom.internal('internal error') + if (salt) { + return salt + } + } catch (e) { + const ip = req.info.remoteAddress + console.error(e.message, { ip }) } - return salt + return Boom.internal('internal error') }) route.PUT('/zkpp/{contract}', { @@ -334,11 +347,16 @@ route.PUT('/zkpp/{contract}', { }) } }, async function (req, h) { - const result = await update(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['hc'], req.payload['Ea']) + try { + const result = await update(req.params['contract'], req.payload['r'], req.payload['s'], req.payload['sig'], req.payload['hc'], req.payload['Ea']) - if (!result) { - return Boom.internal('internal error') + if (result) { + return result + } + } catch (e) { + const ip = req.info.remoteAddress + console.error(e.message, { ip }) } - return result + return Boom.internal('internal error') }) diff --git a/backend/zkppSalt.js b/backend/zkppSalt.js index 8d53207e2f..08d8c396a2 100644 --- a/backend/zkppSalt.js +++ b/backend/zkppSalt.js @@ -34,7 +34,7 @@ const getZkppSaltRecord = async (contract: string) => { const recordObj = JSON.parse(recordString) if (!Array.isArray(recordObj) || recordObj.length !== 3 || !recordObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { - console.log('Error validating encryped JSON object ' + recordId) + console.error('Error validating encrypted JSON object ' + recordId) return null } @@ -46,7 +46,7 @@ const getZkppSaltRecord = async (contract: string) => { contractSalt } } catch { - console.log('Error parsing encrypted JSON object ' + recordId) + console.error('Error parsing encrypted JSON object ' + recordId) // empty } } @@ -108,7 +108,7 @@ const verifyChallenge = (contract: string, r: string, s: string, userSig: string export const registrationKey = async (contract: string, b: string): Promise => { const record = await getZkppSaltRecord(contract) if (record) { - return false + throw new Error('registrationKey: User record already exists') } const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength) @@ -128,7 +128,7 @@ export const registrationKey = async (contract: string, b: string): Promise => { if (!verifyChallenge(contract, clientPublicKey, encryptedSecretKey, userSig)) { console.debug('register: Error validating challenge: ' + JSON.stringify({ contract, clientPublicKey, userSig })) - return false + throw new Error('register: Invalid challenge') } const record = await getZkppSaltRecord(contract) @@ -142,6 +142,7 @@ export const register = async (contract: string, clientPublicKey: string, encryp const encryptionKey = hashStringArray('REG', contract, registrationSecret).slice(0, nacl.secretbox.keyLength) const secretKeyBuf = nacl.secretbox.open(encryptedSecretKeyBuf.slice(nacl.secretbox.nonceLength), encryptedSecretKeyBuf.slice(0, nacl.secretbox.nonceLength), encryptionKey) + // Likely a bad implementation on the client side if (!secretKeyBuf) { console.debug(`register: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`) return false @@ -149,6 +150,7 @@ export const register = async (contract: string, clientPublicKey: string, encryp const parseRegisterSaltRes = parseRegisterSalt(clientPublicKey, secretKeyBuf, encryptedHashedPassword) + // Likely a bad implementation on the client side if (!parseRegisterSaltRes) { console.debug(`register: Error parsing registration salt for contract ID ${contract} (${JSON.stringify({ clientPublicKey, userSig })})`) return false @@ -175,12 +177,13 @@ const contractSaltVerifyC = (h: string, r: string, s: string, userHc: string) => export const getContractSalt = async (contract: string, r: string, s: string, sig: string, hc: string): Promise => { if (!verifyChallenge(contract, r, s, sig)) { console.debug('getContractSalt: Error validating challenge: ' + JSON.stringify({ contract, r, s, sig })) - return false + throw new Error('getContractSalt: Bad challenge') } const record = await getZkppSaltRecord(contract) if (!record) { - console.debug('getContractSalt: Error obtaining ZKPP salt record for contract ID ' + contract) + // This shouldn't happen at this stage as the record was already obtained + console.error('getContractSalt: Error obtaining ZKPP salt record for contract ID ' + contract) return false } @@ -189,8 +192,8 @@ export const getContractSalt = async (contract: string, r: string, s: string, si const c = contractSaltVerifyC(hashedPassword, r, s, hc) if (!c) { - console.debug(`getContractSalt: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) - return false + console.error(`getContractSalt: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) + throw new Error('getContractSalt: Bad challenge') } return encryptContractSalt(c, contractSalt) @@ -199,12 +202,13 @@ export const getContractSalt = async (contract: string, r: string, s: string, si export const update = async (contract: string, r: string, s: string, sig: string, hc: string, encryptedArgs: string): Promise => { if (!verifyChallenge(contract, r, s, sig)) { console.debug('update: Error validating challenge: ' + JSON.stringify({ contract, r, s, sig })) - return false + throw new Error('update: Bad challenge') } const record = await getZkppSaltRecord(contract) if (!record) { - console.debug('update: Error obtaining ZKPP salt record for contract ID ' + contract) + // This shouldn't happen at this stage as the record was already obtained + console.error('update: Error obtaining ZKPP salt record for contract ID ' + contract) return false } const { hashedPassword } = record @@ -212,8 +216,8 @@ export const update = async (contract: string, r: string, s: string, sig: string const c = contractSaltVerifyC(hashedPassword, r, s, hc) if (!c) { - console.debug(`update: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) - return false + console.error(`update: Error verifying challenge for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) + throw new Error('update: Bad challenge') } const encryptionKey = hashRawStringArray('SU', c).slice(0, nacl.secretbox.keyLength) @@ -224,7 +228,7 @@ export const update = async (contract: string, r: string, s: string, sig: string const args = nacl.secretbox.open(encryptedArgsCiphertext, nonce, encryptionKey) if (!args) { - console.debug(`update: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) + console.error(`update: Error decrypting arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } @@ -232,7 +236,7 @@ export const update = async (contract: string, r: string, s: string, sig: string const argsObj = JSON.parse(Buffer.from(args).toString()) if (!Array.isArray(argsObj) || argsObj.length !== 3 || !argsObj.reduce((acc, cv) => acc && typeof cv === 'string', true)) { - console.debug(`update: Error validating the encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) + console.error(`update: Error validating the encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) return false } @@ -242,7 +246,7 @@ export const update = async (contract: string, r: string, s: string, sig: string return true } catch { - console.debug(`update: Error parsing encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) + console.error(`update: Error parsing encrypted arguments for contract ID ${contract} (${JSON.stringify({ r, s, hc })})`) // empty } From 742902e96f084b7b2cc8f6712f548cbd8aad7ee5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Iv=C3=A1n=20Vieitez=20Parra?= Date: Mon, 19 Sep 2022 15:30:29 +0200 Subject: [PATCH 011/411] Implemented OP_KEY_REQUEST --- frontend/controller/actions/group.js | 8 +-- .../contracts/shared/voting/proposals.js | 2 +- .../group-settings/InvitationsTable.vue | 13 +++-- frontend/views/pages/Join.vue | 18 ++++-- shared/domains/chelonia/GIMessage.js | 2 + shared/domains/chelonia/chelonia.js | 56 ++++++++++++++++++- shared/domains/chelonia/internals.js | 8 ++- 7 files changed, 91 insertions(+), 16 deletions(-) diff --git a/frontend/controller/actions/group.js b/frontend/controller/actions/group.js index 75ef781aac..7e3b3eb954 100644 --- a/frontend/controller/actions/group.js +++ b/frontend/controller/actions/group.js @@ -26,6 +26,7 @@ import { GIMessage } from '~/shared/domains/chelonia/chelonia.js' import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js' import type { GIKey } from '~/shared/domains/chelonia/GIMessage.js' import type { GIActionParams } from './types.js' +import type { ChelKeyRequestParams } from '~/shared/domains/chelonia/chelonia.js' export async function leaveAllChatRooms (groupContractID: string, member: string) { // let user leaves all the chatrooms before leaving group @@ -252,15 +253,14 @@ export default (sbp('sbp/selectors/register', { sbp('gi.actions/group/switch', message.contractID()) return message }, - 'gi.actions/group/join': async function (params: $Exact) { + 'gi.actions/group/join': async function (params: $Exact & { options?: { skipInviteAccept: boolean } }) { try { sbp('okTurtles.data/set', 'JOINING_GROUP', true) // post acceptance event to the group contract, unless this is being called // by the loginState synchronization via the identity contract if (!params.options?.skipInviteAccept) { - await sbp('chelonia/out/actionEncrypted', { + await sbp('chelonia/out/keyRequest', { ...omit(params, ['options']), - action: 'gi.contracts/group/inviteAccept', hooks: { prepublish: params.hooks?.prepublish, postpublish: null @@ -322,7 +322,7 @@ export default (sbp('sbp/selectors/register', { throw new GIErrorUIRuntimeError(L('Failed to join the group: {codeError}', { codeError: e.message })) } }, - 'gi.actions/group/joinAndSwitch': async function (params: GIActionParams) { + 'gi.actions/group/joinAndSwitch': async function (params: $Exact & { options?: { skipInviteAccept: boolean } }) { await sbp('gi.actions/group/join', params) // after joining, we can set the current group sbp('gi.actions/group/switch', params.contractID) diff --git a/frontend/model/contracts/shared/voting/proposals.js b/frontend/model/contracts/shared/voting/proposals.js index 3645ed97f3..79703651d4 100644 --- a/frontend/model/contracts/shared/voting/proposals.js +++ b/frontend/model/contracts/shared/voting/proposals.js @@ -25,7 +25,7 @@ export function archiveProposal (state: Object, proposalHash: string): void { } export function buildInvitationUrl (groupId: string, inviteSecret: string): string { - return `${location.origin}/app/join?groupId=${groupId}&secret=${inviteSecret}` + return `${location.origin}/app/join?${new URLSearchParams({ groupId: groupId, secret: inviteSecret })}` } export const proposalSettingsType: any = objectOf({ diff --git a/frontend/views/containers/group-settings/InvitationsTable.vue b/frontend/views/containers/group-settings/InvitationsTable.vue index 43ee9280d7..4cf9cc495f 100644 --- a/frontend/views/containers/group-settings/InvitationsTable.vue +++ b/frontend/views/containers/group-settings/InvitationsTable.vue @@ -159,12 +159,17 @@ export default ({ const invitesList = Object.values(invites) .filter(invite => invite.creator === INVITE_INITIAL_CREATOR || invite.creator === this.ourUsername) .map(this.mapInvite) - const options = { + + return invitesList + + // TODO: Make active and all work + + /* const options = { Active: () => invitesList.filter(invite => invite.status.isActive || (invite.status.isRevoked && invite.inviteSecret === this.ephemeral.inviteRevokedNow)), All: () => invitesList - } + } */ - return options[this.ephemeral.selectbox.selectedOption]() + // return options[this.ephemeral.selectbox.selectedOption]() } }, methods: { @@ -225,7 +230,7 @@ export default ({ const isAnyoneLink = creator === INVITE_INITIAL_CREATOR const isInviteExpired = expiryTime < Date.now() const isInviteRevoked = status === INVITE_STATUS.REVOKED - const numberOfResponses = Object.keys(responses).length + const numberOfResponses = responses ? Object.keys(responses).length : 0 const isAllInviteUsed = numberOfResponses === quantity return { diff --git a/frontend/views/pages/Join.vue b/frontend/views/pages/Join.vue index 2c69aad84e..bbe83a3c71 100644 --- a/frontend/views/pages/Join.vue +++ b/frontend/views/pages/Join.vue @@ -48,7 +48,7 @@ div + + \ No newline at end of file diff --git a/backend/dashboard/main.js b/backend/dashboard/main.js new file mode 100644 index 0000000000..028af97165 --- /dev/null +++ b/backend/dashboard/main.js @@ -0,0 +1,66 @@ +import sbp from '@sbp/sbp' +import '@sbp/okturtles.data' +import '@sbp/okturtles.events' +import Vue from 'vue' +import router from './controller/router.js' +import store from './model/state.js' +import { initTheme } from './model/themes.js' +import './views/utils/vStyle.js' +import './views/utils/vError.js' +import './controller/backend.js' +import '@common/translations.js' + +// custom directive declarations +import './views/utils/custom-directives/index.js' + +// register lazy components +import './views/utils/lazyLoadComponents.js' + +// vue-components +import Modal from '@containers/modal/Modal.vue' +import Toolbar from '@containers/toolbar/Toolbar.vue' +import Navigation from '@containers/navigation/Navigation.vue' +import AppStyles from '@components/AppStyles.vue' + +Vue.config.errorHandler = function (err, vm, info) { + console.error(`uncaught Vue error in ${info}: `, err) +} + +async function startApp () { + sbp('okTurtles.data/set', 'API_URL', window.location.origin) + await sbp('translations/init', 'en-US' /* TODO!: switch back to navigator.language once the development is complete..! */) + + new Vue({ + router, + store, + components: { + Toolbar, + Navigation, + AppStyles, + Modal + }, + data () { + return { + isNavOpen: false + } + }, + computed: { + hideNavigation () { + return ['DesignSystem', 'Landing'].includes(this.$route.name) + }, + hideToolbar () { + return this.$route.name === 'Landing' + } + }, + methods: { + openNav () { + this.$refs.navigation.open() + } + }, + created () { + initTheme() + } + }).$mount('#app') +} + +startApp() diff --git a/backend/dashboard/model/state.js b/backend/dashboard/model/state.js new file mode 100644 index 0000000000..1f7d9a3d53 --- /dev/null +++ b/backend/dashboard/model/state.js @@ -0,0 +1,52 @@ +'use strict' + +import sbp from '@sbp/sbp' +import Vue from 'vue' +import Vuex from 'vuex' +import Colors, { THEME_LIGHT, storeThemeToLocalStorage } from './themes.js' +import { cloneDeep } from '@common/cdLodash.js' + +Vue.use(Vuex) + +const defaultTheme = THEME_LIGHT +const initialState = { + theme: defaultTheme +} + +const mutations = { + setTheme (state, theme) { + state.theme = theme + } +} + +const getters = { + colors (state) { + return Colors[state.theme] + } +} + +const actions = {} + +const store = new Vuex.Store({ + state: cloneDeep(initialState), + mutations, + getters, + actions +}) + +// watchers +store.watch( + state => state.theme, + (theme) => { + document.documentElement.dataset.theme = theme + storeThemeToLocalStorage(theme) + } +) + +sbp('sbp/selectors/register', { + 'state/vuex/state': () => store.state, + 'state/vuex/commit': (id, payload) => store.commit(id, payload), + 'state/vuex/getters': () => store.getters +}) + +export default store diff --git a/backend/dashboard/model/themes.js b/backend/dashboard/model/themes.js new file mode 100644 index 0000000000..dc190d091b --- /dev/null +++ b/backend/dashboard/model/themes.js @@ -0,0 +1,64 @@ +import store from './state.js' + +const common = { + black: '#000000', + white: '#FFFFFF', + text_black: '#1C1C1C', + primary_blue: '#E3F5FF', + primary_purple: '#E5ECF6', + secondary_purple_0: '#95A4FC', + secondary_purple_1: '#C6C7F8', + secondary_blue_0: '#A8C5DA', + secondary_blue_1: '#B1E3FF', + secondary_green_0: '#A1E3CB', + secondary_green_1: '#BAEDBD', + warning: '#FFE999', + danger: '#FF4747' +} + +const light = { + ...common, + background_0: '#F7F9FB', + background_1: '#FFFFFF', + background_active: 'rgba(0, 0, 0, 0.05)', + text_0: '#1C1C1C', + text_1: 'rgba(0, 0, 0, 0.4)', + border: 'rgba(0, 0, 0, 0.1)', + emphasis: '#1C1C1C', + helper: '#9747FF' +} + +const dark = { + ...common, + background_0: '#1C1C1C', + background_1: 'rgba(255, 255, 255, 0.05)', + background_active: 'rgba(255, 255, 255, 0.1)', + text_0: '#FFFFFF', + text_1: 'rgba(255, 255, 255, 0.45)', + border: 'rgba(255, 255, 255, 0.15)', + emphasis: '#C6C7F8', + helper: '#9747FF' +} + +const THEME_STORAGE_KEY = 'chelonia-dashboard-theme' +export const THEME_LIGHT = 'light' +export const THEME_DARK = 'dark' +export const checkSystemTheme = () => { + return window.matchMedia('(prefers-color-scheme: dark)').matches + ? THEME_DARK + : THEME_LIGHT +} +export const initTheme = () => { + // check if there is a value in local-storage that was stored from user previously toggling the theme in the app. + // if not, fallback to system preference, and lastly default to light-theme in case of no system preference in the browser. + const fromStorage = window.localStorage.getItem(THEME_STORAGE_KEY) + store.commit('setTheme', fromStorage || checkSystemTheme()) +} +export const storeThemeToLocalStorage = (theme) => { + window.localStorage.setItem(THEME_STORAGE_KEY, theme) +} + +export default { + light, + dark +} diff --git a/backend/dashboard/views/components/AppStyles.vue b/backend/dashboard/views/components/AppStyles.vue new file mode 100644 index 0000000000..9dcd80c3b8 --- /dev/null +++ b/backend/dashboard/views/components/AppStyles.vue @@ -0,0 +1,37 @@ + + + diff --git a/backend/dashboard/views/components/InfoCard.vue b/backend/dashboard/views/components/InfoCard.vue new file mode 100644 index 0000000000..cbd50d5568 --- /dev/null +++ b/backend/dashboard/views/components/InfoCard.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/backend/dashboard/views/components/PaymentMethods.vue b/backend/dashboard/views/components/PaymentMethods.vue new file mode 100644 index 0000000000..02b41867cb --- /dev/null +++ b/backend/dashboard/views/components/PaymentMethods.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/backend/dashboard/views/components/StatCard.vue b/backend/dashboard/views/components/StatCard.vue new file mode 100644 index 0000000000..47e703852b --- /dev/null +++ b/backend/dashboard/views/components/StatCard.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/backend/dashboard/views/components/TextToCopy.vue b/backend/dashboard/views/components/TextToCopy.vue new file mode 100644 index 0000000000..88c60551c0 --- /dev/null +++ b/backend/dashboard/views/components/TextToCopy.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/backend/dashboard/views/components/Tooltip.vue b/backend/dashboard/views/components/Tooltip.vue new file mode 100644 index 0000000000..2563299091 --- /dev/null +++ b/backend/dashboard/views/components/Tooltip.vue @@ -0,0 +1,119 @@ + + + + + diff --git a/backend/dashboard/views/components/forms/Dropdown.vue b/backend/dashboard/views/components/forms/Dropdown.vue new file mode 100644 index 0000000000..b7cc90fa83 --- /dev/null +++ b/backend/dashboard/views/components/forms/Dropdown.vue @@ -0,0 +1,230 @@ + + + + + diff --git a/backend/dashboard/views/components/forms/StyledInput.vue b/backend/dashboard/views/components/forms/StyledInput.vue new file mode 100644 index 0000000000..a26377e233 --- /dev/null +++ b/backend/dashboard/views/components/forms/StyledInput.vue @@ -0,0 +1,46 @@ + + + diff --git a/backend/dashboard/views/containers/modal/Modal.vue b/backend/dashboard/views/containers/modal/Modal.vue new file mode 100644 index 0000000000..244494b6bd --- /dev/null +++ b/backend/dashboard/views/containers/modal/Modal.vue @@ -0,0 +1,68 @@ + + + diff --git a/backend/dashboard/views/containers/modal/ModalTemplate.vue b/backend/dashboard/views/containers/modal/ModalTemplate.vue new file mode 100644 index 0000000000..a4fd133704 --- /dev/null +++ b/backend/dashboard/views/containers/modal/ModalTemplate.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/backend/dashboard/views/containers/modal/ViewContractManifestModal.vue b/backend/dashboard/views/containers/modal/ViewContractManifestModal.vue new file mode 100644 index 0000000000..1fd9c3aa7f --- /dev/null +++ b/backend/dashboard/views/containers/modal/ViewContractManifestModal.vue @@ -0,0 +1,105 @@ + + + + + diff --git a/backend/dashboard/views/containers/navigation/NavItem.vue b/backend/dashboard/views/containers/navigation/NavItem.vue new file mode 100644 index 0000000000..c3415c118e --- /dev/null +++ b/backend/dashboard/views/containers/navigation/NavItem.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/backend/dashboard/views/containers/navigation/Navigation.vue b/backend/dashboard/views/containers/navigation/Navigation.vue new file mode 100644 index 0000000000..481f42d11f --- /dev/null +++ b/backend/dashboard/views/containers/navigation/Navigation.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/backend/dashboard/views/containers/toolbar/Toolbar.vue b/backend/dashboard/views/containers/toolbar/Toolbar.vue new file mode 100644 index 0000000000..0a6e12ce09 --- /dev/null +++ b/backend/dashboard/views/containers/toolbar/Toolbar.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/backend/dashboard/views/pages/Accounts.vue b/backend/dashboard/views/pages/Accounts.vue new file mode 100644 index 0000000000..fe4f4ec423 --- /dev/null +++ b/backend/dashboard/views/pages/Accounts.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/backend/dashboard/views/pages/Billing.vue b/backend/dashboard/views/pages/Billing.vue new file mode 100644 index 0000000000..e80ea0932f --- /dev/null +++ b/backend/dashboard/views/pages/Billing.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/backend/dashboard/views/pages/Contracts.vue b/backend/dashboard/views/pages/Contracts.vue new file mode 100644 index 0000000000..86ee2e9de6 --- /dev/null +++ b/backend/dashboard/views/pages/Contracts.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/backend/dashboard/views/pages/Dashboard.vue b/backend/dashboard/views/pages/Dashboard.vue new file mode 100644 index 0000000000..2a97b448e3 --- /dev/null +++ b/backend/dashboard/views/pages/Dashboard.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/backend/dashboard/views/pages/PageTemplate.vue b/backend/dashboard/views/pages/PageTemplate.vue new file mode 100644 index 0000000000..4823586cb0 --- /dev/null +++ b/backend/dashboard/views/pages/PageTemplate.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/backend/dashboard/views/pages/Users.vue b/backend/dashboard/views/pages/Users.vue new file mode 100644 index 0000000000..c87077355b --- /dev/null +++ b/backend/dashboard/views/pages/Users.vue @@ -0,0 +1,148 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/CheloniaDesignSystem.vue b/backend/dashboard/views/pages/design-system/CheloniaDesignSystem.vue new file mode 100644 index 0000000000..a98f26483a --- /dev/null +++ b/backend/dashboard/views/pages/design-system/CheloniaDesignSystem.vue @@ -0,0 +1,125 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/design-system-content/ChelButtons.vue b/backend/dashboard/views/pages/design-system/design-system-content/ChelButtons.vue new file mode 100644 index 0000000000..6f0ba578ff --- /dev/null +++ b/backend/dashboard/views/pages/design-system/design-system-content/ChelButtons.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/design-system-content/ChelForms.vue b/backend/dashboard/views/pages/design-system/design-system-content/ChelForms.vue new file mode 100644 index 0000000000..4a74eba96c --- /dev/null +++ b/backend/dashboard/views/pages/design-system/design-system-content/ChelForms.vue @@ -0,0 +1,25 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/design-system-content/ChelIcons.vue b/backend/dashboard/views/pages/design-system/design-system-content/ChelIcons.vue new file mode 100644 index 0000000000..a8443d6edd --- /dev/null +++ b/backend/dashboard/views/pages/design-system/design-system-content/ChelIcons.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/design-system-content/ChelListsTables.vue b/backend/dashboard/views/pages/design-system/design-system-content/ChelListsTables.vue new file mode 100644 index 0000000000..cbd38d0048 --- /dev/null +++ b/backend/dashboard/views/pages/design-system/design-system-content/ChelListsTables.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/backend/dashboard/views/pages/design-system/design-system-content/ContentOutlet.vue b/backend/dashboard/views/pages/design-system/design-system-content/ContentOutlet.vue new file mode 100644 index 0000000000..c04292c664 --- /dev/null +++ b/backend/dashboard/views/pages/design-system/design-system-content/ContentOutlet.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/backend/dashboard/views/pages/miscellaneous/ErrorLoading.vue b/backend/dashboard/views/pages/miscellaneous/ErrorLoading.vue new file mode 100644 index 0000000000..1a2e7335ac --- /dev/null +++ b/backend/dashboard/views/pages/miscellaneous/ErrorLoading.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/backend/dashboard/views/pages/miscellaneous/Landing.vue b/backend/dashboard/views/pages/miscellaneous/Landing.vue new file mode 100644 index 0000000000..7660a7d5c4 --- /dev/null +++ b/backend/dashboard/views/pages/miscellaneous/Landing.vue @@ -0,0 +1,166 @@ + + + + + diff --git a/backend/dashboard/views/pages/miscellaneous/Loading.vue b/backend/dashboard/views/pages/miscellaneous/Loading.vue new file mode 100644 index 0000000000..ffbf50084d --- /dev/null +++ b/backend/dashboard/views/pages/miscellaneous/Loading.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/backend/dashboard/views/utils/3d-animation/animation-utils.js b/backend/dashboard/views/utils/3d-animation/animation-utils.js new file mode 100644 index 0000000000..6a38bfaeaa --- /dev/null +++ b/backend/dashboard/views/utils/3d-animation/animation-utils.js @@ -0,0 +1,149 @@ +import { + LineBasicMaterial, LineDashedMaterial, BufferGeometry, CircleGeometry, CylinderGeometry, + Group, Line, Vector3, MeshLambertMaterial, Mesh, EdgesGeometry, LineSegments, DoubleSide +} from 'three' + +class LineMesh extends Line { + constructor ({ + color = '#000', + dashed = false, + dashOpts = {}, + points = [] + }) { + const geometry = new BufferGeometry().setFromPoints( + points.map(({ x, y, z }) => new Vector3(x, y, z)) + ) + const material = dashed + ? new LineDashedMaterial({ color, ...Object.assign({ gapSize: 1, dashSize: 1 }, dashOpts) }) + : new LineBasicMaterial({ color }) + + super(geometry, material) + this.data = { geometry, material } + + if (dashed) { this.computeLineDistances() } + } +} + +class Axes extends Group { + constructor ({ length = 50, color = '#000000', dashed = false, dashOpts = {} }) { + const xAxis = new LineMesh({ + color, dashed, dashOpts, points: [{ x: 0, y: 0, z: 0 }, { x: length, y: 0, z: 0 }] + }) + const yAxis = new LineMesh({ + color, dashed, dashOpts, points: [{ x: 0, y: 0, z: 0 }, { x: 0, y: length, z: 0 }] + }) + const zAxis = new LineMesh({ + color, dashed, dashOpts, points: [{ x: 0, y: 0, z: 0 }, { x: 0, y: 0, z: length }] + }) + + super() + super.add(xAxis, yAxis, zAxis) + } +} + +class CombineWithEdge extends Group { + constructor ({ + mesh = null, geometry = null, material = null, + edgeColor = '#000000', shadow = false, edgeOpacity = 1 + }) { + const originalMesh = mesh || new Mesh(geometry, material) + if (mesh) { + geometry = mesh.geometry + material = mesh.material + } + originalMesh.castShadow = shadow + + // edges + const edges = new EdgesGeometry(geometry) + const edgesMesh = new LineSegments(edges, new LineBasicMaterial({ color: edgeColor, transparent: true, opacity: edgeOpacity })) + + super() + this.add(originalMesh, edgesMesh) + this.data = { geometry, material } + } +} + +class Edgify extends LineSegments { + constructor ({ + color = '#000000', geometry = null + }) { + const edgeGeometry = new EdgesGeometry(geometry) + const material = new LineBasicMaterial({ color }) + + super(edgeGeometry, material) + this.data = { geometry, material } + } +} + +class Column extends Group { + constructor ({ + sideColor = '#000000', + faceColor = '#000000', + edgeColor = '#000000', + radius = 1, + height = 1 + }) { + const MaterialCommon = MeshLambertMaterial + + const faceGeometry = new CircleGeometry(radius, 64, 0, 2 * Math.PI) + const faceMaterial = new MaterialCommon({ + color: faceColor, + side: DoubleSide, + transparent: true, + opacity: 1 + }) + const faceTop = new CombineWithEdge({ geometry: faceGeometry, material: faceMaterial, edgeColor }) + const faceBottom = new CombineWithEdge({ geometry: faceGeometry, material: faceMaterial }) + + faceTop.rotation.x = Math.PI / 2 * -1 + faceTop.position.set(0, height, 0) + faceBottom.rotation.x = Math.PI / 2 * -1 + + const cylinder = new Mesh( + new CylinderGeometry(radius, radius, height, 64, 1, true), + new MaterialCommon({ color: sideColor, side: DoubleSide, transparent: true, opacity: 1 }) + ) + cylinder.position.y = height / 2 + + super() + super.add(faceTop, faceBottom, cylinder) + } +} + +function resizeRendererToDisplaySize (renderer) { + const pixelRatio = window.devicePixelRatio || 1 + const canvasEl = renderer.domElement + const [desiredWidth, desiredHeight] = [canvasEl.clientWidth * pixelRatio, canvasEl.clientHeight * pixelRatio] + const needResize = canvasEl.width !== desiredWidth || canvasEl.height !== desiredHeight + + if (needResize) { + renderer.setSize(desiredWidth, desiredHeight, false) + } + + return needResize +} + +function adjustCameraAspect (canvasEl, camera) { + camera.aspect = canvasEl.clientWidth / canvasEl.clientHeight + camera.updateProjectionMatrix() +} + +function randomFromMinMax (min = 0, max = 0) { + return min + (max - min) * Math.random() +} + +function degreeToRadian (deg) { + return deg * Math.PI / 180 +} + +export { + resizeRendererToDisplaySize, + adjustCameraAspect, + randomFromMinMax, + degreeToRadian, + LineMesh, + CombineWithEdge, + Edgify, + Column, + Axes +} diff --git a/backend/dashboard/views/utils/3d-animation/landing/BarGraphs.js b/backend/dashboard/views/utils/3d-animation/landing/BarGraphs.js new file mode 100644 index 0000000000..9e9405f056 --- /dev/null +++ b/backend/dashboard/views/utils/3d-animation/landing/BarGraphs.js @@ -0,0 +1,56 @@ +import { Group, BoxGeometry, Mesh, MeshLambertMaterial } from 'three' +import { randomFromMinMax, CombineWithEdge } from '../animation-utils.js' +import GraphBackground from './GraphBackground.js' + +const BG_WIDTH = 28 +const BG_HEIGHT = 16 + +export default class BarGraphs extends Group { + constructor ({ + pairColors = ['#000000', '#000000'], + pairCount = 5, + isDarkTheme = false + } = {}) { + const MaterialCommon = MeshLambertMaterial + const bgThickness = 0.1 + const bgMesh = new GraphBackground({ + lineCount: 6, + bgColor: isDarkTheme ? '#1c1c1c' : '#f7f9fb', + edgeColor: isDarkTheme ? '#e3f5ff' : '#414141', + lineColor: isDarkTheme ? '#e3f5ff' : '#000000', + bgThickness + }) + + // graph background - width: 28, height: 16 + const unitW = BG_WIDTH / pairCount + const choppedW = (unitW / 2) / 5 + const barWidth = choppedW * 2 + const barThickness = 1 + const pairs = new Group() + + for (let i = 0; i < pairCount; i++) { + const pair = new Group() + const mat1 = new MaterialCommon({ color: pairColors[0] }) + const mat2 = new MaterialCommon({ color: pairColors[1] }) + const [h1, h2] = [ + randomFromMinMax(BG_HEIGHT / 4, BG_HEIGHT / 4 * 3), + randomFromMinMax(BG_HEIGHT / 4, BG_HEIGHT / 4 * 3) + ] + const geo1 = new BoxGeometry(barThickness, h1, barWidth) + const geo2 = new BoxGeometry(barThickness, h2, barWidth) + const mesh1 = new CombineWithEdge({ mesh: new Mesh(geo1, mat1), edgeColor: '#414141' }) + const mesh2 = new CombineWithEdge({ mesh: new Mesh(geo2, mat2), edgeColor: '#414141' }) + + mesh1.position.set(0, h1 / 2, -1 * choppedW * 2) + mesh2.position.set(0, h2 / 2, 1 * choppedW * 2) + pair.add(mesh1, mesh2) + pair.position.z = unitW * i + + pairs.add(pair) + } + pairs.position.z = BG_WIDTH * -0.435 + + super() + this.add(bgMesh, pairs) + } +} diff --git a/backend/dashboard/views/utils/3d-animation/landing/CurveGraph.js b/backend/dashboard/views/utils/3d-animation/landing/CurveGraph.js new file mode 100644 index 0000000000..89e3ea0844 --- /dev/null +++ b/backend/dashboard/views/utils/3d-animation/landing/CurveGraph.js @@ -0,0 +1,54 @@ +import { + MeshLambertMaterial, CatmullRomCurve3, SphereGeometry, TubeGeometry, + Mesh, Group, DoubleSide +} from 'three' +import GraphBackground from './GraphBackground.js' + +export default class CurveGraph extends Group { + constructor ({ + points = [], + tubeColor = '#000000', + sphereColor = '#000000', + edgeColor = '#000000', + tubeRadius = 0.5, + sphereRadius = 1, + isDarkTheme = false + }) { + const bgThickness = 0.1 + const MaterialCommon = MeshLambertMaterial + const curve = new CatmullRomCurve3(points, false) + + // graph itself + const tubeGeometry = new TubeGeometry(curve, 120, tubeRadius, 3, false) + const tubeMaterial = new MaterialCommon({ color: tubeColor, side: DoubleSide }) + const tubeMesh = new Mesh(tubeGeometry, tubeMaterial) + tubeMesh.position.x = bgThickness * -1 + + // points on the graph + const spheres = new Group() + const sphereGeometry = new SphereGeometry(sphereRadius, 64, 32) + const sphereMaterial = new MaterialCommon({ color: sphereColor }) + + spheres.add( + ...points.map(point => { + const dot = new Mesh(sphereGeometry, sphereMaterial) + dot.position.copy(point) + + return dot + }) + ) + spheres.position.x = bgThickness * -1 + + // bg + const bgMesh = new GraphBackground({ + lineCount: 6, + bgColor: isDarkTheme ? '#1c1c1c' : '#f7f9fb', + edgeColor: isDarkTheme ? '#e3f5ff' : '#414141', + lineColor: isDarkTheme ? '#e3f5ff' : '#000000', + bgThickness + }) + + super() + super.add(tubeMesh, spheres, bgMesh) + } +} diff --git a/backend/dashboard/views/utils/3d-animation/landing/GraphBackground.js b/backend/dashboard/views/utils/3d-animation/landing/GraphBackground.js new file mode 100644 index 0000000000..915467074f --- /dev/null +++ b/backend/dashboard/views/utils/3d-animation/landing/GraphBackground.js @@ -0,0 +1,55 @@ +import { + BoxGeometry, Group, MeshLambertMaterial, Mesh, Vector3, + LineDashedMaterial, BufferGeometry, Line, DoubleSide +} from 'three' +import { CombineWithEdge } from '../animation-utils.js' + +export default class GraphBackground extends Group { + constructor ({ + width = 30, + height = 16, + bgThickness = 0.1, + bgColor = '#f7f9fb', + lineColor = '#000000', + edgeColor = '#414141', + lineCount = 4 + } = {}) { + // background plane + const bgGeometry = new BoxGeometry(bgThickness, height, width) + const bgMaterial = new MeshLambertMaterial({ + color: bgColor, side: DoubleSide, transparent: true, opacity: 0.625 + }) + const bgMesh = new CombineWithEdge({ mesh: new Mesh(bgGeometry, bgMaterial), edgeColor }) + bgMesh.position.y = height / 2 + bgMesh.position.x = (bgThickness + 0.05) * -1 + + // lines + const lineMaterial = new LineDashedMaterial({ color: lineColor, gapSize: 0.2, dashSize: 0.375 }) + const lineGap = height / (lineCount + 1) + const halfW = width / 2 + const lineEnds = [] + const lineMeshes = new Group() + + for (let i = 1; i <= lineCount; i++) { + lineEnds.push([ + new Vector3(0, lineGap * i, halfW), + new Vector3(0, lineGap * i, halfW * -1) + ]) + } + lineMeshes.add( + ...lineEnds.map((endPoints) => { + const lineMesh = new Line( + new BufferGeometry().setFromPoints(endPoints), + lineMaterial + ) + lineMesh.computeLineDistances() + + return lineMesh + }) + ) + const lineMeshesClone = lineMeshes.clone(true) + lineMeshesClone.position.x = -1 * (bgThickness + 0.2) + super() + this.add(bgMesh, lineMeshes, lineMeshesClone) + } +} diff --git a/backend/dashboard/views/utils/3d-animation/landing/index.js b/backend/dashboard/views/utils/3d-animation/landing/index.js new file mode 100644 index 0000000000..9da69652fe --- /dev/null +++ b/backend/dashboard/views/utils/3d-animation/landing/index.js @@ -0,0 +1,136 @@ +import * as Three from 'three' +import { throttle } from '@common/cdLodash.js' +import { + resizeRendererToDisplaySize, adjustCameraAspect, degreeToRadian, Edgify +} from '../animation-utils.js' +import CurveGraph from './CurveGraph.js' +import BarGraphs from './BarGraphs.js' + +const { + WebGLRenderer, Scene, Group, PerspectiveCamera, + BoxGeometry, Vector2, Vector3, AmbientLight +} = Three + +// constants & settings +const PLANE_HEIGHT = 0.5 +const SCENE_ROTATION_SPEED = degreeToRadian(0.35) * -1 // per frame +const COLORS = { + grey_0: '#414141', + primary_blue: '#e3f5ff', + tube: '#95a4fc', + sphere: '#baedbd', + bar_1: '#95a4fc', + bar_2: '#baedbd', + ambLight: '#f7f9fb' +} +const pointer = new Vector2(0, 0) +const cameraSettings = { pos: { x: 0, y: 15, z: 46 }, lookAt: new Vector3(0, -2.5, 0) } +let animationId + +function initAnimation (canvasEl, theme = 'light') { + addPointerMoveListener() + + // create a scene & root + const isDarkTheme = theme === 'dark' + const scene = new Scene() + const root = new Group() // root object that contains all meshes + root.position.y = -10 + scene.add(root) + scene.root = root + + // create a camera + const camera = new PerspectiveCamera(75, canvasEl.clientWidth / canvasEl.clientHeight, 0.1, 1000) + camera.position.set(cameraSettings.pos.x, cameraSettings.pos.y, cameraSettings.pos.z) + camera.lookAt(cameraSettings.lookAt) + camera.updateProjectionMatrix() + + // create a renderer + const renderer = new WebGLRenderer({ canvas: canvasEl, alpha: true, antialias: true }) + const renderScene = () => renderer.render(scene, camera) + resizeRendererToDisplaySize(renderer) + + // light + const ambLight = new AmbientLight(COLORS.ambLight, 1) + scene.add(ambLight) + + // --- add objects to the scene --- // + + // box + const boxDepth = 20 + const box = new Edgify({ + geometry: new BoxGeometry(18, boxDepth, 38), + color: isDarkTheme ? COLORS.primary_blue : COLORS.grey_0 + }) + box.position.y = boxDepth / 2 + PLANE_HEIGHT + 0.15 + scene.root.add(box) + + // curve-graph + const curveGraph = new CurveGraph({ + points: [ + new Vector3(0, 12.25, 12.25), + new Vector3(0, 8.5, 8.25), + new Vector3(0, 9.75, 4.25), + new Vector3(0, 7.25, -0.25), + new Vector3(0, 10.25, -4.25), + new Vector3(0, 7.75, -8.25), + new Vector3(0, 3.75, -12.25) + ], + tubeRadius: 0.325, + sphereRadius: 0.85, + tubeColor: COLORS.tube, + sphereColor: COLORS.sphere, + isDarkTheme + }) + curveGraph.position.x = -2.5 + scene.root.add(curveGraph) + + // bar-graphs + const barGraphs = new BarGraphs({ + pairColors: [COLORS.bar_1, COLORS.bar_2], + pairCount: 8, + isDarkTheme + }) + barGraphs.position.set(2.5, PLANE_HEIGHT, 0) + scene.root.add(barGraphs) + + function animate () { + if (resizeRendererToDisplaySize(renderer)) { + adjustCameraAspect(canvasEl, camera) + } + + root.rotation.y += SCENE_ROTATION_SPEED + + scene.rotation.z = degreeToRadian(8) * pointer.x + scene.rotation.x = degreeToRadian(8) * pointer.y + + renderScene() + animationId = requestAnimationFrame(animate) + } + + animate() +} + +const onPointerMove = throttle(e => { + const { clientX, clientY } = e + const halfX = innerWidth / 2 + const halfY = innerHeight / 2 + + pointer.set( + (clientX - halfX) / halfX, + (clientY - halfY) / halfY + ) +}, 10) + +function terminateAnimation () { + cancelAnimationFrame(animationId) + window.removeEventListener('pointermove', onPointerMove) +} + +function addPointerMoveListener () { + window.addEventListener('pointermove', onPointerMove) +} + +export { + initAnimation, + terminateAnimation +} diff --git a/backend/dashboard/views/utils/custom-directives/index.js b/backend/dashboard/views/utils/custom-directives/index.js new file mode 100644 index 0000000000..b42ae7d7f0 --- /dev/null +++ b/backend/dashboard/views/utils/custom-directives/index.js @@ -0,0 +1,4 @@ +'use strict' + +import './vSafeHtml.js' +import './vFocus.js' diff --git a/backend/dashboard/views/utils/custom-directives/vFocus.js b/backend/dashboard/views/utils/custom-directives/vFocus.js new file mode 100644 index 0000000000..6370f4c523 --- /dev/null +++ b/backend/dashboard/views/utils/custom-directives/vFocus.js @@ -0,0 +1,7 @@ +import Vue from 'vue' + +Vue.directive('focus', { + inserted: (el, args) => { + if (!args || args.value !== false) el.focus() + } +}) diff --git a/backend/dashboard/views/utils/custom-directives/vSafeHtml.js b/backend/dashboard/views/utils/custom-directives/vSafeHtml.js new file mode 100644 index 0000000000..342fc9f3c7 --- /dev/null +++ b/backend/dashboard/views/utils/custom-directives/vSafeHtml.js @@ -0,0 +1,44 @@ +/* + * Copyright GitLab B.V. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +/* + * This file is mainly a copy of the `safe-html.js` directive found in the + * [gitlab-ui](https://gitlab.com/gitlab-org/gitlab-ui) project, + * slightly modifed for linting purposes and to immediately register it via + * `Vue.directive()` rather than exporting it, consistently with our other + * custom Vue directives. + */ +import dompurify from 'dompurify' +import Vue from 'vue' +import { cloneDeep } from '@common/cdLodash.js' + +export const defaultConfig = { + ALLOWED_ATTR: ['class'], + ALLOWED_TAGS: ['b', 'br', 'em', 'i', 'p', 'small', 'span', 'strong', 'sub', 'sup', 'u'], + RETURN_DOM_FRAGMENT: true +} + +const transform = (el, binding) => { + if (binding.oldValue !== binding.value) { + let config = defaultConfig + if (binding.arg === 'a') { + config = cloneDeep(config) + config.ALLOWED_ATTR.push('href', 'target') + config.ALLOWED_TAGS.push('a') + } + el.textContent = '' + el.appendChild(dompurify.sanitize(binding.value, config)) + } +} + +Vue.directive('safe-html', { + bind: transform, + update: transform +}) diff --git a/backend/dashboard/views/utils/dummy-data.js b/backend/dashboard/views/utils/dummy-data.js new file mode 100644 index 0000000000..bf9a9ea917 --- /dev/null +++ b/backend/dashboard/views/utils/dummy-data.js @@ -0,0 +1,67 @@ +/* eslint-disable */ + +// dummy placeholder data to be used in various pages +import { blake32Hash } from '@common/functions.js' +import { addTimeToDate, MONTHS_MILLIS } from '@common/cdTimeUtils.js' +import L from '@common/translations.js' + +// helper +const fakeUsers = ['Flex Kubin', 'Attila Hun', 'Childish Gambino', 'Ken M', 'Margarida', 'Rosalia', 'Leihla P', 'Andrea'] +const PAST_THREE_MONTHS = -3 * MONTHS_MILLIS +const randomPastDate = () => addTimeToDate(new Date(), Math.floor(Math.random() * PAST_THREE_MONTHS)) +const randomFromArray = arr => arr[Math.floor(Math.random() * arr.length)] +const randomHash = () => blake32Hash(Math.random().toString(16).slice(2)) + +// Contracts.vue // +const contractDummyData = [] +const manifestDummy = { + 'gi.contracts/identity': {"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.1\",\"contract\":{\"hash\":\"21XWnNVpDxBCmdcQvko21FoGWpTXhNydrWfsCxB1VdfWg2wtxA\",\"file\":\"identity.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"identity-slim.js\",\"hash\":\"21XWnNSeKaQFg61hqcw9FvNV3TPZpqdBnNYQSVz3tKPVWygToL\"}}","signature":{"key":"","signature":""}}, + 'gi.contracts/mailbox': {"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.1\",\"contract\":{\"hash\":\"21XWnNQjQwf4fPBBqBJtPee3SMVbHrQSju4tu6SRjHNPfDBo8Q\",\"file\":\"mailbox.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"mailbox-slim.js\",\"hash\":\"21XWnNXUFWQDPrqeADPHPyYueAtVRoAdmwsMT9XBCWfPtP8DLb\"}}","signature":{"key":"","signature":""}}, + 'gi.contracts/chatroom': {"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.1\",\"contract\":{\"hash\":\"21XWnNPTjY6GrUyqb7GzsLstF5JZhs9b5jADnSVufZipPvtQeA\",\"file\":\"chatroom.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"chatroom-slim.js\",\"hash\":\"21XWnNRT58PL4Hj1BUv2SaJYZHVFkFAF8bS2FTwQy7XvuXB1QE\"}}","signature":{"key":"","signature":""}}, + 'gi.contracts/group': {"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.1\",\"contract\":{\"hash\":\"21XWnNXo4eU77dCQuWkPZtNTXST4hxd1DGnGMiBsaBB6vkdTZk\",\"file\":\"group.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"group-slim.js\",\"hash\":\"21XWnNSmnNSZZ6oZfsRmss2KKCSQS5QqVh62Ub7iojRAYwcxRr\"}}","signature":{"key":"","signature":""}} +} + +for (let i=0; i<20; i++) { + const item = { + contractId: randomHash(), + type: randomFromArray(['gi.contracts/identity', 'gi.contracts/mailbox', 'gi.contracts/chatroom', 'gi.contracts/group']), + size: 3 + 10 * Math.random(), + spaceUsed: 10 + 20 * Math.random(), + createdDate: randomPastDate() + } + + item.manifestJSON = manifestDummy[item.type] + contractDummyData.push(item) +} + +// Users.vue +const fakeUserStats = [ + { id: 'total-users', name: L('Total users'), value: 152, icon: 'users' }, + { id: 'active-users', name: L('Active users'), value: 70, icon: 'chart-up' }, + { id: 'inactive-users', name: L('Inactive users'), value: 152, icon: 'trend-down' } +] + +const fakeUserTableData = fakeUsers.map((name) => ({ + name, + id: randomHash(), + groupCount: 1 + Math.floor(Math.random() * 5), + ownedContractsCount: 50 + Math.floor(Math.random() * 200), + contractSize: 0.2 + 5 * Math.random(), + spaceUsed: 5 + 10 * Math.random(), + credits: { used: 1 + Math.floor(Math.random() * 25), limit: 20 } +})) + +// Accounts.vue +const fakeApplicationOptions = [ + { id: 'groupincome', name: L('Group income') }, + { id: 'app-2', name: L('Application 2') }, + { id: 'app-3', name: L('Application 3') } +] + + +export { + contractDummyData, + fakeUserStats, + fakeUserTableData, + fakeApplicationOptions +} diff --git a/backend/dashboard/views/utils/events.js b/backend/dashboard/views/utils/events.js new file mode 100644 index 0000000000..87fedcd438 --- /dev/null +++ b/backend/dashboard/views/utils/events.js @@ -0,0 +1,7 @@ +'use stricts' + +// constants for UI related events + +export const OPEN_MODAL = 'open-modal' +export const CLOSE_MODAL = 'close-modal' +export const REPLACE_MODAL = 'replace-modal' diff --git a/backend/dashboard/views/utils/lazyLoadComponents.js b/backend/dashboard/views/utils/lazyLoadComponents.js new file mode 100644 index 0000000000..108a0fba35 --- /dev/null +++ b/backend/dashboard/views/utils/lazyLoadComponents.js @@ -0,0 +1,35 @@ +import Vue from 'vue' +import LoadingPage from '@pages/miscellaneous/Loading.vue' +import ErrorPage from '@pages/miscellaneous/ErrorLoading.vue' + +export function lazyComponent (name, lazyImport) { + Vue.component(name, lazyImport) + // TODO: + // Create LoadingModal.vue & ErrorModal.vue and + // expand this function to Vue.component(name, () => ({ component: ... , loading: ..., error: ... })) format. +} + +/* +This method of loading components is documented here and is used to ensure compatibility +with lazy-loaded routes: +https://github.com/vuejs/vue-router/pull/2140/files#diff-7d999265ce5b22152fdffee108ca6385 +*/ +export function lazyPage (lazyImport) { + const handler = () => ({ + // HACK: sometimes a bundler bug makes it necessary to use + // `.then(m => m.default ?? m)` when importing a module with `import()`. + component: lazyImport().then(m => m.default ?? m), + loading: LoadingPage, + error: ErrorPage + }) + + return () => Promise.resolve({ + functional: true, + render (h, { data, children }) { + return h(handler, data, children) + } + }) +} + +// register modals +lazyComponent('ViewContractManifestModal', () => import('../containers/modal/ViewContractManifestModal.vue')) diff --git a/backend/dashboard/views/utils/vError.js b/backend/dashboard/views/utils/vError.js new file mode 100644 index 0000000000..981ae7fb28 --- /dev/null +++ b/backend/dashboard/views/utils/vError.js @@ -0,0 +1,56 @@ +import Vue from 'vue' + +// Register a global custom directive called `v-error` +// to automatically display vuelidate error messages +// +// Config: +// validations: { +// form: { +// incomeAmount: { +// [L('field is required')]: required, +// [L('cannot be negative')]: v => v >= 0, +// [L('cannot have more than 2 decimals')]: decimals(2) +// }, +// +// Markup: +// i18n.label(tag='label') Enter your income: +// input.input.is-primary( +// type='text' +// v-model='$v.form.incomeAmount.$model' +// :class='{error: $v.form.incomeAmount.$error}' +// v-error:incomeAmount='{ tag: "p", attrs: { "data-test": "badIncome" } }' +// ) + +Vue.directive('error', { + inserted (el, binding, vnode) { + if (!binding.arg) { + throw new Error(`v-error: missing argument on ${el.outerHTML}`) + } + if (!vnode.context.$v.form[binding.arg]) { + throw new Error(`v-error: vuelidate doesn't have validation for ${binding.arg} on ${el.outerHTML}`) + } + const opts = binding.value || {} + const pErr = document.createElement(opts.tag || 'span') + for (const attr in opts.attrs) { + pErr.setAttribute(attr, opts.attrs[attr]) + } + pErr.classList.add('error', 'is-hidden') + el.insertAdjacentElement('afterend', pErr) + }, + update (el, binding, vnode) { + if (vnode.context.$v.form[binding.arg].$error) { + for (const key in vnode.context.$v.form[binding.arg].$params) { + if (!vnode.context.$v.form[binding.arg][key]) { + el.nextSibling.innerText = key + break + } + } + el.nextElementSibling.classList.remove('is-hidden') + } else { + el.nextElementSibling.classList.add('is-hidden') + } + }, + unbind (el) { + el.nextElementSibling.remove() + } +}) diff --git a/backend/dashboard/views/utils/vStyle.js b/backend/dashboard/views/utils/vStyle.js new file mode 100644 index 0000000000..cfabe22100 --- /dev/null +++ b/backend/dashboard/views/utils/vStyle.js @@ -0,0 +1,10 @@ +import Vue from 'vue' +/* +* Style tag overload because Vue is trying to compile +* the content inside the tag otherwise +*/ +Vue.component('v-style', { + render: function (createElement) { + return createElement('style', this.$slots.default) + } +}) diff --git a/backend/dashboard/views/utils/validationMixin.js b/backend/dashboard/views/utils/validationMixin.js new file mode 100644 index 0000000000..cf64ba7a4d --- /dev/null +++ b/backend/dashboard/views/utils/validationMixin.js @@ -0,0 +1,80 @@ +import { debounce } from '@common/cdLodash.js' +import { validationMixin as vuelidateSetup } from 'vuelidate' + +/** +Methods to debounce vuelidate validations. + +Ex. + +// Using v-model + +input.input( + :class='{ "error": $v.form.email.$error }' + type='email' + v-model='form.email' + @input='debounceField("email")' + @blur='updateField("email")' + v-error:email='' +) + +// without v-model +input.input( + :class='{ "error": $v.form.name.$error }' + name='username' + @input='e => debounceField("username", e.target.value)' + @blur='e => updateField("username", e.target.value)' + v-error:username='' +) + +// -- Debounce both validation and $error feedback (cannot use v-model) +input.input( + :class='{error: $v.form.name.$error}' + name='username' + @input='e => debounceValidation("username", e.target.value)' + @blur='e => updateField("username", e.target.value)' + v-error:username='' +) +*/ + +export default { + mixins: [vuelidateSetup], + methods: { + debounceField (fieldName: string, value: any) { + // Do a field validation, but don't show $error immediately + this.$v.form[fieldName].$reset() + // Wait a little to make sure the user stopped typing.. + this.debounceValidation(fieldName, value) + }, + + /** + * Validate the field and update it immediately. + * - Usually used on @blur. + */ + updateField (fieldName: string, value: any) { + if (value) { + // it means it needs to be manually binded + this.form[fieldName] = value + } + this.$v.form[fieldName].$touch() + }, + + /** + * Debounce field validations + * - You can call it when you want to debounce expensive validations. + */ + debounceValidation: (debounce(function (fieldName, value) { + this.updateField(fieldName, value) + }, 800): any), + + // sometimes, validations for all fields need to be done all at once. + // e.g) validate all fields at the same time when 'submit' is clicked. + validateAll () { + const validationKeys = Object.keys(this.form || {}) + .filter(key => Boolean(this.$v.form[key])) + + for (const key of validationKeys) { + this.$v.form[key].$touch() + } + } + } +} diff --git a/backend/database-fs.js b/backend/database-fs.js new file mode 100644 index 0000000000..9d9db18cbc --- /dev/null +++ b/backend/database-fs.js @@ -0,0 +1,32 @@ +'use strict' + +import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises' +import { join, resolve } from 'node:path' +import { checkKey } from '~/shared/domains/chelonia/db.js' + +// Initialized in `initStorage()`. +let dataFolder = '' + +export async function initStorage (options: Object = {}): Promise { + dataFolder = resolve(options.dirname) + await mkdir(dataFolder, { mode: 0o750, recursive: true }) +} + +// Useful in test hooks. +export function clear (): Promise { + return readdir(dataFolder) + .then(keys => Promise.all(keys.map(key => unlink(join(dataFolder, key))))) +} + +// eslint-disable-next-line require-await +export async function readData (key: string): Promise { + // Necessary here to thwart path traversal attacks. + checkKey(key) + return readFile(join(dataFolder, key)) + .catch(err => undefined) // eslint-disable-line node/handle-callback-err +} + +// eslint-disable-next-line require-await +export async function writeData (key: string, value: Buffer | string): Promise { + return writeFile(join(dataFolder, key), value) +} diff --git a/backend/database-sqlite.js b/backend/database-sqlite.js new file mode 100644 index 0000000000..19a5c76c70 --- /dev/null +++ b/backend/database-sqlite.js @@ -0,0 +1,76 @@ +'use strict' + +import { mkdir } from 'node:fs/promises' +import { join, resolve } from 'node:path' +import sqlite3 from 'sqlite3' + +let db: any = null +let readStatement: any = null +let writeStatement: any = null + +const run = (sql, args) => { + return new Promise((resolve, reject) => { + db.run(sql, args, (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} + +export async function initStorage (options: Object = {}): Promise { + const { dirname, filename } = options + const dataFolder = resolve(dirname) + + await mkdir(dataFolder, { mode: 0o750, recursive: true }) + + await new Promise((resolve, reject) => { + if (db) { + reject(new Error(`The ${filename} SQLite database is already open.`)) + } + db = new sqlite3.Database(join(dataFolder, filename), (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) + await run('CREATE TABLE IF NOT EXISTS Data(key TEXT NOT NULL PRIMARY KEY, value TEXT NOT NULL)') + console.log('Connected to the %s SQLite database.', filename) + readStatement = db.prepare('SELECT value FROM Data WHERE key = ?') + writeStatement = db.prepare('REPLACE INTO Data(key, value) VALUES(?, ?)') +} + +// Useful in test hooks. +export function clear (): Promise { + return run('DELETE FROM Data') +} + +export function readData (key: string): Promise { + return new Promise((resolve, reject) => { + readStatement.get([key], (err, row) => { + if (err) { + reject(err) + } else { + // Note: sqlite remembers the type of every stored value, therefore we + // automatically get back the same JS value that has been inserted. + resolve(row?.value) + } + }) + }) +} + +export function writeData (key: string, value: Buffer | string): Promise { + return new Promise((resolve, reject) => { + writeStatement.run([key, value], (err) => { + if (err) { + reject(err) + } else { + resolve() + } + }) + }) +} diff --git a/backend/database.js b/backend/database.js index b97468f479..6ea58c4e8e 100644 --- a/backend/database.js +++ b/backend/database.js @@ -4,25 +4,39 @@ import sbp from '@sbp/sbp' import { strToB64 } from '~/shared/functions.js' import { Readable } from 'stream' import fs from 'fs' -import util from 'util' -import path from 'path' +import { readdir, readFile } from 'node:fs/promises' +import path from 'node:path' import '@sbp/okturtles.data' -import '~/shared/domains/chelonia/db.js' +import { checkKey, parsePrefixableKey, prefixHandlers } from '~/shared/domains/chelonia/db.js' import LRU from 'lru-cache' const Boom = require('@hapi/boom') -const writeFileAsync = util.promisify(fs.writeFile) -const readFileAsync = util.promisify(fs.readFile) -const dataFolder = path.resolve('./data') +const production = process.env.NODE_ENV === 'production' +// Defaults to `fs` in production. +const persistence = process.env.GI_PERSIST || (production ? 'fs' : undefined) +// Default database options. Other values may be used e.g. in tests. +const options = { + fs: { + dirname: './data' + }, + sqlite: { + dirname: './data', + filename: 'groupincome.db' + } +} + +// Used by `throwIfFileOutsideDataDir()`. +const dataFolder = path.resolve(options.fs.dirname) + +// Create our data folder if it doesn't exist yet. +// This is currently necessary even when not using persistence, e.g. to store file uploads. if (!fs.existsSync(dataFolder)) { fs.mkdirSync(dataFolder, { mode: 0o750 }) } -const production = process.env.NODE_ENV === 'production' - -export default (sbp('sbp/selectors/register', { +sbp('sbp/selectors/register', { 'backend/db/streamEntriesSince': async function (contractID: string, hash: string): Promise<*> { let currentHEAD = await sbp('chelonia/db/latestHash', contractID) if (!currentHEAD) { @@ -139,84 +153,88 @@ export default (sbp('sbp/selectors/register', { await sbp('chelonia/db/set', namespaceKey(name), value) return { name, value } }, - 'backend/db/lookupName': async function (name: string): Promise<*> { + 'backend/db/lookupName': async function (name: string): Promise { const value = await sbp('chelonia/db/get', namespaceKey(name)) return value || Boom.notFound() - }, - // ======================= - // Filesystem API - // - // TODO: add encryption - // ======================= - 'backend/db/readFile': async function (filename: string): Promise<*> { - const filepath = throwIfFileOutsideDataDir(filename) - if (!fs.existsSync(filepath)) { - return Boom.notFound() - } - return await readFileAsync(filepath) - }, - 'backend/db/writeFile': async function (filename: string, data: any): Promise<*> { - // TODO: check for how much space we have, and have a server setting - // that determines how much of the disk space we're allowed to - // use. If the size of the file would cause us to exceed this - // amount, throw an exception - return await writeFileAsync(throwIfFileOutsideDataDir(filename), data) - }, - 'backend/db/writeFileOnce': async function (filename: string, data: any): Promise<*> { - const filepath = throwIfFileOutsideDataDir(filename) - if (fs.existsSync(filepath)) { - console.warn('writeFileOnce: exists:', filepath) - return - } - return await writeFileAsync(filepath, data) } -}): any) +}) function namespaceKey (name: string): string { return 'name=' + name } -function throwIfFileOutsideDataDir (filename: string): string { - const filepath = path.resolve(path.join(dataFolder, filename)) - if (filepath.indexOf(dataFolder) !== 0) { - throw Boom.badRequest(`bad name: ${filename}`) - } - return filepath -} +export default async () => { + // If persistence must be enabled: + // - load and initialize the selected storage backend + // - then overwrite 'chelonia/db/get' and '-set' to use it with an LRU cache + if (persistence) { + const { initStorage, readData, writeData } = await import(`./database-${persistence}.js`) -if (production || process.env.GI_PERSIST) { - // https://github.com/isaacs/node-lru-cache#usage - const cache = new LRU({ - max: Number(process.env.GI_LRU_NUM_ITEMS) || 10000 - }) + await initStorage(options[persistence]) - sbp('sbp/selectors/overwrite', { - // we cannot simply map this to readFile, because 'chelonia/db/getEntry' - // calls this and expects a string, not a Buffer - // 'chelonia/db/get': sbp('sbp/selectors/fn', 'backend/db/readFile'), - 'chelonia/db/get': async function (filename: string) { - const lookupValue = cache.get(filename) - if (lookupValue !== undefined) { - return lookupValue + // https://github.com/isaacs/node-lru-cache#usage + const cache = new LRU({ + max: Number(process.env.GI_LRU_NUM_ITEMS) || 10000 + }) + + sbp('sbp/selectors/overwrite', { + 'chelonia/db/get': async function (prefixableKey: string): Promise { + const lookupValue = cache.get(prefixableKey) + if (lookupValue !== undefined) { + return lookupValue + } + const [prefix, key] = parsePrefixableKey(prefixableKey) + let value = await readData(key) + if (value === undefined) { + return + } + value = prefixHandlers[prefix](value) + cache.set(prefixableKey, value) + return value + }, + 'chelonia/db/set': async function (key: string, value: Buffer | string): Promise { + checkKey(key) + await writeData(key, value) + cache.set(key, value) } - const bufferOrError = await sbp('backend/db/readFile', filename) - if (Boom.isBoom(bufferOrError)) { - return null + }) + sbp('sbp/selectors/lock', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) + } + // TODO: Update this to only run when persistence is disabled when `¢hel deploy` can target SQLite. + if (persistence !== 'fs' || options.fs.dirname !== './data') { + const HASH_LENGTH = 50 + // Remember to keep these values up-to-date. + const CONTRACT_MANIFEST_MAGIC = '{"head":{"manifestVersion"' + const CONTRACT_SOURCE_MAGIC = '"use strict";' + // Preload contract source files and contract manifests into Chelonia DB. + // Note: the data folder may contain other files if the `fs` persistence mode + // has been used before. We won't load them here; that's the job of `chel migrate`. + // Note: our target files are currently deployed with unprefixed hashes as file names. + // We can take advantage of this to recognize them more easily. + // TODO: Update this code when `¢hel deploy` no longer generates unprefixed keys. + const keys = (await readdir(dataFolder)) + // Skip some irrelevant files. + .filter(k => k.length === HASH_LENGTH) + const numKeys = keys.length + let numVisitedKeys = 0 + let numNewKeys = 0 + + console.log('[chelonia.db] Preloading...') + for (const key of keys) { + // Skip keys which are already in the DB. + if (!persistence || !await sbp('chelonia/db/get', key)) { + const value = await readFile(path.join(dataFolder, key), 'utf8') + // Load only contract source files and contract manifests. + if (value.startsWith(CONTRACT_MANIFEST_MAGIC) || value.startsWith(CONTRACT_SOURCE_MAGIC)) { + await sbp('chelonia/db/set', key, value) + numNewKeys++ + } } - const value = bufferOrError.toString('utf8') - cache.set(filename, value) - return value - }, - 'chelonia/db/set': async function (filename: string, data: any): Promise<*> { - // eslint-disable-next-line no-useless-catch - try { - const result = await sbp('backend/db/writeFile', filename, data) - cache.set(filename, data) - return result - } catch (err) { - throw err + numVisitedKeys++ + if (numVisitedKeys % Math.floor(numKeys / 10) === 0) { + console.log(`[chelonia.db] Preloading... ${numVisitedKeys / Math.floor(numKeys / 10)}0% done`) } } - }) - sbp('sbp/selectors/lock', ['chelonia/db/get', 'chelonia/db/set', 'chelonia/db/delete']) + numNewKeys && console.log(`[chelonia.db] Preloaded ${numNewKeys} new entries`) + } } diff --git a/backend/routes.js b/backend/routes.js index 444708427e..28f792618a 100644 --- a/backend/routes.js +++ b/backend/routes.js @@ -14,6 +14,14 @@ import { registrationKey, register, getChallenge, getContractSalt, updateContrac const Boom = require('@hapi/boom') const Joi = require('@hapi/joi') +const isCheloniaDashboard = process.env.IS_CHELONIA_DASHBOARD_DEV +const staticServeConfig = { + routePath: isCheloniaDashboard ? '/dashboard/{path*}' : '/app/{path*}', + distAssets: path.resolve(isCheloniaDashboard ? 'dist-dashboard/assets' : 'dist/assets'), + distIndexHtml: path.resolve(isCheloniaDashboard ? './dist-dashboard/index.html' : './dist/index.html'), + redirect: isCheloniaDashboard ? '/dashboard/' : '/app/' +} + const route = new Proxy({}, { get: function (obj, prop) { return function (path: string, options: Object, handler: Function | Object) { @@ -144,15 +152,15 @@ route.GET('/time', {}, function (request, h) { return new Date().toISOString() }) -// file upload related - // TODO: if the browser deletes our cache then not everyone // has a complete copy of the data and can act as a // new coordinating server... I don't like that. -const MEGABTYE = 1048576 // TODO: add settings for these +const MEGABYTE = 1048576 // TODO: add settings for these const SECOND = 1000 +// File upload route. +// If accepted, the file will be stored in Chelonia DB. route.POST('/file', { // TODO: only allow uploads from registered users payload: { @@ -163,44 +171,44 @@ route.POST('/file', { console.error('failAction error:', err) return err }, - maxBytes: 6 * MEGABTYE, // TODO: make this a configurable setting + maxBytes: 6 * MEGABYTE, // TODO: make this a configurable setting timeout: 10 * SECOND // TODO: make this a configurable setting } }, async function (request, h) { try { console.log('FILE UPLOAD!') - console.log(request.payload) const { hash, data } = request.payload if (!hash) return Boom.badRequest('missing hash') if (!data) return Boom.badRequest('missing data') - // console.log('typeof data:', typeof data) const ourHash = blake32Hash(data) if (ourHash !== hash) { console.error(`hash(${hash}) != ourHash(${ourHash})`) return Boom.badRequest('bad hash!') } - await sbp('backend/db/writeFileOnce', hash, data) + await sbp('chelonia/db/set', hash, data) return '/file/' + hash } catch (err) { return logger(err) } }) +// Serve data from Chelonia DB. +// Note that a `Last-Modified` header isn't included in the response. route.GET('/file/{hash}', { cache: { // Do not set other cache options here, to make sure the 'otherwise' option // will be used so that the 'immutable' directive gets included. otherwise: 'public,max-age=31536000,immutable' - }, - files: { - relativeTo: path.resolve('data') } -}, function (request, h) { +}, async function (request, h) { const { hash } = request.params console.debug(`GET /file/${hash}`) - // Reusing the given `hash` parameter to set the ETag should be faster than - // letting Hapi hash the file to compute an ETag itself. - return h.file(hash, { etagMethod: false }).etag(hash) + + const blobOrString = await sbp('chelonia/db/get', `any:${hash}`) + if (!blobOrString) { + return Boom.notFound() + } + return h.response(blobOrString).etag(hash) }) // SPA routes @@ -222,7 +230,7 @@ route.GET('/assets/{subpath*}', { } }, files: { - relativeTo: path.resolve('dist/assets') + relativeTo: staticServeConfig.distAssets } }, function (request, h) { const { subpath } = request.params @@ -241,12 +249,12 @@ route.GET('/assets/{subpath*}', { return h.file(subpath) }) -route.GET('/app/{path*}', {}, { - file: path.resolve('./dist/index.html') +route.GET(staticServeConfig.routePath, {}, { + file: staticServeConfig.distIndexHtml }) route.GET('/', {}, function (req, h) { - return h.redirect('/app/') + return h.redirect(staticServeConfig.redirect) }) route.POST('/zkpp/register/{contract}', { diff --git a/backend/server.js b/backend/server.js index 67c6efd2a0..d8f2eba420 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,9 +1,9 @@ 'use strict' import sbp from '@sbp/sbp' -import './database.js' import Hapi from '@hapi/hapi' import GiAuth from './auth.js' +import initDB from './database.js' import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js' import { SERVER_RUNNING } from './events.js' import { SERVER_INSTANCE, PUBSUB_INSTANCE } from './instance-keys.js' @@ -94,6 +94,7 @@ sbp('okTurtles.data/set', PUBSUB_INSTANCE, createServer(hapi.listener, { })) ;(async function () { + await initDB() // https://hapi.dev/tutorials/plugins await hapi.register([ { plugin: GiAuth }, diff --git a/contracts/0.0.21/chatroom-slim.js b/contracts/0.0.21/chatroom-slim.js new file mode 100644 index 0000000000..4e14d091cd --- /dev/null +++ b/contracts/0.0.21/chatroom-slim.js @@ -0,0 +1,832 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") + return require.apply(this, arguments); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }); + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); + + // frontend/model/contracts/chatroom.js + var import_sbp3 = __toESM(__require("@sbp/sbp")); + var import_common2 = __require("@common/common.js"); + + // frontend/model/contracts/shared/giLodash.js + function cloneDeep(obj) { + return JSON.parse(JSON.stringify(obj)); + } + function isMergeableObject(val) { + const nonNullObject = val && typeof val === "object"; + return nonNullObject && Object.prototype.toString.call(val) !== "[object RegExp]" && Object.prototype.toString.call(val) !== "[object Date]"; + } + function merge(obj, src) { + for (const key in src) { + const clone = isMergeableObject(src[key]) ? cloneDeep(src[key]) : void 0; + if (clone && isMergeableObject(obj[key])) { + merge(obj[key], clone); + continue; + } + obj[key] = clone || src[key]; + } + return obj; + } + + // frontend/model/contracts/shared/constants.js + var CHATROOM_NAME_LIMITS_IN_CHARS = 50; + var CHATROOM_DESCRIPTION_LIMITS_IN_CHARS = 280; + var CHATROOM_ACTIONS_PER_PAGE = 40; + var CHATROOM_MESSAGE_ACTION = "chatroom-message-action"; + var MESSAGE_RECEIVE = "message-receive"; + var CHATROOM_TYPES = { + INDIVIDUAL: "individual", + GROUP: "group" + }; + var CHATROOM_PRIVACY_LEVEL = { + GROUP: "group", + PRIVATE: "private", + PUBLIC: "public" + }; + var MESSAGE_TYPES = { + POLL: "poll", + TEXT: "text", + INTERACTIVE: "interactive", + NOTIFICATION: "notification" + }; + var MESSAGE_NOTIFICATIONS = { + ADD_MEMBER: "add-member", + JOIN_MEMBER: "join-member", + LEAVE_MEMBER: "leave-member", + KICK_MEMBER: "kick-member", + UPDATE_DESCRIPTION: "update-description", + UPDATE_NAME: "update-name", + DELETE_CHANNEL: "delete-channel", + VOTE: "vote" + }; + var PROPOSAL_VARIANTS = { + CREATED: "created", + EXPIRING: "expiring", + ACCEPTED: "accepted", + REJECTED: "rejected", + EXPIRED: "expired" + }; + var MESSAGE_NOTIFY_SETTINGS = { + ALL_MESSAGES: "all-messages", + DIRECT_MESSAGES: "direct-messages", + NOTHING: "nothing" + }; + + // frontend/model/contracts/misc/flowTyper.js + var EMPTY_VALUE = Symbol("@@empty"); + var isEmpty = (v) => v === EMPTY_VALUE; + var isNil = (v) => v === null; + var isUndef = (v) => typeof v === "undefined"; + var isBoolean = (v) => typeof v === "boolean"; + var isNumber = (v) => typeof v === "number"; + var isString = (v) => typeof v === "string"; + var isObject = (v) => !isNil(v) && typeof v === "object"; + var isFunction = (v) => typeof v === "function"; + var getType = (typeFn, _options) => { + if (isFunction(typeFn.type)) + return typeFn.type(_options); + return typeFn.name || "?"; + }; + var TypeValidatorError = class extends Error { + expectedType; + valueType; + value; + typeScope; + sourceFile; + constructor(message, expectedType, valueType, value, typeName = "", typeScope = "") { + const errMessage = message || `invalid "${valueType}" value type; ${typeName || expectedType} type expected`; + super(errMessage); + this.expectedType = expectedType; + this.valueType = valueType; + this.value = value; + this.typeScope = typeScope || ""; + this.sourceFile = this.getSourceFile(); + this.message = `${errMessage} +${this.getErrorInfo()}`; + this.name = this.constructor.name; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, TypeValidatorError); + } + } + getSourceFile() { + const fileNames = this.stack.match(/(\/[\w_\-.]+)+(\.\w+:\d+:\d+)/g) || []; + return fileNames.find((fileName) => fileName.indexOf("/flowTyper-js/dist/") === -1) || ""; + } + getErrorInfo() { + return ` + file ${this.sourceFile} + scope ${this.typeScope} + expected ${this.expectedType.replace(/\n/g, "")} + type ${this.valueType} + value ${this.value} +`; + } + }; + var validatorError = (typeFn, value, scope, message, expectedType, valueType) => { + return new TypeValidatorError(message, expectedType || getType(typeFn), valueType || typeof value, JSON.stringify(value), typeFn.name, scope); + }; + var arrayOf = (typeFn, _scope = "Array") => { + function array(value) { + if (isEmpty(value)) + return [typeFn(value)]; + if (Array.isArray(value)) { + let index = 0; + return value.map((v) => typeFn(v, `${_scope}[${index++}]`)); + } + throw validatorError(array, value, _scope); + } + array.type = () => `Array<${getType(typeFn)}>`; + return array; + }; + var literalOf = (primitive) => { + function literal(value, _scope = "") { + if (isEmpty(value) || value === primitive) + return primitive; + throw validatorError(literal, value, _scope); + } + literal.type = () => { + if (isBoolean(primitive)) + return `${primitive ? "true" : "false"}`; + else + return `"${primitive}"`; + }; + return literal; + }; + var mapOf = (keyTypeFn, typeFn) => { + function mapOf2(value) { + if (isEmpty(value)) + return {}; + const o = object(value); + const reducer = (acc, key) => Object.assign(acc, { + [keyTypeFn(key, "Map[_]")]: typeFn(o[key], `Map.${key}`) + }); + return Object.keys(o).reduce(reducer, {}); + } + mapOf2.type = () => `{ [_:${getType(keyTypeFn)}]: ${getType(typeFn)} }`; + return mapOf2; + }; + var object = function(value) { + if (isEmpty(value)) + return {}; + if (isObject(value) && !Array.isArray(value)) { + return Object.assign({}, value); + } + throw validatorError(object, value); + }; + var objectOf = (typeObj, _scope = "Object") => { + function object2(value) { + const o = object(value); + const typeAttrs = Object.keys(typeObj); + const unknownAttr = Object.keys(o).find((attr) => !typeAttrs.includes(attr)); + if (unknownAttr) { + throw validatorError(object2, value, _scope, `missing object property '${unknownAttr}' in ${_scope} type`); + } + const undefAttr = typeAttrs.find((property) => { + const propertyTypeFn = typeObj[property]; + return propertyTypeFn.name.includes("maybe") && !o.hasOwnProperty(property); + }); + if (undefAttr) { + throw validatorError(object2, o[undefAttr], `${_scope}.${undefAttr}`, `empty object property '${undefAttr}' for ${_scope} type`, `void | null | ${getType(typeObj[undefAttr]).substr(1)}`, "-"); + } + const reducer = isEmpty(value) ? (acc, key) => Object.assign(acc, { [key]: typeObj[key](value) }) : (acc, key) => { + const typeFn = typeObj[key]; + if (typeFn.name.includes("optional") && !o.hasOwnProperty(key)) { + return Object.assign(acc, {}); + } else { + return Object.assign(acc, { [key]: typeFn(o[key], `${_scope}.${key}`) }); + } + }; + return typeAttrs.reduce(reducer, {}); + } + object2.type = () => { + const props = Object.keys(typeObj).map((key) => { + const ret = typeObj[key].name.includes("optional") ? `${key}?: ${getType(typeObj[key], { noVoid: true })}` : `${key}: ${getType(typeObj[key])}`; + return ret; + }); + return `{| + ${props.join(",\n ")} +|}`; + }; + return object2; + }; + function objectMaybeOf(validations, _scope = "Object") { + return function(data) { + object(data); + for (const key in data) { + validations[key]?.(data[key], `${_scope}.${key}`); + } + return data; + }; + } + var optional = (typeFn) => { + const unionFn = unionOf(typeFn, undef); + function optional2(v) { + return unionFn(v); + } + optional2.type = ({ noVoid }) => !noVoid ? getType(unionFn) : getType(typeFn); + return optional2; + }; + function undef(value, _scope = "") { + if (isEmpty(value) || isUndef(value)) + return void 0; + throw validatorError(undef, value, _scope); + } + undef.type = () => "void"; + var number = function number2(value, _scope = "") { + if (isEmpty(value)) + return 0; + if (isNumber(value)) + return value; + throw validatorError(number2, value, _scope); + }; + var string = function string2(value, _scope = "") { + if (isEmpty(value)) + return ""; + if (isString(value)) + return value; + throw validatorError(string2, value, _scope); + }; + function unionOf_(...typeFuncs) { + function union(value, _scope = "") { + for (const typeFn of typeFuncs) { + try { + return typeFn(value, _scope); + } catch (_) { + } + } + throw validatorError(union, value, _scope); + } + union.type = () => `(${typeFuncs.map((fn) => getType(fn)).join(" | ")})`; + return union; + } + var unionOf = unionOf_; + + // frontend/model/contracts/shared/types.js + var inviteType = objectOf({ + inviteSecret: string, + quantity: number, + creator: string, + invitee: optional(string), + status: string, + responses: mapOf(string, string), + expires: number + }); + var chatRoomAttributesType = objectOf({ + name: string, + description: string, + type: unionOf(...Object.values(CHATROOM_TYPES).map((v) => literalOf(v))), + privacyLevel: unionOf(...Object.values(CHATROOM_PRIVACY_LEVEL).map((v) => literalOf(v))) + }); + var messageType = objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_TYPES).map((v) => literalOf(v))), + text: string, + proposal: objectMaybeOf({ + proposalId: string, + proposalType: string, + expires_date_ms: number, + createdDate: string, + creator: string, + variant: unionOf(...Object.values(PROPOSAL_VARIANTS).map((v) => literalOf(v))) + }), + notification: objectMaybeOf({ + type: unionOf(...Object.values(MESSAGE_NOTIFICATIONS).map((v) => literalOf(v))), + params: mapOf(string, string) + }), + replyingMessage: objectOf({ + hash: string, + text: string + }), + emoticons: mapOf(string, arrayOf(string)), + onlyVisibleTo: arrayOf(string) + }); + + // frontend/model/contracts/shared/functions.js + var import_sbp = __toESM(__require("@sbp/sbp")); + + // frontend/model/contracts/shared/time.js + var import_common = __require("@common/common.js"); + var MINS_MILLIS = 6e4; + var HOURS_MILLIS = 60 * MINS_MILLIS; + var DAYS_MILLIS = 24 * HOURS_MILLIS; + var MONTHS_MILLIS = 30 * DAYS_MILLIS; + + // frontend/views/utils/misc.js + function logExceptNavigationDuplicated(err) { + err.name !== "NavigationDuplicated" && console.error(err); + } + + // frontend/model/contracts/shared/functions.js + function createMessage({ meta, data, hash, id, state }) { + const { type, text, replyingMessage } = data; + const { createdDate } = meta; + let newMessage = { + type, + id, + hash, + from: meta.username, + datetime: new Date(createdDate).toISOString() + }; + if (type === MESSAGE_TYPES.TEXT) { + newMessage = !replyingMessage ? { ...newMessage, text } : { ...newMessage, text, replyingMessage }; + } else if (type === MESSAGE_TYPES.POLL) { + } else if (type === MESSAGE_TYPES.NOTIFICATION) { + const params = { + channelName: state?.attributes.name, + channelDescription: state?.attributes.description, + ...data.notification + }; + delete params.type; + newMessage = { + ...newMessage, + notification: { type: data.notification.type, params } + }; + } else if (type === MESSAGE_TYPES.INTERACTIVE) { + newMessage = { + ...newMessage, + proposal: data.proposal + }; + } + return newMessage; + } + async function leaveChatRoom({ contractID }) { + const rootState = (0, import_sbp.default)("state/vuex/state"); + const rootGetters = (0, import_sbp.default)("state/vuex/getters"); + if (contractID === rootGetters.currentChatRoomId) { + (0, import_sbp.default)("state/vuex/commit", "setCurrentChatRoomId", { + groupId: rootState.currentGroupId + }); + const curRouteName = (0, import_sbp.default)("controller/router").history.current.name; + if (curRouteName === "GroupChat" || curRouteName === "GroupChatConversation") { + await (0, import_sbp.default)("controller/router").push({ name: "GroupChatConversation", params: { chatRoomId: rootGetters.currentChatRoomId } }).catch(logExceptNavigationDuplicated); + } + } + (0, import_sbp.default)("state/vuex/commit", "deleteChatRoomUnread", { chatRoomId: contractID }); + (0, import_sbp.default)("state/vuex/commit", "deleteChatRoomScrollPosition", { chatRoomId: contractID }); + (0, import_sbp.default)("chelonia/contract/remove", contractID).catch((e) => { + console.error(`leaveChatRoom(${contractID}): remove threw ${e.name}:`, e); + }); + } + function findMessageIdx(hash, messages) { + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].hash === hash) { + return i; + } + } + return -1; + } + function makeMentionFromUsername(username) { + return { + me: `@${username}`, + all: "@all" + }; + } + + // frontend/model/contracts/shared/nativeNotification.js + var import_sbp2 = __toESM(__require("@sbp/sbp")); + function makeNotification({ title, body, icon, path }) { + if (Notification?.permission === "granted" && (0, import_sbp2.default)("state/vuex/settings").notificationEnabled) { + const notification = new Notification(title, { body, icon }); + if (path) { + notification.onclick = function(event) { + (0, import_sbp2.default)("controller/router").push({ path }).catch(console.warn); + }; + } + } + } + + // frontend/model/contracts/chatroom.js + function createNotificationData(notificationType, moreParams = {}) { + return { + type: MESSAGE_TYPES.NOTIFICATION, + notification: { + type: notificationType, + ...moreParams + } + }; + } + function emitMessageEvent({ contractID, hash }) { + if ((0, import_sbp3.default)("chelonia/contract/isSyncing", contractID)) { + return; + } + (0, import_sbp3.default)("okTurtles.events/emit", `${CHATROOM_MESSAGE_ACTION}-${contractID}`, { hash }); + } + function messageReceivePostEffect({ contractID, messageHash, datetime, text, isAlreadyAdded, isMentionedMe, username, chatRoomName }) { + if ((0, import_sbp3.default)("chelonia/contract/isSyncing", contractID)) { + return; + } + const rootGetters = (0, import_sbp3.default)("state/vuex/getters"); + const isDirectMessage = rootGetters.isDirectMessage(contractID); + const isDMOrMention = isMentionedMe || isDirectMessage; + if (!isAlreadyAdded && isDMOrMention) { + (0, import_sbp3.default)("state/vuex/commit", "addChatRoomUnreadMention", { + chatRoomId: contractID, + messageHash, + createdDate: datetime + }); + } + let title = `# ${chatRoomName}`; + let partnerProfile; + if (isDirectMessage) { + if (rootGetters.isGroupDirectMessage(contractID)) { + title = `# ${rootGetters.groupDirectMessageInfo(contractID).title}`; + } else { + partnerProfile = rootGetters.ourContactProfiles[username]; + title = `# ${partnerProfile?.displayName || username}`; + } + } + const path = `/group-chat/${contractID}`; + const notificationSettings = rootGetters.notificationSettings[contractID] || rootGetters.notificationSettings.default; + const { messageNotification, messageSound } = notificationSettings; + const shouldNotifyMessage = messageNotification === MESSAGE_NOTIFY_SETTINGS.ALL_MESSAGES || messageNotification === MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES && isDMOrMention; + const shouldSoundMessage = messageSound === MESSAGE_NOTIFY_SETTINGS.ALL_MESSAGES || messageSound === MESSAGE_NOTIFY_SETTINGS.DIRECT_MESSAGES && isDMOrMention; + if (!isAlreadyAdded && shouldNotifyMessage) { + makeNotification({ + title, + body: text, + icon: partnerProfile?.picture, + path + }); + } + if (!isAlreadyAdded && shouldSoundMessage) { + (0, import_sbp3.default)("okTurtles.events/emit", MESSAGE_RECEIVE); + } + } + function updateUnreadPosition({ contractID, hash, createdDate }) { + if ((0, import_sbp3.default)("chelonia/contract/isSyncing", contractID)) { + return; + } + (0, import_sbp3.default)("state/vuex/commit", "setChatRoomUnreadSince", { + chatRoomId: contractID, + messageHash: hash, + createdDate + }); + } + (0, import_sbp3.default)("chelonia/defineContract", { + name: "gi.contracts/chatroom", + metadata: { + validate: objectOf({ + createdDate: string, + username: string, + identityContractID: string + }), + async create() { + const { username, identityContractID } = (0, import_sbp3.default)("state/vuex/state").loggedIn; + return { + createdDate: await fetchServerTime(), + username, + identityContractID + }; + } + }, + getters: { + currentChatRoomState(state) { + return state; + }, + chatRoomSettings(state, getters) { + return getters.currentChatRoomState.settings || {}; + }, + chatRoomAttributes(state, getters) { + return getters.currentChatRoomState.attributes || {}; + }, + chatRoomUsers(state, getters) { + return getters.currentChatRoomState.users || {}; + }, + chatRoomLatestMessages(state, getters) { + return getters.currentChatRoomState.messages || []; + } + }, + actions: { + "gi.contracts/chatroom": { + validate: objectOf({ + attributes: chatRoomAttributesType + }), + process({ meta, data }, { state }) { + const initialState = merge({ + settings: { + actionsPerPage: CHATROOM_ACTIONS_PER_PAGE, + maxNameLength: CHATROOM_NAME_LIMITS_IN_CHARS, + maxDescriptionLength: CHATROOM_DESCRIPTION_LIMITS_IN_CHARS + }, + attributes: { + creator: meta.username, + deletedDate: null, + archivedDate: null + }, + users: {}, + messages: [] + }, data); + for (const key in initialState) { + import_common2.Vue.set(state, key, initialState[key]); + } + } + }, + "gi.contracts/chatroom/join": { + validate: objectOf({ + username: string + }), + process({ data, meta, hash, id }, { state }) { + const { username } = data; + if (!state.onlyRenderMessage && state.users[username]) { + console.warn("Can not join the chatroom which you are already part of"); + return; + } + import_common2.Vue.set(state.users, username, { joinedDate: meta.createdDate }); + if (!state.onlyRenderMessage || state.attributes.type === CHATROOM_TYPES.INDIVIDUAL && state.attributes.privacyLevel === CHATROOM_PRIVACY_LEVEL.PRIVATE) { + return; + } + const notificationType = username === meta.username ? MESSAGE_NOTIFICATIONS.JOIN_MEMBER : MESSAGE_NOTIFICATIONS.ADD_MEMBER; + const notificationData = createNotificationData(notificationType, notificationType === MESSAGE_NOTIFICATIONS.ADD_MEMBER ? { username } : {}); + const newMessage = createMessage({ meta, hash, id, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + }, + "gi.contracts/chatroom/rename": { + validate: objectOf({ + name: string + }), + process({ data, meta, hash, id }, { state }) { + import_common2.Vue.set(state.attributes, "name", data.name); + if (!state.onlyRenderMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_NAME, {}); + const newMessage = createMessage({ meta, hash, id, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + }, + "gi.contracts/chatroom/changeDescription": { + validate: objectOf({ + description: string + }), + process({ data, meta, hash, id }, { state }) { + import_common2.Vue.set(state.attributes, "description", data.description); + if (!state.onlyRenderMessage) { + return; + } + const notificationData = createNotificationData(MESSAGE_NOTIFICATIONS.UPDATE_DESCRIPTION, {}); + const newMessage = createMessage({ meta, hash, id, data: notificationData, state }); + state.messages.push(newMessage); + }, + sideEffect({ contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + }, + "gi.contracts/chatroom/leave": { + validate: objectOf({ + username: optional(string), + member: string + }), + process({ data, meta, hash, id }, { state }) { + const { member } = data; + const isKicked = data.username && member !== data.username; + if (!state.onlyRenderMessage && !state.users[member]) { + throw new Error(`Can not leave the chatroom which ${member} are not part of`); + } + import_common2.Vue.delete(state.users, member); + if (!state.onlyRenderMessage || state.attributes.type === CHATROOM_TYPES.INDIVIDUAL) { + return; + } + const notificationType = !isKicked ? MESSAGE_NOTIFICATIONS.LEAVE_MEMBER : MESSAGE_NOTIFICATIONS.KICK_MEMBER; + const notificationData = createNotificationData(notificationType, isKicked ? { username: member } : {}); + const newMessage = createMessage({ + meta: isKicked ? meta : { ...meta, username: member }, + hash, + id, + data: notificationData, + state + }); + state.messages.push(newMessage); + }, + sideEffect({ data, hash, contractID, meta }, { state }) { + const rootState = (0, import_sbp3.default)("state/vuex/state"); + if (data.member === rootState.loggedIn.username) { + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + if ((0, import_sbp3.default)("chelonia/contract/isSyncing", contractID)) { + return; + } + leaveChatRoom({ contractID }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/delete": { + validate: (data, { state, meta }) => { + if (state.attributes.creator !== meta.username) { + throw new TypeError((0, import_common2.L)("Only the channel creator can delete channel.")); + } + }, + process({ data, meta }, { state, rootState }) { + import_common2.Vue.set(state.attributes, "deletedDate", meta.createdDate); + for (const username in state.users) { + import_common2.Vue.delete(state.users, username); + } + }, + sideEffect({ meta, contractID }, { state }) { + if ((0, import_sbp3.default)("chelonia/contract/isSyncing", contractID)) { + return; + } + leaveChatRoom({ contractID }); + } + }, + "gi.contracts/chatroom/addMessage": { + validate: messageType, + process({ data, meta, hash, id }, { state }) { + if (!state.onlyRenderMessage) { + return; + } + const pendingMsg = state.messages.find((msg) => msg.id === id && msg.pending); + if (pendingMsg) { + delete pendingMsg.pending; + pendingMsg.hash = hash; + } else { + state.messages.push(createMessage({ meta, data, hash, id, state })); + } + }, + sideEffect({ contractID, hash, id, meta, data }, { state, getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const newMessage = createMessage({ meta, data, hash, id, state }); + const mentions = makeMentionFromUsername(me); + const isTextMessage = data.type === MESSAGE_TYPES.TEXT; + const isMentionedMe = isTextMessage && (newMessage.text.includes(mentions.me) || newMessage.text.includes(mentions.all)); + messageReceivePostEffect({ + contractID, + messageHash: newMessage.hash, + datetime: newMessage.datetime, + text: newMessage.text, + isMentionedMe, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + updateUnreadPosition({ contractID, hash, createdDate: meta.createdDate }); + } + }, + "gi.contracts/chatroom/editMessage": { + validate: objectOf({ + hash: string, + createdDate: string, + text: string + }), + process({ data, meta }, { state }) { + if (!state.onlyRenderMessage) { + return; + } + const msgIndex = findMessageIdx(data.hash, state.messages); + if (msgIndex >= 0 && meta.username === state.messages[msgIndex].from) { + state.messages[msgIndex].text = data.text; + state.messages[msgIndex].updatedDate = meta.createdDate; + if (state.onlyRenderMessage && state.messages[msgIndex].pending) { + delete state.messages[msgIndex].pending; + } + } + }, + sideEffect({ contractID, hash, meta, data }, { getters }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (me === meta.username) { + return; + } + const isAlreadyAdded = rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageHash === data.hash); + const mentions = makeMentionFromUsername(me); + const isMentionedMe = data.text.includes(mentions.me) || data.text.includes(mentions.all); + messageReceivePostEffect({ + contractID, + messageHash: data.hash, + datetime: data.createdDate, + text: data.text, + isAlreadyAdded, + isMentionedMe, + username: meta.username, + chatRoomName: getters.chatRoomAttributes.name + }); + if (isAlreadyAdded && !isMentionedMe) { + (0, import_sbp3.default)("state/vuex/commit", "deleteChatRoomUnreadMention", { + chatRoomId: contractID, + messageHash: data.hash + }); + } + } + }, + "gi.contracts/chatroom/deleteMessage": { + validate: objectOf({ hash: string }), + process({ data, meta }, { state }) { + if (!state.onlyRenderMessage) { + return; + } + const msgIndex = findMessageIdx(data.hash, state.messages); + if (msgIndex >= 0) { + state.messages.splice(msgIndex, 1); + } + for (const message of state.messages) { + if (message.replyingMessage?.hash === data.hash) { + message.replyingMessage.hash = null; + message.replyingMessage.text = (0, import_common2.L)("Original message was removed by {username}", { + username: makeMentionFromUsername(meta.username).me + }); + } + } + }, + sideEffect({ data, contractID, hash, meta }) { + emitMessageEvent({ contractID, hash }); + const rootState = (0, import_sbp3.default)("state/vuex/state"); + const me = rootState.loggedIn.username; + if (rootState.chatRoomScrollPosition[contractID] === data.hash) { + (0, import_sbp3.default)("state/vuex/commit", "setChatRoomScrollPosition", { + chatRoomId: contractID, + messageHash: null + }); + } + if (rootState.chatRoomUnread[contractID].since.messageHash === data.hash) { + (0, import_sbp3.default)("state/vuex/commit", "deleteChatRoomUnreadSince", { + chatRoomId: contractID, + deletedDate: meta.createdDate + }); + } + if (me === meta.username) { + return; + } + if (rootState.chatRoomUnread[contractID].mentions.find((m) => m.messageHash === data.hash)) { + (0, import_sbp3.default)("state/vuex/commit", "deleteChatRoomUnreadMention", { + chatRoomId: contractID, + messageHash: data.hash + }); + } + emitMessageEvent({ contractID, hash }); + } + }, + "gi.contracts/chatroom/makeEmotion": { + validate: objectOf({ + hash: string, + emoticon: string + }), + process({ data, meta, contractID }, { state }) { + if (!state.onlyRenderMessage) { + return; + } + const { hash, emoticon } = data; + const msgIndex = findMessageIdx(hash, state.messages); + if (msgIndex >= 0) { + let emoticons = cloneDeep(state.messages[msgIndex].emoticons || {}); + if (emoticons[emoticon]) { + const alreadyAdded = emoticons[emoticon].indexOf(meta.username); + if (alreadyAdded >= 0) { + emoticons[emoticon].splice(alreadyAdded, 1); + if (!emoticons[emoticon].length) { + delete emoticons[emoticon]; + if (!Object.keys(emoticons).length) { + emoticons = null; + } + } + } else { + emoticons[emoticon].push(meta.username); + } + } else { + emoticons[emoticon] = [meta.username]; + } + if (emoticons) { + import_common2.Vue.set(state.messages[msgIndex], "emoticons", emoticons); + } else { + import_common2.Vue.delete(state.messages[msgIndex], "emoticons"); + } + } + }, + sideEffect({ contractID, hash }) { + emitMessageEvent({ contractID, hash }); + } + } + } + }); +})(); diff --git a/contracts/0.0.21/chatroom.0.0.21.manifest.json b/contracts/0.0.21/chatroom.0.0.21.manifest.json new file mode 100644 index 0000000000..7cb2505021 --- /dev/null +++ b/contracts/0.0.21/chatroom.0.0.21.manifest.json @@ -0,0 +1 @@ +{"head":{"manifestVersion":"1.0.0"},"body":"{\"version\":\"0.0.21\",\"contract\":{\"hash\":\"21XWnNJ57zeyzwFJTNRZZdPYs4tZ8SNAGYQp8P3E6uoGGczGmM\",\"file\":\"chatroom.js\"},\"authors\":[{\"cipher\":\"algo\",\"key\":\"\"},{\"cipher\":\"algo\",\"key\":\"\"}],\"contractSlim\":{\"file\":\"chatroom-slim.js\",\"hash\":\"21XWnNGasM1itTwxfhwx6t2F7ycVrsKYiZKPaMxeWNszruRxXk\"}}","signature":{"key":"","signature":""}} \ No newline at end of file diff --git a/contracts/0.0.21/chatroom.js b/contracts/0.0.21/chatroom.js new file mode 100644 index 0000000000..63e345c429 --- /dev/null +++ b/contracts/0.0.21/chatroom.js @@ -0,0 +1,9934 @@ +"use strict"; +(() => { + var __create = Object.create; + var __defProp = Object.defineProperty; + var __getOwnPropDesc = Object.getOwnPropertyDescriptor; + var __getOwnPropNames = Object.getOwnPropertyNames; + var __getProtoOf = Object.getPrototypeOf; + var __hasOwnProp = Object.prototype.hasOwnProperty; + var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { + get: (a, b) => (typeof require !== "undefined" ? require : a)[b] + }) : x)(function(x) { + if (typeof require !== "undefined") + return require.apply(this, arguments); + throw new Error('Dynamic require of "' + x + '" is not supported'); + }); + var __commonJS = (cb, mod) => function __require2() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + }; + var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; + }; + var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2, mod)); + + // node_modules/dompurify/dist/purify.js + var require_purify = __commonJS({ + "node_modules/dompurify/dist/purify.js"(exports, module) { + (function(global2, factory) { + typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global2 = global2 || self, global2.DOMPurify = factory()); + })(exports, function() { + "use strict"; + function _toConsumableArray(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } else { + return Array.from(arr); + } + } + var hasOwnProperty2 = Object.hasOwnProperty, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + var freeze = Object.freeze, seal = Object.seal, create2 = Object.create; + var _ref = typeof Reflect !== "undefined" && Reflect, apply = _ref.apply, construct = _ref.construct; + if (!apply) { + apply = function apply2(fun, thisValue, args) { + return fun.apply(thisValue, args); + }; + } + if (!freeze) { + freeze = function freeze2(x) { + return x; + }; + } + if (!seal) { + seal = function seal2(x) { + return x; + }; + } + if (!construct) { + construct = function construct2(Func, args) { + return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))(); + }; + } + var arrayForEach = unapply(Array.prototype.forEach); + var arrayPop = unapply(Array.prototype.pop); + var arrayPush = unapply(Array.prototype.push); + var stringToLowerCase = unapply(String.prototype.toLowerCase); + var stringMatch = unapply(String.prototype.match); + var stringReplace = unapply(String.prototype.replace); + var stringIndexOf = unapply(String.prototype.indexOf); + var stringTrim = unapply(String.prototype.trim); + var regExpTest = unapply(RegExp.prototype.test); + var typeErrorCreate = unconstruct(TypeError); + function unapply(func) { + return function(thisArg) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + return apply(func, thisArg, args); + }; + } + function unconstruct(func) { + return function() { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + return construct(func, args); + }; + } + function addToSet(set2, array) { + if (setPrototypeOf) { + setPrototypeOf(set2, null); + } + var l = array.length; + while (l--) { + var element = array[l]; + if (typeof element === "string") { + var lcElement = stringToLowerCase(element); + if (lcElement !== element) { + if (!isFrozen(array)) { + array[l] = lcElement; + } + element = lcElement; + } + } + set2[element] = true; + } + return set2; + } + function clone(object2) { + var newObject = create2(null); + var property = void 0; + for (property in object2) { + if (apply(hasOwnProperty2, object2, [property])) { + newObject[property] = object2[property]; + } + } + return newObject; + } + function lookupGetter(object2, prop) { + while (object2 !== null) { + var desc = getOwnPropertyDescriptor(object2, prop); + if (desc) { + if (desc.get) { + return unapply(desc.get); + } + if (typeof desc.value === "function") { + return unapply(desc.value); + } + } + object2 = getPrototypeOf(object2); + } + function fallbackValue(element) { + console.warn("fallback value for", element); + return null; + } + return fallbackValue; + } + var html2 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "section", "select", "shadow", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]); + var svg = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]); + var svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]); + var svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "fedropshadow", "feimage", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]); + var mathMl = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover"]); + var mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]); + var text2 = freeze(["#text"]); + var html$1 = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "noshade", "novalidate", "nowrap", "open", "optimum", "pattern", "placeholder", "playsinline", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "xmlns"]); + var svg$1 = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "targetx", "targety", "transform", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]); + var mathMl$1 = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnsalign", "columnlines", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lspace", "lquote", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]); + var xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]); + var MUSTACHE_EXPR = seal(/\{\{[\s\S]*|[\s\S]*\}\}/gm); + var ERB_EXPR = seal(/<%[\s\S]*|[\s\S]*%>/gm); + var DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]/); + var ARIA_ATTR = seal(/^aria-[\-\w]+$/); + var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i); + var IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); + var ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g); + var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { + return typeof obj; + } : function(obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; + }; + function _toConsumableArray$1(arr) { + if (Array.isArray(arr)) { + for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { + arr2[i] = arr[i]; + } + return arr2; + } else { + return Array.from(arr); + } + } + var getGlobal = function getGlobal2() { + return typeof window === "undefined" ? null : window; + }; + var _createTrustedTypesPolicy = function _createTrustedTypesPolicy2(trustedTypes, document2) { + if ((typeof trustedTypes === "undefined" ? "undefined" : _typeof(trustedTypes)) !== "object" || typeof trustedTypes.createPolicy !== "function") { + return null; + } + var suffix = null; + var ATTR_NAME = "data-tt-policy-suffix"; + if (document2.currentScript && document2.currentScript.hasAttribute(ATTR_NAME)) { + suffix = document2.currentScript.getAttribute(ATTR_NAME); + } + var policyName = "dompurify" + (suffix ? "#" + suffix : ""); + try { + return trustedTypes.createPolicy(policyName, { + createHTML: function createHTML(html$$1) { + return html$$1; + } + }); + } catch (_) { + console.warn("TrustedTypes policy " + policyName + " could not be created."); + return null; + } + }; + function createDOMPurify() { + var window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal(); + var DOMPurify = function DOMPurify2(root) { + return createDOMPurify(root); + }; + DOMPurify.version = "2.2.7"; + DOMPurify.removed = []; + if (!window2 || !window2.document || window2.document.nodeType !== 9) { + DOMPurify.isSupported = false; + return DOMPurify; + } + var originalDocument = window2.document; + var document2 = window2.document; + var DocumentFragment = window2.DocumentFragment, HTMLTemplateElement = window2.HTMLTemplateElement, Node = window2.Node, Element = window2.Element, NodeFilter = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap, Text = window2.Text, Comment = window2.Comment, DOMParser = window2.DOMParser, trustedTypes = window2.trustedTypes; + var ElementPrototype = Element.prototype; + var cloneNode = lookupGetter(ElementPrototype, "cloneNode"); + var getNextSibling = lookupGetter(ElementPrototype, "nextSibling"); + var getChildNodes = lookupGetter(ElementPrototype, "childNodes"); + var getParentNode = lookupGetter(ElementPrototype, "parentNode"); + if (typeof HTMLTemplateElement === "function") { + var template2 = document2.createElement("template"); + if (template2.content && template2.content.ownerDocument) { + document2 = template2.content.ownerDocument; + } + } + var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument); + var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML("") : ""; + var _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, getElementsByTagName = _document.getElementsByTagName, createDocumentFragment = _document.createDocumentFragment; + var importNode = originalDocument.importNode; + var documentMode = {}; + try { + documentMode = clone(document2).documentMode ? document2.documentMode : {}; + } catch (_) { + } + var hooks2 = {}; + DOMPurify.isSupported = typeof getParentNode === "function" && implementation && typeof implementation.createHTMLDocument !== "undefined" && documentMode !== 9; + var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR, ERB_EXPR$$1 = ERB_EXPR, DATA_ATTR$$1 = DATA_ATTR, ARIA_ATTR$$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$$1 = ATTR_WHITESPACE; + var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI; + var ALLOWED_TAGS = null; + var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html2), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text2))); + var ALLOWED_ATTR = null; + var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml))); + var FORBID_TAGS = null; + var FORBID_ATTR = null; + var ALLOW_ARIA_ATTR = true; + var ALLOW_DATA_ATTR = true; + var ALLOW_UNKNOWN_PROTOCOLS = false; + var SAFE_FOR_TEMPLATES = false; + var WHOLE_DOCUMENT = false; + var SET_CONFIG = false; + var FORCE_BODY = false; + var RETURN_DOM = false; + var RETURN_DOM_FRAGMENT = false; + var RETURN_DOM_IMPORT = true; + var RETURN_TRUSTED_TYPE = false; + var SANITIZE_DOM = true; + var KEEP_CONTENT = true; + var IN_PLACE = false; + var USE_PROFILES = {}; + var FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]); + var DATA_URI_TAGS = null; + var DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]); + var URI_SAFE_ATTRIBUTES = null; + var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "summary", "title", "value", "style", "xmlns"]); + var CONFIG = null; + var formElement = document2.createElement("form"); + var _parseConfig = function _parseConfig2(cfg) { + if (CONFIG && CONFIG === cfg) { + return; + } + if (!cfg || (typeof cfg === "undefined" ? "undefined" : _typeof(cfg)) !== "object") { + cfg = {}; + } + cfg = clone(cfg); + ALLOWED_TAGS = "ALLOWED_TAGS" in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS; + ALLOWED_ATTR = "ALLOWED_ATTR" in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR; + URI_SAFE_ATTRIBUTES = "ADD_URI_SAFE_ATTR" in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES; + DATA_URI_TAGS = "ADD_DATA_URI_TAGS" in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS; + FORBID_TAGS = "FORBID_TAGS" in cfg ? addToSet({}, cfg.FORBID_TAGS) : {}; + FORBID_ATTR = "FORBID_ATTR" in cfg ? addToSet({}, cfg.FORBID_ATTR) : {}; + USE_PROFILES = "USE_PROFILES" in cfg ? cfg.USE_PROFILES : false; + ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; + ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; + ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; + SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; + WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; + RETURN_DOM = cfg.RETURN_DOM || false; + RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; + RETURN_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; + RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; + FORCE_BODY = cfg.FORCE_BODY || false; + SANITIZE_DOM = cfg.SANITIZE_DOM !== false; + KEEP_CONTENT = cfg.KEEP_CONTENT !== false; + IN_PLACE = cfg.IN_PLACE || false; + IS_ALLOWED_URI$$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI$$1; + if (SAFE_FOR_TEMPLATES) { + ALLOW_DATA_ATTR = false; + } + if (RETURN_DOM_FRAGMENT) { + RETURN_DOM = true; + } + if (USE_PROFILES) { + ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text2))); + ALLOWED_ATTR = []; + if (USE_PROFILES.html === true) { + addToSet(ALLOWED_TAGS, html2); + addToSet(ALLOWED_ATTR, html$1); + } + if (USE_PROFILES.svg === true) { + addToSet(ALLOWED_TAGS, svg); + addToSet(ALLOWED_ATTR, svg$1); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.svgFilters === true) { + addToSet(ALLOWED_TAGS, svgFilters); + addToSet(ALLOWED_ATTR, svg$1); + addToSet(ALLOWED_ATTR, xml); + } + if (USE_PROFILES.mathMl === true) { + addToSet(ALLOWED_TAGS, mathMl); + addToSet(ALLOWED_ATTR, mathMl$1); + addToSet(ALLOWED_ATTR, xml); + } + } + if (cfg.ADD_TAGS) { + if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { + ALLOWED_TAGS = clone(ALLOWED_TAGS); + } + addToSet(ALLOWED_TAGS, cfg.ADD_TAGS); + } + if (cfg.ADD_ATTR) { + if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { + ALLOWED_ATTR = clone(ALLOWED_ATTR); + } + addToSet(ALLOWED_ATTR, cfg.ADD_ATTR); + } + if (cfg.ADD_URI_SAFE_ATTR) { + addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR); + } + if (KEEP_CONTENT) { + ALLOWED_TAGS["#text"] = true; + } + if (WHOLE_DOCUMENT) { + addToSet(ALLOWED_TAGS, ["html", "head", "body"]); + } + if (ALLOWED_TAGS.table) { + addToSet(ALLOWED_TAGS, ["tbody"]); + delete FORBID_TAGS.tbody; + } + if (freeze) { + freeze(cfg); + } + CONFIG = cfg; + }; + var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]); + var HTML_INTEGRATION_POINTS = addToSet({}, ["foreignobject", "desc", "title", "annotation-xml"]); + var ALL_SVG_TAGS = addToSet({}, svg); + addToSet(ALL_SVG_TAGS, svgFilters); + addToSet(ALL_SVG_TAGS, svgDisallowed); + var ALL_MATHML_TAGS = addToSet({}, mathMl); + addToSet(ALL_MATHML_TAGS, mathMlDisallowed); + var MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML"; + var SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + var HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; + var _checkValidNamespace = function _checkValidNamespace2(element) { + var parent = getParentNode(element); + if (!parent || !parent.tagName) { + parent = { + namespaceURI: HTML_NAMESPACE, + tagName: "template" + }; + } + var tagName2 = stringToLowerCase(element.tagName); + var parentTagName = stringToLowerCase(parent.tagName); + if (element.namespaceURI === SVG_NAMESPACE) { + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName2 === "svg"; + } + if (parent.namespaceURI === MATHML_NAMESPACE) { + return tagName2 === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); + } + return Boolean(ALL_SVG_TAGS[tagName2]); + } + if (element.namespaceURI === MATHML_NAMESPACE) { + if (parent.namespaceURI === HTML_NAMESPACE) { + return tagName2 === "math"; + } + if (parent.namespaceURI === SVG_NAMESPACE) { + return tagName2 === "math" && HTML_INTEGRATION_POINTS[parentTagName]; + } + return Boolean(ALL_MATHML_TAGS[tagName2]); + } + if (element.namespaceURI === HTML_NAMESPACE) { + if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { + return false; + } + if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { + return false; + } + var commonSvgAndHTMLElements = addToSet({}, ["title", "style", "font", "a", "script"]); + return !ALL_MATHML_TAGS[tagName2] && (commonSvgAndHTMLElements[tagName2] || !ALL_SVG_TAGS[tagName2]); + } + return false; + }; + var _forceRemove = function _forceRemove2(node) { + arrayPush(DOMPurify.removed, { element: node }); + try { + node.parentNode.removeChild(node); + } catch (_) { + try { + node.outerHTML = emptyHTML; + } catch (_2) { + node.remove(); + } + } + }; + var _removeAttribute = function _removeAttribute2(name, node) { + try { + arrayPush(DOMPurify.removed, { + attribute: node.getAttributeNode(name), + from: node + }); + } catch (_) { + arrayPush(DOMPurify.removed, { + attribute: null, + from: node + }); + } + node.removeAttribute(name); + if (name === "is" && !ALLOWED_ATTR[name]) { + if (RETURN_DOM || RETURN_DOM_FRAGMENT) { + try { + _forceRemove(node); + } catch (_) { + } + } else { + try { + node.setAttribute(name, ""); + } catch (_) { + } + } + } + }; + var _initDocument = function _initDocument2(dirty) { + var doc = void 0; + var leadingWhitespace = void 0; + if (FORCE_BODY) { + dirty = "" + dirty; + } else { + var matches2 = stringMatch(dirty, /^[\r\n\t ]+/); + leadingWhitespace = matches2 && matches2[0]; + } + var dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; + try { + doc = new DOMParser().parseFromString(dirtyPayload, "text/html"); + } catch (_) { + } + if (!doc || !doc.documentElement) { + doc = implementation.createHTMLDocument(""); + var _doc = doc, body = _doc.body; + body.parentNode.removeChild(body.parentNode.firstElementChild); + body.outerHTML = dirtyPayload; + } + if (dirty && leadingWhitespace) { + doc.body.insertBefore(document2.createTextNode(leadingWhitespace), doc.body.childNodes[0] || null); + } + return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0]; + }; + var _createIterator = function _createIterator2(root) { + return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, function() { + return NodeFilter.FILTER_ACCEPT; + }, false); + }; + var _isClobbered = function _isClobbered2(elm) { + if (elm instanceof Text || elm instanceof Comment) { + return false; + } + if (typeof elm.nodeName !== "string" || typeof elm.textContent !== "string" || typeof elm.removeChild !== "function" || !(elm.attributes instanceof NamedNodeMap) || typeof elm.removeAttribute !== "function" || typeof elm.setAttribute !== "function" || typeof elm.namespaceURI !== "string" || typeof elm.insertBefore !== "function") { + return true; + } + return false; + }; + var _isNode = function _isNode2(object2) { + return (typeof Node === "undefined" ? "undefined" : _typeof(Node)) === "object" ? object2 instanceof Node : object2 && (typeof object2 === "undefined" ? "undefined" : _typeof(object2)) === "object" && typeof object2.nodeType === "number" && typeof object2.nodeName === "string"; + }; + var _executeHook = function _executeHook2(entryPoint, currentNode, data) { + if (!hooks2[entryPoint]) { + return; + } + arrayForEach(hooks2[entryPoint], function(hook) { + hook.call(DOMPurify, currentNode, data, CONFIG); + }); + }; + var _sanitizeElements = function _sanitizeElements2(currentNode) { + var content = void 0; + _executeHook("beforeSanitizeElements", currentNode, null); + if (_isClobbered(currentNode)) { + _forceRemove(currentNode); + return true; + } + if (stringMatch(currentNode.nodeName, /[\u0080-\uFFFF]/)) { + _forceRemove(currentNode); + return true; + } + var tagName2 = stringToLowerCase(currentNode.nodeName); + _executeHook("uponSanitizeElement", currentNode, { + tagName: tagName2, + allowedTags: ALLOWED_TAGS + }); + if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\w]/g, currentNode.innerHTML) && regExpTest(/<[/\w]/g, currentNode.textContent)) { + _forceRemove(currentNode); + return true; + } + if (!ALLOWED_TAGS[tagName2] || FORBID_TAGS[tagName2]) { + if (KEEP_CONTENT && !FORBID_CONTENTS[tagName2]) { + var parentNode2 = getParentNode(currentNode); + var childNodes = getChildNodes(currentNode); + if (childNodes && parentNode2) { + var childCount = childNodes.length; + for (var i = childCount - 1; i >= 0; --i) { + parentNode2.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode)); + } + } + } + _forceRemove(currentNode); + return true; + } + if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { + _forceRemove(currentNode); + return true; + } + if ((tagName2 === "noscript" || tagName2 === "noembed") && regExpTest(/<\/no(script|embed)/i, currentNode.innerHTML)) { + _forceRemove(currentNode); + return true; + } + if (SAFE_FOR_TEMPLATES && currentNode.nodeType === 3) { + content = currentNode.textContent; + content = stringReplace(content, MUSTACHE_EXPR$$1, " "); + content = stringReplace(content, ERB_EXPR$$1, " "); + if (currentNode.textContent !== content) { + arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() }); + currentNode.textContent = content; + } + } + _executeHook("afterSanitizeElements", currentNode, null); + return false; + }; + var _isValidAttribute = function _isValidAttribute2(lcTag, lcName, value) { + if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) { + return false; + } + if (ALLOW_DATA_ATTR && regExpTest(DATA_ATTR$$1, lcName)) + ; + else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)) + ; + else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { + return false; + } else if (URI_SAFE_ATTRIBUTES[lcName]) + ; + else if (regExpTest(IS_ALLOWED_URI$$1, stringReplace(value, ATTR_WHITESPACE$$1, ""))) + ; + else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) + ; + else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$$1, stringReplace(value, ATTR_WHITESPACE$$1, ""))) + ; + else if (!value) + ; + else { + return false; + } + return true; + }; + var _sanitizeAttributes = function _sanitizeAttributes2(currentNode) { + var attr = void 0; + var value = void 0; + var lcName = void 0; + var l = void 0; + _executeHook("beforeSanitizeAttributes", currentNode, null); + var attributes = currentNode.attributes; + if (!attributes) { + return; + } + var hookEvent = { + attrName: "", + attrValue: "", + keepAttr: true, + allowedAttributes: ALLOWED_ATTR + }; + l = attributes.length; + while (l--) { + attr = attributes[l]; + var _attr = attr, name = _attr.name, namespaceURI = _attr.namespaceURI; + value = stringTrim(attr.value); + lcName = stringToLowerCase(name); + hookEvent.attrName = lcName; + hookEvent.attrValue = value; + hookEvent.keepAttr = true; + hookEvent.forceKeepAttr = void 0; + _executeHook("uponSanitizeAttribute", currentNode, hookEvent); + value = hookEvent.attrValue; + if (hookEvent.forceKeepAttr) { + continue; + } + _removeAttribute(name, currentNode); + if (!hookEvent.keepAttr) { + continue; + } + if (regExpTest(/\/>/i, value)) { + _removeAttribute(name, currentNode); + continue; + } + if (SAFE_FOR_TEMPLATES) { + value = stringReplace(value, MUSTACHE_EXPR$$1, " "); + value = stringReplace(value, ERB_EXPR$$1, " "); + } + var lcTag = currentNode.nodeName.toLowerCase(); + if (!_isValidAttribute(lcTag, lcName, value)) { + continue; + } + try { + if (namespaceURI) { + currentNode.setAttributeNS(namespaceURI, name, value); + } else { + currentNode.setAttribute(name, value); + } + arrayPop(DOMPurify.removed); + } catch (_) { + } + } + _executeHook("afterSanitizeAttributes", currentNode, null); + }; + var _sanitizeShadowDOM = function _sanitizeShadowDOM2(fragment) { + var shadowNode = void 0; + var shadowIterator = _createIterator(fragment); + _executeHook("beforeSanitizeShadowDOM", fragment, null); + while (shadowNode = shadowIterator.nextNode()) { + _executeHook("uponSanitizeShadowNode", shadowNode, null); + if (_sanitizeElements(shadowNode)) { + continue; + } + if (shadowNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM2(shadowNode.content); + } + _sanitizeAttributes(shadowNode); + } + _executeHook("afterSanitizeShadowDOM", fragment, null); + }; + DOMPurify.sanitize = function(dirty, cfg) { + var body = void 0; + var importedNode = void 0; + var currentNode = void 0; + var oldNode = void 0; + var returnNode = void 0; + if (!dirty) { + dirty = ""; + } + if (typeof dirty !== "string" && !_isNode(dirty)) { + if (typeof dirty.toString !== "function") { + throw typeErrorCreate("toString is not a function"); + } else { + dirty = dirty.toString(); + if (typeof dirty !== "string") { + throw typeErrorCreate("dirty is not a string, aborting"); + } + } + } + if (!DOMPurify.isSupported) { + if (_typeof(window2.toStaticHTML) === "object" || typeof window2.toStaticHTML === "function") { + if (typeof dirty === "string") { + return window2.toStaticHTML(dirty); + } + if (_isNode(dirty)) { + return window2.toStaticHTML(dirty.outerHTML); + } + } + return dirty; + } + if (!SET_CONFIG) { + _parseConfig(cfg); + } + DOMPurify.removed = []; + if (typeof dirty === "string") { + IN_PLACE = false; + } + if (IN_PLACE) + ; + else if (dirty instanceof Node) { + body = _initDocument(""); + importedNode = body.ownerDocument.importNode(dirty, true); + if (importedNode.nodeType === 1 && importedNode.nodeName === "BODY") { + body = importedNode; + } else if (importedNode.nodeName === "HTML") { + body = importedNode; + } else { + body.appendChild(importedNode); + } + } else { + if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) { + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; + } + body = _initDocument(dirty); + if (!body) { + return RETURN_DOM ? null : emptyHTML; + } + } + if (body && FORCE_BODY) { + _forceRemove(body.firstChild); + } + var nodeIterator = _createIterator(IN_PLACE ? dirty : body); + while (currentNode = nodeIterator.nextNode()) { + if (currentNode.nodeType === 3 && currentNode === oldNode) { + continue; + } + if (_sanitizeElements(currentNode)) { + continue; + } + if (currentNode.content instanceof DocumentFragment) { + _sanitizeShadowDOM(currentNode.content); + } + _sanitizeAttributes(currentNode); + oldNode = currentNode; + } + oldNode = null; + if (IN_PLACE) { + return dirty; + } + if (RETURN_DOM) { + if (RETURN_DOM_FRAGMENT) { + returnNode = createDocumentFragment.call(body.ownerDocument); + while (body.firstChild) { + returnNode.appendChild(body.firstChild); + } + } else { + returnNode = body; + } + if (RETURN_DOM_IMPORT) { + returnNode = importNode.call(originalDocument, returnNode, true); + } + return returnNode; + } + var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; + if (SAFE_FOR_TEMPLATES) { + serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, " "); + serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, " "); + } + return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; + }; + DOMPurify.setConfig = function(cfg) { + _parseConfig(cfg); + SET_CONFIG = true; + }; + DOMPurify.clearConfig = function() { + CONFIG = null; + SET_CONFIG = false; + }; + DOMPurify.isValidAttribute = function(tag, attr, value) { + if (!CONFIG) { + _parseConfig({}); + } + var lcTag = stringToLowerCase(tag); + var lcName = stringToLowerCase(attr); + return _isValidAttribute(lcTag, lcName, value); + }; + DOMPurify.addHook = function(entryPoint, hookFunction) { + if (typeof hookFunction !== "function") { + return; + } + hooks2[entryPoint] = hooks2[entryPoint] || []; + arrayPush(hooks2[entryPoint], hookFunction); + }; + DOMPurify.removeHook = function(entryPoint) { + if (hooks2[entryPoint]) { + arrayPop(hooks2[entryPoint]); + } + }; + DOMPurify.removeHooks = function(entryPoint) { + if (hooks2[entryPoint]) { + hooks2[entryPoint] = []; + } + }; + DOMPurify.removeAllHooks = function() { + hooks2 = {}; + }; + return DOMPurify; + } + var purify = createDOMPurify(); + return purify; + }); + } + }); + + // frontend/model/contracts/chatroom.js + var import_sbp4 = __toESM(__require("@sbp/sbp")); + + // node_modules/vue/dist/vue.esm.js + var emptyObject = Object.freeze({}); + function isUndef(v) { + return v === void 0 || v === null; + } + function isDef(v) { + return v !== void 0 && v !== null; + } + function isTrue(v) { + return v === true; + } + function isFalse(v) { + return v === false; + } + function isPrimitive(value) { + return typeof value === "string" || typeof value === "number" || typeof value === "symbol" || typeof value === "boolean"; + } + function isObject(obj) { + return obj !== null && typeof obj === "object"; + } + var _toString = Object.prototype.toString; + function toRawType(value) { + return _toString.call(value).slice(8, -1); + } + function isPlainObject(obj) { + return _toString.call(obj) === "[object Object]"; + } + function isRegExp(v) { + return _toString.call(v) === "[object RegExp]"; + } + function isValidArrayIndex(val) { + var n = parseFloat(String(val)); + return n >= 0 && Math.floor(n) === n && isFinite(val); + } + function isPromise(val) { + return isDef(val) && typeof val.then === "function" && typeof val.catch === "function"; + } + function toString(val) { + return val == null ? "" : Array.isArray(val) || isPlainObject(val) && val.toString === _toString ? JSON.stringify(val, null, 2) : String(val); + } + function toNumber(val) { + var n = parseFloat(val); + return isNaN(n) ? val : n; + } + function makeMap(str2, expectsLowerCase) { + var map = /* @__PURE__ */ Object.create(null); + var list = str2.split(","); + for (var i = 0; i < list.length; i++) { + map[list[i]] = true; + } + return expectsLowerCase ? function(val) { + return map[val.toLowerCase()]; + } : function(val) { + return map[val]; + }; + } + var isBuiltInTag = makeMap("slot,component", true); + var isReservedAttribute = makeMap("key,ref,slot,slot-scope,is"); + function remove(arr, item) { + if (arr.length) { + var index2 = arr.indexOf(item); + if (index2 > -1) { + return arr.splice(index2, 1); + } + } + } + var hasOwnProperty = Object.prototype.hasOwnProperty; + function hasOwn(obj, key) { + return hasOwnProperty.call(obj, key); + } + function cached(fn) { + var cache = /* @__PURE__ */ Object.create(null); + return function cachedFn(str2) { + var hit = cache[str2]; + return hit || (cache[str2] = fn(str2)); + }; + } + var camelizeRE = /-(\w)/g; + var camelize = cached(function(str2) { + return str2.replace(camelizeRE, function(_, c) { + return c ? c.toUpperCase() : ""; + }); + }); + var capitalize = cached(function(str2) { + return str2.charAt(0).toUpperCase() + str2.slice(1); + }); + var hyphenateRE = /\B([A-Z])/g; + var hyphenate = cached(function(str2) { + return str2.replace(hyphenateRE, "-$1").toLowerCase(); + }); + function polyfillBind(fn, ctx) { + function boundFn(a) { + var l = arguments.length; + return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx); + } + boundFn._length = fn.length; + return boundFn; + } + function nativeBind(fn, ctx) { + return fn.bind(ctx); + } + var bind = Function.prototype.bind ? nativeBind : polyfillBind; + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + function extend(to, _from) { + for (var key in _from) { + to[key] = _from[key]; + } + return to; + } + function toObject(arr) { + var res = {}; + for (var i = 0; i < arr.length; i++) { + if (arr[i]) { + extend(res, arr[i]); + } + } + return res; + } + function noop(a, b, c) { + } + var no = function(a, b, c) { + return false; + }; + var identity = function(_) { + return _; + }; + function genStaticKeys(modules2) { + return modules2.reduce(function(keys, m) { + return keys.concat(m.staticKeys || []); + }, []).join(","); + } + function looseEqual(a, b) { + if (a === b) { + return true; + } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = Array.isArray(a); + var isArrayB = Array.isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function(e, i) { + return looseEqual(e, b[i]); + }); + } else if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function(key) { + return looseEqual(a[key], b[key]); + }); + } else { + return false; + } + } catch (e) { + return false; + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b); + } else { + return false; + } + } + function looseIndexOf(arr, val) { + for (var i = 0; i < arr.length; i++) { + if (looseEqual(arr[i], val)) { + return i; + } + } + return -1; + } + function once(fn) { + var called = false; + return function() { + if (!called) { + called = true; + fn.apply(this, arguments); + } + }; + } + var SSR_ATTR = "data-server-rendered"; + var ASSET_TYPES = [ + "component", + "directive", + "filter" + ]; + var LIFECYCLE_HOOKS = [ + "beforeCreate", + "created", + "beforeMount", + "mounted", + "beforeUpdate", + "updated", + "beforeDestroy", + "destroyed", + "activated", + "deactivated", + "errorCaptured", + "serverPrefetch" + ]; + var config = { + optionMergeStrategies: /* @__PURE__ */ Object.create(null), + silent: false, + productionTip: true, + devtools: true, + performance: false, + errorHandler: null, + warnHandler: null, + ignoredElements: [], + keyCodes: /* @__PURE__ */ Object.create(null), + isReservedTag: no, + isReservedAttr: no, + isUnknownElement: no, + getTagNamespace: noop, + parsePlatformTagName: identity, + mustUseProp: no, + async: true, + _lifecycleHooks: LIFECYCLE_HOOKS + }; + var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; + function isReserved(str2) { + var c = (str2 + "").charCodeAt(0); + return c === 36 || c === 95; + } + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + var bailRE = new RegExp("[^" + unicodeRegExp.source + ".$_\\d]"); + function parsePath(path) { + if (bailRE.test(path)) { + return; + } + var segments = path.split("."); + return function(obj) { + for (var i = 0; i < segments.length; i++) { + if (!obj) { + return; + } + obj = obj[segments[i]]; + } + return obj; + }; + } + var hasProto = "__proto__" in {}; + var inBrowser = typeof window !== "undefined"; + var inWeex = typeof WXEnvironment !== "undefined" && !!WXEnvironment.platform; + var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase(); + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && /msie|trident/.test(UA); + var isIE9 = UA && UA.indexOf("msie 9.0") > 0; + var isEdge = UA && UA.indexOf("edge/") > 0; + var isAndroid = UA && UA.indexOf("android") > 0 || weexPlatform === "android"; + var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA) || weexPlatform === "ios"; + var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; + var isPhantomJS = UA && /phantomjs/.test(UA); + var isFF = UA && UA.match(/firefox\/(\d+)/); + var nativeWatch = {}.watch; + var supportsPassive = false; + if (inBrowser) { + try { + opts = {}; + Object.defineProperty(opts, "passive", { + get: function get3() { + supportsPassive = true; + } + }); + window.addEventListener("test-passive", null, opts); + } catch (e) { + } + } + var opts; + var _isServer; + var isServerRendering = function() { + if (_isServer === void 0) { + if (!inBrowser && !inWeex && typeof global !== "undefined") { + _isServer = global["process"] && global["process"].env.VUE_ENV === "server"; + } else { + _isServer = false; + } + } + return _isServer; + }; + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + function isNative(Ctor) { + return typeof Ctor === "function" && /native code/.test(Ctor.toString()); + } + var hasSymbol = typeof Symbol !== "undefined" && isNative(Symbol) && typeof Reflect !== "undefined" && isNative(Reflect.ownKeys); + var _Set; + if (typeof Set !== "undefined" && isNative(Set)) { + _Set = Set; + } else { + _Set = /* @__PURE__ */ function() { + function Set2() { + this.set = /* @__PURE__ */ Object.create(null); + } + Set2.prototype.has = function has2(key) { + return this.set[key] === true; + }; + Set2.prototype.add = function add2(key) { + this.set[key] = true; + }; + Set2.prototype.clear = function clear() { + this.set = /* @__PURE__ */ Object.create(null); + }; + return Set2; + }(); + } + var warn = noop; + var tip = noop; + var generateComponentTrace = noop; + var formatComponentName = noop; + if (true) { + hasConsole = typeof console !== "undefined"; + classifyRE = /(?:^|[-_])(\w)/g; + classify = function(str2) { + return str2.replace(classifyRE, function(c) { + return c.toUpperCase(); + }).replace(/[-_]/g, ""); + }; + warn = function(msg, vm) { + var trace = vm ? generateComponentTrace(vm) : ""; + if (config.warnHandler) { + config.warnHandler.call(null, msg, vm, trace); + } else if (hasConsole && !config.silent) { + console.error("[Vue warn]: " + msg + trace); + } + }; + tip = function(msg, vm) { + if (hasConsole && !config.silent) { + console.warn("[Vue tip]: " + msg + (vm ? generateComponentTrace(vm) : "")); + } + }; + formatComponentName = function(vm, includeFile) { + if (vm.$root === vm) { + return ""; + } + var options = typeof vm === "function" && vm.cid != null ? vm.options : vm._isVue ? vm.$options || vm.constructor.options : vm; + var name = options.name || options._componentTag; + var file = options.__file; + if (!name && file) { + var match = file.match(/([^/\\]+)\.vue$/); + name = match && match[1]; + } + return (name ? "<" + classify(name) + ">" : "") + (file && includeFile !== false ? " at " + file : ""); + }; + repeat = function(str2, n) { + var res = ""; + while (n) { + if (n % 2 === 1) { + res += str2; + } + if (n > 1) { + str2 += str2; + } + n >>= 1; + } + return res; + }; + generateComponentTrace = function(vm) { + if (vm._isVue && vm.$parent) { + var tree = []; + var currentRecursiveSequence = 0; + while (vm) { + if (tree.length > 0) { + var last = tree[tree.length - 1]; + if (last.constructor === vm.constructor) { + currentRecursiveSequence++; + vm = vm.$parent; + continue; + } else if (currentRecursiveSequence > 0) { + tree[tree.length - 1] = [last, currentRecursiveSequence]; + currentRecursiveSequence = 0; + } + } + tree.push(vm); + vm = vm.$parent; + } + return "\n\nfound in\n\n" + tree.map(function(vm2, i) { + return "" + (i === 0 ? "---> " : repeat(" ", 5 + i * 2)) + (Array.isArray(vm2) ? formatComponentName(vm2[0]) + "... (" + vm2[1] + " recursive calls)" : formatComponentName(vm2)); + }).join("\n"); + } else { + return "\n\n(found in " + formatComponentName(vm) + ")"; + } + }; + } + var hasConsole; + var classifyRE; + var classify; + var repeat; + var uid = 0; + var Dep = function Dep2() { + this.id = uid++; + this.subs = []; + }; + Dep.prototype.addSub = function addSub(sub) { + this.subs.push(sub); + }; + Dep.prototype.removeSub = function removeSub(sub) { + remove(this.subs, sub); + }; + Dep.prototype.depend = function depend() { + if (Dep.target) { + Dep.target.addDep(this); + } + }; + Dep.prototype.notify = function notify() { + var subs = this.subs.slice(); + if (!config.async) { + subs.sort(function(a, b) { + return a.id - b.id; + }); + } + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + Dep.target = null; + var targetStack = []; + function pushTarget(target2) { + targetStack.push(target2); + Dep.target = target2; + } + function popTarget() { + targetStack.pop(); + Dep.target = targetStack[targetStack.length - 1]; + } + var VNode = function VNode2(tag, data, children, text2, elm, context, componentOptions, asyncFactory) { + this.tag = tag; + this.data = data; + this.children = children; + this.text = text2; + this.elm = elm; + this.ns = void 0; + this.context = context; + this.fnContext = void 0; + this.fnOptions = void 0; + this.fnScopeId = void 0; + this.key = data && data.key; + this.componentOptions = componentOptions; + this.componentInstance = void 0; + this.parent = void 0; + this.raw = false; + this.isStatic = false; + this.isRootInsert = true; + this.isComment = false; + this.isCloned = false; + this.isOnce = false; + this.asyncFactory = asyncFactory; + this.asyncMeta = void 0; + this.isAsyncPlaceholder = false; + }; + var prototypeAccessors = { child: { configurable: true } }; + prototypeAccessors.child.get = function() { + return this.componentInstance; + }; + Object.defineProperties(VNode.prototype, prototypeAccessors); + var createEmptyVNode = function(text2) { + if (text2 === void 0) + text2 = ""; + var node = new VNode(); + node.text = text2; + node.isComment = true; + return node; + }; + function createTextVNode(val) { + return new VNode(void 0, void 0, void 0, String(val)); + } + function cloneVNode(vnode) { + var cloned = new VNode(vnode.tag, vnode.data, vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory); + cloned.ns = vnode.ns; + cloned.isStatic = vnode.isStatic; + cloned.key = vnode.key; + cloned.isComment = vnode.isComment; + cloned.fnContext = vnode.fnContext; + cloned.fnOptions = vnode.fnOptions; + cloned.fnScopeId = vnode.fnScopeId; + cloned.asyncMeta = vnode.asyncMeta; + cloned.isCloned = true; + return cloned; + } + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto); + var methodsToPatch = [ + "push", + "pop", + "shift", + "unshift", + "splice", + "sort", + "reverse" + ]; + methodsToPatch.forEach(function(method) { + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + var args = [], len2 = arguments.length; + while (len2--) + args[len2] = arguments[len2]; + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted2; + switch (method) { + case "push": + case "unshift": + inserted2 = args; + break; + case "splice": + inserted2 = args.slice(2); + break; + } + if (inserted2) { + ob.observeArray(inserted2); + } + ob.dep.notify(); + return result; + }); + }); + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + var shouldObserve = true; + function toggleObserving(value) { + shouldObserve = value; + } + var Observer = function Observer2(value) { + this.value = value; + this.dep = new Dep(); + this.vmCount = 0; + def(value, "__ob__", this); + if (Array.isArray(value)) { + if (hasProto) { + protoAugment(value, arrayMethods); + } else { + copyAugment(value, arrayMethods, arrayKeys); + } + this.observeArray(value); + } else { + this.walk(value); + } + }; + Observer.prototype.walk = function walk(obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + defineReactive$$1(obj, keys[i]); + } + }; + Observer.prototype.observeArray = function observeArray(items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + function protoAugment(target2, src) { + target2.__proto__ = src; + } + function copyAugment(target2, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target2, key, src[key]); + } + } + function observe(value, asRootData) { + if (!isObject(value) || value instanceof VNode) { + return; + } + var ob; + if (hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if (shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { + ob = new Observer(value); + } + if (asRootData && ob) { + ob.vmCount++; + } + return ob; + } + function defineReactive$$1(obj, key, val, customSetter, shallow) { + var dep = new Dep(); + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + var getter = property && property.get; + var setter = property && property.set; + if ((!getter || setter) && arguments.length === 2) { + val = obj[key]; + } + var childOb = !shallow && observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + if (Array.isArray(value)) { + dependArray(value); + } + } + } + return value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (newVal === value || newVal !== newVal && value !== value) { + return; + } + if (customSetter) { + customSetter(); + } + if (getter && !setter) { + return; + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = !shallow && observe(newVal); + dep.notify(); + } + }); + } + function set(target2, key, val) { + if (isUndef(target2) || isPrimitive(target2)) { + warn("Cannot set reactive property on undefined, null, or primitive value: " + target2); + } + if (Array.isArray(target2) && isValidArrayIndex(key)) { + target2.length = Math.max(target2.length, key); + target2.splice(key, 1, val); + return val; + } + if (key in target2 && !(key in Object.prototype)) { + target2[key] = val; + return val; + } + var ob = target2.__ob__; + if (target2._isVue || ob && ob.vmCount) { + warn("Avoid adding reactive properties to a Vue instance or its root $data at runtime - declare it upfront in the data option."); + return val; + } + if (!ob) { + target2[key] = val; + return val; + } + defineReactive$$1(ob.value, key, val); + ob.dep.notify(); + return val; + } + function del(target2, key) { + if (isUndef(target2) || isPrimitive(target2)) { + warn("Cannot delete reactive property on undefined, null, or primitive value: " + target2); + } + if (Array.isArray(target2) && isValidArrayIndex(key)) { + target2.splice(key, 1); + return; + } + var ob = target2.__ob__; + if (target2._isVue || ob && ob.vmCount) { + warn("Avoid deleting properties on a Vue instance or its root $data - just set it to null."); + return; + } + if (!hasOwn(target2, key)) { + return; + } + delete target2[key]; + if (!ob) { + return; + } + ob.dep.notify(); + } + function dependArray(value) { + for (var e = void 0, i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + if (Array.isArray(e)) { + dependArray(e); + } + } + } + var strats = config.optionMergeStrategies; + if (true) { + strats.el = strats.propsData = function(parent, child, vm, key) { + if (!vm) { + warn('option "' + key + '" can only be used during instance creation with the `new` keyword.'); + } + return defaultStrat(parent, child); + }; + } + function mergeData(to, from) { + if (!from) { + return to; + } + var key, toVal, fromVal; + var keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if (key === "__ob__") { + continue; + } + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to; + } + function mergeDataOrFn(parentVal, childVal, vm) { + if (!vm) { + if (!childVal) { + return parentVal; + } + if (!parentVal) { + return childVal; + } + return function mergedDataFn() { + return mergeData(typeof childVal === "function" ? childVal.call(this, this) : childVal, typeof parentVal === "function" ? parentVal.call(this, this) : parentVal); + }; + } else { + return function mergedInstanceDataFn() { + var instanceData = typeof childVal === "function" ? childVal.call(vm, vm) : childVal; + var defaultData = typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal; + if (instanceData) { + return mergeData(instanceData, defaultData); + } else { + return defaultData; + } + }; + } + } + strats.data = function(parentVal, childVal, vm) { + if (!vm) { + if (childVal && typeof childVal !== "function") { + warn('The "data" option should be a function that returns a per-instance value in component definitions.', vm); + return parentVal; + } + return mergeDataOrFn(parentVal, childVal); + } + return mergeDataOrFn(parentVal, childVal, vm); + }; + function mergeHook(parentVal, childVal) { + var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal; + return res ? dedupeHooks(res) : res; + } + function dedupeHooks(hooks2) { + var res = []; + for (var i = 0; i < hooks2.length; i++) { + if (res.indexOf(hooks2[i]) === -1) { + res.push(hooks2[i]); + } + } + return res; + } + LIFECYCLE_HOOKS.forEach(function(hook) { + strats[hook] = mergeHook; + }); + function mergeAssets(parentVal, childVal, vm, key) { + var res = Object.create(parentVal || null); + if (childVal) { + assertObjectType(key, childVal, vm); + return extend(res, childVal); + } else { + return res; + } + } + ASSET_TYPES.forEach(function(type) { + strats[type + "s"] = mergeAssets; + }); + strats.watch = function(parentVal, childVal, vm, key) { + if (parentVal === nativeWatch) { + parentVal = void 0; + } + if (childVal === nativeWatch) { + childVal = void 0; + } + if (!childVal) { + return Object.create(parentVal || null); + } + if (true) { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + return childVal; + } + var ret = {}; + extend(ret, parentVal); + for (var key$1 in childVal) { + var parent = ret[key$1]; + var child = childVal[key$1]; + if (parent && !Array.isArray(parent)) { + parent = [parent]; + } + ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; + } + return ret; + }; + strats.props = strats.methods = strats.inject = strats.computed = function(parentVal, childVal, vm, key) { + if (childVal && true) { + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + return childVal; + } + var ret = /* @__PURE__ */ Object.create(null); + extend(ret, parentVal); + if (childVal) { + extend(ret, childVal); + } + return ret; + }; + strats.provide = mergeDataOrFn; + var defaultStrat = function(parentVal, childVal) { + return childVal === void 0 ? parentVal : childVal; + }; + function checkComponents(options) { + for (var key in options.components) { + validateComponentName(key); + } + } + function validateComponentName(name) { + if (!new RegExp("^[a-zA-Z][\\-\\.0-9_" + unicodeRegExp.source + "]*$").test(name)) { + warn('Invalid component name: "' + name + '". Component names should conform to valid custom element name in html5 specification.'); + } + if (isBuiltInTag(name) || config.isReservedTag(name)) { + warn("Do not use built-in or reserved HTML elements as component id: " + name); + } + } + function normalizeProps(options, vm) { + var props2 = options.props; + if (!props2) { + return; + } + var res = {}; + var i, val, name; + if (Array.isArray(props2)) { + i = props2.length; + while (i--) { + val = props2[i]; + if (typeof val === "string") { + name = camelize(val); + res[name] = { type: null }; + } else if (true) { + warn("props must be strings when using array syntax."); + } + } + } else if (isPlainObject(props2)) { + for (var key in props2) { + val = props2[key]; + name = camelize(key); + res[name] = isPlainObject(val) ? val : { type: val }; + } + } else if (true) { + warn('Invalid value for option "props": expected an Array or an Object, but got ' + toRawType(props2) + ".", vm); + } + options.props = res; + } + function normalizeInject(options, vm) { + var inject = options.inject; + if (!inject) { + return; + } + var normalized = options.inject = {}; + if (Array.isArray(inject)) { + for (var i = 0; i < inject.length; i++) { + normalized[inject[i]] = { from: inject[i] }; + } + } else if (isPlainObject(inject)) { + for (var key in inject) { + var val = inject[key]; + normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; + } + } else if (true) { + warn('Invalid value for option "inject": expected an Array or an Object, but got ' + toRawType(inject) + ".", vm); + } + } + function normalizeDirectives(options) { + var dirs = options.directives; + if (dirs) { + for (var key in dirs) { + var def$$1 = dirs[key]; + if (typeof def$$1 === "function") { + dirs[key] = { bind: def$$1, update: def$$1 }; + } + } + } + } + function assertObjectType(name, value, vm) { + if (!isPlainObject(value)) { + warn('Invalid value for option "' + name + '": expected an Object, but got ' + toRawType(value) + ".", vm); + } + } + function mergeOptions(parent, child, vm) { + if (true) { + checkComponents(child); + } + if (typeof child === "function") { + child = child.options; + } + normalizeProps(child, vm); + normalizeInject(child, vm); + normalizeDirectives(child); + if (!child._base) { + if (child.extends) { + parent = mergeOptions(parent, child.extends, vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + parent = mergeOptions(parent, child.mixins[i], vm); + } + } + } + var options = {}; + var key; + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField(key2) { + var strat = strats[key2] || defaultStrat; + options[key2] = strat(parent[key2], child[key2], vm, key2); + } + return options; + } + function resolveAsset(options, type, id, warnMissing) { + if (typeof id !== "string") { + return; + } + var assets = options[type]; + if (hasOwn(assets, id)) { + return assets[id]; + } + var camelizedId = camelize(id); + if (hasOwn(assets, camelizedId)) { + return assets[camelizedId]; + } + var PascalCaseId = capitalize(camelizedId); + if (hasOwn(assets, PascalCaseId)) { + return assets[PascalCaseId]; + } + var res = assets[id] || assets[camelizedId] || assets[PascalCaseId]; + if (warnMissing && !res) { + warn("Failed to resolve " + type.slice(0, -1) + ": " + id, options); + } + return res; + } + function validateProp(key, propOptions, propsData, vm) { + var prop = propOptions[key]; + var absent = !hasOwn(propsData, key); + var value = propsData[key]; + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { + if (absent && !hasOwn(prop, "default")) { + value = false; + } else if (value === "" || value === hyphenate(key)) { + var stringIndex = getTypeIndex(String, prop.type); + if (stringIndex < 0 || booleanIndex < stringIndex) { + value = true; + } + } + } + if (value === void 0) { + value = getPropDefaultValue(vm, prop, key); + var prevShouldObserve = shouldObserve; + toggleObserving(true); + observe(value); + toggleObserving(prevShouldObserve); + } + if (true) { + assertProp(prop, key, value, vm, absent); + } + return value; + } + function getPropDefaultValue(vm, prop, key) { + if (!hasOwn(prop, "default")) { + return void 0; + } + var def2 = prop.default; + if (isObject(def2)) { + warn('Invalid default value for prop "' + key + '": Props with type Object/Array must use a factory function to return the default value.', vm); + } + if (vm && vm.$options.propsData && vm.$options.propsData[key] === void 0 && vm._props[key] !== void 0) { + return vm._props[key]; + } + return typeof def2 === "function" && getType(prop.type) !== "Function" ? def2.call(vm) : def2; + } + function assertProp(prop, name, value, vm, absent) { + if (prop.required && absent) { + warn('Missing required prop: "' + name + '"', vm); + return; + } + if (value == null && !prop.required) { + return; + } + var type = prop.type; + var valid = !type || type === true; + var expectedTypes = []; + if (type) { + if (!Array.isArray(type)) { + type = [type]; + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ""); + valid = assertedType.valid; + } + } + if (!valid) { + warn(getInvalidTypeMessage(name, value, expectedTypes), vm); + return; + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn('Invalid prop: custom validator check failed for prop "' + name + '".', vm); + } + } + } + var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/; + function assertType(value, type) { + var valid; + var expectedType = getType(type); + if (simpleCheckRE.test(expectedType)) { + var t = typeof value; + valid = t === expectedType.toLowerCase(); + if (!valid && t === "object") { + valid = value instanceof type; + } + } else if (expectedType === "Object") { + valid = isPlainObject(value); + } else if (expectedType === "Array") { + valid = Array.isArray(value); + } else { + valid = value instanceof type; + } + return { + valid, + expectedType + }; + } + function getType(fn) { + var match = fn && fn.toString().match(/^\s*function (\w+)/); + return match ? match[1] : ""; + } + function isSameType(a, b) { + return getType(a) === getType(b); + } + function getTypeIndex(type, expectedTypes) { + if (!Array.isArray(expectedTypes)) { + return isSameType(expectedTypes, type) ? 0 : -1; + } + for (var i = 0, len2 = expectedTypes.length; i < len2; i++) { + if (isSameType(expectedTypes[i], type)) { + return i; + } + } + return -1; + } + function getInvalidTypeMessage(name, value, expectedTypes) { + var message = 'Invalid prop: type check failed for prop "' + name + '". Expected ' + expectedTypes.map(capitalize).join(", "); + var expectedType = expectedTypes[0]; + var receivedType = toRawType(value); + var expectedValue = styleValue(value, expectedType); + var receivedValue = styleValue(value, receivedType); + if (expectedTypes.length === 1 && isExplicable(expectedType) && !isBoolean(expectedType, receivedType)) { + message += " with value " + expectedValue; + } + message += ", got " + receivedType + " "; + if (isExplicable(receivedType)) { + message += "with value " + receivedValue + "."; + } + return message; + } + function styleValue(value, type) { + if (type === "String") { + return '"' + value + '"'; + } else if (type === "Number") { + return "" + Number(value); + } else { + return "" + value; + } + } + function isExplicable(value) { + var explicitTypes = ["string", "number", "boolean"]; + return explicitTypes.some(function(elem) { + return value.toLowerCase() === elem; + }); + } + function isBoolean() { + var args = [], len2 = arguments.length; + while (len2--) + args[len2] = arguments[len2]; + return args.some(function(elem) { + return elem.toLowerCase() === "boolean"; + }); + } + function handleError(err, vm, info) { + pushTarget(); + try { + if (vm) { + var cur = vm; + while (cur = cur.$parent) { + var hooks2 = cur.$options.errorCaptured; + if (hooks2) { + for (var i = 0; i < hooks2.length; i++) { + try { + var capture = hooks2[i].call(cur, err, vm, info) === false; + if (capture) { + return; + } + } catch (e) { + globalHandleError(e, cur, "errorCaptured hook"); + } + } + } + } + } + globalHandleError(err, vm, info); + } finally { + popTarget(); + } + } + function invokeWithErrorHandling(handler, context, args, vm, info) { + var res; + try { + res = args ? handler.apply(context, args) : handler.call(context); + if (res && !res._isVue && isPromise(res) && !res._handled) { + res.catch(function(e) { + return handleError(e, vm, info + " (Promise/async)"); + }); + res._handled = true; + } + } catch (e) { + handleError(e, vm, info); + } + return res; + } + function globalHandleError(err, vm, info) { + if (config.errorHandler) { + try { + return config.errorHandler.call(null, err, vm, info); + } catch (e) { + if (e !== err) { + logError(e, null, "config.errorHandler"); + } + } + } + logError(err, vm, info); + } + function logError(err, vm, info) { + if (true) { + warn("Error in " + info + ': "' + err.toString() + '"', vm); + } + if ((inBrowser || inWeex) && typeof console !== "undefined") { + console.error(err); + } else { + throw err; + } + } + var isUsingMicroTask = false; + var callbacks = []; + var pending = false; + function flushCallbacks() { + pending = false; + var copies = callbacks.slice(0); + callbacks.length = 0; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + var timerFunc; + if (typeof Promise !== "undefined" && isNative(Promise)) { + p = Promise.resolve(); + timerFunc = function() { + p.then(flushCallbacks); + if (isIOS) { + setTimeout(noop); + } + }; + isUsingMicroTask = true; + } else if (!isIE && typeof MutationObserver !== "undefined" && (isNative(MutationObserver) || MutationObserver.toString() === "[object MutationObserverConstructor]")) { + counter = 1; + observer = new MutationObserver(flushCallbacks); + textNode = document.createTextNode(String(counter)); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function() { + counter = (counter + 1) % 2; + textNode.data = String(counter); + }; + isUsingMicroTask = true; + } else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) { + timerFunc = function() { + setImmediate(flushCallbacks); + }; + } else { + timerFunc = function() { + setTimeout(flushCallbacks, 0); + }; + } + var p; + var counter; + var observer; + var textNode; + function nextTick(cb, ctx) { + var _resolve; + callbacks.push(function() { + if (cb) { + try { + cb.call(ctx); + } catch (e) { + handleError(e, ctx, "nextTick"); + } + } else if (_resolve) { + _resolve(ctx); + } + }); + if (!pending) { + pending = true; + timerFunc(); + } + if (!cb && typeof Promise !== "undefined") { + return new Promise(function(resolve) { + _resolve = resolve; + }); + } + } + var mark; + var measure; + if (true) { + perf = inBrowser && window.performance; + if (perf && perf.mark && perf.measure && perf.clearMarks && perf.clearMeasures) { + mark = function(tag) { + return perf.mark(tag); + }; + measure = function(name, startTag, endTag2) { + perf.measure(name, startTag, endTag2); + perf.clearMarks(startTag); + perf.clearMarks(endTag2); + }; + } + } + var perf; + var initProxy; + if (true) { + allowedGlobals = makeMap("Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,require"); + warnNonPresent = function(target2, key) { + warn('Property or method "' + key + '" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.', target2); + }; + warnReservedPrefix = function(target2, key) { + warn('Property "' + key + '" must be accessed with "$data.' + key + '" because properties starting with "$" or "_" are not proxied in the Vue instance to prevent conflicts with Vue internals. See: https://vuejs.org/v2/api/#data', target2); + }; + hasProxy = typeof Proxy !== "undefined" && isNative(Proxy); + if (hasProxy) { + isBuiltInModifier = makeMap("stop,prevent,self,ctrl,shift,alt,meta,exact"); + config.keyCodes = new Proxy(config.keyCodes, { + set: function set2(target2, key, value) { + if (isBuiltInModifier(key)) { + warn("Avoid overwriting built-in modifier in config.keyCodes: ." + key); + return false; + } else { + target2[key] = value; + return true; + } + } + }); + } + hasHandler = { + has: function has2(target2, key) { + var has3 = key in target2; + var isAllowed = allowedGlobals(key) || typeof key === "string" && key.charAt(0) === "_" && !(key in target2.$data); + if (!has3 && !isAllowed) { + if (key in target2.$data) { + warnReservedPrefix(target2, key); + } else { + warnNonPresent(target2, key); + } + } + return has3 || !isAllowed; + } + }; + getHandler = { + get: function get3(target2, key) { + if (typeof key === "string" && !(key in target2)) { + if (key in target2.$data) { + warnReservedPrefix(target2, key); + } else { + warnNonPresent(target2, key); + } + } + return target2[key]; + } + }; + initProxy = function initProxy2(vm) { + if (hasProxy) { + var options = vm.$options; + var handlers = options.render && options.render._withStripped ? getHandler : hasHandler; + vm._renderProxy = new Proxy(vm, handlers); + } else { + vm._renderProxy = vm; + } + }; + } + var allowedGlobals; + var warnNonPresent; + var warnReservedPrefix; + var hasProxy; + var isBuiltInModifier; + var hasHandler; + var getHandler; + var seenObjects = new _Set(); + function traverse(val) { + _traverse(val, seenObjects); + seenObjects.clear(); + } + function _traverse(val, seen) { + var i, keys; + var isA = Array.isArray(val); + if (!isA && !isObject(val) || Object.isFrozen(val) || val instanceof VNode) { + return; + } + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return; + } + seen.add(depId); + } + if (isA) { + i = val.length; + while (i--) { + _traverse(val[i], seen); + } + } else { + keys = Object.keys(val); + i = keys.length; + while (i--) { + _traverse(val[keys[i]], seen); + } + } + } + var normalizeEvent = cached(function(name) { + var passive = name.charAt(0) === "&"; + name = passive ? name.slice(1) : name; + var once$$1 = name.charAt(0) === "~"; + name = once$$1 ? name.slice(1) : name; + var capture = name.charAt(0) === "!"; + name = capture ? name.slice(1) : name; + return { + name, + once: once$$1, + capture, + passive + }; + }); + function createFnInvoker(fns, vm) { + function invoker() { + var arguments$1 = arguments; + var fns2 = invoker.fns; + if (Array.isArray(fns2)) { + var cloned = fns2.slice(); + for (var i = 0; i < cloned.length; i++) { + invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler"); + } + } else { + return invokeWithErrorHandling(fns2, null, arguments, vm, "v-on handler"); + } + } + invoker.fns = fns; + return invoker; + } + function updateListeners(on2, oldOn, add2, remove$$12, createOnceHandler2, vm) { + var name, def$$1, cur, old, event; + for (name in on2) { + def$$1 = cur = on2[name]; + old = oldOn[name]; + event = normalizeEvent(name); + if (isUndef(cur)) { + warn('Invalid handler for event "' + event.name + '": got ' + String(cur), vm); + } else if (isUndef(old)) { + if (isUndef(cur.fns)) { + cur = on2[name] = createFnInvoker(cur, vm); + } + if (isTrue(event.once)) { + cur = on2[name] = createOnceHandler2(event.name, cur, event.capture); + } + add2(event.name, cur, event.capture, event.passive, event.params); + } else if (cur !== old) { + old.fns = cur; + on2[name] = old; + } + } + for (name in oldOn) { + if (isUndef(on2[name])) { + event = normalizeEvent(name); + remove$$12(event.name, oldOn[name], event.capture); + } + } + } + function mergeVNodeHook(def2, hookKey, hook) { + if (def2 instanceof VNode) { + def2 = def2.data.hook || (def2.data.hook = {}); + } + var invoker; + var oldHook = def2[hookKey]; + function wrappedHook() { + hook.apply(this, arguments); + remove(invoker.fns, wrappedHook); + } + if (isUndef(oldHook)) { + invoker = createFnInvoker([wrappedHook]); + } else { + if (isDef(oldHook.fns) && isTrue(oldHook.merged)) { + invoker = oldHook; + invoker.fns.push(wrappedHook); + } else { + invoker = createFnInvoker([oldHook, wrappedHook]); + } + } + invoker.merged = true; + def2[hookKey] = invoker; + } + function extractPropsFromVNodeData(data, Ctor, tag) { + var propOptions = Ctor.options.props; + if (isUndef(propOptions)) { + return; + } + var res = {}; + var attrs2 = data.attrs; + var props2 = data.props; + if (isDef(attrs2) || isDef(props2)) { + for (var key in propOptions) { + var altKey = hyphenate(key); + if (true) { + var keyInLowerCase = key.toLowerCase(); + if (key !== keyInLowerCase && attrs2 && hasOwn(attrs2, keyInLowerCase)) { + tip('Prop "' + keyInLowerCase + '" is passed to component ' + formatComponentName(tag || Ctor) + ', but the declared prop name is "' + key + '". Note that HTML attributes are case-insensitive and camelCased props need to use their kebab-case equivalents when using in-DOM templates. You should probably use "' + altKey + '" instead of "' + key + '".'); + } + } + checkProp(res, props2, key, altKey, true) || checkProp(res, attrs2, key, altKey, false); + } + } + return res; + } + function checkProp(res, hash2, key, altKey, preserve) { + if (isDef(hash2)) { + if (hasOwn(hash2, key)) { + res[key] = hash2[key]; + if (!preserve) { + delete hash2[key]; + } + return true; + } else if (hasOwn(hash2, altKey)) { + res[key] = hash2[altKey]; + if (!preserve) { + delete hash2[altKey]; + } + return true; + } + } + return false; + } + function simpleNormalizeChildren(children) { + for (var i = 0; i < children.length; i++) { + if (Array.isArray(children[i])) { + return Array.prototype.concat.apply([], children); + } + } + return children; + } + function normalizeChildren(children) { + return isPrimitive(children) ? [createTextVNode(children)] : Array.isArray(children) ? normalizeArrayChildren(children) : void 0; + } + function isTextNode(node) { + return isDef(node) && isDef(node.text) && isFalse(node.isComment); + } + function normalizeArrayChildren(children, nestedIndex) { + var res = []; + var i, c, lastIndex, last; + for (i = 0; i < children.length; i++) { + c = children[i]; + if (isUndef(c) || typeof c === "boolean") { + continue; + } + lastIndex = res.length - 1; + last = res[lastIndex]; + if (Array.isArray(c)) { + if (c.length > 0) { + c = normalizeArrayChildren(c, (nestedIndex || "") + "_" + i); + if (isTextNode(c[0]) && isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c[0].text); + c.shift(); + } + res.push.apply(res, c); + } + } else if (isPrimitive(c)) { + if (isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c); + } else if (c !== "") { + res.push(createTextVNode(c)); + } + } else { + if (isTextNode(c) && isTextNode(last)) { + res[lastIndex] = createTextVNode(last.text + c.text); + } else { + if (isTrue(children._isVList) && isDef(c.tag) && isUndef(c.key) && isDef(nestedIndex)) { + c.key = "__vlist" + nestedIndex + "_" + i + "__"; + } + res.push(c); + } + } + } + return res; + } + function initProvide(vm) { + var provide = vm.$options.provide; + if (provide) { + vm._provided = typeof provide === "function" ? provide.call(vm) : provide; + } + } + function initInjections(vm) { + var result = resolveInject(vm.$options.inject, vm); + if (result) { + toggleObserving(false); + Object.keys(result).forEach(function(key) { + if (true) { + defineReactive$$1(vm, key, result[key], function() { + warn('Avoid mutating an injected value directly since the changes will be overwritten whenever the provided component re-renders. injection being mutated: "' + key + '"', vm); + }); + } else { + defineReactive$$1(vm, key, result[key]); + } + }); + toggleObserving(true); + } + } + function resolveInject(inject, vm) { + if (inject) { + var result = /* @__PURE__ */ Object.create(null); + var keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key === "__ob__") { + continue; + } + var provideKey = inject[key].from; + var source = vm; + while (source) { + if (source._provided && hasOwn(source._provided, provideKey)) { + result[key] = source._provided[provideKey]; + break; + } + source = source.$parent; + } + if (!source) { + if ("default" in inject[key]) { + var provideDefault = inject[key].default; + result[key] = typeof provideDefault === "function" ? provideDefault.call(vm) : provideDefault; + } else if (true) { + warn('Injection "' + key + '" not found', vm); + } + } + } + return result; + } + } + function resolveSlots(children, context) { + if (!children || !children.length) { + return {}; + } + var slots = {}; + for (var i = 0, l = children.length; i < l; i++) { + var child = children[i]; + var data = child.data; + if (data && data.attrs && data.attrs.slot) { + delete data.attrs.slot; + } + if ((child.context === context || child.fnContext === context) && data && data.slot != null) { + var name = data.slot; + var slot = slots[name] || (slots[name] = []); + if (child.tag === "template") { + slot.push.apply(slot, child.children || []); + } else { + slot.push(child); + } + } else { + (slots.default || (slots.default = [])).push(child); + } + } + for (var name$1 in slots) { + if (slots[name$1].every(isWhitespace)) { + delete slots[name$1]; + } + } + return slots; + } + function isWhitespace(node) { + return node.isComment && !node.asyncFactory || node.text === " "; + } + function normalizeScopedSlots(slots, normalSlots, prevSlots) { + var res; + var hasNormalSlots = Object.keys(normalSlots).length > 0; + var isStable = slots ? !!slots.$stable : !hasNormalSlots; + var key = slots && slots.$key; + if (!slots) { + res = {}; + } else if (slots._normalized) { + return slots._normalized; + } else if (isStable && prevSlots && prevSlots !== emptyObject && key === prevSlots.$key && !hasNormalSlots && !prevSlots.$hasNormal) { + return prevSlots; + } else { + res = {}; + for (var key$1 in slots) { + if (slots[key$1] && key$1[0] !== "$") { + res[key$1] = normalizeScopedSlot(normalSlots, key$1, slots[key$1]); + } + } + } + for (var key$2 in normalSlots) { + if (!(key$2 in res)) { + res[key$2] = proxyNormalSlot(normalSlots, key$2); + } + } + if (slots && Object.isExtensible(slots)) { + slots._normalized = res; + } + def(res, "$stable", isStable); + def(res, "$key", key); + def(res, "$hasNormal", hasNormalSlots); + return res; + } + function normalizeScopedSlot(normalSlots, key, fn) { + var normalized = function() { + var res = arguments.length ? fn.apply(null, arguments) : fn({}); + res = res && typeof res === "object" && !Array.isArray(res) ? [res] : normalizeChildren(res); + return res && (res.length === 0 || res.length === 1 && res[0].isComment) ? void 0 : res; + }; + if (fn.proxy) { + Object.defineProperty(normalSlots, key, { + get: normalized, + enumerable: true, + configurable: true + }); + } + return normalized; + } + function proxyNormalSlot(slots, key) { + return function() { + return slots[key]; + }; + } + function renderList(val, render4) { + var ret, i, l, keys, key; + if (Array.isArray(val) || typeof val === "string") { + ret = new Array(val.length); + for (i = 0, l = val.length; i < l; i++) { + ret[i] = render4(val[i], i); + } + } else if (typeof val === "number") { + ret = new Array(val); + for (i = 0; i < val; i++) { + ret[i] = render4(i + 1, i); + } + } else if (isObject(val)) { + if (hasSymbol && val[Symbol.iterator]) { + ret = []; + var iterator = val[Symbol.iterator](); + var result = iterator.next(); + while (!result.done) { + ret.push(render4(result.value, ret.length)); + result = iterator.next(); + } + } else { + keys = Object.keys(val); + ret = new Array(keys.length); + for (i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + ret[i] = render4(val[key], key, i); + } + } + } + if (!isDef(ret)) { + ret = []; + } + ret._isVList = true; + return ret; + } + function renderSlot(name, fallback, props2, bindObject) { + var scopedSlotFn = this.$scopedSlots[name]; + var nodes; + if (scopedSlotFn) { + props2 = props2 || {}; + if (bindObject) { + if (!isObject(bindObject)) { + warn("slot v-bind without argument expects an Object", this); + } + props2 = extend(extend({}, bindObject), props2); + } + nodes = scopedSlotFn(props2) || fallback; + } else { + nodes = this.$slots[name] || fallback; + } + var target2 = props2 && props2.slot; + if (target2) { + return this.$createElement("template", { slot: target2 }, nodes); + } else { + return nodes; + } + } + function resolveFilter(id) { + return resolveAsset(this.$options, "filters", id, true) || identity; + } + function isKeyNotMatch(expect, actual) { + if (Array.isArray(expect)) { + return expect.indexOf(actual) === -1; + } else { + return expect !== actual; + } + } + function checkKeyCodes(eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName) { + var mappedKeyCode = config.keyCodes[key] || builtInKeyCode; + if (builtInKeyName && eventKeyName && !config.keyCodes[key]) { + return isKeyNotMatch(builtInKeyName, eventKeyName); + } else if (mappedKeyCode) { + return isKeyNotMatch(mappedKeyCode, eventKeyCode); + } else if (eventKeyName) { + return hyphenate(eventKeyName) !== key; + } + } + function bindObjectProps(data, tag, value, asProp, isSync) { + if (value) { + if (!isObject(value)) { + warn("v-bind without argument expects an Object or Array value", this); + } else { + if (Array.isArray(value)) { + value = toObject(value); + } + var hash2; + var loop = function(key2) { + if (key2 === "class" || key2 === "style" || isReservedAttribute(key2)) { + hash2 = data; + } else { + var type = data.attrs && data.attrs.type; + hash2 = asProp || config.mustUseProp(tag, type, key2) ? data.domProps || (data.domProps = {}) : data.attrs || (data.attrs = {}); + } + var camelizedKey = camelize(key2); + var hyphenatedKey = hyphenate(key2); + if (!(camelizedKey in hash2) && !(hyphenatedKey in hash2)) { + hash2[key2] = value[key2]; + if (isSync) { + var on2 = data.on || (data.on = {}); + on2["update:" + key2] = function($event) { + value[key2] = $event; + }; + } + } + }; + for (var key in value) + loop(key); + } + } + return data; + } + function renderStatic(index2, isInFor) { + var cached2 = this._staticTrees || (this._staticTrees = []); + var tree = cached2[index2]; + if (tree && !isInFor) { + return tree; + } + tree = cached2[index2] = this.$options.staticRenderFns[index2].call(this._renderProxy, null, this); + markStatic(tree, "__static__" + index2, false); + return tree; + } + function markOnce(tree, index2, key) { + markStatic(tree, "__once__" + index2 + (key ? "_" + key : ""), true); + return tree; + } + function markStatic(tree, key, isOnce) { + if (Array.isArray(tree)) { + for (var i = 0; i < tree.length; i++) { + if (tree[i] && typeof tree[i] !== "string") { + markStaticNode(tree[i], key + "_" + i, isOnce); + } + } + } else { + markStaticNode(tree, key, isOnce); + } + } + function markStaticNode(node, key, isOnce) { + node.isStatic = true; + node.key = key; + node.isOnce = isOnce; + } + function bindObjectListeners(data, value) { + if (value) { + if (!isPlainObject(value)) { + warn("v-on without argument expects an Object value", this); + } else { + var on2 = data.on = data.on ? extend({}, data.on) : {}; + for (var key in value) { + var existing = on2[key]; + var ours = value[key]; + on2[key] = existing ? [].concat(existing, ours) : ours; + } + } + } + return data; + } + function resolveScopedSlots(fns, res, hasDynamicKeys, contentHashKey) { + res = res || { $stable: !hasDynamicKeys }; + for (var i = 0; i < fns.length; i++) { + var slot = fns[i]; + if (Array.isArray(slot)) { + resolveScopedSlots(slot, res, hasDynamicKeys); + } else if (slot) { + if (slot.proxy) { + slot.fn.proxy = true; + } + res[slot.key] = slot.fn; + } + } + if (contentHashKey) { + res.$key = contentHashKey; + } + return res; + } + function bindDynamicKeys(baseObj, values) { + for (var i = 0; i < values.length; i += 2) { + var key = values[i]; + if (typeof key === "string" && key) { + baseObj[values[i]] = values[i + 1]; + } else if (key !== "" && key !== null) { + warn("Invalid value for dynamic directive argument (expected string or null): " + key, this); + } + } + return baseObj; + } + function prependModifier(value, symbol) { + return typeof value === "string" ? symbol + value : value; + } + function installRenderHelpers(target2) { + target2._o = markOnce; + target2._n = toNumber; + target2._s = toString; + target2._l = renderList; + target2._t = renderSlot; + target2._q = looseEqual; + target2._i = looseIndexOf; + target2._m = renderStatic; + target2._f = resolveFilter; + target2._k = checkKeyCodes; + target2._b = bindObjectProps; + target2._v = createTextVNode; + target2._e = createEmptyVNode; + target2._u = resolveScopedSlots; + target2._g = bindObjectListeners; + target2._d = bindDynamicKeys; + target2._p = prependModifier; + } + function FunctionalRenderContext(data, props2, children, parent, Ctor) { + var this$1 = this; + var options = Ctor.options; + var contextVm; + if (hasOwn(parent, "_uid")) { + contextVm = Object.create(parent); + contextVm._original = parent; + } else { + contextVm = parent; + parent = parent._original; + } + var isCompiled = isTrue(options._compiled); + var needNormalization = !isCompiled; + this.data = data; + this.props = props2; + this.children = children; + this.parent = parent; + this.listeners = data.on || emptyObject; + this.injections = resolveInject(options.inject, parent); + this.slots = function() { + if (!this$1.$slots) { + normalizeScopedSlots(data.scopedSlots, this$1.$slots = resolveSlots(children, parent)); + } + return this$1.$slots; + }; + Object.defineProperty(this, "scopedSlots", { + enumerable: true, + get: function get3() { + return normalizeScopedSlots(data.scopedSlots, this.slots()); + } + }); + if (isCompiled) { + this.$options = options; + this.$slots = this.slots(); + this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots); + } + if (options._scopeId) { + this._c = function(a, b, c, d) { + var vnode = createElement(contextVm, a, b, c, d, needNormalization); + if (vnode && !Array.isArray(vnode)) { + vnode.fnScopeId = options._scopeId; + vnode.fnContext = parent; + } + return vnode; + }; + } else { + this._c = function(a, b, c, d) { + return createElement(contextVm, a, b, c, d, needNormalization); + }; + } + } + installRenderHelpers(FunctionalRenderContext.prototype); + function createFunctionalComponent(Ctor, propsData, data, contextVm, children) { + var options = Ctor.options; + var props2 = {}; + var propOptions = options.props; + if (isDef(propOptions)) { + for (var key in propOptions) { + props2[key] = validateProp(key, propOptions, propsData || emptyObject); + } + } else { + if (isDef(data.attrs)) { + mergeProps(props2, data.attrs); + } + if (isDef(data.props)) { + mergeProps(props2, data.props); + } + } + var renderContext = new FunctionalRenderContext(data, props2, children, contextVm, Ctor); + var vnode = options.render.call(null, renderContext._c, renderContext); + if (vnode instanceof VNode) { + return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext); + } else if (Array.isArray(vnode)) { + var vnodes = normalizeChildren(vnode) || []; + var res = new Array(vnodes.length); + for (var i = 0; i < vnodes.length; i++) { + res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext); + } + return res; + } + } + function cloneAndMarkFunctionalResult(vnode, data, contextVm, options, renderContext) { + var clone = cloneVNode(vnode); + clone.fnContext = contextVm; + clone.fnOptions = options; + if (true) { + (clone.devtoolsMeta = clone.devtoolsMeta || {}).renderContext = renderContext; + } + if (data.slot) { + (clone.data || (clone.data = {})).slot = data.slot; + } + return clone; + } + function mergeProps(to, from) { + for (var key in from) { + to[camelize(key)] = from[key]; + } + } + var componentVNodeHooks = { + init: function init(vnode, hydrating) { + if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) { + var mountedNode = vnode; + componentVNodeHooks.prepatch(mountedNode, mountedNode); + } else { + var child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance); + child.$mount(hydrating ? vnode.elm : void 0, hydrating); + } + }, + prepatch: function prepatch(oldVnode, vnode) { + var options = vnode.componentOptions; + var child = vnode.componentInstance = oldVnode.componentInstance; + updateChildComponent(child, options.propsData, options.listeners, vnode, options.children); + }, + insert: function insert(vnode) { + var context = vnode.context; + var componentInstance = vnode.componentInstance; + if (!componentInstance._isMounted) { + componentInstance._isMounted = true; + callHook(componentInstance, "mounted"); + } + if (vnode.data.keepAlive) { + if (context._isMounted) { + queueActivatedComponent(componentInstance); + } else { + activateChildComponent(componentInstance, true); + } + } + }, + destroy: function destroy(vnode) { + var componentInstance = vnode.componentInstance; + if (!componentInstance._isDestroyed) { + if (!vnode.data.keepAlive) { + componentInstance.$destroy(); + } else { + deactivateChildComponent(componentInstance, true); + } + } + } + }; + var hooksToMerge = Object.keys(componentVNodeHooks); + function createComponent(Ctor, data, context, children, tag) { + if (isUndef(Ctor)) { + return; + } + var baseCtor = context.$options._base; + if (isObject(Ctor)) { + Ctor = baseCtor.extend(Ctor); + } + if (typeof Ctor !== "function") { + if (true) { + warn("Invalid Component definition: " + String(Ctor), context); + } + return; + } + var asyncFactory; + if (isUndef(Ctor.cid)) { + asyncFactory = Ctor; + Ctor = resolveAsyncComponent(asyncFactory, baseCtor); + if (Ctor === void 0) { + return createAsyncPlaceholder(asyncFactory, data, context, children, tag); + } + } + data = data || {}; + resolveConstructorOptions(Ctor); + if (isDef(data.model)) { + transformModel(Ctor.options, data); + } + var propsData = extractPropsFromVNodeData(data, Ctor, tag); + if (isTrue(Ctor.options.functional)) { + return createFunctionalComponent(Ctor, propsData, data, context, children); + } + var listeners = data.on; + data.on = data.nativeOn; + if (isTrue(Ctor.options.abstract)) { + var slot = data.slot; + data = {}; + if (slot) { + data.slot = slot; + } + } + installComponentHooks(data); + var name = Ctor.options.name || tag; + var vnode = new VNode("vue-component-" + Ctor.cid + (name ? "-" + name : ""), data, void 0, void 0, void 0, context, { Ctor, propsData, listeners, tag, children }, asyncFactory); + return vnode; + } + function createComponentInstanceForVnode(vnode, parent) { + var options = { + _isComponent: true, + _parentVnode: vnode, + parent + }; + var inlineTemplate = vnode.data.inlineTemplate; + if (isDef(inlineTemplate)) { + options.render = inlineTemplate.render; + options.staticRenderFns = inlineTemplate.staticRenderFns; + } + return new vnode.componentOptions.Ctor(options); + } + function installComponentHooks(data) { + var hooks2 = data.hook || (data.hook = {}); + for (var i = 0; i < hooksToMerge.length; i++) { + var key = hooksToMerge[i]; + var existing = hooks2[key]; + var toMerge = componentVNodeHooks[key]; + if (existing !== toMerge && !(existing && existing._merged)) { + hooks2[key] = existing ? mergeHook$1(toMerge, existing) : toMerge; + } + } + } + function mergeHook$1(f1, f2) { + var merged = function(a, b) { + f1(a, b); + f2(a, b); + }; + merged._merged = true; + return merged; + } + function transformModel(options, data) { + var prop = options.model && options.model.prop || "value"; + var event = options.model && options.model.event || "input"; + (data.attrs || (data.attrs = {}))[prop] = data.model.value; + var on2 = data.on || (data.on = {}); + var existing = on2[event]; + var callback = data.model.callback; + if (isDef(existing)) { + if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback) { + on2[event] = [callback].concat(existing); + } + } else { + on2[event] = callback; + } + } + var SIMPLE_NORMALIZE = 1; + var ALWAYS_NORMALIZE = 2; + function createElement(context, tag, data, children, normalizationType, alwaysNormalize) { + if (Array.isArray(data) || isPrimitive(data)) { + normalizationType = children; + children = data; + data = void 0; + } + if (isTrue(alwaysNormalize)) { + normalizationType = ALWAYS_NORMALIZE; + } + return _createElement(context, tag, data, children, normalizationType); + } + function _createElement(context, tag, data, children, normalizationType) { + if (isDef(data) && isDef(data.__ob__)) { + warn("Avoid using observed data object as vnode data: " + JSON.stringify(data) + "\nAlways create fresh vnode data objects in each render!", context); + return createEmptyVNode(); + } + if (isDef(data) && isDef(data.is)) { + tag = data.is; + } + if (!tag) { + return createEmptyVNode(); + } + if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)) { + { + warn("Avoid using non-primitive value as key, use string/number value instead.", context); + } + } + if (Array.isArray(children) && typeof children[0] === "function") { + data = data || {}; + data.scopedSlots = { default: children[0] }; + children.length = 0; + } + if (normalizationType === ALWAYS_NORMALIZE) { + children = normalizeChildren(children); + } else if (normalizationType === SIMPLE_NORMALIZE) { + children = simpleNormalizeChildren(children); + } + var vnode, ns; + if (typeof tag === "string") { + var Ctor; + ns = context.$vnode && context.$vnode.ns || config.getTagNamespace(tag); + if (config.isReservedTag(tag)) { + if (isDef(data) && isDef(data.nativeOn)) { + warn("The .native modifier for v-on is only valid on components but it was used on <" + tag + ">.", context); + } + vnode = new VNode(config.parsePlatformTagName(tag), data, children, void 0, void 0, context); + } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, "components", tag))) { + vnode = createComponent(Ctor, data, context, children, tag); + } else { + vnode = new VNode(tag, data, children, void 0, void 0, context); + } + } else { + vnode = createComponent(tag, data, context, children); + } + if (Array.isArray(vnode)) { + return vnode; + } else if (isDef(vnode)) { + if (isDef(ns)) { + applyNS(vnode, ns); + } + if (isDef(data)) { + registerDeepBindings(data); + } + return vnode; + } else { + return createEmptyVNode(); + } + } + function applyNS(vnode, ns, force) { + vnode.ns = ns; + if (vnode.tag === "foreignObject") { + ns = void 0; + force = true; + } + if (isDef(vnode.children)) { + for (var i = 0, l = vnode.children.length; i < l; i++) { + var child = vnode.children[i]; + if (isDef(child.tag) && (isUndef(child.ns) || isTrue(force) && child.tag !== "svg")) { + applyNS(child, ns, force); + } + } + } + } + function registerDeepBindings(data) { + if (isObject(data.style)) { + traverse(data.style); + } + if (isObject(data.class)) { + traverse(data.class); + } + } + function initRender(vm) { + vm._vnode = null; + vm._staticTrees = null; + var options = vm.$options; + var parentVnode = vm.$vnode = options._parentVnode; + var renderContext = parentVnode && parentVnode.context; + vm.$slots = resolveSlots(options._renderChildren, renderContext); + vm.$scopedSlots = emptyObject; + vm._c = function(a, b, c, d) { + return createElement(vm, a, b, c, d, false); + }; + vm.$createElement = function(a, b, c, d) { + return createElement(vm, a, b, c, d, true); + }; + var parentData = parentVnode && parentVnode.data; + if (true) { + defineReactive$$1(vm, "$attrs", parentData && parentData.attrs || emptyObject, function() { + !isUpdatingChildComponent && warn("$attrs is readonly.", vm); + }, true); + defineReactive$$1(vm, "$listeners", options._parentListeners || emptyObject, function() { + !isUpdatingChildComponent && warn("$listeners is readonly.", vm); + }, true); + } else { + defineReactive$$1(vm, "$attrs", parentData && parentData.attrs || emptyObject, null, true); + defineReactive$$1(vm, "$listeners", options._parentListeners || emptyObject, null, true); + } + } + var currentRenderingInstance = null; + function renderMixin(Vue2) { + installRenderHelpers(Vue2.prototype); + Vue2.prototype.$nextTick = function(fn) { + return nextTick(fn, this); + }; + Vue2.prototype._render = function() { + var vm = this; + var ref2 = vm.$options; + var render4 = ref2.render; + var _parentVnode = ref2._parentVnode; + if (_parentVnode) { + vm.$scopedSlots = normalizeScopedSlots(_parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots); + } + vm.$vnode = _parentVnode; + var vnode; + try { + currentRenderingInstance = vm; + vnode = render4.call(vm._renderProxy, vm.$createElement); + } catch (e) { + handleError(e, vm, "render"); + if (vm.$options.renderError) { + try { + vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e); + } catch (e2) { + handleError(e2, vm, "renderError"); + vnode = vm._vnode; + } + } else { + vnode = vm._vnode; + } + } finally { + currentRenderingInstance = null; + } + if (Array.isArray(vnode) && vnode.length === 1) { + vnode = vnode[0]; + } + if (!(vnode instanceof VNode)) { + if (Array.isArray(vnode)) { + warn("Multiple root nodes returned from render function. Render function should return a single root node.", vm); + } + vnode = createEmptyVNode(); + } + vnode.parent = _parentVnode; + return vnode; + }; + } + function ensureCtor(comp, base) { + if (comp.__esModule || hasSymbol && comp[Symbol.toStringTag] === "Module") { + comp = comp.default; + } + return isObject(comp) ? base.extend(comp) : comp; + } + function createAsyncPlaceholder(factory, data, context, children, tag) { + var node = createEmptyVNode(); + node.asyncFactory = factory; + node.asyncMeta = { data, context, children, tag }; + return node; + } + function resolveAsyncComponent(factory, baseCtor) { + if (isTrue(factory.error) && isDef(factory.errorComp)) { + return factory.errorComp; + } + if (isDef(factory.resolved)) { + return factory.resolved; + } + var owner = currentRenderingInstance; + if (owner && isDef(factory.owners) && factory.owners.indexOf(owner) === -1) { + factory.owners.push(owner); + } + if (isTrue(factory.loading) && isDef(factory.loadingComp)) { + return factory.loadingComp; + } + if (owner && !isDef(factory.owners)) { + var owners = factory.owners = [owner]; + var sync = true; + var timerLoading = null; + var timerTimeout = null; + owner.$on("hook:destroyed", function() { + return remove(owners, owner); + }); + var forceRender = function(renderCompleted) { + for (var i = 0, l = owners.length; i < l; i++) { + owners[i].$forceUpdate(); + } + if (renderCompleted) { + owners.length = 0; + if (timerLoading !== null) { + clearTimeout(timerLoading); + timerLoading = null; + } + if (timerTimeout !== null) { + clearTimeout(timerTimeout); + timerTimeout = null; + } + } + }; + var resolve = once(function(res2) { + factory.resolved = ensureCtor(res2, baseCtor); + if (!sync) { + forceRender(true); + } else { + owners.length = 0; + } + }); + var reject = once(function(reason) { + warn("Failed to resolve async component: " + String(factory) + (reason ? "\nReason: " + reason : "")); + if (isDef(factory.errorComp)) { + factory.error = true; + forceRender(true); + } + }); + var res = factory(resolve, reject); + if (isObject(res)) { + if (isPromise(res)) { + if (isUndef(factory.resolved)) { + res.then(resolve, reject); + } + } else if (isPromise(res.component)) { + res.component.then(resolve, reject); + if (isDef(res.error)) { + factory.errorComp = ensureCtor(res.error, baseCtor); + } + if (isDef(res.loading)) { + factory.loadingComp = ensureCtor(res.loading, baseCtor); + if (res.delay === 0) { + factory.loading = true; + } else { + timerLoading = setTimeout(function() { + timerLoading = null; + if (isUndef(factory.resolved) && isUndef(factory.error)) { + factory.loading = true; + forceRender(false); + } + }, res.delay || 200); + } + } + if (isDef(res.timeout)) { + timerTimeout = setTimeout(function() { + timerTimeout = null; + if (isUndef(factory.resolved)) { + reject(true ? "timeout (" + res.timeout + "ms)" : null); + } + }, res.timeout); + } + } + } + sync = false; + return factory.loading ? factory.loadingComp : factory.resolved; + } + } + function isAsyncPlaceholder(node) { + return node.isComment && node.asyncFactory; + } + function getFirstComponentChild(children) { + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) { + var c = children[i]; + if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) { + return c; + } + } + } + } + function initEvents(vm) { + vm._events = /* @__PURE__ */ Object.create(null); + vm._hasHookEvent = false; + var listeners = vm.$options._parentListeners; + if (listeners) { + updateComponentListeners(vm, listeners); + } + } + var target; + function add(event, fn) { + target.$on(event, fn); + } + function remove$1(event, fn) { + target.$off(event, fn); + } + function createOnceHandler(event, fn) { + var _target = target; + return function onceHandler() { + var res = fn.apply(null, arguments); + if (res !== null) { + _target.$off(event, onceHandler); + } + }; + } + function updateComponentListeners(vm, listeners, oldListeners) { + target = vm; + updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm); + target = void 0; + } + function eventsMixin(Vue2) { + var hookRE = /^hook:/; + Vue2.prototype.$on = function(event, fn) { + var vm = this; + if (Array.isArray(event)) { + for (var i = 0, l = event.length; i < l; i++) { + vm.$on(event[i], fn); + } + } else { + (vm._events[event] || (vm._events[event] = [])).push(fn); + if (hookRE.test(event)) { + vm._hasHookEvent = true; + } + } + return vm; + }; + Vue2.prototype.$once = function(event, fn) { + var vm = this; + function on2() { + vm.$off(event, on2); + fn.apply(vm, arguments); + } + on2.fn = fn; + vm.$on(event, on2); + return vm; + }; + Vue2.prototype.$off = function(event, fn) { + var vm = this; + if (!arguments.length) { + vm._events = /* @__PURE__ */ Object.create(null); + return vm; + } + if (Array.isArray(event)) { + for (var i$1 = 0, l = event.length; i$1 < l; i$1++) { + vm.$off(event[i$1], fn); + } + return vm; + } + var cbs = vm._events[event]; + if (!cbs) { + return vm; + } + if (!fn) { + vm._events[event] = null; + return vm; + } + var cb; + var i = cbs.length; + while (i--) { + cb = cbs[i]; + if (cb === fn || cb.fn === fn) { + cbs.splice(i, 1); + break; + } + } + return vm; + }; + Vue2.prototype.$emit = function(event) { + var vm = this; + if (true) { + var lowerCaseEvent = event.toLowerCase(); + if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) { + tip('Event "' + lowerCaseEvent + '" is emitted in component ' + formatComponentName(vm) + ' but the handler is registered for "' + event + '". Note that HTML attributes are case-insensitive and you cannot use v-on to listen to camelCase events when using in-DOM templates. You should probably use "' + hyphenate(event) + '" instead of "' + event + '".'); + } + } + var cbs = vm._events[event]; + if (cbs) { + cbs = cbs.length > 1 ? toArray(cbs) : cbs; + var args = toArray(arguments, 1); + var info = 'event handler for "' + event + '"'; + for (var i = 0, l = cbs.length; i < l; i++) { + invokeWithErrorHandling(cbs[i], vm, args, vm, info); + } + } + return vm; + }; + } + var activeInstance = null; + var isUpdatingChildComponent = false; + function setActiveInstance(vm) { + var prevActiveInstance = activeInstance; + activeInstance = vm; + return function() { + activeInstance = prevActiveInstance; + }; + } + function initLifecycle(vm) { + var options = vm.$options; + var parent = options.parent; + if (parent && !options.abstract) { + while (parent.$options.abstract && parent.$parent) { + parent = parent.$parent; + } + parent.$children.push(vm); + } + vm.$parent = parent; + vm.$root = parent ? parent.$root : vm; + vm.$children = []; + vm.$refs = {}; + vm._watcher = null; + vm._inactive = null; + vm._directInactive = false; + vm._isMounted = false; + vm._isDestroyed = false; + vm._isBeingDestroyed = false; + } + function lifecycleMixin(Vue2) { + Vue2.prototype._update = function(vnode, hydrating) { + var vm = this; + var prevEl = vm.$el; + var prevVnode = vm._vnode; + var restoreActiveInstance = setActiveInstance(vm); + vm._vnode = vnode; + if (!prevVnode) { + vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false); + } else { + vm.$el = vm.__patch__(prevVnode, vnode); + } + restoreActiveInstance(); + if (prevEl) { + prevEl.__vue__ = null; + } + if (vm.$el) { + vm.$el.__vue__ = vm; + } + if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { + vm.$parent.$el = vm.$el; + } + }; + Vue2.prototype.$forceUpdate = function() { + var vm = this; + if (vm._watcher) { + vm._watcher.update(); + } + }; + Vue2.prototype.$destroy = function() { + var vm = this; + if (vm._isBeingDestroyed) { + return; + } + callHook(vm, "beforeDestroy"); + vm._isBeingDestroyed = true; + var parent = vm.$parent; + if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { + remove(parent.$children, vm); + } + if (vm._watcher) { + vm._watcher.teardown(); + } + var i = vm._watchers.length; + while (i--) { + vm._watchers[i].teardown(); + } + if (vm._data.__ob__) { + vm._data.__ob__.vmCount--; + } + vm._isDestroyed = true; + vm.__patch__(vm._vnode, null); + callHook(vm, "destroyed"); + vm.$off(); + if (vm.$el) { + vm.$el.__vue__ = null; + } + if (vm.$vnode) { + vm.$vnode.parent = null; + } + }; + } + function mountComponent(vm, el, hydrating) { + vm.$el = el; + if (!vm.$options.render) { + vm.$options.render = createEmptyVNode; + if (true) { + if (vm.$options.template && vm.$options.template.charAt(0) !== "#" || vm.$options.el || el) { + warn("You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.", vm); + } else { + warn("Failed to mount component: template or render function not defined.", vm); + } + } + } + callHook(vm, "beforeMount"); + var updateComponent; + if (config.performance && mark) { + updateComponent = function() { + var name = vm._name; + var id = vm._uid; + var startTag = "vue-perf-start:" + id; + var endTag2 = "vue-perf-end:" + id; + mark(startTag); + var vnode = vm._render(); + mark(endTag2); + measure("vue " + name + " render", startTag, endTag2); + mark(startTag); + vm._update(vnode, hydrating); + mark(endTag2); + measure("vue " + name + " patch", startTag, endTag2); + }; + } else { + updateComponent = function() { + vm._update(vm._render(), hydrating); + }; + } + new Watcher(vm, updateComponent, noop, { + before: function before() { + if (vm._isMounted && !vm._isDestroyed) { + callHook(vm, "beforeUpdate"); + } + } + }, true); + hydrating = false; + if (vm.$vnode == null) { + vm._isMounted = true; + callHook(vm, "mounted"); + } + return vm; + } + function updateChildComponent(vm, propsData, listeners, parentVnode, renderChildren) { + if (true) { + isUpdatingChildComponent = true; + } + var newScopedSlots = parentVnode.data.scopedSlots; + var oldScopedSlots = vm.$scopedSlots; + var hasDynamicScopedSlot = !!(newScopedSlots && !newScopedSlots.$stable || oldScopedSlots !== emptyObject && !oldScopedSlots.$stable || newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key); + var needsForceUpdate = !!(renderChildren || vm.$options._renderChildren || hasDynamicScopedSlot); + vm.$options._parentVnode = parentVnode; + vm.$vnode = parentVnode; + if (vm._vnode) { + vm._vnode.parent = parentVnode; + } + vm.$options._renderChildren = renderChildren; + vm.$attrs = parentVnode.data.attrs || emptyObject; + vm.$listeners = listeners || emptyObject; + if (propsData && vm.$options.props) { + toggleObserving(false); + var props2 = vm._props; + var propKeys = vm.$options._propKeys || []; + for (var i = 0; i < propKeys.length; i++) { + var key = propKeys[i]; + var propOptions = vm.$options.props; + props2[key] = validateProp(key, propOptions, propsData, vm); + } + toggleObserving(true); + vm.$options.propsData = propsData; + } + listeners = listeners || emptyObject; + var oldListeners = vm.$options._parentListeners; + vm.$options._parentListeners = listeners; + updateComponentListeners(vm, listeners, oldListeners); + if (needsForceUpdate) { + vm.$slots = resolveSlots(renderChildren, parentVnode.context); + vm.$forceUpdate(); + } + if (true) { + isUpdatingChildComponent = false; + } + } + function isInInactiveTree(vm) { + while (vm && (vm = vm.$parent)) { + if (vm._inactive) { + return true; + } + } + return false; + } + function activateChildComponent(vm, direct) { + if (direct) { + vm._directInactive = false; + if (isInInactiveTree(vm)) { + return; + } + } else if (vm._directInactive) { + return; + } + if (vm._inactive || vm._inactive === null) { + vm._inactive = false; + for (var i = 0; i < vm.$children.length; i++) { + activateChildComponent(vm.$children[i]); + } + callHook(vm, "activated"); + } + } + function deactivateChildComponent(vm, direct) { + if (direct) { + vm._directInactive = true; + if (isInInactiveTree(vm)) { + return; + } + } + if (!vm._inactive) { + vm._inactive = true; + for (var i = 0; i < vm.$children.length; i++) { + deactivateChildComponent(vm.$children[i]); + } + callHook(vm, "deactivated"); + } + } + function callHook(vm, hook) { + pushTarget(); + var handlers = vm.$options[hook]; + var info = hook + " hook"; + if (handlers) { + for (var i = 0, j = handlers.length; i < j; i++) { + invokeWithErrorHandling(handlers[i], vm, null, vm, info); + } + } + if (vm._hasHookEvent) { + vm.$emit("hook:" + hook); + } + popTarget(); + } + var MAX_UPDATE_COUNT = 100; + var queue = []; + var activatedChildren = []; + var has = {}; + var circular = {}; + var waiting = false; + var flushing = false; + var index = 0; + function resetSchedulerState() { + index = queue.length = activatedChildren.length = 0; + has = {}; + if (true) { + circular = {}; + } + waiting = flushing = false; + } + var currentFlushTimestamp = 0; + var getNow = Date.now; + if (inBrowser && !isIE) { + performance = window.performance; + if (performance && typeof performance.now === "function" && getNow() > document.createEvent("Event").timeStamp) { + getNow = function() { + return performance.now(); + }; + } + } + var performance; + function flushSchedulerQueue() { + currentFlushTimestamp = getNow(); + flushing = true; + var watcher, id; + queue.sort(function(a, b) { + return a.id - b.id; + }); + for (index = 0; index < queue.length; index++) { + watcher = queue[index]; + if (watcher.before) { + watcher.before(); + } + id = watcher.id; + has[id] = null; + watcher.run(); + if (has[id] != null) { + circular[id] = (circular[id] || 0) + 1; + if (circular[id] > MAX_UPDATE_COUNT) { + warn("You may have an infinite update loop " + (watcher.user ? 'in watcher with expression "' + watcher.expression + '"' : "in a component render function."), watcher.vm); + break; + } + } + } + var activatedQueue = activatedChildren.slice(); + var updatedQueue = queue.slice(); + resetSchedulerState(); + callActivatedHooks(activatedQueue); + callUpdatedHooks(updatedQueue); + if (devtools && config.devtools) { + devtools.emit("flush"); + } + } + function callUpdatedHooks(queue2) { + var i = queue2.length; + while (i--) { + var watcher = queue2[i]; + var vm = watcher.vm; + if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { + callHook(vm, "updated"); + } + } + } + function queueActivatedComponent(vm) { + vm._inactive = false; + activatedChildren.push(vm); + } + function callActivatedHooks(queue2) { + for (var i = 0; i < queue2.length; i++) { + queue2[i]._inactive = true; + activateChildComponent(queue2[i], true); + } + } + function queueWatcher(watcher) { + var id = watcher.id; + if (has[id] == null) { + has[id] = true; + if (!flushing) { + queue.push(watcher); + } else { + var i = queue.length - 1; + while (i > index && queue[i].id > watcher.id) { + i--; + } + queue.splice(i + 1, 0, watcher); + } + if (!waiting) { + waiting = true; + if (!config.async) { + flushSchedulerQueue(); + return; + } + nextTick(flushSchedulerQueue); + } + } + } + var uid$2 = 0; + var Watcher = function Watcher2(vm, expOrFn, cb, options, isRenderWatcher) { + this.vm = vm; + if (isRenderWatcher) { + vm._watcher = this; + } + vm._watchers.push(this); + if (options) { + this.deep = !!options.deep; + this.user = !!options.user; + this.lazy = !!options.lazy; + this.sync = !!options.sync; + this.before = options.before; + } else { + this.deep = this.user = this.lazy = this.sync = false; + } + this.cb = cb; + this.id = ++uid$2; + this.active = true; + this.dirty = this.lazy; + this.deps = []; + this.newDeps = []; + this.depIds = new _Set(); + this.newDepIds = new _Set(); + this.expression = true ? expOrFn.toString() : ""; + if (typeof expOrFn === "function") { + this.getter = expOrFn; + } else { + this.getter = parsePath(expOrFn); + if (!this.getter) { + this.getter = noop; + warn('Failed watching path: "' + expOrFn + '" Watcher only accepts simple dot-delimited paths. For full control, use a function instead.', vm); + } + } + this.value = this.lazy ? void 0 : this.get(); + }; + Watcher.prototype.get = function get() { + pushTarget(this); + var value; + var vm = this.vm; + try { + value = this.getter.call(vm, vm); + } catch (e) { + if (this.user) { + handleError(e, vm, 'getter for watcher "' + this.expression + '"'); + } else { + throw e; + } + } finally { + if (this.deep) { + traverse(value); + } + popTarget(); + this.cleanupDeps(); + } + return value; + }; + Watcher.prototype.addDep = function addDep(dep) { + var id = dep.id; + if (!this.newDepIds.has(id)) { + this.newDepIds.add(id); + this.newDeps.push(dep); + if (!this.depIds.has(id)) { + dep.addSub(this); + } + } + }; + Watcher.prototype.cleanupDeps = function cleanupDeps() { + var i = this.deps.length; + while (i--) { + var dep = this.deps[i]; + if (!this.newDepIds.has(dep.id)) { + dep.removeSub(this); + } + } + var tmp = this.depIds; + this.depIds = this.newDepIds; + this.newDepIds = tmp; + this.newDepIds.clear(); + tmp = this.deps; + this.deps = this.newDeps; + this.newDeps = tmp; + this.newDeps.length = 0; + }; + Watcher.prototype.update = function update() { + if (this.lazy) { + this.dirty = true; + } else if (this.sync) { + this.run(); + } else { + queueWatcher(this); + } + }; + Watcher.prototype.run = function run() { + if (this.active) { + var value = this.get(); + if (value !== this.value || isObject(value) || this.deep) { + var oldValue = this.value; + this.value = value; + if (this.user) { + try { + this.cb.call(this.vm, value, oldValue); + } catch (e) { + handleError(e, this.vm, 'callback for watcher "' + this.expression + '"'); + } + } else { + this.cb.call(this.vm, value, oldValue); + } + } + } + }; + Watcher.prototype.evaluate = function evaluate() { + this.value = this.get(); + this.dirty = false; + }; + Watcher.prototype.depend = function depend2() { + var i = this.deps.length; + while (i--) { + this.deps[i].depend(); + } + }; + Watcher.prototype.teardown = function teardown() { + if (this.active) { + if (!this.vm._isBeingDestroyed) { + remove(this.vm._watchers, this); + } + var i = this.deps.length; + while (i--) { + this.deps[i].removeSub(this); + } + this.active = false; + } + }; + var sharedPropertyDefinition = { + enumerable: true, + configurable: true, + get: noop, + set: noop + }; + function proxy(target2, sourceKey, key) { + sharedPropertyDefinition.get = function proxyGetter() { + return this[sourceKey][key]; + }; + sharedPropertyDefinition.set = function proxySetter(val) { + this[sourceKey][key] = val; + }; + Object.defineProperty(target2, key, sharedPropertyDefinition); + } + function initState(vm) { + vm._watchers = []; + var opts = vm.$options; + if (opts.props) { + initProps(vm, opts.props); + } + if (opts.methods) { + initMethods(vm, opts.methods); + } + if (opts.data) { + initData(vm); + } else { + observe(vm._data = {}, true); + } + if (opts.computed) { + initComputed(vm, opts.computed); + } + if (opts.watch && opts.watch !== nativeWatch) { + initWatch(vm, opts.watch); + } + } + function initProps(vm, propsOptions) { + var propsData = vm.$options.propsData || {}; + var props2 = vm._props = {}; + var keys = vm.$options._propKeys = []; + var isRoot = !vm.$parent; + if (!isRoot) { + toggleObserving(false); + } + var loop = function(key2) { + keys.push(key2); + var value = validateProp(key2, propsOptions, propsData, vm); + if (true) { + var hyphenatedKey = hyphenate(key2); + if (isReservedAttribute(hyphenatedKey) || config.isReservedAttr(hyphenatedKey)) { + warn('"' + hyphenatedKey + '" is a reserved attribute and cannot be used as component prop.', vm); + } + defineReactive$$1(props2, key2, value, function() { + if (!isRoot && !isUpdatingChildComponent) { + warn(`Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "` + key2 + '"', vm); + } + }); + } else { + defineReactive$$1(props2, key2, value); + } + if (!(key2 in vm)) { + proxy(vm, "_props", key2); + } + }; + for (var key in propsOptions) + loop(key); + toggleObserving(true); + } + function initData(vm) { + var data = vm.$options.data; + data = vm._data = typeof data === "function" ? getData(data, vm) : data || {}; + if (!isPlainObject(data)) { + data = {}; + warn("data functions should return an object:\nhttps://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function", vm); + } + var keys = Object.keys(data); + var props2 = vm.$options.props; + var methods = vm.$options.methods; + var i = keys.length; + while (i--) { + var key = keys[i]; + if (true) { + if (methods && hasOwn(methods, key)) { + warn('Method "' + key + '" has already been defined as a data property.', vm); + } + } + if (props2 && hasOwn(props2, key)) { + warn('The data property "' + key + '" is already declared as a prop. Use prop default value instead.', vm); + } else if (!isReserved(key)) { + proxy(vm, "_data", key); + } + } + observe(data, true); + } + function getData(data, vm) { + pushTarget(); + try { + return data.call(vm, vm); + } catch (e) { + handleError(e, vm, "data()"); + return {}; + } finally { + popTarget(); + } + } + var computedWatcherOptions = { lazy: true }; + function initComputed(vm, computed) { + var watchers = vm._computedWatchers = /* @__PURE__ */ Object.create(null); + var isSSR = isServerRendering(); + for (var key in computed) { + var userDef = computed[key]; + var getter = typeof userDef === "function" ? userDef : userDef.get; + if (getter == null) { + warn('Getter is missing for computed property "' + key + '".', vm); + } + if (!isSSR) { + watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions); + } + if (!(key in vm)) { + defineComputed(vm, key, userDef); + } else if (true) { + if (key in vm.$data) { + warn('The computed property "' + key + '" is already defined in data.', vm); + } else if (vm.$options.props && key in vm.$options.props) { + warn('The computed property "' + key + '" is already defined as a prop.', vm); + } + } + } + } + function defineComputed(target2, key, userDef) { + var shouldCache = !isServerRendering(); + if (typeof userDef === "function") { + sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef); + sharedPropertyDefinition.set = noop; + } else { + sharedPropertyDefinition.get = userDef.get ? shouldCache && userDef.cache !== false ? createComputedGetter(key) : createGetterInvoker(userDef.get) : noop; + sharedPropertyDefinition.set = userDef.set || noop; + } + if (sharedPropertyDefinition.set === noop) { + sharedPropertyDefinition.set = function() { + warn('Computed property "' + key + '" was assigned to but it has no setter.', this); + }; + } + Object.defineProperty(target2, key, sharedPropertyDefinition); + } + function createComputedGetter(key) { + return function computedGetter() { + var watcher = this._computedWatchers && this._computedWatchers[key]; + if (watcher) { + if (watcher.dirty) { + watcher.evaluate(); + } + if (Dep.target) { + watcher.depend(); + } + return watcher.value; + } + }; + } + function createGetterInvoker(fn) { + return function computedGetter() { + return fn.call(this, this); + }; + } + function initMethods(vm, methods) { + var props2 = vm.$options.props; + for (var key in methods) { + if (true) { + if (typeof methods[key] !== "function") { + warn('Method "' + key + '" has type "' + typeof methods[key] + '" in the component definition. Did you reference the function correctly?', vm); + } + if (props2 && hasOwn(props2, key)) { + warn('Method "' + key + '" has already been defined as a prop.', vm); + } + if (key in vm && isReserved(key)) { + warn('Method "' + key + '" conflicts with an existing Vue instance method. Avoid defining component methods that start with _ or $.'); + } + } + vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm); + } + } + function initWatch(vm, watch) { + for (var key in watch) { + var handler = watch[key]; + if (Array.isArray(handler)) { + for (var i = 0; i < handler.length; i++) { + createWatcher(vm, key, handler[i]); + } + } else { + createWatcher(vm, key, handler); + } + } + } + function createWatcher(vm, expOrFn, handler, options) { + if (isPlainObject(handler)) { + options = handler; + handler = handler.handler; + } + if (typeof handler === "string") { + handler = vm[handler]; + } + return vm.$watch(expOrFn, handler, options); + } + function stateMixin(Vue2) { + var dataDef = {}; + dataDef.get = function() { + return this._data; + }; + var propsDef = {}; + propsDef.get = function() { + return this._props; + }; + if (true) { + dataDef.set = function() { + warn("Avoid replacing instance root $data. Use nested data properties instead.", this); + }; + propsDef.set = function() { + warn("$props is readonly.", this); + }; + } + Object.defineProperty(Vue2.prototype, "$data", dataDef); + Object.defineProperty(Vue2.prototype, "$props", propsDef); + Vue2.prototype.$set = set; + Vue2.prototype.$delete = del; + Vue2.prototype.$watch = function(expOrFn, cb, options) { + var vm = this; + if (isPlainObject(cb)) { + return createWatcher(vm, expOrFn, cb, options); + } + options = options || {}; + options.user = true; + var watcher = new Watcher(vm, expOrFn, cb, options); + if (options.immediate) { + try { + cb.call(vm, watcher.value); + } catch (error) { + handleError(error, vm, 'callback for immediate watcher "' + watcher.expression + '"'); + } + } + return function unwatchFn() { + watcher.teardown(); + }; + }; + } + var uid$3 = 0; + function initMixin(Vue2) { + Vue2.prototype._init = function(options) { + var vm = this; + vm._uid = uid$3++; + var startTag, endTag2; + if (config.performance && mark) { + startTag = "vue-perf-start:" + vm._uid; + endTag2 = "vue-perf-end:" + vm._uid; + mark(startTag); + } + vm._isVue = true; + if (options && options._isComponent) { + initInternalComponent(vm, options); + } else { + vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor), options || {}, vm); + } + if (true) { + initProxy(vm); + } else { + vm._renderProxy = vm; + } + vm._self = vm; + initLifecycle(vm); + initEvents(vm); + initRender(vm); + callHook(vm, "beforeCreate"); + initInjections(vm); + initState(vm); + initProvide(vm); + callHook(vm, "created"); + if (config.performance && mark) { + vm._name = formatComponentName(vm, false); + mark(endTag2); + measure("vue " + vm._name + " init", startTag, endTag2); + } + if (vm.$options.el) { + vm.$mount(vm.$options.el); + } + }; + } + function initInternalComponent(vm, options) { + var opts = vm.$options = Object.create(vm.constructor.options); + var parentVnode = options._parentVnode; + opts.parent = options.parent; + opts._parentVnode = parentVnode; + var vnodeComponentOptions = parentVnode.componentOptions; + opts.propsData = vnodeComponentOptions.propsData; + opts._parentListeners = vnodeComponentOptions.listeners; + opts._renderChildren = vnodeComponentOptions.children; + opts._componentTag = vnodeComponentOptions.tag; + if (options.render) { + opts.render = options.render; + opts.staticRenderFns = options.staticRenderFns; + } + } + function resolveConstructorOptions(Ctor) { + var options = Ctor.options; + if (Ctor.super) { + var superOptions = resolveConstructorOptions(Ctor.super); + var cachedSuperOptions = Ctor.superOptions; + if (superOptions !== cachedSuperOptions) { + Ctor.superOptions = superOptions; + var modifiedOptions = resolveModifiedOptions(Ctor); + if (modifiedOptions) { + extend(Ctor.extendOptions, modifiedOptions); + } + options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions); + if (options.name) { + options.components[options.name] = Ctor; + } + } + } + return options; + } + function resolveModifiedOptions(Ctor) { + var modified; + var latest = Ctor.options; + var sealed = Ctor.sealedOptions; + for (var key in latest) { + if (latest[key] !== sealed[key]) { + if (!modified) { + modified = {}; + } + modified[key] = latest[key]; + } + } + return modified; + } + function Vue(options) { + if (!(this instanceof Vue)) { + warn("Vue is a constructor and should be called with the `new` keyword"); + } + this._init(options); + } + initMixin(Vue); + stateMixin(Vue); + eventsMixin(Vue); + lifecycleMixin(Vue); + renderMixin(Vue); + function initUse(Vue2) { + Vue2.use = function(plugin) { + var installedPlugins = this._installedPlugins || (this._installedPlugins = []); + if (installedPlugins.indexOf(plugin) > -1) { + return this; + } + var args = toArray(arguments, 1); + args.unshift(this); + if (typeof plugin.install === "function") { + plugin.install.apply(plugin, args); + } else if (typeof plugin === "function") { + plugin.apply(null, args); + } + installedPlugins.push(plugin); + return this; + }; + } + function initMixin$1(Vue2) { + Vue2.mixin = function(mixin) { + this.options = mergeOptions(this.options, mixin); + return this; + }; + } + function initExtend(Vue2) { + Vue2.cid = 0; + var cid = 1; + Vue2.extend = function(extendOptions) { + extendOptions = extendOptions || {}; + var Super = this; + var SuperId = Super.cid; + var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); + if (cachedCtors[SuperId]) { + return cachedCtors[SuperId]; + } + var name = extendOptions.name || Super.options.name; + if (name) { + validateComponentName(name); + } + var Sub = function VueComponent(options) { + this._init(options); + }; + Sub.prototype = Object.create(Super.prototype); + Sub.prototype.constructor = Sub; + Sub.cid = cid++; + Sub.options = mergeOptions(Super.options, extendOptions); + Sub["super"] = Super; + if (Sub.options.props) { + initProps$1(Sub); + } + if (Sub.options.computed) { + initComputed$1(Sub); + } + Sub.extend = Super.extend; + Sub.mixin = Super.mixin; + Sub.use = Super.use; + ASSET_TYPES.forEach(function(type) { + Sub[type] = Super[type]; + }); + if (name) { + Sub.options.components[name] = Sub; + } + Sub.superOptions = Super.options; + Sub.extendOptions = extendOptions; + Sub.sealedOptions = extend({}, Sub.options); + cachedCtors[SuperId] = Sub; + return Sub; + }; + } + function initProps$1(Comp) { + var props2 = Comp.options.props; + for (var key in props2) { + proxy(Comp.prototype, "_props", key); + } + } + function initComputed$1(Comp) { + var computed = Comp.options.computed; + for (var key in computed) { + defineComputed(Comp.prototype, key, computed[key]); + } + } + function initAssetRegisters(Vue2) { + ASSET_TYPES.forEach(function(type) { + Vue2[type] = function(id, definition) { + if (!definition) { + return this.options[type + "s"][id]; + } else { + if (type === "component") { + validateComponentName(id); + } + if (type === "component" && isPlainObject(definition)) { + definition.name = definition.name || id; + definition = this.options._base.extend(definition); + } + if (type === "directive" && typeof definition === "function") { + definition = { bind: definition, update: definition }; + } + this.options[type + "s"][id] = definition; + return definition; + } + }; + }); + } + function getComponentName(opts) { + return opts && (opts.Ctor.options.name || opts.tag); + } + function matches(pattern, name) { + if (Array.isArray(pattern)) { + return pattern.indexOf(name) > -1; + } else if (typeof pattern === "string") { + return pattern.split(",").indexOf(name) > -1; + } else if (isRegExp(pattern)) { + return pattern.test(name); + } + return false; + } + function pruneCache(keepAliveInstance, filter) { + var cache = keepAliveInstance.cache; + var keys = keepAliveInstance.keys; + var _vnode = keepAliveInstance._vnode; + for (var key in cache) { + var cachedNode = cache[key]; + if (cachedNode) { + var name = getComponentName(cachedNode.componentOptions); + if (name && !filter(name)) { + pruneCacheEntry(cache, key, keys, _vnode); + } + } + } + } + function pruneCacheEntry(cache, key, keys, current) { + var cached$$1 = cache[key]; + if (cached$$1 && (!current || cached$$1.tag !== current.tag)) { + cached$$1.componentInstance.$destroy(); + } + cache[key] = null; + remove(keys, key); + } + var patternTypes = [String, RegExp, Array]; + var KeepAlive = { + name: "keep-alive", + abstract: true, + props: { + include: patternTypes, + exclude: patternTypes, + max: [String, Number] + }, + created: function created() { + this.cache = /* @__PURE__ */ Object.create(null); + this.keys = []; + }, + destroyed: function destroyed() { + for (var key in this.cache) { + pruneCacheEntry(this.cache, key, this.keys); + } + }, + mounted: function mounted() { + var this$1 = this; + this.$watch("include", function(val) { + pruneCache(this$1, function(name) { + return matches(val, name); + }); + }); + this.$watch("exclude", function(val) { + pruneCache(this$1, function(name) { + return !matches(val, name); + }); + }); + }, + render: function render() { + var slot = this.$slots.default; + var vnode = getFirstComponentChild(slot); + var componentOptions = vnode && vnode.componentOptions; + if (componentOptions) { + var name = getComponentName(componentOptions); + var ref2 = this; + var include = ref2.include; + var exclude = ref2.exclude; + if (include && (!name || !matches(include, name)) || exclude && name && matches(exclude, name)) { + return vnode; + } + var ref$12 = this; + var cache = ref$12.cache; + var keys = ref$12.keys; + var key = vnode.key == null ? componentOptions.Ctor.cid + (componentOptions.tag ? "::" + componentOptions.tag : "") : vnode.key; + if (cache[key]) { + vnode.componentInstance = cache[key].componentInstance; + remove(keys, key); + keys.push(key); + } else { + cache[key] = vnode; + keys.push(key); + if (this.max && keys.length > parseInt(this.max)) { + pruneCacheEntry(cache, keys[0], keys, this._vnode); + } + } + vnode.data.keepAlive = true; + } + return vnode || slot && slot[0]; + } + }; + var builtInComponents = { + KeepAlive + }; + function initGlobalAPI(Vue2) { + var configDef = {}; + configDef.get = function() { + return config; + }; + if (true) { + configDef.set = function() { + warn("Do not replace the Vue.config object, set individual fields instead."); + }; + } + Object.defineProperty(Vue2, "config", configDef); + Vue2.util = { + warn, + extend, + mergeOptions, + defineReactive: defineReactive$$1 + }; + Vue2.set = set; + Vue2.delete = del; + Vue2.nextTick = nextTick; + Vue2.observable = function(obj) { + observe(obj); + return obj; + }; + Vue2.options = /* @__PURE__ */ Object.create(null); + ASSET_TYPES.forEach(function(type) { + Vue2.options[type + "s"] = /* @__PURE__ */ Object.create(null); + }); + Vue2.options._base = Vue2; + extend(Vue2.options.components, builtInComponents); + initUse(Vue2); + initMixin$1(Vue2); + initExtend(Vue2); + initAssetRegisters(Vue2); + } + initGlobalAPI(Vue); + Object.defineProperty(Vue.prototype, "$isServer", { + get: isServerRendering + }); + Object.defineProperty(Vue.prototype, "$ssrContext", { + get: function get2() { + return this.$vnode && this.$vnode.ssrContext; + } + }); + Object.defineProperty(Vue, "FunctionalRenderContext", { + value: FunctionalRenderContext + }); + Vue.version = "2.6.12"; + var isReservedAttr = makeMap("style,class"); + var acceptValue = makeMap("input,textarea,option,select,progress"); + var mustUseProp = function(tag, type, attr) { + return attr === "value" && acceptValue(tag) && type !== "button" || attr === "selected" && tag === "option" || attr === "checked" && tag === "input" || attr === "muted" && tag === "video"; + }; + var isEnumeratedAttr = makeMap("contenteditable,draggable,spellcheck"); + var isValidContentEditableValue = makeMap("events,caret,typing,plaintext-only"); + var convertEnumeratedValue = function(key, value) { + return isFalsyAttrValue(value) || value === "false" ? "false" : key === "contenteditable" && isValidContentEditableValue(value) ? value : "true"; + }; + var isBooleanAttr = makeMap("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"); + var xlinkNS = "http://www.w3.org/1999/xlink"; + var isXlink = function(name) { + return name.charAt(5) === ":" && name.slice(0, 5) === "xlink"; + }; + var getXlinkProp = function(name) { + return isXlink(name) ? name.slice(6, name.length) : ""; + }; + var isFalsyAttrValue = function(val) { + return val == null || val === false; + }; + function genClassForVnode(vnode) { + var data = vnode.data; + var parentNode2 = vnode; + var childNode = vnode; + while (isDef(childNode.componentInstance)) { + childNode = childNode.componentInstance._vnode; + if (childNode && childNode.data) { + data = mergeClassData(childNode.data, data); + } + } + while (isDef(parentNode2 = parentNode2.parent)) { + if (parentNode2 && parentNode2.data) { + data = mergeClassData(data, parentNode2.data); + } + } + return renderClass(data.staticClass, data.class); + } + function mergeClassData(child, parent) { + return { + staticClass: concat(child.staticClass, parent.staticClass), + class: isDef(child.class) ? [child.class, parent.class] : parent.class + }; + } + function renderClass(staticClass, dynamicClass) { + if (isDef(staticClass) || isDef(dynamicClass)) { + return concat(staticClass, stringifyClass(dynamicClass)); + } + return ""; + } + function concat(a, b) { + return a ? b ? a + " " + b : a : b || ""; + } + function stringifyClass(value) { + if (Array.isArray(value)) { + return stringifyArray(value); + } + if (isObject(value)) { + return stringifyObject(value); + } + if (typeof value === "string") { + return value; + } + return ""; + } + function stringifyArray(value) { + var res = ""; + var stringified; + for (var i = 0, l = value.length; i < l; i++) { + if (isDef(stringified = stringifyClass(value[i])) && stringified !== "") { + if (res) { + res += " "; + } + res += stringified; + } + } + return res; + } + function stringifyObject(value) { + var res = ""; + for (var key in value) { + if (value[key]) { + if (res) { + res += " "; + } + res += key; + } + } + return res; + } + var namespaceMap = { + svg: "http://www.w3.org/2000/svg", + math: "http://www.w3.org/1998/Math/MathML" + }; + var isHTMLTag = makeMap("html,body,base,head,link,meta,style,title,address,article,aside,footer,header,h1,h2,h3,h4,h5,h6,hgroup,nav,section,div,dd,dl,dt,figcaption,figure,picture,hr,img,li,main,ol,p,pre,ul,a,b,abbr,bdi,bdo,br,cite,code,data,dfn,em,i,kbd,mark,q,rp,rt,rtc,ruby,s,samp,small,span,strong,sub,sup,time,u,var,wbr,area,audio,map,track,video,embed,object,param,source,canvas,script,noscript,del,ins,caption,col,colgroup,table,thead,tbody,td,th,tr,button,datalist,fieldset,form,input,label,legend,meter,optgroup,option,output,progress,select,textarea,details,dialog,menu,menuitem,summary,content,element,shadow,template,blockquote,iframe,tfoot"); + var isSVG = makeMap("svg,animate,circle,clippath,cursor,defs,desc,ellipse,filter,font-face,foreignObject,g,glyph,image,line,marker,mask,missing-glyph,path,pattern,polygon,polyline,rect,switch,symbol,text,textpath,tspan,use,view", true); + var isPreTag = function(tag) { + return tag === "pre"; + }; + var isReservedTag = function(tag) { + return isHTMLTag(tag) || isSVG(tag); + }; + function getTagNamespace(tag) { + if (isSVG(tag)) { + return "svg"; + } + if (tag === "math") { + return "math"; + } + } + var unknownElementCache = /* @__PURE__ */ Object.create(null); + function isUnknownElement(tag) { + if (!inBrowser) { + return true; + } + if (isReservedTag(tag)) { + return false; + } + tag = tag.toLowerCase(); + if (unknownElementCache[tag] != null) { + return unknownElementCache[tag]; + } + var el = document.createElement(tag); + if (tag.indexOf("-") > -1) { + return unknownElementCache[tag] = el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement; + } else { + return unknownElementCache[tag] = /HTMLUnknownElement/.test(el.toString()); + } + } + var isTextInputType = makeMap("text,number,password,search,email,tel,url"); + function query(el) { + if (typeof el === "string") { + var selected = document.querySelector(el); + if (!selected) { + warn("Cannot find element: " + el); + return document.createElement("div"); + } + return selected; + } else { + return el; + } + } + function createElement$1(tagName2, vnode) { + var elm = document.createElement(tagName2); + if (tagName2 !== "select") { + return elm; + } + if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== void 0) { + elm.setAttribute("multiple", "multiple"); + } + return elm; + } + function createElementNS(namespace, tagName2) { + return document.createElementNS(namespaceMap[namespace], tagName2); + } + function createTextNode(text2) { + return document.createTextNode(text2); + } + function createComment(text2) { + return document.createComment(text2); + } + function insertBefore(parentNode2, newNode, referenceNode) { + parentNode2.insertBefore(newNode, referenceNode); + } + function removeChild(node, child) { + node.removeChild(child); + } + function appendChild(node, child) { + node.appendChild(child); + } + function parentNode(node) { + return node.parentNode; + } + function nextSibling(node) { + return node.nextSibling; + } + function tagName(node) { + return node.tagName; + } + function setTextContent(node, text2) { + node.textContent = text2; + } + function setStyleScope(node, scopeId) { + node.setAttribute(scopeId, ""); + } + var nodeOps = /* @__PURE__ */ Object.freeze({ + createElement: createElement$1, + createElementNS, + createTextNode, + createComment, + insertBefore, + removeChild, + appendChild, + parentNode, + nextSibling, + tagName, + setTextContent, + setStyleScope + }); + var ref = { + create: function create(_, vnode) { + registerRef(vnode); + }, + update: function update2(oldVnode, vnode) { + if (oldVnode.data.ref !== vnode.data.ref) { + registerRef(oldVnode, true); + registerRef(vnode); + } + }, + destroy: function destroy2(vnode) { + registerRef(vnode, true); + } + }; + function registerRef(vnode, isRemoval) { + var key = vnode.data.ref; + if (!isDef(key)) { + return; + } + var vm = vnode.context; + var ref2 = vnode.componentInstance || vnode.elm; + var refs = vm.$refs; + if (isRemoval) { + if (Array.isArray(refs[key])) { + remove(refs[key], ref2); + } else if (refs[key] === ref2) { + refs[key] = void 0; + } + } else { + if (vnode.data.refInFor) { + if (!Array.isArray(refs[key])) { + refs[key] = [ref2]; + } else if (refs[key].indexOf(ref2) < 0) { + refs[key].push(ref2); + } + } else { + refs[key] = ref2; + } + } + } + var emptyNode = new VNode("", {}, []); + var hooks = ["create", "activate", "update", "remove", "destroy"]; + function sameVnode(a, b) { + return a.key === b.key && (a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) || isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error)); + } + function sameInputType(a, b) { + if (a.tag !== "input") { + return true; + } + var i; + var typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type; + var typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type; + return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB); + } + function createKeyToOldIdx(children, beginIdx, endIdx) { + var i, key; + var map = {}; + for (i = beginIdx; i <= endIdx; ++i) { + key = children[i].key; + if (isDef(key)) { + map[key] = i; + } + } + return map; + } + function createPatchFunction(backend) { + var i, j; + var cbs = {}; + var modules2 = backend.modules; + var nodeOps2 = backend.nodeOps; + for (i = 0; i < hooks.length; ++i) { + cbs[hooks[i]] = []; + for (j = 0; j < modules2.length; ++j) { + if (isDef(modules2[j][hooks[i]])) { + cbs[hooks[i]].push(modules2[j][hooks[i]]); + } + } + } + function emptyNodeAt(elm) { + return new VNode(nodeOps2.tagName(elm).toLowerCase(), {}, [], void 0, elm); + } + function createRmCb(childElm, listeners) { + function remove$$12() { + if (--remove$$12.listeners === 0) { + removeNode(childElm); + } + } + remove$$12.listeners = listeners; + return remove$$12; + } + function removeNode(el) { + var parent = nodeOps2.parentNode(el); + if (isDef(parent)) { + nodeOps2.removeChild(parent, el); + } + } + function isUnknownElement$$1(vnode, inVPre) { + return !inVPre && !vnode.ns && !(config.ignoredElements.length && config.ignoredElements.some(function(ignore) { + return isRegExp(ignore) ? ignore.test(vnode.tag) : ignore === vnode.tag; + })) && config.isUnknownElement(vnode.tag); + } + var creatingElmInVPre = 0; + function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index2) { + if (isDef(vnode.elm) && isDef(ownerArray)) { + vnode = ownerArray[index2] = cloneVNode(vnode); + } + vnode.isRootInsert = !nested; + if (createComponent2(vnode, insertedVnodeQueue, parentElm, refElm)) { + return; + } + var data = vnode.data; + var children = vnode.children; + var tag = vnode.tag; + if (isDef(tag)) { + if (true) { + if (data && data.pre) { + creatingElmInVPre++; + } + if (isUnknownElement$$1(vnode, creatingElmInVPre)) { + warn("Unknown custom element: <" + tag + '> - did you register the component correctly? For recursive components, make sure to provide the "name" option.', vnode.context); + } + } + vnode.elm = vnode.ns ? nodeOps2.createElementNS(vnode.ns, tag) : nodeOps2.createElement(tag, vnode); + setScope(vnode); + { + createChildren(vnode, children, insertedVnodeQueue); + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue); + } + insert2(parentElm, vnode.elm, refElm); + } + if (data && data.pre) { + creatingElmInVPre--; + } + } else if (isTrue(vnode.isComment)) { + vnode.elm = nodeOps2.createComment(vnode.text); + insert2(parentElm, vnode.elm, refElm); + } else { + vnode.elm = nodeOps2.createTextNode(vnode.text); + insert2(parentElm, vnode.elm, refElm); + } + } + function createComponent2(vnode, insertedVnodeQueue, parentElm, refElm) { + var i2 = vnode.data; + if (isDef(i2)) { + var isReactivated = isDef(vnode.componentInstance) && i2.keepAlive; + if (isDef(i2 = i2.hook) && isDef(i2 = i2.init)) { + i2(vnode, false); + } + if (isDef(vnode.componentInstance)) { + initComponent(vnode, insertedVnodeQueue); + insert2(parentElm, vnode.elm, refElm); + if (isTrue(isReactivated)) { + reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); + } + return true; + } + } + } + function initComponent(vnode, insertedVnodeQueue) { + if (isDef(vnode.data.pendingInsert)) { + insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert); + vnode.data.pendingInsert = null; + } + vnode.elm = vnode.componentInstance.$el; + if (isPatchable(vnode)) { + invokeCreateHooks(vnode, insertedVnodeQueue); + setScope(vnode); + } else { + registerRef(vnode); + insertedVnodeQueue.push(vnode); + } + } + function reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) { + var i2; + var innerNode = vnode; + while (innerNode.componentInstance) { + innerNode = innerNode.componentInstance._vnode; + if (isDef(i2 = innerNode.data) && isDef(i2 = i2.transition)) { + for (i2 = 0; i2 < cbs.activate.length; ++i2) { + cbs.activate[i2](emptyNode, innerNode); + } + insertedVnodeQueue.push(innerNode); + break; + } + } + insert2(parentElm, vnode.elm, refElm); + } + function insert2(parent, elm, ref$$1) { + if (isDef(parent)) { + if (isDef(ref$$1)) { + if (nodeOps2.parentNode(ref$$1) === parent) { + nodeOps2.insertBefore(parent, elm, ref$$1); + } + } else { + nodeOps2.appendChild(parent, elm); + } + } + } + function createChildren(vnode, children, insertedVnodeQueue) { + if (Array.isArray(children)) { + if (true) { + checkDuplicateKeys(children); + } + for (var i2 = 0; i2 < children.length; ++i2) { + createElm(children[i2], insertedVnodeQueue, vnode.elm, null, true, children, i2); + } + } else if (isPrimitive(vnode.text)) { + nodeOps2.appendChild(vnode.elm, nodeOps2.createTextNode(String(vnode.text))); + } + } + function isPatchable(vnode) { + while (vnode.componentInstance) { + vnode = vnode.componentInstance._vnode; + } + return isDef(vnode.tag); + } + function invokeCreateHooks(vnode, insertedVnodeQueue) { + for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { + cbs.create[i$1](emptyNode, vnode); + } + i = vnode.data.hook; + if (isDef(i)) { + if (isDef(i.create)) { + i.create(emptyNode, vnode); + } + if (isDef(i.insert)) { + insertedVnodeQueue.push(vnode); + } + } + } + function setScope(vnode) { + var i2; + if (isDef(i2 = vnode.fnScopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } else { + var ancestor = vnode; + while (ancestor) { + if (isDef(i2 = ancestor.context) && isDef(i2 = i2.$options._scopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } + ancestor = ancestor.parent; + } + } + if (isDef(i2 = activeInstance) && i2 !== vnode.context && i2 !== vnode.fnContext && isDef(i2 = i2.$options._scopeId)) { + nodeOps2.setStyleScope(vnode.elm, i2); + } + } + function addVnodes(parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) { + for (; startIdx <= endIdx; ++startIdx) { + createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx); + } + } + function invokeDestroyHook(vnode) { + var i2, j2; + var data = vnode.data; + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.destroy)) { + i2(vnode); + } + for (i2 = 0; i2 < cbs.destroy.length; ++i2) { + cbs.destroy[i2](vnode); + } + } + if (isDef(i2 = vnode.children)) { + for (j2 = 0; j2 < vnode.children.length; ++j2) { + invokeDestroyHook(vnode.children[j2]); + } + } + } + function removeVnodes(vnodes, startIdx, endIdx) { + for (; startIdx <= endIdx; ++startIdx) { + var ch = vnodes[startIdx]; + if (isDef(ch)) { + if (isDef(ch.tag)) { + removeAndInvokeRemoveHook(ch); + invokeDestroyHook(ch); + } else { + removeNode(ch.elm); + } + } + } + } + function removeAndInvokeRemoveHook(vnode, rm) { + if (isDef(rm) || isDef(vnode.data)) { + var i2; + var listeners = cbs.remove.length + 1; + if (isDef(rm)) { + rm.listeners += listeners; + } else { + rm = createRmCb(vnode.elm, listeners); + } + if (isDef(i2 = vnode.componentInstance) && isDef(i2 = i2._vnode) && isDef(i2.data)) { + removeAndInvokeRemoveHook(i2, rm); + } + for (i2 = 0; i2 < cbs.remove.length; ++i2) { + cbs.remove[i2](vnode, rm); + } + if (isDef(i2 = vnode.data.hook) && isDef(i2 = i2.remove)) { + i2(vnode, rm); + } else { + rm(); + } + } else { + removeNode(vnode.elm); + } + } + function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { + var oldStartIdx = 0; + var newStartIdx = 0; + var oldEndIdx = oldCh.length - 1; + var oldStartVnode = oldCh[0]; + var oldEndVnode = oldCh[oldEndIdx]; + var newEndIdx = newCh.length - 1; + var newStartVnode = newCh[0]; + var newEndVnode = newCh[newEndIdx]; + var oldKeyToIdx, idxInOld, vnodeToMove, refElm; + var canMove = !removeOnly; + if (true) { + checkDuplicateKeys(newCh); + } + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + if (isUndef(oldStartVnode)) { + oldStartVnode = oldCh[++oldStartIdx]; + } else if (isUndef(oldEndVnode)) { + oldEndVnode = oldCh[--oldEndIdx]; + } else if (sameVnode(oldStartVnode, newStartVnode)) { + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + } else if (sameVnode(oldEndVnode, newEndVnode)) { + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx); + canMove && nodeOps2.insertBefore(parentElm, oldStartVnode.elm, nodeOps2.nextSibling(oldEndVnode.elm)); + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + canMove && nodeOps2.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + if (isUndef(oldKeyToIdx)) { + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); + if (isUndef(idxInOld)) { + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } else { + vnodeToMove = oldCh[idxInOld]; + if (sameVnode(vnodeToMove, newStartVnode)) { + patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx); + oldCh[idxInOld] = void 0; + canMove && nodeOps2.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); + } else { + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx > oldEndIdx) { + refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; + addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else if (newStartIdx > newEndIdx) { + removeVnodes(oldCh, oldStartIdx, oldEndIdx); + } + } + function checkDuplicateKeys(children) { + var seenKeys = {}; + for (var i2 = 0; i2 < children.length; i2++) { + var vnode = children[i2]; + var key = vnode.key; + if (isDef(key)) { + if (seenKeys[key]) { + warn("Duplicate keys detected: '" + key + "'. This may cause an update error.", vnode.context); + } else { + seenKeys[key] = true; + } + } + } + } + function findIdxInOld(node, oldCh, start, end) { + for (var i2 = start; i2 < end; i2++) { + var c = oldCh[i2]; + if (isDef(c) && sameVnode(node, c)) { + return i2; + } + } + } + function patchVnode(oldVnode, vnode, insertedVnodeQueue, ownerArray, index2, removeOnly) { + if (oldVnode === vnode) { + return; + } + if (isDef(vnode.elm) && isDef(ownerArray)) { + vnode = ownerArray[index2] = cloneVNode(vnode); + } + var elm = vnode.elm = oldVnode.elm; + if (isTrue(oldVnode.isAsyncPlaceholder)) { + if (isDef(vnode.asyncFactory.resolved)) { + hydrate(oldVnode.elm, vnode, insertedVnodeQueue); + } else { + vnode.isAsyncPlaceholder = true; + } + return; + } + if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { + vnode.componentInstance = oldVnode.componentInstance; + return; + } + var i2; + var data = vnode.data; + if (isDef(data) && isDef(i2 = data.hook) && isDef(i2 = i2.prepatch)) { + i2(oldVnode, vnode); + } + var oldCh = oldVnode.children; + var ch = vnode.children; + if (isDef(data) && isPatchable(vnode)) { + for (i2 = 0; i2 < cbs.update.length; ++i2) { + cbs.update[i2](oldVnode, vnode); + } + if (isDef(i2 = data.hook) && isDef(i2 = i2.update)) { + i2(oldVnode, vnode); + } + } + if (isUndef(vnode.text)) { + if (isDef(oldCh) && isDef(ch)) { + if (oldCh !== ch) { + updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); + } + } else if (isDef(ch)) { + if (true) { + checkDuplicateKeys(ch); + } + if (isDef(oldVnode.text)) { + nodeOps2.setTextContent(elm, ""); + } + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { + removeVnodes(oldCh, 0, oldCh.length - 1); + } else if (isDef(oldVnode.text)) { + nodeOps2.setTextContent(elm, ""); + } + } else if (oldVnode.text !== vnode.text) { + nodeOps2.setTextContent(elm, vnode.text); + } + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.postpatch)) { + i2(oldVnode, vnode); + } + } + } + function invokeInsertHook(vnode, queue2, initial) { + if (isTrue(initial) && isDef(vnode.parent)) { + vnode.parent.data.pendingInsert = queue2; + } else { + for (var i2 = 0; i2 < queue2.length; ++i2) { + queue2[i2].data.hook.insert(queue2[i2]); + } + } + } + var hydrationBailed = false; + var isRenderedModule = makeMap("attrs,class,staticClass,staticStyle,key"); + function hydrate(elm, vnode, insertedVnodeQueue, inVPre) { + var i2; + var tag = vnode.tag; + var data = vnode.data; + var children = vnode.children; + inVPre = inVPre || data && data.pre; + vnode.elm = elm; + if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) { + vnode.isAsyncPlaceholder = true; + return true; + } + if (true) { + if (!assertNodeMatch(elm, vnode, inVPre)) { + return false; + } + } + if (isDef(data)) { + if (isDef(i2 = data.hook) && isDef(i2 = i2.init)) { + i2(vnode, true); + } + if (isDef(i2 = vnode.componentInstance)) { + initComponent(vnode, insertedVnodeQueue); + return true; + } + } + if (isDef(tag)) { + if (isDef(children)) { + if (!elm.hasChildNodes()) { + createChildren(vnode, children, insertedVnodeQueue); + } else { + if (isDef(i2 = data) && isDef(i2 = i2.domProps) && isDef(i2 = i2.innerHTML)) { + if (i2 !== elm.innerHTML) { + if (typeof console !== "undefined" && !hydrationBailed) { + hydrationBailed = true; + console.warn("Parent: ", elm); + console.warn("server innerHTML: ", i2); + console.warn("client innerHTML: ", elm.innerHTML); + } + return false; + } + } else { + var childrenMatch = true; + var childNode = elm.firstChild; + for (var i$1 = 0; i$1 < children.length; i$1++) { + if (!childNode || !hydrate(childNode, children[i$1], insertedVnodeQueue, inVPre)) { + childrenMatch = false; + break; + } + childNode = childNode.nextSibling; + } + if (!childrenMatch || childNode) { + if (typeof console !== "undefined" && !hydrationBailed) { + hydrationBailed = true; + console.warn("Parent: ", elm); + console.warn("Mismatching childNodes vs. VNodes: ", elm.childNodes, children); + } + return false; + } + } + } + } + if (isDef(data)) { + var fullInvoke = false; + for (var key in data) { + if (!isRenderedModule(key)) { + fullInvoke = true; + invokeCreateHooks(vnode, insertedVnodeQueue); + break; + } + } + if (!fullInvoke && data["class"]) { + traverse(data["class"]); + } + } + } else if (elm.data !== vnode.text) { + elm.data = vnode.text; + } + return true; + } + function assertNodeMatch(node, vnode, inVPre) { + if (isDef(vnode.tag)) { + return vnode.tag.indexOf("vue-component") === 0 || !isUnknownElement$$1(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()); + } else { + return node.nodeType === (vnode.isComment ? 8 : 3); + } + } + return function patch2(oldVnode, vnode, hydrating, removeOnly) { + if (isUndef(vnode)) { + if (isDef(oldVnode)) { + invokeDestroyHook(oldVnode); + } + return; + } + var isInitialPatch = false; + var insertedVnodeQueue = []; + if (isUndef(oldVnode)) { + isInitialPatch = true; + createElm(vnode, insertedVnodeQueue); + } else { + var isRealElement = isDef(oldVnode.nodeType); + if (!isRealElement && sameVnode(oldVnode, vnode)) { + patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly); + } else { + if (isRealElement) { + if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { + oldVnode.removeAttribute(SSR_ATTR); + hydrating = true; + } + if (isTrue(hydrating)) { + if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { + invokeInsertHook(vnode, insertedVnodeQueue, true); + return oldVnode; + } else if (true) { + warn("The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside

, or missing . Bailing hydration and performing full client-side render."); + } + } + oldVnode = emptyNodeAt(oldVnode); + } + var oldElm = oldVnode.elm; + var parentElm = nodeOps2.parentNode(oldElm); + createElm(vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps2.nextSibling(oldElm)); + if (isDef(vnode.parent)) { + var ancestor = vnode.parent; + var patchable = isPatchable(vnode); + while (ancestor) { + for (var i2 = 0; i2 < cbs.destroy.length; ++i2) { + cbs.destroy[i2](ancestor); + } + ancestor.elm = vnode.elm; + if (patchable) { + for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) { + cbs.create[i$1](emptyNode, ancestor); + } + var insert3 = ancestor.data.hook.insert; + if (insert3.merged) { + for (var i$2 = 1; i$2 < insert3.fns.length; i$2++) { + insert3.fns[i$2](); + } + } + } else { + registerRef(ancestor); + } + ancestor = ancestor.parent; + } + } + if (isDef(parentElm)) { + removeVnodes([oldVnode], 0, 0); + } else if (isDef(oldVnode.tag)) { + invokeDestroyHook(oldVnode); + } + } + } + invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); + return vnode.elm; + }; + } + var directives = { + create: updateDirectives, + update: updateDirectives, + destroy: function unbindDirectives(vnode) { + updateDirectives(vnode, emptyNode); + } + }; + function updateDirectives(oldVnode, vnode) { + if (oldVnode.data.directives || vnode.data.directives) { + _update(oldVnode, vnode); + } + } + function _update(oldVnode, vnode) { + var isCreate = oldVnode === emptyNode; + var isDestroy = vnode === emptyNode; + var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); + var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); + var dirsWithInsert = []; + var dirsWithPostpatch = []; + var key, oldDir, dir; + for (key in newDirs) { + oldDir = oldDirs[key]; + dir = newDirs[key]; + if (!oldDir) { + callHook$1(dir, "bind", vnode, oldVnode); + if (dir.def && dir.def.inserted) { + dirsWithInsert.push(dir); + } + } else { + dir.oldValue = oldDir.value; + dir.oldArg = oldDir.arg; + callHook$1(dir, "update", vnode, oldVnode); + if (dir.def && dir.def.componentUpdated) { + dirsWithPostpatch.push(dir); + } + } + } + if (dirsWithInsert.length) { + var callInsert = function() { + for (var i = 0; i < dirsWithInsert.length; i++) { + callHook$1(dirsWithInsert[i], "inserted", vnode, oldVnode); + } + }; + if (isCreate) { + mergeVNodeHook(vnode, "insert", callInsert); + } else { + callInsert(); + } + } + if (dirsWithPostpatch.length) { + mergeVNodeHook(vnode, "postpatch", function() { + for (var i = 0; i < dirsWithPostpatch.length; i++) { + callHook$1(dirsWithPostpatch[i], "componentUpdated", vnode, oldVnode); + } + }); + } + if (!isCreate) { + for (key in oldDirs) { + if (!newDirs[key]) { + callHook$1(oldDirs[key], "unbind", oldVnode, oldVnode, isDestroy); + } + } + } + } + var emptyModifiers = /* @__PURE__ */ Object.create(null); + function normalizeDirectives$1(dirs, vm) { + var res = /* @__PURE__ */ Object.create(null); + if (!dirs) { + return res; + } + var i, dir; + for (i = 0; i < dirs.length; i++) { + dir = dirs[i]; + if (!dir.modifiers) { + dir.modifiers = emptyModifiers; + } + res[getRawDirName(dir)] = dir; + dir.def = resolveAsset(vm.$options, "directives", dir.name, true); + } + return res; + } + function getRawDirName(dir) { + return dir.rawName || dir.name + "." + Object.keys(dir.modifiers || {}).join("."); + } + function callHook$1(dir, hook, vnode, oldVnode, isDestroy) { + var fn = dir.def && dir.def[hook]; + if (fn) { + try { + fn(vnode.elm, dir, vnode, oldVnode, isDestroy); + } catch (e) { + handleError(e, vnode.context, "directive " + dir.name + " " + hook + " hook"); + } + } + } + var baseModules = [ + ref, + directives + ]; + function updateAttrs(oldVnode, vnode) { + var opts = vnode.componentOptions; + if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { + return; + } + if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { + return; + } + var key, cur, old; + var elm = vnode.elm; + var oldAttrs = oldVnode.data.attrs || {}; + var attrs2 = vnode.data.attrs || {}; + if (isDef(attrs2.__ob__)) { + attrs2 = vnode.data.attrs = extend({}, attrs2); + } + for (key in attrs2) { + cur = attrs2[key]; + old = oldAttrs[key]; + if (old !== cur) { + setAttr(elm, key, cur); + } + } + if ((isIE || isEdge) && attrs2.value !== oldAttrs.value) { + setAttr(elm, "value", attrs2.value); + } + for (key in oldAttrs) { + if (isUndef(attrs2[key])) { + if (isXlink(key)) { + elm.removeAttributeNS(xlinkNS, getXlinkProp(key)); + } else if (!isEnumeratedAttr(key)) { + elm.removeAttribute(key); + } + } + } + } + function setAttr(el, key, value) { + if (el.tagName.indexOf("-") > -1) { + baseSetAttr(el, key, value); + } else if (isBooleanAttr(key)) { + if (isFalsyAttrValue(value)) { + el.removeAttribute(key); + } else { + value = key === "allowfullscreen" && el.tagName === "EMBED" ? "true" : key; + el.setAttribute(key, value); + } + } else if (isEnumeratedAttr(key)) { + el.setAttribute(key, convertEnumeratedValue(key, value)); + } else if (isXlink(key)) { + if (isFalsyAttrValue(value)) { + el.removeAttributeNS(xlinkNS, getXlinkProp(key)); + } else { + el.setAttributeNS(xlinkNS, key, value); + } + } else { + baseSetAttr(el, key, value); + } + } + function baseSetAttr(el, key, value) { + if (isFalsyAttrValue(value)) { + el.removeAttribute(key); + } else { + if (isIE && !isIE9 && el.tagName === "TEXTAREA" && key === "placeholder" && value !== "" && !el.__ieph) { + var blocker = function(e) { + e.stopImmediatePropagation(); + el.removeEventListener("input", blocker); + }; + el.addEventListener("input", blocker); + el.__ieph = true; + } + el.setAttribute(key, value); + } + } + var attrs = { + create: updateAttrs, + update: updateAttrs + }; + function updateClass(oldVnode, vnode) { + var el = vnode.elm; + var data = vnode.data; + var oldData = oldVnode.data; + if (isUndef(data.staticClass) && isUndef(data.class) && (isUndef(oldData) || isUndef(oldData.staticClass) && isUndef(oldData.class))) { + return; + } + var cls = genClassForVnode(vnode); + var transitionClass = el._transitionClasses; + if (isDef(transitionClass)) { + cls = concat(cls, stringifyClass(transitionClass)); + } + if (cls !== el._prevClass) { + el.setAttribute("class", cls); + el._prevClass = cls; + } + } + var klass = { + create: updateClass, + update: updateClass + }; + var validDivisionCharRE = /[\w).+\-_$\]]/; + function parseFilters(exp) { + var inSingle = false; + var inDouble = false; + var inTemplateString = false; + var inRegex = false; + var curly = 0; + var square = 0; + var paren = 0; + var lastFilterIndex = 0; + var c, prev, i, expression, filters; + for (i = 0; i < exp.length; i++) { + prev = c; + c = exp.charCodeAt(i); + if (inSingle) { + if (c === 39 && prev !== 92) { + inSingle = false; + } + } else if (inDouble) { + if (c === 34 && prev !== 92) { + inDouble = false; + } + } else if (inTemplateString) { + if (c === 96 && prev !== 92) { + inTemplateString = false; + } + } else if (inRegex) { + if (c === 47 && prev !== 92) { + inRegex = false; + } + } else if (c === 124 && exp.charCodeAt(i + 1) !== 124 && exp.charCodeAt(i - 1) !== 124 && !curly && !square && !paren) { + if (expression === void 0) { + lastFilterIndex = i + 1; + expression = exp.slice(0, i).trim(); + } else { + pushFilter(); + } + } else { + switch (c) { + case 34: + inDouble = true; + break; + case 39: + inSingle = true; + break; + case 96: + inTemplateString = true; + break; + case 40: + paren++; + break; + case 41: + paren--; + break; + case 91: + square++; + break; + case 93: + square--; + break; + case 123: + curly++; + break; + case 125: + curly--; + break; + } + if (c === 47) { + var j = i - 1; + var p = void 0; + for (; j >= 0; j--) { + p = exp.charAt(j); + if (p !== " ") { + break; + } + } + if (!p || !validDivisionCharRE.test(p)) { + inRegex = true; + } + } + } + } + if (expression === void 0) { + expression = exp.slice(0, i).trim(); + } else if (lastFilterIndex !== 0) { + pushFilter(); + } + function pushFilter() { + (filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim()); + lastFilterIndex = i + 1; + } + if (filters) { + for (i = 0; i < filters.length; i++) { + expression = wrapFilter(expression, filters[i]); + } + } + return expression; + } + function wrapFilter(exp, filter) { + var i = filter.indexOf("("); + if (i < 0) { + return '_f("' + filter + '")(' + exp + ")"; + } else { + var name = filter.slice(0, i); + var args = filter.slice(i + 1); + return '_f("' + name + '")(' + exp + (args !== ")" ? "," + args : args); + } + } + function baseWarn(msg, range2) { + console.error("[Vue compiler]: " + msg); + } + function pluckModuleFunction(modules2, key) { + return modules2 ? modules2.map(function(m) { + return m[key]; + }).filter(function(_) { + return _; + }) : []; + } + function addProp(el, name, value, range2, dynamic) { + (el.props || (el.props = [])).push(rangeSetItem({ name, value, dynamic }, range2)); + el.plain = false; + } + function addAttr(el, name, value, range2, dynamic) { + var attrs2 = dynamic ? el.dynamicAttrs || (el.dynamicAttrs = []) : el.attrs || (el.attrs = []); + attrs2.push(rangeSetItem({ name, value, dynamic }, range2)); + el.plain = false; + } + function addRawAttr(el, name, value, range2) { + el.attrsMap[name] = value; + el.attrsList.push(rangeSetItem({ name, value }, range2)); + } + function addDirective(el, name, rawName, value, arg, isDynamicArg, modifiers, range2) { + (el.directives || (el.directives = [])).push(rangeSetItem({ + name, + rawName, + value, + arg, + isDynamicArg, + modifiers + }, range2)); + el.plain = false; + } + function prependModifierMarker(symbol, name, dynamic) { + return dynamic ? "_p(" + name + ',"' + symbol + '")' : symbol + name; + } + function addHandler(el, name, value, modifiers, important, warn2, range2, dynamic) { + modifiers = modifiers || emptyObject; + if (warn2 && modifiers.prevent && modifiers.passive) { + warn2("passive and prevent can't be used together. Passive handler can't prevent default event.", range2); + } + if (modifiers.right) { + if (dynamic) { + name = "(" + name + ")==='click'?'contextmenu':(" + name + ")"; + } else if (name === "click") { + name = "contextmenu"; + delete modifiers.right; + } + } else if (modifiers.middle) { + if (dynamic) { + name = "(" + name + ")==='click'?'mouseup':(" + name + ")"; + } else if (name === "click") { + name = "mouseup"; + } + } + if (modifiers.capture) { + delete modifiers.capture; + name = prependModifierMarker("!", name, dynamic); + } + if (modifiers.once) { + delete modifiers.once; + name = prependModifierMarker("~", name, dynamic); + } + if (modifiers.passive) { + delete modifiers.passive; + name = prependModifierMarker("&", name, dynamic); + } + var events2; + if (modifiers.native) { + delete modifiers.native; + events2 = el.nativeEvents || (el.nativeEvents = {}); + } else { + events2 = el.events || (el.events = {}); + } + var newHandler = rangeSetItem({ value: value.trim(), dynamic }, range2); + if (modifiers !== emptyObject) { + newHandler.modifiers = modifiers; + } + var handlers = events2[name]; + if (Array.isArray(handlers)) { + important ? handlers.unshift(newHandler) : handlers.push(newHandler); + } else if (handlers) { + events2[name] = important ? [newHandler, handlers] : [handlers, newHandler]; + } else { + events2[name] = newHandler; + } + el.plain = false; + } + function getRawBindingAttr(el, name) { + return el.rawAttrsMap[":" + name] || el.rawAttrsMap["v-bind:" + name] || el.rawAttrsMap[name]; + } + function getBindingAttr(el, name, getStatic) { + var dynamicValue = getAndRemoveAttr(el, ":" + name) || getAndRemoveAttr(el, "v-bind:" + name); + if (dynamicValue != null) { + return parseFilters(dynamicValue); + } else if (getStatic !== false) { + var staticValue = getAndRemoveAttr(el, name); + if (staticValue != null) { + return JSON.stringify(staticValue); + } + } + } + function getAndRemoveAttr(el, name, removeFromMap) { + var val; + if ((val = el.attrsMap[name]) != null) { + var list = el.attrsList; + for (var i = 0, l = list.length; i < l; i++) { + if (list[i].name === name) { + list.splice(i, 1); + break; + } + } + } + if (removeFromMap) { + delete el.attrsMap[name]; + } + return val; + } + function getAndRemoveAttrByRegex(el, name) { + var list = el.attrsList; + for (var i = 0, l = list.length; i < l; i++) { + var attr = list[i]; + if (name.test(attr.name)) { + list.splice(i, 1); + return attr; + } + } + } + function rangeSetItem(item, range2) { + if (range2) { + if (range2.start != null) { + item.start = range2.start; + } + if (range2.end != null) { + item.end = range2.end; + } + } + return item; + } + function genComponentModel(el, value, modifiers) { + var ref2 = modifiers || {}; + var number3 = ref2.number; + var trim = ref2.trim; + var baseValueExpression = "$$v"; + var valueExpression = baseValueExpression; + if (trim) { + valueExpression = "(typeof " + baseValueExpression + " === 'string'? " + baseValueExpression + ".trim(): " + baseValueExpression + ")"; + } + if (number3) { + valueExpression = "_n(" + valueExpression + ")"; + } + var assignment = genAssignmentCode(value, valueExpression); + el.model = { + value: "(" + value + ")", + expression: JSON.stringify(value), + callback: "function (" + baseValueExpression + ") {" + assignment + "}" + }; + } + function genAssignmentCode(value, assignment) { + var res = parseModel(value); + if (res.key === null) { + return value + "=" + assignment; + } else { + return "$set(" + res.exp + ", " + res.key + ", " + assignment + ")"; + } + } + var len; + var str; + var chr; + var index$1; + var expressionPos; + var expressionEndPos; + function parseModel(val) { + val = val.trim(); + len = val.length; + if (val.indexOf("[") < 0 || val.lastIndexOf("]") < len - 1) { + index$1 = val.lastIndexOf("."); + if (index$1 > -1) { + return { + exp: val.slice(0, index$1), + key: '"' + val.slice(index$1 + 1) + '"' + }; + } else { + return { + exp: val, + key: null + }; + } + } + str = val; + index$1 = expressionPos = expressionEndPos = 0; + while (!eof()) { + chr = next(); + if (isStringStart(chr)) { + parseString(chr); + } else if (chr === 91) { + parseBracket(chr); + } + } + return { + exp: val.slice(0, expressionPos), + key: val.slice(expressionPos + 1, expressionEndPos) + }; + } + function next() { + return str.charCodeAt(++index$1); + } + function eof() { + return index$1 >= len; + } + function isStringStart(chr2) { + return chr2 === 34 || chr2 === 39; + } + function parseBracket(chr2) { + var inBracket = 1; + expressionPos = index$1; + while (!eof()) { + chr2 = next(); + if (isStringStart(chr2)) { + parseString(chr2); + continue; + } + if (chr2 === 91) { + inBracket++; + } + if (chr2 === 93) { + inBracket--; + } + if (inBracket === 0) { + expressionEndPos = index$1; + break; + } + } + } + function parseString(chr2) { + var stringQuote = chr2; + while (!eof()) { + chr2 = next(); + if (chr2 === stringQuote) { + break; + } + } + } + var warn$1; + var RANGE_TOKEN = "__r"; + var CHECKBOX_RADIO_TOKEN = "__c"; + function model(el, dir, _warn) { + warn$1 = _warn; + var value = dir.value; + var modifiers = dir.modifiers; + var tag = el.tag; + var type = el.attrsMap.type; + if (true) { + if (tag === "input" && type === "file") { + warn$1("<" + el.tag + ' v-model="' + value + '" type="file">:\nFile inputs are read only. Use a v-on:change listener instead.', el.rawAttrsMap["v-model"]); + } + } + if (el.component) { + genComponentModel(el, value, modifiers); + return false; + } else if (tag === "select") { + genSelect(el, value, modifiers); + } else if (tag === "input" && type === "checkbox") { + genCheckboxModel(el, value, modifiers); + } else if (tag === "input" && type === "radio") { + genRadioModel(el, value, modifiers); + } else if (tag === "input" || tag === "textarea") { + genDefaultModel(el, value, modifiers); + } else if (!config.isReservedTag(tag)) { + genComponentModel(el, value, modifiers); + return false; + } else if (true) { + warn$1("<" + el.tag + ' v-model="' + value + `">: v-model is not supported on this element type. If you are working with contenteditable, it's recommended to wrap a library dedicated for that purpose inside a custom component.`, el.rawAttrsMap["v-model"]); + } + return true; + } + function genCheckboxModel(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var valueBinding = getBindingAttr(el, "value") || "null"; + var trueValueBinding = getBindingAttr(el, "true-value") || "true"; + var falseValueBinding = getBindingAttr(el, "false-value") || "false"; + addProp(el, "checked", "Array.isArray(" + value + ")?_i(" + value + "," + valueBinding + ")>-1" + (trueValueBinding === "true" ? ":(" + value + ")" : ":_q(" + value + "," + trueValueBinding + ")")); + addHandler(el, "change", "var $$a=" + value + ",$$el=$event.target,$$c=$$el.checked?(" + trueValueBinding + "):(" + falseValueBinding + ");if(Array.isArray($$a)){var $$v=" + (number3 ? "_n(" + valueBinding + ")" : valueBinding) + ",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&(" + genAssignmentCode(value, "$$a.concat([$$v])") + ")}else{$$i>-1&&(" + genAssignmentCode(value, "$$a.slice(0,$$i).concat($$a.slice($$i+1))") + ")}}else{" + genAssignmentCode(value, "$$c") + "}", null, true); + } + function genRadioModel(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var valueBinding = getBindingAttr(el, "value") || "null"; + valueBinding = number3 ? "_n(" + valueBinding + ")" : valueBinding; + addProp(el, "checked", "_q(" + value + "," + valueBinding + ")"); + addHandler(el, "change", genAssignmentCode(value, valueBinding), null, true); + } + function genSelect(el, value, modifiers) { + var number3 = modifiers && modifiers.number; + var selectedVal = 'Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return ' + (number3 ? "_n(val)" : "val") + "})"; + var assignment = "$event.target.multiple ? $$selectedVal : $$selectedVal[0]"; + var code = "var $$selectedVal = " + selectedVal + ";"; + code = code + " " + genAssignmentCode(value, assignment); + addHandler(el, "change", code, null, true); + } + function genDefaultModel(el, value, modifiers) { + var type = el.attrsMap.type; + if (true) { + var value$1 = el.attrsMap["v-bind:value"] || el.attrsMap[":value"]; + var typeBinding = el.attrsMap["v-bind:type"] || el.attrsMap[":type"]; + if (value$1 && !typeBinding) { + var binding = el.attrsMap["v-bind:value"] ? "v-bind:value" : ":value"; + warn$1(binding + '="' + value$1 + '" conflicts with v-model on the same element because the latter already expands to a value binding internally', el.rawAttrsMap[binding]); + } + } + var ref2 = modifiers || {}; + var lazy = ref2.lazy; + var number3 = ref2.number; + var trim = ref2.trim; + var needCompositionGuard = !lazy && type !== "range"; + var event = lazy ? "change" : type === "range" ? RANGE_TOKEN : "input"; + var valueExpression = "$event.target.value"; + if (trim) { + valueExpression = "$event.target.value.trim()"; + } + if (number3) { + valueExpression = "_n(" + valueExpression + ")"; + } + var code = genAssignmentCode(value, valueExpression); + if (needCompositionGuard) { + code = "if($event.target.composing)return;" + code; + } + addProp(el, "value", "(" + value + ")"); + addHandler(el, event, code, null, true); + if (trim || number3) { + addHandler(el, "blur", "$forceUpdate()"); + } + } + function normalizeEvents(on2) { + if (isDef(on2[RANGE_TOKEN])) { + var event = isIE ? "change" : "input"; + on2[event] = [].concat(on2[RANGE_TOKEN], on2[event] || []); + delete on2[RANGE_TOKEN]; + } + if (isDef(on2[CHECKBOX_RADIO_TOKEN])) { + on2.change = [].concat(on2[CHECKBOX_RADIO_TOKEN], on2.change || []); + delete on2[CHECKBOX_RADIO_TOKEN]; + } + } + var target$1; + function createOnceHandler$1(event, handler, capture) { + var _target = target$1; + return function onceHandler() { + var res = handler.apply(null, arguments); + if (res !== null) { + remove$2(event, onceHandler, capture, _target); + } + }; + } + var useMicrotaskFix = isUsingMicroTask && !(isFF && Number(isFF[1]) <= 53); + function add$1(name, handler, capture, passive) { + if (useMicrotaskFix) { + var attachedTimestamp = currentFlushTimestamp; + var original = handler; + handler = original._wrapper = function(e) { + if (e.target === e.currentTarget || e.timeStamp >= attachedTimestamp || e.timeStamp <= 0 || e.target.ownerDocument !== document) { + return original.apply(this, arguments); + } + }; + } + target$1.addEventListener(name, handler, supportsPassive ? { capture, passive } : capture); + } + function remove$2(name, handler, capture, _target) { + (_target || target$1).removeEventListener(name, handler._wrapper || handler, capture); + } + function updateDOMListeners(oldVnode, vnode) { + if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { + return; + } + var on2 = vnode.data.on || {}; + var oldOn = oldVnode.data.on || {}; + target$1 = vnode.elm; + normalizeEvents(on2); + updateListeners(on2, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context); + target$1 = void 0; + } + var events = { + create: updateDOMListeners, + update: updateDOMListeners + }; + var svgContainer; + function updateDOMProps(oldVnode, vnode) { + if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) { + return; + } + var key, cur; + var elm = vnode.elm; + var oldProps = oldVnode.data.domProps || {}; + var props2 = vnode.data.domProps || {}; + if (isDef(props2.__ob__)) { + props2 = vnode.data.domProps = extend({}, props2); + } + for (key in oldProps) { + if (!(key in props2)) { + elm[key] = ""; + } + } + for (key in props2) { + cur = props2[key]; + if (key === "textContent" || key === "innerHTML") { + if (vnode.children) { + vnode.children.length = 0; + } + if (cur === oldProps[key]) { + continue; + } + if (elm.childNodes.length === 1) { + elm.removeChild(elm.childNodes[0]); + } + } + if (key === "value" && elm.tagName !== "PROGRESS") { + elm._value = cur; + var strCur = isUndef(cur) ? "" : String(cur); + if (shouldUpdateValue(elm, strCur)) { + elm.value = strCur; + } + } else if (key === "innerHTML" && isSVG(elm.tagName) && isUndef(elm.innerHTML)) { + svgContainer = svgContainer || document.createElement("div"); + svgContainer.innerHTML = "" + cur + ""; + var svg = svgContainer.firstChild; + while (elm.firstChild) { + elm.removeChild(elm.firstChild); + } + while (svg.firstChild) { + elm.appendChild(svg.firstChild); + } + } else if (cur !== oldProps[key]) { + try { + elm[key] = cur; + } catch (e) { + } + } + } + } + function shouldUpdateValue(elm, checkVal) { + return !elm.composing && (elm.tagName === "OPTION" || isNotInFocusAndDirty(elm, checkVal) || isDirtyWithModifiers(elm, checkVal)); + } + function isNotInFocusAndDirty(elm, checkVal) { + var notInFocus = true; + try { + notInFocus = document.activeElement !== elm; + } catch (e) { + } + return notInFocus && elm.value !== checkVal; + } + function isDirtyWithModifiers(elm, newVal) { + var value = elm.value; + var modifiers = elm._vModifiers; + if (isDef(modifiers)) { + if (modifiers.number) { + return toNumber(value) !== toNumber(newVal); + } + if (modifiers.trim) { + return value.trim() !== newVal.trim(); + } + } + return value !== newVal; + } + var domProps = { + create: updateDOMProps, + update: updateDOMProps + }; + var parseStyleText = cached(function(cssText) { + var res = {}; + var listDelimiter = /;(?![^(]*\))/g; + var propertyDelimiter = /:(.+)/; + cssText.split(listDelimiter).forEach(function(item) { + if (item) { + var tmp = item.split(propertyDelimiter); + tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()); + } + }); + return res; + }); + function normalizeStyleData(data) { + var style2 = normalizeStyleBinding(data.style); + return data.staticStyle ? extend(data.staticStyle, style2) : style2; + } + function normalizeStyleBinding(bindingStyle) { + if (Array.isArray(bindingStyle)) { + return toObject(bindingStyle); + } + if (typeof bindingStyle === "string") { + return parseStyleText(bindingStyle); + } + return bindingStyle; + } + function getStyle(vnode, checkChild) { + var res = {}; + var styleData; + if (checkChild) { + var childNode = vnode; + while (childNode.componentInstance) { + childNode = childNode.componentInstance._vnode; + if (childNode && childNode.data && (styleData = normalizeStyleData(childNode.data))) { + extend(res, styleData); + } + } + } + if (styleData = normalizeStyleData(vnode.data)) { + extend(res, styleData); + } + var parentNode2 = vnode; + while (parentNode2 = parentNode2.parent) { + if (parentNode2.data && (styleData = normalizeStyleData(parentNode2.data))) { + extend(res, styleData); + } + } + return res; + } + var cssVarRE = /^--/; + var importantRE = /\s*!important$/; + var setProp = function(el, name, val) { + if (cssVarRE.test(name)) { + el.style.setProperty(name, val); + } else if (importantRE.test(val)) { + el.style.setProperty(hyphenate(name), val.replace(importantRE, ""), "important"); + } else { + var normalizedName = normalize(name); + if (Array.isArray(val)) { + for (var i = 0, len2 = val.length; i < len2; i++) { + el.style[normalizedName] = val[i]; + } + } else { + el.style[normalizedName] = val; + } + } + }; + var vendorNames = ["Webkit", "Moz", "ms"]; + var emptyStyle; + var normalize = cached(function(prop) { + emptyStyle = emptyStyle || document.createElement("div").style; + prop = camelize(prop); + if (prop !== "filter" && prop in emptyStyle) { + return prop; + } + var capName = prop.charAt(0).toUpperCase() + prop.slice(1); + for (var i = 0; i < vendorNames.length; i++) { + var name = vendorNames[i] + capName; + if (name in emptyStyle) { + return name; + } + } + }); + function updateStyle(oldVnode, vnode) { + var data = vnode.data; + var oldData = oldVnode.data; + if (isUndef(data.staticStyle) && isUndef(data.style) && isUndef(oldData.staticStyle) && isUndef(oldData.style)) { + return; + } + var cur, name; + var el = vnode.elm; + var oldStaticStyle = oldData.staticStyle; + var oldStyleBinding = oldData.normalizedStyle || oldData.style || {}; + var oldStyle = oldStaticStyle || oldStyleBinding; + var style2 = normalizeStyleBinding(vnode.data.style) || {}; + vnode.data.normalizedStyle = isDef(style2.__ob__) ? extend({}, style2) : style2; + var newStyle = getStyle(vnode, true); + for (name in oldStyle) { + if (isUndef(newStyle[name])) { + setProp(el, name, ""); + } + } + for (name in newStyle) { + cur = newStyle[name]; + if (cur !== oldStyle[name]) { + setProp(el, name, cur == null ? "" : cur); + } + } + } + var style = { + create: updateStyle, + update: updateStyle + }; + var whitespaceRE = /\s+/; + function addClass(el, cls) { + if (!cls || !(cls = cls.trim())) { + return; + } + if (el.classList) { + if (cls.indexOf(" ") > -1) { + cls.split(whitespaceRE).forEach(function(c) { + return el.classList.add(c); + }); + } else { + el.classList.add(cls); + } + } else { + var cur = " " + (el.getAttribute("class") || "") + " "; + if (cur.indexOf(" " + cls + " ") < 0) { + el.setAttribute("class", (cur + cls).trim()); + } + } + } + function removeClass(el, cls) { + if (!cls || !(cls = cls.trim())) { + return; + } + if (el.classList) { + if (cls.indexOf(" ") > -1) { + cls.split(whitespaceRE).forEach(function(c) { + return el.classList.remove(c); + }); + } else { + el.classList.remove(cls); + } + if (!el.classList.length) { + el.removeAttribute("class"); + } + } else { + var cur = " " + (el.getAttribute("class") || "") + " "; + var tar = " " + cls + " "; + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, " "); + } + cur = cur.trim(); + if (cur) { + el.setAttribute("class", cur); + } else { + el.removeAttribute("class"); + } + } + } + function resolveTransition(def$$1) { + if (!def$$1) { + return; + } + if (typeof def$$1 === "object") { + var res = {}; + if (def$$1.css !== false) { + extend(res, autoCssTransition(def$$1.name || "v")); + } + extend(res, def$$1); + return res; + } else if (typeof def$$1 === "string") { + return autoCssTransition(def$$1); + } + } + var autoCssTransition = cached(function(name) { + return { + enterClass: name + "-enter", + enterToClass: name + "-enter-to", + enterActiveClass: name + "-enter-active", + leaveClass: name + "-leave", + leaveToClass: name + "-leave-to", + leaveActiveClass: name + "-leave-active" + }; + }); + var hasTransition = inBrowser && !isIE9; + var TRANSITION = "transition"; + var ANIMATION = "animation"; + var transitionProp = "transition"; + var transitionEndEvent = "transitionend"; + var animationProp = "animation"; + var animationEndEvent = "animationend"; + if (hasTransition) { + if (window.ontransitionend === void 0 && window.onwebkittransitionend !== void 0) { + transitionProp = "WebkitTransition"; + transitionEndEvent = "webkitTransitionEnd"; + } + if (window.onanimationend === void 0 && window.onwebkitanimationend !== void 0) { + animationProp = "WebkitAnimation"; + animationEndEvent = "webkitAnimationEnd"; + } + } + var raf = inBrowser ? window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : setTimeout : function(fn) { + return fn(); + }; + function nextFrame(fn) { + raf(function() { + raf(fn); + }); + } + function addTransitionClass(el, cls) { + var transitionClasses = el._transitionClasses || (el._transitionClasses = []); + if (transitionClasses.indexOf(cls) < 0) { + transitionClasses.push(cls); + addClass(el, cls); + } + } + function removeTransitionClass(el, cls) { + if (el._transitionClasses) { + remove(el._transitionClasses, cls); + } + removeClass(el, cls); + } + function whenTransitionEnds(el, expectedType, cb) { + var ref2 = getTransitionInfo(el, expectedType); + var type = ref2.type; + var timeout = ref2.timeout; + var propCount = ref2.propCount; + if (!type) { + return cb(); + } + var event = type === TRANSITION ? transitionEndEvent : animationEndEvent; + var ended = 0; + var end = function() { + el.removeEventListener(event, onEnd); + cb(); + }; + var onEnd = function(e) { + if (e.target === el) { + if (++ended >= propCount) { + end(); + } + } + }; + setTimeout(function() { + if (ended < propCount) { + end(); + } + }, timeout + 1); + el.addEventListener(event, onEnd); + } + var transformRE = /\b(transform|all)(,|$)/; + function getTransitionInfo(el, expectedType) { + var styles = window.getComputedStyle(el); + var transitionDelays = (styles[transitionProp + "Delay"] || "").split(", "); + var transitionDurations = (styles[transitionProp + "Duration"] || "").split(", "); + var transitionTimeout = getTimeout(transitionDelays, transitionDurations); + var animationDelays = (styles[animationProp + "Delay"] || "").split(", "); + var animationDurations = (styles[animationProp + "Duration"] || "").split(", "); + var animationTimeout = getTimeout(animationDelays, animationDurations); + var type; + var timeout = 0; + var propCount = 0; + if (expectedType === TRANSITION) { + if (transitionTimeout > 0) { + type = TRANSITION; + timeout = transitionTimeout; + propCount = transitionDurations.length; + } + } else if (expectedType === ANIMATION) { + if (animationTimeout > 0) { + type = ANIMATION; + timeout = animationTimeout; + propCount = animationDurations.length; + } + } else { + timeout = Math.max(transitionTimeout, animationTimeout); + type = timeout > 0 ? transitionTimeout > animationTimeout ? TRANSITION : ANIMATION : null; + propCount = type ? type === TRANSITION ? transitionDurations.length : animationDurations.length : 0; + } + var hasTransform = type === TRANSITION && transformRE.test(styles[transitionProp + "Property"]); + return { + type, + timeout, + propCount, + hasTransform + }; + } + function getTimeout(delays, durations) { + while (delays.length < durations.length) { + delays = delays.concat(delays); + } + return Math.max.apply(null, durations.map(function(d, i) { + return toMs(d) + toMs(delays[i]); + })); + } + function toMs(s) { + return Number(s.slice(0, -1).replace(",", ".")) * 1e3; + } + function enter(vnode, toggleDisplay) { + var el = vnode.elm; + if (isDef(el._leaveCb)) { + el._leaveCb.cancelled = true; + el._leaveCb(); + } + var data = resolveTransition(vnode.data.transition); + if (isUndef(data)) { + return; + } + if (isDef(el._enterCb) || el.nodeType !== 1) { + return; + } + var css = data.css; + var type = data.type; + var enterClass = data.enterClass; + var enterToClass = data.enterToClass; + var enterActiveClass = data.enterActiveClass; + var appearClass = data.appearClass; + var appearToClass = data.appearToClass; + var appearActiveClass = data.appearActiveClass; + var beforeEnter = data.beforeEnter; + var enter2 = data.enter; + var afterEnter = data.afterEnter; + var enterCancelled = data.enterCancelled; + var beforeAppear = data.beforeAppear; + var appear = data.appear; + var afterAppear = data.afterAppear; + var appearCancelled = data.appearCancelled; + var duration = data.duration; + var context = activeInstance; + var transitionNode = activeInstance.$vnode; + while (transitionNode && transitionNode.parent) { + context = transitionNode.context; + transitionNode = transitionNode.parent; + } + var isAppear = !context._isMounted || !vnode.isRootInsert; + if (isAppear && !appear && appear !== "") { + return; + } + var startClass = isAppear && appearClass ? appearClass : enterClass; + var activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass; + var toClass = isAppear && appearToClass ? appearToClass : enterToClass; + var beforeEnterHook = isAppear ? beforeAppear || beforeEnter : beforeEnter; + var enterHook = isAppear ? typeof appear === "function" ? appear : enter2 : enter2; + var afterEnterHook = isAppear ? afterAppear || afterEnter : afterEnter; + var enterCancelledHook = isAppear ? appearCancelled || enterCancelled : enterCancelled; + var explicitEnterDuration = toNumber(isObject(duration) ? duration.enter : duration); + if (explicitEnterDuration != null) { + checkDuration(explicitEnterDuration, "enter", vnode); + } + var expectsCSS = css !== false && !isIE9; + var userWantsControl = getHookArgumentsLength(enterHook); + var cb = el._enterCb = once(function() { + if (expectsCSS) { + removeTransitionClass(el, toClass); + removeTransitionClass(el, activeClass); + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, startClass); + } + enterCancelledHook && enterCancelledHook(el); + } else { + afterEnterHook && afterEnterHook(el); + } + el._enterCb = null; + }); + if (!vnode.data.show) { + mergeVNodeHook(vnode, "insert", function() { + var parent = el.parentNode; + var pendingNode = parent && parent._pending && parent._pending[vnode.key]; + if (pendingNode && pendingNode.tag === vnode.tag && pendingNode.elm._leaveCb) { + pendingNode.elm._leaveCb(); + } + enterHook && enterHook(el, cb); + }); + } + beforeEnterHook && beforeEnterHook(el); + if (expectsCSS) { + addTransitionClass(el, startClass); + addTransitionClass(el, activeClass); + nextFrame(function() { + removeTransitionClass(el, startClass); + if (!cb.cancelled) { + addTransitionClass(el, toClass); + if (!userWantsControl) { + if (isValidDuration(explicitEnterDuration)) { + setTimeout(cb, explicitEnterDuration); + } else { + whenTransitionEnds(el, type, cb); + } + } + } + }); + } + if (vnode.data.show) { + toggleDisplay && toggleDisplay(); + enterHook && enterHook(el, cb); + } + if (!expectsCSS && !userWantsControl) { + cb(); + } + } + function leave(vnode, rm) { + var el = vnode.elm; + if (isDef(el._enterCb)) { + el._enterCb.cancelled = true; + el._enterCb(); + } + var data = resolveTransition(vnode.data.transition); + if (isUndef(data) || el.nodeType !== 1) { + return rm(); + } + if (isDef(el._leaveCb)) { + return; + } + var css = data.css; + var type = data.type; + var leaveClass = data.leaveClass; + var leaveToClass = data.leaveToClass; + var leaveActiveClass = data.leaveActiveClass; + var beforeLeave = data.beforeLeave; + var leave2 = data.leave; + var afterLeave = data.afterLeave; + var leaveCancelled = data.leaveCancelled; + var delayLeave = data.delayLeave; + var duration = data.duration; + var expectsCSS = css !== false && !isIE9; + var userWantsControl = getHookArgumentsLength(leave2); + var explicitLeaveDuration = toNumber(isObject(duration) ? duration.leave : duration); + if (isDef(explicitLeaveDuration)) { + checkDuration(explicitLeaveDuration, "leave", vnode); + } + var cb = el._leaveCb = once(function() { + if (el.parentNode && el.parentNode._pending) { + el.parentNode._pending[vnode.key] = null; + } + if (expectsCSS) { + removeTransitionClass(el, leaveToClass); + removeTransitionClass(el, leaveActiveClass); + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, leaveClass); + } + leaveCancelled && leaveCancelled(el); + } else { + rm(); + afterLeave && afterLeave(el); + } + el._leaveCb = null; + }); + if (delayLeave) { + delayLeave(performLeave); + } else { + performLeave(); + } + function performLeave() { + if (cb.cancelled) { + return; + } + if (!vnode.data.show && el.parentNode) { + (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode; + } + beforeLeave && beforeLeave(el); + if (expectsCSS) { + addTransitionClass(el, leaveClass); + addTransitionClass(el, leaveActiveClass); + nextFrame(function() { + removeTransitionClass(el, leaveClass); + if (!cb.cancelled) { + addTransitionClass(el, leaveToClass); + if (!userWantsControl) { + if (isValidDuration(explicitLeaveDuration)) { + setTimeout(cb, explicitLeaveDuration); + } else { + whenTransitionEnds(el, type, cb); + } + } + } + }); + } + leave2 && leave2(el, cb); + if (!expectsCSS && !userWantsControl) { + cb(); + } + } + } + function checkDuration(val, name, vnode) { + if (typeof val !== "number") { + warn(" explicit " + name + " duration is not a valid number - got " + JSON.stringify(val) + ".", vnode.context); + } else if (isNaN(val)) { + warn(" explicit " + name + " duration is NaN - the duration expression might be incorrect.", vnode.context); + } + } + function isValidDuration(val) { + return typeof val === "number" && !isNaN(val); + } + function getHookArgumentsLength(fn) { + if (isUndef(fn)) { + return false; + } + var invokerFns = fn.fns; + if (isDef(invokerFns)) { + return getHookArgumentsLength(Array.isArray(invokerFns) ? invokerFns[0] : invokerFns); + } else { + return (fn._length || fn.length) > 1; + } + } + function _enter(_, vnode) { + if (vnode.data.show !== true) { + enter(vnode); + } + } + var transition = inBrowser ? { + create: _enter, + activate: _enter, + remove: function remove$$1(vnode, rm) { + if (vnode.data.show !== true) { + leave(vnode, rm); + } else { + rm(); + } + } + } : {}; + var platformModules = [ + attrs, + klass, + events, + domProps, + style, + transition + ]; + var modules = platformModules.concat(baseModules); + var patch = createPatchFunction({ nodeOps, modules }); + if (isIE9) { + document.addEventListener("selectionchange", function() { + var el = document.activeElement; + if (el && el.vmodel) { + trigger(el, "input"); + } + }); + } + var directive = { + inserted: function inserted(el, binding, vnode, oldVnode) { + if (vnode.tag === "select") { + if (oldVnode.elm && !oldVnode.elm._vOptions) { + mergeVNodeHook(vnode, "postpatch", function() { + directive.componentUpdated(el, binding, vnode); + }); + } else { + setSelected(el, binding, vnode.context); + } + el._vOptions = [].map.call(el.options, getValue); + } else if (vnode.tag === "textarea" || isTextInputType(el.type)) { + el._vModifiers = binding.modifiers; + if (!binding.modifiers.lazy) { + el.addEventListener("compositionstart", onCompositionStart); + el.addEventListener("compositionend", onCompositionEnd); + el.addEventListener("change", onCompositionEnd); + if (isIE9) { + el.vmodel = true; + } + } + } + }, + componentUpdated: function componentUpdated(el, binding, vnode) { + if (vnode.tag === "select") { + setSelected(el, binding, vnode.context); + var prevOptions = el._vOptions; + var curOptions = el._vOptions = [].map.call(el.options, getValue); + if (curOptions.some(function(o, i) { + return !looseEqual(o, prevOptions[i]); + })) { + var needReset = el.multiple ? binding.value.some(function(v) { + return hasNoMatchingOption(v, curOptions); + }) : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions); + if (needReset) { + trigger(el, "change"); + } + } + } + } + }; + function setSelected(el, binding, vm) { + actuallySetSelected(el, binding, vm); + if (isIE || isEdge) { + setTimeout(function() { + actuallySetSelected(el, binding, vm); + }, 0); + } + } + function actuallySetSelected(el, binding, vm) { + var value = binding.value; + var isMultiple = el.multiple; + if (isMultiple && !Array.isArray(value)) { + warn('