@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 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[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.220",
3
+ "version": "0.0.22-beta.221",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",