@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.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- 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,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
|
+
});
|