// 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 , NotificationIface } from "../constants/app" ;
import {
accountsDBPromise ,
retrieveSettingsForActiveAccount ,
updateAccountSettings ,
updateDefaultSettings ,
} from "../db/index" ;
import { Account } from "../db/tables/accounts" ;
import { Contact } from "../db/tables/contacts" ;
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings" ;
import { deriveAddress , generateSeed , newIdentifier } from "../libs/crypto" ;
import * as serverUtil from "../libs/endorserServer" ;
import {
containsHiddenDid ,
GenericCredWrapper ,
GenericVerifiableCredential ,
GiveSummaryRecord ,
OfferVerifiableCredential ,
} 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 enum OnboardPage {
Home = "HOME" ,
Discover = "DISCOVER" ,
Create = "CREATE" ,
Contact = "CONTACT" ,
Account = "ACCOUNT" ,
}
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 < string , string > = {
"BTC" : "BTC" ,
"BX" : "BX" ,
"ETH" : "ETH" ,
"HUR" : "Hours" ,
"USD" : "US $" ,
} ;
/* eslint-enable prettier/prettier */
/* eslint-disable prettier/prettier */
export const UNIT_LONG : Record < string , string > = {
"BTC" : "Bitcoin" ,
"BX" : "Buxbe" ,
"ETH" : "Ethereum" ,
"HUR" : "hours" ,
"USD" : "dollars" ,
} ;
/* eslint-enable prettier/prettier */
const UNIT_CODES : Record < string , Record < string , string > > = {
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 isGiveClaimType = ( claimType? : string ) = > {
return claimType === "GiveAction" ;
} ;
export const isGiveAction = (
veriClaim : GenericCredWrapper < GenericVerifiableCredential > ,
) = > {
return isGiveClaimType ( veriClaim . claimType ) ;
} ;
export const nameForDid = (
activeDid : string ,
contacts : Array < Contact > ,
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 ) ) ;
} ;
export interface ConfirmerData {
confirmerIdList : string [ ] ;
confsVisibleToIdList : string [ ] ;
numConfsNotVisible : number ;
}
// // This is meant to be a second argument to JSON.stringify to avoid circular references.
// // Usage: JSON.stringify(error, getCircularReplacer())
// // Beware: we've seen this return "undefined" when there is actually a message, eg: DatabaseClosedError: Error DEXIE ENCRYPT ADDON: Encryption key has changed
// function getCircularReplacer() {
// const seen = new WeakSet();
// // eslint-disable-next-line @typescript-eslint/no-explicit-any
// return (obj: any, key: string, value: any): any => {
// if (typeof value === "object" && value !== null) {
// if (seen.has(value)) {
// return "[circular ref]";
// }
// seen.add(value);
// }
// return value;
// };
// }
/ * *
* @return only confirmers , excluding the issuer and hidden DIDs
* /
export async function retrieveConfirmerIdList (
apiServer : string ,
claimId : string ,
claimIssuerId : string ,
userDid : string ,
) : Promise < ConfirmerData | undefined > {
const confirmUrl =
apiServer +
"/api/report/issuersWhoClaimedOrConfirmed?claimId=" +
encodeURIComponent ( serverUtil . stripEndorserPrefix ( claimId ) ) ;
const confirmHeaders = await serverUtil . getHeaders ( userDid ) ;
const response = await axios . get ( confirmUrl , {
headers : confirmHeaders ,
} ) ;
if ( response . status === 200 ) {
const resultList1 = response . data . result || [ ] ;
//const publicUrls = resultList.publicUrls || [];
delete resultList1 . publicUrls ;
// exclude hidden DIDs
const resultList2 = R . reject ( serverUtil . isHiddenDid , resultList1 ) ;
// exclude the issuer
const resultList3 = R . reject (
( did : string ) = > did === claimIssuerId ,
resultList2 ,
) ;
const confirmerIdList = resultList3 ;
let numConfsNotVisible = resultList1 . length - resultList2 . length ;
if ( resultList3 . length === resultList2 . length ) {
// the issuer was not in the "visible" list so they must be hidden
// so subtract them from the non-visible confirmers count
numConfsNotVisible = numConfsNotVisible - 1 ;
}
const confsVisibleToIdList = response . data . result . resultVisibleToDids || [ ] ;
const result : ConfirmerData = {
confirmerIdList ,
confsVisibleToIdList ,
numConfsNotVisible ,
} ;
return result ;
} else {
console . error (
"Bad response status of" ,
response . status ,
"for confirmers:" ,
response ,
) ;
return undefined ;
}
}
/ * *
* @returns true if the user can confirm the claim
* @param veriClaim is expected to have fields : claim , claimType , and issuer
* /
export function isGiveRecordTheUserCanConfirm (
isRegistered : boolean ,
veriClaim : GenericCredWrapper < GenericVerifiableCredential > ,
activeDid : string ,
confirmerIdList : string [ ] = [ ] ,
) : boolean {
return (
isRegistered &&
isGiveAction ( veriClaim ) &&
! confirmerIdList . includes ( activeDid ) &&
veriClaim . issuer !== activeDid &&
! containsHiddenDid ( veriClaim . claim )
) ;
}
export function notifyWhyCannotConfirm (
notifyFun : ( notification : NotificationIface , timeout : number ) = > void ,
isRegistered : boolean ,
claimType : string | undefined ,
giveDetails : GiveSummaryRecord | undefined ,
activeDid : string ,
confirmerIdList : string [ ] = [ ] ,
) {
if ( ! isRegistered ) {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Not Registered" ,
text : "Someone needs to register you before you can confirm." ,
} ,
3000 ,
) ;
} else if ( ! isGiveClaimType ( claimType ) ) {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Not A Give" ,
text : "This is not a giving action to confirm." ,
} ,
3000 ,
) ;
} else if ( confirmerIdList . includes ( activeDid ) ) {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Already Confirmed" ,
text : "You already confirmed this claim." ,
} ,
3000 ,
) ;
} else if ( giveDetails ? . issuerDid == activeDid ) {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Cannot Confirm" ,
text : "You cannot confirm this because you issued this claim." ,
} ,
3000 ,
) ;
} else if ( serverUtil . containsHiddenDid ( giveDetails ? . fullClaim ) ) {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Cannot Confirm" ,
text : "You cannot confirm this because some people are hidden." ,
} ,
3000 ,
) ;
} else {
notifyFun (
{
group : "alert" ,
type : "info" ,
title : "Cannot Confirm" ,
text : "You cannot confirm this claim. There are no other details -- we can help more if you contact us and send us screenshots." ,
} ,
3000 ,
) ;
}
}
export async function blobToBase64 ( blob : Blob ) : Promise < string > {
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 function offerGiverDid (
veriClaim : GenericCredWrapper < OfferVerifiableCredential > ,
) : string | undefined {
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 < GenericVerifiableCredential > ,
) = > {
return (
veriClaim . claimType === "Offer" &&
! ! offerGiverDid ( veriClaim as GenericCredWrapper < OfferVerifiableCredential > )
) ;
} ;
// 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 < string , Array < string > > {
if ( Array . isArray ( input ) ) {
const result : Record < string , Array < string > > = { } ;
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 < string , Array < string > > = { } ;
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 retrieveAccountCount = async ( ) : Promise < number > = > {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
return await accountsDB . accounts . count ( ) ;
} ;
export const retrieveAccountDids = async ( ) : Promise < string [ ] > = > {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
const allAccounts = await accountsDB . accounts . toArray ( ) ;
const allDids = allAccounts . map ( ( acc ) = > acc . did ) ;
return allDids ;
} ;
// This is provided and recommended when the full key is not necessary so that
// future work could separate this info from the sensitive key material.
export const retrieveAccountMetadata = async (
activeDid : string ,
) : Promise < AccountKeyInfo | undefined > = > {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
const account = ( await accountsDB . accounts
. where ( "did" )
. equals ( activeDid )
. first ( ) ) as Account ;
if ( account ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity , mnemonic , . . . metadata } = account ;
return metadata ;
} else {
return undefined ;
}
} ;
export const retrieveAllAccountsMetadata = async ( ) : Promise < Account [ ] > = > {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
const array = await accountsDB . accounts . toArray ( ) ;
return array . map ( ( account ) = > {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity , mnemonic , . . . metadata } = account ;
return metadata ;
} ) ;
} ;
export const retrieveFullyDecryptedAccount = async (
activeDid : string ,
) : Promise < AccountKeyInfo | undefined > = > {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
const account = ( await accountsDB . accounts
. where ( "did" )
. equals ( activeDid )
. first ( ) ) as Account ;
return account ;
} ;
// let's try and eliminate this
export const retrieveAllFullyDecryptedAccounts = async ( ) : Promise <
Array < AccountKeyInfo >
> = > {
const accountsDB = await accountsDBPromise ;
const allAccounts = await accountsDB . accounts . toArray ( ) ;
return allAccounts ;
} ;
/ * *
* Generates a new identity , saves it to the database , and sets it as the active identity .
* @return { Promise < string > } with the DID of the new identity
* /
export const generateSaveAndActivateIdentity = async ( ) : Promise < string > = > {
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 ) ;
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
await accountsDB . accounts . add ( {
dateCreated : new Date ( ) . toISOString ( ) ,
derivationPath : derivationPath ,
did : newId.did ,
identity : identity ,
mnemonic : mnemonic ,
publicKeyHex : newId.keys [ 0 ] . publicKeyHex ,
} ) ;
await updateDefaultSettings ( { activeDid : newId.did } ) ;
//console.log("Updated default settings in util");
await updateAccountSettings ( newId . did , { isRegistered : false } ) ;
return newId . did ;
} ;
export const registerAndSavePasskey = async (
keyName : string ,
) : Promise < Account > = > {
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" ) ,
} ;
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise ;
await accountsDB . accounts . add ( account ) ;
return account ;
} ;
export const registerSaveAndActivatePasskey = async (
keyName : string ,
) : Promise < Account > = > {
const account = await registerAndSavePasskey ( keyName ) ;
await updateDefaultSettings ( { activeDid : account.did } ) ;
await updateAccountSettings ( account . did , { isRegistered : false } ) ;
return account ;
} ;
export const getPasskeyExpirationSeconds = async ( ) : Promise < number > = > {
const settings = await retrieveSettingsForActiveAccount ( ) ;
return (
( settings ? . passkeyExpirationMinutes ? ? DEFAULT_PASSKEY_EXPIRATION_MINUTES ) *
60
) ;
} ;
// These are shared with the service worker and should be a constant. Look for the same name in additional-scripts.js
export const DAILY_CHECK_TITLE = "DAILY_CHECK" ;
// This is a special value that tells the service worker to send a direct notification to the device, skipping filters.
export const DIRECT_PUSH_TITLE = "DIRECT_NOTIFICATION" ;
export const sendTestThroughPushServer = async (
subscriptionJSON : PushSubscriptionJSON ,
skipFilter : boolean ,
) : Promise < AxiosResponse > = > {
const settings = await retrieveSettingsForActiveAccount ( ) ;
let pushUrl : string = DEFAULT_PUSH_SERVER as string ;
if ( settings ? . webPushServer ) {
pushUrl = settings . webPushServer ;
}
const newPayload = {
. . . subscriptionJSON ,
// ... overridden with the following
// 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" ,
} ;
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 ;
} ;