You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							502 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							502 lines
						
					
					
						
							14 KiB
						
					
					
				
								/**
							 | 
						|
								 * DID Creation and Registration Flow
							 | 
						|
								 * @author Matthew Raymer
							 | 
						|
								 * 
							 | 
						|
								 * This module implements the creation and registration of Decentralized Identifiers (DIDs)
							 | 
						|
								 * with the endorser.ch service. It matches the Python implementation in new_flow.py.
							 | 
						|
								 */
							 | 
						|
								
							 | 
						|
								import { HDNode } from '@ethersproject/hdnode';
							 | 
						|
								import { Wallet } from '@ethersproject/wallet';
							 | 
						|
								import { createJWT, ES256KSigner, SimpleSigner } from 'did-jwt';
							 | 
						|
								import axios from 'axios';
							 | 
						|
								import { mkdir, writeFile } from 'fs/promises';
							 | 
						|
								import { join } from 'path';
							 | 
						|
								
							 | 
						|
								// Constants
							 | 
						|
								const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
							 | 
						|
								const API_SERVER = "https://test-api.endorser.ch";
							 | 
						|
								const ENDORSER_DID = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
							 | 
						|
								const ENDORSER_PRIVATE_KEY = "2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b";
							 | 
						|
								
							 | 
						|
								interface IKey {
							 | 
						|
								    id: string;
							 | 
						|
								    type: string;
							 | 
						|
								    controller: string;
							 | 
						|
								    ethereumAddress: string;
							 | 
						|
								    publicKeyHex: string;
							 | 
						|
								    privateKeyHex: string;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								interface IIdentifier {
							 | 
						|
								    did: string;
							 | 
						|
								    keys: IKey[];
							 | 
						|
								    services: any[];
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Create .generated directory if it doesn't exist
							 | 
						|
								const GENERATED_DIR = '.generated';
							 | 
						|
								
							 | 
						|
								// Initialize directory creation
							 | 
						|
								async function initializeDirectories() {
							 | 
						|
								    try {
							 | 
						|
								        await mkdir(GENERATED_DIR, { recursive: true });
							 | 
						|
								    } catch (err) {
							 | 
						|
								        console.error('Error creating .generated directory:', err);
							 | 
						|
								        throw err;
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Generate a new mnemonic seed phrase
							 | 
						|
								 */
							 | 
						|
								function generateMnemonic(): string {
							 | 
						|
								    return Wallet.createRandom().mnemonic.phrase;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Derive Ethereum address and keys from a mnemonic phrase
							 | 
						|
								 */
							 | 
						|
								async function deriveAddress(
							 | 
						|
								    mnemonic: string,
							 | 
						|
								    derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
							 | 
						|
								): Promise<[string, string, string, string]> {
							 | 
						|
								    mnemonic = mnemonic.trim().toLowerCase();
							 | 
						|
								    const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
							 | 
						|
								    const rootNode: HDNode = hdnode.derivePath(derivationPath);
							 | 
						|
								    
							 | 
						|
								    console.log("\nKey Derivation:");
							 | 
						|
								    console.log(`  Mnemonic (first 4 words): ${mnemonic.split(' ').slice(0,4).join(' ')}...`);
							 | 
						|
								    console.log(`  Derivation Path: ${derivationPath}`);
							 | 
						|
								    
							 | 
						|
								    const privateHex = rootNode.privateKey.substring(2); // remove '0x'
							 | 
						|
								    const publicHex = rootNode.publicKey.substring(2); // remove '0x'
							 | 
						|
								    const address = rootNode.address;
							 | 
						|
								
							 | 
						|
								    console.log(`  Address: ${address}`);
							 | 
						|
								    console.log(`  Private Key: ${privateHex.substring(0,8)}...`);
							 | 
						|
								    console.log(`  Public Key: ${publicHex.substring(0,8)}...`);
							 | 
						|
								
							 | 
						|
								    // Save derivation data
							 | 
						|
								    const derivationData = {
							 | 
						|
								        mnemonic,
							 | 
						|
								        derivationPath,
							 | 
						|
								        address,
							 | 
						|
								        privateKey: privateHex,
							 | 
						|
								        publicKey: publicHex
							 | 
						|
								    };
							 | 
						|
								    try {
							 | 
						|
								        await mkdir(GENERATED_DIR, { recursive: true });
							 | 
						|
								        await writeFile(
							 | 
						|
								            join(GENERATED_DIR, 'key_derivation.json'),
							 | 
						|
								            JSON.stringify(derivationData, null, 2)
							 | 
						|
								        );
							 | 
						|
								    } catch (err) {
							 | 
						|
								        console.error('Error saving derivation data:', err);
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    return [address, privateHex, publicHex, derivationPath];
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Create a new DID identifier with associated keys
							 | 
						|
								 */
							 | 
						|
								function newIdentifier(
							 | 
						|
								    address: string,
							 | 
						|
								    publicKeyHex: string,
							 | 
						|
								    privateKeyHex: string,
							 | 
						|
								    derivationPath: string
							 | 
						|
								): IIdentifier {
							 | 
						|
								    const did = `did:ethr:${address}`;
							 | 
						|
								    const keyId = `${did}#keys-1`;
							 | 
						|
								    
							 | 
						|
								    const identifier: IIdentifier = {
							 | 
						|
								        did,
							 | 
						|
								        keys: [{
							 | 
						|
								            id: keyId,
							 | 
						|
								            type: "Secp256k1VerificationKey2018",
							 | 
						|
								            controller: did,
							 | 
						|
								            ethereumAddress: address,
							 | 
						|
								            publicKeyHex,
							 | 
						|
								            privateKeyHex
							 | 
						|
								        }],
							 | 
						|
								        services: []
							 | 
						|
								    };
							 | 
						|
								    return identifier;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Initialize a new DID account or load existing one
							 | 
						|
								 */
							 | 
						|
								async function initializeAccount(): Promise<IIdentifier> {
							 | 
						|
								    const mnemonic = generateMnemonic();
							 | 
						|
								    const [address, privateHex, publicHex, derivationPath] = await deriveAddress(mnemonic);
							 | 
						|
								    const identity = newIdentifier(address, publicHex, privateHex, derivationPath);
							 | 
						|
								
							 | 
						|
								    // Format and display account data
							 | 
						|
								    const accountData = {
							 | 
						|
								        did: identity.did,
							 | 
						|
								        identity,
							 | 
						|
								        mnemonic,
							 | 
						|
								        derivation_path: derivationPath
							 | 
						|
								    };
							 | 
						|
								    console.log("\nAccount initialized:");
							 | 
						|
								    console.log(JSON.stringify(accountData, null, 2));
							 | 
						|
								    console.log();
							 | 
						|
								
							 | 
						|
								    // Save account data
							 | 
						|
								    await writeFile(
							 | 
						|
								        join(GENERATED_DIR, 'account_init.json'),
							 | 
						|
								        JSON.stringify(accountData, null, 2)
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    return identity;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Create a signed JWT for DID registration
							 | 
						|
								 */
							 | 
						|
								async function createEndorserJwt(
							 | 
						|
								    did: string,
							 | 
						|
								    privateKeyHex: string,
							 | 
						|
								    payload: any,
							 | 
						|
								    subDid?: string,
							 | 
						|
								    expiresIn: number = 3600
							 | 
						|
								): Promise<string> {
							 | 
						|
								    const signer = await SimpleSigner(privateKeyHex);
							 | 
						|
								    
							 | 
						|
								    const jwt = await createJWT(
							 | 
						|
								        payload,
							 | 
						|
								        {
							 | 
						|
								            issuer: did,
							 | 
						|
								            signer,
							 | 
						|
								            expiresIn,
							 | 
						|
								            ...(subDid && { subject: subDid })
							 | 
						|
								        }
							 | 
						|
								    );
							 | 
						|
								    
							 | 
						|
								    return jwt;
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Register a DID with the endorser service
							 | 
						|
								 */
							 | 
						|
								async function register(
							 | 
						|
								    activeDid: string,
							 | 
						|
								    privateKeyHex: string,
							 | 
						|
								    apiServer: string = API_SERVER
							 | 
						|
								): Promise<{
							 | 
						|
								    success?: boolean;
							 | 
						|
								    handleId?: string;
							 | 
						|
								    registrationId?: number;
							 | 
						|
								    claimId?: string;
							 | 
						|
								    error?: string;
							 | 
						|
								    details?: any;
							 | 
						|
								}> {
							 | 
						|
								    console.log("\nEndorser DID:", ENDORSER_DID);
							 | 
						|
								    console.log("Active DID:", activeDid);
							 | 
						|
								
							 | 
						|
								    const vcPayload = {
							 | 
						|
								        vc: {
							 | 
						|
								            "@context": ["https://www.w3.org/2018/credentials/v1"],
							 | 
						|
								            type: ["VerifiableCredential"],
							 | 
						|
								            credentialSubject: {
							 | 
						|
								                "@context": "https://schema.org",
							 | 
						|
								                "@type": "RegisterAction",
							 | 
						|
								                "agent": {
							 | 
						|
								                    "identifier": ENDORSER_DID
							 | 
						|
								                },
							 | 
						|
								                "participant": {
							 | 
						|
								                    "identifier": activeDid
							 | 
						|
								                },
							 | 
						|
								                "object": "endorser.ch",
							 | 
						|
								                "endTime": Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    // Debug output
							 | 
						|
								    console.log("\nRegistration Claim:");
							 | 
						|
								    console.log(JSON.stringify(vcPayload, null, 2));
							 | 
						|
								    console.log();
							 | 
						|
								
							 | 
						|
								    // Sign with endorser's DID and private key
							 | 
						|
								    const jwtToken = await createEndorserJwt(
							 | 
						|
								        ENDORSER_DID,
							 | 
						|
								        ENDORSER_PRIVATE_KEY,
							 | 
						|
								        vcPayload,
							 | 
						|
								        activeDid
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    // Debug output
							 | 
						|
								    console.log("Generated JWT:");
							 | 
						|
								    console.log(jwtToken);
							 | 
						|
								    console.log();
							 | 
						|
								
							 | 
						|
								    // Debug JWT parts
							 | 
						|
								    const [header, payload, signature] = jwtToken.split('.');
							 | 
						|
								    console.log("\nJWT Header:", JSON.parse(Buffer.from(header, 'base64url').toString()));
							 | 
						|
								    console.log("JWT Payload:", JSON.parse(Buffer.from(payload, 'base64url').toString()));
							 | 
						|
								    console.log("JWT Signature length:", Buffer.from(signature, 'base64url').length, "bytes");
							 | 
						|
								    console.log();
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								        const response = await axios.post(
							 | 
						|
								            `${apiServer}/api/v2/claim`,
							 | 
						|
								            { jwtEncoded: jwtToken },
							 | 
						|
								            { headers: { 'Content-Type': 'application/json' } }
							 | 
						|
								        );
							 | 
						|
								
							 | 
						|
								        if (response.status === 200 && response.data.success?.handleId) {
							 | 
						|
								            // Return all success details
							 | 
						|
								            const registrationData = {
							 | 
						|
								                activeDid,
							 | 
						|
								                jwtToken,
							 | 
						|
								                response: response.data.success
							 | 
						|
								            };
							 | 
						|
								            await writeFile(
							 | 
						|
								                join(GENERATED_DIR, 'registration.json'),
							 | 
						|
								                JSON.stringify(registrationData, null, 2)
							 | 
						|
								            );
							 | 
						|
								            return {
							 | 
						|
								                success: true,
							 | 
						|
								                handleId: response.data.success.handleId,
							 | 
						|
								                registrationId: response.data.success.registrationId,
							 | 
						|
								                claimId: response.data.success.claimId
							 | 
						|
								            };
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        // Log full error response
							 | 
						|
								        console.error("\nAPI Error Response:");
							 | 
						|
								        console.error(JSON.stringify(response.data, null, 2));
							 | 
						|
								        console.error();
							 | 
						|
								
							 | 
						|
								        // If we have success data but no handleId, it might be a partial success
							 | 
						|
								        if (response.data.success) {
							 | 
						|
								            const registrationData = {
							 | 
						|
								                activeDid,
							 | 
						|
								                jwtToken,
							 | 
						|
								                response: response.data.success
							 | 
						|
								            };
							 | 
						|
								            await writeFile(
							 | 
						|
								                join(GENERATED_DIR, 'registration.json'),
							 | 
						|
								                JSON.stringify(registrationData, null, 2)
							 | 
						|
								            );
							 | 
						|
								            return {
							 | 
						|
								                success: true,
							 | 
						|
								                ...response.data.success
							 | 
						|
								            };
							 | 
						|
								        } else {
							 | 
						|
								            const registrationData = {
							 | 
						|
								                activeDid,
							 | 
						|
								                jwtToken,
							 | 
						|
								                response: response.data
							 | 
						|
								            };
							 | 
						|
								            await writeFile(
							 | 
						|
								                join(GENERATED_DIR, 'registration.json'),
							 | 
						|
								                JSON.stringify(registrationData, null, 2)
							 | 
						|
								            );
							 | 
						|
								            return { error: "Registration failed", details: response.data };
							 | 
						|
								        }
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								        // Log detailed error information
							 | 
						|
								        console.error("\nAPI Request Error:");
							 | 
						|
								        if (error.response) {
							 | 
						|
								            console.error("Status:", error.response.status);
							 | 
						|
								            console.error("Headers:", error.response.headers);
							 | 
						|
								            console.error("Data:", JSON.stringify(error.response.data, null, 2));
							 | 
						|
								        } else if (error.request) {
							 | 
						|
								            console.error("No response received");
							 | 
						|
								            console.error(error.request);
							 | 
						|
								        } else {
							 | 
						|
								            console.error("Error setting up request:", error.message);
							 | 
						|
								        }
							 | 
						|
								        console.error();
							 | 
						|
								
							 | 
						|
								        const registrationData = {
							 | 
						|
								            activeDid,
							 | 
						|
								            jwtToken,
							 | 
						|
								            response: { error: `Error submitting claim: ${error.message}` }
							 | 
						|
								        };
							 | 
						|
								        await writeFile(
							 | 
						|
								            join(GENERATED_DIR, 'registration.json'),
							 | 
						|
								            JSON.stringify(registrationData, null, 2)
							 | 
						|
								        );
							 | 
						|
								        return { error: `Error submitting claim: ${error.message}` };
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Fetch claim details from the endorser service
							 | 
						|
								 */
							 | 
						|
								async function fetchClaim(
							 | 
						|
								    claimId: string,
							 | 
						|
								    activeDid: string,
							 | 
						|
								    privateKeyHex: string,
							 | 
						|
								    apiServer: string = API_SERVER
							 | 
						|
								): Promise<any> {
							 | 
						|
								    // Create authentication JWT
							 | 
						|
								    const authPayload = {
							 | 
						|
								        intent: 'claim.read',
							 | 
						|
								        claimId: claimId
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    // Sign with the active DID (not endorser DID)
							 | 
						|
								    const authToken = await createEndorserJwt(
							 | 
						|
								        activeDid,
							 | 
						|
								        privateKeyHex,
							 | 
						|
								        authPayload
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    try {
							 | 
						|
								        const response = await axios.get(
							 | 
						|
								            `${apiServer}/api/claim/byHandle/${claimId}`,
							 | 
						|
								            {
							 | 
						|
								                headers: {
							 | 
						|
								                    'Authorization': `Bearer ${authToken}`,
							 | 
						|
								                    'Content-Type': 'application/json'
							 | 
						|
								                }
							 | 
						|
								            }
							 | 
						|
								        );
							 | 
						|
								
							 | 
						|
								        const claimData = response.data;
							 | 
						|
								        
							 | 
						|
								        // Save claim data
							 | 
						|
								        try {
							 | 
						|
								            await writeFile(
							 | 
						|
								                join(GENERATED_DIR, 'claim_details.json'),
							 | 
						|
								                JSON.stringify({
							 | 
						|
								                    claim_id: claimId,
							 | 
						|
								                    active_did: activeDid,
							 | 
						|
								                    response: claimData
							 | 
						|
								                }, null, 2)
							 | 
						|
								            );
							 | 
						|
								        } catch (err) {
							 | 
						|
								            console.error('Error saving claim data:', err);
							 | 
						|
								        }
							 | 
						|
								        
							 | 
						|
								        return claimData;
							 | 
						|
								
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								        console.error("\nError fetching claim:");
							 | 
						|
								        if (error.response) {
							 | 
						|
								            console.error("Status:", error.response.status);
							 | 
						|
								            console.error("Data:", JSON.stringify(error.response.data, null, 2));
							 | 
						|
								        } else {
							 | 
						|
								            console.error(error.message);
							 | 
						|
								        }
							 | 
						|
								        // Save error state
							 | 
						|
								        try {
							 | 
						|
								            await writeFile(
							 | 
						|
								                join(GENERATED_DIR, 'claim_details.json'),
							 | 
						|
								                JSON.stringify({
							 | 
						|
								                    claim_id: claimId,
							 | 
						|
								                    active_did: activeDid,
							 | 
						|
								                    error: error.message || 'Unknown error',
							 | 
						|
								                    response: error.response?.data
							 | 
						|
								                }, null, 2)
							 | 
						|
								            );
							 | 
						|
								        } catch (writeErr) {
							 | 
						|
								            console.error('Error saving claim error:', writeErr);
							 | 
						|
								        }
							 | 
						|
								        throw error;
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Generate test environment data for deeplink testing
							 | 
						|
								 */
							 | 
						|
								async function generateTestEnv(
							 | 
						|
								    identity: IIdentifier,
							 | 
						|
								    jwtToken: string,
							 | 
						|
								    apiServer: string = API_SERVER
							 | 
						|
								): Promise<void> {
							 | 
						|
								    // Create test data structure
							 | 
						|
								    const testEnvData = {
							 | 
						|
								        CONTACT1_DID: identity.did,
							 | 
						|
								        CONTACT1_KEY: identity.keys[0].privateKeyHex,
							 | 
						|
								        CONTACT2_DID: `did:ethr:${Wallet.createRandom().address}`, // Generate random DID for contact 2
							 | 
						|
								        CONTACT2_KEY: Wallet.createRandom().privateKey.substring(2), // Remove '0x'
							 | 
						|
								        ISSUER_DID: ENDORSER_DID,
							 | 
						|
								        ISSUER_KEY: ENDORSER_PRIVATE_KEY,
							 | 
						|
								        TEST_JWT: jwtToken,
							 | 
						|
								        API_SERVER: apiServer
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    // Write test environment variables
							 | 
						|
								    const envContent = Object.entries(testEnvData)
							 | 
						|
								        .map(([key, value]) => `export ${key}="${value}"`)
							 | 
						|
								        .join('\n');
							 | 
						|
								    
							 | 
						|
								    await writeFile(
							 | 
						|
								        join(GENERATED_DIR, 'test-env.sh'),
							 | 
						|
								        envContent + '\n',
							 | 
						|
								        'utf-8'
							 | 
						|
								    );
							 | 
						|
								
							 | 
						|
								    // Write test contacts data
							 | 
						|
								    const contactsData = {
							 | 
						|
								        contacts: [
							 | 
						|
								            { did: testEnvData.CONTACT1_DID, name: 'Test Contact 1' },
							 | 
						|
								            { did: testEnvData.CONTACT2_DID, name: 'Test Contact 2' }
							 | 
						|
								        ]
							 | 
						|
								    };
							 | 
						|
								
							 | 
						|
								    await writeFile(
							 | 
						|
								        join(GENERATED_DIR, 'contacts.json'),
							 | 
						|
								        JSON.stringify(contactsData, null, 2),
							 | 
						|
								        'utf-8'
							 | 
						|
								    );
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								/**
							 | 
						|
								 * Main execution flow
							 | 
						|
								 */
							 | 
						|
								async function main() {
							 | 
						|
								    // Initialize directories first
							 | 
						|
								    await initializeDirectories();
							 | 
						|
								    
							 | 
						|
								    try {
							 | 
						|
								        // Create a new DID
							 | 
						|
								        const identity = await initializeAccount();
							 | 
						|
								        const activeDid = identity.did;
							 | 
						|
								        const privateKeyHex = identity.keys[0].privateKeyHex;
							 | 
						|
								
							 | 
						|
								        // Register the DID
							 | 
						|
								        const result = await register(activeDid, privateKeyHex);
							 | 
						|
								        console.log("Registration result:", result);
							 | 
						|
								
							 | 
						|
								        // If registration was successful, fetch the claim details
							 | 
						|
								        if (result.success && result.claimId) {
							 | 
						|
								            console.log("\nFetching claim details...");
							 | 
						|
								            const claimDetails = await fetchClaim(
							 | 
						|
								                result.claimId,
							 | 
						|
								                activeDid,
							 | 
						|
								                privateKeyHex
							 | 
						|
								            );
							 | 
						|
								            console.log("\nClaim Details:");
							 | 
						|
								            console.log(JSON.stringify(claimDetails, null, 2));
							 | 
						|
								
							 | 
						|
								            // Generate test environment data
							 | 
						|
								            await generateTestEnv(identity, result.claimId);
							 | 
						|
								        }
							 | 
						|
								    } catch (error: any) {
							 | 
						|
								        console.error("Error:", error.message);
							 | 
						|
								    }
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Run if called directly
							 | 
						|
								if (require.main === module) {
							 | 
						|
								    main().catch(console.error);
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								export {
							 | 
						|
								    generateMnemonic,
							 | 
						|
								    deriveAddress,
							 | 
						|
								    newIdentifier,
							 | 
						|
								    initializeAccount,
							 | 
						|
								    createEndorserJwt,
							 | 
						|
								    register,
							 | 
						|
								    fetchClaim,
							 | 
						|
								    generateTestEnv
							 | 
						|
								}; 
							 |