@absolutejs/voice 0.0.22-beta.227 → 0.0.22-beta.228
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/index.js +157 -4
- package/dist/observabilityExport.d.ts +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2781,6 +2781,19 @@ app.use(
|
|
|
2781
2781
|
keyPrefix: 'voice/observability-exports',
|
|
2782
2782
|
kind: 's3',
|
|
2783
2783
|
label: 'S3 customer-owned observability archive'
|
|
2784
|
+
},
|
|
2785
|
+
{
|
|
2786
|
+
kind: 'sqlite',
|
|
2787
|
+
path: '.voice-runtime/observability-exports.sqlite',
|
|
2788
|
+
tableName: 'voice_observability_exports',
|
|
2789
|
+
label: 'SQLite customer-owned observability warehouse'
|
|
2790
|
+
},
|
|
2791
|
+
{
|
|
2792
|
+
connectionString: process.env.VOICE_OBSERVABILITY_EXPORT_POSTGRES_URL,
|
|
2793
|
+
kind: 'postgres',
|
|
2794
|
+
schemaName: 'voice',
|
|
2795
|
+
tableName: 'observability_exports',
|
|
2796
|
+
label: 'Postgres customer-owned observability warehouse'
|
|
2784
2797
|
}
|
|
2785
2798
|
],
|
|
2786
2799
|
deliveryReceipts: observabilityReceipts,
|
|
@@ -2819,7 +2832,7 @@ const exportReport = await buildVoiceObservabilityExport({
|
|
|
2819
2832
|
});
|
|
2820
2833
|
```
|
|
2821
2834
|
|
|
2822
|
-
The route helper exposes JSON at `/api/voice/observability-export`, an artifact index at `/api/voice/observability-export/artifacts`, per-artifact downloads at `/api/voice/observability-export/artifacts/:artifactId`, delivery at `POST /api/voice/observability-export/deliveries`, delivery history at `GET /api/voice/observability-export/deliveries`, Markdown at `/voice/observability-export.md`, and HTML at `/voice/observability-export`. Path-backed artifacts are hashed with SHA-256 by default, include byte size and freshness metadata, and can fail the export when required evidence is missing or stale. File delivery writes `manifest.json`, `artifact-index.json`, and artifact files into a customer-owned archive directory; webhook delivery posts the manifest and artifact index to a buyer-owned collector, SIEM bridge, or warehouse endpoint; S3 delivery writes the same manifest, index, and artifact files through Bun's native S3 client. Delivery receipt stores persist run id, destinations, status, and target history so operators can prove exports have been continuously healthy. Failed trace/audit deliveries fail the export report, pending deliveries warn, and every trace/audit envelope includes the linked operations-record URL when one is configured. This is the primitive to use when customers ask how voice evidence leaves the app without going through a hosted vendor dashboard.
|
|
2835
|
+
The route helper exposes JSON at `/api/voice/observability-export`, an artifact index at `/api/voice/observability-export/artifacts`, per-artifact downloads at `/api/voice/observability-export/artifacts/:artifactId`, delivery at `POST /api/voice/observability-export/deliveries`, delivery history at `GET /api/voice/observability-export/deliveries`, Markdown at `/voice/observability-export.md`, and HTML at `/voice/observability-export`. Path-backed artifacts are hashed with SHA-256 by default, include byte size and freshness metadata, and can fail the export when required evidence is missing or stale. File delivery writes `manifest.json`, `artifact-index.json`, and artifact files into a customer-owned archive directory; webhook delivery posts the manifest and artifact index to a buyer-owned collector, SIEM bridge, or warehouse endpoint; S3 delivery writes the same manifest, index, and artifact files through Bun's native S3 client; SQLite and Postgres delivery persist the manifest, artifact index, checksum metadata, status, run id, and timestamps into buyer-owned database tables. Delivery receipt stores persist run id, destinations, status, and target history so operators can prove exports have been continuously healthy. Failed trace/audit deliveries fail the export report, pending deliveries warn, and every trace/audit envelope includes the linked operations-record URL when one is configured. This is the primitive to use when customers ask how voice evidence leaves the app without going through a hosted vendor dashboard.
|
|
2823
2836
|
|
|
2824
2837
|
Pass the same report into production readiness when export health should block deploys:
|
|
2825
2838
|
|
package/dist/index.js
CHANGED
|
@@ -22448,6 +22448,7 @@ var createVoiceOpsRecoveryRoutes = (options = {}) => {
|
|
|
22448
22448
|
|
|
22449
22449
|
// src/observabilityExport.ts
|
|
22450
22450
|
import { Elysia as Elysia39 } from "elysia";
|
|
22451
|
+
import { Database } from "bun:sqlite";
|
|
22451
22452
|
import { createHash } from "crypto";
|
|
22452
22453
|
import { mkdir as mkdir4, readFile as readFile2, stat, unlink } from "fs/promises";
|
|
22453
22454
|
import { join as join3 } from "path";
|
|
@@ -23038,6 +23039,116 @@ var writeS3Object = async (input) => {
|
|
|
23038
23039
|
type: input.contentType
|
|
23039
23040
|
});
|
|
23040
23041
|
};
|
|
23042
|
+
var quoteObservabilityIdentifier = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
23043
|
+
var normalizeObservabilityIdentifier = (value) => value?.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice_observability_exports";
|
|
23044
|
+
var buildObservabilityExportDatabaseRecord = (input) => ({
|
|
23045
|
+
artifactCount: input.artifactIndex.summary.total,
|
|
23046
|
+
artifactIndex: input.artifactIndex,
|
|
23047
|
+
checkedAt: input.checkedAt,
|
|
23048
|
+
exportStatus: input.report.status,
|
|
23049
|
+
manifest: input.report,
|
|
23050
|
+
runId: input.runId,
|
|
23051
|
+
status: input.report.status
|
|
23052
|
+
});
|
|
23053
|
+
var deliverObservabilityExportToSQLite = async (input) => {
|
|
23054
|
+
if (!input.destination.database && !input.destination.path) {
|
|
23055
|
+
throw new Error("SQLite observability export delivery requires destination.database or destination.path.");
|
|
23056
|
+
}
|
|
23057
|
+
const database = input.destination.database ?? new Database(input.destination.path, { create: true });
|
|
23058
|
+
const table = quoteObservabilityIdentifier(normalizeObservabilityIdentifier(input.destination.tableName));
|
|
23059
|
+
const record = buildObservabilityExportDatabaseRecord(input);
|
|
23060
|
+
database.exec(`CREATE TABLE IF NOT EXISTS ${table} (
|
|
23061
|
+
run_id TEXT PRIMARY KEY,
|
|
23062
|
+
checked_at INTEGER NOT NULL,
|
|
23063
|
+
status TEXT NOT NULL,
|
|
23064
|
+
export_status TEXT NOT NULL,
|
|
23065
|
+
artifact_count INTEGER NOT NULL,
|
|
23066
|
+
manifest_json TEXT NOT NULL,
|
|
23067
|
+
artifact_index_json TEXT NOT NULL,
|
|
23068
|
+
payload_json TEXT NOT NULL
|
|
23069
|
+
)`);
|
|
23070
|
+
database.query(`INSERT INTO ${table}
|
|
23071
|
+
(run_id, checked_at, status, export_status, artifact_count, manifest_json, artifact_index_json, payload_json)
|
|
23072
|
+
VALUES ($runId, $checkedAt, $status, $exportStatus, $artifactCount, $manifest, $artifactIndex, $payload)
|
|
23073
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
23074
|
+
checked_at = excluded.checked_at,
|
|
23075
|
+
status = excluded.status,
|
|
23076
|
+
export_status = excluded.export_status,
|
|
23077
|
+
artifact_count = excluded.artifact_count,
|
|
23078
|
+
manifest_json = excluded.manifest_json,
|
|
23079
|
+
artifact_index_json = excluded.artifact_index_json,
|
|
23080
|
+
payload_json = excluded.payload_json`).run({
|
|
23081
|
+
$artifactCount: input.artifactIndex.summary.total,
|
|
23082
|
+
$artifactIndex: input.index,
|
|
23083
|
+
$checkedAt: input.checkedAt,
|
|
23084
|
+
$exportStatus: input.report.status,
|
|
23085
|
+
$manifest: input.manifest,
|
|
23086
|
+
$payload: JSON.stringify(record),
|
|
23087
|
+
$runId: input.runId,
|
|
23088
|
+
$status: input.report.status
|
|
23089
|
+
});
|
|
23090
|
+
return input.destination.path ? `sqlite://${input.destination.path}/${table.replaceAll('"', "")}` : `sqlite://memory/${table.replaceAll('"', "")}`;
|
|
23091
|
+
};
|
|
23092
|
+
var deliverObservabilityExportToPostgres = async (input) => {
|
|
23093
|
+
const sql = input.destination.sql ?? (input.destination.connectionString ? (() => {
|
|
23094
|
+
const client = new Bun.SQL(input.destination.connectionString);
|
|
23095
|
+
return { unsafe: client.unsafe.bind(client) };
|
|
23096
|
+
})() : undefined);
|
|
23097
|
+
if (!sql) {
|
|
23098
|
+
throw new Error("Postgres observability export delivery requires destination.sql or destination.connectionString.");
|
|
23099
|
+
}
|
|
23100
|
+
const schema = normalizeObservabilityIdentifier(input.destination.schemaName ?? "public");
|
|
23101
|
+
const table = normalizeObservabilityIdentifier(input.destination.tableName);
|
|
23102
|
+
const qualifiedTable = `${quoteObservabilityIdentifier(schema)}.${quoteObservabilityIdentifier(table)}`;
|
|
23103
|
+
const record = buildObservabilityExportDatabaseRecord(input);
|
|
23104
|
+
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quoteObservabilityIdentifier(schema)}`);
|
|
23105
|
+
await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
|
|
23106
|
+
run_id TEXT PRIMARY KEY,
|
|
23107
|
+
checked_at BIGINT NOT NULL,
|
|
23108
|
+
status TEXT NOT NULL,
|
|
23109
|
+
export_status TEXT NOT NULL,
|
|
23110
|
+
artifact_count INTEGER NOT NULL,
|
|
23111
|
+
manifest_json JSONB NOT NULL,
|
|
23112
|
+
artifact_index_json JSONB NOT NULL,
|
|
23113
|
+
payload JSONB NOT NULL
|
|
23114
|
+
)`);
|
|
23115
|
+
await sql.unsafe(`INSERT INTO ${qualifiedTable}
|
|
23116
|
+
(run_id, checked_at, status, export_status, artifact_count, manifest_json, artifact_index_json, payload)
|
|
23117
|
+
VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7::jsonb, $8::jsonb)
|
|
23118
|
+
ON CONFLICT (run_id) DO UPDATE SET
|
|
23119
|
+
checked_at = EXCLUDED.checked_at,
|
|
23120
|
+
status = EXCLUDED.status,
|
|
23121
|
+
export_status = EXCLUDED.export_status,
|
|
23122
|
+
artifact_count = EXCLUDED.artifact_count,
|
|
23123
|
+
manifest_json = EXCLUDED.manifest_json,
|
|
23124
|
+
artifact_index_json = EXCLUDED.artifact_index_json,
|
|
23125
|
+
payload = EXCLUDED.payload`, [
|
|
23126
|
+
input.runId,
|
|
23127
|
+
input.checkedAt,
|
|
23128
|
+
input.report.status,
|
|
23129
|
+
input.report.status,
|
|
23130
|
+
input.artifactIndex.summary.total,
|
|
23131
|
+
input.manifest,
|
|
23132
|
+
input.index,
|
|
23133
|
+
JSON.stringify(record)
|
|
23134
|
+
]);
|
|
23135
|
+
return `postgres://${schema}/${table}`;
|
|
23136
|
+
};
|
|
23137
|
+
var observabilityExportDeliveryFailureTarget = (destination) => {
|
|
23138
|
+
if (destination.kind === "file") {
|
|
23139
|
+
return destination.directory;
|
|
23140
|
+
}
|
|
23141
|
+
if (destination.kind === "s3") {
|
|
23142
|
+
return destination.bucket ? `s3://${destination.bucket}/${normalizeExportS3KeyPrefix(destination.keyPrefix)}` : normalizeExportS3KeyPrefix(destination.keyPrefix);
|
|
23143
|
+
}
|
|
23144
|
+
if (destination.kind === "sqlite") {
|
|
23145
|
+
return destination.path ?? "sqlite://memory";
|
|
23146
|
+
}
|
|
23147
|
+
if (destination.kind === "postgres") {
|
|
23148
|
+
return destination.connectionString ?? "postgres://configured-client";
|
|
23149
|
+
}
|
|
23150
|
+
return destination.url;
|
|
23151
|
+
};
|
|
23041
23152
|
var createVoiceMemoryObservabilityExportDeliveryReceiptStore = () => {
|
|
23042
23153
|
const receipts = new Map;
|
|
23043
23154
|
return {
|
|
@@ -23437,7 +23548,7 @@ var deliverVoiceObservabilityExport = async (options) => {
|
|
|
23437
23548
|
`;
|
|
23438
23549
|
const destinations = await Promise.all(options.destinations.map(async (destination) => {
|
|
23439
23550
|
const destinationId = destination.id ?? `${destination.kind}-${destination.label ?? "export"}`;
|
|
23440
|
-
const label = destination.label ?? (destination.kind === "file" ? "File observability export" : "Webhook observability export");
|
|
23551
|
+
const label = destination.label ?? (destination.kind === "file" ? "File observability export" : destination.kind === "s3" ? "S3 observability export" : destination.kind === "sqlite" ? "SQLite observability export" : destination.kind === "postgres" ? "Postgres observability export" : "Webhook observability export");
|
|
23441
23552
|
try {
|
|
23442
23553
|
if (destination.kind === "file") {
|
|
23443
23554
|
const target = join3(destination.directory, runId);
|
|
@@ -23507,6 +23618,48 @@ var deliverVoiceObservabilityExport = async (options) => {
|
|
|
23507
23618
|
target: destination.bucket ? `s3://${destination.bucket}/${rootKey}` : rootKey
|
|
23508
23619
|
};
|
|
23509
23620
|
}
|
|
23621
|
+
if (destination.kind === "sqlite") {
|
|
23622
|
+
const target = await deliverObservabilityExportToSQLite({
|
|
23623
|
+
artifactIndex,
|
|
23624
|
+
checkedAt,
|
|
23625
|
+
destination,
|
|
23626
|
+
index,
|
|
23627
|
+
manifest,
|
|
23628
|
+
report: options.report,
|
|
23629
|
+
runId
|
|
23630
|
+
});
|
|
23631
|
+
return {
|
|
23632
|
+
artifactCount: artifactIndex.summary.total,
|
|
23633
|
+
deliveredAt: Date.now(),
|
|
23634
|
+
destinationId,
|
|
23635
|
+
destinationKind: destination.kind,
|
|
23636
|
+
label,
|
|
23637
|
+
manifestBytes: byteLength(manifest),
|
|
23638
|
+
status: "delivered",
|
|
23639
|
+
target
|
|
23640
|
+
};
|
|
23641
|
+
}
|
|
23642
|
+
if (destination.kind === "postgres") {
|
|
23643
|
+
const target = await deliverObservabilityExportToPostgres({
|
|
23644
|
+
artifactIndex,
|
|
23645
|
+
checkedAt,
|
|
23646
|
+
destination,
|
|
23647
|
+
index,
|
|
23648
|
+
manifest,
|
|
23649
|
+
report: options.report,
|
|
23650
|
+
runId
|
|
23651
|
+
});
|
|
23652
|
+
return {
|
|
23653
|
+
artifactCount: artifactIndex.summary.total,
|
|
23654
|
+
deliveredAt: Date.now(),
|
|
23655
|
+
destinationId,
|
|
23656
|
+
destinationKind: destination.kind,
|
|
23657
|
+
label,
|
|
23658
|
+
manifestBytes: byteLength(manifest),
|
|
23659
|
+
status: "delivered",
|
|
23660
|
+
target
|
|
23661
|
+
};
|
|
23662
|
+
}
|
|
23510
23663
|
const controller = new AbortController;
|
|
23511
23664
|
const timeout = setTimeout(() => controller.abort(), destination.timeoutMs ?? 1e4);
|
|
23512
23665
|
try {
|
|
@@ -23551,7 +23704,7 @@ var deliverVoiceObservabilityExport = async (options) => {
|
|
|
23551
23704
|
label,
|
|
23552
23705
|
manifestBytes: byteLength(manifest),
|
|
23553
23706
|
status: "failed",
|
|
23554
|
-
target:
|
|
23707
|
+
target: observabilityExportDeliveryFailureTarget(destination)
|
|
23555
23708
|
};
|
|
23556
23709
|
}
|
|
23557
23710
|
}));
|
|
@@ -26701,7 +26854,7 @@ var createVoiceTraceDeliveryRoutes = (options) => {
|
|
|
26701
26854
|
return routes;
|
|
26702
26855
|
};
|
|
26703
26856
|
// src/sqliteStore.ts
|
|
26704
|
-
import { Database } from "bun:sqlite";
|
|
26857
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
26705
26858
|
var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
|
|
26706
26859
|
var resolveTableName = (input) => {
|
|
26707
26860
|
if (input.options.tableName) {
|
|
@@ -26712,7 +26865,7 @@ var resolveTableName = (input) => {
|
|
|
26712
26865
|
return `${prefix}_${fallback}`;
|
|
26713
26866
|
};
|
|
26714
26867
|
var openVoiceSQLiteDatabase = (path) => {
|
|
26715
|
-
const database = new
|
|
26868
|
+
const database = new Database2(path, {
|
|
26716
26869
|
create: true
|
|
26717
26870
|
});
|
|
26718
26871
|
database.exec("PRAGMA journal_mode = WAL;");
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Elysia } from 'elysia';
|
|
2
2
|
import type { S3Client, S3Options } from 'bun';
|
|
3
|
+
import { Database } from 'bun:sqlite';
|
|
3
4
|
import { type VoiceAuditSinkDeliveryQueueSummary, type VoiceAuditSinkDeliveryRecord, type VoiceAuditSinkDeliveryStore } from './auditSinks';
|
|
4
5
|
import type { VoiceAuditEventStore, VoiceAuditEventType } from './audit';
|
|
5
6
|
import { type VoiceOperationsRecord } from './operationsRecord';
|
|
6
7
|
import { type VoiceTraceSinkDeliveryQueueSummary } from './queue';
|
|
7
8
|
import { type StoredVoiceTraceEvent, type VoiceTraceEventStore, type VoiceTraceEventType, type VoiceTraceRedactionConfig, type VoiceTraceSinkDeliveryRecord, type VoiceTraceSinkDeliveryStore, type VoiceTraceSummary } from './trace';
|
|
9
|
+
import type { VoicePostgresClient } from './postgresStore';
|
|
8
10
|
export type VoiceObservabilityExportStatus = 'fail' | 'pass' | 'warn';
|
|
9
11
|
export type VoiceObservabilityExportArtifactKind = 'incident' | 'markdown' | 'operations-record' | 'proof-pack' | 'readiness' | 'screenshot' | 'slo' | 'trace' | 'audit' | 'custom';
|
|
10
12
|
export type VoiceObservabilityExportArtifactChecksum = {
|
|
@@ -123,6 +125,21 @@ export type VoiceObservabilityExportDeliveryDestination = {
|
|
|
123
125
|
kind: 's3';
|
|
124
126
|
label?: string;
|
|
125
127
|
}) | {
|
|
128
|
+
database?: Database;
|
|
129
|
+
id?: string;
|
|
130
|
+
kind: 'sqlite';
|
|
131
|
+
label?: string;
|
|
132
|
+
path?: string;
|
|
133
|
+
tableName?: string;
|
|
134
|
+
} | {
|
|
135
|
+
connectionString?: string;
|
|
136
|
+
id?: string;
|
|
137
|
+
kind: 'postgres';
|
|
138
|
+
label?: string;
|
|
139
|
+
schemaName?: string;
|
|
140
|
+
sql?: VoicePostgresClient;
|
|
141
|
+
tableName?: string;
|
|
142
|
+
} | {
|
|
126
143
|
fetch?: typeof fetch;
|
|
127
144
|
headers?: Record<string, string>;
|
|
128
145
|
id?: string;
|