@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,76 @@
1
+ import type { FsBackend } from "@nkmc/agent-fs";
2
+ import type { RegistryStore } from "./types.js";
3
+
4
+ export interface VirtualFileOptions {
5
+ inner: FsBackend;
6
+ domain: string;
7
+ store: RegistryStore;
8
+ }
9
+
10
+ const VIRTUAL_FILES = ["_pricing.json", "_versions.json", "skill.md"];
11
+
12
+ export class VirtualFileBackend implements FsBackend {
13
+ private inner: FsBackend;
14
+ private domain: string;
15
+ private store: RegistryStore;
16
+
17
+ constructor(options: VirtualFileOptions) {
18
+ this.inner = options.inner;
19
+ this.domain = options.domain;
20
+ this.store = options.store;
21
+ }
22
+
23
+ async list(path: string): Promise<string[]> {
24
+ const entries = await this.inner.list(path);
25
+ // Append virtual files at root level
26
+ if (path === "/" || path === "" || path === ".") {
27
+ return [...entries, ...VIRTUAL_FILES];
28
+ }
29
+ return entries;
30
+ }
31
+
32
+ async read(path: string): Promise<unknown> {
33
+ const cleaned = path.replace(/^\/+/, "").replace(/\/+$/, "");
34
+
35
+ if (cleaned === "_pricing.json") {
36
+ const record = await this.store.get(this.domain);
37
+ if (!record) return { endpoints: [] };
38
+ return {
39
+ domain: this.domain,
40
+ endpoints: record.endpoints
41
+ .filter((ep) => ep.pricing)
42
+ .map((ep) => ({
43
+ method: ep.method,
44
+ path: ep.path,
45
+ description: ep.description,
46
+ pricing: ep.pricing,
47
+ })),
48
+ };
49
+ }
50
+
51
+ if (cleaned === "_versions.json") {
52
+ const versions = await this.store.listVersions(this.domain);
53
+ return { domain: this.domain, versions };
54
+ }
55
+
56
+ if (cleaned === "skill.md") {
57
+ const record = await this.store.get(this.domain);
58
+ if (!record) return "# Not found\n";
59
+ return record.skillMd;
60
+ }
61
+
62
+ return this.inner.read(path);
63
+ }
64
+
65
+ async write(path: string, data: unknown): Promise<{ id: string }> {
66
+ return this.inner.write(path, data);
67
+ }
68
+
69
+ async remove(path: string): Promise<void> {
70
+ return this.inner.remove(path);
71
+ }
72
+
73
+ async search(path: string, pattern: string): Promise<unknown[]> {
74
+ return this.inner.search(path, pattern);
75
+ }
76
+ }
@@ -0,0 +1,64 @@
1
+ import Database from "better-sqlite3";
2
+ import type {
3
+ D1Database,
4
+ D1PreparedStatement,
5
+ D1Result,
6
+ D1RunResult,
7
+ } from "../d1/types.js";
8
+
9
+ class SqlitePreparedStatement implements D1PreparedStatement {
10
+ private params: unknown[] = [];
11
+
12
+ constructor(
13
+ private db: Database.Database,
14
+ private sql: string,
15
+ ) {}
16
+
17
+ bind(...values: unknown[]): D1PreparedStatement {
18
+ this.params = values;
19
+ return this;
20
+ }
21
+
22
+ async first<T = Record<string, unknown>>(): Promise<T | null> {
23
+ const stmt = this.db.prepare(this.sql);
24
+ const row = stmt.get(...this.params) as T | undefined;
25
+ return row ?? null;
26
+ }
27
+
28
+ async all<T = Record<string, unknown>>(): Promise<D1Result<T>> {
29
+ const stmt = this.db.prepare(this.sql);
30
+ const rows = stmt.all(...this.params) as T[];
31
+ return { results: rows, success: true };
32
+ }
33
+
34
+ async run(): Promise<D1RunResult> {
35
+ const stmt = this.db.prepare(this.sql);
36
+ const info = stmt.run(...this.params);
37
+ return {
38
+ success: true,
39
+ changes: info.changes,
40
+ lastRowId: Number(info.lastInsertRowid),
41
+ };
42
+ }
43
+ }
44
+
45
+ export class SqliteD1 implements D1Database {
46
+ private db: Database.Database;
47
+
48
+ constructor(path?: string) {
49
+ this.db = new Database(path ?? ":memory:");
50
+ this.db.pragma("journal_mode = WAL");
51
+ }
52
+
53
+ prepare(sql: string): D1PreparedStatement {
54
+ return new SqlitePreparedStatement(this.db, sql);
55
+ }
56
+
57
+ async exec(sql: string): Promise<void> {
58
+ this.db.exec(sql);
59
+ }
60
+
61
+ close(): void {
62
+ this.db.close();
63
+ }
64
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { SqliteD1 } from "./testing/sqlite-d1.js";
2
+ export type { D1Database, D1PreparedStatement, D1Result, D1RunResult } from "./d1/types.js";
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { CloudflareTunnelProvider } from "../cloudflare-provider.js";
3
+
4
+ const CF_API = "https://api.cloudflare.com/client/v4";
5
+
6
+ const ACCOUNT_ID = "acct-123";
7
+ const API_TOKEN = "cf-api-token";
8
+ const TUNNEL_DOMAIN = "tunnel.example.com";
9
+ const ZONE_ID = "zone-456";
10
+
11
+ function cfOk<T>(result: T): Response {
12
+ return new Response(
13
+ JSON.stringify({ success: true, errors: [], result }),
14
+ { status: 200, headers: { "Content-Type": "application/json" } },
15
+ );
16
+ }
17
+
18
+ function cfError(code: number, message: string): Response {
19
+ return new Response(
20
+ JSON.stringify({ success: false, errors: [{ code, message }], result: null }),
21
+ { status: 400, headers: { "Content-Type": "application/json" } },
22
+ );
23
+ }
24
+
25
+ describe("CloudflareTunnelProvider", () => {
26
+ const originalFetch = globalThis.fetch;
27
+ const mockFetch = vi.fn<typeof fetch>();
28
+
29
+ beforeEach(() => {
30
+ globalThis.fetch = mockFetch;
31
+ });
32
+
33
+ afterEach(() => {
34
+ globalThis.fetch = originalFetch;
35
+ mockFetch.mockReset();
36
+ });
37
+
38
+ function createProvider(): CloudflareTunnelProvider {
39
+ return new CloudflareTunnelProvider(
40
+ ACCOUNT_ID,
41
+ API_TOKEN,
42
+ TUNNEL_DOMAIN,
43
+ ZONE_ID,
44
+ );
45
+ }
46
+
47
+ // -------------------------------------------------------------------------
48
+ // create()
49
+ // -------------------------------------------------------------------------
50
+
51
+ describe("create()", () => {
52
+ it("calls correct CF API endpoints in order and returns tunnelId + tunnelToken", async () => {
53
+ const tunnelId = "tun-abc-123";
54
+ const tunnelToken = "eyJ0b2tlbiI6InRlc3QifQ==";
55
+
56
+ // 1. Create tunnel
57
+ mockFetch.mockResolvedValueOnce(cfOk({ id: tunnelId }));
58
+ // 2. Create DNS CNAME
59
+ mockFetch.mockResolvedValueOnce(cfOk({ id: "dns-rec-1" }));
60
+ // 3. Configure tunnel ingress
61
+ mockFetch.mockResolvedValueOnce(cfOk({}));
62
+ // 4. Get tunnel token
63
+ mockFetch.mockResolvedValueOnce(cfOk(tunnelToken));
64
+
65
+ const provider = createProvider();
66
+ const result = await provider.create("nkmc-agent-1", "a1.tunnel.example.com");
67
+
68
+ expect(result).toEqual({ tunnelId, tunnelToken });
69
+
70
+ // Verify call order
71
+ expect(mockFetch).toHaveBeenCalledTimes(4);
72
+
73
+ // Call 1: Create tunnel
74
+ const [url1, opts1] = mockFetch.mock.calls[0];
75
+ expect(url1).toBe(`${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel`);
76
+ expect(opts1?.method).toBe("POST");
77
+ const body1 = JSON.parse(opts1?.body as string);
78
+ expect(body1.name).toBe("nkmc-agent-1");
79
+ expect(body1.tunnel_secret).toBeDefined();
80
+ expect(opts1?.headers).toMatchObject({
81
+ Authorization: `Bearer ${API_TOKEN}`,
82
+ "Content-Type": "application/json",
83
+ });
84
+
85
+ // Call 2: Create DNS CNAME
86
+ const [url2, opts2] = mockFetch.mock.calls[1];
87
+ expect(url2).toBe(`${CF_API}/zones/${ZONE_ID}/dns_records`);
88
+ expect(opts2?.method).toBe("POST");
89
+ const body2 = JSON.parse(opts2?.body as string);
90
+ expect(body2).toEqual({
91
+ type: "CNAME",
92
+ name: "a1.tunnel.example.com",
93
+ content: `${tunnelId}.cfargotunnel.com`,
94
+ proxied: true,
95
+ });
96
+
97
+ // Call 3: Configure tunnel ingress
98
+ const [url3, opts3] = mockFetch.mock.calls[2];
99
+ expect(url3).toBe(
100
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}/configurations`,
101
+ );
102
+ expect(opts3?.method).toBe("PUT");
103
+ const body3 = JSON.parse(opts3?.body as string);
104
+ expect(body3.config.ingress).toEqual([
105
+ { hostname: "a1.tunnel.example.com", service: "http://localhost:9090" },
106
+ { service: "http_status:404" },
107
+ ]);
108
+
109
+ // Call 4: Get tunnel token
110
+ const [url4] = mockFetch.mock.calls[3];
111
+ expect(url4).toBe(
112
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}/token`,
113
+ );
114
+ });
115
+
116
+ it("cleans up tunnel on DNS creation failure", async () => {
117
+ const tunnelId = "tun-cleanup-1";
118
+
119
+ // 1. Create tunnel succeeds
120
+ mockFetch.mockResolvedValueOnce(cfOk({ id: tunnelId }));
121
+ // 2. DNS creation fails
122
+ mockFetch.mockResolvedValueOnce(cfError(1003, "DNS record already exists"));
123
+ // 3. Cleanup: cascade delete
124
+ mockFetch.mockResolvedValueOnce(cfOk({}));
125
+
126
+ const provider = createProvider();
127
+
128
+ await expect(
129
+ provider.create("nkmc-cleanup", "c1.tunnel.example.com"),
130
+ ).rejects.toThrow("Cloudflare API error: DNS record already exists");
131
+
132
+ // Verify cleanup was called
133
+ expect(mockFetch).toHaveBeenCalledTimes(3);
134
+ const [cleanupUrl, cleanupOpts] = mockFetch.mock.calls[2];
135
+ expect(cleanupUrl).toBe(
136
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}?cascade=true`,
137
+ );
138
+ expect(cleanupOpts?.method).toBe("DELETE");
139
+ });
140
+
141
+ it("cleans up tunnel on ingress config failure", async () => {
142
+ const tunnelId = "tun-cleanup-2";
143
+
144
+ // 1. Create tunnel succeeds
145
+ mockFetch.mockResolvedValueOnce(cfOk({ id: tunnelId }));
146
+ // 2. DNS creation succeeds
147
+ mockFetch.mockResolvedValueOnce(cfOk({ id: "dns-1" }));
148
+ // 3. Ingress config fails
149
+ mockFetch.mockResolvedValueOnce(cfError(1004, "Invalid config"));
150
+ // 4. Cleanup: cascade delete
151
+ mockFetch.mockResolvedValueOnce(cfOk({}));
152
+
153
+ const provider = createProvider();
154
+
155
+ await expect(
156
+ provider.create("nkmc-cleanup-2", "c2.tunnel.example.com"),
157
+ ).rejects.toThrow("Cloudflare API error: Invalid config");
158
+
159
+ // Verify cleanup cascade delete
160
+ expect(mockFetch).toHaveBeenCalledTimes(4);
161
+ const [cleanupUrl, cleanupOpts] = mockFetch.mock.calls[3];
162
+ expect(cleanupUrl).toBe(
163
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}?cascade=true`,
164
+ );
165
+ expect(cleanupOpts?.method).toBe("DELETE");
166
+ });
167
+
168
+ it("still throws original error if cleanup also fails", async () => {
169
+ const tunnelId = "tun-cleanup-3";
170
+
171
+ // 1. Create tunnel succeeds
172
+ mockFetch.mockResolvedValueOnce(cfOk({ id: tunnelId }));
173
+ // 2. DNS creation fails
174
+ mockFetch.mockResolvedValueOnce(cfError(1003, "DNS conflict"));
175
+ // 3. Cleanup also fails
176
+ mockFetch.mockResolvedValueOnce(cfError(9999, "Internal error"));
177
+
178
+ const provider = createProvider();
179
+
180
+ // Should throw the original error, not the cleanup error
181
+ await expect(
182
+ provider.create("nkmc-fail", "f1.tunnel.example.com"),
183
+ ).rejects.toThrow("Cloudflare API error: DNS conflict");
184
+ });
185
+ });
186
+
187
+ // -------------------------------------------------------------------------
188
+ // delete()
189
+ // -------------------------------------------------------------------------
190
+
191
+ describe("delete()", () => {
192
+ it("calls cascade delete endpoint", async () => {
193
+ const tunnelId = "tun-del-1";
194
+
195
+ // 1. List DNS records -> no matching records
196
+ mockFetch.mockResolvedValueOnce(cfOk([]));
197
+ // 2. Cascade delete
198
+ mockFetch.mockResolvedValueOnce(cfOk({}));
199
+
200
+ const provider = createProvider();
201
+ await provider.delete(tunnelId);
202
+
203
+ expect(mockFetch).toHaveBeenCalledTimes(2);
204
+
205
+ // Verify cascade delete call
206
+ const [delUrl, delOpts] = mockFetch.mock.calls[1];
207
+ expect(delUrl).toBe(
208
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}?cascade=true`,
209
+ );
210
+ expect(delOpts?.method).toBe("DELETE");
211
+ });
212
+
213
+ it("cleans up DNS records before deleting tunnel", async () => {
214
+ const tunnelId = "tun-del-dns";
215
+
216
+ // 1. List DNS records -> 2 matching records
217
+ mockFetch.mockResolvedValueOnce(
218
+ cfOk([
219
+ { id: "dns-rec-a", content: `${tunnelId}.cfargotunnel.com` },
220
+ { id: "dns-rec-b", content: `${tunnelId}.cfargotunnel.com` },
221
+ ]),
222
+ );
223
+ // 2. Delete DNS record A
224
+ mockFetch.mockResolvedValueOnce(cfOk({}));
225
+ // 3. Delete DNS record B
226
+ mockFetch.mockResolvedValueOnce(cfOk({}));
227
+ // 4. Cascade delete tunnel
228
+ mockFetch.mockResolvedValueOnce(cfOk({}));
229
+
230
+ const provider = createProvider();
231
+ await provider.delete(tunnelId);
232
+
233
+ expect(mockFetch).toHaveBeenCalledTimes(4);
234
+
235
+ // Verify DNS lookup
236
+ const [listUrl] = mockFetch.mock.calls[0];
237
+ expect(listUrl).toBe(
238
+ `${CF_API}/zones/${ZONE_ID}/dns_records?type=CNAME&content=${tunnelId}.cfargotunnel.com`,
239
+ );
240
+
241
+ // Verify DNS deletes
242
+ const [dnsDelUrlA] = mockFetch.mock.calls[1];
243
+ expect(dnsDelUrlA).toBe(`${CF_API}/zones/${ZONE_ID}/dns_records/dns-rec-a`);
244
+
245
+ const [dnsDelUrlB] = mockFetch.mock.calls[2];
246
+ expect(dnsDelUrlB).toBe(`${CF_API}/zones/${ZONE_ID}/dns_records/dns-rec-b`);
247
+
248
+ // Verify tunnel cascade delete
249
+ const [tunDelUrl] = mockFetch.mock.calls[3];
250
+ expect(tunDelUrl).toBe(
251
+ `${CF_API}/accounts/${ACCOUNT_ID}/cfd_tunnel/${tunnelId}?cascade=true`,
252
+ );
253
+ });
254
+ });
255
+ });