UNPKG

agent-file

Version:

Self-contained HTML agent format with built-in security

105 lines (104 loc) 3.98 kB
/** * AgentFile Security Utilities * Minimal implementation of integrity, permissions, and validation */ // ============================================================================ // INTEGRITY (SHA-256) // ============================================================================ export async function sha256(text) { const buffer = new TextEncoder().encode(text); const hash = await crypto.subtle.digest('SHA-256', buffer); return Array.from(new Uint8Array(hash)) .map(b => b.toString(16).padStart(2, '0')) .join(''); } export async function generateHashes(manifest, code) { return { manifest: `sha256-${await sha256(manifest)}`, code: `sha256-${await sha256(code)}` }; } export async function verifyHashes(doc) { const expectedManifest = doc.querySelector('meta[name="agent-hash-manifest"]')?.getAttribute('content'); const expectedCode = doc.querySelector('meta[name="agent-hash-code"]')?.getAttribute('content'); if (!expectedManifest || !expectedCode) return true; // Optional const manifest = doc.getElementById('agent-manifest')?.textContent || ''; const code = doc.getElementById('agent-code')?.textContent || ''; const actualManifest = `sha256-${await sha256(manifest)}`; const actualCode = `sha256-${await sha256(code)}`; return expectedManifest === actualManifest && expectedCode === actualCode; } export function checkPermission(permissions, action, target) { if (!permissions) return false; if (action === 'fetch' && target) { if (!permissions.network) return false; const url = new URL(target); return permissions.network.some(d => url.hostname === d || url.hostname.endsWith('.' + d) || d === '*'); } if (action === 'storage') return permissions.storage === true; if (action === 'code') return permissions.code === true; return false; } // ============================================================================ // VALIDATION // ============================================================================ export function validate(manifest) { const errors = []; if (!manifest.id) errors.push('Missing id'); if (!manifest.name) errors.push('Missing name'); if (!manifest.version) errors.push('Missing version'); if (manifest.id && !/^[a-z0-9-]+$/.test(manifest.id)) { errors.push('Invalid id format'); } if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version)) { errors.push('Invalid version format'); } return errors; } // ============================================================================ // VERSION CHECKING // ============================================================================ export function checkFeature(feature) { if (typeof window === 'undefined') return false; switch (feature) { case 'crypto': return typeof crypto !== 'undefined' && !!crypto.subtle; case 'indexeddb': return typeof indexedDB !== 'undefined'; case 'fetch': return typeof fetch !== 'undefined'; default: return false; } } export function checkVersion(current, required) { const parseVer = (v) => parseInt(v.match(/\d+/)?.[0] || '0'); const currentNum = parseVer(current); // Parse operators: >=90, <100, etc. const checks = required.match(/([><=]+)(\d+)/g) || []; return checks.every(check => { const match = check.match(/([><=]+)(\d+)/); if (!match) return true; const [, op, ver] = match; const verNum = parseInt(ver); if (op === '>=') return currentNum >= verNum; if (op === '>') return currentNum > verNum; if (op === '<=') return currentNum <= verNum; if (op === '<') return currentNum < verNum; return currentNum === verNum; }); }