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
JavaScript
/**
* 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