/** Identity, scope, trigger and time guards for HUX-09. */ import type { HuxSurface, OnboardingScope, RawIdentity, ScopedIdentity, SuggestionContext, SuggestionTrigger, } 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 CONTROL = /[\u0000-\u001f\u007f]/; const SURFACES = new Set(["chat", "worker", "telegram", "voice", "api"]); const CONTEXTS = new Set([ "first_session", "empty_project", "after_artifact", "after_research", "after_approval", "idle", ]); export function exactKeys( value: Record, required: readonly string[], optional: readonly string[] = [], ): boolean { const allowed = new Set([...required, ...optional]); return required.every((key) => Object.prototype.hasOwnProperty.call(value, key)) && Object.keys(value).every((key) => allowed.has(key)); } export function asRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? value as Record : 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 isSafeText(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): ScopedIdentity | null { const value = raw as RawIdentity | null; if (!value || typeof value !== "object" || Array.isArray(value)) return null; if (!exactKeys(value as Record, ["tenant_ref", "user_ref", "surface"])) return null; if (typeof value.tenant_ref !== "string" || !TENANT.test(value.tenant_ref) || typeof value.user_ref !== "string" || !USER.test(value.user_ref) || typeof value.surface !== "string" || !SURFACES.has(value.surface as HuxSurface)) return null; return {tenantRef: value.tenant_ref, userRef: value.user_ref, surface: value.surface as HuxSurface}; } export function sameIdentity(left: ScopedIdentity, right: ScopedIdentity): boolean { return left.tenantRef === right.tenantRef && left.userRef === right.userRef && left.surface === right.surface; } export function normalizeTrigger(raw: unknown): SuggestionTrigger | null { const value = asRecord(raw); if (!value || !exactKeys(value, ["surface", "context"]) || !SURFACES.has(value.surface as HuxSurface) || !CONTEXTS.has(value.context as SuggestionContext)) return null; return {surface: value.surface as HuxSurface, context: value.context as SuggestionContext}; } export function sameTrigger(left: SuggestionTrigger, right: SuggestionTrigger): boolean { return left.surface === right.surface && left.context === right.context; } export function normalizeScope(raw: OnboardingScope, identity: ScopedIdentity): OnboardingScope | null { const value = asRecord(raw); if (!value || !exactKeys(value, ["sessionId", "conversationId", "trigger"]) || !isId(value.sessionId, "ses") || !isId(value.conversationId, "conv")) return null; const trigger = normalizeTrigger(value.trigger); if (!trigger || trigger.surface !== identity.surface) return null; return {sessionId: value.sessionId, conversationId: value.conversationId, trigger}; } export function scopedSuggestionPath(base: string, suggestionId: string): string { if (!base.startsWith("/") || base.startsWith("//") || /[?#\\]/.test(base) || base.includes("..") || !isId(suggestionId, "sug")) { throw new TypeError("Onboarding path is not safely scoped"); } return `${base.replace(/\/$/, "")}/${encodeURIComponent(suggestionId)}/decisions`; }