@absolutejs/voice 0.0.22-beta.223 → 0.0.22-beta.224

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
@@ -2761,6 +2761,12 @@ app.use(
2761
2761
  directory: '.voice-runtime/observability-exports',
2762
2762
  kind: 'file',
2763
2763
  label: 'Local customer-owned observability archive'
2764
+ },
2765
+ {
2766
+ bucket: process.env.VOICE_OBSERVABILITY_EXPORT_S3_BUCKET,
2767
+ keyPrefix: 'voice/observability-exports',
2768
+ kind: 's3',
2769
+ label: 'S3 customer-owned observability archive'
2764
2770
  }
2765
2771
  ],
2766
2772
  artifacts: [
@@ -2798,7 +2804,7 @@ const exportReport = await buildVoiceObservabilityExport({
2798
2804
  });
2799
2805
  ```
2800
2806
 
2801
- 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`, 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. 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.
2807
+ 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`, 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. 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.
2802
2808
 
2803
2809
  Pass the same report into production readiness when export health should block deploys:
2804
2810
 
package/dist/index.js CHANGED
@@ -24988,6 +24988,14 @@ var safeArtifactFileName = (artifact) => {
24988
24988
  const extension = artifact.contentType === "image/png" ? ".png" : artifact.contentType?.includes("markdown") ? ".md" : artifact.contentType?.includes("json") ? ".json" : "";
24989
24989
  return `${artifact.id.replace(/[^a-z0-9_.-]/gi, "-")}${extension}`;
24990
24990
  };
24991
+ var normalizeExportS3KeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/observability-exports";
24992
+ var joinS3Key = (...parts) => parts.map((part) => part.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
24993
+ var writeS3Object = async (input) => {
24994
+ const file = input.client.file(input.key, input.options);
24995
+ await file.write(input.value, {
24996
+ type: input.contentType
24997
+ });
24998
+ };
24991
24999
  var inferContentType = (artifact) => {
24992
25000
  if (artifact.contentType) {
24993
25001
  return artifact.contentType;
@@ -25352,6 +25360,50 @@ var deliverVoiceObservabilityExport = async (options) => {
25352
25360
  target
25353
25361
  };
25354
25362
  }
25363
+ if (destination.kind === "s3") {
25364
+ const keyPrefix = normalizeExportS3KeyPrefix(destination.keyPrefix);
25365
+ const rootKey = joinS3Key(keyPrefix, runId);
25366
+ const client = destination.client ?? new Bun.S3Client(destination);
25367
+ const s3Options = destination;
25368
+ await writeS3Object({
25369
+ client,
25370
+ contentType: "application/json",
25371
+ key: joinS3Key(rootKey, "manifest.json"),
25372
+ options: s3Options,
25373
+ value: manifest
25374
+ });
25375
+ await writeS3Object({
25376
+ client,
25377
+ contentType: "application/json",
25378
+ key: joinS3Key(rootKey, "artifact-index.json"),
25379
+ options: s3Options,
25380
+ value: index
25381
+ });
25382
+ if (destination.includeArtifacts !== false) {
25383
+ for (const artifact of options.report.artifacts) {
25384
+ if (!artifact.path) {
25385
+ continue;
25386
+ }
25387
+ await writeS3Object({
25388
+ client,
25389
+ contentType: artifact.contentType ?? inferContentType(artifact),
25390
+ key: joinS3Key(rootKey, "artifacts", safeArtifactFileName(artifact)),
25391
+ options: s3Options,
25392
+ value: await readFile2(stripArtifactPathAnchor(artifact.path))
25393
+ });
25394
+ }
25395
+ }
25396
+ return {
25397
+ artifactCount: destination.includeArtifacts === false ? 0 : options.report.artifacts.filter((artifact) => artifact.path).length,
25398
+ deliveredAt: Date.now(),
25399
+ destinationId,
25400
+ destinationKind: destination.kind,
25401
+ label,
25402
+ manifestBytes: byteLength(manifest),
25403
+ status: "delivered",
25404
+ target: destination.bucket ? `s3://${destination.bucket}/${rootKey}` : rootKey
25405
+ };
25406
+ }
25355
25407
  const controller = new AbortController;
25356
25408
  const timeout = setTimeout(() => controller.abort(), destination.timeoutMs ?? 1e4);
25357
25409
  try {
@@ -25396,7 +25448,7 @@ var deliverVoiceObservabilityExport = async (options) => {
25396
25448
  label,
25397
25449
  manifestBytes: byteLength(manifest),
25398
25450
  status: "failed",
25399
- target: destination.kind === "file" ? destination.directory : destination.url
25451
+ target: destination.kind === "file" ? destination.directory : destination.kind === "s3" ? destination.bucket ? `s3://${destination.bucket}/${normalizeExportS3KeyPrefix(destination.keyPrefix)}` : normalizeExportS3KeyPrefix(destination.keyPrefix) : destination.url
25400
25452
  };
25401
25453
  }
25402
25454
  }));
@@ -1,4 +1,5 @@
1
1
  import { Elysia } from 'elysia';
2
+ import type { S3Client, S3Options } from 'bun';
2
3
  import { type VoiceAuditSinkDeliveryQueueSummary, type VoiceAuditSinkDeliveryRecord, type VoiceAuditSinkDeliveryStore } from './auditSinks';
3
4
  import type { VoiceAuditEventStore, VoiceAuditEventType } from './audit';
4
5
  import { type VoiceOperationsRecord } from './operationsRecord';
@@ -113,7 +114,15 @@ export type VoiceObservabilityExportDeliveryDestination = {
113
114
  includeArtifacts?: boolean;
114
115
  kind: 'file';
115
116
  label?: string;
116
- } | {
117
+ } | (S3Options & {
118
+ bucket?: string;
119
+ client?: Pick<S3Client, 'file'>;
120
+ id?: string;
121
+ includeArtifacts?: boolean;
122
+ keyPrefix?: string;
123
+ kind: 's3';
124
+ label?: string;
125
+ }) | {
117
126
  fetch?: typeof fetch;
118
127
  headers?: Record<string, string>;
119
128
  id?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.223",
3
+ "version": "0.0.22-beta.224",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",