Standalone per-card browser model/security/view modules for HUX-01..10 plus node+pytest suites that read the hux.v1 contract schemas directly. Reconciled drift found on integration: the activity model now accepts all 32 hux.event.v1 kinds (delegation.*, memory.suppressed, memory.retrieval_removed, budget.exhausted, side_effect.*), the autonomy model carries the external_side_effect capability, and the foundation boundary test now asserts the shipped static HUX surface exists on disk and that images never bake activated HUX_FLAGS. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
60 lines
2.6 KiB
TypeScript
60 lines
2.6 KiB
TypeScript
/** Small, fail-closed validation helpers for HUX-05. */
|
|
|
|
import type { HuxIdentity } 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 UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/;
|
|
const CONTROLS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
const SECRET = /\b(password|token|api[-_ ]?key|secret|authorization|cookie)\b\s*[:=]\s*[^\s,;]+/gi;
|
|
const BEARER = /\bbearer\s+[A-Za-z0-9._~+/-]+=*/gi;
|
|
|
|
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 = 280): string {
|
|
if (typeof value !== "string") return fallback;
|
|
const clean = value.replace(SECRET, "$1=[redacted]").replace(BEARER, "Bearer [redacted]")
|
|
.replace(CONTROLS, " ").replace(/\s+/g, " ").trim();
|
|
if (!clean) return fallback;
|
|
return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
|
|
}
|
|
|
|
export function normalizeIdentity(raw: unknown): HuxIdentity | null {
|
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
const value = raw as Record<string, unknown>;
|
|
const tenantRef = value.tenant_ref ?? value.tenantRef;
|
|
const userRef = value.user_ref ?? value.userRef;
|
|
const surfaces = new Set(["chat", "worker", "telegram", "voice", "api"]);
|
|
if (typeof tenantRef !== "string" || !TENANT.test(tenantRef) ||
|
|
typeof userRef !== "string" || !USER.test(userRef) ||
|
|
typeof value.surface !== "string" || !surfaces.has(value.surface)) return null;
|
|
return {tenantRef, userRef, surface: value.surface as HuxIdentity["surface"]};
|
|
}
|
|
|
|
export function sameIdentity(left: HuxIdentity, right: HuxIdentity): boolean {
|
|
return left.tenantRef === right.tenantRef && left.userRef === right.userRef &&
|
|
left.surface === right.surface;
|
|
}
|
|
|
|
export function identityKey(value: HuxIdentity): string {
|
|
return `${value.tenantRef}:${value.userRef}:${value.surface}`;
|
|
}
|
|
|
|
export function exactKeys(value: object, keys: readonly string[]): boolean {
|
|
const actual = Object.keys(value).sort();
|
|
return actual.length === keys.length && actual.every((key, index) => key === [...keys].sort()[index]);
|
|
}
|
|
|
|
export function boundedInteger(value: unknown, maximum: number): number | null {
|
|
return Number.isSafeInteger(value) && (value as number) >= 0 &&
|
|
(value as number) <= maximum ? value as number : null;
|
|
}
|