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
79 lines
3.2 KiB
TypeScript
79 lines
3.2 KiB
TypeScript
/** 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;
|
|
}
|
|
|