@kya-os/mcp-i
Version:
The TypeScript MCP framework with identity features built-in
318 lines (317 loc) • 12.5 kB
JavaScript
;
/**
* 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();
}