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);
@@ -2875,7 +3436,8 @@ var require_message_security_service = __commonJS({
2875
3436
  stableStringify,
2876
3437
  deriveBroadcastKey,
2877
3438
  legacyBroadcastKey,
2878
- DEFAULT_SECURITY_OPTIONS
3439
+ DEFAULT_SECURITY_OPTIONS,
3440
+ DEFAULT_APP_PASSWORD
2879
3441
  };
2880
3442
  }
2881
3443
  });
@@ -2945,7 +3507,17 @@ var require_dignity_p2p = __commonJS({
2945
3507
  var nacl = require_nacl_fast();
2946
3508
  var naclUtil = require_nacl_util();
2947
3509
  var EventEmitter = require_event_emitter();
2948
- 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();
2949
3521
  var {
2950
3522
  DEFAULT_PEER_GROUP_OPTIONS,
2951
3523
  peerGroupScope,
@@ -2990,6 +3562,9 @@ var require_dignity_p2p = __commonJS({
2990
3562
  this.defaultGossipMaxHops = security && typeof security.gossipMaxHops === "number" ? security.gossipMaxHops : DEFAULT_PEER_GROUP_OPTIONS.maxHops;
2991
3563
  this.globalMaxOpenConnections = security && typeof security.globalMaxOpenConnections === "number" ? security.globalMaxOpenConnections : 32;
2992
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();
2993
3568
  this.maxAppliedOperations = security && typeof security.maxAppliedOperations === "number" ? security.maxAppliedOperations : 5e4;
2994
3569
  this.state = /* @__PURE__ */ new Map();
2995
3570
  this.appliedOperations = /* @__PURE__ */ new Map();
@@ -2998,6 +3573,13 @@ var require_dignity_p2p = __commonJS({
2998
3573
  async start() {
2999
3574
  this.networkAdapter.onMessage(this.boundMessageHandler);
3000
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
+ }
3001
3583
  }
3002
3584
  async stop() {
3003
3585
  const joinedGroups = Array.from(this.peerGroups.keys());
@@ -3293,8 +3875,138 @@ var require_dignity_p2p = __commonJS({
3293
3875
  connectToPeers: this.resolveReplicationPeers(collectionName, id, options, { fromRecord: existing })
3294
3876
  });
3295
3877
  }
3296
- registerPeerPublicKey(peerId, publicKey) {
3297
- 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;
3298
4010
  }
3299
4011
  trustPeerPublicKey(peerId, publicKey) {
3300
4012
  if (!peerId || !publicKey) {
@@ -3416,6 +4128,13 @@ var require_dignity_p2p = __commonJS({
3416
4128
  this.seenGossipIds.delete(gossipId);
3417
4129
  }
3418
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
+ }
3419
4138
  }
3420
4139
  hasSeenGossip(gossipId) {
3421
4140
  if (!gossipId) {
@@ -3429,6 +4148,7 @@ var require_dignity_p2p = __commonJS({
3429
4148
  return;
3430
4149
  }
3431
4150
  this.seenGossipIds.set(gossipId, this.now() + this.gossipIdTtlMs);
4151
+ this.pruneSeenGossip();
3432
4152
  }
3433
4153
  listConnectedPeerIds() {
3434
4154
  if (typeof this.networkAdapter.listOpenPeerIds === "function") {
@@ -3506,6 +4226,15 @@ var require_dignity_p2p = __commonJS({
3506
4226
  if (!group && options.allowUnjoined !== true) {
3507
4227
  throw new Error(`PeerGroup ${groupId} has not been joined`);
3508
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
+ }
3509
4238
  const fanout = typeof options.fanout === "number" ? options.fanout : group ? group.fanout : this.defaultPeerGroupFanout;
3510
4239
  const maxActivePeers = group ? group.maxActivePeers : this.defaultPeerGroupMaxActivePeers;
3511
4240
  const maxHop = typeof options.maxHops === "number" ? options.maxHops : group ? group.maxHops : this.defaultGossipMaxHops;
@@ -3516,6 +4245,7 @@ var require_dignity_p2p = __commonJS({
3516
4245
  }
3517
4246
  const gossipId = options.gossipId || this.idGenerator();
3518
4247
  this.markSeenGossip(gossipId);
4248
+ this.lastGossipPublishAt.set(groupId, this.now());
3519
4249
  await this.broadcastMessage("peer-group:gossip", {
3520
4250
  groupId,
3521
4251
  gossipId,
@@ -3822,6 +4552,35 @@ var require_dignity_p2p = __commonJS({
3822
4552
  if (!decrypted || decrypted.ignored) {
3823
4553
  return;
3824
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
+ }
3825
4584
  if (message && message.senderId && message.senderPublicKey) {
3826
4585
  this.trustPeerPublicKey(message.senderId, message.senderPublicKey);
3827
4586
  }
@@ -3833,7 +4592,10 @@ var require_dignity_p2p = __commonJS({
3833
4592
  const payload = decrypted.payload || {};
3834
4593
  const { collectionName, record } = payload;
3835
4594
  if (collectionName && record) {
3836
- const applied = this.restoreRecord(collectionName, record);
4595
+ const applied = this.restoreRecord(collectionName, record, {
4596
+ rejectOnHashMismatch: true,
4597
+ via: "direct-mesh"
4598
+ });
3837
4599
  if (applied) {
3838
4600
  this.emit("change", {
3839
4601
  kind: "snapshot",
@@ -11705,8 +12467,20 @@ var require_index = __commonJS({
11705
12467
  var SlothPermutation = require_sloth_vdf();
11706
12468
  var {
11707
12469
  MessageSecurityService,
11708
- DEFAULT_SECURITY_OPTIONS
12470
+ DEFAULT_SECURITY_OPTIONS,
12471
+ DEFAULT_APP_PASSWORD
11709
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();
11710
12484
  var {
11711
12485
  PEER_GROUP_SCOPE_PREFIX,
11712
12486
  DEFAULT_PEER_GROUP_OPTIONS,
@@ -11731,6 +12505,18 @@ var require_index = __commonJS({
11731
12505
  SlothPermutation,
11732
12506
  MessageSecurityService,
11733
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,
11734
12520
  PEER_GROUP_SCOPE_PREFIX,
11735
12521
  DEFAULT_PEER_GROUP_OPTIONS,
11736
12522
  peerGroupScope,