bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
143 lines
5.6 KiB
JavaScript
143 lines
5.6 KiB
JavaScript
/**
|
|
* hermes.mjs — AAMOS Hermes Event Fabric adapter
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
* Thin adapter between domain modules and Hermes.
|
|
* Publishes events via Redis pub/sub (primary) with JSONL file fallback.
|
|
*
|
|
* Event envelope:
|
|
* {
|
|
* id: string — UUIDv4
|
|
* trace_id: string — propagated across the full operation chain
|
|
* correlation_id:string — groups related events (e.g. one invoice = many events)
|
|
* event_type: string — e.g. 'finance.journal.posted'
|
|
* source: string — service name, e.g. 'aamos-ledger'
|
|
* tenant_id: string — org/tenant isolation
|
|
* user_id: string — actor
|
|
* entity_type: string — e.g. 'journal_entry'
|
|
* entity_id: string — UUID of the affected entity
|
|
* decision_source: string — 'system' | 'user' | 'agent' (audit principle)
|
|
* payload: object — event-specific data
|
|
* ts: string — ISO-8601 UTC
|
|
* schema_version: string — '1.0'
|
|
* }
|
|
*
|
|
* Usage:
|
|
* import { hermes } from './hermes.mjs';
|
|
* await hermes.emit('finance.journal.posted', ctx, { entry_id, amount });
|
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
*/
|
|
|
|
import { createClient } from 'redis';
|
|
import { appendFileSync, mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
import { randomUUID } from 'crypto';
|
|
|
|
const REDIS_URL = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
|
|
const HERMES_CHANNEL = process.env.HERMES_CHANNEL || 'aamos:hermes';
|
|
const LOG_DIR = process.env.HERMES_LOG_DIR || '/opt/amos/data/hermes';
|
|
const LOG_FILE = join(LOG_DIR, 'events.jsonl');
|
|
const SERVICE = process.env.SERVICE_NAME || 'aamos-unknown';
|
|
|
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
|
|
// ── Redis client (lazy, non-blocking) ────────────────────────────────────────
|
|
let redis = null;
|
|
let redisReady = false;
|
|
let redisDisabled = false; // Once down, don't retry — avoids blocking emit()
|
|
|
|
async function getRedis() {
|
|
if (redisReady) return redis;
|
|
if (redisDisabled) return null;
|
|
try {
|
|
redis = createClient({
|
|
url: REDIS_URL,
|
|
socket: {
|
|
connectTimeout: 3000, // fail after 3s instead of blocking forever
|
|
reconnectStrategy: false, // disable auto-reconnect — we use JSONL fallback
|
|
},
|
|
});
|
|
redis.on('error', (e) => {
|
|
redisReady = false;
|
|
redisDisabled = true;
|
|
redis = null;
|
|
console.warn(`[hermes] Redis error (fallback to JSONL): ${e.message}`);
|
|
});
|
|
redis.on('ready', () => { redisReady = true; redisDisabled = false; });
|
|
await redis.connect();
|
|
redisReady = true;
|
|
} catch (e) {
|
|
console.warn(`[hermes] Redis unavailable (fallback to JSONL): ${e.message}`);
|
|
redis = null;
|
|
redisDisabled = true;
|
|
}
|
|
return redis;
|
|
}
|
|
|
|
// ── Event factory ─────────────────────────────────────────────────────────────
|
|
function makeEvent(event_type, ctx = {}, payload = {}) {
|
|
return {
|
|
id: randomUUID(),
|
|
trace_id: ctx.trace_id || randomUUID(),
|
|
correlation_id: ctx.correlation_id || randomUUID(),
|
|
event_type,
|
|
source: SERVICE,
|
|
tenant_id: ctx.tenant_id || 'wavult-group',
|
|
user_id: ctx.user_id || 'system',
|
|
entity_type: ctx.entity_type || null,
|
|
entity_id: ctx.entity_id || null,
|
|
decision_source: ctx.decision_source || 'system',
|
|
payload,
|
|
ts: new Date().toISOString(),
|
|
schema_version: '1.0',
|
|
};
|
|
}
|
|
|
|
// ── Publish ───────────────────────────────────────────────────────────────────
|
|
async function emit(event_type, ctx = {}, payload = {}) {
|
|
const event = makeEvent(event_type, ctx, payload);
|
|
const raw = JSON.stringify(event);
|
|
|
|
// Always write to JSONL (durable fallback + queryable log)
|
|
try {
|
|
appendFileSync(LOG_FILE, raw + '\n');
|
|
} catch (e) {
|
|
console.error('[hermes] JSONL write failed:', e.message);
|
|
}
|
|
|
|
// Publish to Redis pub/sub
|
|
const client = await getRedis();
|
|
if (client && redisReady) {
|
|
try {
|
|
await client.publish(HERMES_CHANNEL, raw);
|
|
} catch (e) {
|
|
console.warn('[hermes] Redis publish failed (JSONL still written):', e.message);
|
|
}
|
|
}
|
|
|
|
return event;
|
|
}
|
|
|
|
// ── Subscribe (for services that consume events) ──────────────────────────────
|
|
async function subscribe(handler) {
|
|
const client = await getRedis();
|
|
if (!client) {
|
|
console.warn('[hermes] subscribe: Redis unavailable, no live events');
|
|
return null;
|
|
}
|
|
const sub = client.duplicate();
|
|
await sub.connect();
|
|
await sub.subscribe(HERMES_CHANNEL, (raw) => {
|
|
try {
|
|
const event = JSON.parse(raw);
|
|
handler(event);
|
|
} catch (e) {
|
|
console.warn('[hermes] subscribe parse error:', e.message);
|
|
}
|
|
});
|
|
console.log(`[hermes] subscribed to ${HERMES_CHANNEL}`);
|
|
return sub;
|
|
}
|
|
|
|
export const hermes = { emit, subscribe, makeEvent };
|
|
export default hermes;
|