@lunora/browser 1.0.0-alpha.37 → 1.0.0-alpha.39

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
@@ -304,9 +304,14 @@ interface Browser {
304
304
  * `fn` (e.g. for multi-page flows or APIs not surfaced here).
305
305
  *
306
306
  * The browser is **always closed** when `fn` resolves or throws — unless
307
- * `keepAlive` is set, which holds the session open for that many seconds so
308
- * a later {@link Browser.connect} can re-attach. Do not retain references to
309
- * the browser past the callback either way.
307
+ * `keepAlive` is a number of seconds **between 10 and 600**, which holds the
308
+ * session open for that long so a later {@link Browser.connect} can
309
+ * re-attach. `0`, a negative value and `NaN` all mean "do not keep alive"
310
+ * and take the always-close path (a held session is billed, so the
311
+ * ambiguous values fall to the safe side); a positive value outside the
312
+ * 10–600s window Browser Rendering accepts throws `BAD_REQUEST` rather than
313
+ * being sent on for the provider to refuse. Do not retain references to the
314
+ * browser past the callback either way.
310
315
  */
311
316
  launch: <T>(function_: (browser: BrowserLike) => Promise<T>, options?: {
312
317
  keepAlive?: number;
package/dist/index.d.ts CHANGED
@@ -304,9 +304,14 @@ interface Browser {
304
304
  * `fn` (e.g. for multi-page flows or APIs not surfaced here).
305
305
  *
306
306
  * The browser is **always closed** when `fn` resolves or throws — unless
307
- * `keepAlive` is set, which holds the session open for that many seconds so
308
- * a later {@link Browser.connect} can re-attach. Do not retain references to
309
- * the browser past the callback either way.
307
+ * `keepAlive` is a number of seconds **between 10 and 600**, which holds the
308
+ * session open for that long so a later {@link Browser.connect} can
309
+ * re-attach. `0`, a negative value and `NaN` all mean "do not keep alive"
310
+ * and take the always-close path (a held session is billed, so the
311
+ * ambiguous values fall to the safe side); a positive value outside the
312
+ * 10–600s window Browser Rendering accepts throws `BAD_REQUEST` rather than
313
+ * being sent on for the provider to refuse. Do not retain references to the
314
+ * browser past the callback either way.
310
315
  */
311
316
  launch: <T>(function_: (browser: BrowserLike) => Promise<T>, options?: {
312
317
  keepAlive?: number;
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{createBrowser as o}from"./packem_shared/createBrowser-BicVf2xu.mjs";export{o as createBrowser};
1
+ import{createBrowser as o}from"./packem_shared/createBrowser-CN6x72HL.mjs";export{o as createBrowser};
@@ -0,0 +1 @@
1
+ import{LunoraError as u}from"@lunora/errors";const x=/^\d{1,3}$/u,C=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,F=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,X=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,Q=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,z=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,G=/^\[|\]$/gu,j=/\.$/u,v=t=>{const e=t.split(".");if(e.length!==4)return;const r=e.map(n=>x.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,D=(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])},L=t=>{const e=t.toLowerCase(),r=C.exec(e);if(r)return D(r[1],r[2]);const n=F.exec(e);if(n){const f=v(n[1]??"");return f===void 0||y(f)}const i=X.exec(e);if(i){const f=v(i[1]??"");return f===void 0||y(f)}const c=Q.exec(e);return c?D(c[1],c[2]):z.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(G,"").replace(j,"").toLowerCase(),O=t=>{const e=h(t);if(e.includes(":"))return L(e);const r=v(e);return r===void 0?q(e):y(r)},Y="https://cloudflare-dns.com/dns-query",T=1,N=28,J=2e3,Z=(t,e)=>{if(e===T){const r=v(t);return r===void 0||y(r)}return L(t.toLowerCase())},S=async(t,e,r)=>{try{const n=await fetch(`${Y}?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}},K=async(t,e=J)=>{const r=h(t);if(r.includes(":")||v(r)!==void 0)return{kind:"unknown"};const[n,i]=await Promise.all([S(r,T,e),S(r,N,e)]);if(n===void 0&&i===void 0)return{kind:"unknown"};for(const c of[...n??[],...i??[]])if((c.type===T||c.type===N)&&Z(c.data,c.type))return{address:c.data,kind:"private"};return{kind:"public"}},A=3e4,tt=12e4,et=3840,rt=4320,R=10,M=600,U=5e3,$=async(t,e=U)=>{const r=h(new URL(t).hostname),n=await K(r,e);if(n.kind==="private")throw new u("FORBIDDEN",`@lunora/browser: url host "${r}" resolves to a private/internal address (${n.address}); refusing to navigate (DNS-rebinding guard)`)},B=(t,e,r)=>{if(typeof t!="string"||t.length===0)throw new u("BAD_REQUEST","@lunora/browser: url must be a non-empty string");let n;try{n=new URL(t)}catch{throw new u("BAD_REQUEST",`@lunora/browser: url must be an absolute http(s) URL (got "${t}")`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new u("BAD_REQUEST",`@lunora/browser: url protocol must be http(s) (got "${n.protocol}")`);if(n.username!==""||n.password!=="")throw new u("BAD_REQUEST","@lunora/browser: url must not embed credentials (strip the `user:pass@` userinfo)");if(r&&r.length>0){const i=h(n.hostname);if(!r.some(c=>h(c)===i))throw new u("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is not in the configured allowedHosts allowlist`)}if(!e&&O(n.hostname))throw new u("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is a private/internal address; pass createBrowser({ …, allowPrivateTargets: true }) to allow it`);return n.toString()},H=(t,e)=>Number.isFinite(t)?Math.min(Math.max(1,Math.floor(t)),e):e,nt=t=>({height:H(t.height,rt),width:H(t.width,et)}),ot=(t,e)=>{const r=t??e??A,n=Number.isFinite(r)?r:A;return Math.min(Math.max(1,Math.floor(n)),tt)},st=async(t,e)=>{let r;try{return await Promise.race([t(),new Promise((n,i)=>{r=setTimeout(()=>{i(new u("BROWSER_TIMEOUT",`@lunora/browser: navigation + operation exceeded the ${String(e)}ms timeout budget`,{status:504}))},e)})])}finally{r!==void 0&&clearTimeout(r)}},k=async t=>{try{await t.close()}catch{}},ht=t=>{if(!t.binding)throw new TypeError("@lunora/browser: `binding` is required (env.BROWSER)");const e=()=>{if(!t.launch)throw new u("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=(s,o)=>{if(!s)throw new u("INTERNAL",`@lunora/browser: \`${o}\` is not available — install the \`@cloudflare/playwright\` peer dependency. The generated worker wires it for you; outside codegen pass it via createBrowser({ binding, ${o} }).`);return s},n=async(s,o)=>{const a=o!==void 0&&Number.isFinite(o)&&o>0?o:void 0;if(a!==void 0&&(a<R||a>M))throw new u("BAD_REQUEST",`@lunora/browser: keepAlive must be between ${String(R)} and ${String(M)} seconds (Browser Rendering accepts keep_alive from 10s to 10min; got ${String(o)})`);const l=await e()(t.binding,a===void 0?void 0:{keep_alive:a*1e3});if(a!==void 0)return await s(l);try{return await s(l)}finally{await k(l)}},i=async(s,o,a,l)=>{const p=t.allowPrivateTargets??!1,_=B(s,p,t.allowedHosts),E=ot(o.timeoutMs,t.timeoutMs),I=t.resolveDns??(t.allowedHosts?.length??0)===0,P=Math.min(E,U);!p&&I&&await $(_,P);const W=async m=>{B(m,p,t.allowedHosts),!p&&I&&await $(m,P)},V=m=>{let g;try{g=new URL(m)}catch{return!1}if(g.protocol!=="http:"&&g.protocol!=="https:")return!1;if(t.allowedHosts&&t.allowedHosts.length>0){const d=h(g.hostname);if(!t.allowedHosts.some(w=>h(w)===d))return!0}return O(g.hostname)};return n(async m=>{const d=await(await m.newContext()).newPage();return d.route&&(!p||(t.allowedHosts?.length??0)>0)&&await d.route("**/*",async w=>{const b=w.request();if(!(b.isNavigationRequest?.()??!0)){if(V(b.url())){await w.abort("blockedbyclient");return}await w.continue();return}try{await W(b.url())}catch{await w.abort("blockedbyclient");return}await w.continue()}),l&&d.setViewportSize&&await d.setViewportSize(nt(l)),st(async()=>(await d.goto(_,{timeout:E,waitUntil:o.waitUntil??"load"}),a(d)),E)})};return{connect:async(s,o,a={})=>{const l=await r(t.connect,"connect")(t.binding,s);if(a.close!==!0)return await o(l);try{return await o(l)}finally{await k(l)}},content:async(s,o={})=>i(s,o,async a=>a.content()),launch:async(s,o={})=>n(s,o.keepAlive),pdf:async(s,o={})=>i(s,o,async a=>a.pdf({format:o.format,printBackground:o.printBackground??!1}),o.viewport),scrape:async(s,o,a={})=>i(s,a,async l=>l.evaluate(o)),screenshot:async(s,o={})=>i(s,o,async a=>a.screenshot({fullPage:o.fullPage??!1,type:o.type??"png"}),o.viewport),sessions:async()=>await r(t.sessions,"sessions")(t.binding)}};export{ht as createBrowser};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/browser",
3
- "version": "1.0.0-alpha.37",
3
+ "version": "1.0.0-alpha.39",
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 O=/^\d{1,3}$/u,x=/^::ffff:([\da-f]{1,4}):([\da-f]{1,4})$/u,V=/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/u,C=/^::(\d{1,3}(?:\.\d{1,3}){3})$/u,F=/^::([\da-f]{1,4}):([\da-f]{1,4})$/u,q=/^64:ff9b::[\da-f]{1,4}:[\da-f]{1,4}$/u,X=/^\[|\]$/gu,Q=/\.$/u,v=t=>{const e=t.split(".");if(e.length!==4)return;const r=e.map(n=>O.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,D=(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])},$=t=>{const e=t.toLowerCase(),r=x.exec(e);if(r)return D(r[1],r[2]);const n=V.exec(e);if(n){const f=v(n[1]??"");return f===void 0||y(f)}const a=C.exec(e);if(a){const f=v(a[1]??"");return f===void 0||y(f)}const c=F.exec(e);return c?D(c[1],c[2]):q.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")},z=t=>t==="localhost"||t.endsWith(".localhost")||t.endsWith(".local")||t.endsWith(".internal")||t.endsWith(".home.arpa"),h=t=>t.replaceAll(X,"").replace(Q,"").toLowerCase(),L=t=>{const e=h(t);if(e.includes(":"))return $(e);const r=v(e);return r===void 0?z(e):y(r)},G="https://cloudflare-dns.com/dns-query",E=1,R=28,j=2e3,Y=(t,e)=>{if(e===E){const r=v(t);return r===void 0||y(r)}return $(t.toLowerCase())},N=async(t,e,r)=>{try{const n=await fetch(`${G}?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}},K=async(t,e=j)=>{const r=h(t);if(r.includes(":")||v(r)!==void 0)return{kind:"unknown"};const[n,a]=await Promise.all([N(r,E,e),N(r,R,e)]);if(n===void 0&&a===void 0)return{kind:"unknown"};for(const c of[...n??[],...a??[]])if((c.type===E||c.type===R)&&Y(c.data,c.type))return{address:c.data,kind:"private"};return{kind:"public"}},A=3e4,J=12e4,Z=3840,tt=4320,W=5e3,M=async(t,e=W)=>{const r=h(new URL(t).hostname),n=await K(r,e);if(n.kind==="private")throw new l("FORBIDDEN",`@lunora/browser: url host "${r}" resolves to a private/internal address (${n.address}); refusing to navigate (DNS-rebinding guard)`)},H=(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 n;try{n=new URL(t)}catch{throw new l("BAD_REQUEST",`@lunora/browser: url must be an absolute http(s) URL (got "${t}")`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new l("BAD_REQUEST",`@lunora/browser: url protocol must be http(s) (got "${n.protocol}")`);if(n.username!==""||n.password!=="")throw new l("BAD_REQUEST","@lunora/browser: url must not embed credentials (strip the `user:pass@` userinfo)");if(r&&r.length>0){const a=h(n.hostname);if(!r.some(c=>h(c)===a))throw new l("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is not in the configured allowedHosts allowlist`)}if(!e&&L(n.hostname))throw new l("FORBIDDEN",`@lunora/browser: url host "${n.hostname}" is a private/internal address; pass createBrowser({ …, allowPrivateTargets: true }) to allow it`);return n.toString()},S=(t,e)=>Number.isFinite(t)?Math.min(Math.max(1,Math.floor(t)),e):e,et=t=>({height:S(t.height,tt),width:S(t.width,Z)}),rt=(t,e)=>{const r=t??e??A,n=Number.isFinite(r)?r:A;return Math.min(Math.max(1,Math.floor(n)),J)},nt=async(t,e)=>{let r;try{return await Promise.race([t(),new Promise((n,a)=>{r=setTimeout(()=>{a(new l("BROWSER_TIMEOUT",`@lunora/browser: navigation + operation exceeded the ${String(e)}ms timeout budget`,{status:504}))},e)})])}finally{r!==void 0&&clearTimeout(r)}},B=async t=>{try{await t.close()}catch{}},dt=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=(o,s)=>{if(!o)throw new l("INTERNAL",`@lunora/browser: \`${s}\` is not available — install the \`@cloudflare/playwright\` peer dependency. The generated worker wires it for you; outside codegen pass it via createBrowser({ binding, ${s} }).`);return o},n=async(o,s)=>{const i=await e()(t.binding,s===void 0?void 0:{keep_alive:s*1e3});if(s!==void 0)return await o(i);try{return await o(i)}finally{await B(i)}},a=async(o,s,i,u)=>{const g=t.allowPrivateTargets??!1,I=H(o,g,t.allowedHosts),b=rt(s.timeoutMs,t.timeoutMs),P=t.resolveDns??(t.allowedHosts?.length??0)===0,_=Math.min(b,W);!g&&P&&await M(I,_);const k=async m=>{H(m,g,t.allowedHosts),!g&&P&&await M(m,_)},U=m=>{let p;try{p=new URL(m)}catch{return!1}if(p.protocol!=="http:"&&p.protocol!=="https:")return!1;if(t.allowedHosts&&t.allowedHosts.length>0){const d=h(p.hostname);if(!t.allowedHosts.some(w=>h(w)===d))return!0}return L(p.hostname)};return n(async m=>{const d=await(await m.newContext()).newPage();return d.route&&(!g||(t.allowedHosts?.length??0)>0)&&await d.route("**/*",async w=>{const T=w.request();if(!(T.isNavigationRequest?.()??!0)){if(U(T.url())){await w.abort("blockedbyclient");return}await w.continue();return}try{await k(T.url())}catch{await w.abort("blockedbyclient");return}await w.continue()}),u&&d.setViewportSize&&await d.setViewportSize(et(u)),nt(async()=>(await d.goto(I,{timeout:b,waitUntil:s.waitUntil??"load"}),i(d)),b)})};return{connect:async(o,s,i={})=>{const u=await r(t.connect,"connect")(t.binding,o);if(i.close!==!0)return await s(u);try{return await s(u)}finally{await B(u)}},content:async(o,s={})=>a(o,s,async i=>i.content()),launch:async(o,s={})=>n(o,s.keepAlive),pdf:async(o,s={})=>a(o,s,async i=>i.pdf({format:s.format,printBackground:s.printBackground??!1}),s.viewport),scrape:async(o,s,i={})=>a(o,i,async u=>u.evaluate(s)),screenshot:async(o,s={})=>a(o,s,async i=>i.screenshot({fullPage:s.fullPage??!1,type:s.type??"png"}),s.viewport),sessions:async()=>await r(t.sessions,"sessions")(t.binding)}};export{dt as createBrowser};