// many of these are also found in endorser-mobile utility.ts import axios, { AxiosResponse } from "axios"; import { Buffer } from "buffer"; import * as R from "ramda"; import { useClipboard } from "@vueuse/core"; import { DEFAULT_PUSH_SERVER } from "@/constants/app"; import { accountsDB, db } from "@/db/index"; import { Account } from "@/db/tables/accounts"; import { Contact } from "@/db/tables/contacts"; import { DEFAULT_PASSKEY_EXPIRATION_MINUTES, MASTER_SETTINGS_KEY, } from "@/db/tables/settings"; import { deriveAddress, generateSeed, newIdentifier } from "@/libs/crypto"; import { containsHiddenDid, GenericCredWrapper, GenericVerifiableCredential, OfferVerifiableCredential, } from "@/libs/endorserServer"; import * as serverUtil from "@/libs/endorserServer"; import { KeyMeta } from "@/libs/crypto/vc"; import { createPeerDid } from "@/libs/crypto/vc/didPeer"; import { registerCredential } from "@/libs/crypto/vc/passkeyDidPeer"; export interface GiverReceiverInputInfo { did?: string; name?: string; } export const PRIVACY_MESSAGE = "The data you send will be visible to the world -- except: your IDs and the IDs of anyone you tag will stay private, only visible to them and others you explicitly allow."; export const SHARED_PHOTO_BASE64_KEY = "shared-photo-base64"; /* eslint-disable prettier/prettier */ export const UNIT_SHORT: Record = { "BTC": "BTC", "BX": "BX", "ETH": "ETH", "HUR": "Hours", "USD": "US $", }; /* eslint-enable prettier/prettier */ /* eslint-disable prettier/prettier */ export const UNIT_LONG: Record = { "BTC": "Bitcoin", "BX": "Buxbe", "ETH": "Ethereum", "HUR": "hours", "USD": "dollars", }; /* eslint-enable prettier/prettier */ const UNIT_CODES: Record> = { BTC: { name: "Bitcoin", faIcon: "bitcoin-sign", }, HUR: { name: "hours", faIcon: "clock", }, USD: { name: "US Dollars", faIcon: "dollar", }, }; export function iconForUnitCode(unitCode: string) { return UNIT_CODES[unitCode]?.faIcon || "question"; } // from https://stackoverflow.com/a/175787/845494 // ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places // export function isNumeric(str: string): boolean { // This ignore commentary is because typescript complains when you pass a string to isNaN. // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return !isNaN(str) && !isNaN(parseFloat(str)); } export function numberOrZero(str: string): number { return isNumeric(str) ? +str : 0; } export const isGlobalUri = (uri: string) => { return uri && uri.match(new RegExp(/^[A-Za-z][A-Za-z0-9+.-]+:/)); }; export const isGiveAction = ( veriClaim: GenericCredWrapper, ) => { return veriClaim.claimType === "GiveAction"; }; export const nameForDid = ( activeDid: string, contacts: Array, did: string, ): string => { if (did === activeDid) { return "you"; } const contact = R.find((con) => con.did == did, contacts); return nameForContact(contact); }; export const nameForContact = ( contact?: Contact, capitalize?: boolean, ): string => { return ( (contact?.name as string) || (capitalize ? "This" : "this") + " unnamed user" ); }; export const doCopyTwoSecRedo = (text: string, fn: () => void) => { fn(); useClipboard() .copy(text) .then(() => setTimeout(fn, 2000)); }; /** * @returns true if the user can confirm the claim * @param veriClaim is expected to have fields: claim, claimType, and issuer */ export const isGiveRecordTheUserCanConfirm = ( isRegistered: boolean, veriClaim: GenericCredWrapper, activeDid: string, confirmerIdList: string[] = [], ) => { return ( isRegistered && isGiveAction(veriClaim) && !confirmerIdList.includes(activeDid) && veriClaim.issuer !== activeDid && !containsHiddenDid(veriClaim.claim) ); }; export async function blobToBase64(blob: Blob): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); // potential problem if it returns an ArrayBuffer? reader.onerror = reject; reader.readAsDataURL(blob); }); } export function base64ToBlob(base64DataUrl: string, sliceSize = 512) { // Extract the content type and the Base64 data const [metadata, base64] = base64DataUrl.split(","); const contentTypeMatch = metadata.match(/data:(.*?);base64/); const contentType = contentTypeMatch ? contentTypeMatch[1] : ""; const byteCharacters = atob(base64); const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { const slice = byteCharacters.slice(offset, offset + sliceSize); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } return new Blob(byteArrays, { type: contentType }); } /** * @returns the DID of the person who offered, or undefined if hidden * @param veriClaim is expected to have fields: claim and issuer */ export const offerGiverDid: ( arg0: GenericCredWrapper, ) => string | undefined = (veriClaim) => { let giver; if ( veriClaim.claim.offeredBy?.identifier && !serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string) ) { giver = veriClaim.claim.offeredBy.identifier; } else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) { giver = veriClaim.issuer; } return giver; }; /** * @returns true if the user can fulfill the offer * @param veriClaim is expected to have fields: claim, claimType, and issuer */ export const canFulfillOffer = ( veriClaim: GenericCredWrapper, ) => { return !!( veriClaim.claimType === "Offer" && offerGiverDid(veriClaim as GenericCredWrapper) ); }; // return object with paths and arrays of DIDs for any keys ending in "VisibleToDid" export function findAllVisibleToDids( // eslint-disable-next-line @typescript-eslint/no-explicit-any input: any, humanReadable = false, ): Record> { if (Array.isArray(input)) { const result: Record> = {}; for (let i = 0; i < input.length; i++) { const inside = findAllVisibleToDids(input[i], humanReadable); for (const key in inside) { const pathKey = humanReadable ? "#" + (i + 1) + " " + key : "[" + i + "]" + key; result[pathKey] = inside[key]; } } return result; } else if (input instanceof Object) { // regular map (non-array) object const result: Record> = {}; for (const key in input) { if (key.endsWith("VisibleToDids")) { const newKey = key.slice(0, -"VisibleToDids".length); const pathKey = humanReadable ? newKey : "." + newKey; result[pathKey] = input[key]; } else { const inside = findAllVisibleToDids(input[key], humanReadable); for (const insideKey in inside) { const pathKey = humanReadable ? key + "'s " + insideKey : "." + key + insideKey; result[pathKey] = inside[insideKey]; } } } return result; } else { return {}; } } /** * Test findAllVisibleToDids * pkgx +deno.land sh deno import * as R from 'ramda'; //import { findAllVisibleToDids } from './src/libs/util'; // doesn't work because other dependencies fail so gotta copy-and-paste function console.log(R.equals(findAllVisibleToDids(null), {})); console.log(R.equals(findAllVisibleToDids(9), {})); console.log(R.equals(findAllVisibleToDids([]), {})); console.log(R.equals(findAllVisibleToDids({}), {})); console.log(R.equals(findAllVisibleToDids({ issuer: "abc" }), {})); console.log(R.equals(findAllVisibleToDids({ issuerVisibleToDids: ["abc"] }), { ".issuer": ["abc"] })); console.log(R.equals(findAllVisibleToDids([{ issuerVisibleToDids: ["abc"] }]), { "[0].issuer": ["abc"] })); console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] } }]), { "[1].fluff.issuer": ["abc"] })); console.log(R.equals(findAllVisibleToDids(["xyz", { fluff: { issuerVisibleToDids: ["abc"] }, stuff: [ { did: "HIDDEN", agentDidVisibleToDids: ["def", "ghi"] } ] }]), { "[1].fluff.issuer": ["abc"], "[1].stuff[0].agentDid": ["def", "ghi"] })); * **/ export interface AccountKeyInfo extends Account, KeyMeta {} export const getAccount = async ( activeDid: string, ): Promise => { await accountsDB.open(); const account = (await accountsDB.accounts .where("did") .equals(activeDid) .first()) as Account; return account; }; /** * Generates a new identity, saves it to the database, and sets it as the active identity. * @return {Promise} with the DID of the new identity */ export const generateSaveAndActivateIdentity = async (): Promise => { const mnemonic = generateSeed(); // address is 0x... ETH address, without "did:eth:" const [address, privateHex, publicHex, derivationPath] = deriveAddress(mnemonic); const newId = newIdentifier(address, publicHex, privateHex, derivationPath); const identity = JSON.stringify(newId); await accountsDB.open(); await accountsDB.accounts.add({ dateCreated: new Date().toISOString(), derivationPath: derivationPath, did: newId.did, identity: identity, mnemonic: mnemonic, publicKeyHex: newId.keys[0].publicKeyHex, }); await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: newId.did, }); return newId.did; }; export const registerAndSavePasskey = async ( keyName: string, ): Promise => { const cred = await registerCredential(keyName); const publicKeyBytes = cred.publicKeyBytes; const did = createPeerDid(publicKeyBytes as Uint8Array); const passkeyCredIdHex = cred.credIdHex as string; const account = { dateCreated: new Date().toISOString(), did, passkeyCredIdHex, publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"), }; await accountsDB.open(); await accountsDB.accounts.add(account); return account; }; export const registerSaveAndActivatePasskey = async ( keyName: string, ): Promise => { const account = await registerAndSavePasskey(keyName); await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: account.did, }); return account; }; export const getPasskeyExpirationSeconds = async (): Promise => { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); const passkeyExpirationSeconds = (settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) * 60; return passkeyExpirationSeconds; }; export const sendTestThroughPushServer = async ( subscriptionJSON: PushSubscriptionJSON, skipFilter: boolean, ): Promise => { await db.open(); const settings = await db.settings.get(MASTER_SETTINGS_KEY); let pushUrl: string = DEFAULT_PUSH_SERVER as string; if (settings?.webPushServer) { pushUrl = settings.webPushServer; } // This is a special value that tells the service worker to send a direct notification to the device, skipping filters. // This is shared with the service worker and should be a constant. Look for the same name in additional-scripts.js // Use something other than "Daily Update" https://gitea.anomalistdesign.com/trent_larson/py-push-server/src/commit/3c0e196c11bc98060ec5934e99e7dbd591b5da4d/app.py#L213 const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION"; const newPayload = { // eslint-disable-next-line prettier/prettier message: `Test, where you will see this message ${ skipFilter ? "un" : "" }filtered.`, title: skipFilter ? DIRECT_PUSH_TITLE : "Your Web Push", ...subscriptionJSON, }; console.log("Sending a test web push message:", newPayload); const payloadStr = JSON.stringify(newPayload); const response = await axios.post( pushUrl + "/web-push/send-test", payloadStr, { headers: { "Content-Type": "application/json", }, }, ); console.log("Got response from web push server:", response); return response; };