/** Identity, text, URL, and collection guards for HUX-03. */ import type { RawIdentity, ScopedIdentity } from "./types.ts"; const ID = /^[a-z]{2,6}_[A-Za-z0-9._-]{4,80}$/; const TENANT = /^tnt_[0-9a-f]{16,64}$/; const USER = /^usr_[0-9a-f]{16,64}$/; const CURSOR = /^[A-Za-z0-9._~-]{1,256}$/; const TAG = /^[a-z0-9][a-z0-9-]{0,39}$/; const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/; const CONTROL = /[\u0000-\u001f\u007f]/g; const SURFACES = new Set(["chat", "worker", "telegram", "voice", "api"]); export function isOpaqueId(value: unknown, prefix?: string): value is string { return typeof value === "string" && ID.test(value) && (!prefix || value.startsWith(`${prefix}_`)); } export function isUtc(value: unknown): value is string { return typeof value === "string" && UTC.test(value) && !Number.isNaN(Date.parse(value)); } export function safeText(value: unknown, fallback: string, limit: number): string { if (typeof value !== "string") return fallback; const clean = value.replace(CONTROL, " ").replace(/\s+/g, " ").trim(); if (!clean) return fallback; return clean.length <= limit ? clean : `${clean.slice(0, limit - 1).trimEnd()}…`; } export function safeTags(value: unknown): string[] | null { if (!Array.isArray(value) || value.length > 32) return null; const tags = value.filter((tag): tag is string => typeof tag === "string"); if (tags.length !== value.length || tags.some((tag) => !TAG.test(tag))) return null; const unique = [...new Set(tags)]; return unique.length === tags.length ? unique : null; } export function normalizeIdentity(raw: unknown): ScopedIdentity | null { const value = raw as RawIdentity | null; if (!value || typeof value !== "object") return null; if (typeof value.tenant_ref !== "string" || !TENANT.test(value.tenant_ref)) return null; if (typeof value.user_ref !== "string" || !USER.test(value.user_ref)) return null; if (typeof value.surface !== "string" || !SURFACES.has(value.surface)) return null; return { tenantRef: value.tenant_ref, userRef: value.user_ref, surface: value.surface as ScopedIdentity["surface"], }; } export function sameIdentity(left: ScopedIdentity, right: ScopedIdentity): boolean { return left.tenantRef === right.tenantRef && left.userRef === right.userRef && left.surface === right.surface; } export function safeCursor(value: unknown): string | null | undefined { if (value === null || value === undefined || value === "") return null; return typeof value === "string" && CURSOR.test(value) ? value : undefined; } export function scopedPath(base: string, ...parts: string[]): string { if (!base.startsWith("/") || base.startsWith("//") || /[?#\\]/.test(base) || base.includes("..")) { throw new TypeError("HUX base URL must be a same-origin path"); } const cleanBase = base.replace(/\/$/, ""); const encoded = parts.map((part) => { if (!isOpaqueId(part)) throw new TypeError("HUX path identifier is invalid"); return encodeURIComponent(part); }); return [cleanBase, ...encoded].join("/"); } export function boundedLimit(value: unknown, fallback = 40): number { return Number.isInteger(value) && (value as number) > 0 ? Math.min(value as number, 100) : fallback; }