/* node:coverage disable */ (function (root, factory) { 'use strict'; const foundation = typeof module === 'object' && module.exports ? require('../foundation.js') : root.HermesHuxFoundation; const api = factory(foundation); if (typeof module === 'object' && module.exports) module.exports = api; else root.HermesHuxWaveAContract = api; }(typeof globalThis === 'object' ? globalThis : this, function (foundation) { /* node:coverage enable */ 'use strict'; const ACCEPT = 'application/vnd.hermes.hux+json; version=1'; const ID = /^[a-z]{2,6}_[A-Za-z0-9._-]{4,80}$/; const USER = /^usr_[0-9a-f]{16,64}$/; const SLOT = /^slot-[0-9]{1,3}$/; const UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/; const CARD_FLAGS = Object.freeze({ 'HUX-11': 'hux.foundation', 'HUX-01': 'hux.activity_timeline', 'HUX-02': 'hux.memory_control', 'HUX-03': 'hux.projects', 'HUX-04': 'hux.artifacts', 'HUX-05': 'hux.autonomy', 'HUX-06': 'hux.friendly_modes', 'HUX-07': 'hux.multimodal', 'HUX-08': 'hux.research', 'HUX-09': 'hux.onboarding', 'HUX-10': 'hux.privacy', 'HUX-12': 'hux.release_followthrough', }); const DEPENDENCIES = Object.freeze({ 'hux.activity_timeline': ['hux.foundation'], 'hux.memory_control': ['hux.foundation', 'hux.privacy'], 'hux.projects': ['hux.foundation'], 'hux.artifacts': ['hux.foundation', 'hux.projects'], 'hux.autonomy': ['hux.foundation', 'hux.activity_timeline'], 'hux.friendly_modes': ['hux.foundation'], 'hux.multimodal': ['hux.foundation', 'hux.artifacts', 'hux.autonomy'], 'hux.research': ['hux.foundation', 'hux.friendly_modes'], 'hux.onboarding': ['hux.foundation', 'hux.projects', 'hux.friendly_modes'], 'hux.privacy': ['hux.foundation'], 'hux.foundation': [], 'hux.release_followthrough': [], }); const EVENT_KINDS = new Set([ 'message.user', 'message.assistant', 'decision.route', 'decision.plan', 'tool.call', 'tool.result', 'approval.requested', 'approval.resolved', 'memory.proposed', 'memory.committed', 'memory.forgotten', 'artifact.created', 'artifact.version', 'artifact.promoted', 'citation.attached', 'mode.changed', 'run.started', 'run.cancelled', 'run.completed', 'run.failed', 'privacy.notice', 'suggestion.shown', 'suggestion.dismissed', 'release.transition', 'delegation.started', 'delegation.completed', 'delegation.failed', 'memory.suppressed', 'memory.retrieval_removed', 'budget.exhausted', 'side_effect.blocked', 'side_effect.released', ]); const EVIDENCE = new Set([ 'message', 'tool_call', 'tool_result', 'artifact_version', 'source', 'passage', 'memory', 'approval', 'run', 'url', 'file', 'build', 'flux', 'pod', ]); const SECRET_PATTERNS = [ [/\b(password|passwd|token|secret|api[-_ ]?key|authorization|cookie)\b\s*[:=]\s*[^\s,;]+/gi, '$1=[redacted]'], [/\bbearer\s+[A-Za-z0-9._~+/-]+=*/gi, 'Bearer [redacted]'], [/-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/gi, '[redacted]'], ]; class WaveAContractError extends Error { constructor(message) { super(message); this.name = 'WaveAContractError'; } } function immutable(value) { if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value; Object.values(value).forEach(immutable); return Object.freeze(value); } function record(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : null; } function isId(value, prefix) { return typeof value === 'string' && ID.test(value) && (!prefix || value.startsWith(`${prefix}_`)); } function isUtc(value) { return typeof value === 'string' && UTC.test(value) && !Number.isNaN(Date.parse(value)); } function safeText(value, fallback, limit) { if (typeof value !== 'string') return fallback; let clean = value.replace(/[\u0000-\u001f\u007f]/g, ' ').replace(/\s+/g, ' ').trim(); SECRET_PATTERNS.forEach(([pattern, replacement]) => { clean = clean.replace(pattern, replacement); }); if (!clean) return fallback; return clean.length <= limit ? clean : `${clean.slice(0, Math.max(0, limit - 1)).trimEnd()}…`; } function normalizeExpectedIdentity(value) { if (!record(value) || !SLOT.test(value.tenantSlot) || !USER.test(value.userRef) || !['chat', 'worker', 'telegram', 'voice', 'api'].includes(value.surface) || !['router', 'relay', 'worker'].includes(value.trust)) { throw new WaveAContractError('Canonical HUX identity is invalid'); } return immutable({tenantSlot: value.tenantSlot, userRef: value.userRef, surface: value.surface, trust: value.trust}); } function sameCanonicalIdentity(raw, expected) { return Boolean(record(raw) && raw.tenant_slot === expected.tenantSlot && raw.subject === expected.userRef && raw.surface === expected.surface && raw.trust === expected.trust); } function safeBase(value) { const base = String(value || '/hux/v1').replace(/\/$/, ''); if (!base.startsWith('/') || base.startsWith('//') || /[?#\\]/.test(base) || base.includes('..')) { throw new TypeError('HUX base URL must be same-origin'); } return base; } function safeEndpoint(base, path) { const value = String(path || ''); if (!value.startsWith('/') || value.startsWith('//') || value.includes('..') || /[\\]|%(?:2e|2f|5c)/i.test(value)) throw new TypeError('HUX endpoint must be a scoped relative path'); return base + value; } function safeIdempotencyKey(value) { if (typeof value !== 'string' || !/^[A-Za-z0-9._:-]{8,120}$/.test(value)) { throw new TypeError('Canonical Idempotency-Key is invalid'); } return value; } function normalizeCapabilities(payload, expected) { if (!record(payload) || payload.schema !== 'hux.capabilities.v1' || !/^1\.\d+\.\d+$/.test(String(payload.contract_version)) || !sameCanonicalIdentity(payload.identity, expected) || !Array.isArray(payload.cards) || payload.cards.length > foundation.FLAGS.length * 2) { throw new WaveAContractError('Canonical HUX capabilities could not be verified'); } const enabled = new Set(); const seen = new Set(); payload.cards.forEach((card) => { if (!record(card) || typeof card.card !== 'string' || seen.has(card.card) || typeof card.flag !== 'string' || typeof card.enabled !== 'boolean' || !Array.isArray(card.routes) || card.routes.some((path) => typeof path !== 'string' || !path.startsWith('/hux/v1/')) || (CARD_FLAGS[card.card] && CARD_FLAGS[card.card] !== card.flag)) { throw new WaveAContractError('Canonical HUX capability card is invalid'); } seen.add(card.card); if (card.enabled && foundation.FLAGS.includes(card.flag)) enabled.add(card.flag); }); function dependenciesEnabled(flag, seen) { if (!enabled.has(flag) || seen.has(flag)) return enabled.has(flag); return DEPENDENCIES[flag].every((dependency) => dependenciesEnabled(dependency, new Set(seen).add(flag))); } const resolved = [...enabled].filter((flag) => dependenciesEnabled(flag, new Set())); if (!enabled.has('hux.foundation')) resolved.length = 0; return immutable({schema: payload.schema, contractVersion: payload.contract_version, identity: expected, flags: resolved}); } function createCanonicalClient(options) { const settings = options || {}; const fetcher = Object.prototype.hasOwnProperty.call(settings, 'fetcher') ? settings.fetcher : (typeof fetch === 'function' ? fetch.bind(globalThis) : null); if (!fetcher) throw new TypeError('A fetch implementation is required'); const identity = normalizeExpectedIdentity(settings.expectedIdentity); const base = safeBase(settings.baseUrl); let state = immutable({phase: 'disabled', capabilities: null, error: null}); let generation = 0; const listeners = new Set(); function publish(next) { state = immutable(next); listeners.forEach((listener) => listener(state)); return state; } async function negotiate() { const current = ++generation; publish({phase: 'loading', capabilities: null, error: null}); try { const response = await fetcher(`${base}/capabilities`, {cache: 'no-store', credentials: 'same-origin', headers: {Accept: ACCEPT}}); if (current !== generation) return state; if (response.status === 404) return publish({phase: 'disabled', capabilities: null, error: null}); if (!response.ok) throw new Error('capabilities unavailable'); const capabilities = normalizeCapabilities(await response.json(), identity); return publish(capabilities.flags.includes('hux.foundation') ? {phase: 'ready', capabilities, error: null} : {phase: 'disabled', capabilities: null, error: null}); } catch (error) { if (current !== generation) return state; return publish({phase: 'error', capabilities: null, error: error instanceof WaveAContractError ? 'Hermes workspace identity could not be verified.' : 'Hermes workspace features are temporarily unavailable.'}); } } function subscribe(listener) { if (typeof listener !== 'function') throw new TypeError('Listener must be a function'); listeners.add(listener); listener(state); return () => listeners.delete(listener); } async function request(path, init) { if (state.phase !== 'ready') throw new WaveAContractError('HUX capabilities are not ready'); const settings = init || {}; const headers = {Accept: ACCEPT, ...(settings.headers || {})}; const body = Object.prototype.hasOwnProperty.call(settings, 'body') ? JSON.stringify(settings.body) : undefined; if (body !== undefined) headers['Content-Type'] = 'application/json'; const response = await fetcher(safeEndpoint(base, path), {cache: 'no-store', credentials: 'same-origin', method: settings.method || 'GET', headers, ...(body === undefined ? {} : {body})}); if (!response.ok) { const error = new WaveAContractError(response.status === 409 ? 'This item changed. Refresh before trying again.' : `Hermes workspace request failed (${response.status}).`); error.status = response.status; throw error; } return Object.freeze({body: await response.json(), etag: response.headers && typeof response.headers.get === 'function' ? response.headers.get('ETag') : null}); } return Object.freeze({apiVersion: foundation.API_VERSION, identity, getState: () => state, negotiate, subscribe, request, enabled: (flag) => state.phase === 'ready' && foundation.FLAGS.includes(flag) && state.capabilities.flags.includes(flag), endpoint: (path) => safeEndpoint(base, path)}); } function assertRecordIdentity(raw, expected) { if (!sameCanonicalIdentity(raw && raw.identity, expected)) { throw new WaveAContractError('HUX record crossed its tenant or user boundary'); } } function normalizeEvent(raw, expected, conversationId) { assertRecordIdentity(raw, expected); if (!record(raw) || raw.schema !== 'hux.event.v1' || !isId(raw.id, 'evt') || !Number.isSafeInteger(raw.seq) || raw.seq < 0 || !isUtc(raw.ts) || raw.conversation_id !== conversationId || !EVENT_KINDS.has(raw.kind) || !['public', 'personal', 'sensitive', 'restricted'].includes(raw.sensitivity) || !record(raw.redaction) || !['none', 'partial', 'full'].includes(raw.redaction.level)) { throw new WaveAContractError('Activity event is invalid or out of scope'); } const hidden = raw.redaction.level === 'full'; const evidence = Array.isArray(raw.evidence) ? raw.evidence.slice(0, 64).map((item) => ({ kind: String(item && item.kind || ''), id: safeText(item && item.id, '', 200), })).filter((item) => EVIDENCE.has(item.kind) && item.id) : []; return immutable({id: raw.id, seq: raw.seq, timestamp: raw.ts, kind: raw.kind, summary: hidden ? 'Details hidden by privacy controls.' : safeText(raw.summary, 'Activity details unavailable.', 280), sensitivity: raw.sensitivity, redacted: hidden, evidence}); } function normalizeEventPage(raw, expected, conversationId) { if (!record(raw) || !Array.isArray(raw.items) || raw.items.length > 200 || !(raw.next === null || Number.isSafeInteger(raw.next))) throw new WaveAContractError('Activity page is invalid'); const items = raw.items.map((item) => normalizeEvent(item, expected, conversationId)); if (items.some((item, index) => index > 0 && item.seq <= items[index - 1].seq)) { throw new WaveAContractError('Activity sequence is not monotonic'); } return immutable({items, next: raw.next}); } function normalizeScope(raw, conversationId) { if (!record(raw) || !['global', 'project', 'conversation'].includes(raw.level) || (raw.level !== 'global' && !isId(raw.scope_id)) || (raw.level === 'conversation' && raw.scope_id !== conversationId)) { throw new WaveAContractError('Memory scope crossed this conversation'); } return raw.level === 'global' ? immutable({level: 'global', scopeId: null}) : immutable({level: raw.level, scopeId: raw.scope_id}); } function normalizeMemory(raw, expected, conversationId) { assertRecordIdentity(raw, expected); if (!record(raw) || raw.schema !== 'hux.memory.v1' || !isId(raw.id, 'mem') || raw.owner !== expected.userRef || !Number.isSafeInteger(raw.revision) || raw.revision < 1 || !['preference', 'fact', 'instruction', 'context'].includes(raw.kind) || !['proposed', 'active', 'rejected', 'expired', 'forgotten', 'no_store'].includes(raw.status) || !['automatic', 'ask', 'no_store'].includes(raw.approval_mode) || !['public', 'personal', 'sensitive', 'restricted'].includes(raw.sensitivity) || typeof raw.retrievable !== 'boolean' || !isUtc(raw.created_at) || !isUtc(raw.updated_at) || raw.updated_at < raw.created_at || typeof raw.content !== 'string' || raw.content.length > 2000 || typeof raw.reason !== 'string' || !raw.reason || raw.reason.length > 280) { throw new WaveAContractError('Memory record is invalid or out of scope'); } const scope = normalizeScope(raw.scope, conversationId); const hidden = ['rejected', 'expired', 'forgotten', 'no_store'].includes(raw.status) || raw.sensitivity === 'restricted'; if (hidden && raw.content) throw new WaveAContractError('Terminal memory retained forbidden content'); const ttl = record(raw.ttl); if (!ttl || !['never', 'expires_at', 'decay'].includes(ttl.policy) || (ttl.policy === 'expires_at' && !isUtc(ttl.expires_at)) || (ttl.policy === 'decay' && (!Number.isSafeInteger(ttl.decay_days) || ttl.decay_days < 1 || ttl.decay_days > 3650))) { throw new WaveAContractError('Memory retention policy is invalid'); } return immutable({id: raw.id, revision: raw.revision, kind: raw.kind, status: raw.status, approvalMode: raw.approval_mode, sensitivity: raw.sensitivity, scope, content: hidden ? 'Content removed by privacy controls.' : safeText(raw.content, 'Content unavailable.', 2000), reason: safeText(raw.reason, 'No reason supplied.', 280), retrievable: raw.retrievable, createdAt: raw.created_at, updatedAt: raw.updated_at, retention: ttl.policy === 'decay' ? `decays after ${ttl.decay_days} days` : ttl.policy === 'expires_at' ? `expires ${ttl.expires_at}` : 'does not expire'}); } function normalizeMemoryPage(raw, expected, conversationId) { if (!record(raw) || !Array.isArray(raw.items) || raw.items.length > 500 || raw.next !== null) { throw new WaveAContractError('Memory page is invalid'); } const items = raw.items.map((item) => normalizeMemory(item, expected, conversationId)); if (new Set(items.map((item) => item.id)).size !== items.length) throw new WaveAContractError('Memory page has duplicate entries'); return immutable({items}); } return Object.freeze({ACCEPT, WaveAContractError, createCanonicalClient, isId, normalizeCapabilities, normalizeEventPage, normalizeExpectedIdentity, normalizeMemoryPage, safeEndpoint, safeIdempotencyKey, safeText, sameCanonicalIdentity}); }));