lib/linear-store.js
Rama main · réplica de lectura del repositorio original.
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
function attachLinearStore(root) {
var directory = path.join(root, 'linear');
function filename(owner) {
if (typeof owner !== 'string' || !/^[a-zA-Z0-9_-]{1,128}$/.test(owner)) throw new Error('Invalid Linear account owner.');
return path.join(directory, owner + '.json');
}
function read(owner) {
try { return JSON.parse(fs.readFileSync(filename(owner), 'utf8')); }
catch (error) { if (error.code === 'ENOENT') return {}; throw new Error('Could not read Linear connection.'); }
}
function masterKey() {
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
var file = path.join(directory, '.key');
try { fs.writeFileSync(file, crypto.randomBytes(32), { flag: 'wx', mode: 0o600 }); }
catch (error) { if (error.code !== 'EEXIST') throw error; }
return fs.readFileSync(file);
}
function record(owner, id) {
var data = read(owner);
return !id || id === "default" ? data : Object.hasOwn(data.connections || {}, id) ? data.connections[id] : {};
}
function key(owner, id) {
var data = record(owner, id);
if (!data.secret) return null;
var parts = data.secret.split('.').map(function (part) { return Buffer.from(part, 'base64'); });
var cipher = crypto.createDecipheriv('aes-256-gcm', masterKey(), parts[0]);
cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner)); cipher.setAuthTag(parts[1]);
return Buffer.concat([cipher.update(parts[2]), cipher.final()]).toString('utf8');
}
function view(owner, id) {
var data = record(owner, id);
return { connected: !!data.secret, workspace: data.workspace || null, revision: data.revision || null };
}
function save(owner, secret, workspace, id, name) {
if (id && (!/^[a-zA-Z0-9_-]{1,64}$/.test(id) || ['__proto__', 'constructor', 'prototype'].indexOf(id) >= 0)) throw new Error("Invalid connection.");
var previous = read(owner);
var file = filename(owner), data = { revision: crypto.randomBytes(16).toString('hex') };
if (secret !== null) {
if (typeof secret !== 'string' || !secret.trim() || secret.length > 4096 || /[\r\n]/.test(secret)) throw new Error('Enter a valid Linear API key.');
var iv = crypto.randomBytes(12), cipher = crypto.createCipheriv('aes-256-gcm', masterKey(), iv);
cipher.setAAD(Buffer.from(id && id !== 'default' ? owner + ':' + id : owner));
var bytes = Buffer.concat([cipher.update(secret.trim(), 'utf8'), cipher.final()]);
data.secret = [iv, cipher.getAuthTag(), bytes].map(function (part) { return part.toString('base64'); }).join('.');
data.workspace = workspace;
}
if (name !== undefined) {
if (typeof name !== 'string' || !name.trim() || name.trim().length > 80) throw new Error('Enter a connection name of up to 80 characters.');
data.name = name.trim();
} else data.name = record(owner, id).name;
if (id && id !== 'default') {
previous.connections = previous.connections || {};
if (secret === null) delete previous.connections[id];
else {
if (!previous.connections[id] && Object.keys(previous.connections).length >= 30) throw new Error('Connection limit reached.');
previous.connections[id] = data;
}
data = previous;
} else data.connections = previous.connections;
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
var temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp';
try { fs.writeFileSync(temp, JSON.stringify(data), { mode: 0o600 }); fs.renameSync(temp, file); }
finally { try { fs.unlinkSync(temp); } catch (error) { if (error.code !== 'ENOENT') throw error; } }
return view(owner, id);
}
function list(owner) {
var data = read(owner), entries = [];
if (data.secret) entries.push(Object.assign({ id: 'default', name: data.name || 'Existing connection', legacy: true }, view(owner)));
Object.keys(data.connections || {}).forEach(function (id) {
entries.push(Object.assign({ id: id, name: data.connections[id].name || 'Linear', legacy: false }, view(owner, id)));
});
return entries;
}
return { key: key, view: view, save: save, list: list };
}
module.exports = { attachLinearStore: attachLinearStore };