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.
@@ -15,7 +15,8 @@
15
15
  }
16
16
  },
17
17
  "read": {
18
- "method": "read(collection, id)"
18
+ "method": "read(collection, id)",
19
+ "returns": "active record or null; see recordShape"
19
20
  },
20
21
  "update": {
21
22
  "method": "update(collection, id, patch, options)",
@@ -33,7 +34,8 @@
33
34
  },
34
35
  "pushRecordSnapshot": {
35
36
  "method": "pushRecordSnapshot(collection, id, options)",
36
- "description": "broadcast full record for late joiners who missed the initial create"
37
+ "description": "broadcast full record for late joiners who missed the initial create",
38
+ "returns": "active record; see recordShape"
37
39
  },
38
40
  "getRecordPeerIds": {
39
41
  "method": "getRecordPeerIds(collection, id, options)",
@@ -46,7 +48,8 @@
46
48
  },
47
49
  "collections/{collection}": {
48
50
  "list": {
49
- "method": "list(collection, options)"
51
+ "method": "list(collection, options)",
52
+ "returns": "active records with hash; deleted stubs omit hash when includeDeleted is true"
50
53
  }
51
54
  },
52
55
  "peers": {
@@ -60,11 +63,35 @@
60
63
  "events": {
61
64
  "change": "object create/update/delete/snapshot applied",
62
65
  "conflict": "local or remote version mismatch",
63
- "warning": "orphan-operation, peer-connect-failed, presence failures",
66
+ "warning": "orphan-operation, peer-connect-failed, presence failures, content-hash-mismatch",
64
67
  "peerdiscovered": "peer joined discovery scope",
65
68
  "peerleft": "peer left or timed out",
66
69
  "message": "custom decrypted message received"
67
70
  },
71
+ "recordShape": {
72
+ "active": {
73
+ "id": "string",
74
+ "ownerId": "string",
75
+ "collaboratorIds": [
76
+ "peer-id"
77
+ ],
78
+ "data": "application payload object",
79
+ "hash": "sha512:<hex>; computed from canonicalized data only",
80
+ "createdAt": "unix ms timestamp",
81
+ "updatedAt": "unix ms timestamp",
82
+ "version": "monotonic integer"
83
+ },
84
+ "deletedStub": {
85
+ "id": "string",
86
+ "ownerId": "string",
87
+ "deletedAt": "unix ms timestamp",
88
+ "version": "monotonic integer"
89
+ },
90
+ "notes": [
91
+ "hash uses stableStringify(data) so object key order does not change the digest",
92
+ "restoreRecord recomputes hash locally and emits warning.type=content-hash-mismatch on mismatch"
93
+ ]
94
+ },
68
95
  "persistence": {
69
96
  "IndexedDBPersistence": {
70
97
  "method": "attach(node)",
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Node.js chess example.
3
+ *
4
+ * Run this example with:
5
+ * npm run example:chess
6
+ *
7
+ * For the browser-based demo, see:
8
+ * docs/chess/
9
+ */
1
10
  const { DignityP2P, InMemoryNetworkHub, InMemoryNetworkAdapter } = require('../src');
2
11
 
3
12
  function initialBoard() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dignity.js",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "P2P object API for decentralized JavaScript applications",
5
5
  "homepage": "https://jose-compu.github.io/dignity.js/",
6
6
  "repository": {
@@ -1,5 +1,15 @@
1
+ const nacl = require('tweetnacl');
2
+ const naclUtil = require('tweetnacl-util');
1
3
  const EventEmitter = require('../utils/event-emitter');
2
- const { MessageSecurityService } = require('../security/message-security-service');
4
+ const { MessageSecurityService, stableStringify } = require('../security/message-security-service');
5
+
6
+ function computeContentHash(data) {
7
+ const canonical = stableStringify(data || {});
8
+ const bytes = naclUtil.decodeUTF8(canonical);
9
+ const hash = nacl.hash(bytes);
10
+ const hex = Array.from(hash, (b) => b.toString(16).padStart(2, '0')).join('');
11
+ return `sha512:${hex}`;
12
+ }
3
13
 
4
14
  /**
5
15
  * Core node API for replicated object collections.
@@ -101,6 +111,8 @@ class DignityP2P extends EventEmitter {
101
111
  return null;
102
112
  }
103
113
 
114
+ const normalizedData = { ...(record.data || {}) };
115
+
104
116
  return {
105
117
  id: record.id,
106
118
  ownerId: record.ownerId,
@@ -108,7 +120,8 @@ class DignityP2P extends EventEmitter {
108
120
  createdAt: record.createdAt,
109
121
  updatedAt: record.updatedAt,
110
122
  version: record.version,
111
- data: { ...record.data }
123
+ hash: record.hash || computeContentHash(normalizedData),
124
+ data: normalizedData
112
125
  };
113
126
  }
114
127
 
@@ -204,7 +217,7 @@ class DignityP2P extends EventEmitter {
204
217
  ownerId: this.nodeId,
205
218
  collaboratorIds,
206
219
  timestamp,
207
- payload: { ...data }
220
+ payload: { ...(data || {}) }
208
221
  };
209
222
 
210
223
  this.applyOperation(operation);
@@ -855,11 +868,24 @@ class DignityP2P extends EventEmitter {
855
868
  return false;
856
869
  }
857
870
 
871
+ const restoredData = { ...(record.data || {}) };
872
+ const computedHash = computeContentHash(restoredData);
873
+ if (record.hash && record.hash !== computedHash) {
874
+ this.emit('warning', {
875
+ type: 'content-hash-mismatch',
876
+ collection: collectionName,
877
+ id: record.id,
878
+ advertisedHash: record.hash,
879
+ computedHash
880
+ });
881
+ }
882
+
858
883
  collection.set(record.id, {
859
884
  id: record.id,
860
885
  ownerId: record.ownerId,
861
886
  collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
862
- data: { ...(record.data || {}) },
887
+ data: restoredData,
888
+ hash: computedHash,
863
889
  createdAt: record.createdAt,
864
890
  updatedAt: record.updatedAt,
865
891
  deletedAt: record.deletedAt || null,
@@ -881,7 +907,8 @@ class DignityP2P extends EventEmitter {
881
907
  id: raw.id,
882
908
  ownerId: raw.ownerId,
883
909
  collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
884
- data: { ...raw.data },
910
+ data: { ...(raw.data || {}) },
911
+ hash: raw.hash || computeContentHash(raw.data || {}),
885
912
  createdAt: raw.createdAt,
886
913
  updatedAt: raw.updatedAt,
887
914
  deletedAt: raw.deletedAt || null,
@@ -917,7 +944,8 @@ class DignityP2P extends EventEmitter {
917
944
  id: operation.id,
918
945
  ownerId: operation.ownerId,
919
946
  collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
920
- data: { ...operation.payload },
947
+ data: { ...(operation.payload || {}) },
948
+ hash: computeContentHash(operation.payload || {}),
921
949
  createdAt: operation.timestamp,
922
950
  updatedAt: operation.timestamp,
923
951
  deletedAt: null,
@@ -1035,6 +1063,7 @@ class DignityP2P extends EventEmitter {
1035
1063
  ...current.data,
1036
1064
  ...operation.payload
1037
1065
  };
1066
+ current.hash = computeContentHash(current.data);
1038
1067
 
1039
1068
  if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
1040
1069
  current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
@@ -73,6 +73,7 @@ class IndexedDBPersistence {
73
73
  ownerId: record.ownerId,
74
74
  collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
75
75
  data: { ...record.data },
76
+ hash: record.hash || null,
76
77
  createdAt: record.createdAt,
77
78
  updatedAt: record.updatedAt,
78
79
  deletedAt: record.deletedAt,
@@ -143,6 +144,7 @@ class IndexedDBPersistence {
143
144
  ownerId: stored.ownerId,
144
145
  collaboratorIds: stored.collaboratorIds,
145
146
  data: stored.data,
147
+ hash: stored.hash || null,
146
148
  createdAt: stored.createdAt,
147
149
  updatedAt: stored.updatedAt,
148
150
  deletedAt: stored.deletedAt,
@@ -6,6 +6,12 @@ class SlothPermutation {
6
6
  static p = BigInt(
7
7
  '170082004324204494273811327264862981553264701145937538369570764779791492622392118654022654452947093285873855529044371650895045691292912712699015605832276411308653107069798639938826015099738961427172366594187783204437869906954750443653318078358839409699824714551430573905637228307966826784684174483831608534979'
8
8
  );
9
+ // precompute values for optimization:
10
+ // (p - 1) / 2
11
+ static pHalf = (SlothPermutation.p - BigInt(1)) >> BigInt(1);
12
+ // (p + 1) / 4
13
+ // p ≡ 3 (mod 4) ⇒ (p+1) divisible by 4
14
+ static pQuarter = (SlothPermutation.p + BigInt(1)) >> BigInt(2);
9
15
 
10
16
  fastPow(base, exponent, modulus) {
11
17
  if (modulus === BigInt(1)) {
@@ -17,11 +23,11 @@ class SlothPermutation {
17
23
  let powExponent = exponent;
18
24
 
19
25
  while (powExponent > 0) {
20
- if (powExponent % BigInt(2) === BigInt(1)) {
26
+ if ((powExponent & BigInt(1)) === BigInt(1)) {
21
27
  result = (result * powBase) % modulus;
22
28
  }
23
29
 
24
- powExponent = powExponent / BigInt(2);
30
+ powExponent = powExponent >> BigInt(1);
25
31
  powBase = (powBase * powBase) % modulus;
26
32
  }
27
33
 
@@ -29,7 +35,7 @@ class SlothPermutation {
29
35
  }
30
36
 
31
37
  quadRes(x) {
32
- return this.fastPow(x, (SlothPermutation.p - BigInt(1)) / BigInt(2), SlothPermutation.p) === BigInt(1);
38
+ return this.fastPow(x, SlothPermutation.pHalf, SlothPermutation.p) === BigInt(1);
33
39
  }
34
40
 
35
41
  modSqrtOp(x) {
@@ -37,10 +43,10 @@ class SlothPermutation {
37
43
  let value = x;
38
44
 
39
45
  if (this.quadRes(value)) {
40
- y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
46
+ y = this.fastPow(value, SlothPermutation.pQuarter, SlothPermutation.p);
41
47
  } else {
42
48
  value = (-value + SlothPermutation.p) % SlothPermutation.p;
43
- y = this.fastPow(value, (SlothPermutation.p + BigInt(1)) / BigInt(4), SlothPermutation.p);
49
+ y = this.fastPow(value, SlothPermutation.pQuarter, SlothPermutation.p);
44
50
  }
45
51
 
46
52
  return y;
@@ -1,3 +1,12 @@
1
+ function randomBase36(length) {
2
+ let value = '';
3
+ while (value.length < length) {
4
+ const chunk = Math.random().toString(36).slice(2);
5
+ value += chunk.length > 0 ? chunk : '0';
6
+ }
7
+ return value.slice(0, length);
8
+ }
9
+
1
10
  class WebSocketSignalingProvider {
2
11
  constructor({ id, url, WebSocketImpl, priority = 0 }) {
3
12
  if (!url) {
@@ -50,8 +59,8 @@ class WebSocketSignalingProvider {
50
59
  return this.url;
51
60
  }
52
61
 
53
- const connectionId = `dignityjs_${Math.random().toString(36).slice(2, 12)}`;
54
- const token = Math.random().toString(36).slice(2, 12);
62
+ const connectionId = `dignityjs_${randomBase36(10)}`;
63
+ const token = randomBase36(10);
55
64
  const hasQuery = this.url.includes('?');
56
65
  const hasId = /[?&]id=/.test(this.url);
57
66
  const hasToken = /[?&]token=/.test(this.url);