/** * @module identity/keystone-sync-service * * On-Demand Keystone Sync Service. * Provides functions to check for keystone updates on the server and sync updated keystone DAG graphs locally. */ import { extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { getTimelinesGroupedByTjp, getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs"; import { isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs"; import { KEYSTONE_VERB_SYNC } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { getApiBridge } from "../api/ibgib-api-bridge.mjs"; import { getGlobalMetaspace_waitIfNeeded } from "../helpers.mjs"; import { getDeviceDelegateSecret, getThisDeviceDelegate } from "./device-delegate-helper.mjs"; import { EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE, EVENT_IBGIB_IDENTITY_RENEWAL_REQUIRED } from "../ui/ui-constants.mjs"; import { EventIbGibIdentityUpdateAvailableDetail, EventIbGibIdentityRenewalRequiredDetail } from "../ui/ui-types.mjs"; export interface KeystoneUpdateCheckResult { hasUpdate: boolean; serverN: number; localN: number; serverLatestAddr: string; graph: Record; } export class KeystoneSyncService { protected lc: string = `[KeystoneSyncService]`; private dispatchUpdateAvailableEvent(detail: EventIbGibIdentityUpdateAvailableDetail): void { if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE, { detail, bubbles: true, composed: true, })); } } private dispatchRenewalRequiredEvent(detail: EventIbGibIdentityRenewalRequiredDetail): void { if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_RENEWAL_REQUIRED, { detail, bubbles: true, composed: true, })); } } /** * Retrieves the stored device delegate secret ($S_D$) for a delegate timeline from IndexedDB. */ public async getDeviceDelegateSecret({ delegateTjpAddr }: { delegateTjpAddr: string; }): Promise { return getDeviceDelegateSecret({ delegateTjpAddr }); } /** * Checks if the active identity has a valid, verified, unexpired sync delegate on this device. */ public async checkDelegateStatus({ activePrimaryAddr, metaspace, space }: { activePrimaryAddr: string; metaspace?: any; space?: any; }): Promise<{ isValid: boolean; reason?: 'expired' | 'missing_secret' }> { const lc = `${this.lc}[${this.checkDelegateStatus.name}]`; try { if (!metaspace) metaspace = await getGlobalMetaspace_waitIfNeeded(); if (!metaspace) { throw new Error(`${lc} (UNEXPECTED) metaspace falsy? (E: 4b36c8f35ac8791703e5bbd841dcf126)`); } if (!space) space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`${lc} (UNEXPECTED) space falsy? (E: d1f6c8f0a4b107b9d818149461781626)`); } const info = getGibInfo({ ibGibAddr: activePrimaryAddr }); const { ib } = getIbAndGib({ ibGibAddr: activePrimaryAddr }); const primaryTjpAddr = info.tjpGib ? `${ib}^${info.tjpGib}` : activePrimaryAddr; const keystoneService = new KeystoneService_V1(); const keystone = await keystoneService.getLatestKeystone({ addr: activePrimaryAddr, metaspace, space }); if (!keystone) { throw new Error(`${lc} (UNEXPECTED) latest primary keystone not found for addr: ${activePrimaryAddr} (E: f69a11601765367cd33b519ff329c826)`); } const deviceDelegate = await getThisDeviceDelegate({ domainIdentity: keystone, verb: KEYSTONE_VERB_SYNC, metaspace, space }); if (!deviceDelegate) { this.dispatchRenewalRequiredEvent({ activePrimaryAddr, primaryTjpAddr, reason: 'missing_secret' }); return { isValid: false, reason: 'missing_secret' }; } if (isDelegateExpired({ delegateInfo: deviceDelegate.delegateInfo })) { this.dispatchRenewalRequiredEvent({ activePrimaryAddr, primaryTjpAddr, reason: 'expired' }); return { isValid: false, reason: 'expired' }; } return { isValid: true }; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); return { isValid: false }; } } /** * Checks if a server update exists for the given active primary keystone address. */ public async checkForKeystoneUpdate({ activePrimaryAddr, metaspace, space }: { activePrimaryAddr: string; metaspace?: any; space?: any; }): Promise { const lc = `${this.lc}[${this.checkForKeystoneUpdate.name}]`; try { if (!metaspace) { metaspace = await getGlobalMetaspace_waitIfNeeded(); } if (!metaspace) { throw new Error("Metaspace not available."); } if (!space) { space = await metaspace.getLocalUserSpace({ lock: false }); } if (!space) { throw new Error("Local user space not available."); } // Determine local latest address & frame const localLatestAddr = (await metaspace.getLatestAddr({ addr: activePrimaryAddr, space })) || activePrimaryAddr; const resLocal = await metaspace.get({ addrs: [localLatestAddr], space }); const localFrame = (resLocal.success && resLocal.ibGibs && resLocal.ibGibs.length > 0) ? (resLocal.ibGibs[0] as KeystoneIbGib_V1) : undefined; const localN = localFrame?.data?.n ?? 0; // Fetch server graph using activePrimaryAddr (resolving to full domain TJP address, e.g. "keystone^hash") const { ib } = getIbAndGib({ ibGibAddr: activePrimaryAddr }); const info = getGibInfo({ ibGibAddr: activePrimaryAddr }); const fallbackTjp = info.tjpGib ? `${ib}^${info.tjpGib}` : activePrimaryAddr; const domainTjpAddr = (localFrame ? getTjpAddr({ ibGib: localFrame }) : undefined) || fallbackTjp; const resGraph = await getApiBridge().getKeystoneGraph(domainTjpAddr); if (!resGraph.success || !resGraph.graph) { const res: KeystoneUpdateCheckResult = { hasUpdate: false, serverN: localN, localN, serverLatestAddr: localLatestAddr, graph: {} }; this.dispatchUpdateAvailableEvent({ activePrimaryAddr, hasUpdate: res.hasUpdate, serverN: res.serverN, localN: res.localN, serverLatestAddr: res.serverLatestAddr, }); return res; } const graph = resGraph.graph as Record; const graphIbGibs = Object.values(graph); // Group timelines by TJP to isolate primary domain timeline const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs }); // Look for timeline under domainTjpAddr or activePrimaryAddr or first available const domainTimeline = timelinesMap[domainTjpAddr] || timelinesMap[activePrimaryAddr] || timelinesMap[Object.keys(timelinesMap)[0]] || []; if (domainTimeline.length === 0) { const res: KeystoneUpdateCheckResult = { hasUpdate: false, serverN: localN, localN, serverLatestAddr: localLatestAddr, graph }; this.dispatchUpdateAvailableEvent({ activePrimaryAddr, hasUpdate: res.hasUpdate, serverN: res.serverN, localN: res.localN, serverLatestAddr: res.serverLatestAddr, }); return res; } // Domain timeline sorted ascending by n const sortedDomainTimeline = [...domainTimeline].sort((a, b) => (a.data?.n ?? 0) - (b.data?.n ?? 0)); const serverLatestFrame = sortedDomainTimeline[sortedDomainTimeline.length - 1] as KeystoneIbGib_V1; const serverN = serverLatestFrame.data?.n ?? 0; const serverLatestAddr = getIbGibAddr({ ibGib: serverLatestFrame }); const hasUpdate = serverN > localN; const res: KeystoneUpdateCheckResult = { hasUpdate, serverN, localN, serverLatestAddr, graph }; this.dispatchUpdateAvailableEvent({ activePrimaryAddr, hasUpdate: res.hasUpdate, serverN: res.serverN, localN: res.localN, serverLatestAddr: res.serverLatestAddr, }); return res; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); const res: KeystoneUpdateCheckResult = { hasUpdate: false, serverN: 0, localN: 0, serverLatestAddr: activePrimaryAddr, graph: {} }; this.dispatchUpdateAvailableEvent({ activePrimaryAddr, hasUpdate: false, serverN: 0, localN: 0, serverLatestAddr: activePrimaryAddr, }); return res; } } /** * Checks for updates and registers any missing server keystone frames into local space. * Returns the updated (or current) primary keystone frame. */ public async updateActiveKeystone({ activePrimaryAddr, metaspace, space }: { activePrimaryAddr: string; metaspace?: any; space?: any; }): Promise { const lc = `${this.lc}[${this.updateActiveKeystone.name}]`; try { if (!metaspace) { metaspace = await getGlobalMetaspace_waitIfNeeded(); } if (!metaspace) { throw new Error("Metaspace not available."); } if (!space) { space = await metaspace.getLocalUserSpace({ lock: false }); } if (!space) { throw new Error("Local user space not available."); } const updateInfo = await this.checkForKeystoneUpdate({ activePrimaryAddr, metaspace, space }); if (updateInfo.hasUpdate && Object.keys(updateInfo.graph).length > 0) { const graphIbGibs = Object.values(updateInfo.graph); // Put all graph ibgibs into space await metaspace.put({ ibGibs: graphIbGibs, space }); // Group by TJP and register each frame ascending by n const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs }); for (const tjpKey of Object.keys(timelinesMap)) { const timeline = timelinesMap[tjpKey]; const sorted = [...timeline].sort((a, b) => (a.data?.n ?? 0) - (b.data?.n ?? 0)); for (const frame of sorted) { await metaspace.registerNewIbGib({ ibGib: frame, space }); } } // Get updated latest primary frame const domainTjp = getGibInfo({ ibGibAddr: activePrimaryAddr }).tjpGib ?? activePrimaryAddr; const domainTimeline = timelinesMap[domainTjp] || timelinesMap[activePrimaryAddr] || timelinesMap[Object.keys(timelinesMap)[0]]; const sortedDomain = [...domainTimeline].sort((a, b) => (a.data?.n ?? 0) - (b.data?.n ?? 0)); const updatedLatestFrame = sortedDomain[sortedDomain.length - 1] as KeystoneIbGib_V1; if (updatedLatestFrame) { // Update special index locally await updateSpecialIndex({ type: "keystones", rel8nInfos: [{ rel8nName: "keystone", ibGibs: [toDto({ ibGib: updatedLatestFrame })] }], metaspace, space }); return updatedLatestFrame; } } // If no update or update failed, return current local frame const localLatestAddr = (await metaspace.getLatestAddr({ addr: activePrimaryAddr, space })) || activePrimaryAddr; const resLocal = await metaspace.get({ addrs: [localLatestAddr], space }); if (resLocal.success && resLocal.ibGibs && resLocal.ibGibs.length > 0) { return resLocal.ibGibs[0] as KeystoneIbGib_V1; } return undefined; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } } let _keystoneSyncSvc: KeystoneSyncService | undefined; export function getKeystoneSyncService(): KeystoneSyncService { if (!_keystoneSyncSvc) { _keystoneSyncSvc = new KeystoneSyncService(); } return _keystoneSyncSvc; }