Skip to content

Commit c83252d

Browse files
wangmiao0668000666altaywtf
authored andcommitted
test(anthropic-cloudflare): trim proof to SSRF-block only, remove synthetic loopback server
1 parent 0111e56 commit c83252d

1 file changed

Lines changed: 6 additions & 133 deletions

File tree

Lines changed: 6 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
1-
// Anthropic Cloudflare AI Gateway constructor guard-specific proof: SSRF-blocks a
2-
// private IP before the SDK's default global fetch is ever reached, then
3-
// streams Anthropic SSE end-to-end through buildGuardedModelFetch against
4-
// a real local HTTP server.
1+
// Anthropic Cloudflare AI Gateway constructor guard-specific proof: the SSRF
2+
// guard blocks a private-IP request before the SDK's default global fetch is
3+
// ever reached. This proves `buildGuardedModelFetch` is actively wired into
4+
// the Cloudflare branch, not just present in constructor options.
55
//
66
// Unlike anthropic.test.ts (which mocks the Anthropic SDK to verify
7-
// constructor options), this test does NOT mock the SDK for the SSRF
8-
// block or loopback proof — it stubs globalThis.fetch to COUNT calls and
9-
// proves the guard intercepts the request before the final fetch hop.
10-
// Behavior only buildGuardedModelFetch can produce.
11-
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
12-
import type { AddressInfo } from "node:net";
7+
// constructor options), this test stubs `globalThis.fetch` to COUNT calls.
8+
// Behavior only `buildGuardedModelFetch` can produce.
139
import { afterEach, describe, expect, it, vi } from "vitest";
1410
import type { Context, Model } from "../types.js";
1511

@@ -63,126 +59,3 @@ describe("Anthropic Cloudflare guard-specific SSRF blocking proof", () => {
6359
expect(globalFetchCalled).toBe(0);
6460
});
6561
});
66-
67-
describe("Anthropic Cloudflare guard real wire loopback proof (http.createServer)", () => {
68-
it("streams Anthropic SSE end-to-end through buildGuardedModelFetch against a real loopback server", async () => {
69-
const recorded: Array<{
70-
method: string;
71-
url: string;
72-
bodyBytes: number;
73-
authorization: string;
74-
contentType: string;
75-
}> = [];
76-
const sseEvents: Array<Record<string, unknown>> = [
77-
{
78-
type: "message_start",
79-
message: {
80-
id: "msg_loopback",
81-
type: "message",
82-
role: "assistant",
83-
model: "claude-sonnet-4-6",
84-
content: [],
85-
stop_reason: null,
86-
usage: { input_tokens: 9, output_tokens: 1 },
87-
},
88-
},
89-
{
90-
type: "content_block_start",
91-
index: 0,
92-
content_block: { type: "text", text: "" },
93-
},
94-
{
95-
type: "content_block_delta",
96-
index: 0,
97-
delta: { type: "text_delta", text: "Hello from local Cloudflare Anthropic loopback." },
98-
},
99-
{
100-
type: "content_block_stop",
101-
index: 0,
102-
},
103-
{
104-
type: "message_delta",
105-
delta: { stop_reason: "end_turn" },
106-
usage: { output_tokens: 7 },
107-
},
108-
{
109-
type: "message_stop",
110-
},
111-
];
112-
const sseBody = sseEvents
113-
.map((e) => `event: ${String(e["type"])}\ndata: ${JSON.stringify(e)}\n\n`)
114-
.join("");
115-
116-
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
117-
const chunks: Buffer[] = [];
118-
req.on("data", (c: Buffer) => chunks.push(c));
119-
req.on("end", () => {
120-
const body = Buffer.concat(chunks);
121-
recorded.push({
122-
method: req.method ?? "?",
123-
url: req.url ?? "?",
124-
bodyBytes: body.byteLength,
125-
authorization: (req.headers.authorization as string) ?? "<absent>",
126-
contentType: (req.headers["content-type"] as string) ?? "<absent>",
127-
});
128-
res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8" });
129-
res.end(sseBody);
130-
});
131-
});
132-
133-
await new Promise<void>((resolve) => {
134-
server.listen(0, "127.0.0.1", () => resolve());
135-
});
136-
const port = (server.address() as AddressInfo).port;
137-
138-
try {
139-
const { attachModelProviderRequestTransport } =
140-
await import("../../agents/provider-request-config.js");
141-
const loopbackModel = attachModelProviderRequestTransport(
142-
{
143-
...CLOUDFLARE_ANTHROPIC_MODEL,
144-
baseUrl: `http://127.0.0.1:${port}`,
145-
},
146-
{ allowPrivateNetwork: true },
147-
);
148-
149-
const { streamAnthropic } = await import("./anthropic.js");
150-
const stream = streamAnthropic(loopbackModel, context, {
151-
apiKey: "sk-ant-loopback-proof",
152-
});
153-
154-
let textBytes = 0;
155-
for await (const event of stream) {
156-
if (event.type === "text_delta") {
157-
textBytes += event.delta.length;
158-
}
159-
}
160-
const result = await stream.result();
161-
162-
const hit = recorded[0];
163-
expect(recorded.length).toBeGreaterThanOrEqual(1);
164-
expect(hit.method).toBe("POST");
165-
// Anthropic SDK appends "/v1/messages" to baseUrl, so with
166-
// baseUrl "http://127.0.0.1:PORT" the wire URL is "/v1/messages".
167-
expect(hit.url).toBe("/v1/messages");
168-
expect(hit.bodyBytes).toBeGreaterThan(0);
169-
expect(hit.contentType).toBe("application/json");
170-
171-
expect(textBytes).toBeGreaterThan(0);
172-
expect(result.stopReason).toBe("stop");
173-
expect(result.errorMessage).toBeUndefined();
174-
175-
console.log(
176-
`[layer3 loopback proof] server_hits=${recorded.length} ` +
177-
`request_url=${hit.url} request_bytes=${hit.bodyBytes} ` +
178-
`content_type=${hit.contentType} ` +
179-
`text_bytes=${textBytes} ` +
180-
`stop_reason=${result.stopReason}`,
181-
);
182-
} finally {
183-
await new Promise<void>((resolve) => {
184-
server.close(() => resolve());
185-
});
186-
}
187-
});
188-
});

0 commit comments

Comments
 (0)