/** * @module identity/device-delegate-helper * * Client-side helper functions for deriving, retrieving, persisting, and clearing local * cryptographic device delegate signing tokens ($S_D$) in IndexedDB. * * Applicable symmetrically to Sovereign, Custodial, and Hybrid identities. */ import { extractErrorMsg, getSaferSubstring } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { deriveDeviceDelegateSecret, deriveDelegateSecret, createSyncDelegateKeystone, createAuthorDelegateKeystone, isDelegateExpired, } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs"; import { KEYSTONE_ROLE_AUTHOR_DELEGATE } from "@ibgib/core-gib/dist/keystone/delegate/delegate-constants.mjs"; import { getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { verifyPoolSecret } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { KeystoneIbGib_V1, DelegateKeystoneInfo } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { KeystoneVerb, KEYSTONE_VERB_AUTHOR, KEYSTONE_VERB_SYNC } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { generateDefaultDeviceTag } from "../util/device-helper.mjs"; import { storageGet, storagePut, storageDelete } from "../storage/storage-helpers.web.mjs"; import { getGlobalDbName, getGlobalStoreName } from "../helpers.web.mjs"; export { generateDefaultDeviceTag }; export const DEVICE_DELEGATE_SECRET_PREFIX = 'ibgib_device_delegate_secret_'; export const DEVICE_AUTHOR_DELEGATE_SECRET_PREFIX = 'ibgib_device_author_delegate_secret_'; /** * Builds the IndexedDB storage key for a device delegate secret given its delegate TJP address. */ export function getDeviceDelegateSecretKey({ delegateTjpAddr }: { delegateTjpAddr: string }): string { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to get device delegate secret key (E: ffded6bca518d51fb8365ad81cd81226)'); } return `${DEVICE_DELEGATE_SECRET_PREFIX}${delegateTjpAddr}`; } /** * Builds the IndexedDB storage key for a device author delegate secret given its delegate TJP address. */ export function getDeviceAuthorDelegateSecretKey({ delegateTjpAddr }: { delegateTjpAddr: string }): string { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to get device author delegate secret key (E: 1fd8a2e4b3c04217ae88e09f58273126)'); } return `${DEVICE_AUTHOR_DELEGATE_SECRET_PREFIX}${delegateTjpAddr}`; } const inMemoryDelegateSecrets = new Map(); /** * Retrieves the stored device delegate secret for the given delegate TJP address, * checking in-memory cache first, then IndexedDB if present. */ export async function getDeviceDelegateSecret({ delegateTjpAddr, }: { delegateTjpAddr: string; }): Promise { const lc = `[${getDeviceDelegateSecret.name}]`; try { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to get device delegate secret (E: f55d08cfb548e0fea8610d95b0befd26)'); } const key = getDeviceDelegateSecretKey({ delegateTjpAddr }); const inMemory = inMemoryDelegateSecrets.get(key); if (inMemory) { return inMemory; } const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); try { const secret = await storageGet({ dbName, storeName, key }); if (secret) { inMemoryDelegateSecrets.set(key, secret); } return secret; } catch (storageErr) { console.warn(`${lc} storageGet failed (IndexedDB may not be ready): ${extractErrorMsg(storageErr)}`); return undefined; } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Persists a device delegate secret for the given delegate TJP address into memory and optionally IndexedDB. */ export async function setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret, persistToStorage = true, }: { delegateTjpAddr: string; delegateSecret: string; persistToStorage?: boolean; }): Promise { const lc = `[${setDeviceDelegateSecret.name}]`; try { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to set device delegate secret (E: cbc078280958c613d24bff8747537826)'); } if (!delegateSecret) { throw new Error('delegateSecret is required to set device delegate secret (E: 65ebf6395a1ee6b5c87e4448df61f926)'); } const key = getDeviceDelegateSecretKey({ delegateTjpAddr }); inMemoryDelegateSecrets.set(key, delegateSecret); if (persistToStorage) { const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); await storagePut({ dbName, storeName, key, value: delegateSecret }); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Retrieves an existing device delegate secret from IndexedDB, or generates a high-entropy 512-bit * secret, persists it, and returns it. */ export async function getOrCreateDeviceDelegateSecret({ delegateTjpAddr, deviceTag, }: { delegateTjpAddr: string; deviceTag?: string; }): Promise { const lc = `[${getOrCreateDeviceDelegateSecret.name}]`; try { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to get or create device delegate secret (E: 846c4e72e12803c5bac83688b7f6f826)'); } const existingSecret = await getDeviceDelegateSecret({ delegateTjpAddr }); if (existingSecret) { return existingSecret; } const newSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr: delegateTjpAddr, deviceTag }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret: newSecret }); return newSecret; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Retrieves the stored device author delegate secret for the given delegate TJP address, * checking in-memory cache first, then IndexedDB if present. * Falls back to checking generic device delegate secret. */ export async function getDeviceAuthorDelegateSecret({ delegateTjpAddr, }: { delegateTjpAddr: string; }): Promise { const lc = `[${getDeviceAuthorDelegateSecret.name}]`; try { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to get device author delegate secret (E: 2c4d9a71e80931bf8901bce578491226)'); } const key = getDeviceAuthorDelegateSecretKey({ delegateTjpAddr }); const inMemory = inMemoryDelegateSecrets.get(key); if (inMemory) { return inMemory; } const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); try { const secret = await storageGet({ dbName, storeName, key }); if (secret) { inMemoryDelegateSecrets.set(key, secret); return secret; } } catch (storageErr) { console.warn(`${lc} storageGet failed (IndexedDB may not be ready): ${extractErrorMsg(storageErr)}`); } return await getDeviceDelegateSecret({ delegateTjpAddr }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Persists a device author delegate secret for the given delegate TJP address into memory and optionally IndexedDB. */ export async function setDeviceAuthorDelegateSecret({ delegateTjpAddr, delegateSecret, persistToStorage = true, }: { delegateTjpAddr: string; delegateSecret: string; persistToStorage?: boolean; }): Promise { const lc = `[${setDeviceAuthorDelegateSecret.name}]`; try { if (!delegateTjpAddr) { throw new Error('delegateTjpAddr is required to set device author delegate secret (E: 3a5b8c9d0e1f427a91823bcdef567890)'); } if (!delegateSecret) { throw new Error('delegateSecret is required to set device author delegate secret (E: 4b6c9d0e1f2a538b02934cdef6789012)'); } const key = getDeviceAuthorDelegateSecretKey({ delegateTjpAddr }); inMemoryDelegateSecrets.set(key, delegateSecret); if (persistToStorage) { const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); await storagePut({ dbName, storeName, key, value: delegateSecret }); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Removes the stored local device author delegate secret from memory and IndexedDB. */ export async function clearDeviceAuthorDelegateSecret({ delegateTjpAddr, }: { delegateTjpAddr: string; }): Promise { const lc = `[${clearDeviceAuthorDelegateSecret.name}]`; try { if (!delegateTjpAddr) { return; } const key = getDeviceAuthorDelegateSecretKey({ delegateTjpAddr }); inMemoryDelegateSecrets.delete(key); try { const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); await storageDelete({ dbName, storeName, key }); } catch (err) { console.warn(`${lc} storageDelete failed: ${extractErrorMsg(err)}`); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Removes the stored local device delegate secret from memory and IndexedDB (both generic and author). */ export async function clearDeviceDelegateSecret({ delegateTjpAddr, }: { delegateTjpAddr: string; }): Promise { const lc = `[${clearDeviceDelegateSecret.name}]`; try { if (!delegateTjpAddr) { return; } const key = getDeviceDelegateSecretKey({ delegateTjpAddr }); inMemoryDelegateSecrets.delete(key); try { const dbName = getGlobalDbName(); const storeName = getGlobalStoreName(); await storageDelete({ dbName, storeName, key }); } catch (err) { console.warn(`${lc} storageDelete failed: ${extractErrorMsg(err)}`); } await clearDeviceAuthorDelegateSecret({ delegateTjpAddr }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } } /** * Creates a Sovereign Password-based sync delegate for this device. * Standardized format: `pwd-${safeUsername}-${defaultDeviceTag}` */ export async function createSovereignDeviceSyncDelegate({ username, primaryTjpAddr, masterSecret, metaspace, space, persistToStorage = true, }: { username: string; primaryTjpAddr: string; masterSecret: string; metaspace?: any; space?: any; persistToStorage?: boolean; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const delegateSecret = await deriveDelegateSecret({ masterSecret }); const safeUser = getSaferSubstring({ text: username, length: 8, keepLiterals: ['-'] }) || 'user'; const defaultDeviceTag = generateDefaultDeviceTag(); const deviceTag = `pwd-${safeUser}-${defaultDeviceTag}`; const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC; if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing.'); } const delegateKeystone = await createSyncDelegateKeystone({ profileName: sovereignSyncProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret, persistToStorage }); return { delegateKeystone, deviceTag, delegateSecret }; } /** * Creates an OAuth2 Custodial sync delegate for this device. * Standardized format: `oauth2-${providerId}-${defaultDeviceTag}` */ export async function createOAuth2DeviceSyncDelegate({ providerId, username = 'user', primaryTjpAddr, custodianReqs, metaspace, space, }: { providerId: string; username?: string; primaryTjpAddr: string; custodianReqs?: any; metaspace?: any; space?: any; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const defaultDeviceTag = generateDefaultDeviceTag(); const deviceTag = `oauth2-${providerId}-${defaultDeviceTag}`; const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag }); const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC; if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing.'); } const delegateKeystone = await createSyncDelegateKeystone({ profileName: custodianSyncProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, custodianReqs, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret }); return { delegateKeystone, deviceTag, delegateSecret }; } /** * Creates an Email Custodial sync delegate for this device. * Standardized format: `email-${safeUser}@${safeDomain}-${defaultDeviceTag}` */ export async function createEmailDeviceSyncDelegate({ email, username: customUsername, primaryTjpAddr, custodianReqs, metaspace, space, }: { email: string; username?: string; primaryTjpAddr: string; custodianReqs?: any; metaspace?: any; space?: any; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const defaultDeviceTag = generateDefaultDeviceTag(); const [emailUser = '', emailDomain = ''] = (email || '').split('@'); const safeUser = getSaferSubstring({ text: customUsername || emailUser, length: 62, keepLiterals: [], replaceMap: { ' ': '_' } }).toLowerCase() || 'user'; const domainPart = emailDomain.split('.')[0] || ''; const safeDomain = domainPart ? getSaferSubstring({ text: domainPart, length: 8, keepLiterals: ['-'] }) : ''; const emailTag = safeDomain ? `${safeUser.slice(0, 8)}@${safeDomain}` : safeUser.slice(0, 8); const deviceTag = `email-${emailTag}-${defaultDeviceTag}`; const username = safeUser; const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag }); const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC; if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing.'); } const delegateKeystone = await createSyncDelegateKeystone({ profileName: custodianSyncProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, custodianReqs, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret }); return { delegateKeystone, deviceTag, delegateSecret }; } /** * Creates a Sovereign Password-based author delegate for this device. * Standardized format: `author-pwd-${safeUsername}-${defaultDeviceTag}` */ export async function createSovereignDeviceAuthorDelegate({ username, primaryTjpAddr, masterSecret, metaspace, space, persistToStorage = true, }: { username: string; primaryTjpAddr: string; masterSecret: string; metaspace?: any; space?: any; persistToStorage?: boolean; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const delegateSecret = await deriveDelegateSecret({ masterSecret }); const safeUser = getSaferSubstring({ text: username, length: 8, keepLiterals: ['-'] }) || 'user'; const defaultDeviceTag = generateDefaultDeviceTag(); const deviceTag = `author-pwd-${safeUser}-${defaultDeviceTag}`; const sovereignAuthorProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_AUTHOR; if (!sovereignAuthorProfileName || !sovereignAuthorProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_AUTHOR environment variable is missing.'); } const delegateKeystone = await createAuthorDelegateKeystone({ profileName: sovereignAuthorProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceAuthorDelegateSecret({ delegateTjpAddr, delegateSecret, persistToStorage }); return { delegateKeystone, deviceTag, delegateSecret }; } /** * Creates an OAuth2 Custodial author delegate for this device. * Standardized format: `author-oauth2-${providerId}-${defaultDeviceTag}` */ export async function createOAuth2DeviceAuthorDelegate({ providerId, username = 'user', primaryTjpAddr, metaspace, space, }: { providerId: string; username?: string; primaryTjpAddr: string; metaspace?: any; space?: any; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const defaultDeviceTag = generateDefaultDeviceTag(); const deviceTag = `author-oauth2-${providerId}-${defaultDeviceTag}`; const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag }); const authorProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_AUTHOR; if (!authorProfileName || !authorProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_AUTHOR environment variable is missing.'); } const delegateKeystone = await createAuthorDelegateKeystone({ profileName: authorProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceAuthorDelegateSecret({ delegateTjpAddr, delegateSecret }); return { delegateKeystone, deviceTag, delegateSecret }; } /** * Creates an Email Custodial author delegate for this device. * Standardized format: `author-email-${emailTag}-${defaultDeviceTag}` */ export async function createEmailDeviceAuthorDelegate({ email, username: customUsername, primaryTjpAddr, metaspace, space, }: { email: string; username?: string; primaryTjpAddr: string; metaspace?: any; space?: any; }): Promise<{ delegateKeystone: any; deviceTag: string; delegateSecret: string }> { const defaultDeviceTag = generateDefaultDeviceTag(); const [emailUser = '', emailDomain = ''] = (email || '').split('@'); const safeUser = getSaferSubstring({ text: customUsername || emailUser, length: 62, keepLiterals: [], replaceMap: { ' ': '_' } }).toLowerCase() || 'user'; const domainPart = emailDomain.split('.')[0] || ''; const safeDomain = domainPart ? getSaferSubstring({ text: domainPart, length: 8, keepLiterals: ['-'] }) : ''; const emailTag = safeDomain ? `${safeUser.slice(0, 8)}@${safeDomain}` : safeUser.slice(0, 8); const deviceTag = `author-email-${emailTag}-${defaultDeviceTag}`; const username = safeUser; const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag }); const authorProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_AUTHOR; if (!authorProfileName || !authorProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_AUTHOR environment variable is missing.'); } const delegateKeystone = await createAuthorDelegateKeystone({ profileName: authorProfileName.trim() as any, username, deviceTag, primaryTjpAddr, delegateSecret, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceAuthorDelegateSecret({ delegateTjpAddr, delegateSecret }); return { delegateKeystone, deviceTag, delegateSecret }; } export interface GetThisDeviceDelegateParams { domainIdentity: KeystoneIbGib_V1; verb: KeystoneVerb | string; role?: string; metaspace: any; space: any; } export interface ThisDeviceDelegateResult { delegateKeystone: KeystoneIbGib_V1; delegateTjpAddr: string; delegateSecret: string; delegateInfo: DelegateKeystoneInfo; } /** * Resolves the active delegate keystone and secret for THIS device and verb. * Checks local space and IndexedDB to match the local device's registered delegate, * and cryptographically verifies that the stored secret satisfies the delegate's challenge pool. */ export async function getThisDeviceDelegate( params: GetThisDeviceDelegateParams ): Promise { const lc = `[${getThisDeviceDelegate.name}]`; const { domainIdentity, verb, role, metaspace, space } = params; const delegates = domainIdentity.data?.delegates ?? {}; const keystoneService = new KeystoneService_V1(); for (const [delegateTjpAddr, info] of Object.entries(delegates)) { if (role && (info as any).role && (info as any).role !== role) { continue; } if (!info.allowedVerbs?.includes(verb as any)) { continue; } if (isDelegateExpired({ delegateInfo: info })) { continue; } // Confirm delegate is present in THIS device's local space const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space }); if (resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0) { let delegateSecret = await getDeviceAuthorDelegateSecret({ delegateTjpAddr }); if (!delegateSecret) { delegateSecret = await getDeviceDelegateSecret({ delegateTjpAddr }); } if (!delegateSecret) { continue; } try { const latestDelegate = await keystoneService.getLatestKeystone({ addr: delegateTjpAddr, metaspace, space, }); if (!latestDelegate?.data?.challengePools) { continue; } const targetPool = latestDelegate.data.challengePools.find((p: any) => p.allowedVerbs?.includes(verb as any) || p.id === verb ); if (!targetPool) { continue; } const isSecretValid = await verifyPoolSecret({ pool: targetPool, signingSecret: delegateSecret }); if (!isSecretValid) { continue; } return { delegateKeystone: latestDelegate, delegateTjpAddr, delegateSecret, delegateInfo: info, }; } catch (err) { console.warn(`${lc} Found delegate ${delegateTjpAddr} in local space but verification failed: ${extractErrorMsg(err)}`); } } } return undefined; }