dignity.js 0.5.3 → 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 +36 -0
- package/dist/dignity.cjs.js +92 -50
- package/dist/dignity.cjs.js.map +3 -3
- package/dist/dignity.esm.js +92 -50
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +5 -5
- package/docs/openapi-like.json +31 -4
- package/examples/decentralized-chess-lite.js +9 -0
- package/package.json +1 -1
- package/src/core/dignity-p2p.js +35 -6
- package/src/persistence/indexeddb-persistence.js +2 -0
- package/src/security/sloth-vdf.js +11 -5
- package/src/signaling/websocket-signaling-provider.js +11 -2
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ REST-like P2P object API for decentralized JavaScript applications.
|
|
|
29
29
|
- Optimistic concurrency helpers (`expectedVersion`, `updateWithRetry`, `conflict` events)
|
|
30
30
|
- PeerJS mesh bootstrap: connect before announce/broadcast, auto `publicKey` in presence
|
|
31
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`)
|
|
32
33
|
- Auto `connectToPeers` on create/update/delete replication (owner + collaborators)
|
|
33
34
|
- Optional IndexedDB persistence for browser reload survival
|
|
34
35
|
- Optional React hooks via `dignity.js/react`
|
|
@@ -161,6 +162,23 @@ await node.updateWithRetry('games', 'g1', (current) => ({
|
|
|
161
162
|
|
|
162
163
|
Use `expectedVersion` for fail-fast local writes. Use `updateWithRetry` for read-modify-write loops in fast multiplayer state.
|
|
163
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
|
+
|
|
164
182
|
## IndexedDB Persistence
|
|
165
183
|
|
|
166
184
|
Persist replicated collections across page reloads:
|
|
@@ -291,6 +309,24 @@ The joiner applies the snapshot via `restoreRecord`, then subsequent move update
|
|
|
291
309
|
|
|
292
310
|
## Development
|
|
293
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
|
+
|
|
294
330
|
```bash
|
|
295
331
|
npm test
|
|
296
332
|
npm run build
|
package/dist/dignity.cjs.js
CHANGED
|
@@ -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
|
|
2354
|
+
if ((powExponent & BigInt(1)) === BigInt(1)) {
|
|
2349
2355
|
result = result * powBase % modulus;
|
|
2350
2356
|
}
|
|
2351
|
-
powExponent = powExponent
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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_${
|
|
3816
|
-
const token =
|
|
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,
|