UNPKG

@nuwa-ai/identity-kit-web

Version:

Web extensions for Nuwa Identity Kit

196 lines (169 loc) 5.79 kB
import type { KeyType, AddKeyRequestPayloadV1, VerificationRelationship } from '@nuwa-ai/identity-kit'; import { KeyManager, CryptoUtils, StoredKey, MultibaseCodec, KeyTypeInput, toKeyType, } from '@nuwa-ai/identity-kit'; import { LocalStorageKeyStore } from '../keystore'; export interface ConnectOptions { cadopDomain?: string; keyType?: KeyTypeInput; // Default: KeyType.ED25519 idFragment?: string; relationships?: VerificationRelationship[]; // Default: ['authentication'] redirectPath?: string; // Default: '/callback' agentDid?: string; // Target Agent DID, optional } export interface AuthResult { success: boolean; agentDid?: string; keyId?: string; error?: string; } interface TempKey { privateKeyMultibase: string; publicKeyMultibase: string; keyType: KeyType; idFragment: string; } /** * Manages deep link authentication flow */ export class DeepLinkManager { private keyManager: KeyManager; private sessionStorage: Storage; constructor(options: { keyManager?: KeyManager; sessionStorage?: Storage; } = {}) { 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: ConnectOptions = {}): Promise<{ url: string; state: string; privateKeyMultibase: string; publicKeyMultibase: string; }> { 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') as KeyTypeInput); 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: AddKeyRequestPayloadV1 = { 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: string): Promise<AuthResult> { 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: TempKey = JSON.parse(tempKeyJson); // Create a stored key object const storedKey: 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 */ private generateRandomState(): string { const array = new Uint8Array(16); crypto.getRandomValues(array); return Array.from(array) .map(b => b.toString(16).padStart(2, '0')) .join(''); } }