@mycadet/assistant 0.12.11 → 0.13.0

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.
Files changed (176) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  3. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  5. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  6. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +2 -2
  7. package/node_modules/@mycadet/service-contracts/src/ingress.ts +2 -2
  8. package/openapi.yaml +2 -708
  9. package/package.json +2 -3
  10. package/scripts/sync-web-search-catalog.ts +1 -2
  11. package/src/__tests__/cloud-profile-guard.test.ts +2 -2
  12. package/src/__tests__/config-schema.test.ts +6 -38
  13. package/src/__tests__/conversation-queue.test.ts +45 -6
  14. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  15. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  16. package/src/__tests__/persona-resolver.test.ts +0 -51
  17. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  18. package/src/__tests__/run-due-schedules.test.ts +0 -4
  19. package/src/__tests__/schedule-store.test.ts +0 -12
  20. package/src/__tests__/stt-stream-session.test.ts +4 -4
  21. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  22. package/src/__tests__/voice-config-update.test.ts +12 -167
  23. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  24. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  25. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  26. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  27. package/src/calls/resolve-call-tts-provider.ts +6 -25
  28. package/src/calls/telephony-credential-preflight.ts +0 -10
  29. package/src/calls/telephony-tts-capability.ts +1 -17
  30. package/src/cli/commands/tts.ts +1 -2
  31. package/src/cli/index.help.ts +0 -2
  32. package/src/cli/program.ts +0 -2
  33. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  34. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  35. package/src/config/schemas/services.ts +5 -6
  36. package/src/config/schemas/stt.ts +1 -3
  37. package/src/config/schemas/tts.ts +1 -30
  38. package/src/daemon/daemon-readiness.ts +4 -29
  39. package/src/daemon/lifecycle.ts +0 -31
  40. package/src/daemon/providers-setup.ts +0 -8
  41. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  42. package/src/heartbeat/heartbeat-service.ts +1 -66
  43. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  44. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  45. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  46. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  47. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  48. package/src/live-voice/live-voice-session.ts +10 -6
  49. package/src/live-voice/live-voice-tts.ts +1 -8
  50. package/src/plugins/defaults/memory/worker.ts +3 -3
  51. package/src/prompts/persona-resolver.ts +0 -35
  52. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  53. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  54. package/src/providers/search-provider-catalog.ts +7 -39
  55. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  56. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  57. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  58. package/src/providers/speech-to-text/resolve.ts +3 -120
  59. package/src/runtime/http-router-types.ts +1 -3
  60. package/src/runtime/http-server.ts +1 -1
  61. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  62. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  63. package/src/runtime/routes/index.ts +0 -4
  64. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  65. package/src/runtime/routes/live-voice-routes.ts +8 -11
  66. package/src/runtime/routes/secret-routes.ts +0 -6
  67. package/src/runtime/routes/tts-routes.ts +0 -38
  68. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  69. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  70. package/src/schedule/schedule-store.ts +0 -7
  71. package/src/schedule/scheduler.ts +0 -5
  72. package/src/schedule/worker.ts +1 -1
  73. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  74. package/src/stt/daemon-batch-transcriber.ts +4 -38
  75. package/src/stt/types.ts +1 -9
  76. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  77. package/src/tools/network/web-search.ts +4 -165
  78. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  79. package/src/tts/provider-catalog.ts +0 -4
  80. package/src/tts/types.ts +0 -1
  81. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  82. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  83. package/src/__tests__/migration-export-http.test.ts +0 -557
  84. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  85. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  86. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  87. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  88. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  89. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  90. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  91. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  92. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  93. package/src/__tests__/migration-transport.test.ts +0 -1089
  94. package/src/__tests__/migration-validate-http.test.ts +0 -634
  95. package/src/__tests__/migration-wizard.test.ts +0 -1395
  96. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  97. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  98. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  99. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  100. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  101. package/src/background-wake/background-wake-routes.test.ts +0 -906
  102. package/src/background-wake/cloud-client.test.ts +0 -328
  103. package/src/background-wake/cloud-client.ts +0 -196
  104. package/src/background-wake/next-wake.test.ts +0 -335
  105. package/src/background-wake/next-wake.ts +0 -188
  106. package/src/background-wake/publisher.ts +0 -93
  107. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  108. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  109. package/src/cli/commands/backup.help.ts +0 -180
  110. package/src/cli/commands/backup.ts +0 -398
  111. package/src/cloud/cloud-speech.test.ts +0 -335
  112. package/src/cloud/cloud-speech.ts +0 -300
  113. package/src/config/cloud-speech-defaults.ts +0 -300
  114. package/src/config/sanitize-for-transfer.ts +0 -111
  115. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  116. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  117. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  118. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  119. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  120. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  121. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  122. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  123. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  124. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  125. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  126. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  127. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  128. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  129. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  130. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  131. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  132. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  133. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  134. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  135. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  136. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  137. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  138. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  139. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  140. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  141. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  142. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  143. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  144. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  146. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  147. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  148. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  149. package/src/runtime/migrations/job-registry.ts +0 -297
  150. package/src/runtime/migrations/migration-transport.ts +0 -612
  151. package/src/runtime/migrations/migration-wizard.ts +0 -747
  152. package/src/runtime/migrations/origin-mode.ts +0 -42
  153. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  154. package/src/runtime/migrations/staged-import-path.ts +0 -116
  155. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  156. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  157. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  158. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  159. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  160. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  161. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  162. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  163. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  164. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  165. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  166. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  167. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  168. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  169. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  170. package/src/runtime/routes/background-wake-routes.ts +0 -387
  171. package/src/runtime/routes/migration-routes.ts +0 -2584
  172. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  173. package/src/tools/network/cloud-search-proxy.ts +0 -211
  174. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  175. package/src/tts/providers/cadet-provider.ts +0 -244
  176. package/src/types/tar-stream.d.ts +0 -66
@@ -1,598 +0,0 @@
1
- /**
2
- * Tests for the streaming validator primitives:
3
- * - `readAndValidateManifest` consumes the first tar entry and runs the
4
- * full manifest validation pipeline.
5
- * - `createHashVerifier` is a passthrough Transform that aborts with a
6
- * typed error on digest/size mismatch.
7
- *
8
- * Happy-path fixtures are built with the existing `buildVBundle` helper.
9
- * Negative-path fixtures (non-manifest first, oversize manifest, malformed
10
- * JSON, schema fail, sha mismatch) are hand-constructed because
11
- * `buildVBundle` always produces valid, manifest-first archives.
12
- */
13
-
14
- import { createHash } from "node:crypto";
15
- import { Readable } from "node:stream";
16
- import { pipeline } from "node:stream/promises";
17
- import { gzipSync } from "node:zlib";
18
- import { describe, expect, test } from "bun:test";
19
-
20
- import { buildVBundle } from "../vbundle-builder.js";
21
- import {
22
- createHashVerifier,
23
- readAndValidateManifest,
24
- StreamingValidationError,
25
- } from "../vbundle-streaming-validator.js";
26
- import {
27
- parseVBundleStream,
28
- type StreamedTarEntry,
29
- } from "../vbundle-tar-stream.js";
30
- import {
31
- computeLegacyManifestSha256,
32
- computeManifestChecksum,
33
- MANIFEST_MAX_BYTES,
34
- } from "../vbundle-validator.js";
35
- import { defaultV1Options } from "./v1-test-helpers.js";
36
-
37
- // ---------------------------------------------------------------------------
38
- // Helpers
39
- // ---------------------------------------------------------------------------
40
-
41
- const BLOCK_SIZE = 512;
42
-
43
- function readableFromBuffer(buf: Uint8Array): Readable {
44
- return Readable.from([Buffer.from(buf)]);
45
- }
46
-
47
- /** Minimal hand-rolled tar entry builder (ustar regular file). */
48
- function buildTarEntry(name: string, data: Uint8Array): Uint8Array {
49
- const encoder = new TextEncoder();
50
- const nameBytes = encoder.encode(name);
51
- if (nameBytes.length > 100) {
52
- throw new Error(`test helper: name too long (${nameBytes.length} bytes)`);
53
- }
54
-
55
- const header = new Uint8Array(BLOCK_SIZE);
56
- header.set(nameBytes, 0);
57
-
58
- const writeOctal = (offset: number, length: number, value: number) => {
59
- const str = value.toString(8).padStart(length - 1, "0");
60
- for (let i = 0; i < str.length; i++) {
61
- header[offset + i] = str.charCodeAt(i);
62
- }
63
- header[offset + length - 1] = 0;
64
- };
65
-
66
- writeOctal(100, 8, 0o644); // mode
67
- writeOctal(108, 8, 0); // uid
68
- writeOctal(116, 8, 0); // gid
69
- writeOctal(124, 12, data.length); // size
70
- writeOctal(136, 12, Math.floor(Date.now() / 1000)); // mtime
71
- header[156] = "0".charCodeAt(0); // typeflag = regular file
72
-
73
- const magic = encoder.encode("ustar\0");
74
- header.set(magic, 257);
75
- header[263] = "0".charCodeAt(0);
76
- header[264] = "0".charCodeAt(0);
77
-
78
- // Header checksum: sum of all bytes with the checksum field treated
79
- // as 8 ASCII spaces.
80
- let sum = 0;
81
- for (let i = 0; i < BLOCK_SIZE; i++) {
82
- sum += i >= 148 && i < 156 ? 0x20 : header[i];
83
- }
84
- writeOctal(148, 7, sum);
85
- header[155] = 0x20;
86
-
87
- // Pad data out to the next 512-byte boundary.
88
- const remainder = data.length % BLOCK_SIZE;
89
- const padded =
90
- remainder === 0
91
- ? data
92
- : (() => {
93
- const out = new Uint8Array(data.length + (BLOCK_SIZE - remainder));
94
- out.set(data, 0);
95
- return out;
96
- })();
97
-
98
- const entry = new Uint8Array(header.length + padded.length);
99
- entry.set(header, 0);
100
- entry.set(padded, header.length);
101
- return entry;
102
- }
103
-
104
- /** Hand-build a gzipped tar archive from the given entries (no manifest injected). */
105
- function buildRawVBundle(
106
- entries: Array<{ name: string; data: Uint8Array }>,
107
- ): Uint8Array {
108
- const parts: Uint8Array[] = entries.map((e) => buildTarEntry(e.name, e.data));
109
- // End-of-archive marker: two zero blocks.
110
- parts.push(new Uint8Array(BLOCK_SIZE * 2));
111
-
112
- const total = parts.reduce((n, p) => n + p.length, 0);
113
- const tar = new Uint8Array(total);
114
- let offset = 0;
115
- for (const p of parts) {
116
- tar.set(p, offset);
117
- offset += p.length;
118
- }
119
- return gzipSync(tar);
120
- }
121
-
122
- /** Fetch the first entry of a streaming archive; drain+close the iterator after. */
123
- async function firstEntryOf(
124
- archive: Uint8Array,
125
- ): Promise<{ entry: StreamedTarEntry; drainRest: () => Promise<void> }> {
126
- const iter = parseVBundleStream(readableFromBuffer(archive));
127
- const first = await iter.next();
128
- if (first.done || !first.value) {
129
- throw new Error("archive contained no entries");
130
- }
131
- const drainRest = async () => {
132
- for await (const rest of iter) {
133
- rest.body.resume();
134
- }
135
- };
136
- return { entry: first.value, drainRest };
137
- }
138
-
139
- // ---------------------------------------------------------------------------
140
- // readAndValidateManifest — happy path
141
- // ---------------------------------------------------------------------------
142
-
143
- describe("readAndValidateManifest — happy path", () => {
144
- test("parses manifest and populates expected map from manifest.contents", async () => {
145
- const fileA = new TextEncoder().encode("alpha payload\n");
146
- const fileB = new TextEncoder().encode("beta payload\n");
147
- const { archive, manifest } = buildVBundle({
148
- files: [
149
- {
150
- path: "data/db/assistant.db",
151
- data: new Uint8Array(),
152
- },
153
- { path: "workspace/a.txt", data: fileA },
154
- { path: "workspace/b.txt", data: fileB },
155
- ],
156
- ...defaultV1Options(),
157
- });
158
-
159
- const { entry, drainRest } = await firstEntryOf(archive);
160
- const result = await readAndValidateManifest(entry);
161
- await drainRest();
162
-
163
- expect(result.manifest.schema_version).toBe(manifest.schema_version);
164
- // Includes the synthetic data/db/assistant.db entry plus a/b.txt.
165
- expect(result.manifest.contents).toHaveLength(3);
166
- expect(result.manifest.checksum).toBe(manifest.checksum);
167
-
168
- expect(result.expected.size).toBe(3);
169
- const expectA = result.expected.get("workspace/a.txt");
170
- expect(expectA?.size).toBe(fileA.length);
171
- expect(expectA?.sha256).toBe(
172
- manifest.contents.find((f) => f.path === "workspace/a.txt")?.sha256,
173
- );
174
- const expectB = result.expected.get("workspace/b.txt");
175
- expect(expectB?.size).toBe(fileB.length);
176
- expect(expectB?.sha256).toBe(
177
- manifest.contents.find((f) => f.path === "workspace/b.txt")?.sha256,
178
- );
179
- });
180
- });
181
-
182
- // ---------------------------------------------------------------------------
183
- // readAndValidateManifest — negative paths
184
- // ---------------------------------------------------------------------------
185
-
186
- describe("readAndValidateManifest — negative paths", () => {
187
- test("throws manifest_not_first when first entry is not manifest.json", async () => {
188
- const archive = buildRawVBundle([
189
- { name: "workspace/a.txt", data: new TextEncoder().encode("hello") },
190
- ]);
191
- const { entry, drainRest } = await firstEntryOf(archive);
192
-
193
- let err: StreamingValidationError | null = null;
194
- try {
195
- await readAndValidateManifest(entry);
196
- } catch (e) {
197
- err = e as StreamingValidationError;
198
- }
199
- await drainRest();
200
-
201
- expect(err).toBeInstanceOf(StreamingValidationError);
202
- expect(err?.code).toBe("manifest_not_first");
203
- });
204
-
205
- test("throws manifest_too_large and fails fast before draining the whole body", async () => {
206
- // Fake a tar entry whose body would emit 10x the cap if fully drained.
207
- // The validator must destroy() the stream and throw the moment the
208
- // running byte count crosses MANIFEST_MAX_BYTES; it must NOT keep
209
- // pulling chunks.
210
- //
211
- // We count bytes emitted via a _read implementation, and after the
212
- // throw assert both that destroy() fired and that far less than the
213
- // full body was ever pulled out of the stream.
214
- const CHUNK = MANIFEST_MAX_BYTES / 2;
215
- const TOTAL_CHUNKS = 20;
216
- let chunksEmitted = 0;
217
- let bytesEmitted = 0;
218
- const body = new Readable({
219
- read() {
220
- if (chunksEmitted >= TOTAL_CHUNKS) {
221
- this.push(null);
222
- return;
223
- }
224
- const buf = Buffer.alloc(CHUNK, 0x20);
225
- chunksEmitted += 1;
226
- bytesEmitted += buf.length;
227
- this.push(buf);
228
- },
229
- });
230
-
231
- const entry: StreamedTarEntry = {
232
- header: {
233
- name: "manifest.json",
234
- size: CHUNK * TOTAL_CHUNKS,
235
- type: "file",
236
- },
237
- body,
238
- };
239
-
240
- let err: StreamingValidationError | null = null;
241
- try {
242
- await readAndValidateManifest(entry);
243
- } catch (e) {
244
- err = e as StreamingValidationError;
245
- }
246
-
247
- expect(err).toBeInstanceOf(StreamingValidationError);
248
- expect(err?.code).toBe("manifest_too_large");
249
- // Fail-fast assertions: destroy() was called, and we didn't drain past
250
- // the cap + one chunk. If the validator had drained to EOF we'd see all
251
- // 20 chunks here.
252
- expect(body.destroyed).toBe(true);
253
- expect(chunksEmitted).toBeLessThanOrEqual(3);
254
- expect(bytesEmitted).toBeLessThan(2 * MANIFEST_MAX_BYTES);
255
- });
256
-
257
- test("throws bundle_too_many_entries when manifest.contents exceeds maxEntries", async () => {
258
- const { archive } = buildVBundle({
259
- files: [
260
- { path: "data/db/assistant.db", data: new Uint8Array() },
261
- { path: "workspace/a.txt", data: new TextEncoder().encode("a") },
262
- { path: "workspace/b.txt", data: new TextEncoder().encode("b") },
263
- ],
264
- ...defaultV1Options(),
265
- });
266
- const { entry, drainRest } = await firstEntryOf(archive);
267
-
268
- let err: StreamingValidationError | null = null;
269
- try {
270
- await readAndValidateManifest(entry, { maxEntries: 2 });
271
- } catch (e) {
272
- err = e as StreamingValidationError;
273
- }
274
- await drainRest();
275
-
276
- expect(err).toBeInstanceOf(StreamingValidationError);
277
- expect(err?.code).toBe("bundle_too_many_entries");
278
- });
279
-
280
- test("throws manifest_malformed when manifest body is not valid JSON", async () => {
281
- const archive = buildRawVBundle([
282
- { name: "manifest.json", data: new TextEncoder().encode("{not-json") },
283
- ]);
284
- const { entry, drainRest } = await firstEntryOf(archive);
285
-
286
- let err: StreamingValidationError | null = null;
287
- try {
288
- await readAndValidateManifest(entry);
289
- } catch (e) {
290
- err = e as StreamingValidationError;
291
- }
292
- await drainRest();
293
-
294
- expect(err).toBeInstanceOf(StreamingValidationError);
295
- expect(err?.code).toBe("manifest_malformed");
296
- });
297
-
298
- test("throws manifest_schema when a required field is missing", async () => {
299
- // Valid JSON but missing `files`, `manifest_sha256`, etc.
300
- const bogus = new TextEncoder().encode(
301
- JSON.stringify({ schema_version: "1.0", created_at: "now" }),
302
- );
303
- const archive = buildRawVBundle([{ name: "manifest.json", data: bogus }]);
304
- const { entry, drainRest } = await firstEntryOf(archive);
305
-
306
- let err: StreamingValidationError | null = null;
307
- try {
308
- await readAndValidateManifest(entry);
309
- } catch (e) {
310
- err = e as StreamingValidationError;
311
- }
312
- await drainRest();
313
-
314
- expect(err).toBeInstanceOf(StreamingValidationError);
315
- expect(err?.code).toBe("manifest_schema");
316
- });
317
-
318
- test("throws manifest_sha256 when the declared digest doesn't match canonical JSON", async () => {
319
- const badManifest = {
320
- schema_version: 1,
321
- bundle_id: "00000000-0000-4000-8000-000000000000",
322
- created_at: new Date().toISOString(),
323
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
324
- origin: { mode: "self-hosted-local" },
325
- compatibility: {
326
- min_runtime_version: "0.0.0-test",
327
- max_runtime_version: null,
328
- },
329
- contents: [
330
- {
331
- path: "data/db/assistant.db",
332
- sha256:
333
- "1111111111111111111111111111111111111111111111111111111111111111",
334
- size_bytes: 10,
335
- },
336
- ],
337
- // Deliberately wrong digest. The canonical hash of a v1 manifest
338
- // won't match this, regardless of ordering.
339
- checksum:
340
- "0000000000000000000000000000000000000000000000000000000000000000",
341
- secrets_redacted: false,
342
- export_options: {
343
- include_logs: false,
344
- include_browser_state: false,
345
- include_memory_vectors: false,
346
- },
347
- };
348
- const archive = buildRawVBundle([
349
- {
350
- name: "manifest.json",
351
- data: new TextEncoder().encode(JSON.stringify(badManifest)),
352
- },
353
- ]);
354
- const { entry, drainRest } = await firstEntryOf(archive);
355
-
356
- let err: StreamingValidationError | null = null;
357
- try {
358
- await readAndValidateManifest(entry);
359
- } catch (e) {
360
- err = e as StreamingValidationError;
361
- }
362
- await drainRest();
363
-
364
- expect(err).toBeInstanceOf(StreamingValidationError);
365
- expect(err?.code).toBe("manifest_sha256");
366
- });
367
-
368
- test("throws manifest_duplicate_path when the same archive path appears twice", async () => {
369
- const baseManifest = {
370
- schema_version: 1,
371
- bundle_id: "00000000-0000-4000-8000-000000000001",
372
- created_at: new Date().toISOString(),
373
- assistant: { id: "self", name: "Test", runtime_version: "0.0.0-test" },
374
- origin: { mode: "self-hosted-local" },
375
- compatibility: {
376
- min_runtime_version: "0.0.0-test",
377
- max_runtime_version: null,
378
- },
379
- contents: [
380
- {
381
- path: "data/db/assistant.db",
382
- sha256:
383
- "1111111111111111111111111111111111111111111111111111111111111111",
384
- size_bytes: 10,
385
- },
386
- {
387
- // Deliberately duplicate path — malicious bundle could exploit
388
- // this to bypass per-entry integrity checks if we silently
389
- // collapsed.
390
- path: "data/db/assistant.db",
391
- sha256:
392
- "2222222222222222222222222222222222222222222222222222222222222222",
393
- size_bytes: 20,
394
- },
395
- ],
396
- checksum: "",
397
- secrets_redacted: false,
398
- export_options: {
399
- include_logs: false,
400
- include_browser_state: false,
401
- include_memory_vectors: false,
402
- },
403
- };
404
- baseManifest.checksum = computeManifestChecksum(baseManifest);
405
-
406
- const archive = buildRawVBundle([
407
- {
408
- name: "manifest.json",
409
- data: new TextEncoder().encode(JSON.stringify(baseManifest)),
410
- },
411
- ]);
412
- const { entry, drainRest } = await firstEntryOf(archive);
413
-
414
- let err: StreamingValidationError | null = null;
415
- try {
416
- await readAndValidateManifest(entry);
417
- } catch (e) {
418
- err = e as StreamingValidationError;
419
- }
420
- await drainRest();
421
-
422
- expect(err).toBeInstanceOf(StreamingValidationError);
423
- expect(err?.code).toBe("manifest_duplicate_path");
424
- expect(err?.message).toContain("data/db/assistant.db");
425
- });
426
- });
427
-
428
- // ---------------------------------------------------------------------------
429
- // createHashVerifier
430
- // ---------------------------------------------------------------------------
431
-
432
- describe("createHashVerifier — identity + integrity", () => {
433
- const payload = Buffer.from(
434
- "the quick brown fox jumps over the lazy dog".repeat(100),
435
- "utf8",
436
- );
437
- // Precomputed digest for the payload above.
438
- const payloadSha = createHash("sha256").update(payload).digest("hex");
439
-
440
- test("is an identity Transform for correct inputs", async () => {
441
- const verifier = createHashVerifier({
442
- sha256: payloadSha,
443
- size: payload.length,
444
- archivePath: "workspace/ok.txt",
445
- });
446
-
447
- // Feed the payload in two chunks to exercise multi-call _transform.
448
- const half = payload.length >>> 1;
449
- const source = Readable.from([
450
- payload.subarray(0, half),
451
- payload.subarray(half),
452
- ]);
453
-
454
- const collected: Buffer[] = [];
455
- verifier.on("data", (chunk: Buffer) => collected.push(chunk));
456
-
457
- await pipeline(source, verifier);
458
-
459
- const out = Buffer.concat(collected);
460
- expect(out.length).toBe(payload.length);
461
- expect(out.equals(payload)).toBe(true);
462
- });
463
-
464
- test("errors with code entry_hash on digest mismatch", async () => {
465
- const verifier = createHashVerifier({
466
- sha256:
467
- // Wrong digest.
468
- "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
469
- size: payload.length,
470
- archivePath: "workspace/bad-hash.txt",
471
- });
472
-
473
- let err: StreamingValidationError | null = null;
474
- try {
475
- await pipeline(
476
- Readable.from([payload]),
477
- verifier,
478
- async function* (source: AsyncIterable<Buffer>) {
479
- // Drain the transform so _flush runs.
480
- for await (const _chunk of source) {
481
- // discard
482
- }
483
- },
484
- );
485
- } catch (e) {
486
- err = e as StreamingValidationError;
487
- }
488
-
489
- expect(err).toBeInstanceOf(StreamingValidationError);
490
- expect(err?.code).toBe("entry_hash");
491
- expect(err?.archivePath).toBe("workspace/bad-hash.txt");
492
- });
493
-
494
- test("errors with code entry_size on byte-count mismatch", async () => {
495
- // Right digest, wrong declared size.
496
- const verifier = createHashVerifier({
497
- sha256: payloadSha,
498
- size: payload.length + 1,
499
- archivePath: "workspace/bad-size.txt",
500
- });
501
-
502
- let err: StreamingValidationError | null = null;
503
- try {
504
- await pipeline(
505
- Readable.from([payload]),
506
- verifier,
507
- async function* (source: AsyncIterable<Buffer>) {
508
- for await (const _chunk of source) {
509
- // discard
510
- }
511
- },
512
- );
513
- } catch (e) {
514
- err = e as StreamingValidationError;
515
- }
516
-
517
- expect(err).toBeInstanceOf(StreamingValidationError);
518
- expect(err?.code).toBe("entry_size");
519
- expect(err?.archivePath).toBe("workspace/bad-size.txt");
520
- });
521
- });
522
-
523
- // ---------------------------------------------------------------------------
524
- // readAndValidateManifest — legacy fallback (backwards compatibility)
525
- // ---------------------------------------------------------------------------
526
-
527
- describe("readAndValidateManifest — legacy fallback", () => {
528
- test("accepts a valid legacy six-field manifest and translates to v1 shape", async () => {
529
- const dbBytes = new TextEncoder().encode("legacy-db");
530
- const dbSha = createHash("sha256").update(dbBytes).digest("hex");
531
-
532
- const legacyManifest: Record<string, unknown> = {
533
- schema_version: "1.0",
534
- created_at: new Date().toISOString(),
535
- source: "runtime-export",
536
- description: "legacy fixture",
537
- files: [
538
- { path: "data/db/assistant.db", sha256: dbSha, size: dbBytes.length },
539
- ],
540
- manifest_sha256: "",
541
- };
542
- legacyManifest.manifest_sha256 =
543
- computeLegacyManifestSha256(legacyManifest);
544
-
545
- const archive = buildRawVBundle([
546
- {
547
- name: "manifest.json",
548
- data: new TextEncoder().encode(JSON.stringify(legacyManifest)),
549
- },
550
- { name: "data/db/assistant.db", data: dbBytes },
551
- ]);
552
-
553
- const { entry, drainRest } = await firstEntryOf(archive);
554
- const result = await readAndValidateManifest(entry);
555
- await drainRest();
556
-
557
- expect(result.manifest.schema_version).toBe(1);
558
- expect(result.manifest.contents).toHaveLength(1);
559
- expect(result.manifest.contents[0]?.path).toBe("data/db/assistant.db");
560
- expect(result.manifest.contents[0]?.size_bytes).toBe(dbBytes.length);
561
- expect(result.expected.get("data/db/assistant.db")?.sha256).toBe(dbSha);
562
- });
563
-
564
- test("throws manifest_sha256 on a legacy manifest with a wrong checksum", async () => {
565
- const dbBytes = new TextEncoder().encode("legacy-db");
566
- const dbSha = createHash("sha256").update(dbBytes).digest("hex");
567
-
568
- const badLegacy = {
569
- schema_version: "1.0",
570
- created_at: new Date().toISOString(),
571
- files: [
572
- { path: "data/db/assistant.db", sha256: dbSha, size: dbBytes.length },
573
- ],
574
- // Deliberately wrong checksum.
575
- manifest_sha256:
576
- "0000000000000000000000000000000000000000000000000000000000000000",
577
- };
578
-
579
- const archive = buildRawVBundle([
580
- {
581
- name: "manifest.json",
582
- data: new TextEncoder().encode(JSON.stringify(badLegacy)),
583
- },
584
- ]);
585
- const { entry, drainRest } = await firstEntryOf(archive);
586
-
587
- let err: StreamingValidationError | null = null;
588
- try {
589
- await readAndValidateManifest(entry);
590
- } catch (e) {
591
- err = e as StreamingValidationError;
592
- }
593
- await drainRest();
594
-
595
- expect(err).toBeInstanceOf(StreamingValidationError);
596
- expect(err?.code).toBe("manifest_sha256");
597
- });
598
- });