@agent-native/creative-context 0.6.0 → 0.6.2
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/connectors/rendered-page.d.ts +7 -11
- package/dist/connectors/rendered-page.d.ts.map +1 -1
- package/dist/connectors/rendered-page.js +298 -37
- package/dist/connectors/rendered-page.js.map +1 -1
- package/package.json +3 -2
- package/src/connectors/rendered-page.spec.ts +287 -1
- package/src/connectors/rendered-page.ts +393 -46
- package/src/connectors/website.spec.ts +3 -3
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
const isBlockedExtensionUrlWithDns = vi.hoisted(() => vi.fn(async () => false));
|
|
4
|
+
const ssrfSafeFetch = vi.hoisted(() => vi.fn());
|
|
4
5
|
|
|
5
6
|
vi.mock("@agent-native/core/extensions/url-safety", () => ({
|
|
6
7
|
isBlockedExtensionUrlWithDns,
|
|
7
|
-
ssrfSafeFetch
|
|
8
|
+
ssrfSafeFetch,
|
|
8
9
|
}));
|
|
9
10
|
|
|
10
11
|
const { renderWithPlaywright } = await import("./rendered-page.js");
|
|
@@ -12,6 +13,7 @@ const { renderWithPlaywright } = await import("./rendered-page.js");
|
|
|
12
13
|
describe("renderWithPlaywright lifecycle", () => {
|
|
13
14
|
afterEach(() => {
|
|
14
15
|
vi.useRealTimers();
|
|
16
|
+
ssrfSafeFetch.mockReset();
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
it("closes isolated contexts and reports bounded stabilization failures", async () => {
|
|
@@ -91,4 +93,288 @@ describe("renderWithPlaywright lifecycle", () => {
|
|
|
91
93
|
);
|
|
92
94
|
expect(result.diagnostics).toEqual(expect.arrayContaining(result.warnings));
|
|
93
95
|
});
|
|
96
|
+
|
|
97
|
+
it("hydrates through the SSRF-safe network proxy without forwarding cookies", async () => {
|
|
98
|
+
ssrfSafeFetch
|
|
99
|
+
.mockResolvedValueOnce(
|
|
100
|
+
new Response(
|
|
101
|
+
"<!doctype html><html><head><title>Proxy example</title></head><body>Hydrated CTA</body></html>",
|
|
102
|
+
{ status: 200, headers: { "content-type": "text/html" } },
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
.mockResolvedValueOnce(
|
|
106
|
+
new Response(".cta{color:rgb(13 20 27)}", {
|
|
107
|
+
status: 200,
|
|
108
|
+
headers: { "content-type": "text/css" },
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
let routeHandler:
|
|
113
|
+
| ((route: {
|
|
114
|
+
request: () => {
|
|
115
|
+
url: () => string;
|
|
116
|
+
isNavigationRequest: () => boolean;
|
|
117
|
+
resourceType: () => string;
|
|
118
|
+
method: () => string;
|
|
119
|
+
headers: () => Record<string, string>;
|
|
120
|
+
};
|
|
121
|
+
continue: () => Promise<void>;
|
|
122
|
+
abort: () => Promise<void>;
|
|
123
|
+
fulfill: (options: {
|
|
124
|
+
status: number;
|
|
125
|
+
headers: Record<string, string>;
|
|
126
|
+
body: Uint8Array;
|
|
127
|
+
}) => Promise<void>;
|
|
128
|
+
}) => Promise<void>)
|
|
129
|
+
| undefined;
|
|
130
|
+
const fulfilled: Array<{
|
|
131
|
+
status: number;
|
|
132
|
+
headers: Record<string, string>;
|
|
133
|
+
body: Uint8Array;
|
|
134
|
+
}> = [];
|
|
135
|
+
|
|
136
|
+
const invokeRoute = async (
|
|
137
|
+
url: string,
|
|
138
|
+
navigation: boolean,
|
|
139
|
+
type: string,
|
|
140
|
+
) => {
|
|
141
|
+
if (!routeHandler) throw new Error("route handler was not installed");
|
|
142
|
+
await routeHandler({
|
|
143
|
+
request: () => ({
|
|
144
|
+
url: () => url,
|
|
145
|
+
isNavigationRequest: () => navigation,
|
|
146
|
+
resourceType: () => type,
|
|
147
|
+
method: () => "GET",
|
|
148
|
+
headers: () => ({
|
|
149
|
+
accept: "text/html",
|
|
150
|
+
cookie: "session=must-not-forward",
|
|
151
|
+
"user-agent": "fixture-browser",
|
|
152
|
+
}),
|
|
153
|
+
}),
|
|
154
|
+
continue: async () => undefined,
|
|
155
|
+
abort: async () => undefined,
|
|
156
|
+
fulfill: async (options) => {
|
|
157
|
+
fulfilled.push(options);
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let evaluateCalls = 0;
|
|
163
|
+
const page = {
|
|
164
|
+
async route(_pattern: string, handler: (route: never) => Promise<void>) {
|
|
165
|
+
routeHandler = handler as typeof routeHandler;
|
|
166
|
+
},
|
|
167
|
+
async goto() {
|
|
168
|
+
await invokeRoute("https://example.com/", true, "document");
|
|
169
|
+
await invokeRoute(
|
|
170
|
+
"https://example.com/styles.css",
|
|
171
|
+
false,
|
|
172
|
+
"stylesheet",
|
|
173
|
+
);
|
|
174
|
+
},
|
|
175
|
+
async waitForLoadState() {},
|
|
176
|
+
async title() {
|
|
177
|
+
return "Proxy example";
|
|
178
|
+
},
|
|
179
|
+
url() {
|
|
180
|
+
return "https://example.com/";
|
|
181
|
+
},
|
|
182
|
+
locator() {
|
|
183
|
+
return { innerText: async () => "Hydrated CTA" };
|
|
184
|
+
},
|
|
185
|
+
async setViewportSize() {},
|
|
186
|
+
async screenshot() {
|
|
187
|
+
return new Uint8Array([1]);
|
|
188
|
+
},
|
|
189
|
+
async evaluate<T>() {
|
|
190
|
+
evaluateCalls += 1;
|
|
191
|
+
if (evaluateCalls === 3) {
|
|
192
|
+
return {
|
|
193
|
+
title: "Proxy example",
|
|
194
|
+
text: "Hydrated CTA",
|
|
195
|
+
assets: [],
|
|
196
|
+
internalLinks: [],
|
|
197
|
+
designTokens: {
|
|
198
|
+
colors: ["rgb(13 20 27)"],
|
|
199
|
+
typography: [],
|
|
200
|
+
spacing: [],
|
|
201
|
+
radii: [],
|
|
202
|
+
cssVariables: {},
|
|
203
|
+
},
|
|
204
|
+
} as T;
|
|
205
|
+
}
|
|
206
|
+
return undefined as T;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const context = {
|
|
210
|
+
async newPage() {
|
|
211
|
+
return page;
|
|
212
|
+
},
|
|
213
|
+
async close() {},
|
|
214
|
+
};
|
|
215
|
+
const browser = {
|
|
216
|
+
contexts: () => [],
|
|
217
|
+
async newContext() {
|
|
218
|
+
return context;
|
|
219
|
+
},
|
|
220
|
+
async close() {},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const result = await renderWithPlaywright(
|
|
224
|
+
{
|
|
225
|
+
chromium: {
|
|
226
|
+
async launch() {
|
|
227
|
+
return browser;
|
|
228
|
+
},
|
|
229
|
+
async connectOverCDP() {
|
|
230
|
+
return browser;
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
} as never,
|
|
234
|
+
{ url: "https://example.com/", timeoutMs: 5_000 },
|
|
235
|
+
[],
|
|
236
|
+
"local-playwright",
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
expect(result).toMatchObject({
|
|
240
|
+
rendered: true,
|
|
241
|
+
method: "local-playwright",
|
|
242
|
+
text: "Hydrated CTA",
|
|
243
|
+
});
|
|
244
|
+
expect(fulfilled).toHaveLength(2);
|
|
245
|
+
expect(new TextDecoder().decode(fulfilled[0].body)).toContain(
|
|
246
|
+
"Proxy example",
|
|
247
|
+
);
|
|
248
|
+
expect(ssrfSafeFetch).toHaveBeenCalledTimes(2);
|
|
249
|
+
expect(ssrfSafeFetch.mock.calls[0][1].headers).toEqual({
|
|
250
|
+
accept: "text/html",
|
|
251
|
+
"user-agent": "fixture-browser",
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("reserves browser resource slots before overlapping proxy fetches", async () => {
|
|
256
|
+
let releaseFetch!: () => void;
|
|
257
|
+
const fetchGate = new Promise<void>((resolve) => {
|
|
258
|
+
releaseFetch = resolve;
|
|
259
|
+
});
|
|
260
|
+
let fetchCount = 0;
|
|
261
|
+
ssrfSafeFetch.mockImplementation(async () => {
|
|
262
|
+
fetchCount += 1;
|
|
263
|
+
await fetchGate;
|
|
264
|
+
return new Response("resource", { status: 200 });
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
let routeHandler:
|
|
268
|
+
| ((route: {
|
|
269
|
+
request: () => {
|
|
270
|
+
url: () => string;
|
|
271
|
+
isNavigationRequest: () => boolean;
|
|
272
|
+
resourceType: () => string;
|
|
273
|
+
method: () => string;
|
|
274
|
+
headers: () => Record<string, string>;
|
|
275
|
+
};
|
|
276
|
+
continue: () => Promise<void>;
|
|
277
|
+
abort: (reason?: string) => Promise<void>;
|
|
278
|
+
fulfill: (options: {
|
|
279
|
+
status: number;
|
|
280
|
+
headers: Record<string, string>;
|
|
281
|
+
body: Uint8Array;
|
|
282
|
+
}) => Promise<void>;
|
|
283
|
+
}) => Promise<void>)
|
|
284
|
+
| undefined;
|
|
285
|
+
let abortCount = 0;
|
|
286
|
+
|
|
287
|
+
const page = {
|
|
288
|
+
async route(_pattern: string, handler: (route: never) => Promise<void>) {
|
|
289
|
+
routeHandler = handler as typeof routeHandler;
|
|
290
|
+
},
|
|
291
|
+
async goto() {
|
|
292
|
+
if (!routeHandler) throw new Error("route handler was not installed");
|
|
293
|
+
const requests = Array.from({ length: 401 }, (_, index) =>
|
|
294
|
+
routeHandler!({
|
|
295
|
+
request: () => ({
|
|
296
|
+
url: () => `https://example.com/resource-${index}`,
|
|
297
|
+
isNavigationRequest: () => index === 0,
|
|
298
|
+
resourceType: () => "script",
|
|
299
|
+
method: () => "GET",
|
|
300
|
+
headers: () => ({}),
|
|
301
|
+
}),
|
|
302
|
+
continue: async () => undefined,
|
|
303
|
+
abort: async () => {
|
|
304
|
+
abortCount += 1;
|
|
305
|
+
},
|
|
306
|
+
fulfill: async () => undefined,
|
|
307
|
+
}),
|
|
308
|
+
);
|
|
309
|
+
while (fetchCount < 400) await Promise.resolve();
|
|
310
|
+
releaseFetch();
|
|
311
|
+
await Promise.all(requests);
|
|
312
|
+
},
|
|
313
|
+
async waitForLoadState() {},
|
|
314
|
+
async title() {
|
|
315
|
+
return "Example";
|
|
316
|
+
},
|
|
317
|
+
url() {
|
|
318
|
+
return "https://example.com/";
|
|
319
|
+
},
|
|
320
|
+
locator() {
|
|
321
|
+
return { innerText: async () => "Example" };
|
|
322
|
+
},
|
|
323
|
+
async setViewportSize() {},
|
|
324
|
+
async screenshot() {
|
|
325
|
+
return new Uint8Array([1]);
|
|
326
|
+
},
|
|
327
|
+
async evaluate<T>() {
|
|
328
|
+
return {
|
|
329
|
+
title: "Example",
|
|
330
|
+
text: "Example",
|
|
331
|
+
assets: [],
|
|
332
|
+
internalLinks: [],
|
|
333
|
+
designTokens: {
|
|
334
|
+
colors: [],
|
|
335
|
+
typography: [],
|
|
336
|
+
spacing: [],
|
|
337
|
+
radii: [],
|
|
338
|
+
cssVariables: {},
|
|
339
|
+
},
|
|
340
|
+
} as T;
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
const context = {
|
|
344
|
+
pages: () => [],
|
|
345
|
+
async newPage() {
|
|
346
|
+
return page;
|
|
347
|
+
},
|
|
348
|
+
async close() {},
|
|
349
|
+
};
|
|
350
|
+
const browser = {
|
|
351
|
+
contexts: () => [],
|
|
352
|
+
async newContext() {
|
|
353
|
+
return context;
|
|
354
|
+
},
|
|
355
|
+
async close() {},
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const result = await renderWithPlaywright(
|
|
359
|
+
{
|
|
360
|
+
chromium: {
|
|
361
|
+
async launch() {
|
|
362
|
+
return browser;
|
|
363
|
+
},
|
|
364
|
+
async connectOverCDP() {
|
|
365
|
+
return browser;
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
} as never,
|
|
369
|
+
{ url: "https://example.com/", timeoutMs: 5_000 },
|
|
370
|
+
[],
|
|
371
|
+
"local-playwright",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(fetchCount).toBe(400);
|
|
375
|
+
expect(abortCount).toBe(1);
|
|
376
|
+
expect(result.warnings).toContain(
|
|
377
|
+
"Browser resource budget reached (400 requests).",
|
|
378
|
+
);
|
|
379
|
+
});
|
|
94
380
|
});
|