dignity.js 0.3.0 → 0.5.1

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.
Files changed (43) hide show
  1. package/README.md +142 -4
  2. package/dist/dignity.cjs.js +768 -20
  3. package/dist/dignity.cjs.js.map +4 -4
  4. package/dist/dignity.esm.js +768 -20
  5. package/dist/dignity.esm.js.map +3 -3
  6. package/dist/dignity.min.js +18 -18
  7. package/docs/assets/dignity.esm.js +11205 -0
  8. package/docs/assets/docs.js +47 -0
  9. package/docs/assets/favicon.svg +8 -0
  10. package/docs/assets/highlight/github-dark.min.css +10 -0
  11. package/docs/assets/highlight/github.min.css +10 -0
  12. package/docs/assets/highlight/highlight.min.js +1244 -0
  13. package/docs/assets/styles.css +449 -38
  14. package/docs/chess/assets/chess-app.js +58022 -0
  15. package/docs/chess/assets/chess-app.js.map +7 -0
  16. package/docs/chess/assets/chess.css +584 -0
  17. package/docs/chess/favicon.ico +0 -0
  18. package/docs/chess/index.html +16 -0
  19. package/docs/chess/src/App.jsx +128 -0
  20. package/docs/chess/src/components/Board3D.jsx +364 -0
  21. package/docs/chess/src/components/GameView.jsx +847 -0
  22. package/docs/chess/src/components/JoinGate.jsx +68 -0
  23. package/docs/chess/src/components/LinkPanel.jsx +132 -0
  24. package/docs/chess/src/components/Lobby.jsx +154 -0
  25. package/docs/chess/src/components/MovePanel.jsx +123 -0
  26. package/docs/chess/src/lib/audio.js +50 -0
  27. package/docs/chess/src/lib/dignitySetup.js +42 -0
  28. package/docs/chess/src/lib/links.js +124 -0
  29. package/docs/chess/src/lib/localGames.js +160 -0
  30. package/docs/chess/src/lib/p2pDebug.js +192 -0
  31. package/docs/chess/src/main.jsx +5 -0
  32. package/docs/favicon.ico +0 -0
  33. package/docs/index.html +605 -81
  34. package/docs/openapi-like.json +74 -7
  35. package/examples/decentralized-chess-lite.js +52 -30
  36. package/package.json +30 -4
  37. package/src/core/dignity-p2p.js +466 -15
  38. package/src/index.js +8 -0
  39. package/src/network/peerjs-network.js +234 -0
  40. package/src/persistence/indexeddb-persistence.js +184 -0
  41. package/src/react/index.js +256 -0
  42. package/src/signaling/parse-peerjs-url.js +24 -0
  43. package/src/signaling/peerjs-signaling-provider.js +2 -8
@@ -2934,12 +2934,71 @@ var require_dignity_p2p = __commonJS({
2934
2934
  return {
2935
2935
  id: record.id,
2936
2936
  ownerId: record.ownerId,
2937
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
2937
2938
  createdAt: record.createdAt,
2938
2939
  updatedAt: record.updatedAt,
2939
2940
  version: record.version,
2940
2941
  data: { ...record.data }
2941
2942
  };
2942
2943
  }
2944
+ canUpdateRecord(record, actorId) {
2945
+ if (!record || !actorId) {
2946
+ return false;
2947
+ }
2948
+ if (record.ownerId === actorId) {
2949
+ return true;
2950
+ }
2951
+ return Array.isArray(record.collaboratorIds) && record.collaboratorIds.includes(actorId);
2952
+ }
2953
+ normalizeCollaboratorIds(collaborators) {
2954
+ if (!Array.isArray(collaborators)) {
2955
+ return [];
2956
+ }
2957
+ return [...new Set(collaborators.filter(Boolean))];
2958
+ }
2959
+ getRecordPeerIds(collectionName, id, options = {}) {
2960
+ const record = options.fromRecord || this.getCollection(collectionName).get(id);
2961
+ if (!record) {
2962
+ return [];
2963
+ }
2964
+ const includeSelf = options.includeSelf === true;
2965
+ const peerIds = [record.ownerId, ...record.collaboratorIds || []];
2966
+ return [...new Set(peerIds.filter(Boolean).filter((peerId) => includeSelf || peerId !== this.nodeId))];
2967
+ }
2968
+ resolveReplicationPeers(collectionName, id, options = {}, hints = {}) {
2969
+ if (options.connectToPeers === false) {
2970
+ return void 0;
2971
+ }
2972
+ if (Array.isArray(options.connectToPeers)) {
2973
+ return options.connectToPeers;
2974
+ }
2975
+ const peerIds = /* @__PURE__ */ new Set();
2976
+ if (hints.fromRecord) {
2977
+ for (const peerId of this.getRecordPeerIds(collectionName, id, {
2978
+ fromRecord: hints.fromRecord,
2979
+ includeSelf: true
2980
+ })) {
2981
+ peerIds.add(peerId);
2982
+ }
2983
+ } else if (id) {
2984
+ for (const peerId of this.getRecordPeerIds(collectionName, id, { includeSelf: true })) {
2985
+ peerIds.add(peerId);
2986
+ }
2987
+ }
2988
+ if (Array.isArray(options.collaborators)) {
2989
+ for (const peerId of this.normalizeCollaboratorIds(options.collaborators)) {
2990
+ peerIds.add(peerId);
2991
+ }
2992
+ }
2993
+ if (Array.isArray(hints.extraPeerIds)) {
2994
+ for (const peerId of hints.extraPeerIds) {
2995
+ if (peerId) {
2996
+ peerIds.add(peerId);
2997
+ }
2998
+ }
2999
+ }
3000
+ return [...peerIds].filter((peerId) => peerId && peerId !== this.nodeId);
3001
+ }
2943
3002
  async create(collectionName, data, options = {}) {
2944
3003
  const collection = this.getCollection(collectionName);
2945
3004
  const id = options.id || this.idGenerator();
@@ -2947,6 +3006,7 @@ var require_dignity_p2p = __commonJS({
2947
3006
  throw new Error(`Object ${id} already exists in ${collectionName}`);
2948
3007
  }
2949
3008
  const timestamp = this.now();
3009
+ const collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
2950
3010
  const operation = {
2951
3011
  opId: this.idGenerator(),
2952
3012
  kind: "create",
@@ -2954,6 +3014,7 @@ var require_dignity_p2p = __commonJS({
2954
3014
  id,
2955
3015
  actorId: this.nodeId,
2956
3016
  ownerId: this.nodeId,
3017
+ collaboratorIds,
2957
3018
  timestamp,
2958
3019
  payload: { ...data }
2959
3020
  };
@@ -2963,6 +3024,9 @@ var require_dignity_p2p = __commonJS({
2963
3024
  messageType: "operation",
2964
3025
  operation,
2965
3026
  collectionName
3027
+ }),
3028
+ connectToPeers: this.resolveReplicationPeers(collectionName, null, options, {
3029
+ extraPeerIds: options.collaborators
2966
3030
  })
2967
3031
  });
2968
3032
  return this.read(collectionName, id);
@@ -2997,8 +3061,26 @@ var require_dignity_p2p = __commonJS({
2997
3061
  if (!existing || existing.deletedAt) {
2998
3062
  throw new Error(`Object ${id} does not exist in ${collectionName}`);
2999
3063
  }
3000
- if (existing.ownerId !== this.nodeId) {
3001
- throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
3064
+ if (!this.canUpdateRecord(existing, this.nodeId)) {
3065
+ throw new Error(`Only owner ${existing.ownerId} or collaborators can update object ${id}`);
3066
+ }
3067
+ if (options.collaborators !== void 0 && existing.ownerId !== this.nodeId) {
3068
+ throw new Error(`Only owner ${existing.ownerId} can change collaborators on object ${id}`);
3069
+ }
3070
+ if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
3071
+ this.emitConflict({
3072
+ kind: "update",
3073
+ collection: collectionName,
3074
+ id,
3075
+ expectedVersion: options.expectedVersion,
3076
+ currentVersion: existing.version,
3077
+ phase: "local"
3078
+ });
3079
+ const error = new Error(
3080
+ `Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
3081
+ );
3082
+ error.code = "VERSION_CONFLICT";
3083
+ throw error;
3002
3084
  }
3003
3085
  const operation = {
3004
3086
  opId: this.idGenerator(),
@@ -3010,12 +3092,73 @@ var require_dignity_p2p = __commonJS({
3010
3092
  baseVersion: existing.version,
3011
3093
  payload: { ...partialData }
3012
3094
  };
3095
+ if (options.collaborators !== void 0) {
3096
+ operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
3097
+ }
3098
+ this.applyOperation(operation);
3099
+ await this.broadcastMessage("operation", operation, {
3100
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3101
+ messageType: "operation",
3102
+ operation,
3103
+ collectionName
3104
+ }),
3105
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3106
+ });
3107
+ return this.read(collectionName, id);
3108
+ }
3109
+ async updateWithRetry(collectionName, id, patchFn, options = {}) {
3110
+ const maxAttempts = typeof options.maxAttempts === "number" ? options.maxAttempts : 5;
3111
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3112
+ const current = this.read(collectionName, id);
3113
+ if (!current) {
3114
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3115
+ }
3116
+ const patch = await patchFn(current);
3117
+ try {
3118
+ return await this.update(collectionName, id, patch, {
3119
+ ...options,
3120
+ expectedVersion: current.version
3121
+ });
3122
+ } catch (error) {
3123
+ if (error.code !== "VERSION_CONFLICT" || attempt === maxAttempts - 1) {
3124
+ throw error;
3125
+ }
3126
+ }
3127
+ }
3128
+ throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3129
+ }
3130
+ async transferOwnership(collectionName, id, newOwnerId, options = {}) {
3131
+ if (!newOwnerId) {
3132
+ throw new Error("newOwnerId is required");
3133
+ }
3134
+ const existing = this.getCollection(collectionName).get(id);
3135
+ if (!existing || existing.deletedAt) {
3136
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3137
+ }
3138
+ if (existing.ownerId !== this.nodeId) {
3139
+ throw new Error(`Only owner ${existing.ownerId} can transfer object ${id}`);
3140
+ }
3141
+ const operation = {
3142
+ opId: this.idGenerator(),
3143
+ kind: "transfer-ownership",
3144
+ collectionName,
3145
+ id,
3146
+ actorId: this.nodeId,
3147
+ timestamp: this.now(),
3148
+ baseVersion: existing.version,
3149
+ newOwnerId,
3150
+ keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
3151
+ };
3013
3152
  this.applyOperation(operation);
3014
3153
  await this.broadcastMessage("operation", operation, {
3015
3154
  broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3016
3155
  messageType: "operation",
3017
3156
  operation,
3018
3157
  collectionName
3158
+ }),
3159
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, {
3160
+ fromRecord: existing,
3161
+ extraPeerIds: [newOwnerId]
3019
3162
  })
3020
3163
  });
3021
3164
  return this.read(collectionName, id);
@@ -3043,16 +3186,74 @@ var require_dignity_p2p = __commonJS({
3043
3186
  messageType: "operation",
3044
3187
  operation,
3045
3188
  collectionName
3046
- })
3189
+ }),
3190
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3047
3191
  });
3048
3192
  }
3049
3193
  registerPeerPublicKey(peerId, publicKey) {
3050
3194
  this.securityService.registerPeerPublicKey(peerId, publicKey);
3051
3195
  }
3196
+ trustPeerPublicKey(peerId, publicKey) {
3197
+ if (!peerId || !publicKey) {
3198
+ return false;
3199
+ }
3200
+ try {
3201
+ this.registerPeerPublicKey(peerId, publicKey);
3202
+ return true;
3203
+ } catch (error) {
3204
+ this.emit("warning", { type: "peer-key-trust-failed", peerId, error });
3205
+ return false;
3206
+ }
3207
+ }
3208
+ trustPeerFromMetadata(peerId, metadata) {
3209
+ if (!metadata || !metadata.publicKey) {
3210
+ return false;
3211
+ }
3212
+ return this.trustPeerPublicKey(peerId, metadata.publicKey);
3213
+ }
3052
3214
  getPublicKey() {
3053
3215
  return this.securityService.getPublicKey();
3054
3216
  }
3217
+ async connectToPeer(peerId) {
3218
+ if (!peerId || peerId === this.nodeId) {
3219
+ return null;
3220
+ }
3221
+ if (typeof this.networkAdapter.connectToPeer !== "function") {
3222
+ throw new Error("Network adapter does not support connectToPeer");
3223
+ }
3224
+ return this.networkAdapter.connectToPeer(peerId);
3225
+ }
3226
+ getConnectionStats() {
3227
+ const adapter = this.networkAdapter;
3228
+ if (!adapter) {
3229
+ return { openCount: 0, peerIds: [] };
3230
+ }
3231
+ const peerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3232
+ const openCount = typeof adapter.getOpenConnectionCount === "function" ? adapter.getOpenConnectionCount() : peerIds.length;
3233
+ return { openCount, peerIds };
3234
+ }
3235
+ async ensureConnectedToPeers(peerIds = []) {
3236
+ const normalized = [...new Set((peerIds || []).filter(Boolean))];
3237
+ const results = [];
3238
+ for (const peerId of normalized) {
3239
+ if (peerId === this.nodeId) {
3240
+ continue;
3241
+ }
3242
+ try {
3243
+ await this.connectToPeer(peerId);
3244
+ results.push({ peerId, ok: true });
3245
+ } catch (error) {
3246
+ this.emit("warning", { type: "peer-connect-failed", peerId, error });
3247
+ results.push({ peerId, ok: false, error });
3248
+ }
3249
+ }
3250
+ return results;
3251
+ }
3055
3252
  async broadcastMessage(messageType, payload, securityContext = {}) {
3253
+ const connectToPeers = securityContext.connectToPeers;
3254
+ if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3255
+ await this.ensureConnectedToPeers(connectToPeers);
3256
+ }
3056
3257
  const envelope = await this.securityService.secureOutgoingMessage({
3057
3258
  messageType,
3058
3259
  payload,
@@ -3062,6 +3263,13 @@ var require_dignity_p2p = __commonJS({
3062
3263
  await this.networkAdapter.broadcast(envelope);
3063
3264
  }
3064
3265
  async sendDirectMessage(targetId, messageType, payload) {
3266
+ if (targetId) {
3267
+ try {
3268
+ await this.connectToPeer(targetId);
3269
+ } catch (error) {
3270
+ this.emit("warning", { type: "direct-message-connect-failed", targetId, error });
3271
+ }
3272
+ }
3065
3273
  const envelope = await this.securityService.secureOutgoingMessage({
3066
3274
  messageType,
3067
3275
  payload,
@@ -3086,6 +3294,7 @@ var require_dignity_p2p = __commonJS({
3086
3294
  expiresAt: announcedAt + ttlMs
3087
3295
  };
3088
3296
  map.set(peerId, next);
3297
+ this.trustPeerFromMetadata(peerId, next.metadata);
3089
3298
  if (!existing) {
3090
3299
  this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
3091
3300
  }
@@ -3108,11 +3317,18 @@ var require_dignity_p2p = __commonJS({
3108
3317
  const normalizedScope = scope || "main";
3109
3318
  const heartbeatIntervalMs = options.heartbeatIntervalMs || this.defaultDiscoveryHeartbeatMs;
3110
3319
  const ttlMs = options.ttlMs || this.defaultPresenceTtlMs;
3111
- const metadata = options.metadata || {};
3320
+ const metadata = {
3321
+ publicKey: this.getPublicKey(),
3322
+ ...options.metadata || {}
3323
+ };
3324
+ const bootstrapPeerIds = Array.isArray(options.bootstrapPeerIds) ? [...new Set(options.bootstrapPeerIds.filter(Boolean))] : [];
3112
3325
  const existing = this.discoveryRooms.get(normalizedScope);
3113
3326
  if (existing && existing.timer) {
3114
3327
  clearInterval(existing.timer);
3115
3328
  }
3329
+ if (bootstrapPeerIds.length > 0) {
3330
+ await this.ensureConnectedToPeers(bootstrapPeerIds);
3331
+ }
3116
3332
  const timer = setInterval(() => {
3117
3333
  this.announcePresence(normalizedScope).catch((error) => {
3118
3334
  this.emit("warning", { type: "presence-heartbeat-failed", scope: normalizedScope, error });
@@ -3120,6 +3336,7 @@ var require_dignity_p2p = __commonJS({
3120
3336
  }, heartbeatIntervalMs);
3121
3337
  this.discoveryRooms.set(normalizedScope, {
3122
3338
  metadata,
3339
+ bootstrapPeerIds,
3123
3340
  heartbeatIntervalMs,
3124
3341
  ttlMs,
3125
3342
  timer
@@ -3200,6 +3417,9 @@ var require_dignity_p2p = __commonJS({
3200
3417
  });
3201
3418
  return;
3202
3419
  }
3420
+ if (message && message.senderId && message.senderPublicKey) {
3421
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3422
+ }
3203
3423
  let decrypted;
3204
3424
  try {
3205
3425
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3221,6 +3441,21 @@ var require_dignity_p2p = __commonJS({
3221
3441
  this.applyOperation(decrypted.payload);
3222
3442
  return;
3223
3443
  }
3444
+ if (decrypted.messageType === "record:snapshot") {
3445
+ const payload = decrypted.payload || {};
3446
+ const { collectionName, record } = payload;
3447
+ if (collectionName && record) {
3448
+ const applied = this.restoreRecord(collectionName, record);
3449
+ if (applied) {
3450
+ this.emit("change", {
3451
+ kind: "snapshot",
3452
+ collection: collectionName,
3453
+ id: record.id
3454
+ });
3455
+ }
3456
+ }
3457
+ return;
3458
+ }
3224
3459
  if (decrypted.messageType === "presence:announce") {
3225
3460
  const payload = decrypted.payload || {};
3226
3461
  const scope = payload.scope || "main";
@@ -3238,6 +3473,11 @@ var require_dignity_p2p = __commonJS({
3238
3473
  payload.announcedAt || this.now()
3239
3474
  );
3240
3475
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3476
+ if (typeof this.networkAdapter.connectToPeer === "function") {
3477
+ Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
3478
+ this.emit("warning", { type: "peer-connect-failed", scope, peerId, error });
3479
+ });
3480
+ }
3241
3481
  this.announcePresence(scope).catch((error) => {
3242
3482
  this.emit("warning", { type: "presence-handshake-failed", scope, error });
3243
3483
  });
@@ -3297,6 +3537,56 @@ var require_dignity_p2p = __commonJS({
3297
3537
  isPeerBanned(peerId) {
3298
3538
  return this.getBanInfo(peerId) !== null;
3299
3539
  }
3540
+ emitConflict(details) {
3541
+ this.emit("conflict", details);
3542
+ }
3543
+ restoreRecord(collectionName, record) {
3544
+ if (!record || !record.id) {
3545
+ return false;
3546
+ }
3547
+ const collection = this.getCollection(collectionName);
3548
+ const current = collection.get(record.id);
3549
+ if (current && current.version >= record.version) {
3550
+ return false;
3551
+ }
3552
+ collection.set(record.id, {
3553
+ id: record.id,
3554
+ ownerId: record.ownerId,
3555
+ collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3556
+ data: { ...record.data || {} },
3557
+ createdAt: record.createdAt,
3558
+ updatedAt: record.updatedAt,
3559
+ deletedAt: record.deletedAt || null,
3560
+ version: record.version
3561
+ });
3562
+ return true;
3563
+ }
3564
+ async pushRecordSnapshot(collectionName, id, options = {}) {
3565
+ const collection = this.getCollection(collectionName);
3566
+ const raw = collection.get(id);
3567
+ if (!raw || raw.deletedAt) {
3568
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3569
+ }
3570
+ const record = {
3571
+ id: raw.id,
3572
+ ownerId: raw.ownerId,
3573
+ collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3574
+ data: { ...raw.data },
3575
+ createdAt: raw.createdAt,
3576
+ updatedAt: raw.updatedAt,
3577
+ deletedAt: raw.deletedAt || null,
3578
+ version: raw.version
3579
+ };
3580
+ await this.broadcastMessage("record:snapshot", { collectionName, record }, {
3581
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3582
+ messageType: "record:snapshot",
3583
+ collectionName,
3584
+ id
3585
+ }),
3586
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: raw })
3587
+ });
3588
+ return record;
3589
+ }
3300
3590
  applyOperation(operation) {
3301
3591
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3302
3592
  return false;
@@ -3310,6 +3600,7 @@ var require_dignity_p2p = __commonJS({
3310
3600
  collection.set(operation.id, {
3311
3601
  id: operation.id,
3312
3602
  ownerId: operation.ownerId,
3603
+ collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3313
3604
  data: { ...operation.payload },
3314
3605
  createdAt: operation.timestamp,
3315
3606
  updatedAt: operation.timestamp,
@@ -3321,12 +3612,91 @@ var require_dignity_p2p = __commonJS({
3321
3612
  return true;
3322
3613
  }
3323
3614
  if (!current || current.deletedAt) {
3615
+ if (operation.kind !== "create") {
3616
+ this.emit("warning", {
3617
+ type: "orphan-operation",
3618
+ kind: operation.kind,
3619
+ collection: operation.collectionName,
3620
+ id: operation.id,
3621
+ actorId: operation.actorId,
3622
+ hint: "Peer is missing the record; pushRecordSnapshot from the owner to catch up."
3623
+ });
3624
+ }
3324
3625
  return false;
3325
3626
  }
3326
- if (operation.actorId !== current.ownerId) {
3627
+ if (operation.kind === "transfer-ownership") {
3628
+ if (operation.actorId !== current.ownerId) {
3629
+ return false;
3630
+ }
3631
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3632
+ this.emitConflict({
3633
+ kind: operation.kind,
3634
+ collection: operation.collectionName,
3635
+ id: operation.id,
3636
+ expectedVersion: operation.baseVersion,
3637
+ currentVersion: current.version,
3638
+ phase: "remote",
3639
+ operation
3640
+ });
3641
+ return false;
3642
+ }
3643
+ const previousOwnerId = current.ownerId;
3644
+ current.ownerId = operation.newOwnerId;
3645
+ if (operation.keepPreviousOwnerAsCollaborator !== false) {
3646
+ const collaborators = this.normalizeCollaboratorIds(current.collaboratorIds);
3647
+ if (!collaborators.includes(previousOwnerId)) {
3648
+ collaborators.push(previousOwnerId);
3649
+ }
3650
+ current.collaboratorIds = collaborators.filter((peerId) => peerId !== operation.newOwnerId);
3651
+ }
3652
+ current.updatedAt = operation.timestamp;
3653
+ current.version += 1;
3654
+ this.appliedOperations.add(operation.opId);
3655
+ this.emit("change", {
3656
+ kind: "transfer-ownership",
3657
+ collection: operation.collectionName,
3658
+ id: operation.id,
3659
+ previousOwnerId,
3660
+ newOwnerId: operation.newOwnerId
3661
+ });
3662
+ return true;
3663
+ }
3664
+ if (operation.kind === "delete") {
3665
+ if (operation.actorId !== current.ownerId) {
3666
+ return false;
3667
+ }
3668
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3669
+ this.emitConflict({
3670
+ kind: operation.kind,
3671
+ collection: operation.collectionName,
3672
+ id: operation.id,
3673
+ expectedVersion: operation.baseVersion,
3674
+ currentVersion: current.version,
3675
+ phase: "remote",
3676
+ operation
3677
+ });
3678
+ return false;
3679
+ }
3680
+ current.deletedAt = operation.timestamp;
3681
+ current.updatedAt = operation.timestamp;
3682
+ current.version += 1;
3683
+ this.appliedOperations.add(operation.opId);
3684
+ this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3685
+ return true;
3686
+ }
3687
+ if (!this.canUpdateRecord(current, operation.actorId)) {
3327
3688
  return false;
3328
3689
  }
3329
3690
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3691
+ this.emitConflict({
3692
+ kind: operation.kind,
3693
+ collection: operation.collectionName,
3694
+ id: operation.id,
3695
+ expectedVersion: operation.baseVersion,
3696
+ currentVersion: current.version,
3697
+ phase: "remote",
3698
+ operation
3699
+ });
3330
3700
  return false;
3331
3701
  }
3332
3702
  if (operation.kind === "update") {
@@ -3334,20 +3704,15 @@ var require_dignity_p2p = __commonJS({
3334
3704
  ...current.data,
3335
3705
  ...operation.payload
3336
3706
  };
3707
+ if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3708
+ current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3709
+ }
3337
3710
  current.updatedAt = operation.timestamp;
3338
3711
  current.version += 1;
3339
3712
  this.appliedOperations.add(operation.opId);
3340
3713
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3341
3714
  return true;
3342
3715
  }
3343
- if (operation.kind === "delete") {
3344
- current.deletedAt = operation.timestamp;
3345
- current.updatedAt = operation.timestamp;
3346
- current.version += 1;
3347
- this.appliedOperations.add(operation.opId);
3348
- this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3349
- return true;
3350
- }
3351
3716
  return false;
3352
3717
  }
3353
3718
  };
@@ -3502,6 +3867,28 @@ var require_websocket_signaling_provider = __commonJS({
3502
3867
  }
3503
3868
  });
3504
3869
 
3870
+ // src/signaling/parse-peerjs-url.js
3871
+ var require_parse_peerjs_url = __commonJS({
3872
+ "src/signaling/parse-peerjs-url.js"(exports, module) {
3873
+ function parsePeerJsServerUrl(url) {
3874
+ const parsed = new URL(url);
3875
+ const secure = parsed.protocol === "wss:";
3876
+ const host = parsed.hostname;
3877
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
3878
+ const key = parsed.searchParams.get("key") || "peerjs";
3879
+ let path = parsed.pathname || "/";
3880
+ if (path.endsWith("/peerjs")) {
3881
+ path = path.slice(0, -"/peerjs".length) || "/";
3882
+ }
3883
+ if (path !== "/" && !path.endsWith("/")) {
3884
+ path += "/";
3885
+ }
3886
+ return { secure, host, port, path, key };
3887
+ }
3888
+ module.exports = parsePeerJsServerUrl;
3889
+ }
3890
+ });
3891
+
3505
3892
  // node_modules/peerjs-js-binarypack/dist/binarypack.cjs
3506
3893
  var require_binarypack = __commonJS({
3507
3894
  "node_modules/peerjs-js-binarypack/dist/binarypack.cjs"(exports, module) {
@@ -10101,6 +10488,7 @@ var require_bundler = __commonJS({
10101
10488
  var require_peerjs_signaling_provider = __commonJS({
10102
10489
  "src/signaling/peerjs-signaling-provider.js"(exports, module) {
10103
10490
  var WebSocketSignalingProvider = require_websocket_signaling_provider();
10491
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10104
10492
  var PeerJSSignalingProvider = class {
10105
10493
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 1e4 }) {
10106
10494
  if (!url) {
@@ -10128,13 +10516,7 @@ var require_peerjs_signaling_provider = __commonJS({
10128
10516
  }
10129
10517
  }
10130
10518
  parsePeerJsServerUrl() {
10131
- const parsed = new URL(this.url);
10132
- const secure = parsed.protocol === "wss:";
10133
- const host = parsed.hostname;
10134
- const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
10135
- const path = parsed.pathname || "/";
10136
- const key = parsed.searchParams.get("key") || "peerjs";
10137
- return { secure, host, port, path, key };
10519
+ return parsePeerJsServerUrl(this.url);
10138
10520
  }
10139
10521
  shouldUseWebSocketFallback() {
10140
10522
  return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== "function";
@@ -10414,6 +10796,364 @@ var require_in_memory_network = __commonJS({
10414
10796
  }
10415
10797
  });
10416
10798
 
10799
+ // src/network/peerjs-network.js
10800
+ var require_peerjs_network = __commonJS({
10801
+ "src/network/peerjs-network.js"(exports, module) {
10802
+ var { DEFAULT_CLOUDFLARE_SIGNALING_URLS } = require_default_signaling_config();
10803
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10804
+ function resolvePeerImplementation(PeerImpl) {
10805
+ if (PeerImpl) {
10806
+ return PeerImpl;
10807
+ }
10808
+ try {
10809
+ const peerjs = require_bundler();
10810
+ return peerjs.Peer || peerjs;
10811
+ } catch (error) {
10812
+ return null;
10813
+ }
10814
+ }
10815
+ var PeerJSNetworkAdapter = class {
10816
+ constructor({
10817
+ url,
10818
+ urls,
10819
+ PeerImpl,
10820
+ connectTimeoutMs = 12e3
10821
+ } = {}) {
10822
+ this.urls = urls || (url ? [url] : [...DEFAULT_CLOUDFLARE_SIGNALING_URLS]);
10823
+ this.url = this.urls[0];
10824
+ this.PeerImpl = resolvePeerImplementation(PeerImpl);
10825
+ this.connectTimeoutMs = connectTimeoutMs;
10826
+ this.nodeId = null;
10827
+ this.peer = null;
10828
+ this.connections = /* @__PURE__ */ new Map();
10829
+ this.pendingConnections = /* @__PURE__ */ new Map();
10830
+ this.messageHandlers = /* @__PURE__ */ new Set();
10831
+ }
10832
+ async start(nodeId) {
10833
+ if (!nodeId) {
10834
+ throw new Error("PeerJSNetworkAdapter requires nodeId on start");
10835
+ }
10836
+ if (!this.PeerImpl) {
10837
+ throw new Error("PeerJS implementation is not available");
10838
+ }
10839
+ if (this.peer) {
10840
+ await this.stop();
10841
+ }
10842
+ let lastError;
10843
+ for (const candidateUrl of this.urls) {
10844
+ try {
10845
+ await this.startWithUrl(nodeId, candidateUrl);
10846
+ this.url = candidateUrl;
10847
+ return;
10848
+ } catch (error) {
10849
+ lastError = error;
10850
+ }
10851
+ }
10852
+ throw lastError || new Error("Unable to connect PeerJS network adapter");
10853
+ }
10854
+ async startWithUrl(nodeId, url) {
10855
+ this.nodeId = nodeId;
10856
+ const server = parsePeerJsServerUrl(url);
10857
+ await new Promise((resolve, reject) => {
10858
+ const peer = new this.PeerImpl(nodeId, {
10859
+ host: server.host,
10860
+ port: server.port,
10861
+ path: server.path,
10862
+ secure: server.secure,
10863
+ key: server.key
10864
+ });
10865
+ const timeout = setTimeout(() => {
10866
+ peer.destroy?.();
10867
+ reject(new Error(`Unable to connect PeerJS network adapter to ${url}`));
10868
+ }, this.connectTimeoutMs);
10869
+ peer.on("open", () => {
10870
+ clearTimeout(timeout);
10871
+ this.peer = peer;
10872
+ resolve();
10873
+ });
10874
+ peer.on("connection", (connection) => {
10875
+ this.attachConnectionHandlers(connection);
10876
+ });
10877
+ peer.on("error", (error) => {
10878
+ clearTimeout(timeout);
10879
+ peer.destroy?.();
10880
+ reject(error || new Error(`Unable to connect PeerJS network adapter to ${url}`));
10881
+ });
10882
+ });
10883
+ }
10884
+ attachConnectionHandlers(connection) {
10885
+ const remoteId = connection.peer;
10886
+ if (!remoteId) {
10887
+ return;
10888
+ }
10889
+ this.connections.set(remoteId, connection);
10890
+ connection.on("data", (payload) => {
10891
+ const deliveries = [];
10892
+ for (const handler of this.messageHandlers) {
10893
+ deliveries.push(handler(payload));
10894
+ }
10895
+ return Promise.all(deliveries);
10896
+ });
10897
+ connection.on("close", () => {
10898
+ this.connections.delete(remoteId);
10899
+ });
10900
+ }
10901
+ async connectToPeer(remotePeerId) {
10902
+ if (!remotePeerId || remotePeerId === this.nodeId) {
10903
+ return null;
10904
+ }
10905
+ const existing = this.connections.get(remotePeerId);
10906
+ if (existing && existing.open) {
10907
+ return existing;
10908
+ }
10909
+ if (this.pendingConnections.has(remotePeerId)) {
10910
+ return this.pendingConnections.get(remotePeerId);
10911
+ }
10912
+ if (!this.peer) {
10913
+ throw new Error("PeerJS network adapter has not been started");
10914
+ }
10915
+ const pending = new Promise((resolve, reject) => {
10916
+ const connection = this.peer.connect(remotePeerId, {
10917
+ reliable: true,
10918
+ serialization: "json"
10919
+ });
10920
+ const timeout = setTimeout(() => {
10921
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10922
+ }, this.connectTimeoutMs);
10923
+ connection.on("open", () => {
10924
+ clearTimeout(timeout);
10925
+ this.attachConnectionHandlers(connection);
10926
+ resolve(connection);
10927
+ });
10928
+ connection.on("error", () => {
10929
+ clearTimeout(timeout);
10930
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10931
+ });
10932
+ }).finally(() => {
10933
+ this.pendingConnections.delete(remotePeerId);
10934
+ });
10935
+ this.pendingConnections.set(remotePeerId, pending);
10936
+ return pending;
10937
+ }
10938
+ onMessage(handler) {
10939
+ this.messageHandlers.add(handler);
10940
+ }
10941
+ offMessage(handler) {
10942
+ this.messageHandlers.delete(handler);
10943
+ }
10944
+ async broadcast(message) {
10945
+ if (!this.peer) {
10946
+ throw new Error("PeerJS network adapter has not been started");
10947
+ }
10948
+ const deliveries = [];
10949
+ for (const connection of this.connections.values()) {
10950
+ if (connection.open) {
10951
+ deliveries.push(connection.send(message));
10952
+ }
10953
+ }
10954
+ await Promise.all(deliveries);
10955
+ }
10956
+ getOpenConnectionCount() {
10957
+ return this.listOpenPeerIds().length;
10958
+ }
10959
+ listOpenPeerIds() {
10960
+ const ids = [];
10961
+ for (const [peerId, connection] of this.connections.entries()) {
10962
+ if (connection.open) {
10963
+ ids.push(peerId);
10964
+ }
10965
+ }
10966
+ return ids;
10967
+ }
10968
+ isConnectedTo(remotePeerId) {
10969
+ const connection = this.connections.get(remotePeerId);
10970
+ return Boolean(connection && connection.open);
10971
+ }
10972
+ async stop() {
10973
+ for (const connection of this.connections.values()) {
10974
+ if (typeof connection.close === "function") {
10975
+ connection.close();
10976
+ }
10977
+ }
10978
+ this.connections.clear();
10979
+ this.pendingConnections.clear();
10980
+ if (this.peer && typeof this.peer.destroy === "function") {
10981
+ this.peer.destroy();
10982
+ }
10983
+ this.peer = null;
10984
+ this.nodeId = null;
10985
+ }
10986
+ };
10987
+ function createPeerJSNetworkAdapter(options = {}) {
10988
+ return new PeerJSNetworkAdapter(options);
10989
+ }
10990
+ module.exports = {
10991
+ PeerJSNetworkAdapter,
10992
+ createPeerJSNetworkAdapter,
10993
+ parsePeerJsServerUrl
10994
+ };
10995
+ }
10996
+ });
10997
+
10998
+ // src/persistence/indexeddb-persistence.js
10999
+ var require_indexeddb_persistence = __commonJS({
11000
+ "src/persistence/indexeddb-persistence.js"(exports, module) {
11001
+ var IndexedDBPersistence = class {
11002
+ constructor({
11003
+ dbName = "dignity",
11004
+ storeName = "records",
11005
+ collections = null,
11006
+ indexedDB = typeof globalThis !== "undefined" ? globalThis.indexedDB : null
11007
+ } = {}) {
11008
+ this.dbName = dbName;
11009
+ this.storeName = storeName;
11010
+ this.collections = collections;
11011
+ this.indexedDB = indexedDB;
11012
+ this.node = null;
11013
+ this.changeHandler = null;
11014
+ }
11015
+ recordKey(collection, id) {
11016
+ return `${collection}:${id}`;
11017
+ }
11018
+ shouldPersist(collection) {
11019
+ if (!this.collections) {
11020
+ return true;
11021
+ }
11022
+ return this.collections.includes(collection);
11023
+ }
11024
+ openDb() {
11025
+ if (!this.indexedDB) {
11026
+ return Promise.reject(new Error("IndexedDB is not available"));
11027
+ }
11028
+ return new Promise((resolve, reject) => {
11029
+ const request = this.indexedDB.open(this.dbName, 1);
11030
+ request.onupgradeneeded = () => {
11031
+ const db = request.result;
11032
+ if (!db.objectStoreNames.contains(this.storeName)) {
11033
+ db.createObjectStore(this.storeName, { keyPath: "key" });
11034
+ }
11035
+ };
11036
+ request.onsuccess = () => resolve(request.result);
11037
+ request.onerror = () => reject(request.error || new Error("Unable to open IndexedDB"));
11038
+ });
11039
+ }
11040
+ runTransaction(mode, handler) {
11041
+ return this.openDb().then((db) => new Promise((resolve, reject) => {
11042
+ const transaction = db.transaction(this.storeName, mode);
11043
+ const store = transaction.objectStore(this.storeName);
11044
+ Promise.resolve(handler(store)).then(resolve).catch(reject);
11045
+ transaction.oncomplete = () => db.close();
11046
+ transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
11047
+ transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
11048
+ }));
11049
+ }
11050
+ serializeRecord(collection, id) {
11051
+ const record = this.node.getCollection(collection).get(id);
11052
+ if (!record) {
11053
+ return null;
11054
+ }
11055
+ return {
11056
+ key: this.recordKey(collection, id),
11057
+ collection,
11058
+ id,
11059
+ ownerId: record.ownerId,
11060
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11061
+ data: { ...record.data },
11062
+ createdAt: record.createdAt,
11063
+ updatedAt: record.updatedAt,
11064
+ deletedAt: record.deletedAt,
11065
+ version: record.version
11066
+ };
11067
+ }
11068
+ async persistRecord(collection, id) {
11069
+ if (!this.node || !this.shouldPersist(collection)) {
11070
+ return;
11071
+ }
11072
+ const serialized = this.serializeRecord(collection, id);
11073
+ const key = this.recordKey(collection, id);
11074
+ if (!serialized) {
11075
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11076
+ const request = store.delete(key);
11077
+ request.onsuccess = () => resolve();
11078
+ request.onerror = () => reject(request.error);
11079
+ }));
11080
+ return;
11081
+ }
11082
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11083
+ const request = store.put(serialized);
11084
+ request.onsuccess = () => resolve();
11085
+ request.onerror = () => reject(request.error);
11086
+ }));
11087
+ }
11088
+ persistChange(event) {
11089
+ if (!event || !event.collection || !event.id) {
11090
+ return;
11091
+ }
11092
+ this.persistRecord(event.collection, event.id).catch((error) => {
11093
+ this.node.emit("warning", {
11094
+ type: "persistence-failed",
11095
+ collection: event.collection,
11096
+ id: event.id,
11097
+ error
11098
+ });
11099
+ });
11100
+ }
11101
+ async loadAllRecords() {
11102
+ return this.runTransaction("readonly", (store) => new Promise((resolve, reject) => {
11103
+ const request = store.getAll();
11104
+ request.onsuccess = () => resolve(request.result || []);
11105
+ request.onerror = () => reject(request.error);
11106
+ }));
11107
+ }
11108
+ async hydrate() {
11109
+ if (!this.node) {
11110
+ throw new Error("IndexedDBPersistence requires an attached node before hydrate");
11111
+ }
11112
+ const storedRecords = await this.loadAllRecords();
11113
+ for (const stored of storedRecords) {
11114
+ if (!this.shouldPersist(stored.collection)) {
11115
+ continue;
11116
+ }
11117
+ this.node.restoreRecord(stored.collection, {
11118
+ id: stored.id,
11119
+ ownerId: stored.ownerId,
11120
+ collaboratorIds: stored.collaboratorIds,
11121
+ data: stored.data,
11122
+ createdAt: stored.createdAt,
11123
+ updatedAt: stored.updatedAt,
11124
+ deletedAt: stored.deletedAt,
11125
+ version: stored.version
11126
+ });
11127
+ }
11128
+ }
11129
+ async attach(node) {
11130
+ if (!node) {
11131
+ throw new Error("IndexedDBPersistence.attach requires a DignityP2P node");
11132
+ }
11133
+ this.node = node;
11134
+ await this.hydrate();
11135
+ this.changeHandler = (event) => this.persistChange(event);
11136
+ node.on("change", this.changeHandler);
11137
+ }
11138
+ async detach() {
11139
+ if (this.node && this.changeHandler) {
11140
+ this.node.off("change", this.changeHandler);
11141
+ }
11142
+ this.changeHandler = null;
11143
+ this.node = null;
11144
+ }
11145
+ async clear() {
11146
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11147
+ const request = store.clear();
11148
+ request.onsuccess = () => resolve();
11149
+ request.onerror = () => reject(request.error);
11150
+ }));
11151
+ }
11152
+ };
11153
+ module.exports = IndexedDBPersistence;
11154
+ }
11155
+ });
11156
+
10417
11157
  // src/index.js
10418
11158
  var require_index = __commonJS({
10419
11159
  "src/index.js"(exports, module) {
@@ -10426,6 +11166,11 @@ var require_index = __commonJS({
10426
11166
  InMemoryNetworkHub,
10427
11167
  InMemoryNetworkAdapter
10428
11168
  } = require_in_memory_network();
11169
+ var {
11170
+ PeerJSNetworkAdapter,
11171
+ createPeerJSNetworkAdapter
11172
+ } = require_peerjs_network();
11173
+ var IndexedDBPersistence = require_indexeddb_persistence();
10429
11174
  var {
10430
11175
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10431
11176
  DEFAULT_SIGNALING_FALLBACK_URLS
@@ -10444,6 +11189,9 @@ var require_index = __commonJS({
10444
11189
  PeerJSSignalingProvider,
10445
11190
  InMemoryNetworkHub,
10446
11191
  InMemoryNetworkAdapter,
11192
+ PeerJSNetworkAdapter,
11193
+ createPeerJSNetworkAdapter,
11194
+ IndexedDBPersistence,
10447
11195
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10448
11196
  DEFAULT_SIGNALING_FALLBACK_URLS,
10449
11197
  VDF,