@lunora/browser 1.0.0-alpha.13 → 1.0.0-alpha.14

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/index.d.mts CHANGED
@@ -84,6 +84,17 @@ interface BrowserContextLike {
84
84
  interface BrowserLike {
85
85
  close: () => Promise<void>;
86
86
  newContext: () => Promise<BrowserContextLike>;
87
+ /**
88
+ * The Browser Rendering session this browser is attached to, when the
89
+ * runtime exposes it.
90
+ *
91
+ * Optional because this is a structural projection, not a re-declaration of
92
+ * the upstream Playwright type — but without it there is no way to learn
93
+ * the id of a session you just held open with `launch(fn, { keepAlive })`,
94
+ * which makes {@link Browser.connect} unreachable except by guessing from
95
+ * {@link Browser.sessions}.
96
+ */
97
+ sessionId?: () => string | undefined;
87
98
  }
88
99
  /**
89
100
  * Structural projection of `@cloudflare/playwright`'s `launch` export
@@ -95,6 +106,30 @@ interface BrowserLike {
95
106
  * @experimental
96
107
  */
97
108
  type BrowserLaunchLike = (binding: BrowserBindingLike, options?: Record<string, unknown>) => Promise<BrowserLike>;
109
+ /**
110
+ * One live Browser Rendering session, as `@cloudflare/playwright`'s `sessions()`
111
+ * reports it. `connectionId` is set while another worker holds the session — you
112
+ * can only {@link Browser.connect} to a free one.
113
+ * @experimental
114
+ */
115
+ interface BrowserSession {
116
+ connectionId?: string;
117
+ sessionId: string;
118
+ startTime?: number;
119
+ }
120
+ /**
121
+ * Structural projection of `@cloudflare/playwright`'s `connect` export —
122
+ * re-attaches to an existing session rather than starting a new browser.
123
+ * Injected like {@link BrowserLaunchLike} so the peer dep stays optional.
124
+ * @experimental
125
+ */
126
+ type BrowserConnectLike = (binding: BrowserBindingLike, sessionId: string) => Promise<BrowserLike>;
127
+ /**
128
+ * Structural projection of `@cloudflare/playwright`'s `sessions` export — lists
129
+ * the account's live Browser Rendering sessions for this binding.
130
+ * @experimental
131
+ */
132
+ type BrowserSessionsLike = (binding: BrowserBindingLike) => Promise<ReadonlyArray<BrowserSession>>;
98
133
  /**
99
134
  * Options shared by the page-driving helpers ({@link Browser.screenshot} etc.).
100
135
  * @experimental
@@ -176,6 +211,11 @@ interface LunoraBrowserOptions {
176
211
  allowPrivateTargets?: boolean;
177
212
  /** The Cloudflare Browser Rendering binding (`env.BROWSER`). Required. */
178
213
  binding: BrowserBindingLike;
214
+ /**
215
+ * The `@cloudflare/playwright` `connect` function, injected like
216
+ * {@link LunoraBrowserOptions.launch}. Required for {@link Browser.connect}.
217
+ */
218
+ connect?: BrowserConnectLike;
179
219
  /**
180
220
  * The `@cloudflare/playwright` `launch` function. Injected rather than
181
221
  * imported at module top so the optional peer dep stays out of the bundle
@@ -196,6 +236,11 @@ interface LunoraBrowserOptions {
196
236
  * For a hard guarantee prefer {@link LunoraBrowserOptions.allowedHosts}.
197
237
  */
198
238
  resolveDns?: boolean;
239
+ /**
240
+ * The `@cloudflare/playwright` `sessions` function, injected like
241
+ * {@link LunoraBrowserOptions.launch}. Required for {@link Browser.sessions}.
242
+ */
243
+ sessions?: BrowserSessionsLike;
199
244
  /**
200
245
  * Default navigation timeout (ms) applied when a per-call `timeoutMs` is not
201
246
  * given. Clamped to the factory's `MAX_TIMEOUT_MS`. Default 30000.
@@ -214,15 +259,42 @@ interface LunoraBrowserOptions {
214
259
  * @experimental
215
260
  */
216
261
  interface Browser {
262
+ /**
263
+ * Re-attach to an existing session and hand the browser to `fn`.
264
+ *
265
+ * Get the id either by reading it inside the call that opened the session
266
+ * (`launch(async (browser) => browser.sessionId?.(), { keepAlive: 600 })`)
267
+ * and persisting it, or by picking a free one out of
268
+ * {@link Browser.sessions} — an entry with a `connectionId` is already held
269
+ * by another worker.
270
+ *
271
+ * The session is deliberately **left open** afterwards — closing it is the
272
+ * whole thing you are avoiding. Close it when the flow is done by passing
273
+ * `close: true`, or let `keepAlive` lapse.
274
+ *
275
+ * This is what makes agent-style browsing possible: a model calls
276
+ * `navigate`, then `click`, then `extract` as three separate action
277
+ * invocations, and the page has to survive between them. With only the
278
+ * per-call lifecycle each step got a fresh browser, so `click` ran against
279
+ * a blank page — silently, which is the worst shape for that bug.
280
+ */
281
+ connect: <T>(sessionId: string, function_: (browser: BrowserLike) => Promise<T>, options?: {
282
+ close?: boolean;
283
+ }) => Promise<T>;
217
284
  /** Serialized HTML of `url` after navigation settles. */
218
285
  content: (url: string, options?: NavigateOptions) => Promise<string>;
219
286
  /**
220
287
  * Low-level escape hatch: launch a raw Playwright `Browser` and hand it to
221
- * `fn` (e.g. for multi-page flows or APIs not surfaced here). The browser is
222
- * **always closed** when `fn` resolves or throws — do not retain references
223
- * to it past the callback.
288
+ * `fn` (e.g. for multi-page flows or APIs not surfaced here).
289
+ *
290
+ * The browser is **always closed** when `fn` resolves or throws — unless
291
+ * `keepAlive` is set, which holds the session open for that many seconds so
292
+ * a later {@link Browser.connect} can re-attach. Do not retain references to
293
+ * the browser past the callback either way.
224
294
  */
225
- launch: <T>(function_: (browser: BrowserLike) => Promise<T>) => Promise<T>;
295
+ launch: <T>(function_: (browser: BrowserLike) => Promise<T>, options?: {
296
+ keepAlive?: number;
297
+ }) => Promise<T>;
226
298
  /** Render `url` to a PDF buffer. */
227
299
  pdf: (url: string, options?: PdfOptions) => Promise<Uint8Array>;
228
300
  /**
@@ -233,6 +305,12 @@ interface Browser {
233
305
  scrape: <T>(url: string, function_: (...args: never[]) => T, options?: NavigateOptions) => Promise<T>;
234
306
  /** Render `url` to an image buffer (PNG by default). */
235
307
  screenshot: (url: string, options?: ScreenshotOptions) => Promise<Uint8Array>;
308
+ /**
309
+ * List the live Browser Rendering sessions for this binding, so a caller can
310
+ * pick a free one to {@link Browser.connect} to. An entry with a
311
+ * `connectionId` is already held by another worker.
312
+ */
313
+ sessions: () => Promise<ReadonlyArray<BrowserSession>>;
236
314
  }
237
315
  /**
238
316
  * `createBrowser` is part of the experimental `@lunora/browser` API and may change without a major version bump.
package/dist/index.d.ts CHANGED
@@ -84,6 +84,17 @@ interface BrowserContextLike {
84
84
  interface BrowserLike {
85
85
  close: () => Promise<void>;
86
86
  newContext: () => Promise<BrowserContextLike>;
87
+ /**
88
+ * The Browser Rendering session this browser is attached to, when the
89
+ * runtime exposes it.
90
+ *
91
+ * Optional because this is a structural projection, not a re-declaration of
92
+ * the upstream Playwright type — but without it there is no way to learn
93
+ * the id of a session you just held open with `launch(fn, { keepAlive })`,
94
+ * which makes {@link Browser.connect} unreachable except by guessing from
95
+ * {@link Browser.sessions}.
96
+ */
97
+ sessionId?: () => string | undefined;
87
98
  }
88
99
  /**
89
100
  * Structural projection of `@cloudflare/playwright`'s `launch` export
@@ -95,6 +106,30 @@ interface BrowserLike {
95
106
  * @experimental
96
107
  */
97
108
  type BrowserLaunchLike = (binding: BrowserBindingLike, options?: Record<string, unknown>) => Promise<BrowserLike>;
109
+ /**
110
+ * One live Browser Rendering session, as `@cloudflare/playwright`'s `sessions()`
111
+ * reports it. `connectionId` is set while another worker holds the session — you
112
+ * can only {@link Browser.connect} to a free one.
113
+ * @experimental
114
+ */
115
+ interface BrowserSession {
116
+ connectionId?: string;
117
+ sessionId: string;
118
+ startTime?: number;
119
+ }
120
+ /**
121
+ * Structural projection of `@cloudflare/playwright`'s `connect` export —
122
+ * re-attaches to an existing session rather than starting a new browser.
123
+ * Injected like {@link BrowserLaunchLike} so the peer dep stays optional.
124
+ * @experimental
125
+ */
126
+ type BrowserConnectLike = (binding: BrowserBindingLike, sessionId: string) => Promise<BrowserLike>;
127
+ /**
128
+ * Structural projection of `@cloudflare/playwright`'s `sessions` export — lists
129
+ * the account's live Browser Rendering sessions for this binding.
130
+ * @experimental
131
+ */
132
+ type BrowserSessionsLike = (binding: BrowserBindingLike) => Promise<ReadonlyArray<BrowserSession>>;
98
133
  /**
99
134
  * Options shared by the page-driving helpers ({@link Browser.screenshot} etc.).
100
135
  * @experimental
@@ -176,6 +211,11 @@ interface LunoraBrowserOptions {
176
211
  allowPrivateTargets?: boolean;
177
212
  /** The Cloudflare Browser Rendering binding (`env.BROWSER`). Required. */
178
213
  binding: BrowserBindingLike;
214
+ /**
215
+ * The `@cloudflare/playwright` `connect` function, injected like
216
+ * {@link LunoraBrowserOptions.launch}. Required for {@link Browser.connect}.
217
+ */
218
+ connect?: BrowserConnectLike;
179
219
  /**
180
220
  * The `@cloudflare/playwright` `launch` function. Injected rather than
181
221
  * imported at module top so the optional peer dep stays out of the bundle
@@ -196,6 +236,11 @@ interface LunoraBrowserOptions {
196
236
  * For a hard guarantee prefer {@link LunoraBrowserOptions.allowedHosts}.
197
237
  */
198
238
  resolveDns?: boolean;
239
+ /**
240
+ * The `@cloudflare/playwright` `sessions` function, injected like
241
+ * {@link LunoraBrowserOptions.launch}. Required for {@link Browser.sessions}.
242
+ */
243
+ sessions?: BrowserSessionsLike;
199
244
  /**
200
245
  * Default navigation timeout (ms) applied when a per-call `timeoutMs` is not
201
246
  * given. Clamped to the factory's `MAX_TIMEOUT_MS`. Default 30000.
@@ -214,15 +259,42 @@ interface LunoraBrowserOptions {
214
259
  * @experimental
215
260
  */
216
261
  interface Browser {
262
+ /**
263
+ * Re-attach to an existing session and hand the browser to `fn`.
264
+ *
265
+ * Get the id either by reading it inside the call that opened the session
266
+ * (`launch(async (browser) => browser.sessionId?.(), { keepAlive: 600 })`)
267
+ * and persisting it, or by picking a free one out of
268
+ * {@link Browser.sessions} — an entry with a `connectionId` is already held
269
+ * by another worker.
270
+ *
271
+ * The session is deliberately **left open** afterwards — closing it is the
272
+ * whole thing you are avoiding. Close it when the flow is done by passing
273
+ * `close: true`, or let `keepAlive` lapse.
274
+ *
275
+ * This is what makes agent-style browsing possible: a model calls
276
+ * `navigate`, then `click`, then `extract` as three separate action
277
+ * invocations, and the page has to survive between them. With only the
278
+ * per-call lifecycle each step got a fresh browser, so `click` ran against
279
+ * a blank page — silently, which is the worst shape for that bug.
280
+ */
281
+ connect: <T>(sessionId: string, function_: (browser: BrowserLike) => Promise<T>, options?: {
282
+ close?: boolean;
283
+ }) => Promise<T>;
217
284
  /** Serialized HTML of `url` after navigation settles. */
218
285
  content: (url: string, options?: NavigateOptions) => Promise<string>;
219
286
  /**
220
287
  * Low-level escape hatch: launch a raw Playwright `Browser` and hand it to
221
- * `fn` (e.g. for multi-page flows or APIs not surfaced here). The browser is
222
- * **always closed** when `fn` resolves or throws — do not retain references
223
- * to it past the callback.
288
+ * `fn` (e.g. for multi-page flows or APIs not surfaced here).
289
+ *
290
+ * The browser is **always closed** when `fn` resolves or throws — unless
291
+ * `keepAlive` is set, which holds the session open for that many seconds so
292
+ * a later {@link Browser.connect} can re-attach. Do not retain references to
293
+ * the browser past the callback either way.
224
294
  */
225
- launch: <T>(function_: (browser: BrowserLike) => Promise<T>) => Promise<T>;
295
+ launch: <T>(function_: (browser: BrowserLike) => Promise<T>, options?: {
296
+ keepAlive?: number;
297
+ }) => Promise<T>;
226
298
  /** Render `url` to a PDF buffer. */
227
299
  pdf: (url: string, options?: PdfOptions) => Promise<Uint8Array>;
228
300
  /**
@@ -233,6 +305,12 @@ interface Browser {
233
305
  scrape: <T>(url: string, function_: (...args: never[]) => T, options?: NavigateOptions) => Promise<T>;
234
306
  /** Render `url` to an image buffer (PNG by default). */
235
307
  screenshot: (url: string, options?: ScreenshotOptions) => Promise<Uint8Array>;
308
+ /**
309
+ * List the live Browser Rendering sessions for this binding, so a caller can
310
+ * pick a free one to {@link Browser.connect} to. An entry with a
311
+ * `connectionId` is already held by another worker.
312
+ */
313
+ sessions: () => Promise<ReadonlyArray<BrowserSession>>;
236
314
  }
237
315
  /**
238
316
  * `createBrowser` is part of the experimental `@lunora/browser` API and may change without a major version bump.
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createBrowser as o}from"./packem_shared/createBrowser-7KtY5lSV.mjs";export{o as createBrowser};
1
+ import{createBrowser as o}from"./packem_shared/createBrowser-DrmNpKuh.mjs";export{o as createBrowser};
@@ -0,0 +1 @@
1
+ import{LunoraError as c}from"@lunora/errors";const k=/^\d{1,3}$/u,x=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,P=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,F=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,H=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,_=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,O=/^\[|\]$/gu,q=/\.$/u,m=t=>{const e=t.split(".");if(e.length!==4)return;const r=e.map(n=>k.test(n)?Number(n):-1);if(!r.some(n=>n<0||n>255))return[r[0],r[1],r[2],r[3]]},y=([t,e])=>t===0||t===10||t===127||t===100&&e>=64&&e<=127||t===169&&e===254||t===172&&e>=16&&e<=31||t===192&&e===168||t>=224,B=(t,e)=>{const r=Number.parseInt(t??"",16),n=Number.parseInt(e??"",16);return!Number.isFinite(r)||!Number.isFinite(n)?!0:y([Math.floor(r/256),r%256,Math.floor(n/256),n%256])},U=t=>{const e=t.toLowerCase(),r=x.exec(e);if(r)return B(r[1],r[2]);const n=P.exec(e);if(n){const a=m(n[1]??"");return a===void 0||y(a)}const i=F.exec(e);if(i){const a=m(i[1]??"");return a===void 0||y(a)}const o=H.exec(e);return o?B(o[1],o[2]):_.test(e)||e.startsWith("2002:")||e.startsWith("2001:0:")?!0:e==="::"||e==="::1"||e.startsWith("fc")||e.startsWith("fd")||e.startsWith("fe8")||e.startsWith("fe9")||e.startsWith("fea")||e.startsWith("feb")},C=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),d=t=>t.replaceAll(O,"").replace(q,"").toLowerCase(),A=t=>{const e=d(t);if(e.includes(":"))return U(e);const r=m(e);return r===void 0?C(e):y(r)},N=3e4,Q=12e4,V=3840,j=4320,z="https://cloudflare-dns.com/dns-query",v=5e3,b=1,T=28,G=(t,e)=>{if(e===b){const r=m(t);return r===void 0||y(r)}return U(t.toLowerCase())},W=async(t,e,r=v)=>{try{const n=await fetch(`${z}?name=${encodeURIComponent(t)}&type=${String(e)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(r)});return n.ok?(await n.json()).Answer??[]:void 0}catch{return}},S=async(t,e=v)=>{const r=d(new URL(t).hostname);if(r.includes(":")||m(r)!==void 0)return;const[n,i]=await Promise.all([W(r,b,e),W(r,T,e)]);if(!(n===void 0&&i===void 0)){for(const o of[...n??[],...i??[]])if((o.type===b||o.type===T)&&G(o.data,o.type))throw new c("FORBIDDEN",`@lunora/browser: url host "${r}" resolves to a private/internal address (${o.data}); refusing to navigate (DNS-rebinding guard)`)}},D=(t,e,r)=>{if(typeof t!="string"||t.length===0)throw new c("BAD_REQUEST","@lunora/browser: url must be a non-empty string");let n;try{n=new URL(t)}catch{throw new c("BAD_REQUEST",`@lunora/browser: url must be an absolute http(s) URL (got "${t}")`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new c("BAD_REQUEST",`@lunora/browser: url protocol must be http(s) (got "${n.protocol}")`);if(n.username!==""||n.password!=="")throw new c("BAD_REQUEST","@lunora/browser: url must not embed credentials (strip the `user:pass@` userinfo)");if(r&&r.length>0){const i=d(n.hostname);if(!r.some(o=>d(o)===i))throw new c("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is not in the configured allowedHosts allowlist`)}if(!e&&A(n.hostname))throw new c("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is a private/internal address; pass createBrowser({ …, allowPrivateTargets: true }) to allow it`);return n.toString()},M=(t,e)=>Number.isFinite(t)?Math.min(Math.max(1,Math.floor(t)),e):e,J=t=>({height:M(t.height,j),width:M(t.width,V)}),K=(t,e)=>{const r=t??e??N,n=Number.isFinite(r)?r:N;return Math.min(Math.max(1,Math.floor(n)),Q)},X=async(t,e)=>{let r;try{return await Promise.race([t(),new Promise((n,i)=>{r=setTimeout(()=>{i(new c("BROWSER_TIMEOUT",`@lunora/browser: navigation + operation exceeded the ${String(e)}ms timeout budget`,{status:504}))},e)})])}finally{r!==void 0&&clearTimeout(r)}},Z=t=>{if(!t.binding)throw new TypeError("@lunora/browser: `binding` is required (env.BROWSER)");const e=()=>{if(!t.launch)throw new c("INTERNAL",'@lunora/browser: `launch` is not available — install the `@cloudflare/playwright` peer dependency. The generated worker wires it for you; outside codegen pass it via createBrowser({ binding, launch }) (import { launch } from "@cloudflare/playwright").');return t.launch},r=(o,a)=>{if(!o)throw new c("INTERNAL",`@lunora/browser: \`${a}\` is not available — install the \`@cloudflare/playwright\` peer dependency. The generated worker wires it for you; outside codegen pass it via createBrowser({ binding, ${a} }).`);return o},n=async(o,a)=>{const s=await e()(t.binding,a===void 0?void 0:{keep_alive:a*1e3});if(a!==void 0)return await o(s);try{return await o(s)}finally{try{await s.close()}catch{}}},i=async(o,a,s,u)=>{const f=t.allowPrivateTargets??!1,$=D(o,f,t.allowedHosts),g=K(a.timeoutMs,t.timeoutMs),E=t.resolveDns??!1,R=Math.min(g,v);!f&&E&&await S($,R);const I=async h=>{D(h,f,t.allowedHosts),!f&&E&&await S(h,R)},L=h=>{let l;try{l=new URL(h)}catch{return!1}if(l.protocol!=="http:"&&l.protocol!=="https:")return!1;if(t.allowedHosts&&t.allowedHosts.length>0){const w=d(l.hostname);if(!t.allowedHosts.some(p=>d(p)===w))return!0}return A(l.hostname)};return n(async h=>{const l=await(await h.newContext()).newPage();return l.route&&(!f||(t.allowedHosts?.length??0)>0)&&await l.route("**/*",async w=>{const p=w.request();if(!(p.isNavigationRequest?.()??!0)){if(L(p.url())){await w.abort("blockedbyclient");return}await w.continue();return}try{await I(p.url())}catch{await w.abort("blockedbyclient");return}await w.continue()}),u&&l.setViewportSize&&await l.setViewportSize(J(u)),X(async()=>(await l.goto($,{timeout:g,waitUntil:a.waitUntil??"load"}),s(l)),g)})};return{connect:async(o,a,s={})=>{const u=await r(t.connect,"connect")(t.binding,o);if(s.close!==!0)return await a(u);try{return await a(u)}finally{try{await u.close()}catch{}}},content:async(o,a={})=>i(o,a,async s=>s.content()),launch:async(o,a={})=>n(o,a.keepAlive),pdf:async(o,a={})=>i(o,a,async s=>s.pdf({format:a.format,printBackground:a.printBackground??!1}),a.viewport),scrape:async(o,a,s={})=>i(o,s,async u=>u.evaluate(a)),screenshot:async(o,a={})=>i(o,a,async s=>s.screenshot({fullPage:a.fullPage??!1,type:a.type??"png"}),a.viewport),sessions:async()=>await r(t.sessions,"sessions")(t.binding)}};export{Z as createBrowser};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/browser",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.14",
4
4
  "description": "Cloudflare Browser Rendering for Lunora: ctx.browser screenshots, PDF, and scraping in actions",
5
5
  "keywords": [
6
6
  "browser-rendering",
@@ -1 +0,0 @@
1
- import{LunoraError as l}from"@lunora/errors";const L=/^\d{1,3}$/u,P=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,A=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,F=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,H=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,_=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,k=/^\[|\]$/gu,O=/\.$/u,f=t=>{const e=t.split(".");if(e.length!==4)return;const r=e.map(o=>L.test(o)?Number(o):-1);if(!r.some(o=>o<0||o>255))return[r[0],r[1],r[2],r[3]]},p=([t,e])=>t===0||t===10||t===127||t===100&&e>=64&&e<=127||t===169&&e===254||t===172&&e>=16&&e<=31||t===192&&e===168||t>=224,R=(t,e)=>{const r=Number.parseInt(t??"",16),o=Number.parseInt(e??"",16);return!Number.isFinite(r)||!Number.isFinite(o)?!0:p([Math.floor(r/256),r%256,Math.floor(o/256),o%256])},S=t=>{const e=t.toLowerCase(),r=P.exec(e);if(r)return R(r[1],r[2]);const o=A.exec(e);if(o){const s=f(o[1]??"");return s===void 0||p(s)}const n=F.exec(e);if(n){const s=f(n[1]??"");return s===void 0||p(s)}const a=H.exec(e);return a?R(a[1],a[2]):_.test(e)||e.startsWith("2002:")||e.startsWith("2001:0:")?!0:e==="::"||e==="::1"||e.startsWith("fc")||e.startsWith("fd")||e.startsWith("fe8")||e.startsWith("fe9")||e.startsWith("fea")||e.startsWith("feb")},q=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),h=t=>t.replaceAll(k,"").replace(O,"").toLowerCase(),U=t=>{const e=h(t);if(e.includes(":"))return S(e);const r=f(e);return r===void 0?q(e):p(r)},B=3e4,C=12e4,Q=3840,V=4320,j="https://cloudflare-dns.com/dns-query",b=5e3,g=1,W=28,z=(t,e)=>{if(e===g){const r=f(t);return r===void 0||p(r)}return S(t.toLowerCase())},N=async(t,e,r=b)=>{try{const o=await fetch(`${j}?name=${encodeURIComponent(t)}&type=${String(e)}`,{headers:{accept:"application/dns-json"},signal:AbortSignal.timeout(r)});return o.ok?(await o.json()).Answer??[]:void 0}catch{return}},T=async(t,e=b)=>{const r=h(new URL(t).hostname);if(r.includes(":")||f(r)!==void 0)return;const[o,n]=await Promise.all([N(r,g,e),N(r,W,e)]);if(!(o===void 0&&n===void 0)){for(const a of[...o??[],...n??[]])if((a.type===g||a.type===W)&&z(a.data,a.type))throw new l("FORBIDDEN",`@lunora/browser: url host "${r}" resolves to a private/internal address (${a.data}); refusing to navigate (DNS-rebinding guard)`)}},D=(t,e,r)=>{if(typeof t!="string"||t.length===0)throw new l("BAD_REQUEST","@lunora/browser: url must be a non-empty string");let o;try{o=new URL(t)}catch{throw new l("BAD_REQUEST",`@lunora/browser: url must be an absolute http(s) URL (got "${t}")`)}if(o.protocol!=="http:"&&o.protocol!=="https:")throw new l("BAD_REQUEST",`@lunora/browser: url protocol must be http(s) (got "${o.protocol}")`);if(o.username!==""||o.password!=="")throw new l("BAD_REQUEST","@lunora/browser: url must not embed credentials (strip the `user:pass@` userinfo)");if(r&&r.length>0){const n=h(o.hostname);if(!r.some(a=>h(a)===n))throw new l("FORBIDDEN",`@lunora/browser: url host "${o.hostname}" is not in the configured allowedHosts allowlist`)}if(!e&&U(o.hostname))throw new l("FORBIDDEN",`@lunora/browser: url host "${o.hostname}" is a private/internal address; pass createBrowser({ …, allowPrivateTargets: true }) to allow it`);return o.toString()},M=(t,e)=>Number.isFinite(t)?Math.min(Math.max(1,Math.floor(t)),e):e,G=t=>({height:M(t.height,V),width:M(t.width,Q)}),J=(t,e)=>{const r=t??e??B,o=Number.isFinite(r)?r:B;return Math.min(Math.max(1,Math.floor(o)),C)},K=async(t,e)=>{let r;try{return await Promise.race([t(),new Promise((o,n)=>{r=setTimeout(()=>{n(new l("BROWSER_TIMEOUT",`@lunora/browser: navigation + operation exceeded the ${String(e)}ms timeout budget`,{status:504}))},e)})])}finally{r!==void 0&&clearTimeout(r)}},Y=t=>{if(!t.binding)throw new TypeError("@lunora/browser: `binding` is required (env.BROWSER)");const e=()=>{if(!t.launch)throw new l("INTERNAL",'@lunora/browser: `launch` is not available — install the `@cloudflare/playwright` peer dependency. The generated worker wires it for you; outside codegen pass it via createBrowser({ binding, launch }) (import { launch } from "@cloudflare/playwright").');return t.launch},r=async n=>{const a=await e()(t.binding);try{return await n(a)}finally{try{await a.close()}catch{}}},o=async(n,a,s,m)=>{const w=t.allowPrivateTargets??!1,v=D(n,w,t.allowedHosts),y=J(a.timeoutMs,t.timeoutMs),$=t.resolveDns??!1,E=Math.min(y,b);!w&&$&&await T(v,E);const x=async c=>{D(c,w,t.allowedHosts),!w&&$&&await T(c,E)},I=c=>{let i;try{i=new URL(c)}catch{return!1}if(i.protocol!=="http:"&&i.protocol!=="https:")return!1;if(t.allowedHosts&&t.allowedHosts.length>0){const u=h(i.hostname);if(!t.allowedHosts.some(d=>h(d)===u))return!0}return U(i.hostname)};return r(async c=>{const i=await(await c.newContext()).newPage();return i.route&&(!w||(t.allowedHosts?.length??0)>0)&&await i.route("**/*",async u=>{const d=u.request();if(!(d.isNavigationRequest?.()??!0)){if(I(d.url())){await u.abort("blockedbyclient");return}await u.continue();return}try{await x(d.url())}catch{await u.abort("blockedbyclient");return}await u.continue()}),m&&i.setViewportSize&&await i.setViewportSize(G(m)),K(async()=>(await i.goto(v,{timeout:y,waitUntil:a.waitUntil??"load"}),s(i)),y)})};return{content:async(n,a={})=>o(n,a,async s=>s.content()),launch:async n=>r(n),pdf:async(n,a={})=>o(n,a,async s=>s.pdf({format:a.format,printBackground:a.printBackground??!1}),a.viewport),scrape:async(n,a,s={})=>o(n,s,async m=>m.evaluate(a)),screenshot:async(n,a={})=>o(n,a,async s=>s.screenshot({fullPage:a.fullPage??!1,type:a.type??"png"}),a.viewport)}};export{Y as createBrowser};