Ctrl K
ComunidadGuías…

lib/mcp-connection-store.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Owner-scoped remote records and project-scoped computer preferences with encrypted credentials; never sent to the UI.
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var equal = require('util').isDeepStrictEqual;

function createConnectionStore(root, project, options) {
  var directory = path.join(root, 'mcp-connections');
  var shared = !!(options && options.shared);
  function scope(owner) {
    if (typeof owner !== 'string' || !owner || owner.length > 160) throw new Error('MCP account owner is required.');
    return shared ? JSON.stringify([owner]) : JSON.stringify([project, owner]);
  }
  function filename(owner) { return path.join(directory, crypto.createHash('sha256').update(scope(owner)).digest('hex') + '.json'); }
  function legacyFilename(owner) { return path.join(directory, crypto.createHash('sha256').update(JSON.stringify([project, owner])).digest('hex') + '.json'); }
  function key() {
    fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
    var file = path.join(directory, '.key'), bytes;
    try { bytes = fs.readFileSync(file); }
    catch (error) {
      if (error.code !== 'ENOENT') throw error;
      var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
      try {
        fs.writeFileSync(temp, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 });
        try { fs.linkSync(temp, file); } catch (publishError) { if (publishError.code !== 'EEXIST') throw publishError; }
      } finally { fs.unlinkSync(temp); }
      bytes = fs.readFileSync(file);
    }
    if (bytes.length !== 32) throw new Error('MCP encryption key is invalid.');
    return bytes;
  }
  function decode(file, aad) {
    var data;
    try { data = JSON.parse(fs.readFileSync(file, 'utf8')); }
    catch (error) { if (error.code === 'ENOENT') return { records: [], revision: null, migratedProjects: [], retired: [] }; throw new Error('Could not read MCP connections.'); }
    var parts = data.encrypted.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
    var cipher = crypto.createDecipheriv('aes-256-gcm', key(), parts[0]);
    cipher.setAAD(Buffer.from(aad)); cipher.setAuthTag(parts[1]);
    var value = JSON.parse(Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8'));
    if (Array.isArray(value)) return { records: value, revision: null, migratedProjects: [], retired: [] };
    return { records: Array.isArray(value.records) ? value.records : [], revision: value.revision || null, migratedProjects: Array.isArray(value.migratedProjects) ? value.migratedProjects : [], retired: value.retired || [] };
  }
  function encode(owner, state, expectedRevision) {
    var current = decode(filename(owner), scope(owner));
    if (expectedRevision !== undefined && current.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
    var next = { records: state.records, revision: crypto.randomBytes(16).toString('hex'), migratedProjects: state.migratedProjects || [], retired: state.retired || [] };
    var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', key(), iv);
    cipher.setAAD(Buffer.from(scope(owner)));
    var bytes = Buffer.concat([cipher.update(JSON.stringify(next), 'utf8'), cipher.final()]);
    var value = { encrypted: [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.') };
    var file = filename(owner), temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
    try { fs.writeFileSync(temp, JSON.stringify(value), { mode: 0o600 }); fs.renameSync(temp, file); }
    finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
    return next.revision;
  }
  function visible(records) { if (!shared) return records; return records.filter(function (record) { return record.source !== 'computer' || record.project === project; }); }
  function connectionConflict(left, right) { return left.id && left.id === right.id || (left.url && right.url && left.url === right.url); }
  function migrate(owner, state) {
    if (!shared || state.migratedProjects.indexOf(project) >= 0) return state;
    var legacy = decode(legacyFilename(owner), JSON.stringify([project, owner])).records;
    var merged = state.records.slice();
    legacy.forEach(function (record) {
      var copy = Object.assign({}, record);
      if (copy.source === 'computer') copy.project = project;
      else if ((state.retired || []).some(function (removed) { return connectionConflict(removed, copy); })) return;
      var existing = merged.filter(function (current) { return current.source === copy.source && (copy.source === 'computer' ? current.project === project && current.name === copy.name : connectionConflict(current, copy)); });
      if (existing.length && existing.some(function (current) { return !equal(current, copy); })) throw new Error('MCP connection migration conflict. Resolve the legacy project connection before retrying.');
      if (!existing.length) merged.push(copy);
    });
    state.records = merged;
    state.migratedProjects = state.migratedProjects.concat([project]);
    return state;
  }
  function readUnlocked(owner) {
    var state = decode(filename(owner), scope(owner));
    if (shared) {
      var unique = [];
      state.records.forEach(function (record) {
        var existing = unique.find(function (current) {
          return record.source === 'computer' ? current.source === 'computer' && record.project === current.project && record.name === current.name : current.source !== 'computer' && connectionConflict(record, current);
        });
        if (existing && !equal(existing, record)) throw new Error('Conflicting duplicate MCP records require resolution.');
        if (!existing) unique.push(record);
      });
      if (unique.length !== state.records.length) { state.records = unique; state.revision = encode(owner, state, state.revision); }
    }
    if (shared && state.migratedProjects.indexOf(project) < 0) { state = migrate(owner, state); state.revision = encode(owner, state, state.revision); }
    return state;
  }
  // Fail closed on contention. The lock covers migration and the full CAS read/write,
  // including callers in a second process using the same storage root.
  function locked(owner, operation) {
    fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
    var file = filename(owner) + '.lock', fd;
    try { fd = fs.openSync(file, 'wx', 0o600); }
    catch (error) { if (error.code === 'EEXIST') throw new Error('MCP connections are busy. Please retry.'); throw error; }
    try { fs.writeFileSync(fd, String(process.pid)); return operation(); }
    finally { fs.closeSync(fd); fs.unlinkSync(file); }
  }
  function snapshot(owner) {
    return locked(owner, function () {
      var state = readUnlocked(owner);
      return { records: visible(state.records), revision: state.revision };
    });
  }
  function load(owner) { return snapshot(owner).records; }
  function revision(owner) { return snapshot(owner).revision; }
  function validate(records) {
    if (!Array.isArray(records)) throw new Error('MCP connection snapshot must be an array.');
    records.forEach(function (record, index) {
      if (!record || typeof record !== 'object') throw new Error('Invalid MCP connection record.');
      if (records.slice(0, index).some(function (previous) {
        return record.id && previous.id === record.id ||
          record.source === 'computer' && previous.source === 'computer' && record.name === previous.name ||
          record.source !== 'computer' && previous.source !== 'computer' && record.url && record.url === previous.url;
      })) throw new Error('Duplicate MCP connection in snapshot.');
      if (shared && record.source === 'computer' && record.project && record.project !== project) throw new Error('Foreign project computer preference.');
    });
  }
  // Remote rows are the complete owner snapshot. Computer rows replace only this
  // project's preferences; omitted remote rows are deleted, never merged back.
  function save(owner, records, expectedRevision) {
    validate(records);
    return locked(owner, function () {
      var state = readUnlocked(owner);
      if (expectedRevision !== undefined && state.revision !== expectedRevision) throw new Error('MCP connections changed. Please retry.');
      if (!shared) return encode(owner, { records: records, migratedProjects: [] }, state.revision);
      var incomingRemote = records.filter(function (record) { return record.source !== 'computer'; });
      var currentComputer = records.filter(function (record) { return record.source === 'computer'; }).map(function (record) { return Object.assign({}, record, { project: project }); });
      var otherComputer = state.records.filter(function (record) { return record.source === 'computer' && record.project !== project; });
      var removed = state.records.filter(function (record) {
        return record.source !== 'computer' && !incomingRemote.some(function (incoming) { return connectionConflict(record, incoming); });
      }).map(function (record) { return { id: record.id, url: record.url }; });
      state.retired = (state.retired || []).concat(removed).filter(function (record, index, all) {
        return all.findIndex(function (other) { return other.id === record.id && other.url === record.url; }) === index;
      });
      state.records = incomingRemote.concat(otherComputer, currentComputer);
      return encode(owner, state, state.revision);
    });
  }
  return { load: load, save: save, revision: revision, snapshot: snapshot, namespace: fs.existsSync(root) ? fs.realpathSync(root) : path.resolve(root) };
}
module.exports = { createConnectionStore: createConnectionStore };