@nkmc/gateway 0.1.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 (130) hide show
  1. package/dist/chunk-56RA53VS.js +37 -0
  2. package/dist/chunk-CZJ75YTV.js +969 -0
  3. package/dist/chunk-QGM4M3NI.js +37 -0
  4. package/dist/http.cjs +1772 -0
  5. package/dist/http.d.cts +49 -0
  6. package/dist/http.d.ts +49 -0
  7. package/dist/http.js +748 -0
  8. package/dist/index.cjs +2436 -0
  9. package/dist/index.d.cts +436 -0
  10. package/dist/index.d.ts +436 -0
  11. package/dist/index.js +1434 -0
  12. package/dist/proxy-ClPcDgsO.d.cts +283 -0
  13. package/dist/proxy-qpda1ANS.d.ts +283 -0
  14. package/dist/proxy.cjs +148 -0
  15. package/dist/proxy.d.cts +6 -0
  16. package/dist/proxy.d.ts +6 -0
  17. package/dist/proxy.js +90 -0
  18. package/dist/testing.cjs +865 -0
  19. package/dist/testing.d.cts +12 -0
  20. package/dist/testing.d.ts +12 -0
  21. package/dist/testing.js +831 -0
  22. package/dist/tunnels-BviBEaih.d.cts +12 -0
  23. package/dist/tunnels-DFHNgmN7.d.ts +12 -0
  24. package/dist/types-C6JC9oTm.d.cts +21 -0
  25. package/dist/types-C6JC9oTm.d.ts +21 -0
  26. package/package.json +47 -0
  27. package/src/__tests__/sqlite-integration.test.ts +384 -0
  28. package/src/credential/d1-vault.ts +134 -0
  29. package/src/credential/memory-vault.ts +50 -0
  30. package/src/credential/types.ts +16 -0
  31. package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
  32. package/src/d1/sqlite-adapter.ts +59 -0
  33. package/src/d1/types.ts +22 -0
  34. package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
  35. package/src/federation/__tests__/peer-client.test.ts +205 -0
  36. package/src/federation/__tests__/peer-store.test.ts +114 -0
  37. package/src/federation/d1-peer-store.ts +164 -0
  38. package/src/federation/peer-backend.ts +60 -0
  39. package/src/federation/peer-client.ts +122 -0
  40. package/src/federation/peer-store.ts +45 -0
  41. package/src/federation/types.ts +39 -0
  42. package/src/http/app.ts +152 -0
  43. package/src/http/lib/dns.ts +30 -0
  44. package/src/http/middleware/admin-auth.ts +18 -0
  45. package/src/http/middleware/agent-auth.ts +27 -0
  46. package/src/http/middleware/publish-auth.ts +39 -0
  47. package/src/http/routes/__tests__/federation.test.ts +364 -0
  48. package/src/http/routes/__tests__/peers.test.ts +290 -0
  49. package/src/http/routes/__tests__/proxy.test.ts +159 -0
  50. package/src/http/routes/auth.ts +39 -0
  51. package/src/http/routes/byok.ts +62 -0
  52. package/src/http/routes/credentials.ts +40 -0
  53. package/src/http/routes/domains.ts +174 -0
  54. package/src/http/routes/federation.ts +170 -0
  55. package/src/http/routes/fs.ts +89 -0
  56. package/src/http/routes/peers.ts +103 -0
  57. package/src/http/routes/proxy.ts +57 -0
  58. package/src/http/routes/registry.ts +222 -0
  59. package/src/http/routes/tunnels.ts +124 -0
  60. package/src/http.ts +9 -0
  61. package/src/index.ts +63 -0
  62. package/src/metering/d1-store.ts +123 -0
  63. package/src/metering/memory-store.ts +29 -0
  64. package/src/metering/pricing-guard.ts +68 -0
  65. package/src/metering/types.ts +25 -0
  66. package/src/onboard/apis-guru.ts +64 -0
  67. package/src/onboard/index.ts +4 -0
  68. package/src/onboard/manifest.ts +362 -0
  69. package/src/onboard/pipeline.ts +214 -0
  70. package/src/onboard/types.ts +72 -0
  71. package/src/proxy/__tests__/tool-registry.test.ts +93 -0
  72. package/src/proxy/tool-registry.ts +122 -0
  73. package/src/proxy.ts +12 -0
  74. package/src/registry/context7-backend.ts +93 -0
  75. package/src/registry/context7.ts +54 -0
  76. package/src/registry/d1-store.ts +242 -0
  77. package/src/registry/memory-store.ts +101 -0
  78. package/src/registry/openapi-compiler.ts +284 -0
  79. package/src/registry/resolver.ts +196 -0
  80. package/src/registry/rpc-compiler.ts +142 -0
  81. package/src/registry/skill-parser.ts +119 -0
  82. package/src/registry/skill-to-config.ts +239 -0
  83. package/src/registry/source-refresher.ts +83 -0
  84. package/src/registry/types.ts +129 -0
  85. package/src/registry/virtual-files.ts +76 -0
  86. package/src/testing/sqlite-d1.ts +64 -0
  87. package/src/testing.ts +2 -0
  88. package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
  89. package/src/tunnel/__tests__/tunnel.test.ts +542 -0
  90. package/src/tunnel/cloudflare-provider.ts +121 -0
  91. package/src/tunnel/memory-store.ts +30 -0
  92. package/src/tunnel/types.ts +28 -0
  93. package/test/credential/d1-vault.test.ts +127 -0
  94. package/test/credential/injection.test.ts +67 -0
  95. package/test/credential/memory-vault.test.ts +63 -0
  96. package/test/http/app.test.ts +300 -0
  97. package/test/http/byok-e2e.test.ts +240 -0
  98. package/test/http/byok.test.ts +115 -0
  99. package/test/http/credentials.test.ts +57 -0
  100. package/test/http/e2e.test.ts +260 -0
  101. package/test/integration/authenticated-apis.test.ts +185 -0
  102. package/test/integration/free-apis-e2e.test.ts +222 -0
  103. package/test/metering/d1-store.test.ts +82 -0
  104. package/test/metering/memory-store.test.ts +76 -0
  105. package/test/metering/pricing-guard.test.ts +108 -0
  106. package/test/onboard/apis-guru.test.ts +57 -0
  107. package/test/onboard/e2e.test.ts +70 -0
  108. package/test/onboard/pipeline.test.ts +318 -0
  109. package/test/onboard/real-apis.test.ts +483 -0
  110. package/test/registry/compilation-correctness.test.ts +132 -0
  111. package/test/registry/context7-backend.test.ts +88 -0
  112. package/test/registry/context7-e2e.test.ts +92 -0
  113. package/test/registry/context7.test.ts +73 -0
  114. package/test/registry/d1-store.test.ts +184 -0
  115. package/test/registry/integration.test.ts +129 -0
  116. package/test/registry/lazy-mount.test.ts +138 -0
  117. package/test/registry/memory-store.test.ts +171 -0
  118. package/test/registry/openapi-compiler.test.ts +267 -0
  119. package/test/registry/openapi-e2e.test.ts +154 -0
  120. package/test/registry/passthrough-e2e.test.ts +109 -0
  121. package/test/registry/resolver-peer.test.ts +299 -0
  122. package/test/registry/resolver.test.ts +228 -0
  123. package/test/registry/rpc-compiler.test.ts +112 -0
  124. package/test/registry/skill-parser.test.ts +151 -0
  125. package/test/registry/skill-to-config.test.ts +151 -0
  126. package/test/registry/skill-to-rpc-config.test.ts +142 -0
  127. package/test/registry/source-refresher.test.ts +90 -0
  128. package/test/registry/virtual-files.test.ts +96 -0
  129. package/tsconfig.json +4 -0
  130. package/tsup.config.ts +8 -0
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { discoverFromApisGuru } from "../../src/onboard/apis-guru.js";
3
+
4
+ const MOCK_CATALOG = {
5
+ "petstore.com:1.0.0": {
6
+ preferred: "1.0.0",
7
+ versions: {
8
+ "1.0.0": {
9
+ swaggerUrl: "https://petstore.com/openapi.json",
10
+ info: { title: "Petstore", description: "A pet store API" },
11
+ },
12
+ },
13
+ },
14
+ "weather.com:2.0": {
15
+ preferred: "2.0",
16
+ versions: {
17
+ "2.0": {
18
+ swaggerUrl: "https://weather.com/v2/openapi.json",
19
+ info: { title: "Weather API", description: "Global weather data" },
20
+ },
21
+ },
22
+ },
23
+ "nospec.com:1.0": {
24
+ preferred: "1.0",
25
+ versions: { "1.0": { info: { title: "No Spec" } } },
26
+ },
27
+ };
28
+
29
+ function mockFetch() {
30
+ return async () =>
31
+ new Response(JSON.stringify(MOCK_CATALOG), {
32
+ status: 200,
33
+ headers: { "Content-Type": "application/json" },
34
+ });
35
+ }
36
+
37
+ describe("discoverFromApisGuru", () => {
38
+ it("should discover APIs from catalog", async () => {
39
+ const entries = await discoverFromApisGuru({ fetchFn: mockFetch() as any });
40
+ // nospec.com has no swaggerUrl → skipped
41
+ expect(entries).toHaveLength(2);
42
+ expect(entries[0].domain).toBe("petstore.com");
43
+ expect(entries[0].specUrl).toBe("https://petstore.com/openapi.json");
44
+ expect(entries[0].tags).toContain("apis-guru");
45
+ });
46
+
47
+ it("should respect limit", async () => {
48
+ const entries = await discoverFromApisGuru({ limit: 1, fetchFn: mockFetch() as any });
49
+ expect(entries).toHaveLength(1);
50
+ });
51
+
52
+ it("should filter by keyword", async () => {
53
+ const entries = await discoverFromApisGuru({ filter: "weather", fetchFn: mockFetch() as any });
54
+ expect(entries).toHaveLength(1);
55
+ expect(entries[0].domain).toBe("weather.com");
56
+ });
57
+ });
@@ -0,0 +1,70 @@
1
+ /**
2
+ * E2E: onboard real APIs from apis.guru, then interact via AgentFs.
3
+ */
4
+ import { describe, it, expect } from "vitest";
5
+ import { OnboardPipeline } from "../../src/onboard/pipeline.js";
6
+ import { discoverFromApisGuru } from "../../src/onboard/apis-guru.js";
7
+ import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
8
+ import { createRegistryResolver } from "../../src/registry/resolver.js";
9
+ import { AgentFs } from "@nkmc/agent-fs";
10
+
11
+ describe("Onboard E2E: apis.guru → AgentFs (real network)", () => {
12
+ it("should discover, onboard, and browse a real public API", async () => {
13
+ const store = new MemoryRegistryStore();
14
+
15
+ // 1. Discover — pick a known small API
16
+ const pipeline = new OnboardPipeline({
17
+ store,
18
+ smokeTest: false,
19
+ });
20
+
21
+ const result = await pipeline.onboardOne({
22
+ domain: "petstore3.swagger.io",
23
+ specUrl: "https://petstore3.swagger.io/api/v3/openapi.json",
24
+ });
25
+
26
+ expect(result.status).toBe("ok");
27
+ expect(result.source).toBe("openapi");
28
+ expect(result.endpoints).toBeGreaterThan(0);
29
+ expect(result.resources).toBeGreaterThan(0);
30
+
31
+ // 2. Browse via AgentFs
32
+ const { onMiss, listDomains } = createRegistryResolver({ store, wrapVirtualFiles: false });
33
+ const fs = new AgentFs({ mounts: [], onMiss, listDomains });
34
+
35
+ // ls / — should show the registered service
36
+ const lsRoot = await fs.execute("ls /");
37
+ expect(lsRoot.ok).toBe(true);
38
+ expect(lsRoot.data).toContain("petstore3.swagger.io/");
39
+
40
+ // ls /domain/ — should show resources from the spec
41
+ const lsDomain = await fs.execute("ls /petstore3.swagger.io/");
42
+ expect(lsDomain.ok).toBe(true);
43
+ const entries = lsDomain.data as string[];
44
+ expect(entries.some((e) => e.includes("pet"))).toBe(true);
45
+ expect(entries).toContain("_api/");
46
+ }, 30_000);
47
+
48
+ it("should batch-onboard multiple APIs from apis.guru", async () => {
49
+ // Discover 3 APIs with keyword filter
50
+ const entries = await discoverFromApisGuru({ limit: 3 });
51
+ expect(entries.length).toBeGreaterThan(0);
52
+
53
+ const store = new MemoryRegistryStore();
54
+ const pipeline = new OnboardPipeline({
55
+ store,
56
+ smokeTest: false,
57
+ concurrency: 3,
58
+ });
59
+
60
+ const report = await pipeline.onboardMany(entries);
61
+
62
+ // At least some should succeed (some specs might be unreachable)
63
+ expect(report.total).toBe(entries.length);
64
+ expect(report.ok).toBeGreaterThan(0);
65
+
66
+ // Verify store has services
67
+ const services = await store.list();
68
+ expect(services.length).toBe(report.ok);
69
+ }, 60_000);
70
+ });
@@ -0,0 +1,318 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { OnboardPipeline } from "../../src/onboard/pipeline.js";
3
+ import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
4
+ import { MemoryCredentialVault } from "../../src/credential/memory-vault.js";
5
+ import type { ManifestEntry } from "../../src/onboard/types.js";
6
+
7
+ const PETSTORE_SPEC = {
8
+ openapi: "3.0.0",
9
+ info: { title: "Petstore", description: "A pet store", version: "1.0.0" },
10
+ paths: {
11
+ "/pets": { get: { summary: "List pets" }, post: { summary: "Create pet" } },
12
+ "/pets/{petId}": { get: { summary: "Get pet" } },
13
+ },
14
+ };
15
+
16
+ function mockFetch(overrides?: Record<string, { status: number; body: any; contentType?: string }>) {
17
+ return async (url: string) => {
18
+ for (const [pattern, resp] of Object.entries(overrides ?? {})) {
19
+ if (url.includes(pattern)) {
20
+ const ct = resp.contentType ?? "application/json";
21
+ const body = ct.includes("json") ? JSON.stringify(resp.body) : resp.body;
22
+ return new Response(body, {
23
+ status: resp.status,
24
+ headers: { "Content-Type": ct },
25
+ });
26
+ }
27
+ }
28
+ return new Response(JSON.stringify(PETSTORE_SPEC), {
29
+ status: 200,
30
+ headers: { "Content-Type": "application/json" },
31
+ });
32
+ };
33
+ }
34
+
35
+ describe("OnboardPipeline", () => {
36
+ let store: MemoryRegistryStore;
37
+ let vault: MemoryCredentialVault;
38
+
39
+ beforeEach(() => {
40
+ store = new MemoryRegistryStore();
41
+ vault = new MemoryCredentialVault();
42
+ });
43
+
44
+ it("should onboard a service from OpenAPI spec URL", async () => {
45
+ const pipeline = new OnboardPipeline({
46
+ store,
47
+ vault,
48
+ smokeTest: false,
49
+ fetchFn: mockFetch() as any,
50
+ });
51
+
52
+ const result = await pipeline.onboardOne({
53
+ domain: "petstore.com",
54
+ specUrl: "https://petstore.com/openapi.json",
55
+ });
56
+
57
+ expect(result.status).toBe("ok");
58
+ expect(result.source).toBe("openapi");
59
+ expect(result.endpoints).toBe(3);
60
+ expect(result.resources).toBeGreaterThan(0);
61
+
62
+ // Verify service is registered
63
+ const record = await store.get("petstore.com");
64
+ expect(record).not.toBeNull();
65
+ expect(record!.name).toBe("Petstore");
66
+ });
67
+
68
+ it("should onboard a service from inline skill.md", async () => {
69
+ const pipeline = new OnboardPipeline({
70
+ store,
71
+ smokeTest: false,
72
+ fetchFn: mockFetch() as any,
73
+ });
74
+
75
+ const result = await pipeline.onboardOne({
76
+ domain: "acme.com",
77
+ skillMd: `---
78
+ name: "Acme"
79
+ gateway: nkmc
80
+ version: "1.0"
81
+ roles: [agent]
82
+ ---
83
+
84
+ # Acme
85
+
86
+ A service.
87
+
88
+ ## API
89
+
90
+ ### Get data
91
+
92
+ \`GET /api/data\` — public
93
+ `,
94
+ });
95
+
96
+ expect(result.status).toBe("ok");
97
+ expect(result.source).toBe("skillmd");
98
+ expect(result.endpoints).toBe(1);
99
+ });
100
+
101
+ it("should store pool credentials from manifest", async () => {
102
+ const pipeline = new OnboardPipeline({
103
+ store,
104
+ vault,
105
+ smokeTest: false,
106
+ fetchFn: mockFetch() as any,
107
+ });
108
+
109
+ const result = await pipeline.onboardOne({
110
+ domain: "petstore.com",
111
+ specUrl: "https://petstore.com/openapi.json",
112
+ auth: { type: "bearer", token: "secret-123" },
113
+ });
114
+
115
+ expect(result.status).toBe("ok");
116
+ expect(result.hasCredentials).toBe(true);
117
+
118
+ // Verify credential is stored
119
+ const cred = await vault.get("petstore.com");
120
+ expect(cred).not.toBeNull();
121
+ expect(cred!.auth).toEqual({ type: "bearer", token: "secret-123" });
122
+ });
123
+
124
+ it("should resolve ${ENV_VAR} in auth", async () => {
125
+ process.env._TEST_TOKEN = "resolved-secret";
126
+ const pipeline = new OnboardPipeline({
127
+ store,
128
+ vault,
129
+ smokeTest: false,
130
+ fetchFn: mockFetch() as any,
131
+ });
132
+
133
+ const result = await pipeline.onboardOne({
134
+ domain: "petstore.com",
135
+ specUrl: "https://petstore.com/openapi.json",
136
+ auth: { type: "bearer", token: "${_TEST_TOKEN}" },
137
+ });
138
+
139
+ expect(result.status).toBe("ok");
140
+ const cred = await vault.get("petstore.com");
141
+ expect(cred!.auth).toEqual({ type: "bearer", token: "resolved-secret" });
142
+ delete process.env._TEST_TOKEN;
143
+ });
144
+
145
+ it("should skip disabled entries", async () => {
146
+ const pipeline = new OnboardPipeline({
147
+ store,
148
+ smokeTest: false,
149
+ fetchFn: mockFetch() as any,
150
+ });
151
+
152
+ const result = await pipeline.onboardOne({
153
+ domain: "skip.com",
154
+ specUrl: "https://skip.com/openapi.json",
155
+ disabled: true,
156
+ });
157
+
158
+ expect(result.status).toBe("skipped");
159
+ });
160
+
161
+ it("should report failure on bad spec URL", async () => {
162
+ const pipeline = new OnboardPipeline({
163
+ store,
164
+ smokeTest: false,
165
+ fetchFn: mockFetch({
166
+ "bad.com": { status: 404, body: "not found" },
167
+ }) as any,
168
+ });
169
+
170
+ const result = await pipeline.onboardOne({
171
+ domain: "bad.com",
172
+ specUrl: "https://bad.com/openapi.json",
173
+ });
174
+
175
+ expect(result.status).toBe("failed");
176
+ expect(result.error).toContain("404");
177
+ });
178
+
179
+ it("should skip entries with no source", async () => {
180
+ const pipeline = new OnboardPipeline({
181
+ store,
182
+ smokeTest: false,
183
+ fetchFn: mockFetch() as any,
184
+ });
185
+
186
+ const result = await pipeline.onboardOne({ domain: "empty.com" });
187
+ expect(result.status).toBe("skipped");
188
+ });
189
+
190
+ it("should run smoke test on registered service", async () => {
191
+ const pipeline = new OnboardPipeline({
192
+ store,
193
+ vault,
194
+ smokeTest: true,
195
+ fetchFn: mockFetch() as any,
196
+ });
197
+
198
+ const result = await pipeline.onboardOne({
199
+ domain: "petstore.com",
200
+ specUrl: "https://petstore.com/openapi.json",
201
+ });
202
+
203
+ expect(result.status).toBe("ok");
204
+ // Smoke test ls should work (returns local resources)
205
+ expect(result.smokeTest?.ls).toBe(true);
206
+ });
207
+
208
+ it("should process batch with concurrency", async () => {
209
+ const pipeline = new OnboardPipeline({
210
+ store,
211
+ smokeTest: false,
212
+ concurrency: 2,
213
+ fetchFn: mockFetch() as any,
214
+ });
215
+
216
+ const entries: ManifestEntry[] = [
217
+ { domain: "api1.com", specUrl: "https://api1.com/spec.json" },
218
+ { domain: "api2.com", specUrl: "https://api2.com/spec.json" },
219
+ { domain: "api3.com", specUrl: "https://api3.com/spec.json" },
220
+ { domain: "skip.com", disabled: true },
221
+ ];
222
+
223
+ const report = await pipeline.onboardMany(entries);
224
+
225
+ expect(report.total).toBe(4);
226
+ expect(report.ok).toBe(3);
227
+ expect(report.skipped).toBe(1);
228
+ expect(report.failed).toBe(0);
229
+ expect(report.durationMs).toBeGreaterThanOrEqual(0);
230
+ });
231
+
232
+ it("should onboard a service from RPC definition", async () => {
233
+ const pipeline = new OnboardPipeline({
234
+ store,
235
+ smokeTest: false,
236
+ });
237
+
238
+ const result = await pipeline.onboardOne({
239
+ domain: "rpc.example.com",
240
+ rpcDef: {
241
+ url: "https://rpc.example.com/eth",
242
+ convention: "evm",
243
+ methods: [
244
+ { rpcMethod: "eth_blockNumber", description: "Latest block", resource: "blocks", fsOp: "list" },
245
+ { rpcMethod: "eth_getBlockByNumber", description: "Get block", resource: "blocks", fsOp: "read" },
246
+ { rpcMethod: "eth_getBalance", description: "Get balance", resource: "balances", fsOp: "read" },
247
+ { rpcMethod: "eth_chainId", description: "Chain ID" },
248
+ ],
249
+ },
250
+ });
251
+
252
+ expect(result.status).toBe("ok");
253
+ expect(result.source).toBe("jsonrpc");
254
+ expect(result.endpoints).toBe(4);
255
+ expect(result.resources).toBeGreaterThan(0);
256
+
257
+ // Verify service is registered with RPC source metadata
258
+ const record = await store.get("rpc.example.com");
259
+ expect(record).not.toBeNull();
260
+ expect(record!.source?.type).toBe("jsonrpc");
261
+ expect(record!.source?.rpc?.convention).toBe("evm");
262
+ expect(record!.source?.rpc?.rpcUrl).toBe("https://rpc.example.com/eth");
263
+
264
+ // Verify resources are compiled
265
+ const blocks = record!.source?.rpc?.resources.find((r) => r.name === "blocks");
266
+ expect(blocks).toBeDefined();
267
+ expect(blocks!.methods).toEqual({
268
+ list: "eth_blockNumber",
269
+ read: "eth_getBlockByNumber",
270
+ });
271
+ });
272
+
273
+ it("should include RPC services in batch onboard", async () => {
274
+ const pipeline = new OnboardPipeline({
275
+ store,
276
+ smokeTest: false,
277
+ fetchFn: mockFetch() as any,
278
+ });
279
+
280
+ const entries: ManifestEntry[] = [
281
+ { domain: "api.example.com", specUrl: "https://api.example.com/spec.json" },
282
+ {
283
+ domain: "rpc.example.com",
284
+ rpcDef: {
285
+ url: "https://rpc.example.com",
286
+ convention: "evm",
287
+ methods: [
288
+ { rpcMethod: "eth_blockNumber", description: "Latest block", resource: "blocks", fsOp: "list" },
289
+ ],
290
+ },
291
+ },
292
+ ];
293
+
294
+ const report = await pipeline.onboardMany(entries);
295
+ expect(report.ok).toBe(2);
296
+ expect(report.results[0].source).toBe("openapi");
297
+ expect(report.results[1].source).toBe("jsonrpc");
298
+ });
299
+
300
+ it("should call onProgress callback", async () => {
301
+ const progress: { domain: string; index: number }[] = [];
302
+ const pipeline = new OnboardPipeline({
303
+ store,
304
+ smokeTest: false,
305
+ fetchFn: mockFetch() as any,
306
+ onProgress: (result, index) => {
307
+ progress.push({ domain: result.domain, index });
308
+ },
309
+ });
310
+
311
+ await pipeline.onboardMany([
312
+ { domain: "a.com", specUrl: "https://a.com/spec.json" },
313
+ { domain: "b.com", specUrl: "https://b.com/spec.json" },
314
+ ]);
315
+
316
+ expect(progress).toHaveLength(2);
317
+ });
318
+ });