From 82f7475b9133ea119edc6b85b195d2af4fd3bd7d Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Mon, 7 Sep 2026 18:11:06 +0200 Subject: [PATCH] fix: audit only newly created durable search definitions --- src/search/search-service.ts | 37 +++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/search/search-service.ts b/src/search/search-service.ts index 5a3450a..65f5934 100644 --- a/src/search/search-service.ts +++ b/src/search/search-service.ts @@ -37,19 +37,18 @@ const ENVELOPE_MAPPINGS: Record = { export class SearchService { private readonly definitions = new Map(); private readonly lineage?: LineageEmitter; + private readonly aud0?: Aud0Client; private readonly definitionRepository?: SearchDefinitionRepository; constructor( private readonly adapter: OpenSearchAdapter, opts?: { aud0Client?: Aud0Client; definitionRepository?: SearchDefinitionRepository }, ) { + this.aud0 = opts?.aud0Client; this.lineage = opts?.aud0Client ? new LineageEmitter(opts.aud0Client) : undefined; this.definitionRepository = opts?.definitionRepository; } - // --------------------------------------------------------------------- - // Index definitions - // --------------------------------------------------------------------- private validateDefinition(def: SearchIndexDefinition): void { if (!def.index_name?.trim() || !/^[a-z0-9][a-z0-9_-]{0,62}$/i.test(def.index_name)) { throw new SiaxError("INVALID_REQUEST", "0pt.search", "index_name must be 1-63 letters, digits, _ or -"); @@ -92,12 +91,38 @@ export class SearchService { return copy; } - /** Production registration: versioned durable write followed by cache refresh. */ + /** Production registration: durable versioned write, one stable audit event, then cache refresh. */ async registerIndexDefinitionDurable(def: SearchIndexDefinition, actorId?: string): Promise { this.validateDefinition(def); if (!this.definitionRepository) return this.registerIndexDefinition(def); const stored = await this.definitionRepository.save(def, actorId); - return this.registerIndexDefinition(stored); + const registered = this.registerIndexDefinition(stored.definition); + if (stored.created && this.aud0) { + const id = `search-definition:${registered.index_name}:v${registered.version}`; + await this.aud0.ingestEvent({ + event_id: `evt_0pt_${id}`, + event_type: "0pt.search_definition_registered", + tenant_id: "t_platform", + project_id: "p_platform", + actor_id: stored.created_by ?? actorId ?? "system", + app_id: "0pt", + capability_id: "0pt.search.definition.register.v1", + resource_type: "search_index_definition", + resource_id: `${registered.index_name}@${registered.version}`, + risk_level: "L2", + result: "success", + payload: { + index_name: registered.index_name, + version: registered.version, + fields: Object.keys(registered.fields).sort(), + default_search_fields: registered.default_search_fields, + }, + created_at: stored.created_at, + occurred_at: stored.created_at, + idempotency_key: id, + }); + } + return registered; } getIndexDefinition(indexName: string): SearchIndexDefinition { @@ -269,8 +294,6 @@ export class SearchService { await flush(); await this.adapter.refresh(physical); } catch (error) { - // Best-effort cleanup of an incomplete generation. Preserve the original - // failure even if cleanup itself fails. await this.adapter.deleteIndex(physical).catch(() => undefined); throw error; }