{"version":3,"file":"delegation-store.mjs","names":[],"sources":["../../../src/services/delegation-store.ts"],"sourcesContent":["/**\n * Delegation Store — File-based persistence for full SignedDelegation structs.\n *\n * The DelegationInfo on Policy stores lightweight metadata (hash, addresses,\n * status). This store persists the complete signed delegation including caveats\n * and signature bytes — required for on-chain redemption and revocation.\n *\n * Layout:\n *   ~/.openclawnch/delegations/<policyId>.json\n *\n * Each file contains a StoredDelegation: the full SignedDelegation struct\n * plus chainId and metadata. Bigints are serialized as hex strings.\n *\n * Follows the same patterns as policy-store.ts: atomic writes, 0o600\n * permissions, singleton instance, in-memory cache.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { createCipheriv, createDecipheriv, randomBytes, createHash } from 'node:crypto';\nimport type { Address, Hex } from 'viem';\nimport type { SignedDelegation, Caveat } from './delegation-types.js';\n\nconst HOME = process.env.HOME ?? '/home/openclawnch';\nconst DELEGATIONS_DIR = join(HOME, '.openclawnch', 'delegations');\n\n/** File permissions (owner read/write only). */\nconst FILE_MODE = 0o600;\n\n// ─── Serialized Format ──────────────────────────────────────────────────\n// bigint fields (salt) are stored as hex strings for JSON compatibility.\n\ninterface StoredDelegation {\n  /** The full signed delegation struct. */\n  delegate: Address;\n  delegator: Address;\n  authority: Hex;\n  caveats: Caveat[];\n  /** Salt as hex string (bigint serialized). */\n  salt: string;\n  signature: Hex;\n  /** Chain ID this delegation targets. */\n  chainId: number;\n  /** ISO timestamp when stored. */\n  storedAt: string;\n  /** Policy ID this delegation belongs to. */\n  policyId: string;\n}\n\n// ─── Ensure directory exists ────────────────────────────────────────────\n\nfunction ensureDir(): void {\n  if (!existsSync(DELEGATIONS_DIR)) {\n    mkdirSync(DELEGATIONS_DIR, { recursive: true });\n  }\n}\n\nfunction delegationPath(policyId: string): string {\n  // Sanitize policyId for filesystem safety\n  const safe = policyId.replace(/[^a-zA-Z0-9_-]/g, '_');\n  return join(DELEGATIONS_DIR, `${safe}.json`);\n}\n\n// ─── Atomic Write ───────────────────────────────────────────────────────\n\nfunction atomicWrite(targetPath: string, data: string): void {\n  const tmpPath = targetPath + '.tmp.' + Date.now();\n  writeFileSync(tmpPath, data, { mode: FILE_MODE });\n  renameSync(tmpPath, targetPath);\n}\n\n// ─── Encryption (AES-256-GCM) ───────────────────────────────────────────\n// Encrypts delegation JSON at rest. Key derived from DELEGATION_STORE_KEY\n// env var or wallet address (prevents casual filesystem reads).\n\nfunction getEncryptionKey(): Buffer | null {\n  const envKey = process.env.DELEGATION_STORE_KEY;\n  if (envKey && envKey.length > 0) {\n    return createHash('sha256').update(envKey).digest();\n  }\n  // No encryption configured — store plaintext (backward compatible)\n  return null;\n}\n\nfunction encrypt(plaintext: string): string {\n  const key = getEncryptionKey();\n  if (!key) return plaintext;\n\n  const iv = randomBytes(12);\n  const cipher = createCipheriv('aes-256-gcm', key, iv);\n  const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n  const tag = cipher.getAuthTag();\n  // Format: ENC:base64(iv + tag + ciphertext)\n  const combined = Buffer.concat([iv, tag, encrypted]);\n  return 'ENC:' + combined.toString('base64');\n}\n\nfunction decrypt(data: string): string {\n  if (!data.startsWith('ENC:')) return data; // plaintext (unencrypted legacy)\n\n  const key = getEncryptionKey();\n  if (!key) return data; // no key configured — return raw (will fail JSON.parse)\n\n  const combined = Buffer.from(data.slice(4), 'base64');\n  const iv = combined.subarray(0, 12);\n  const tag = combined.subarray(12, 28);\n  const ciphertext = combined.subarray(28);\n  const decipher = createDecipheriv('aes-256-gcm', key, iv);\n  decipher.setAuthTag(tag);\n  return decipher.update(ciphertext) + decipher.final('utf8');\n}\n\n// ─── Conversion Helpers ─────────────────────────────────────────────────\n\nfunction toStored(\n  delegation: SignedDelegation,\n  chainId: number,\n  policyId: string,\n): StoredDelegation {\n  return {\n    delegate: delegation.delegate,\n    delegator: delegation.delegator,\n    authority: delegation.authority,\n    caveats: delegation.caveats.map(c => ({\n      enforcer: c.enforcer,\n      terms: c.terms,\n      args: c.args,\n    })),\n    salt: '0x' + delegation.salt.toString(16),\n    signature: delegation.signature,\n    chainId,\n    policyId,\n    storedAt: new Date().toISOString(),\n  };\n}\n\nfunction fromStored(stored: StoredDelegation): SignedDelegation {\n  return {\n    delegate: stored.delegate as Address,\n    delegator: stored.delegator as Address,\n    authority: stored.authority as Hex,\n    caveats: stored.caveats.map(c => ({\n      enforcer: c.enforcer as Address,\n      terms: c.terms as Hex,\n      args: c.args as Hex,\n    })),\n    salt: BigInt(stored.salt),\n    signature: stored.signature as Hex,\n  };\n}\n\n// ─── DelegationStore Class ──────────────────────────────────────────────\n\nexport class DelegationStore {\n  private cache = new Map<string, StoredDelegation>();\n\n  /** Save a signed delegation for a policy. Overwrites any existing. */\n  save(delegation: SignedDelegation, chainId: number, policyId: string): void {\n    ensureDir();\n    const stored = toStored(delegation, chainId, policyId);\n    this.cache.set(policyId, stored);\n    atomicWrite(delegationPath(policyId), encrypt(JSON.stringify(stored, null, 2)));\n  }\n\n  /** Load a signed delegation by policy ID. Returns null if not found. */\n  load(policyId: string): { delegation: SignedDelegation; chainId: number } | null {\n    // Check cache first\n    const cached = this.cache.get(policyId);\n    if (cached) {\n      return { delegation: fromStored(cached), chainId: cached.chainId };\n    }\n\n    // Try disk\n    const path = delegationPath(policyId);\n    if (!existsSync(path)) return null;\n\n    try {\n      const raw = readFileSync(path, 'utf8');\n      const data = JSON.parse(decrypt(raw)) as StoredDelegation;\n      this.cache.set(policyId, data);\n      return { delegation: fromStored(data), chainId: data.chainId };\n    } catch (err) {\n      // H4/H5: Don't delete corrupted files — rename to .corrupt for recovery.\n      // The on-chain delegation still exists; deleting the file makes it\n      // irrevocable through this tool.\n      const msg = err instanceof Error ? err.message : String(err);\n      console.error(`[delegation-store] Failed to load ${policyId}: ${msg}. File preserved as .corrupt`);\n      try {\n        const corruptPath = path + '.corrupt.' + Date.now();\n        renameSync(path, corruptPath);\n      } catch { /* best effort */ }\n      return null;\n    }\n  }\n\n  /** Check if a delegation exists for a policy. */\n  has(policyId: string): boolean {\n    if (this.cache.has(policyId)) return true;\n    return existsSync(delegationPath(policyId));\n  }\n\n  /** Delete a stored delegation. */\n  delete(policyId: string): boolean {\n    this.cache.delete(policyId);\n    const path = delegationPath(policyId);\n    if (existsSync(path)) {\n      try { unlinkSync(path); return true; } catch { return false; }\n    }\n    return false;\n  }\n\n  /** Clear all caches (for testing). */\n  reset(): void {\n    this.cache.clear();\n  }\n}\n\n// ─── Singleton ──────────────────────────────────────────────────────────\n\nlet _instance: DelegationStore | null = null;\n\nexport function getDelegationStore(): DelegationStore {\n  if (!_instance) _instance = new DelegationStore();\n  return _instance;\n}\n\nexport function resetDelegationStore(): void {\n  _instance?.reset();\n  _instance = null;\n}\n"],"mappings":"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"}