dignity.js 0.6.0 → 0.7.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.
@@ -2444,19 +2444,511 @@ var require_vdf = __commonJS({
2444
2444
  }
2445
2445
  });
2446
2446
 
2447
+ // src/security/derive-key-pair.js
2448
+ var require_derive_key_pair = __commonJS({
2449
+ "src/security/derive-key-pair.js"(exports, module) {
2450
+ var nacl = require_nacl_fast();
2451
+ var naclUtil = require_nacl_util();
2452
+ var { deriveBroadcastKey, DEFAULT_SECURITY_OPTIONS } = require_message_security_service();
2453
+ var SIGNING_INFO = "dignity-signing-v1";
2454
+ var ENCRYPTION_INFO = "dignity-encryption-v1";
2455
+ var COLD_RECOVERY_INFO = "dignity-cold-recovery-v1";
2456
+ function utf8ToBytes(value) {
2457
+ return naclUtil.decodeUTF8(value);
2458
+ }
2459
+ function concatBytes(...parts) {
2460
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
2461
+ const result = new Uint8Array(total);
2462
+ let offset = 0;
2463
+ for (const part of parts) {
2464
+ result.set(part, offset);
2465
+ offset += part.length;
2466
+ }
2467
+ return result;
2468
+ }
2469
+ function buildColdRecoverySalt(username, pepper = "") {
2470
+ if (!username || typeof username !== "string") {
2471
+ throw new Error("deriveColdRecoverySigningKey requires username");
2472
+ }
2473
+ const segments = ["dignity-cold-recovery-v1"];
2474
+ if (pepper) {
2475
+ segments.push(pepper);
2476
+ }
2477
+ segments.push(username, COLD_RECOVERY_INFO);
2478
+ return utf8ToBytes(segments.join("\0"));
2479
+ }
2480
+ async function deriveColdRecoverySigningKey({
2481
+ username,
2482
+ coldPassword,
2483
+ pepper = "",
2484
+ kdfIterations
2485
+ } = {}) {
2486
+ if (!coldPassword || typeof coldPassword !== "string") {
2487
+ throw new Error("deriveColdRecoverySigningKey requires coldPassword");
2488
+ }
2489
+ const salt = buildColdRecoverySalt(username, pepper);
2490
+ const iterations = typeof kdfIterations === "number" ? kdfIterations : DEFAULT_SECURITY_OPTIONS.kdfIterations;
2491
+ const seed = await deriveBroadcastKey(coldPassword, salt, iterations);
2492
+ const signing = nacl.sign.keyPair.fromSeed(seed);
2493
+ return {
2494
+ signing,
2495
+ recoveryPublicKey: naclUtil.encodeBase64(signing.publicKey)
2496
+ };
2497
+ }
2498
+ function buildIdentitySalt(username, info, pepper = "", generation = 1) {
2499
+ if (!username || typeof username !== "string") {
2500
+ throw new Error("deriveKeyPairFromCredentials requires username");
2501
+ }
2502
+ if (!info || typeof info !== "string") {
2503
+ throw new Error("deriveKeyPairFromCredentials requires info label");
2504
+ }
2505
+ const normalizedGeneration = Number(generation);
2506
+ if (!Number.isInteger(normalizedGeneration) || normalizedGeneration < 1) {
2507
+ throw new Error("deriveKeyPairFromCredentials requires generation >= 1");
2508
+ }
2509
+ const segments = ["dignity-identity-v1"];
2510
+ if (pepper) {
2511
+ segments.push(pepper);
2512
+ }
2513
+ segments.push(username, `gen:${normalizedGeneration}`, info);
2514
+ return utf8ToBytes(segments.join("\0"));
2515
+ }
2516
+ async function deriveIdentitySeed({ password, username, info, pepper, generation, kdfIterations }) {
2517
+ if (!password || typeof password !== "string") {
2518
+ throw new Error("deriveKeyPairFromCredentials requires password");
2519
+ }
2520
+ const salt = buildIdentitySalt(username, info, pepper, generation);
2521
+ const iterations = typeof kdfIterations === "number" ? kdfIterations : DEFAULT_SECURITY_OPTIONS.kdfIterations;
2522
+ return deriveBroadcastKey(password, salt, iterations);
2523
+ }
2524
+ async function deriveKeyPairFromCredentials({
2525
+ username,
2526
+ password,
2527
+ pepper = "",
2528
+ generation = 1,
2529
+ kdfIterations
2530
+ } = {}) {
2531
+ const signingSeed = await deriveIdentitySeed({
2532
+ password,
2533
+ username,
2534
+ info: SIGNING_INFO,
2535
+ pepper,
2536
+ generation,
2537
+ kdfIterations
2538
+ });
2539
+ const encryptionSecret = await deriveIdentitySeed({
2540
+ password,
2541
+ username,
2542
+ info: ENCRYPTION_INFO,
2543
+ pepper,
2544
+ generation,
2545
+ kdfIterations
2546
+ });
2547
+ return {
2548
+ signing: nacl.sign.keyPair.fromSeed(signingSeed),
2549
+ encryption: nacl.box.keyPair.fromSecretKey(encryptionSecret),
2550
+ generation
2551
+ };
2552
+ }
2553
+ function keyPairToPublicBundle(keyPair) {
2554
+ return {
2555
+ signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
2556
+ encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
2557
+ };
2558
+ }
2559
+ module.exports = {
2560
+ deriveKeyPairFromCredentials,
2561
+ deriveColdRecoverySigningKey,
2562
+ keyPairToPublicBundle,
2563
+ buildIdentitySalt,
2564
+ buildColdRecoverySalt,
2565
+ SIGNING_INFO,
2566
+ ENCRYPTION_INFO,
2567
+ COLD_RECOVERY_INFO,
2568
+ concatBytes
2569
+ };
2570
+ }
2571
+ });
2572
+
2573
+ // src/security/identity-rotation.js
2574
+ var require_identity_rotation = __commonJS({
2575
+ "src/security/identity-rotation.js"(exports, module) {
2576
+ var nacl = require_nacl_fast();
2577
+ var naclUtil = require_nacl_util();
2578
+ var { stableStringify } = require_message_security_service();
2579
+ var {
2580
+ deriveKeyPairFromCredentials,
2581
+ deriveColdRecoverySigningKey,
2582
+ keyPairToPublicBundle
2583
+ } = require_derive_key_pair();
2584
+ var ROTATION_TYPES = /* @__PURE__ */ new Set(["compromise-recovery", "password-change"]);
2585
+ function utf8ToBytes(value) {
2586
+ return naclUtil.decodeUTF8(value);
2587
+ }
2588
+ function normalizePublicKeyBundle(publicKey) {
2589
+ if (!publicKey || !publicKey.signingPublicKey || !publicKey.encryptionPublicKey) {
2590
+ throw new Error("Public key bundle requires signingPublicKey and encryptionPublicKey");
2591
+ }
2592
+ return {
2593
+ signingPublicKey: publicKey.signingPublicKey,
2594
+ encryptionPublicKey: publicKey.encryptionPublicKey
2595
+ };
2596
+ }
2597
+ function buildIdentityRotationPayload({
2598
+ username,
2599
+ fromGeneration,
2600
+ toGeneration,
2601
+ previousPublicKey,
2602
+ nextPublicKey,
2603
+ rotationKind,
2604
+ reason,
2605
+ timestamp
2606
+ }) {
2607
+ if (!username) {
2608
+ throw new Error("Identity rotation requires username");
2609
+ }
2610
+ if (!ROTATION_TYPES.has(rotationKind)) {
2611
+ throw new Error(`Identity rotation kind must be one of: ${[...ROTATION_TYPES].join(", ")}`);
2612
+ }
2613
+ if (toGeneration !== fromGeneration + 1) {
2614
+ throw new Error("Identity rotation must advance generation by exactly 1");
2615
+ }
2616
+ return {
2617
+ version: 1,
2618
+ type: "identity:rotate",
2619
+ username,
2620
+ fromGeneration,
2621
+ toGeneration,
2622
+ previousPublicKey: normalizePublicKeyBundle(previousPublicKey),
2623
+ nextPublicKey: normalizePublicKeyBundle(nextPublicKey),
2624
+ rotationKind,
2625
+ reason: reason || "",
2626
+ timestamp: typeof timestamp === "number" ? timestamp : Date.now()
2627
+ };
2628
+ }
2629
+ function signIdentityRotationPayload(payload, signingSecretKey) {
2630
+ const message = utf8ToBytes(stableStringify(payload));
2631
+ const signature = nacl.sign.detached(message, signingSecretKey);
2632
+ return naclUtil.encodeBase64(signature);
2633
+ }
2634
+ function verifyDetachedSignature(payload, signatureBase64, signingPublicKeyBase64) {
2635
+ const message = utf8ToBytes(stableStringify(payload));
2636
+ const signatureBytes = naclUtil.decodeBase64(signatureBase64);
2637
+ const signingPublicKey = naclUtil.decodeBase64(signingPublicKeyBase64);
2638
+ return nacl.sign.detached.verify(message, signatureBytes, signingPublicKey);
2639
+ }
2640
+ function createIdentityRotation({
2641
+ username,
2642
+ fromGeneration,
2643
+ toGeneration,
2644
+ previousPublicKey,
2645
+ nextKeyPair,
2646
+ rotationKind,
2647
+ reason,
2648
+ timestamp,
2649
+ coldRecoverySigningSecretKey
2650
+ }) {
2651
+ if (!nextKeyPair || !nextKeyPair.signing || !nextKeyPair.signing.secretKey) {
2652
+ throw new Error("Identity rotation requires nextKeyPair with signing secret");
2653
+ }
2654
+ const payload = buildIdentityRotationPayload({
2655
+ username,
2656
+ fromGeneration,
2657
+ toGeneration,
2658
+ previousPublicKey,
2659
+ nextPublicKey: keyPairToPublicBundle(nextKeyPair),
2660
+ rotationKind,
2661
+ reason,
2662
+ timestamp
2663
+ });
2664
+ const rotation = {
2665
+ ...payload,
2666
+ signature: signIdentityRotationPayload(payload, nextKeyPair.signing.secretKey)
2667
+ };
2668
+ if (coldRecoverySigningSecretKey) {
2669
+ rotation.recoverySignature = signIdentityRotationPayload(
2670
+ payload,
2671
+ coldRecoverySigningSecretKey
2672
+ );
2673
+ }
2674
+ return rotation;
2675
+ }
2676
+ function buildColdRecoveryEnrollmentPayload({ username, recoveryPublicKey, timestamp }) {
2677
+ if (!username) {
2678
+ throw new Error("Cold recovery enrollment requires username");
2679
+ }
2680
+ if (!recoveryPublicKey) {
2681
+ throw new Error("Cold recovery enrollment requires recoveryPublicKey");
2682
+ }
2683
+ return {
2684
+ version: 1,
2685
+ type: "identity:cold-enroll",
2686
+ username,
2687
+ recoveryPublicKey,
2688
+ timestamp: typeof timestamp === "number" ? timestamp : Date.now()
2689
+ };
2690
+ }
2691
+ function createColdRecoveryEnrollment({
2692
+ username,
2693
+ coldRecoverySigningSecretKey,
2694
+ recoveryPublicKey,
2695
+ timestamp
2696
+ }) {
2697
+ if (!coldRecoverySigningSecretKey) {
2698
+ throw new Error("Cold recovery enrollment requires cold recovery signing secret");
2699
+ }
2700
+ if (!recoveryPublicKey) {
2701
+ throw new Error("Cold recovery enrollment requires recoveryPublicKey");
2702
+ }
2703
+ const payload = buildColdRecoveryEnrollmentPayload({
2704
+ username,
2705
+ recoveryPublicKey,
2706
+ timestamp
2707
+ });
2708
+ return {
2709
+ ...payload,
2710
+ signature: signIdentityRotationPayload(payload, coldRecoverySigningSecretKey)
2711
+ };
2712
+ }
2713
+ function verifyColdRecoveryEnrollment(enrollment) {
2714
+ if (!enrollment || enrollment.type !== "identity:cold-enroll" || enrollment.version !== 1) {
2715
+ return { ok: false, error: "invalid-enrollment-shape" };
2716
+ }
2717
+ if (!enrollment.signature || !enrollment.recoveryPublicKey) {
2718
+ return { ok: false, error: "missing-enrollment-fields" };
2719
+ }
2720
+ const { signature, ...payload } = enrollment;
2721
+ const verified = verifyDetachedSignature(payload, signature, enrollment.recoveryPublicKey);
2722
+ if (!verified) {
2723
+ return { ok: false, error: "invalid-enrollment-signature" };
2724
+ }
2725
+ return { ok: true, enrollment };
2726
+ }
2727
+ function verifyIdentityRotation(rotation, options = {}) {
2728
+ if (!rotation || rotation.type !== "identity:rotate" || rotation.version !== 1) {
2729
+ return { ok: false, error: "invalid-rotation-shape" };
2730
+ }
2731
+ if (!rotation.signature || !rotation.nextPublicKey || !rotation.previousPublicKey) {
2732
+ return { ok: false, error: "missing-rotation-fields" };
2733
+ }
2734
+ if (rotation.toGeneration !== rotation.fromGeneration + 1) {
2735
+ return { ok: false, error: "invalid-generation-step" };
2736
+ }
2737
+ if (!ROTATION_TYPES.has(rotation.rotationKind)) {
2738
+ return { ok: false, error: "invalid-rotation-kind" };
2739
+ }
2740
+ const { signature, recoverySignature, ...payload } = rotation;
2741
+ const verified = verifyDetachedSignature(payload, signature, rotation.nextPublicKey.signingPublicKey);
2742
+ if (!verified) {
2743
+ return { ok: false, error: "invalid-signature" };
2744
+ }
2745
+ const requiredRecoveryPublicKey = options.requiredRecoveryPublicKey || null;
2746
+ if (requiredRecoveryPublicKey) {
2747
+ if (!recoverySignature) {
2748
+ return { ok: false, error: "missing-recovery-signature" };
2749
+ }
2750
+ const recoveryVerified = verifyDetachedSignature(
2751
+ payload,
2752
+ recoverySignature,
2753
+ requiredRecoveryPublicKey
2754
+ );
2755
+ if (!recoveryVerified) {
2756
+ return { ok: false, error: "invalid-recovery-signature" };
2757
+ }
2758
+ }
2759
+ if (rotation.previousPublicKey.signingPublicKey === rotation.nextPublicKey.signingPublicKey) {
2760
+ return { ok: false, error: "unchanged-signing-key" };
2761
+ }
2762
+ return { ok: true, rotation };
2763
+ }
2764
+ async function resolveColdRecoverySigningSecretKey({
2765
+ username,
2766
+ coldPassword,
2767
+ pepper,
2768
+ kdfIterations
2769
+ }) {
2770
+ if (!coldPassword) {
2771
+ return null;
2772
+ }
2773
+ const coldRecovery = await deriveColdRecoverySigningKey({
2774
+ username,
2775
+ coldPassword,
2776
+ pepper,
2777
+ kdfIterations
2778
+ });
2779
+ return coldRecovery.signing.secretKey;
2780
+ }
2781
+ async function revokeAndRotateIdentity({
2782
+ username,
2783
+ password,
2784
+ coldPassword,
2785
+ currentGeneration = 1,
2786
+ reason = "compromise-recovery",
2787
+ pepper = "",
2788
+ kdfIterations,
2789
+ timestamp
2790
+ } = {}) {
2791
+ const currentKeyPair = await deriveKeyPairFromCredentials({
2792
+ username,
2793
+ password,
2794
+ generation: currentGeneration,
2795
+ pepper,
2796
+ kdfIterations
2797
+ });
2798
+ const nextGeneration = currentGeneration + 1;
2799
+ const nextKeyPair = await deriveKeyPairFromCredentials({
2800
+ username,
2801
+ password,
2802
+ generation: nextGeneration,
2803
+ pepper,
2804
+ kdfIterations
2805
+ });
2806
+ const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
2807
+ username,
2808
+ coldPassword,
2809
+ pepper,
2810
+ kdfIterations
2811
+ });
2812
+ const rotation = createIdentityRotation({
2813
+ username,
2814
+ fromGeneration: currentGeneration,
2815
+ toGeneration: nextGeneration,
2816
+ previousPublicKey: keyPairToPublicBundle(currentKeyPair),
2817
+ nextKeyPair,
2818
+ rotationKind: "compromise-recovery",
2819
+ reason,
2820
+ timestamp,
2821
+ coldRecoverySigningSecretKey
2822
+ });
2823
+ return {
2824
+ rotation,
2825
+ currentKeyPair,
2826
+ nextKeyPair,
2827
+ nextGeneration,
2828
+ coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
2829
+ };
2830
+ }
2831
+ async function rotateIdentityPassword({
2832
+ username,
2833
+ currentPassword,
2834
+ newPassword,
2835
+ coldPassword,
2836
+ currentGeneration = 1,
2837
+ reason = "password-change",
2838
+ pepper = "",
2839
+ kdfIterations,
2840
+ timestamp
2841
+ } = {}) {
2842
+ const currentKeyPair = await deriveKeyPairFromCredentials({
2843
+ username,
2844
+ password: currentPassword,
2845
+ generation: currentGeneration,
2846
+ pepper,
2847
+ kdfIterations
2848
+ });
2849
+ const nextGeneration = currentGeneration + 1;
2850
+ const nextKeyPair = await deriveKeyPairFromCredentials({
2851
+ username,
2852
+ password: newPassword,
2853
+ generation: nextGeneration,
2854
+ pepper,
2855
+ kdfIterations
2856
+ });
2857
+ const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
2858
+ username,
2859
+ coldPassword,
2860
+ pepper,
2861
+ kdfIterations
2862
+ });
2863
+ const rotation = createIdentityRotation({
2864
+ username,
2865
+ fromGeneration: currentGeneration,
2866
+ toGeneration: nextGeneration,
2867
+ previousPublicKey: keyPairToPublicBundle(currentKeyPair),
2868
+ nextKeyPair,
2869
+ rotationKind: "password-change",
2870
+ reason,
2871
+ timestamp,
2872
+ coldRecoverySigningSecretKey
2873
+ });
2874
+ return {
2875
+ rotation,
2876
+ currentKeyPair,
2877
+ nextKeyPair,
2878
+ nextGeneration,
2879
+ coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
2880
+ };
2881
+ }
2882
+ async function enrollColdRecoveryPassword({
2883
+ username,
2884
+ coldPassword,
2885
+ pepper = "",
2886
+ kdfIterations,
2887
+ timestamp
2888
+ } = {}) {
2889
+ const coldRecovery = await deriveColdRecoverySigningKey({
2890
+ username,
2891
+ coldPassword,
2892
+ pepper,
2893
+ kdfIterations
2894
+ });
2895
+ const enrollment = createColdRecoveryEnrollment({
2896
+ username,
2897
+ coldRecoverySigningSecretKey: coldRecovery.signing.secretKey,
2898
+ recoveryPublicKey: coldRecovery.recoveryPublicKey,
2899
+ timestamp
2900
+ });
2901
+ return {
2902
+ enrollment,
2903
+ recoveryPublicKey: coldRecovery.recoveryPublicKey,
2904
+ coldRecovery
2905
+ };
2906
+ }
2907
+ function shouldApplyIdentityRotation(currentState, rotation, options = {}) {
2908
+ const requiredRecoveryPublicKey = options.enrolledRecoveryPublicKey || currentState && currentState.recoveryPublicKey || null;
2909
+ const verified = verifyIdentityRotation(rotation, { requiredRecoveryPublicKey });
2910
+ if (!verified.ok) {
2911
+ return { apply: false, reason: verified.error };
2912
+ }
2913
+ if (currentState && rotation.toGeneration <= currentState.generation) {
2914
+ return { apply: false, reason: "stale-generation" };
2915
+ }
2916
+ if (currentState && currentState.publicKey && currentState.publicKey.signingPublicKey !== rotation.previousPublicKey.signingPublicKey) {
2917
+ return { apply: false, reason: "previous-key-mismatch" };
2918
+ }
2919
+ return { apply: true, rotation: verified.rotation };
2920
+ }
2921
+ module.exports = {
2922
+ createIdentityRotation,
2923
+ createColdRecoveryEnrollment,
2924
+ verifyIdentityRotation,
2925
+ verifyColdRecoveryEnrollment,
2926
+ revokeAndRotateIdentity,
2927
+ rotateIdentityPassword,
2928
+ enrollColdRecoveryPassword,
2929
+ shouldApplyIdentityRotation,
2930
+ keyPairToPublicBundle,
2931
+ buildIdentityRotationPayload,
2932
+ buildColdRecoveryEnrollmentPayload,
2933
+ signIdentityRotationPayload
2934
+ };
2935
+ }
2936
+ });
2937
+
2447
2938
  // src/security/message-security-service.js
2448
2939
  var require_message_security_service = __commonJS({
2449
2940
  "src/security/message-security-service.js"(exports, module) {
2450
2941
  var nacl = require_nacl_fast();
2451
2942
  var naclUtil = require_nacl_util();
2452
2943
  var VDF = require_vdf();
2944
+ var DEFAULT_APP_PASSWORD = "change-this-app-password";
2453
2945
  var DEFAULT_SECURITY_OPTIONS = {
2454
2946
  enabled: true,
2455
2947
  signingEnabled: true,
2456
2948
  encryptionEnabled: true,
2457
2949
  powEnabled: true,
2458
2950
  powTargetMs: 1e3,
2459
- appPassword: "change-this-app-password",
2951
+ appPassword: DEFAULT_APP_PASSWORD,
2460
2952
  broadcastPasswords: {},
2461
2953
  resolveBroadcastPassword: null,
2462
2954
  powSteps: 22,
@@ -2551,16 +3043,85 @@ var require_message_security_service = __commonJS({
2551
3043
  encryptionPublicKey: naclUtil.encodeBase64(this.encryptionPublicKey)
2552
3044
  };
2553
3045
  this.peerPublicKeys = /* @__PURE__ */ new Map();
3046
+ this.peerIdentityGenerations = /* @__PURE__ */ new Map();
3047
+ this.peerRecoveryPublicKeys = /* @__PURE__ */ new Map();
2554
3048
  for (const [peerId, peerKey] of Object.entries(this.options.trustedPeerKeys || {})) {
2555
3049
  this.peerPublicKeys.set(peerId, normalizePeerPublicKey(peerKey));
3050
+ this.peerIdentityGenerations.set(peerId, 1);
2556
3051
  }
2557
3052
  this.calibratedPowSteps = this.options.powSteps;
2558
3053
  }
2559
3054
  getPublicKey() {
2560
3055
  return { ...this.publicKeyBundle };
2561
3056
  }
2562
- registerPeerPublicKey(peerId, publicKey) {
2563
- this.peerPublicKeys.set(peerId, normalizePeerPublicKey(publicKey));
3057
+ registerPeerPublicKey(peerId, publicKey, options = {}) {
3058
+ const normalized = normalizePeerPublicKey(publicKey);
3059
+ const generation = typeof options.generation === "number" ? options.generation : 1;
3060
+ const currentGeneration = this.peerIdentityGenerations.get(peerId) || 0;
3061
+ if (generation < currentGeneration && options.allowDowngrade !== true) {
3062
+ throw new Error(`Refusing older identity generation for peer ${peerId}`);
3063
+ }
3064
+ this.peerPublicKeys.set(peerId, normalized);
3065
+ this.peerIdentityGenerations.set(peerId, Math.max(currentGeneration, generation));
3066
+ }
3067
+ getPeerIdentityGeneration(peerId) {
3068
+ return this.peerIdentityGenerations.get(peerId) || 0;
3069
+ }
3070
+ getPeerIdentityState(peerId) {
3071
+ const publicKey = this.peerPublicKeys.get(peerId);
3072
+ const recoveryPublicKey = this.peerRecoveryPublicKeys.get(peerId) || null;
3073
+ if (!publicKey && !recoveryPublicKey) {
3074
+ return null;
3075
+ }
3076
+ return {
3077
+ publicKey: publicKey ? { ...publicKey } : null,
3078
+ generation: this.getPeerIdentityGeneration(peerId),
3079
+ recoveryPublicKey
3080
+ };
3081
+ }
3082
+ registerPeerRecoveryPublicKey(peerId, recoveryPublicKey) {
3083
+ if (!peerId || !recoveryPublicKey) {
3084
+ throw new Error("registerPeerRecoveryPublicKey requires peerId and recoveryPublicKey");
3085
+ }
3086
+ this.peerRecoveryPublicKeys.set(peerId, recoveryPublicKey);
3087
+ }
3088
+ getPeerRecoveryPublicKey(peerId) {
3089
+ return this.peerRecoveryPublicKeys.get(peerId) || null;
3090
+ }
3091
+ applyColdRecoveryEnrollment(peerId, enrollment) {
3092
+ const { verifyColdRecoveryEnrollment } = require_identity_rotation();
3093
+ const verified = verifyColdRecoveryEnrollment(enrollment);
3094
+ if (!verified.ok) {
3095
+ const error = new Error(`Invalid cold recovery enrollment: ${verified.error}`);
3096
+ error.code = "INVALID_COLD_RECOVERY_ENROLLMENT";
3097
+ throw error;
3098
+ }
3099
+ this.registerPeerRecoveryPublicKey(peerId, enrollment.recoveryPublicKey);
3100
+ return { applied: true, recoveryPublicKey: enrollment.recoveryPublicKey };
3101
+ }
3102
+ applyIdentityRotation(peerId, rotation) {
3103
+ const { shouldApplyIdentityRotation } = require_identity_rotation();
3104
+ const currentState = this.getPeerIdentityState(peerId);
3105
+ const decision = shouldApplyIdentityRotation(currentState, rotation, {
3106
+ enrolledRecoveryPublicKey: this.getPeerRecoveryPublicKey(peerId)
3107
+ });
3108
+ if (!decision.apply) {
3109
+ if (decision.reason && decision.reason !== "stale-generation" && decision.reason !== "previous-key-mismatch") {
3110
+ const error = new Error(`Invalid identity rotation: ${decision.reason}`);
3111
+ error.code = "INVALID_IDENTITY_ROTATION";
3112
+ throw error;
3113
+ }
3114
+ return { applied: false, reason: decision.reason };
3115
+ }
3116
+ this.registerPeerPublicKey(peerId, rotation.nextPublicKey, {
3117
+ generation: rotation.toGeneration
3118
+ });
3119
+ return {
3120
+ applied: true,
3121
+ fromGeneration: rotation.fromGeneration,
3122
+ toGeneration: rotation.toGeneration,
3123
+ rotationKind: rotation.rotationKind
3124
+ };
2564
3125
  }
2565
3126
  resolvePeerPublicKey(peerId, fallbackPublicKey) {
2566
3127
  const trusted = this.peerPublicKeys.get(peerId);
@@ -2769,8 +3330,14 @@ var require_message_security_service = __commonJS({
2769
3330
  const nonce = naclUtil.decodeBase64(encryption.nonce);
2770
3331
  let key;
2771
3332
  if (encryption.kdf === "pbkdf2") {
2772
- const iterations = encryption.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
2773
- key = await deriveBroadcastKey(password, salt, iterations);
3333
+ const configuredIterations = this.options.kdfIterations || DEFAULT_SECURITY_OPTIONS.kdfIterations;
3334
+ const requestedIterations = encryption.kdfIterations || configuredIterations;
3335
+ const minIterations = Math.max(1e3, Math.floor(configuredIterations * 0.1));
3336
+ const maxIterations = configuredIterations * 2;
3337
+ if (requestedIterations < minIterations || requestedIterations > maxIterations) {
3338
+ throw new Error(`Invalid kdfIterations: ${requestedIterations}`);
3339
+ }
3340
+ key = await deriveBroadcastKey(password, salt, requestedIterations);
2774
3341
  } else {
2775
3342
  key = legacyBroadcastKey(password, salt);
2776
3343
  }
@@ -2869,7 +3436,8 @@ var require_message_security_service = __commonJS({
2869
3436
  stableStringify,
2870
3437
  deriveBroadcastKey,
2871
3438
  legacyBroadcastKey,
2872
- DEFAULT_SECURITY_OPTIONS
3439
+ DEFAULT_SECURITY_OPTIONS,
3440
+ DEFAULT_APP_PASSWORD
2873
3441
  };
2874
3442
  }
2875
3443
  });
@@ -2939,7 +3507,17 @@ var require_dignity_p2p = __commonJS({
2939
3507
  var nacl = require_nacl_fast();
2940
3508
  var naclUtil = require_nacl_util();
2941
3509
  var EventEmitter = require_event_emitter();
2942
- var { MessageSecurityService, stableStringify } = require_message_security_service();
3510
+ var {
3511
+ MessageSecurityService,
3512
+ stableStringify,
3513
+ DEFAULT_APP_PASSWORD
3514
+ } = require_message_security_service();
3515
+ var {
3516
+ revokeAndRotateIdentity,
3517
+ rotateIdentityPassword,
3518
+ enrollColdRecoveryPassword
3519
+ } = require_identity_rotation();
3520
+ var { deriveKeyPairFromCredentials } = require_derive_key_pair();
2943
3521
  var {
2944
3522
  DEFAULT_PEER_GROUP_OPTIONS,
2945
3523
  peerGroupScope,
@@ -2984,13 +3562,24 @@ var require_dignity_p2p = __commonJS({
2984
3562
  this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS.maxHops;
2985
3563
  this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2986
3564
  this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
3565
+ this.maxSeenGossipIds = security && typeof security.maxSeenGossipIds === "number" ? security.maxSeenGossipIds : 1e5;
3566
+ this.gossipPublishMinIntervalMs = security && typeof security.gossipPublishMinIntervalMs === "number" ? security.gossipPublishMinIntervalMs : 0;
3567
+ this.lastGossipPublishAt = /* @__PURE__ */ new Map();
3568
+ this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
2987
3569
  this.state = /* @__PURE__ */ new Map();
2988
- this.appliedOperations = /* @__PURE__ */ new Set();
3570
+ this.appliedOperations = /* @__PURE__ */ new Map();
2989
3571
  this.boundMessageHandler = this.handleIncomingMessage.bind(this);
2990
3572
  }
2991
3573
  async start() {
2992
3574
  this.networkAdapter.onMessage(this.boundMessageHandler);
2993
3575
  await this.networkAdapter.start(this.nodeId);
3576
+ const appPassword = this.securityService.options.appPassword;
3577
+ if (!appPassword || appPassword === DEFAULT_APP_PASSWORD) {
3578
+ this.emit("warning", {
3579
+ type: "default-app-password",
3580
+ message: "Using the default appPassword is insecure; set a strong shared secret in production."
3581
+ });
3582
+ }
2994
3583
  }
2995
3584
  async stop() {
2996
3585
  const joinedGroups = Array.from(this.peerGroups.keys());
@@ -3286,8 +3875,138 @@ var require_dignity_p2p = __commonJS({
3286
3875
  connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3287
3876
  });
3288
3877
  }
3289
- registerPeerPublicKey(peerId, publicKey) {
3290
- this.securityService.registerPeerPublicKey(peerId, publicKey);
3878
+ registerPeerPublicKey(peerId, publicKey, options = {}) {
3879
+ this.securityService.registerPeerPublicKey(peerId, publicKey, options);
3880
+ }
3881
+ getPeerIdentityGeneration(peerId) {
3882
+ return this.securityService.getPeerIdentityGeneration(peerId);
3883
+ }
3884
+ getPeerIdentityState(peerId) {
3885
+ return this.securityService.getPeerIdentityState(peerId);
3886
+ }
3887
+ applyPeerIdentityRotation(peerId, rotation) {
3888
+ const result = this.securityService.applyIdentityRotation(peerId, rotation);
3889
+ if (result.applied) {
3890
+ this.emit("identityrotated", {
3891
+ peerId,
3892
+ username: rotation.username,
3893
+ fromGeneration: result.fromGeneration,
3894
+ toGeneration: result.toGeneration,
3895
+ rotationKind: result.rotationKind
3896
+ });
3897
+ }
3898
+ return result;
3899
+ }
3900
+ async broadcastIdentityRotation(rotation, options = {}) {
3901
+ return this.broadcastMessage("identity:rotate", rotation, options);
3902
+ }
3903
+ async broadcastColdRecoveryEnrollment(enrollment, options = {}) {
3904
+ return this.broadcastMessage("identity:cold-enroll", enrollment, options);
3905
+ }
3906
+ applyPeerColdRecoveryEnrollment(peerId, enrollment) {
3907
+ const result = this.securityService.applyColdRecoveryEnrollment(peerId, enrollment);
3908
+ if (result.applied) {
3909
+ this.emit("coldrecoveryenrolled", {
3910
+ peerId,
3911
+ username: enrollment.username,
3912
+ recoveryPublicKey: enrollment.recoveryPublicKey
3913
+ });
3914
+ }
3915
+ return result;
3916
+ }
3917
+ async enrollAndBroadcastColdRecovery({
3918
+ username,
3919
+ coldPassword,
3920
+ pepper = "",
3921
+ kdfIterations,
3922
+ broadcastOptions = {}
3923
+ } = {}) {
3924
+ const result = await enrollColdRecoveryPassword({
3925
+ username,
3926
+ coldPassword,
3927
+ pepper,
3928
+ kdfIterations
3929
+ });
3930
+ await this.broadcastColdRecoveryEnrollment(result.enrollment, broadcastOptions);
3931
+ return result;
3932
+ }
3933
+ async revokeAndRotateDerivedIdentity({
3934
+ username,
3935
+ password,
3936
+ coldPassword,
3937
+ currentGeneration = 1,
3938
+ reason = "compromise-recovery",
3939
+ pepper = "",
3940
+ kdfIterations,
3941
+ broadcast = false,
3942
+ broadcastOptions = {}
3943
+ } = {}) {
3944
+ const result = await revokeAndRotateIdentity({
3945
+ username,
3946
+ password,
3947
+ coldPassword,
3948
+ currentGeneration,
3949
+ reason,
3950
+ pepper,
3951
+ kdfIterations
3952
+ });
3953
+ if (broadcast) {
3954
+ await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
3955
+ }
3956
+ return result;
3957
+ }
3958
+ async rotateDerivedIdentityPassword({
3959
+ username,
3960
+ currentPassword,
3961
+ newPassword,
3962
+ coldPassword,
3963
+ currentGeneration = 1,
3964
+ reason = "password-change",
3965
+ pepper = "",
3966
+ kdfIterations,
3967
+ broadcast = false,
3968
+ broadcastOptions = {}
3969
+ } = {}) {
3970
+ const result = await rotateIdentityPassword({
3971
+ username,
3972
+ currentPassword,
3973
+ newPassword,
3974
+ coldPassword,
3975
+ currentGeneration,
3976
+ reason,
3977
+ pepper,
3978
+ kdfIterations
3979
+ });
3980
+ if (broadcast) {
3981
+ await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
3982
+ }
3983
+ return result;
3984
+ }
3985
+ async adoptDerivedIdentityKeyPair(keyPair, { generation = 1 } = {}) {
3986
+ if (!keyPair || !keyPair.signing || !keyPair.encryption) {
3987
+ throw new Error("adoptDerivedIdentityKeyPair requires a derived keyPair");
3988
+ }
3989
+ this.securityService.signingSecretKey = keyPair.signing.secretKey;
3990
+ this.securityService.signingPublicKey = keyPair.signing.publicKey;
3991
+ this.securityService.encryptionSecretKey = keyPair.encryption.secretKey;
3992
+ this.securityService.encryptionPublicKey = keyPair.encryption.publicKey;
3993
+ this.securityService.publicKeyBundle = {
3994
+ signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
3995
+ encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
3996
+ };
3997
+ this.securityService.options.keyPair = keyPair;
3998
+ this.securityService.options.identityGeneration = generation;
3999
+ }
4000
+ async deriveAndAdoptIdentity({ username, password, generation = 1, pepper = "", kdfIterations } = {}) {
4001
+ const keyPair = await deriveKeyPairFromCredentials({
4002
+ username,
4003
+ password,
4004
+ generation,
4005
+ pepper,
4006
+ kdfIterations
4007
+ });
4008
+ await this.adoptDerivedIdentityKeyPair(keyPair, { generation });
4009
+ return keyPair;
3291
4010
  }
3292
4011
  trustPeerPublicKey(peerId, publicKey) {
3293
4012
  if (!peerId || !publicKey) {
@@ -3409,6 +4128,13 @@ var require_dignity_p2p = __commonJS({
3409
4128
  this.seenGossipIds.delete(gossipId);
3410
4129
  }
3411
4130
  }
4131
+ while (this.seenGossipIds.size > this.maxSeenGossipIds) {
4132
+ const oldestGossipId = this.seenGossipIds.keys().next().value;
4133
+ if (!oldestGossipId) {
4134
+ break;
4135
+ }
4136
+ this.seenGossipIds.delete(oldestGossipId);
4137
+ }
3412
4138
  }
3413
4139
  hasSeenGossip(gossipId) {
3414
4140
  if (!gossipId) {
@@ -3422,6 +4148,7 @@ var require_dignity_p2p = __commonJS({
3422
4148
  return;
3423
4149
  }
3424
4150
  this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
4151
+ this.pruneSeenGossip();
3425
4152
  }
3426
4153
  listConnectedPeerIds() {
3427
4154
  if (typeof this.networkAdapter.listOpenPeerIds === "function") {
@@ -3499,6 +4226,15 @@ var require_dignity_p2p = __commonJS({
3499
4226
  if (!group && options.allowUnjoined !== true) {
3500
4227
  throw new Error(`PeerGroup ${groupId} has not been joined`);
3501
4228
  }
4229
+ if (this.gossipPublishMinIntervalMs > 0) {
4230
+ const lastPublishAt = this.lastGossipPublishAt.get(groupId) || 0;
4231
+ const elapsed = this.now() - lastPublishAt;
4232
+ if (elapsed < this.gossipPublishMinIntervalMs) {
4233
+ const error = new Error(`Gossip publish rate limit exceeded for group ${groupId}`);
4234
+ error.code = "GOSSIP_RATE_LIMIT";
4235
+ throw error;
4236
+ }
4237
+ }
3502
4238
  const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3503
4239
  const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3504
4240
  const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
@@ -3509,9 +4245,11 @@ var require_dignity_p2p = __commonJS({
3509
4245
  }
3510
4246
  const gossipId = options.gossipId || this.idGenerator();
3511
4247
  this.markSeenGossip(gossipId);
4248
+ this.lastGossipPublishAt.set(groupId, this.now());
3512
4249
  await this.broadcastMessage("peer-group:gossip", {
3513
4250
  groupId,
3514
4251
  gossipId,
4252
+ publisherId: this.nodeId,
3515
4253
  hop: 0,
3516
4254
  maxHop,
3517
4255
  innerMessageType,
@@ -3539,12 +4277,16 @@ var require_dignity_p2p = __commonJS({
3539
4277
  const {
3540
4278
  groupId,
3541
4279
  gossipId,
4280
+ publisherId = decrypted.senderId,
3542
4281
  hop = 0,
3543
- maxHop = this.defaultGossipMaxHops,
4282
+ maxHop: payloadMaxHop,
3544
4283
  innerMessageType,
3545
4284
  innerPayload
3546
4285
  } = payload;
3547
- if (!groupId || !innerMessageType) {
4286
+ if (!groupId || !innerMessageType || !gossipId) {
4287
+ return;
4288
+ }
4289
+ if (!this.peerGroups.has(groupId)) {
3548
4290
  return;
3549
4291
  }
3550
4292
  if (this.hasSeenGossip(gossipId)) {
@@ -3553,9 +4295,12 @@ var require_dignity_p2p = __commonJS({
3553
4295
  this.markSeenGossip(gossipId);
3554
4296
  await this.dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, {
3555
4297
  groupId,
3556
- senderId: decrypted.senderId
4298
+ senderId: decrypted.senderId,
4299
+ publisherId
3557
4300
  });
3558
4301
  const group = this.peerGroups.get(groupId);
4302
+ const configuredMaxHop = group ? group.maxHops : this.defaultGossipMaxHops;
4303
+ const maxHop = typeof payloadMaxHop === "number" ? Math.min(payloadMaxHop, configuredMaxHop) : configuredMaxHop;
3559
4304
  if (!group || group.relayEnabled === false || hop >= maxHop) {
3560
4305
  return;
3561
4306
  }
@@ -3571,6 +4316,7 @@ var require_dignity_p2p = __commonJS({
3571
4316
  await this.broadcastMessage("peer-group:gossip", {
3572
4317
  groupId,
3573
4318
  gossipId,
4319
+ publisherId,
3574
4320
  hop: hop + 1,
3575
4321
  maxHop,
3576
4322
  innerMessageType,
@@ -3580,15 +4326,49 @@ var require_dignity_p2p = __commonJS({
3580
4326
  fanoutPeerIds: relayPeers
3581
4327
  });
3582
4328
  }
4329
+ normalizeGossipOperation(operation, publisherId) {
4330
+ if (!operation || !publisherId) {
4331
+ return null;
4332
+ }
4333
+ if (operation.actorId && operation.actorId !== publisherId) {
4334
+ this.emit("warning", {
4335
+ type: "gossip-operation-actor-mismatch",
4336
+ publisherId,
4337
+ actorId: operation.actorId,
4338
+ kind: operation.kind,
4339
+ collection: operation.collectionName,
4340
+ id: operation.id
4341
+ });
4342
+ return null;
4343
+ }
4344
+ const normalized = {
4345
+ ...operation,
4346
+ actorId: publisherId
4347
+ };
4348
+ if (normalized.kind === "create") {
4349
+ normalized.ownerId = publisherId;
4350
+ }
4351
+ return normalized;
4352
+ }
3583
4353
  async dispatchPeerGroupInnerMessage(innerMessageType, innerPayload, context = {}) {
3584
4354
  if (innerMessageType === "operation") {
3585
- this.applyOperation(innerPayload);
4355
+ const operation = this.normalizeGossipOperation(
4356
+ innerPayload,
4357
+ context.publisherId || context.senderId
4358
+ );
4359
+ if (operation) {
4360
+ this.applyOperation(operation);
4361
+ }
3586
4362
  return;
3587
4363
  }
3588
4364
  if (innerMessageType === "record:snapshot") {
3589
4365
  const { collectionName, record } = innerPayload || {};
3590
4366
  if (collectionName && record) {
3591
- const applied = this.restoreRecord(collectionName, record);
4367
+ const applied = this.restoreRecord(collectionName, record, {
4368
+ rejectOnHashMismatch: true,
4369
+ rejectOnOwnershipMismatch: true,
4370
+ via: "peer-group"
4371
+ });
3592
4372
  if (applied) {
3593
4373
  this.emit("change", {
3594
4374
  kind: "snapshot",
@@ -3738,6 +4518,13 @@ var require_dignity_p2p = __commonJS({
3738
4518
  }
3739
4519
  async handleIncomingMessage(message) {
3740
4520
  if (message && message.opId && message.kind) {
4521
+ if (this.securityService.options.enabled) {
4522
+ this.emit("messageignored", {
4523
+ reason: "raw-operation-rejected",
4524
+ hint: "Unsigned raw operations are disabled when security is enabled"
4525
+ });
4526
+ return;
4527
+ }
3741
4528
  this.applyOperation(message);
3742
4529
  return;
3743
4530
  }
@@ -3748,9 +4535,6 @@ var require_dignity_p2p = __commonJS({
3748
4535
  });
3749
4536
  return;
3750
4537
  }
3751
- if (message && message.senderId && message.senderPublicKey) {
3752
- this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3753
- }
3754
4538
  let decrypted;
3755
4539
  try {
3756
4540
  decrypted = await this.securityService.decryptIncomingMessage(message);
@@ -3768,6 +4552,38 @@ var require_dignity_p2p = __commonJS({
3768
4552
  if (!decrypted || decrypted.ignored) {
3769
4553
  return;
3770
4554
  }
4555
+ if (decrypted.messageType === "identity:rotate") {
4556
+ const peerId = decrypted.senderId || decrypted.payload?.username;
4557
+ if (peerId && decrypted.payload) {
4558
+ const result = this.applyPeerIdentityRotation(peerId, decrypted.payload);
4559
+ if (!result.applied) {
4560
+ this.emit("warning", {
4561
+ type: "identity-rotation-ignored",
4562
+ peerId,
4563
+ reason: result.reason
4564
+ });
4565
+ }
4566
+ }
4567
+ return;
4568
+ }
4569
+ if (decrypted.messageType === "identity:cold-enroll") {
4570
+ const peerId = decrypted.senderId || decrypted.payload?.username;
4571
+ if (peerId && decrypted.payload) {
4572
+ try {
4573
+ this.applyPeerColdRecoveryEnrollment(peerId, decrypted.payload);
4574
+ } catch (error) {
4575
+ this.emit("warning", {
4576
+ type: "cold-recovery-enrollment-rejected",
4577
+ peerId,
4578
+ error
4579
+ });
4580
+ }
4581
+ }
4582
+ return;
4583
+ }
4584
+ if (message && message.senderId && message.senderPublicKey) {
4585
+ this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
4586
+ }
3771
4587
  if (decrypted.messageType === "operation") {
3772
4588
  this.applyOperation(decrypted.payload);
3773
4589
  return;
@@ -3776,7 +4592,10 @@ var require_dignity_p2p = __commonJS({
3776
4592
  const payload = decrypted.payload || {};
3777
4593
  const { collectionName, record } = payload;
3778
4594
  if (collectionName && record) {
3779
- const applied = this.restoreRecord(collectionName, record);
4595
+ const applied = this.restoreRecord(collectionName, record, {
4596
+ rejectOnHashMismatch: true,
4597
+ via: "direct-mesh"
4598
+ });
3780
4599
  if (applied) {
3781
4600
  this.emit("change", {
3782
4601
  kind: "snapshot",
@@ -3791,18 +4610,27 @@ var require_dignity_p2p = __commonJS({
3791
4610
  const payload = decrypted.payload || {};
3792
4611
  const scope = payload.scope || "main";
3793
4612
  const peerId = payload.peerId || decrypted.senderId;
3794
- if (!peerId) {
4613
+ if (!peerId || peerId !== decrypted.senderId) {
4614
+ return;
4615
+ }
4616
+ if (!this.discoveryRooms.has(scope)) {
3795
4617
  return;
3796
4618
  }
4619
+ const room = this.discoveryRooms.get(scope);
3797
4620
  const presenceMap = this.getPresenceMap(scope);
3798
4621
  const isNewPeerInScope = !presenceMap.has(peerId);
4622
+ const requestedTtl = typeof payload.ttlMs === "number" ? payload.ttlMs : room.ttlMs;
4623
+ const ttlMs = Math.min(requestedTtl, room.ttlMs);
3799
4624
  this.upsertPresence(
3800
4625
  scope,
3801
4626
  peerId,
3802
4627
  payload.metadata || {},
3803
- payload.ttlMs || this.defaultPresenceTtlMs,
3804
- payload.announcedAt || this.now()
4628
+ ttlMs,
4629
+ this.now()
3805
4630
  );
4631
+ if (payload.metadata && payload.metadata.publicKey) {
4632
+ this.trustPeerPublicKey(peerId, payload.metadata.publicKey);
4633
+ }
3806
4634
  if (isNewPeerInScope && peerId !== this.nodeId && this.discoveryRooms.has(scope)) {
3807
4635
  if (typeof this.networkAdapter.connectToPeer === "function") {
3808
4636
  Promise.resolve(this.connectToPeer(peerId)).catch((error) => {
@@ -3819,6 +4647,9 @@ var require_dignity_p2p = __commonJS({
3819
4647
  const payload = decrypted.payload || {};
3820
4648
  const scope = payload.scope || "main";
3821
4649
  const peerId = payload.peerId || decrypted.senderId;
4650
+ if (!peerId || peerId !== decrypted.senderId) {
4651
+ return;
4652
+ }
3822
4653
  const map = this.presenceByScope.get(scope);
3823
4654
  if (map && peerId && map.has(peerId)) {
3824
4655
  map.delete(peerId);
@@ -3875,7 +4706,7 @@ var require_dignity_p2p = __commonJS({
3875
4706
  emitConflict(details) {
3876
4707
  this.emit("conflict", details);
3877
4708
  }
3878
- restoreRecord(collectionName, record) {
4709
+ restoreRecord(collectionName, record, options = {}) {
3879
4710
  if (!record || !record.id) {
3880
4711
  return false;
3881
4712
  }
@@ -3886,14 +4717,42 @@ var require_dignity_p2p = __commonJS({
3886
4717
  }
3887
4718
  const restoredData = { ...record.data || {} };
3888
4719
  const computedHash = computeContentHash(restoredData);
3889
- if (record.hash && record.hash !== computedHash) {
4720
+ const rejectOnHashMismatch = options.rejectOnHashMismatch === true;
4721
+ const rejectOnOwnershipMismatch = options.rejectOnOwnershipMismatch === true;
4722
+ if (rejectOnOwnershipMismatch && current && record.ownerId && current.ownerId !== record.ownerId) {
4723
+ this.emit("warning", {
4724
+ type: "ownership-mismatch",
4725
+ collection: collectionName,
4726
+ id: record.id,
4727
+ currentOwnerId: current.ownerId,
4728
+ advertisedOwnerId: record.ownerId,
4729
+ via: options.via || null
4730
+ });
4731
+ return false;
4732
+ }
4733
+ if (!record.hash) {
4734
+ const warning = {
4735
+ type: "content-hash-missing",
4736
+ collection: collectionName,
4737
+ id: record.id,
4738
+ via: options.via || null
4739
+ };
4740
+ this.emit("warning", warning);
4741
+ if (rejectOnHashMismatch) {
4742
+ return false;
4743
+ }
4744
+ } else if (record.hash !== computedHash) {
3890
4745
  this.emit("warning", {
3891
4746
  type: "content-hash-mismatch",
3892
4747
  collection: collectionName,
3893
4748
  id: record.id,
3894
4749
  advertisedHash: record.hash,
3895
- computedHash
4750
+ computedHash,
4751
+ via: options.via || null
3896
4752
  });
4753
+ if (rejectOnHashMismatch) {
4754
+ return false;
4755
+ }
3897
4756
  }
3898
4757
  collection.set(record.id, {
3899
4758
  id: record.id,
@@ -3935,6 +4794,15 @@ var require_dignity_p2p = __commonJS({
3935
4794
  });
3936
4795
  return record;
3937
4796
  }
4797
+ pruneAppliedOperations() {
4798
+ while (this.appliedOperations.size > this.maxAppliedOperations) {
4799
+ const oldestOpId = this.appliedOperations.keys().next().value;
4800
+ if (!oldestOpId) {
4801
+ break;
4802
+ }
4803
+ this.appliedOperations.delete(oldestOpId);
4804
+ }
4805
+ }
3938
4806
  applyOperation(operation) {
3939
4807
  if (!operation || !operation.opId || this.appliedOperations.has(operation.opId)) {
3940
4808
  return false;
@@ -3956,7 +4824,8 @@ var require_dignity_p2p = __commonJS({
3956
4824
  deletedAt: null,
3957
4825
  version: 1
3958
4826
  });
3959
- this.appliedOperations.add(operation.opId);
4827
+ this.appliedOperations.set(operation.opId, this.now());
4828
+ this.pruneAppliedOperations();
3960
4829
  this.emit("change", { kind: "create", collection: operation.collectionName, id: operation.id });
3961
4830
  return true;
3962
4831
  }
@@ -4000,7 +4869,8 @@ var require_dignity_p2p = __commonJS({
4000
4869
  }
4001
4870
  current.updatedAt = operation.timestamp;
4002
4871
  current.version += 1;
4003
- this.appliedOperations.add(operation.opId);
4872
+ this.appliedOperations.set(operation.opId, this.now());
4873
+ this.pruneAppliedOperations();
4004
4874
  this.emit("change", {
4005
4875
  kind: "transfer-ownership",
4006
4876
  collection: operation.collectionName,
@@ -4029,7 +4899,8 @@ var require_dignity_p2p = __commonJS({
4029
4899
  current.deletedAt = operation.timestamp;
4030
4900
  current.updatedAt = operation.timestamp;
4031
4901
  current.version += 1;
4032
- this.appliedOperations.add(operation.opId);
4902
+ this.appliedOperations.set(operation.opId, this.now());
4903
+ this.pruneAppliedOperations();
4033
4904
  this.emit("change", { kind: "delete", collection: operation.collectionName, id: operation.id });
4034
4905
  return true;
4035
4906
  }
@@ -4059,7 +4930,8 @@ var require_dignity_p2p = __commonJS({
4059
4930
  }
4060
4931
  current.updatedAt = operation.timestamp;
4061
4932
  current.version += 1;
4062
- this.appliedOperations.add(operation.opId);
4933
+ this.appliedOperations.set(operation.opId, this.now());
4934
+ this.pruneAppliedOperations();
4063
4935
  this.emit("change", { kind: "update", collection: operation.collectionName, id: operation.id });
4064
4936
  return true;
4065
4937
  }
@@ -11595,12 +12467,25 @@ var require_index = __commonJS({
11595
12467
  var SlothPermutation = require_sloth_vdf();
11596
12468
  var {
11597
12469
  MessageSecurityService,
11598
- DEFAULT_SECURITY_OPTIONS
12470
+ DEFAULT_SECURITY_OPTIONS,
12471
+ DEFAULT_APP_PASSWORD
11599
12472
  } = require_message_security_service();
12473
+ var { deriveKeyPairFromCredentials, keyPairToPublicBundle, deriveColdRecoverySigningKey } = require_derive_key_pair();
12474
+ var {
12475
+ createIdentityRotation,
12476
+ verifyIdentityRotation,
12477
+ revokeAndRotateIdentity,
12478
+ rotateIdentityPassword,
12479
+ enrollColdRecoveryPassword,
12480
+ verifyColdRecoveryEnrollment,
12481
+ shouldApplyIdentityRotation
12482
+ } = require_identity_rotation();
12483
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
11600
12484
  var {
11601
12485
  PEER_GROUP_SCOPE_PREFIX,
11602
12486
  DEFAULT_PEER_GROUP_OPTIONS,
11603
12487
  peerGroupScope,
12488
+ parsePeerGroupScope,
11604
12489
  selectFanoutPeers
11605
12490
  } = require_peer_group();
11606
12491
  module.exports = {
@@ -11620,9 +12505,22 @@ var require_index = __commonJS({
11620
12505
  SlothPermutation,
11621
12506
  MessageSecurityService,
11622
12507
  DEFAULT_SECURITY_OPTIONS,
12508
+ DEFAULT_APP_PASSWORD,
12509
+ deriveKeyPairFromCredentials,
12510
+ deriveColdRecoverySigningKey,
12511
+ keyPairToPublicBundle,
12512
+ createIdentityRotation,
12513
+ verifyIdentityRotation,
12514
+ revokeAndRotateIdentity,
12515
+ rotateIdentityPassword,
12516
+ enrollColdRecoveryPassword,
12517
+ verifyColdRecoveryEnrollment,
12518
+ shouldApplyIdentityRotation,
12519
+ parsePeerJsServerUrl,
11623
12520
  PEER_GROUP_SCOPE_PREFIX,
11624
12521
  DEFAULT_PEER_GROUP_OPTIONS,
11625
12522
  peerGroupScope,
12523
+ parsePeerGroupScope,
11626
12524
  selectFanoutPeers
11627
12525
  };
11628
12526
  }