{
  "name": "dignity.js",
  "version": "1.1.0",
  "description": "The scalable data layer of the decentralized browser application ecosystem — P2P object replication with CQRS read replicas, Dignity Apps, and verification policies",
  "lifecycle": {
    "start": "attach network message handler and begin receiving",
    "stop": "leave discovery scopes and detach handler"
  },
  "resources": {
    "collections/{collection}/{id}": {
      "create": {
        "method": "create(collection, data, options)",
        "owner": "actor that creates the object",
        "options": {
          "id": "optional stable id",
          "collaborators": "optional peer id list",
          "broadcastScope": "scoped broadcast password namespace",
          "connectToPeers": "optional; defaults to collaborators on PeerJS mesh",
          "peerGroupId": "optional; auto-publish signed domain event to this PeerGroup (v0.8+, publisher role)"
        }
      },
      "read": {
        "method": "read(collection, id)",
        "returns": "active record or null; see recordShape"
      },
      "update": {
        "method": "update(collection, id, patch, options)",
        "authorization": "owner or collaborator",
        "options": {
          "expectedVersion": "optional number; throws VERSION_CONFLICT when mismatched",
          "broadcastScope": "optional scoped broadcast password namespace",
          "collaborators": "owner may replace collaborator list",
          "connectToPeers": "optional; defaults to owner + collaborators",
          "peerGroupId": "optional; auto-publish domain event when publisher (v0.8+)"
        }
      },
      "updateWithRetry": {
        "method": "updateWithRetry(collection, id, patchFn, options)",
        "description": "read-modify-write helper with automatic retry on version conflicts"
      },
      "pushRecordSnapshot": {
        "method": "pushRecordSnapshot(collection, id, options)",
        "description": "broadcast full record for late joiners who missed the initial create",
        "returns": "active record; see recordShape"
      },
      "getRecordPeerIds": {
        "method": "getRecordPeerIds(collection, id, options)",
        "description": "returns owner + collaborator peer ids for connectToPeers"
      },
      "delete": {
        "method": "remove(collection, id)",
        "authorization": "owner-only",
        "options": {
          "peerGroupId": "optional; auto-publish domain event when publisher (v0.8+)"
        }
      },
      "transferOwnership": {
        "method": "transferOwnership(collection, id, newOwnerId, options)",
        "authorization": "owner-only",
        "options": {
          "keepAsCollaborator": "default true; previous owner stays collaborator"
        }
      },
      "proposeUpdate": {
        "method": "proposeUpdate(collection, id, patch, options)",
        "authorization": "non-owner only; owner should call update()",
        "description": "signed direct proposal to record owner (#13 turn-based games)",
        "returns": "{ proposalId }",
        "options": {
          "connectToPeers": "ensure connection to owner before send"
        }
      },
      "acceptProposal": {
        "method": "acceptProposal(proposal, options)",
        "authorization": "owner-only",
        "description": "validate and apply proposal patch via update(); sends proposal:result to proposer"
      },
      "rejectProposal": {
        "method": "rejectProposal(proposal, reason)",
        "authorization": "owner-only",
        "description": "reject without applying patch; sends proposal:result to proposer"
      },
      "registerVerification": {
        "method": "registerVerification(collection, { code, version?, policy?, reflective? })",
        "description": "local collection rules; embeds verificationHash/verificationVersion on operations and snapshots (#115–#117)",
        "options": {
          "code": "string, object, or function business rules",
          "version": "optional semver for version-to-hash registry (#116)",
          "policy": "strict | backward-compatible | patch-only | minor-and-patch | advisory (default)",
          "reflective": "when true, fingerprint nested functions via AST-canonical toString (#123)"
        }
      },
      "registerPublisherVerification": {
        "method": "registerPublisherVerification(publisherId, collection, { code, version?, policy?, dappId?, reflective? })",
        "description": "decentralized official dapp version trust keyed by publisherId (#123); no central registry"
      },
      "getVerificationEntry": {
        "method": "getVerificationEntry(collection)",
        "description": "returns local verification registry entry for collection, or null"
      },
      "getPublisherVerificationEntry": {
        "method": "getPublisherVerificationEntry(publisherId, collection)",
        "description": "returns publisher-scoped verification entry trusted locally for that publisher + collection"
      },
      "resolveVerificationEntry": {
        "method": "resolveVerificationEntry(collection, senderId?)",
        "description": "publisher entry when senderId matches a registered official publisher; otherwise local entry"
      },
      "restoreRecord": {
        "method": "restoreRecord(collection, record, options)",
        "description": "apply remote snapshot or persistence record locally; recomputes hash and runs verification ingest checks"
      }
    },
    "collections/{collection}": {
      "list": {
        "method": "list(collection, options)",
        "returns": "active records with hash; deleted stubs omit hash when includeDeleted is true",
        "options": {
          "includeDeleted": "include tombstone stubs in list results"
        }
      }
    },
    "peers": {
      "connectToPeer": {
        "method": "connectToPeer(peerId, options)",
        "description": "open PeerJS data channel to peer id"
      },
      "getConnectionStats": {
        "method": "getConnectionStats()",
        "returns": "{ openCount, peerIds }"
      },
      "ensureConnectedToPeers": {
        "method": "ensureConnectedToPeers(peerIds, options)",
        "description": "connect to many peers before broadcast"
      },
      "joinDiscovery": {
        "method": "joinDiscovery(scope, options)",
        "description": "scoped presence; options.bootstrapPeerIds connects before announce; advertises verification metadata when registered"
      },
      "leaveDiscovery": {
        "method": "leaveDiscovery(scope)",
        "description": "stop heartbeat and remove local presence from scope"
      },
      "listPeers": {
        "method": "listPeers(scope)",
        "description": "list presence entries in a discovery scope"
      },
      "announcePresence": {
        "method": "announcePresence(scope, options)",
        "description": "manual presence heartbeat for a joined scope"
      },
      "broadcastMessage": {
        "method": "broadcastMessage(scope, type, payload, options)",
        "description": "custom app messages; options.connectToPeers"
      },
      "sendDirectMessage": {
        "method": "sendDirectMessage(targetId, type, payload, options)",
        "description": "encrypted direct message to targetId"
      },
      "registerPeerPublicKey": {
        "method": "registerPeerPublicKey(peerId, bundle, options)",
        "description": "trust peer signing/encryption keys with optional generation"
      },
      "trustPeerPublicKey": {
        "method": "trustPeerPublicKey(peerId, bundle)",
        "description": "register trusted keys without generation metadata"
      },
      "getPublicKey": {
        "method": "getPublicKey()",
        "description": "returns this node's public key bundle"
      },
      "unbanPeer": {
        "method": "unbanPeer(peerId)",
        "description": "clear manual or automatic peer ban"
      },
      "getBanInfo": {
        "method": "getBanInfo(peerId)",
        "returns": "ban expiry and reason for peerId, or null"
      },
      "banPeer": {
        "method": "banPeer(peerId, durationMs)",
        "description": "ban peer for durationMs (default peerBanDurationMs); emits peerbanned"
      }
    },
    "identity": {
      "getPeerIdentityGeneration": {
        "method": "getPeerIdentityGeneration(peerId)",
        "description": "trusted identity generation for peerId"
      },
      "getPeerIdentityState": {
        "method": "getPeerIdentityState(peerId)",
        "description": "public key + generation state for peerId"
      },
      "adoptDerivedIdentityKeyPair": {
        "method": "adoptDerivedIdentityKeyPair(keyPair)",
        "description": "install credential-derived keys locally"
      },
      "deriveAndAdoptIdentity": {
        "method": "deriveAndAdoptIdentity({ username, password, ... })",
        "description": "derive keys from username/password and adopt"
      },
      "broadcastIdentityRotation": {
        "method": "broadcastIdentityRotation(options)",
        "description": "broadcast signed identity:rotate to peers"
      },
      "broadcastColdRecoveryEnrollment": {
        "method": "broadcastColdRecoveryEnrollment(options)",
        "description": "broadcast cold-recovery enrollment to peers (low-level; pair with enrollColdRecoveryPassword)"
      },
      "enrollAndBroadcastColdRecovery": {
        "method": "enrollAndBroadcastColdRecovery(options)",
        "description": "convenience: enroll cold recovery key and announce in one call"
      },
      "revokeAndRotateDerivedIdentity": {
        "method": "revokeAndRotateDerivedIdentity(options)",
        "description": "compromise recovery rotation helper"
      },
      "rotateDerivedIdentityPassword": {
        "method": "rotateDerivedIdentityPassword(options)",
        "description": "password-change rotation helper"
      },
      "applyPeerIdentityRotation": {
        "method": "applyPeerIdentityRotation(message)",
        "description": "apply remote identity:rotate message"
      },
      "applyPeerColdRecoveryEnrollment": {
        "method": "applyPeerColdRecoveryEnrollment(message)",
        "description": "apply remote cold-recovery enrollment"
      }
    },
    "peerGroups": {
      "joinPeerGroup": "join scalable gossip group; options: fanout, maxActivePeers, maxHops (default 64), role, tiered, liveCap, tierMode, domainEvents, publisherId",
      "leavePeerGroup": "leave gossip group and scoped presence",
      "listPeerGroupMembers": "list presence in gossip:{groupId} scope",
      "publishToPeerGroup": "epidemic publish to live tier when tiered; inner types: operation, record:snapshot, domain:event, custom",
      "publishPeerGroupBulk": "bulk-tier publish (publisher only); batched domain events and checkpoints",
      "publishPeerGroupCheckpoint": "publish domain-event chain checkpoint to bulk tier",
      "publishRecordToPeerGroup": "publish normalized record snapshot into gossip group",
      "getPeerGroupConfig": "returns joined group config including tier and domainEvents flags",
      "getPeerGroupStats": "{ joinedGroups, seenGossipCount, openConnectionCount, globalMaxOpenConnections }"
    },
    "verification": {
      "hashVerificationCode": "canonical sha512 hash of verification code + compatibility policy",
      "hashReflectiveLogic": "walk object graphs with Reflect.ownKeys; AST-canonicalize nested functions before hashing (#123)",
      "normalizeFunctionSource": "parse function.toString() via acorn/astring when possible; fallback whitespace strip",
      "collectReflectiveFingerprints": "return { canonical, fingerprints } map of paths to normalized function sources",
      "normalizeVerificationCode": "stable canonical form for functions, strings, or rule objects",
      "parseSemver": "validate semver string",
      "compareSemver": "compare two semver strings",
      "buildVerificationEntry": "build registry entry with version-to-hash map and optional fingerprintList",
      "buildPublisherVerificationKey": "stable key for publisherId + collection registry slot",
      "evaluateVerificationCompatibility": "policy engine for remote hash/version acceptance on ingest",
      "buildVerificationPresenceMetadata": "local verification summary for joinDiscovery presence",
      "buildPublisherVerificationPresenceMetadata": "publisher-scoped verification summary for presence",
      "COMPATIBILITY_POLICIES": "strict | backward-compatible | patch-only | minor-and-patch | advisory",
      "DEFAULT_COMPATIBILITY_POLICY": "advisory"
    },
    "cqrs": {
      "DignityQueryReplica": "read-only materialized view from domain events; methods: start, stop, read, list, verifyChain, getViewStats",
      "domainEvents": "operationToDomainEvent, signDomainEvent, verifyDomainEvent, verifyEventChain, buildCheckpoint, createEmptyView, applyDomainEventToView, DOMAIN_EVENT_SCHEMA_VERSION",
      "tiers": "assignPeerGroupTier, DEFAULT_LIVE_CAP (5000), DEFAULT_BULK_INTERVAL_MS, filterPeersByTier",
      "bulkRelay": "electBulkRelays, DEFAULT_BULK_RELAY_COUNT"
    },
    "dignityApps": {
      "_intro": "v0.11+ sandboxed iframe host, bridge, stored commands; verification pins in v0.13+",
      "validateDignityAppManifest": "validate manifest; returns { ok, manifest | reason }; supports dappVersion, logicHash, publisherId, storedLogic (#123)",
      "collectionAllowed": "check collection against manifest allowlist",
      "getStoredCommand": "lookup pre-declared stored command by id",
      "buildAppCsp": "build immutable CSP content from manifest allowedCspOrigins",
      "prepareSandboxedAppHtml": "inject CSP meta and violation reporter into app HTML",
      "injectCspMeta": "prepend CSP meta tag to HTML head",
      "DignityAppHost": "sandboxed iframe host; mount(container, html), unmount(), rpc() for tests",
      "DEFAULT_SANDBOX": "allow-scripts only (no allow-same-origin)",
      "createHostRpcHandler": "MessageChannel RPC: ready, query, list, runStoredCommand, log, error",
      "RPC_METHODS": "allowlisted RPC method names",
      "createDignityAppClient": "app-side SDK bound to transferred MessagePort",
      "connectDignityAppClient": "wait for parent handshake postMessage in iframe",
      "buildClientBootstrapScript": "self-contained iframe bootstrap; auto-captures console.error, onerror, unhandledrejection (#106)",
      "HANDSHAKE_TYPE": "postMessage type string for parent ↔ iframe port transfer",
      "attachErrorPanel": "host DOM error/log panel; subscribes to applog, apperror, apprpcerror (#106)",
      "sanitizeCaptureMessage": "cap and sanitize forwarded log/error strings",
      "sanitizeCaptureValue": "redact sensitive keys in forwarded capture payloads",
      "executeStoredCommand": "run manifest-declared write on publisher node; enforces publisher verification when manifest pins logic (#123)",
      "isPublisherCommandCapable": "true when attached node can execute stored commands"
    },
    "network": {
      "InMemoryNetworkHub": "test/local hub wiring multiple InMemoryNetworkAdapter instances",
      "InMemoryNetworkAdapter": "synchronous in-process transport for unit tests",
      "PeerJSNetworkAdapter": "browser WebRTC mesh adapter",
      "createPeerJSNetworkAdapter": "factory; options: urls, iceServers, peerOptions, maxOpenConnections",
      "parsePeerJsServerUrl": "normalize PeerJS signaling URL"
    },
    "signalingExports": {
      "createDefaultSignalingPool": "Cloudflare + fallback WebSocket providers",
      "SignalingPool": "multi-provider signaling with failover",
      "WebSocketSignalingProvider": "raw WebSocket signaling transport",
      "PeerJSSignalingProvider": "PeerJS-compatible signaling transport",
      "DEFAULT_CLOUDFLARE_SIGNALING_URLS": "default Cloudflare relay URLs",
      "DEFAULT_SIGNALING_FALLBACK_URLS": "default public fallback URLs"
    },
    "identityExports": {
      "exportIdentityMnemonic": "BIP39-style 48-word phrase encoding Ed25519 seed + Curve25519 secret (#130)",
      "importIdentityMnemonic": "restore keyPair from 48-word phrase; normalizes case/whitespace/NFC",
      "exportIdentityMnemonicEncrypted": "passphrase-encrypted identity backup for password managers",
      "importIdentityMnemonicEncrypted": "decrypt passphrase-protected identity backup to keyPair",
      "normalizeMnemonicPhrase": "trim, lowercase, NFC, and split recovery words"
    },
    "packageExports": {
      "security": "VDF, SlothPermutation, MessageSecurityService, DEFAULT_SECURITY_OPTIONS, DEFAULT_APP_PASSWORD, deriveKeyPairFromCredentials, deriveColdRecoverySigningKey, keyPairToPublicBundle, exportIdentityMnemonic, importIdentityMnemonic, exportIdentityMnemonicEncrypted, importIdentityMnemonicEncrypted, normalizeMnemonicPhrase",
      "identity": "createIdentityRotation, verifyIdentityRotation, revokeAndRotateIdentity, rotateIdentityPassword, enrollColdRecoveryPassword, verifyColdRecoveryEnrollment, shouldApplyIdentityRotation, exportIdentityMnemonic, importIdentityMnemonic, exportIdentityMnemonicEncrypted, importIdentityMnemonicEncrypted",
      "gossip": "PEER_GROUP_SCOPE_PREFIX, DEFAULT_PEER_GROUP_OPTIONS, peerGroupScope, parsePeerGroupScope, selectFanoutPeers",
      "apps": "DIGNITY_APP_MANIFEST_SCHEMA_VERSION"
    }
  },
  "events": {
    "change": "object create/update/delete/snapshot applied",
    "conflict": "local or remote version mismatch",
    "warning": "see warningSubtypes; emitted for non-fatal issues",
    "domainevent": "signed domain event applied or emitted locally",
    "chainbroken": "domain event hash chain verification failed",
    "bulkrelaychanged": "bulk relay peer set changed for a tiered group",
    "checkpointpublished": "publisher published domain-event checkpoint",
    "peerdiscovered": "peer joined discovery scope",
    "peergroupjoined": "local node joined a PeerGroup",
    "peergroupleft": "local node left a PeerGroup",
    "peergroupmessage": "raw inner message received on a joined PeerGroup (used by DignityQueryReplica)",
    "peerleft": "peer left or timed out",
    "peerbanned": "peer auto- or manually banned",
    "peerunbanned": "peer ban cleared",
    "identityrotated": "local or remote identity rotation applied",
    "coldrecoveryenrolled": "cold recovery enrollment applied",
    "message": "custom decrypted message received",
    "proposal": "owner received signed update proposal from non-owner (#13)",
    "proposalresult": "proposer received accept/reject result for a proposal (#13)",
    "verificationmismatch": "remote verificationHash differs from local registry (#115); advisory unless policy is strict",
    "policyrejected": "operation/snapshot/event rejected by compatibility policy or untrusted publisher (#117, #123)",
    "securityerror": "signature, PoW, or decrypt failure on incoming message",
    "messageignored": "message dropped (wrong target, banned peer, etc.)"
  },
  "warningSubtypes": {
    "orphan-operation": "remote update/delete before local record exists",
    "peer-connect-failed": "connectToPeer or bootstrap failed",
    "content-hash-mismatch": "remote record hash differs from local recompute",
    "content-hash-missing": "record missing hash field on ingest",
    "domain-event-rejected": "domain event failed verification or policy",
    "tier-announce-failed": "tier metadata announce failed",
    "verification-version-spoof": "remote claims known semver with wrong hash",
    "verification-hash-missing": "remote write missing verificationHash when required",
    "verification-hash-mismatch": "remote verificationHash differs (non-policy path)",
    "stored-command-rejected": "stored command failed validation or verification",
    "persistence-failed": "IndexedDB write failed",
    "default-app-password": "node started with default app password sentinel"
  },
  "queryReplicaEvents": {
    "started": "replica began listening for domain events",
    "stopped": "replica stopped",
    "checkpoint": "checkpoint applied to materialized view",
    "change": "view updated from domain event",
    "warning": "domain-event-rejected or domain-event-not-applied"
  },
  "dignityAppHostEvents": {
    "ready": "sandboxed app completed ready RPC handshake",
    "applog": "log RPC forwarded from iframe",
    "apperror": "error RPC or CSP securitypolicyviolation forwarded from iframe",
    "apprpcerror": "failed query/list/runStoredCommand RPC from iframe (#106)"
  },
  "recordShape": {
    "active": {
      "id": "string",
      "ownerId": "string",
      "collaboratorIds": [
        "peer-id"
      ],
      "data": "application payload object",
      "hash": "sha512:<hex>; computed from canonicalized data only",
      "createdAt": "unix ms timestamp",
      "updatedAt": "unix ms timestamp",
      "version": "monotonic integer",
      "verificationHash": "optional; present when collection has registerVerification (#115)",
      "verificationVersion": "optional semver bound to verificationHash (#116)"
    },
    "deletedStub": {
      "id": "string",
      "ownerId": "string",
      "deletedAt": "unix ms timestamp",
      "version": "monotonic integer"
    },
    "notes": [
      "hash uses stableStringify(data) so object key order does not change the digest",
      "restoreRecord recomputes hash locally and emits warning.type=content-hash-mismatch on mismatch",
      "remote ingest runs checkVerificationOnIngest against local or publisher-scoped registry"
    ]
  },
  "manifestShape": {
    "required": [
      "id",
      "title",
      "collections"
    ],
    "optional": {
      "schemaVersion": "1",
      "description": "human-readable summary",
      "peerGroupId": "CQRS publisher group for stored commands",
      "publisherId": "official logic publisher peer id (#123)",
      "dappVersion": "semver for official bundle (#123)",
      "logicHash": "sha512:<hex> reflective or code hash (#123)",
      "storedLogic": "map of logicRef → { version, hash, validate? }",
      "storedCommands": "pre-declared writes with optional logicRef/logicVersion/logicHash",
      "allowedCspOrigins": "https origins allowed in iframe connect-src",
      "forwardConsoleLog": "forward iframe console.log to host applog events"
    },
    "notes": [
      "dappVersion and logicHash must be set together",
      "stored command logic pins are enforced against registerPublisherVerification on the host publisher node"
    ]
  },
  "persistence": {
    "IndexedDBPersistence": {
      "method": "attach(node)",
      "options": [
        "dbName",
        "storeName",
        "collections"
      ]
    }
  },
  "react": {
    "entrypoint": "dignity.js/react",
    "hooks": [
      "useDignity",
      "useCollection",
      "useObject",
      "usePeers",
      "useDiscovery",
      "useConnectionStats",
      "useRoom",
      "useMessages"
    ]
  },
  "signaling": {
    "defaults": {
      "cloudflare": "enabled by default",
      "fallback": "enabled"
    },
    "customization": {
      "factory": "createDefaultSignalingPool(options)",
      "override": [
        "cloudflareUrls",
        "fallbackUrls",
        "customProviders"
      ]
    }
  },
  "messageSecurity": {
    "defaults": {
      "signingEnabled": true,
      "encryptionEnabled": true,
      "powEnabled": true,
      "powSteps": 22,
      "powTargetMs": 1000,
      "kdfIterations": 100000,
      "banDurationMs": 172800000
    },
    "broadcast": {
      "encryption": "AES secretbox with PBKDF2-SHA256 derived key",
      "scopePasswords": "broadcastPasswords map keyed by broadcastScope",
      "legacyKdf": "single-hash fallback accepted for older peers"
    },
    "direct": {
      "encryption": "NaCl box (X25519 + XSalsa20-Poly1305) to recipient public key"
    },
    "pow": {
      "algorithm": "Sloth VDF",
      "config": [
        "powTargetMs",
        "powSteps"
      ]
    }
  }
}
