Files
Termix/plugins/termix-identity/src/backend/handles.ts
T

70 lines
2.2 KiB
TypeScript
Raw Normal View History

2026-10-01 16:20:16 -05:00
/** A GitHub-like public namespace: lowercase, starts alphanumeric, 1-39 chars. */
export const HANDLE_REGEX = /^[a-z0-9][a-z0-9_-]{0,38}$/;
const RESERVED_HANDLES = new Set(["u", "me", "keys", "check", "admin", "api"]);
const MAX_DESCRIPTION_LENGTH = 500;
export function isValidHandle(handle: string): boolean {
return HANDLE_REGEX.test(handle) && !RESERVED_HANDLES.has(handle);
}
export function cleanDescription(value: unknown): string | null {
if (typeof value !== "string") return null;
return value.trim().slice(0, MAX_DESCRIPTION_LENGTH) || null;
}
export function cleanLabel(value: unknown): string | null {
return typeof value === "string" ? value.trim() || null : null;
}
/** Validity in days: a positive whole number, at most ten years. */
export function clampValidityDays(value: unknown, fallback: number): number {
const days = Number(value);
if (!Number.isFinite(days) || days <= 0) return fallback;
return Math.min(Math.floor(days), 3650);
}
interface DbError {
code?: string;
errno?: number;
message?: string;
cause?: unknown;
}
function errorChain(error: unknown): DbError[] {
const chain: DbError[] = [];
let current = error as DbError | undefined;
while (current && typeof current === "object" && chain.length < 5) {
chain.push(current);
current = current.cause as DbError | undefined;
}
return chain;
}
/** A UNIQUE violation on sqlite, Postgres or MySQL. */
export function isUniqueViolation(error: unknown): boolean {
return errorChain(error).some(
(entry) =>
entry.code === "SQLITE_CONSTRAINT_UNIQUE" ||
entry.code === "23505" ||
entry.code === "ER_DUP_ENTRY" ||
entry.errno === 1062 ||
(typeof entry.message === "string" &&
entry.message.includes("UNIQUE constraint failed")),
);
}
/** Whether a UNIQUE violation was on user_id (one handle per user) rather than the handle. */
export function isUserIdViolation(error: unknown): boolean {
return errorChain(error).some(
(entry) =>
typeof entry.message === "string" &&
(entry.message.includes("user_id") || entry.message.includes("userId")),
);
}
export function asError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error));
}