release: WRNexusJS 0.7.0

This commit is contained in:
2026-08-01 10:04:42 +05:30
parent c54144f2e4
commit 87507edf59
207 changed files with 12607 additions and 679 deletions
+107 -21
View File
@@ -37,15 +37,47 @@ export interface LocalStorageSnapshot {
}
const SESSION_COOKIE = "wrnexus.sid";
/** Idle timeout: a session expires this long after its last access. */
const SESSION_TTL_MS = 1000 * 60 * 60 * 24; // 24 hours
/** Run a background sweep after this many new sessions (bounds memory). */
const SESSION_TTL_MS = 1000 * 60 * 60 * 24;
const SESSION_ABSOLUTE_TTL_MS = 1000 * 60 * 60 * 24 * 7;
const SESSION_GC_EVERY = 500;
export interface SessionPolicy {
cookieName?: string;
idleTimeoutMs?: number;
absoluteTimeoutMs?: number;
sameSite?: NonNullable<CookieOptions["sameSite"]>;
secure?: boolean;
}
let sessionPolicy: Required<
Pick<SessionPolicy, "cookieName" | "idleTimeoutMs" | "absoluteTimeoutMs" | "sameSite">
> &
Pick<SessionPolicy, "secure"> = {
cookieName: SESSION_COOKIE,
idleTimeoutMs: SESSION_TTL_MS,
absoluteTimeoutMs: SESSION_ABSOLUTE_TTL_MS,
sameSite: "Lax",
};
export function setSessionPolicy(policy: SessionPolicy): void {
sessionPolicy = {
...sessionPolicy,
...policy,
idleTimeoutMs: Math.max(60_000, policy.idleTimeoutMs ?? sessionPolicy.idleTimeoutMs),
absoluteTimeoutMs: Math.max(
policy.idleTimeoutMs ?? sessionPolicy.idleTimeoutMs,
policy.absoluteTimeoutMs ?? sessionPolicy.absoluteTimeoutMs,
),
};
}
/** A stored session: its data plus an absolute expiry timestamp (ms). */
export interface SessionEntry {
data: Record<string, unknown>;
expiresAt: number;
/** Creation time used for the absolute session lifetime. Optional for old backends. */
createdAt?: number;
lastAccessAt?: number;
}
/**
@@ -70,7 +102,13 @@ function createMemorySessionBackend(): SessionBackend {
set: (id, entry) => void map.set(id, entry),
delete: (id) => void map.delete(id),
gc: (now) => {
for (const [key, entry] of map) if (entry.expiresAt <= now) map.delete(key);
for (const [key, entry] of map) {
if (
entry.expiresAt <= now ||
(entry.createdAt ?? now) + sessionPolicy.absoluteTimeoutMs <= now
)
map.delete(key);
}
},
};
}
@@ -102,13 +140,26 @@ export interface AsyncSessionBackend {
*/
export function loadSession(
backend: AsyncSessionBackend,
options: { ttlMs?: number } = {},
options: {
ttlMs?: number;
absoluteTtlMs?: number;
cookieName?: string;
sameSite?: NonNullable<CookieOptions["sameSite"]>;
secure?: boolean;
} = {},
): Middleware {
const ttlMs = options.ttlMs ?? SESSION_TTL_MS;
const ttlMs = options.ttlMs ?? sessionPolicy.idleTimeoutMs;
const absoluteTtlMs = options.absoluteTtlMs ?? sessionPolicy.absoluteTimeoutMs;
const cookieName = options.cookieName ?? sessionPolicy.cookieName;
return async (ctx: Context, next) => {
let id = ctx.cookies.get(SESSION_COOKIE);
let id = ctx.cookies.get(cookieName);
let entry = id ? await backend.load(id) : undefined;
if (id && entry && entry.expiresAt <= Date.now()) {
if (
id &&
entry &&
(entry.expiresAt <= Date.now() ||
(entry.createdAt ?? Date.now()) + absoluteTtlMs <= Date.now())
) {
await backend.destroy(id);
entry = undefined;
id = undefined;
@@ -120,9 +171,16 @@ export function loadSession(
const ensure = (): Record<string, unknown> => {
if (!id) {
id = randomId();
ctx.cookies.set(SESSION_COOKIE, id, sessionCookieOptions(ctx.url.protocol === "https:"));
ctx.cookies.set(
cookieName,
id,
sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite),
);
}
if (!entry) {
const now = Date.now();
entry = { data: {}, expiresAt: now + ttlMs, createdAt: now, lastAccessAt: now };
}
if (!entry) entry = { data: {}, expiresAt: Date.now() + ttlMs };
return entry.data;
};
@@ -147,14 +205,22 @@ export function loadSession(
const data = entry?.data ?? {};
if (id) destroys.add(id);
id = randomId();
entry = { data, expiresAt: Date.now() + ttlMs };
ctx.cookies.set(SESSION_COOKIE, id, sessionCookieOptions(ctx.url.protocol === "https:"));
const now = Date.now();
entry = { data, expiresAt: now + ttlMs, createdAt: now, lastAccessAt: now };
ctx.cookies.set(
cookieName,
id,
sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite),
);
},
clear() {
if (id) destroys.add(id);
entry = undefined;
id = undefined;
ctx.cookies.delete(SESSION_COOKIE, sessionCookieOptions(ctx.url.protocol === "https:"));
ctx.cookies.delete(
cookieName,
sessionCookieOptions(options.secure ?? ctx.url.protocol === "https:", options.sameSite),
);
},
};
@@ -164,6 +230,7 @@ export function loadSession(
for (const gone of destroys) if (gone !== id) await backend.destroy(gone);
if (id && entry) {
entry.expiresAt = Date.now() + ttlMs;
entry.lastAccessAt = Date.now();
await backend.save(id, entry);
}
}
@@ -176,11 +243,14 @@ const COOKIE_NAME = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
function readSessionEntry(id: string): SessionEntry | undefined {
const entry = sessionBackend.get(id);
if (!entry) return undefined;
if (entry.expiresAt <= Date.now()) {
const now = Date.now();
if (entry.expiresAt <= now || (entry.createdAt ?? now) + sessionPolicy.absoluteTimeoutMs <= now) {
sessionBackend.delete(id);
return undefined;
}
entry.expiresAt = Date.now() + SESSION_TTL_MS; // sliding idle expiry
entry.lastAccessAt = now;
entry.createdAt ??= now;
entry.expiresAt = now + sessionPolicy.idleTimeoutMs; // sliding idle expiry
sessionBackend.set(id, entry); // persist the slide (matters for external backends)
return entry;
}
@@ -223,8 +293,8 @@ export function createCookieStore(req: Request): CookieStore {
export function createSessionStore(
cookies: CookieStore,
req: Request,
cookieName = SESSION_COOKIE,
secure = new URL(req.url).protocol === "https:",
cookieName = sessionPolicy.cookieName,
secure = sessionPolicy.secure ?? new URL(req.url).protocol === "https:",
): SessionStore {
let id = cookies.get(cookieName);
let entry = id ? readSessionEntry(id) : undefined;
@@ -245,7 +315,13 @@ export function createSessionStore(
sessionsSinceGc = 0;
sessionBackend.gc?.(Date.now());
}
entry = { data: {}, expiresAt: Date.now() + SESSION_TTL_MS };
const now = Date.now();
entry = {
data: {},
expiresAt: now + sessionPolicy.idleTimeoutMs,
createdAt: now,
lastAccessAt: now,
};
sessionBackend.set(id, entry);
}
return entry.data;
@@ -278,7 +354,13 @@ export function createSessionStore(
const data = entry?.data ?? {};
if (id) sessionBackend.delete(id);
id = randomId();
entry = { data, expiresAt: Date.now() + SESSION_TTL_MS };
const now = Date.now();
entry = {
data,
expiresAt: now + sessionPolicy.idleTimeoutMs,
createdAt: now,
lastAccessAt: now,
};
sessionBackend.set(id, entry);
cookies.set(cookieName, id, sessionCookieOptions(secure));
},
@@ -341,11 +423,14 @@ function serializeCookie(name: string, value: string, options: CookieOptions): s
return parts.join("; ");
}
function sessionCookieOptions(secure: boolean): CookieOptions {
function sessionCookieOptions(
secure: boolean,
sameSite: NonNullable<CookieOptions["sameSite"]> = sessionPolicy.sameSite,
): CookieOptions {
return {
httpOnly: true,
path: "/",
sameSite: "Lax",
sameSite,
secure,
};
}
@@ -358,6 +443,7 @@ function parseLocalStorageHeader(header: string | null): Record<string, string>
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(parsed)) {
if (key === "__proto__" || key === "prototype" || key === "constructor") continue;
if (typeof value === "string") out[key] = value;
}
return out;