lib/account-session-store.js
Rama main · réplica de lectura del repositorio original.
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var DAY = 86400000;
function digest(value) { return crypto.createHash('sha256').update(value).digest('hex'); }
function attachSessionStore(ctx) {
var records = {};
function save(next) {
fs.mkdirSync(path.dirname(ctx.file), { recursive: true });
fs.writeFileSync(ctx.file + '.tmp', JSON.stringify({ version: 2, sessions: next }), { mode: 0o600 });
fs.renameSync(ctx.file + '.tmp', ctx.file);
records = next;
}
function metadata(userId, managed, req) {
var now = Date.now();
return { id: crypto.randomBytes(16).toString('hex'), userId: userId, createdAt: now, lastSeenAt: now,
expiresAt: managed ? now + 30 * DAY : null, idleMs: managed ? 7 * DAY : null,
userAgent: req && String(req.headers['user-agent'] || '').slice(0, 256) || '',
address: req ? ctx.clientAddress(req) : '', observedAt: req ? now : null };
}
try {
var data = JSON.parse(fs.readFileSync(ctx.file, 'utf8'));
if (data.version === 2 && data.sessions && typeof data.sessions === 'object') records = data.sessions;
else if (data && typeof data === 'object' && !Array.isArray(data)) {
var migrated = {};
Object.keys(data).forEach(function (token) {
var old = data[token]; var userId = typeof old === 'string' ? old : old && old.userId;
if (!userId) return;
var entry = metadata(userId, ctx.managed()); entry.migrated = true;
if (old.credential) entry.credentialVersion = digest(old.credential);
if (old.passkeyId) entry.passkeyId = old.passkeyId;
migrated[digest(token)] = entry;
});
save(migrated);
}
} catch (error) {
if (error.code !== 'ENOENT') throw new Error('Could not load or migrate account sessions. Existing session data was not replaced.');
}
function live(entry) {
return !!entry && (!entry.expiresAt || Date.now() < entry.expiresAt) && (!entry.idleMs || Date.now() < entry.lastSeenAt + entry.idleMs);
}
function create(userId, credential, passkeyId, req, previousToken) {
var token = crypto.randomBytes(32).toString('hex');
var entry = metadata(userId, ctx.managed(), req);
if (credential) entry.credentialVersion = digest(credential);
if (passkeyId) entry.passkeyId = passkeyId;
var next = {};
var previousKey = previousToken ? digest(previousToken) : '';
Object.keys(records).forEach(function (key) {
if (live(records[key]) && !(key === previousKey && records[key].userId === userId)) next[key] = records[key];
});
next[digest(token)] = entry; save(next);
return { token: token, entry: entry };
}
function get(token, touch, req) {
if (typeof token !== 'string' || token.length > 256) return null;
var key = digest(token); var entry = records[key];
if (!live(entry)) return null;
if (touch && (!entry.observedAt || Date.now() - entry.lastSeenAt >= 60000)) {
var next = Object.assign({}, records); next[key] = Object.assign({}, entry, { lastSeenAt: Date.now() });
if (req) {
next[key].observedAt = Date.now();
next[key].userAgent = String(req.headers['user-agent'] || '').slice(0, 256);
next[key].address = ctx.clientAddress(req);
}
try { save(next); entry = next[key]; } catch (error) { /* Keep the earlier durable idle deadline on write failure. */ }
}
return entry;
}
function remove(test) {
var next = {}; var changed = false;
Object.keys(records).forEach(function (key) { if (test(records[key], key)) changed = true; else next[key] = records[key]; });
if (changed) save(next);
}
function list(userId) { return Object.values(records).filter(function (entry) { return entry.userId === userId && live(entry); }); }
return { create: create, get: get, list: list,
revokeUser: function (id) { remove(function (entry) { return entry.userId === id; }); },
revokeToken: function (token) { if (token) remove(function (entry, key) { return key === digest(token); }); },
revoke: function (userId, id, except) { remove(function (entry) { return entry.userId === userId && (id === 'others' ? entry.id !== except : entry.id === id); }); },
};
}
module.exports = { attachSessionStore: attachSessionStore, digest: digest };