@lunora/browser 1.0.0-alpha.6 → 1.0.0-alpha.8
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/README.md
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* It is intentionally opaque: callers never touch the binding directly, they
|
|
10
10
|
* hand it to {@link LunoraBrowserOptions.binding} and the Playwright layer
|
|
11
|
-
* consumes it.
|
|
12
|
-
*
|
|
11
|
+
* consumes it. `fetch` is REQUIRED (the real binding is a `Fetcher`, so it
|
|
12
|
+
* always has one) so the marker actually excludes an arbitrary value like `{}` —
|
|
13
|
+
* a bare object fails to type-check where a binding is required, catching the
|
|
14
|
+
* misuse at the call site instead of deferring to an opaque launch error.
|
|
15
|
+
* @experimental
|
|
13
16
|
*/
|
|
14
17
|
interface BrowserBindingLike {
|
|
15
|
-
readonly fetch
|
|
18
|
+
readonly fetch: (...args: never[]) => unknown;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
21
|
* Minimal projection of a Playwright `Route` (the argument the `page.route`
|
|
@@ -35,6 +38,7 @@ interface RouteLike {
|
|
|
35
38
|
* Minimal projection of a Playwright `Page` — just the methods the helpers drive.
|
|
36
39
|
* Declared structurally so a test can inject a plain stub instead of a real
|
|
37
40
|
* headless page (which needs workerd + the Browser Rendering binding).
|
|
41
|
+
* @experimental
|
|
38
42
|
*/
|
|
39
43
|
interface PageLike {
|
|
40
44
|
/** Return the page's serialized HTML after the navigation settles. */
|
|
@@ -66,6 +70,7 @@ interface PageLike {
|
|
|
66
70
|
/**
|
|
67
71
|
* Minimal projection of a Playwright `BrowserContext`. Only `newPage` is used;
|
|
68
72
|
* declared structurally for the same test-double reason as {@link PageLike}.
|
|
73
|
+
* @experimental
|
|
69
74
|
*/
|
|
70
75
|
interface BrowserContextLike {
|
|
71
76
|
newPage: () => Promise<PageLike>;
|
|
@@ -74,6 +79,7 @@ interface BrowserContextLike {
|
|
|
74
79
|
* Minimal projection of a Playwright `Browser` (the value `launch` resolves to).
|
|
75
80
|
* Only `newContext`/`close` are used; declared structurally for the same
|
|
76
81
|
* test-double reason as {@link PageLike}.
|
|
82
|
+
* @experimental
|
|
77
83
|
*/
|
|
78
84
|
interface BrowserLike {
|
|
79
85
|
close: () => Promise<void>;
|
|
@@ -86,9 +92,13 @@ interface BrowserLike {
|
|
|
86
92
|
* `@cloudflare/playwright` at module top — that keeps the heavy optional peer
|
|
87
93
|
* dep out of the bundle for apps that never screenshot, and lets tests pass a
|
|
88
94
|
* fake. Calling it with the Browser Rendering binding resolves a {@link BrowserLike}.
|
|
95
|
+
* @experimental
|
|
89
96
|
*/
|
|
90
97
|
type BrowserLaunchLike = (binding: BrowserBindingLike, options?: Record<string, unknown>) => Promise<BrowserLike>;
|
|
91
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Options shared by the page-driving helpers ({@link Browser.screenshot} etc.).
|
|
100
|
+
* @experimental
|
|
101
|
+
*/
|
|
92
102
|
interface NavigateOptions {
|
|
93
103
|
/**
|
|
94
104
|
* Hard timeout in milliseconds for the navigation + operation. Clamped to a
|
|
@@ -102,7 +112,10 @@ interface NavigateOptions {
|
|
|
102
112
|
*/
|
|
103
113
|
waitUntil?: "commit" | "domcontentloaded" | "load" | "networkidle";
|
|
104
114
|
}
|
|
105
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Options for {@link Browser.screenshot}.
|
|
117
|
+
* @experimental
|
|
118
|
+
*/
|
|
106
119
|
interface ScreenshotOptions extends NavigateOptions {
|
|
107
120
|
/** Capture the full scrollable page rather than just the viewport. */
|
|
108
121
|
fullPage?: boolean;
|
|
@@ -117,7 +130,10 @@ interface ScreenshotOptions extends NavigateOptions {
|
|
|
117
130
|
width: number;
|
|
118
131
|
};
|
|
119
132
|
}
|
|
120
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Options for {@link Browser.pdf}.
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
121
137
|
interface PdfOptions extends NavigateOptions {
|
|
122
138
|
/** Paper format (`A4`, `Letter`, …) forwarded to Playwright. */
|
|
123
139
|
format?: string;
|
|
@@ -132,6 +148,10 @@ interface PdfOptions extends NavigateOptions {
|
|
|
132
148
|
width: number;
|
|
133
149
|
};
|
|
134
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* `LunoraBrowserOptions` is part of the experimental `@lunora/browser` API and may change without a major version bump.
|
|
153
|
+
* @experimental
|
|
154
|
+
*/
|
|
135
155
|
interface LunoraBrowserOptions {
|
|
136
156
|
/**
|
|
137
157
|
* Strict host allowlist. When set (non-empty), a navigation URL is refused
|
|
@@ -191,6 +211,7 @@ interface LunoraBrowserOptions {
|
|
|
191
211
|
* browser, opens a context + page, navigates, performs the op, and always
|
|
192
212
|
* closes the browser in a `finally` (a leaked session is billed and
|
|
193
213
|
* rate-limited).
|
|
214
|
+
* @experimental
|
|
194
215
|
*/
|
|
195
216
|
interface Browser {
|
|
196
217
|
/** Serialized HTML of `url` after navigation settles. */
|
|
@@ -213,5 +234,9 @@ interface Browser {
|
|
|
213
234
|
/** Render `url` to an image buffer (PNG by default). */
|
|
214
235
|
screenshot: (url: string, options?: ScreenshotOptions) => Promise<Uint8Array>;
|
|
215
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* `createBrowser` is part of the experimental `@lunora/browser` API and may change without a major version bump.
|
|
239
|
+
* @experimental
|
|
240
|
+
*/
|
|
216
241
|
declare const createBrowser: (options: LunoraBrowserOptions) => Browser;
|
|
217
242
|
export { type Browser, type BrowserBindingLike, type BrowserContextLike, type BrowserLaunchLike, type BrowserLike, type LunoraBrowserOptions, type NavigateOptions, type PageLike, type PdfOptions, type ScreenshotOptions, createBrowser };
|
package/dist/index.d.ts
CHANGED
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
*
|
|
9
9
|
* It is intentionally opaque: callers never touch the binding directly, they
|
|
10
10
|
* hand it to {@link LunoraBrowserOptions.binding} and the Playwright layer
|
|
11
|
-
* consumes it.
|
|
12
|
-
*
|
|
11
|
+
* consumes it. `fetch` is REQUIRED (the real binding is a `Fetcher`, so it
|
|
12
|
+
* always has one) so the marker actually excludes an arbitrary value like `{}` —
|
|
13
|
+
* a bare object fails to type-check where a binding is required, catching the
|
|
14
|
+
* misuse at the call site instead of deferring to an opaque launch error.
|
|
15
|
+
* @experimental
|
|
13
16
|
*/
|
|
14
17
|
interface BrowserBindingLike {
|
|
15
|
-
readonly fetch
|
|
18
|
+
readonly fetch: (...args: never[]) => unknown;
|
|
16
19
|
}
|
|
17
20
|
/**
|
|
18
21
|
* Minimal projection of a Playwright `Route` (the argument the `page.route`
|
|
@@ -35,6 +38,7 @@ interface RouteLike {
|
|
|
35
38
|
* Minimal projection of a Playwright `Page` — just the methods the helpers drive.
|
|
36
39
|
* Declared structurally so a test can inject a plain stub instead of a real
|
|
37
40
|
* headless page (which needs workerd + the Browser Rendering binding).
|
|
41
|
+
* @experimental
|
|
38
42
|
*/
|
|
39
43
|
interface PageLike {
|
|
40
44
|
/** Return the page's serialized HTML after the navigation settles. */
|
|
@@ -66,6 +70,7 @@ interface PageLike {
|
|
|
66
70
|
/**
|
|
67
71
|
* Minimal projection of a Playwright `BrowserContext`. Only `newPage` is used;
|
|
68
72
|
* declared structurally for the same test-double reason as {@link PageLike}.
|
|
73
|
+
* @experimental
|
|
69
74
|
*/
|
|
70
75
|
interface BrowserContextLike {
|
|
71
76
|
newPage: () => Promise<PageLike>;
|
|
@@ -74,6 +79,7 @@ interface BrowserContextLike {
|
|
|
74
79
|
* Minimal projection of a Playwright `Browser` (the value `launch` resolves to).
|
|
75
80
|
* Only `newContext`/`close` are used; declared structurally for the same
|
|
76
81
|
* test-double reason as {@link PageLike}.
|
|
82
|
+
* @experimental
|
|
77
83
|
*/
|
|
78
84
|
interface BrowserLike {
|
|
79
85
|
close: () => Promise<void>;
|
|
@@ -86,9 +92,13 @@ interface BrowserLike {
|
|
|
86
92
|
* `@cloudflare/playwright` at module top — that keeps the heavy optional peer
|
|
87
93
|
* dep out of the bundle for apps that never screenshot, and lets tests pass a
|
|
88
94
|
* fake. Calling it with the Browser Rendering binding resolves a {@link BrowserLike}.
|
|
95
|
+
* @experimental
|
|
89
96
|
*/
|
|
90
97
|
type BrowserLaunchLike = (binding: BrowserBindingLike, options?: Record<string, unknown>) => Promise<BrowserLike>;
|
|
91
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Options shared by the page-driving helpers ({@link Browser.screenshot} etc.).
|
|
100
|
+
* @experimental
|
|
101
|
+
*/
|
|
92
102
|
interface NavigateOptions {
|
|
93
103
|
/**
|
|
94
104
|
* Hard timeout in milliseconds for the navigation + operation. Clamped to a
|
|
@@ -102,7 +112,10 @@ interface NavigateOptions {
|
|
|
102
112
|
*/
|
|
103
113
|
waitUntil?: "commit" | "domcontentloaded" | "load" | "networkidle";
|
|
104
114
|
}
|
|
105
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Options for {@link Browser.screenshot}.
|
|
117
|
+
* @experimental
|
|
118
|
+
*/
|
|
106
119
|
interface ScreenshotOptions extends NavigateOptions {
|
|
107
120
|
/** Capture the full scrollable page rather than just the viewport. */
|
|
108
121
|
fullPage?: boolean;
|
|
@@ -117,7 +130,10 @@ interface ScreenshotOptions extends NavigateOptions {
|
|
|
117
130
|
width: number;
|
|
118
131
|
};
|
|
119
132
|
}
|
|
120
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Options for {@link Browser.pdf}.
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
121
137
|
interface PdfOptions extends NavigateOptions {
|
|
122
138
|
/** Paper format (`A4`, `Letter`, …) forwarded to Playwright. */
|
|
123
139
|
format?: string;
|
|
@@ -132,6 +148,10 @@ interface PdfOptions extends NavigateOptions {
|
|
|
132
148
|
width: number;
|
|
133
149
|
};
|
|
134
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* `LunoraBrowserOptions` is part of the experimental `@lunora/browser` API and may change without a major version bump.
|
|
153
|
+
* @experimental
|
|
154
|
+
*/
|
|
135
155
|
interface LunoraBrowserOptions {
|
|
136
156
|
/**
|
|
137
157
|
* Strict host allowlist. When set (non-empty), a navigation URL is refused
|
|
@@ -191,6 +211,7 @@ interface LunoraBrowserOptions {
|
|
|
191
211
|
* browser, opens a context + page, navigates, performs the op, and always
|
|
192
212
|
* closes the browser in a `finally` (a leaked session is billed and
|
|
193
213
|
* rate-limited).
|
|
214
|
+
* @experimental
|
|
194
215
|
*/
|
|
195
216
|
interface Browser {
|
|
196
217
|
/** Serialized HTML of `url` after navigation settles. */
|
|
@@ -213,5 +234,9 @@ interface Browser {
|
|
|
213
234
|
/** Render `url` to an image buffer (PNG by default). */
|
|
214
235
|
screenshot: (url: string, options?: ScreenshotOptions) => Promise<Uint8Array>;
|
|
215
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* `createBrowser` is part of the experimental `@lunora/browser` API and may change without a major version bump.
|
|
239
|
+
* @experimental
|
|
240
|
+
*/
|
|
216
241
|
declare const createBrowser: (options: LunoraBrowserOptions) => Browser;
|
|
217
242
|
export { type Browser, type BrowserBindingLike, type BrowserContextLike, type BrowserLaunchLike, type BrowserLike, type LunoraBrowserOptions, type NavigateOptions, type PageLike, type PdfOptions, type ScreenshotOptions, createBrowser };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { createBrowser } from './packem_shared/createBrowser-
|
|
1
|
+
export { createBrowser } from './packem_shared/createBrowser-CZmfQ38r.mjs';
|
|
@@ -129,19 +129,19 @@ const isPrivateTarget = (parsed) => {
|
|
|
129
129
|
};
|
|
130
130
|
const validateUrl = (url, allowPrivateTargets, allowedHosts) => {
|
|
131
131
|
if (typeof url !== "string" || url.length === 0) {
|
|
132
|
-
throw new
|
|
132
|
+
throw new LunoraError("BAD_REQUEST", "@lunora/browser: url must be a non-empty string");
|
|
133
133
|
}
|
|
134
134
|
let parsed;
|
|
135
135
|
try {
|
|
136
136
|
parsed = new URL(url);
|
|
137
137
|
} catch {
|
|
138
|
-
throw new
|
|
138
|
+
throw new LunoraError("BAD_REQUEST", `@lunora/browser: url must be an absolute http(s) URL (got "${url}")`);
|
|
139
139
|
}
|
|
140
140
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
141
|
-
throw new
|
|
141
|
+
throw new LunoraError("BAD_REQUEST", `@lunora/browser: url protocol must be http(s) (got "${parsed.protocol}")`);
|
|
142
142
|
}
|
|
143
143
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
144
|
-
throw new LunoraError("
|
|
144
|
+
throw new LunoraError("BAD_REQUEST", "@lunora/browser: url must not embed credentials (strip the `user:pass@` userinfo)");
|
|
145
145
|
}
|
|
146
146
|
if (allowedHosts && allowedHosts.length > 0) {
|
|
147
147
|
const host = normalizeHost(parsed.hostname);
|
|
@@ -151,7 +151,7 @@ const validateUrl = (url, allowPrivateTargets, allowedHosts) => {
|
|
|
151
151
|
}
|
|
152
152
|
if (!allowPrivateTargets && isPrivateTarget(parsed)) {
|
|
153
153
|
throw new LunoraError(
|
|
154
|
-
"
|
|
154
|
+
"FORBIDDEN",
|
|
155
155
|
`@lunora/browser: url host "${parsed.hostname}" is a private/internal address; pass createBrowser({ …, allowPrivateTargets: true }) to allow it`
|
|
156
156
|
);
|
|
157
157
|
}
|
|
@@ -174,6 +174,27 @@ const resolveTimeout = (callTimeout, factoryTimeout) => {
|
|
|
174
174
|
const safe = Number.isFinite(requested) ? requested : DEFAULT_TIMEOUT_MS;
|
|
175
175
|
return Math.min(Math.max(1, Math.floor(safe)), MAX_TIMEOUT_MS);
|
|
176
176
|
};
|
|
177
|
+
const withDeadline = async (operation, timeoutMs) => {
|
|
178
|
+
let timer;
|
|
179
|
+
try {
|
|
180
|
+
return await Promise.race([
|
|
181
|
+
operation(),
|
|
182
|
+
new Promise((_resolve, reject) => {
|
|
183
|
+
timer = setTimeout(() => {
|
|
184
|
+
reject(
|
|
185
|
+
new LunoraError("BROWSER_TIMEOUT", `@lunora/browser: navigation + operation exceeded the ${String(timeoutMs)}ms timeout budget`, {
|
|
186
|
+
status: 504
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
}, timeoutMs);
|
|
190
|
+
})
|
|
191
|
+
]);
|
|
192
|
+
} finally {
|
|
193
|
+
if (timer !== void 0) {
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
177
198
|
const createBrowser = (options) => {
|
|
178
199
|
if (!options.binding) {
|
|
179
200
|
throw new TypeError("@lunora/browser: `binding` is required (env.BROWSER)");
|
|
@@ -209,18 +230,40 @@ const createBrowser = (options) => {
|
|
|
209
230
|
}
|
|
210
231
|
const assertNavigationAllowed = async (requestUrl) => {
|
|
211
232
|
validateUrl(requestUrl, allowPrivateTargets, options.allowedHosts);
|
|
212
|
-
if (resolveDns) {
|
|
233
|
+
if (!allowPrivateTargets && resolveDns) {
|
|
213
234
|
await assertResolvedHostIsPublic(requestUrl, dohTimeout);
|
|
214
235
|
}
|
|
215
236
|
};
|
|
237
|
+
const isBlockedSubresource = (rawUrl) => {
|
|
238
|
+
let parsed;
|
|
239
|
+
try {
|
|
240
|
+
parsed = new URL(rawUrl);
|
|
241
|
+
} catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (options.allowedHosts && options.allowedHosts.length > 0) {
|
|
248
|
+
const host = normalizeHost(parsed.hostname);
|
|
249
|
+
if (!options.allowedHosts.some((entry) => normalizeHost(entry) === host)) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return isPrivateTarget(parsed);
|
|
254
|
+
};
|
|
216
255
|
return withBrowser(async (browser) => {
|
|
217
256
|
const context = await browser.newContext();
|
|
218
257
|
const page = await context.newPage();
|
|
219
|
-
if (!allowPrivateTargets
|
|
258
|
+
if (page.route && (!allowPrivateTargets || (options.allowedHosts?.length ?? 0) > 0)) {
|
|
220
259
|
await page.route("**/*", async (route) => {
|
|
221
260
|
const request = route.request();
|
|
222
261
|
const isNavigation = request.isNavigationRequest?.() ?? true;
|
|
223
262
|
if (!isNavigation) {
|
|
263
|
+
if (isBlockedSubresource(request.url())) {
|
|
264
|
+
await route.abort("blockedbyclient");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
224
267
|
await route.continue();
|
|
225
268
|
return;
|
|
226
269
|
}
|
|
@@ -236,8 +279,10 @@ const createBrowser = (options) => {
|
|
|
236
279
|
if (viewport && page.setViewportSize) {
|
|
237
280
|
await page.setViewportSize(clampViewport(viewport));
|
|
238
281
|
}
|
|
239
|
-
|
|
240
|
-
|
|
282
|
+
return withDeadline(async () => {
|
|
283
|
+
await page.goto(target, { timeout, waitUntil: navigate.waitUntil ?? "load" });
|
|
284
|
+
return use(page);
|
|
285
|
+
}, timeout);
|
|
241
286
|
});
|
|
242
287
|
};
|
|
243
288
|
const screenshot = async (url, screenshotOptions = {}) => withPage(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/browser",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.8",
|
|
4
4
|
"description": "Cloudflare Browser Rendering for Lunora: ctx.browser screenshots, PDF, and scraping in actions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"browser-rendering",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
49
|
+
"@lunora/errors": "1.0.0-alpha.5"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"@cloudflare/playwright": ">=1.0.0"
|