forked from jsnbuchanan/crowd-funder-for-time-pwa
Changes: - Update SQL query to use registration table instead of accounts - Add proper column names for registration table schema - Add issuanceDate sorting for latest admin DID - Improve error messages for database queries - Add TypeScript types for database row results This fixes DID generation by using the correct table schema from the endorser database.
257 lines
7.9 KiB
TypeScript
257 lines
7.9 KiB
TypeScript
/**
|
|
* DID Generator Script
|
|
* @author Matthew Raymer
|
|
*
|
|
* This script generates and registers Decentralized Identifiers (DIDs) with admin authorization.
|
|
* It supports the creation of Ethereum-based DIDs (did:ethr) and handles the complete
|
|
* registration flow with the endorser.ch API.
|
|
*
|
|
* Features:
|
|
* - Ethereum keypair generation using ethers.js
|
|
* - JWT creation and signing using did-jwt
|
|
* - DID registration with admin authorization
|
|
* - Detailed error handling and logging
|
|
* - Command-line interface for admin DID input
|
|
*
|
|
* Dependencies:
|
|
* did-jwt: For JWT creation and signing
|
|
* ethers: For Ethereum account operations
|
|
* node-fetch: For API communication
|
|
*/
|
|
|
|
/// <reference types="node" />
|
|
// Add at the top of your file to ignore dom types
|
|
import * as didJwt from 'did-jwt';
|
|
import { ethers } from 'ethers';
|
|
import fetch from 'node-fetch';
|
|
import { Database } from 'sqlite3';
|
|
import { homedir } from 'os';
|
|
import { join } from 'path';
|
|
import { existsSync } from 'fs';
|
|
import { program } from 'commander';
|
|
|
|
/**
|
|
* Result interface for DID creation process
|
|
*/
|
|
interface DIDCreationResult {
|
|
did: string; // The generated DID
|
|
privateKey: string; // Private key without 0x prefix
|
|
publicKey: string; // Public key with 0x prefix
|
|
isValid: boolean; // Validation status
|
|
jwt: string; // Signed JWT for registration
|
|
}
|
|
|
|
/**
|
|
* Result interface for DID registration attempt
|
|
*/
|
|
interface RegistrationResult {
|
|
success: boolean; // Registration success status
|
|
error?: string; // Optional error message
|
|
response?: any; // Optional API response data
|
|
}
|
|
|
|
/**
|
|
* Creates and validates a new DID with admin authorization
|
|
*
|
|
* Workflow:
|
|
* 1. Validates admin DID input
|
|
* 2. Generates Ethereum keypair
|
|
* 3. Creates registration claim
|
|
* 4. Signs claim as JWT
|
|
*
|
|
* @param adminDid - Administrator DID for authorization
|
|
* @returns Promise<DIDCreationResult> - Generated DID details and JWT
|
|
* @throws Error if admin DID is missing
|
|
*/
|
|
async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult> {
|
|
if (!adminDid) {
|
|
throw new Error('Admin DID is required for registration');
|
|
}
|
|
|
|
console.log('Using admin DID:', adminDid);
|
|
|
|
// 1. Generate keypair
|
|
console.log('Generating new keypair...');
|
|
const wallet = ethers.Wallet.createRandom();
|
|
const did = `did:ethr:${wallet.address}`;
|
|
const privateKey = wallet.privateKey.slice(2);
|
|
const publicKey = wallet.publicKey;
|
|
|
|
// Create registration claim with admin as agent
|
|
const registerClaim = {
|
|
"@context": "https://schema.org",
|
|
"@type": "RegisterAction",
|
|
agent: { did: adminDid },
|
|
participant: { did: did },
|
|
object: "endorser.ch"
|
|
};
|
|
|
|
const vcPayload = {
|
|
iat: Math.floor(Date.now() / 1000),
|
|
exp: Math.floor(Date.now() / 1000) + 300,
|
|
sub: "RegisterAction",
|
|
vc: {
|
|
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
|
type: ["VerifiableCredential"],
|
|
credentialSubject: registerClaim
|
|
}
|
|
};
|
|
|
|
console.log('\nGenerated DID Details:');
|
|
console.log('----------------------');
|
|
console.log('DID:', did);
|
|
console.log('Admin DID:', adminDid);
|
|
console.log('Address:', wallet.address);
|
|
console.log('Private Key:', wallet.privateKey);
|
|
console.log('Public Key:', wallet.publicKey);
|
|
|
|
console.log('\nDebug Details:');
|
|
console.log('-------------');
|
|
console.log('Private Key (hex):', privateKey); // Should be without 0x
|
|
console.log('Public Key (hex):', publicKey); // Should be with 0x
|
|
console.log('Header:', {
|
|
typ: "JWT",
|
|
alg: "ES256K"
|
|
});
|
|
console.log('Payload:', vcPayload);
|
|
|
|
// Create and sign JWT
|
|
console.log('\nCreating JWT...');
|
|
const signer = didJwt.SimpleSigner(privateKey);
|
|
const jwt = await didJwt.createJWT(vcPayload, {
|
|
issuer: did,
|
|
signer: signer
|
|
});
|
|
|
|
console.log('\nJWT Parts:');
|
|
const [header, payload, signature] = jwt.split('.');
|
|
console.log('Header (base64):', header);
|
|
console.log('Payload (base64):', payload);
|
|
console.log('Signature (base64):', signature);
|
|
|
|
return { did, privateKey, publicKey, isValid: true, jwt };
|
|
}
|
|
|
|
/**
|
|
* Registers a DID with the endorser.ch API
|
|
*
|
|
* Workflow:
|
|
* 1. Submits JWT to API endpoint
|
|
* 2. Processes response
|
|
* 3. Handles success/error cases
|
|
*
|
|
* @param jwt - Signed JWT for registration
|
|
* @returns Promise<RegistrationResult> - Registration result
|
|
*/
|
|
async function registerDID(jwt: string): Promise<RegistrationResult> {
|
|
try {
|
|
const response = await fetch('https://api.endorser.ch/api/v2/claim', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ jwtEncoded: jwt }),
|
|
});
|
|
|
|
console.log(`\nServer Response Status: ${response.status}`);
|
|
const responseText = await response.text();
|
|
console.log(`Server Response Body: ${responseText}`);
|
|
|
|
if (response.ok) {
|
|
return {
|
|
success: true,
|
|
response: JSON.parse(responseText)
|
|
};
|
|
} else {
|
|
try {
|
|
const errorJson = JSON.parse(responseText);
|
|
const errorMsg = errorJson.error?.message || 'Unknown error';
|
|
return {
|
|
success: false,
|
|
error: `Registration failed (${response.status}): ${errorMsg}`,
|
|
response: errorJson
|
|
};
|
|
} catch {
|
|
return {
|
|
success: false,
|
|
error: `Registration failed (${response.status}): ${responseText}`,
|
|
response: responseText
|
|
};
|
|
}
|
|
}
|
|
} catch (e) {
|
|
return {
|
|
success: false,
|
|
error: `Request failed: ${e}`
|
|
};
|
|
}
|
|
}
|
|
|
|
async function getRootDidFromDb(dbPath?: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const defaultPath = join(homedir(), '.endorser', 'accounts.db');
|
|
const path = dbPath || defaultPath;
|
|
|
|
if (!existsSync(path)) {
|
|
reject(new Error(`Database not found at ${path}`));
|
|
return;
|
|
}
|
|
|
|
const db = new Database(path);
|
|
db.get(
|
|
'SELECT issuer as did FROM registration WHERE type = "RegisterAction" ORDER BY issuanceDate DESC LIMIT 1',
|
|
(err, row: { did: string }) => {
|
|
db.close();
|
|
if (err) reject(err);
|
|
else if (!row) reject(new Error('No admin DID found in registration table'));
|
|
else resolve(row.did);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
// Command line handling
|
|
program
|
|
.option('--admin-did <did>', 'Admin DID (e.g., did:ethr:0x0000...)')
|
|
.option('--db-path <path>', 'Path to SQLite database (e.g., ../endorser-ch-test-local.sqlite3)')
|
|
.parse(process.argv);
|
|
|
|
const options = program.opts();
|
|
|
|
console.log('Starting DID Generation...\n');
|
|
|
|
(async () => {
|
|
let adminDid = options.adminDid;
|
|
if (!adminDid && options.dbPath) {
|
|
try {
|
|
adminDid = await getRootDidFromDb(options.dbPath);
|
|
console.log(`Found root DID in database: ${adminDid}`);
|
|
} catch (e) {
|
|
console.error(`Error: ${e.message}`);
|
|
console.error('Please provide --admin-did argument');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const result = await createAndValidateDID(adminDid);
|
|
console.log('\nSuccessfully generated DID with admin authorization!');
|
|
console.log('Registration JWT:', result.jwt.substring(0, 50) + '...');
|
|
|
|
console.log('\nAttempting registration...');
|
|
const registrationResult = await registerDID(result.jwt);
|
|
if (registrationResult.success) {
|
|
console.log('Registration successful!');
|
|
console.log('Response:', JSON.stringify(registrationResult.response, null, 2));
|
|
} else {
|
|
console.log('Registration failed!');
|
|
console.log('Error:', registrationResult.error);
|
|
if (registrationResult.response) {
|
|
console.log('Full response:', JSON.stringify(registrationResult.response, null, 2));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('\nError:', error);
|
|
process.exit(1);
|
|
}
|
|
})(); |