{"id":50908963,"url":"https://github.com/jose-compu/dignity.js","last_synced_at":"2026-06-16T08:01:47.406Z","repository":{"id":350693924,"uuid":"1207323731","full_name":"jose-compu/dignity.js","owner":"jose-compu","description":"The Scalable Data Layer of the Decentralized Browser Application Ecosystem.","archived":false,"fork":false,"pushed_at":"2026-06-10T19:37:55.000Z","size":2496,"stargazers_count":1,"open_issues_count":33,"forks_count":4,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T00:09:44.445Z","etag":null,"topics":["decentralized-applications","encrypted","local-first","p2p","p2p-network","p2p-node","peer-to-peer","peerjs","web3"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jose-compu.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-10T20:18:14.000Z","updated_at":"2026-06-12T17:58:11.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jose-compu/dignity.js","commit_stats":null,"previous_names":["jose-compu/dignity.js"],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/jose-compu/dignity.js","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jose-compu%2Fdignity.js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jose-compu%2Fdignity.js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jose-compu%2Fdignity.js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jose-compu%2Fdignity.js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jose-compu","download_url":"https://codeload.github.com/jose-compu/dignity.js/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jose-compu%2Fdignity.js/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34396430,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decentralized-applications","encrypted","local-first","p2p","p2p-network","p2p-node","peer-to-peer","peerjs","web3"],"created_at":"2026-06-16T08:01:46.657Z","updated_at":"2026-06-16T08:01:47.400Z","avatar_url":"https://github.com/jose-compu.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dignity.js\n\n![dignity.js logo](https://raw.githubusercontent.com/jose-compu/dignity.js/refs/heads/main/docs/assets/dignity-logo.png)\n\n[![docs](https://img.shields.io/badge/docs-online-5B7FFF)](https://jose-compu.github.io/dignity.js/)\n[![npm version](https://img.shields.io/npm/v/dignity.js?color=cb3837\u0026label=npm)](https://www.npmjs.com/package/dignity.js)\n[![npm downloads](https://img.shields.io/npm/dm/dignity.js?color=blue)](https://www.npmjs.com/package/dignity.js)\n[![CI](https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml/badge.svg)](https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml)\n![tests](https://img.shields.io/badge/tests-213%2B%20passing-brightgreen)\n![coverage](https://img.shields.io/badge/coverage-92%25-brightgreen)\n![license](https://img.shields.io/badge/license-Apache%202.0-black)\n\nREST-like P2P object API for decentralized JavaScript applications.\n\n`dignity.js` lets many browsers synchronize shared objects with ownership rules and built-in anti-abuse + privacy controls.\n\n## Highlights\n\n- REST-like API over P2P replication: `create`, `read`, `list`, `update`, `remove`\n- Owner authorization model by default (only creator can update/delete)\n- Security defaults enabled:\n  - message signing (Ed25519)\n  - broadcast encryption (shared password)\n  - direct encryption (recipient public key)\n  - Sloth VDF proof-of-work per message\n  - default `powSteps: 22` (calibrated on this machine to about 1000ms)\n  - automatic peer ban on invalid signature/PoW (`48h` default)\n- Team/subapp scoped broadcast passwords (`broadcastScope` + `broadcastPasswords`)\n- Optimistic concurrency helpers (`expectedVersion`, `updateWithRetry`, `conflict` events)\n- PeerJS mesh bootstrap: connect before announce/broadcast, auto `publicKey` in presence\n- Late-joiner sync via `pushRecordSnapshot` (full record catch-up when create was missed)\n- Content hashes on active records via `record.hash` (`sha512:` over canonicalized `data`)\n- Auto `connectToPeers` on create/update/delete replication (owner + collaborators)\n- Optional IndexedDB persistence for browser reload survival\n- Optional React hooks via `dignity.js/react`\n- Browser-first: published npm package includes IIFE, ESM, and CJS builds\n\n## Install\n\n```bash\nnpm install dignity.js\n```\n\n## Tutorial\n\n**New to dignity.js?** Start with [TUTORIAL.md](./TUTORIAL.md) — eight short lessons from two in-memory peers to browser PeerJS and PeerGroup spectators. The [docs site tutorial](https://jose-compu.github.io/dignity.js/#tutorial) covers the same path.\n\n## Quick Start\n\n```js\nconst {\n  DignityP2P,\n  InMemoryNetworkHub,\n  InMemoryNetworkAdapter\n} = require('dignity.js');\n\nconst hub = new InMemoryNetworkHub();\n\nconst alice = new DignityP2P({\n  nodeId: 'alice',\n  networkAdapter: new InMemoryNetworkAdapter(hub),\n  security: {\n    appPassword: 'shared-out-of-band-password',\n    powSteps: 22\n  }\n});\n\nconst bob = new DignityP2P({\n  nodeId: 'bob',\n  networkAdapter: new InMemoryNetworkAdapter(hub),\n  security: {\n    appPassword: 'shared-out-of-band-password',\n    powSteps: 22\n  }\n});\n\nawait alice.start();\nawait bob.start();\n\nawait alice.joinDiscovery('main', {\n  metadata: { nickname: 'alice' }\n});\nawait bob.joinDiscovery('main', {\n  metadata: { nickname: 'bob' }\n});\n\nconst visiblePeers = alice.listPeers('main', { includeSelf: false });\nconsole.log('Peers in main room:', visiblePeers.map((peer) =\u003e peer.peerId));\n\nawait alice.create('notes', { title: 'hello decentralized world' }, {\n  id: 'note-1',\n  broadcastScope: 'main'\n});\nconsole.log(bob.read('notes', 'note-1'));\n\nawait alice.leaveDiscovery('main');\nawait bob.leaveDiscovery('main');\n```\n\n## Team / Subapp Scoped Passwords\n\nUse a different broadcast password per cooperative team, room, or sub-application namespace.\n\n```js\nconst node = new DignityP2P({\n  nodeId: 'player-1',\n  networkAdapter,\n  security: {\n    appPassword: 'fallback-password',\n    broadcastPasswords: {\n      'coop:red': 'red-team-secret',\n      'coop:blue': 'blue-team-secret'\n    },\n    powSteps: 22,\n    banDurationMs: 48 * 60 * 60 * 1000\n  }\n});\n\nawait node.create('matches', { mode: 'coop' }, {\n  id: 'm-1',\n  broadcastScope: 'coop:red'\n});\n```\n\nPeers with a different password for `coop:red` cannot decrypt that broadcast traffic.\n\n## PeerGroup Gossip (scalable PubSub)\n\nFor high-fanout object updates (millions of subscribers per published object), use multiplexed gossip groups. Each peer keeps a bounded number of active transports (`maxActivePeers` per group, `globalMaxOpenConnections` per node).\n\n```js\n// Follow 200 accounts = 200 joined groups, few connections each\nawait node.joinPeerGroup('feed:alice', {\n  bootstrapPeerIds: ['publisher-peer-id'],\n  fanout: 3,\n  maxActivePeers: 8\n});\n\nawait node.publishRecordToPeerGroup('feed:alice', 'posts', 'post-1');\nawait node.leavePeerGroup('feed:alice');\n```\n\nSmall collaborations (chess players, document co-editing) should keep using direct `connectToPeers` mesh. Large read-only audiences (chess spectators, public timelines) should use PeerGroup gossip. See the [docs PeerGroup section](https://jose-compu.github.io/dignity.js/#peer-groups).\n\n## Room / Team Discovery\n\nUse scoped discovery to find active peers in a room (for example `main`, `team:red`, `raid-42`).\n\n```js\nawait node.joinDiscovery('team:red', {\n  metadata: { nickname: 'alice' },\n  heartbeatIntervalMs: 15000,\n  ttlMs: 45000\n});\n\nconst peers = node.listPeers('team:red', { includeSelf: false });\nawait node.leaveDiscovery('team:red');\n```\n\n## Direct Secure Messaging\n\n```js\nalice.registerPeerPublicKey('bob', bob.getPublicKey());\nbob.registerPeerPublicKey('alice', alice.getPublicKey());\n\nawait alice.sendDirectMessage('bob', 'dm', { text: 'private payload' });\n```\n\n## Optimistic Concurrency\n\nUpdates carry a monotonic `version`. Remote peers reject stale operations when `baseVersion` does not match.\n\n```js\nnode.on('conflict', (event) =\u003e {\n  console.log('conflict', event.phase, event.expectedVersion, event.currentVersion);\n});\n\nawait node.update('games', 'g1', { score: 10 }, { expectedVersion: 3 });\n\nawait node.updateWithRetry('games', 'g1', (current) =\u003e ({\n  score: current.data.score + 1\n}));\n```\n\nUse `expectedVersion` for fail-fast local writes. Use `updateWithRetry` for read-modify-write loops in fast multiplayer state.\n\n## Record Content Hashes\n\nActive records returned by `create`, `read`, `list`, `update`, and `pushRecordSnapshot` include a `hash` field:\n\n```js\nconst record = await node.create('notes', { title: 'hello' }, { id: 'n1' });\nconsole.log(record.hash); // sha512:...\n```\n\nHash details:\n\n- The algorithm is `sha512`, matching `tweetnacl.hash` in both browser and Node builds.\n- The digest covers only `record.data`, not `id`, `ownerId`, timestamps, collaborators, or version.\n- Data is canonicalized with `stableStringify`, so object key order does not affect the hash.\n- Snapshot restore recomputes the digest locally and emits a `warning` with type `content-hash-mismatch` if a remote advertised hash does not match the received `data`.\n- Deleted tombstones returned by `list(collection, { includeDeleted: true })` intentionally omit `hash`.\n\n## IndexedDB Persistence\n\nPersist replicated collections across page reloads:\n\n```js\nconst { DignityP2P, IndexedDBPersistence } = require('dignity.js');\n\nconst node = new DignityP2P({ nodeId, networkAdapter, security });\nconst persistence = new IndexedDBPersistence({\n  dbName: 'my-app',\n  collections: ['games', 'matches']\n});\n\nawait node.start();\nawait persistence.attach(node);\n```\n\n## React Hooks\n\nOptional React integration (`react \u003e= 18` peer dependency):\n\n```js\nimport { createElement } from 'react';\nimport { useDignity, useCollection, usePeers } from 'dignity.js/react';\n\nfunction Room() {\n  const { node, status } = useDignity(config);\n  const games = useCollection(node, 'games');\n  const peers = usePeers(node, 'room:chess', { includeSelf: false });\n\n  return createElement('pre', null, JSON.stringify({ status, games, peers }, null, 2));\n}\n```\n\n## Browser Usage\n\nThe published npm package includes pre-built bundles (IIFE, ESM, CJS) generated at publish time. The `dist/` folder is not checked into the repository.\n\n```text\n\u003cscript src=\"https://unpkg.com/dignity.js/dist/dignity.min.js\"\u003e\u003c/script\u003e\n\u003cscript\u003e\n  const { DignityP2P } = DignityJS;\n\u003c/script\u003e\n```\n\n## Security Model\n\n`dignity.js` provides two encryption modes:\n\n- **Direct mode** (`targetId` set): true end-to-end encryption using X25519 key exchange between sender and recipient. Only the intended recipient can decrypt.\n- **Broadcast mode** (no `targetId`): symmetric encryption using a shared password. All peers that know the password can decrypt all broadcast traffic in that scope. This is a **group shared-secret cipher**, not end-to-end encryption.\n\nBroadcast encryption uses PBKDF2-SHA256 (default 100,000 iterations) with a random salt per message to derive the symmetric key. This protects against offline brute-force of weak passwords. The iteration count is configurable via `kdfIterations`.\n\nMessages from peers running older versions that used the legacy single-hash KDF are still accepted and decrypted automatically (backward compatible).\n\n**Important:** if the broadcast password leaks, all past captured traffic for that scope is retroactively decryptable. For sensitive data, use direct mode with per-peer public keys.\n\n## Signaling Servers\n\nDefault signaling URLs include PeerJS-compatible public endpoints:\n\n- `wss://peerjs.92k.de/peerjs?key=peerjs`\n- `wss://0.peerjs.com/peerjs?key=peerjs`\n\nYou can also deploy your own server with [peerjs-server](https://github.com/peers/peerjs-server) and point `createDefaultSignalingPool` (or `WebSocketSignalingProvider`) to your own `wss://.../peerjs?key=...` URL.\n\nCompatibility note:\n- `dignity.js` now includes a dedicated `PeerJSSignalingProvider` backed by the official `peerjs` client for PeerJS protocol compatibility.\n- In non-WebRTC runtimes (for example Node test runners), it automatically falls back to WebSocket transport checks for connectivity testing.\n\n### PeerJS mesh bootstrap\n\nUnlike the in-memory test adapter (which fan-outs to every registered node), **PeerJS only delivers messages over open data channels**. Discovery broadcasts do not reach anyone until at least one side has connected.\n\nFor browser apps (see the bundled 3D chess demo), pass a known peer id from your invite link:\n\n```js\nawait node.joinDiscovery('room:my-game', {\n  metadata: { nickname: 'alice', role: 'host' },\n  bootstrapPeerIds: ['host-peer-id-from-link']\n});\n\nawait node.broadcastMessage('claim-seat', payload, {\n  broadcastScope: 'room:my-game',\n  connectToPeers: ['host-peer-id-from-link']\n});\n```\n\nLibrary helpers:\n\n- `node.connectToPeer(peerId)` — open a PeerJS data channel\n- `node.getConnectionStats()` — `{ openCount, peerIds }`\n- `node.getRecordPeerIds(collection, id)` — owner + collaborators (for custom broadcasts)\n- `node.joinDiscovery(scope, { bootstrapPeerIds })` — connect before the first presence announce\n- `broadcastMessage(..., { connectToPeers })` — connect, then broadcast\n- `node.pushRecordSnapshot(collection, id, options)` — send full record state to late joiners\n- `create` / `update` / `remove` auto-connect to record peers when `connectToPeers` is omitted\n- Presence metadata automatically includes `publicKey`; remote keys are trusted from presence and message envelopes (direct messages work without manual `registerPeerPublicKey`)\n\nReact: `useRoom(node, scope, options)` combines discovery, peers, and connection stats.\n\n### Late joiners (missed create)\n\nOn PeerJS, a peer that comes online **after** the host creates an object never receives the initial `create` operation. Later `update` operations are ignored until that peer has a local copy of the record.\n\nAfter accepting a joiner (or on `orphan-operation` warnings), push a full snapshot:\n\n```js\nnode.on('warning', (event) =\u003e {\n  if (event.type === 'orphan-operation') {\n    // optional: request resync from owner\n  }\n});\n\nawait host.update('chess-matches', gameId, { blackPlayerId: joinerId, status: 'playing' }, {\n  collaborators: [hostId, joinerId],\n  broadcastScope: scope\n});\n\nawait host.pushRecordSnapshot('chess-matches', gameId, {\n  broadcastScope: scope,\n  connectToPeers: [joinerId]\n});\n```\n\nThe joiner applies the snapshot via `restoreRecord`, then subsequent move updates replicate normally.\n\n## Development\n\n| Script | Purpose | Notes |\n| --- | --- | --- |\n| `npm test` | Run the full Jest suite with coverage. | Standard local validation before opening a PR or publishing. |\n| `npm run test:unit` | Run the unit-test subset only. | Useful for faster local iteration. |\n| `npm run test:cloudflare-live` | Run the live Cloudflare signaling integration test. | Opt-in; set `RUN_CLOUDFLARE_LIVE_TESTS=1`. |\n| `npm run test:pow-calibrate` | Run the Sloth VDF timing calibration test without coverage. | Opt-in; set `RUN_POW_CALIBRATE=1`. |\n| `npm run build` | Build the published package bundles into `dist/`. | Run after changing library source files. |\n| `npm run build:chess` | Rebuild the browser chess demo bundle only. | Used by the docs site and local chess demo. |\n| `npm run docs:favicon` | Regenerate the docs favicon assets. | Docs maintenance helper. |\n| `npm run docs:build` | Build the docs-specific assets. | Currently rebuilds the chess demo bundle. |\n| `npm run docs:dev` | Start the local docs server. | Serves the main docs and chess demo; auto-builds chess if needed. |\n| `npm run docs:serve` | Start the same local docs server via an alias. | Equivalent to `docs:dev`. |\n| `npm run docs:stop` | Stop the background docs server from a previous run. | Useful if port `4173` is stuck. |\n| `npm run docs:check` | Verify the generated docs assets exist. | Good quick check after docs asset generation. |\n| `npm run example:tictactoe` | Run the Node tic-tac-toe example. | Demonstrates a minimal replicated game flow. |\n| `npm run example:chess` | Run the Node chess example. | Demonstrates the lighter-weight chess sample. |\n| `npm run prepublishOnly` | Run the publish gate locally. | Publish/CI-oriented hook; runs tests and build before `npm publish`. |\n\n```bash\nnpm test\nnpm run build\nnpm run docs:dev          # docs + 3D chess at http://127.0.0.1:4173/\nnpm run docs:build        # rebuild chess bundle only\nnpm run example:tictactoe\nnpm run example:chess\nnpm run test:pow-calibrate\n```\n\nLocal docs (auto-builds chess if `docs/chess/assets/chess-app.js` is missing):\n\n```bash\nnpm run docs:dev\n# Docs:  http://127.0.0.1:4173/\n# Chess: http://127.0.0.1:4173/chess/\n```\n\nUse `DOCS_NO_OPEN=1 npm run docs:dev` to skip opening the browser, or `DOCS_PORT=8080` for another port.\n\nIf port 4173 is stuck from an old session:\n\n```bash\nnpm run docs:stop\nnpm run docs:dev\n```\n\nIf 4173 is busy, `docs:dev` auto-picks the next free port (4174, 4175, …) and prints the URLs.\n\n## Docs and Examples\n\n- **Documentation:** [jose-compu.github.io/dignity.js](https://jose-compu.github.io/dignity.js/)\n- Docs site source: `docs/index.html` (local: `npm run docs:dev`)\n- **3D Chess demo:** `docs/chess/` — PeerJS mesh, dual-signed resume links, IndexedDB → [local chess demo](http://127.0.0.1:4173/chess/) when `docs:dev` is running\n- API metadata: `docs/openapi-like.json`\n- Minimal demos:\n  - `examples/decentralized-tictactoe.js`\n  - `examples/decentralized-chess-lite.js`\n\n## Publish\n\n```bash\nnpm publish --access public\n```\n\nThe `prepublishOnly` script runs tests and build automatically.\n\n## License\n\nApache 2.0 — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjose-compu%2Fdignity.js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjose-compu%2Fdignity.js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjose-compu%2Fdignity.js/lists"}