@absolutejs/voice 0.0.22-beta.220 → 0.0.22-beta.221
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 +9 -2
- package/dist/index.js +98 -2
- package/dist/observabilityExport.d.ts +23 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2753,12 +2753,16 @@ import {
|
|
|
2753
2753
|
|
|
2754
2754
|
app.use(
|
|
2755
2755
|
createVoiceObservabilityExportRoutes({
|
|
2756
|
+
artifactIntegrity: {
|
|
2757
|
+
maxAgeMs: 15 * 60 * 1000
|
|
2758
|
+
},
|
|
2756
2759
|
artifacts: [
|
|
2757
2760
|
{
|
|
2758
2761
|
id: 'latest-proof-pack',
|
|
2759
2762
|
kind: 'proof-pack',
|
|
2760
2763
|
label: 'Latest proof pack',
|
|
2761
|
-
path: '.voice-runtime/proof-pack/latest.md'
|
|
2764
|
+
path: '.voice-runtime/proof-pack/latest.md',
|
|
2765
|
+
required: true
|
|
2762
2766
|
}
|
|
2763
2767
|
],
|
|
2764
2768
|
audit: runtimeStorage.audit,
|
|
@@ -2773,6 +2777,9 @@ app.use(
|
|
|
2773
2777
|
);
|
|
2774
2778
|
|
|
2775
2779
|
const exportReport = await buildVoiceObservabilityExport({
|
|
2780
|
+
artifactIntegrity: {
|
|
2781
|
+
maxAgeMs: 15 * 60 * 1000
|
|
2782
|
+
},
|
|
2776
2783
|
audit: runtimeStorage.audit,
|
|
2777
2784
|
auditDeliveries: runtimeStorage.auditDeliveries,
|
|
2778
2785
|
links: {
|
|
@@ -2784,7 +2791,7 @@ const exportReport = await buildVoiceObservabilityExport({
|
|
|
2784
2791
|
});
|
|
2785
2792
|
```
|
|
2786
2793
|
|
|
2787
|
-
The route helper exposes JSON at `/api/voice/observability-export`, Markdown at `/voice/observability-export.md`, and HTML at `/voice/observability-export`. 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.
|
|
2794
|
+
The route helper exposes JSON at `/api/voice/observability-export`, 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. 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.
|
|
2788
2795
|
|
|
2789
2796
|
Pass the same report into production readiness when export health should block deploys:
|
|
2790
2797
|
|
package/dist/index.js
CHANGED
|
@@ -24942,6 +24942,8 @@ var createVoiceOperationsRecordRoutes = (options) => {
|
|
|
24942
24942
|
};
|
|
24943
24943
|
// src/observabilityExport.ts
|
|
24944
24944
|
import { Elysia as Elysia42 } from "elysia";
|
|
24945
|
+
import { createHash } from "crypto";
|
|
24946
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
24945
24947
|
var isDeliveryStore = (value) => !Array.isArray(value) && typeof value.list === "function";
|
|
24946
24948
|
var getString18 = (value) => typeof value === "string" ? value : undefined;
|
|
24947
24949
|
var getProviderKind = (payload) => getString18(payload.kind) ?? getString18(payload.providerKind);
|
|
@@ -24965,6 +24967,78 @@ var createOperationArtifact = (record, href) => ({
|
|
|
24965
24967
|
status: record.status === "failed" ? "fail" : record.status === "warning" ? "warn" : "pass"
|
|
24966
24968
|
});
|
|
24967
24969
|
var unique2 = (values) => [...new Set(values)].sort();
|
|
24970
|
+
var stripArtifactPathAnchor = (path) => path.split("#")[0] ?? path;
|
|
24971
|
+
var toEpochMs = (value) => {
|
|
24972
|
+
if (typeof value === "number") {
|
|
24973
|
+
return Number.isFinite(value) ? value : undefined;
|
|
24974
|
+
}
|
|
24975
|
+
if (typeof value === "string") {
|
|
24976
|
+
const parsed = Date.parse(value);
|
|
24977
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
24978
|
+
}
|
|
24979
|
+
return;
|
|
24980
|
+
};
|
|
24981
|
+
var checksumFile = async (path) => {
|
|
24982
|
+
const buffer = await readFile2(path);
|
|
24983
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
24984
|
+
};
|
|
24985
|
+
var verifyArtifact = async (artifact, options) => {
|
|
24986
|
+
const now = options.now ?? Date.now();
|
|
24987
|
+
const maxAgeMs = artifact.maxAgeMs ?? options.maxAgeMs;
|
|
24988
|
+
if (!artifact.path) {
|
|
24989
|
+
return maxAgeMs === undefined ? artifact : {
|
|
24990
|
+
...artifact,
|
|
24991
|
+
freshness: {
|
|
24992
|
+
checkedAt: now,
|
|
24993
|
+
generatedAt: artifact.generatedAt,
|
|
24994
|
+
maxAgeMs,
|
|
24995
|
+
status: artifact.status ?? "pass"
|
|
24996
|
+
}
|
|
24997
|
+
};
|
|
24998
|
+
}
|
|
24999
|
+
const filePath = stripArtifactPathAnchor(artifact.path);
|
|
25000
|
+
try {
|
|
25001
|
+
const file = await stat(filePath);
|
|
25002
|
+
const generatedAt = artifact.generatedAt ?? file.mtimeMs;
|
|
25003
|
+
const generatedAtMs = toEpochMs(generatedAt);
|
|
25004
|
+
const ageMs = generatedAtMs === undefined ? undefined : now - generatedAtMs;
|
|
25005
|
+
const isStale = maxAgeMs !== undefined && ageMs !== undefined && ageMs > maxAgeMs;
|
|
25006
|
+
const checksum = options.checksum === false ? artifact.checksum : {
|
|
25007
|
+
algorithm: "sha256",
|
|
25008
|
+
value: await checksumFile(filePath)
|
|
25009
|
+
};
|
|
25010
|
+
return {
|
|
25011
|
+
...artifact,
|
|
25012
|
+
bytes: artifact.bytes ?? file.size,
|
|
25013
|
+
checksum,
|
|
25014
|
+
freshness: maxAgeMs === undefined && generatedAt === undefined ? artifact.freshness : {
|
|
25015
|
+
ageMs,
|
|
25016
|
+
checkedAt: now,
|
|
25017
|
+
generatedAt,
|
|
25018
|
+
maxAgeMs,
|
|
25019
|
+
status: isStale ? options.staleSeverity ?? "fail" : "pass"
|
|
25020
|
+
},
|
|
25021
|
+
generatedAt,
|
|
25022
|
+
status: isStale && (options.staleSeverity ?? "fail") === "fail" ? "fail" : artifact.status
|
|
25023
|
+
};
|
|
25024
|
+
} catch {
|
|
25025
|
+
const severity = artifact.required ? options.missingSeverity ?? "fail" : "warn";
|
|
25026
|
+
return {
|
|
25027
|
+
...artifact,
|
|
25028
|
+
freshness: {
|
|
25029
|
+
checkedAt: now,
|
|
25030
|
+
generatedAt: artifact.generatedAt,
|
|
25031
|
+
maxAgeMs,
|
|
25032
|
+
status: severity
|
|
25033
|
+
},
|
|
25034
|
+
status: severity
|
|
25035
|
+
};
|
|
25036
|
+
}
|
|
25037
|
+
};
|
|
25038
|
+
var verifyArtifacts = (artifacts, options) => {
|
|
25039
|
+
const integrity = options ?? {};
|
|
25040
|
+
return Promise.all(artifacts.map((artifact) => verifyArtifact(artifact, integrity)));
|
|
25041
|
+
};
|
|
24968
25042
|
var collectSessionIds = (input) => unique2([
|
|
24969
25043
|
...input.sessionIds ?? [],
|
|
24970
25044
|
...input.events.map((event) => event.sessionId),
|
|
@@ -24991,6 +25065,27 @@ var collectIssues = (input) => {
|
|
|
24991
25065
|
value: failedOperationsRecords
|
|
24992
25066
|
});
|
|
24993
25067
|
}
|
|
25068
|
+
for (const artifact of input.artifacts) {
|
|
25069
|
+
if (artifact.path && artifact.status !== "pass" && artifact.required && artifact.bytes === undefined && artifact.freshness?.ageMs === undefined) {
|
|
25070
|
+
issues.push({
|
|
25071
|
+
code: "voice.observability.artifact_missing",
|
|
25072
|
+
detail: `${artifact.label} was required but could not be verified at ${artifact.path}.`,
|
|
25073
|
+
label: "Required artifact",
|
|
25074
|
+
severity: artifact.status === "fail" ? "fail" : "warn",
|
|
25075
|
+
value: artifact.id
|
|
25076
|
+
});
|
|
25077
|
+
continue;
|
|
25078
|
+
}
|
|
25079
|
+
if (artifact.freshness?.maxAgeMs !== undefined && artifact.freshness.ageMs !== undefined && artifact.freshness.ageMs > artifact.freshness.maxAgeMs) {
|
|
25080
|
+
issues.push({
|
|
25081
|
+
code: "voice.observability.artifact_stale",
|
|
25082
|
+
detail: `${artifact.label} is older than the configured freshness window.`,
|
|
25083
|
+
label: "Stale artifact",
|
|
25084
|
+
severity: artifact.freshness.status === "fail" ? "fail" : "warn",
|
|
25085
|
+
value: artifact.id
|
|
25086
|
+
});
|
|
25087
|
+
}
|
|
25088
|
+
}
|
|
24994
25089
|
if ((input.auditDeliveries?.failed ?? 0) > 0) {
|
|
24995
25090
|
issues.push({
|
|
24996
25091
|
code: "voice.observability.audit_delivery_failed",
|
|
@@ -25076,7 +25171,7 @@ var buildVoiceObservabilityExport = async (options = {}) => {
|
|
|
25076
25171
|
const traceDeliverySummary = traceDeliveries ? await summarizeVoiceTraceSinkDeliveries(traceDeliveries) : undefined;
|
|
25077
25172
|
const auditDeliverySummary = auditDeliveries ? await summarizeVoiceAuditSinkDeliveries(auditDeliveries) : undefined;
|
|
25078
25173
|
const operationArtifacts = operationsRecords.map((record) => createOperationArtifact(record, options.links?.operationsRecord?.(record.sessionId)));
|
|
25079
|
-
const artifacts = [...operationArtifacts, ...options.artifacts ?? []];
|
|
25174
|
+
const artifacts = await verifyArtifacts([...operationArtifacts, ...options.artifacts ?? []], options.artifactIntegrity);
|
|
25080
25175
|
const operationHrefBySessionId = new Map(sessionIds.map((sessionId) => [
|
|
25081
25176
|
sessionId,
|
|
25082
25177
|
options.links?.operationsRecord?.(sessionId)
|
|
@@ -25086,6 +25181,7 @@ var buildVoiceObservabilityExport = async (options = {}) => {
|
|
|
25086
25181
|
...auditEvents.map((event) => buildAuditEnvelope(event, event.sessionId ? operationHrefBySessionId.get(event.sessionId) : undefined))
|
|
25087
25182
|
].sort((left, right) => left.at - right.at);
|
|
25088
25183
|
const issues = collectIssues({
|
|
25184
|
+
artifacts,
|
|
25089
25185
|
auditDeliveries: auditDeliverySummary,
|
|
25090
25186
|
operationsRecords,
|
|
25091
25187
|
totalEvidence: events.length + auditEvents.length + operationsRecords.length + artifacts.length,
|
|
@@ -25121,7 +25217,7 @@ var renderVoiceObservabilityExportMarkdown = (report, options = {}) => {
|
|
|
25121
25217
|
const title = options.title ?? "Voice Observability Export";
|
|
25122
25218
|
const issues = report.issues.map((issue) => `- ${issue.severity}: ${issue.label}${issue.value !== undefined ? ` (${issue.value})` : ""}${issue.detail ? ` - ${issue.detail}` : ""}`).join(`
|
|
25123
25219
|
`) || "No observability export issues.";
|
|
25124
|
-
const artifacts = report.artifacts.map((artifact) => `- ${artifact.label}: ${artifact.kind}${artifact.href ? ` (${artifact.href})` : ""}${artifact.status ? ` - ${artifact.status}` : ""}`).join(`
|
|
25220
|
+
const artifacts = report.artifacts.map((artifact) => `- ${artifact.label}: ${artifact.kind}${artifact.href ? ` (${artifact.href})` : ""}${artifact.status ? ` - ${artifact.status}` : ""}${artifact.bytes !== undefined ? `, ${artifact.bytes} bytes` : ""}${artifact.checksum ? `, sha256 ${artifact.checksum.value}` : ""}${artifact.freshness?.ageMs !== undefined ? `, age ${Math.round(artifact.freshness.ageMs)}ms` : ""}`).join(`
|
|
25125
25221
|
`) || "No artifacts attached.";
|
|
25126
25222
|
return `# ${title}
|
|
25127
25223
|
|
|
@@ -6,15 +6,30 @@ import { type VoiceTraceSinkDeliveryQueueSummary } from './queue';
|
|
|
6
6
|
import { type StoredVoiceTraceEvent, type VoiceTraceEventStore, type VoiceTraceEventType, type VoiceTraceRedactionConfig, type VoiceTraceSinkDeliveryRecord, type VoiceTraceSinkDeliveryStore, type VoiceTraceSummary } from './trace';
|
|
7
7
|
export type VoiceObservabilityExportStatus = 'fail' | 'pass' | 'warn';
|
|
8
8
|
export type VoiceObservabilityExportArtifactKind = 'incident' | 'markdown' | 'operations-record' | 'proof-pack' | 'readiness' | 'screenshot' | 'slo' | 'trace' | 'audit' | 'custom';
|
|
9
|
+
export type VoiceObservabilityExportArtifactChecksum = {
|
|
10
|
+
algorithm: 'sha256';
|
|
11
|
+
value: string;
|
|
12
|
+
};
|
|
13
|
+
export type VoiceObservabilityExportArtifactFreshness = {
|
|
14
|
+
ageMs?: number;
|
|
15
|
+
checkedAt: number;
|
|
16
|
+
generatedAt?: number | string;
|
|
17
|
+
maxAgeMs?: number;
|
|
18
|
+
status: VoiceObservabilityExportStatus;
|
|
19
|
+
};
|
|
9
20
|
export type VoiceObservabilityExportArtifact = {
|
|
10
21
|
bytes?: number;
|
|
22
|
+
checksum?: VoiceObservabilityExportArtifactChecksum;
|
|
11
23
|
contentType?: string;
|
|
24
|
+
freshness?: VoiceObservabilityExportArtifactFreshness;
|
|
12
25
|
generatedAt?: number | string;
|
|
13
26
|
href?: string;
|
|
14
27
|
id: string;
|
|
15
28
|
kind: VoiceObservabilityExportArtifactKind;
|
|
16
29
|
label: string;
|
|
30
|
+
maxAgeMs?: number;
|
|
17
31
|
path?: string;
|
|
32
|
+
required?: boolean;
|
|
18
33
|
sessionId?: string;
|
|
19
34
|
status?: VoiceObservabilityExportStatus;
|
|
20
35
|
};
|
|
@@ -31,7 +46,7 @@ export type VoiceObservabilityExportEnvelope = {
|
|
|
31
46
|
severity: VoiceObservabilityExportStatus;
|
|
32
47
|
traceId?: string;
|
|
33
48
|
};
|
|
34
|
-
export type VoiceObservabilityExportIssueCode = 'voice.observability.no_evidence' | 'voice.observability.operation_failed' | 'voice.observability.audit_delivery_failed' | 'voice.observability.audit_delivery_pending' | 'voice.observability.trace_delivery_failed' | 'voice.observability.trace_delivery_pending';
|
|
49
|
+
export type VoiceObservabilityExportIssueCode = 'voice.observability.no_evidence' | 'voice.observability.operation_failed' | 'voice.observability.artifact_missing' | 'voice.observability.artifact_stale' | 'voice.observability.audit_delivery_failed' | 'voice.observability.audit_delivery_pending' | 'voice.observability.trace_delivery_failed' | 'voice.observability.trace_delivery_pending';
|
|
35
50
|
export type VoiceObservabilityExportIssue = {
|
|
36
51
|
code: VoiceObservabilityExportIssueCode;
|
|
37
52
|
detail?: string;
|
|
@@ -67,6 +82,13 @@ export type VoiceObservabilityExportReport = {
|
|
|
67
82
|
};
|
|
68
83
|
export type VoiceObservabilityExportOptions = {
|
|
69
84
|
artifacts?: VoiceObservabilityExportArtifact[];
|
|
85
|
+
artifactIntegrity?: {
|
|
86
|
+
checksum?: false | 'sha256';
|
|
87
|
+
maxAgeMs?: number;
|
|
88
|
+
missingSeverity?: Exclude<VoiceObservabilityExportStatus, 'pass'>;
|
|
89
|
+
now?: number;
|
|
90
|
+
staleSeverity?: Exclude<VoiceObservabilityExportStatus, 'pass'>;
|
|
91
|
+
};
|
|
70
92
|
audit?: VoiceAuditEventStore;
|
|
71
93
|
auditDeliveries?: VoiceAuditSinkDeliveryRecord[] | VoiceAuditSinkDeliveryStore;
|
|
72
94
|
events?: StoredVoiceTraceEvent[];
|