@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 +2 -2
- package/ARCHITECTURE.md +6 -6
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/__tests__/credential-reader.test.ts +94 -229
- package/src/__tests__/feature-flags-route.test.ts +197 -108
- package/src/__tests__/remote-feature-flag-sync.test.ts +373 -0
- package/src/__tests__/telegram-webhook-manager.test.ts +84 -0
- package/src/credential-reader.ts +73 -358
- package/src/credential-watcher.ts +16 -42
- package/src/feature-flag-registry.json +57 -41
- package/src/feature-flag-remote-store.ts +119 -0
- package/src/http/routes/feature-flags.ts +10 -6
- package/src/index.ts +66 -65
- package/src/remote-feature-flag-sync.ts +153 -0
- package/src/slack/thread-context.test.ts +204 -0
- package/src/slack/thread-context.ts +153 -0
- package/src/telegram/api.ts +3 -3
- package/src/telegram/webhook-manager.ts +102 -14
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
|
|
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/
|
|
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
|
|
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 `
|
|
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 (`
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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,5 @@
|
|
|
1
1
|
import { describe, test, expect, mock, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdirSync, writeFileSync, rmSync
|
|
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
|
-
|
|
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:
|
|
216
|
+
// Tests: generic readServiceCredentials
|
|
362
217
|
// ---------------------------------------------------------------------------
|
|
363
218
|
|
|
364
|
-
describe("
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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("
|
|
232
|
+
[credentialKey("telegram", "bot_token")]: "my-bot-token",
|
|
233
|
+
[credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
|
|
373
234
|
});
|
|
374
235
|
|
|
375
|
-
const result = await
|
|
376
|
-
expect(result).
|
|
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("
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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("
|
|
274
|
+
[credentialKey("telegram", "bot_token")]: "my-bot-token",
|
|
275
|
+
[credentialKey("telegram", "webhook_secret")]: "my-webhook-secret",
|
|
386
276
|
});
|
|
387
277
|
|
|
388
|
-
const result = await
|
|
389
|
-
expect(result).
|
|
278
|
+
const result = await readServiceCredentials(telegramSpec);
|
|
279
|
+
expect(result).toBeNull();
|
|
390
280
|
});
|
|
391
281
|
|
|
392
|
-
test("
|
|
393
|
-
const
|
|
394
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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("
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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("
|
|
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
|
|
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();
|