@slashfi/agents-sdk 0.16.0 → 0.17.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 (93) hide show
  1. package/dist/agent-definitions/auth.d.ts.map +1 -1
  2. package/dist/agent-definitions/auth.js +44 -11
  3. package/dist/agent-definitions/auth.js.map +1 -1
  4. package/dist/agent-definitions/integrations.d.ts.map +1 -1
  5. package/dist/agent-definitions/integrations.js +106 -45
  6. package/dist/agent-definitions/integrations.js.map +1 -1
  7. package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
  8. package/dist/agent-definitions/remote-registry.js +174 -45
  9. package/dist/agent-definitions/remote-registry.js.map +1 -1
  10. package/dist/agent-definitions/secrets.d.ts.map +1 -1
  11. package/dist/agent-definitions/secrets.js +1 -4
  12. package/dist/agent-definitions/secrets.js.map +1 -1
  13. package/dist/agent-definitions/users.d.ts.map +1 -1
  14. package/dist/agent-definitions/users.js +14 -3
  15. package/dist/agent-definitions/users.js.map +1 -1
  16. package/dist/define-config.d.ts +125 -0
  17. package/dist/define-config.d.ts.map +1 -0
  18. package/dist/define-config.js +75 -0
  19. package/dist/define-config.js.map +1 -0
  20. package/dist/define.d.ts +11 -2
  21. package/dist/define.d.ts.map +1 -1
  22. package/dist/define.js +57 -26
  23. package/dist/define.js.map +1 -1
  24. package/dist/events.d.ts +133 -0
  25. package/dist/events.d.ts.map +1 -0
  26. package/dist/events.js +57 -0
  27. package/dist/events.js.map +1 -0
  28. package/dist/index.d.ts +15 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +9 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/integration-interface.d.ts +3 -3
  33. package/dist/integration-interface.d.ts.map +1 -1
  34. package/dist/integration-interface.js +29 -21
  35. package/dist/integration-interface.js.map +1 -1
  36. package/dist/integrations-store.d.ts +2 -2
  37. package/dist/integrations-store.d.ts.map +1 -1
  38. package/dist/integrations-store.js +3 -3
  39. package/dist/integrations-store.js.map +1 -1
  40. package/dist/jwt.d.ts.map +1 -1
  41. package/dist/jwt.js +7 -5
  42. package/dist/jwt.js.map +1 -1
  43. package/dist/key-manager.d.ts.map +1 -1
  44. package/dist/key-manager.js +5 -3
  45. package/dist/key-manager.js.map +1 -1
  46. package/dist/oidc-signin.d.ts +32 -0
  47. package/dist/oidc-signin.d.ts.map +1 -0
  48. package/dist/oidc-signin.js +138 -0
  49. package/dist/oidc-signin.js.map +1 -0
  50. package/dist/registry-consumer.d.ts +104 -0
  51. package/dist/registry-consumer.d.ts.map +1 -0
  52. package/dist/registry-consumer.js +230 -0
  53. package/dist/registry-consumer.js.map +1 -0
  54. package/dist/registry.d.ts +5 -0
  55. package/dist/registry.d.ts.map +1 -1
  56. package/dist/registry.js +76 -4
  57. package/dist/registry.js.map +1 -1
  58. package/dist/secret-collection.d.ts.map +1 -1
  59. package/dist/secret-collection.js.map +1 -1
  60. package/dist/server.d.ts +3 -0
  61. package/dist/server.d.ts.map +1 -1
  62. package/dist/server.js +222 -27
  63. package/dist/server.js.map +1 -1
  64. package/dist/test-utils/mock-oidc-server.d.ts +36 -0
  65. package/dist/test-utils/mock-oidc-server.d.ts.map +1 -0
  66. package/dist/test-utils/mock-oidc-server.js +96 -0
  67. package/dist/test-utils/mock-oidc-server.js.map +1 -0
  68. package/dist/types.d.ts +17 -0
  69. package/dist/types.d.ts.map +1 -1
  70. package/package.json +1 -1
  71. package/src/agent-definitions/auth.ts +106 -38
  72. package/src/agent-definitions/integrations.ts +201 -73
  73. package/src/agent-definitions/remote-registry.ts +262 -65
  74. package/src/agent-definitions/secrets.ts +22 -8
  75. package/src/agent-definitions/users.ts +16 -4
  76. package/src/consumer.test.ts +536 -0
  77. package/src/define-config.ts +205 -0
  78. package/src/define.ts +134 -46
  79. package/src/events.ts +237 -0
  80. package/src/index.ts +89 -8
  81. package/src/integration-interface.ts +52 -28
  82. package/src/integrations-store.ts +9 -5
  83. package/src/jwt.ts +48 -19
  84. package/src/key-manager.test.ts +22 -13
  85. package/src/key-manager.ts +8 -10
  86. package/src/oidc-signin.ts +223 -0
  87. package/src/registry-consumer.ts +413 -0
  88. package/src/registry.ts +115 -9
  89. package/src/secret-collection.ts +2 -1
  90. package/src/server.test.ts +304 -238
  91. package/src/server.ts +371 -69
  92. package/src/test-utils/mock-oidc-server.ts +123 -0
  93. package/src/types.ts +69 -18
@@ -0,0 +1,536 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import {
3
+ createAgentRegistry,
4
+ createAgentServer,
5
+ createRegistryConsumer,
6
+ defineAgent,
7
+ defineTool,
8
+ } from "./index";
9
+ import type { AgentServer } from "./index";
10
+
11
+ // ─── Helpers ─────────────────────────────────────────────────────
12
+
13
+ const echo = defineTool({
14
+ name: "echo",
15
+ description: "Echo back the input",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: { message: { type: "string" } },
19
+ required: ["message"],
20
+ },
21
+ execute: async (input: { message: string }) => ({ echoed: input.message }),
22
+ });
23
+
24
+ const add = defineTool({
25
+ name: "add",
26
+ description: "Add two numbers",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ a: { type: "number" },
31
+ b: { type: "number" },
32
+ },
33
+ required: ["a", "b"],
34
+ },
35
+ execute: async (input: { a: number; b: number }) => ({
36
+ result: input.a + input.b,
37
+ }),
38
+ });
39
+
40
+ const mathAgent = defineAgent({
41
+ path: "@math",
42
+ entrypoint: "A math agent",
43
+ tools: [add],
44
+ visibility: "public",
45
+ });
46
+
47
+ const echoAgent = defineAgent({
48
+ path: "@echo",
49
+ entrypoint: "An echo agent",
50
+ tools: [echo],
51
+ visibility: "public",
52
+ });
53
+
54
+ // ─── E2E: Registry Consumer ──────────────────────────────────────
55
+
56
+ describe("Registry Consumer E2E", () => {
57
+ let server: AgentServer;
58
+ const PORT = 19890;
59
+
60
+ beforeAll(async () => {
61
+ const registry = createAgentRegistry();
62
+ registry.register(mathAgent);
63
+ registry.register(echoAgent);
64
+
65
+ server = createAgentServer(registry, { port: PORT });
66
+ await server.start();
67
+ });
68
+
69
+ afterAll(async () => {
70
+ await server.stop();
71
+ });
72
+
73
+ test("discover registry via .well-known/configuration", async () => {
74
+ const config = {
75
+ registries: [`http://localhost:${PORT}`],
76
+ };
77
+
78
+ const consumer = await createRegistryConsumer(config);
79
+ const discovery = await consumer.discover(`http://localhost:${PORT}`);
80
+
81
+ expect(discovery).toBeDefined();
82
+ expect(discovery.issuer).toBeDefined();
83
+ });
84
+
85
+ test("list agents from registry", async () => {
86
+ const config = {
87
+ registries: [`http://localhost:${PORT}`],
88
+ refs: ["@math", "@echo"],
89
+ };
90
+
91
+ const consumer = await createRegistryConsumer(config);
92
+ const agents = await consumer.list();
93
+
94
+ expect(agents.length).toBeGreaterThanOrEqual(2);
95
+
96
+ const paths = agents.map((a) => a.path);
97
+ expect(paths).toContain("@math");
98
+ expect(paths).toContain("@echo");
99
+
100
+ // Every agent should have the publisher from the registry
101
+ for (const agent of agents) {
102
+ expect(agent.publisher).toBe("localhost");
103
+ }
104
+ });
105
+
106
+ test("refs returns configured refs", async () => {
107
+ const config = {
108
+ registries: [`http://localhost:${PORT}`],
109
+ refs: [
110
+ "@math",
111
+ { ref: "@echo", as: "my-echo", config: { greeting: "hello" } },
112
+ ],
113
+ };
114
+
115
+ const consumer = await createRegistryConsumer(config);
116
+ const refs = consumer.refs();
117
+
118
+ expect(refs).toHaveLength(2);
119
+ expect(refs[0].name).toBe("@math");
120
+ expect(refs[0].ref).toBe("@math");
121
+ expect(refs[1].name).toBe("my-echo");
122
+ expect(refs[1].ref).toBe("@echo");
123
+ expect(refs[1].config).toEqual({ greeting: "hello" });
124
+ });
125
+
126
+ test("call a tool on a ref", async () => {
127
+ const config = {
128
+ registries: [`http://localhost:${PORT}`],
129
+ refs: ["@math"],
130
+ };
131
+
132
+ const consumer = await createRegistryConsumer(config);
133
+ const result = await consumer.call("@math", "add", { a: 2, b: 3 });
134
+
135
+ expect(result).toBeDefined();
136
+ });
137
+
138
+ test("call throws on unknown ref", async () => {
139
+ const config = {
140
+ registries: [`http://localhost:${PORT}`],
141
+ refs: ["@math"],
142
+ };
143
+
144
+ const consumer = await createRegistryConsumer(config);
145
+
146
+ await expect(consumer.call("@nonexistent", "anything", {})).rejects.toThrow(
147
+ 'Ref "@nonexistent" not found',
148
+ );
149
+ });
150
+
151
+ test("multi-instance refs with as: alias", async () => {
152
+ const config = {
153
+ registries: [`http://localhost:${PORT}`],
154
+ refs: [
155
+ { ref: "@echo", as: "echo-1", config: { prefix: "first" } },
156
+ { ref: "@echo", as: "echo-2", config: { prefix: "second" } },
157
+ ],
158
+ };
159
+
160
+ const consumer = await createRegistryConsumer(config);
161
+ const refs = consumer.refs();
162
+
163
+ expect(refs).toHaveLength(2);
164
+ expect(refs[0].name).toBe("echo-1");
165
+ expect(refs[0].ref).toBe("@echo");
166
+ expect(refs[1].name).toBe("echo-2");
167
+ expect(refs[1].ref).toBe("@echo");
168
+ });
169
+
170
+ test("index produces serialized config", async () => {
171
+ const config = {
172
+ registries: [`http://localhost:${PORT}`],
173
+ refs: ["@math", "@echo"],
174
+ meta: { owner: "test", description: "test config" },
175
+ };
176
+
177
+ const consumer = await createRegistryConsumer(config);
178
+ const indexed = consumer.index();
179
+
180
+ expect(indexed.resolvedAt).toBeDefined();
181
+ expect(indexed.sourceHash).toBeDefined();
182
+ expect(indexed.registries).toHaveLength(1);
183
+ expect(indexed.refs).toHaveLength(2);
184
+ expect(indexed.meta?.owner).toBe("test");
185
+
186
+ // Should be JSON-serializable
187
+ const json = JSON.stringify(indexed);
188
+ const parsed = JSON.parse(json);
189
+ expect(parsed.refs).toHaveLength(2);
190
+ });
191
+
192
+ test("available returns agents not in config", async () => {
193
+ const config = {
194
+ registries: [`http://localhost:${PORT}`],
195
+ refs: ["@math"],
196
+ };
197
+
198
+ const consumer = await createRegistryConsumer(config);
199
+ const available = await consumer.available();
200
+
201
+ // @math is configured, @echo should be available
202
+ const paths = available.map((a) => a.path);
203
+ expect(paths).not.toContain("@math");
204
+ expect(paths).toContain("@echo");
205
+ });
206
+
207
+ test("registries returns normalized entries", async () => {
208
+ const config = {
209
+ registries: [
210
+ `http://localhost:${PORT}`,
211
+ { url: "https://twin.slash.com/tenants/test", publisher: "slash" },
212
+ ],
213
+ };
214
+
215
+ const consumer = await createRegistryConsumer(config);
216
+ const registries = consumer.registries();
217
+
218
+ expect(registries).toHaveLength(2);
219
+ expect(registries[0].url).toBe(`http://localhost:${PORT}`);
220
+ expect(registries[0].publisher).toBe("localhost");
221
+ expect(registries[1].url).toBe("https://twin.slash.com/tenants/test");
222
+ expect(registries[1].publisher).toBe("slash");
223
+ });
224
+ });
225
+
226
+ // ─── Unit: normalizeRef / normalizeRegistry ──────────────────────
227
+
228
+ import { isSecretUrl, normalizeRef, normalizeRegistry } from "./define-config";
229
+
230
+ describe("normalizeRef", () => {
231
+ test("string ref", () => {
232
+ const result = normalizeRef("notion");
233
+ expect(result).toEqual({
234
+ ref: "notion",
235
+ name: "notion",
236
+ config: {},
237
+ });
238
+ });
239
+
240
+ test("object ref with alias", () => {
241
+ const result = normalizeRef({
242
+ ref: "postgres",
243
+ as: "prod-db",
244
+ config: { url: "https://twin.slash.com/secrets/db" },
245
+ });
246
+ expect(result.ref).toBe("postgres");
247
+ expect(result.name).toBe("prod-db");
248
+ expect(result.config).toEqual({
249
+ url: "https://twin.slash.com/secrets/db",
250
+ });
251
+ });
252
+
253
+ test("object ref without alias uses ref as name", () => {
254
+ const result = normalizeRef({ ref: "github" });
255
+ expect(result.name).toBe("github");
256
+ });
257
+ });
258
+
259
+ describe("normalizeRegistry", () => {
260
+ test("string URL", () => {
261
+ const result = normalizeRegistry("https://registry.slash.com");
262
+ expect(result.url).toBe("https://registry.slash.com");
263
+ expect(result.name).toBe("registry.slash.com");
264
+ expect(result.publisher).toBe("registry");
265
+ expect(result.auth).toEqual({ type: "none" });
266
+ });
267
+
268
+ test("object with custom publisher", () => {
269
+ const result = normalizeRegistry({
270
+ url: "https://twin.slash.com",
271
+ publisher: "slash",
272
+ name: "Slash Private",
273
+ });
274
+ expect(result.publisher).toBe("slash");
275
+ expect(result.name).toBe("Slash Private");
276
+ });
277
+
278
+ test("object with auth", () => {
279
+ const result = normalizeRegistry({
280
+ url: "https://twin.slash.com",
281
+ auth: { type: "bearer", token: "test" },
282
+ });
283
+ expect(result.auth).toEqual({ type: "bearer", token: "test" });
284
+ });
285
+ });
286
+
287
+ describe("isSecretUrl", () => {
288
+ test("detects secret URLs", () => {
289
+ expect(
290
+ isSecretUrl("https://twin.slash.com/users/abc/secrets/notion-key"),
291
+ ).toBe(true);
292
+ expect(
293
+ isSecretUrl("https://twin.slash.com/tenants/slash/secrets/db-url"),
294
+ ).toBe(true);
295
+ });
296
+
297
+ test("rejects non-secret URIs", () => {
298
+ expect(isSecretUrl("just-a-string")).toBe(false);
299
+ expect(isSecretUrl(42)).toBe(false);
300
+ expect(isSecretUrl(null)).toBe(false);
301
+ expect(isSecretUrl("ftp://server/file")).toBe(false);
302
+ });
303
+ });
304
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
305
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
306
+ import { tmpdir } from "node:os";
307
+ import { join } from "node:path";
308
+ import {
309
+ createAgentRegistry,
310
+ createAgentServer,
311
+ createRegistryConsumer,
312
+ defineAgent,
313
+ defineTool,
314
+ isSecretUri,
315
+ isSecretUrl,
316
+ } from "./index";
317
+ import type { AgentServer, ConsumerConfig } from "./index";
318
+
319
+ describe("Secret URI resolution", () => {
320
+ let tmpDir: string;
321
+ let server: AgentServer;
322
+ let authToken: string;
323
+ const PORT = 19896;
324
+ const BASE = `http://localhost:${PORT}`;
325
+
326
+ beforeAll(async () => {
327
+ // Create temp dir with secret files
328
+ tmpDir = await mkdtemp(join(tmpdir(), "secrets-test-"));
329
+ await writeFile(join(tmpDir, "notion-client-id"), "notion_cid_abc123");
330
+ await writeFile(
331
+ join(tmpDir, "notion-client-secret"),
332
+ "notion_cs_secret456",
333
+ );
334
+ await writeFile(join(tmpDir, "api-key"), "sk-test-key-789");
335
+
336
+ // Set env var for env:// test
337
+ process.env.TEST_SECRET_VALUE = "env-secret-value";
338
+
339
+ // Create a mock notion agent that echoes back received params
340
+ const notionAgent = defineAgent({
341
+ path: "notion",
342
+ entrypoint: "Mock Notion agent",
343
+ config: { name: "Notion", description: "Notion integration" },
344
+ visibility: "public" as const,
345
+ tools: [
346
+ defineTool({
347
+ name: "search_pages",
348
+ description: "Search Notion pages",
349
+ inputSchema: {
350
+ type: "object",
351
+ properties: {
352
+ query: { type: "string" },
353
+ clientId: { type: "string" },
354
+ },
355
+ required: ["query"],
356
+ },
357
+ execute: async (input: { query: string; clientId?: string }) => ({
358
+ results: [{ title: `Found: ${input.query}` }],
359
+ authenticatedWith: input.clientId ?? "none",
360
+ }),
361
+ }),
362
+ ],
363
+ });
364
+
365
+ const registry = createAgentRegistry();
366
+ registry.register(notionAgent);
367
+
368
+ server = createAgentServer(registry, { port: PORT });
369
+ await server.initKeys();
370
+ await server.start();
371
+
372
+ authToken = await server.signJwt({ sub: "test-user" });
373
+ });
374
+
375
+ afterAll(async () => {
376
+ await server?.stop?.();
377
+ await rm(tmpDir, { recursive: true, force: true });
378
+ process.env.TEST_SECRET_VALUE = undefined;
379
+ });
380
+
381
+ // ─── isSecretUri ───
382
+
383
+ test("isSecretUri recognizes file:// URIs", () => {
384
+ expect(isSecretUri("file:///tmp/secrets/key")).toBe(true);
385
+ });
386
+
387
+ test("isSecretUri recognizes env:// URIs", () => {
388
+ expect(isSecretUri("env://MY_VAR")).toBe(true);
389
+ });
390
+
391
+ test("isSecretUri recognizes https:// URIs", () => {
392
+ expect(isSecretUri("https://vault.example.com/secrets/key")).toBe(true);
393
+ });
394
+
395
+ test("isSecretUri rejects non-URI strings", () => {
396
+ expect(isSecretUri("just-a-string")).toBe(false);
397
+ expect(isSecretUri(42)).toBe(false);
398
+ expect(isSecretUri(null)).toBe(false);
399
+ });
400
+
401
+ test("isSecretUrl is aliased to isSecretUri", () => {
402
+ expect(isSecretUrl("file:///tmp/key")).toBe(true);
403
+ expect(isSecretUrl("not-a-uri")).toBe(false);
404
+ });
405
+
406
+ // ─── file:// resolution ───
407
+
408
+ test("resolveSecret handles file:// URIs", async () => {
409
+ const consumer = await createRegistryConsumer(
410
+ { registries: [BASE] },
411
+ { token: authToken },
412
+ );
413
+
414
+ const value = await consumer.resolveSecret(
415
+ `file://${join(tmpDir, "notion-client-id")}`,
416
+ );
417
+ expect(value).toBe("notion_cid_abc123");
418
+ });
419
+
420
+ // ─── env:// resolution ───
421
+
422
+ test("resolveSecret handles env:// URIs", async () => {
423
+ const consumer = await createRegistryConsumer(
424
+ { registries: [BASE] },
425
+ { token: authToken },
426
+ );
427
+
428
+ const value = await consumer.resolveSecret("env://TEST_SECRET_VALUE");
429
+ expect(value).toBe("env-secret-value");
430
+ });
431
+
432
+ test("resolveSecret throws on missing env var", async () => {
433
+ const consumer = await createRegistryConsumer(
434
+ { registries: [BASE] },
435
+ { token: authToken },
436
+ );
437
+
438
+ expect(consumer.resolveSecret("env://DOES_NOT_EXIST")).rejects.toThrow(
439
+ "Environment variable not set",
440
+ );
441
+ });
442
+
443
+ // ─── resolveConfig with file secrets ───
444
+
445
+ test("resolveConfig resolves mixed URI schemes", async () => {
446
+ const consumer = await createRegistryConsumer(
447
+ { registries: [BASE] },
448
+ { token: authToken },
449
+ );
450
+
451
+ const resolved = await consumer.resolveConfig({
452
+ clientId: `file://${join(tmpDir, "notion-client-id")}`,
453
+ clientSecret: `file://${join(tmpDir, "notion-client-secret")}`,
454
+ apiKey: `file://${join(tmpDir, "api-key")}`,
455
+ region: "us-east-1", // plain value, not a secret
456
+ });
457
+
458
+ expect(resolved.clientId).toBe("notion_cid_abc123");
459
+ expect(resolved.clientSecret).toBe("notion_cs_secret456");
460
+ expect(resolved.apiKey).toBe("sk-test-key-789");
461
+ expect(resolved.region).toBe("us-east-1");
462
+ });
463
+
464
+ // ─── E2E: consumer config with agent URL + file secrets ───
465
+
466
+ test("E2E: consumer config with agent URL and file:// secrets", async () => {
467
+ // This is the pattern: ref points to agent URL, secrets in file://
468
+ const config: ConsumerConfig = {
469
+ refs: [
470
+ {
471
+ ref: "notion",
472
+ url: `${BASE}/agents/notion`,
473
+ config: {
474
+ clientId: `file://${join(tmpDir, "notion-client-id")}`,
475
+ clientSecret: `file://${join(tmpDir, "notion-client-secret")}`,
476
+ },
477
+ },
478
+ ],
479
+ };
480
+
481
+ // Consumer resolves the config
482
+ const consumer = await createRegistryConsumer(config, { token: authToken });
483
+
484
+ // Resolve the ref's secrets
485
+ const ref = config.refs?.[0];
486
+ const refConfig = typeof ref === "string" ? {} : (ref.config ?? {});
487
+ const resolved = await consumer.resolveConfig(refConfig);
488
+
489
+ expect(resolved.clientId).toBe("notion_cid_abc123");
490
+ expect(resolved.clientSecret).toBe("notion_cs_secret456");
491
+
492
+ // Agent is discoverable at its URL
493
+ const agentUrl = typeof ref === "string" ? ref : ref.url!;
494
+ const infoRes = await fetch(agentUrl);
495
+ expect(infoRes.status).toBe(200);
496
+ const info = (await infoRes.json()) as { path: string; name: string };
497
+ expect(info.name).toBe("Notion");
498
+
499
+ // Call the agent with resolved secrets
500
+ const callRes = await fetch(agentUrl, {
501
+ method: "POST",
502
+ headers: {
503
+ "Content-Type": "application/json",
504
+ Authorization: `Bearer ${authToken}`,
505
+ },
506
+ body: JSON.stringify({
507
+ jsonrpc: "2.0",
508
+ id: 1,
509
+ method: "tools/call",
510
+ params: {
511
+ name: "search_pages",
512
+ arguments: {
513
+ query: "meeting notes",
514
+ clientId: resolved.clientId,
515
+ },
516
+ },
517
+ }),
518
+ });
519
+ expect(callRes.status).toBe(200);
520
+ const data = (await callRes.json()) as { result: unknown };
521
+ expect(data.result).toBeDefined();
522
+ });
523
+
524
+ // ─── unsupported scheme ───
525
+
526
+ test("resolveSecret throws on unsupported scheme", async () => {
527
+ const consumer = await createRegistryConsumer(
528
+ { registries: [BASE] },
529
+ { token: authToken },
530
+ );
531
+
532
+ expect(consumer.resolveSecret("ftp://some-server/secret")).rejects.toThrow(
533
+ "Unsupported secret URI scheme",
534
+ );
535
+ });
536
+ });