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.
@@ -2424,19 +2424,511 @@ var require_vdf = __commonJS({
2424
2424
  }
2425
2425
  });
2426
2426
 
2427
+ // src/security/derive-key-pair.js
2428
+ var require_derive_key_pair = __commonJS({
2429
+ "src/security/derive-key-pair.js"(exports2, module2) {
2430
+ var nacl = require_nacl_fast();
2431
+ var naclUtil = require_nacl_util();
2432
+ var { deriveBroadcastKey, DEFAULT_SECURITY_OPTIONS: DEFAULT_SECURITY_OPTIONS2 } = require_message_security_service();
2433
+ var SIGNING_INFO = "dignity-signing-v1";
2434
+ var ENCRYPTION_INFO = "dignity-encryption-v1";
2435
+ var COLD_RECOVERY_INFO = "dignity-cold-recovery-v1";
2436
+ function utf8ToBytes(value) {
2437
+ return naclUtil.decodeUTF8(value);
2438
+ }
2439
+ function concatBytes(...parts) {
2440
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
2441
+ const result = new Uint8Array(total);
2442
+ let offset = 0;
2443
+ for (const part of parts) {
2444
+ result.set(part, offset);
2445
+ offset += part.length;
2446
+ }
2447
+ return result;
2448
+ }
2449
+ function buildColdRecoverySalt(username, pepper = "") {
2450
+ if (!username || typeof username !== "string") {
2451
+ throw new Error("deriveColdRecoverySigningKey requires username");
2452
+ }
2453
+ const segments = ["dignity-cold-recovery-v1"];
2454
+ if (pepper) {
2455
+ segments.push(pepper);
2456
+ }
2457
+ segments.push(username, COLD_RECOVERY_INFO);
2458
+ return utf8ToBytes(segments.join("\0"));
2459
+ }
2460
+ async function deriveColdRecoverySigningKey2({
2461
+ username,
2462
+ coldPassword,
2463
+ pepper = "",
2464
+ kdfIterations
2465
+ } = {}) {
2466
+ if (!coldPassword || typeof coldPassword !== "string") {
2467
+ throw new Error("deriveColdRecoverySigningKey requires coldPassword");
2468
+ }
2469
+ const salt = buildColdRecoverySalt(username, pepper);
2470
+ const iterations = typeof kdfIterations === "number" ? kdfIterations : DEFAULT_SECURITY_OPTIONS2.kdfIterations;
2471
+ const seed = await deriveBroadcastKey(coldPassword, salt, iterations);
2472
+ const signing = nacl.sign.keyPair.fromSeed(seed);
2473
+ return {
2474
+ signing,
2475
+ recoveryPublicKey: naclUtil.encodeBase64(signing.publicKey)
2476
+ };
2477
+ }
2478
+ function buildIdentitySalt(username, info, pepper = "", generation = 1) {
2479
+ if (!username || typeof username !== "string") {
2480
+ throw new Error("deriveKeyPairFromCredentials requires username");
2481
+ }
2482
+ if (!info || typeof info !== "string") {
2483
+ throw new Error("deriveKeyPairFromCredentials requires info label");
2484
+ }
2485
+ const normalizedGeneration = Number(generation);
2486
+ if (!Number.isInteger(normalizedGeneration) || normalizedGeneration < 1) {
2487
+ throw new Error("deriveKeyPairFromCredentials requires generation >= 1");
2488
+ }
2489
+ const segments = ["dignity-identity-v1"];
2490
+ if (pepper) {
2491
+ segments.push(pepper);
2492
+ }
2493
+ segments.push(username, `gen:${normalizedGeneration}`, info);
2494
+ return utf8ToBytes(segments.join("\0"));
2495
+ }
2496
+ async function deriveIdentitySeed({ password, username, info, pepper, generation, kdfIterations }) {
2497
+ if (!password || typeof password !== "string") {
2498
+ throw new Error("deriveKeyPairFromCredentials requires password");
2499
+ }
2500
+ const salt = buildIdentitySalt(username, info, pepper, generation);
2501
+ const iterations = typeof kdfIterations === "number" ? kdfIterations : DEFAULT_SECURITY_OPTIONS2.kdfIterations;
2502
+ return deriveBroadcastKey(password, salt, iterations);
2503
+ }
2504
+ async function deriveKeyPairFromCredentials2({
2505
+ username,
2506
+ password,
2507
+ pepper = "",
2508
+ generation = 1,
2509
+ kdfIterations
2510
+ } = {}) {
2511
+ const signingSeed = await deriveIdentitySeed({
2512
+ password,
2513
+ username,
2514
+ info: SIGNING_INFO,
2515
+ pepper,
2516
+ generation,
2517
+ kdfIterations
2518
+ });
2519
+ const encryptionSecret = await deriveIdentitySeed({
2520
+ password,
2521
+ username,
2522
+ info: ENCRYPTION_INFO,
2523
+ pepper,
2524
+ generation,
2525
+ kdfIterations
2526
+ });
2527
+ return {
2528
+ signing: nacl.sign.keyPair.fromSeed(signingSeed),
2529
+ encryption: nacl.box.keyPair.fromSecretKey(encryptionSecret),
2530
+ generation
2531
+ };
2532
+ }
2533
+ function keyPairToPublicBundle2(keyPair) {
2534
+ return {
2535
+ signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
2536
+ encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
2537
+ };
2538
+ }
2539
+ module2.exports = {
2540
+ deriveKeyPairFromCredentials: deriveKeyPairFromCredentials2,
2541
+ deriveColdRecoverySigningKey: deriveColdRecoverySigningKey2,
2542
+ keyPairToPublicBundle: keyPairToPublicBundle2,
2543
+ buildIdentitySalt,
2544
+ buildColdRecoverySalt,
2545
+ SIGNING_INFO,
2546
+ ENCRYPTION_INFO,
2547
+ COLD_RECOVERY_INFO,
2548
+ concatBytes
2549
+ };
2550
+ }
2551
+ });
2552
+
2553
+ // src/security/identity-rotation.js
2554
+ var require_identity_rotation = __commonJS({
2555
+ "src/security/identity-rotation.js"(exports2, module2) {
2556
+ var nacl = require_nacl_fast();
2557
+ var naclUtil = require_nacl_util();
2558
+ var { stableStringify } = require_message_security_service();
2559
+ var {
2560
+ deriveKeyPairFromCredentials: deriveKeyPairFromCredentials2,
2561
+ deriveColdRecoverySigningKey: deriveColdRecoverySigningKey2,
2562
+ keyPairToPublicBundle: keyPairToPublicBundle2
2563
+ } = require_derive_key_pair();
2564
+ var ROTATION_TYPES = /* @__PURE__ */ new Set(["compromise-recovery", "password-change"]);
2565
+ function utf8ToBytes(value) {
2566
+ return naclUtil.decodeUTF8(value);
2567
+ }
2568
+ function normalizePublicKeyBundle(publicKey) {
2569
+ if (!publicKey || !publicKey.signingPublicKey || !publicKey.encryptionPublicKey) {
2570
+ throw new Error("Public key bundle requires signingPublicKey and encryptionPublicKey");
2571
+ }
2572
+ return {
2573
+ signingPublicKey: publicKey.signingPublicKey,
2574
+ encryptionPublicKey: publicKey.encryptionPublicKey
2575
+ };
2576
+ }
2577
+ function buildIdentityRotationPayload({
2578
+ username,
2579
+ fromGeneration,
2580
+ toGeneration,
2581
+ previousPublicKey,
2582
+ nextPublicKey,
2583
+ rotationKind,
2584
+ reason,
2585
+ timestamp
2586
+ }) {
2587
+ if (!username) {
2588
+ throw new Error("Identity rotation requires username");
2589
+ }
2590
+ if (!ROTATION_TYPES.has(rotationKind)) {
2591
+ throw new Error(`Identity rotation kind must be one of: ${[...ROTATION_TYPES].join(", ")}`);
2592
+ }
2593
+ if (toGeneration !== fromGeneration + 1) {
2594
+ throw new Error("Identity rotation must advance generation by exactly 1");
2595
+ }
2596
+ return {
2597
+ version: 1,
2598
+ type: "identity:rotate",
2599
+ username,
2600
+ fromGeneration,
2601
+ toGeneration,
2602
+ previousPublicKey: normalizePublicKeyBundle(previousPublicKey),
2603
+ nextPublicKey: normalizePublicKeyBundle(nextPublicKey),
2604
+ rotationKind,
2605
+ reason: reason || "",
2606
+ timestamp: typeof timestamp === "number" ? timestamp : Date.now()
2607
+ };
2608
+ }
2609
+ function signIdentityRotationPayload(payload, signingSecretKey) {
2610
+ const message = utf8ToBytes(stableStringify(payload));
2611
+ const signature = nacl.sign.detached(message, signingSecretKey);
2612
+ return naclUtil.encodeBase64(signature);
2613
+ }
2614
+ function verifyDetachedSignature(payload, signatureBase64, signingPublicKeyBase64) {
2615
+ const message = utf8ToBytes(stableStringify(payload));
2616
+ const signatureBytes = naclUtil.decodeBase64(signatureBase64);
2617
+ const signingPublicKey = naclUtil.decodeBase64(signingPublicKeyBase64);
2618
+ return nacl.sign.detached.verify(message, signatureBytes, signingPublicKey);
2619
+ }
2620
+ function createIdentityRotation2({
2621
+ username,
2622
+ fromGeneration,
2623
+ toGeneration,
2624
+ previousPublicKey,
2625
+ nextKeyPair,
2626
+ rotationKind,
2627
+ reason,
2628
+ timestamp,
2629
+ coldRecoverySigningSecretKey
2630
+ }) {
2631
+ if (!nextKeyPair || !nextKeyPair.signing || !nextKeyPair.signing.secretKey) {
2632
+ throw new Error("Identity rotation requires nextKeyPair with signing secret");
2633
+ }
2634
+ const payload = buildIdentityRotationPayload({
2635
+ username,
2636
+ fromGeneration,
2637
+ toGeneration,
2638
+ previousPublicKey,
2639
+ nextPublicKey: keyPairToPublicBundle2(nextKeyPair),
2640
+ rotationKind,
2641
+ reason,
2642
+ timestamp
2643
+ });
2644
+ const rotation = {
2645
+ ...payload,
2646
+ signature: signIdentityRotationPayload(payload, nextKeyPair.signing.secretKey)
2647
+ };
2648
+ if (coldRecoverySigningSecretKey) {
2649
+ rotation.recoverySignature = signIdentityRotationPayload(
2650
+ payload,
2651
+ coldRecoverySigningSecretKey
2652
+ );
2653
+ }
2654
+ return rotation;
2655
+ }
2656
+ function buildColdRecoveryEnrollmentPayload({ username, recoveryPublicKey, timestamp }) {
2657
+ if (!username) {
2658
+ throw new Error("Cold recovery enrollment requires username");
2659
+ }
2660
+ if (!recoveryPublicKey) {
2661
+ throw new Error("Cold recovery enrollment requires recoveryPublicKey");
2662
+ }
2663
+ return {
2664
+ version: 1,
2665
+ type: "identity:cold-enroll",
2666
+ username,
2667
+ recoveryPublicKey,
2668
+ timestamp: typeof timestamp === "number" ? timestamp : Date.now()
2669
+ };
2670
+ }
2671
+ function createColdRecoveryEnrollment({
2672
+ username,
2673
+ coldRecoverySigningSecretKey,
2674
+ recoveryPublicKey,
2675
+ timestamp
2676
+ }) {
2677
+ if (!coldRecoverySigningSecretKey) {
2678
+ throw new Error("Cold recovery enrollment requires cold recovery signing secret");
2679
+ }
2680
+ if (!recoveryPublicKey) {
2681
+ throw new Error("Cold recovery enrollment requires recoveryPublicKey");
2682
+ }
2683
+ const payload = buildColdRecoveryEnrollmentPayload({
2684
+ username,
2685
+ recoveryPublicKey,
2686
+ timestamp
2687
+ });
2688
+ return {
2689
+ ...payload,
2690
+ signature: signIdentityRotationPayload(payload, coldRecoverySigningSecretKey)
2691
+ };
2692
+ }
2693
+ function verifyColdRecoveryEnrollment2(enrollment) {
2694
+ if (!enrollment || enrollment.type !== "identity:cold-enroll" || enrollment.version !== 1) {
2695
+ return { ok: false, error: "invalid-enrollment-shape" };
2696
+ }
2697
+ if (!enrollment.signature || !enrollment.recoveryPublicKey) {
2698
+ return { ok: false, error: "missing-enrollment-fields" };
2699
+ }
2700
+ const { signature, ...payload } = enrollment;
2701
+ const verified = verifyDetachedSignature(payload, signature, enrollment.recoveryPublicKey);
2702
+ if (!verified) {
2703
+ return { ok: false, error: "invalid-enrollment-signature" };
2704
+ }
2705
+ return { ok: true, enrollment };
2706
+ }
2707
+ function verifyIdentityRotation2(rotation, options = {}) {
2708
+ if (!rotation || rotation.type !== "identity:rotate" || rotation.version !== 1) {
2709
+ return { ok: false, error: "invalid-rotation-shape" };
2710
+ }
2711
+ if (!rotation.signature || !rotation.nextPublicKey || !rotation.previousPublicKey) {
2712
+ return { ok: false, error: "missing-rotation-fields" };
2713
+ }
2714
+ if (rotation.toGeneration !== rotation.fromGeneration + 1) {
2715
+ return { ok: false, error: "invalid-generation-step" };
2716
+ }
2717
+ if (!ROTATION_TYPES.has(rotation.rotationKind)) {
2718
+ return { ok: false, error: "invalid-rotation-kind" };
2719
+ }
2720
+ const { signature, recoverySignature, ...payload } = rotation;
2721
+ const verified = verifyDetachedSignature(payload, signature, rotation.nextPublicKey.signingPublicKey);
2722
+ if (!verified) {
2723
+ return { ok: false, error: "invalid-signature" };
2724
+ }
2725
+ const requiredRecoveryPublicKey = options.requiredRecoveryPublicKey || null;
2726
+ if (requiredRecoveryPublicKey) {
2727
+ if (!recoverySignature) {
2728
+ return { ok: false, error: "missing-recovery-signature" };
2729
+ }
2730
+ const recoveryVerified = verifyDetachedSignature(
2731
+ payload,
2732
+ recoverySignature,
2733
+ requiredRecoveryPublicKey
2734
+ );
2735
+ if (!recoveryVerified) {
2736
+ return { ok: false, error: "invalid-recovery-signature" };
2737
+ }
2738
+ }
2739
+ if (rotation.previousPublicKey.signingPublicKey === rotation.nextPublicKey.signingPublicKey) {
2740
+ return { ok: false, error: "unchanged-signing-key" };
2741
+ }
2742
+ return { ok: true, rotation };
2743
+ }
2744
+ async function resolveColdRecoverySigningSecretKey({
2745
+ username,
2746
+ coldPassword,
2747
+ pepper,
2748
+ kdfIterations
2749
+ }) {
2750
+ if (!coldPassword) {
2751
+ return null;
2752
+ }
2753
+ const coldRecovery = await deriveColdRecoverySigningKey2({
2754
+ username,
2755
+ coldPassword,
2756
+ pepper,
2757
+ kdfIterations
2758
+ });
2759
+ return coldRecovery.signing.secretKey;
2760
+ }
2761
+ async function revokeAndRotateIdentity2({
2762
+ username,
2763
+ password,
2764
+ coldPassword,
2765
+ currentGeneration = 1,
2766
+ reason = "compromise-recovery",
2767
+ pepper = "",
2768
+ kdfIterations,
2769
+ timestamp
2770
+ } = {}) {
2771
+ const currentKeyPair = await deriveKeyPairFromCredentials2({
2772
+ username,
2773
+ password,
2774
+ generation: currentGeneration,
2775
+ pepper,
2776
+ kdfIterations
2777
+ });
2778
+ const nextGeneration = currentGeneration + 1;
2779
+ const nextKeyPair = await deriveKeyPairFromCredentials2({
2780
+ username,
2781
+ password,
2782
+ generation: nextGeneration,
2783
+ pepper,
2784
+ kdfIterations
2785
+ });
2786
+ const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
2787
+ username,
2788
+ coldPassword,
2789
+ pepper,
2790
+ kdfIterations
2791
+ });
2792
+ const rotation = createIdentityRotation2({
2793
+ username,
2794
+ fromGeneration: currentGeneration,
2795
+ toGeneration: nextGeneration,
2796
+ previousPublicKey: keyPairToPublicBundle2(currentKeyPair),
2797
+ nextKeyPair,
2798
+ rotationKind: "compromise-recovery",
2799
+ reason,
2800
+ timestamp,
2801
+ coldRecoverySigningSecretKey
2802
+ });
2803
+ return {
2804
+ rotation,
2805
+ currentKeyPair,
2806
+ nextKeyPair,
2807
+ nextGeneration,
2808
+ coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
2809
+ };
2810
+ }
2811
+ async function rotateIdentityPassword2({
2812
+ username,
2813
+ currentPassword,
2814
+ newPassword,
2815
+ coldPassword,
2816
+ currentGeneration = 1,
2817
+ reason = "password-change",
2818
+ pepper = "",
2819
+ kdfIterations,
2820
+ timestamp
2821
+ } = {}) {
2822
+ const currentKeyPair = await deriveKeyPairFromCredentials2({
2823
+ username,
2824
+ password: currentPassword,
2825
+ generation: currentGeneration,
2826
+ pepper,
2827
+ kdfIterations
2828
+ });
2829
+ const nextGeneration = currentGeneration + 1;
2830
+ const nextKeyPair = await deriveKeyPairFromCredentials2({
2831
+ username,
2832
+ password: newPassword,
2833
+ generation: nextGeneration,
2834
+ pepper,
2835
+ kdfIterations
2836
+ });
2837
+ const coldRecoverySigningSecretKey = await resolveColdRecoverySigningSecretKey({
2838
+ username,
2839
+ coldPassword,
2840
+ pepper,
2841
+ kdfIterations
2842
+ });
2843
+ const rotation = createIdentityRotation2({
2844
+ username,
2845
+ fromGeneration: currentGeneration,
2846
+ toGeneration: nextGeneration,
2847
+ previousPublicKey: keyPairToPublicBundle2(currentKeyPair),
2848
+ nextKeyPair,
2849
+ rotationKind: "password-change",
2850
+ reason,
2851
+ timestamp,
2852
+ coldRecoverySigningSecretKey
2853
+ });
2854
+ return {
2855
+ rotation,
2856
+ currentKeyPair,
2857
+ nextKeyPair,
2858
+ nextGeneration,
2859
+ coldRecoveryUsed: Boolean(coldRecoverySigningSecretKey)
2860
+ };
2861
+ }
2862
+ async function enrollColdRecoveryPassword2({
2863
+ username,
2864
+ coldPassword,
2865
+ pepper = "",
2866
+ kdfIterations,
2867
+ timestamp
2868
+ } = {}) {
2869
+ const coldRecovery = await deriveColdRecoverySigningKey2({
2870
+ username,
2871
+ coldPassword,
2872
+ pepper,
2873
+ kdfIterations
2874
+ });
2875
+ const enrollment = createColdRecoveryEnrollment({
2876
+ username,
2877
+ coldRecoverySigningSecretKey: coldRecovery.signing.secretKey,
2878
+ recoveryPublicKey: coldRecovery.recoveryPublicKey,
2879
+ timestamp
2880
+ });
2881
+ return {
2882
+ enrollment,
2883
+ recoveryPublicKey: coldRecovery.recoveryPublicKey,
2884
+ coldRecovery
2885
+ };
2886
+ }
2887
+ function shouldApplyIdentityRotation2(currentState, rotation, options = {}) {
2888
+ const requiredRecoveryPublicKey = options.enrolledRecoveryPublicKey || currentState && currentState.recoveryPublicKey || null;
2889
+ const verified = verifyIdentityRotation2(rotation, { requiredRecoveryPublicKey });
2890
+ if (!verified.ok) {
2891
+ return { apply: false, reason: verified.error };
2892
+ }
2893
+ if (currentState && rotation.toGeneration <= currentState.generation) {
2894
+ return { apply: false, reason: "stale-generation" };
2895
+ }
2896
+ if (currentState && currentState.publicKey && currentState.publicKey.signingPublicKey !== rotation.previousPublicKey.signingPublicKey) {
2897
+ return { apply: false, reason: "previous-key-mismatch" };
2898
+ }
2899
+ return { apply: true, rotation: verified.rotation };
2900
+ }
2901
+ module2.exports = {
2902
+ createIdentityRotation: createIdentityRotation2,
2903
+ createColdRecoveryEnrollment,
2904
+ verifyIdentityRotation: verifyIdentityRotation2,
2905
+ verifyColdRecoveryEnrollment: verifyColdRecoveryEnrollment2,
2906
+ revokeAndRotateIdentity: revokeAndRotateIdentity2,
2907
+ rotateIdentityPassword: rotateIdentityPassword2,
2908
+ enrollColdRecoveryPassword: enrollColdRecoveryPassword2,
2909
+ shouldApplyIdentityRotation: shouldApplyIdentityRotation2,
2910
+ keyPairToPublicBundle: keyPairToPublicBundle2,
2911
+ buildIdentityRotationPayload,
2912
+ buildColdRecoveryEnrollmentPayload,
2913
+ signIdentityRotationPayload
2914
+ };
2915
+ }
2916
+ });
2917
+
2427
2918
  // src/security/message-security-service.js
2428
2919
  var require_message_security_service = __commonJS({
2429
2920
  "src/security/message-security-service.js"(exports2, module2) {
2430
2921
  var nacl = require_nacl_fast();
2431
2922
  var naclUtil = require_nacl_util();
2432
2923
  var VDF2 = require_vdf();
2924
+ var DEFAULT_APP_PASSWORD2 = "change-this-app-password";
2433
2925
  var DEFAULT_SECURITY_OPTIONS2 = {
2434
2926
  enabled: true,
2435
2927
  signingEnabled: true,
2436
2928
  encryptionEnabled: true,
2437
2929
  powEnabled: true,
2438
2930
  powTargetMs: 1e3,
2439
- appPassword: "change-this-app-password",
2931
+ appPassword: DEFAULT_APP_PASSWORD2,
2440
2932
  broadcastPasswords: {},
2441
2933
  resolveBroadcastPassword: null,
2442
2934
  powSteps: 22,
@@ -2531,16 +3023,85 @@ var require_message_security_service = __commonJS({
2531
3023
  encryptionPublicKey: naclUtil.encodeBase64(this.encryptionPublicKey)
2532
3024
  };
2533
3025
  this.peerPublicKeys = /* @__PURE__ */ new Map();
3026
+ this.peerIdentityGenerations = /* @__PURE__ */ new Map();
3027
+ this.peerRecoveryPublicKeys = /* @__PURE__ */ new Map();
2534
3028
  for (const [peerId, peerKey] of Object.entries(this.options.trustedPeerKeys || {})) {
2535
3029
  this.peerPublicKeys.set(peerId, normalizePeerPublicKey(peerKey));
3030
+ this.peerIdentityGenerations.set(peerId, 1);
2536
3031
  }
2537
3032
  this.calibratedPowSteps = this.options.powSteps;
2538
3033
  }
2539
3034
  getPublicKey() {
2540
3035
  return { ...this.publicKeyBundle };
2541
3036
  }
2542
- registerPeerPublicKey(peerId, publicKey) {
2543
- this.peerPublicKeys.set(peerId, normalizePeerPublicKey(publicKey));
3037
+ registerPeerPublicKey(peerId, publicKey, options = {}) {
3038
+ const normalized = normalizePeerPublicKey(publicKey);
3039
+ const generation = typeof options.generation === "number" ? options.generation : 1;
3040
+ const currentGeneration = this.peerIdentityGenerations.get(peerId) || 0;
3041
+ if (generation < currentGeneration && options.allowDowngrade !== true) {
3042
+ throw new Error(`Refusing older identity generation for peer ${peerId}`);
3043
+ }
3044
+ this.peerPublicKeys.set(peerId, normalized);
3045
+ this.peerIdentityGenerations.set(peerId, Math.max(currentGeneration, generation));
3046
+ }
3047
+ getPeerIdentityGeneration(peerId) {
3048
+ return this.peerIdentityGenerations.get(peerId) || 0;
3049
+ }
3050
+ getPeerIdentityState(peerId) {
3051
+ const publicKey = this.peerPublicKeys.get(peerId);
3052
+ const recoveryPublicKey = this.peerRecoveryPublicKeys.get(peerId) || null;
3053
+ if (!publicKey && !recoveryPublicKey) {
3054
+ return null;
3055
+ }
3056
+ return {
3057
+ publicKey: publicKey ? { ...publicKey } : null,
3058
+ generation: this.getPeerIdentityGeneration(peerId),
3059
+ recoveryPublicKey
3060
+ };
3061
+ }
3062
+ registerPeerRecoveryPublicKey(peerId, recoveryPublicKey) {
3063
+ if (!peerId || !recoveryPublicKey) {
3064
+ throw new Error("registerPeerRecoveryPublicKey requires peerId and recoveryPublicKey");
3065
+ }
3066
+ this.peerRecoveryPublicKeys.set(peerId, recoveryPublicKey);
3067
+ }
3068
+ getPeerRecoveryPublicKey(peerId) {
3069
+ return this.peerRecoveryPublicKeys.get(peerId) || null;
3070
+ }
3071
+ applyColdRecoveryEnrollment(peerId, enrollment) {
3072
+ const { verifyColdRecoveryEnrollment: verifyColdRecoveryEnrollment2 } = require_identity_rotation();
3073
+ const verified = verifyColdRecoveryEnrollment2(enrollment);
3074
+ if (!verified.ok) {
3075
+ const error = new Error(`Invalid cold recovery enrollment: ${verified.error}`);
3076
+ error.code = "INVALID_COLD_RECOVERY_ENROLLMENT";
3077
+ throw error;
3078
+ }
3079
+ this.registerPeerRecoveryPublicKey(peerId, enrollment.recoveryPublicKey);
3080
+ return { applied: true, recoveryPublicKey: enrollment.recoveryPublicKey };
3081
+ }
3082
+ applyIdentityRotation(peerId, rotation) {
3083
+ const { shouldApplyIdentityRotation: shouldApplyIdentityRotation2 } = require_identity_rotation();
3084
+ const currentState = this.getPeerIdentityState(peerId);
3085
+ const decision = shouldApplyIdentityRotation2(currentState, rotation, {
3086
+ enrolledRecoveryPublicKey: this.getPeerRecoveryPublicKey(peerId)
3087
+ });
3088
+ if (!decision.apply) {
3089
+ if (decision.reason && decision.reason !== "stale-generation" && decision.reason !== "previous-key-mismatch") {
3090
+ const error = new Error(`Invalid identity rotation: ${decision.reason}`);
3091
+ error.code = "INVALID_IDENTITY_ROTATION";
3092
+ throw error;
3093
+ }
3094
+ return { applied: false, reason: decision.reason };
3095
+ }
3096
+ this.registerPeerPublicKey(peerId, rotation.nextPublicKey, {
3097
+ generation: rotation.toGeneration
3098
+ });
3099
+ return {
3100
+ applied: true,
3101
+ fromGeneration: rotation.fromGeneration,
3102
+ toGeneration: rotation.toGeneration,
3103
+ rotationKind: rotation.rotationKind
3104
+ };
2544
3105
  }
2545
3106
  resolvePeerPublicKey(peerId, fallbackPublicKey) {
2546
3107
  const trusted = this.peerPublicKeys.get(peerId);
@@ -2855,7 +3416,8 @@ var require_message_security_service = __commonJS({
2855
3416
  stableStringify,
2856
3417
  deriveBroadcastKey,
2857
3418
  legacyBroadcastKey,
2858
- DEFAULT_SECURITY_OPTIONS: DEFAULT_SECURITY_OPTIONS2
3419
+ DEFAULT_SECURITY_OPTIONS: DEFAULT_SECURITY_OPTIONS2,
3420
+ DEFAULT_APP_PASSWORD: DEFAULT_APP_PASSWORD2
2859
3421
  };
2860
3422
  }
2861
3423
  });
@@ -2925,7 +3487,17 @@ var require_dignity_p2p = __commonJS({
2925
3487
  var nacl = require_nacl_fast();
2926
3488
  var naclUtil = require_nacl_util();
2927
3489
  var EventEmitter = require_event_emitter();
2928
- var { MessageSecurityService: MessageSecurityService2, stableStringify } = require_message_security_service();
3490
+ var {
3491
+ MessageSecurityService: MessageSecurityService2,
3492
+ stableStringify,
3493
+ DEFAULT_APP_PASSWORD: DEFAULT_APP_PASSWORD2
3494
+ } = require_message_security_service();
3495
+ var {
3496
+ revokeAndRotateIdentity: revokeAndRotateIdentity2,
3497
+ rotateIdentityPassword: rotateIdentityPassword2,
3498
+ enrollColdRecoveryPassword: enrollColdRecoveryPassword2
3499
+ } = require_identity_rotation();
3500
+ var { deriveKeyPairFromCredentials: deriveKeyPairFromCredentials2 } = require_derive_key_pair();
2929
3501
  var {
2930
3502
  DEFAULT_PEER_GROUP_OPTIONS: DEFAULT_PEER_GROUP_OPTIONS2,
2931
3503
  peerGroupScope: peerGroupScope2,
@@ -2970,6 +3542,9 @@ var require_dignity_p2p = __commonJS({
2970
3542
  this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS2.maxHops;
2971
3543
  this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2972
3544
  this.gossipIdTtlMs = security && typeof security.gossipIdTtlMs === "number" ? security.gossipIdTtlMs : 5 * 60 * 1e3;
3545
+ this.maxSeenGossipIds = security && typeof security.maxSeenGossipIds === "number" ? security.maxSeenGossipIds : 1e5;
3546
+ this.gossipPublishMinIntervalMs = security && typeof security.gossipPublishMinIntervalMs === "number" ? security.gossipPublishMinIntervalMs : 0;
3547
+ this.lastGossipPublishAt = /* @__PURE__ */ new Map();
2973
3548
  this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
2974
3549
  this.state = /* @__PURE__ */ new Map();
2975
3550
  this.appliedOperations = /* @__PURE__ */ new Map();
@@ -2978,6 +3553,13 @@ var require_dignity_p2p = __commonJS({
2978
3553
  async start() {
2979
3554
  this.networkAdapter.onMessage(this.boundMessageHandler);
2980
3555
  await this.networkAdapter.start(this.nodeId);
3556
+ const appPassword = this.securityService.options.appPassword;
3557
+ if (!appPassword || appPassword === DEFAULT_APP_PASSWORD2) {
3558
+ this.emit("warning", {
3559
+ type: "default-app-password",
3560
+ message: "Using the default appPassword is insecure; set a strong shared secret in production."
3561
+ });
3562
+ }
2981
3563
  }
2982
3564
  async stop() {
2983
3565
  const joinedGroups = Array.from(this.peerGroups.keys());
@@ -3273,8 +3855,138 @@ var require_dignity_p2p = __commonJS({
3273
3855
  connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3274
3856
  });
3275
3857
  }
3276
- registerPeerPublicKey(peerId, publicKey) {
3277
- this.securityService.registerPeerPublicKey(peerId, publicKey);
3858
+ registerPeerPublicKey(peerId, publicKey, options = {}) {
3859
+ this.securityService.registerPeerPublicKey(peerId, publicKey, options);
3860
+ }
3861
+ getPeerIdentityGeneration(peerId) {
3862
+ return this.securityService.getPeerIdentityGeneration(peerId);
3863
+ }
3864
+ getPeerIdentityState(peerId) {
3865
+ return this.securityService.getPeerIdentityState(peerId);
3866
+ }
3867
+ applyPeerIdentityRotation(peerId, rotation) {
3868
+ const result = this.securityService.applyIdentityRotation(peerId, rotation);
3869
+ if (result.applied) {
3870
+ this.emit("identityrotated", {
3871
+ peerId,
3872
+ username: rotation.username,
3873
+ fromGeneration: result.fromGeneration,
3874
+ toGeneration: result.toGeneration,
3875
+ rotationKind: result.rotationKind
3876
+ });
3877
+ }
3878
+ return result;
3879
+ }
3880
+ async broadcastIdentityRotation(rotation, options = {}) {
3881
+ return this.broadcastMessage("identity:rotate", rotation, options);
3882
+ }
3883
+ async broadcastColdRecoveryEnrollment(enrollment, options = {}) {
3884
+ return this.broadcastMessage("identity:cold-enroll", enrollment, options);
3885
+ }
3886
+ applyPeerColdRecoveryEnrollment(peerId, enrollment) {
3887
+ const result = this.securityService.applyColdRecoveryEnrollment(peerId, enrollment);
3888
+ if (result.applied) {
3889
+ this.emit("coldrecoveryenrolled", {
3890
+ peerId,
3891
+ username: enrollment.username,
3892
+ recoveryPublicKey: enrollment.recoveryPublicKey
3893
+ });
3894
+ }
3895
+ return result;
3896
+ }
3897
+ async enrollAndBroadcastColdRecovery({
3898
+ username,
3899
+ coldPassword,
3900
+ pepper = "",
3901
+ kdfIterations,
3902
+ broadcastOptions = {}
3903
+ } = {}) {
3904
+ const result = await enrollColdRecoveryPassword2({
3905
+ username,
3906
+ coldPassword,
3907
+ pepper,
3908
+ kdfIterations
3909
+ });
3910
+ await this.broadcastColdRecoveryEnrollment(result.enrollment, broadcastOptions);
3911
+ return result;
3912
+ }
3913
+ async revokeAndRotateDerivedIdentity({
3914
+ username,
3915
+ password,
3916
+ coldPassword,
3917
+ currentGeneration = 1,
3918
+ reason = "compromise-recovery",
3919
+ pepper = "",
3920
+ kdfIterations,
3921
+ broadcast = false,
3922
+ broadcastOptions = {}
3923
+ } = {}) {
3924
+ const result = await revokeAndRotateIdentity2({
3925
+ username,
3926
+ password,
3927
+ coldPassword,
3928
+ currentGeneration,
3929
+ reason,
3930
+ pepper,
3931
+ kdfIterations
3932
+ });
3933
+ if (broadcast) {
3934
+ await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
3935
+ }
3936
+ return result;
3937
+ }
3938
+ async rotateDerivedIdentityPassword({
3939
+ username,
3940
+ currentPassword,
3941
+ newPassword,
3942
+ coldPassword,
3943
+ currentGeneration = 1,
3944
+ reason = "password-change",
3945
+ pepper = "",
3946
+ kdfIterations,
3947
+ broadcast = false,
3948
+ broadcastOptions = {}
3949
+ } = {}) {
3950
+ const result = await rotateIdentityPassword2({
3951
+ username,
3952
+ currentPassword,
3953
+ newPassword,
3954
+ coldPassword,
3955
+ currentGeneration,
3956
+ reason,
3957
+ pepper,
3958
+ kdfIterations
3959
+ });
3960
+ if (broadcast) {
3961
+ await this.broadcastIdentityRotation(result.rotation, broadcastOptions);
3962
+ }
3963
+ return result;
3964
+ }
3965
+ async adoptDerivedIdentityKeyPair(keyPair, { generation = 1 } = {}) {
3966
+ if (!keyPair || !keyPair.signing || !keyPair.encryption) {
3967
+ throw new Error("adoptDerivedIdentityKeyPair requires a derived keyPair");
3968
+ }
3969
+ this.securityService.signingSecretKey = keyPair.signing.secretKey;
3970
+ this.securityService.signingPublicKey = keyPair.signing.publicKey;
3971
+ this.securityService.encryptionSecretKey = keyPair.encryption.secretKey;
3972
+ this.securityService.encryptionPublicKey = keyPair.encryption.publicKey;
3973
+ this.securityService.publicKeyBundle = {
3974
+ signingPublicKey: naclUtil.encodeBase64(keyPair.signing.publicKey),
3975
+ encryptionPublicKey: naclUtil.encodeBase64(keyPair.encryption.publicKey)
3976
+ };
3977
+ this.securityService.options.keyPair = keyPair;
3978
+ this.securityService.options.identityGeneration = generation;
3979
+ }
3980
+ async deriveAndAdoptIdentity({ username, password, generation = 1, pepper = "", kdfIterations } = {}) {
3981
+ const keyPair = await deriveKeyPairFromCredentials2({
3982
+ username,
3983
+ password,
3984
+ generation,
3985
+ pepper,
3986
+ kdfIterations
3987
+ });
3988
+ await this.adoptDerivedIdentityKeyPair(keyPair, { generation });
3989
+ return keyPair;
3278
3990
  }
3279
3991
  trustPeerPublicKey(peerId, publicKey) {
3280
3992
  if (!peerId || !publicKey) {
@@ -3396,6 +4108,13 @@ var require_dignity_p2p = __commonJS({
3396
4108
  this.seenGossipIds.delete(gossipId);
3397
4109
  }
3398
4110
  }
4111
+ while (this.seenGossipIds.size > this.maxSeenGossipIds) {
4112
+ const oldestGossipId = this.seenGossipIds.keys().next().value;
4113
+ if (!oldestGossipId) {
4114
+ break;
4115
+ }
4116
+ this.seenGossipIds.delete(oldestGossipId);
4117
+ }
3399
4118
  }
3400
4119
  hasSeenGossip(gossipId) {
3401
4120
  if (!gossipId) {
@@ -3409,6 +4128,7 @@ var require_dignity_p2p = __commonJS({
3409
4128
  return;
3410
4129
  }
3411
4130
  this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
4131
+ this.pruneSeenGossip();
3412
4132
  }
3413
4133
  listConnectedPeerIds() {
3414
4134
  if (typeof this.networkAdapter.listOpenPeerIds === "function") {
@@ -3486,6 +4206,15 @@ var require_dignity_p2p = __commonJS({
3486
4206
  if (!group && options.allowUnjoined !== true) {
3487
4207
  throw new Error(`PeerGroup ${groupId} has not been joined`);
3488
4208
  }
4209
+ if (this.gossipPublishMinIntervalMs > 0) {
4210
+ const lastPublishAt = this.lastGossipPublishAt.get(groupId) || 0;
4211
+ const elapsed = this.now() - lastPublishAt;
4212
+ if (elapsed < this.gossipPublishMinIntervalMs) {
4213
+ const error = new Error(`Gossip publish rate limit exceeded for group ${groupId}`);
4214
+ error.code = "GOSSIP_RATE_LIMIT";
4215
+ throw error;
4216
+ }
4217
+ }
3489
4218
  const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3490
4219
  const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3491
4220
  const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
@@ -3496,6 +4225,7 @@ var require_dignity_p2p = __commonJS({
3496
4225
  }
3497
4226
  const gossipId = options.gossipId || this.idGenerator();
3498
4227
  this.markSeenGossip(gossipId);
4228
+ this.lastGossipPublishAt.set(groupId, this.now());
3499
4229
  await this.broadcastMessage("peer-group:gossip", {
3500
4230
  groupId,
3501
4231
  gossipId,
@@ -3802,6 +4532,35 @@ var require_dignity_p2p = __commonJS({
3802
4532
  if (!decrypted || decrypted.ignored) {
3803
4533
  return;
3804
4534
  }
4535
+ if (decrypted.messageType === "identity:rotate") {
4536
+ const peerId = decrypted.senderId || decrypted.payload?.username;
4537
+ if (peerId && decrypted.payload) {
4538
+ const result = this.applyPeerIdentityRotation(peerId, decrypted.payload);
4539
+ if (!result.applied) {
4540
+ this.emit("warning", {
4541
+ type: "identity-rotation-ignored",
4542
+ peerId,
4543
+ reason: result.reason
4544
+ });
4545
+ }
4546
+ }
4547
+ return;
4548
+ }
4549
+ if (decrypted.messageType === "identity:cold-enroll") {
4550
+ const peerId = decrypted.senderId || decrypted.payload?.username;
4551
+ if (peerId && decrypted.payload) {
4552
+ try {
4553
+ this.applyPeerColdRecoveryEnrollment(peerId, decrypted.payload);
4554
+ } catch (error) {
4555
+ this.emit("warning", {
4556
+ type: "cold-recovery-enrollment-rejected",
4557
+ peerId,
4558
+ error
4559
+ });
4560
+ }
4561
+ }
4562
+ return;
4563
+ }
3805
4564
  if (message && message.senderId && message.senderPublicKey) {
3806
4565
  this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3807
4566
  }
@@ -3813,7 +4572,10 @@ var require_dignity_p2p = __commonJS({
3813
4572
  const payload = decrypted.payload || {};
3814
4573
  const { collectionName, record } = payload;
3815
4574
  if (collectionName && record) {
3816
- const applied = this.restoreRecord(collectionName, record);
4575
+ const applied = this.restoreRecord(collectionName, record, {
4576
+ rejectOnHashMismatch: true,
4577
+ via: "direct-mesh"
4578
+ });
3817
4579
  if (applied) {
3818
4580
  this.emit("change", {
3819
4581
  kind: "snapshot",
@@ -4318,7 +5080,7 @@ var require_websocket_signaling_provider = __commonJS({
4318
5080
  // src/signaling/parse-peerjs-url.js
4319
5081
  var require_parse_peerjs_url = __commonJS({
4320
5082
  "src/signaling/parse-peerjs-url.js"(exports2, module2) {
4321
- function parsePeerJsServerUrl(url) {
5083
+ function parsePeerJsServerUrl2(url) {
4322
5084
  const parsed = new URL(url);
4323
5085
  const secure = parsed.protocol === "wss:";
4324
5086
  const host = parsed.hostname;
@@ -4333,7 +5095,7 @@ var require_parse_peerjs_url = __commonJS({
4333
5095
  }
4334
5096
  return { secure, host, port, path, key };
4335
5097
  }
4336
- module2.exports = parsePeerJsServerUrl;
5098
+ module2.exports = parsePeerJsServerUrl2;
4337
5099
  }
4338
5100
  });
4339
5101
 
@@ -10926,7 +11688,7 @@ var require_bundler = __commonJS({
10926
11688
  var require_peerjs_signaling_provider = __commonJS({
10927
11689
  "src/signaling/peerjs-signaling-provider.js"(exports2, module2) {
10928
11690
  var WebSocketSignalingProvider2 = require_websocket_signaling_provider();
10929
- var parsePeerJsServerUrl = require_parse_peerjs_url();
11691
+ var parsePeerJsServerUrl2 = require_parse_peerjs_url();
10930
11692
  var PeerJSSignalingProvider2 = class {
10931
11693
  constructor({ id, url, PeerImpl, WebSocketImpl, priority = 0, connectTimeoutMs = 1e4 }) {
10932
11694
  if (!url) {
@@ -10954,7 +11716,7 @@ var require_peerjs_signaling_provider = __commonJS({
10954
11716
  }
10955
11717
  }
10956
11718
  parsePeerJsServerUrl() {
10957
- return parsePeerJsServerUrl(this.url);
11719
+ return parsePeerJsServerUrl2(this.url);
10958
11720
  }
10959
11721
  shouldUseWebSocketFallback() {
10960
11722
  return !this.isCustomPeerImpl && typeof globalThis.RTCPeerConnection !== "function";
@@ -11271,7 +12033,7 @@ var require_in_memory_network = __commonJS({
11271
12033
  var require_peerjs_network = __commonJS({
11272
12034
  "src/network/peerjs-network.js"(exports2, module2) {
11273
12035
  var { DEFAULT_CLOUDFLARE_SIGNALING_URLS: DEFAULT_CLOUDFLARE_SIGNALING_URLS2 } = require_default_signaling_config();
11274
- var parsePeerJsServerUrl = require_parse_peerjs_url();
12036
+ var parsePeerJsServerUrl2 = require_parse_peerjs_url();
11275
12037
  function resolvePeerImplementation(PeerImpl) {
11276
12038
  if (PeerImpl) {
11277
12039
  return PeerImpl;
@@ -11324,7 +12086,7 @@ var require_peerjs_network = __commonJS({
11324
12086
  }
11325
12087
  async startWithUrl(nodeId, url) {
11326
12088
  this.nodeId = nodeId;
11327
- const server = parsePeerJsServerUrl(url);
12089
+ const server = parsePeerJsServerUrl2(url);
11328
12090
  await new Promise((resolve, reject) => {
11329
12091
  const peer = new this.PeerImpl(nodeId, {
11330
12092
  host: server.host,
@@ -11484,7 +12246,7 @@ var require_peerjs_network = __commonJS({
11484
12246
  module2.exports = {
11485
12247
  PeerJSNetworkAdapter: PeerJSNetworkAdapter2,
11486
12248
  createPeerJSNetworkAdapter: createPeerJSNetworkAdapter2,
11487
- parsePeerJsServerUrl
12249
+ parsePeerJsServerUrl: parsePeerJsServerUrl2
11488
12250
  };
11489
12251
  }
11490
12252
  });
@@ -11673,8 +12435,20 @@ var VDF = require_vdf();
11673
12435
  var SlothPermutation = require_sloth_vdf();
11674
12436
  var {
11675
12437
  MessageSecurityService,
11676
- DEFAULT_SECURITY_OPTIONS
12438
+ DEFAULT_SECURITY_OPTIONS,
12439
+ DEFAULT_APP_PASSWORD
11677
12440
  } = require_message_security_service();
12441
+ var { deriveKeyPairFromCredentials, keyPairToPublicBundle, deriveColdRecoverySigningKey } = require_derive_key_pair();
12442
+ var {
12443
+ createIdentityRotation,
12444
+ verifyIdentityRotation,
12445
+ revokeAndRotateIdentity,
12446
+ rotateIdentityPassword,
12447
+ enrollColdRecoveryPassword,
12448
+ verifyColdRecoveryEnrollment,
12449
+ shouldApplyIdentityRotation
12450
+ } = require_identity_rotation();
12451
+ var parsePeerJsServerUrl = require_parse_peerjs_url();
11678
12452
  var {
11679
12453
  PEER_GROUP_SCOPE_PREFIX,
11680
12454
  DEFAULT_PEER_GROUP_OPTIONS,
@@ -11699,6 +12473,18 @@ module.exports = {
11699
12473
  SlothPermutation,
11700
12474
  MessageSecurityService,
11701
12475
  DEFAULT_SECURITY_OPTIONS,
12476
+ DEFAULT_APP_PASSWORD,
12477
+ deriveKeyPairFromCredentials,
12478
+ deriveColdRecoverySigningKey,
12479
+ keyPairToPublicBundle,
12480
+ createIdentityRotation,
12481
+ verifyIdentityRotation,
12482
+ revokeAndRotateIdentity,
12483
+ rotateIdentityPassword,
12484
+ enrollColdRecoveryPassword,
12485
+ verifyColdRecoveryEnrollment,
12486
+ shouldApplyIdentityRotation,
12487
+ parsePeerJsServerUrl,
11702
12488
  PEER_GROUP_SCOPE_PREFIX,
11703
12489
  DEFAULT_PEER_GROUP_OPTIONS,
11704
12490
  peerGroupScope,