Files
openmaic/OpenMAIC/lib/server/access-token-policy.ts
2026-08-16 14:58:47 +08:00

82 lines
2.6 KiB
TypeScript

/**
* Runtime-neutral ACCESS_CODE session policy shared by Node routes and the
* Edge-compatible middleware verifier. Keep this module free of Node APIs.
*/
export const ACCESS_CODE_COOKIE_NAME = 'openmaic_access';
export const DEFAULT_ACCESS_CODE_SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
const MAX_ACCESS_CODE_SESSION_TTL_SECONDS = 60 * 60 * 24 * 365;
const HMAC_SHA256_HEX_LENGTH = 64;
export interface AccessTokenValidationOptions {
nowMs?: number;
ttlSeconds?: number;
}
export interface ParsedAccessToken {
issuedAtMs: number;
issuedAtText: string;
signatureHex: string;
}
export function resolveAccessCodeSessionTtlSeconds(value: string | undefined): number {
if (!value) return DEFAULT_ACCESS_CODE_SESSION_TTL_SECONDS;
const parsed = Number(value);
if (
!Number.isSafeInteger(parsed) ||
parsed <= 0 ||
parsed > MAX_ACCESS_CODE_SESSION_TTL_SECONDS
) {
return DEFAULT_ACCESS_CODE_SESSION_TTL_SECONDS;
}
return parsed;
}
export function parseAccessToken(token: string): ParsedAccessToken | null {
const dotIndex = token.indexOf('.');
if (dotIndex <= 0 || dotIndex !== token.lastIndexOf('.')) return null;
const issuedAtText = token.slice(0, dotIndex);
const signatureHex = token.slice(dotIndex + 1);
if (!/^\d+$/.test(issuedAtText)) return null;
if (!new RegExp(`^[0-9a-f]{${HMAC_SHA256_HEX_LENGTH}}$`, 'i').test(signatureHex)) return null;
const issuedAtMs = Number(issuedAtText);
if (!Number.isSafeInteger(issuedAtMs) || issuedAtMs < 0) return null;
return { issuedAtMs, issuedAtText, signatureHex };
}
/**
* Parse a token and reject sessions issued in the future or whose TTL has
* elapsed. Expiration is exclusive: a token is invalid at issuedAt + TTL.
*/
export function parseFreshAccessToken(
token: string,
options: AccessTokenValidationOptions = {},
): ParsedAccessToken | null {
const parsed = parseAccessToken(token);
if (!parsed) return null;
const nowMs = options.nowMs ?? Date.now();
const ttlSeconds = options.ttlSeconds ?? DEFAULT_ACCESS_CODE_SESSION_TTL_SECONDS;
const ttlMs = ttlSeconds * 1000;
if (!Number.isFinite(nowMs) || !Number.isSafeInteger(ttlMs) || ttlMs <= 0) return null;
if (parsed.issuedAtMs > nowMs) return null;
if (nowMs - parsed.issuedAtMs >= ttlMs) return null;
return parsed;
}
export function hexToBytes(hex: string): Uint8Array | null {
if (hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) return null;
const bytes = new Uint8Array(hex.length / 2);
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
}
return bytes;
}