@coreframe/preview 0.1.13 → 0.1.15

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.
@@ -0,0 +1 @@
1
+ import{isIP as e}from"node:net";const t=`CLOUDFLARE_API_TOKEN`;async function n({accountId:e,appOrigin:n,config:o,env:c,hostname:u,request:d=fetch,tunnel:f}){let p=c[t]?.trim();if(!p)throw Error(`Missing required environment variable ${t} for preview.access. The token must grant Access: Apps and Policies Write.`);let m=await a(o.bypass.ips,{appOrigin:n,hostname:u,tunnel:f}),h=s({accountId:e,apiToken:p,request:d}),g=await r({api:h,applicationId:o.applicationId,hostname:u}),_=o.bypass.policyName??`Coreframe preview bypass: ${f.profile}/${f.name}`,v=(await i(h,g.id)).filter(e=>e.name===_);if(v.length>1)throw Error(`Cloudflare Access has multiple policies named ${JSON.stringify(_)} for application ${g.id}. Rename or remove the duplicates before running preview.`);let y=v[0];if(y&&y.decision!==`bypass`)throw Error(`Cloudflare Access policy ${JSON.stringify(_)} already exists but is not a bypass policy. Choose another preview.access.bypass.policyName.`);let b=JSON.stringify({decision:`bypass`,include:m.map(e=>({ip:{ip:e}})),name:_});console.log(`${y?`Updating`:`Creating`} Cloudflare Access bypass policy ${JSON.stringify(_)}...`);let x=l((await h.request(`/access/apps/${encodeURIComponent(g.id)}/policies${y?`/${encodeURIComponent(y.id)}`:``}`,{body:b,method:y?`PUT`:`POST`})).result,`policy response`);if(x.id!==y?.id&&y)throw Error(`Cloudflare returned an Access policy with an unexpected ID.`);if(x.name!==_||x.decision!==`bypass`)throw Error(`Cloudflare returned an Access policy that did not match the preview request.`);return{applicationId:g.id,ips:m,policyId:x.id,policyName:_}}async function r({api:e,applicationId:t,hostname:n}){if(t)return c((await e.request(`/access/apps/${encodeURIComponent(t)}`)).result,n,`application response`);let r=new URLSearchParams({domain:n,exact:`true`,per_page:`1000`}),i=u((await e.request(`/access/apps?${r}`)).result,`application list`).map(e=>c(e,n,`application list`)).filter(e=>e.domain===n);if(i.length===0)throw Error(`No self-hosted Cloudflare Access application protects ${n}. Create one first or set preview.access.applicationId to an existing application UUID.`);if(i.length>1)throw Error(`Multiple self-hosted Cloudflare Access applications protect ${n}. Set preview.access.applicationId to the intended application UUID.`);return i[0]}async function i(e,t){return u((await e.request(`/access/apps/${encodeURIComponent(t)}/policies`)).result,`policy list`).map(e=>l(e,`policy list`))}async function a(e,t){let n=typeof e==`function`?await e(t):e,r=[...new Set((Array.isArray(n)?n:[n]).map(o))];if(r.length===0)throw Error(`preview.access.bypass.ips must resolve to at least one IP range.`);return r}function o(t){if(typeof t!=`string`)throw Error(`preview.access.bypass.ips must contain only IP addresses or CIDR ranges.`);let n=t.trim().split(`/`);if(n.length>2||!n[0])throw Error(`Invalid preview.access.bypass IP range ${JSON.stringify(t)}.`);let r=n[0],i=e(r);if(i===0)throw Error(`Invalid preview.access.bypass IP range ${JSON.stringify(t)}.`);let a=i===4?32:128,o=n[1]===void 0?a:Number(n[1]);if(n[1]!==void 0&&(!/^\d+$/u.test(n[1])||!Number.isInteger(o)||o<0||o>a))throw Error(`Invalid preview.access.bypass IP range ${JSON.stringify(t)}.`);return`${r}/${o}`}function s({accountId:e,apiToken:t,request:n}){return{async request(r,i={}){let a=await n(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(e)}${r}`,{...i,headers:{...i.headers,Authorization:`Bearer ${t}`,...i.body?{"Content-Type":`application/json`}:{}}}),o=await a.json().catch(()=>void 0);if(!d(o))throw Error(`Cloudflare Access API returned an invalid response (${a.status}).`);let s=o;if(!a.ok||s.success!==!0){let e=s.errors?.map(e=>e.message).filter(Boolean).join(`; `);throw Error(`Cloudflare Access API request failed (${a.status})${e?`: ${e}`:`.`}`)}return s}}}function c(e,t,n){if(!d(e)||typeof e.domain!=`string`||typeof e.id!=`string`||typeof e.type!=`string`)throw Error(`Cloudflare returned an invalid Access ${n}.`);if(e.type!==`self_hosted`)throw Error(`Cloudflare Access application ${e.id} is not self-hosted.`);if(e.domain!==t)throw Error(`Cloudflare Access application ${e.id} does not protect the preview hostname ${t}.`);return{domain:e.domain,id:e.id,type:e.type}}function l(e,t){if(!d(e)||typeof e.decision!=`string`||typeof e.id!=`string`||typeof e.name!=`string`)throw Error(`Cloudflare returned an invalid Access ${t}.`);return{decision:e.decision,id:e.id,name:e.name}}function u(e,t){if(!Array.isArray(e))throw Error(`Cloudflare returned an invalid Access ${t} response.`);return e}function d(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}export{n as preparePreviewAccess};
@@ -1 +1 @@
1
- import{keyring as e}from"./keyring.js";import{mkdir as t,mkdtemp as n,readFile as r,rm as i,writeFile as a}from"node:fs/promises";import{join as o}from"node:path";import s from"nano-spawn";import{tmpdir as c}from"node:os";const l=process.platform===`win32`?`cloudflared.exe`:`cloudflared`,u=`dev.coreframe.preview.cloudflare.account`,d=`dev.coreframe.preview.cloudflare.tunnel`;async function f({config:e,cwd:t,env:r=process.env,now:s=()=>new Date().toISOString(),run:l=j}){let u=await p({config:e,env:r,now:s,run:l}),d=await m({account:u,config:e,cwd:t,env:r,now:s,run:l});e.dns===`managed`&&await _({account:u,config:e,cwd:t,env:r,run:l,tunnel:d});let f=await n(o(c(),`coreframe-preview-tunnel-`)),h=o(f,`tunnel.json`),g=!1;try{await a(h,`${JSON.stringify(d.credentials)}\n`,{mode:384})}catch(e){throw await i(f,{force:!0,recursive:!0}),e}return{credentialsFile:h,async dispose(){g||(g=!0,await i(f,{force:!0,recursive:!0}))},tunnel:{name:d.name,profile:d.profile,tunnelId:d.tunnelId}}}async function p({config:t,env:n,now:r,run:i}){let a=await e.get(u,t.profile);if(a)return C(a,t.profile);console.log([`Cloudflare authentication is required for preview tunnel "${t.profile}/${t.name}".`,"Coreframe will run `cloudflared tunnel login` now.",`Follow the browser prompt or URL, sign in to Cloudflare, and authorize the zone for ${t.hostname}.`,`Coreframe will store the resulting credentials in your operating system credential store.`].join(` `));let o=await v({env:n,run:i}),s={createdAt:r(),profile:t.profile,originCertificate:o,version:1};return await e.set(u,t.profile,JSON.stringify(s)),s}async function m({account:t,config:n,cwd:r,env:i,now:a,run:o}){let s=A(n.profile,n.name),c=await e.get(d,s);if(c)return w(c,n);let l=(await h({account:t,cwd:r,env:i,run:o})).find(e=>e.name===n.name);if(l)throw Error(`Cloudflare tunnel "${n.name}" already exists as ${l.id}, but Coreframe has no Keychain credentials for it. Coreframe will not adopt an existing tunnel; choose a new preview.tunnel.name.`);let f=await g({account:t,config:n,cwd:r,env:i,run:o}),p={accountTag:f.AccountTag,createdAt:a(),credentials:f,hostname:n.hostname,name:n.name,profile:n.profile,tunnelId:f.TunnelID,version:1};return await e.set(d,s,JSON.stringify(p)),await e.set(u,n.profile,JSON.stringify({...t,lastAccountTag:p.accountTag,lastTunnelId:p.tunnelId,lastTunnelName:p.name})),p}async function h({account:e,cwd:t,env:n,run:r}){return y(e,async e=>D((await r([`tunnel`,`--origincert`,e,`list`,`--output`,`json`],{cwd:t,env:n})).stdout))}async function g({account:e,config:t,cwd:n,env:i,run:a}){return b(`coreframe-cloudflared-create-`,async s=>{let c=o(s,`cert.pem`),l=o(s,`tunnel.json`);return await x(c,e.originCertificate),await a([`tunnel`,`--origincert`,c,`create`,`--credentials-file`,l,t.name],{cwd:n,env:i}),T(E(await r(l,`utf8`),`tunnel credentials`))})}async function _({account:e,config:t,cwd:n,env:r,run:i,tunnel:a}){await y(e,async e=>{await i([`tunnel`,`--origincert`,e,`route`,`dns`,`--overwrite-dns`,a.tunnelId,t.hostname],{cwd:n,env:r})})}async function v({env:e,run:a}){let s=await n(o(c(),`coreframe-cloudflared-login-`)),l=o(s,`.cloudflared`);await t(l,{mode:448});try{await a([`tunnel`,`login`],{cwd:s,env:S(e,s),stdio:`inherit`});let t=await r(o(l,`cert.pem`),`utf8`);if(!t.trim())throw Error(`Cloudflare login completed without producing an origin certificate.`);return t}finally{await i(s,{force:!0,recursive:!0})}}async function y(e,t){return b(`coreframe-cloudflared-command-`,async n=>{let r=o(n,`cert.pem`);return await x(r,e.originCertificate),t(r)})}async function b(e,t){let r=await n(o(c(),e));try{return await t(r)}finally{await i(r,{force:!0,recursive:!0})}}async function x(e,t){await a(e,t,{mode:384})}function S(e,t){let n={...e};return delete n.TUNNEL_ORIGIN_CERT,{...n,HOME:t,USERPROFILE:t}}function C(e,t){let n;try{n=JSON.parse(e)}catch{throw k(`account`,t)}if(!O(n)||n.version!==1||n.profile!==t||typeof n.createdAt!=`string`||typeof n.originCertificate!=`string`||!n.originCertificate.trim())throw k(`account`,t);return n}function w(e,t){let n;try{n=JSON.parse(e)}catch{throw k(`tunnel`,`${t.profile}/${t.name}`)}if(!O(n)||n.version!==1||n.profile!==t.profile||n.name!==t.name||n.hostname!==t.hostname||typeof n.accountTag!=`string`||typeof n.createdAt!=`string`||typeof n.tunnelId!=`string`)throw k(`tunnel`,`${t.profile}/${t.name}`);let r=T(n.credentials);if(r.TunnelID!==n.tunnelId||r.AccountTag!==n.accountTag)throw k(`tunnel`,`${t.profile}/${t.name}`);return{...n,credentials:r}}function T(e){if(!O(e)||typeof e.AccountTag!=`string`||!e.AccountTag.trim()||typeof e.TunnelID!=`string`||!e.TunnelID.trim()||typeof e.TunnelSecret!=`string`||!e.TunnelSecret.trim()||e.Endpoint!==void 0&&typeof e.Endpoint!=`string`)throw Error(`Cloudflare returned invalid tunnel credentials.`);return{AccountTag:e.AccountTag,Endpoint:e.Endpoint,TunnelID:e.TunnelID,TunnelSecret:e.TunnelSecret}}function E(e,t){try{return JSON.parse(e)}catch{throw Error(`Cloudflare returned invalid ${t}.`)}}function D(e){let t;try{t=JSON.parse(e)}catch{throw Error(`Cloudflare returned an invalid tunnel list.`)}if(t===null)return[];let n=Array.isArray(t)?t:O(t)&&Array.isArray(t.result)?t.result:void 0;if(!n)throw Error(`Cloudflare returned an invalid tunnel list.`);return n.map(e=>{if(!O(e)||typeof e.id!=`string`||!e.id.trim()||typeof e.name!=`string`||!e.name.trim())throw Error(`Cloudflare returned an invalid tunnel list.`);return{id:e.id,name:e.name}})}function O(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function k(e,t){return Error(`The Coreframe Cloudflare ${e} credential for "${t}" is invalid. Clear the matching Coreframe Keychain entry and run coreframe preview again.`)}function A(e,t){return`${e}/${t}`}async function j(e,{cwd:t,env:n,stdio:r=`pipe`}){let i=await s(l,e,{cwd:t,env:n,stdio:r});return{stderr:i.stderr,stdout:i.stdout}}export{f as preparePreviewTunnel};
1
+ import{preparePreviewAccess as e}from"./cloudflare-access.js";import{keyring as t}from"./keyring.js";import{mkdir as n,mkdtemp as r,readFile as i,rm as a,writeFile as o}from"node:fs/promises";import{tmpdir as s}from"node:os";import{join as c}from"node:path";import l from"nano-spawn";const u=process.platform===`win32`?`cloudflared.exe`:`cloudflared`,d=`dev.coreframe.preview.cloudflare.account`,f=`dev.coreframe.preview.cloudflare.tunnel`;async function p({appOrigin:t,access:n,accessEnv:i,config:l,cwd:u,env:d=process.env,now:f=()=>new Date().toISOString(),request:p,run:g=M}){let _=await m({config:l,env:d,now:f,run:g}),y=await h({account:_,config:l,cwd:u,env:d,now:f,run:g});l.dns===`managed`&&await v({account:_,config:l,cwd:u,env:d,run:g,tunnel:y});let b=n?await e({accountId:y.accountTag,appOrigin:t??`https://${l.hostname}/`,config:n,env:i??d,hostname:l.hostname,request:p,tunnel:{name:y.name,profile:y.profile}}):void 0,x=await r(c(s(),`coreframe-preview-tunnel-`)),S=c(x,`tunnel.json`),C=!1;try{await o(S,`${JSON.stringify(y.credentials)}\n`,{mode:384})}catch(e){throw await a(x,{force:!0,recursive:!0}),e}return{access:b,credentialsFile:S,async dispose(){C||(C=!0,await a(x,{force:!0,recursive:!0}))},tunnel:{name:y.name,profile:y.profile,tunnelId:y.tunnelId}}}async function m({config:e,env:n,now:r,run:i}){let a=await t.get(d,e.profile);if(a)return w(a,e.profile);console.log([`Cloudflare authentication is required for preview tunnel "${e.profile}/${e.name}".`,"Coreframe will run `cloudflared tunnel login` now.",`Follow the browser prompt or URL, sign in to Cloudflare, and authorize the zone for ${e.hostname}.`,`Coreframe will store the resulting credentials in your operating system credential store.`].join(` `));let o=await y({env:n,run:i}),s={createdAt:r(),profile:e.profile,originCertificate:o,version:1};return await t.set(d,e.profile,JSON.stringify(s)),s}async function h({account:e,config:n,cwd:r,env:i,now:a,run:o}){let s=j(n.profile,n.name),c=await t.get(f,s);if(c)return T(c,n);let l=(await g({account:e,cwd:r,env:i,run:o})).find(e=>e.name===n.name);if(l)throw Error(`Cloudflare tunnel "${n.name}" already exists as ${l.id}, but Coreframe has no Keychain credentials for it. Coreframe will not adopt an existing tunnel; choose a new preview.tunnel.name.`);console.log(`Creating Cloudflare tunnel ${JSON.stringify(`${n.profile}/${n.name}`)}...`);let u=await _({account:e,config:n,cwd:r,env:i,run:o}),p={accountTag:u.AccountTag,createdAt:a(),credentials:u,hostname:n.hostname,name:n.name,profile:n.profile,tunnelId:u.TunnelID,version:1};return await t.set(f,s,JSON.stringify(p)),await t.set(d,n.profile,JSON.stringify({...e,lastAccountTag:p.accountTag,lastTunnelId:p.tunnelId,lastTunnelName:p.name})),p}async function g({account:e,cwd:t,env:n,run:r}){return b(e,async e=>O((await r([`tunnel`,`--origincert`,e,`list`,`--output`,`json`],{cwd:t,env:n})).stdout))}async function _({account:e,config:t,cwd:n,env:r,run:a}){return x(`coreframe-cloudflared-create-`,async o=>{let s=c(o,`cert.pem`),l=c(o,`tunnel.json`);return await S(s,e.originCertificate),await a([`tunnel`,`--origincert`,s,`create`,`--credentials-file`,l,t.name],{cwd:n,env:r}),E(D(await i(l,`utf8`),`tunnel credentials`))})}async function v({account:e,config:t,cwd:n,env:r,run:i,tunnel:a}){await b(e,async e=>{await i([`tunnel`,`--origincert`,e,`route`,`dns`,`--overwrite-dns`,a.tunnelId,t.hostname],{cwd:n,env:r})})}async function y({env:e,run:t}){let o=await r(c(s(),`coreframe-cloudflared-login-`)),l=c(o,`.cloudflared`);await n(l,{mode:448});try{await t([`tunnel`,`login`],{cwd:o,env:C(e,o),stdio:`inherit`});let n=await i(c(l,`cert.pem`),`utf8`);if(!n.trim())throw Error(`Cloudflare login completed without producing an origin certificate.`);return n}finally{await a(o,{force:!0,recursive:!0})}}async function b(e,t){return x(`coreframe-cloudflared-command-`,async n=>{let r=c(n,`cert.pem`);return await S(r,e.originCertificate),t(r)})}async function x(e,t){let n=await r(c(s(),e));try{return await t(n)}finally{await a(n,{force:!0,recursive:!0})}}async function S(e,t){await o(e,t,{mode:384})}function C(e,t){let n={...e};return delete n.TUNNEL_ORIGIN_CERT,{...n,HOME:t,USERPROFILE:t}}function w(e,t){let n;try{n=JSON.parse(e)}catch{throw A(`account`,t)}if(!k(n)||n.version!==1||n.profile!==t||typeof n.createdAt!=`string`||typeof n.originCertificate!=`string`||!n.originCertificate.trim())throw A(`account`,t);return n}function T(e,t){let n;try{n=JSON.parse(e)}catch{throw A(`tunnel`,`${t.profile}/${t.name}`)}if(!k(n)||n.version!==1||n.profile!==t.profile||n.name!==t.name||n.hostname!==t.hostname||typeof n.accountTag!=`string`||typeof n.createdAt!=`string`||typeof n.tunnelId!=`string`)throw A(`tunnel`,`${t.profile}/${t.name}`);let r=E(n.credentials);if(r.TunnelID!==n.tunnelId||r.AccountTag!==n.accountTag)throw A(`tunnel`,`${t.profile}/${t.name}`);return{...n,credentials:r}}function E(e){if(!k(e)||typeof e.AccountTag!=`string`||!e.AccountTag.trim()||typeof e.TunnelID!=`string`||!e.TunnelID.trim()||typeof e.TunnelSecret!=`string`||!e.TunnelSecret.trim()||e.Endpoint!==void 0&&typeof e.Endpoint!=`string`)throw Error(`Cloudflare returned invalid tunnel credentials.`);return{AccountTag:e.AccountTag,Endpoint:e.Endpoint,TunnelID:e.TunnelID,TunnelSecret:e.TunnelSecret}}function D(e,t){try{return JSON.parse(e)}catch{throw Error(`Cloudflare returned invalid ${t}.`)}}function O(e){let t;try{t=JSON.parse(e)}catch{throw Error(`Cloudflare returned an invalid tunnel list.`)}if(t===null)return[];let n=Array.isArray(t)?t:k(t)&&Array.isArray(t.result)?t.result:void 0;if(!n)throw Error(`Cloudflare returned an invalid tunnel list.`);return n.map(e=>{if(!k(e)||typeof e.id!=`string`||!e.id.trim()||typeof e.name!=`string`||!e.name.trim())throw Error(`Cloudflare returned an invalid tunnel list.`);return{id:e.id,name:e.name}})}function k(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function A(e,t){return Error(`The Coreframe Cloudflare ${e} credential for "${t}" is invalid. Clear the matching Coreframe Keychain entry and run coreframe preview again.`)}function j(e,t){return`${e}/${t}`}async function M(e,{cwd:t,env:n,stdio:r=`pipe`}){let i=await l(u,e,{cwd:t,env:n,stdio:r});return{stderr:i.stderr,stdout:i.stdout}}export{p as preparePreviewTunnel};
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { CoreframeConfig, PreviewTunnelConfig } from "@coreframe/config";
1
+ import { CoreframeConfig, PreviewAccessConfig, PreviewTunnelConfig } from "@coreframe/config";
2
2
 
3
3
  //#region src/index.d.ts
4
4
  type ResolvedPreviewConfig = {
5
+ access?: PreviewAccessConfig;
5
6
  appOrigin?: string;
6
7
  host: string;
7
8
  port: number;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{preparePreviewTunnel as e}from"./cloudflare.js";import{copyFile as t,glob as n,mkdir as r,readFile as i,stat as a}from"node:fs/promises";import o from"node:path";import s from"node:process";import{parseEnv as c}from"node:util";import{coreframeLocalWorkerVariables as l,readProjectConfig as u}from"@coreframe/config";import{migrateLocalDb as d,runProjectSeed as f,startProjectPostgres as p,startProjectTurso as m}from"@coreframe/db";import h from"nano-spawn";import{supervise as g}from"procband";const _=[`DATABASE_URL`,`PGDATABASE`,`PGHOST`,`PGPASSWORD`,`PGPORT`,`PGUSER`,`TURSO_AUTH_TOKEN`,`TURSO_DATABASE_URL`],v=[`POSTHOG_`,`VITE_POSTHOG_`,`VITE_GA_`],y=[`CI`,`COLORTERM`,`COMSPEC`,`ComSpec`,`FORCE_COLOR`,`HOME`,`LANG`,`NODE_EXTRA_CA_CERTS`,`NODE_OPTIONS`,`NO_COLOR`,`PATH`,`PATHEXT`,`PNPM_HOME`,`SHELL`,`SSL_CERT_DIR`,`SSL_CERT_FILE`,`SYSTEMROOT`,`SystemRoot`,`TEMP`,`TERM`,`TMP`,`TMPDIR`,`USERPROFILE`],b=s.platform===`win32`?`pnpm.cmd`:`pnpm`;async function x({cwd:e=s.cwd(),env:t=s.env,host:n,port:r},i={}){let a=await u({cwd:e}),o=S(a,{host:n,port:r}),c=await C({appOrigin:o.appOrigin,config:a,cwd:e,env:t}),l=E({config:a,cwd:e,env:c}),d,f,p,m=new Set,h=!1,g,_=()=>{if(!h){h=!0;for(let e of m)e.nodeChildProcess.then(e=>e.kill(`SIGTERM`)).catch(()=>void 0);d?.kill(`SIGTERM`),f?.kill(`SIGTERM`),l.stop()}},v=[`SIGINT`,`SIGTERM`].map(e=>{let t=()=>{g??=e,_()};return s.on(e,t),{handler:t,signal:e}});try{await l.start();let n=w({config:o,cwd:e,database:l,env:c,processEnvironment:t,commands:m,onServe(e){d=e.server,f=e.tunnel,p=e.disposeTunnel},isStopping:()=>h,onStop:_});await n.database.migrate(),await n.database.seed(),await i.beforeBuild?.(n),await n.build(),await i.afterBuild?.(n),await n.serve()}finally{let e=[...m];_();try{await l.dispose()}finally{await Promise.allSettled([...e,d?.wait(),f?.wait()].filter(Boolean)),await p?.();for(let{handler:e,signal:t}of v)s.off(t,e);g&&(s.exitCode??=g===`SIGINT`?130:143)}}}function S(e,t={}){let n=e.preview??{},r=t.host??n.host??`127.0.0.1`,i=t.port??n.port??4173,a=n.strictPort??!0,o=n.appOrigin;if(!Number.isInteger(i)||i<1||i>65535)throw Error(`Preview port must be an integer between 1 and 65535.`);if(!r.trim())throw Error(`Preview host must not be empty.`);return o&&A(o),n.tunnel&&k(n.tunnel,o,a),{appOrigin:o,host:r,port:i,strictPort:a,tunnel:n.tunnel}}async function C({appOrigin:e,config:t,cwd:n,env:r}){let i=await D(n),a=Object.keys(t.worker?.environment??{}).filter(e=>!v.some(t=>e.startsWith(t))),o=new Set([...l,..._,...a]),s={},c=new Set([...Object.keys(r),...Object.keys(i)].filter(e=>e.startsWith(`VITE_`)&&!v.some(t=>e.startsWith(t))));for(let e of[...y,...Object.keys(r).filter(e=>e.startsWith(`LC_`))])r[e]!==void 0&&(s[e]=r[e]);for(let e of o){let t=r[e]??i[e];t!==void 0&&(s[e]=t)}for(let e of c){let t=r[e]??i[e];t!==void 0&&(s[e]=t)}return e!==void 0&&(o.add(`APP_ORIGIN`),s.APP_ORIGIN=e),s.CLOUDFLARE_ENV=`preview`,s.COREFRAME_ENV=`preview`,s.COREFRAME_PLATFORM=`web`,s.COREFRAME_PREVIEW_WORKER_VARIABLES=JSON.stringify([...o].filter(e=>!v.some(t=>e.startsWith(t)))),s}function w({config:i,cwd:c,database:l,env:u,processEnvironment:d,commands:f,onServe:p,isStopping:m,onStop:_}){let v=!1,y={cwd:c,database:l,env:u,host:i.host,origin:i.appOrigin??M(i.host,i.port),port:i.port,async build(){await y.run(b,[`run`,`build`,`--mode`,`preview`])},report(e,t){console.log(t===void 0?e:`${e}: ${t}`)},async run(e,t=[]){let n=h(e,t,{cwd:c,env:u,stdio:`inherit`});f.add(n);try{await n}finally{f.delete(n)}},async serve(){if(v)throw Error(`Preview serving can only be started once.`);v=!0;let t,n,r;try{if(t=i.tunnel?await e({config:i.tunnel,cwd:c,env:d}):void 0,m()){await t?.dispose();return}n=g({name:`preview`,command:b,args:[`exec`,`vite`,`preview`,`--mode`,`preview`,`--outDir`,`dist/client`,`--host`,i.host,`--port`,String(i.port),...i.strictPort?[`--strictPort`]:[]],cwd:c,env:u,prefix:!1,stdin:s.stdin}),r=t?g({name:`cloudflared`,command:s.platform===`win32`?`cloudflared.exe`:`cloudflared`,args:[`tunnel`,`run`,`--url`,`http://${P(N(i.host))}:${i.port}`,`--credentials-file`,t.credentialsFile,t.tunnel.tunnelId],cwd:c,env:u,prefix:!1}):void 0}catch(e){throw n?.kill(`SIGTERM`),await n?.wait(),await t?.dispose(),e}try{p({disposeTunnel:t?.dispose,server:n,tunnel:r}),y.report(`Preview`,y.url(`/`)),t&&y.report(`Cloudflare tunnel`,`${t.tunnel.name} (${t.tunnel.tunnelId})`),await Promise.race(r?[n.expectSuccess(),r.expectSuccess()]:[n.expectSuccess()])}catch(e){if(m()||_(),await t?.dispose(),m())return;throw e}},async stageArtifact({label:e,pathname:i,source:s}){let l=T(c,i),u=[];for await(let e of n(s,{cwd:c}))(await a(o.resolve(c,e))).isFile()&&u.push(e);if(u.length===0)throw Error(`Preview artifact source ${JSON.stringify(s)} did not match a file.`);if(u.length>1)throw Error(`Preview artifact source ${JSON.stringify(s)} matched multiple files: ${u.sort().join(`, `)}.`);await r(o.dirname(l),{recursive:!0}),await t(o.resolve(c,u[0]),l);let d=y.url(i);return y.report(e,d),d},url(e=`/`){return new URL(e,`${y.origin.replace(/\/$/u,``)}/`).toString()}};return y}function T(e,t){let n=new URL(t,`https://coreframe.invalid`);if(!t.startsWith(`/`)||t===`/`||t.includes(`\\`)||t.includes(`?`)||t.includes(`#`)||n.origin!==`https://coreframe.invalid`||n.pathname!==t||o.posix.normalize(t)!==t)throw Error(`Preview artifact pathname must be a normalized absolute URL path.`);return o.resolve(e,`dist/client`,`.${t}`)}function E({config:e,cwd:t,env:n}){let r=e.database?.dialect??`postgres`,i,a,o,s,c=!1,l=n,u=()=>a?(s??=a[Symbol.asyncDispose](),s):Promise.resolve(),h=()=>{c||(c=!0,u().catch(()=>void 0))};return{dialect:r,get env(){return l},async migrate(){await d({config:e,cwd:t,env:l})},async seed(){await f(`default`,{cwd:t,env:l,seedEntryPoint:e.database?.seed})},async start(){if(r===`sqlite`){a=await m({config:e,cwd:t,env:l,useSupervisorLease:!0}),Object.assign(l,a.env);return}i=await p({config:e,cwd:t,env:l,failIfRunning:!0,postgresOutput:`on-error`,useSupervisorLease:!0}),Object.assign(l,i.env)},stop:h,async dispose(){o??=(async()=>{h(),await Promise.allSettled([i?.[Symbol.asyncDispose](),u()]),i=void 0,a=void 0})(),await o}}}async function D(e){let t={};for(let n of[`.env`,`.env.local`,`.env.preview`,`.env.preview.local`]){let r=await O(o.resolve(e,n));r&&(t={...t,...r})}let n=await O(o.resolve(e,`.dev.vars`));n&&(t={...t,...n});let r=await O(o.resolve(e,`.dev.vars.preview`));return r&&(t={...t,...r}),t}async function O(e){try{return Object.fromEntries(Object.entries(c(await i(e,`utf8`))).filter(e=>e[1]!==void 0))}catch(e){if(e.code===`ENOENT`)return;throw e}}function k(e,t,n){if(!t)throw Error(`preview.appOrigin is required when preview.tunnel is configured.`);if(!n)throw Error(`preview.strictPort must be true when preview.tunnel is configured.`);let r=[`credentialsFile`,`tokenEnv`,`tokenFile`],i=e,a=r.find(e=>e in i);if(a)throw Error(`preview.tunnel.${a} is no longer supported. Coreframe preview now provisions Cloudflare credentials through the operating system credential store.`);if(!e.name.trim())throw Error(`preview.tunnel.name must not be empty.`);if(!/^[a-z0-9][a-z0-9._-]*$/u.test(e.profile))throw Error(`preview.tunnel.profile must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, underscores, or hyphens.`);if(e.dns!==`managed`&&e.dns!==`external`)throw Error(`preview.tunnel.dns must be either "managed" or "external".`);let o=j(e.hostname);if(new URL(t).hostname!==o)throw Error(`preview.appOrigin hostname must match preview.tunnel.hostname.`)}function A(e){let t;try{t=new URL(e)}catch{throw Error(`preview.appOrigin must be an exact HTTPS browser origin.`)}if(t.protocol!==`https:`||t.origin!==e||t.pathname!==`/`||t.search||t.hash||t.username||t.password)throw Error(`preview.appOrigin must be an exact HTTPS browser origin.`);return t}function j(e){let t;try{t=new URL(`https://${e}`)}catch{throw Error(`preview.tunnel.hostname must be an exact hostname.`)}if(t.hostname!==e||t.origin!==`https://${e}`)throw Error(`preview.tunnel.hostname must be an exact hostname.`);return t.hostname}function M(e,t){return`http://${P(N(e))}:${t}`}function N(e){return e===`0.0.0.0`||e===`::`?`127.0.0.1`:e}function P(e){return e.includes(`:`)&&!e.startsWith(`[`)?`[${e}]`:e}export{C as createPreviewEnvironment,S as resolvePreviewConfig,x as runPreview};
1
+ import{preparePreviewTunnel as e}from"./cloudflare.js";import{copyFile as t,glob as n,mkdir as r,mkdtemp as i,open as a,readFile as o,stat as s}from"node:fs/promises";import{tmpdir as c}from"node:os";import l from"node:path";import u from"node:process";import{parseEnv as d}from"node:util";import{coreframeLocalWorkerVariables as f,readProjectConfig as p}from"@coreframe/config";import{migrateLocalDb as m,runProjectSeed as h,startProjectPostgres as g,startProjectTurso as _}from"@coreframe/db";import v from"nano-spawn";import{supervise as y}from"procband";const b=[`DATABASE_URL`,`PGDATABASE`,`PGHOST`,`PGPASSWORD`,`PGPORT`,`PGUSER`,`TURSO_AUTH_TOKEN`,`TURSO_DATABASE_URL`],x=[`POSTHOG_`,`VITE_POSTHOG_`,`VITE_GA_`],S=[`CI`,`COLORTERM`,`COMSPEC`,`ComSpec`,`FORCE_COLOR`,`HOME`,`LANG`,`NODE_EXTRA_CA_CERTS`,`NODE_OPTIONS`,`NO_COLOR`,`PATH`,`PATHEXT`,`PNPM_HOME`,`SHELL`,`SSL_CERT_DIR`,`SSL_CERT_FILE`,`SYSTEMROOT`,`SystemRoot`,`TEMP`,`TERM`,`TMP`,`TMPDIR`,`USERPROFILE`],C=u.platform===`win32`?`pnpm.cmd`:`pnpm`;async function w({cwd:e=u.cwd(),env:t=u.env,host:n,port:r},i={}){let a=await p({cwd:e}),o=T(a,{host:n,port:r}),s=await M(e),c=D({appOrigin:o.appOrigin,config:a,env:t,environmentFiles:s}),l=o.access?{CLOUDFLARE_API_TOKEN:t.CLOUDFLARE_API_TOKEN??s.CLOUDFLARE_API_TOKEN}:void 0,d=j({config:a,cwd:e,env:c}),f=await k();console.log(`Preview output: ${f.path}`);let m,h,g=new Set,_=!1,v,y=()=>{if(!_){_=!0;for(let e of g)e.nodeChildProcess.then(e=>e.kill(`SIGTERM`)).catch(()=>void 0);m?.kill(`SIGTERM`),d.stop()}},b=[`SIGINT`,`SIGTERM`].map(e=>{let t=()=>{v??=e,y()};return u.on(e,t),{handler:t,signal:e}});try{await d.start();let n=O({config:o,cwd:e,database:d,env:c,accessEnvironment:l,output:f,processEnvironment:t,commands:g,onServe(e){m=e.server,h=e.disposeTunnel},isStopping:()=>_,onStop:y});await n.database.migrate(),await n.database.seed(),await i.beforeBuild?.(n),await n.build(),await i.afterBuild?.(n),await n.serve()}finally{let e=[...g];y();try{await d.dispose()}finally{try{await Promise.allSettled([...e,m?.wait()].filter(Boolean)),await h?.()}finally{try{await f.close()}finally{for(let{handler:e,signal:t}of b)u.off(t,e)}}v&&(u.exitCode??=v===`SIGINT`?130:143)}}}function T(e,t={}){let n=e.preview??{},r=t.host??n.host??`127.0.0.1`,i=t.port??n.port??4173,a=n.strictPort??!0,o=n.appOrigin;if(!Number.isInteger(i)||i<1||i>65535)throw Error(`Preview port must be an integer between 1 and 65535.`);if(!r.trim())throw Error(`Preview host must not be empty.`);return o&&I(o),n.tunnel&&P(n.tunnel,o,a),n.access&&F(n.access,n.tunnel,o),{access:n.access,appOrigin:o,host:r,port:i,strictPort:a,tunnel:n.tunnel}}async function E({appOrigin:e,config:t,cwd:n,env:r}){return D({appOrigin:e,config:t,env:r,environmentFiles:await M(n)})}function D({appOrigin:e,config:t,env:n,environmentFiles:r}){let i=Object.keys(t.worker?.environment??{}).filter(e=>!x.some(t=>e.startsWith(t))),a=new Set([...f,...b,...i]),o={},s=new Set([...Object.keys(n),...Object.keys(r)].filter(e=>e.startsWith(`VITE_`)&&!x.some(t=>e.startsWith(t))));for(let e of[...S,...Object.keys(n).filter(e=>e.startsWith(`LC_`))])n[e]!==void 0&&(o[e]=n[e]);for(let e of a){let t=n[e]??r[e];t!==void 0&&(o[e]=t)}for(let e of s){let t=n[e]??r[e];t!==void 0&&(o[e]=t)}return e!==void 0&&(a.add(`APP_ORIGIN`),o.APP_ORIGIN=e),o.CLOUDFLARE_ENV=`preview`,o.COREFRAME_ENV=`preview`,o.COREFRAME_PLATFORM=`web`,o.COREFRAME_PREVIEW_WORKER_VARIABLES=JSON.stringify([...a].filter(e=>!x.some(t=>e.startsWith(t)))),o}function O({accessEnvironment:i,config:a,cwd:o,database:c,env:d,output:f,processEnvironment:p,commands:m,onServe:h,isStopping:g,onStop:_}){let b=!1,x={cwd:o,database:c,env:d,host:a.host,origin:a.appOrigin??R(a.host,a.port),port:a.port,async build(){await x.run(C,[`run`,`build`,`--mode`,`preview`])},report(e,t){console.log(t===void 0?e:`${e}: ${t}`)},async run(e,t=[]){let n=v(e,t,{cwd:o,env:d,stdin:`inherit`,stderr:f.fd,stdout:f.fd});m.add(n);try{await n}finally{m.delete(n)}},async serve(){if(b)throw Error(`Preview serving can only be started once.`);b=!0;let t,n,r;try{if(t=a.tunnel?await e({appOrigin:a.appOrigin,access:a.access,accessEnv:i,config:a.tunnel,cwd:o,env:p}):void 0,g()){await t?.dispose();return}n=y({name:`preview`,command:C,args:[`exec`,`vite`,`preview`,`--mode`,`preview`,`--outDir`,`dist/client`,`--host`,a.host,`--port`,String(a.port),...a.strictPort?[`--strictPort`]:[]],cwd:o,env:d,prefix:!1,stdin:u.stdin}),r=t?v(u.platform===`win32`?`cloudflared.exe`:`cloudflared`,[`tunnel`,`run`,`--url`,`http://${B(z(a.host))}:${a.port}`,`--credentials-file`,t.credentialsFile,t.tunnel.tunnelId],{cwd:o,env:d,stdin:`ignore`,stderr:f.fd,stdout:f.fd}):void 0,r&&m.add(r)}catch(e){throw n?.kill(`SIGTERM`),await n?.wait(),await t?.dispose(),e}try{h({disposeTunnel:t?.dispose,server:n}),x.report(`Preview`,x.url(`/`)),t&&(x.report(`Cloudflare tunnel`,`${t.tunnel.name} (${t.tunnel.tunnelId})`),t.access&&x.report(`Cloudflare Access bypass`,t.access.policyName)),await Promise.race(r?[n.expectSuccess(),r]:[n.expectSuccess()])}catch(e){if(g()||_(),await t?.dispose(),g())return;throw e}},async stageArtifact({label:e,pathname:i,source:a}){let c=A(o,i),u=[];for await(let e of n(a,{cwd:o}))(await s(l.resolve(o,e))).isFile()&&u.push(e);if(u.length===0)throw Error(`Preview artifact source ${JSON.stringify(a)} did not match a file.`);if(u.length>1)throw Error(`Preview artifact source ${JSON.stringify(a)} matched multiple files: ${u.sort().join(`, `)}.`);await r(l.dirname(c),{recursive:!0}),await t(l.resolve(o,u[0]),c);let d=x.url(i);return x.report(e,d),d},url(e=`/`){return new URL(e,`${x.origin.replace(/\/$/u,``)}/`).toString()}};return x}async function k(){let e=await i(l.join(c(),`coreframe-preview-`)),t=l.join(e,`preview.log`),n=await a(t,`wx`,384);return{close:()=>n.close(),fd:n.fd,path:t}}function A(e,t){let n=new URL(t,`https://coreframe.invalid`);if(!t.startsWith(`/`)||t===`/`||t.includes(`\\`)||t.includes(`?`)||t.includes(`#`)||n.origin!==`https://coreframe.invalid`||n.pathname!==t||l.posix.normalize(t)!==t)throw Error(`Preview artifact pathname must be a normalized absolute URL path.`);return l.resolve(e,`dist/client`,`.${t}`)}function j({config:e,cwd:t,env:n}){let r=e.database?.dialect??`postgres`,i,a,o,s,c=!1,l=n,u=()=>a?(s??=a[Symbol.asyncDispose](),s):Promise.resolve(),d=()=>{c||(c=!0,u().catch(()=>void 0))};return{dialect:r,get env(){return l},async migrate(){await m({config:e,cwd:t,env:l})},async seed(){await h(`default`,{cwd:t,env:l,seedEntryPoint:e.database?.seed})},async start(){if(r===`sqlite`){a=await _({config:e,cwd:t,env:l,useSupervisorLease:!0}),Object.assign(l,a.env);return}i=await g({config:e,cwd:t,env:l,failIfRunning:!0,postgresOutput:`on-error`,useSupervisorLease:!0}),Object.assign(l,i.env)},stop:d,async dispose(){o??=(async()=>{d(),await Promise.allSettled([i?.[Symbol.asyncDispose](),u()]),i=void 0,a=void 0})(),await o}}}async function M(e){let t={};for(let n of[`.env`,`.env.local`,`.env.preview`,`.env.preview.local`]){let r=await N(l.resolve(e,n));r&&(t={...t,...r})}let n=await N(l.resolve(e,`.dev.vars`));n&&(t={...t,...n});let r=await N(l.resolve(e,`.dev.vars.preview`));return r&&(t={...t,...r}),t}async function N(e){try{return Object.fromEntries(Object.entries(d(await o(e,`utf8`))).filter(e=>e[1]!==void 0))}catch(e){if(e.code===`ENOENT`)return;throw e}}function P(e,t,n){if(!t)throw Error(`preview.appOrigin is required when preview.tunnel is configured.`);if(!n)throw Error(`preview.strictPort must be true when preview.tunnel is configured.`);let r=[`credentialsFile`,`tokenEnv`,`tokenFile`],i=e,a=r.find(e=>e in i);if(a)throw Error(`preview.tunnel.${a} is no longer supported. Coreframe preview now provisions Cloudflare credentials through the operating system credential store.`);if(!e.name.trim())throw Error(`preview.tunnel.name must not be empty.`);if(!/^[a-z0-9][a-z0-9._-]*$/u.test(e.profile))throw Error(`preview.tunnel.profile must start with a lowercase letter or number and contain only lowercase letters, numbers, dots, underscores, or hyphens.`);if(e.dns!==`managed`&&e.dns!==`external`)throw Error(`preview.tunnel.dns must be either "managed" or "external".`);let o=L(e.hostname);if(new URL(t).hostname!==o)throw Error(`preview.appOrigin hostname must match preview.tunnel.hostname.`)}function F(e,t,n){if(!t)throw Error(`preview.tunnel is required when preview.access is configured.`);if(!n)throw Error(`preview.appOrigin is required when preview.access is configured.`);if(e.applicationId!==void 0&&!e.applicationId.trim())throw Error(`preview.access.applicationId must not be empty.`);if(!e.bypass||typeof e.bypass!=`object`)throw Error(`preview.access.bypass must be configured.`);if(e.bypass.policyName!==void 0&&!e.bypass.policyName.trim())throw Error(`preview.access.bypass.policyName must not be empty.`);let{ips:r}=e.bypass;if(typeof r!=`string`&&!Array.isArray(r)&&typeof r!=`function`)throw Error(`preview.access.bypass.ips must be an IP, an array of IPs, or a resolver.`);if(Array.isArray(r)&&r.length===0)throw Error(`preview.access.bypass.ips must contain at least one IP.`)}function I(e){let t;try{t=new URL(e)}catch{throw Error(`preview.appOrigin must be an exact HTTPS browser origin.`)}if(t.protocol!==`https:`||t.origin!==e||t.pathname!==`/`||t.search||t.hash||t.username||t.password)throw Error(`preview.appOrigin must be an exact HTTPS browser origin.`);return t}function L(e){let t;try{t=new URL(`https://${e}`)}catch{throw Error(`preview.tunnel.hostname must be an exact hostname.`)}if(t.hostname!==e||t.origin!==`https://${e}`)throw Error(`preview.tunnel.hostname must be an exact hostname.`);return t.hostname}function R(e,t){return`http://${B(z(e))}:${t}`}function z(e){return e===`0.0.0.0`||e===`::`?`127.0.0.1`:e}function B(e){return e.includes(`:`)&&!e.startsWith(`[`)?`[${e}]`:e}export{E as createPreviewEnvironment,T as resolvePreviewConfig,w as runPreview};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coreframe/preview",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Production-like local preview lifecycle utilities for Coreframe apps",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,8 +21,8 @@
21
21
  "@napi-rs/keyring": "1.3.0",
22
22
  "nano-spawn": "^2.1.0",
23
23
  "procband": "^0.3.8",
24
- "@coreframe/db": "^0.1.14",
25
- "@coreframe/config": "^0.1.10"
24
+ "@coreframe/config": "^0.1.12",
25
+ "@coreframe/db": "^0.1.16"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^26.1.0",