import { respecfully, ifWe, ifWeMight, iReckon } from '@ibgib/helper-gib/dist/respec-gib/respec-gib.mjs'; import { getIbGibGlobalThis_IbGib } from '../helpers.mjs'; import { bufferToHex, hexToUint8Array, normalizeSaltTo32Bytes, isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate } from './passkey-helper.mjs'; import { getDeviceDelegateSecret } from './device-delegate-helper.mjs'; import { getTjpAddr } from '@ibgib/core-gib/dist/common/other/ibgib-helper.mjs'; import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs'; const sir = `[${import.meta.url}]`; // Initialize global db & store names for tests const globalThisIbGib = getIbGibGlobalThis_IbGib(); globalThisIbGib.dbName_hack = 'space_gib_db'; globalThisIbGib.storeName_hack = 'space_gib_store'; // Mock storage substrate for Node test environment const inMemoryStore = new Map(); if (typeof globalThis.indexedDB === 'undefined') { (globalThis as any).indexedDB = { open: (dbName: string) => { const req: any = {}; setTimeout(() => { const db = { objectStoreNames: { contains: () => true }, transaction: (storeName: string) => ({ objectStore: (sName: string) => ({ get: (key: string) => { const getReq: any = {}; setTimeout(() => { const val = inMemoryStore.get(key); getReq.result = val !== undefined ? { value: val } : undefined; if (getReq.onsuccess) getReq.onsuccess({ target: getReq }); }, 0); return getReq; }, put: (datum: any) => { const putReq: any = {}; setTimeout(() => { inMemoryStore.set(datum.key, datum.value); if (putReq.onsuccess) putReq.onsuccess({ target: putReq }); }, 0); return putReq; }, delete: (key: string) => { const delReq: any = {}; setTimeout(() => { inMemoryStore.delete(key); if (delReq.onsuccess) delReq.onsuccess({ target: delReq }); }, 0); return delReq; } }) }), close: () => {} }; req.result = db; if (req.onsuccess) req.onsuccess({ target: req }); }, 0); return req; } }; } // Setup environment profile process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC = 'sync-profile.dev'; class MockIbGibSpace { store = new Map(); ib: string = 'witness space mock-space mock_space_id user'; constructor(public name: string = "mock_space") { } async put({ ibGib }: { ibGib: any }): Promise { const addr = `${ibGib.ib}^${ibGib.gib}`; this.store.set(addr, JSON.parse(JSON.stringify(ibGib))); } async get({ addr }: { addr: string }): Promise { const data = this.store.get(addr); return data ? JSON.parse(JSON.stringify(data)) : null; } async argy({ argData, ibGibs }: { argData: any; ibGibs?: any[] }): Promise { return { ib: 'arg^gib', gib: 'arg_gib', data: argData, ibGibs, }; } async witness(arg: any): Promise { return { data: { success: true } }; } } class MockMetaspaceService { constructor(public space: MockIbGibSpace) { } async getLocalUserSpace({ lock }: { lock?: boolean } = {}): Promise { return this.space; } async put(args: any): Promise { const target = args.space || this.space; return target.put(args); } async registerNewIbGib({ ibGib }: { ibGib: any }): Promise { return this.put({ ibGib }); } async get(args: { addr?: string; addrs?: string[]; space?: any }): Promise<{ success: boolean; ibGibs?: any[] }> { const target = args.space || this.space; if (args.addr) { const x = await target.get({ addr: args.addr }); return { success: !!x, ibGibs: x ? [x] : [] }; } return { success: true, ibGibs: [] }; } } const mockSpace = new MockIbGibSpace(); const mockMetaspace = new MockMetaspaceService(mockSpace); await respecfully(sir, 'Passkey WebAuthn PRF Helper Service', async () => { await ifWe(sir, 'bufferToHex and hexToUint8Array roundtrip correctly', async () => { const originalBytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]); const hex = bufferToHex(originalBytes.buffer); iReckon(sir, hex).willEqual('deadbeef0123456789abcdef'); const roundtripBytes = hexToUint8Array(hex); iReckon(sir, roundtripBytes.length).willEqual(originalBytes.length); for (let i = 0; i < originalBytes.length; i++) { iReckon(sir, roundtripBytes[i]).willEqual(originalBytes[i]); } }); await ifWe(sir, 'normalizeSaltTo32Bytes normalizes various salt inputs to 32 bytes', async () => { // 1. Hex string of 64 chars (32 bytes) const hex64 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const normHex = normalizeSaltTo32Bytes(hex64); iReckon(sir, normHex.length).willEqual(32); iReckon(sir, bufferToHex(normHex.buffer)).willEqual(hex64); // 2. Text string const textSalt = 'ibgib_passkey_salt_domain_123'; const normText = normalizeSaltTo32Bytes(textSalt); iReckon(sir, normText.length).willEqual(32); // 3. Uint8Array const rawBytes = new Uint8Array(32); rawBytes.fill(0x42); const normRaw = normalizeSaltTo32Bytes(rawBytes); iReckon(sir, normRaw.length).willEqual(32); iReckon(sir, normRaw[0]).willEqual(0x42); }); await ifWe(sir, 'isPasskeyPrfSupported evaluates capabilities cleanly', async () => { // In Node environment without window const supported = await isPasskeyPrfSupported(); iReckon(sir, typeof supported).willEqual('boolean'); }); await ifWe(sir, 'createPasskeyDeviceSyncDelegate creates standard passkey sync delegate', async () => { const username = 'alice'; const primaryTjpAddr = 'keystone^alice_primary_tjp.addr'; const passkeySecret = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; const { delegateKeystone, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({ username, primaryTjpAddr, passkeySecret, transports: ['internal'], metaspace: mockMetaspace as any, space: mockSpace as any, }); iReckon(sir, delegateKeystone).asTo('delegateKeystone is generated').isGonnaBeTruthy(); iReckon(sir, deviceTag.startsWith('passkey-alice-')).asTo('deviceTag follows passkey naming format').isGonnaBeTrue(); iReckon(sir, delegateSecret).asTo('delegateSecret is derived').isGonnaBeTruthy(); // Verify stored in IndexedDB const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); const storedSecret = await getDeviceDelegateSecret({ delegateTjpAddr }); iReckon(sir, storedSecret).willEqual(delegateSecret); }); });