UNPKG

@nuwa-ai/identity-kit-web

Version:

Web extensions for Nuwa Identity Kit

133 lines (132 loc) 5.27 kB
import { KeyManager, CryptoUtils, MultibaseCodec, toKeyType, } from '@nuwa-ai/identity-kit'; import { LocalStorageKeyStore } from '../keystore'; /** * Manages deep link authentication flow */ export class DeepLinkManager { constructor(options = {}) { this.keyManager = options.keyManager || new KeyManager({ store: new LocalStorageKeyStore() }); this.sessionStorage = options.sessionStorage || window.sessionStorage; } /** * Build a deep link URL for adding a key to a DID */ async buildAddKeyUrl(opts = {}) { const cadopDomainRaw = opts.cadopDomain || 'id.nuwa.dev'; const cadopDomain = cadopDomainRaw.startsWith('http://') || cadopDomainRaw.startsWith('https://') ? cadopDomainRaw.replace(/\/$/, '') // trim trailing slash : (/^(localhost|\d+\.\d+\.\d+\.\d+(:\d+)?)$/.test(cadopDomainRaw) ? `http://${cadopDomainRaw}` : `https://${cadopDomainRaw}`); const keyType = toKeyType((opts.keyType ?? 'Ed25519VerificationKey2020')); const idFragment = opts.idFragment || `key-${Date.now()}`; const relationships = opts.relationships || ['authentication']; const redirectPath = opts.redirectPath || '/callback'; // Generate a random state to prevent CSRF const state = this.generateRandomState(); // Generate a key pair const { privateKey, publicKey } = await CryptoUtils.generateKeyPair(keyType); const privateKeyMultibase = MultibaseCodec.encodeBase58btc(privateKey); const publicKeyMultibase = MultibaseCodec.encodeBase58btc(publicKey); // Store the private key temporarily in session storage this.sessionStorage.setItem(`nuwa_temp_key_${state}`, JSON.stringify({ privateKeyMultibase: privateKeyMultibase, publicKeyMultibase: publicKeyMultibase, keyType, idFragment, })); // Build payload per spec (versioned JSON -> Base64URL) const redirectUri = new URL(redirectPath, window.location.origin).toString(); const payload = { version: 1, verificationMethod: { type: keyType, publicKeyMultibase: publicKeyMultibase, idFragment, }, verificationRelationships: relationships, redirectUri, state, }; if (opts.agentDid) { payload.agentDid = opts.agentDid; } const encodedPayload = MultibaseCodec.encodeBase64url(JSON.stringify(payload)); return { url: `${cadopDomain}/add-key?payload=${encodedPayload}`, state, privateKeyMultibase, publicKeyMultibase, }; } /** * Handle the callback from the deep link */ async handleCallback(search) { const params = new URLSearchParams(search); const state = params.get('state'); const agentDid = params.get('agentDid') || params.get('agent'); const keyId = params.get('key_id') || params.get('keyId'); const errorRaw = params.get('error'); const error = errorRaw ? decodeURIComponent(errorRaw) : undefined; // Immediate failure if error param present and no success flag if (error && !params.has('success')) { return { success: false, error }; } const successFlag = params.get('success'); if (successFlag === '0') { return { success: false, error: error || 'Operation cancelled' }; } if (!state || !agentDid || !keyId) { return { success: false, error: 'Missing required parameters in callback' }; } // Retrieve the temporary key from session storage const tempKeyJson = this.sessionStorage.getItem(`nuwa_temp_key_${state}`); if (!tempKeyJson) { return { success: false, error: 'No matching key found for the provided state' }; } try { const tempKey = JSON.parse(tempKeyJson); // Create a stored key object const storedKey = { keyId, keyType: tempKey.keyType, publicKeyMultibase: tempKey.publicKeyMultibase, privateKeyMultibase: tempKey.privateKeyMultibase, }; // Save the key to the key manager await this.keyManager.importKey(storedKey); // Clean up the temporary key this.sessionStorage.removeItem(`nuwa_temp_key_${state}`); return { success: true, agentDid, keyId, }; } catch (e) { return { success: false, error: `Failed to process callback: ${e instanceof Error ? e.message : String(e)}` }; } } /** * Generate a random state string */ generateRandomState() { const array = new Uint8Array(16); crypto.getRandomValues(array); return Array.from(array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } }