/** * @module helpers.web * * helper functions specific to the web version. */ import { delay, extractErrorMsg, pretty, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { isPrimitive } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs"; import { validateIbGibIntrinsically } from "@ibgib/ts-gib/dist/V1/validate-helper.mjs"; import { MetaspaceService } from "@ibgib/core-gib/dist/witness/space/metaspace/metaspace-types.mjs"; import { RawExportIbGib_V1 } from "@ibgib/core-gib/dist/common/import-export/import-export-types.mjs"; import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs"; import { decompressIbGibGraphFromString, deserializeStringToGraph } from "@ibgib/core-gib/dist/common/other/other-helper.web.mjs"; import { getRawExportIbGib } from "@ibgib/core-gib/dist/common/import-export/import-export-helper.web.mjs"; import { IbGibSpaceAny } from "@ibgib/core-gib/dist/witness/space/space-base-v1.mjs"; import { GLOBAL_LOG_A_LOT, GEMINI_API_KEY_REGEXP, CONFIG_OPTION_GEMINI_API_KEY_LOCATION_HELP } from "./constants.mjs"; import { showFullscreenDialog } from "./ui/ui-helpers.mjs"; import { initializeStorage, storageCreateStoreIfNotExist, storageDBExists, storagePut } from "./storage/storage-helpers.web.mjs"; import { getIbGibGlobalThis_IbGib } from "./helpers.mjs"; /** * used in verbose logging */ const logalot = GLOBAL_LOG_A_LOT; /** * converts string to Uint8Array. * @param str to convert * @returns array */ export function stringToUint8Array(str: string): Uint8Array { const encoder = new TextEncoder(); return encoder.encode(str); } /** * converts Uint8Array to string. * @param array to convert * @returns string */ export function uint8ArrayToString(array: Uint8Array) { const decoder = new TextDecoder(); return decoder.decode(array); } /** * I'm pulling out node from @ibgib/ibgib rcli code. This interface is just to * aid in that transition. */ export interface PathUtilsHelper { isAbsolute(path: string): boolean; isRelative(path: string): boolean; // for now I'm taking out resolve since it requires being stateful and isn't // used AFAICT in the code I'm porting over // resolve(path: string): string; join(...paths: string[]): string; dirname(path: string): string; basename(path: string, ext?: string): string; } /** * Helper class that exposes pathUtils functions. Note that this is relative to * an individual space. */ export class PathUtilsHelper implements PathUtilsHelper { /** class-level log context */ lc: string = `[${PathUtilsHelper.name}]`; // /** // * this path utils helper is per space, so this is the id of that space. // */ // spaceId: SpaceId; /** * */ // constructor({ spaceId, initialCwd, }: { spaceId: SpaceId, initialCwd: string }) { // const lc = `${this.lc}[ctor]`; // this.spaceId = spaceId; // let ibGibGlobalThis = getIbGibGlobalThis(); // if (!ibGibGlobalThis.spaceShim[spaceId]) { // console.log(`${lc} initializing IbGibGlobalThis data`); // // initialize // ibGibGlobalThis.spaceShim[spaceId] = { // cwd: initialCwd, // initialCwd, // }; // } else { // if (logalot) { console.log(`${lc} ibgib global this already initialized (I: e48b0cb4075e536d150919e87ddc1f24)`); } // } // } /** * */ constructor() { } isAbsolute(path: string): boolean { if (!path) { throw new Error(`(UNEXPECTED) path falsy? (E: dcb92e11770ba06205c0afe3e6560b24)`); } return path.startsWith('/'); } isRelative(path: string): boolean { return !this.isAbsolute(path); } // resolve(path: string): string { // const lc = `${this.lc}[${this.resolve.name}]`; // if (!path) { throw new Error(`(UNEXPECTED) path falsy? (E: 76161b12dbd446a0c2a9e18f4847fc24)`); } // // for our implementation, we will throw if there is a relative path // // specifier (. or ..) in the middle of the path. the proper // // implementation probably resolves these concretely. // if (this.isAbsolute(path)) { // if (path.includes('/../') || path.endsWith('/..')) { throw new Error(`path is absolute but contains "/../" in it or ends with "../"? (E: 16145bf37d874abc886111c32c035124)`); } // return path; // } // // we have a relative path. get the cwd from global for this particular space. If this space has no // let ibGibGlobalThis = getIbGibGlobalThis(); // let info = ibGibGlobalThis.spaceShim[this.spaceId]; // if (!info) { throw new Error(`(UNEXPECTED) no info for spaceId in ibgibglobalthis? (E: c063f34ed784e70ccd19a6b2026eb924)`); } // const { cwd } = info; // if (!cwd) { throw new Error(`(UNEXPECTED) cwd falsy? (E: 91cbd7418a51d9d152ae53d4f87b3a24)`); } // if (path.startsWith(cwd)) { // console.log(`${lc} path already resolved (starts with cwd). path: ${path} (I: 1d99a4bad47398174fe127b85088fb24)`); // } // if (cwd.endsWith('/')) { // // cwd does end with slash, so slice off initial if needed // return cwd + (path.startsWith('/') ? path.slice(1) : path); // } else { // // cwd does NOT end with slash, so add if needed // return cwd + (path.startsWith('/') ? path : `/${path}`); // } // } join(...paths: string[]): string { const lc = `${this.lc}[${this.join.name}]`; if (paths.length === 0) { console.log(`${lc} paths.length === 0. returning '.' per node convention. (I: 153da80765ff6160675fae895ebac225)`); return '.'; /* <<<< returns early */ } if (paths.length === 1 && paths[0] === '..') { console.log(`${lc} paths.length === 1 && paths[0] === '..'. returning '..' per node convention. (I: 9af242b7615430a8c6899b1a332e9925)`); return '..'; /* <<<< returns early */ } let joined = ''; for (let i = 0; i < paths.length; i++) { let path = paths[i]; if (path === '..') { joined = joined.split('/').slice(0, -1).join('/'); } else { if (path.startsWith('/')) { path = path.slice(1); } if (path.endsWith('/')) { path = path.slice(0, -1); } joined += (i > 0 ? '/' : '') + path; } } return joined; } dirname(path: string): string { return path.split('/').slice(0, -1).join('/'); } basename(path: string, ext?: string): string { return path.split('/').pop() ?? ''; } } export async function copyToClipboard({ data, logalot, }: { data: { title?: string, text?: string, url?: string }, logalot?: boolean, }): Promise { const lc = `[${copyToClipboard.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 37d6a4e3c3ee9fa46a0b1cd5c8a5cf24)`); } let { title, text, url } = data; const textToCopy = [ `${data.title || ''}`, `${data.text || ''}`, `${data.url || ''}`, ].join('\n').trim(); await navigator.clipboard.writeText(textToCopy); console.log('Content copied to clipboard!'); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * helper to trigger browser's share functionality (or fallback). */ export async function shareContent({ data, logalot, }: { data: { title?: string, text?: string, url?: string }, logalot?: boolean, }): Promise { const lc = `[${shareContent.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: bfc7e484eba91f9126c3f5fc381e6424)`); } if (navigator.share) { await navigator.share(data); console.log('Content shared successfully!'); } else { console.warn('Web Share API not supported. Copying to clipboard instead.'); alert(`Share info copied to clipboard!`); await copyToClipboard({ data, logalot }); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * useful for knowing if we are executing in our website or within an iframe. * * ## driving use case * * When we are loading an embedded iframe for link-gib, it is trying to trigger * the router which is no bueno. * * This is causing a problem though when testing inside an iframe in Project * IDX...I guess I'll have to whitelist embedded sites like link-gib * * ## usage * * Put something like this in the html in : * ```html * * ``` * Then create constants like this (I put them in a constants file): * * ```typescript * export const HTML_META_APP_ID_NAME = "ibgib-app-id-yo"; * export const HTML_META_APP_ID_CONTENT = "0f73bca49d2856a446044768fbab8d25"; * ``` * * Then you can call this like this: * * ```typescript * const isInWebAppProper = isExecutingInBlankGibWebAppProper({ * metaName_appId: HTML_META_APP_ID_NAME, * metaContent_appId: HTML_META_APP_ID_CONTENT * }); * ``` * * @returns true if we are executing in an iframe, false otherwise. */ export function isExecutingInBlankGibWebAppProper({ metaName_appId, metaContent_appId }: { /** * this runs code: * * `const metaElement = document.querySelector(`meta[name=${metaName_appId}]`) as (HTMLMetaElement | null);` * * in the index, you should tag the meta tag with attribute of this. */ metaName_appId: string, /** * this is an appId on the `metaElement.content` */ metaContent_appId: string, }): boolean { const metaElement = document.querySelector(`meta[name=${metaName_appId}]`) as (HTMLMetaElement | null); return !!metaElement && metaElement.content === metaContent_appId; } /** * Low-level plumbing for prompting the user for some text. * * ## notes * * * if an empty string is entered, confirm is automatic/always happens. * * if confirm is true, will confirm on any entry * * can just hit ENTER to confirm anything * * this should be parameterized * * @returns the text the user entered. */ export async function promptForText({ msg, title, confirm, noNewLine, rl, defaultValue, dontCloseRl, cancelable, }: { msg: string, title?: string, confirm?: boolean, noNewLine?: boolean, /** * ignored in web version. used in node version. this is after all, a hack * port */ rl?: any, defaultValue?: string, /** * ignored in web version. used in node version. this is after all, a hack * port */ dontCloseRl?: boolean, /** * if true, cancel will show and an empty string will be returned */ cancelable?: boolean, }): Promise { const lc = `[${promptForText.name}]`; try { if (!msg) { throw new Error(`msg required (E: 7f4d67bca4cf98ea95fa110aabc98924)`); } let result: string | undefined; let attempts = 0; let maxAttempts = 5; do { attempts++; if (attempts >= maxAttempts) { throw new Error(`max attempts reached (E: 1ef20ae955622d4b39479f3da91a1d25)`); } await delay(50); result = await showFullscreenDialog({ title: title || 'Prompt for Info', msg, prompt: true, showInput: true, isPassword: false, defaultValue, }); } while (result === undefined && !cancelable); return result ?? ''; // let userText: string | undefined = undefined; // try { // do { // title = title ? `[${title}] ` : ''; // let promptMessage = `${title}${msg}${noNewLine ? ' ' : '\n'}`; // let prompt = window.prompt; // // Handle default value (Note: `prompt` can't pre-fill like readline.write) // if (defaultValue) { // if (defaultValue.includes('\n')) { // if (logalot) { console.log(`defaultValue includes newline. (I: 147d4ea942995d0f92a19c3534c18a24)`); } // // We can't pre-fill with newline in prompt, so we'll just inform if needed // } // promptMessage += ` (Default: ${defaultValue})`; // } // let userText1 = prompt(promptMessage); // // Check for cancellation (prompt returns null on cancel) // if (userText1 === null) { // // return null; // User cancelled // throw new Error(`user cancelled (E: bf3a221626f8cd14b2a945bfe661cb24)`); // } // if (userText1 === "") { // let userText2 = prompt(`empty string entered. confirm:${noNewLine ? ' ' : '\n'}`); // if (userText2 === userText1) { // userText = userText1; // break; // } else { // console.log(`confirm failed. please try again.`); // } // } else if (confirm) { // let userText2 = prompt(`confirm by either hitting ENTER (empty string) or retype input:${noNewLine ? ' ' : '\n'}`); // if (userText2 === '' || userText2 === userText1) { // userText = userText1; // } else { // console.log(`confirm failed. please try again.`); // } // } else { // userText = userText1; // } // } while (userText === undefined); // return userText; // } catch (error) { // throw error; // } } catch (error) { debugger; // in error block promptForText console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } import { evaluatePasskeyPrf, isPasskeyPrfSupported } from "./identity/passkey-helper.mjs"; import { KeystonePasskeyEntry } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; export interface AuthoritySecretResult { secret: string; poolId: string; } /** * Prompts the user for a secret, supporting both master password and passkey/hardware authenticators. */ export async function promptForSecret({ msg, confirm, passkeys, }: { msg?: string; confirm: boolean; passkeys?: Record; }): Promise { const lc = `[${promptForSecret.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 795a4ce348ddb9a5119770f4c572ac24)`); } let result: any = undefined; let firstTry = true; let attempts = 0; let maxAttempts = 5; const prfSupported = await isPasskeyPrfSupported().catch(() => false); const onPasskeyAuth = (prfSupported && passkeys && Object.keys(passkeys).length > 0) ? async (passkeyEntry?: any) => { if (!passkeyEntry) return undefined; const { passkeySecret } = await evaluatePasskeyPrf({ credentialId: passkeyEntry.credentialId, salt: passkeyEntry.salt, transports: passkeyEntry.transports, userVerification: 'preferred' }); return passkeySecret; } : undefined; do { attempts++; if (attempts >= maxAttempts) { throw new Error(`max attempts reached (E: d4166b4bf37aff0c6d44567792e9ad25)`); } await delay(50); result = await showFullscreenDialog({ title: firstTry ? 'Shh...' : 'Secrets didn\'t match, try again...', msg: msg || 'Enter Your Secret...', prompt: true, showInput: true, isPassword: true, defaultValue: undefined, passkeys, onPasskeyAuth, }); if (result === undefined) { throw new Error(`user cancelled (E: 91519d04378e84106f3df8eb36d99725)`); } if (typeof result === 'object' && result?.secret !== undefined) { result = result.secret; } if (confirm) { await delay(250); let result2: any = await showFullscreenDialog({ title: 'Shh...', msg: 'Confirm...', prompt: true, showInput: true, isPassword: true, defaultValue: undefined, }); if (result2 === undefined) { result = undefined; } if (typeof result2 === 'object' && result2?.secret !== undefined) { result2 = result2.secret; } if (result !== result2) { result = undefined; firstTry = false; await delay(250); } } } while (result === undefined); return result; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * Unified authority prompt that inspects the candidate pools on a keystone and prompts the user * to authorize via either Master Password or a specific Passkey. * Returns both the verified secret and the exact signingPoolId to use for keystone operations. */ export async function promptForAuthoritySecret({ keystone, aggregatedDetails, title, msg, verb = 'manage', }: { keystone: any; aggregatedDetails?: any; title?: string; msg?: string; verb?: string; }): Promise { const lc = `[${promptForAuthoritySecret.name}]`; try { const passkeysMap = aggregatedDetails?.passkeys || (keystone.data as any)?.frameDetails?.passkeys || (keystone.data as any)?.passkeys || {}; const prfSupported = await isPasskeyPrfSupported().catch(() => false); const onPasskeyAuth = (prfSupported && passkeysMap && Object.keys(passkeysMap).length > 0) ? async (passkeyEntry?: any) => { if (!passkeyEntry) return undefined; const { passkeySecret } = await evaluatePasskeyPrf({ credentialId: passkeyEntry.credentialId, salt: passkeyEntry.salt, transports: passkeyEntry.transports, userVerification: 'preferred' }); return passkeySecret; } : undefined; const candidatePools = (keystone.data?.challengePools || []).filter((p: any) => p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config?.allowedVerbs && verb && p.config.allowedVerbs.includes(verb)) ); const hasPasswordPool = candidatePools.some((p: any) => p.id === 'manage'); const dialogResult: any = await showFullscreenDialog({ title: title || 'Authorize Action', msg: msg || 'Authorize this action with your master password or passkey:', prompt: true, showInput: hasPasswordPool, isPassword: true, defaultValue: undefined, passkeys: passkeysMap, onPasskeyAuth, }); if (!dialogResult) { throw new Error("Action cancelled by user."); } if (typeof dialogResult === 'object' && dialogResult.secret !== undefined) { return { secret: dialogResult.secret, poolId: dialogResult.poolId || 'manage' }; } return { secret: String(dialogResult), poolId: 'manage' }; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * prompt specifically looking to confirm something. if "cancelled", returns * false. */ export async function promptForConfirm({ msg, yesLabel, noLabel, }: { /** * if provided, this is the message displayed when confirming. */ msg?: string, yesLabel?: string, noLabel?: string, }): Promise { const lc = `[${promptForConfirm.name}]`; try { await delay(50); let result = await showFullscreenDialog({ title: 'Confirm?', msg: msg ?? 'Confirm?', okButtonTitle: yesLabel || undefined, cancelButtonTitle: noLabel || undefined, prompt: true, }); return !!result; // msg ||= 'confirm?'; // yesLabel ||= '(y)es'; // noLabel ||= '(n)o'; // try { // const fnAnswerMatchesLabel = (answer: string, label: string) => { // if (answer === label.replace(/[\(\)]/g, '').toLowerCase()) { // // matched full label // return true; /* <<<< returns early */ // } else { // // check for short answers // const regexCaptureCharsInsideParens = /\((\w+)\)/; // const shortLabel = label.match(regexCaptureCharsInsideParens); // if ((shortLabel ?? []).length >= 2) { // if (answer === shortLabel![1].toLowerCase()) { // // matched short label // return true; /* <<<< returns early */ // } // } // } // // if got this far, nothing positive matched // return false; // }; // const prompt = window.prompt; // // try up to three times. if match found, either way, return early. // const maxTries = 3; // for (let i = 0; i < maxTries; i++) { // let answer = prompt(`${msg} [${yesLabel}, ${noLabel}]\n`); // if (answer) { // answer = answer.toLowerCase(); // const matchesYesLabel = fnAnswerMatchesLabel(answer, yesLabel); // if (matchesYesLabel) { return true; /* <<<< returns early */ } // const matchesNoLabel = fnAnswerMatchesLabel(answer, noLabel); // if (matchesNoLabel) { return false; /* <<<< returns early */ } // } else { // `no answer provided. defaulting to no...`; // console.log('huh?'); // } // } // // max tries reached // throw new Error(`could not confirm (E: ebbd2f642bde103c41d0102cf7e38d24)`); // } catch (error) { // throw error; // } } catch (error) { console.error(`${lc} ${error.message}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } export async function alertUser({ msg, title, skipHitEnterToContinue, }: { msg: string, title?: string, /** * ignored in web version atow (12/2024) */ skipHitEnterToContinue?: boolean, }): Promise { const lc = `[${alertUser.name}]`; try { if (!msg) { throw new Error(`msg required (E: ffcf7dd00324df7cea2792223d229524)`); } try { title ??= ''; await delay(50); await showFullscreenDialog({ title, msg, }); } catch (error) { throw error; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } export function getUserPreferredColorScheme(): 'light' | 'dark' { const lc = `[${getUserPreferredColorScheme.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 196286d17b656bce5ce16f358f386525)`); } const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; return prefersDark ? 'dark' : 'light'; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * helper function to get a query param in the current url. * * # ONLY IN BROWSER * * not for use in node.js * * @returns raw */ export function getQueryParam({ paramName, parseJSON, logalot, }: { paramName: string, parseJSON?: boolean, logalot?: boolean, }): TValue | undefined { const lc = `[${getQueryParam.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: f1a61ecdbb52cff1b958d1ceaea85f24)`); } const url = new URL(window.location.href); const rawValue = url.searchParams.get(paramName); if (rawValue) { const decodedValue = decodeURIComponent(rawValue); if (parseJSON) { try { return JSON.parse(decodedValue) as TValue; } catch (parseError) { console.error(`${lc} Error parsing JSON from query parameter ${paramName}: ${parseError} (E: fa8128dbe8ed41fc9fdf57a8146d88de)`); throw parseError; } } else { return decodedValue as TValue; } } else { return undefined; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * helper function to set a query param in the current url. * * # ONLY IN BROWSER * * not for use in node.js */ export function setQueryParam({ paramName, paramValue, rmParam, logalot, }: { paramName: string, paramValue?: TValue, /** * if true, we will remove the param from the URL. */ rmParam?: boolean, logalot?: boolean, }): void { const lc = `[${setQueryParam.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: f3c557d19f5b3a723611c59aa4a20324)`); } const url = new URL(window.location.href); if (rmParam) { url.searchParams.delete(paramName); } else { const actualValue = typeof paramValue === 'string' ? paramValue : JSON.stringify(paramValue); url.searchParams.set(paramName, actualValue); } // Update the URL without reloading the page window.history.replaceState({}, '', url.toString()); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * helper to highlight an element with border/background. * * ## requires * * .highlight and .unhighlight classes * * The unhighlight is for correctly showing the transition/animation. * * ## intent * * Just making this to make highlighting things (drawing the user's eye) * easier and more consistent. * * @see {@link unhighlightElement} */ export async function highlightElement({ el, magicHighlightTimingMs, scrollIntoView, }: { el: HTMLElement, /** * if provided, will unhighlight the element after this delay in ms */ magicHighlightTimingMs?: number, scrollIntoView?: boolean, }): Promise { const lc = `[${highlightElement.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 1820329f88bb937b21ff98e624c03e25)`); } if (scrollIntoView) { el.scrollIntoView({ behavior: 'smooth' }); } el.classList.add('highlight'); if (magicHighlightTimingMs) { await delay(magicHighlightTimingMs); el.classList.add('unhighlight'); el.classList.remove('highlight'); await delay(magicHighlightTimingMs); el.classList.remove('unhighlight'); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * @see {@link highlightElement} */ export async function unhighlightElement({ el, }: { el: HTMLElement, }): Promise { const lc = `[${unhighlightElement.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: ee5894b05ec31085883098cc512e1c25)`); } el.classList.add('unhighlight'); el.classList.remove('highlight'); await delay(500); el.classList.remove('unhighlight'); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * Takes an incoming string. Assumes it's invalid. Does best effort at validating this. * Gets the raw export ibgib if it can. If so, and if it is compressed, decompresses the * compressed graph contained in the export ibgib. * * @returns RawExportIbGib_V1 and its decompressed graph */ export async function getRawExportIbGibAndGraphFromJsonString({ exportIbGibJsonString, metaspace, }: { exportIbGibJsonString: string, metaspace: MetaspaceService, }): Promise<[RawExportIbGib_V1, FlatIbGibGraph]> { const lc = `[${getRawExportIbGibAndGraphFromJsonString.name}]`; try { debugger; // walk thru getRawExportIbGibAndGraphFromJsonString if (logalot) { console.log(`${lc} starting... (I: b837df7fe8d1ee8831c1701210b55826)`); } if (!metaspace) { throw new Error(`(UNEXPECTED) metaspace falsy? (E: d2a16a6177a8d6aa6801a53bbce3e626)`); } const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local user space? (E: 109c7868b5a8a511e82391d8b91c9826)`); } let parsedIbGib_notYetValidated: IbGib_V1; let parsedExportIbGib_dependenciesNotValidated: RawExportIbGib_V1; try { parsedIbGib_notYetValidated = JSON.parse(exportIbGibJsonString) as IbGib_V1; } catch (error) { const emsg = `Error during parse JSON. error: ${extractErrorMsg(error)} (E: 24912680953c28e55dcf4784e4114826)`; const errorHack = new Error(emsg); (errorHack as any).cause = error; throw errorHack; } try { const validationErrors = await validateIbGibIntrinsically({ ibGib: parsedIbGib_notYetValidated }) ?? []; if (validationErrors.length > 0) { throw new Error(`validation errors when validating the parsed ibgib that should be a RawExportIbGib_V1: ${validationErrors} (E: 8ba178d3b26e8eca28a67c182fe16825)`); } // todo: other validation raw export ibgib, maybe to see if it's a full dependency graph, etc. parsedExportIbGib_dependenciesNotValidated = parsedIbGib_notYetValidated as RawExportIbGib_V1; if (!parsedExportIbGib_dependenciesNotValidated.data) { throw new Error(`(UNEXPECTED) rawExportIbGib.data falsy? (E: ef1824d1cca5584ab8e34288346e9825)`); } } catch (error) { const emsg = `Error during parse JSON. error: ${extractErrorMsg(error)} (E: 9607589c692857f5280c4cc884624326)`; const errorHack = new Error(emsg); (errorHack as any).cause = error; throw errorHack; } // at this point, we have a possibly quite large ibGib whose data // includes a dependencyGraphAsString. this could possibly be // compressed. let payloadGraph: FlatIbGibGraph; if (!!parsedExportIbGib_dependenciesNotValidated.data.compression) { switch (parsedExportIbGib_dependenciesNotValidated.data.compression) { case 'gzip': payloadGraph = await decompressIbGibGraphFromString({ compressedBase64: parsedExportIbGib_dependenciesNotValidated.data.dependencyGraphAsString, }); break; default: throw new Error(`unknown value of rawExportIbGib.data.compression: ${parsedExportIbGib_dependenciesNotValidated.data.compression} (E: 784672fae568312d887ddbb841199925)`); } } else { payloadGraph = await deserializeStringToGraph({ jsonString: parsedExportIbGib_dependenciesNotValidated.data.dependencyGraphAsString }); } const validationErrorMap: { [addr: string]: string[] } = {}; for (const [addr, payloadIbGib] of Object.entries(payloadGraph)) { const validationErrors = await validateIbGibIntrinsically({ ibGib: payloadIbGib }) ?? []; if (validationErrors.length > 0) { validationErrorMap[addr] = validationErrors; } } if (Object.keys(validationErrorMap).length > 0) { throw new Error(`there were validation errors for the RawExportIbGib_V1.data payload (contained in data.dependencyGraphAsString). Here is the validationErrorMap:\n${pretty({ validationErrorMap })} (E: e63f38b182a84122d83a55befb369825)`); } const exportIbGib_validated = parsedExportIbGib_dependenciesNotValidated; return [exportIbGib_validated, payloadGraph]; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * exports a given ibgib to the default local userspace in the metaspace. */ export async function exportIbGib({ ibGib, compress, // downloadAnchorEl, metaspace, space, }: { ibGib: IbGib_V1, compress: boolean, // downloadAnchorEl: HTMLAnchorElement, metaspace: MetaspaceService, space: IbGibSpaceAny, }): Promise { const lc = `[${exportIbGib.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 623d9463d919683be81f749821a95825)`); } if (isPrimitive({ ibGib })) { throw new Error(`ibGib is primitive. Can't export primitive. (E: dd42c42e9fda0707e81c692af1a2a225)`); } const resGetExport = await getRawExportIbGib({ ibGib, live: true, compress, metaspace: metaspace, space, }); const { rawExportIbGib: exportIbGib, errors: exportErrors } = resGetExport; if ((exportErrors ?? []).length > 0) { throw new Error(`Export had errors: ${exportErrors!} (E: 05faf83f25d3449ab809dfd24eeaf825)`); } // at this point, we have a possibly quite large ibGib whose data includes // every single other ibgib that ibGib relates to (its dependency graph). This // so now we can save this file and later import from it. /** * exportIbGib is guaranteed to have a serializable structure, i.e., * the data has only primitives. (there are no Uint8Array) */ const exportIbGibAsString = JSON.stringify(exportIbGib); // thank you SO, OP and volzotan at https://stackoverflow.com/questions/19721439/download-json-object-as-a-file-from-browser // set the anchor's href to a data stream const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(exportIbGibAsString); // get the filename for the anchor to suggest for the "download" const exportAddr = getIbGibAddr({ ibGib: exportIbGib }); const filename = `${exportAddr}.json`; // trigger the click const downloadAnchorEl = document.createElement('a'); downloadAnchorEl.setAttribute("style", "none"); downloadAnchorEl.setAttribute("href", dataStr); downloadAnchorEl.setAttribute("download", filename); const div = document.getElementsByTagName('div')[0]; // just append to the first div then remove it div.appendChild(downloadAnchorEl); downloadAnchorEl.click(); div.removeChild(downloadAnchorEl); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } export async function promptForAPIKey({ msg, }: { /** * msg to tell the user when prompting for the API key (secret) * * originally in blankgib, this is CHAT_WITH_AGENT_NEED_API_KEY */ msg: string, }): Promise { const lc = `[${promptForAPIKey.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: c5f528270ec8ecb1287d1b786b815826)`); } let resAPIKey = ''; let tryAgain = false; let attempts = 0; let maxAttempts = 5; do { attempts++; if (attempts >= maxAttempts) { throw new Error(`too many attempts (E: 1cb6f7f9cd0ef382b8566a1917e1e826)`); } resAPIKey = await promptForSecret({ msg, confirm: false, }); if (resAPIKey) { if (resAPIKey.match(GEMINI_API_KEY_REGEXP)) { tryAgain = false; // return resAPIKey; /* <<<< returns early */ } else { await alertUser({ title: `That's an API Key?`, msg: `That doesn't look like a valid API key, please try again, or hit cancel if you don't want to at this time.`, }); tryAgain = true; } } else { console.log(`${lc} user cancelled entering API key. (I: 6034e1cdaaa8b1e9e8ef4942d804a826)`); tryAgain = false; } } while (tryAgain); return resAPIKey; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * updates the given {@link apiKey}. if {@link force} is true and `apiKey` is * empty, then this will set an empty string for the apiKey, effectively * deleting it. */ export async function updateAPIKeyInStorage({ dbName, storeName, key, apiKey, force, }: { dbName: string, storeName: string, key: string, apiKey: string, force: boolean, }): Promise { const lc = `[${updateAPIKeyInStorage.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 64951b38da35ec5ff847004d53b2d425)`); } const matches = apiKey.match(GEMINI_API_KEY_REGEXP); if (matches || force) { await storagePut({ dbName, storeName, key, value: apiKey, }); if (apiKey) { // set a new truthy key await alertUser({ title: `need to reload...`, msg: `Cool! We successfully SAVED your Gemini API key in your browser's IndexedDB.\n\nIf you'd like to remove it later, you can clear it out by using the ${CONFIG_OPTION_GEMINI_API_KEY_LOCATION_HELP}.\n\nWe must now reload the page to use the new API key... (investment would be a good thing to improve this UX workflow!)` }); } else { // cleared out the key await alertUser({ title: `need to reload...`, msg: `Cool! We successfully CLEARED your Gemini API key from your browser's IndexedDB.\n\nIf you'd like to set a new key, use the ${CONFIG_OPTION_GEMINI_API_KEY_LOCATION_HELP}.\n\nThe API key is already removed, but we must now reload the page for this to propagate throughout the web page... (investment would be a good thing to improve this UX workflow!)` }); } window.location.reload(); } else { throw new Error(`apiKey does not match apiKeyRegex (${GEMINI_API_KEY_REGEXP.source}) and force is false. (E: 2d6c798803e6b71bf6aa70bd60170825)`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * wrapper for simplifying/asserting an element that is expected to exist. * * ## intent * * This avoids using TypeScript truthy assertions (!) or excessive * falsy/exception checks. */ export function getElementById_throwsIfFalsy({ id, shadowRoot, }: { id: string; /** * If set, will get element here, else will use document */ shadowRoot?: ShadowRoot; }): TElement { const lc = `[${getElementById_throwsIfFalsy.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 18a338c6dab8690c72b8759b8d952925)`); } let resElement = !!shadowRoot ? shadowRoot.getElementById(id) as TElement | null : document.getElementById(id) as TElement | null; if (resElement) { return resElement; } else { throw new Error(`${!!shadowRoot ? 'shadowRoot' : 'document'}.getElementById(${id}) returned null. So...Element was not found. (E: 5e501f6835cc74cd4c2faf0d9e387825)`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * wrapper that throws if element not found. * @param id of Element * @returns Element of type TElement or throws. * @see {@link shadowRoot_getElementById} * * ## intent * * This avoids using TypeScript truthy assertions (!) or excessive * falsy/exception checks. */ export function document_getElementById(id: string): TElement { return getElementById_throwsIfFalsy({ id }); } /** * wrapper that throws if element not found. * @param shadowRoot in which the element MUST be truthy or throws * @param id of Element * @returns Element of type TElement or throws. * @see {@link document_getElementById} * * ## intent * * This avoids using TypeScript truthy assertions (!) or excessive * falsy/exception checks. */ export function shadowRoot_getElementById(shadowRoot: ShadowRoot, id: string): TElement { return getElementById_throwsIfFalsy({ id, shadowRoot }); } export function getGlobalStoreName(): string { const lc = `[${getGlobalStoreName.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 62a29f9982b4b5ecd91ff58ca4dd6825)`); } // ARMY_STORE in blankgib const hackGlobal = getIbGibGlobalThis_IbGib(); if (!hackGlobal.storeName_hack) { throw new Error(`(UNEXPECTED) global store name falsy? (E: 3b9b680f6a980debbb877c9a13fb2625)`); } return hackGlobal.storeName_hack; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } export function getGlobalDbName(): string { const lc = `[${getGlobalDbName.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 8c8f08728fc7384f87697377358cc825)`); } // BLANK_GIB_DB_NAME in blankgib const hackGlobal = getIbGibGlobalThis_IbGib(); if (!hackGlobal.dbName_hack) { throw new Error(`(UNEXPECTED) global db name falsy? (E: 3aa028d5e4088f3f3841aae94ac02825)`); } return hackGlobal.dbName_hack; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } export function getGlobalIndexedDbKey_APIKey(): string { const lc = `[${getGlobalIndexedDbKey_APIKey.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 59a2510a85051f97ed15fbc8825b6825)`); } // BEE_KEY in blankgib const hackGlobal = getIbGibGlobalThis_IbGib(); if (!hackGlobal.apiKeyName_hack) { throw new Error(`(UNEXPECTED) global api key name falsy? (E: 1782e812f0c87f54b8059468a5b60125)`); } return hackGlobal.apiKeyName_hack; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } /** * helper info for {@link initAppStorage} */ interface AppStorageInfo { dbName: string; storeNames: string[]; } /** * ensures db and stores are created and can be used. */ export async function initAppStorage({ infos, }: { infos: AppStorageInfo[], }): Promise { const lc = `[${initAppStorage.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 4d41880fb748ee2302a624a255665825)`); } for (const info of infos) { const { dbName, storeNames } = info; const dbExists = await storageDBExists({ dbName, // dbName: BLANK_GIB_DB_NAME, }); if (!dbExists) { await initializeStorage({ dbName, // dbName: BLANK_GIB_DB_NAME, }); } for (const storeName of storeNames) { await storageCreateStoreIfNotExist({ dbName, storeName, // dbName: BLANK_GIB_DB_NAME, // storeName: ARMY_STORE, }); } // await storageCreateStoreIfNotExist({ // dbName: BLANK_GIB_DB_NAME, // storeName: ARMY_STORE, // }); // await storageCreateStoreIfNotExist({ // dbName: BLANK_GIB_DB_NAME, // storeName: ZERO_SPACE_ID, // logalot, // }); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } }