dignity.js 0.2.0 → 0.4.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.
@@ -2454,7 +2454,8 @@ var require_message_security_service = __commonJS({
2454
2454
  broadcastPasswords: {},
2455
2455
  resolveBroadcastPassword: null,
2456
2456
  powSteps: 22,
2457
- trustedPeerKeys: {}
2457
+ trustedPeerKeys: {},
2458
+ kdfIterations: 1e5
2458
2459
  };
2459
2460
  function stableStringify(value) {
2460
2461
  if (value === null || typeof value !== "object") {
@@ -2481,6 +2482,33 @@ var require_message_security_service = __commonJS({
2481
2482
  function utf8ToBytes(value) {
2482
2483
  return naclUtil.decodeUTF8(value);
2483
2484
  }
2485
+ async function deriveBroadcastKey(password, salt, iterations) {
2486
+ const subtle = globalThis.crypto && globalThis.crypto.subtle;
2487
+ if (subtle) {
2488
+ const keyMaterial = await subtle.importKey(
2489
+ "raw",
2490
+ utf8ToBytes(password),
2491
+ "PBKDF2",
2492
+ false,
2493
+ ["deriveBits"]
2494
+ );
2495
+ const bits = await subtle.deriveBits(
2496
+ { name: "PBKDF2", salt, iterations, hash: "SHA-256" },
2497
+ keyMaterial,
2498
+ 256
2499
+ );
2500
+ return new Uint8Array(bits);
2501
+ }
2502
+ try {
2503
+ const { pbkdf2Sync } = __require("crypto");
2504
+ return new Uint8Array(pbkdf2Sync(password, Buffer.from(salt), iterations, 32, "sha256"));
2505
+ } catch (_ignored) {
2506
+ return hash32(concatBytes(utf8ToBytes(password), salt));
2507
+ }
2508
+ }
2509
+ function legacyBroadcastKey(password, salt) {
2510
+ return hash32(concatBytes(utf8ToBytes(password), salt));
2511
+ }
2484
2512
  function normalizePeerPublicKey(publicKey) {
2485
2513
  if (!publicKey || typeof publicKey !== "object") {
2486
2514
  throw new Error("Public key must be an object with signingPublicKey and encryptionPublicKey");
@@ -2641,7 +2669,7 @@ var require_message_security_service = __commonJS({
2641
2669
  if (envelope.security && envelope.security.signing && envelope.security.signing.enabled && this.options.signingEnabled) {
2642
2670
  this.verifySignature(envelope);
2643
2671
  }
2644
- const payload = this.decryptPayload(envelope);
2672
+ const payload = await this.decryptPayload(envelope);
2645
2673
  return {
2646
2674
  ignored: false,
2647
2675
  messageType: envelope.messageType,
@@ -2706,7 +2734,8 @@ var require_message_security_service = __commonJS({
2706
2734
  const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
2707
2735
  const salt = nacl.randomBytes(16);
2708
2736
  const password = this.resolveBroadcastPassword(scope);
2709
- const key = hash32(concatBytes(utf8ToBytes(password), salt));
2737
+ const iterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2738
+ const key = await deriveBroadcastKey(password, salt, iterations);
2710
2739
  const encrypted = nacl.secretbox(plainText, nonce, key);
2711
2740
  return {
2712
2741
  payload: naclUtil.encodeBase64(encrypted),
@@ -2715,11 +2744,13 @@ var require_message_security_service = __commonJS({
2715
2744
  mode: "broadcast",
2716
2745
  scope,
2717
2746
  nonce: naclUtil.encodeBase64(nonce),
2718
- salt: naclUtil.encodeBase64(salt)
2747
+ salt: naclUtil.encodeBase64(salt),
2748
+ kdf: "pbkdf2",
2749
+ kdfIterations: iterations
2719
2750
  }
2720
2751
  };
2721
2752
  }
2722
- decryptPayload(envelope) {
2753
+ async decryptPayload(envelope) {
2723
2754
  const encryption = envelope.security ? envelope.security.encryption : null;
2724
2755
  if (!encryption || !encryption.enabled || !this.options.encryptionEnabled) {
2725
2756
  return envelope.payload;
@@ -2730,7 +2761,13 @@ var require_message_security_service = __commonJS({
2730
2761
  const password = this.resolveBroadcastPassword(scope);
2731
2762
  const salt = naclUtil.decodeBase64(encryption.salt);
2732
2763
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2733
- const key = hash32(concatBytes(utf8ToBytes(password), salt));
2764
+ let key;
2765
+ if (encryption.kdf === "pbkdf2") {
2766
+ const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2767
+ key = await deriveBroadcastKey(password, salt, iterations);
2768
+ } else {
2769
+ key = legacyBroadcastKey(password, salt);
2770
+ }
2734
2771
  const decrypted = nacl.secretbox.open(encryptedBuffer, nonce, key);
2735
2772
  if (!decrypted) {
2736
2773
  throw new Error("Unable to decrypt broadcast payload");
@@ -2824,6 +2861,8 @@ var require_message_security_service = __commonJS({
2824
2861
  module.exports = {
2825
2862
  MessageSecurityService,
2826
2863
  stableStringify,
2864
+ deriveBroadcastKey,
2865
+ legacyBroadcastKey,
2827
2866
  DEFAULT_SECURITY_OPTIONS
2828
2867
  };
2829
2868
  }
@@ -2961,6 +3000,21 @@ var require_dignity_p2p = __commonJS({
2961
3000
  if (existing.ownerId !== this.nodeId) {
2962
3001
  throw new Error(`Only owner ${existing.ownerId} can update object ${id}`);
2963
3002
  }
3003
+ if (typeof options.expectedVersion === "number" && existing.version !== options.expectedVersion) {
3004
+ this.emitConflict({
3005
+ kind: "update",
3006
+ collection: collectionName,
3007
+ id,
3008
+ expectedVersion: options.expectedVersion,
3009
+ currentVersion: existing.version,
3010
+ phase: "local"
3011
+ });
3012
+ const error = new Error(
3013
+ `Version conflict on ${collectionName}/${id}: expected ${options.expectedVersion}, current ${existing.version}`
3014
+ );
3015
+ error.code = "VERSION_CONFLICT";
3016
+ throw error;
3017
+ }
2964
3018
  const operation = {
2965
3019
  opId: this.idGenerator(),
2966
3020
  kind: "update",
@@ -2981,6 +3035,27 @@ var require_dignity_p2p = __commonJS({
2981
3035
  });
2982
3036
  return this.read(collectionName, id);
2983
3037
  }
3038
+ async updateWithRetry(collectionName, id, patchFn, options = {}) {
3039
+ const maxAttempts = typeof options.maxAttempts === "number" ? options.maxAttempts : 5;
3040
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3041
+ const current = this.read(collectionName, id);
3042
+ if (!current) {
3043
+ throw new Error(`Object ${id} does not exist in ${collectionName}`);
3044
+ }
3045
+ const patch = await patchFn(current);
3046
+ try {
3047
+ return await this.update(collectionName, id, patch, {
3048
+ ...options,
3049
+ expectedVersion: current.version
3050
+ });
3051
+ } catch (error) {
3052
+ if (error.code !== "VERSION_CONFLICT" || attempt === maxAttempts - 1) {
3053
+ throw error;
3054
+ }
3055
+ }
3056
+ }
3057
+ throw new Error(`Unable to update ${collectionName}/${id} after ${maxAttempts} attempts`);
3058
+ }
2984
3059
  async remove(collectionName, id, options = {}) {
2985
3060
  const existing = this.getCollection(collectionName).get(id);
2986
3061
  if (!existing || existing.deletedAt) {
@@ -3258,6 +3333,29 @@ var require_dignity_p2p = __commonJS({
3258
3333
  isPeerBanned(peerId) {
3259
3334
  return this.getBanInfo(peerId) !== null;
3260
3335
  }
3336
+ emitConflict(details) {
3337
+ this.emit("conflict", details);
3338
+ }
3339
+ restoreRecord(collectionName, record) {
3340
+ if (!record || !record.id) {
3341
+ return false;
3342
+ }
3343
+ const collection = this.getCollection(collectionName);
3344
+ const current = collection.get(record.id);
3345
+ if (current && current.version >= record.version) {
3346
+ return false;
3347
+ }
3348
+ collection.set(record.id, {
3349
+ id: record.id,
3350
+ ownerId: record.ownerId,
3351
+ data: { ...record.data || {} },
3352
+ createdAt: record.createdAt,
3353
+ updatedAt: record.updatedAt,
3354
+ deletedAt: record.deletedAt || null,
3355
+ version: record.version
3356
+ });
3357
+ return true;
3358
+ }
3261
3359
  applyOperation(operation) {
3262
3360
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3263
3361
  return false;
@@ -3288,6 +3386,15 @@ var require_dignity_p2p = __commonJS({
3288
3386
  return false;
3289
3387
  }
3290
3388
  if (typeof operation.baseVersion === "number" && operation.baseVersion !== current.version) {
3389
+ this.emitConflict({
3390
+ kind: operation.kind,
3391
+ collection: operation.collectionName,
3392
+ id: operation.id,
3393
+ expectedVersion: operation.baseVersion,
3394
+ currentVersion: current.version,
3395
+ phase: "remote",
3396
+ operation
3397
+ });
3291
3398
  return false;
3292
3399
  }
3293
3400
  if (operation.kind === "update") {
@@ -10375,6 +10482,163 @@ var require_in_memory_network = __commonJS({
10375
10482
  }
10376
10483
  });
10377
10484
 
10485
+ // src/persistence/indexeddb-persistence.js
10486
+ var require_indexeddb_persistence = __commonJS({
10487
+ "src/persistence/indexeddb-persistence.js"(exports, module) {
10488
+ var IndexedDBPersistence = class {
10489
+ constructor({
10490
+ dbName = "dignity",
10491
+ storeName = "records",
10492
+ collections = null,
10493
+ indexedDB = typeof globalThis !== "undefined" ? globalThis.indexedDB : null
10494
+ } = {}) {
10495
+ this.dbName = dbName;
10496
+ this.storeName = storeName;
10497
+ this.collections = collections;
10498
+ this.indexedDB = indexedDB;
10499
+ this.node = null;
10500
+ this.changeHandler = null;
10501
+ }
10502
+ recordKey(collection, id) {
10503
+ return `${collection}:${id}`;
10504
+ }
10505
+ shouldPersist(collection) {
10506
+ if (!this.collections) {
10507
+ return true;
10508
+ }
10509
+ return this.collections.includes(collection);
10510
+ }
10511
+ openDb() {
10512
+ if (!this.indexedDB) {
10513
+ return Promise.reject(new Error("IndexedDB is not available"));
10514
+ }
10515
+ return new Promise((resolve, reject) => {
10516
+ const request = this.indexedDB.open(this.dbName, 1);
10517
+ request.onupgradeneeded = () => {
10518
+ const db = request.result;
10519
+ if (!db.objectStoreNames.contains(this.storeName)) {
10520
+ db.createObjectStore(this.storeName, { keyPath: "key" });
10521
+ }
10522
+ };
10523
+ request.onsuccess = () => resolve(request.result);
10524
+ request.onerror = () => reject(request.error || new Error("Unable to open IndexedDB"));
10525
+ });
10526
+ }
10527
+ runTransaction(mode, handler) {
10528
+ return this.openDb().then((db) => new Promise((resolve, reject) => {
10529
+ const transaction = db.transaction(this.storeName, mode);
10530
+ const store = transaction.objectStore(this.storeName);
10531
+ Promise.resolve(handler(store)).then(resolve).catch(reject);
10532
+ transaction.oncomplete = () => db.close();
10533
+ transaction.onerror = () => reject(transaction.error || new Error("IndexedDB transaction failed"));
10534
+ transaction.onabort = () => reject(transaction.error || new Error("IndexedDB transaction aborted"));
10535
+ }));
10536
+ }
10537
+ serializeRecord(collection, id) {
10538
+ const record = this.node.getCollection(collection).get(id);
10539
+ if (!record) {
10540
+ return null;
10541
+ }
10542
+ return {
10543
+ key: this.recordKey(collection, id),
10544
+ collection,
10545
+ id,
10546
+ ownerId: record.ownerId,
10547
+ data: { ...record.data },
10548
+ createdAt: record.createdAt,
10549
+ updatedAt: record.updatedAt,
10550
+ deletedAt: record.deletedAt,
10551
+ version: record.version
10552
+ };
10553
+ }
10554
+ async persistRecord(collection, id) {
10555
+ if (!this.node || !this.shouldPersist(collection)) {
10556
+ return;
10557
+ }
10558
+ const serialized = this.serializeRecord(collection, id);
10559
+ const key = this.recordKey(collection, id);
10560
+ if (!serialized) {
10561
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10562
+ const request = store.delete(key);
10563
+ request.onsuccess = () => resolve();
10564
+ request.onerror = () => reject(request.error);
10565
+ }));
10566
+ return;
10567
+ }
10568
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10569
+ const request = store.put(serialized);
10570
+ request.onsuccess = () => resolve();
10571
+ request.onerror = () => reject(request.error);
10572
+ }));
10573
+ }
10574
+ persistChange(event) {
10575
+ if (!event || !event.collection || !event.id) {
10576
+ return;
10577
+ }
10578
+ this.persistRecord(event.collection, event.id).catch((error) => {
10579
+ this.node.emit("warning", {
10580
+ type: "persistence-failed",
10581
+ collection: event.collection,
10582
+ id: event.id,
10583
+ error
10584
+ });
10585
+ });
10586
+ }
10587
+ async loadAllRecords() {
10588
+ return this.runTransaction("readonly", (store) => new Promise((resolve, reject) => {
10589
+ const request = store.getAll();
10590
+ request.onsuccess = () => resolve(request.result || []);
10591
+ request.onerror = () => reject(request.error);
10592
+ }));
10593
+ }
10594
+ async hydrate() {
10595
+ if (!this.node) {
10596
+ throw new Error("IndexedDBPersistence requires an attached node before hydrate");
10597
+ }
10598
+ const storedRecords = await this.loadAllRecords();
10599
+ for (const stored of storedRecords) {
10600
+ if (!this.shouldPersist(stored.collection)) {
10601
+ continue;
10602
+ }
10603
+ this.node.restoreRecord(stored.collection, {
10604
+ id: stored.id,
10605
+ ownerId: stored.ownerId,
10606
+ data: stored.data,
10607
+ createdAt: stored.createdAt,
10608
+ updatedAt: stored.updatedAt,
10609
+ deletedAt: stored.deletedAt,
10610
+ version: stored.version
10611
+ });
10612
+ }
10613
+ }
10614
+ async attach(node) {
10615
+ if (!node) {
10616
+ throw new Error("IndexedDBPersistence.attach requires a DignityP2P node");
10617
+ }
10618
+ this.node = node;
10619
+ await this.hydrate();
10620
+ this.changeHandler = (event) => this.persistChange(event);
10621
+ node.on("change", this.changeHandler);
10622
+ }
10623
+ async detach() {
10624
+ if (this.node && this.changeHandler) {
10625
+ this.node.off("change", this.changeHandler);
10626
+ }
10627
+ this.changeHandler = null;
10628
+ this.node = null;
10629
+ }
10630
+ async clear() {
10631
+ await this.runTransaction("readwrite", (store) => new Promise((resolve, reject) => {
10632
+ const request = store.clear();
10633
+ request.onsuccess = () => resolve();
10634
+ request.onerror = () => reject(request.error);
10635
+ }));
10636
+ }
10637
+ };
10638
+ module.exports = IndexedDBPersistence;
10639
+ }
10640
+ });
10641
+
10378
10642
  // src/index.js
10379
10643
  var require_index = __commonJS({
10380
10644
  "src/index.js"(exports, module) {
@@ -10387,6 +10651,7 @@ var require_index = __commonJS({
10387
10651
  InMemoryNetworkHub,
10388
10652
  InMemoryNetworkAdapter
10389
10653
  } = require_in_memory_network();
10654
+ var IndexedDBPersistence = require_indexeddb_persistence();
10390
10655
  var {
10391
10656
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10392
10657
  DEFAULT_SIGNALING_FALLBACK_URLS
@@ -10405,6 +10670,7 @@ var require_index = __commonJS({
10405
10670
  PeerJSSignalingProvider,
10406
10671
  InMemoryNetworkHub,
10407
10672
  InMemoryNetworkAdapter,
10673
+ IndexedDBPersistence,
10408
10674
  DEFAULT_CLOUDFLARE_SIGNALING_URLS,
10409
10675
  DEFAULT_SIGNALING_FALLBACK_URLS,
10410
10676
  VDF,