You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							1086 lines
						
					
					
						
							33 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							1086 lines
						
					
					
						
							33 KiB
						
					
					
				
								// 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 { copyToClipboard } from "../services/ClipboardService";
							 | 
						|
								
							 | 
						|
								import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
							 | 
						|
								import { Account, AccountEncrypted } from "../db/tables/accounts";
							 | 
						|
								import { Contact } from "../db/tables/contacts";
							 | 
						|
								import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
							 | 
						|
								import {
							 | 
						|
								  arrayBufferToBase64,
							 | 
						|
								  base64ToArrayBuffer,
							 | 
						|
								  deriveAddress,
							 | 
						|
								  generateSeed,
							 | 
						|
								  newIdentifier,
							 | 
						|
								  simpleDecrypt,
							 | 
						|
								  simpleEncrypt,
							 | 
						|
								} from "../libs/crypto";
							 | 
						|
								import * as serverUtil from "../libs/endorserServer";
							 | 
						|
								import { containsHiddenDid } from "../libs/endorserServer";
							 | 
						|
								import {
							 | 
						|
								  GenericCredWrapper,
							 | 
						|
								  GenericVerifiableCredential,
							 | 
						|
								  KeyMetaWithPrivate,
							 | 
						|
								} from "../interfaces/common";
							 | 
						|
								import { GiveSummaryRecord } from "../interfaces/records";
							 | 
						|
								import { OfferClaim } from "../interfaces/claims";
							 | 
						|
								import { createPeerDid } from "../libs/crypto/vc/didPeer";
							 | 
						|
								import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
							 | 
						|
								import { logger } from "../utils/logger";
							 | 
						|
								import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
							 | 
						|
								import { IIdentifier } from "@veramo/core";
							 | 
						|
								import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
							 | 
						|
								import { UNNAMED_PERSON } from "@/constants/entities";
							 | 
						|
								
							 | 
						|
								// Consolidate this with src/utils/PlatformServiceMixin.mapQueryResultToValues
							 | 
						|
								function mapQueryResultToValues(
							 | 
						|
								  record: { columns: string[]; values: unknown[][] } | undefined,
							 | 
						|
								): Array<Record<string, unknown>> {
							 | 
						|
								  if (!record || !record.columns || !record.values) {
							 | 
						|
								    return [];
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return record.values.map((row) => {
							 | 
						|
								    const obj: Record<string, unknown> = {};
							 | 
						|
								    record.columns.forEach((column, index) => {
							 | 
						|
								      obj[column] = row[index];
							 | 
						|
								    });
							 | 
						|
								    return obj;
							 | 
						|
								  });
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Platform service access for database operations
							 | 
						|
								async function getPlatformService() {
							 | 
						|
								  return PlatformServiceFactory.getInstance();
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export interface GiverReceiverInputInfo {
							 | 
						|
								  did?: string; // only for people
							 | 
						|
								  name?: string;
							 | 
						|
								  image?: string;
							 | 
						|
								  handleId?: string; // only for projects
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								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,
							 | 
						|
								  { name: string; faIcon: string; decimals: number }
							 | 
						|
								> = {
							 | 
						|
								  BTC: {
							 | 
						|
								    name: "Bitcoin",
							 | 
						|
								    faIcon: "bitcoin-sign",
							 | 
						|
								    decimals: 4,
							 | 
						|
								  },
							 | 
						|
								  HUR: {
							 | 
						|
								    name: "hours",
							 | 
						|
								    faIcon: "clock",
							 | 
						|
								    decimals: 0,
							 | 
						|
								  },
							 | 
						|
								  USD: {
							 | 
						|
								    name: "US Dollars",
							 | 
						|
								    faIcon: "dollar",
							 | 
						|
								    decimals: 2,
							 | 
						|
								  },
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export function iconForUnitCode(unitCode: string) {
							 | 
						|
								  return UNIT_CODES[unitCode]?.faIcon || "question";
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export function formattedAmount(amount: number, unitCode: string) {
							 | 
						|
								  const unit = UNIT_CODES[unitCode];
							 | 
						|
								  const amountStr = amount.toFixed(unit?.decimals ?? 4);
							 | 
						|
								  const unitName = unit?.name || "?";
							 | 
						|
								  return amountStr + " " + unitName;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// 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;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * from https://tools.ietf.org/html/rfc3986#section-3
							 | 
						|
								 * also useful is https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Definition
							 | 
						|
								 **/
							 | 
						|
								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 interface OfferFulfillment {
							 | 
						|
								  offerHandleId: string;
							 | 
						|
								  offerType: string;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								interface FulfillmentItem {
							 | 
						|
								  "@type": string;
							 | 
						|
								  identifier?: string;
							 | 
						|
								  [key: string]: unknown;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Extract offer fulfillment information from the fulfills field
							 | 
						|
								 * Handles both array and single object cases
							 | 
						|
								 */
							 | 
						|
								export const extractOfferFulfillment = (
							 | 
						|
								  fulfills: FulfillmentItem | FulfillmentItem[] | null | undefined,
							 | 
						|
								): OfferFulfillment | null => {
							 | 
						|
								  if (!fulfills) {
							 | 
						|
								    return null;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Handle both array and single object cases
							 | 
						|
								  let offerFulfill = null;
							 | 
						|
								
							 | 
						|
								  if (Array.isArray(fulfills)) {
							 | 
						|
								    // Find the Offer in the fulfills array
							 | 
						|
								    offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
							 | 
						|
								  } else if (fulfills["@type"] === "Offer") {
							 | 
						|
								    // fulfills is a single Offer object
							 | 
						|
								    offerFulfill = fulfills;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  if (offerFulfill) {
							 | 
						|
								    return {
							 | 
						|
								      offerHandleId: offerFulfill.identifier || "",
							 | 
						|
								      offerType: offerFulfill["@type"],
							 | 
						|
								    };
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return null;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const shortDid = (did: string) => {
							 | 
						|
								  if (did.startsWith("did:peer:")) {
							 | 
						|
								    return (
							 | 
						|
								      did.substring(0, "did:peer:".length + 2) +
							 | 
						|
								      "..." +
							 | 
						|
								      did.substring("did:peer:".length + 18, "did:peer:".length + 25) +
							 | 
						|
								      "..."
							 | 
						|
								    );
							 | 
						|
								  } else if (did.startsWith("did:ethr:")) {
							 | 
						|
								    return did.substring(0, "did:ethr:".length + 9) + "...";
							 | 
						|
								  } else {
							 | 
						|
								    return did.substring(0, did.indexOf(":", 4) + 7) + "...";
							 | 
						|
								  }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								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_PERSON
							 | 
						|
								  );
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const doCopyTwoSecRedo = async (
							 | 
						|
								  text: string,
							 | 
						|
								  fn: () => void,
							 | 
						|
								): Promise<void> => {
							 | 
						|
								  fn();
							 | 
						|
								  try {
							 | 
						|
								    await copyToClipboard(text);
							 | 
						|
								    setTimeout(fn, 2000);
							 | 
						|
								  } catch (error) {
							 | 
						|
								    // Note: This utility function doesn't have access to notification system
							 | 
						|
								    // The calling component should handle error notifications
							 | 
						|
								    // Error is silently caught to avoid breaking the 2-second redo pattern
							 | 
						|
								  }
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								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 {
							 | 
						|
								    logger.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<OfferClaim>,
							 | 
						|
								): string | undefined {
							 | 
						|
								  const innerClaim = veriClaim.claim as OfferClaim;
							 | 
						|
								  let giver: string | undefined = undefined;
							 | 
						|
								
							 | 
						|
								  giver = innerClaim.offeredBy?.identifier;
							 | 
						|
								  if (giver && !serverUtil.isHiddenDid(giver)) {
							 | 
						|
								    return giver;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  giver = veriClaim.issuer;
							 | 
						|
								  if (giver && !serverUtil.isHiddenDid(giver)) {
							 | 
						|
								    return giver;
							 | 
						|
								  }
							 | 
						|
								  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>,
							 | 
						|
								  isRegistered: boolean,
							 | 
						|
								) => {
							 | 
						|
								  return (
							 | 
						|
								    isRegistered &&
							 | 
						|
								    veriClaim.claimType === "Offer" &&
							 | 
						|
								    !!offerGiverDid(veriClaim as GenericCredWrapper<OfferClaim>)
							 | 
						|
								  );
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								// 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 type AccountKeyInfo = Account & KeyMetaWithPrivate;
							 | 
						|
								
							 | 
						|
								export const retrieveAccountCount = async (): Promise<number> => {
							 | 
						|
								  let result = 0;
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const dbResult = await platformService.dbQuery(
							 | 
						|
								    `SELECT COUNT(*) FROM accounts`,
							 | 
						|
								  );
							 | 
						|
								  if (dbResult?.values?.[0]?.[0]) {
							 | 
						|
								    result = dbResult.values[0][0] as number;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  return result;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const retrieveAccountDids = async (): Promise<string[]> => {
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const dbAccounts = await platformService.dbQuery(`SELECT did FROM accounts`);
							 | 
						|
								  const allDids =
							 | 
						|
								    mapQueryResultToValues(dbAccounts)?.map((row) => row[0] as string) || [];
							 | 
						|
								  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.
							 | 
						|
								 *
							 | 
						|
								 * If you need the private key data, use retrieveFullyDecryptedAccount instead.
							 | 
						|
								 */
							 | 
						|
								export const retrieveAccountMetadata = async (
							 | 
						|
								  activeDid: string,
							 | 
						|
								): Promise<Account | undefined> => {
							 | 
						|
								  let result: Account | undefined = undefined;
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const dbAccount = await platformService.dbQuery(
							 | 
						|
								    `SELECT * FROM accounts WHERE did = ?`,
							 | 
						|
								    [activeDid],
							 | 
						|
								  );
							 | 
						|
								  const account = mapQueryResultToValues(dbAccount)[0] as Account;
							 | 
						|
								  if (account) {
							 | 
						|
								    // eslint-disable-next-line @typescript-eslint/no-unused-vars
							 | 
						|
								    const { identity, mnemonic, ...metadata } = account;
							 | 
						|
								    result = metadata;
							 | 
						|
								  } else {
							 | 
						|
								    result = undefined;
							 | 
						|
								  }
							 | 
						|
								  return result;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * This contains sensitive data. If possible, use retrieveAccountMetadata instead.
							 | 
						|
								 *
							 | 
						|
								 * @param activeDid
							 | 
						|
								 * @returns account info with private key data decrypted
							 | 
						|
								 */
							 | 
						|
								export const retrieveFullyDecryptedAccount = async (
							 | 
						|
								  activeDid: string,
							 | 
						|
								): Promise<Account | undefined> => {
							 | 
						|
								  let result: Account | undefined = undefined;
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const dbSecrets = await platformService.dbQuery(
							 | 
						|
								    `SELECT secretBase64 from secret`,
							 | 
						|
								  );
							 | 
						|
								  if (
							 | 
						|
								    !dbSecrets ||
							 | 
						|
								    dbSecrets.values.length === 0 ||
							 | 
						|
								    dbSecrets.values[0].length === 0
							 | 
						|
								  ) {
							 | 
						|
								    throw new Error(
							 | 
						|
								      "No secret found. We recommend you clear your data and start over.",
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								  const secretBase64 = dbSecrets.values[0][0] as string;
							 | 
						|
								  const secret = base64ToArrayBuffer(secretBase64);
							 | 
						|
								  const dbAccount = await platformService.dbQuery(
							 | 
						|
								    `SELECT * FROM accounts WHERE did = ?`,
							 | 
						|
								    [activeDid],
							 | 
						|
								  );
							 | 
						|
								  if (
							 | 
						|
								    !dbAccount ||
							 | 
						|
								    dbAccount.values.length === 0 ||
							 | 
						|
								    dbAccount.values[0].length === 0
							 | 
						|
								  ) {
							 | 
						|
								    throw new Error("Account not found.");
							 | 
						|
								  }
							 | 
						|
								  const fullAccountData = mapQueryResultToValues(
							 | 
						|
								    dbAccount,
							 | 
						|
								  )[0] as AccountEncrypted;
							 | 
						|
								  const identityEncr = base64ToArrayBuffer(fullAccountData.identityEncrBase64);
							 | 
						|
								  const mnemonicEncr = base64ToArrayBuffer(fullAccountData.mnemonicEncrBase64);
							 | 
						|
								  fullAccountData.identity = await simpleDecrypt(identityEncr, secret);
							 | 
						|
								  fullAccountData.mnemonic = await simpleDecrypt(mnemonicEncr, secret);
							 | 
						|
								  result = fullAccountData;
							 | 
						|
								
							 | 
						|
								  return result;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const retrieveAllAccountsMetadata = async (): Promise<
							 | 
						|
								  AccountEncrypted[]
							 | 
						|
								> => {
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
							 | 
						|
								  const accounts = mapQueryResultToValues(dbAccounts) as Account[];
							 | 
						|
								  const result = accounts.map((account) => {
							 | 
						|
								    return account as AccountEncrypted;
							 | 
						|
								  });
							 | 
						|
								  return result;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Saves a new identity to SQL database
							 | 
						|
								 */
							 | 
						|
								export async function saveNewIdentity(
							 | 
						|
								  identity: IIdentifier,
							 | 
						|
								  mnemonic: string,
							 | 
						|
								  derivationPath: string,
							 | 
						|
								): Promise<void> {
							 | 
						|
								  // add to the new sql db
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								
							 | 
						|
								  // Check if account already exists before attempting to save
							 | 
						|
								  const existingAccount = await platformService.dbQuery(
							 | 
						|
								    "SELECT did FROM accounts WHERE did = ?",
							 | 
						|
								    [identity.did],
							 | 
						|
								  );
							 | 
						|
								
							 | 
						|
								  if (existingAccount?.values?.length) {
							 | 
						|
								    throw new Error(
							 | 
						|
								      `Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const secrets = await platformService.dbQuery(
							 | 
						|
								    `SELECT secretBase64 FROM secret`,
							 | 
						|
								  );
							 | 
						|
								  if (!secrets?.values?.length || !secrets.values[0]?.length) {
							 | 
						|
								    throw new Error(
							 | 
						|
								      "No initial encryption supported. We recommend you clear your data and start over.",
							 | 
						|
								    );
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  const secretBase64 = secrets.values[0][0] as string;
							 | 
						|
								
							 | 
						|
								  const secret = base64ToArrayBuffer(secretBase64);
							 | 
						|
								  const identityStr = JSON.stringify(identity);
							 | 
						|
								  const encryptedIdentity = await simpleEncrypt(identityStr, secret);
							 | 
						|
								  const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
							 | 
						|
								  const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
							 | 
						|
								  const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
							 | 
						|
								
							 | 
						|
								  const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
							 | 
						|
								      VALUES (?, ?, ?, ?, ?, ?)`;
							 | 
						|
								  const params = [
							 | 
						|
								    new Date().toISOString(),
							 | 
						|
								    derivationPath,
							 | 
						|
								    identity.did,
							 | 
						|
								    encryptedIdentityBase64,
							 | 
						|
								    encryptedMnemonicBase64,
							 | 
						|
								    identity.keys[0].publicKeyHex,
							 | 
						|
								  ];
							 | 
						|
								  await platformService.dbExec(sql, params);
							 | 
						|
								
							 | 
						|
								  // Update active identity in the active_identity table instead of settings
							 | 
						|
								  await platformService.updateActiveDid(identity.did);
							 | 
						|
								
							 | 
						|
								  await platformService.insertNewDidIntoSettings(identity.did);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * 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);
							 | 
						|
								
							 | 
						|
								  await saveNewIdentity(newId, mnemonic, derivationPath);
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  await platformService.updateDidSpecificSettings(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"),
							 | 
						|
								  };
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const insertStatement = platformService.generateInsertStatement(
							 | 
						|
								    account,
							 | 
						|
								    "accounts",
							 | 
						|
								  );
							 | 
						|
								  await platformService.dbExec(insertStatement.sql, insertStatement.params);
							 | 
						|
								  return account;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const registerSaveAndActivatePasskey = async (
							 | 
						|
								  keyName: string,
							 | 
						|
								): Promise<Account> => {
							 | 
						|
								  const account = await registerAndSavePasskey(keyName);
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  // Update active identity in the active_identity table instead of settings
							 | 
						|
								  await platformService.updateActiveDid(account.did);
							 | 
						|
								  await platformService.updateDidSpecificSettings(account.did, {
							 | 
						|
								    isRegistered: false,
							 | 
						|
								  });
							 | 
						|
								  return account;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								export const getPasskeyExpirationSeconds = async (): Promise<number> => {
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const settings = await platformService.retrieveSettingsForActiveAccount();
							 | 
						|
								  return (
							 | 
						|
								    ((settings?.passkeyExpirationMinutes ??
							 | 
						|
								      DEFAULT_PASSKEY_EXPIRATION_MINUTES) as number) * 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 platformService = await getPlatformService();
							 | 
						|
								  const settings = await platformService.retrieveSettingsForActiveAccount();
							 | 
						|
								  let pushUrl: string = DEFAULT_PUSH_SERVER as string;
							 | 
						|
								  if (settings?.webPushServer) {
							 | 
						|
								    pushUrl = settings.webPushServer as string;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  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",
							 | 
						|
								  };
							 | 
						|
								  logger.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",
							 | 
						|
								      },
							 | 
						|
								    },
							 | 
						|
								  );
							 | 
						|
								
							 | 
						|
								  logger.log("Got response from web push server:", response);
							 | 
						|
								  return response;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Converts a Contact object to a CSV line string following the established format.
							 | 
						|
								 * The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
							 | 
						|
								 * where contactMethods is stored as a stringified JSON array.
							 | 
						|
								 *
							 | 
						|
								 * @param contact - The Contact object to convert
							 | 
						|
								 * @returns A CSV-formatted string representing the contact
							 | 
						|
								 * @throws {Error} If the contact object is missing required fields
							 | 
						|
								 */
							 | 
						|
								export const contactToCsvLine = (contact: Contact): string => {
							 | 
						|
								  if (!contact.did) {
							 | 
						|
								    throw new Error("Contact must have a did field");
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Escape fields that might contain commas or quotes
							 | 
						|
								  const escapeField = (field: string | boolean | undefined): string => {
							 | 
						|
								    if (field === undefined) return "";
							 | 
						|
								    const str = String(field);
							 | 
						|
								    if (str.includes(",") || str.includes('"')) {
							 | 
						|
								      return `"${str.replace(/"/g, '""')}"`;
							 | 
						|
								    }
							 | 
						|
								    return str;
							 | 
						|
								  };
							 | 
						|
								
							 | 
						|
								  // Handle contactMethods array by stringifying it
							 | 
						|
								  const contactMethodsStr = contact.contactMethods
							 | 
						|
								    ? escapeField(JSON.stringify(contact.contactMethods))
							 | 
						|
								    : "";
							 | 
						|
								
							 | 
						|
								  const fields = [
							 | 
						|
								    escapeField(contact.name),
							 | 
						|
								    escapeField(contact.did),
							 | 
						|
								    escapeField(contact.publicKeyBase64),
							 | 
						|
								    escapeField(contact.seesMe),
							 | 
						|
								    escapeField(contact.registered),
							 | 
						|
								    contactMethodsStr,
							 | 
						|
								  ];
							 | 
						|
								
							 | 
						|
								  return fields.join(",");
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Parses a CSV line into a Contact object. See contactToCsvLine for the format.
							 | 
						|
								 * @param lineRaw - The CSV line to parse
							 | 
						|
								 * @returns A Contact object
							 | 
						|
								 */
							 | 
						|
								export const csvLineToContact = (lineRaw: string): Contact => {
							 | 
						|
								  // Note that Endorser Mobile puts name first, then did, etc.
							 | 
						|
								  let line = lineRaw.trim();
							 | 
						|
								  let did, publicKeyInput, seesMe, registered;
							 | 
						|
								  let name;
							 | 
						|
								  let commaPos1 = -1;
							 | 
						|
								  if (line.startsWith('"')) {
							 | 
						|
								    let doubleDoubleQuotePos = line.lastIndexOf('""') + 2;
							 | 
						|
								    if (doubleDoubleQuotePos === -1) {
							 | 
						|
								      doubleDoubleQuotePos = 1;
							 | 
						|
								    }
							 | 
						|
								    const quote2Pos = line.indexOf('"', doubleDoubleQuotePos);
							 | 
						|
								    if (quote2Pos > -1) {
							 | 
						|
								      commaPos1 = line.indexOf(",", quote2Pos);
							 | 
						|
								      name = line.substring(1, quote2Pos).trim();
							 | 
						|
								      name = name.replace(/""/g, '"');
							 | 
						|
								    } else {
							 | 
						|
								      // something is weird with one " to start, so ignore it and start after "
							 | 
						|
								      line = line.substring(1);
							 | 
						|
								      commaPos1 = line.indexOf(",");
							 | 
						|
								      name = line.substring(0, commaPos1).trim();
							 | 
						|
								    }
							 | 
						|
								  } else {
							 | 
						|
								    commaPos1 = line.indexOf(",");
							 | 
						|
								    name = line.substring(0, commaPos1).trim();
							 | 
						|
								  }
							 | 
						|
								  if (commaPos1 > -1) {
							 | 
						|
								    did = line.substring(commaPos1 + 1).trim();
							 | 
						|
								    const commaPos2 = line.indexOf(",", commaPos1 + 1);
							 | 
						|
								    if (commaPos2 > -1) {
							 | 
						|
								      did = line.substring(commaPos1 + 1, commaPos2).trim();
							 | 
						|
								      publicKeyInput = line.substring(commaPos2 + 1).trim();
							 | 
						|
								      const commaPos3 = line.indexOf(",", commaPos2 + 1);
							 | 
						|
								      if (commaPos3 > -1) {
							 | 
						|
								        publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
							 | 
						|
								        seesMe = line.substring(commaPos3 + 1).trim() == "true";
							 | 
						|
								        const commaPos4 = line.indexOf(",", commaPos3 + 1);
							 | 
						|
								        if (commaPos4 > -1) {
							 | 
						|
								          seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
							 | 
						|
								          registered = line.substring(commaPos4 + 1).trim() == "true";
							 | 
						|
								        }
							 | 
						|
								      }
							 | 
						|
								    }
							 | 
						|
								  }
							 | 
						|
								  // help with potential mistakes while this sharing requires copy-and-paste
							 | 
						|
								  let publicKeyBase64 = publicKeyInput;
							 | 
						|
								  if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
							 | 
						|
								    // it must be all hex (compressed public key), so convert
							 | 
						|
								    publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
							 | 
						|
								  }
							 | 
						|
								  const newContact: Contact = {
							 | 
						|
								    did: did || "",
							 | 
						|
								    name,
							 | 
						|
								    publicKeyBase64,
							 | 
						|
								    seesMe,
							 | 
						|
								    registered,
							 | 
						|
								  };
							 | 
						|
								  return newContact;
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Interface for the JSON export format of database tables
							 | 
						|
								 */
							 | 
						|
								export interface TableExportData {
							 | 
						|
								  tableName: string;
							 | 
						|
								  rows: Array<Record<string, unknown>>;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Interface for the complete database export format
							 | 
						|
								 */
							 | 
						|
								export interface DatabaseExport {
							 | 
						|
								  data: {
							 | 
						|
								    data: Array<TableExportData>;
							 | 
						|
								  };
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Converts an array of contacts to the export JSON format.
							 | 
						|
								 * This format is used for data migration and backup purposes.
							 | 
						|
								 *
							 | 
						|
								 * @param contacts - Array of Contact objects to convert
							 | 
						|
								 * @returns DatabaseExport object in the standardized format
							 | 
						|
								 */
							 | 
						|
								export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
							 | 
						|
								  return {
							 | 
						|
								    data: {
							 | 
						|
								      data: [
							 | 
						|
								        {
							 | 
						|
								          tableName: "contacts",
							 | 
						|
								          rows: contacts,
							 | 
						|
								        },
							 | 
						|
								      ],
							 | 
						|
								    },
							 | 
						|
								  };
							 | 
						|
								};
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Imports an account from a mnemonic phrase
							 | 
						|
								 * @param mnemonic - The seed phrase to import from
							 | 
						|
								 * @param derivationPath - The derivation path to use (defaults to DEFAULT_ROOT_DERIVATION_PATH)
							 | 
						|
								 * @param shouldErase - Whether to erase existing accounts before importing
							 | 
						|
								 * @returns Promise that resolves when import is complete
							 | 
						|
								 * @throws Error if mnemonic is invalid or import fails
							 | 
						|
								 */
							 | 
						|
								export async function importFromMnemonic(
							 | 
						|
								  mnemonic: string,
							 | 
						|
								  derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
							 | 
						|
								  shouldErase: boolean = false,
							 | 
						|
								): Promise<void> {
							 | 
						|
								  const mne: string = mnemonic.trim().toLowerCase();
							 | 
						|
								
							 | 
						|
								  // Derive address and keys from mnemonic
							 | 
						|
								  const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath);
							 | 
						|
								
							 | 
						|
								  // Create new identifier
							 | 
						|
								  const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
							 | 
						|
								
							 | 
						|
								  // Handle erasures
							 | 
						|
								  if (shouldErase) {
							 | 
						|
								    const platformService = await getPlatformService();
							 | 
						|
								    await platformService.dbExec("DELETE FROM accounts");
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Save the new identity
							 | 
						|
								  await saveNewIdentity(newId, mne, derivationPath);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Checks if an account with the given DID already exists in the database
							 | 
						|
								 *
							 | 
						|
								 * @param did - The DID to check for duplicates
							 | 
						|
								 * @returns Promise<boolean> - True if account already exists, false otherwise
							 | 
						|
								 * @throws Error if database query fails
							 | 
						|
								 */
							 | 
						|
								export async function checkForDuplicateAccount(did: string): Promise<boolean>;
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Checks if an account with the given DID already exists in the database
							 | 
						|
								 *
							 | 
						|
								 * @param mnemonic - The mnemonic phrase to derive DID from
							 | 
						|
								 * @param derivationPath - The derivation path to use
							 | 
						|
								 * @returns Promise<boolean> - True if account already exists, false otherwise
							 | 
						|
								 * @throws Error if database query fails
							 | 
						|
								 */
							 | 
						|
								export async function checkForDuplicateAccount(
							 | 
						|
								  mnemonic: string,
							 | 
						|
								  derivationPath: string,
							 | 
						|
								): Promise<boolean>;
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Implementation of checkForDuplicateAccount with overloaded signatures
							 | 
						|
								 */
							 | 
						|
								export async function checkForDuplicateAccount(
							 | 
						|
								  didOrMnemonic: string,
							 | 
						|
								  derivationPath?: string,
							 | 
						|
								): Promise<boolean> {
							 | 
						|
								  let didToCheck: string;
							 | 
						|
								
							 | 
						|
								  if (derivationPath) {
							 | 
						|
								    // Derive the DID from mnemonic and derivation path
							 | 
						|
								    const [address, privateHex, publicHex] = deriveAddress(
							 | 
						|
								      didOrMnemonic.trim().toLowerCase(),
							 | 
						|
								      derivationPath,
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
							 | 
						|
								    didToCheck = newId.did;
							 | 
						|
								  } else {
							 | 
						|
								    // Use the provided DID directly
							 | 
						|
								    didToCheck = didOrMnemonic;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  // Check if an account with this DID already exists
							 | 
						|
								  const platformService = await getPlatformService();
							 | 
						|
								  const existingAccount = await platformService.dbQuery(
							 | 
						|
								    "SELECT did FROM accounts WHERE did = ?",
							 | 
						|
								    [didToCheck],
							 | 
						|
								  );
							 | 
						|
								
							 | 
						|
								  return (existingAccount?.values?.length ?? 0) > 0;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export class PromiseTracker<T> {
							 | 
						|
								  private _promise: Promise<T>;
							 | 
						|
								  private _resolved = false;
							 | 
						|
								  private _value: T | undefined;
							 | 
						|
								
							 | 
						|
								  constructor(promise: Promise<T>) {
							 | 
						|
								    this._promise = promise.then((value) => {
							 | 
						|
								      this._resolved = true;
							 | 
						|
								      this._value = value;
							 | 
						|
								      return value;
							 | 
						|
								    });
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get isResolved(): boolean {
							 | 
						|
								    return this._resolved;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get value(): T | undefined {
							 | 
						|
								    return this._value;
							 | 
						|
								  }
							 | 
						|
								
							 | 
						|
								  get promise(): Promise<T> {
							 | 
						|
								    return this._promise;
							 | 
						|
								  }
							 | 
						|
								}
							 | 
						|
								
							 |