Files
memind/auth.mjs
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

118 lines
2.8 KiB
JavaScript

import crypto from 'node:crypto';
export const AUTH_COOKIE = 'tkmind_h5_session';
export function parseCookies(header = '') {
return Object.fromEntries(
header
.split(';')
.map((part) => part.trim())
.filter(Boolean)
.map((part) => {
const separator = part.indexOf('=');
if (separator < 0) return [part, ''];
return [
decodeURIComponent(part.slice(0, separator)),
decodeURIComponent(part.slice(separator + 1)),
];
}),
);
}
function safeEqual(left, right) {
const leftBuffer = Buffer.from(left);
const rightBuffer = Buffer.from(right);
return (
leftBuffer.length === rightBuffer.length &&
crypto.timingSafeEqual(leftBuffer, rightBuffer)
);
}
export function createAuthManager({
password,
ttlMs = 7 * 24 * 60 * 60 * 1000,
maxFailures = 5,
failureWindowMs = 5 * 60 * 1000,
}) {
if (!password) {
throw new Error('H5_ACCESS_PASSWORD is required');
}
const sessions = new Map();
const failures = new Map();
const prune = (now = Date.now()) => {
for (const [token, expiresAt] of sessions) {
if (expiresAt <= now) sessions.delete(token);
}
for (const [ip, state] of failures) {
if (state.resetAt <= now) failures.delete(ip);
}
};
const login = (candidate, ip = 'unknown', now = Date.now()) => {
prune(now);
const failure = failures.get(ip);
if (failure && failure.count >= maxFailures && failure.resetAt > now) {
return { ok: false, retryAfterMs: failure.resetAt - now };
}
if (!safeEqual(candidate, password)) {
const current =
failure && failure.resetAt > now
? failure
: { count: 0, resetAt: now + failureWindowMs };
current.count += 1;
failures.set(ip, current);
return { ok: false, retryAfterMs: 0 };
}
failures.delete(ip);
const token = crypto.randomBytes(32).toString('base64url');
sessions.set(token, now + ttlMs);
return { ok: true, token };
};
const verify = (token, now = Date.now()) => {
if (!token) return false;
prune(now);
const expiresAt = sessions.get(token);
if (!expiresAt || expiresAt <= now) {
sessions.delete(token);
return false;
}
sessions.set(token, now + ttlMs);
return true;
};
const revoke = (token) => {
if (token) sessions.delete(token);
};
return { login, verify, revoke };
}
export function sessionCookie(token, secure) {
const parts = [
`${AUTH_COOKIE}=${encodeURIComponent(token)}`,
'Path=/',
'HttpOnly',
'SameSite=Lax',
'Max-Age=604800',
];
if (secure) parts.push('Secure');
return parts.join('; ');
}
export function clearSessionCookie(secure) {
const parts = [
`${AUTH_COOKIE}=`,
'Path=/',
'HttpOnly',
'SameSite=Lax',
'Max-Age=0',
];
if (secure) parts.push('Secure');
return parts.join('; ');
}