peerpigeon
Version: 
WebRTC-based peer-to-peer mesh networking library with intelligent routing and signaling server
1,529 lines (1,320 loc) • 55.7 kB
JavaScript
import { EventEmitter } from './EventEmitter.js';
import DebugLogger from './DebugLogger.js';
import { createLexicalInterface } from './LexicalStorageInterface.js';
// Dynamic import for unsea to handle both Node.js and browser environments
let unsea = null;
async function initializeUnsea() {
  if (unsea) return unsea;
  try {
    // Check if UnSEA is bundled (browser bundle case)
    if (typeof window !== 'undefined' && window.__PEERPIGEON_UNSEA__) {
      unsea = window.__PEERPIGEON_UNSEA__;
      console.log('✅ Using bundled UnSEA crypto for storage');
      return unsea;
    }
    // Check global scope for bundled version
    if (typeof globalThis !== 'undefined' && globalThis.__PEERPIGEON_UNSEA__) {
      unsea = globalThis.__PEERPIGEON_UNSEA__;
      console.log('✅ Using bundled UnSEA crypto for storage');
      return unsea;
    }
    // Detect environment - prioritize Node.js detection for tests
    const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
    if (isNode) {
      // For Node.js environments (including tests), use npm package
      unsea = await import('unsea');
      console.log('✅ Loaded unsea from npm package (Node.js) for storage');
    } else {
      // For browser environments without bundle, try CDN sources as fallback
      try {
        unsea = await import('https://cdn.jsdelivr.net/npm/unsea@latest/+esm');
        console.log('✅ Loaded unsea from jsDelivr CDN for storage');
      } catch (jsDelivrError) {
        console.warn('Failed to load from jsDelivr, trying unpkg:', jsDelivrError);
        try {
          unsea = await import('https://unpkg.com/unsea@latest/dist/unsea.esm.js');
          console.log('✅ Loaded unsea from unpkg CDN for storage');
        } catch (unpkgError) {
          console.warn('Failed to load from unpkg, trying Skypack:', unpkgError);
          unsea = await import('https://cdn.skypack.dev/unsea');
          console.log('✅ Loaded unsea from Skypack CDN for storage');
        }
      }
    }
    if (!unsea) {
      throw new Error('Unsea not found after loading');
    }
    return unsea;
  } catch (error) {
    console.error('Failed to load unsea for storage:', error);
    throw error;
  }
}
/**
 * DistributedStorageManager - High-level distributed storage layer for PeerPigeon mesh
 *
 * Features:
 * - Encrypted storage using unsea directly
 * - Optional public/private data visibility with access control
 * - Optional immutability with CRDT support for collaborative editing
 * - Mutable layer for data originators
 * - Uses WebDHT as low-level storage backend (but is separate conceptually)
 * - Space separation for preventing data overwrites (Private, Public, Frozen)
 * 
 * IMPORTANT: This is a HIGH-LEVEL storage system that provides encryption, 
 * access control, and advanced features. It uses WebDHT as its storage backend
 * but they are separate concepts:
 * - WebDHT: Low-level distributed hash table for raw key-value storage
 * - DistributedStorageManager: High-level storage with encryption/access control
 */
export class DistributedStorageManager extends EventEmitter {
  constructor(mesh) {
    super();
    this.debug = DebugLogger.create('DistributedStorageManager');
    this.mesh = mesh;
    
    // IMPORTANT: DistributedStorageManager uses WebDHT as its storage backend
    // but they are conceptually separate systems:
    // - WebDHT: Low-level DHT for raw key-value storage across the mesh
    // - DistributedStorageManager: High-level storage with encryption, access control, spaces
    this.webDHT = mesh.webDHT; // Uses WebDHT as backend storage layer
    this.cryptoManager = mesh.cryptoManager;
    // Initialize unsea for encryption
    this.unsea = null;
    this.storageKeypair = null;
    this.initializeCrypto();
    // Storage configuration
    this.config = {
      encryptionEnabled: true,
      defaultTTL: null, // No expiration by default
      maxValueSize: 1024 * 1024, // 1MB default max size
      enableCRDT: true,
      conflictResolution: 'last-write-wins', // or 'crdt-merge'
      spaceEnforcement: true // Enable space-based access control
    };
    // Storage spaces for data separation
    this.spaces = {
      PRIVATE: 'private', // Only owner can read/write, encrypted
      PUBLIC: 'public', // Anyone can read, only owner can write
      FROZEN: 'frozen' // Immutable once set, anyone can read
    };
    // Local storage for metadata and permissions
    this.storageMetadata = new Map(); // keyId -> metadata
    this.accessControl = new Map(); // keyId -> access control info
    this.crdtStates = new Map(); // keyId -> CRDT state for collaborative data
    // Track data ownership by space
    this.ownedKeys = new Set(); // Keys owned by this peer
    this.spaceOwnership = new Map(); // space:key -> owner mapping
    this.keyToSpaceMapping = new Map(); // baseKey -> {space, storageKey} mapping for transparent access
    // Track enabled state
    this.enabled = true; // Enable by default
    this.debug.log(`DistributedStorageManager initialized for peer ${this.mesh.peerId?.substring(0, 8)}...`);
  }
  /**
   * Initialize unsea encryption for storage
   * @private
   */
  async initializeCrypto() {
    try {
      this.unsea = await initializeUnsea();
      // Generate a keypair for storage encryption
      // Use the mesh's keypair if available, otherwise generate one
      if (this.cryptoManager && this.cryptoManager.keypair) {
        this.storageKeypair = this.cryptoManager.keypair;
      } else {
        this.storageKeypair = await this.unsea.generateRandomPair();
      }
      this.debug.log('📦 Storage encryption initialized with unsea');
    } catch (error) {
      this.debug.warn('📦 Failed to initialize storage encryption:', error);
      // Disable encryption if unsea fails to load
      this.config.encryptionEnabled = false;
    }
  }
  /**
   * Wait for crypto initialization to complete
   * @returns {Promise<void>}
   */
  async waitForCrypto() {
    if (this.unsea && this.storageKeypair) {
      return;
    }
    // Wait up to 5 seconds for crypto to initialize
    const timeout = 5000;
    const start = Date.now();
    while ((!this.unsea || !this.storageKeypair) && (Date.now() - start) < timeout) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }
  /**
   * Resolve a key to its actual storage location across spaces
   * @param {string} key - The storage key (base key only)
   * @returns {Object} - {space, baseKey, keyId}
   */
  async resolveKey(key) {
    // Use the key directly - NO PREFIX PARSING
    const baseKey = key;
    
    // Generate keyId from baseKey (no prefixes)
    const keyId = await this.webDHT.hash(baseKey);
    // For bare keys, check if we have a mapping
    const mapping = this.keyToSpaceMapping.get(baseKey);
    if (mapping) {
      return {
        space: mapping.space,
        baseKey,
        keyId
      };
    }
    // If no mapping exists, check if we have metadata for this key
    if (this.storageMetadata.has(keyId)) {
      const metadata = this.storageMetadata.get(keyId);
      const space = metadata.space || this.spaces.PRIVATE;
      // Update the mapping for future use
      this.keyToSpaceMapping.set(baseKey, { space, storageKey: baseKey });
      return {
        space,
        baseKey,
        keyId
      };
    }
    // Default to private space if no other information is available
    const defaultSpace = this.spaces.PRIVATE;
    this.keyToSpaceMapping.set(baseKey, { space: defaultSpace, storageKey: baseKey });
    return {
      space: defaultSpace,
      baseKey,
      keyId
    };
  }
  /**
   * Check if a peer can access a key in a specific space
   * @param {string} space - The storage space
   * @param {string} key - The storage key
   * @param {string} peerId - The peer ID requesting access
   * @param {string} operation - The operation (read, write)
   * @returns {boolean} - Whether access is allowed
   */
  canAccessSpace(space, key, peerId, operation = 'read') {
    if (!this.config.spaceEnforcement) {
      return true; // Space enforcement disabled
    }
    const spaceKey = `${space}:${key}`;
    const owner = this.spaceOwnership.get(spaceKey);
    switch (space) {
      case this.spaces.PRIVATE:
        // Private: Only owner can read/write, or if no owner exists (initial write)
        return !owner || owner === peerId;
      case this.spaces.PUBLIC:
        if (operation === 'read') {
          return true; // Anyone can read public data
        } else {
          return !owner || owner === peerId; // Only owner can write, or initial write
        }
      case this.spaces.FROZEN:
        if (operation === 'read') {
          return true; // Anyone can read frozen data
        } else {
          // For frozen data, only the owner can write (both initial and updates)
          return !owner || owner === peerId; // Only owner can write, or initial write
        }
      default:
        return false;
    }
  }
  /**
   * Determine appropriate space and access control from options
   * @param {Object} options - Storage options
   * @returns {Object} - {space, isPublic, isImmutable}
   */
  determineSpaceFromOptions(options = {}) {
    // Allow explicit space specification
    if (options.space && Object.values(this.spaces).includes(options.space)) {
      const space = options.space;
      return {
        space,
        isPublic: space !== this.spaces.PRIVATE,
        isImmutable: space === this.spaces.FROZEN
      };
    }
    // Legacy compatibility - determine space from isPublic/isImmutable
    if (options.isImmutable) {
      return {
        space: this.spaces.FROZEN,
        isPublic: true,
        isImmutable: true
      };
    } else if (options.isPublic) {
      return {
        space: this.spaces.PUBLIC,
        isPublic: true,
        isImmutable: false
      };
    } else {
      return {
        space: this.spaces.PRIVATE,
        isPublic: false,
        isImmutable: false
      };
    }
  }
  /**
   * Find if a base key exists in a different space
   * @param {string} baseKey - The base key to search for
   * @param {string} excludeSpace - The space to exclude from search
   * @returns {string|null} - The space where the key exists, or null if not found
   */
  findKeyInDifferentSpace(baseKey, excludeSpace) {
    // Check our metadata to see if this baseKey exists in a different space
    for (const metadata of this.storageMetadata.values()) {
      if (metadata.baseKey === baseKey && metadata.space !== excludeSpace) {
        return metadata.space;
      }
    }
    
    // Also check space ownership using baseKey directly
    if (this.spaceOwnership.has(baseKey)) {
      const mapping = this.keyToSpaceMapping.get(baseKey);
      if (mapping && mapping.space !== excludeSpace) {
        return mapping.space;
      }
    }
    
    return null;
  }
  /**
   * Check if the current peer has read access to a key with space awareness
   * @param {string} keyId - The key ID
   * @param {Object} metadata - The metadata object
   * @param {string} space - The storage space
   * @returns {boolean} - Whether access is allowed
   */
  hasReadAccessWithSpace(keyId, metadata, space) {
    const accessControl = this.accessControl.get(keyId);
    if (!accessControl && metadata) {
      // Create access control from metadata
      this.accessControl.set(keyId, {
        isPublic: metadata.isPublic,
        owner: metadata.owner,
        allowedPeers: new Set(metadata.allowedPeers || []),
        isImmutable: metadata.isImmutable,
        space: metadata.space || space
      });
      return this.hasReadAccessWithSpace(keyId, metadata, space);
    }
    if (!accessControl) {
      return false;
    }
    // Owner always has access
    if (accessControl.owner === this.mesh.peerId) {
      return true;
    }
    // Space-based access control
    switch (space) {
      case this.spaces.PRIVATE:
        // Private: only owner and specifically allowed peers
        return accessControl.allowedPeers.has(this.mesh.peerId);
      case this.spaces.PUBLIC:
      case this.spaces.FROZEN:
        // Public and frozen: anyone can read
        return true;
      default:
        return false;
    }
  }
  /**
   * Store data with encryption and access control
   * @param {string} key - The storage key
   * @param {any} value - The value to store
   * @param {Object} options - Storage options
   * @param {string} options.space - Storage space ('private', 'public', 'frozen')
   * @param {boolean} options.isPublic - Whether data is publicly readable (legacy, use space instead)
   * @param {boolean} options.isImmutable - Whether data is immutable (legacy, use frozen space instead)
   * @param {boolean} options.enableCRDT - Whether to enable CRDT for collaborative editing (default: false)
   * @param {number} options.ttl - Time to live in milliseconds
   * @param {Array<string>} options.allowedPeers - Specific peers allowed to read private data
   * @returns {Promise<boolean>} Success status
   */
  async store(key, value, options = {}) {
    if (!this.enabled) {
      return false;
    }
    if (!this.webDHT) {
      throw new Error('WebDHT not available - ensure it is enabled in mesh configuration');
    }
    // Wait for crypto initialization if encryption is enabled
    if (this.config.encryptionEnabled) {
      await this.waitForCrypto();
    }
    // Use the key directly - NO PREFIX PARSING
    const spaceConfig = this.determineSpaceFromOptions(options);
    // Use the base key directly - NO SPACE PREFIXES!
    const baseKey = key;
    const space = options.space || spaceConfig.space;
    
    // The stored key is just the base key - space info goes in metadata
    const storageKey = baseKey;
    // Check space access permissions
    if (!this.canAccessSpace(space, baseKey, this.mesh.peerId, 'write')) {
      throw new Error(`Write access denied for space "${space}" and key "${baseKey}"`);
    }
    const keyId = await this.webDHT.hash(storageKey);
    const timestamp = Date.now();
    this.debug.log(`📦 Storing key: ${storageKey} in ${space} space, keyId: ${keyId.toString(16).substring(0, 8)}...`);
    // Validate value size
    const serializedValue = JSON.stringify(value);
    if (serializedValue.length > this.config.maxValueSize) {
      throw new Error(`Value size exceeds maximum allowed size of ${this.config.maxValueSize} bytes`);
    }
    // Create storage metadata with type and space information
    const metadata = {
      key: baseKey,  // Just the base key, no prefixes - space is its own attribute
      baseKey,
      space,
      keyId,
      owner: this.mesh.peerId,
      isPublic: spaceConfig.isPublic,
      isImmutable: spaceConfig.isImmutable,
      enableCRDT: options.enableCRDT || false,
      allowedPeers: options.allowedPeers || [],
      createdAt: timestamp,
      updatedAt: timestamp,
      version: 1,
      ttl: options.ttl || this.config.defaultTTL,
      type: 'storage', // Mark this as storage data type
      dataType: 'distributed-storage' // Specific storage system identifier
    };
    // Store metadata locally
    this.storageMetadata.set(keyId, metadata);
    this.ownedKeys.add(keyId);
    // Track space ownership using just the base key
    this.spaceOwnership.set(baseKey, this.mesh.peerId);
    // Track base key to space mapping for transparent access
    this.keyToSpaceMapping.set(baseKey, { space, storageKey });
    // Set up access control
    this.accessControl.set(keyId, {
      isPublic: metadata.isPublic,
      owner: metadata.owner,
      allowedPeers: new Set(metadata.allowedPeers),
      isImmutable: metadata.isImmutable,
      space
    });
    // Initialize CRDT state if enabled
    if (metadata.enableCRDT) {
      this.crdtStates.set(keyId, {
        vectorClock: { [this.mesh.peerId]: 1 },
        operations: [],
        lastMerged: timestamp
      });
    }
    // Prepare the storage payload
    let storagePayload = {
      value,
      metadata,
      encrypted: false
    };
    // Only encrypt private space data
    if (space === this.spaces.PRIVATE && this.config.encryptionEnabled && this.unsea && this.storageKeypair) {
      try {
        // For private data, encrypt with the owner's keypair so only the owner can decrypt
        const encryptedValue = await this.unsea.encryptMessageWithMeta(serializedValue, this.storageKeypair);
        storagePayload = {
          value: encryptedValue,
          metadata,
          encrypted: true,
          encryptedBy: this.mesh.peerId // Track who encrypted it
        };
        this.debug.log(`📦 Encrypted private space storage data for key: ${storageKey} (owner-only access)`);
      } catch (error) {
        this.debug.warn(`Failed to encrypt private space storage data for key ${storageKey}:`, error);
        // Fall back to unencrypted storage if encryption fails
      }
    } else {
      this.debug.log(`📦 Storing ${space} space data for key: ${storageKey}`);
    }
    // CRITICAL DEBUG: Log exactly what we're storing
    this.debug.log(`📦 STORING PAYLOAD STRUCTURE for key ${storageKey}:`, {
      hasValue: !!storagePayload.value,
      hasMetadata: !!storagePayload.metadata,
      valueType: typeof storagePayload.value,
      metadataType: typeof storagePayload.metadata,
      metadataKeys: storagePayload.metadata ? Object.keys(storagePayload.metadata) : 'none',
      encrypted: storagePayload.encrypted,
      payloadKeys: Object.keys(storagePayload)
    });
    // Store in WebDHT with storage namespace prefix to separate from raw DHT data
    // This maintains separation between low-level DHT operations and high-level storage
    try {
      this.debug.log(`📦 Storing payload for key ${storageKey}:`, {
        hasValue: !!storagePayload.value,
        hasMetadata: !!storagePayload.metadata,
        encrypted: storagePayload.encrypted,
        space
      });
      // Store directly with clean key - NO PREFIXES, type information is in metadata
      await this.webDHT.put(storageKey, storagePayload, {
        ttl: metadata.ttl,
        space: space // Pass space for space-aware replication
      });
      this.debug.log(`📦 Stored ${space} space data for key: ${storageKey}`);
      // Emit storage event
      this.emit('dataStored', {
        key: storageKey,
        baseKey,
        space,
        keyId,
        isPublic: metadata.isPublic,
        isImmutable: metadata.isImmutable,
        enableCRDT: metadata.enableCRDT
      });
      return true;
    } catch (error) {
      this.debug.error(`Failed to store data for key ${storageKey}:`, error);
      // Clean up local metadata on failure
      this.storageMetadata.delete(keyId);
      this.ownedKeys.delete(keyId);
      this.accessControl.delete(keyId);
      this.crdtStates.delete(keyId);
      this.spaceOwnership.delete(baseKey);
      this.keyToSpaceMapping.delete(baseKey);
      throw error;
    }
  }
  /**
   * Retrieve data with access control and decryption
   * @param {string} key - The storage key (base key only, no prefixes)
   * @param {Object} options - Retrieval options
   * @param {string} options.space - Specific space to look in (optional)
   * @param {boolean} options.forceRefresh - Force refresh from network
   * @returns {Promise<any>} The stored value or null if not accessible
   */
  async retrieve(key, options = {}) {
    if (!this.webDHT) {
      throw new Error('WebDHT not available - ensure it is enabled in mesh configuration');
    }
    // Wait for crypto initialization if encryption is enabled
    if (this.config.encryptionEnabled) {
      await this.waitForCrypto();
    }
    // Use the key directly - NO PREFIX STRIPPING
    const baseKey = key;
    
    this.debug.log(`📦 Retrieving data for base key: ${baseKey}`);
    try {
      // Get data from WebDHT using just the clean base key - NO PREFIXES
      const webDHTPayload = await this.webDHT.get(baseKey, {
        forceRefresh: options.forceRefresh,
        space: options.space // Pass space for space-aware replication
      });
      this.debug.log(`📦 Retrieved WebDHT payload for key ${baseKey}:`, {
        payloadExists: !!webDHTPayload,
        payloadType: typeof webDHTPayload,
        keys: webDHTPayload ? Object.keys(webDHTPayload) : 'none'
      });
      if (!webDHTPayload || typeof webDHTPayload !== 'object') {
        this.debug.log(`📦 No data found for key: ${baseKey}`);
        return null;
      }
      // Process the data using the metadata to determine space and access control
      return await this.processRetrievedData(baseKey, webDHTPayload);
    } catch (error) {
      this.debug.error(`Failed to retrieve data for key ${baseKey}:`, error);
      return null;
    }
  }
  /**
   * Process retrieved data payload with decryption and access control
   * @private
   */
  async processRetrievedData(baseKey, webDHTPayload) {
    try {
      // Generate keyId for this baseKey
      const keyId = await this.webDHT.hash(baseKey);
      // WebDHT returns the storage payload directly as {value, metadata, encrypted}
      const storagePayload = webDHTPayload;
      console.log(`📦 CRITICAL DEBUG: WebDHT payload structure for ${baseKey}:`, {
        webDHTKeys: Object.keys(webDHTPayload),
        payloadType: typeof webDHTPayload,
        directValue: !!webDHTPayload.value,
        directMetadata: !!webDHTPayload.metadata,
        directEncrypted: webDHTPayload.encrypted
      });
      if (!storagePayload || typeof storagePayload !== 'object') {
        console.log(`📦 CRITICAL: Invalid storage payload for key: ${baseKey}`);
        this.debug.log(`📦 Invalid storage payload for key: ${baseKey}`);
        return null;
      }
      // Safely destructure the payload with detailed logging
      const value = storagePayload.value;
      const metadata = storagePayload.metadata;
      const encrypted = storagePayload.encrypted || false;
      const encryptedBy = storagePayload.encryptedBy;
      if (!metadata) {
        this.debug.warn(`📦 Invalid storage payload for key ${baseKey}: missing metadata`);
        return null;
      }
      // Validate this is actually storage data by checking metadata type
      if (!metadata.type || metadata.type !== 'storage') {
        this.debug.log(`📦 Data for key ${baseKey} is not storage data (type: ${metadata.type || 'unknown'}) - ignoring`);
        return null;
      }
      // Get space from metadata - this is where space info is stored!
      const space = metadata.space;
      this.debug.log('📦 Payload components:', {
        hasValue: value !== undefined,
        valueType: typeof value,
        hasMetadata: metadata !== undefined,
        metadataType: typeof metadata,
        encrypted,
        space: space || 'unknown',
        encryptedBy: encryptedBy?.substring(0, 8) + '...' || 'unknown'
      });
      // Check space access permissions using space from metadata
      if (!this.canAccessSpace(space, baseKey, this.mesh.peerId, 'read')) {
        this.debug.warn(`📦 Access denied for key: ${baseKey} in space: ${space}`);
        return null;
      }
      // Check access permissions using space-aware logic
      if (!this.hasReadAccessWithSpace(keyId, metadata, space)) {
        this.debug.warn(`📦 Access denied for key: ${baseKey} in space: ${space}`);
        return null;
      }
      // Decrypt if necessary
      let finalValue = value;
      if (encrypted && this.unsea && this.storageKeypair) {
        // Only allow decryption if this peer is the owner (encrypted the data)
        if (encryptedBy && encryptedBy !== this.mesh.peerId) {
          this.debug.warn(`📦 Cannot decrypt data for key ${baseKey}: encrypted by different peer (${encryptedBy.substring(0, 8)}...), current peer: ${this.mesh.peerId.substring(0, 8)}...`);
          // For private data encrypted by another peer, deny access
          if (!metadata.isPublic) {
            this.debug.warn(`Access denied for key: ${baseKey} - private data encrypted by different peer`);
            return null;
          }
        }
        try {
          const decryptedValue = await this.unsea.decryptMessageWithMeta(value, this.storageKeypair.epriv);
          finalValue = JSON.parse(decryptedValue);
          this.debug.log(`🔓 Decrypted storage data for key: ${baseKey}`);
        } catch (error) {
          this.debug.error(`Failed to decrypt data for key ${baseKey}:`, error);
          // If this is private data and decryption fails, deny access
          if (!metadata.isPublic) {
            return null;
          }
          // For public data, if decryption fails, try to use the raw value
          this.debug.warn(`📦 Using raw value for public key ${baseKey} due to decryption failure`);
          finalValue = value;
        }
      }
      // Update local metadata if we don't have it
      if (!this.storageMetadata.has(keyId)) {
        this.storageMetadata.set(keyId, metadata);
        this.accessControl.set(keyId, {
          isPublic: metadata.isPublic,
          owner: metadata.owner,
          allowedPeers: new Set(metadata.allowedPeers),
          isImmutable: metadata.isImmutable,
          space: metadata.space || space
        });
        // Track space ownership from retrieved metadata using baseKey
        if (metadata.owner && !this.spaceOwnership.has(baseKey)) {
          this.spaceOwnership.set(baseKey, metadata.owner);
        }
        // Update key mapping for future transparent access
        this.keyToSpaceMapping.set(baseKey, { space, storageKey: baseKey });
      }
      this.debug.log(`📦 Retrieved ${space} space data for key: ${baseKey}`);
      // Emit retrieval event
      this.emit('dataRetrieved', {
        key: baseKey,
        baseKey,
        space,
        keyId,
        isPublic: metadata.isPublic,
        owner: metadata.owner
      });
      return finalValue;
    } catch (error) {
      this.debug.error(`Failed to retrieve data for key ${baseKey}:`, error);
      return null;
    }
  }
  /**
   * Update existing data (only allowed for owners or if mutable)
   * @param {string} key - The storage key
   * @param {any} newValue - The new value
   * @param {Object} options - Update options
   * @param {boolean} options.forceCRDTMerge - Force CRDT merge even if not owner
   * @returns {Promise<boolean>} Success status
   */
  async update(key, newValue, options = {}) {
    if (!this.webDHT) {
      throw new Error('WebDHT not available - ensure it is enabled in mesh configuration');
    }
    // Resolve the key to its actual storage location
    const resolved = await this.resolveKey(key);
    const { baseKey, keyId } = resolved;
    const timestamp = Date.now();
    // Check if we have access to update
    let accessControl = this.accessControl.get(keyId);
    let metadata = this.storageMetadata.get(keyId);
    if (!accessControl || !metadata) {
      // Try to retrieve metadata first
      const existingData = await this.retrieve(baseKey);
      if (!existingData) {
        throw new Error(`Key ${key} does not exist or is not accessible`);
      }
      // Check if metadata is now available after retrieve
      const updatedAccessControl = this.accessControl.get(keyId);
      const updatedMetadata = this.storageMetadata.get(keyId);
      if (!updatedAccessControl || !updatedMetadata) {
        throw new Error(`Key ${key} metadata could not be loaded - unable to update`);
      }
      // Update local references and continue with update logic
      accessControl = updatedAccessControl;
      metadata = updatedMetadata;
    }
    const isOwner = accessControl.owner === this.mesh.peerId;
    const canUpdate = isOwner || (!accessControl.isImmutable) || (metadata.enableCRDT && options.forceCRDTMerge);
    if (!canUpdate) {
      throw new Error(`Update not allowed for key ${key}: immutable data and not owner`);
    }
    // Handle CRDT merge if enabled
    if (metadata.enableCRDT && !isOwner) {
      return this.applyCRDTUpdate(key, keyId, newValue, options);
    }
    // Update metadata
    metadata.updatedAt = timestamp;
    metadata.version += 1;
    // Prepare storage payload
    let storagePayload = {
      value: newValue,
      metadata,
      encrypted: false
    };
    // Encrypt if not public and crypto is available
    if (!metadata.isPublic && this.config.encryptionEnabled && this.unsea && this.storageKeypair) {
      try {
        const serializedValue = JSON.stringify(newValue);
        const encryptedValue = await this.unsea.encryptMessageWithMeta(serializedValue, this.storageKeypair);
        storagePayload = {
          value: encryptedValue,
          metadata,
          encrypted: true,
          encryptedBy: this.mesh.peerId // Track who encrypted it
        };
        this.debug.log(`📦 Encrypted updated storage data for key: ${key}`);
      } catch (error) {
        this.debug.warn(`Failed to encrypt updated storage data for key ${key}:`, error);
      }
    }
    try {
      // Use WebDHT update to notify all replicas and subscribers - NO PREFIXES
      await this.webDHT.update(baseKey, storagePayload, {
        ttl: metadata.ttl
      });
      // Update local metadata
      this.storageMetadata.set(keyId, metadata);
      this.debug.log(`📦 Updated ${metadata.isPublic ? 'public' : 'private'} data for key: ${baseKey}`);
      // Emit update event
      this.emit('dataUpdated', {
        key: baseKey,
        keyId,
        isPublic: metadata.isPublic,
        version: metadata.version,
        isOwner
      });
      return true;
    } catch (error) {
      this.debug.error(`Failed to update data for key ${baseKey}:`, error);
      throw error;
    }
  }
  /**
   * Apply CRDT-based update for collaborative editing
   * @private
   */
  async applyCRDTUpdate(key, keyId, operation, options = {}) {
    const crdtState = this.crdtStates.get(keyId);
    const metadata = this.storageMetadata.get(keyId);
    if (!crdtState || !metadata) {
      throw new Error(`CRDT state not found for key ${key}`);
    }
    // Increment vector clock for this peer
    const currentClock = crdtState.vectorClock[this.mesh.peerId] || 0;
    crdtState.vectorClock[this.mesh.peerId] = currentClock + 1;
    // Add operation to CRDT state
    const crdtOperation = {
      peerId: this.mesh.peerId,
      timestamp: Date.now(),
      vectorClock: { ...crdtState.vectorClock },
      operation,
      type: options.operationType || 'replace'
    };
    crdtState.operations.push(crdtOperation);
    crdtState.lastMerged = Date.now();
    // Apply CRDT merge logic (simplified last-write-wins for now)
    const mergedValue = this.mergeCRDTOperations(crdtState.operations);
    // Update the stored value
    return this.update(key, mergedValue, { ...options, forceCRDTMerge: false });
  }
  /**
   * Simple CRDT merge implementation (can be extended for more sophisticated CRDTs)
   * @private
   */
  mergeCRDTOperations(operations) {
    // Sort operations by timestamp and apply in order
    const sortedOps = operations.sort((a, b) => a.timestamp - b.timestamp);
    let result = null;
    for (const op of sortedOps) {
      switch (op.type) {
        case 'replace':
          result = op.operation;
          break;
        case 'merge':
          if (result && typeof result === 'object' && typeof op.operation === 'object') {
            result = { ...result, ...op.operation };
          } else {
            result = op.operation;
          }
          break;
        default:
          result = op.operation;
      }
    }
    return result;
  }
  /**
   * Delete data (only allowed for owners)
   * @param {string} key - The storage key
   * @returns {Promise<boolean>} Success status
   */
  async delete(key) {
    if (!this.webDHT) {
      throw new Error('WebDHT not available - ensure it is enabled in mesh configuration');
    }
    // Resolve the key to its actual storage location
    const resolved = await this.resolveKey(key);
    const { baseKey, keyId } = resolved;
    const accessControl = this.accessControl.get(keyId);
    if (!accessControl || accessControl.owner !== this.mesh.peerId) {
      throw new Error(`Delete not allowed for key ${key}: not owner`);
    }
    try {
      // Create a tombstone entry to mark as deleted
      const tombstone = {
        deleted: true,
        deletedAt: Date.now(),
        deletedBy: this.mesh.peerId
      };
      // Mark as deleted in WebDHT - NO PREFIXES
      await this.webDHT.update(baseKey, tombstone);
      // Clean up local state
      this.storageMetadata.delete(keyId);
      this.ownedKeys.delete(keyId);
      this.accessControl.delete(keyId);
      this.crdtStates.delete(keyId);
      // Clean up space ownership tracking and key mapping
      this.spaceOwnership.delete(baseKey);
      this.keyToSpaceMapping.delete(baseKey);
      this.debug.log(`📦 Deleted data for key: ${baseKey}`);
      // Emit deletion event
      this.emit('dataDeleted', { key: baseKey, keyId });
      return true;
    } catch (error) {
      this.debug.error(`Failed to delete data for key ${baseKey}:`, error);
      throw error;
    }
  }
  /**
   * Subscribe to changes for a storage key
   * @param {string} key - The storage key
   * @returns {Promise<any>} Current value or null
   */
  async subscribe(key) {
    if (!this.webDHT) {
      throw new Error('WebDHT not available - ensure it is enabled in mesh configuration');
    }
    // Subscribe to the WebDHT key - NO PREFIXES
    const currentValue = await this.webDHT.subscribe(key);
    this.debug.log(`📦 Subscribed to storage key: ${key}`);
    return currentValue;
  }
  /**
   * Unsubscribe from changes for a storage key
   * @param {string} key - The storage key
   */
  async unsubscribe(key) {
    if (!this.webDHT) {
      return;
    }
    // Unsubscribe from the WebDHT key - NO PREFIXES
    await this.webDHT.unsubscribe(key);
    this.debug.log(`📦 Unsubscribed from storage key: ${key}`);
  }
  /**
   * Check if the current peer has read access to a key
   * @private
   */
  hasReadAccess(keyId, metadata) {
    const accessControl = this.accessControl.get(keyId);
    if (!accessControl && metadata) {
      // Create access control from metadata
      this.accessControl.set(keyId, {
        isPublic: metadata.isPublic,
        owner: metadata.owner,
        allowedPeers: new Set(metadata.allowedPeers || []),
        isImmutable: metadata.isImmutable
      });
      return this.hasReadAccess(keyId, metadata);
    }
    if (!accessControl) {
      return false;
    }
    // Owner always has access
    if (accessControl.owner === this.mesh.peerId) {
      return true;
    }
    // Public data is accessible to everyone
    if (accessControl.isPublic) {
      return true;
    }
    // Check if peer is in allowed list
    return accessControl.allowedPeers.has(this.mesh.peerId);
  }
  /**
   * Grant access to a peer for a private key (only owner can do this)
   * @param {string} key - The storage key
   * @param {string} peerId - The peer to grant access to
   * @returns {Promise<boolean>} Success status
   */
  async grantAccess(key, peerId) {
    // Resolve the key to its actual storage location
    const resolved = await this.resolveKey(key);
    const { baseKey, keyId } = resolved;
    const accessControl = this.accessControl.get(keyId);
    const metadata = this.storageMetadata.get(keyId);
    if (!accessControl || !metadata || accessControl.owner !== this.mesh.peerId) {
      throw new Error(`Cannot grant access for key ${key}: not owner`);
    }
    if (accessControl.isPublic) {
      throw new Error(`Cannot grant access for key ${key}: already public`);
    }
    // Add peer to allowed list
    accessControl.allowedPeers.add(peerId);
    metadata.allowedPeers.push(peerId);
    metadata.updatedAt = Date.now();
    // Update the stored metadata
    try {
      const currentPayload = await this.webDHT.get(baseKey);
      if (currentPayload) {
        currentPayload.metadata = metadata;
        await this.webDHT.update(baseKey, currentPayload);
      }
      this.debug.log(`📦 Granted access to peer ${peerId.substring(0, 8)}... for key: ${baseKey}`);
      // Emit access granted event
      this.emit('accessGranted', { key: baseKey, keyId, peerId });
      return true;
    } catch (error) {
      // Rollback changes
      accessControl.allowedPeers.delete(peerId);
      metadata.allowedPeers = metadata.allowedPeers.filter(p => p !== peerId);
      throw error;
    }
  }
  /**
   * Revoke access from a peer for a private key (only owner can do this)
   * @param {string} key - The storage key
   * @param {string} peerId - The peer to revoke access from
   * @returns {Promise<boolean>} Success status
   */
  async revokeAccess(key, peerId) {
    // Resolve the key to its actual storage location
    const resolved = await this.resolveKey(key);
    const { baseKey, keyId } = resolved;
    const accessControl = this.accessControl.get(keyId);
    const metadata = this.storageMetadata.get(keyId);
    if (!accessControl || !metadata || accessControl.owner !== this.mesh.peerId) {
      throw new Error(`Cannot revoke access for key ${key}: not owner`);
    }
    // Remove peer from allowed list
    accessControl.allowedPeers.delete(peerId);
    metadata.allowedPeers = metadata.allowedPeers.filter(p => p !== peerId);
    metadata.updatedAt = Date.now();
    // Update the stored metadata
    try {
      const currentPayload = await this.webDHT.get(baseKey);
      if (currentPayload) {
        currentPayload.metadata = metadata;
        await this.webDHT.update(baseKey, currentPayload);
      }
      this.debug.log(`📦 Revoked access from peer ${peerId.substring(0, 8)}... for key: ${baseKey}`);
      // Emit access revoked event
      this.emit('accessRevoked', { key: baseKey, keyId, peerId });
      return true;
    } catch (error) {
      // Rollback changes
      accessControl.allowedPeers.add(peerId);
      metadata.allowedPeers.push(peerId);
      throw error;
    }
  }
  /**
   * List all keys owned by this peer
   * @returns {Array<Object>} Array of owned key metadata
   */
  getOwnedKeys() {
    const ownedKeys = [];
    for (const keyId of this.ownedKeys) {
      const metadata = this.storageMetadata.get(keyId);
      if (metadata) {
        ownedKeys.push({
          key: metadata.key,
          keyId,
          isPublic: metadata.isPublic,
          isImmutable: metadata.isImmutable,
          enableCRDT: metadata.enableCRDT,
          createdAt: metadata.createdAt,
          updatedAt: metadata.updatedAt,
          version: metadata.version
        });
      }
    }
    return ownedKeys;
  }
  /**
   * Get statistics about the storage manager
   * @returns {Promise<Object>} Storage statistics
   */
  async getStats() {
    let totalSize = 0;
    let itemCount = 0;
    // Calculate sizes for owned keys
    for (const keyId of this.ownedKeys) {
      const metadata = this.storageMetadata.get(keyId);
      if (metadata) {
        try {
          const value = await this.retrieve(metadata.key);
          if (value !== null) {
            const serializedSize = JSON.stringify(value).length;
            totalSize += serializedSize;
            itemCount++;
          }
        } catch (error) {
          // Skip keys that can't be retrieved
        }
      }
    }
    return {
      enabled: this.enabled,
      itemCount,
      totalSize,
      ownedKeys: this.ownedKeys.size,
      totalKeys: this.storageMetadata.size,
      crdtKeys: this.crdtStates.size,
      encryptionEnabled: this.config.encryptionEnabled && !!this.unsea && !!this.storageKeypair,
      maxValueSize: this.config.maxValueSize
    };
  }
  /**
   * Clean up expired data and old CRDT operations
   */
  cleanup() {
    const now = Date.now();
    // Clean up expired metadata
    for (const [keyId, metadata] of this.storageMetadata.entries()) {
      if (metadata.ttl && (metadata.createdAt + metadata.ttl) < now) {
        this.storageMetadata.delete(keyId);
        this.accessControl.delete(keyId);
        this.ownedKeys.delete(keyId);
        this.debug.log(`📦 Cleaned up expired metadata for key: ${metadata.key}`);
      }
    }
    // Clean up old CRDT operations (keep last 100 operations per key)
    for (const [keyId, crdtState] of this.crdtStates.entries()) {
      if (crdtState.operations.length > 100) {
        crdtState.operations = crdtState.operations.slice(-100);
        this.debug.log(`📦 Cleaned up old CRDT operations for key: ${keyId.substring(0, 8)}...`);
      }
    }
  }
  /**
   * Backup all owned data to a serializable format
   * @returns {Object} Backup data that can be restored later
   */
  async backup() {
    const backupData = {
      version: '1.0.0',
      timestamp: Date.now(),
      peerId: this.mesh.peerId,
      keys: []
    };
    for (const keyId of this.ownedKeys) {
      const metadata = this.storageMetadata.get(keyId);
      if (metadata) {
        try {
          // Get the current value from storage
          const value = await this.retrieve(metadata.key);
          if (value !== null) {
            backupData.keys.push({
              key: metadata.key,
              value,
              metadata: {
                isPublic: metadata.isPublic,
                isImmutable: metadata.isImmutable,
                enableCRDT: metadata.enableCRDT,
                allowedPeers: metadata.allowedPeers,
                ttl: metadata.ttl
              }
            });
          }
        } catch (error) {
          this.debug.warn(`Failed to backup key ${metadata.key}:`, error);
        }
      }
    }
    this.debug.log(`📦 Created backup with ${backupData.keys.length} keys`);
    return backupData;
  }
  /**
   * Restore data from a backup
   * @param {Object} backupData - Backup data created by backup()
   * @param {Object} options - Restore options
   * @param {boolean} options.overwrite - Whether to overwrite existing keys (default: false)
   * @returns {Promise<Object>} Restore results
   */
  async restore(backupData, options = {}) {
    if (!backupData || !backupData.keys || !Array.isArray(backupData.keys)) {
      throw new Error('Invalid backup data format');
    }
    const results = {
      restored: 0,
      skipped: 0,
      failed: 0,
      errors: []
    };
    for (const keyData of backupData.keys) {
      try {
        const { key, value, metadata } = keyData;
        // Check if key already exists
        const existing = await this.retrieve(key);
        if (existing !== null && !options.overwrite) {
          results.skipped++;
          continue;
        }
        // Restore the key
        await this.store(key, value, metadata);
        results.restored++;
      } catch (error) {
        results.failed++;
        results.errors.push({
          key: keyData.key,
          error: error.message
        });
        this.debug.warn(`Failed to restore key ${keyData.key}:`, error);
      }
    }
    this.debug.log(`📦 Restore complete: ${results.restored} restored, ${results.skipped} skipped, ${results.failed} failed`);
    return results;
  }
  /**
   * List all accessible keys (owned + granted access)
   * @returns {Array<Object>} Array of accessible key metadata
   */
  async listAccessibleKeys() {
    const accessibleKeys = [];
    // Add owned keys
    for (const keyId of this.ownedKeys) {
      const metadata = this.storageMetadata.get(keyId);
      if (metadata) {
        accessibleKeys.push({
          key: metadata.key,
          keyId,
          isPublic: metadata.isPublic,
          isImmutable: metadata.isImmutable,
          enableCRDT: metadata.enableCRDT,
          createdAt: metadata.createdAt,
          updatedAt: metadata.updatedAt,
          version: metadata.version,
          owned: true,
          accessible: true
        });
      }
    }
    // Add keys we have access to but don't own
    for (const [keyId] of this.accessControl.entries()) {
      if (!this.ownedKeys.has(keyId) && this.hasReadAccess(keyId)) {
        const metadata = this.storageMetadata.get(keyId);
        if (metadata) {
          accessibleKeys.push({
            key: metadata.key,
            keyId,
            isPublic: metadata.isPublic,
            isImmutable: metadata.isImmutable,
            enableCRDT: metadata.enableCRDT,
            createdAt: metadata.createdAt,
            updatedAt: metadata.updatedAt,
            version: metadata.version,
            owned: false,
            accessible: true
          });
        }
      }
    }
    return accessibleKeys;
  }
  /**
   * Bulk store multiple key-value pairs
   * @param {Array<Object>} items - Array of {key, value, options} objects
   * @param {Object} globalOptions - Options to apply to all items
   * @returns {Promise<Object>} Results summary
   */
  async bulkStore(items, globalOptions = {}) {
    const results = {
      stored: 0,
      failed: 0,
      errors: []
    };
    const storePromises = items.map(async (item) => {
      try {
        const options = { ...globalOptions, ...item.options };
        await this.store(item.key, item.value, options);
        results.stored++;
      } catch (error) {
        results.failed++;
        results.errors.push({
          key: item.key,
          error: error.message
        });
      }
    });
    await Promise.allSettled(storePromises);
    this.debug.log(`📦 Bulk store complete: ${results.stored} stored, ${results.failed} failed`);
    return results;
  }
  /**
   * Bulk retrieve multiple keys
   * @param {Array<string>} keys - Array of keys to retrieve
   * @param {Object} options - Retrieval options
   * @returns {Promise<Object>} Map of key -> value (null for inaccessible keys)
   */
  async bulkRetrieve(keys, options = {}) {
    const results = {};
    const retrievePromises = keys.map(async (key) => {
      try {
        const value = await this.retrieve(key, options);
        results[key] = value;
      } catch (error) {
        this.debug.warn(`Failed to retrieve key ${key}:`, error);
        results[key] = null;
      }
    });
    await Promise.allSettled(retrievePromises);
    return results;
  }
  /**
   * Search for keys by pattern or metadata
   * @param {Object} criteria - Search criteria
   * @param {string} criteria.keyPattern - Regex pattern to match keys
   * @param {boolean} criteria.isPublic - Filter by public/private
   * @param {boolean} criteria.owned - Filter by ownership
   * @param {string} criteria.owner - Filter by specific owner
   * @returns {Array<Object>} Matching keys
   */
  searchKeys(criteria = {}) {
    const matches = [];
    for (const [keyId, metadata] of this.storageMetadata.entries()) {
      let match = true;
      // Check key pattern
      if (criteria.keyPattern) {
        const regex = new RegExp(criteria.keyPattern);
        if (!regex.test(metadata.key)) {
          match = false;
        }
      }
      // Check public/private
      if (criteria.isPublic !== undefined && metadata.isPublic !== criteria.isPublic) {
        match = false;
      }
      // Check ownership
      if (criteria.owned !== undefined) {
        const isOwned = this.ownedKeys.has(keyId);
        if (isOwned !== criteria.owned) {
          match = false;
        }
      }
      // Check specific owner
      if (criteria.owner && metadata.owner !== criteria.owner) {
        match = false;
      }
      if (match) {
        matches.push({
          key: metadata.key,
          keyId,
          isPublic: metadata.isPublic,
          isImmutable: metadata.isImmutable,
          enableCRDT: metadata.enableCRDT,
          owner: metadata.owner,
          createdAt: metadata.createdAt,
          updatedAt: metadata.updatedAt,
          version: metadata.version,
          owned: this.ownedKeys.has(keyId)
        });
      }
    }
    return matches;
  }
  /**
   * Watch for changes to multiple keys
   * @param {Array<string>} keys - Keys to watch
   * @param {Function} callback - Callback function for changes
   * @returns {Function} Unwatch function
   */
  async watchKeys(keys, callback) {
    const subscriptions = new Set();
    // Subscribe to each key
    for (const key of keys) {
      try {
        await this.subscribe(key);
        subscriptions.add(key);
      } catch (error) {
        this.debug.warn(`Failed to subscribe to key ${key}:`, error);
      }
    }
    // Set up event listener
    const eventHandler = (event) => {
      if (keys.includes(event.key)) {
        callback(event);
      }
    };
    this.addEventListener('dataUpdated', eventHandler);
    this.addEventListener('dataDeleted', eventHandler);
    // Return unwatch function
    return async () => {
      // Unsubscribe from keys
      for (const key of subscriptions) {
        try {
          await this.unsubscribe(key);
        } catch (error) {
          this.debug.warn(`Failed to unsubscribe from key ${key}:`, error);
        }
      }
      // Remove event listeners
      this.removeEventListener('dataUpdated', eventHandler);
      this.removeEventListener('dataDeleted', eventHandler);
    };
  }
  /**
   * Get detailed information about a key including access control
   * @param {string} key - The storage key
   * @returns {Promise<Object|null>} Key information or null if not found
   */
  async getKeyInfo(key) {
    // Resolve the key to its actual storage location
    const resolved = await this.resolveKey(key);
    const { baseKey, keyId } = resolved;
    let metadata = this.storageMetadata.get(keyId);
    // If metadata is not found locally, try to retrieve it from the network
    if (!metadata) {
      try {
        const existingData = await this.retrieve(baseKey);
        if (existingData !== null) {
          // Metadata should now be available after retrieve
          metadata = this.storageMetadata.get(keyId);
        }
      } catch (error) {
        this.debug.warn(`Failed to retrieve m