dignity.js 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,14 @@
1
1
  # dignity.js
2
2
 
3
- <p align="center">
4
- <img src="https://raw.githubusercontent.com/jose-compu/dignity.js/refs/heads/main/docs/assets/dignity-logo.svg" alt="dignity.js logo" width="860" />
5
- </p>
6
-
7
- <p align="center">
8
- <img src="https://img.shields.io/badge/dignity.js-decentralized%20object%20api-5B7FFF?style=for-the-badge" alt="dignity.js" />
9
- <img src="https://img.shields.io/badge/browser-first-00C2A8?style=for-the-badge" alt="browser-first" />
10
- <img src="https://img.shields.io/badge/security-default%20on-111827?style=for-the-badge" alt="security default on" />
11
- </p>
12
-
13
- <p align="center">
14
- <a href="https://jose-compu.github.io/dignity.js/"><img src="https://img.shields.io/badge/docs-online-5B7FFF" alt="documentation"></a>
15
- <a href="https://www.npmjs.com/package/dignity.js"><img src="https://img.shields.io/npm/v/dignity.js?color=cb3837&label=npm" alt="npm version"></a>
16
- <a href="https://www.npmjs.com/package/dignity.js"><img src="https://img.shields.io/npm/dm/dignity.js?color=blue" alt="npm downloads"></a>
17
- <a href="https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml"><img src="https://github.com/jose-compu/dignity.js/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
18
- <img src="https://img.shields.io/badge/tests-150%2B%20passing-brightgreen" alt="tests passing">
19
- <img src="https://img.shields.io/badge/coverage-99%25-brightgreen" alt="coverage">
20
- <img src="https://img.shields.io/badge/license-Apache%202.0-black" alt="license">
21
- </p>
3
+ ![dignity.js logo](https://raw.githubusercontent.com/jose-compu/dignity.js/refs/heads/main/docs/assets/dignity-logo.png)
4
+
5
+ [![docs](https://img.shields.io/badge/docs-online-5B7FFF)](https://jose-compu.github.io/dignity.js/)
6
+ [![npm version](https://img.shields.io/npm/v/dignity.js?color=cb3837&label=npm)](https://www.npmjs.com/package/dignity.js)
7
+ [![npm downloads](https://img.shields.io/npm/dm/dignity.js?color=blue)](https://www.npmjs.com/package/dignity.js)
8
+ [![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)
9
+ ![tests](https://img.shields.io/badge/tests-150%2B%20passing-brightgreen)
10
+ ![coverage](https://img.shields.io/badge/coverage-99%25-brightgreen)
11
+ ![license](https://img.shields.io/badge/license-Apache%202.0-black)
22
12
 
23
13
  REST-like P2P object API for decentralized JavaScript applications.
24
14
 
@@ -39,6 +29,7 @@ REST-like P2P object API for decentralized JavaScript applications.
39
29
  - Optimistic concurrency helpers (`expectedVersion`, `updateWithRetry`, `conflict` events)
40
30
  - PeerJS mesh bootstrap: connect before announce/broadcast, auto `publicKey` in presence
41
31
  - Late-joiner sync via `pushRecordSnapshot` (full record catch-up when create was missed)
32
+ - Content hashes on active records via `record.hash` (`sha512:` over canonicalized `data`)
42
33
  - Auto `connectToPeers` on create/update/delete replication (owner + collaborators)
43
34
  - Optional IndexedDB persistence for browser reload survival
44
35
  - Optional React hooks via `dignity.js/react`
@@ -171,6 +162,23 @@ await node.updateWithRetry('games', 'g1', (current) => ({
171
162
 
172
163
  Use `expectedVersion` for fail-fast local writes. Use `updateWithRetry` for read-modify-write loops in fast multiplayer state.
173
164
 
165
+ ## Record Content Hashes
166
+
167
+ Active records returned by `create`, `read`, `list`, `update`, and `pushRecordSnapshot` include a `hash` field:
168
+
169
+ ```js
170
+ const record = await node.create('notes', { title: 'hello' }, { id: 'n1' });
171
+ console.log(record.hash); // sha512:...
172
+ ```
173
+
174
+ Hash details:
175
+
176
+ - The algorithm is `sha512`, matching `tweetnacl.hash` in both browser and Node builds.
177
+ - The digest covers only `record.data`, not `id`, `ownerId`, timestamps, collaborators, or version.
178
+ - Data is canonicalized with `stableStringify`, so object key order does not affect the hash.
179
+ - 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`.
180
+ - Deleted tombstones returned by `list(collection, { includeDeleted: true })` intentionally omit `hash`.
181
+
174
182
  ## IndexedDB Persistence
175
183
 
176
184
  Persist replicated collections across page reloads:
@@ -193,6 +201,7 @@ await persistence.attach(node);
193
201
  Optional React integration (`react >= 18` peer dependency):
194
202
 
195
203
  ```js
204
+ import { createElement } from 'react';
196
205
  import { useDignity, useCollection, usePeers } from 'dignity.js/react';
197
206
 
198
207
  function Room() {
@@ -200,7 +209,7 @@ function Room() {
200
209
  const games = useCollection(node, 'games');
201
210
  const peers = usePeers(node, 'room:chess', { includeSelf: false });
202
211
 
203
- return <pre>{JSON.stringify({ status, games, peers }, null, 2)}</pre>;
212
+ return createElement('pre', null, JSON.stringify({ status, games, peers }, null, 2));
204
213
  }
205
214
  ```
206
215
 
@@ -208,7 +217,7 @@ function Room() {
208
217
 
209
218
  The published npm package includes pre-built bundles (IIFE, ESM, CJS) generated at publish time. The `dist/` folder is not checked into the repository.
210
219
 
211
- ```html
220
+ ```text
212
221
  <script src="https://unpkg.com/dignity.js/dist/dignity.min.js"></script>
213
222
  <script>
214
223
  const { DignityP2P } = DignityJS;
@@ -300,6 +309,24 @@ The joiner applies the snapshot via `restoreRecord`, then subsequent move update
300
309
 
301
310
  ## Development
302
311
 
312
+ | Script | Purpose | Notes |
313
+ | --- | --- | --- |
314
+ | `npm test` | Run the full Jest suite with coverage. | Standard local validation before opening a PR or publishing. |
315
+ | `npm run test:unit` | Run the unit-test subset only. | Useful for faster local iteration. |
316
+ | `npm run test:cloudflare-live` | Run the live Cloudflare signaling integration test. | Opt-in; set `RUN_CLOUDFLARE_LIVE_TESTS=1`. |
317
+ | `npm run test:pow-calibrate` | Run the Sloth VDF timing calibration test without coverage. | Opt-in; set `RUN_POW_CALIBRATE=1`. |
318
+ | `npm run build` | Build the published package bundles into `dist/`. | Run after changing library source files. |
319
+ | `npm run build:chess` | Rebuild the browser chess demo bundle only. | Used by the docs site and local chess demo. |
320
+ | `npm run docs:favicon` | Regenerate the docs favicon assets. | Docs maintenance helper. |
321
+ | `npm run docs:build` | Build the docs-specific assets. | Currently rebuilds the chess demo bundle. |
322
+ | `npm run docs:dev` | Start the local docs server. | Serves the main docs and chess demo; auto-builds chess if needed. |
323
+ | `npm run docs:serve` | Start the same local docs server via an alias. | Equivalent to `docs:dev`. |
324
+ | `npm run docs:stop` | Stop the background docs server from a previous run. | Useful if port `4173` is stuck. |
325
+ | `npm run docs:check` | Verify the generated docs assets exist. | Good quick check after docs asset generation. |
326
+ | `npm run example:tictactoe` | Run the Node tic-tac-toe example. | Demonstrates a minimal replicated game flow. |
327
+ | `npm run example:chess` | Run the Node chess example. | Demonstrates the lighter-weight chess sample. |
328
+ | `npm run prepublishOnly` | Run the publish gate locally. | Publish/CI-oriented hook; runs tests and build before `npm publish`. |
329
+
303
330
  ```bash
304
331
  npm test
305
332
  npm run build
@@ -3,43 +3,6 @@ var __commonJS = (cb, mod) => function __require() {
3
3
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
4
4
  };
5
5
 
6
- // src/utils/event-emitter.js
7
- var require_event_emitter = __commonJS({
8
- "src/utils/event-emitter.js"(exports2, module2) {
9
- var EventEmitter = class {
10
- constructor() {
11
- this.handlers = /* @__PURE__ */ new Map();
12
- }
13
- on(eventName, handler) {
14
- if (!this.handlers.has(eventName)) {
15
- this.handlers.set(eventName, /* @__PURE__ */ new Set());
16
- }
17
- this.handlers.get(eventName).add(handler);
18
- }
19
- off(eventName, handler) {
20
- const eventHandlers = this.handlers.get(eventName);
21
- if (!eventHandlers) {
22
- return;
23
- }
24
- eventHandlers.delete(handler);
25
- if (eventHandlers.size === 0) {
26
- this.handlers.delete(eventName);
27
- }
28
- }
29
- emit(eventName, payload) {
30
- const eventHandlers = this.handlers.get(eventName);
31
- if (!eventHandlers) {
32
- return;
33
- }
34
- for (const handler of eventHandlers) {
35
- handler(payload);
36
- }
37
- }
38
- };
39
- module2.exports = EventEmitter;
40
- }
41
- });
42
-
43
6
  // node_modules/tweetnacl/nacl-fast.js
44
7
  var require_nacl_fast = __commonJS({
45
8
  "node_modules/tweetnacl/nacl-fast.js"(exports2, module2) {
@@ -2330,6 +2293,43 @@ var require_nacl_util = __commonJS({
2330
2293
  }
2331
2294
  });
2332
2295
 
2296
+ // src/utils/event-emitter.js
2297
+ var require_event_emitter = __commonJS({
2298
+ "src/utils/event-emitter.js"(exports2, module2) {
2299
+ var EventEmitter = class {
2300
+ constructor() {
2301
+ this.handlers = /* @__PURE__ */ new Map();
2302
+ }
2303
+ on(eventName, handler) {
2304
+ if (!this.handlers.has(eventName)) {
2305
+ this.handlers.set(eventName, /* @__PURE__ */ new Set());
2306
+ }
2307
+ this.handlers.get(eventName).add(handler);
2308
+ }
2309
+ off(eventName, handler) {
2310
+ const eventHandlers = this.handlers.get(eventName);
2311
+ if (!eventHandlers) {
2312
+ return;
2313
+ }
2314
+ eventHandlers.delete(handler);
2315
+ if (eventHandlers.size === 0) {
2316
+ this.handlers.delete(eventName);
2317
+ }
2318
+ }
2319
+ emit(eventName, payload) {
2320
+ const eventHandlers = this.handlers.get(eventName);
2321
+ if (!eventHandlers) {
2322
+ return;
2323
+ }
2324
+ for (const handler of eventHandlers) {
2325
+ handler(payload);
2326
+ }
2327
+ }
2328
+ };
2329
+ module2.exports = EventEmitter;
2330
+ }
2331
+ });
2332
+
2333
2333
  // src/security/sloth-vdf.js
2334
2334
  var require_sloth_vdf = __commonJS({
2335
2335
  "src/security/sloth-vdf.js"(exports2, module2) {
@@ -2337,6 +2337,12 @@ var require_sloth_vdf = __commonJS({
2337
2337
  static p = BigInt(
2338
2338
  "170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979"
2339
2339
  );
2340
+ // precompute values for optimization:
2341
+ // (p - 1) / 2
2342
+ static pHalf = _SlothPermutation.p - BigInt(1) >> BigInt(1);
2343
+ // (p + 1) / 4
2344
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
2345
+ static pQuarter = _SlothPermutation.p + BigInt(1) >> BigInt(2);
2340
2346
  fastPow(base, exponent, modulus) {
2341
2347
  if (modulus === BigInt(1)) {
2342
2348
  return BigInt(0);
@@ -2345,25 +2351,25 @@ var require_sloth_vdf = __commonJS({
2345
2351
  let powBase = base % modulus;
2346
2352
  let powExponent = exponent;
2347
2353
  while (powExponent > 0) {
2348
- if (powExponent % BigInt(2) === BigInt(1)) {
2354
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
2349
2355
  result = result * powBase % modulus;
2350
2356
  }
2351
- powExponent = powExponent / BigInt(2);
2357
+ powExponent = powExponent >> BigInt(1);
2352
2358
  powBase = powBase * powBase % modulus;
2353
2359
  }
2354
2360
  return result;
2355
2361
  }
2356
2362
  quadRes(x) {
2357
- return this.fastPow(x, (_SlothPermutation.p - BigInt(1)) / BigInt(2), _SlothPermutation.p) === BigInt(1);
2363
+ return this.fastPow(x, _SlothPermutation.pHalf, _SlothPermutation.p) === BigInt(1);
2358
2364
  }
2359
2365
  modSqrtOp(x) {
2360
2366
  let y;
2361
2367
  let value = x;
2362
2368
  if (this.quadRes(value)) {
2363
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2369
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2364
2370
  } else {
2365
2371
  value = (-value + _SlothPermutation.p) % _SlothPermutation.p;
2366
- y = this.fastPow(value, (_SlothPermutation.p + BigInt(1)) / BigInt(4), _SlothPermutation.p);
2372
+ y = this.fastPow(value, _SlothPermutation.pQuarter, _SlothPermutation.p);
2367
2373
  }
2368
2374
  return y;
2369
2375
  }
@@ -2851,8 +2857,17 @@ var require_message_security_service = __commonJS({
2851
2857
  // src/core/dignity-p2p.js
2852
2858
  var require_dignity_p2p = __commonJS({
2853
2859
  "src/core/dignity-p2p.js"(exports2, module2) {
2860
+ var nacl = require_nacl_fast();
2861
+ var naclUtil = require_nacl_util();
2854
2862
  var EventEmitter = require_event_emitter();
2855
- var { MessageSecurityService: MessageSecurityService2 } = require_message_security_service();
2863
+ var { MessageSecurityService: MessageSecurityService2, stableStringify } = require_message_security_service();
2864
+ function computeContentHash(data) {
2865
+ const canonical = stableStringify(data || {});
2866
+ const bytes = naclUtil.decodeUTF8(canonical);
2867
+ const hash = nacl.hash(bytes);
2868
+ const hex = Array.from(hash, (b) => b.toString(16).padStart(2, "0")).join("");
2869
+ return `sha512:${hex}`;
2870
+ }
2856
2871
  var DignityP2P2 = class extends EventEmitter {
2857
2872
  constructor({ nodeId, networkAdapter, idGenerator, now, security } = {}) {
2858
2873
  super();
@@ -2911,6 +2926,7 @@ var require_dignity_p2p = __commonJS({
2911
2926
  if (!record || record.deletedAt) {
2912
2927
  return null;
2913
2928
  }
2929
+ const normalizedData = { ...record.data || {} };
2914
2930
  return {
2915
2931
  id: record.id,
2916
2932
  ownerId: record.ownerId,
@@ -2918,7 +2934,8 @@ var require_dignity_p2p = __commonJS({
2918
2934
  createdAt: record.createdAt,
2919
2935
  updatedAt: record.updatedAt,
2920
2936
  version: record.version,
2921
- data: { ...record.data }
2937
+ hash: record.hash || computeContentHash(normalizedData),
2938
+ data: normalizedData
2922
2939
  };
2923
2940
  }
2924
2941
  canUpdateRecord(record, actorId) {
@@ -2996,7 +3013,7 @@ var require_dignity_p2p = __commonJS({
2996
3013
  ownerId: this.nodeId,
2997
3014
  collaboratorIds,
2998
3015
  timestamp,
2999
- payload: { ...data }
3016
+ payload: { ...data || {} }
3000
3017
  };
3001
3018
  this.applyOperation(operation);
3002
3019
  await this.broadcastMessage("operation", operation, {
@@ -3529,11 +3546,23 @@ var require_dignity_p2p = __commonJS({
3529
3546
  if (current && current.version >= record.version) {
3530
3547
  return false;
3531
3548
  }
3549
+ const restoredData = { ...record.data || {} };
3550
+ const computedHash = computeContentHash(restoredData);
3551
+ if (record.hash && record.hash !== computedHash) {
3552
+ this.emit("warning", {
3553
+ type: "content-hash-mismatch",
3554
+ collection: collectionName,
3555
+ id: record.id,
3556
+ advertisedHash: record.hash,
3557
+ computedHash
3558
+ });
3559
+ }
3532
3560
  collection.set(record.id, {
3533
3561
  id: record.id,
3534
3562
  ownerId: record.ownerId,
3535
3563
  collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3536
- data: { ...record.data || {} },
3564
+ data: restoredData,
3565
+ hash: computedHash,
3537
3566
  createdAt: record.createdAt,
3538
3567
  updatedAt: record.updatedAt,
3539
3568
  deletedAt: record.deletedAt || null,
@@ -3551,7 +3580,8 @@ var require_dignity_p2p = __commonJS({
3551
3580
  id: raw.id,
3552
3581
  ownerId: raw.ownerId,
3553
3582
  collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3554
- data: { ...raw.data },
3583
+ data: { ...raw.data || {} },
3584
+ hash: raw.hash || computeContentHash(raw.data || {}),
3555
3585
  createdAt: raw.createdAt,
3556
3586
  updatedAt: raw.updatedAt,
3557
3587
  deletedAt: raw.deletedAt || null,
@@ -3581,7 +3611,8 @@ var require_dignity_p2p = __commonJS({
3581
3611
  id: operation.id,
3582
3612
  ownerId: operation.ownerId,
3583
3613
  collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3584
- data: { ...operation.payload },
3614
+ data: { ...operation.payload || {} },
3615
+ hash: computeContentHash(operation.payload || {}),
3585
3616
  createdAt: operation.timestamp,
3586
3617
  updatedAt: operation.timestamp,
3587
3618
  deletedAt: null,
@@ -3684,6 +3715,7 @@ var require_dignity_p2p = __commonJS({
3684
3715
  ...current.data,
3685
3716
  ...operation.payload
3686
3717
  };
3718
+ current.hash = computeContentHash(current.data);
3687
3719
  if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3688
3720
  current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3689
3721
  }
@@ -3769,6 +3801,14 @@ var require_signaling_pool = __commonJS({
3769
3801
  // src/signaling/websocket-signaling-provider.js
3770
3802
  var require_websocket_signaling_provider = __commonJS({
3771
3803
  "src/signaling/websocket-signaling-provider.js"(exports2, module2) {
3804
+ function randomBase36(length) {
3805
+ let value = "";
3806
+ while (value.length < length) {
3807
+ const chunk = Math.random().toString(36).slice(2);
3808
+ value += chunk.length > 0 ? chunk : "0";
3809
+ }
3810
+ return value.slice(0, length);
3811
+ }
3772
3812
  var WebSocketSignalingProvider2 = class {
3773
3813
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3774
3814
  if (!url) {
@@ -3812,8 +3852,8 @@ var require_websocket_signaling_provider = __commonJS({
3812
3852
  if (!peerJsHostPattern.test(this.url)) {
3813
3853
  return this.url;
3814
3854
  }
3815
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
3816
- const token = Math.random().toString(36).slice(2, 12);
3855
+ const connectionId = `dignityjs_${randomBase36(10)}`;
3856
+ const token = randomBase36(10);
3817
3857
  const hasQuery = this.url.includes("?");
3818
3858
  const hasId = /[?&]id=/.test(this.url);
3819
3859
  const hasToken = /[?&]token=/.test(this.url);
@@ -11029,6 +11069,7 @@ var require_indexeddb_persistence = __commonJS({
11029
11069
  ownerId: record.ownerId,
11030
11070
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11031
11071
  data: { ...record.data },
11072
+ hash: record.hash || null,
11032
11073
  createdAt: record.createdAt,
11033
11074
  updatedAt: record.updatedAt,
11034
11075
  deletedAt: record.deletedAt,
@@ -11089,6 +11130,7 @@ var require_indexeddb_persistence = __commonJS({
11089
11130
  ownerId: stored.ownerId,
11090
11131
  collaboratorIds: stored.collaboratorIds,
11091
11132
  data: stored.data,
11133
+ hash: stored.hash || null,
11092
11134
  createdAt: stored.createdAt,
11093
11135
  updatedAt: stored.updatedAt,
11094
11136
  deletedAt: stored.deletedAt,