@gfargo/doorman 3.10.1 → 3.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/run.js CHANGED
@@ -37,9 +37,9 @@ ${W}`)}),R.result}),{toAdd:l,toUpdate:c,toDelete:d}=this.diffRules(lt(t.rules),a
37
37
  ${l.formatWarning(a)}`)}),o.push(s.result)}return o}async syncRules(t,o={}){let{dryRun:n=!1}=o;try{let{OperationSafety:s}=(bt(),ye(vt)),a=await s.performDryRunValidation(t,"sync rules",async v=>this.getChanges(v));if(!a.valid)throw new Error(`Dry-run validation failed: ${a.issues.join(", ")}`);let l=a.changes,{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u}=l,p=l.ipsToAdd||[],g=l.ipsToDelete||[];if(n)return r.info("Dry run mode. The following changes would be made:"),r.info(`Custom Rules - Add: ${c.length}, Update: ${d.length}, Delete: ${u.length}`),r.info(`IP Rules - Add: ${p.length}, Delete: ${g.length}`),{success:!0,rulesAdded:0,rulesUpdated:0,rulesDeleted:0,ipsAdded:0,ipsUpdated:0,ipsDeleted:0,warnings:a.warnings};let h=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Fastly workspace ${this.client.workspaceId}`,changes:l,riskLevel:h,skipConfirmation:o.force||!1,dryRun:!1,allowDeletions:o.allowDeletions||!1}))throw new Error("Operation cancelled by user");let R=[];Rt(t.rules)&&R.push("Rule `priority` has no effect on Fastly: Next-Gen WAF rules are evaluated independently and have no API-exposed ordering concept to honour.");let N=c.map(v=>({rule:v,translated:k.unifiedToFastly(v)})),H=d.map(v=>({rule:v,translated:k.unifiedToFastly(v)})),W=[],ue=(v,V)=>`${v}: ${V instanceof Error?V.message:String(V)}`,me=[],_e=[],_=[],Ce=[];for(let v of u)if(v.id)try{r.debug(`Deleting Fastly rule: ${v.id}`),await this.client.deleteRule(v.id),_.push(v.id)}catch(V){r.error(`Failed to delete Fastly rule ${v.id}:`,V),W.push(ue(`Failed to delete rule ${v.id}`,V))}for(let{rule:v,translated:V}of N){let ie=V.result;try{r.debug(`Adding new Fastly rule: ${ie.description}`);let ae=await this.client.createRule(ie);me.push(ae),Ce.push({oldId:v.id||void 0,newId:ae.id,name:ie.description})}catch(ae){r.error(`Failed to add Fastly rule "${ie.description}":`,ae),W.push(ue(`Failed to add rule "${ie.description}"`,ae))}}for(let{rule:v,translated:V}of H)if(v.id)try{r.debug(`Updating Fastly rule: ${v.id}`);let ie=await this.client.updateRule(v.id,V.result);_e.push(ie)}catch(ie){r.error(`Failed to update Fastly rule ${v.id}:`,ie),W.push(ue(`Failed to update rule ${v.id}`,ie))}let ve=0,be=0,We=t.ips||[],{result:Se,warnings:je}=k.unifiedIPsToFastlyEntries(We);je.forEach(v=>R.push(v.message));try{for(let[v,V]of[[dt,Se.denyEntries],[ut,Se.allowEntries]]){let ae=(await this.client.findList(v))?.entries??[],it=V.filter(f=>!ae.includes(f)),ot=ae.filter(f=>!V.includes(f));if(it.length===0&&ot.length===0)continue;let Ee=await this.client.getOrCreateList(v);await this.client.updateListEntries(Ee.id,V),ve+=it.length,be+=ot.length}}catch(v){r.error("Failed to sync Fastly IP lists:",v),W.push(ue("Failed to sync IP lists",v))}return r.debug(`Custom Rules: Added: ${me.length}, Updated: ${_e.length}, Deleted: ${_.length}`),r.debug(`IP Rules: Added: ${ve}, Deleted: ${be}`),{success:W.length===0,rulesAdded:me.length,rulesUpdated:_e.length,rulesDeleted:_.length,ipsAdded:ve,ipsUpdated:0,ipsDeleted:be,idRemappings:Ce.length>0?Ce:void 0,errors:W.length>0?W:void 0,warnings:R.length>0?R:void 0}}catch(s){throw r.error("Error during sync:",s),new Error(`Failed to synchronize firewall rules: ${s instanceof Error?s.message:String(s)}`,{cause:s})}}async getChanges(t){let o=Le.safeParse(t);if(!o.success)throw new Error(`Invalid firewall configuration: ${o.error.message}`);try{r.debug("Fetching existing Fastly configuration");let[n,s,a]=await Promise.all([this.client.fetchRules(),this.client.findList(dt),this.client.findList(ut)]),l=this.translateFastlyRules(n),{toAdd:c,toUpdate:d,toDelete:u}=this.diffRules(t.rules,l),p=s?.entries??[],g=a?.entries??[],h=[...k.fastlyListEntriesToUnified(p,"deny"),...k.fastlyListEntriesToUnified(g,"allow")],{ipsToAdd:I,ipsToDelete:R}=this.diffIPs(t.ips||[],h);return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:I,ipsToUpdate:[],ipsToDelete:R,hasChanges:c.length>0||d.length>0||u.length>0||I.length>0||R.length>0}}catch(n){throw r.error("Error fetching existing Fastly configuration:",n),new Error("Failed to fetch existing Fastly configuration")}}getSupportedFeatures(){return{supportsCustomRules:!0,supportsIPBlocking:!0,supportsRateLimiting:!0,supportsManagedRules:!1,supportsGeoBlocking:!0,supportsRedirect:!0,supportsChallenge:!0}}async verifyCredentials(){return this.client.verifyCredentials()}validateConfig(t){let o=super.validateConfig(t),n=[...o.errors],s=[...o.warnings];return t.provider&&t.provider!=="fastly"&&n.push({path:"provider",message:"Provider must be 'fastly' for FastlyFirewallService",code:"INVALID_PROVIDER"}),{valid:n.length===0,errors:n,warnings:s}}getHealthScore(t){let o=super.getHealthScore(t),n=[...o.issues],s=o.score;return t.rules.filter(l=>l.action?.type==="rate_limit").length===0&&(s-=10,n.push({severity:"info",category:"security",message:"No rate limiting rules configured",suggestion:"Consider adding rate limiting to protect against abuse"})),(!t.ips||t.ips.length===0)&&n.push({severity:"info",category:"security",message:"No IP blocking rules configured",suggestion:"Consider blocking known malicious IPs"}),{score:Math.max(0,s),grade:o.grade,issues:n,recommendations:o.recommendations}}diffRules(t,o){let n=[],s=[],a=[...o];for(let l of t){let c=o.find(d=>d.id===l.id);if(!c)n.push(l);else{Ke(ke(l),ke(c))||s.push({...l,id:c.id});let d=a.findIndex(u=>u.id===c.id);d!==-1&&a.splice(d,1)}}return{toAdd:n,toUpdate:s,toDelete:a}}diffIPs(t,o){let n=l=>`${l.action}:${l.ip}`,s=new Set(o.map(n)),a=new Set(t.map(n));return{ipsToAdd:t.filter(l=>!s.has(n(l))),ipsToDelete:o.filter(l=>!a.has(n(l)))}}};T();var tt=class{static fromEnv(){let e=process.env.FASTLY_API_TOKEN,t=process.env.FASTLY_WORKSPACE_ID;if(!e)throw new Error("FASTLY_API_TOKEN environment variable is required");if(!t)throw new Error("FASTLY_WORKSPACE_ID environment variable is required");let o=new Xe(t,e);return new et(o)}static fromConfig(e){let t=e.apiToken||process.env.FASTLY_API_TOKEN,o=e.workspaceId||process.env.FASTLY_WORKSPACE_ID;if(!t)throw new Error("Fastly API token is required (provide apiToken or set FASTLY_API_TOKEN env var)");if(!o)throw new Error("Fastly workspace ID is required (provide workspaceId or set FASTLY_WORKSPACE_ID env var)");r.debug("Creating Fastly provider with config:",{workspaceId:o});let n=new Xe(o,t);return new et(n)}static create(e,t){return this.fromConfig({workspaceId:e,apiToken:t})}};var Zt={provider:"vercel",fields:[{key:"token",envVar:"VERCEL_TOKEN",label:"Vercel API Token",required:!0,secret:!0},{key:"projectId",envVar:"VERCEL_PROJECT_ID",label:"Vercel Project ID",required:!0,configKey:"projectId"},{key:"teamId",envVar:"VERCEL_TEAM_ID",label:"Vercel Team ID",required:!1,configKey:"teamId"}]};var Yt={provider:"cloudflare",fields:[{key:"apiToken",envVar:"CLOUDFLARE_API_TOKEN",label:"Cloudflare API Token",required:!0,secret:!0},{key:"zoneId",envVar:"CLOUDFLARE_ZONE_ID",label:"Cloudflare Zone ID",required:!0,configKey:"zoneId"},{key:"accountId",envVar:"CLOUDFLARE_ACCOUNT_ID",label:"Cloudflare Account ID",required:!1,configKey:"accountId"}]};var Jt={provider:"fastly",fields:[{key:"apiToken",envVar:"FASTLY_API_TOKEN",label:"Fastly API Token",required:!0,secret:!0},{key:"workspaceId",envVar:"FASTLY_WORKSPACE_ID",label:"Fastly Next-Gen WAF Workspace ID",required:!0,configKey:"workspaceId"}]};function Tt(i,e={}){let{explicit:t={},config:o={},env:n=process.env}=e,s={};for(let a of i.fields)s[a.key]=t[a.key]||o[a.key]||n[a.envVar];return s}async function fo(i){let e;if(i.provider)e=i.provider,r.debug(`Using explicitly specified provider: ${e}`);else{let t=mt.detect(i.config);t.provider?(e=t.provider,r.debug(`Auto-detected provider: ${e} (${t.confidence} confidence)`),t.reasons.length>0&&r.debug(`Reasons: ${t.reasons.join(", ")}`)):i.interactive!==!1?e=await ar():(e="vercel",r.warn("No provider detected, defaulting to Vercel"))}try{if(e==="vercel")return await nr(i);if(e==="cloudflare")return{provider:await rr(i)};if(e==="fastly")return{provider:await sr(i)};throw new Error(`Unknown provider: ${e}`)}catch(t){throw r.error(`Failed to initialize ${e} provider:`,t),t}}async function nr(i){let e=i.config,{token:t,projectId:o,teamId:n}=Tt(Zt,{explicit:{token:i.token,projectId:i.projectId,teamId:i.teamId},config:{projectId:e?.providers?.vercel?.projectId??e?.projectId,teamId:e?.providers?.vercel?.teamId??e?.teamId}});if(!t||!o){if(i.interactive===!1)throw new Error("Vercel credentials missing. Provide token and projectId.");r.warn("Missing Vercel credentials"),r.info("Please provide your Vercel credentials:");let{promptForCredentials:a}=await Promise.resolve().then(()=>(co(),lo)),l=await a({token:t,projectId:o,teamId:n});return{provider:Ze.fromConfig({token:l.token,projectId:l.projectId,teamId:l.teamId}),vercelCredentials:l}}return{provider:Ze.fromConfig({token:t,projectId:o,teamId:n}),vercelCredentials:{token:t,projectId:o,teamId:n}}}async function rr(i){let e=i.config,{apiToken:t,zoneId:o,accountId:n}=Tt(Yt,{explicit:{apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId},config:{zoneId:e?.providers?.cloudflare?.zoneId,accountId:e?.providers?.cloudflare?.accountId}});if(!t||!o){if(i.interactive===!1)throw new Error("Cloudflare credentials missing. Provide apiToken and zoneId.");r.warn("Missing Cloudflare credentials"),r.info("Please provide your Cloudflare credentials:");let s=t||await rt("Cloudflare API Token: "),a=o||await w("Cloudflare Zone ID:",{type:"text"}),l=n||await w("Cloudflare Account ID (optional):",{type:"text"});return Qe.fromConfig({apiToken:s,zoneId:a,accountId:l||void 0})}return Qe.fromConfig({apiToken:t,zoneId:o,accountId:n})}async function sr(i){let e=i.config,{apiToken:t,workspaceId:o}=Tt(Jt,{explicit:{apiToken:i.apiToken,workspaceId:i.workspaceId},config:{workspaceId:e?.providers?.fastly?.workspaceId}});if(!t||!o){if(i.interactive===!1)throw new Error("Fastly credentials missing. Provide apiToken and workspaceId.");r.warn("Missing Fastly credentials"),r.info("Please provide your Fastly credentials:");let n=t||await rt("Fastly API Token: "),s=o||await w("Fastly Next-Gen WAF Workspace ID:",{type:"text"});return tt.fromConfig({apiToken:n,workspaceId:s})}return tt.fromConfig({apiToken:t,workspaceId:o})}async function ar(){r.info(po.default.yellow("Unable to auto-detect firewall provider.")),r.info(`Please select a provider:
38
38
  `),re.forEach((s,a)=>{r.info(` ${a+1}. ${uo(s)}`)}),r.info("");let i=await w(`Select provider (1-${re.length}):`,{type:"text"}),e=String(i).trim().toLowerCase(),t=re.find(s=>s===e);if(t)return t;let o=Number(e);if(Number.isInteger(o)&&o>=1&&o<=re.length)return re[o-1];let n=re[0];return r.warn(`Invalid choice: ${i}, defaulting to ${uo(n)}`),n}function uo(i){switch(i){case"vercel":return"Vercel Firewall";case"cloudflare":return"Cloudflare WAF";case"fastly":return"Fastly Next-Gen WAF";default:return i}}async function J(i,e){try{i.debug&&(r.level=go.LogLevels.debug);let t;if(i.optionalConfig)try{t=await j(i.config,"optional")}catch{t={}}else i.skipValidation?t=await j(i.config,"raw"):t=await j(i.config,"required");let{provider:o,vercelCredentials:n}=await fo({provider:i.provider,config:t,interactive:!i.ci,token:i.token,projectId:i.projectId,teamId:i.teamId,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId}),s=n?.token??"",a=n?.projectId??"",l=n?.teamId??"";await e({config:t,provider:o,token:s,projectId:a,teamId:l})}catch(t){Z(t,i.errorContext)}}var lr="backup",cr="Backup or restore firewall configurations",dr={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},output:{alias:"o",type:"string",description:"Output directory for backups",default:"./backups"},restore:{alias:"r",type:"string",description:"Restore from backup file"},list:{alias:"l",type:"boolean",description:"List available backups",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},ur=async i=>{try{i.debug&&(r.level=mo.LogLevels.debug);let e=i.output||"./backups";if(i.list){if(!(0,ge.existsSync)(e)){r.info(Q.default.yellow("No backup directory found."));return}let t=(0,ge.readdirSync)(e).filter(o=>o.endsWith(".json")).map(o=>{let n=(0,At.join)(e,o),s=(0,ge.statSync)(n);return{name:o,size:s.size,created:s.mtime}}).sort((o,n)=>n.created.getTime()-o.created.getTime());if(t.length===0){r.info(Q.default.yellow("No backups found."));return}r.log(Q.default.bold(`
39
39
  \u{1F4E6} Available Backups:
40
- `)),t.forEach(o=>{r.log(`${Q.default.cyan(o.name)}`),r.log(` ${Q.default.dim("Created:")} ${o.created.toLocaleString()}`),r.log(` ${Q.default.dim("Size:")} ${(o.size/1024).toFixed(1)} KB`),r.log("")});return}if(i.restore){let t=i.restore.startsWith("/")?i.restore:(0,At.join)(e,i.restore);(0,ge.existsSync)(t)||(r.error(`Backup file not found: ${t}`),process.exit(1));let o=await j(t,"raw"),n=i.config||".doorman.json";if((0,ge.existsSync)(n)&&!await w(`Config file ${n} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(Q.default.yellow("Restore cancelled."));return}let{backup:s,...a}=o;await q(a,n),r.success(Q.default.green(`\u2705 Restored configuration from ${t} to ${n}`));return}await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"creating backup"},async({provider:t,projectId:o,teamId:n})=>{r.start("Fetching current remote configuration...");let s=await t.fetchConfig(),a=s.metadata?.version??s.version;Re.getInstance().validateConfig(s),(0,ge.existsSync)(e)||(0,ge.mkdirSync)(e,{recursive:!0});let c=new Date,d=c.toISOString().split("T")[0]??"unknown-date",u=(c.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",g=`firewall-backup-${`${d}_${u}`}.json`,h=(0,At.join)(e,g),I={...s,backup:{createdAt:new Date().toISOString(),source:"remote",provider:t.name,...t.name==="vercel"?{projectId:o,teamId:n}:{},originalVersion:a}};await q(I,h,{validate:!1}),r.success(Q.default.green(`\u2705 Backup created: ${h}`)),r.log(""),r.log(Q.default.bold("Backup Details:")),r.log(`${Q.default.dim("Version:")} ${a??"unknown"}`),r.log(`${Q.default.dim("Rules:")} ${s.rules.length} custom, ${s.ips?.length??0} IP blocking`),r.log(`${Q.default.dim("Created:")} ${new Date().toLocaleString()}`),r.log(""),r.log(Q.default.dim("To restore this backup later, run:")),r.log(Q.default.cyan(`doorman backup --restore ${g}`))})}catch(e){Z(e,"managing backup")}};var Xt={};M(Xt,{builder:()=>gr,command:()=>pr,desc:()=>fr,handler:()=>mr});var X=S(require("chalk"));T();function le(i){if(we(i))return i;let e=i,t=e.rules.map(n=>k.vercelToUnified(n).result),o=(e.ips??[]).map(n=>k.vercelIPToUnified(n));return{version:"2.0",provider:"vercel",providers:{vercel:{projectId:e.projectId,teamId:e.teamId}},rules:t,ips:o,metadata:{version:e.version,updatedAt:e.updatedAt}}}function Pt(i,e){let t=!1,o=we(i),n=o?{...i,metadata:{...i.metadata}}:{...i};if(e.version!==void 0)if(o){let s=n;s.metadata?.version!==e.version&&(s.metadata={...s.metadata,version:e.version,updatedAt:e.updatedAt??new Date().toISOString()},t=!0)}else{let s=n;s.version!==e.version&&(s.version=e.version,s.updatedAt=e.updatedAt??new Date().toISOString(),t=!0)}if(e.idRemappings&&e.idRemappings.length>0){let s=new Map(e.idRemappings.filter(d=>d.oldId).map(d=>[d.oldId,d.newId])),a=new Map(e.idRemappings.filter(d=>!d.oldId).map(d=>[d.name,d.newId])),l=!1,c=n.rules.map(d=>{let u=d.id?s.get(d.id):a.get(d.name);return u&&u!==d.id?(l=!0,{...d,id:u}):d});l&&(n={...n,rules:c},t=!0)}return{config:n,changed:t}}function ho(i,e){if(we(e))return{...e,...i};let t=e,o=i.rules.map(s=>k.unifiedToVercel(s).result),n=(i.ips??[]).map(s=>({id:s.id||"",ip:s.ip,hostname:s.hostname||"",action:s.action,notes:s.notes}));return{...t,projectId:i.providers?.vercel?.projectId??t.projectId,teamId:i.providers?.vercel?.teamId??t.teamId,version:i.metadata?.version,updatedAt:i.metadata?.updatedAt,rules:o,ips:n}}var pr="diff",fr="Show detailed differences between local and remote firewall configuration",gr={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",choices:["table","json"],description:"Output format",default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},mr=async i=>{await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"calculating diff"},async({config:e,provider:t})=>{await hr(t,le(e),i)})};async function hr(i,e,t){r.start("Calculating differences...");let o=await i.getChanges(e);if(!o.hasChanges){r.success(X.default.green("No differences found. Local and remote configurations are in sync."));return}let n=o.ipsToAdd??[],s=o.ipsToUpdate??[],a=o.ipsToDelete??[];if(t.format==="json"){r.log(JSON.stringify({rules:{toAdd:o.rulesToAdd,toUpdate:o.rulesToUpdate,toDelete:o.rulesToDelete},ips:{toAdd:n,toUpdate:s,toDelete:a},summary:{hasChanges:o.hasChanges,ruleChanges:o.rulesToAdd.length+o.rulesToUpdate.length+o.rulesToDelete.length,ipChanges:n.length+s.length+a.length}},null,2));return}r.log(X.default.bold(`
40
+ `)),t.forEach(o=>{r.log(`${Q.default.cyan(o.name)}`),r.log(` ${Q.default.dim("Created:")} ${o.created.toLocaleString()}`),r.log(` ${Q.default.dim("Size:")} ${(o.size/1024).toFixed(1)} KB`),r.log("")});return}if(i.restore){let t=i.restore.startsWith("/")?i.restore:(0,At.join)(e,i.restore);(0,ge.existsSync)(t)||(r.error(`Backup file not found: ${t}`),process.exit(1));let o=await j(t,"raw"),n=i.config||".doorman.json";if((0,ge.existsSync)(n)&&!await w(`Config file ${n} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(Q.default.yellow("Restore cancelled."));return}let{backup:s,...a}=o;await q(a,n),r.success(Q.default.green(`\u2705 Restored configuration from ${t} to ${n}`));return}await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"creating backup"},async({provider:t,projectId:o,teamId:n})=>{r.start("Fetching current remote configuration...");let s=await t.fetchConfig(),a=s.metadata?.version??s.version;Re.getInstance().validateConfig(s),(0,ge.existsSync)(e)||(0,ge.mkdirSync)(e,{recursive:!0});let c=new Date,d=c.toISOString().split("T")[0]??"unknown-date",u=(c.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",g=`firewall-backup-${`${d}_${u}`}.json`,h=(0,At.join)(e,g),I={...s,backup:{createdAt:new Date().toISOString(),source:"remote",provider:t.name,...t.name==="vercel"?{projectId:o,teamId:n}:{},originalVersion:a}};await q(I,h,{validate:!1}),r.success(Q.default.green(`\u2705 Backup created: ${h}`)),r.log(""),r.log(Q.default.bold("Backup Details:")),r.log(`${Q.default.dim("Version:")} ${a??"unknown"}`),r.log(`${Q.default.dim("Rules:")} ${s.rules.length} custom, ${s.ips?.length??0} IP blocking`),r.log(`${Q.default.dim("Created:")} ${new Date().toLocaleString()}`),r.log(""),r.log(Q.default.dim("To restore this backup later, run:")),r.log(Q.default.cyan(`doorman backup --restore ${g}`))})}catch(e){Z(e,"managing backup")}};var Xt={};M(Xt,{builder:()=>gr,command:()=>pr,desc:()=>fr,handler:()=>mr});var X=S(require("chalk"));T();function le(i){if(we(i))return i;let e=i,t=e.rules.map(n=>k.vercelToUnified(n).result),o=(e.ips??[]).map(n=>k.vercelIPToUnified(n));return{version:"2.0",provider:"vercel",providers:{vercel:{projectId:e.projectId,teamId:e.teamId}},rules:t,ips:o,metadata:{version:e.version,updatedAt:e.updatedAt}}}function Pt(i,e){let t=!1,o=we(i),n=o?{...i,metadata:{...i.metadata}}:{...i};if(e.version!==void 0)if(o){let s=n;s.metadata?.version!==e.version&&(s.metadata={...s.metadata,version:e.version,updatedAt:e.updatedAt??new Date().toISOString()},t=!0)}else{let s=n;s.version!==e.version&&(s.version=e.version,s.updatedAt=e.updatedAt??new Date().toISOString(),t=!0)}if(e.idRemappings&&e.idRemappings.length>0){let s=new Map(e.idRemappings.filter(d=>d.oldId).map(d=>[d.oldId,d.newId])),a=new Map(e.idRemappings.filter(d=>!d.oldId).map(d=>[d.name,d.newId])),l=!1,c=n.rules.map(d=>{let u=d.id?s.get(d.id):a.get(d.name);return u&&u!==d.id?(l=!0,{...d,id:u}):d});l&&(n={...n,rules:c},t=!0)}return{config:n,changed:t}}function ho(i,e){if(we(e))return{...e,...i};let t=e,o=i.rules.map(s=>k.unifiedToVercel(s).result),n=(i.ips??[]).map(s=>({id:s.id||"",ip:s.ip,hostname:s.hostname||"",action:s.action,notes:s.notes}));return{...t,projectId:i.providers?.vercel?.projectId??t.projectId,teamId:i.providers?.vercel?.teamId??t.teamId,version:i.metadata?.version,updatedAt:i.metadata?.updatedAt,rules:o,ips:n}}var pr="diff",fr="Show detailed differences between local and remote firewall configuration",gr={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",choices:["table","json"],description:"Output format",default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},mr=async i=>{await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"calculating diff"},async({config:e,provider:t})=>{await hr(t,le(e),i)})};async function hr(i,e,t){let o=t.format==="json";o||r.start("Calculating differences...");let n=await i.getChanges(e),s=n.ipsToAdd??[],a=n.ipsToUpdate??[],l=n.ipsToDelete??[];if(o){r.log(JSON.stringify({rules:{toAdd:n.rulesToAdd,toUpdate:n.rulesToUpdate,toDelete:n.rulesToDelete},ips:{toAdd:s,toUpdate:a,toDelete:l},summary:{hasChanges:n.hasChanges,ruleChanges:n.rulesToAdd.length+n.rulesToUpdate.length+n.rulesToDelete.length,ipChanges:s.length+a.length+l.length}},null,2));return}if(!n.hasChanges){r.success(X.default.green("No differences found. Local and remote configurations are in sync."));return}r.log(X.default.bold(`
41
41
  \u{1F50D} ${i.name} Configuration Differences
42
- `)),r.log(X.default.bold("Rule Changes:")),r.log(` ${X.default.green("+")} ${o.rulesToAdd.length} to add`),o.rulesToAdd.forEach(c=>r.log(` ${X.default.green("+")} ${c.name}`)),r.log(` ${X.default.cyan("~")} ${o.rulesToUpdate.length} to update`),o.rulesToUpdate.forEach(c=>r.log(` ${X.default.cyan("~")} ${c.name}`)),r.log(` ${X.default.red("-")} ${o.rulesToDelete.length} to delete`),o.rulesToDelete.forEach(c=>r.log(` ${X.default.red("-")} ${c.name}`)),(n.length||s.length||a.length)&&(r.log(""),r.log(X.default.bold("IP Rule Changes:")),r.log(` ${X.default.green("+")} ${n.length} to add`),r.log(` ${X.default.cyan("~")} ${s.length} to update`),r.log(` ${X.default.red("-")} ${a.length} to delete`));let l=o.rulesToAdd.length+o.rulesToUpdate.length+o.rulesToDelete.length+n.length+s.length+a.length;r.log(""),r.log(X.default.bold(`Summary: ${l} total changes detected`)),r.log(X.default.dim("Run `sync` to apply these changes to the remote configuration."))}var ei={};M(ei,{builder:()=>Ir,command:()=>yr,desc:()=>wr,handler:()=>Rr});var Ve=S(require("chalk")),yo=require("zod");T();pe();var yr="download [configVersion]",wr="Download remote Vercel Firewall rules and update local config, optionally for a specific configuration version",Ir={configVersion:{type:"number",description:"Specific configuration version to download (defaults to latest)"},config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},dryRun:{alias:"d",type:"boolean",description:"Show rules that would be downloaded without making changes",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Rr=async i=>{await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"downloading firewall rules"},async({config:e,provider:t})=>{if(i.configVersion!==void 0)try{ft.parse(i.configVersion)}catch(o){throw o instanceof yo.z.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),o}await Cr(t,e,i)})};async function Cr(i,e,t){r.start(`Fetching remote firewall configuration${t.configVersion?` version ${t.configVersion}`:""} ...`);let o=await i.fetchConfig(t.configVersion),n=o.rules,s=o.ips??[];if(n.length>0?(r.log(Ve.default.bold(`
42
+ `)),r.log(X.default.bold("Rule Changes:")),r.log(` ${X.default.green("+")} ${n.rulesToAdd.length} to add`),n.rulesToAdd.forEach(d=>r.log(` ${X.default.green("+")} ${d.name}`)),r.log(` ${X.default.cyan("~")} ${n.rulesToUpdate.length} to update`),n.rulesToUpdate.forEach(d=>r.log(` ${X.default.cyan("~")} ${d.name}`)),r.log(` ${X.default.red("-")} ${n.rulesToDelete.length} to delete`),n.rulesToDelete.forEach(d=>r.log(` ${X.default.red("-")} ${d.name}`)),(s.length||a.length||l.length)&&(r.log(""),r.log(X.default.bold("IP Rule Changes:")),r.log(` ${X.default.green("+")} ${s.length} to add`),r.log(` ${X.default.cyan("~")} ${a.length} to update`),r.log(` ${X.default.red("-")} ${l.length} to delete`));let c=n.rulesToAdd.length+n.rulesToUpdate.length+n.rulesToDelete.length+s.length+a.length+l.length;r.log(""),r.log(X.default.bold(`Summary: ${c} total changes detected`)),r.log(X.default.dim("Run `sync` to apply these changes to the remote configuration."))}var ei={};M(ei,{builder:()=>Ir,command:()=>yr,desc:()=>wr,handler:()=>Rr});var Ve=S(require("chalk")),yo=require("zod");T();pe();var yr="download [configVersion]",wr="Download remote Vercel Firewall rules and update local config, optionally for a specific configuration version",Ir={configVersion:{type:"number",description:"Specific configuration version to download (defaults to latest)"},config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},dryRun:{alias:"d",type:"boolean",description:"Show rules that would be downloaded without making changes",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Rr=async i=>{await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"downloading firewall rules"},async({config:e,provider:t})=>{if(i.configVersion!==void 0)try{ft.parse(i.configVersion)}catch(o){throw o instanceof yo.z.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),o}await Cr(t,e,i)})};async function Cr(i,e,t){r.start(`Fetching remote firewall configuration${t.configVersion?` version ${t.configVersion}`:""} ...`);let o=await i.fetchConfig(t.configVersion),n=o.rules,s=o.ips??[];if(n.length>0?(r.log(Ve.default.bold(`
43
43
  Remote Custom Rules to Download (${n.length}):
44
44
  `)),n.forEach(c=>r.log(` - ${c.name}${c.enabled?"":Ve.default.dim(" (disabled)")}`))):r.info("No custom rules to download..."),s.length>0?(r.log(Ve.default.bold(`
45
45
  Remote IP Blocking Rules to Download (${s.length}):
@@ -94,24 +94,24 @@ Remote IP Blocking Rules to Download (${s.length}):
94
94
  `,o+=` action = "${n.action}"
95
95
  `,o+=`}
96
96
 
97
- `}),o},kr=async i=>{try{let e;i.source==="remote"?await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"exporting configuration"},async({provider:s})=>{r.start("Fetching remote configuration..."),e=await s.fetchConfig()}):e=await j(i.config),r.start(`Exporting configuration in ${i.format} format...`);let t,o;switch(i.format){case"json":t=JSON.stringify(e,null,2),o="json";break;case"yaml":{let s=le(e);t=`# Firewall Configuration (${s.provider??"unknown"})
98
- `,t+=`version: ${s.metadata?.version??"unknown"}
99
- `,t+=`updatedAt: "${s.metadata?.updatedAt??""}"
100
- `,t+=`rules:
101
- `,s.rules.forEach(a=>{t+=` - name: "${a.name}"
102
- `,t+=` id: "${a.id??""}"
103
- `,t+=` enabled: ${a.enabled}
104
- `,t+=` action: "${a.action.type}"
105
- `}),o="yaml";break}case"terraform":t=Pr(le(e)),o="tf";break;case"markdown":t=Ar(le(e)),o="md";break;default:throw new Error(`Unsupported format: ${i.format}`)}let n=i.output||`firewall-export.${o}`;i.output==="-"||!i.output?r.log(t):((0,wo.writeFileSync)(n,t,"utf8"),r.success(Be.default.green(`\u2705 Exported to ${n}`)),r.log(""),r.log(Be.default.bold("Export Summary:")),r.log(`${Be.default.dim("Format:")} ${i.format}`),r.log(`${Be.default.dim("Source:")} ${i.source}`),r.log(`${Be.default.dim("Rules:")} ${e.rules.length} custom, ${(e.ips||[]).length} IP blocking`),r.log(`${Be.default.dim("Size:")} ${(t.length/1024).toFixed(1)} KB`))}catch(e){Z(e,"exporting configuration")}};var ii={};M(ii,{BASIC_TEMPLATE_RULES:()=>Ro,SECURITY_FOCUSED_TEMPLATE_RULES:()=>Co,builder:()=>Sr,command:()=>$r,desc:()=>_r,handler:()=>xr});var y=S(require("chalk")),Io=require("fs");T();pe();var Ro=[{id:"rule_block_bad_bots",name:"Block Bad Bots",description:"Block known malicious bots and crawlers based on user agent patterns",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"bot"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_block_scrapers",name:"Block Scrapers",description:"Block common scraping tools and automated requests",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"scraper"}]}],action:{mitigate:{action:"deny"}},active:!1}],Co=[{id:"rule_rate_limit_api",name:"Rate Limit API",description:"Rate limit API endpoints to prevent abuse and DoS attacks",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/api/"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:100,window:"1m"}}},active:!1},{id:"rule_block_suspicious_ips",name:"Block Suspicious IPs",description:"Block requests from a known suspicious IP range",conditionGroup:[{conditions:[{type:"ip_address",op:"eq",value:"10.0.0.0/24"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_protect_admin",name:"Protect Admin Routes",description:"Add extra protection for admin and sensitive routes",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/admin"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:10,window:"1m"}}},active:!1}],$r="init [template]",_r="Initialize a new Doorman configuration",Sr={template:{type:"string",description:"Template to use for initialization (empty, basic, or security-focused)",choices:["empty","basic","security-focused"],default:"empty"},config:{alias:"c",type:"string",description:"Path for the new config file",default:".doorman.json"},force:{alias:"f",type:"boolean",description:"Overwrite existing config file",default:!1},interactive:{alias:"i",type:"boolean",description:"Interactive setup with guided prompts",default:!0},projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"}},Lr=()=>{r.log(""),r.log(y.default.bold.cyan("\u{1F6AA} Welcome to Doorman!")),r.log(y.default.dim(`Let's set up your firewall configuration.
97
+ `}),o},kr=async i=>{try{let e=i.output==="-"||!i.output,t;i.source==="remote"?await J({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"exporting configuration"},async({provider:a})=>{e||r.start("Fetching remote configuration..."),t=await a.fetchConfig()}):t=await j(i.config),e||r.start(`Exporting configuration in ${i.format} format...`);let o,n;switch(i.format){case"json":o=JSON.stringify(t,null,2),n="json";break;case"yaml":{let a=le(t);o=`# Firewall Configuration (${a.provider??"unknown"})
98
+ `,o+=`version: ${a.metadata?.version??"unknown"}
99
+ `,o+=`updatedAt: "${a.metadata?.updatedAt??""}"
100
+ `,o+=`rules:
101
+ `,a.rules.forEach(l=>{o+=` - name: "${l.name}"
102
+ `,o+=` id: "${l.id??""}"
103
+ `,o+=` enabled: ${l.enabled}
104
+ `,o+=` action: "${l.action.type}"
105
+ `}),n="yaml";break}case"terraform":o=Pr(le(t)),n="tf";break;case"markdown":o=Ar(le(t)),n="md";break;default:throw new Error(`Unsupported format: ${i.format}`)}let s=i.output||`firewall-export.${n}`;e?r.log(o):((0,wo.writeFileSync)(s,o,"utf8"),r.success(Be.default.green(`\u2705 Exported to ${s}`)),r.log(""),r.log(Be.default.bold("Export Summary:")),r.log(`${Be.default.dim("Format:")} ${i.format}`),r.log(`${Be.default.dim("Source:")} ${i.source}`),r.log(`${Be.default.dim("Rules:")} ${t.rules.length} custom, ${(t.ips||[]).length} IP blocking`),r.log(`${Be.default.dim("Size:")} ${(o.length/1024).toFixed(1)} KB`))}catch(e){Z(e,"exporting configuration")}};var ii={};M(ii,{BASIC_TEMPLATE_RULES:()=>Ro,SECURITY_FOCUSED_TEMPLATE_RULES:()=>Co,builder:()=>Sr,command:()=>$r,desc:()=>_r,handler:()=>xr});var y=S(require("chalk")),Io=require("fs");T();pe();var Ro=[{id:"rule_block_bad_bots",name:"Block Bad Bots",description:"Block known malicious bots and crawlers based on user agent patterns",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"bot"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_block_scrapers",name:"Block Scrapers",description:"Block common scraping tools and automated requests",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"scraper"}]}],action:{mitigate:{action:"deny"}},active:!1}],Co=[{id:"rule_rate_limit_api",name:"Rate Limit API",description:"Rate limit API endpoints to prevent abuse and DoS attacks",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/api/"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:100,window:"1m"}}},active:!1},{id:"rule_block_suspicious_ips",name:"Block Suspicious IPs",description:"Block requests from a known suspicious IP range",conditionGroup:[{conditions:[{type:"ip_address",op:"eq",value:"10.0.0.0/24"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_protect_admin",name:"Protect Admin Routes",description:"Add extra protection for admin and sensitive routes",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/admin"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:10,window:"1m"}}},active:!1}],$r="init [template]",_r="Initialize a new Doorman configuration",Sr={template:{type:"string",description:"Template to use for initialization (empty, basic, or security-focused)",choices:["empty","basic","security-focused"],default:"empty"},config:{alias:"c",type:"string",description:"Path for the new config file",default:".doorman.json"},force:{alias:"f",type:"boolean",description:"Overwrite existing config file",default:!1},interactive:{alias:"i",type:"boolean",description:"Interactive setup with guided prompts",default:!0},projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"}},Lr=()=>{r.log(""),r.log(y.default.bold.cyan("\u{1F6AA} Welcome to Doorman!")),r.log(y.default.dim(`Let's set up your firewall configuration.
106
106
  `))},Ur=()=>{r.log(y.default.bold(`
107
107
  \u{1F4DA} Helpful Resources:`)),r.log(""),r.log(y.default.cyan("\u{1F517} Find your Project ID:")),r.log(y.default.dim(" https://vercel.com/dashboard \u2192 Select your project \u2192 Settings \u2192 General")),r.log(""),r.log(y.default.cyan("\u{1F517} Find your Team ID:")),r.log(y.default.dim(" https://vercel.com/dashboard \u2192 Team Settings \u2192 General")),r.log(y.default.dim(" (Leave empty if using personal account)")),r.log(""),r.log(y.default.cyan("\u{1F517} Create API Token:")),r.log(y.default.dim(" https://vercel.com/account/tokens \u2192 Create Token")),r.log(y.default.dim(" Scopes needed: Read & Write for your project")),r.log(""),r.log(y.default.cyan("\u{1F517} Documentation:")),r.log(y.default.dim(" https://doorman.griffen.codes/getting-started")),r.log("")},Dr=async i=>{let e=i.projectId,t=i.teamId;return i.interactive&&(r.log(y.default.bold("\u{1F4CB} Project Configuration")),r.log(y.default.dim(`We need your Vercel project details to configure the firewall.
108
108
  `)),e||(e=await w("Enter your Vercel Project ID:",{type:"text"})),t||await w("Are you using a Vercel team account?",{type:"confirm"})&&(t=await w("Enter your Vercel Team ID:",{type:"text"})),process.env.VERCEL_TOKEN?r.log(y.default.green("\u2705 VERCEL_TOKEN environment variable found")):(r.log(""),r.log(y.default.yellow("\u26A0\uFE0F VERCEL_TOKEN environment variable not found")),r.log(y.default.dim("You'll need to set this before running sync commands.")),await w("Would you like to see how to create an API token?",{type:"confirm"})&&(r.log(""),r.log(y.default.bold("\u{1F511} Creating a Vercel API Token:")),r.log("1. Go to https://vercel.com/account/tokens"),r.log('2. Click "Create Token"'),r.log('3. Give it a descriptive name (e.g., "Doorman Firewall")'),r.log("4. Set expiration as needed"),r.log("5. Copy the token and set it as an environment variable:"),r.log(""),r.log(y.default.cyan(' export VERCEL_TOKEN="your-token-here"')),r.log(y.default.dim(" # Add this to your ~/.bashrc, ~/.zshrc, or .env file")),r.log("")))),{projectId:e,teamId:t}},xr=async i=>{try{let e=i.config||".doorman.json";if(i.interactive&&Lr(),(0,Io.existsSync)(e)&&!i.force&&!await w(`Config file ${e} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(y.default.yellow("Initialization cancelled."));return}let{projectId:t,teamId:o}=await Dr(i);i.interactive&&(r.log(""),r.log(y.default.bold("\u{1F3A8} Template Selection")),r.log(y.default.dim(`Choose a starting template for your firewall configuration:
109
- `)),r.log(y.default.cyan("\u{1F4DD} empty")+y.default.dim(" - Minimal config, add your own rules")),r.log(y.default.cyan("\u{1F6E1}\uFE0F basic")+y.default.dim(" - Includes basic bot protection")),r.log(y.default.cyan("\u{1F512} security-focused")+y.default.dim(" - Comprehensive security rules")),r.log(""));let n=i.template;i.interactive&&!i.template&&(n=await w("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),r.start(`Creating ${n} configuration...`);let s=ht();switch(t&&(s.projectId=t),o&&(s.teamId=o),n){case"basic":s={...s,rules:Ro};break;case"security-focused":s={...s,rules:Co};break;default:break}await q(s,e),r.success(y.default.green(`\u2705 Created ${e}`)),r.log(""),r.log(y.default.bold("\u{1F4CB} Configuration Summary:")),r.log(`${y.default.dim("File:")} ${e}`),r.log(`${y.default.dim("Template:")} ${n}`),r.log(`${y.default.dim("Project ID:")} ${t||y.default.yellow("Not set - add manually")}`),r.log(`${y.default.dim("Team ID:")} ${o||y.default.dim("None (personal account)")}`),r.log(`${y.default.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),r.log(""),r.log(y.default.bold("\u{1F680} Next Steps:")),t||r.log(`1. ${y.default.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||r.log(`${t?"1":"2"}. ${y.default.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;r.log(`${a}. ${y.default.dim("Review and enable rules in")} ${e}`),r.log(`${a+1}. ${y.default.dim("Run")} ${y.default.cyan("doorman validate")} ${y.default.dim("to check your config")}`),r.log(`${a+2}. ${y.default.dim("Run")} ${y.default.cyan("doorman status")} ${y.default.dim("to see sync status")}`),r.log(`${a+3}. ${y.default.dim("Run")} ${y.default.cyan("doorman sync")} ${y.default.dim("to deploy your rules")}`),n!=="empty"&&(r.log(""),r.log(y.default.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),r.log(y.default.dim(" Review each rule and set active: true when ready."))),i.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Ur()}catch(e){Z(e,"initializing configuration")}};var oi={};M(oi,{builder:()=>Nr,command:()=>Or,desc:()=>Fr,handler:()=>Vr});var ce=S(require("chalk")),vo=require("zod");T();var Or="list [configVersion]",Fr="List Vercel Firewall rules, optionally for a specific configuration version",Nr={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file or VERCEL_PROJECT_ID environment variable)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file or VERCEL_TEAM_ID environment variable)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",description:"Output format (json or table)",choices:["json","table"],default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Vr=async i=>{await J({provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({provider:e})=>{if(i.configVersion!==void 0)try{ft.parse(i.configVersion)}catch(t){throw t instanceof vo.z.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),t}await Br(e,i)})};async function Br(i,e){r.start(`Fetching firewall configuration${e.configVersion?` version ${e.configVersion}`:""} ...`);let t=await i.fetchConfig(e.configVersion),o=t.rules,n=t.ips??[],s=t.metadata?.version??t.version,a=t.metadata?.updatedAt;if(r.info(`Found ${ce.default.cyan(o.length)} custom rules and ${ce.default.cyan(n.length)} IP blocking rules
110
- `+ce.default.dim(`Version: ${ce.default.yellow(s??"unknown")}${a?` \u2022 Last Updated: ${ce.default.yellow(a)}`:""}`)),e.format==="json"){r.info(JSON.stringify({version:s,updatedAt:a,rules:o,ips:n},null,2));return}if(o.length===0&&n.length===0){r.info(ce.default.yellow("No rules found"));return}o.length>0?(r.log(ce.default.bold.underline(`
109
+ `)),r.log(y.default.cyan("\u{1F4DD} empty")+y.default.dim(" - Minimal config, add your own rules")),r.log(y.default.cyan("\u{1F6E1}\uFE0F basic")+y.default.dim(" - Includes basic bot protection")),r.log(y.default.cyan("\u{1F512} security-focused")+y.default.dim(" - Comprehensive security rules")),r.log(""));let n=i.template;i.interactive&&!i.template&&(n=await w("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),r.start(`Creating ${n} configuration...`);let s=ht();switch(t&&(s.projectId=t),o&&(s.teamId=o),n){case"basic":s={...s,rules:Ro};break;case"security-focused":s={...s,rules:Co};break;default:break}await q(s,e),r.success(y.default.green(`\u2705 Created ${e}`)),r.log(""),r.log(y.default.bold("\u{1F4CB} Configuration Summary:")),r.log(`${y.default.dim("File:")} ${e}`),r.log(`${y.default.dim("Template:")} ${n}`),r.log(`${y.default.dim("Project ID:")} ${t||y.default.yellow("Not set - add manually")}`),r.log(`${y.default.dim("Team ID:")} ${o||y.default.dim("None (personal account)")}`),r.log(`${y.default.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),r.log(""),r.log(y.default.bold("\u{1F680} Next Steps:")),t||r.log(`1. ${y.default.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||r.log(`${t?"1":"2"}. ${y.default.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;r.log(`${a}. ${y.default.dim("Review and enable rules in")} ${e}`),r.log(`${a+1}. ${y.default.dim("Run")} ${y.default.cyan("doorman validate")} ${y.default.dim("to check your config")}`),r.log(`${a+2}. ${y.default.dim("Run")} ${y.default.cyan("doorman status")} ${y.default.dim("to see sync status")}`),r.log(`${a+3}. ${y.default.dim("Run")} ${y.default.cyan("doorman sync")} ${y.default.dim("to deploy your rules")}`),n!=="empty"&&(r.log(""),r.log(y.default.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),r.log(y.default.dim(" Review each rule and set active: true when ready."))),i.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Ur()}catch(e){Z(e,"initializing configuration")}};var oi={};M(oi,{builder:()=>Nr,command:()=>Or,desc:()=>Fr,handler:()=>Vr});var ce=S(require("chalk")),vo=require("zod");T();var Or="list [configVersion]",Fr="List Vercel Firewall rules, optionally for a specific configuration version",Nr={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:Y,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file or VERCEL_PROJECT_ID environment variable)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file or VERCEL_TEAM_ID environment variable)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",description:"Output format (json or table)",choices:["json","table"],default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Vr=async i=>{await J({provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({provider:e})=>{if(i.configVersion!==void 0)try{ft.parse(i.configVersion)}catch(t){throw t instanceof vo.z.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),t}await Br(e,i)})};async function Br(i,e){let t=e.format==="json";t||r.start(`Fetching firewall configuration${e.configVersion?` version ${e.configVersion}`:""} ...`);let o=await i.fetchConfig(e.configVersion),n=o.rules,s=o.ips??[],a=o.metadata?.version??o.version,l=o.metadata?.updatedAt;if(t){r.log(JSON.stringify({version:a,updatedAt:l,rules:n,ips:s},null,2));return}if(r.info(`Found ${ce.default.cyan(n.length)} custom rules and ${ce.default.cyan(s.length)} IP blocking rules
110
+ `+ce.default.dim(`Version: ${ce.default.yellow(a??"unknown")}${l?` \u2022 Last Updated: ${ce.default.yellow(l)}`:""}`)),n.length===0&&s.length===0){r.info(ce.default.yellow("No rules found"));return}n.length>0?(r.log(ce.default.bold.underline(`
111
111
  Custom Rules:`),`
112
- `),o.forEach(l=>r.log(` - ${l.name}${l.enabled?"":ce.default.dim(" (disabled)")}`))):r.info(ce.default.cyan("No custom rules found")),n.length>0?(r.log(ce.default.bold.underline(`
112
+ `),n.forEach(c=>r.log(` - ${c.name}${c.enabled?"":ce.default.dim(" (disabled)")}`))):r.info(ce.default.cyan("No custom rules found")),s.length>0?(r.log(ce.default.bold.underline(`
113
113
  IP Blocking Rules:`),`
114
- `),n.forEach(l=>r.log(` - ${l.ip} (${l.action})`))):r.info(ce.default.cyan("No IP blocking rules found"))}var ri={};M(ri,{aliases:()=>jr,builder:()=>zr,command:()=>Wr,desc:()=>qr,findFuzzyMatches:()=>Po,handler:()=>Mr,levenshtein:()=>Ao});var U=S(require("chalk")),To=require("consola");T();pe();var Wr="remove [type]",jr=["rm","delete"],qr="Remove a firewall rule or IP entry from your configuration",zr={type:{type:"string",description:'What to remove: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Select rules to remove from a list",default:!1},name:{alias:"n",type:"string",description:"Remove rule by name (exact match)"},id:{type:"array",string:!0,description:"Remove rule by ID (supports multiple)"},ip:{type:"string",description:"Remove IP rule by IP address"},disabled:{type:"boolean",description:"Remove all disabled (active: false) rules",default:!1},all:{type:"boolean",description:"Remove all rules (requires confirmation)",default:!1},force:{alias:"f",type:"boolean",description:"Skip confirmation prompt",default:!1},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be removed without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}};function Ao(i,e){let t=i.length,o=e.length;if(t===0)return o;if(o===0)return t;let n=Array.from({length:t+1},(a,l)=>l),s=new Array(t+1).fill(0);for(let a=1;a<=o;a++){s[0]=a;for(let l=1;l<=t;l++)e.charAt(a-1)===i.charAt(l-1)?s[l]=n[l-1]:s[l]=Math.min(n[l-1]+1,s[l-1]+1,n[l]+1);for(let l=0;l<=t;l++)n[l]=s[l]}return n[t]}function Po(i,e,t=3){let o=i.toLowerCase(),n=e.filter(l=>l.name.toLowerCase().includes(o)||o.includes(l.name.toLowerCase()));if(n.length>0)return n.slice(0,t);let s=e.map(l=>({rule:l,distance:Ao(o,l.name.toLowerCase())})).sort((l,c)=>l.distance-c.distance),a=Math.max(i.length*.6,3);return s.filter(l=>l.distance<=a).slice(0,t).map(l=>l.rule)}function ni(i){for(let e of i){let t=e.id?` (${e.id})`:"",o=e.active?"":U.default.dim(" [disabled]"),n=e.action.mitigate.action;r.log(U.default.dim(` - ${e.name}${t} \u2014 ${n}${o}`))}}function bo(i){for(let e of i){let t=e.notes?` \u2014 ${e.notes}`:"";r.log(U.default.dim(` - ${e.ip}${t}`))}}async function Eo(i,e,t,o="rule"){if(e)return!0;if(t)return await w(U.default.red(`\u26A0\uFE0F Are you sure you want to remove ALL ${o}s? Type "YES" to confirm:`),{type:"text"})==="YES";let n=i===1?`Confirm removal of 1 ${o}?`:`Confirm removal of ${i} ${o}s?`;return await w(n,{type:"confirm",initial:!1})}var Mr=async i=>{try{i.debug&&(r.level=To.LogLevels.debug),r.debug("Remove command arguments:",i);let e=i.type||"rule";r.start("Loading configuration...");let t=await j(i.config);if(e==="ip"){let o=t.ips||[];if(o.length===0){r.info("No IP rules in configuration.");return}let n=[];if(i.interactive){let l=o.map(d=>{let u=d.notes?` \u2014 ${d.notes}`:"";return`${d.ip}${u}`}),c=await w("Select IP rules to remove:",{type:"multiselect",options:l});n=o.filter(d=>{let u=`${d.ip}${d.notes?` \u2014 ${d.notes}`:""}`;return c.includes(u)})}else i.ip?(n=o.filter(l=>l.ip===i.ip),n.length===0&&(r.error(U.default.red(`No IP rule found matching "${i.ip}"`)),o.length>0&&(r.log(U.default.dim(`
114
+ `),s.forEach(c=>r.log(` - ${c.ip} (${c.action})`))):r.info(ce.default.cyan("No IP blocking rules found"))}var ri={};M(ri,{aliases:()=>jr,builder:()=>zr,command:()=>Wr,desc:()=>qr,findFuzzyMatches:()=>Po,handler:()=>Mr,levenshtein:()=>Ao});var U=S(require("chalk")),To=require("consola");T();pe();var Wr="remove [type]",jr=["rm","delete"],qr="Remove a firewall rule or IP entry from your configuration",zr={type:{type:"string",description:'What to remove: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Select rules to remove from a list",default:!1},name:{alias:"n",type:"string",description:"Remove rule by name (exact match)"},id:{type:"array",string:!0,description:"Remove rule by ID (supports multiple)"},ip:{type:"string",description:"Remove IP rule by IP address"},disabled:{type:"boolean",description:"Remove all disabled (active: false) rules",default:!1},all:{type:"boolean",description:"Remove all rules (requires confirmation)",default:!1},force:{alias:"f",type:"boolean",description:"Skip confirmation prompt",default:!1},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be removed without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}};function Ao(i,e){let t=i.length,o=e.length;if(t===0)return o;if(o===0)return t;let n=Array.from({length:t+1},(a,l)=>l),s=new Array(t+1).fill(0);for(let a=1;a<=o;a++){s[0]=a;for(let l=1;l<=t;l++)e.charAt(a-1)===i.charAt(l-1)?s[l]=n[l-1]:s[l]=Math.min(n[l-1]+1,s[l-1]+1,n[l]+1);for(let l=0;l<=t;l++)n[l]=s[l]}return n[t]}function Po(i,e,t=3){let o=i.toLowerCase(),n=e.filter(l=>l.name.toLowerCase().includes(o)||o.includes(l.name.toLowerCase()));if(n.length>0)return n.slice(0,t);let s=e.map(l=>({rule:l,distance:Ao(o,l.name.toLowerCase())})).sort((l,c)=>l.distance-c.distance),a=Math.max(i.length*.6,3);return s.filter(l=>l.distance<=a).slice(0,t).map(l=>l.rule)}function ni(i){for(let e of i){let t=e.id?` (${e.id})`:"",o=e.active?"":U.default.dim(" [disabled]"),n=e.action.mitigate.action;r.log(U.default.dim(` - ${e.name}${t} \u2014 ${n}${o}`))}}function bo(i){for(let e of i){let t=e.notes?` \u2014 ${e.notes}`:"";r.log(U.default.dim(` - ${e.ip}${t}`))}}async function Eo(i,e,t,o="rule"){if(e)return!0;if(t)return await w(U.default.red(`\u26A0\uFE0F Are you sure you want to remove ALL ${o}s? Type "YES" to confirm:`),{type:"text"})==="YES";let n=i===1?`Confirm removal of 1 ${o}?`:`Confirm removal of ${i} ${o}s?`;return await w(n,{type:"confirm",initial:!1})}var Mr=async i=>{try{i.debug&&(r.level=To.LogLevels.debug),r.debug("Remove command arguments:",i);let e=i.type||"rule";r.start("Loading configuration...");let t=await j(i.config);if(e==="ip"){let o=t.ips||[];if(o.length===0){r.info("No IP rules in configuration.");return}let n=[];if(i.interactive){let l=o.map(d=>{let u=d.notes?` \u2014 ${d.notes}`:"";return`${d.ip}${u}`}),c=await w("Select IP rules to remove:",{type:"multiselect",options:l});n=o.filter(d=>{let u=`${d.ip}${d.notes?` \u2014 ${d.notes}`:""}`;return c.includes(u)})}else i.ip?(n=o.filter(l=>l.ip===i.ip),n.length===0&&(r.error(U.default.red(`No IP rule found matching "${i.ip}"`)),o.length>0&&(r.log(U.default.dim(`
115
115
  Current IP rules:`)),bo(o)),process.exit(1))):i.all?n=[...o]:(r.error("Specify --ip, --interactive, or --all to select IP rules to remove."),process.exit(1));if(n.length===0){r.info("No IP rules selected for removal.");return}if(r.log(U.default.cyan(`
116
116
  ${i.dryRun?"Would remove":"Removing"} ${n.length} IP rule(s):`)),bo(n),i.dryRun){r.log(U.default.dim(`
117
117
  Dry run \u2014 no changes made.`));return}if(!await Eo(n.length,i.force||!1,i.all||!1,"IP rule")){r.info("Cancelled.");return}let s=new Set(n.map(l=>l.ip)),a={...t,ips:o.filter(l=>!s.has(l.ip))};await q(a,i.config),r.success(U.default.green(`\u2714 Removed ${n.length} IP rule(s) from configuration`))}else{let o=t.rules||[];if(o.length===0){r.info("No rules in configuration.");return}let n=[];if(i.interactive){let l=o.map((d,u)=>{let p=d.id?` (${d.id})`:"",g=d.active?"":" [disabled]",h=d.action.mitigate.action;return`[${u}] ${d.name}${p} \u2014 ${h}${g}`}),c=await w("Select rules to remove:",{type:"multiselect",options:l});n=o.filter((d,u)=>c.some(p=>p.startsWith(`[${u}]`)))}else if(i.name){if(n=o.filter(l=>l.name===i.name),n.length>1&&(r.warn(U.default.yellow(`
package/dist/bin/run.mjs CHANGED
@@ -37,9 +37,9 @@ ${B}`)}),R.result}),{toAdd:l,toUpdate:c,toDelete:d}=this.diffRules(ot(t.rules),a
37
37
  ${l.formatWarning(a)}`)}),o.push(s.result)}return o}async syncRules(t,o={}){let{dryRun:n=!1}=o;try{let{OperationSafety:s}=(ht(),ge(mt)),a=await s.performDryRunValidation(t,"sync rules",async C=>this.getChanges(C));if(!a.valid)throw new Error(`Dry-run validation failed: ${a.issues.join(", ")}`);let l=a.changes,{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u}=l,p=l.ipsToAdd||[],g=l.ipsToDelete||[];if(n)return r.info("Dry run mode. The following changes would be made:"),r.info(`Custom Rules - Add: ${c.length}, Update: ${d.length}, Delete: ${u.length}`),r.info(`IP Rules - Add: ${p.length}, Delete: ${g.length}`),{success:!0,rulesAdded:0,rulesUpdated:0,rulesDeleted:0,ipsAdded:0,ipsUpdated:0,ipsDeleted:0,warnings:a.warnings};let h=s.assessOperationRisk(l,t);if(!await s.confirmDestructiveOperation({operation:"sync rules",target:`Fastly workspace ${this.client.workspaceId}`,changes:l,riskLevel:h,skipConfirmation:o.force||!1,dryRun:!1,allowDeletions:o.allowDeletions||!1}))throw new Error("Operation cancelled by user");let R=[];gt(t.rules)&&R.push("Rule `priority` has no effect on Fastly: Next-Gen WAF rules are evaluated independently and have no API-exposed ordering concept to honour.");let F=c.map(C=>({rule:C,translated:P.unifiedToFastly(C)})),K=d.map(C=>({rule:C,translated:P.unifiedToFastly(C)})),B=[],ae=(C,N)=>`${C}: ${N instanceof Error?N.message:String(N)}`,pe=[],Ae=[],_=[],we=[];for(let C of u)if(C.id)try{r.debug(`Deleting Fastly rule: ${C.id}`),await this.client.deleteRule(C.id),_.push(C.id)}catch(N){r.error(`Failed to delete Fastly rule ${C.id}:`,N),B.push(ae(`Failed to delete rule ${C.id}`,N))}for(let{rule:C,translated:N}of F){let ee=N.result;try{r.debug(`Adding new Fastly rule: ${ee.description}`);let re=await this.client.createRule(ee);pe.push(re),we.push({oldId:C.id||void 0,newId:re.id,name:ee.description})}catch(re){r.error(`Failed to add Fastly rule "${ee.description}":`,re),B.push(ae(`Failed to add rule "${ee.description}"`,re))}}for(let{rule:C,translated:N}of K)if(C.id)try{r.debug(`Updating Fastly rule: ${C.id}`);let ee=await this.client.updateRule(C.id,N.result);Ae.push(ee)}catch(ee){r.error(`Failed to update Fastly rule ${C.id}:`,ee),B.push(ae(`Failed to update rule ${C.id}`,ee))}let Ie=0,Re=0,xe=t.ips||[],{result:Pe,warnings:Oe}=P.unifiedIPsToFastlyEntries(xe);Oe.forEach(C=>R.push(C.message));try{for(let[C,N]of[[rt,Pe.denyEntries],[st,Pe.allowEntries]]){let re=(await this.client.findList(C))?.entries??[],Qe=N.filter(f=>!re.includes(f)),Xe=re.filter(f=>!N.includes(f));if(Qe.length===0&&Xe.length===0)continue;let Ce=await this.client.getOrCreateList(C);await this.client.updateListEntries(Ce.id,N),Ie+=Qe.length,Re+=Xe.length}}catch(C){r.error("Failed to sync Fastly IP lists:",C),B.push(ae("Failed to sync IP lists",C))}return r.debug(`Custom Rules: Added: ${pe.length}, Updated: ${Ae.length}, Deleted: ${_.length}`),r.debug(`IP Rules: Added: ${Ie}, Deleted: ${Re}`),{success:B.length===0,rulesAdded:pe.length,rulesUpdated:Ae.length,rulesDeleted:_.length,ipsAdded:Ie,ipsUpdated:0,ipsDeleted:Re,idRemappings:we.length>0?we:void 0,errors:B.length>0?B:void 0,warnings:R.length>0?R:void 0}}catch(s){throw r.error("Error during sync:",s),new Error(`Failed to synchronize firewall rules: ${s instanceof Error?s.message:String(s)}`,{cause:s})}}async getChanges(t){let o=ke.safeParse(t);if(!o.success)throw new Error(`Invalid firewall configuration: ${o.error.message}`);try{r.debug("Fetching existing Fastly configuration");let[n,s,a]=await Promise.all([this.client.fetchRules(),this.client.findList(rt),this.client.findList(st)]),l=this.translateFastlyRules(n),{toAdd:c,toUpdate:d,toDelete:u}=this.diffRules(t.rules,l),p=s?.entries??[],g=a?.entries??[],h=[...P.fastlyListEntriesToUnified(p,"deny"),...P.fastlyListEntriesToUnified(g,"allow")],{ipsToAdd:I,ipsToDelete:R}=this.diffIPs(t.ips||[],h);return{rulesToAdd:c,rulesToUpdate:d,rulesToDelete:u,ipsToAdd:I,ipsToUpdate:[],ipsToDelete:R,hasChanges:c.length>0||d.length>0||u.length>0||I.length>0||R.length>0}}catch(n){throw r.error("Error fetching existing Fastly configuration:",n),new Error("Failed to fetch existing Fastly configuration")}}getSupportedFeatures(){return{supportsCustomRules:!0,supportsIPBlocking:!0,supportsRateLimiting:!0,supportsManagedRules:!1,supportsGeoBlocking:!0,supportsRedirect:!0,supportsChallenge:!0}}async verifyCredentials(){return this.client.verifyCredentials()}validateConfig(t){let o=super.validateConfig(t),n=[...o.errors],s=[...o.warnings];return t.provider&&t.provider!=="fastly"&&n.push({path:"provider",message:"Provider must be 'fastly' for FastlyFirewallService",code:"INVALID_PROVIDER"}),{valid:n.length===0,errors:n,warnings:s}}getHealthScore(t){let o=super.getHealthScore(t),n=[...o.issues],s=o.score;return t.rules.filter(l=>l.action?.type==="rate_limit").length===0&&(s-=10,n.push({severity:"info",category:"security",message:"No rate limiting rules configured",suggestion:"Consider adding rate limiting to protect against abuse"})),(!t.ips||t.ips.length===0)&&n.push({severity:"info",category:"security",message:"No IP blocking rules configured",suggestion:"Consider blocking known malicious IPs"}),{score:Math.max(0,s),grade:o.grade,issues:n,recommendations:o.recommendations}}diffRules(t,o){let n=[],s=[],a=[...o];for(let l of t){let c=o.find(d=>d.id===l.id);if(!c)n.push(l);else{We(Te(l),Te(c))||s.push({...l,id:c.id});let d=a.findIndex(u=>u.id===c.id);d!==-1&&a.splice(d,1)}}return{toAdd:n,toUpdate:s,toDelete:a}}diffIPs(t,o){let n=l=>`${l.action}:${l.ip}`,s=new Set(o.map(n)),a=new Set(t.map(n));return{ipsToAdd:t.filter(l=>!s.has(n(l))),ipsToDelete:o.filter(l=>!a.has(n(l)))}}};E();var Ze=class{static fromEnv(){let e=process.env.FASTLY_API_TOKEN,t=process.env.FASTLY_WORKSPACE_ID;if(!e)throw new Error("FASTLY_API_TOKEN environment variable is required");if(!t)throw new Error("FASTLY_WORKSPACE_ID environment variable is required");let o=new Ke(t,e);return new He(o)}static fromConfig(e){let t=e.apiToken||process.env.FASTLY_API_TOKEN,o=e.workspaceId||process.env.FASTLY_WORKSPACE_ID;if(!t)throw new Error("Fastly API token is required (provide apiToken or set FASTLY_API_TOKEN env var)");if(!o)throw new Error("Fastly workspace ID is required (provide workspaceId or set FASTLY_WORKSPACE_ID env var)");r.debug("Creating Fastly provider with config:",{workspaceId:o});let n=new Ke(o,t);return new He(n)}static create(e,t){return this.fromConfig({workspaceId:e,apiToken:t})}};var jt={provider:"vercel",fields:[{key:"token",envVar:"VERCEL_TOKEN",label:"Vercel API Token",required:!0,secret:!0},{key:"projectId",envVar:"VERCEL_PROJECT_ID",label:"Vercel Project ID",required:!0,configKey:"projectId"},{key:"teamId",envVar:"VERCEL_TEAM_ID",label:"Vercel Team ID",required:!1,configKey:"teamId"}]};var qt={provider:"cloudflare",fields:[{key:"apiToken",envVar:"CLOUDFLARE_API_TOKEN",label:"Cloudflare API Token",required:!0,secret:!0},{key:"zoneId",envVar:"CLOUDFLARE_ZONE_ID",label:"Cloudflare Zone ID",required:!0,configKey:"zoneId"},{key:"accountId",envVar:"CLOUDFLARE_ACCOUNT_ID",label:"Cloudflare Account ID",required:!1,configKey:"accountId"}]};var zt={provider:"fastly",fields:[{key:"apiToken",envVar:"FASTLY_API_TOKEN",label:"Fastly API Token",required:!0,secret:!0},{key:"workspaceId",envVar:"FASTLY_WORKSPACE_ID",label:"Fastly Next-Gen WAF Workspace ID",required:!0,configKey:"workspaceId"}]};function wt(i,e={}){let{explicit:t={},config:o={},env:n=process.env}=e,s={};for(let a of i.fields)s[a.key]=t[a.key]||o[a.key]||n[a.envVar];return s}async function Qi(i){let e;if(i.provider)e=i.provider,r.debug(`Using explicitly specified provider: ${e}`);else{let t=ct.detect(i.config);t.provider?(e=t.provider,r.debug(`Auto-detected provider: ${e} (${t.confidence} confidence)`),t.reasons.length>0&&r.debug(`Reasons: ${t.reasons.join(", ")}`)):i.interactive!==!1?e=await er():(e="vercel",r.warn("No provider detected, defaulting to Vercel"))}try{if(e==="vercel")return await Jn(i);if(e==="cloudflare")return{provider:await Qn(i)};if(e==="fastly")return{provider:await Xn(i)};throw new Error(`Unknown provider: ${e}`)}catch(t){throw r.error(`Failed to initialize ${e} provider:`,t),t}}async function Jn(i){let e=i.config,{token:t,projectId:o,teamId:n}=wt(jt,{explicit:{token:i.token,projectId:i.projectId,teamId:i.teamId},config:{projectId:e?.providers?.vercel?.projectId??e?.projectId,teamId:e?.providers?.vercel?.teamId??e?.teamId}});if(!t||!o){if(i.interactive===!1)throw new Error("Vercel credentials missing. Provide token and projectId.");r.warn("Missing Vercel credentials"),r.info("Please provide your Vercel credentials:");let{promptForCredentials:a}=await Promise.resolve().then(()=>(Yi(),Zi)),l=await a({token:t,projectId:o,teamId:n});return{provider:qe.fromConfig({token:l.token,projectId:l.projectId,teamId:l.teamId}),vercelCredentials:l}}return{provider:qe.fromConfig({token:t,projectId:o,teamId:n}),vercelCredentials:{token:t,projectId:o,teamId:n}}}async function Qn(i){let e=i.config,{apiToken:t,zoneId:o,accountId:n}=wt(qt,{explicit:{apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId},config:{zoneId:e?.providers?.cloudflare?.zoneId,accountId:e?.providers?.cloudflare?.accountId}});if(!t||!o){if(i.interactive===!1)throw new Error("Cloudflare credentials missing. Provide apiToken and zoneId.");r.warn("Missing Cloudflare credentials"),r.info("Please provide your Cloudflare credentials:");let s=t||await et("Cloudflare API Token: "),a=o||await w("Cloudflare Zone ID:",{type:"text"}),l=n||await w("Cloudflare Account ID (optional):",{type:"text"});return Ge.fromConfig({apiToken:s,zoneId:a,accountId:l||void 0})}return Ge.fromConfig({apiToken:t,zoneId:o,accountId:n})}async function Xn(i){let e=i.config,{apiToken:t,workspaceId:o}=wt(zt,{explicit:{apiToken:i.apiToken,workspaceId:i.workspaceId},config:{workspaceId:e?.providers?.fastly?.workspaceId}});if(!t||!o){if(i.interactive===!1)throw new Error("Fastly credentials missing. Provide apiToken and workspaceId.");r.warn("Missing Fastly credentials"),r.info("Please provide your Fastly credentials:");let n=t||await et("Fastly API Token: "),s=o||await w("Fastly Next-Gen WAF Workspace ID:",{type:"text"});return Ze.fromConfig({apiToken:n,workspaceId:s})}return Ze.fromConfig({apiToken:t,workspaceId:o})}async function er(){r.info(Yn.yellow("Unable to auto-detect firewall provider.")),r.info(`Please select a provider:
38
38
  `),ie.forEach((s,a)=>{r.info(` ${a+1}. ${Ji(s)}`)}),r.info("");let i=await w(`Select provider (1-${ie.length}):`,{type:"text"}),e=String(i).trim().toLowerCase(),t=ie.find(s=>s===e);if(t)return t;let o=Number(e);if(Number.isInteger(o)&&o>=1&&o<=ie.length)return ie[o-1];let n=ie[0];return r.warn(`Invalid choice: ${i}, defaulting to ${Ji(n)}`),n}function Ji(i){switch(i){case"vercel":return"Vercel Firewall";case"cloudflare":return"Cloudflare WAF";case"fastly":return"Fastly Next-Gen WAF";default:return i}}async function Y(i,e){try{i.debug&&(r.level=tr.debug);let t;if(i.optionalConfig)try{t=await W(i.config,"optional")}catch{t={}}else i.skipValidation?t=await W(i.config,"raw"):t=await W(i.config,"required");let{provider:o,vercelCredentials:n}=await Qi({provider:i.provider,config:t,interactive:!i.ci,token:i.token,projectId:i.projectId,teamId:i.teamId,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId}),s=n?.token??"",a=n?.projectId??"",l=n?.teamId??"";await e({config:t,provider:o,token:s,projectId:a,teamId:l})}catch(t){H(t,i.errorContext)}}var sr="backup",ar="Backup or restore firewall configurations",lr={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},output:{alias:"o",type:"string",description:"Output directory for backups",default:"./backups"},restore:{alias:"r",type:"string",description:"Restore from backup file"},list:{alias:"l",type:"boolean",description:"List available backups",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},cr=async i=>{try{i.debug&&(r.level=rr.debug);let e=i.output||"./backups";if(i.list){if(!It(e)){r.info(J.yellow("No backup directory found."));return}let t=or(e).filter(o=>o.endsWith(".json")).map(o=>{let n=Mt(e,o),s=nr(n);return{name:o,size:s.size,created:s.mtime}}).sort((o,n)=>n.created.getTime()-o.created.getTime());if(t.length===0){r.info(J.yellow("No backups found."));return}r.log(J.bold(`
39
39
  \u{1F4E6} Available Backups:
40
- `)),t.forEach(o=>{r.log(`${J.cyan(o.name)}`),r.log(` ${J.dim("Created:")} ${o.created.toLocaleString()}`),r.log(` ${J.dim("Size:")} ${(o.size/1024).toFixed(1)} KB`),r.log("")});return}if(i.restore){let t=i.restore.startsWith("/")?i.restore:Mt(e,i.restore);It(t)||(r.error(`Backup file not found: ${t}`),process.exit(1));let o=await W(t,"raw"),n=i.config||".doorman.json";if(It(n)&&!await w(`Config file ${n} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(J.yellow("Restore cancelled."));return}let{backup:s,...a}=o;await j(a,n),r.success(J.green(`\u2705 Restored configuration from ${t} to ${n}`));return}await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"creating backup"},async({provider:t,projectId:o,teamId:n})=>{r.start("Fetching current remote configuration...");let s=await t.fetchConfig(),a=s.metadata?.version??s.version;ye.getInstance().validateConfig(s),It(e)||ir(e,{recursive:!0});let c=new Date,d=c.toISOString().split("T")[0]??"unknown-date",u=(c.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",g=`firewall-backup-${`${d}_${u}`}.json`,h=Mt(e,g),I={...s,backup:{createdAt:new Date().toISOString(),source:"remote",provider:t.name,...t.name==="vercel"?{projectId:o,teamId:n}:{},originalVersion:a}};await j(I,h,{validate:!1}),r.success(J.green(`\u2705 Backup created: ${h}`)),r.log(""),r.log(J.bold("Backup Details:")),r.log(`${J.dim("Version:")} ${a??"unknown"}`),r.log(`${J.dim("Rules:")} ${s.rules.length} custom, ${s.ips?.length??0} IP blocking`),r.log(`${J.dim("Created:")} ${new Date().toLocaleString()}`),r.log(""),r.log(J.dim("To restore this backup later, run:")),r.log(J.cyan(`doorman backup --restore ${g}`))})}catch(e){H(e,"managing backup")}};var Kt={};z(Kt,{builder:()=>pr,command:()=>dr,desc:()=>ur,handler:()=>fr});E();import Q from"chalk";function se(i){if(me(i))return i;let e=i,t=e.rules.map(n=>P.vercelToUnified(n).result),o=(e.ips??[]).map(n=>P.vercelIPToUnified(n));return{version:"2.0",provider:"vercel",providers:{vercel:{projectId:e.projectId,teamId:e.teamId}},rules:t,ips:o,metadata:{version:e.version,updatedAt:e.updatedAt}}}function Rt(i,e){let t=!1,o=me(i),n=o?{...i,metadata:{...i.metadata}}:{...i};if(e.version!==void 0)if(o){let s=n;s.metadata?.version!==e.version&&(s.metadata={...s.metadata,version:e.version,updatedAt:e.updatedAt??new Date().toISOString()},t=!0)}else{let s=n;s.version!==e.version&&(s.version=e.version,s.updatedAt=e.updatedAt??new Date().toISOString(),t=!0)}if(e.idRemappings&&e.idRemappings.length>0){let s=new Map(e.idRemappings.filter(d=>d.oldId).map(d=>[d.oldId,d.newId])),a=new Map(e.idRemappings.filter(d=>!d.oldId).map(d=>[d.name,d.newId])),l=!1,c=n.rules.map(d=>{let u=d.id?s.get(d.id):a.get(d.name);return u&&u!==d.id?(l=!0,{...d,id:u}):d});l&&(n={...n,rules:c},t=!0)}return{config:n,changed:t}}function Xi(i,e){if(me(e))return{...e,...i};let t=e,o=i.rules.map(s=>P.unifiedToVercel(s).result),n=(i.ips??[]).map(s=>({id:s.id||"",ip:s.ip,hostname:s.hostname||"",action:s.action,notes:s.notes}));return{...t,projectId:i.providers?.vercel?.projectId??t.projectId,teamId:i.providers?.vercel?.teamId??t.teamId,version:i.metadata?.version,updatedAt:i.metadata?.updatedAt,rules:o,ips:n}}var dr="diff",ur="Show detailed differences between local and remote firewall configuration",pr={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",choices:["table","json"],description:"Output format",default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},fr=async i=>{await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"calculating diff"},async({config:e,provider:t})=>{await gr(t,se(e),i)})};async function gr(i,e,t){r.start("Calculating differences...");let o=await i.getChanges(e);if(!o.hasChanges){r.success(Q.green("No differences found. Local and remote configurations are in sync."));return}let n=o.ipsToAdd??[],s=o.ipsToUpdate??[],a=o.ipsToDelete??[];if(t.format==="json"){r.log(JSON.stringify({rules:{toAdd:o.rulesToAdd,toUpdate:o.rulesToUpdate,toDelete:o.rulesToDelete},ips:{toAdd:n,toUpdate:s,toDelete:a},summary:{hasChanges:o.hasChanges,ruleChanges:o.rulesToAdd.length+o.rulesToUpdate.length+o.rulesToDelete.length,ipChanges:n.length+s.length+a.length}},null,2));return}r.log(Q.bold(`
40
+ `)),t.forEach(o=>{r.log(`${J.cyan(o.name)}`),r.log(` ${J.dim("Created:")} ${o.created.toLocaleString()}`),r.log(` ${J.dim("Size:")} ${(o.size/1024).toFixed(1)} KB`),r.log("")});return}if(i.restore){let t=i.restore.startsWith("/")?i.restore:Mt(e,i.restore);It(t)||(r.error(`Backup file not found: ${t}`),process.exit(1));let o=await W(t,"raw"),n=i.config||".doorman.json";if(It(n)&&!await w(`Config file ${n} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(J.yellow("Restore cancelled."));return}let{backup:s,...a}=o;await j(a,n),r.success(J.green(`\u2705 Restored configuration from ${t} to ${n}`));return}await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"creating backup"},async({provider:t,projectId:o,teamId:n})=>{r.start("Fetching current remote configuration...");let s=await t.fetchConfig(),a=s.metadata?.version??s.version;ye.getInstance().validateConfig(s),It(e)||ir(e,{recursive:!0});let c=new Date,d=c.toISOString().split("T")[0]??"unknown-date",u=(c.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",g=`firewall-backup-${`${d}_${u}`}.json`,h=Mt(e,g),I={...s,backup:{createdAt:new Date().toISOString(),source:"remote",provider:t.name,...t.name==="vercel"?{projectId:o,teamId:n}:{},originalVersion:a}};await j(I,h,{validate:!1}),r.success(J.green(`\u2705 Backup created: ${h}`)),r.log(""),r.log(J.bold("Backup Details:")),r.log(`${J.dim("Version:")} ${a??"unknown"}`),r.log(`${J.dim("Rules:")} ${s.rules.length} custom, ${s.ips?.length??0} IP blocking`),r.log(`${J.dim("Created:")} ${new Date().toLocaleString()}`),r.log(""),r.log(J.dim("To restore this backup later, run:")),r.log(J.cyan(`doorman backup --restore ${g}`))})}catch(e){H(e,"managing backup")}};var Kt={};z(Kt,{builder:()=>pr,command:()=>dr,desc:()=>ur,handler:()=>fr});E();import Q from"chalk";function se(i){if(me(i))return i;let e=i,t=e.rules.map(n=>P.vercelToUnified(n).result),o=(e.ips??[]).map(n=>P.vercelIPToUnified(n));return{version:"2.0",provider:"vercel",providers:{vercel:{projectId:e.projectId,teamId:e.teamId}},rules:t,ips:o,metadata:{version:e.version,updatedAt:e.updatedAt}}}function Rt(i,e){let t=!1,o=me(i),n=o?{...i,metadata:{...i.metadata}}:{...i};if(e.version!==void 0)if(o){let s=n;s.metadata?.version!==e.version&&(s.metadata={...s.metadata,version:e.version,updatedAt:e.updatedAt??new Date().toISOString()},t=!0)}else{let s=n;s.version!==e.version&&(s.version=e.version,s.updatedAt=e.updatedAt??new Date().toISOString(),t=!0)}if(e.idRemappings&&e.idRemappings.length>0){let s=new Map(e.idRemappings.filter(d=>d.oldId).map(d=>[d.oldId,d.newId])),a=new Map(e.idRemappings.filter(d=>!d.oldId).map(d=>[d.name,d.newId])),l=!1,c=n.rules.map(d=>{let u=d.id?s.get(d.id):a.get(d.name);return u&&u!==d.id?(l=!0,{...d,id:u}):d});l&&(n={...n,rules:c},t=!0)}return{config:n,changed:t}}function Xi(i,e){if(me(e))return{...e,...i};let t=e,o=i.rules.map(s=>P.unifiedToVercel(s).result),n=(i.ips??[]).map(s=>({id:s.id||"",ip:s.ip,hostname:s.hostname||"",action:s.action,notes:s.notes}));return{...t,projectId:i.providers?.vercel?.projectId??t.projectId,teamId:i.providers?.vercel?.teamId??t.teamId,version:i.metadata?.version,updatedAt:i.metadata?.updatedAt,rules:o,ips:n}}var dr="diff",ur="Show detailed differences between local and remote firewall configuration",pr={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",choices:["table","json"],description:"Output format",default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},fr=async i=>{await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"calculating diff"},async({config:e,provider:t})=>{await gr(t,se(e),i)})};async function gr(i,e,t){let o=t.format==="json";o||r.start("Calculating differences...");let n=await i.getChanges(e),s=n.ipsToAdd??[],a=n.ipsToUpdate??[],l=n.ipsToDelete??[];if(o){r.log(JSON.stringify({rules:{toAdd:n.rulesToAdd,toUpdate:n.rulesToUpdate,toDelete:n.rulesToDelete},ips:{toAdd:s,toUpdate:a,toDelete:l},summary:{hasChanges:n.hasChanges,ruleChanges:n.rulesToAdd.length+n.rulesToUpdate.length+n.rulesToDelete.length,ipChanges:s.length+a.length+l.length}},null,2));return}if(!n.hasChanges){r.success(Q.green("No differences found. Local and remote configurations are in sync."));return}r.log(Q.bold(`
41
41
  \u{1F50D} ${i.name} Configuration Differences
42
- `)),r.log(Q.bold("Rule Changes:")),r.log(` ${Q.green("+")} ${o.rulesToAdd.length} to add`),o.rulesToAdd.forEach(c=>r.log(` ${Q.green("+")} ${c.name}`)),r.log(` ${Q.cyan("~")} ${o.rulesToUpdate.length} to update`),o.rulesToUpdate.forEach(c=>r.log(` ${Q.cyan("~")} ${c.name}`)),r.log(` ${Q.red("-")} ${o.rulesToDelete.length} to delete`),o.rulesToDelete.forEach(c=>r.log(` ${Q.red("-")} ${c.name}`)),(n.length||s.length||a.length)&&(r.log(""),r.log(Q.bold("IP Rule Changes:")),r.log(` ${Q.green("+")} ${n.length} to add`),r.log(` ${Q.cyan("~")} ${s.length} to update`),r.log(` ${Q.red("-")} ${a.length} to delete`));let l=o.rulesToAdd.length+o.rulesToUpdate.length+o.rulesToDelete.length+n.length+s.length+a.length;r.log(""),r.log(Q.bold(`Summary: ${l} total changes detected`)),r.log(Q.dim("Run `sync` to apply these changes to the remote configuration."))}var Ht={};z(Ht,{builder:()=>wr,command:()=>hr,desc:()=>yr,handler:()=>Ir});E();import Ye from"chalk";import{z as mr}from"zod";le();var hr="download [configVersion]",yr="Download remote Vercel Firewall rules and update local config, optionally for a specific configuration version",wr={configVersion:{type:"number",description:"Specific configuration version to download (defaults to latest)"},config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},dryRun:{alias:"d",type:"boolean",description:"Show rules that would be downloaded without making changes",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Ir=async i=>{await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"downloading firewall rules"},async({config:e,provider:t})=>{if(i.configVersion!==void 0)try{at.parse(i.configVersion)}catch(o){throw o instanceof mr.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),o}await Rr(t,e,i)})};async function Rr(i,e,t){r.start(`Fetching remote firewall configuration${t.configVersion?` version ${t.configVersion}`:""} ...`);let o=await i.fetchConfig(t.configVersion),n=o.rules,s=o.ips??[];if(n.length>0?(r.log(Ye.bold(`
42
+ `)),r.log(Q.bold("Rule Changes:")),r.log(` ${Q.green("+")} ${n.rulesToAdd.length} to add`),n.rulesToAdd.forEach(d=>r.log(` ${Q.green("+")} ${d.name}`)),r.log(` ${Q.cyan("~")} ${n.rulesToUpdate.length} to update`),n.rulesToUpdate.forEach(d=>r.log(` ${Q.cyan("~")} ${d.name}`)),r.log(` ${Q.red("-")} ${n.rulesToDelete.length} to delete`),n.rulesToDelete.forEach(d=>r.log(` ${Q.red("-")} ${d.name}`)),(s.length||a.length||l.length)&&(r.log(""),r.log(Q.bold("IP Rule Changes:")),r.log(` ${Q.green("+")} ${s.length} to add`),r.log(` ${Q.cyan("~")} ${a.length} to update`),r.log(` ${Q.red("-")} ${l.length} to delete`));let c=n.rulesToAdd.length+n.rulesToUpdate.length+n.rulesToDelete.length+s.length+a.length+l.length;r.log(""),r.log(Q.bold(`Summary: ${c} total changes detected`)),r.log(Q.dim("Run `sync` to apply these changes to the remote configuration."))}var Ht={};z(Ht,{builder:()=>wr,command:()=>hr,desc:()=>yr,handler:()=>Ir});E();import Ye from"chalk";import{z as mr}from"zod";le();var hr="download [configVersion]",yr="Download remote Vercel Firewall rules and update local config, optionally for a specific configuration version",wr={configVersion:{type:"number",description:"Specific configuration version to download (defaults to latest)"},config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},dryRun:{alias:"d",type:"boolean",description:"Show rules that would be downloaded without making changes",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Ir=async i=>{await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"downloading firewall rules"},async({config:e,provider:t})=>{if(i.configVersion!==void 0)try{at.parse(i.configVersion)}catch(o){throw o instanceof mr.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),o}await Rr(t,e,i)})};async function Rr(i,e,t){r.start(`Fetching remote firewall configuration${t.configVersion?` version ${t.configVersion}`:""} ...`);let o=await i.fetchConfig(t.configVersion),n=o.rules,s=o.ips??[];if(n.length>0?(r.log(Ye.bold(`
43
43
  Remote Custom Rules to Download (${n.length}):
44
44
  `)),n.forEach(c=>r.log(` - ${c.name}${c.enabled?"":Ye.dim(" (disabled)")}`))):r.info("No custom rules to download..."),s.length>0?(r.log(Ye.bold(`
45
45
  Remote IP Blocking Rules to Download (${s.length}):
@@ -94,24 +94,24 @@ Remote IP Blocking Rules to Download (${s.length}):
94
94
  `,o+=` action = "${n.action}"
95
95
  `,o+=`}
96
96
 
97
- `}),o},kr=async i=>{try{let e;i.source==="remote"?await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"exporting configuration"},async({provider:s})=>{r.start("Fetching remote configuration..."),e=await s.fetchConfig()}):e=await W(i.config),r.start(`Exporting configuration in ${i.format} format...`);let t,o;switch(i.format){case"json":t=JSON.stringify(e,null,2),o="json";break;case"yaml":{let s=se(e);t=`# Firewall Configuration (${s.provider??"unknown"})
98
- `,t+=`version: ${s.metadata?.version??"unknown"}
99
- `,t+=`updatedAt: "${s.metadata?.updatedAt??""}"
100
- `,t+=`rules:
101
- `,s.rules.forEach(a=>{t+=` - name: "${a.name}"
102
- `,t+=` id: "${a.id??""}"
103
- `,t+=` enabled: ${a.enabled}
104
- `,t+=` action: "${a.action.type}"
105
- `}),o="yaml";break}case"terraform":t=Pr(se(e)),o="tf";break;case"markdown":t=Ar(se(e)),o="md";break;default:throw new Error(`Unsupported format: ${i.format}`)}let n=i.output||`firewall-export.${o}`;i.output==="-"||!i.output?r.log(t):(Cr(n,t,"utf8"),r.success(Je.green(`\u2705 Exported to ${n}`)),r.log(""),r.log(Je.bold("Export Summary:")),r.log(`${Je.dim("Format:")} ${i.format}`),r.log(`${Je.dim("Source:")} ${i.source}`),r.log(`${Je.dim("Rules:")} ${e.rules.length} custom, ${(e.ips||[]).length} IP blocking`),r.log(`${Je.dim("Size:")} ${(t.length/1024).toFixed(1)} KB`))}catch(e){H(e,"exporting configuration")}};var Yt={};z(Yt,{BASIC_TEMPLATE_RULES:()=>eo,SECURITY_FOCUSED_TEMPLATE_RULES:()=>to,builder:()=>Lr,command:()=>_r,desc:()=>Sr,handler:()=>Or});E();le();import y from"chalk";import{existsSync as $r}from"fs";var eo=[{id:"rule_block_bad_bots",name:"Block Bad Bots",description:"Block known malicious bots and crawlers based on user agent patterns",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"bot"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_block_scrapers",name:"Block Scrapers",description:"Block common scraping tools and automated requests",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"scraper"}]}],action:{mitigate:{action:"deny"}},active:!1}],to=[{id:"rule_rate_limit_api",name:"Rate Limit API",description:"Rate limit API endpoints to prevent abuse and DoS attacks",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/api/"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:100,window:"1m"}}},active:!1},{id:"rule_block_suspicious_ips",name:"Block Suspicious IPs",description:"Block requests from a known suspicious IP range",conditionGroup:[{conditions:[{type:"ip_address",op:"eq",value:"10.0.0.0/24"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_protect_admin",name:"Protect Admin Routes",description:"Add extra protection for admin and sensitive routes",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/admin"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:10,window:"1m"}}},active:!1}],_r="init [template]",Sr="Initialize a new Doorman configuration",Lr={template:{type:"string",description:"Template to use for initialization (empty, basic, or security-focused)",choices:["empty","basic","security-focused"],default:"empty"},config:{alias:"c",type:"string",description:"Path for the new config file",default:".doorman.json"},force:{alias:"f",type:"boolean",description:"Overwrite existing config file",default:!1},interactive:{alias:"i",type:"boolean",description:"Interactive setup with guided prompts",default:!0},projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"}},Ur=()=>{r.log(""),r.log(y.bold.cyan("\u{1F6AA} Welcome to Doorman!")),r.log(y.dim(`Let's set up your firewall configuration.
97
+ `}),o},kr=async i=>{try{let e=i.output==="-"||!i.output,t;i.source==="remote"?await Y({config:i.config,provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,errorContext:"exporting configuration"},async({provider:a})=>{e||r.start("Fetching remote configuration..."),t=await a.fetchConfig()}):t=await W(i.config),e||r.start(`Exporting configuration in ${i.format} format...`);let o,n;switch(i.format){case"json":o=JSON.stringify(t,null,2),n="json";break;case"yaml":{let a=se(t);o=`# Firewall Configuration (${a.provider??"unknown"})
98
+ `,o+=`version: ${a.metadata?.version??"unknown"}
99
+ `,o+=`updatedAt: "${a.metadata?.updatedAt??""}"
100
+ `,o+=`rules:
101
+ `,a.rules.forEach(l=>{o+=` - name: "${l.name}"
102
+ `,o+=` id: "${l.id??""}"
103
+ `,o+=` enabled: ${l.enabled}
104
+ `,o+=` action: "${l.action.type}"
105
+ `}),n="yaml";break}case"terraform":o=Pr(se(t)),n="tf";break;case"markdown":o=Ar(se(t)),n="md";break;default:throw new Error(`Unsupported format: ${i.format}`)}let s=i.output||`firewall-export.${n}`;e?r.log(o):(Cr(s,o,"utf8"),r.success(Je.green(`\u2705 Exported to ${s}`)),r.log(""),r.log(Je.bold("Export Summary:")),r.log(`${Je.dim("Format:")} ${i.format}`),r.log(`${Je.dim("Source:")} ${i.source}`),r.log(`${Je.dim("Rules:")} ${t.rules.length} custom, ${(t.ips||[]).length} IP blocking`),r.log(`${Je.dim("Size:")} ${(o.length/1024).toFixed(1)} KB`))}catch(e){H(e,"exporting configuration")}};var Yt={};z(Yt,{BASIC_TEMPLATE_RULES:()=>eo,SECURITY_FOCUSED_TEMPLATE_RULES:()=>to,builder:()=>Lr,command:()=>_r,desc:()=>Sr,handler:()=>Or});E();le();import y from"chalk";import{existsSync as $r}from"fs";var eo=[{id:"rule_block_bad_bots",name:"Block Bad Bots",description:"Block known malicious bots and crawlers based on user agent patterns",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"bot"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_block_scrapers",name:"Block Scrapers",description:"Block common scraping tools and automated requests",conditionGroup:[{conditions:[{type:"user_agent",op:"sub",value:"scraper"}]}],action:{mitigate:{action:"deny"}},active:!1}],to=[{id:"rule_rate_limit_api",name:"Rate Limit API",description:"Rate limit API endpoints to prevent abuse and DoS attacks",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/api/"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:100,window:"1m"}}},active:!1},{id:"rule_block_suspicious_ips",name:"Block Suspicious IPs",description:"Block requests from a known suspicious IP range",conditionGroup:[{conditions:[{type:"ip_address",op:"eq",value:"10.0.0.0/24"}]}],action:{mitigate:{action:"deny"}},active:!1},{id:"rule_protect_admin",name:"Protect Admin Routes",description:"Add extra protection for admin and sensitive routes",conditionGroup:[{conditions:[{type:"path",op:"pre",value:"/admin"}]}],action:{mitigate:{action:"rate_limit",rateLimit:{requests:10,window:"1m"}}},active:!1}],_r="init [template]",Sr="Initialize a new Doorman configuration",Lr={template:{type:"string",description:"Template to use for initialization (empty, basic, or security-focused)",choices:["empty","basic","security-focused"],default:"empty"},config:{alias:"c",type:"string",description:"Path for the new config file",default:".doorman.json"},force:{alias:"f",type:"boolean",description:"Overwrite existing config file",default:!1},interactive:{alias:"i",type:"boolean",description:"Interactive setup with guided prompts",default:!0},projectId:{alias:"p",type:"string",description:"Vercel Project ID"},teamId:{alias:"t",type:"string",description:"Vercel Team ID"}},Ur=()=>{r.log(""),r.log(y.bold.cyan("\u{1F6AA} Welcome to Doorman!")),r.log(y.dim(`Let's set up your firewall configuration.
106
106
  `))},Dr=()=>{r.log(y.bold(`
107
107
  \u{1F4DA} Helpful Resources:`)),r.log(""),r.log(y.cyan("\u{1F517} Find your Project ID:")),r.log(y.dim(" https://vercel.com/dashboard \u2192 Select your project \u2192 Settings \u2192 General")),r.log(""),r.log(y.cyan("\u{1F517} Find your Team ID:")),r.log(y.dim(" https://vercel.com/dashboard \u2192 Team Settings \u2192 General")),r.log(y.dim(" (Leave empty if using personal account)")),r.log(""),r.log(y.cyan("\u{1F517} Create API Token:")),r.log(y.dim(" https://vercel.com/account/tokens \u2192 Create Token")),r.log(y.dim(" Scopes needed: Read & Write for your project")),r.log(""),r.log(y.cyan("\u{1F517} Documentation:")),r.log(y.dim(" https://doorman.griffen.codes/getting-started")),r.log("")},xr=async i=>{let e=i.projectId,t=i.teamId;return i.interactive&&(r.log(y.bold("\u{1F4CB} Project Configuration")),r.log(y.dim(`We need your Vercel project details to configure the firewall.
108
108
  `)),e||(e=await w("Enter your Vercel Project ID:",{type:"text"})),t||await w("Are you using a Vercel team account?",{type:"confirm"})&&(t=await w("Enter your Vercel Team ID:",{type:"text"})),process.env.VERCEL_TOKEN?r.log(y.green("\u2705 VERCEL_TOKEN environment variable found")):(r.log(""),r.log(y.yellow("\u26A0\uFE0F VERCEL_TOKEN environment variable not found")),r.log(y.dim("You'll need to set this before running sync commands.")),await w("Would you like to see how to create an API token?",{type:"confirm"})&&(r.log(""),r.log(y.bold("\u{1F511} Creating a Vercel API Token:")),r.log("1. Go to https://vercel.com/account/tokens"),r.log('2. Click "Create Token"'),r.log('3. Give it a descriptive name (e.g., "Doorman Firewall")'),r.log("4. Set expiration as needed"),r.log("5. Copy the token and set it as an environment variable:"),r.log(""),r.log(y.cyan(' export VERCEL_TOKEN="your-token-here"')),r.log(y.dim(" # Add this to your ~/.bashrc, ~/.zshrc, or .env file")),r.log("")))),{projectId:e,teamId:t}},Or=async i=>{try{let e=i.config||".doorman.json";if(i.interactive&&Ur(),$r(e)&&!i.force&&!await w(`Config file ${e} already exists. Do you want to overwrite it?`,{type:"confirm"})){r.info(y.yellow("Initialization cancelled."));return}let{projectId:t,teamId:o}=await xr(i);i.interactive&&(r.log(""),r.log(y.bold("\u{1F3A8} Template Selection")),r.log(y.dim(`Choose a starting template for your firewall configuration:
109
- `)),r.log(y.cyan("\u{1F4DD} empty")+y.dim(" - Minimal config, add your own rules")),r.log(y.cyan("\u{1F6E1}\uFE0F basic")+y.dim(" - Includes basic bot protection")),r.log(y.cyan("\u{1F512} security-focused")+y.dim(" - Comprehensive security rules")),r.log(""));let n=i.template;i.interactive&&!i.template&&(n=await w("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),r.start(`Creating ${n} configuration...`);let s=dt();switch(t&&(s.projectId=t),o&&(s.teamId=o),n){case"basic":s={...s,rules:eo};break;case"security-focused":s={...s,rules:to};break;default:break}await j(s,e),r.success(y.green(`\u2705 Created ${e}`)),r.log(""),r.log(y.bold("\u{1F4CB} Configuration Summary:")),r.log(`${y.dim("File:")} ${e}`),r.log(`${y.dim("Template:")} ${n}`),r.log(`${y.dim("Project ID:")} ${t||y.yellow("Not set - add manually")}`),r.log(`${y.dim("Team ID:")} ${o||y.dim("None (personal account)")}`),r.log(`${y.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),r.log(""),r.log(y.bold("\u{1F680} Next Steps:")),t||r.log(`1. ${y.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||r.log(`${t?"1":"2"}. ${y.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;r.log(`${a}. ${y.dim("Review and enable rules in")} ${e}`),r.log(`${a+1}. ${y.dim("Run")} ${y.cyan("doorman validate")} ${y.dim("to check your config")}`),r.log(`${a+2}. ${y.dim("Run")} ${y.cyan("doorman status")} ${y.dim("to see sync status")}`),r.log(`${a+3}. ${y.dim("Run")} ${y.cyan("doorman sync")} ${y.dim("to deploy your rules")}`),n!=="empty"&&(r.log(""),r.log(y.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),r.log(y.dim(" Review each rule and set active: true when ready."))),i.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Dr()}catch(e){H(e,"initializing configuration")}};var Jt={};z(Jt,{builder:()=>Br,command:()=>Nr,desc:()=>Vr,handler:()=>Wr});E();import de from"chalk";import{z as Fr}from"zod";var Nr="list [configVersion]",Vr="List Vercel Firewall rules, optionally for a specific configuration version",Br={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file or VERCEL_PROJECT_ID environment variable)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file or VERCEL_TEAM_ID environment variable)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",description:"Output format (json or table)",choices:["json","table"],default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Wr=async i=>{await Y({provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({provider:e})=>{if(i.configVersion!==void 0)try{at.parse(i.configVersion)}catch(t){throw t instanceof Fr.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),t}await jr(e,i)})};async function jr(i,e){r.start(`Fetching firewall configuration${e.configVersion?` version ${e.configVersion}`:""} ...`);let t=await i.fetchConfig(e.configVersion),o=t.rules,n=t.ips??[],s=t.metadata?.version??t.version,a=t.metadata?.updatedAt;if(r.info(`Found ${de.cyan(o.length)} custom rules and ${de.cyan(n.length)} IP blocking rules
110
- `+de.dim(`Version: ${de.yellow(s??"unknown")}${a?` \u2022 Last Updated: ${de.yellow(a)}`:""}`)),e.format==="json"){r.info(JSON.stringify({version:s,updatedAt:a,rules:o,ips:n},null,2));return}if(o.length===0&&n.length===0){r.info(de.yellow("No rules found"));return}o.length>0?(r.log(de.bold.underline(`
109
+ `)),r.log(y.cyan("\u{1F4DD} empty")+y.dim(" - Minimal config, add your own rules")),r.log(y.cyan("\u{1F6E1}\uFE0F basic")+y.dim(" - Includes basic bot protection")),r.log(y.cyan("\u{1F512} security-focused")+y.dim(" - Comprehensive security rules")),r.log(""));let n=i.template;i.interactive&&!i.template&&(n=await w("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),r.start(`Creating ${n} configuration...`);let s=dt();switch(t&&(s.projectId=t),o&&(s.teamId=o),n){case"basic":s={...s,rules:eo};break;case"security-focused":s={...s,rules:to};break;default:break}await j(s,e),r.success(y.green(`\u2705 Created ${e}`)),r.log(""),r.log(y.bold("\u{1F4CB} Configuration Summary:")),r.log(`${y.dim("File:")} ${e}`),r.log(`${y.dim("Template:")} ${n}`),r.log(`${y.dim("Project ID:")} ${t||y.yellow("Not set - add manually")}`),r.log(`${y.dim("Team ID:")} ${o||y.dim("None (personal account)")}`),r.log(`${y.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),r.log(""),r.log(y.bold("\u{1F680} Next Steps:")),t||r.log(`1. ${y.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||r.log(`${t?"1":"2"}. ${y.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;r.log(`${a}. ${y.dim("Review and enable rules in")} ${e}`),r.log(`${a+1}. ${y.dim("Run")} ${y.cyan("doorman validate")} ${y.dim("to check your config")}`),r.log(`${a+2}. ${y.dim("Run")} ${y.cyan("doorman status")} ${y.dim("to see sync status")}`),r.log(`${a+3}. ${y.dim("Run")} ${y.cyan("doorman sync")} ${y.dim("to deploy your rules")}`),n!=="empty"&&(r.log(""),r.log(y.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),r.log(y.dim(" Review each rule and set active: true when ready."))),i.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Dr()}catch(e){H(e,"initializing configuration")}};var Jt={};z(Jt,{builder:()=>Br,command:()=>Nr,desc:()=>Vr,handler:()=>Wr});E();import de from"chalk";import{z as Fr}from"zod";var Nr="list [configVersion]",Vr="List Vercel Firewall rules, optionally for a specific configuration version",Br={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:Z,projectId:{alias:"p",type:"string",description:"Vercel Project ID (can be set in config file or VERCEL_PROJECT_ID environment variable)"},teamId:{alias:"t",type:"string",description:"Vercel Team ID (can be set in config file or VERCEL_TEAM_ID environment variable)"},token:{type:"string",description:"Vercel API token (defaults to VERCEL_TOKEN env var)"},apiToken:{type:"string",description:"Cloudflare API token (defaults to CLOUDFLARE_API_TOKEN env var)"},zoneId:{type:"string",description:"Cloudflare Zone ID (defaults to CLOUDFLARE_ZONE_ID env var)"},accountId:{type:"string",description:"Cloudflare Account ID (optional)"},workspaceId:{type:"string",description:"Fastly Next-Gen WAF Workspace ID (defaults to FASTLY_WORKSPACE_ID env var)"},format:{alias:"f",type:"string",description:"Output format (json or table)",choices:["json","table"],default:"table"},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Wr=async i=>{await Y({provider:i.provider,projectId:i.projectId,teamId:i.teamId,token:i.token,apiToken:i.apiToken,zoneId:i.zoneId,accountId:i.accountId,workspaceId:i.workspaceId,debug:i.debug,ci:i.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({provider:e})=>{if(i.configVersion!==void 0)try{at.parse(i.configVersion)}catch(t){throw t instanceof Fr.ZodError&&(r.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),t}await jr(e,i)})};async function jr(i,e){let t=e.format==="json";t||r.start(`Fetching firewall configuration${e.configVersion?` version ${e.configVersion}`:""} ...`);let o=await i.fetchConfig(e.configVersion),n=o.rules,s=o.ips??[],a=o.metadata?.version??o.version,l=o.metadata?.updatedAt;if(t){r.log(JSON.stringify({version:a,updatedAt:l,rules:n,ips:s},null,2));return}if(r.info(`Found ${de.cyan(n.length)} custom rules and ${de.cyan(s.length)} IP blocking rules
110
+ `+de.dim(`Version: ${de.yellow(a??"unknown")}${l?` \u2022 Last Updated: ${de.yellow(l)}`:""}`)),n.length===0&&s.length===0){r.info(de.yellow("No rules found"));return}n.length>0?(r.log(de.bold.underline(`
111
111
  Custom Rules:`),`
112
- `),o.forEach(l=>r.log(` - ${l.name}${l.enabled?"":de.dim(" (disabled)")}`))):r.info(de.cyan("No custom rules found")),n.length>0?(r.log(de.bold.underline(`
112
+ `),n.forEach(c=>r.log(` - ${c.name}${c.enabled?"":de.dim(" (disabled)")}`))):r.info(de.cyan("No custom rules found")),s.length>0?(r.log(de.bold.underline(`
113
113
  IP Blocking Rules:`),`
114
- `),n.forEach(l=>r.log(` - ${l.ip} (${l.action})`))):r.info(de.cyan("No IP blocking rules found"))}var Xt={};z(Xt,{aliases:()=>Mr,builder:()=>Kr,command:()=>zr,desc:()=>Gr,findFuzzyMatches:()=>ro,handler:()=>Hr,levenshtein:()=>no});E();le();import D from"chalk";import{LogLevels as qr}from"consola";var zr="remove [type]",Mr=["rm","delete"],Gr="Remove a firewall rule or IP entry from your configuration",Kr={type:{type:"string",description:'What to remove: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Select rules to remove from a list",default:!1},name:{alias:"n",type:"string",description:"Remove rule by name (exact match)"},id:{type:"array",string:!0,description:"Remove rule by ID (supports multiple)"},ip:{type:"string",description:"Remove IP rule by IP address"},disabled:{type:"boolean",description:"Remove all disabled (active: false) rules",default:!1},all:{type:"boolean",description:"Remove all rules (requires confirmation)",default:!1},force:{alias:"f",type:"boolean",description:"Skip confirmation prompt",default:!1},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be removed without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}};function no(i,e){let t=i.length,o=e.length;if(t===0)return o;if(o===0)return t;let n=Array.from({length:t+1},(a,l)=>l),s=new Array(t+1).fill(0);for(let a=1;a<=o;a++){s[0]=a;for(let l=1;l<=t;l++)e.charAt(a-1)===i.charAt(l-1)?s[l]=n[l-1]:s[l]=Math.min(n[l-1]+1,s[l-1]+1,n[l]+1);for(let l=0;l<=t;l++)n[l]=s[l]}return n[t]}function ro(i,e,t=3){let o=i.toLowerCase(),n=e.filter(l=>l.name.toLowerCase().includes(o)||o.includes(l.name.toLowerCase()));if(n.length>0)return n.slice(0,t);let s=e.map(l=>({rule:l,distance:no(o,l.name.toLowerCase())})).sort((l,c)=>l.distance-c.distance),a=Math.max(i.length*.6,3);return s.filter(l=>l.distance<=a).slice(0,t).map(l=>l.rule)}function Qt(i){for(let e of i){let t=e.id?` (${e.id})`:"",o=e.active?"":D.dim(" [disabled]"),n=e.action.mitigate.action;r.log(D.dim(` - ${e.name}${t} \u2014 ${n}${o}`))}}function io(i){for(let e of i){let t=e.notes?` \u2014 ${e.notes}`:"";r.log(D.dim(` - ${e.ip}${t}`))}}async function oo(i,e,t,o="rule"){if(e)return!0;if(t)return await w(D.red(`\u26A0\uFE0F Are you sure you want to remove ALL ${o}s? Type "YES" to confirm:`),{type:"text"})==="YES";let n=i===1?`Confirm removal of 1 ${o}?`:`Confirm removal of ${i} ${o}s?`;return await w(n,{type:"confirm",initial:!1})}var Hr=async i=>{try{i.debug&&(r.level=qr.debug),r.debug("Remove command arguments:",i);let e=i.type||"rule";r.start("Loading configuration...");let t=await W(i.config);if(e==="ip"){let o=t.ips||[];if(o.length===0){r.info("No IP rules in configuration.");return}let n=[];if(i.interactive){let l=o.map(d=>{let u=d.notes?` \u2014 ${d.notes}`:"";return`${d.ip}${u}`}),c=await w("Select IP rules to remove:",{type:"multiselect",options:l});n=o.filter(d=>{let u=`${d.ip}${d.notes?` \u2014 ${d.notes}`:""}`;return c.includes(u)})}else i.ip?(n=o.filter(l=>l.ip===i.ip),n.length===0&&(r.error(D.red(`No IP rule found matching "${i.ip}"`)),o.length>0&&(r.log(D.dim(`
114
+ `),s.forEach(c=>r.log(` - ${c.ip} (${c.action})`))):r.info(de.cyan("No IP blocking rules found"))}var Xt={};z(Xt,{aliases:()=>Mr,builder:()=>Kr,command:()=>zr,desc:()=>Gr,findFuzzyMatches:()=>ro,handler:()=>Hr,levenshtein:()=>no});E();le();import D from"chalk";import{LogLevels as qr}from"consola";var zr="remove [type]",Mr=["rm","delete"],Gr="Remove a firewall rule or IP entry from your configuration",Kr={type:{type:"string",description:'What to remove: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Select rules to remove from a list",default:!1},name:{alias:"n",type:"string",description:"Remove rule by name (exact match)"},id:{type:"array",string:!0,description:"Remove rule by ID (supports multiple)"},ip:{type:"string",description:"Remove IP rule by IP address"},disabled:{type:"boolean",description:"Remove all disabled (active: false) rules",default:!1},all:{type:"boolean",description:"Remove all rules (requires confirmation)",default:!1},force:{alias:"f",type:"boolean",description:"Skip confirmation prompt",default:!1},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be removed without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}};function no(i,e){let t=i.length,o=e.length;if(t===0)return o;if(o===0)return t;let n=Array.from({length:t+1},(a,l)=>l),s=new Array(t+1).fill(0);for(let a=1;a<=o;a++){s[0]=a;for(let l=1;l<=t;l++)e.charAt(a-1)===i.charAt(l-1)?s[l]=n[l-1]:s[l]=Math.min(n[l-1]+1,s[l-1]+1,n[l]+1);for(let l=0;l<=t;l++)n[l]=s[l]}return n[t]}function ro(i,e,t=3){let o=i.toLowerCase(),n=e.filter(l=>l.name.toLowerCase().includes(o)||o.includes(l.name.toLowerCase()));if(n.length>0)return n.slice(0,t);let s=e.map(l=>({rule:l,distance:no(o,l.name.toLowerCase())})).sort((l,c)=>l.distance-c.distance),a=Math.max(i.length*.6,3);return s.filter(l=>l.distance<=a).slice(0,t).map(l=>l.rule)}function Qt(i){for(let e of i){let t=e.id?` (${e.id})`:"",o=e.active?"":D.dim(" [disabled]"),n=e.action.mitigate.action;r.log(D.dim(` - ${e.name}${t} \u2014 ${n}${o}`))}}function io(i){for(let e of i){let t=e.notes?` \u2014 ${e.notes}`:"";r.log(D.dim(` - ${e.ip}${t}`))}}async function oo(i,e,t,o="rule"){if(e)return!0;if(t)return await w(D.red(`\u26A0\uFE0F Are you sure you want to remove ALL ${o}s? Type "YES" to confirm:`),{type:"text"})==="YES";let n=i===1?`Confirm removal of 1 ${o}?`:`Confirm removal of ${i} ${o}s?`;return await w(n,{type:"confirm",initial:!1})}var Hr=async i=>{try{i.debug&&(r.level=qr.debug),r.debug("Remove command arguments:",i);let e=i.type||"rule";r.start("Loading configuration...");let t=await W(i.config);if(e==="ip"){let o=t.ips||[];if(o.length===0){r.info("No IP rules in configuration.");return}let n=[];if(i.interactive){let l=o.map(d=>{let u=d.notes?` \u2014 ${d.notes}`:"";return`${d.ip}${u}`}),c=await w("Select IP rules to remove:",{type:"multiselect",options:l});n=o.filter(d=>{let u=`${d.ip}${d.notes?` \u2014 ${d.notes}`:""}`;return c.includes(u)})}else i.ip?(n=o.filter(l=>l.ip===i.ip),n.length===0&&(r.error(D.red(`No IP rule found matching "${i.ip}"`)),o.length>0&&(r.log(D.dim(`
115
115
  Current IP rules:`)),io(o)),process.exit(1))):i.all?n=[...o]:(r.error("Specify --ip, --interactive, or --all to select IP rules to remove."),process.exit(1));if(n.length===0){r.info("No IP rules selected for removal.");return}if(r.log(D.cyan(`
116
116
  ${i.dryRun?"Would remove":"Removing"} ${n.length} IP rule(s):`)),io(n),i.dryRun){r.log(D.dim(`
117
117
  Dry run \u2014 no changes made.`));return}if(!await oo(n.length,i.force||!1,i.all||!1,"IP rule")){r.info("Cancelled.");return}let s=new Set(n.map(l=>l.ip)),a={...t,ips:o.filter(l=>!s.has(l.ip))};await j(a,i.config),r.success(D.green(`\u2714 Removed ${n.length} IP rule(s) from configuration`))}else{let o=t.rules||[];if(o.length===0){r.info("No rules in configuration.");return}let n=[];if(i.interactive){let l=o.map((d,u)=>{let p=d.id?` (${d.id})`:"",g=d.active?"":" [disabled]",h=d.action.mitigate.action;return`[${u}] ${d.name}${p} \u2014 ${h}${g}`}),c=await w("Select rules to remove:",{type:"multiselect",options:l});n=o.filter((d,u)=>c.some(p=>p.startsWith(`[${u}]`)))}else if(i.name){if(n=o.filter(l=>l.name===i.name),n.length>1&&(r.warn(D.yellow(`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gfargo/doorman",
3
- "version": "3.10.1",
3
+ "version": "3.10.2",
4
4
  "description": "Manage firewall rules as code across multiple providers",
5
5
  "publishConfig": {
6
6
  "access": "public"