65 lines
3.0 KiB
JavaScript
65 lines
3.0 KiB
JavaScript
"use strict";
|
|
const crypto = require("node:crypto");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
class SessionManager {
|
|
constructor({ absoluteTtl = 12 * 3600, idleTtl = 3600, stateFile = "" } = {}) {
|
|
this.absoluteTtl = absoluteTtl;
|
|
this.idleTtl = idleTtl;
|
|
this.stateFile = stateFile;
|
|
this.sessions = new Map();
|
|
this.load();
|
|
}
|
|
create(identity, server, scopes, actions = [], now = Date.now() / 1000) {
|
|
if (typeof actions === "number") { now = actions; actions = []; }
|
|
const token = crypto.randomBytes(32).toString("hex");
|
|
const hash = this.hash(token);
|
|
this.sessions.set(hash, { pid: identity.pid, name: identity.name, server, scopes: [...new Set(scopes)], actions: [...new Set(actions)], created: now, last_used: now, expires: now + this.absoluteTtl });
|
|
this.persist();
|
|
return { token, expires_in: this.absoluteTtl, idle_timeout: this.idleTtl };
|
|
}
|
|
verify(token, identity, server, scope, action = "", now = Date.now() / 1000) {
|
|
if (typeof action === "number") { now = action; action = ""; }
|
|
const hash = this.hash(token || "");
|
|
const session = this.sessions.get(hash);
|
|
if (!session) return { ok: false, reason: "session_not_found" };
|
|
if (now > session.expires || now - session.last_used > this.idleTtl) { this.sessions.delete(hash); this.persist(); return { ok: false, reason: "session_expired" }; }
|
|
if (session.pid !== identity.pid || session.server !== server) return { ok: false, reason: "session_binding_mismatch" };
|
|
if (!session.scopes.includes(scope)) return { ok: false, reason: "session_scope_denied" };
|
|
if (!Array.isArray(session.actions) || !session.actions.includes(action)) return { ok: false, reason: "session_action_denied" };
|
|
session.last_used = now;
|
|
this.persist();
|
|
return { ok: true, session };
|
|
}
|
|
end(token, identity) {
|
|
const hash = this.hash(token || "");
|
|
const session = this.sessions.get(hash);
|
|
if (!session || session.pid !== identity.pid) return false;
|
|
this.sessions.delete(hash);
|
|
this.persist();
|
|
return true;
|
|
}
|
|
load(now = Date.now() / 1000) {
|
|
if (!this.stateFile || !fs.existsSync(this.stateFile)) return;
|
|
try {
|
|
const state = JSON.parse(fs.readFileSync(this.stateFile, "utf8"));
|
|
for (const [hash, session] of Object.entries(state.sessions || {})) {
|
|
if (typeof hash !== "string" || !session || now > session.expires || now - session.last_used > this.idleTtl) continue;
|
|
this.sessions.set(hash, session);
|
|
}
|
|
} catch {
|
|
this.sessions.clear();
|
|
}
|
|
}
|
|
persist() {
|
|
if (!this.stateFile) return;
|
|
fs.mkdirSync(path.dirname(this.stateFile), { recursive: true, mode: 0o700 });
|
|
const temp = `${this.stateFile}.${process.pid}.tmp`;
|
|
fs.writeFileSync(temp, JSON.stringify({ version: 1, sessions: Object.fromEntries(this.sessions) }), { mode: 0o600 });
|
|
fs.renameSync(temp, this.stateFile);
|
|
}
|
|
hash(token) { return crypto.createHash("sha256").update(token).digest("hex"); }
|
|
}
|
|
module.exports = { SessionManager };
|