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
@@ -2914,12 +2914,71 @@ var require_dignity_p2p = __commonJS({
2914
2914
  return {
2915
2915
  id: record.id,
2916
2916
  ownerId: record.ownerId,
2917
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
2917
2918
  createdAt: record.createdAt,
2918
2919
  updatedAt: record.updatedAt,
2919
2920
  version: record.version,
2920
2921
  data: { ...record.data }
2921
2922
  };
2922
2923
  }
2924
+ canUpdateRecord(record, actorId) {
2925
+ if (!record || !actorId) {
2926
+ return false;
2927
+ }
2928
+ if (record.ownerId === actorId) {
2929
+ return true;
2930
+ }
2931
+ return Array.isArray(record.collaboratorIds) && record.collaboratorIds.includes(actorId);
2932
+ }
2933
+ normalizeCollaboratorIds(collaborators) {
2934
+ if (!Array.isArray(collaborators)) {
2935
+ return [];
2936
+ }
2937
+ return [...new Set(collaborators.filter(Boolean))];
2938
+ }
2939
+ getRecordPeerIds(collectionName, id, options = {}) {
2940
+ const record = options.fromRecord || this.getCollection(collectionName).get(id);
2941
+ if (!record) {
2942
+ return [];
2943
+ }
2944
+ const includeSelf = options.includeSelf === true;
2945
+ const peerIds = [record.ownerId, ...record.collaboratorIds || []];
2946
+ return [...new Set(peerIds.filter(Boolean).filter((peerId) => includeSelf || peerId !== this.nodeId))];
2947
+ }
2948
+ resolveReplicationPeers(collectionName, id, options = {}, hints = {}) {
2949
+ if (options.connectToPeers === false) {
2950
+ return void 0;
2951
+ }
2952
+ if (Array.isArray(options.connectToPeers)) {
2953
+ return options.connectToPeers;
2954
+ }
2955
+ const peerIds = /* @__PURE__ */ new Set();
2956
+ if (hints.fromRecord) {
2957
+ for (const peerId of this.getRecordPeerIds(collectionName, id, {
2958
+ fromRecord: hints.fromRecord,
2959
+ includeSelf: true
2960
+ })) {
2961
+ peerIds.add(peerId);
2962
+ }
2963
+ } else if (id) {
2964
+ for (const peerId of this.getRecordPeerIds(collectionName, id, { includeSelf: true })) {
2965
+ peerIds.add(peerId);
2966
+ }
2967
+ }
2968
+ if (Array.isArray(options.collaborators)) {
2969
+ for (const peerId of this.normalizeCollaboratorIds(options.collaborators)) {
2970
+ peerIds.add(peerId);
2971
+ }
2972
+ }
2973
+ if (Array.isArray(hints.extraPeerIds)) {
2974
+ for (const peerId of hints.extraPeerIds) {
2975
+ if (peerId) {
2976
+ peerIds.add(peerId);
2977
+ }
2978
+ }
2979
+ }
2980
+ return [...peerIds].filter((peerId) => peerId && peerId !== this.nodeId);
2981
+ }
2923
2982
  async create(collectionName, data, options = {}) {
2924
2983
  const collection = this.getCollection(collectionName);
2925
2984
  const id = options.id || this.idGenerator();
@@ -2927,6 +2986,7 @@ var require_dignity_p2p = __commonJS({
2927
2986
  throw new Error(`Object ${id} already exists in ${collectionName}`);
2928
2987
  }
2929
2988
  const timestamp = this.now();
2989
+ const collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
2930
2990
  const operation = {
2931
2991
  opId: this.idGenerator(),
2932
2992
  kind: "create",
@@ -2934,6 +2994,7 @@ var require_dignity_p2p = __commonJS({
2934
2994
  id,
2935
2995
  actorId: this.nodeId,
2936
2996
  ownerId: this.nodeId,
2997
+ collaboratorIds,
2937
2998
  timestamp,
2938
2999
  payload: { ...data }
2939
3000
  };
@@ -2943,6 +3004,9 @@ var require_dignity_p2p = __commonJS({
2943
3004
  messageType: "operation",
2944
3005
  operation,
2945
3006
  collectionName
3007
+ }),
3008
+ connectToPeers: this.resolveReplicationPeers(collectionName, null, options, {
3009
+ extraPeerIds: options.collaborators
2946
3010
  })
2947
3011
  });
2948
3012
  return this.read(collectionName, id);
@@ -2977,8 +3041,26 @@ var require_dignity_p2p = __commonJS({
2977
3041
  if (!existing || existing.deletedAt) {
2978
3042
  throw new Error(`Object ${id} does not exist in ${collectionName}`);
2979
3043
  }
2980
- if (existing.ownerId !== this.nodeId) {
2981
- throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
3044
+ if (!this.canUpdateRecord(existing, this.nodeId)) {
3045
+ throw new Error(`Only owner ${existing.ownerId} or collaborators can update object ${id}`);
3046
+ }
3047
+ if (options.collaborators !== void 0 && existing.ownerId !== this.nodeId) {
3048
+ throw new Error(`Only owner ${existing.ownerId} can change collaborators on object ${id}`);
3049
+ }
3050
+ if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
3051
+ this.emitConflict({
3052
+ kind: "update",
3053
+ collection: collectionName,
3054
+ id,
3055
+ expectedVersion: options.expectedVersion,
3056
+ currentVersion: existing.version,
3057
+ phase: "local"
3058
+ });
3059
+ const error = new Error(
3060
+ `Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
3061
+ );
3062
+ error.code = "VERSION_CONFLICT";
3063
+ throw error;
2982
3064
  }
2983
3065
  const operation = {
2984
3066
  opId: this.idGenerator(),
@@ -2990,12 +3072,73 @@ var require_dignity_p2p = __commonJS({
2990
3072
  baseVersion: existing.version,
2991
3073
  payload: { ...partialData }
2992
3074
  };
3075
+ if (options.collaborators !== void 0) {
3076
+ operation.collaboratorIds = this.normalizeCollaboratorIds(options.collaborators);
3077
+ }
3078
+ this.applyOperation(operation);
3079
+ await this.broadcastMessage("operation", operation, {
3080
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3081
+ messageType: "operation",
3082
+ operation,
3083
+ collectionName
3084
+ }),
3085
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3086
+ });
3087
+ return this.read(collectionName, id);
3088
+ }
3089
+ async updateWithRetry(collectionName, id, patchFn, options = {}) {
3090
+ const maxAttempts = typeof options.maxAttempts === "number" ? options.maxAttempts : 5;
3091
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3092
+ const current = this.read(collectionName, id);
3093
+ if (!current) {
3094
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3095
+ }
3096
+ const patch = await patchFn(current);
3097
+ try {
3098
+ return await this.update(collectionName, id, patch, {
3099
+ ...options,
3100
+ expectedVersion: current.version
3101
+ });
3102
+ } catch (error) {
3103
+ if (error.code !== "VERSION_CONFLICT" || attempt === maxAttempts - 1) {
3104
+ throw error;
3105
+ }
3106
+ }
3107
+ }
3108
+ throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3109
+ }
3110
+ async transferOwnership(collectionName, id, newOwnerId, options = {}) {
3111
+ if (!newOwnerId) {
3112
+ throw new Error("newOwnerId is required");
3113
+ }
3114
+ const existing = this.getCollection(collectionName).get(id);
3115
+ if (!existing || existing.deletedAt) {
3116
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3117
+ }
3118
+ if (existing.ownerId !== this.nodeId) {
3119
+ throw new Error(`Only owner ${existing.ownerId} can transfer object ${id}`);
3120
+ }
3121
+ const operation = {
3122
+ opId: this.idGenerator(),
3123
+ kind: "transfer-ownership",
3124
+ collectionName,
3125
+ id,
3126
+ actorId: this.nodeId,
3127
+ timestamp: this.now(),
3128
+ baseVersion: existing.version,
3129
+ newOwnerId,
3130
+ keepPreviousOwnerAsCollaborator: options.keepAsCollaborator !== false
3131
+ };
2993
3132
  this.applyOperation(operation);
2994
3133
  await this.broadcastMessage("operation", operation, {
2995
3134
  broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
2996
3135
  messageType: "operation",
2997
3136
  operation,
2998
3137
  collectionName
3138
+ }),
3139
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, {
3140
+ fromRecord: existing,
3141
+ extraPeerIds: [newOwnerId]
2999
3142
  })
3000
3143
  });
3001
3144
  return this.read(collectionName, id);
@@ -3023,16 +3166,74 @@ var require_dignity_p2p = __commonJS({
3023
3166
  messageType: "operation",
3024
3167
  operation,
3025
3168
  collectionName
3026
- })
3169
+ }),
3170
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3027
3171
  });
3028
3172
  }
3029
3173
  registerPeerPublicKey(peerId, publicKey) {
3030
3174
  this.securityService.registerPeerPublicKey(peerId, publicKey);
3031
3175
  }
3176
+ trustPeerPublicKey(peerId, publicKey) {
3177
+ if (!peerId || !publicKey) {
3178
+ return false;
3179
+ }
3180
+ try {
3181
+ this.registerPeerPublicKey(peerId, publicKey);
3182
+ return true;
3183
+ } catch (error) {
3184
+ this.emit("warning", { type: "peer-key-trust-failed", peerId, error });
3185
+ return false;
3186
+ }
3187
+ }
3188
+ trustPeerFromMetadata(peerId, metadata) {
3189
+ if (!metadata || !metadata.publicKey) {
3190
+ return false;
3191
+ }
3192
+ return this.trustPeerPublicKey(peerId, metadata.publicKey);
3193
+ }
3032
3194
  getPublicKey() {
3033
3195
  return this.securityService.getPublicKey();
3034
3196
  }
3197
+ async connectToPeer(peerId) {
3198
+ if (!peerId || peerId === this.nodeId) {
3199
+ return null;
3200
+ }
3201
+ if (typeof this.networkAdapter.connectToPeer !== "function") {
3202
+ throw new Error("Network adapter does not support connectToPeer");
3203
+ }
3204
+ return this.networkAdapter.connectToPeer(peerId);
3205
+ }
3206
+ getConnectionStats() {
3207
+ const adapter = this.networkAdapter;
3208
+ if (!adapter) {
3209
+ return { openCount: 0, peerIds: [] };
3210
+ }
3211
+ const peerIds = typeof adapter.listOpenPeerIds === "function" ? adapter.listOpenPeerIds() : [];
3212
+ const openCount = typeof adapter.getOpenConnectionCount === "function" ? adapter.getOpenConnectionCount() : peerIds.length;
3213
+ return { openCount, peerIds };
3214
+ }
3215
+ async ensureConnectedToPeers(peerIds = []) {
3216
+ const normalized = [...new Set((peerIds || []).filter(Boolean))];
3217
+ const results = [];
3218
+ for (const peerId of normalized) {
3219
+ if (peerId === this.nodeId) {
3220
+ continue;
3221
+ }
3222
+ try {
3223
+ await this.connectToPeer(peerId);
3224
+ results.push({ peerId, ok: true });
3225
+ } catch (error) {
3226
+ this.emit("warning", { type: "peer-connect-failed", peerId, error });
3227
+ results.push({ peerId, ok: false, error });
3228
+ }
3229
+ }
3230
+ return results;
3231
+ }
3035
3232
  async broadcastMessage(messageType, payload, securityContext = {}) {
3233
+ const connectToPeers = securityContext.connectToPeers;
3234
+ if (Array.isArray(connectToPeers) && connectToPeers.length > 0) {
3235
+ await this.ensureConnectedToPeers(connectToPeers);
3236
+ }
3036
3237
  const envelope = await this.securityService.secureOutgoingMessage({
3037
3238
  messageType,
3038
3239
  payload,
@@ -3042,6 +3243,13 @@ var require_dignity_p2p = __commonJS({
3042
3243
  await this.networkAdapter.broadcast(envelope);
3043
3244
  }
3044
3245
  async sendDirectMessage(targetId, messageType, payload) {
3246
+ if (targetId) {
3247
+ try {
3248
+ await this.connectToPeer(targetId);
3249
+ } catch (error) {
3250
+ this.emit("warning", { type: "direct-message-connect-failed", targetId, error });
3251
+ }
3252
+ }
3045
3253
  const envelope = await this.securityService.secureOutgoingMessage({
3046
3254
  messageType,
3047
3255
  payload,
@@ -3066,6 +3274,7 @@ var require_dignity_p2p = __commonJS({
3066
3274
  expiresAt: announcedAt + ttlMs
3067
3275
  };
3068
3276
  map.set(peerId, next);
3277
+ this.trustPeerFromMetadata(peerId, next.metadata);
3069
3278
  if (!existing) {
3070
3279
  this.emit("peerdiscovered", { scope, peerId, metadata: next.metadata });
3071
3280
  }
@@ -3088,11 +3297,18 @@ var require_dignity_p2p = __commonJS({
3088
3297
  const normalizedScope = scope || "main";
3089
3298
  const heartbeatIntervalMs = options.heartbeatIntervalMs || this.defaultDiscoveryHeartbeatMs;
3090
3299
  const ttlMs = options.ttlMs || this.defaultPresenceTtlMs;
3091
- const metadata = options.metadata || {};
3300
+ const metadata = {
3301
+ publicKey: this.getPublicKey(),
3302
+ ...options.metadata || {}
3303
+ };
3304
+ const bootstrapPeerIds = Array.isArray(options.bootstrapPeerIds) ? [...new Set(options.bootstrapPeerIds.filter(Boolean))] : [];
3092
3305
  const existing = this.discoveryRooms.get(normalizedScope);
3093
3306
  if (existing && existing.timer) {
3094
3307
  clearInterval(existing.timer);
3095
3308
  }
3309
+ if (bootstrapPeerIds.length > 0) {
3310
+ await this.ensureConnectedToPeers(bootstrapPeerIds);
3311
+ }
3096
3312
  const timer = setInterval(() => {
3097
3313
  this.announcePresence(normalizedScope).catch((error) => {
3098
3314
  this.emit("warning", { type: "presence-heartbeat-failed", scope: normalizedScope, error });
@@ -3100,6 +3316,7 @@ var require_dignity_p2p = __commonJS({
3100
3316
  }, heartbeatIntervalMs);
3101
3317
  this.discoveryRooms.set(normalizedScope, {
3102
3318
  metadata,
3319
+ bootstrapPeerIds,
3103
3320
  heartbeatIntervalMs,
3104
3321
  ttlMs,
3105
3322
  timer
@@ -3180,6 +3397,9 @@ var require_dignity_p2p = __commonJS({
3180
3397
  });
3181
3398
  return;
3182
3399
  }
3400
+ if (message && message.senderId && message.senderPublicKey) {
3401
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3402
+ }
3183
3403
  let decrypted;
3184
3404
  try {
3185
3405
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3201,6 +3421,21 @@ var require_dignity_p2p = __commonJS({
3201
3421
  this.applyOperation(decrypted.payload);
3202
3422
  return;
3203
3423
  }
3424
+ if (decrypted.messageType === "record:snapshot") {
3425
+ const payload = decrypted.payload || {};
3426
+ const { collectionName, record } = payload;
3427
+ if (collectionName && record) {
3428
+ const applied = this.restoreRecord(collectionName, record);
3429
+ if (applied) {
3430
+ this.emit("change", {
3431
+ kind: "snapshot",
3432
+ collection: collectionName,
3433
+ id: record.id
3434
+ });
3435
+ }
3436
+ }
3437
+ return;
3438
+ }
3204
3439
  if (decrypted.messageType === "presence:announce") {
3205
3440
  const payload = decrypted.payload || {};
3206
3441
  const scope = payload.scope || "main";
@@ -3218,6 +3453,11 @@ var require_dignity_p2p = __commonJS({
3218
3453
  payload.announcedAt || this.now()
3219
3454
  );
3220
3455
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3456
+ if (typeof this.networkAdapter.connectToPeer === "function") {
3457
+ Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
3458
+ this.emit("warning", { type: "peer-connect-failed", scope, peerId, error });
3459
+ });
3460
+ }
3221
3461
  this.announcePresence(scope).catch((error) => {
3222
3462
  this.emit("warning", { type: "presence-handshake-failed", scope, error });
3223
3463
  });
@@ -3277,6 +3517,56 @@ var require_dignity_p2p = __commonJS({
3277
3517
  isPeerBanned(peerId) {
3278
3518
  return this.getBanInfo(peerId) !== null;
3279
3519
  }
3520
+ emitConflict(details) {
3521
+ this.emit("conflict", details);
3522
+ }
3523
+ restoreRecord(collectionName, record) {
3524
+ if (!record || !record.id) {
3525
+ return false;
3526
+ }
3527
+ const collection = this.getCollection(collectionName);
3528
+ const current = collection.get(record.id);
3529
+ if (current && current.version >= record.version) {
3530
+ return false;
3531
+ }
3532
+ collection.set(record.id, {
3533
+ id: record.id,
3534
+ ownerId: record.ownerId,
3535
+ collaboratorIds: this.normalizeCollaboratorIds(record.collaboratorIds),
3536
+ data: { ...record.data || {} },
3537
+ createdAt: record.createdAt,
3538
+ updatedAt: record.updatedAt,
3539
+ deletedAt: record.deletedAt || null,
3540
+ version: record.version
3541
+ });
3542
+ return true;
3543
+ }
3544
+ async pushRecordSnapshot(collectionName, id, options = {}) {
3545
+ const collection = this.getCollection(collectionName);
3546
+ const raw = collection.get(id);
3547
+ if (!raw || raw.deletedAt) {
3548
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3549
+ }
3550
+ const record = {
3551
+ id: raw.id,
3552
+ ownerId: raw.ownerId,
3553
+ collaboratorIds: Array.isArray(raw.collaboratorIds) ? [...raw.collaboratorIds] : [],
3554
+ data: { ...raw.data },
3555
+ createdAt: raw.createdAt,
3556
+ updatedAt: raw.updatedAt,
3557
+ deletedAt: raw.deletedAt || null,
3558
+ version: raw.version
3559
+ };
3560
+ await this.broadcastMessage("record:snapshot", { collectionName, record }, {
3561
+ broadcastScope: options.broadcastScope || this.resolveBroadcastScope({
3562
+ messageType: "record:snapshot",
3563
+ collectionName,
3564
+ id
3565
+ }),
3566
+ connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: raw })
3567
+ });
3568
+ return record;
3569
+ }
3280
3570
  applyOperation(operation) {
3281
3571
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3282
3572
  return false;
@@ -3290,6 +3580,7 @@ var require_dignity_p2p = __commonJS({
3290
3580
  collection.set(operation.id, {
3291
3581
  id: operation.id,
3292
3582
  ownerId: operation.ownerId,
3583
+ collaboratorIds: this.normalizeCollaboratorIds(operation.collaboratorIds),
3293
3584
  data: { ...operation.payload },
3294
3585
  createdAt: operation.timestamp,
3295
3586
  updatedAt: operation.timestamp,
@@ -3301,12 +3592,91 @@ var require_dignity_p2p = __commonJS({
3301
3592
  return true;
3302
3593
  }
3303
3594
  if (!current || current.deletedAt) {
3595
+ if (operation.kind !== "create") {
3596
+ this.emit("warning", {
3597
+ type: "orphan-operation",
3598
+ kind: operation.kind,
3599
+ collection: operation.collectionName,
3600
+ id: operation.id,
3601
+ actorId: operation.actorId,
3602
+ hint: "Peer is missing the record; pushRecordSnapshot from the owner to catch up."
3603
+ });
3604
+ }
3304
3605
  return false;
3305
3606
  }
3306
- if (operation.actorId !== current.ownerId) {
3607
+ if (operation.kind === "transfer-ownership") {
3608
+ if (operation.actorId !== current.ownerId) {
3609
+ return false;
3610
+ }
3611
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3612
+ this.emitConflict({
3613
+ kind: operation.kind,
3614
+ collection: operation.collectionName,
3615
+ id: operation.id,
3616
+ expectedVersion: operation.baseVersion,
3617
+ currentVersion: current.version,
3618
+ phase: "remote",
3619
+ operation
3620
+ });
3621
+ return false;
3622
+ }
3623
+ const previousOwnerId = current.ownerId;
3624
+ current.ownerId = operation.newOwnerId;
3625
+ if (operation.keepPreviousOwnerAsCollaborator !== false) {
3626
+ const collaborators = this.normalizeCollaboratorIds(current.collaboratorIds);
3627
+ if (!collaborators.includes(previousOwnerId)) {
3628
+ collaborators.push(previousOwnerId);
3629
+ }
3630
+ current.collaboratorIds = collaborators.filter((peerId) => peerId !== operation.newOwnerId);
3631
+ }
3632
+ current.updatedAt = operation.timestamp;
3633
+ current.version += 1;
3634
+ this.appliedOperations.add(operation.opId);
3635
+ this.emit("change", {
3636
+ kind: "transfer-ownership",
3637
+ collection: operation.collectionName,
3638
+ id: operation.id,
3639
+ previousOwnerId,
3640
+ newOwnerId: operation.newOwnerId
3641
+ });
3642
+ return true;
3643
+ }
3644
+ if (operation.kind === "delete") {
3645
+ if (operation.actorId !== current.ownerId) {
3646
+ return false;
3647
+ }
3648
+ if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3649
+ this.emitConflict({
3650
+ kind: operation.kind,
3651
+ collection: operation.collectionName,
3652
+ id: operation.id,
3653
+ expectedVersion: operation.baseVersion,
3654
+ currentVersion: current.version,
3655
+ phase: "remote",
3656
+ operation
3657
+ });
3658
+ return false;
3659
+ }
3660
+ current.deletedAt = operation.timestamp;
3661
+ current.updatedAt = operation.timestamp;
3662
+ current.version += 1;
3663
+ this.appliedOperations.add(operation.opId);
3664
+ this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3665
+ return true;
3666
+ }
3667
+ if (!this.canUpdateRecord(current, operation.actorId)) {
3307
3668
  return false;
3308
3669
  }
3309
3670
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3671
+ this.emitConflict({
3672
+ kind: operation.kind,
3673
+ collection: operation.collectionName,
3674
+ id: operation.id,
3675
+ expectedVersion: operation.baseVersion,
3676
+ currentVersion: current.version,
3677
+ phase: "remote",
3678
+ operation
3679
+ });
3310
3680
  return false;
3311
3681
  }
3312
3682
  if (operation.kind === "update") {
@@ -3314,20 +3684,15 @@ var require_dignity_p2p = __commonJS({
3314
3684
  ...current.data,
3315
3685
  ...operation.payload
3316
3686
  };
3687
+ if (Array.isArray(operation.collaboratorIds) && operation.actorId === current.ownerId) {
3688
+ current.collaboratorIds = this.normalizeCollaboratorIds(operation.collaboratorIds);
3689
+ }
3317
3690
  current.updatedAt = operation.timestamp;
3318
3691
  current.version += 1;
3319
3692
  this.appliedOperations.add(operation.opId);
3320
3693
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
3321
3694
  return true;
3322
3695
  }
3323
- if (operation.kind === "delete") {
3324
- current.deletedAt = operation.timestamp;
3325
- current.updatedAt = operation.timestamp;
3326
- current.version += 1;
3327
- this.appliedOperations.add(operation.opId);
3328
- this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
3329
- return true;
3330
- }
3331
3696
  return false;
3332
3697
  }
3333
3698
  };
@@ -3482,6 +3847,28 @@ var require_websocket_signaling_provider = __commonJS({
3482
3847
  }
3483
3848
  });
3484
3849
 
3850
+ // src/signaling/parse-peerjs-url.js
3851
+ var require_parse_peerjs_url = __commonJS({
3852
+ "src/signaling/parse-peerjs-url.js"(exports2, module2) {
3853
+ function parsePeerJsServerUrl(url) {
3854
+ const parsed = new URL(url);
3855
+ const secure = parsed.protocol === "wss:";
3856
+ const host = parsed.hostname;
3857
+ const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
3858
+ const key = parsed.searchParams.get("key") || "peerjs";
3859
+ let path = parsed.pathname || "/";
3860
+ if (path.endsWith("/peerjs")) {
3861
+ path = path.slice(0, -"/peerjs".length) || "/";
3862
+ }
3863
+ if (path !== "/" && !path.endsWith("/")) {
3864
+ path += "/";
3865
+ }
3866
+ return { secure, host, port, path, key };
3867
+ }
3868
+ module2.exports = parsePeerJsServerUrl;
3869
+ }
3870
+ });
3871
+
3485
3872
  // node_modules/peerjs-js-binarypack/dist/binarypack.cjs
3486
3873
  var require_binarypack = __commonJS({
3487
3874
  "node_modules/peerjs-js-binarypack/dist/binarypack.cjs"(exports2, module2) {
@@ -10071,6 +10458,7 @@ var require_bundler = __commonJS({
10071
10458
  var require_peerjs_signaling_provider = __commonJS({
10072
10459
  "src/signaling/peerjs-signaling-provider.js"(exports2, module2) {
10073
10460
  var WebSocketSignalingProvider2 = require_websocket_signaling_provider();
10461
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10074
10462
  var PeerJSSignalingProvider2 = class {
10075
10463
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 1e4 }) {
10076
10464
  if (!url) {
@@ -10098,13 +10486,7 @@ var require_peerjs_signaling_provider = __commonJS({
10098
10486
  }
10099
10487
  }
10100
10488
  parsePeerJsServerUrl() {
10101
- const parsed = new URL(this.url);
10102
- const secure = parsed.protocol === "wss:";
10103
- const host = parsed.hostname;
10104
- const port = parsed.port ? Number(parsed.port) : secure ? 443 : 80;
10105
- const path = parsed.pathname || "/";
10106
- const key = parsed.searchParams.get("key") || "peerjs";
10107
- return { secure, host, port, path, key };
10489
+ return parsePeerJsServerUrl(this.url);
10108
10490
  }
10109
10491
  shouldUseWebSocketFallback() {
10110
10492
  return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== "function";
@@ -10384,6 +10766,364 @@ var require_in_memory_network = __commonJS({
10384
10766
  }
10385
10767
  });
10386
10768
 
10769
+ // src/network/peerjs-network.js
10770
+ var require_peerjs_network = __commonJS({
10771
+ "src/network/peerjs-network.js"(exports2, module2) {
10772
+ var { DEFAULT_CLOUDFLARE_SIGNALING_URLS: DEFAULT_CLOUDFLARE_SIGNALING_URLS2 } = require_default_signaling_config();
10773
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
10774
+ function resolvePeerImplementation(PeerImpl) {
10775
+ if (PeerImpl) {
10776
+ return PeerImpl;
10777
+ }
10778
+ try {
10779
+ const peerjs = require_bundler();
10780
+ return peerjs.Peer || peerjs;
10781
+ } catch (error) {
10782
+ return null;
10783
+ }
10784
+ }
10785
+ var PeerJSNetworkAdapter2 = class {
10786
+ constructor({
10787
+ url,
10788
+ urls,
10789
+ PeerImpl,
10790
+ connectTimeoutMs = 12e3
10791
+ } = {}) {
10792
+ this.urls = urls || (url ? [url] : [...DEFAULT_CLOUDFLARE_SIGNALING_URLS2]);
10793
+ this.url = this.urls[0];
10794
+ this.PeerImpl = resolvePeerImplementation(PeerImpl);
10795
+ this.connectTimeoutMs = connectTimeoutMs;
10796
+ this.nodeId = null;
10797
+ this.peer = null;
10798
+ this.connections = /* @__PURE__ */ new Map();
10799
+ this.pendingConnections = /* @__PURE__ */ new Map();
10800
+ this.messageHandlers = /* @__PURE__ */ new Set();
10801
+ }
10802
+ async start(nodeId) {
10803
+ if (!nodeId) {
10804
+ throw new Error("PeerJSNetworkAdapter requires nodeId on start");
10805
+ }
10806
+ if (!this.PeerImpl) {
10807
+ throw new Error("PeerJS implementation is not available");
10808
+ }
10809
+ if (this.peer) {
10810
+ await this.stop();
10811
+ }
10812
+ let lastError;
10813
+ for (const candidateUrl of this.urls) {
10814
+ try {
10815
+ await this.startWithUrl(nodeId, candidateUrl);
10816
+ this.url = candidateUrl;
10817
+ return;
10818
+ } catch (error) {
10819
+ lastError = error;
10820
+ }
10821
+ }
10822
+ throw lastError || new Error("Unable to connect PeerJS network adapter");
10823
+ }
10824
+ async startWithUrl(nodeId, url) {
10825
+ this.nodeId = nodeId;
10826
+ const server = parsePeerJsServerUrl(url);
10827
+ await new Promise((resolve, reject) => {
10828
+ const peer = new this.PeerImpl(nodeId, {
10829
+ host: server.host,
10830
+ port: server.port,
10831
+ path: server.path,
10832
+ secure: server.secure,
10833
+ key: server.key
10834
+ });
10835
+ const timeout = setTimeout(() => {
10836
+ peer.destroy?.();
10837
+ reject(new Error(`Unable to connect PeerJS network adapter to ${url}`));
10838
+ }, this.connectTimeoutMs);
10839
+ peer.on("open", () => {
10840
+ clearTimeout(timeout);
10841
+ this.peer = peer;
10842
+ resolve();
10843
+ });
10844
+ peer.on("connection", (connection) => {
10845
+ this.attachConnectionHandlers(connection);
10846
+ });
10847
+ peer.on("error", (error) => {
10848
+ clearTimeout(timeout);
10849
+ peer.destroy?.();
10850
+ reject(error || new Error(`Unable to connect PeerJS network adapter to ${url}`));
10851
+ });
10852
+ });
10853
+ }
10854
+ attachConnectionHandlers(connection) {
10855
+ const remoteId = connection.peer;
10856
+ if (!remoteId) {
10857
+ return;
10858
+ }
10859
+ this.connections.set(remoteId, connection);
10860
+ connection.on("data", (payload) => {
10861
+ const deliveries = [];
10862
+ for (const handler of this.messageHandlers) {
10863
+ deliveries.push(handler(payload));
10864
+ }
10865
+ return Promise.all(deliveries);
10866
+ });
10867
+ connection.on("close", () => {
10868
+ this.connections.delete(remoteId);
10869
+ });
10870
+ }
10871
+ async connectToPeer(remotePeerId) {
10872
+ if (!remotePeerId || remotePeerId === this.nodeId) {
10873
+ return null;
10874
+ }
10875
+ const existing = this.connections.get(remotePeerId);
10876
+ if (existing && existing.open) {
10877
+ return existing;
10878
+ }
10879
+ if (this.pendingConnections.has(remotePeerId)) {
10880
+ return this.pendingConnections.get(remotePeerId);
10881
+ }
10882
+ if (!this.peer) {
10883
+ throw new Error("PeerJS network adapter has not been started");
10884
+ }
10885
+ const pending = new Promise((resolve, reject) => {
10886
+ const connection = this.peer.connect(remotePeerId, {
10887
+ reliable: true,
10888
+ serialization: "json"
10889
+ });
10890
+ const timeout = setTimeout(() => {
10891
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10892
+ }, this.connectTimeoutMs);
10893
+ connection.on("open", () => {
10894
+ clearTimeout(timeout);
10895
+ this.attachConnectionHandlers(connection);
10896
+ resolve(connection);
10897
+ });
10898
+ connection.on("error", () => {
10899
+ clearTimeout(timeout);
10900
+ reject(new Error(`Unable to connect to peer ${remotePeerId}`));
10901
+ });
10902
+ }).finally(() => {
10903
+ this.pendingConnections.delete(remotePeerId);
10904
+ });
10905
+ this.pendingConnections.set(remotePeerId, pending);
10906
+ return pending;
10907
+ }
10908
+ onMessage(handler) {
10909
+ this.messageHandlers.add(handler);
10910
+ }
10911
+ offMessage(handler) {
10912
+ this.messageHandlers.delete(handler);
10913
+ }
10914
+ async broadcast(message) {
10915
+ if (!this.peer) {
10916
+ throw new Error("PeerJS network adapter has not been started");
10917
+ }
10918
+ const deliveries = [];
10919
+ for (const connection of this.connections.values()) {
10920
+ if (connection.open) {
10921
+ deliveries.push(connection.send(message));
10922
+ }
10923
+ }
10924
+ await Promise.all(deliveries);
10925
+ }
10926
+ getOpenConnectionCount() {
10927
+ return this.listOpenPeerIds().length;
10928
+ }
10929
+ listOpenPeerIds() {
10930
+ const ids = [];
10931
+ for (const [peerId, connection] of this.connections.entries()) {
10932
+ if (connection.open) {
10933
+ ids.push(peerId);
10934
+ }
10935
+ }
10936
+ return ids;
10937
+ }
10938
+ isConnectedTo(remotePeerId) {
10939
+ const connection = this.connections.get(remotePeerId);
10940
+ return Boolean(connection && connection.open);
10941
+ }
10942
+ async stop() {
10943
+ for (const connection of this.connections.values()) {
10944
+ if (typeof connection.close === "function") {
10945
+ connection.close();
10946
+ }
10947
+ }
10948
+ this.connections.clear();
10949
+ this.pendingConnections.clear();
10950
+ if (this.peer && typeof this.peer.destroy === "function") {
10951
+ this.peer.destroy();
10952
+ }
10953
+ this.peer = null;
10954
+ this.nodeId = null;
10955
+ }
10956
+ };
10957
+ function createPeerJSNetworkAdapter2(options = {}) {
10958
+ return new PeerJSNetworkAdapter2(options);
10959
+ }
10960
+ module2.exports = {
10961
+ PeerJSNetworkAdapter: PeerJSNetworkAdapter2,
10962
+ createPeerJSNetworkAdapter: createPeerJSNetworkAdapter2,
10963
+ parsePeerJsServerUrl
10964
+ };
10965
+ }
10966
+ });
10967
+
10968
+ // src/persistence/indexeddb-persistence.js
10969
+ var require_indexeddb_persistence = __commonJS({
10970
+ "src/persistence/indexeddb-persistence.js"(exports2, module2) {
10971
+ var IndexedDBPersistence2 = class {
10972
+ constructor({
10973
+ dbName = "dignity",
10974
+ storeName = "records",
10975
+ collections = null,
10976
+ indexedDB = typeof globalThis !== "undefined" ? globalThis.indexedDB : null
10977
+ } = {}) {
10978
+ this.dbName = dbName;
10979
+ this.storeName = storeName;
10980
+ this.collections = collections;
10981
+ this.indexedDB = indexedDB;
10982
+ this.node = null;
10983
+ this.changeHandler = null;
10984
+ }
10985
+ recordKey(collection, id) {
10986
+ return `${collection}:${id}`;
10987
+ }
10988
+ shouldPersist(collection) {
10989
+ if (!this.collections) {
10990
+ return true;
10991
+ }
10992
+ return this.collections.includes(collection);
10993
+ }
10994
+ openDb() {
10995
+ if (!this.indexedDB) {
10996
+ return Promise.reject(new Error("IndexedDB is not available"));
10997
+ }
10998
+ return new Promise((resolve, reject) => {
10999
+ const request = this.indexedDB.open(this.dbName, 1);
11000
+ request.onupgradeneeded = () => {
11001
+ const db = request.result;
11002
+ if (!db.objectStoreNames.contains(this.storeName)) {
11003
+ db.createObjectStore(this.storeName, { keyPath: "key" });
11004
+ }
11005
+ };
11006
+ request.onsuccess = () => resolve(request.result);
11007
+ request.onerror = () => reject(request.error || new Error("Unable to open IndexedDB"));
11008
+ });
11009
+ }
11010
+ runTransaction(mode, handler) {
11011
+ return this.openDb().then((db) => new Promise((resolve, reject) => {
11012
+ const transaction = db.transaction(this.storeName, mode);
11013
+ const store = transaction.objectStore(this.storeName);
11014
+ Promise.resolve(handler(store)).then(resolve).catch(reject);
11015
+ transaction.oncomplete = () => db.close();
11016
+ transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
11017
+ transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
11018
+ }));
11019
+ }
11020
+ serializeRecord(collection, id) {
11021
+ const record = this.node.getCollection(collection).get(id);
11022
+ if (!record) {
11023
+ return null;
11024
+ }
11025
+ return {
11026
+ key: this.recordKey(collection, id),
11027
+ collection,
11028
+ id,
11029
+ ownerId: record.ownerId,
11030
+ collaboratorIds: Array.isArray(record.collaboratorIds) ? [...record.collaboratorIds] : [],
11031
+ data: { ...record.data },
11032
+ createdAt: record.createdAt,
11033
+ updatedAt: record.updatedAt,
11034
+ deletedAt: record.deletedAt,
11035
+ version: record.version
11036
+ };
11037
+ }
11038
+ async persistRecord(collection, id) {
11039
+ if (!this.node || !this.shouldPersist(collection)) {
11040
+ return;
11041
+ }
11042
+ const serialized = this.serializeRecord(collection, id);
11043
+ const key = this.recordKey(collection, id);
11044
+ if (!serialized) {
11045
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11046
+ const request = store.delete(key);
11047
+ request.onsuccess = () => resolve();
11048
+ request.onerror = () => reject(request.error);
11049
+ }));
11050
+ return;
11051
+ }
11052
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11053
+ const request = store.put(serialized);
11054
+ request.onsuccess = () => resolve();
11055
+ request.onerror = () => reject(request.error);
11056
+ }));
11057
+ }
11058
+ persistChange(event) {
11059
+ if (!event || !event.collection || !event.id) {
11060
+ return;
11061
+ }
11062
+ this.persistRecord(event.collection, event.id).catch((error) => {
11063
+ this.node.emit("warning", {
11064
+ type: "persistence-failed",
11065
+ collection: event.collection,
11066
+ id: event.id,
11067
+ error
11068
+ });
11069
+ });
11070
+ }
11071
+ async loadAllRecords() {
11072
+ return this.runTransaction("readonly", (store) => new Promise((resolve, reject) => {
11073
+ const request = store.getAll();
11074
+ request.onsuccess = () => resolve(request.result || []);
11075
+ request.onerror = () => reject(request.error);
11076
+ }));
11077
+ }
11078
+ async hydrate() {
11079
+ if (!this.node) {
11080
+ throw new Error("IndexedDBPersistence requires an attached node before hydrate");
11081
+ }
11082
+ const storedRecords = await this.loadAllRecords();
11083
+ for (const stored of storedRecords) {
11084
+ if (!this.shouldPersist(stored.collection)) {
11085
+ continue;
11086
+ }
11087
+ this.node.restoreRecord(stored.collection, {
11088
+ id: stored.id,
11089
+ ownerId: stored.ownerId,
11090
+ collaboratorIds: stored.collaboratorIds,
11091
+ data: stored.data,
11092
+ createdAt: stored.createdAt,
11093
+ updatedAt: stored.updatedAt,
11094
+ deletedAt: stored.deletedAt,
11095
+ version: stored.version
11096
+ });
11097
+ }
11098
+ }
11099
+ async attach(node) {
11100
+ if (!node) {
11101
+ throw new Error("IndexedDBPersistence.attach requires a DignityP2P node");
11102
+ }
11103
+ this.node = node;
11104
+ await this.hydrate();
11105
+ this.changeHandler = (event) => this.persistChange(event);
11106
+ node.on("change", this.changeHandler);
11107
+ }
11108
+ async detach() {
11109
+ if (this.node && this.changeHandler) {
11110
+ this.node.off("change", this.changeHandler);
11111
+ }
11112
+ this.changeHandler = null;
11113
+ this.node = null;
11114
+ }
11115
+ async clear() {
11116
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
11117
+ const request = store.clear();
11118
+ request.onsuccess = () => resolve();
11119
+ request.onerror = () => reject(request.error);
11120
+ }));
11121
+ }
11122
+ };
11123
+ module2.exports = IndexedDBPersistence2;
11124
+ }
11125
+ });
11126
+
10387
11127
  // src/index.js
10388
11128
  var DignityP2P = require_dignity_p2p();
10389
11129
  var createDefaultSignalingPool = require_create_default_signaling_pool();
@@ -10394,6 +11134,11 @@ var {
10394
11134
  InMemoryNetworkHub,
10395
11135
  InMemoryNetworkAdapter
10396
11136
  } = require_in_memory_network();
11137
+ var {
11138
+ PeerJSNetworkAdapter,
11139
+ createPeerJSNetworkAdapter
11140
+ } = require_peerjs_network();
11141
+ var IndexedDBPersistence = require_indexeddb_persistence();
10397
11142
  var {
10398
11143
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10399
11144
  DEFAULT_SIGNALING_FALLBACK_URLS
@@ -10412,6 +11157,9 @@ module.exports = {
10412
11157
  PeerJSSignalingProvider,
10413
11158
  InMemoryNetworkHub,
10414
11159
  InMemoryNetworkAdapter,
11160
+ PeerJSNetworkAdapter,
11161
+ createPeerJSNetworkAdapter,
11162
+ IndexedDBPersistence,
10415
11163
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10416
11164
  DEFAULT_SIGNALING_FALLBACK_URLS,
10417
11165
  VDF,