@vellumai/vellum-gateway 0.5.9 → 0.5.11

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/AGENTS.md CHANGED
@@ -25,9 +25,9 @@ All assistant API requests from clients, CLI, skills, and user-facing tooling **
25
25
 
26
26
  **Ban on hardcoded runtime hosts/ports:** Do not embed `localhost:7821`, `127.0.0.1:7821`, or runtime-port-derived URLs in docs, skills, or user-facing guidance. Always reference gateway URLs instead. A CI guard test (`gateway-only-guard.test.ts`) enforces this — any new direct runtime URL reference in production code or skills will fail CI.
27
27
 
28
- **SKILL.md retrieval contract:** For config/status retrieval in bundled skills, use `bash` + canonical CLI surfaces. Start with `assistant config get` for generic config keys and secure credential surfaces (`credential_store`, `assistant keys`) for secrets. Do not use direct gateway `curl` for read-only retrieval paths. Do not use keychain lookup commands (`security find-generic-password`, `secret-tool`) in SKILL.md. `host_bash` is not allowed for Vellum CLI retrieval commands unless a documented exception is intentionally allowlisted.
28
+ **SKILL.md retrieval contract:** For config/status retrieval in bundled skills, use `bash` + canonical CLI surfaces. Start with `assistant config get` for generic config keys and secure credential surfaces (`credential_store`, `assistant keys`) for secrets. Do not use direct gateway `curl` for read-only retrieval paths. Do not use credential store lookup commands (`security find-generic-password`, `secret-tool`) in SKILL.md. `host_bash` is not allowed for Vellum CLI retrieval commands unless a documented exception is intentionally allowlisted.
29
29
 
30
- **SKILL.md proxied outbound pattern:** For outbound third-party API calls from skills that require stored credentials, default to `bash` with `network_mode: "proxied"` and `credential_ids` instead of manual token/keychain plumbing. This keeps credentials out of chat and enforces credential policies consistently.
30
+ **SKILL.md proxied outbound pattern:** For outbound third-party API calls from skills that require stored credentials, default to `bash` with `network_mode: "proxied"` and `credential_ids` instead of manual token/credential store plumbing. This keeps credentials out of chat and enforces credential policies consistently.
31
31
 
32
32
  **SKILL.md gateway URL pattern:** For gateway control-plane writes/actions that are not exposed through a CLI read command, use `$INTERNAL_GATEWAY_BASE_URL` (injected by `bash` and `host_bash`). Do not hardcode `localhost`/ports in skill examples, and do not instruct users/agents to manually export the variable from Settings. For public ingress URLs (e.g. OAuth redirect URIs, webhook registration), use `assistant config get ingress.publicBaseUrl` or load the `public-ingress` skill — do not inject public URLs as environment variables.
33
33
 
package/ARCHITECTURE.md CHANGED
@@ -43,11 +43,11 @@ The gateway exposes a REST API for reading and mutating assistant feature flags.
43
43
  | Method | Path | Description |
44
44
  | ------ | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
45
45
  | GET | `/v1/feature-flags` | List all declared assistant feature flags from the defaults registry, merged with persisted values from the feature flag store. Returns `{ flags: FeatureFlagEntry[] }` where each entry has `key`, `enabled`, `defaultEnabled`, and `description`. |
46
- | PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must match `feature_flags.<flagId>.enabled` and be declared in the defaults registry. Writes to `~/.vellum/protected/feature-flags.json`. |
46
+ | PATCH | `/v1/feature-flags/:key` | Set a single assistant feature flag. Body: `{ "enabled": true\|false }`. Key must be a simple kebab-case flag key declared in the defaults registry. Writes to `~/.vellum/protected/feature-flags.json`. |
47
47
 
48
48
  **Unified registry:** All declared feature flags and their default values are defined in the unified registry at `meta/feature-flags/feature-flag-registry.json` (bundled copy at `gateway/src/feature-flag-registry.json`). The gateway loads this registry on startup via `gateway/src/feature-flag-defaults.ts`, filtering to `scope: "assistant"` flags. Labels come from the registry. The GET endpoint merges persisted overrides with registry defaults to produce the full flag list. The PATCH endpoint validates that the target flag key exists in the registry before accepting a write. Only declared keys are exposed by this API.
49
49
 
50
- **Flag key format:** The canonical key format is `feature_flags.<flagId>.enabled`. Only keys matching this pattern are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the protected feature flag store (`~/.vellum/protected/feature-flags.json`).
50
+ **Flag key format:** The canonical key format is simple kebab-case (e.g., `browser`, `ces-tools`). Only keys matching this pattern and declared in the registry are accepted by the PATCH endpoint; other patterns are rejected with 400. All writes use the canonical format and are stored in the protected feature flag store (`~/.vellum/protected/feature-flags.json`).
51
51
 
52
52
  **Storage:** Flag overrides are persisted in `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store uses a versioned JSON format (`{ version: 1, values: Record<string, boolean> }`). The GET endpoint reads from the feature flag store and merges with registry defaults. The gateway writes atomically (temp file + rename, 0o600 permissions). The daemon's config watcher monitors the protected directory and hot-reloads changes, so flag mutations take effect on the next session or tool resolution without a restart.
53
53
 
@@ -62,7 +62,7 @@ The assistant feature flags API uses a dedicated feature-flag token stored at `~
62
62
 
63
63
  The feature-flag token is auto-generated on first gateway startup if the file does not exist. The gateway watches the token file for changes and hot-reloads without restart.
64
64
 
65
- **Protected feature flag store:** The canonical storage for assistant feature flag overrides is `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store is managed by `gateway/src/feature-flag-store.ts` and uses a versioned JSON format with `Record<string, boolean>` values keyed by canonical flag keys (`feature_flags.<id>.enabled`). The gateway's PATCH handler writes exclusively to this store. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
65
+ **Protected feature flag store:** The canonical storage for assistant feature flag overrides is `~/.vellum/protected/feature-flags.json` (local) or `GATEWAY_SECURITY_DIR/feature-flags.json` (Docker). The store is managed by `gateway/src/feature-flag-store.ts` and uses a versioned JSON format with `Record<string, boolean>` values keyed by canonical flag keys (simple kebab-case, e.g., `browser`). The gateway's PATCH handler writes exclusively to this store. The daemon's resolver reads it with highest priority, falling back to the defaults registry. Undeclared keys are ignored by the resolver.
66
66
 
67
67
  **Key source files:**
68
68
 
@@ -569,7 +569,7 @@ If no guardian binding exists for the channel, escalation fails closed -- the me
569
569
 
570
570
  ### Telegram Credential Flow
571
571
 
572
- In desktop deployments, Telegram bot tokens are stored in secure storage (the encrypted file store at `~/.vellum/protected/keys.enc`, with macOS Keychain access available via the keychain broker) and never in plaintext config files. When deploying the gateway standalone, operators may also supply credentials via environment variables (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`).
572
+ In desktop deployments, Telegram bot tokens are stored in secure storage (CES HTTP API when available, or the encrypted file store at `~/.vellum/protected/keys.enc`) and never in plaintext config files. When deploying the gateway standalone, operators may also supply credentials via environment variables (`TELEGRAM_BOT_TOKEN`, `TELEGRAM_WEBHOOK_SECRET`).
573
573
 
574
574
  ```
575
575
  Entry points:
@@ -603,7 +603,7 @@ The `telegram_config` HTTP endpoint supports three actions:
603
603
  - **`set`** — validates the bot token against the Telegram API, stores it in secure storage, auto-generates a webhook secret if none exists (with rollback on failure), and self-heals webhook_secret metadata if it already exists. The gateway's credential watcher detects the storage change and triggers webhook reconciliation automatically
604
604
  - **`clear`** — deregisters the webhook by calling Telegram's `deleteWebhook` API directly (while the token is still available), then deletes the bot token and webhook secret from both secure storage and credential metadata. The gateway's credential watcher detects the storage change and updates its readiness state automatically
605
605
 
606
- The gateway reads Telegram credentials via its `credential-reader` module (`gateway/src/credential-reader.ts`), which uses a broker-first fallback strategy: it tries the keychain broker first, then falls back to the encrypted file store (`~/.vellum/protected/keys.enc`). When the broker is unavailable (e.g., daemon not running or non-macOS platform), the encrypted store is used directly.
606
+ The gateway reads Telegram credentials via its `credential-reader` module (`gateway/src/credential-reader.ts`), which uses a CES-first fallback strategy: it tries the CES HTTP API first (when `CES_CREDENTIAL_URL` is configured), then falls back to the encrypted file store (`~/.vellum/protected/keys.enc`).
607
607
 
608
608
  ### Webhook Reconciliation
609
609
 
@@ -660,7 +660,7 @@ The Slack channel requires two tokens:
660
660
  | App token | `xapp-...` | Used for `apps.connections.open` to establish the Socket Mode WebSocket connection |
661
661
  | Bot token | `xoxb-...` | Used for `chat.postMessage` to send outbound messages and for `auth.test` validation |
662
662
 
663
- Both tokens are stored in secure storage (`credential/slack_channel/app_token`, `credential/slack_channel/bot_token`) via the assistant's Slack channel config endpoints (see `assistant/ARCHITECTURE.md`). The gateway reads them via its `credential-reader` module using the same broker-first fallback strategy as Telegram credentials.
663
+ Both tokens are stored in secure storage (`credential/slack_channel/app_token`, `credential/slack_channel/bot_token`) via the assistant's Slack channel config endpoints (see `assistant/ARCHITECTURE.md`). The gateway reads them via its `credential-reader` module using the same CES-first fallback strategy as Telegram credentials.
664
664
 
665
665
  **Auto-reconnect behavior:**
666
666
 
package/README.md CHANGED
@@ -28,8 +28,8 @@ bun run dev
28
28
 
29
29
  | Variable | Required | Default | Description |
30
30
  | ------------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
31
- | `TELEGRAM_BOT_TOKEN` | No | — | Bot token from @BotFather (Telegram disabled when unset). When not set as an env var, the gateway reads from the assistant's secure credential store: keychain broker first (UDS to the assistant process), then the encrypted file store (`~/.vellum/protected/keys.enc`). When the broker is unavailable (assistant not running or non-macOS), the encrypted store is used directly. |
32
- | `TELEGRAM_WEBHOOK_SECRET` | No | — | Secret for verifying webhook requests (Telegram disabled when unset). Same credential reader fallback behavior as `TELEGRAM_BOT_TOKEN`. |
31
+ | `TELEGRAM_BOT_TOKEN` | No | — | Bot token from @BotFather (Telegram disabled when unset). When not set as an env var, the gateway reads from the assistant's secure credential store: CES HTTP API first (when `CES_CREDENTIAL_URL` is configured), then the encrypted file store (`~/.vellum/protected/keys.enc`). |
32
+ | `TELEGRAM_WEBHOOK_SECRET` | No | — | Secret for verifying webhook requests (Telegram disabled when unset). Same credential reader fallback behavior as `TELEGRAM_BOT_TOKEN`. |
33
33
  | `GATEWAY_PORT` | No | `7830` | Port for the gateway HTTP server |
34
34
 
35
35
  Most gateway behavior is now configured via hardcoded defaults or workspace config (`~/.vellum/workspace/config.json`) rather than environment variables. Channel operational settings (Telegram API base URL, timeouts, deliver auth bypass flags, runtime base URL, routing, proxy settings, attachment limits, shutdown drain) are managed via `workspace/config.json` through `ConfigFileCache`. See the channel-specific sections in `ARCHITECTURE.md` for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/vellum-gateway",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,5 @@
1
1
  import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
2
- import { mkdirSync, writeFileSync, rmSync, unlinkSync } from "node:fs";
3
- import { createServer, type Server } from "node:net";
2
+ import { mkdirSync, writeFileSync, rmSync } from "node:fs";
4
3
  import { join } from "node:path";
5
4
  import { hostname, tmpdir, userInfo } from "node:os";
6
5
  import { createCipheriv, pbkdf2Sync, randomBytes } from "node:crypto";
@@ -26,7 +25,8 @@ mock.module("../logger.js", () => ({
26
25
 
27
26
  import {
28
27
  readCredential,
29
- readTelegramCredentials,
28
+ readServiceCredentials,
29
+ type ServiceCredentialSpec,
30
30
  } from "../credential-reader.js";
31
31
 
32
32
  // ---------------------------------------------------------------------------
@@ -144,88 +144,6 @@ function writeEncryptedStoreV2(entries: Record<string, string>): void {
144
144
  writeFileSync(join(protectedDir, "keys.enc"), JSON.stringify(store));
145
145
  }
146
146
 
147
- // ---------------------------------------------------------------------------
148
- // Broker test helpers — mock UDS server
149
- // ---------------------------------------------------------------------------
150
-
151
- const TEST_TOKEN = "test-auth-token-abc123";
152
-
153
- function writeBrokerToken(token: string): void {
154
- const tokenDir = join(testDir, ".vellum", "protected");
155
- mkdirSync(tokenDir, { recursive: true });
156
- writeFileSync(join(tokenDir, "keychain-broker.token"), token);
157
- }
158
-
159
- /**
160
- * Create a mock keychain broker UDS server that responds to key.get requests.
161
- * Listens on the derived socket path (getRootDir() + keychain-broker.sock).
162
- * Returns the socket path and a handle to close the server.
163
- */
164
- function createMockBroker(credentials: Record<string, string>): {
165
- socketPath: string;
166
- server: Server;
167
- close: () => void;
168
- } {
169
- const socketPath = join(testDir, ".vellum", "keychain-broker.sock");
170
- mkdirSync(join(testDir, ".vellum"), { recursive: true });
171
-
172
- const server = createServer((conn) => {
173
- let buf = "";
174
- conn.on("data", (chunk) => {
175
- buf += chunk.toString();
176
- const idx = buf.indexOf("\n");
177
- if (idx !== -1) {
178
- const line = buf.slice(0, idx);
179
- buf = buf.slice(idx + 1);
180
- try {
181
- const req = JSON.parse(line);
182
- if (req.token !== TEST_TOKEN) {
183
- conn.write(
184
- JSON.stringify({
185
- id: req.id,
186
- ok: false,
187
- error: { code: "UNAUTHORIZED", message: "bad token" },
188
- }) + "\n",
189
- );
190
- return;
191
- }
192
- if (req.method === "key.get") {
193
- const account = req.params?.account;
194
- const value = credentials[account];
195
- conn.write(
196
- JSON.stringify({
197
- id: req.id,
198
- ok: true,
199
- result:
200
- value !== undefined
201
- ? { found: true, value }
202
- : { found: false },
203
- }) + "\n",
204
- );
205
- }
206
- } catch {
207
- // ignore malformed requests
208
- }
209
- }
210
- });
211
- });
212
-
213
- server.listen(socketPath);
214
-
215
- return {
216
- socketPath,
217
- server,
218
- close: () => {
219
- server.close();
220
- try {
221
- unlinkSync(socketPath);
222
- } catch {
223
- // best-effort
224
- }
225
- },
226
- };
227
- }
228
-
229
147
  // ---------------------------------------------------------------------------
230
148
  // Setup / teardown
231
149
  // ---------------------------------------------------------------------------
@@ -244,51 +162,6 @@ afterEach(() => {
244
162
  }
245
163
  });
246
164
 
247
- // ---------------------------------------------------------------------------
248
- // Tests: encrypted store (existing)
249
- // ---------------------------------------------------------------------------
250
-
251
- describe("readTelegramCredentials", () => {
252
- test("returns null when metadata file does not exist", async () => {
253
- const result = await readTelegramCredentials();
254
- expect(result).toBeNull();
255
- });
256
-
257
- test("returns null when metadata has no Telegram entries", async () => {
258
- writeMetadata([{ service: "github", field: "token" }]);
259
- const result = await readTelegramCredentials();
260
- expect(result).toBeNull();
261
- });
262
-
263
- test("returns null when metadata exists but secrets are missing from encrypted store", async () => {
264
- writeMetadata([
265
- { service: "telegram", field: "bot_token" },
266
- { service: "telegram", field: "webhook_secret" },
267
- ]);
268
-
269
- const result = await readTelegramCredentials();
270
- expect(result).toBeNull();
271
- });
272
-
273
- test("returns credentials from encrypted store", async () => {
274
- writeMetadata([
275
- { service: "telegram", field: "bot_token" },
276
- { service: "telegram", field: "webhook_secret" },
277
- ]);
278
-
279
- writeEncryptedStore({
280
- [credentialKey("telegram", "bot_token")]: "enc-bot-token",
281
- [credentialKey("telegram", "webhook_secret")]: "enc-webhook-secret",
282
- });
283
-
284
- const result = await readTelegramCredentials();
285
- expect(result).toEqual({
286
- botToken: "enc-bot-token",
287
- webhookSecret: "enc-webhook-secret",
288
- });
289
- });
290
- });
291
-
292
165
  // ---------------------------------------------------------------------------
293
166
  // Tests: v2 encrypted store (store.key)
294
167
  // ---------------------------------------------------------------------------
@@ -322,24 +195,6 @@ describe("v2 encrypted store with store.key", () => {
322
195
  const result = await readCredential(credentialKey("test", "key"));
323
196
  expect(result).toBeUndefined();
324
197
  });
325
-
326
- test("returns Telegram credentials from v2 store", async () => {
327
- writeMetadata([
328
- { service: "telegram", field: "bot_token" },
329
- { service: "telegram", field: "webhook_secret" },
330
- ]);
331
-
332
- writeEncryptedStoreV2({
333
- [credentialKey("telegram", "bot_token")]: "v2-bot-token",
334
- [credentialKey("telegram", "webhook_secret")]: "v2-webhook-secret",
335
- });
336
-
337
- const result = await readTelegramCredentials();
338
- expect(result).toEqual({
339
- botToken: "v2-bot-token",
340
- webhookSecret: "v2-webhook-secret",
341
- });
342
- });
343
198
  });
344
199
 
345
200
  // ---------------------------------------------------------------------------
@@ -358,84 +213,111 @@ describe("v1 encrypted store backward compatibility", () => {
358
213
  });
359
214
 
360
215
  // ---------------------------------------------------------------------------
361
- // Tests: broker credential reading
216
+ // Tests: generic readServiceCredentials
362
217
  // ---------------------------------------------------------------------------
363
218
 
364
- describe("readCredential broker integration", () => {
365
- test("returns undefined when socket file does not exist", async () => {
366
- const result = await readCredential(credentialKey("test", "key"));
367
- expect(result).toBeUndefined();
368
- });
219
+ describe("readServiceCredentials", () => {
220
+ const telegramSpec: ServiceCredentialSpec = {
221
+ service: "telegram",
222
+ requiredFields: ["bot_token", "webhook_secret"],
223
+ };
224
+
225
+ test("returns correct Record<string, string> for a valid spec", async () => {
226
+ writeMetadata([
227
+ { service: "telegram", field: "bot_token" },
228
+ { service: "telegram", field: "webhook_secret" },
229
+ ]);
369
230
 
370
- test("falls back to encrypted store when socket file does not exist", async () => {
371
231
  writeEncryptedStore({
372
- [credentialKey("test", "key")]: "encrypted-value",
232
+ [credentialKey("telegram", "bot_token")]: "my-bot-token",
233
+ [credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
373
234
  });
374
235
 
375
- const result = await readCredential(credentialKey("test", "key"));
376
- expect(result).toBe("encrypted-value");
236
+ const result = await readServiceCredentials(telegramSpec);
237
+ expect(result).toEqual({
238
+ bot_token: "my-bot-token",
239
+ webhook_secret: "my-webhook-secret",
240
+ });
241
+ });
242
+
243
+ test("returns null when metadata is missing", async () => {
244
+ // No metadata file written at all
245
+ const result = await readServiceCredentials(telegramSpec);
246
+ expect(result).toBeNull();
377
247
  });
378
248
 
379
- test("falls back to encrypted store when broker token file is missing", async () => {
380
- // Create socket file but no token file
381
- mkdirSync(join(testDir, ".vellum"), { recursive: true });
382
- writeFileSync(join(testDir, ".vellum", "keychain-broker.sock"), "");
249
+ test("returns null when metadata has no entries for the service", async () => {
250
+ writeMetadata([{ service: "github", field: "token" }]);
251
+
252
+ const result = await readServiceCredentials(telegramSpec);
253
+ expect(result).toBeNull();
254
+ });
255
+
256
+ test("returns null when metadata exists but encrypted values cannot be read", async () => {
257
+ writeMetadata([
258
+ { service: "telegram", field: "bot_token" },
259
+ { service: "telegram", field: "webhook_secret" },
260
+ ]);
261
+ // No encrypted store written — secrets are unreadable
262
+
263
+ const result = await readServiceCredentials(telegramSpec);
264
+ expect(result).toBeNull();
265
+ });
266
+
267
+ test("returns null when only some required fields exist in metadata", async () => {
268
+ writeMetadata([
269
+ { service: "telegram", field: "bot_token" },
270
+ // webhook_secret is missing from metadata
271
+ ]);
383
272
 
384
273
  writeEncryptedStore({
385
- [credentialKey("test", "key")]: "encrypted-value",
274
+ [credentialKey("telegram", "bot_token")]: "my-bot-token",
275
+ [credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
386
276
  });
387
277
 
388
- const result = await readCredential(credentialKey("test", "key"));
389
- expect(result).toBe("encrypted-value");
278
+ const result = await readServiceCredentials(telegramSpec);
279
+ expect(result).toBeNull();
390
280
  });
391
281
 
392
- test("reads credential from broker when available", async () => {
393
- const broker = createMockBroker({
394
- [credentialKey("test", "key")]: "broker-secret-value",
282
+ test("works for a hypothetical new service spec (extensibility)", async () => {
283
+ const customSpec: ServiceCredentialSpec = {
284
+ service: "test_service",
285
+ requiredFields: ["api_key", "secret"],
286
+ };
287
+
288
+ writeMetadata([
289
+ { service: "test_service", field: "api_key" },
290
+ { service: "test_service", field: "secret" },
291
+ ]);
292
+
293
+ writeEncryptedStore({
294
+ [credentialKey("test_service", "api_key")]: "custom-api-key",
295
+ [credentialKey("test_service", "secret")]: "custom-secret",
395
296
  });
396
- try {
397
- writeBrokerToken(TEST_TOKEN);
398
-
399
- const result = await readCredential(credentialKey("test", "key"));
400
- expect(result).toBe("broker-secret-value");
401
- } finally {
402
- broker.close();
403
- }
404
- });
405
297
 
406
- test("broker result takes priority over encrypted store", async () => {
407
- const broker = createMockBroker({
408
- [credentialKey("test", "key")]: "broker-value",
298
+ const result = await readServiceCredentials(customSpec);
299
+ expect(result).toEqual({
300
+ api_key: "custom-api-key",
301
+ secret: "custom-secret",
409
302
  });
410
- try {
411
- writeBrokerToken(TEST_TOKEN);
412
-
413
- writeEncryptedStore({
414
- [credentialKey("test", "key")]: "encrypted-value",
415
- });
416
-
417
- const result = await readCredential(credentialKey("test", "key"));
418
- expect(result).toBe("broker-value");
419
- } finally {
420
- broker.close();
421
- }
422
303
  });
423
304
 
424
- test("falls back to encrypted store when broker returns not found", async () => {
425
- // Broker has no entry for the test credential key
426
- const broker = createMockBroker({});
427
- try {
428
- writeBrokerToken(TEST_TOKEN);
429
-
430
- writeEncryptedStore({
431
- [credentialKey("test", "key")]: "encrypted-value",
432
- });
433
-
434
- const result = await readCredential(credentialKey("test", "key"));
435
- expect(result).toBe("encrypted-value");
436
- } finally {
437
- broker.close();
438
- }
305
+ test("works with v2 encrypted store", async () => {
306
+ writeMetadata([
307
+ { service: "telegram", field: "bot_token" },
308
+ { service: "telegram", field: "webhook_secret" },
309
+ ]);
310
+
311
+ writeEncryptedStoreV2({
312
+ [credentialKey("telegram", "bot_token")]: "v2-bot-token",
313
+ [credentialKey("telegram", "webhook_secret")]: "v2-webhook-secret",
314
+ });
315
+
316
+ const result = await readServiceCredentials(telegramSpec);
317
+ expect(result).toEqual({
318
+ bot_token: "v2-bot-token",
319
+ webhook_secret: "v2-webhook-secret",
320
+ });
439
321
  });
440
322
  });
441
323
 
@@ -448,26 +330,6 @@ describe("secret leak prevention", () => {
448
330
  return JSON.stringify(logCalls);
449
331
  }
450
332
 
451
- test("broker read does not leak secret values into logs", async () => {
452
- const secretValue = "super-secret-broker-credential-value";
453
- const broker = createMockBroker({
454
- [credentialKey("leak-test", "key")]: secretValue,
455
- });
456
- try {
457
- writeBrokerToken(TEST_TOKEN);
458
-
459
- const result = await readCredential(credentialKey("leak-test", "key"));
460
- expect(result).toBe(secretValue);
461
-
462
- const serialized = allLogStrings();
463
- expect(serialized).not.toContain(secretValue);
464
- // The auth token used for broker handshake should also stay out of logs
465
- expect(serialized).not.toContain(TEST_TOKEN);
466
- } finally {
467
- broker.close();
468
- }
469
- });
470
-
471
333
  test("encrypted store read does not leak secret values into logs", async () => {
472
334
  const secretValue = "super-secret-encrypted-credential-value";
473
335
 
@@ -482,7 +344,7 @@ describe("secret leak prevention", () => {
482
344
  expect(serialized).not.toContain(secretValue);
483
345
  });
484
346
 
485
- test("failed encrypted store read does not leak secret values into logs", async () => {
347
+ test("service credential read does not leak secret values into logs", async () => {
486
348
  const secretValue = "super-secret-telegram-token";
487
349
 
488
350
  writeMetadata([
@@ -494,7 +356,10 @@ describe("secret leak prevention", () => {
494
356
  [credentialKey("telegram", "webhook_secret")]: "webhook-secret-value",
495
357
  });
496
358
 
497
- const result = await readTelegramCredentials();
359
+ const result = await readServiceCredentials({
360
+ service: "telegram",
361
+ requiredFields: ["bot_token", "webhook_secret"],
362
+ });
498
363
  expect(result).not.toBeNull();
499
364
 
500
365
  const serialized = allLogStrings();