@mycadet/assistant 0.12.10 → 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 (180) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  2. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  3. package/node_modules/@mycadet/ces-client/src/http-credentials.ts +18 -1
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  5. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  6. package/node_modules/@mycadet/gateway-client/src/gateway-ipc-contracts.ts +0 -20
  7. package/node_modules/@mycadet/service-contracts/src/__tests__/ingress.test.ts +10 -10
  8. package/node_modules/@mycadet/service-contracts/src/ingress.ts +3 -3
  9. package/openapi.yaml +2 -708
  10. package/package.json +2 -3
  11. package/scripts/sync-web-search-catalog.ts +1 -2
  12. package/src/__tests__/cloud-profile-guard.test.ts +41 -5
  13. package/src/__tests__/config-schema.test.ts +6 -38
  14. package/src/__tests__/conversation-queue.test.ts +45 -6
  15. package/src/__tests__/db-readiness-http-gate.test.ts +0 -6
  16. package/src/__tests__/persist-onboarding-artifacts.test.ts +0 -1
  17. package/src/__tests__/persona-resolver.test.ts +0 -51
  18. package/src/__tests__/provider-cloud-proxy-integration.test.ts +2 -110
  19. package/src/__tests__/run-due-schedules.test.ts +0 -4
  20. package/src/__tests__/schedule-store.test.ts +0 -12
  21. package/src/__tests__/stt-stream-session.test.ts +4 -4
  22. package/src/__tests__/telephony-tts-guard.test.ts +2 -50
  23. package/src/__tests__/voice-config-update.test.ts +12 -167
  24. package/src/__tests__/web-search-catalog-parity.test.ts +0 -2
  25. package/src/__tests__/workspace-migration-130-speech-mode-to-provider.test.ts +9 -7
  26. package/src/__tests__/workspace-migration-132-web-search-mode-to-provider.test.ts +6 -6
  27. package/src/calls/__tests__/voice-session-bridge.test.ts +1 -4
  28. package/src/calls/resolve-call-tts-provider.ts +6 -25
  29. package/src/calls/telephony-credential-preflight.ts +0 -10
  30. package/src/calls/telephony-tts-capability.ts +1 -17
  31. package/src/cli/commands/tts.ts +1 -2
  32. package/src/cli/index.help.ts +0 -2
  33. package/src/cli/program.ts +0 -2
  34. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +4 -42
  35. package/src/config/schemas/__tests__/stt.test.ts +10 -14
  36. package/src/config/schemas/llm.ts +3 -5
  37. package/src/config/schemas/services.ts +5 -6
  38. package/src/config/schemas/stt.ts +1 -3
  39. package/src/config/schemas/tts.ts +1 -30
  40. package/src/daemon/daemon-readiness.ts +4 -29
  41. package/src/daemon/lifecycle.ts +0 -31
  42. package/src/daemon/providers-setup.ts +0 -8
  43. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  44. package/src/heartbeat/heartbeat-service.ts +1 -66
  45. package/src/ipc/__tests__/db-readiness-ipc-gate.test.ts +1 -9
  46. package/src/live-voice/__tests__/live-voice-credential-preflight.test.ts +0 -44
  47. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +4 -6
  48. package/src/live-voice/__tests__/live-voice-stt.test.ts +1 -1
  49. package/src/live-voice/live-voice-credential-preflight.ts +9 -33
  50. package/src/live-voice/live-voice-session.ts +10 -6
  51. package/src/live-voice/live-voice-tts.ts +1 -8
  52. package/src/plugins/defaults/memory/worker.ts +3 -3
  53. package/src/prompts/persona-resolver.ts +0 -35
  54. package/src/providers/__tests__/search-provider-catalog.test.ts +5 -8
  55. package/src/providers/__tests__/voice-error-copy.test.ts +9 -10
  56. package/src/providers/search-provider-catalog.ts +7 -39
  57. package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +3 -47
  58. package/src/providers/speech-to-text/__tests__/resolve.test.ts +6 -372
  59. package/src/providers/speech-to-text/provider-catalog.ts +2 -73
  60. package/src/providers/speech-to-text/resolve.ts +3 -120
  61. package/src/runtime/http-router-types.ts +1 -3
  62. package/src/runtime/http-server.ts +1 -1
  63. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +13 -11
  64. package/src/runtime/routes/__tests__/tts-routes.test.ts +2 -6
  65. package/src/runtime/routes/conversation-query-routes.ts +0 -96
  66. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +7 -7
  67. package/src/runtime/routes/index.ts +0 -4
  68. package/src/runtime/routes/live-voice-routes.test.ts +4 -11
  69. package/src/runtime/routes/live-voice-routes.ts +8 -11
  70. package/src/runtime/routes/secret-routes.ts +0 -6
  71. package/src/runtime/routes/tts-routes.ts +0 -38
  72. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +1 -7
  73. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +0 -4
  74. package/src/schedule/schedule-store.ts +0 -7
  75. package/src/schedule/scheduler.ts +0 -5
  76. package/src/schedule/worker.ts +1 -1
  77. package/src/stt/__tests__/daemon-batch-transcriber.test.ts +2 -10
  78. package/src/stt/daemon-batch-transcriber.ts +4 -38
  79. package/src/stt/types.ts +1 -9
  80. package/src/tools/network/__tests__/web-search.test.ts +3 -250
  81. package/src/tools/network/web-search.ts +4 -165
  82. package/src/tts/__tests__/provider-resolution.test.ts +0 -5
  83. package/src/tts/provider-catalog.ts +0 -4
  84. package/src/tts/types.ts +0 -1
  85. package/src/__tests__/cloud-speech-defaults.test.ts +0 -330
  86. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1708
  87. package/src/__tests__/migration-export-http.test.ts +0 -557
  88. package/src/__tests__/migration-export-streaming.test.ts +0 -350
  89. package/src/__tests__/migration-export-to-gcs.test.ts +0 -706
  90. package/src/__tests__/migration-import-commit-http.test.ts +0 -1176
  91. package/src/__tests__/migration-import-from-gcs.test.ts +0 -534
  92. package/src/__tests__/migration-import-from-path.test.ts +0 -324
  93. package/src/__tests__/migration-import-from-url.test.ts +0 -588
  94. package/src/__tests__/migration-import-preflight-http.test.ts +0 -739
  95. package/src/__tests__/migration-jobs-status.test.ts +0 -139
  96. package/src/__tests__/migration-parity-persistence.test.ts +0 -1891
  97. package/src/__tests__/migration-transport.test.ts +0 -1089
  98. package/src/__tests__/migration-validate-http.test.ts +0 -634
  99. package/src/__tests__/migration-wizard.test.ts +0 -1395
  100. package/src/__tests__/rebind-secrets-screen.test.ts +0 -876
  101. package/src/__tests__/sanitize-config-for-transfer.test.ts +0 -259
  102. package/src/__tests__/transfer-progress-screen.test.ts +0 -1217
  103. package/src/__tests__/validation-results-screen.test.ts +0 -1130
  104. package/src/__tests__/vbundle-pax-and-symlink.test.ts +0 -147
  105. package/src/background-wake/background-wake-routes.test.ts +0 -906
  106. package/src/background-wake/cloud-client.test.ts +0 -328
  107. package/src/background-wake/cloud-client.ts +0 -196
  108. package/src/background-wake/next-wake.test.ts +0 -335
  109. package/src/background-wake/next-wake.ts +0 -188
  110. package/src/background-wake/publisher.ts +0 -93
  111. package/src/background-wake/wake-intent-hooks.test.ts +0 -234
  112. package/src/cli/commands/__tests__/backup.test.ts +0 -363
  113. package/src/cli/commands/backup.help.ts +0 -180
  114. package/src/cli/commands/backup.ts +0 -398
  115. package/src/cloud/cloud-speech.test.ts +0 -335
  116. package/src/cloud/cloud-speech.ts +0 -300
  117. package/src/config/cloud-speech-defaults.ts +0 -300
  118. package/src/config/sanitize-for-transfer.ts +0 -111
  119. package/src/live-voice/__tests__/live-voice-cloud-speech-fallback.test.ts +0 -250
  120. package/src/providers/speech-to-text/__tests__/cadet-cloud-realtime.test.ts +0 -457
  121. package/src/providers/speech-to-text/__tests__/cadet-cloud.test.ts +0 -271
  122. package/src/providers/speech-to-text/__tests__/cadet-speech-relay-connection.test.ts +0 -126
  123. package/src/providers/speech-to-text/cadet-cloud-flux-realtime.ts +0 -309
  124. package/src/providers/speech-to-text/cadet-cloud-realtime.ts +0 -315
  125. package/src/providers/speech-to-text/cadet-cloud.ts +0 -108
  126. package/src/providers/speech-to-text/cadet-speech-relay-connection.ts +0 -176
  127. package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +0 -254
  128. package/src/runtime/migrations/__tests__/job-registry.test.ts +0 -346
  129. package/src/runtime/migrations/__tests__/origin-mode.test.ts +0 -62
  130. package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +0 -184
  131. package/src/runtime/migrations/__tests__/staged-import-path.test.ts +0 -110
  132. package/src/runtime/migrations/__tests__/v1-test-helpers.ts +0 -112
  133. package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +0 -285
  134. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +0 -126
  135. package/src/runtime/migrations/__tests__/vbundle-builder-v1-shape.test.ts +0 -253
  136. package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +0 -208
  137. package/src/runtime/migrations/__tests__/vbundle-import-parity.test.ts +0 -485
  138. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +0 -349
  139. package/src/runtime/migrations/__tests__/vbundle-import-version-compat.test.ts +0 -196
  140. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +0 -551
  141. package/src/runtime/migrations/__tests__/vbundle-metadata-merge-integration.test.ts +0 -428
  142. package/src/runtime/migrations/__tests__/vbundle-metadata-merge.test.ts +0 -220
  143. package/src/runtime/migrations/__tests__/vbundle-streaming-importer.test.ts +0 -1823
  144. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +0 -598
  145. package/src/runtime/migrations/__tests__/vbundle-symlink-importer.test.ts +0 -463
  146. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming-importer.test.ts +0 -538
  147. package/src/runtime/migrations/__tests__/vbundle-symlink-streaming.test.ts +0 -515
  148. package/src/runtime/migrations/__tests__/vbundle-symlink-tar.test.ts +0 -453
  149. package/src/runtime/migrations/__tests__/vbundle-symlink-walker.test.ts +0 -319
  150. package/src/runtime/migrations/__tests__/vbundle-tar-stream.test.ts +0 -240
  151. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +0 -473
  152. package/src/runtime/migrations/gcs-signed-url.ts +0 -190
  153. package/src/runtime/migrations/job-registry.ts +0 -297
  154. package/src/runtime/migrations/migration-transport.ts +0 -612
  155. package/src/runtime/migrations/migration-wizard.ts +0 -747
  156. package/src/runtime/migrations/origin-mode.ts +0 -42
  157. package/src/runtime/migrations/rebind-secrets-screen.ts +0 -412
  158. package/src/runtime/migrations/staged-import-path.ts +0 -116
  159. package/src/runtime/migrations/transfer-progress-screen.ts +0 -323
  160. package/src/runtime/migrations/validation-results-screen.ts +0 -469
  161. package/src/runtime/migrations/vbundle-builder.ts +0 -1382
  162. package/src/runtime/migrations/vbundle-import-analyzer.ts +0 -405
  163. package/src/runtime/migrations/vbundle-import-policy.ts +0 -241
  164. package/src/runtime/migrations/vbundle-importer.ts +0 -826
  165. package/src/runtime/migrations/vbundle-metadata-merge.ts +0 -113
  166. package/src/runtime/migrations/vbundle-streaming-importer.ts +0 -2844
  167. package/src/runtime/migrations/vbundle-streaming-validator.ts +0 -426
  168. package/src/runtime/migrations/vbundle-tar-stream.ts +0 -244
  169. package/src/runtime/migrations/vbundle-validator.ts +0 -724
  170. package/src/runtime/routes/__tests__/migration-cadet-metadata-reconcile.test.ts +0 -227
  171. package/src/runtime/routes/__tests__/migration-export-secrets-redacted.test.ts +0 -54
  172. package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +0 -70
  173. package/src/runtime/routes/__tests__/migration-import-credential-filter.test.ts +0 -257
  174. package/src/runtime/routes/background-wake-routes.ts +0 -387
  175. package/src/runtime/routes/migration-routes.ts +0 -2584
  176. package/src/tools/network/__tests__/cloud-search-proxy.test.ts +0 -282
  177. package/src/tools/network/cloud-search-proxy.ts +0 -211
  178. package/src/tts/__tests__/cadet-provider.test.ts +0 -253
  179. package/src/tts/providers/cadet-provider.ts +0 -244
  180. package/src/types/tar-stream.d.ts +0 -66
@@ -1,254 +0,0 @@
1
- /**
2
- * Tests for the GCS signed-URL validator.
3
- *
4
- * Covers:
5
- * - A valid V4-signed URL (`X-Goog-Signature=...`) is accepted and the
6
- * returned host/path match the input.
7
- * - A valid V2-signed URL (`Signature=...`) is accepted.
8
- * - `http://` is rejected with reason `scheme`.
9
- * - A non-GCS host is rejected with reason `host`.
10
- * - A GCS URL with no signature query param is rejected with reason
11
- * `missing_signature`; SigV4 (`X-Amz-Signature`) is accepted only for
12
- * hosts on a non-default allowlist, and unsigned URLs are rejected
13
- * everywhere.
14
- * - A GCS URL with a non-default explicit port (e.g. `:1234`, `:80`) is
15
- * rejected with reason `port`. A URL with `:443` is accepted because
16
- * WHATWG URL normalizes the default HTTPS port to an empty port
17
- * string.
18
- * - Path traversal (`/bucket/../foo`) is rejected with reason
19
- * `path_traversal`, even though the URL parser would normalize it.
20
- * - Encoded-slash traversal (`%2e%2e%2fother`), backslash-separator
21
- * traversal (`\..\secret`), uppercase percent encoding, and an
22
- * encoded dot-dot at the start of the path are all rejected.
23
- * - A malformed URL string is rejected with reason `invalid_url`.
24
- */
25
-
26
- import { describe, expect, test } from "bun:test";
27
-
28
- import { validateGcsSignedUrl } from "../gcs-signed-url.js";
29
-
30
- describe("validateGcsSignedUrl", () => {
31
- test("accepts a V4-signed URL", () => {
32
- const url =
33
- "https://storage.googleapis.com/my-bucket/path/to/object.tgz" +
34
- "?X-Goog-Algorithm=GOOG4-RSA-SHA256" +
35
- "&X-Goog-Credential=service-account%40project.iam.gserviceaccount.com" +
36
- "&X-Goog-Date=20260420T000000Z" +
37
- "&X-Goog-Expires=3600" +
38
- "&X-Goog-SignedHeaders=host" +
39
- "&X-Goog-Signature=deadbeef";
40
-
41
- const result = validateGcsSignedUrl(url);
42
- expect(result.ok).toBe(true);
43
- if (result.ok) {
44
- expect(result.host).toBe("storage.googleapis.com");
45
- expect(result.path).toBe("/my-bucket/path/to/object.tgz");
46
- }
47
- });
48
-
49
- test("accepts a V2-signed URL", () => {
50
- const url =
51
- "https://storage.googleapis.com/my-bucket/object.tgz" +
52
- "?GoogleAccessId=service-account%40project.iam.gserviceaccount.com" +
53
- "&Expires=1700000000" +
54
- "&Signature=abc123";
55
-
56
- const result = validateGcsSignedUrl(url);
57
- expect(result.ok).toBe(true);
58
- if (result.ok) {
59
- expect(result.host).toBe("storage.googleapis.com");
60
- expect(result.path).toBe("/my-bucket/object.tgz");
61
- }
62
- });
63
-
64
- test("rejects http:// scheme", () => {
65
- const url =
66
- "http://storage.googleapis.com/my-bucket/object.tgz" +
67
- "?X-Goog-Signature=deadbeef";
68
-
69
- const result = validateGcsSignedUrl(url);
70
- expect(result).toEqual({ ok: false, reason: "scheme" });
71
- });
72
-
73
- test("accepts http:// for a host on a non-default allowlist", () => {
74
- // A non-default allowlist (e.g. local/minikube where the bundle is
75
- // served over http from host.docker.internal) relaxes the scheme check
76
- // to also accept http and skips the explicit-port check.
77
- const url =
78
- "http://host.docker.internal:8000/bucket/object.tgz" +
79
- "?X-Goog-Signature=deadbeef";
80
-
81
- const result = validateGcsSignedUrl(url, {
82
- allowedHosts: ["host.docker.internal"],
83
- });
84
- expect(result.ok).toBe(true);
85
- if (result.ok) {
86
- expect(result.host).toBe("host.docker.internal");
87
- expect(result.path).toBe("/bucket/object.tgz");
88
- }
89
- });
90
-
91
- test("still rejects a host absent from a non-default allowlist", () => {
92
- const url =
93
- "http://evil.internal/bucket/object.tgz" + "?X-Goog-Signature=deadbeef";
94
-
95
- const result = validateGcsSignedUrl(url, {
96
- allowedHosts: ["host.docker.internal"],
97
- });
98
- expect(result).toEqual({ ok: false, reason: "host" });
99
- });
100
-
101
- test("rejects a non-GCS host", () => {
102
- const url =
103
- "https://evil.com/my-bucket/object.tgz" + "?X-Goog-Signature=deadbeef";
104
-
105
- const result = validateGcsSignedUrl(url);
106
- expect(result).toEqual({ ok: false, reason: "host" });
107
- });
108
-
109
- test("rejects a GCS URL with no signature", () => {
110
- const url = "https://storage.googleapis.com/bucket/key";
111
-
112
- const result = validateGcsSignedUrl(url);
113
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
114
- });
115
-
116
- test("accepts a SigV4-signed URL for a host on a non-default allowlist", () => {
117
- // The local/minikube platform serves SigV4 presigned URLs (SeaweedFS)
118
- // over plain http — X-Amz-Signature satisfies the signature-shape
119
- // requirement for explicitly allowlisted hosts.
120
- const url =
121
- "http://host.docker.internal:8000/bucket/object.tgz" +
122
- "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Signature=deadbeef";
123
-
124
- const result = validateGcsSignedUrl(url, {
125
- allowedHosts: ["host.docker.internal"],
126
- });
127
- expect(result.ok).toBe(true);
128
- if (result.ok) {
129
- expect(result.host).toBe("host.docker.internal");
130
- expect(result.path).toBe("/bucket/object.tgz");
131
- }
132
- });
133
-
134
- test("rejects an unsigned URL even on a non-default allowlist", () => {
135
- const url = "http://host.docker.internal:8000/bucket/object.tgz";
136
-
137
- const result = validateGcsSignedUrl(url, {
138
- allowedHosts: ["host.docker.internal"],
139
- });
140
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
141
- });
142
-
143
- test("rejects X-Amz-Signature on the default GCS allowlist", () => {
144
- // SigV4 is only meaningful for allowlisted S3-compatible hosts; a GCS
145
- // URL signed with the wrong scheme's param is not a GCS presigned URL.
146
- const url =
147
- "https://storage.googleapis.com/bucket/key?X-Amz-Signature=deadbeef";
148
-
149
- const result = validateGcsSignedUrl(url);
150
- expect(result).toEqual({ ok: false, reason: "missing_signature" });
151
- });
152
-
153
- test("still rejects traversal in a SigV4-signed allowlisted URL", () => {
154
- const url =
155
- "http://host.docker.internal:8000/bucket/../secret" +
156
- "?X-Amz-Signature=deadbeef";
157
-
158
- const result = validateGcsSignedUrl(url, {
159
- allowedHosts: ["host.docker.internal"],
160
- });
161
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
162
- });
163
-
164
- test("rejects a GCS URL with a non-default port (1234)", () => {
165
- const url =
166
- "https://storage.googleapis.com:1234/bucket/key" +
167
- "?X-Goog-Signature=deadbeef";
168
-
169
- const result = validateGcsSignedUrl(url);
170
- expect(result).toEqual({ ok: false, reason: "port" });
171
- });
172
-
173
- test("rejects a GCS URL with HTTP default port (:80)", () => {
174
- const url =
175
- "https://storage.googleapis.com:80/bucket/key" +
176
- "?X-Goog-Signature=deadbeef";
177
-
178
- const result = validateGcsSignedUrl(url);
179
- expect(result).toEqual({ ok: false, reason: "port" });
180
- });
181
-
182
- test("accepts a GCS URL with explicit :443 (normalized to empty port)", () => {
183
- // WHATWG URL normalizes `:443` to an empty port string for HTTPS,
184
- // so the port check (`parsed.port !== ""`) does not reject it.
185
- // A correctly-issued signed URL with an explicit default port is
186
- // therefore unaffected by the port guard.
187
- const url =
188
- "https://storage.googleapis.com:443/my-bucket/object.tgz" +
189
- "?X-Goog-Signature=deadbeef";
190
-
191
- // Sanity-check the WHATWG normalization assumption.
192
- expect(new URL(url).port).toBe("");
193
-
194
- const result = validateGcsSignedUrl(url);
195
- expect(result.ok).toBe(true);
196
- if (result.ok) {
197
- expect(result.host).toBe("storage.googleapis.com");
198
- expect(result.path).toBe("/my-bucket/object.tgz");
199
- }
200
- });
201
-
202
- test("rejects path traversal", () => {
203
- const url =
204
- "https://storage.googleapis.com/bucket/../foo" +
205
- "?X-Goog-Signature=deadbeef";
206
-
207
- const result = validateGcsSignedUrl(url);
208
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
209
- });
210
-
211
- test("rejects path traversal with encoded slash (%2e%2e%2fother)", () => {
212
- const url =
213
- "https://storage.googleapis.com/bucket/%2e%2e%2fother" +
214
- "?X-Goog-Signature=deadbeef";
215
-
216
- const result = validateGcsSignedUrl(url);
217
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
218
- });
219
-
220
- test("rejects path traversal with backslash separators", () => {
221
- // Note: the WHATWG URL parser normalizes `\` to `/` for special
222
- // schemes like `https:`, so `new URL()` would collapse this to
223
- // `/secret`. The raw-path guard must still reject it.
224
- const url =
225
- "https://storage.googleapis.com/bucket\\..\\secret" +
226
- "?X-Goog-Signature=deadbeef";
227
-
228
- const result = validateGcsSignedUrl(url);
229
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
230
- });
231
-
232
- test("rejects path traversal with uppercase percent encoding", () => {
233
- const url =
234
- "https://storage.googleapis.com/bucket/%2E%2E/other" +
235
- "?X-Goog-Signature=deadbeef";
236
-
237
- const result = validateGcsSignedUrl(url);
238
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
239
- });
240
-
241
- test("rejects encoded dot-dot at the start of the path", () => {
242
- const url =
243
- "https://storage.googleapis.com/%2e%2e/bucket/key" +
244
- "?X-Goog-Signature=deadbeef";
245
-
246
- const result = validateGcsSignedUrl(url);
247
- expect(result).toEqual({ ok: false, reason: "path_traversal" });
248
- });
249
-
250
- test("rejects a malformed URL", () => {
251
- const result = validateGcsSignedUrl("not a url at all");
252
- expect(result).toEqual({ ok: false, reason: "invalid_url" });
253
- });
254
- });
@@ -1,346 +0,0 @@
1
- /**
2
- * Tests for the in-memory migration job registry.
3
- *
4
- * Covered:
5
- * - Happy path: start -> poll pending -> poll running -> poll complete with result.
6
- * - Failure path: runner throws -> job ends in `failed` with mapped error.
7
- * - `kFetchBodyError` path: tagged thrown object maps to `fetch_failed` with
8
- * optional `upstreamStatus` preserved.
9
- * - Concurrent limit: second export while one is pending/running rejects with
10
- * `JobAlreadyInProgressError`; a job of the other type is allowed alongside.
11
- * - TTL sweep: completed job older than the TTL is evicted; job that is not
12
- * yet past TTL is retained.
13
- */
14
-
15
- import { describe, expect, test } from "bun:test";
16
-
17
- import {
18
- JobAlreadyInProgressError,
19
- type MigrationJob,
20
- MigrationJobRegistry,
21
- } from "../job-registry.js";
22
-
23
- const kFetchBodyError = Symbol.for("cadet.migrationImport.fetchBodyError");
24
-
25
- /** Spin the microtask queue N times so `queueMicrotask`-scheduled work runs. */
26
- async function flushMicrotasks(n = 4): Promise<void> {
27
- for (let i = 0; i < n; i++) {
28
- await Promise.resolve();
29
- }
30
- }
31
-
32
- describe("MigrationJobRegistry", () => {
33
- test("happy path: pending -> running -> complete with result", async () => {
34
- const registry = new MigrationJobRegistry();
35
-
36
- let release: (value: string) => void = () => {};
37
- const gate = new Promise<string>((resolve) => {
38
- release = resolve;
39
- });
40
-
41
- const job = registry.startJob("export", async (): Promise<string> => {
42
- return await gate;
43
- });
44
-
45
- // Returned synchronously in pending state.
46
- expect(job.status).toBe("pending");
47
- expect(job.id).toMatch(
48
- /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
49
- );
50
- expect(job.type).toBe("export");
51
- expect(job.createdAt).toBeGreaterThan(0);
52
- expect(job.startedAt).toBeUndefined();
53
- expect(job.completedAt).toBeUndefined();
54
-
55
- // Let the microtask run -> status flips to running with `startedAt` set.
56
- await flushMicrotasks();
57
- const running = registry.getJob(job.id);
58
- expect(running).not.toBeNull();
59
- expect(running!.status).toBe("running");
60
- expect(running!.startedAt).toBeGreaterThanOrEqual(running!.createdAt);
61
-
62
- // Resolve the runner and let it settle.
63
- release("the-result");
64
- await flushMicrotasks();
65
-
66
- const done = registry.getJob(job.id);
67
- expect(done).not.toBeNull();
68
- expect(done!.status).toBe("complete");
69
- expect(done!.result).toBe("the-result");
70
- expect(done!.completedAt).toBeGreaterThanOrEqual(done!.startedAt ?? 0);
71
- expect(done!.error).toBeUndefined();
72
-
73
- // `listJobs` sees it too.
74
- expect(registry.listJobs().map((j) => j.id)).toContain(job.id);
75
- });
76
-
77
- test("failure path: runner throws -> failed with mapped error", async () => {
78
- const registry = new MigrationJobRegistry();
79
- const job = registry.startJob("export", async () => {
80
- throw new Error("boom");
81
- });
82
-
83
- await flushMicrotasks();
84
-
85
- const done = registry.getJob(job.id);
86
- expect(done).not.toBeNull();
87
- expect(done!.status).toBe("failed");
88
- expect(done!.error).toEqual({ code: "unknown", message: "boom" });
89
- expect(done!.result).toBeUndefined();
90
- expect(done!.completedAt).toBeGreaterThanOrEqual(done!.startedAt ?? 0);
91
- });
92
-
93
- test("runner-provided `code` overrides the default `unknown`", async () => {
94
- const registry = new MigrationJobRegistry();
95
- const job = registry.startJob("import", async () => {
96
- const err = new Error("invalid manifest") as Error & { code: string };
97
- err.code = "invalid_manifest";
98
- throw err;
99
- });
100
-
101
- await flushMicrotasks();
102
-
103
- const done = registry.getJob(job.id);
104
- expect(done!.status).toBe("failed");
105
- expect(done!.error).toEqual({
106
- code: "invalid_manifest",
107
- message: "invalid manifest",
108
- });
109
- });
110
-
111
- test("kFetchBodyError path: tagged error maps to `fetch_failed`", async () => {
112
- const registry = new MigrationJobRegistry();
113
- const job = registry.startJob("import", async () => {
114
- const err = new Error("upstream hung up") as Error & {
115
- upstreamStatus?: number;
116
- };
117
- (err as unknown as Record<symbol, boolean>)[kFetchBodyError] = true;
118
- err.upstreamStatus = 502;
119
- throw err;
120
- });
121
-
122
- await flushMicrotasks();
123
-
124
- const done = registry.getJob(job.id);
125
- expect(done).not.toBeNull();
126
- expect(done!.status).toBe("failed");
127
- expect(done!.error).toEqual({
128
- code: "fetch_failed",
129
- message: "upstream hung up",
130
- upstreamStatus: 502,
131
- });
132
- });
133
-
134
- test("kFetchBodyError without upstreamStatus omits the field", async () => {
135
- const registry = new MigrationJobRegistry();
136
- const job = registry.startJob("import", async () => {
137
- const err = new Error("aborted") as Error;
138
- (err as unknown as Record<symbol, boolean>)[kFetchBodyError] = true;
139
- throw err;
140
- });
141
-
142
- await flushMicrotasks();
143
-
144
- const done = registry.getJob(job.id)!;
145
- expect(done.status).toBe("failed");
146
- expect(done.error).toEqual({
147
- code: "fetch_failed",
148
- message: "aborted",
149
- });
150
- expect(done.error?.upstreamStatus).toBeUndefined();
151
- });
152
-
153
- test("concurrent limit: second same-type job rejects; other type is allowed", async () => {
154
- const registry = new MigrationJobRegistry();
155
-
156
- let release: () => void = () => {};
157
- const gate = new Promise<void>((resolve) => {
158
- release = resolve;
159
- });
160
-
161
- // First export is pending (microtask hasn't run yet) — second export
162
- // immediately rejects on the still-pending first job.
163
- const first = registry.startJob("export", async () => {
164
- await gate;
165
- });
166
- expect(first.status).toBe("pending");
167
-
168
- expect(() => registry.startJob("export", async () => {})).toThrow(
169
- JobAlreadyInProgressError,
170
- );
171
-
172
- // Advance to running — a same-type attempt still rejects.
173
- await flushMicrotasks();
174
- expect(registry.getJob(first.id)!.status).toBe("running");
175
-
176
- let caught: unknown = null;
177
- try {
178
- registry.startJob("export", async () => {});
179
- } catch (err) {
180
- caught = err;
181
- }
182
- expect(caught).toBeInstanceOf(JobAlreadyInProgressError);
183
- expect((caught as JobAlreadyInProgressError).existingJobId).toBe(first.id);
184
-
185
- // A job of the *other* type is allowed in parallel.
186
- const importJob = registry.startJob("import", async () => "ok");
187
- expect(importJob.type).toBe("import");
188
- expect(importJob.id).not.toBe(first.id);
189
-
190
- // Settle both before leaving the test so no promises leak.
191
- release();
192
- await flushMicrotasks();
193
- expect(registry.getJob(first.id)!.status).toBe("complete");
194
- expect(registry.getJob(importJob.id)!.status).toBe("complete");
195
-
196
- // And now a new export is allowed since nothing is in flight.
197
- const third = registry.startJob("export", async () => "again");
198
- expect(third.status).toBe("pending");
199
- await flushMicrotasks();
200
- expect(registry.getJob(third.id)!.status).toBe("complete");
201
- });
202
-
203
- test("returned job snapshots are decoupled from internal registry state", async () => {
204
- const registry = new MigrationJobRegistry();
205
-
206
- let release: () => void = () => {};
207
- const gate = new Promise<void>((resolve) => {
208
- release = resolve;
209
- });
210
-
211
- const first = registry.startJob("export", async () => {
212
- await gate;
213
- });
214
-
215
- // The synchronous return of startJob must be a snapshot, not the
216
- // internal record. Mutating it (e.g. to simulate a caller stomping on
217
- // `status`) must not unblock the single-in-flight invariant while the
218
- // runner is still active.
219
- first.status = "complete";
220
- first.completedAt = Date.now();
221
-
222
- await flushMicrotasks();
223
- // Internal state is still running — the snapshot mutation did not leak.
224
- expect(registry.getJob(first.id)!.status).toBe("running");
225
-
226
- // Attempting a second same-type job must still reject.
227
- expect(() => registry.startJob("export", async () => {})).toThrow(
228
- JobAlreadyInProgressError,
229
- );
230
-
231
- // getJob snapshots are also decoupled: mutating the return value of
232
- // getJob does not leak into listJobs or subsequent getJob calls.
233
- const polled = registry.getJob(first.id)!;
234
- polled.status = "failed";
235
- expect(registry.getJob(first.id)!.status).toBe("running");
236
- expect(registry.listJobs().find((j) => j.id === first.id)!.status).toBe(
237
- "running",
238
- );
239
-
240
- // Settle.
241
- release();
242
- await flushMicrotasks();
243
- expect(registry.getJob(first.id)!.status).toBe("complete");
244
- });
245
-
246
- test("TTL sweep: completed jobs older than TTL are evicted", async () => {
247
- const registry = new MigrationJobRegistry();
248
- registry.completedJobTtlMs = 1_000; // 1s for test purposes
249
-
250
- const job = registry.startJob("export", async () => "done");
251
- await flushMicrotasks();
252
- const completed = registry.getJob(job.id)!;
253
- expect(completed.status).toBe("complete");
254
-
255
- // Before TTL: sweep is a no-op.
256
- const originalNow = Date.now;
257
- try {
258
- Date.now = () => (completed.completedAt ?? 0) + 500;
259
- registry.sweep();
260
- expect(registry.getJob(job.id)).not.toBeNull();
261
- expect(registry.listJobs().map((j) => j.id)).toContain(job.id);
262
-
263
- // Past TTL: sweep evicts the completed job.
264
- Date.now = () => (completed.completedAt ?? 0) + 2_000;
265
- registry.sweep();
266
- expect(registry.getJob(job.id)).toBeNull();
267
- expect(registry.listJobs().map((j) => j.id)).not.toContain(job.id);
268
- } finally {
269
- Date.now = originalNow;
270
- }
271
- });
272
-
273
- test("TTL sweep: failed jobs are also evicted past TTL", async () => {
274
- const registry = new MigrationJobRegistry();
275
- registry.completedJobTtlMs = 500;
276
-
277
- const job = registry.startJob("import", async () => {
278
- throw new Error("nope");
279
- });
280
- await flushMicrotasks();
281
- const failed = registry.getJob(job.id)!;
282
- expect(failed.status).toBe("failed");
283
-
284
- const originalNow = Date.now;
285
- try {
286
- Date.now = () => (failed.completedAt ?? 0) + 10_000;
287
- registry.sweep();
288
- expect(registry.getJob(job.id)).toBeNull();
289
- } finally {
290
- Date.now = originalNow;
291
- }
292
- });
293
-
294
- test("TTL sweep: running jobs are never evicted", async () => {
295
- const registry = new MigrationJobRegistry();
296
- registry.completedJobTtlMs = 1;
297
-
298
- let release: () => void = () => {};
299
- const gate = new Promise<void>((resolve) => {
300
- release = resolve;
301
- });
302
- const job = registry.startJob("export", async () => {
303
- await gate;
304
- });
305
- await flushMicrotasks();
306
- expect(registry.getJob(job.id)!.status).toBe("running");
307
-
308
- const originalNow = Date.now;
309
- try {
310
- Date.now = () => Number.MAX_SAFE_INTEGER;
311
- registry.sweep();
312
- expect(registry.getJob(job.id)).not.toBeNull();
313
- } finally {
314
- Date.now = originalNow;
315
- }
316
-
317
- // Cleanup: let the runner resolve.
318
- release();
319
- await flushMicrotasks();
320
- });
321
-
322
- test("getJob returns null for unknown ids", () => {
323
- const registry = new MigrationJobRegistry();
324
- expect(registry.getJob("nope")).toBeNull();
325
- });
326
-
327
- test("JobAlreadyInProgressError carries the existing job id", () => {
328
- const err = new JobAlreadyInProgressError("export", "abc-123");
329
- expect(err).toBeInstanceOf(Error);
330
- expect(err.name).toBe("JobAlreadyInProgressError");
331
- expect(err.existingJobId).toBe("abc-123");
332
- expect(err.message).toContain("export");
333
- expect(err.message).toContain("abc-123");
334
- });
335
-
336
- test("MigrationJob interface shape is preserved on success", async () => {
337
- const registry = new MigrationJobRegistry();
338
- const job = registry.startJob("export", async () => ({ bytes: 42 }));
339
- await flushMicrotasks();
340
- const done: MigrationJob = registry.getJob(job.id)!;
341
- expect(done.id).toBe(job.id);
342
- expect(done.type).toBe("export");
343
- expect(done.status).toBe("complete");
344
- expect(done.result).toEqual({ bytes: 42 });
345
- });
346
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * Tests for `getOriginMode` — the vbundle manifest `origin.mode` derivation.
3
- *
4
- * Covers:
5
- * - `IS_CLOUD=true` → "managed" (wins over IS_CONTAINERIZED).
6
- * - `IS_CONTAINERIZED=true` (no IS_CLOUD) → "self-hosted-remote".
7
- * - Neither → "self-hosted-local".
8
- * - Regression: a logged-in local assistant holding cloud-proxy
9
- * credentials (platform URL + assistant_api_key) must NOT be classified
10
- * "managed" — every logged-in local has those credentials, and a
11
- * "managed" stamp trips the importer's `secrets_redacted must be true
12
- * when origin.mode is 'managed'` refine, breaking local→platform
13
- * teleport. The helper keys on the IS_CLOUD deployment env only —
14
- * nothing here consults the credential store, so the regression case is
15
- * simply "no env flags set".
16
- */
17
-
18
- import { afterEach, describe, expect, test } from "bun:test";
19
-
20
- import { getOriginMode } from "../origin-mode.js";
21
-
22
- const ORIGINAL_IS_CLOUD = process.env.IS_CLOUD;
23
- const ORIGINAL_IS_CONTAINERIZED = process.env.IS_CONTAINERIZED;
24
-
25
- function restore(name: string, value: string | undefined): void {
26
- if (value === undefined) {
27
- delete process.env[name];
28
- } else {
29
- process.env[name] = value;
30
- }
31
- }
32
-
33
- afterEach(() => {
34
- restore("IS_CLOUD", ORIGINAL_IS_CLOUD);
35
- restore("IS_CONTAINERIZED", ORIGINAL_IS_CONTAINERIZED);
36
- });
37
-
38
- describe("getOriginMode", () => {
39
- test("IS_CLOUD=true → managed", async () => {
40
- process.env.IS_CLOUD = "true";
41
- delete process.env.IS_CONTAINERIZED;
42
- expect(await getOriginMode()).toBe("managed");
43
- });
44
-
45
- test("IS_CLOUD wins over IS_CONTAINERIZED", async () => {
46
- process.env.IS_CLOUD = "1";
47
- process.env.IS_CONTAINERIZED = "true";
48
- expect(await getOriginMode()).toBe("managed");
49
- });
50
-
51
- test("IS_CONTAINERIZED=true without IS_CLOUD → self-hosted-remote", async () => {
52
- delete process.env.IS_CLOUD;
53
- process.env.IS_CONTAINERIZED = "true";
54
- expect(await getOriginMode()).toBe("self-hosted-remote");
55
- });
56
-
57
- test("neither flag → self-hosted-local (incl. logged-in locals with cloud-proxy creds)", async () => {
58
- delete process.env.IS_CLOUD;
59
- delete process.env.IS_CONTAINERIZED;
60
- expect(await getOriginMode()).toBe("self-hosted-local");
61
- });
62
- });