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 | |
| }; 
 |