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.
- package/README.md +78 -15
- package/dist/dignity.cjs.js +272 -6
- package/dist/dignity.cjs.js.map +4 -4
- package/dist/dignity.esm.js +272 -6
- package/dist/dignity.esm.js.map +3 -3
- package/dist/dignity.min.js +5 -5
- package/docs/assets/docs.js +47 -0
- package/docs/assets/highlight/github-dark.min.css +10 -0
- package/docs/assets/highlight/github.min.css +10 -0
- package/docs/assets/highlight/highlight.min.js +1244 -0
- package/docs/assets/styles.css +449 -38
- package/docs/index.html +601 -81
- package/docs/openapi-like.json +44 -6
- package/package.json +22 -4
- package/src/core/dignity-p2p.js +79 -0
- package/src/index.js +2 -0
- package/src/persistence/indexeddb-persistence.js +182 -0
- package/src/react/index.js +114 -0
- package/src/security/message-security-service.js +51 -6
package/dist/dignity.esm.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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,
|