feat: add cookie-based auth utilities for browser user flows

This commit is contained in:
Codex
2026-02-18 12:44:31 +00:00
parent b85356978b
commit d95e9215e8
2 changed files with 108 additions and 0 deletions

60
src/lib/auth.js Normal file
View File

@@ -0,0 +1,60 @@
"use strict";
const COOKIE_NAME = "xartaudio_user";
function parseCookies(cookieHeader) {
if (!cookieHeader) {
return {};
}
return String(cookieHeader)
.split(";")
.map((part) => part.trim())
.filter(Boolean)
.reduce((acc, pair) => {
const eq = pair.indexOf("=");
if (eq <= 0) {
return acc;
}
const key = pair.slice(0, eq).trim();
const value = pair.slice(eq + 1).trim();
acc[key] = decodeURIComponent(value);
return acc;
}, {});
}
function serializeUserCookie(userId, maxAgeSeconds) {
if (!userId) {
throw new Error("user_id_required");
}
const encoded = encodeURIComponent(String(userId));
const maxAge = Number.isInteger(maxAgeSeconds) && maxAgeSeconds > 0
? maxAgeSeconds
: 60 * 60 * 24 * 30;
return `${COOKIE_NAME}=${encoded}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`;
}
function clearUserCookie() {
return `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
}
function getAuthenticatedUserId(headers) {
const safeHeaders = headers || {};
if (safeHeaders["x-user-id"]) {
return safeHeaders["x-user-id"];
}
const cookies = parseCookies(safeHeaders.cookie || "");
return cookies[COOKIE_NAME] || null;
}
module.exports = {
COOKIE_NAME,
parseCookies,
serializeUserCookie,
clearUserCookie,
getAuthenticatedUserId,
};

48
test/auth.test.js Normal file
View File

@@ -0,0 +1,48 @@
"use strict";
const test = require("node:test");
const assert = require("node:assert/strict");
const {
COOKIE_NAME,
parseCookies,
serializeUserCookie,
clearUserCookie,
getAuthenticatedUserId,
} = require("../src/lib/auth");
test("parseCookies handles multiple cookies", () => {
const cookies = parseCookies("a=1; xartaudio_user=user-1; b=hello%20world");
assert.equal(cookies.a, "1");
assert.equal(cookies.xartaudio_user, "user-1");
assert.equal(cookies.b, "hello world");
});
test("serializeUserCookie builds secure-ish cookie string", () => {
const cookie = serializeUserCookie("user-1", 120);
assert.match(cookie, new RegExp(`^${COOKIE_NAME}=user-1;`));
assert.match(cookie, /HttpOnly/);
assert.match(cookie, /SameSite=Lax/);
assert.match(cookie, /Max-Age=120/);
});
test("clearUserCookie expires session cookie", () => {
const cookie = clearUserCookie();
assert.match(cookie, /Max-Age=0/);
});
test("getAuthenticatedUserId prefers x-user-id header", () => {
const userId = getAuthenticatedUserId({
"x-user-id": "header-user",
cookie: "xartaudio_user=cookie-user",
});
assert.equal(userId, "header-user");
});
test("getAuthenticatedUserId falls back to cookie", () => {
const userId = getAuthenticatedUserId({
cookie: "xartaudio_user=cookie-user",
});
assert.equal(userId, "cookie-user");
});