UNPKG

mcard-js

Version:

MCard - Content-addressable storage with cryptographic hashing, handle resolution, and vector search for Node.js and browsers

573 lines 21.5 kB
/** * VCard Model (Application Plane) - Petri Net TOKEN / PRE-CONDITION CARRIER * * This module defines VCard, the sovereign decision layer in the MVP Cards architecture. * VCard is the Applicative that manages all side effects and authorization. * * ## Category Theory Role: APPLICATIVE * * VCard is the **Applicative** in the MVP Cards categorical hierarchy: * - **MCard (Monad)**: Data container with unit and bind * - **PCard (Functor)**: Pure transformation * - **VCard (Applicative)**: Context-aware application `<*> :: F (a -> b) -> F a -> F b` * * The Applicative nature means VCard can encode **static pre-conditions**: * the authorization graph is known ahead of time, while actual execution * depends on runtime results. * * ## Petri Net Role: TOKEN + PRE-CONDITION * * In the Categorical Petri Net model, VCard serves dual roles: * * ### As Token * - VCard is the **Token** that resides in Places (Handles) * - Tokens enable Transitions (PCards) to fire * - Token movement represents verification cascade * * ### As Pre-Condition (Firing Guard) * A Petri Net transition fires when all input VCards exist: * * ``` * M' = M - •t + t• * ``` * * VCard as Input Arc encodes: * - **Input Place**: Handle pointing to required VCard * - **Token in Place**: VCard content in `card` table * - **Arc Guard**: VCard's Applicative authorization check * - **Token "Consumption"**: Recording `previous_hash` in new VCard * * ### Hash as Token AND Certificate (Dual Role) * * | Role | Function | Implementation | * |------|----------|----------------| * | **Token** | Triggers firing (enables transitions) | `handle_registry.current_hash IS NOT NULL` | * | **Certificate** | Proves correctness after verification | VCard content witnesses Hoare Triple | * * ``` * {h_pre} PCard {h_post} * ``` * * Where: * - h_pre = hash of input VCard (precondition satisfied) * - PCard = CLM specification (transformation logic) * - h_post = hash of output VCard (postcondition witnessed) * * ## Cascade Pattern: Propagating Verification * * VCards enable **verification cascades** across the network: * * ``` * T1 → V1 → T2 → V2 → T3 → ... → Vn * ``` * * Each VerificationVCard (Vi) serves as: * 1. **Post-condition** of transition Ti (what it produces) * 2. **Pre-condition** of transition Ti+1 (what enables next step) * * ## DOTS Vocabulary Role: ARENA + ACTION * * VCard is both: * - **Arena**: Interface type defining what can interact (subject_did, capabilities, external_refs) * - **Action**: Morphism where interactions (PCards) act on systems (MCards) * * ## The Four Roles: * 1. Identity & Credential Container (The "Who") * 2. Verification Hub (The "Rules") * 3. Side Effect Manager (The "Bridge") * 4. Input/Output Gatekeeper (The "Gate") * * ## VCard as Egress Gate: The Sovereign Boundary * * When a VerificationVCard is produced, it may also serve as **egress authorization**: * * | Egress Role | VCard Content | Enables | * |-------------|--------------|---------| * | Export Authorization | `egress_capability: { destination_did, scope }` | Sharing to specific PKCs | * | Cross-PKC Transition | `transferable: true` | Token moves to destination's Petri Net | * | Federation Trigger | Signed VCard bundle | Downstream PKC's transitions enabled | * * @see docs/VCard_Impl.md for full implementation specification */ import { parse } from 'yaml'; import { MCard } from './MCard'; import { HashValidator } from './hash/HashValidator'; import { GTime } from './GTime'; import { ContentTypeInterpreter } from './ContentTypeInterpreter'; import { createVCardDOTSMetadata } from '../types/dots'; /** * Scope of a capability token. */ export var CapabilityScope; (function (CapabilityScope) { CapabilityScope["READ"] = "read"; CapabilityScope["WRITE"] = "write"; CapabilityScope["EXECUTE"] = "execute"; CapabilityScope["ADMIN"] = "admin"; CapabilityScope["DELEGATE"] = "delegate"; })(CapabilityScope || (CapabilityScope = {})); /** * Direction of gatekeeper authorization. */ export var GatekeeperDirection; (function (GatekeeperDirection) { GatekeeperDirection["INGRESS"] = "ingress"; GatekeeperDirection["EGRESS"] = "egress"; })(GatekeeperDirection || (GatekeeperDirection = {})); // ---------------------------------------------------------------------------- // Functional Helpers (UPTV API) // ---------------------------------------------------------------------------- /** * Check if an MCard follows the VCard structure. */ export function isVCard(card) { try { const content = parseVCardContent(card); // Check for root 'vcard' key or legacy flat structure // The new standard requires { vcard: { ... } } // We support both for robustness in this transition if (content.vcard) return true; // Legacy check return content.type === 'VCard' || (content.subjectDid && content.capabilities); } catch { return false; } } /** * Extract PCard reference hashes from VCard. */ export function getPCardRefs(vcard) { const content = parseVCardContent(vcard); const data = content.vcard || content; // Handle nested or legacy // Check 'verification.pcard_refs' (New) or 'externalRefs' (Legacy) const verificationRefs = data.verification?.pcard_refs || []; const hashes = []; for (const r of verificationRefs) { if (typeof r === 'string') hashes.push(r); else if (r.hash) hashes.push(r.hash); } return hashes; } /** * Extract subject DID from VCard. */ export function getSubjectDid(vcard) { const content = parseVCardContent(vcard); const data = content.vcard || content; return data.identity?.subject_did || data.subjectDid; } /** * Internal helper to parse VCard content (JSON or YAML). */ function parseVCardContent(card) { try { const text = card.getContentAsText(); // Try JSON first try { return JSON.parse(text); } catch { return parse(text); // Fallback to YAML } } catch { return {}; } } /** * VCard - The Application Plane unit (Petri Net Token) * * Implements the Empty Schema Principle: VCard IS an MCard. * This class provides a view and runtime state management over the immutable content. * * In Petri Net terms: * - VCard is a **Token** that resides in a **Place** (Handle) * - VCard enables **Transitions** (PCards) to fire when present * - VCard serves as both **Pre-condition** (what enables) and **Post-condition** (what is produced) */ export class VCard extends MCard { // Mutable Runtime State (initialized via _initializeMutableState) _subjectDid; _controllerPubkeys; _capabilities; _externalRefs; _exportManifest; _gatekeeperLog; _pcardRefsHashes; constructor(content, hash, g_time, contentType, hashFunction, initialData) { super(content, hash, g_time, contentType, hashFunction); // Initialize Mutable State from Content this._gatekeeperLog = []; this._exportManifest = []; this._initializeMutableState(initialData); } _initializeMutableState(data) { // Handle { vcard: ... } nesting const v = data.vcard || data; // 1. Identity this._subjectDid = v.identity?.subject_did || v.subjectDid || ''; this._controllerPubkeys = v.identity?.controller_pubkeys || v.controllerPubkeys || []; // 2. Capabilities const rawCaps = v.gatekeeper?.capabilities || v.capabilities || []; this._capabilities = rawCaps.map((c) => ({ capabilityId: c.id || c.capabilityId, actorDid: c.actor || c.actorDid, scope: c.scope, resourcePattern: c.resourcePattern || c.resource_pattern, expiresAt: c.expiresAt || c.expires_at ? new Date(c.expiresAt || c.expires_at) : undefined, transferable: c.transferable || false, constraints: c.constraints })); // 3. External Refs const rawRefs = v.externalRefs || v.external_refs || []; this._externalRefs = rawRefs.map((r) => ({ uri: r.uri, contentHash: r.contentHash || r.content_hash, status: r.status, qosMetrics: r.qos || r.qosMetrics })); // 4. PCard Refs const rawPCardRefs = v.verification?.pcard_refs || []; this._pcardRefsHashes = rawPCardRefs.map((r) => typeof r === 'string' ? r : r.hash); } /** * Create a new VCard from parameters. * Follows strict UPTV structure { vcard: { ... } }. */ static async createVCard(subjectDid, controllerPubkeys, capabilities = [], externalRefs = [], hashAlgorithm = 'sha256') { // Construct the Canoncal Structure const structure = { vcard: { type: 'authentication-authorization', identity: { subject_did: subjectDid, controller_pubkeys: controllerPubkeys }, gatekeeper: { capabilities: capabilities.map(c => ({ id: c.capabilityId, actor: c.actorDid, scope: c.scope, resource_pattern: c.resourcePattern, expires_at: c.expiresAt?.toISOString(), transferable: c.transferable, constraints: c.constraints })) }, verification: { pcard_refs: [] // Populated if provided in a separate arg or we filter externalRefs? // For now, we assume externalRefs are generic. // To strictly follow PCard Logic, we could expose pcardRefs arg, but keeping signature for now. }, external_refs: externalRefs.map(r => ({ uri: r.uri, content_hash: r.contentHash, status: r.status, qos: r.qosMetrics })) } }; const contentString = JSON.stringify(structure, null, 2); // Pretty print or compact? Python uses sort_keys. JSON.stringify order is not guaranteed for non-array? // Actually, for content addressing, canonical JSON is better. // We will use standard stringify for now, node usually preserves key order. const bytes = new TextEncoder().encode(contentString); const hash = await HashValidator.computeHash(bytes, hashAlgorithm); const g_time = GTime.stampNow(hashAlgorithm); const contentType = ContentTypeInterpreter.detect(bytes); // Pass structure directly to avoid re-parsing return new VCard(bytes, hash, g_time, contentType, hashAlgorithm, structure); } /** * Create a VCard wrapper from an existing MCard. */ static async fromMCard(card) { const content = parseVCardContent(card); // Validates it mimics VCard structure? // We just wrap it. return new VCard(card.content, card.hash, card.g_time, card.contentType, card.hashFunction, content); } getDOTSMetadata() { return createVCardDOTSMetadata(); } // ========================================================================= // Accessors // ========================================================================= get subjectDid() { return this._subjectDid; } get controllerPubkeys() { return this._controllerPubkeys; } get capabilities() { return this._capabilities; } get externalRefs() { return this._externalRefs; } // ========================================================================= // Runtime Mutability (Gatekeeper Logic) // ========================================================================= addCapability(capability) { this._capabilities.push(capability); } getValidCapabilities() { const now = new Date(); return this._capabilities.filter(c => c.expiresAt === undefined || (new Date(c.expiresAt) > now)); } hasCapability(scope, resourceHash) { for (const cap of this.getValidCapabilities()) { if (cap.scope === scope) { const regex = new RegExp(cap.resourcePattern); if (regex.test(resourceHash)) return true; } } return false; } addPCardReference(pcardHash) { if (!this._pcardRefsHashes.includes(pcardHash)) { this._pcardRefsHashes.push(pcardHash); } // Also add as external ref for compatibility this.addExternalRef({ uri: `pcard://${pcardHash}`, contentHash: pcardHash, status: 'verified' }); } getPCardReferences() { const set = new Set(this._pcardRefsHashes); this._externalRefs.forEach(r => { if (r.uri.startsWith('pcard://')) set.add(r.contentHash); }); return Array.from(set); } addExternalRef(ref) { this._externalRefs.push(ref); } getExternalRefsByStatus(status) { return this._externalRefs.filter(r => r.status === status); } verifyExternalRef(uri, newHash) { for (const ref of this._externalRefs) { if (ref.uri === uri) { if (ref.contentHash === newHash) { ref.status = 'verified'; ref.lastVerified = new Date(); return true; } else { ref.status = 'stale'; return false; } } } return false; } authorizeIngress(sourceDid, contentHash, capabilityId) { let authorized = false; let usedCapability; for (const cap of this.getValidCapabilities()) { if (cap.actorDid === sourceDid && (cap.scope === CapabilityScope.WRITE || cap.scope === CapabilityScope.ADMIN)) { if (capabilityId === undefined || cap.capabilityId === capabilityId) { authorized = true; usedCapability = cap.capabilityId; break; } } } const event = { direction: GatekeeperDirection.INGRESS, timestamp: new Date(), sourceDid, contentHash, authorized, capabilityUsed: usedCapability }; this._gatekeeperLog.push(event); return authorized; } registerForEgress(contentHash) { if (!this._exportManifest.includes(contentHash)) { this._exportManifest.push(contentHash); return true; } return false; } authorizeEgress(destinationDid, contentHash, capabilityId) { if (!this._exportManifest.includes(contentHash)) { this._logEgress(destinationDid, contentHash, false, undefined); return false; } let authorized = false; let usedCapability; for (const cap of this.getValidCapabilities()) { if (cap.scope === CapabilityScope.READ || cap.scope === CapabilityScope.ADMIN) { const regex = new RegExp(cap.resourcePattern); if (regex.test(contentHash)) { if (capabilityId === undefined || cap.capabilityId === capabilityId) { authorized = true; usedCapability = cap.capabilityId; break; } } } } this._logEgress(destinationDid, contentHash, authorized, usedCapability); return authorized; } _logEgress(dst, hash, auth, cap) { const event = { direction: GatekeeperDirection.EGRESS, timestamp: new Date(), destinationDid: dst, contentHash: hash, authorized: auth, capabilityUsed: cap }; this._gatekeeperLog.push(event); } getGatekeeperLog(direction) { if (direction === undefined) return this._gatekeeperLog; return this._gatekeeperLog.filter(e => e.direction === direction); } getExportManifest() { return [...this._exportManifest]; } // ========================================================================= // EOS Compliance // ========================================================================= simulateMode() { return new VCardSimulation(this); } // ========================================================================= // Petri Net Token Semantics // ========================================================================= /** * Get the handle where this VCard token currently resides * * In Petri Net terms, this is the "Place" where the token is located. * * @returns Handle string if available in content, or hash-based handle */ getTokenHandle() { const content = parseVCardContent(this); const v = content.vcard || content; return v.handle || v.token_handle || `vcard://${this.hash.substring(0, 16)}`; } /** * Check if this VCard is a VerificationVCard (result of PCard execution) * * VerificationVCards are produced by PCard transitions and contain * execution results with provenance chain. * * @returns True if this is a verification token */ isVerificationVCard() { const content = parseVCardContent(this); const v = content.vcard || content; return v.type === 'verification' || v.type === 'verification-result' || Boolean(v.verification?.execution_result); } /** * Get the previous hash in the provenance chain * * In Petri Net terms, this links to the input VCard that was "consumed" * when the transition fired to produce this VCard. * * @returns Previous VCard hash if this is part of a verification cascade */ getPreviousHash() { const content = parseVCardContent(this); const v = content.vcard || content; return v.previous_hash || v.previousHash || v.verification?.previous_hash; } /** * Get the PCard hash that produced this VCard (if verification) * * This links the output token to the transition that created it. * * @returns PCard hash that produced this VCard */ getSourcePCardHash() { const content = parseVCardContent(this); const v = content.vcard || content; return v.source_pcard || v.verification?.pcard_hash || v.produced_by; } /** * Create a VerificationVCard from PCard execution result * * This is the factory method for producing output tokens in the Petri Net. * * @param pcard - The PCard (Transition) that executed * @param result - Execution result * @param previousVCard - Input VCard (pre-condition) if any * @param success - Whether execution succeeded * @returns New VerificationVCard */ static async createVerificationVCard(pcard, result, previousVCard, success = true, hashAlgorithm = 'sha256') { const handle = typeof pcard.getBalancedHandle === 'function' ? pcard.getBalancedHandle() : `clm://hash/${pcard.hash.substring(0, 16)}/balanced`; const structure = { vcard: { type: 'verification', handle, identity: { subject_did: 'did:ptr:system', controller_pubkeys: [] }, verification: { pcard_hash: pcard.hash, execution_result: result, success, timestamp: new Date().toISOString(), previous_hash: previousVCard?.hash }, gatekeeper: { capabilities: [] }, external_refs: [] } }; const contentString = JSON.stringify(structure, null, 2); const bytes = new TextEncoder().encode(contentString); const hash = await HashValidator.computeHash(bytes, hashAlgorithm); const g_time = GTime.stampNow(hashAlgorithm); const contentType = ContentTypeInterpreter.detect(bytes); return new VCard(bytes, hash, g_time, contentType, hashAlgorithm, structure); } /** * Check if this VCard enables a specific PCard to fire * * @param requiredHandle - The handle where a precondition VCard must exist * @returns True if this VCard satisfies that precondition */ enablesTransition(requiredHandle) { const myHandle = this.getTokenHandle(); return myHandle === requiredHandle; } } /** * Simulation context for VCard (EOS Compliance). */ export class VCardSimulation { vcard; log; constructor(vcard) { this.vcard = vcard; this.log = []; } logEffect(effectType, details) { this.log.push({ timestamp: new Date(), effectType, details, simulated: true }); } getSimulationLog() { return this.log; } } //# sourceMappingURL=VCard.js.map