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
77 lines
3.4 KiB
TypeScript
77 lines
3.4 KiB
TypeScript
/** Identity, scope, URL, text, and record primitives for HUX-08. */
|
|
|
|
import type { HuxIdentity, HuxSurface, ResearchScope } 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 HASH = /^sha256:[0-9a-f]{64}$/;
|
|
const CONTROL = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
const SURFACES = new Set<HuxSurface>(["chat", "worker", "telegram", "voice", "api"]);
|
|
|
|
export function asRecord(value: unknown): Record<string, unknown> | null {
|
|
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
}
|
|
|
|
export function isId(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 isHash(value: unknown): value is string {
|
|
return typeof value === "string" && HASH.test(value);
|
|
}
|
|
|
|
export function boundedText(value: unknown, min: number, max: number): value is string {
|
|
return typeof value === "string" && value.length >= min && value.length <= max && !CONTROL.test(value);
|
|
}
|
|
|
|
export function normalizeIdentity(raw: unknown): HuxIdentity | null {
|
|
const value = asRecord(raw);
|
|
if (!value || typeof value.tenant_ref !== "string" || !TENANT.test(value.tenant_ref) ||
|
|
typeof value.user_ref !== "string" || !USER.test(value.user_ref) || !SURFACES.has(value.surface as HuxSurface)) return null;
|
|
return {tenantRef: value.tenant_ref, userRef: value.user_ref, surface: value.surface as HuxSurface};
|
|
}
|
|
|
|
export function normalizeScope(raw: unknown): ResearchScope | null {
|
|
const value = asRecord(raw);
|
|
return value && isId(value.project_id, "prj") && isId(value.conversation_id, "conv") ?
|
|
{projectId: value.project_id, conversationId: value.conversation_id} : null;
|
|
}
|
|
|
|
export function sameIdentity(left: HuxIdentity | null, right: HuxIdentity): boolean {
|
|
return Boolean(left && left.tenantRef === right.tenantRef && left.userRef === right.userRef &&
|
|
left.surface === right.surface);
|
|
}
|
|
|
|
export function sameScope(left: ResearchScope | null, right: ResearchScope): boolean {
|
|
return Boolean(left && left.projectId === right.projectId && left.conversationId === right.conversationId);
|
|
}
|
|
|
|
/** Allow HTTPS sources and same-origin HUX paths only. */
|
|
export function safeSourceUri(value: unknown): string | null {
|
|
if (value === undefined || value === null || value === "") return null;
|
|
if (typeof value !== "string" || value.length > 2_000 || CONTROL.test(value)) return null;
|
|
if (/^\/hux\/v1\/(?!.*(?:\.\.|[?#\\]))[A-Za-z0-9._~!$&'()*+,;=:@%/-]+$/.test(value)) return value;
|
|
try {
|
|
const parsed = new URL(value);
|
|
return parsed.protocol === "https:" && !parsed.username && !parsed.password ? parsed.href : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function safeMessageId(value: unknown): string {
|
|
if (!boundedText(value, 1, 120) || /[/\\?#]/.test(value)) throw new TypeError("Safe message id required");
|
|
return encodeURIComponent(value);
|
|
}
|
|
|
|
export function safePathId(value: unknown, prefix: string): string {
|
|
if (!isId(value, prefix)) throw new TypeError(`Opaque ${prefix} id required`);
|
|
return encodeURIComponent(value);
|
|
}
|