UNPKG

@kya-os/mcp-i

Version:

The TypeScript MCP framework with identity features built-in

318 lines (317 loc) 12.5 kB
"use strict"; /** * Identity Management System for XMCP-I Runtime * * Handles identity loading, generation, and validation for both development * and production environments according to requirements 4.1-4.4. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.extractAgentSlug = exports.extractAgentId = exports.defaultIdentityManager = exports.IdentityManager = exports.IDENTITY_ERRORS = void 0; exports.ensureIdentity = ensureIdentity; const promises_1 = require("fs/promises"); const fs_1 = require("fs"); const path_1 = require("path"); const jose_1 = require("jose"); const crypto_1 = require("crypto"); /** * Error codes for identity management */ exports.IDENTITY_ERRORS = { ENOIDENTITY: "XMCP_I_ENOIDENTITY", ECONFIG: "XMCP_I_ECONFIG", }; /** * Identity management class */ class IdentityManager { config; cachedIdentity; constructor(config = { environment: "development" }) { // Resolve devIdentityPath to absolute if needed const path = require("path"); let resolvedDevIdentityPath = config.devIdentityPath || ".mcpi/identity.json"; // If path is relative, resolve it relative to process.cwd() (the project root when the server starts) if (!path.isAbsolute(resolvedDevIdentityPath)) { resolvedDevIdentityPath = path.resolve(process.cwd(), resolvedDevIdentityPath); } this.config = { privacyMode: false, // Single public DID default ...config, devIdentityPath: resolvedDevIdentityPath, }; // Debug: log the actual config being used if (this.config.debug) { console.error("[IdentityManager] Constructed with config:", { environment: this.config.environment, devIdentityPath: this.config.devIdentityPath, }); } } /** * Load or generate agent identity * Requirements: 4.1, 4.2, 4.3, 4.4 */ async ensureIdentity() { if (this.cachedIdentity) { return this.cachedIdentity; } if (this.config.environment === "development") { this.cachedIdentity = await this.loadOrGenerateDevIdentity(); } else { this.cachedIdentity = await this.loadProdIdentity(); } return this.cachedIdentity; } /** * Load development identity from .mcpi/identity.json or generate new one * Requirement: 4.1 */ async loadOrGenerateDevIdentity() { const identityPath = this.config.devIdentityPath; try { if ((0, fs_1.existsSync)(identityPath)) { const content = await (0, promises_1.readFile)(identityPath, "utf-8"); const devIdentity = JSON.parse(content); // Handle backward compatibility: support both 'kid' and old 'keyId' format let kid = devIdentity.kid || devIdentity.keyId; // If we have old keyId format, migrate to multibase format if (devIdentity.keyId && !devIdentity.kid) { // Check if it's the old format (key-[hex]) if (kid.startsWith('key-')) { // Generate new multibase kid from public key kid = this.generateMultibaseKid(devIdentity.publicKey); // Save migrated identity const migratedIdentity = { did: devIdentity.did, kid, privateKey: devIdentity.privateKey, publicKey: devIdentity.publicKey, createdAt: devIdentity.createdAt, lastRotated: devIdentity.lastRotated, }; await this.saveDevIdentity(migratedIdentity); console.error(`✅ Migrated identity to new multibase kid format: ${kid}`); return migratedIdentity; } } return { did: devIdentity.did, kid, privateKey: devIdentity.privateKey, publicKey: devIdentity.publicKey, createdAt: devIdentity.createdAt, lastRotated: devIdentity.lastRotated, }; } } catch (error) { // If file exists but is corrupted, we'll regenerate console.warn(`Warning: Could not load identity from ${identityPath}, generating new one`, error instanceof Error ? error.message : error); } // Generate new identity return await this.generateDevIdentity(); } /** * Generate new development identity * Requirements: 4.1, 4.4 */ async generateDevIdentity() { // Generate Ed25519 keypair const keyPair = await (0, jose_1.generateKeyPair)("EdDSA", { crv: "Ed25519" }); // Export keys to JWK format const privateKeyJwk = await (0, jose_1.exportJWK)(keyPair.privateKey); if (!privateKeyJwk.x || !privateKeyJwk.d) { throw new Error("Failed to generate Ed25519 keypair"); } const privateKey = Buffer.from(privateKeyJwk.d, "base64url").toString("base64"); const publicKey = Buffer.from(privateKeyJwk.x, "base64url").toString("base64"); // Generate multibase-encoded key ID const kid = this.generateMultibaseKid(publicKey); // Generate DID (for dev, use localhost) // Extract a short identifier for the DID path (first 8 chars of hash for readability) const shortId = (0, crypto_1.createHash)("sha256").update(publicKey).digest("hex").substring(0, 8); const did = `did:web:localhost:3000:agents:${shortId}`; const now = new Date().toISOString(); const identity = { did, kid: kid, // Using kid but keeping field name for now for compatibility privateKey, publicKey, createdAt: now, }; // Save to file await this.saveDevIdentity(identity); return identity; } /** * Generate multibase-encoded key identifier (z-prefix base58btc) */ generateMultibaseKid(base64PublicKey) { const publicKeyBytes = Buffer.from(base64PublicKey, "base64"); // Ed25519 public key prefix (0xed01) + key bytes const prefixedKey = Buffer.concat([ Buffer.from([0xed, 0x01]), // Ed25519 multicodec prefix publicKeyBytes, ]); // Convert to base58btc const base58 = this.encodeBase58(prefixedKey); return `z${base58}`; // 'z' prefix indicates base58btc } /** * Simple base58 encoding (matching well-known.ts implementation) */ encodeBase58(buffer) { const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; let num = BigInt("0x" + buffer.toString("hex")); let result = ""; while (num > 0n) { const remainder = num % 58n; result = alphabet[Number(remainder)] + result; num = num / 58n; } // Handle leading zeros for (let i = 0; i < buffer.length && buffer[i] === 0; i++) { result = "1" + result; } return result; } /** * Save development identity to .mcpi/identity.json */ async saveDevIdentity(identity) { const identityPath = this.config.devIdentityPath; // Ensure directory exists await (0, promises_1.mkdir)((0, path_1.dirname)(identityPath), { recursive: true }); // Use 'kid' in the saved file (conforming to new schema) const devIdentity = { version: "1.0", did: identity.did, kid: identity.kid, // Save as 'kid' in file privateKey: identity.privateKey, publicKey: identity.publicKey, createdAt: identity.createdAt, lastRotated: identity.lastRotated, }; await (0, promises_1.writeFile)(identityPath, JSON.stringify(devIdentity, null, 2), { mode: 0o600, }); console.error(`✅ Identity saved to ${identityPath}`); console.error(` DID: ${identity.did}`); console.error(` Key ID: ${identity.kid}`); } /** * Load production identity from environment variables * Requirements: 4.2, 4.3 */ async loadProdIdentity() { const requiredEnvVars = [ "AGENT_PRIVATE_KEY", "AGENT_KEY_ID", "AGENT_DID", "KYA_VOUCHED_API_KEY", ]; const missing = []; const env = {}; for (const varName of requiredEnvVars) { const value = process.env[varName]; if (!value) { missing.push(varName); } else { env[varName] = value; } } if (missing.length > 0) { const error = new Error(`Missing required environment variables for production identity: ${missing.join(", ")}\n` + "Required variables:\n" + " AGENT_PRIVATE_KEY - Base64-encoded Ed25519 private key\n" + " AGENT_KEY_ID - Key identifier\n" + " AGENT_DID - Agent DID\n" + " KYA_VOUCHED_API_KEY - Know-That-AI API key"); error.code = exports.IDENTITY_ERRORS.ENOIDENTITY; throw error; } // For production, we expect the private key to be base64-encoded // We'll derive a placeholder public key since we don't need it for signing // In a real implementation, the public key would be derived properly const privateKeyBuffer = Buffer.from(env.AGENT_PRIVATE_KEY, "base64"); // Generate a deterministic public key placeholder from the private key const publicKey = (0, crypto_1.createHash)("sha256") .update(privateKeyBuffer) .digest("base64"); return { did: env.AGENT_DID, kid: env.AGENT_KEY_ID, privateKey: env.AGENT_PRIVATE_KEY, publicKey, createdAt: new Date().toISOString(), // We don't have creation time in prod }; } /** * Validate identity configuration */ async validateIdentity(identity) { try { // Basic validation if (!identity.did || !identity.kid || !identity.privateKey || !identity.publicKey) { return false; } // Validate DID format if (!identity.did.startsWith("did:")) { return false; } // Validate key format (base64) Buffer.from(identity.privateKey, "base64"); Buffer.from(identity.publicKey, "base64"); return true; } catch { return false; } } /** * Clear cached identity (useful for testing) */ clearCache() { this.cachedIdentity = undefined; } /** * Get current configuration */ getConfig() { return { ...this.config }; } } exports.IdentityManager = IdentityManager; /** * Default identity manager instance */ exports.defaultIdentityManager = new IdentityManager(); /** * Extract agent ID from DID * @deprecated Use extractAgentId from @kya-os/mcp-i-core/utils/did-helpers instead * This re-export is maintained for backward compatibility */ var did_helpers_1 = require("@kya-os/mcp-i-core/utils/did-helpers"); Object.defineProperty(exports, "extractAgentId", { enumerable: true, get: function () { return did_helpers_1.extractAgentId; } }); /** * Extract agent slug from DID * @deprecated Use extractAgentSlug from @kya-os/mcp-i-core/utils/did-helpers instead * This re-export is maintained for backward compatibility */ var did_helpers_2 = require("@kya-os/mcp-i-core/utils/did-helpers"); Object.defineProperty(exports, "extractAgentSlug", { enumerable: true, get: function () { return did_helpers_2.extractAgentSlug; } }); /** * Convenience function to ensure identity */ async function ensureIdentity(config) { if (config) { const manager = new IdentityManager(config); return manager.ensureIdentity(); } return exports.defaultIdentityManager.ensureIdentity(); }