dignity.js 0.5.4 → 0.6.0
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 +25 -3
- package/dist/dignity.cjs.js +513 -17
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +513 -17
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +27 -27
- package/docs/assets/dignity.esm.js +477 -51
- package/docs/index.html +346 -6
- package/docs/openapi-like.json +9 -1
- package/examples/decentralized-chess-lite.js +2 -2
- package/package.json +2 -1
- package/src/core/dignity-p2p.js +473 -16
- package/src/gossip/peer-group.js +64 -0
- package/src/index.js +13 -1
- package/src/network/in-memory-network.js +42 -0
- package/src/network/peerjs-network.js +28 -0
- package/src/security/message-security-service.js +10 -2
package/dist/dignity.esm.js
CHANGED
|
@@ -2769,8 +2769,14 @@ var require_message_security_service = __commonJS({
|
|
|
2769
2769
|
const nonce = naclUtil.decodeBase64(encryption.nonce);
|
|
2770
2770
|
let key;
|
|
2771
2771
|
if (encryption.kdf === "pbkdf2") {
|
|
2772
|
-
const
|
|
2773
|
-
|
|
2772
|
+
const configuredIterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
|
|
2773
|
+
const requestedIterations = encryption.kdfIterations || configuredIterations;
|
|
2774
|
+
const minIterations = Math.max(1e3, Math.floor(configuredIterations * 0.1));
|
|
2775
|
+
const maxIterations = configuredIterations * 2;
|
|
2776
|
+
if (requestedIterations < minIterations || requestedIterations > maxIterations) {
|
|
2777
|
+
throw new Error(`Invalid kdfIterations: ${requestedIterations}`);
|
|
2778
|
+
}
|
|
2779
|
+
key = await deriveBroadcastKey(password, salt, requestedIterations);
|
|
2774
2780
|
} else {
|
|
2775
2781
|
key = legacyBroadcastKey(password, salt);
|
|
2776
2782
|
}
|
|
@@ -2874,6 +2880,65 @@ var require_message_security_service = __commonJS({
|
|
|
2874
2880
|
}
|
|
2875
2881
|
});
|
|
2876
2882
|
|
|
2883
|
+
// src/gossip/peer-group.js
|
|
2884
|
+
var require_peer_group = __commonJS({
|
|
2885
|
+
"src/gossip/peer-group.js"(exports, module) {
|
|
2886
|
+
var PEER_GROUP_SCOPE_PREFIX = "gossip:";
|
|
2887
|
+
var DEFAULT_PEER_GROUP_OPTIONS = {
|
|
2888
|
+
fanout: 3,
|
|
2889
|
+
maxActivePeers: 8,
|
|
2890
|
+
maxHops: 6,
|
|
2891
|
+
relayEnabled: true
|
|
2892
|
+
};
|
|
2893
|
+
function peerGroupScope(groupId) {
|
|
2894
|
+
if (!groupId) {
|
|
2895
|
+
throw new Error("peerGroupScope requires groupId");
|
|
2896
|
+
}
|
|
2897
|
+
return `${PEER_GROUP_SCOPE_PREFIX}${groupId}`;
|
|
2898
|
+
}
|
|
2899
|
+
function parsePeerGroupScope(scope) {
|
|
2900
|
+
if (!scope || !scope.startsWith(PEER_GROUP_SCOPE_PREFIX)) {
|
|
2901
|
+
return null;
|
|
2902
|
+
}
|
|
2903
|
+
return scope.slice(PEER_GROUP_SCOPE_PREFIX.length);
|
|
2904
|
+
}
|
|
2905
|
+
function shufflePeerIds(peerIds, randomFn = Math.random) {
|
|
2906
|
+
const list = [...peerIds];
|
|
2907
|
+
for (let i = list.length - 1; i > 0; i -= 1) {
|
|
2908
|
+
const j = Math.floor(randomFn() * (i + 1));
|
|
2909
|
+
[list[i], list[j]] = [list[j], list[i]];
|
|
2910
|
+
}
|
|
2911
|
+
return list;
|
|
2912
|
+
}
|
|
2913
|
+
function selectFanoutPeers({
|
|
2914
|
+
peers,
|
|
2915
|
+
count,
|
|
2916
|
+
excludePeerIds = [],
|
|
2917
|
+
connectedPeerIds = [],
|
|
2918
|
+
randomFn = Math.random
|
|
2919
|
+
}) {
|
|
2920
|
+
const excluded = new Set(excludePeerIds.filter(Boolean));
|
|
2921
|
+
const candidates = peers.map((entry) => entry.peerId || entry).filter((peerId) => peerId && !excluded.has(peerId));
|
|
2922
|
+
const connected = new Set(connectedPeerIds.filter(Boolean));
|
|
2923
|
+
const preferred = candidates.filter((peerId) => connected.has(peerId));
|
|
2924
|
+
const others = candidates.filter((peerId) => !connected.has(peerId));
|
|
2925
|
+
const ordered = [
|
|
2926
|
+
...shufflePeerIds(preferred, randomFn),
|
|
2927
|
+
...shufflePeerIds(others, randomFn)
|
|
2928
|
+
];
|
|
2929
|
+
return ordered.slice(0, Math.max(0, count));
|
|
2930
|
+
}
|
|
2931
|
+
module.exports = {
|
|
2932
|
+
PEER_GROUP_SCOPE_PREFIX,
|
|
2933
|
+
DEFAULT_PEER_GROUP_OPTIONS,
|
|
2934
|
+
peerGroupScope,
|
|
2935
|
+
parsePeerGroupScope,
|
|
2936
|
+
shufflePeerIds,
|
|
2937
|
+
selectFanoutPeers
|
|
2938
|
+
};
|
|
2939
|
+
}
|
|
2940
|
+
});
|
|
2941
|
+
|
|
2877
2942
|
// src/core/dignity-p2p.js
|
|
2878
2943
|
var require_dignity_p2p = __commonJS({
|
|
2879
2944
|
"src/core/dignity-p2p.js"(exports, module) {
|
|
@@ -2881,6 +2946,11 @@ var require_dignity_p2p = __commonJS({
|
|
|
2881
2946
|
var naclUtil = require_nacl_util();
|
|
2882
2947
|
var EventEmitter = require_event_emitter();
|
|
2883
2948
|
var { MessageSecurityService, stableStringify } = require_message_security_service();
|
|
2949
|
+
var {
|
|
2950
|
+
DEFAULT_PEER_GROUP_OPTIONS,
|
|
2951
|
+
peerGroupScope,
|
|
2952
|
+
selectFanoutPeers
|
|
2953
|
+
} = require_peer_group();
|
|
2884
2954
|
function computeContentHash(data) {
|
|
2885
2955
|
const canonical = stableStringify(data || {});
|
|
2886
2956
|
const bytes = naclUtil.decodeUTF8(canonical);
|
|
@@ -2913,8 +2983,16 @@ var require_dignity_p2p = __commonJS({
|
|
|
2913
2983
|
this.defaultPresenceTtlMs = security && typeof security.presenceTtlMs === "number" ? security.presenceTtlMs : 45e3;
|
|
2914
2984
|
this.discoveryRooms = /* @__PURE__ */ new Map();
|
|
2915
2985
|
this.presenceByScope = /* @__PURE__ */ new Map();
|
|
2986
|
+
this.peerGroups = /* @__PURE__ */ new Map();
|
|
2987
|
+
this.seenGossipIds = /* @__PURE__ */ new Map();
|
|
2988
|
+
this.defaultPeerGroupFanout = security && typeof security.peerGroupFanout === "number" ? security.peerGroupFanout : DEFAULT_PEER_GROUP_OPTIONS.fanout;
|
|
2989
|
+
this.defaultPeerGroupMaxActivePeers = security && typeof security.peerGroupMaxActivePeers === "number" ? security.peerGroupMaxActivePeers : DEFAULT_PEER_GROUP_OPTIONS.maxActivePeers;
|
|
2990
|
+
this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS.maxHops;
|
|
2991
|
+
this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
|
|
2992
|
+
this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
|
|
2993
|
+
this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
|
|
2916
2994
|
this.state = /* @__PURE__ */ new Map();
|
|
2917
|
-
this.appliedOperations = /* @__PURE__ */ new
|
|
2995
|
+
this.appliedOperations = /* @__PURE__ */ new Map();
|
|
2918
2996
|
this.boundMessageHandler = this.handleIncomingMessage.bind(this);
|
|
2919
2997
|
}
|
|
2920
2998
|
async start() {
|
|
@@ -2922,6 +3000,14 @@ var require_dignity_p2p = __commonJS({
|
|
|
2922
3000
|
await this.networkAdapter.start(this.nodeId);
|
|
2923
3001
|
}
|
|
2924
3002
|
async stop() {
|
|
3003
|
+
const joinedGroups = Array.from(this.peerGroups.keys());
|
|
3004
|
+
for (const groupId of joinedGroups) {
|
|
3005
|
+
try {
|
|
3006
|
+
await this.leavePeerGroup(groupId);
|
|
3007
|
+
} catch (error) {
|
|
3008
|
+
this.emit("warning", { type: "peer-group-leave-failed", groupId, error });
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
2925
3011
|
const joinedScopes = Array.from(this.discoveryRooms.keys());
|
|
2926
3012
|
for (const scope of joinedScopes) {
|
|
2927
3013
|
try {
|
|
@@ -3270,6 +3356,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3270
3356
|
const connectToPeers = securityContext.connectToPeers;
|
|
3271
3357
|
if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
|
|
3272
3358
|
await this.ensureConnectedToPeers(connectToPeers);
|
|
3359
|
+
await this.enforceConnectionBudget();
|
|
3273
3360
|
}
|
|
3274
3361
|
const envelope = await this.securityService.secureOutgoingMessage({
|
|
3275
3362
|
messageType,
|
|
@@ -3277,6 +3364,11 @@ var require_dignity_p2p = __commonJS({
|
|
|
3277
3364
|
targetId: null,
|
|
3278
3365
|
securityContext
|
|
3279
3366
|
});
|
|
3367
|
+
const fanoutPeerIds = securityContext.fanoutPeerIds;
|
|
3368
|
+
if (Array.isArray(fanoutPeerIds) && fanoutPeerIds.length > 0 && typeof this.networkAdapter.sendToPeers === "function") {
|
|
3369
|
+
await this.networkAdapter.sendToPeers(envelope, fanoutPeerIds);
|
|
3370
|
+
return;
|
|
3371
|
+
}
|
|
3280
3372
|
await this.networkAdapter.broadcast(envelope);
|
|
3281
3373
|
}
|
|
3282
3374
|
async sendDirectMessage(targetId, messageType, payload) {
|
|
@@ -3292,8 +3384,280 @@ var require_dignity_p2p = __commonJS({
|
|
|
3292
3384
|
payload,
|
|
3293
3385
|
targetId
|
|
3294
3386
|
});
|
|
3387
|
+
if (targetId && typeof this.networkAdapter.sendToPeers === "function") {
|
|
3388
|
+
await this.networkAdapter.sendToPeers(envelope, [targetId]);
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3295
3391
|
await this.networkAdapter.broadcast(envelope);
|
|
3296
3392
|
}
|
|
3393
|
+
peerGroupScopeFor(groupId) {
|
|
3394
|
+
return peerGroupScope(groupId);
|
|
3395
|
+
}
|
|
3396
|
+
getPeerGroupConfig(groupId) {
|
|
3397
|
+
return this.peerGroups.get(groupId) || null;
|
|
3398
|
+
}
|
|
3399
|
+
listPeerGroupMembers(groupId, options = {}) {
|
|
3400
|
+
return this.listPeers(this.peerGroupScopeFor(groupId), options);
|
|
3401
|
+
}
|
|
3402
|
+
getPeerGroupStats() {
|
|
3403
|
+
const adapter = this.networkAdapter;
|
|
3404
|
+
const openPeerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
|
|
3405
|
+
return {
|
|
3406
|
+
joinedGroups: Array.from(this.peerGroups.keys()),
|
|
3407
|
+
seenGossipCount: this.seenGossipIds.size,
|
|
3408
|
+
openConnectionCount: openPeerIds.length,
|
|
3409
|
+
globalMaxOpenConnections: this.globalMaxOpenConnections
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
pruneSeenGossip() {
|
|
3413
|
+
const now = this.now();
|
|
3414
|
+
for (const [gossipId, expiresAt] of this.seenGossipIds.entries()) {
|
|
3415
|
+
if (expiresAt <= now) {
|
|
3416
|
+
this.seenGossipIds.delete(gossipId);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
hasSeenGossip(gossipId) {
|
|
3421
|
+
if (!gossipId) {
|
|
3422
|
+
return false;
|
|
3423
|
+
}
|
|
3424
|
+
this.pruneSeenGossip();
|
|
3425
|
+
return this.seenGossipIds.has(gossipId);
|
|
3426
|
+
}
|
|
3427
|
+
markSeenGossip(gossipId) {
|
|
3428
|
+
if (!gossipId) {
|
|
3429
|
+
return;
|
|
3430
|
+
}
|
|
3431
|
+
this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
|
|
3432
|
+
}
|
|
3433
|
+
listConnectedPeerIds() {
|
|
3434
|
+
if (typeof this.networkAdapter.listOpenPeerIds === "function") {
|
|
3435
|
+
return this.networkAdapter.listOpenPeerIds();
|
|
3436
|
+
}
|
|
3437
|
+
return [];
|
|
3438
|
+
}
|
|
3439
|
+
selectPeerGroupFanout(groupId, count, excludePeerIds = []) {
|
|
3440
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
3441
|
+
const peers = this.listPeers(scope, { includeSelf: false });
|
|
3442
|
+
return selectFanoutPeers({
|
|
3443
|
+
peers,
|
|
3444
|
+
count,
|
|
3445
|
+
excludePeerIds: [...excludePeerIds, this.nodeId],
|
|
3446
|
+
connectedPeerIds: this.listConnectedPeerIds()
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
async enforceConnectionBudget() {
|
|
3450
|
+
const adapter = this.networkAdapter;
|
|
3451
|
+
if (typeof adapter.listOpenPeerIds !== "function" || typeof adapter.disconnectPeer !== "function") {
|
|
3452
|
+
return;
|
|
3453
|
+
}
|
|
3454
|
+
const openPeerIds = adapter.listOpenPeerIds();
|
|
3455
|
+
if (openPeerIds.length <= this.globalMaxOpenConnections) {
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const excess = openPeerIds.length - this.globalMaxOpenConnections;
|
|
3459
|
+
const toClose = openPeerIds.slice(0, excess);
|
|
3460
|
+
for (const peerId of toClose) {
|
|
3461
|
+
try {
|
|
3462
|
+
await adapter.disconnectPeer(peerId);
|
|
3463
|
+
} catch (error) {
|
|
3464
|
+
this.emit("warning", { type: "peer-disconnect-failed", peerId, error });
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
async joinPeerGroup(groupId, options = {}) {
|
|
3469
|
+
if (!groupId) {
|
|
3470
|
+
throw new Error("joinPeerGroup requires groupId");
|
|
3471
|
+
}
|
|
3472
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
3473
|
+
const config = {
|
|
3474
|
+
fanout: typeof options.fanout === "number" ? options.fanout : this.defaultPeerGroupFanout,
|
|
3475
|
+
maxActivePeers: typeof options.maxActivePeers === "number" ? options.maxActivePeers : this.defaultPeerGroupMaxActivePeers,
|
|
3476
|
+
maxHops: typeof options.maxHops === "number" ? options.maxHops : this.defaultGossipMaxHops,
|
|
3477
|
+
relayEnabled: options.relayEnabled !== false
|
|
3478
|
+
};
|
|
3479
|
+
await this.joinDiscovery(scope, {
|
|
3480
|
+
metadata: {
|
|
3481
|
+
peerGroup: groupId,
|
|
3482
|
+
...options.metadata || {}
|
|
3483
|
+
},
|
|
3484
|
+
bootstrapPeerIds: options.bootstrapPeerIds,
|
|
3485
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
3486
|
+
ttlMs: options.ttlMs
|
|
3487
|
+
});
|
|
3488
|
+
this.peerGroups.set(groupId, config);
|
|
3489
|
+
this.emit("peergroupjoined", { groupId, config });
|
|
3490
|
+
return config;
|
|
3491
|
+
}
|
|
3492
|
+
async leavePeerGroup(groupId) {
|
|
3493
|
+
if (!groupId) {
|
|
3494
|
+
return;
|
|
3495
|
+
}
|
|
3496
|
+
const scope = this.peerGroupScopeFor(groupId);
|
|
3497
|
+
await this.leaveDiscovery(scope);
|
|
3498
|
+
this.peerGroups.delete(groupId);
|
|
3499
|
+
this.emit("peergroupleft", { groupId });
|
|
3500
|
+
}
|
|
3501
|
+
async publishToPeerGroup(groupId, innerMessageType, innerPayload, options = {}) {
|
|
3502
|
+
if (!groupId) {
|
|
3503
|
+
throw new Error("publishToPeerGroup requires groupId");
|
|
3504
|
+
}
|
|
3505
|
+
const group = this.peerGroups.get(groupId);
|
|
3506
|
+
if (!group && options.allowUnjoined !== true) {
|
|
3507
|
+
throw new Error(`PeerGroup ${groupId} has not been joined`);
|
|
3508
|
+
}
|
|
3509
|
+
const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
|
|
3510
|
+
const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
|
|
3511
|
+
const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
|
|
3512
|
+
const fanoutPeerIds = this.selectPeerGroupFanout(groupId, fanout, [this.nodeId]);
|
|
3513
|
+
if (fanoutPeerIds.length > 0) {
|
|
3514
|
+
await this.ensureConnectedToPeers(fanoutPeerIds.slice(0, maxActivePeers));
|
|
3515
|
+
await this.enforceConnectionBudget();
|
|
3516
|
+
}
|
|
3517
|
+
const gossipId = options.gossipId || this.idGenerator();
|
|
3518
|
+
this.markSeenGossip(gossipId);
|
|
3519
|
+
await this.broadcastMessage("peer-group:gossip", {
|
|
3520
|
+
groupId,
|
|
3521
|
+
gossipId,
|
|
3522
|
+
publisherId: this.nodeId,
|
|
3523
|
+
hop: 0,
|
|
3524
|
+
maxHop,
|
|
3525
|
+
innerMessageType,
|
|
3526
|
+
innerPayload
|
|
3527
|
+
}, {
|
|
3528
|
+
broadcastScope: this.peerGroupScopeFor(groupId),
|
|
3529
|
+
fanoutPeerIds
|
|
3530
|
+
});
|
|
3531
|
+
return { gossipId, fanoutPeerIds };
|
|
3532
|
+
}
|
|
3533
|
+
async publishRecordToPeerGroup(groupId, collectionName, id, options = {}) {
|
|
3534
|
+
const collection = this.getCollection(collectionName);
|
|
3535
|
+
const raw = collection.get(id);
|
|
3536
|
+
if (!raw || raw.deletedAt) {
|
|
3537
|
+
throw new Error(`Object ${id} does not exist in ${collectionName}`);
|
|
3538
|
+
}
|
|
3539
|
+
const record = this.normalizeRecord(raw);
|
|
3540
|
+
return this.publishToPeerGroup(groupId, "record:snapshot", {
|
|
3541
|
+
collectionName,
|
|
3542
|
+
record
|
|
3543
|
+
}, options);
|
|
3544
|
+
}
|
|
3545
|
+
async handlePeerGroupGossip(decrypted) {
|
|
3546
|
+
const payload = decrypted.payload || {};
|
|
3547
|
+
const {
|
|
3548
|
+
groupId,
|
|
3549
|
+
gossipId,
|
|
3550
|
+
publisherId = decrypted.senderId,
|
|
3551
|
+
hop = 0,
|
|
3552
|
+
maxHop: payloadMaxHop,
|
|
3553
|
+
innerMessageType,
|
|
3554
|
+
innerPayload
|
|
3555
|
+
} = payload;
|
|
3556
|
+
if (!groupId || !innerMessageType || !gossipId) {
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
if (!this.peerGroups.has(groupId)) {
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
if (this.hasSeenGossip(gossipId)) {
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
this.markSeenGossip(gossipId);
|
|
3566
|
+
await this.dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, {
|
|
3567
|
+
groupId,
|
|
3568
|
+
senderId: decrypted.senderId,
|
|
3569
|
+
publisherId
|
|
3570
|
+
});
|
|
3571
|
+
const group = this.peerGroups.get(groupId);
|
|
3572
|
+
const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
|
|
3573
|
+
const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
|
|
3574
|
+
if (!group || group.relayEnabled === false || hop >= maxHop) {
|
|
3575
|
+
return;
|
|
3576
|
+
}
|
|
3577
|
+
const relayPeers = this.selectPeerGroupFanout(groupId, group.fanout, [
|
|
3578
|
+
decrypted.senderId,
|
|
3579
|
+
this.nodeId
|
|
3580
|
+
]);
|
|
3581
|
+
if (relayPeers.length === 0) {
|
|
3582
|
+
return;
|
|
3583
|
+
}
|
|
3584
|
+
await this.ensureConnectedToPeers(relayPeers.slice(0, group.maxActivePeers));
|
|
3585
|
+
await this.enforceConnectionBudget();
|
|
3586
|
+
await this.broadcastMessage("peer-group:gossip", {
|
|
3587
|
+
groupId,
|
|
3588
|
+
gossipId,
|
|
3589
|
+
publisherId,
|
|
3590
|
+
hop: hop + 1,
|
|
3591
|
+
maxHop,
|
|
3592
|
+
innerMessageType,
|
|
3593
|
+
innerPayload
|
|
3594
|
+
}, {
|
|
3595
|
+
broadcastScope: this.peerGroupScopeFor(groupId),
|
|
3596
|
+
fanoutPeerIds: relayPeers
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
normalizeGossipOperation(operation, publisherId) {
|
|
3600
|
+
if (!operation || !publisherId) {
|
|
3601
|
+
return null;
|
|
3602
|
+
}
|
|
3603
|
+
if (operation.actorId && operation.actorId !== publisherId) {
|
|
3604
|
+
this.emit("warning", {
|
|
3605
|
+
type: "gossip-operation-actor-mismatch",
|
|
3606
|
+
publisherId,
|
|
3607
|
+
actorId: operation.actorId,
|
|
3608
|
+
kind: operation.kind,
|
|
3609
|
+
collection: operation.collectionName,
|
|
3610
|
+
id: operation.id
|
|
3611
|
+
});
|
|
3612
|
+
return null;
|
|
3613
|
+
}
|
|
3614
|
+
const normalized = {
|
|
3615
|
+
...operation,
|
|
3616
|
+
actorId: publisherId
|
|
3617
|
+
};
|
|
3618
|
+
if (normalized.kind === "create") {
|
|
3619
|
+
normalized.ownerId = publisherId;
|
|
3620
|
+
}
|
|
3621
|
+
return normalized;
|
|
3622
|
+
}
|
|
3623
|
+
async dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, context = {}) {
|
|
3624
|
+
if (innerMessageType === "operation") {
|
|
3625
|
+
const operation = this.normalizeGossipOperation(
|
|
3626
|
+
innerPayload,
|
|
3627
|
+
context.publisherId || context.senderId
|
|
3628
|
+
);
|
|
3629
|
+
if (operation) {
|
|
3630
|
+
this.applyOperation(operation);
|
|
3631
|
+
}
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
if (innerMessageType === "record:snapshot") {
|
|
3635
|
+
const { collectionName, record } = innerPayload || {};
|
|
3636
|
+
if (collectionName && record) {
|
|
3637
|
+
const applied = this.restoreRecord(collectionName, record, {
|
|
3638
|
+
rejectOnHashMismatch: true,
|
|
3639
|
+
rejectOnOwnershipMismatch: true,
|
|
3640
|
+
via: "peer-group"
|
|
3641
|
+
});
|
|
3642
|
+
if (applied) {
|
|
3643
|
+
this.emit("change", {
|
|
3644
|
+
kind: "snapshot",
|
|
3645
|
+
collection: collectionName,
|
|
3646
|
+
id: record.id,
|
|
3647
|
+
via: "peer-group",
|
|
3648
|
+
groupId: context.groupId
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
}
|
|
3652
|
+
return;
|
|
3653
|
+
}
|
|
3654
|
+
this.emit("peergroupmessage", {
|
|
3655
|
+
groupId: context.groupId,
|
|
3656
|
+
senderId: context.senderId,
|
|
3657
|
+
type: innerMessageType,
|
|
3658
|
+
payload: innerPayload
|
|
3659
|
+
});
|
|
3660
|
+
}
|
|
3297
3661
|
getPresenceMap(scope) {
|
|
3298
3662
|
if (!this.presenceByScope.has(scope)) {
|
|
3299
3663
|
this.presenceByScope.set(scope, /* @__PURE__ */ new Map());
|
|
@@ -3424,6 +3788,13 @@ var require_dignity_p2p = __commonJS({
|
|
|
3424
3788
|
}
|
|
3425
3789
|
async handleIncomingMessage(message) {
|
|
3426
3790
|
if (message && message.opId && message.kind) {
|
|
3791
|
+
if (this.securityService.options.enabled) {
|
|
3792
|
+
this.emit("messageignored", {
|
|
3793
|
+
reason: "raw-operation-rejected",
|
|
3794
|
+
hint: "Unsigned raw operations are disabled when security is enabled"
|
|
3795
|
+
});
|
|
3796
|
+
return;
|
|
3797
|
+
}
|
|
3427
3798
|
this.applyOperation(message);
|
|
3428
3799
|
return;
|
|
3429
3800
|
}
|
|
@@ -3434,9 +3805,6 @@ var require_dignity_p2p = __commonJS({
|
|
|
3434
3805
|
});
|
|
3435
3806
|
return;
|
|
3436
3807
|
}
|
|
3437
|
-
if (message && message.senderId && message.senderPublicKey) {
|
|
3438
|
-
this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
|
|
3439
|
-
}
|
|
3440
3808
|
let decrypted;
|
|
3441
3809
|
try {
|
|
3442
3810
|
decrypted = await this.securityService.decryptIncomingMessage(message);
|
|
@@ -3454,6 +3822,9 @@ var require_dignity_p2p = __commonJS({
|
|
|
3454
3822
|
if (!decrypted || decrypted.ignored) {
|
|
3455
3823
|
return;
|
|
3456
3824
|
}
|
|
3825
|
+
if (message && message.senderId && message.senderPublicKey) {
|
|
3826
|
+
this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
|
|
3827
|
+
}
|
|
3457
3828
|
if (decrypted.messageType === "operation") {
|
|
3458
3829
|
this.applyOperation(decrypted.payload);
|
|
3459
3830
|
return;
|
|
@@ -3477,18 +3848,27 @@ var require_dignity_p2p = __commonJS({
|
|
|
3477
3848
|
const payload = decrypted.payload || {};
|
|
3478
3849
|
const scope = payload.scope || "main";
|
|
3479
3850
|
const peerId = payload.peerId || decrypted.senderId;
|
|
3480
|
-
if (!peerId) {
|
|
3851
|
+
if (!peerId || peerId !== decrypted.senderId) {
|
|
3852
|
+
return;
|
|
3853
|
+
}
|
|
3854
|
+
if (!this.discoveryRooms.has(scope)) {
|
|
3481
3855
|
return;
|
|
3482
3856
|
}
|
|
3857
|
+
const room = this.discoveryRooms.get(scope);
|
|
3483
3858
|
const presenceMap = this.getPresenceMap(scope);
|
|
3484
3859
|
const isNewPeerInScope = !presenceMap.has(peerId);
|
|
3860
|
+
const requestedTtl = typeof payload.ttlMs === "number" ? payload.ttlMs : room.ttlMs;
|
|
3861
|
+
const ttlMs = Math.min(requestedTtl, room.ttlMs);
|
|
3485
3862
|
this.upsertPresence(
|
|
3486
3863
|
scope,
|
|
3487
3864
|
peerId,
|
|
3488
3865
|
payload.metadata || {},
|
|
3489
|
-
|
|
3490
|
-
|
|
3866
|
+
ttlMs,
|
|
3867
|
+
this.now()
|
|
3491
3868
|
);
|
|
3869
|
+
if (payload.metadata && payload.metadata.publicKey) {
|
|
3870
|
+
this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
|
|
3871
|
+
}
|
|
3492
3872
|
if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
|
|
3493
3873
|
if (typeof this.networkAdapter.connectToPeer === "function") {
|
|
3494
3874
|
Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
|
|
@@ -3505,6 +3885,9 @@ var require_dignity_p2p = __commonJS({
|
|
|
3505
3885
|
const payload = decrypted.payload || {};
|
|
3506
3886
|
const scope = payload.scope || "main";
|
|
3507
3887
|
const peerId = payload.peerId || decrypted.senderId;
|
|
3888
|
+
if (!peerId || peerId !== decrypted.senderId) {
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3508
3891
|
const map = this.presenceByScope.get(scope);
|
|
3509
3892
|
if (map && peerId && map.has(peerId)) {
|
|
3510
3893
|
map.delete(peerId);
|
|
@@ -3512,6 +3895,10 @@ var require_dignity_p2p = __commonJS({
|
|
|
3512
3895
|
}
|
|
3513
3896
|
return;
|
|
3514
3897
|
}
|
|
3898
|
+
if (decrypted.messageType === "peer-group:gossip") {
|
|
3899
|
+
await this.handlePeerGroupGossip(decrypted);
|
|
3900
|
+
return;
|
|
3901
|
+
}
|
|
3515
3902
|
this.emit("message", {
|
|
3516
3903
|
senderId: decrypted.senderId,
|
|
3517
3904
|
targetId: decrypted.targetId,
|
|
@@ -3557,7 +3944,7 @@ var require_dignity_p2p = __commonJS({
|
|
|
3557
3944
|
emitConflict(details) {
|
|
3558
3945
|
this.emit("conflict", details);
|
|
3559
3946
|
}
|
|
3560
|
-
restoreRecord(collectionName, record) {
|
|
3947
|
+
restoreRecord(collectionName, record, options = {}) {
|
|
3561
3948
|
if (!record || !record.id) {
|
|
3562
3949
|
return false;
|
|
3563
3950
|
}
|
|
@@ -3568,14 +3955,42 @@ var require_dignity_p2p = __commonJS({
|
|
|
3568
3955
|
}
|
|
3569
3956
|
const restoredData = { ...record.data || {} };
|
|
3570
3957
|
const computedHash = computeContentHash(restoredData);
|
|
3571
|
-
|
|
3958
|
+
const rejectOnHashMismatch = options.rejectOnHashMismatch === true;
|
|
3959
|
+
const rejectOnOwnershipMismatch = options.rejectOnOwnershipMismatch === true;
|
|
3960
|
+
if (rejectOnOwnershipMismatch && current && record.ownerId && current.ownerId !== record.ownerId) {
|
|
3961
|
+
this.emit("warning", {
|
|
3962
|
+
type: "ownership-mismatch",
|
|
3963
|
+
collection: collectionName,
|
|
3964
|
+
id: record.id,
|
|
3965
|
+
currentOwnerId: current.ownerId,
|
|
3966
|
+
advertisedOwnerId: record.ownerId,
|
|
3967
|
+
via: options.via || null
|
|
3968
|
+
});
|
|
3969
|
+
return false;
|
|
3970
|
+
}
|
|
3971
|
+
if (!record.hash) {
|
|
3972
|
+
const warning = {
|
|
3973
|
+
type: "content-hash-missing",
|
|
3974
|
+
collection: collectionName,
|
|
3975
|
+
id: record.id,
|
|
3976
|
+
via: options.via || null
|
|
3977
|
+
};
|
|
3978
|
+
this.emit("warning", warning);
|
|
3979
|
+
if (rejectOnHashMismatch) {
|
|
3980
|
+
return false;
|
|
3981
|
+
}
|
|
3982
|
+
} else if (record.hash !== computedHash) {
|
|
3572
3983
|
this.emit("warning", {
|
|
3573
3984
|
type: "content-hash-mismatch",
|
|
3574
3985
|
collection: collectionName,
|
|
3575
3986
|
id: record.id,
|
|
3576
3987
|
advertisedHash: record.hash,
|
|
3577
|
-
computedHash
|
|
3988
|
+
computedHash,
|
|
3989
|
+
via: options.via || null
|
|
3578
3990
|
});
|
|
3991
|
+
if (rejectOnHashMismatch) {
|
|
3992
|
+
return false;
|
|
3993
|
+
}
|
|
3579
3994
|
}
|
|
3580
3995
|
collection.set(record.id, {
|
|
3581
3996
|
id: record.id,
|
|
@@ -3617,6 +4032,15 @@ var require_dignity_p2p = __commonJS({
|
|
|
3617
4032
|
});
|
|
3618
4033
|
return record;
|
|
3619
4034
|
}
|
|
4035
|
+
pruneAppliedOperations() {
|
|
4036
|
+
while (this.appliedOperations.size > this.maxAppliedOperations) {
|
|
4037
|
+
const oldestOpId = this.appliedOperations.keys().next().value;
|
|
4038
|
+
if (!oldestOpId) {
|
|
4039
|
+
break;
|
|
4040
|
+
}
|
|
4041
|
+
this.appliedOperations.delete(oldestOpId);
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
3620
4044
|
applyOperation(operation) {
|
|
3621
4045
|
if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
|
|
3622
4046
|
return false;
|
|
@@ -3638,7 +4062,8 @@ var require_dignity_p2p = __commonJS({
|
|
|
3638
4062
|
deletedAt: null,
|
|
3639
4063
|
version: 1
|
|
3640
4064
|
});
|
|
3641
|
-
this.appliedOperations.
|
|
4065
|
+
this.appliedOperations.set(operation.opId, this.now());
|
|
4066
|
+
this.pruneAppliedOperations();
|
|
3642
4067
|
this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
|
|
3643
4068
|
return true;
|
|
3644
4069
|
}
|
|
@@ -3682,7 +4107,8 @@ var require_dignity_p2p = __commonJS({
|
|
|
3682
4107
|
}
|
|
3683
4108
|
current.updatedAt = operation.timestamp;
|
|
3684
4109
|
current.version += 1;
|
|
3685
|
-
this.appliedOperations.
|
|
4110
|
+
this.appliedOperations.set(operation.opId, this.now());
|
|
4111
|
+
this.pruneAppliedOperations();
|
|
3686
4112
|
this.emit("change", {
|
|
3687
4113
|
kind: "transfer-ownership",
|
|
3688
4114
|
collection: operation.collectionName,
|
|
@@ -3711,7 +4137,8 @@ var require_dignity_p2p = __commonJS({
|
|
|
3711
4137
|
current.deletedAt = operation.timestamp;
|
|
3712
4138
|
current.updatedAt = operation.timestamp;
|
|
3713
4139
|
current.version += 1;
|
|
3714
|
-
this.appliedOperations.
|
|
4140
|
+
this.appliedOperations.set(operation.opId, this.now());
|
|
4141
|
+
this.pruneAppliedOperations();
|
|
3715
4142
|
this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
|
|
3716
4143
|
return true;
|
|
3717
4144
|
}
|
|
@@ -3741,7 +4168,8 @@ var require_dignity_p2p = __commonJS({
|
|
|
3741
4168
|
}
|
|
3742
4169
|
current.updatedAt = operation.timestamp;
|
|
3743
4170
|
current.version += 1;
|
|
3744
|
-
this.appliedOperations.
|
|
4171
|
+
this.appliedOperations.set(operation.opId, this.now());
|
|
4172
|
+
this.pruneAppliedOperations();
|
|
3745
4173
|
this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
|
|
3746
4174
|
return true;
|
|
3747
4175
|
}
|
|
@@ -10789,6 +11217,16 @@ var require_in_memory_network = __commonJS({
|
|
|
10789
11217
|
}
|
|
10790
11218
|
await Promise.all(deliveries);
|
|
10791
11219
|
}
|
|
11220
|
+
async sendToPeers(senderId, message, peerIds = []) {
|
|
11221
|
+
const targets = new Set((peerIds || []).filter((peerId) => peerId && peerId !== senderId));
|
|
11222
|
+
const deliveries = [];
|
|
11223
|
+
for (const [nodeId, adapter] of this.adapters.entries()) {
|
|
11224
|
+
if (nodeId !== senderId && targets.has(nodeId)) {
|
|
11225
|
+
deliveries.push(adapter.receive(message));
|
|
11226
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
await Promise.all(deliveries);
|
|
11229
|
+
}
|
|
10792
11230
|
};
|
|
10793
11231
|
var InMemoryNetworkAdapter = class {
|
|
10794
11232
|
constructor(hub) {
|
|
@@ -10798,6 +11236,7 @@ var require_in_memory_network = __commonJS({
|
|
|
10798
11236
|
this.hub = hub;
|
|
10799
11237
|
this.nodeId = null;
|
|
10800
11238
|
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
11239
|
+
this.connectedPeers = /* @__PURE__ */ new Set();
|
|
10801
11240
|
}
|
|
10802
11241
|
async start(nodeId) {
|
|
10803
11242
|
this.nodeId = nodeId;
|
|
@@ -10808,6 +11247,13 @@ var require_in_memory_network = __commonJS({
|
|
|
10808
11247
|
this.hub.unregister(this.nodeId);
|
|
10809
11248
|
}
|
|
10810
11249
|
this.nodeId = null;
|
|
11250
|
+
this.connectedPeers.clear();
|
|
11251
|
+
}
|
|
11252
|
+
async connectToPeer(remotePeerId) {
|
|
11253
|
+
if (!remotePeerId || remotePeerId === this.nodeId) {
|
|
11254
|
+
return;
|
|
11255
|
+
}
|
|
11256
|
+
this.connectedPeers.add(remotePeerId);
|
|
10811
11257
|
}
|
|
10812
11258
|
async broadcast(message) {
|
|
10813
11259
|
if (!this.nodeId) {
|
|
@@ -10815,6 +11261,21 @@ var require_in_memory_network = __commonJS({
|
|
|
10815
11261
|
}
|
|
10816
11262
|
await this.hub.broadcast(this.nodeId, message);
|
|
10817
11263
|
}
|
|
11264
|
+
async sendToPeers(message, peerIds = []) {
|
|
11265
|
+
if (!this.nodeId) {
|
|
11266
|
+
throw new Error("Network adapter has not been started");
|
|
11267
|
+
}
|
|
11268
|
+
await this.hub.sendToPeers(this.nodeId, message, peerIds);
|
|
11269
|
+
}
|
|
11270
|
+
listOpenPeerIds() {
|
|
11271
|
+
return [...this.connectedPeers];
|
|
11272
|
+
}
|
|
11273
|
+
getOpenConnectionCount() {
|
|
11274
|
+
return this.connectedPeers.size;
|
|
11275
|
+
}
|
|
11276
|
+
isConnectedTo(remotePeerId) {
|
|
11277
|
+
return this.connectedPeers.has(remotePeerId);
|
|
11278
|
+
}
|
|
10818
11279
|
onMessage(handler) {
|
|
10819
11280
|
this.messageHandlers.add(handler);
|
|
10820
11281
|
}
|
|
@@ -10993,6 +11454,29 @@ var require_peerjs_network = __commonJS({
|
|
|
10993
11454
|
}
|
|
10994
11455
|
await Promise.all(deliveries);
|
|
10995
11456
|
}
|
|
11457
|
+
async sendToPeers(message, peerIds = []) {
|
|
11458
|
+
if (!this.peer) {
|
|
11459
|
+
throw new Error("PeerJS network adapter has not been started");
|
|
11460
|
+
}
|
|
11461
|
+
const targets = new Set((peerIds || []).filter(Boolean));
|
|
11462
|
+
if (targets.size === 0) {
|
|
11463
|
+
return;
|
|
11464
|
+
}
|
|
11465
|
+
const deliveries = [];
|
|
11466
|
+
for (const [peerId, connection] of this.connections.entries()) {
|
|
11467
|
+
if (targets.has(peerId) && connection.open) {
|
|
11468
|
+
deliveries.push(connection.send(message));
|
|
11469
|
+
}
|
|
11470
|
+
}
|
|
11471
|
+
await Promise.all(deliveries);
|
|
11472
|
+
}
|
|
11473
|
+
async disconnectPeer(remotePeerId) {
|
|
11474
|
+
const connection = this.connections.get(remotePeerId);
|
|
11475
|
+
if (connection && typeof connection.close === "function") {
|
|
11476
|
+
connection.close();
|
|
11477
|
+
}
|
|
11478
|
+
this.connections.delete(remotePeerId);
|
|
11479
|
+
}
|
|
10996
11480
|
getOpenConnectionCount() {
|
|
10997
11481
|
return this.listOpenPeerIds().length;
|
|
10998
11482
|
}
|
|
@@ -11223,6 +11707,13 @@ var require_index = __commonJS({
|
|
|
11223
11707
|
MessageSecurityService,
|
|
11224
11708
|
DEFAULT_SECURITY_OPTIONS
|
|
11225
11709
|
} = require_message_security_service();
|
|
11710
|
+
var {
|
|
11711
|
+
PEER_GROUP_SCOPE_PREFIX,
|
|
11712
|
+
DEFAULT_PEER_GROUP_OPTIONS,
|
|
11713
|
+
peerGroupScope,
|
|
11714
|
+
parsePeerGroupScope,
|
|
11715
|
+
selectFanoutPeers
|
|
11716
|
+
} = require_peer_group();
|
|
11226
11717
|
module.exports = {
|
|
11227
11718
|
DignityP2P,
|
|
11228
11719
|
createDefaultSignalingPool,
|
|
@@ -11239,7 +11730,12 @@ var require_index = __commonJS({
|
|
|
11239
11730
|
VDF,
|
|
11240
11731
|
SlothPermutation,
|
|
11241
11732
|
MessageSecurityService,
|
|
11242
|
-
DEFAULT_SECURITY_OPTIONS
|
|
11733
|
+
DEFAULT_SECURITY_OPTIONS,
|
|
11734
|
+
PEER_GROUP_SCOPE_PREFIX,
|
|
11735
|
+
DEFAULT_PEER_GROUP_OPTIONS,
|
|
11736
|
+
peerGroupScope,
|
|
11737
|
+
parsePeerGroupScope,
|
|
11738
|
+
selectFanoutPeers
|
|
11243
11739
|
};
|
|
11244
11740
|
}
|
|
11245
11741
|
});
|