@gfargo/doorman 2.1.0 → 3.0.0
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 +10 -10
- package/dist/bin/run.mjs +10 -10
- package/package.json +1 -1
package/dist/bin/run.js
CHANGED
|
@@ -9,14 +9,14 @@
|
|
|
9
9
|
\u{1F4BE} Backup Recommended: ${c.reason}`),c.instructions.forEach(g=>{o.info(` \u2022 ${g}`)}));let d=this.getRollbackGuidance(t);d.available&&(o.info(`
|
|
10
10
|
\u{1F504} Rollback Available: ${d.method}`),d.timeWindow&&o.info(` Time Window: ${d.timeWindow}`));let u=this.getConfirmationMessage(s,t);return await R(u,{type:"confirm"})?(o.info("\u2705 Operation confirmed by user"),!0):(o.info("Operation cancelled by user"),!1)}static async performDryRunValidation(e,t,i){o.info(`\u{1F50D} Performing dry-run validation for ${t}...`);let r=[],s={rulesToAdd:[],rulesToUpdate:[],rulesToDelete:[],ipsToAdd:[],ipsToUpdate:[],ipsToDelete:[],hasChanges:!1};try{this.validateConfigurationStructure(e,r),s=await i(e),this.validateChanges(s,r),this.checkForPotentialIssues(e,s,r);let a=r.length===0;return a?o.info("\u2705 Dry-run validation passed"):(o.warn(`\u26A0\uFE0F Dry-run validation found ${r.length} issues:`),r.forEach(l=>o.warn(` \u2022 ${l}`))),{valid:a,changes:s,issues:r}}catch(a){let l=`Dry-run validation failed: ${a instanceof Error?a.message:String(a)}`;return r.push(l),o.error(l),{valid:!1,changes:s,issues:r}}}static getBackupRecommendation(e,t){return{"sync rules":{recommended:t!=="low",reason:"Rule synchronization can overwrite existing firewall configuration",instructions:['Run "doorman download" to backup current Cloudflare rules',"Save the downloaded configuration file with a timestamp","Consider using version control for configuration files"]},"delete ruleset":{recommended:!0,reason:"Ruleset deletion is irreversible and removes all associated rules",instructions:['Export current ruleset with "doorman export"',"Document the ruleset ID and configuration","Ensure you have the original configuration files"]},"update rules":{recommended:t==="high",reason:"Rule updates can affect traffic flow and security posture",instructions:["Download current configuration as backup","Test changes in a staging environment if possible","Have rollback plan ready"]},"clear all rules":{recommended:!0,reason:"Clearing all rules removes all firewall protection",instructions:['Export complete configuration with "doorman export"',"Save configuration to version control","Document business justification for clearing rules"]}}[e]||{recommended:t!=="low",reason:"Operation may modify existing configuration",instructions:["Download current configuration as backup","Document the changes being made"]}}static getRollbackGuidance(e){return{"sync rules":{available:!0,method:"Restore from backup configuration",instructions:['Use "doorman sync" with your backup configuration file',"Or manually restore rules in Cloudflare dashboard","Check that all rules are functioning as expected"],timeWindow:"No time limit - can rollback anytime"},"delete ruleset":{available:!1,method:"Recreation required",instructions:["Deleted rulesets cannot be restored","Must recreate ruleset and rules from backup",'Use "doorman sync" with backup configuration']},"update rules":{available:!0,method:"Restore previous rule version",instructions:["Sync with previous configuration version","Or use Cloudflare dashboard to revert changes","Verify rule functionality after rollback"],timeWindow:"Immediate - no time restrictions"},"clear all rules":{available:!0,method:"Restore from backup",instructions:["Sync with backup configuration file","Verify all rules are restored correctly","Test firewall functionality"],timeWindow:"No time limit"}}[e]||{available:!0,method:"Restore from backup",instructions:["Use backup configuration to restore previous state","Verify changes are reverted correctly"]}}static formatRiskLevel(e){let t=require("chalk");switch(e){case"low":return t.green("LOW");case"medium":return t.yellow("MEDIUM");case"high":return t.red("HIGH");default:return String(e).toUpperCase()}}static displayChangeSummary(e){o.info(`
|
|
11
11
|
\u{1F4CA} Change Summary:`),e.rulesToAdd?.length&&o.info(` \u2022 Rules to add: ${e.rulesToAdd.length}`),e.rulesToUpdate?.length&&o.info(` \u2022 Rules to update: ${e.rulesToUpdate.length}`),e.rulesToDelete?.length&&o.info(` \u2022 Rules to delete: ${e.rulesToDelete.length}`),e.ipsToAdd?.length&&o.info(` \u2022 IPs to add: ${e.ipsToAdd.length}`),e.ipsToUpdate?.length&&o.info(` \u2022 IPs to update: ${e.ipsToUpdate.length}`),e.ipsToDelete?.length&&o.info(` \u2022 IPs to delete: ${e.ipsToDelete.length}`),e.hasChanges||o.info(" \u2022 No changes detected")}static displayRiskWarnings(e,t){let i={low:["This operation has minimal risk of disrupting service"],medium:["This operation may temporarily affect traffic filtering","Monitor your application after the change"],high:["\u26A0\uFE0F This operation can significantly impact security and traffic flow","\u26A0\uFE0F Ensure you have tested the configuration in a safe environment","\u26A0\uFE0F Have a rollback plan ready before proceeding"]},r={"clear all rules":["\u{1F6A8} This will remove ALL firewall protection","\u{1F6A8} Your application will be unprotected until new rules are added"],"delete ruleset":["\u{1F6A8} This action is IRREVERSIBLE","\u{1F6A8} All rules in the ruleset will be permanently deleted"]};o.info(`
|
|
12
|
-
\u26A0\uFE0F Warnings:`),i[e]?.forEach(s=>{o.warn(` ${s}`)}),r[t]?.forEach(s=>{o.warn(` ${s}`)})}static getConfirmationMessage(e,t){let i=`Do you want to proceed with ${t}?`;switch(e){case"high":return`${i} (Type 'yes' to confirm high-risk operation)`;case"medium":return`${i} (Proceed with caution)`;default:return i}}static validateConfigurationStructure(e,t){if(!e){t.push("Configuration is null or undefined");return}e.version||t.push("Configuration missing version field"),e.provider||t.push("Configuration missing provider field"),Array.isArray(e.rules)||t.push("Configuration rules field is not an array"),e.ips&&!Array.isArray(e.ips)&&t.push("Configuration ips field is not an array")}static validateChanges(e,t){let i=(e.rulesToDelete?.length||0)+(e.ipsToDelete?.length||0),r=(e.rulesToAdd?.length||0)+(e.rulesToUpdate?.length||0)+(e.ipsToAdd?.length||0)+(e.ipsToUpdate?.length||0)+i;if(i>0&&r>0){let s=i/r;s>.5&&t.push(`High deletion ratio detected: ${Math.round(s*100)}% of changes are deletions`)}r>100&&t.push(`Large number of changes detected: ${r} total changes`)}static checkForPotentialIssues(e,t,i){t.rulesToDelete?.length===e.rules.length&&e.rules.length>0&&i.push("All existing rules will be deleted - this removes all firewall protection");let r=e.rules.filter(l=>l.action.type==="deny"&&l.conditions.some(c=>c.field==="path"&&(c.value==="/"||c.value==="*")));r.length>0&&i.push(`Found ${r.length} rules that may block all traffic`),e.ips&&e.ips.length>1e3&&i.push(`Large IP list detected: ${e.ips.length} IPs may impact performance`);let s=e.rules.map(l=>l.name),a=s.filter((l,c)=>s.indexOf(l)!==c);a.length>0&&i.push(`Duplicate rule names found: ${[...new Set(a)].join(", ")}`)}}});var Vi=require("dotenv"),Bi=P(require("yargs"));var gt={};q(gt,{builder:()=>so,command:()=>no,desc:()=>ro,generateRuleId:()=>pt,handler:()=>go});var D=P(require("chalk")),ti=require("consola");T();var A=require("zod"),Ze=A.z.number().int().positive().optional(),Gi=A.z.string().ip().or(A.z.string().regex(/^(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/)),Mt=A.z.string().optional(),Hi=A.z.enum(["eq","pre","suf","inc","sub","re","ex","nex"]),Zi=A.z.enum(["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]),Ki=A.z.object({op:Hi,neg:A.z.boolean().optional(),type:Zi,value:A.z.union([A.z.string(),A.z.array(A.z.string()),A.z.array(A.z.number())]).optional(),key:A.z.string().optional()}).refine(n=>{switch(n.type){case"ip_address":case"method":case"environment":case"protocol":return n.op==="eq"||n.op==="inc";default:return!0}},"Invalid operator for the given condition type").refine(n=>{let e=["header","cookie"];return!(e.includes(n.type)&&!n.key||e.includes(n.type)&&n.op!=="ex"&&n.op!=="nex"&&!n.value)},"Missing key from condition").refine(n=>!(["ex","nex"].includes(n.op)&&n.neg),"Negation is not supported for the given operator").refine(n=>!(n.op==="inc"&&!Array.isArray(n.value)),"When using `inc` operator, value must be an array"),ot=A.z.object({conditions:A.z.array(Ki).min(1,"Condition group must have at least one condition")}),Ji=A.z.string().regex(/^\d+[smhd]$|^permanent$/),nt=A.z.object({requests:A.z.number().positive().int().refine(n=>n>0,"requests must be positive"),window:A.z.string().regex(/^\d+[smhd]$/).refine(n=>parseInt(n)>0,"window duration must be positive")}),Xi=A.z.object({location:A.z.string(),permanent:A.z.boolean().optional()}),Qi=A.z.enum(["log","deny","challenge","bypass","rate_limit","redirect"]),Yi=A.z.object({action:Qi,rateLimit:nt.nullable().optional(),redirect:Xi.nullable().optional(),actionDuration:Ji.nullable().optional()}),eo=A.z.object({mitigate:Yi}),rt=A.z.object({id:Mt,name:A.z.string(),description:A.z.string().optional(),conditionGroup:A.z.array(ot),action:eo,active:A.z.boolean()});var st=A.z.object({id:Mt,ip:Gi,hostname:A.z.string(),notes:A.z.string().optional(),action:A.z.literal("deny")}),to=A.z.object({projectId:A.z.string().optional(),teamId:A.z.string().optional()}),fe=to.extend({rules:A.z.array(rt),ips:A.z.array(st).optional(),version:A.z.number().optional(),updatedAt:A.z.string().optional()});ne();var ye=require("fs"),Xt=require("path");T();var Kt=P(require("ajv"));var Ht="https://doorman.griffen.codes/schema.json",Zt={$schema:"http://json-schema.org/draft-07/schema#",title:"Doorman Config",description:"Schema for
|
|
12
|
+
\u26A0\uFE0F Warnings:`),i[e]?.forEach(s=>{o.warn(` ${s}`)}),r[t]?.forEach(s=>{o.warn(` ${s}`)})}static getConfirmationMessage(e,t){let i=`Do you want to proceed with ${t}?`;switch(e){case"high":return`${i} (Type 'yes' to confirm high-risk operation)`;case"medium":return`${i} (Proceed with caution)`;default:return i}}static validateConfigurationStructure(e,t){if(!e){t.push("Configuration is null or undefined");return}e.version||t.push("Configuration missing version field"),e.provider||t.push("Configuration missing provider field"),Array.isArray(e.rules)||t.push("Configuration rules field is not an array"),e.ips&&!Array.isArray(e.ips)&&t.push("Configuration ips field is not an array")}static validateChanges(e,t){let i=(e.rulesToDelete?.length||0)+(e.ipsToDelete?.length||0),r=(e.rulesToAdd?.length||0)+(e.rulesToUpdate?.length||0)+(e.ipsToAdd?.length||0)+(e.ipsToUpdate?.length||0)+i;if(i>0&&r>0){let s=i/r;s>.5&&t.push(`High deletion ratio detected: ${Math.round(s*100)}% of changes are deletions`)}r>100&&t.push(`Large number of changes detected: ${r} total changes`)}static checkForPotentialIssues(e,t,i){t.rulesToDelete?.length===e.rules.length&&e.rules.length>0&&i.push("All existing rules will be deleted - this removes all firewall protection");let r=e.rules.filter(l=>l.action.type==="deny"&&l.conditions.some(c=>c.field==="path"&&(c.value==="/"||c.value==="*")));r.length>0&&i.push(`Found ${r.length} rules that may block all traffic`),e.ips&&e.ips.length>1e3&&i.push(`Large IP list detected: ${e.ips.length} IPs may impact performance`);let s=e.rules.map(l=>l.name),a=s.filter((l,c)=>s.indexOf(l)!==c);a.length>0&&i.push(`Duplicate rule names found: ${[...new Set(a)].join(", ")}`)}}});var Vi=require("dotenv"),Bi=P(require("yargs"));var gt={};q(gt,{builder:()=>so,command:()=>no,desc:()=>ro,generateRuleId:()=>pt,handler:()=>go});var D=P(require("chalk")),ti=require("consola");T();var A=require("zod"),Ze=A.z.number().int().positive().optional(),Gi=A.z.string().ip().or(A.z.string().regex(/^(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/)),Mt=A.z.string().optional(),Hi=A.z.enum(["eq","pre","suf","inc","sub","re","ex","nex"]),Zi=A.z.enum(["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]),Ki=A.z.object({op:Hi,neg:A.z.boolean().optional(),type:Zi,value:A.z.union([A.z.string(),A.z.array(A.z.string()),A.z.array(A.z.number())]).optional(),key:A.z.string().optional()}).refine(n=>{switch(n.type){case"ip_address":case"method":case"environment":case"protocol":return n.op==="eq"||n.op==="inc";default:return!0}},"Invalid operator for the given condition type").refine(n=>{let e=["header","cookie"];return!(e.includes(n.type)&&!n.key||e.includes(n.type)&&n.op!=="ex"&&n.op!=="nex"&&!n.value)},"Missing key from condition").refine(n=>!(["ex","nex"].includes(n.op)&&n.neg),"Negation is not supported for the given operator").refine(n=>!(n.op==="inc"&&!Array.isArray(n.value)),"When using `inc` operator, value must be an array"),ot=A.z.object({conditions:A.z.array(Ki).min(1,"Condition group must have at least one condition")}),Ji=A.z.string().regex(/^\d+[smhd]$|^permanent$/),nt=A.z.object({requests:A.z.number().positive().int().refine(n=>n>0,"requests must be positive"),window:A.z.string().regex(/^\d+[smhd]$/).refine(n=>parseInt(n)>0,"window duration must be positive")}),Xi=A.z.object({location:A.z.string(),permanent:A.z.boolean().optional()}),Qi=A.z.enum(["log","deny","challenge","bypass","rate_limit","redirect"]),Yi=A.z.object({action:Qi,rateLimit:nt.nullable().optional(),redirect:Xi.nullable().optional(),actionDuration:Ji.nullable().optional()}),eo=A.z.object({mitigate:Yi}),rt=A.z.object({id:Mt,name:A.z.string(),description:A.z.string().optional(),conditionGroup:A.z.array(ot),action:eo,active:A.z.boolean()});var st=A.z.object({id:Mt,ip:Gi,hostname:A.z.string(),notes:A.z.string().optional(),action:A.z.literal("deny")}),to=A.z.object({projectId:A.z.string().optional(),teamId:A.z.string().optional()}),fe=to.extend({rules:A.z.array(rt),ips:A.z.array(st).optional(),version:A.z.number().optional(),updatedAt:A.z.string().optional()});ne();var ye=require("fs"),Xt=require("path");T();var Kt=P(require("ajv"));var Ht="https://doorman.griffen.codes/schema.json",Zt={$schema:"http://json-schema.org/draft-07/schema#",title:"Doorman Config",description:"Schema for doorman project configuration files",$ref:"#/definitions/FirewallConfig",definitions:{FirewallConfig:{type:"object",properties:{projectId:{type:"string"},teamId:{type:"string"},$schema:{type:"string"},version:{type:"number"},firewallEnabled:{type:"boolean"},rules:{type:"array",items:{$ref:"#/definitions/CustomRule"}},ips:{type:"array",items:{$ref:"#/definitions/IPBlockingRule"}},updatedAt:{type:"string"}},required:["rules"],additionalProperties:!1,description:"The main configuration type for Vercel Doorman"},CustomRule:{type:"object",properties:{id:{type:"string"},name:{type:"string"},description:{type:"string"},conditionGroup:{type:"array",items:{$ref:"#/definitions/ConditionGroup"}},action:{$ref:"#/definitions/RuleAction"},active:{type:"boolean"}},required:["name","conditionGroup","action","active"],additionalProperties:!1,description:"Rule Types"},ConditionGroup:{type:"object",properties:{conditions:{type:"array",items:{$ref:"#/definitions/RuleCondition"}}},required:["conditions"],additionalProperties:!1},RuleCondition:{type:"object",properties:{op:{$ref:"#/definitions/RuleOperator"},neg:{type:"boolean"},type:{$ref:"#/definitions/RuleType"},key:{type:"string"},value:{anyOf:[{type:"string"},{type:"number"},{type:"array",items:{type:"string"}},{type:"array",items:{type:"number"}}]}},required:["op","type"],additionalProperties:!1,description:"Rule Condition Types"},RuleOperator:{type:"string",enum:["eq","pre","suf","inc","sub","re","ex","nex"]},RuleType:{type:"string",enum:["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]},RuleAction:{type:"object",properties:{mitigate:{$ref:"#/definitions/MitigationAction"}},required:["mitigate"],additionalProperties:!1},MitigationAction:{type:"object",properties:{action:{$ref:"#/definitions/ActionType"},rateLimit:{anyOf:[{$ref:"#/definitions/RateLimit"},{type:"null"}]},redirect:{anyOf:[{$ref:"#/definitions/Redirect"},{type:"null"}]},actionDuration:{type:["string","null"]}},required:["action"],additionalProperties:!1},ActionType:{type:"string",enum:["log","deny","challenge","bypass","rate_limit","redirect"],description:"Core Types"},RateLimit:{type:"object",properties:{requests:{type:"number"},window:{type:"string"}},required:["requests","window"],additionalProperties:!1,description:"Action Types"},Redirect:{type:"object",properties:{location:{type:"string"},permanent:{type:"boolean"}},required:["location"],additionalProperties:!1},IPBlockingRule:{type:"object",properties:{id:{type:"string"},ip:{type:"string"},hostname:{type:"string"},notes:{type:"string"},action:{type:"string",const:"deny"}},required:["ip","hostname","action"],additionalProperties:!1}}};var te=P(require("chalk")),he=class{static formatValidationError(e){let t=e.instancePath||"/",i=te.default.cyan(t);switch(e.keyword){case"type":return`${i}: Expected ${te.default.green(e.schema)}, got ${te.default.red(typeof e.data)}`;case"enum":{let r=e.params.allowedValues;return`${i}: Value must be one of: ${te.default.green(r.join(", "))}`}case"required":{let r=e.params;return`${i}: Missing required property ${te.default.yellow(r.missingProperty)}`}case"additionalProperties":{let r=e.params;return`${i}: Unknown property ${te.default.red(r.additionalProperty)}`}default:return`${i}: ${e.message}`}}static formatCustomError(e,t){return`${te.default.cyan(e)}: ${t}`}static wrapErrorBlock(e){let t=te.default.red.bold("Validation Errors:"),i=e.map(r=>` ${r}`).join(`
|
|
13
13
|
`);return`${t}
|
|
14
14
|
${i}`}static formatDetailedRuleValidation(e,t){let i=te.default.cyan(`Rule: ${e}`),r=t.map(s=>{let a=s.passed?te.default.green("\u2713"):te.default.red("\u2717"),l=s.passed?te.default.green(s.name):te.default.red(s.name);return` ${a} ${l}`}).join(`
|
|
15
15
|
`);return`${i}
|
|
16
16
|
${r}`}};var x=class n extends Error{constructor(t,i,r,s=[]){super(t);this.ajvErrors=i;this.zodError=r;this.customErrors=s;this.name="ValidationError"}ajvErrors;zodError;customErrors;static formatErrors(t,i,r=[]){let s=[];return t?.length&&s.push(...t.map(he.formatValidationError)),i&&s.push(...i.errors.map(a=>`${a.path.join(".")}: ${a.message}`)),r.length&&s.push(...r),he.wrapErrorBlock(s)}getFormattedMessage(){return n.formatErrors(this.ajvErrors,this.zodError,this.customErrors)}},Ie=class n{static instance;ajv;schema;constructor(){this.ajv=new Kt.default({allErrors:!0,verbose:!0}),this.schema=Zt}static getInstance(){return n.instance||(n.instance=new n),n.instance}validateConfig(e){let t=this.ajv.compile(this.schema),i=t(e),r=t.errors,s=fe.safeParse(e),a;s.success||(a=s.error);let l=[];if(i&&s.success){try{this.validateRuleNames(e),this.validateRuleConditionGroup(e),this.validateRuleAction(e)}catch(c){if(c instanceof x)l.push(c.message,...c.customErrors||[]);else throw c}for(let c of e.rules){if(c.action.mitigate?.rateLimit){let d=nt.safeParse(c.action.mitigate.rateLimit);d.success||l.push(...d.error.errors.map(u=>u.message))}if(c.conditionGroup)for(let d of c.conditionGroup){let u=ot.safeParse(d);u.success||l.push(...u.error.errors.map(p=>p.message))}}}if(!i||!s.success||l.length>0)throw new x(`Invalid firewall configuration:
|
|
17
|
-
`+x.formatErrors(r,a,l),r,a,l)}validateRuleNames(e){let t=new Set;for(let i of e.rules){if(t.has(i.name))throw new x(`Duplicate rule name found: "${i.name}"`,null);t.add(i.name)}}validateRuleConditionGroup(e){for(let t of e.rules)if(t.conditionGroup)for(let i of t.conditionGroup){if(!i.conditions||i.conditions.length===0)throw new x(`Rule "${t.name}" has an empty condition group`,null);for(let r of i.conditions){if(!r.type||!r.op)throw new x(`Rule "${t.name}" has an invalid condition`,null);let s=r.type==="header"||r.type==="cookie",a=r.op==="ex"||r.op==="nex";if(s){if(!r.key||typeof r.key!="string")throw new x(`Rule "${t.name}" has an invalid condition`,null);if(!a&&(r.value===void 0||r.value===null))throw new x(`Rule "${t.name}" has an invalid condition`,null)}else if(r.value===void 0||r.value===null)throw new x(`Rule "${t.name}" has an invalid condition`,null);let l=Array.isArray(r.value)?r.value:r.value!==void 0?[r.value]:[];if(Array.isArray(r.value)&&l.length===0)throw new x(`Rule "${t.name}" has an invalid value in condition`,null);if(r.type==="ip_address"){for(let c of l)if(typeof c!="string"||!this.isValidIP(c))throw new x(`Rule "${t.name}" has an invalid IP address in condition`,null)}if(r.type==="geo_as_number")for(let c of l){let d=typeof c=="number"?c:parseInt(String(c),10);if(!this.isValidASN(String(d)))throw new x(`Rule "${t.name}" has an invalid ASN in condition`,null)}if(r.type==="path"&&(r.op==="eq"||r.op==="pre")){for(let c of l)if(typeof c!="string"||!this.isValidPath(c))throw new x(`Rule "${t.name}" has an invalid path in condition`,null)}}}else throw new x("Either conditionGroup or type+values must be provided",null)}validateRuleAction(e){for(let t of e.rules)if(typeof t.action=="object"&&"mitigate"in t.action){let i=t.action.mitigate;if(i?.rateLimit){if(i.rateLimit.requests<=0)throw new x("Invalid rate limit configuration: requests must be positive",null);if(!i.rateLimit.window.match(/^\d+[smhd]$/))throw new x("Invalid rate limit configuration: invalid window format",null);if(parseInt(i.rateLimit.window)<=0)throw new x("Invalid rate limit configuration: window duration must be positive",null)}if(i?.actionDuration&&!i.actionDuration.match(/^\d+[smhd]$|^permanent$/))throw new x("Invalid action duration format: "+i.actionDuration,null);if(i?.redirect&&!i.redirect.location)throw new x("Invalid redirect configuration: location is required",null)}}isValidIP(e){let t=e.split("/"),i=t[0]??"",r=t[1],s=d=>{let u=d.split(".");return u.length!==4?!1:u.every(p=>{if(!/^\d{1,3}$/.test(p))return!1;let g=parseInt(p,10);return g>=0&&g<=255})},a=d=>{let u=d.split(":");return u.length!==8?!1:u.every(p=>/^[0-9a-fA-F]{1,4}$/.test(p))},l=s(i),c=a(i);if(!l&&!c)return!1;if(r!==void 0){let d=parseInt(r,10);if(Number.isNaN(d)||l&&(d<0||d>32)||c&&(d<0||d>128))return!1}return!0}isValidASN(e){let t=parseInt(e,10);return!isNaN(t)&&t>=1&&t<=4294967295}isValidPath(e){return e.startsWith("/")&&!e.includes(" ")}};var Jt=require("fs"),Be=require("path"),at=[".doorman.json","vercel-firewall.config.json"],io=at[0],be=class{static async findConfig(e){let{findUp:t}=await import("find-up");for(let i of at){let r=await t(i,{cwd:e||process.cwd()});if(r)return r}}static async findProjectRoot(e){let t=await this.findConfig(e);return t?(0,Be.dirname)(t):null}static configExists(e){return(0,Jt.existsSync)((0,Be.resolve)(e))}static getDefaultConfigPath(){return(0,Be.resolve)(process.cwd(),io)}static getSupportedFileNames(){return at}};async function z(n,e="required"){let t=oo(e),i=n||await be.findConfig();if(!i||!(0,ye.existsSync)(i)){if(t==="optional")return o.debug("No config file found, returning empty config"),{};let s=be.getDefaultConfigPath();throw new Error(`No config file found. Run \`vercel-doorman init\` to create one at ${s}, or use --config to specify a custom path.`)}let r;try{let s=(0,ye.readFileSync)(i,"utf8");r=JSON.parse(s)}catch(s){throw s instanceof SyntaxError?new Error(`Invalid JSON in config file (${i}): ${s.message}`):s}if(t!=="raw")try{Ie.getInstance().validateConfig(r)}catch(s){if(t==="lenient")o.warn("Config validation failed:",s);else throw s}return r}async function F(n,e,t={validate:!0,throwOnError:!0}){let i=e||await be.findConfig()||be.getDefaultConfigPath();if(t.validate)try{Ie.getInstance().validateConfig(n)}catch(s){if(o.error("Config validation failed:",s),t.throwOnError)throw s}let r=(0,Xt.dirname)(i);(0,ye.existsSync)(r)||(0,ye.mkdirSync)(r,{recursive:!0}),(0,ye.writeFileSync)(i,JSON.stringify(n,null,2)),o.debug(`Config saved to ${i}`)}function oo(n){if(typeof n=="string")return n;let{validate:e=!0,throwOnError:t=!0}=n;return e?t?"required":"lenient":"raw"}var lt=P(require("chalk")),Qt=require("zod");T();function H(n,e){n instanceof SyntaxError?o.log(he.wrapErrorBlock(["Invalid JSON format in config file:",` ${n.message}`])):n instanceof Qt.ZodError?(o.error(lt.default.red("Schema validation failed:")),n.errors.forEach(t=>{let i=t.path.join(".");o.error(lt.default.red(` - ${i}: ${t.message}`))})):n instanceof Error&&n.name==="ValidationError"?o.error(n):n instanceof Error&&n.message.includes("Forbidden")?o.log(he.wrapErrorBlock([`Error ${e}:`," Access denied. Check that your token has the correct scope."," If using a team, ensure the token is scoped to that team."])):o.error(he.wrapErrorBlock([`Error ${e}:`,` ${n instanceof Error?n.message:String(n)}`])),process.exit(1)}var no="add [type]",ro="Add a new firewall rule to your configuration",so={type:{type:"string",description:'Rule type: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Guided prompts for rule creation",default:!1},name:{alias:"n",type:"string",description:"Rule name"},description:{type:"string",description:"Rule description"},field:{type:"string",description:"Condition field type (path, method, user_agent, etc.)"},op:{type:"string",description:"Operator (eq, pre, suf, sub, inc, re, ex, nex)"},value:{type:"string",description:"Match value (string or comma-separated for arrays)"},key:{type:"string",description:"Header/query/cookie key (required for header, cookie types)"},neg:{type:"boolean",description:"Negate the condition",default:!1},action:{alias:"a",type:"string",description:"Action type (deny, challenge, rate_limit, redirect, log, bypass)",default:"deny"},active:{type:"boolean",description:"Enable rule immediately",default:!0},requests:{type:"number",description:"Rate limit: max requests"},window:{type:"string",description:'Rate limit: time window (e.g., "60s", "5m")'},duration:{type:"string",description:'Action duration (e.g., "1h", "permanent")'},location:{type:"string",description:"Redirect URL"},permanent:{type:"boolean",description:"Redirect: use 301 instead of 302"},ip:{type:"string",description:"IP address or CIDR for IP blocking rules"},hostname:{type:"string",description:"Hostname for IP blocking rules",default:"*"},notes:{type:"string",description:"Notes for IP blocking rules"},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be added without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}},ct=["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"],dt=["eq","pre","suf","inc","sub","re","ex","nex"],ut=["log","deny","challenge","bypass","rate_limit","redirect"];function pt(n){let e=n.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");if(!e)throw new Error("Cannot generate rule ID: name must contain at least one alphanumeric character");return"rule_"+e}function ii(n,e){return e==="inc"?n.split(",").map(t=>t.trim()):n}async function ao(){let n=await R("Rule name:",{type:"text"});if(!n.trim())throw new Error("Rule name cannot be empty");let e=await R("Description (optional):",{type:"text"}),t=[],i=!0;for(;i;){let u=[],p=!0;for(;p;){let g=await R("Condition type:",{type:"select",options:ct,initial:"path"}),C=await R("Operator:",{type:"select",options:dt,initial:"eq"}),m;(g==="header"||g==="cookie"||g==="query")&&(m=await R(`${g} key:`,{type:"text"}));let w;if(C!=="ex"&&C!=="nex"){let b=await R("Value:",{type:"text"});w=ii(b,C)}let _=C!=="ex"&&C!=="nex"?await R("Negate this condition?",{type:"confirm",initial:!1}):!1,N={type:g,op:C,...w!==void 0&&{value:w},...m&&{key:m},..._&&{neg:_}};u.push(N),p=await R("Add another condition to this group? (AND logic)",{type:"confirm",initial:!1})}t.push({conditions:u}),i=await R("Add another condition group? (OR logic)",{type:"confirm",initial:!1})}let r=await R("Action:",{type:"select",options:ut,initial:"deny"}),s;if(r==="rate_limit"){let u=parseInt(await R("Max requests:",{type:"text",initial:"100"}),10),p=await R('Time window (e.g., "60s", "1m", "1h"):',{type:"text",initial:"60s"});s={requests:u,window:p}}let a;if(r==="redirect"){let u=await R("Redirect URL:",{type:"text"}),p=await R("Permanent redirect (301)?",{type:"confirm",initial:!1});a={location:u,...p&&{permanent:p}}}let l;(r==="deny"||r==="challenge")&&await R("Set action duration? (default: permanent)",{type:"confirm",initial:!1})&&(l=await R('Duration (e.g., "1h", "1d", "permanent"):',{type:"text",initial:"permanent"}));let c=await R("Enable rule immediately?",{type:"confirm",initial:!0});return{id:pt(n),name:n.trim(),...e.trim()&&{description:e.trim()},conditionGroup:t,action:{mitigate:{action:r,...s&&{rateLimit:s},...a&&{redirect:a},...l&&{actionDuration:l}}},active:c}}function lo(n){if(!n.name)throw new Error("--name is required in inline mode. Use --interactive for guided prompts.");if(!n.field)throw new Error("--field is required in inline mode (e.g., --field path)");if(!n.op)throw new Error("--op is required in inline mode (e.g., --op eq)");let e=n.op;if(!dt.includes(e))throw new Error(`Invalid operator "${n.op}". Valid operators: ${dt.join(", ")}`);let t=n.field;if(!ct.includes(t))throw new Error(`Invalid field type "${n.field}". Valid types: ${ct.join(", ")}`);let i=n.action||"deny";if(!ut.includes(i))throw new Error(`Invalid action "${n.action}". Valid actions: ${ut.join(", ")}`);if(e!=="ex"&&e!=="nex"&&!n.value)throw new Error("--value is required for the given operator");if((t==="header"||t==="cookie"||t==="query")&&!n.key)throw new Error(`--key is required for "${t}" condition type`);if(i==="rate_limit"&&(!n.requests||!n.window))throw new Error("--requests and --window are required when action is rate_limit");if(i==="redirect"&&!n.location)throw new Error("--location is required when action is redirect");let r=n.value?ii(n.value,e):void 0,s={type:t,op:e,...r!==void 0&&{value:r},...n.key&&{key:n.key},...n.neg&&{neg:!0}};return{id:pt(n.name),name:n.name.trim(),...n.description&&{description:n.description},conditionGroup:[{conditions:[s]}],action:{mitigate:{action:i,...i==="rate_limit"&&n.requests&&n.window&&{rateLimit:{requests:n.requests,window:n.window}},...i==="redirect"&&n.location&&{redirect:{location:n.location,...n.permanent&&{permanent:!0}}},...n.duration&&{actionDuration:n.duration}}},active:n.active!==!1}}async function co(){let n=await R("IP address or CIDR (e.g., 192.168.1.100/32):",{type:"text"});if(!n.trim())throw new Error("IP address cannot be empty");let e=await R("Hostname (use * for all):",{type:"text",initial:"*"}),t=await R("Notes (optional):",{type:"text"});return{ip:n.trim(),hostname:e.trim()||"*",...t.trim()&&{notes:t.trim()},action:"deny"}}function uo(n){if(!n.ip)throw new Error("--ip is required for IP blocking rules");return{ip:n.ip.trim(),hostname:n.hostname||"*",...n.notes&&{notes:n.notes},action:"deny"}}function po(n,e){if(n.rules.map(r=>r.name.toLowerCase()).includes(e.name.toLowerCase()))return`A rule named "${e.name}" already exists`;let i=n.rules.map(r=>r.id).filter(Boolean);return e.id&&i.includes(e.id)?`A rule with ID "${e.id}" already exists`:null}function Yt(n){o.log(""),o.log(D.default.bold(` Rule: ${n.name}`)),n.description&&o.log(D.default.dim(` Description: ${n.description}`)),o.log(D.default.dim(` ID: ${n.id}`));let e=n.conditionGroup.map(r=>r.conditions.map(s=>{let a=s.neg?"NOT ":"",l=s.key?`[${s.key}] `:"",c=Array.isArray(s.value)?s.value.join(", "):s.value||"";return`${a}${s.type} ${l}${s.op} "${c}"`}).join(" AND "));o.log(D.default.dim(` Conditions: ${e.join(" OR ")}`));let t=n.action.mitigate.action,i=[];n.action.mitigate.rateLimit&&i.push(`${n.action.mitigate.rateLimit.requests} req/${n.action.mitigate.rateLimit.window}`),n.action.mitigate.redirect&&i.push(`\u2192 ${n.action.mitigate.redirect.location}`),n.action.mitigate.actionDuration&&i.push(`duration: ${n.action.mitigate.actionDuration}`),o.log(D.default.dim(` Action: ${t}${i.length?` (${i.join(", ")})`:""}`)),o.log(D.default.dim(` Active: ${n.active?"\u2705":"\u274C"}`))}function ei(n){o.log(""),o.log(D.default.bold(` IP Rule: ${n.ip}`)),o.log(D.default.dim(` Hostname: ${n.hostname}`)),n.notes&&o.log(D.default.dim(` Notes: ${n.notes}`)),o.log(D.default.dim(` Action: ${n.action}`))}var go=async n=>{try{if(n.debug&&(o.level=ti.LogLevels.debug),o.debug("Add command arguments:",n),(n.type||"rule")==="ip"){let t=n.interactive?await co():uo(n),i=st.safeParse(t);if(i.success||(o.error(D.default.red("Rule validation failed:")),i.error.errors.forEach(a=>{let l=a.path.join(".");o.error(D.default.red(` - ${l||"ip"}: ${a.message}`))}),process.exit(1)),n.dryRun){o.info(D.default.cyan(`
|
|
17
|
+
`+x.formatErrors(r,a,l),r,a,l)}validateRuleNames(e){let t=new Set;for(let i of e.rules){if(t.has(i.name))throw new x(`Duplicate rule name found: "${i.name}"`,null);t.add(i.name)}}validateRuleConditionGroup(e){for(let t of e.rules)if(t.conditionGroup)for(let i of t.conditionGroup){if(!i.conditions||i.conditions.length===0)throw new x(`Rule "${t.name}" has an empty condition group`,null);for(let r of i.conditions){if(!r.type||!r.op)throw new x(`Rule "${t.name}" has an invalid condition`,null);let s=r.type==="header"||r.type==="cookie",a=r.op==="ex"||r.op==="nex";if(s){if(!r.key||typeof r.key!="string")throw new x(`Rule "${t.name}" has an invalid condition`,null);if(!a&&(r.value===void 0||r.value===null))throw new x(`Rule "${t.name}" has an invalid condition`,null)}else if(r.value===void 0||r.value===null)throw new x(`Rule "${t.name}" has an invalid condition`,null);let l=Array.isArray(r.value)?r.value:r.value!==void 0?[r.value]:[];if(Array.isArray(r.value)&&l.length===0)throw new x(`Rule "${t.name}" has an invalid value in condition`,null);if(r.type==="ip_address"){for(let c of l)if(typeof c!="string"||!this.isValidIP(c))throw new x(`Rule "${t.name}" has an invalid IP address in condition`,null)}if(r.type==="geo_as_number")for(let c of l){let d=typeof c=="number"?c:parseInt(String(c),10);if(!this.isValidASN(String(d)))throw new x(`Rule "${t.name}" has an invalid ASN in condition`,null)}if(r.type==="path"&&(r.op==="eq"||r.op==="pre")){for(let c of l)if(typeof c!="string"||!this.isValidPath(c))throw new x(`Rule "${t.name}" has an invalid path in condition`,null)}}}else throw new x("Either conditionGroup or type+values must be provided",null)}validateRuleAction(e){for(let t of e.rules)if(typeof t.action=="object"&&"mitigate"in t.action){let i=t.action.mitigate;if(i?.rateLimit){if(i.rateLimit.requests<=0)throw new x("Invalid rate limit configuration: requests must be positive",null);if(!i.rateLimit.window.match(/^\d+[smhd]$/))throw new x("Invalid rate limit configuration: invalid window format",null);if(parseInt(i.rateLimit.window)<=0)throw new x("Invalid rate limit configuration: window duration must be positive",null)}if(i?.actionDuration&&!i.actionDuration.match(/^\d+[smhd]$|^permanent$/))throw new x("Invalid action duration format: "+i.actionDuration,null);if(i?.redirect&&!i.redirect.location)throw new x("Invalid redirect configuration: location is required",null)}}isValidIP(e){let t=e.split("/"),i=t[0]??"",r=t[1],s=d=>{let u=d.split(".");return u.length!==4?!1:u.every(p=>{if(!/^\d{1,3}$/.test(p))return!1;let g=parseInt(p,10);return g>=0&&g<=255})},a=d=>{let u=d.split(":");return u.length!==8?!1:u.every(p=>/^[0-9a-fA-F]{1,4}$/.test(p))},l=s(i),c=a(i);if(!l&&!c)return!1;if(r!==void 0){let d=parseInt(r,10);if(Number.isNaN(d)||l&&(d<0||d>32)||c&&(d<0||d>128))return!1}return!0}isValidASN(e){let t=parseInt(e,10);return!isNaN(t)&&t>=1&&t<=4294967295}isValidPath(e){return e.startsWith("/")&&!e.includes(" ")}};var Jt=require("fs"),Be=require("path"),at=[".doorman.json","vercel-firewall.config.json"],io=at[0],be=class{static async findConfig(e){let{findUp:t}=await import("find-up");for(let i of at){let r=await t(i,{cwd:e||process.cwd()});if(r)return r}}static async findProjectRoot(e){let t=await this.findConfig(e);return t?(0,Be.dirname)(t):null}static configExists(e){return(0,Jt.existsSync)((0,Be.resolve)(e))}static getDefaultConfigPath(){return(0,Be.resolve)(process.cwd(),io)}static getSupportedFileNames(){return at}};async function z(n,e="required"){let t=oo(e),i=n||await be.findConfig();if(!i||!(0,ye.existsSync)(i)){if(t==="optional")return o.debug("No config file found, returning empty config"),{};let s=be.getDefaultConfigPath();throw new Error(`No config file found. Run \`doorman init\` to create one at ${s}, or use --config to specify a custom path.`)}let r;try{let s=(0,ye.readFileSync)(i,"utf8");r=JSON.parse(s)}catch(s){throw s instanceof SyntaxError?new Error(`Invalid JSON in config file (${i}): ${s.message}`):s}if(t!=="raw")try{Ie.getInstance().validateConfig(r)}catch(s){if(t==="lenient")o.warn("Config validation failed:",s);else throw s}return r}async function F(n,e,t={validate:!0,throwOnError:!0}){let i=e||await be.findConfig()||be.getDefaultConfigPath();if(t.validate)try{Ie.getInstance().validateConfig(n)}catch(s){if(o.error("Config validation failed:",s),t.throwOnError)throw s}let r=(0,Xt.dirname)(i);(0,ye.existsSync)(r)||(0,ye.mkdirSync)(r,{recursive:!0}),(0,ye.writeFileSync)(i,JSON.stringify(n,null,2)),o.debug(`Config saved to ${i}`)}function oo(n){if(typeof n=="string")return n;let{validate:e=!0,throwOnError:t=!0}=n;return e?t?"required":"lenient":"raw"}var lt=P(require("chalk")),Qt=require("zod");T();function H(n,e){n instanceof SyntaxError?o.log(he.wrapErrorBlock(["Invalid JSON format in config file:",` ${n.message}`])):n instanceof Qt.ZodError?(o.error(lt.default.red("Schema validation failed:")),n.errors.forEach(t=>{let i=t.path.join(".");o.error(lt.default.red(` - ${i}: ${t.message}`))})):n instanceof Error&&n.name==="ValidationError"?o.error(n):n instanceof Error&&n.message.includes("Forbidden")?o.log(he.wrapErrorBlock([`Error ${e}:`," Access denied. Check that your token has the correct scope."," If using a team, ensure the token is scoped to that team."])):o.error(he.wrapErrorBlock([`Error ${e}:`,` ${n instanceof Error?n.message:String(n)}`])),process.exit(1)}var no="add [type]",ro="Add a new firewall rule to your configuration",so={type:{type:"string",description:'Rule type: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Guided prompts for rule creation",default:!1},name:{alias:"n",type:"string",description:"Rule name"},description:{type:"string",description:"Rule description"},field:{type:"string",description:"Condition field type (path, method, user_agent, etc.)"},op:{type:"string",description:"Operator (eq, pre, suf, sub, inc, re, ex, nex)"},value:{type:"string",description:"Match value (string or comma-separated for arrays)"},key:{type:"string",description:"Header/query/cookie key (required for header, cookie types)"},neg:{type:"boolean",description:"Negate the condition",default:!1},action:{alias:"a",type:"string",description:"Action type (deny, challenge, rate_limit, redirect, log, bypass)",default:"deny"},active:{type:"boolean",description:"Enable rule immediately",default:!0},requests:{type:"number",description:"Rate limit: max requests"},window:{type:"string",description:'Rate limit: time window (e.g., "60s", "5m")'},duration:{type:"string",description:'Action duration (e.g., "1h", "permanent")'},location:{type:"string",description:"Redirect URL"},permanent:{type:"boolean",description:"Redirect: use 301 instead of 302"},ip:{type:"string",description:"IP address or CIDR for IP blocking rules"},hostname:{type:"string",description:"Hostname for IP blocking rules",default:"*"},notes:{type:"string",description:"Notes for IP blocking rules"},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be added without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}},ct=["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"],dt=["eq","pre","suf","inc","sub","re","ex","nex"],ut=["log","deny","challenge","bypass","rate_limit","redirect"];function pt(n){let e=n.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");if(!e)throw new Error("Cannot generate rule ID: name must contain at least one alphanumeric character");return"rule_"+e}function ii(n,e){return e==="inc"?n.split(",").map(t=>t.trim()):n}async function ao(){let n=await R("Rule name:",{type:"text"});if(!n.trim())throw new Error("Rule name cannot be empty");let e=await R("Description (optional):",{type:"text"}),t=[],i=!0;for(;i;){let u=[],p=!0;for(;p;){let g=await R("Condition type:",{type:"select",options:ct,initial:"path"}),C=await R("Operator:",{type:"select",options:dt,initial:"eq"}),m;(g==="header"||g==="cookie"||g==="query")&&(m=await R(`${g} key:`,{type:"text"}));let w;if(C!=="ex"&&C!=="nex"){let b=await R("Value:",{type:"text"});w=ii(b,C)}let _=C!=="ex"&&C!=="nex"?await R("Negate this condition?",{type:"confirm",initial:!1}):!1,N={type:g,op:C,...w!==void 0&&{value:w},...m&&{key:m},..._&&{neg:_}};u.push(N),p=await R("Add another condition to this group? (AND logic)",{type:"confirm",initial:!1})}t.push({conditions:u}),i=await R("Add another condition group? (OR logic)",{type:"confirm",initial:!1})}let r=await R("Action:",{type:"select",options:ut,initial:"deny"}),s;if(r==="rate_limit"){let u=parseInt(await R("Max requests:",{type:"text",initial:"100"}),10),p=await R('Time window (e.g., "60s", "1m", "1h"):',{type:"text",initial:"60s"});s={requests:u,window:p}}let a;if(r==="redirect"){let u=await R("Redirect URL:",{type:"text"}),p=await R("Permanent redirect (301)?",{type:"confirm",initial:!1});a={location:u,...p&&{permanent:p}}}let l;(r==="deny"||r==="challenge")&&await R("Set action duration? (default: permanent)",{type:"confirm",initial:!1})&&(l=await R('Duration (e.g., "1h", "1d", "permanent"):',{type:"text",initial:"permanent"}));let c=await R("Enable rule immediately?",{type:"confirm",initial:!0});return{id:pt(n),name:n.trim(),...e.trim()&&{description:e.trim()},conditionGroup:t,action:{mitigate:{action:r,...s&&{rateLimit:s},...a&&{redirect:a},...l&&{actionDuration:l}}},active:c}}function lo(n){if(!n.name)throw new Error("--name is required in inline mode. Use --interactive for guided prompts.");if(!n.field)throw new Error("--field is required in inline mode (e.g., --field path)");if(!n.op)throw new Error("--op is required in inline mode (e.g., --op eq)");let e=n.op;if(!dt.includes(e))throw new Error(`Invalid operator "${n.op}". Valid operators: ${dt.join(", ")}`);let t=n.field;if(!ct.includes(t))throw new Error(`Invalid field type "${n.field}". Valid types: ${ct.join(", ")}`);let i=n.action||"deny";if(!ut.includes(i))throw new Error(`Invalid action "${n.action}". Valid actions: ${ut.join(", ")}`);if(e!=="ex"&&e!=="nex"&&!n.value)throw new Error("--value is required for the given operator");if((t==="header"||t==="cookie"||t==="query")&&!n.key)throw new Error(`--key is required for "${t}" condition type`);if(i==="rate_limit"&&(!n.requests||!n.window))throw new Error("--requests and --window are required when action is rate_limit");if(i==="redirect"&&!n.location)throw new Error("--location is required when action is redirect");let r=n.value?ii(n.value,e):void 0,s={type:t,op:e,...r!==void 0&&{value:r},...n.key&&{key:n.key},...n.neg&&{neg:!0}};return{id:pt(n.name),name:n.name.trim(),...n.description&&{description:n.description},conditionGroup:[{conditions:[s]}],action:{mitigate:{action:i,...i==="rate_limit"&&n.requests&&n.window&&{rateLimit:{requests:n.requests,window:n.window}},...i==="redirect"&&n.location&&{redirect:{location:n.location,...n.permanent&&{permanent:!0}}},...n.duration&&{actionDuration:n.duration}}},active:n.active!==!1}}async function co(){let n=await R("IP address or CIDR (e.g., 192.168.1.100/32):",{type:"text"});if(!n.trim())throw new Error("IP address cannot be empty");let e=await R("Hostname (use * for all):",{type:"text",initial:"*"}),t=await R("Notes (optional):",{type:"text"});return{ip:n.trim(),hostname:e.trim()||"*",...t.trim()&&{notes:t.trim()},action:"deny"}}function uo(n){if(!n.ip)throw new Error("--ip is required for IP blocking rules");return{ip:n.ip.trim(),hostname:n.hostname||"*",...n.notes&&{notes:n.notes},action:"deny"}}function po(n,e){if(n.rules.map(r=>r.name.toLowerCase()).includes(e.name.toLowerCase()))return`A rule named "${e.name}" already exists`;let i=n.rules.map(r=>r.id).filter(Boolean);return e.id&&i.includes(e.id)?`A rule with ID "${e.id}" already exists`:null}function Yt(n){o.log(""),o.log(D.default.bold(` Rule: ${n.name}`)),n.description&&o.log(D.default.dim(` Description: ${n.description}`)),o.log(D.default.dim(` ID: ${n.id}`));let e=n.conditionGroup.map(r=>r.conditions.map(s=>{let a=s.neg?"NOT ":"",l=s.key?`[${s.key}] `:"",c=Array.isArray(s.value)?s.value.join(", "):s.value||"";return`${a}${s.type} ${l}${s.op} "${c}"`}).join(" AND "));o.log(D.default.dim(` Conditions: ${e.join(" OR ")}`));let t=n.action.mitigate.action,i=[];n.action.mitigate.rateLimit&&i.push(`${n.action.mitigate.rateLimit.requests} req/${n.action.mitigate.rateLimit.window}`),n.action.mitigate.redirect&&i.push(`\u2192 ${n.action.mitigate.redirect.location}`),n.action.mitigate.actionDuration&&i.push(`duration: ${n.action.mitigate.actionDuration}`),o.log(D.default.dim(` Action: ${t}${i.length?` (${i.join(", ")})`:""}`)),o.log(D.default.dim(` Active: ${n.active?"\u2705":"\u274C"}`))}function ei(n){o.log(""),o.log(D.default.bold(` IP Rule: ${n.ip}`)),o.log(D.default.dim(` Hostname: ${n.hostname}`)),n.notes&&o.log(D.default.dim(` Notes: ${n.notes}`)),o.log(D.default.dim(` Action: ${n.action}`))}var go=async n=>{try{if(n.debug&&(o.level=ti.LogLevels.debug),o.debug("Add command arguments:",n),(n.type||"rule")==="ip"){let t=n.interactive?await co():uo(n),i=st.safeParse(t);if(i.success||(o.error(D.default.red("Rule validation failed:")),i.error.errors.forEach(a=>{let l=a.path.join(".");o.error(D.default.red(` - ${l||"ip"}: ${a.message}`))}),process.exit(1)),n.dryRun){o.info(D.default.cyan(`
|
|
18
18
|
Dry run - The following IP rule would be added:`)),ei(t),o.log(""),o.log(D.default.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await z(n.config,"raw"),s={...r,ips:[...r.ips||[],t]};await F(s,n.config),o.success(D.default.green(`\u2714 IP rule "${t.ip}" added to configuration`)),ei(t)}else{let t=n.interactive?await ao():lo(n),i=rt.safeParse(t);if(i.success||(o.error(D.default.red("Rule validation failed:")),i.error.errors.forEach(c=>{let d=c.path.join(".")||"rule";o.error(D.default.red(` - ${d}: ${c.message}`))}),process.exit(1)),n.dryRun){o.info(D.default.cyan(`
|
|
19
|
-
Dry run - The following rule would be added:`)),Yt(t),o.log(""),o.log(D.default.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await z(n.config,"raw"),s=r.rules||[],a=po({...r,rules:s},t);if(a&&(o.warn(D.default.yellow(`\u26A0\uFE0F ${a}`)),!await R("Proceed anyway?",{type:"confirm",initial:!1}))){o.info("Cancelled.");return}let l={...r,rules:[...s,t]};await F(l,n.config),o.success(D.default.green(`\u2714 Rule "${t.name}" added to configuration`)),Yt(t)}o.log(""),o.log(D.default.dim(`Run ${D.default.cyan("
|
|
19
|
+
Dry run - The following rule would be added:`)),Yt(t),o.log(""),o.log(D.default.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await z(n.config,"raw"),s=r.rules||[],a=po({...r,rules:s},t);if(a&&(o.warn(D.default.yellow(`\u26A0\uFE0F ${a}`)),!await R("Proceed anyway?",{type:"confirm",initial:!1}))){o.info("Cancelled.");return}let l={...r,rules:[...s,t]};await F(l,n.config),o.success(D.default.green(`\u2714 Rule "${t.name}" added to configuration`)),Yt(t)}o.log(""),o.log(D.default.dim(`Run ${D.default.cyan("doorman sync")} to deploy this rule.`))}catch(e){H(e,"adding rule")}};var It={};q(It,{builder:()=>$o,command:()=>Eo,desc:()=>Ao,handler:()=>To});var J=P(require("chalk")),ue=require("fs"),et=require("path"),mi=require("consola");T();ne();var fi=require("consola");T();var Z=P(require("chalk")),ni=require("consola");T();function ie(n,e){if(n===e)return!0;if(typeof n!="object"||n===null||typeof e!="object"||e===null||Array.isArray(n)!==Array.isArray(e))return!1;let t=Object.keys(n),i=Object.keys(e);if(t.length!==i.length)return!1;for(let r of t)if(!i.includes(r)||!ie(n[r],e[r]))return!1;return!0}function U(n){let{id:e,...t}=n;return t}T();async function Q(n,e={}){let{maxAttempts:t=3,delayMs:i=1e3,backoff:r=!0}=e,s;for(let a=1;a<=t;a++)try{return await n()}catch(l){if(s=l instanceof Error?l:new Error(String(l)),a===t)break;let c=r?i*a:i;o.debug(`Retry attempt ${a}/${t} failed. Retrying in ${c}ms...`),await new Promise(d=>setTimeout(d,c))}throw new Error(`Operation failed after ${t} attempts. Last error: ${s?.message}`)}function oi(n){if(!n||n.trim().length===0)return"";let e=n.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2"),t=["IP","HTTP","XML","API"];return t.forEach(i=>{let r=new RegExp(`\\b${i}\\b`,"g");e=e.replace(r,i.toLowerCase())}),e=e.replace(/[A-Z]{2,}/g,i=>t.includes(i)?i.toLowerCase():i.split("").join("_")).toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"").replace(/_+/g,"_"),t.forEach(i=>{let r=i.toLowerCase();e=e.replace(new RegExp(`([a-z])${r}`,"g"),`$1_${r}`)}),e}var Ke=class{constructor(e){this.client=e}client;async getChanges(e){try{let t=fe.safeParse(e);if(!t.success)throw new Error(`Invalid firewall configuration: ${t.error.message}`);o.debug("Fetching existing firewall configuration");let i=await this.client.fetchFirewallConfig();o.debug(`Fetched ${i.rules.length} custom rules and ${i.ips.length} IP blocking rules`);let r=e.rules,{toAdd:s,toUpdate:a,toDelete:l}=this.diffRules(r,i.rules);o.debug("Pre Filter IP Rules:",{ips:e.ips,activeConfig:i.ips});let{ipsToAdd:c,ipsToUpdate:d,ipsToDelete:u}=this.diffIPRules(e.ips||[],i.ips);return{version:i.version,toAdd:s,toUpdate:a,toDelete:l,ipsToAdd:c,ipsToUpdate:d,ipsToDelete:u}}catch(t){throw o.error("Error fetching existing firewall configuration:",t),t instanceof Error&&t.message.includes("Invalid firewall configuration")?t:new Error("Failed to fetch existing firewall configuration. Please check your network connection and try again.")}}async syncRules(e,t={}){let{dryRun:i=!1,retryAttempts:r=3,debug:s=!1}=t;s&&(o.level=ni.LogLevels.debug);try{let{toAdd:a,toUpdate:l,toDelete:c,ipsToAdd:d,ipsToUpdate:u,ipsToDelete:p}=await this.getChanges(e);if(i)return o.info("Dry run mode. The following changes would be made:"),o.info(`Custom Rules - Add: ${a.length}, Update: ${l.length}, Delete: ${c.length}`),o.info(`IP Rules - Add: ${d.length}, Update: ${u.length}, Delete: ${p.length}`),{addedRules:[],updatedRules:[],deletedRules:[],addedIPRules:[],updatedIPRules:[],deletedIPRules:[],rulesToUpdateLocally:[]};let g=[],C=[],m=[],w=[],_=[],N=[],b=[];for(let f of c)o.debug(`Deleting custom rule: ${f.id}`),await Q(()=>this.client.deleteFirewallRule(f),{maxAttempts:r}),m.push(f),o.debug(`Custom rule deleted: ${f.id}`);for(let f of p)o.debug(`Deleting IP blocking rule: ${f.id}`),await Q(()=>this.client.deleteIPBlockingRule(f),{maxAttempts:r}),N.push(f),o.debug(`IP blocking rule deleted: ${f.id}`);for(let f of a){let $=`rule_${oi(f.name)}`;o.debug(`Adding new custom rule: ${f.name}`);let h=await Q(()=>this.client.createFirewallRule(f),{maxAttempts:r});g.push(h),o.debug(`New custom rule added: ${h.id}`),f.id!==$&&(b.push({oldId:f.id??"",newId:$,name:f.name}),o.debug(`Rule ID needs update locally: ${f.id} -> ${$}`))}for(let f of d){o.debug(`Adding new IP blocking rule: ${f.ip}`);let $=await Q(()=>this.client.createIPBlockingRule(f),{maxAttempts:r});w.push($),o.debug(`New IP blocking rule added: (hostname): ${$.hostname} (ip): ${$.ip}`)}for(let f of l){o.debug(`Updating custom rule: ${f.id}`);let $=await Q(()=>this.client.updateFirewallRule(f),{maxAttempts:r});C.push($),o.debug(`Custom rule updated: ${$.id}`)}for(let f of u){o.debug(`Updating IP blocking rule: ${f.id}`);let $=await Q(()=>this.client.updateIPBlockingRule(f),{maxAttempts:r});_.push($),o.debug(`IP blocking rule updated: ${$.id}`)}return o.debug(`${Z.default.underline("Custom Rules:")} ${Z.default.green("Added:")} ${Z.default.green(g.length)}, ${Z.default.cyan("Updated:")} ${Z.default.cyan(C.length)}, ${Z.default.red("Deleted:")} ${Z.default.red(m.length)}`),o.debug(`${Z.default.underline("IP Rules:")} ${Z.default.green("Added:")} ${Z.default.green(w.length)}, ${Z.default.cyan("Updated:")} ${Z.default.cyan(_.length)}, ${Z.default.red("Deleted:")} ${Z.default.red(N.length)}`),{addedRules:g,updatedRules:C,deletedRules:m,addedIPRules:w,updatedIPRules:_,deletedIPRules:N,rulesToUpdateLocally:b}}catch(a){throw o.error("Error during sync:",a),new Error("Failed to synchronize firewall rules. Please check the error logs and try again.")}}diffRules(e,t){let i=[],r=[],s=[...t];for(let a of e){let l=t.find(c=>c.id===a.id);if(!l)i.push(a);else{ie(U(a),U(l))||r.push({...a,id:l.id});let c=s.findIndex(d=>d.id===l.id);c!==-1&&s.splice(c,1)}}return{toAdd:i,toUpdate:r,toDelete:s}}diffIPRules(e,t){let i=[],r=[],s=[...t];for(let a of e){let l=t.find(c=>c.id===a.id);if(!l&&!a.id&&(l=t.find(c=>c.ip===a.ip&&c.hostname===a.hostname&&c.action===a.action)),!l)o.debug("Rule not found in existing rules:",{configRule:a}),i.push(a);else{ie(U(a),U(l))||r.push({...l,...a});let c=s.findIndex(d=>d.id===l.id);c!==-1&&s.splice(c,1)}}return{ipsToAdd:i,ipsToUpdate:r,ipsToDelete:s}}async validateAndUpdateConfig(e,t,i={}){if(i.dryRun)return o.info(Z.default.cyan("Dry run: Config metadata would be updated after successful sync")),e;await new Promise(h=>setTimeout(h,2e3));let r=await Q(()=>this.client.fetchFirewallConfig(),{maxAttempts:5,delayMs:1500,backoff:!0}),s=r.rules,a=r.ips,l=new Set(t.addedRules.map(h=>h.id)),c=new Set(t.updatedRules.map(h=>h.id)),d=new Set(t.deletedRules.map(h=>h.id)),u=new Set(t.addedIPRules.map(h=>h.id).filter(Boolean)),p=new Set(t.updatedIPRules.map(h=>h.id).filter(Boolean)),g=new Set(t.deletedIPRules.map(h=>h.id).filter(Boolean)),C=new Map(t.addedRules.map(h=>[h.name,h])),m=t.addedIPRules.map(h=>U(h)),w=s.filter(h=>{if(l.has(h.id)||c.has(h.id))return!1;let v=C.get(h.name);if(v&&ie(U(h),U(v)))return!1;if(d.has(h.id))return!0;let ee=e.rules.find(ge=>ge.id===h.id);return ee?!ie(U(h),U(ee)):!e.rules.find(it=>ie(U(h),U(it)))}),_=a.filter(h=>u.has(h.id)||p.has(h.id)||m.some(ge=>ie(U(h),ge))?!1:!!(g.has(h.id)||!e.ips?.find(ge=>ie(U(h),U(ge))))),N=[];if(w.length>0){let h=w.map(v=>` - ${v.name} (${v.id})`).join(`
|
|
20
20
|
`);N.push(`The following custom rules have unexpected state:
|
|
21
21
|
`+h)}if(_.length>0){let h=_.map(v=>` - ${v.ip} (${v.id})`).join(`
|
|
22
22
|
`);N.push(`The following IP blocking rules have unexpected state:
|
|
@@ -47,7 +47,7 @@ ${v}`)}),f.result}),{toAdd:s,toUpdate:a,toDelete:l}=this.diffRules(r,i.rules),c=
|
|
|
47
47
|
`),o.info(" 1. Vercel Firewall"),o.info(` 2. Cloudflare WAF
|
|
48
48
|
`);let n=await R("Select provider (1 or 2):",{type:"text"}),e=String(n).toLowerCase();return e==="1"||e==="vercel"?"vercel":e==="2"||e==="cloudflare"?"cloudflare":(o.warn(`Invalid choice: ${n}, defaulting to Vercel`),"vercel")}async function K(n,e){try{n.debug&&(o.level=fi.LogLevels.debug);let t;if(n.optionalConfig)try{t=await z(n.config,"optional")}catch{t={}}else n.skipValidation?t=await z(n.config,"raw"):t=await z(n.config,"required");let i=await gi({provider:n.provider,config:t,interactive:!n.ci,token:n.token,projectId:n.projectId,teamId:n.teamId,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId}),r,s,a="",l="",c="";if(i.name==="vercel"){let d=await mt({token:n.token,projectId:n.projectId||t.projectId,teamId:n.teamId||t.teamId});a=d.token,l=d.projectId,c=d.teamId,r=new Je(l,c,a),s=new Ke(r)}else r={},s={};await e({config:t,provider:i,client:r,service:s,token:a,projectId:l,teamId:c})}catch(t){H(t,n.errorContext)}}var Eo="backup",Ao="Backup or restore firewall configurations",$o={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},To=async n=>{try{n.debug&&(o.level=mi.LogLevels.debug);let e=n.output||"./backups";if(n.list){if(!(0,ue.existsSync)(e)){o.info(J.default.yellow("No backup directory found."));return}let t=(0,ue.readdirSync)(e).filter(i=>i.endsWith(".json")).map(i=>{let r=(0,et.join)(e,i),s=(0,ue.statSync)(r);return{name:i,size:s.size,created:s.mtime}}).sort((i,r)=>r.created.getTime()-i.created.getTime());if(t.length===0){o.info(J.default.yellow("No backups found."));return}o.log(J.default.bold(`
|
|
49
49
|
\u{1F4E6} Available Backups:
|
|
50
|
-
`)),t.forEach(i=>{o.log(`${J.default.cyan(i.name)}`),o.log(` ${J.default.dim("Created:")} ${i.created.toLocaleString()}`),o.log(` ${J.default.dim("Size:")} ${(i.size/1024).toFixed(1)} KB`),o.log("")});return}if(n.restore){let t=n.restore.startsWith("/")?n.restore:(0,et.join)(e,n.restore);(0,ue.existsSync)(t)||(o.error(`Backup file not found: ${t}`),process.exit(1));let i=await z(t,"raw"),r=n.config||".doorman.json";if((0,ue.existsSync)(r)&&!await R(`Config file ${r} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(J.default.yellow("Restore cancelled."));return}await F(i,r),o.success(J.default.green(`\u2705 Restored configuration from ${t} to ${r}`));return}await K({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"creating backup"},async({client:t,projectId:i,teamId:r})=>{o.start("Fetching current remote configuration...");let s=await t.fetchFirewallConfig();(0,ue.existsSync)(e)||(0,ue.mkdirSync)(e,{recursive:!0});let a=new Date,l=a.toISOString().split("T")[0]??"unknown-date",c=(a.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",u=`firewall-backup-${`${l}_${c}`}.json`,p=(0,et.join)(e,u),g={...s,backup:{createdAt:new Date().toISOString(),source:"remote",projectId:i,teamId:r,originalVersion:s.version}};await F(g,p),o.success(J.default.green(`\u2705 Backup created: ${p}`)),o.log(""),o.log(J.default.bold("Backup Details:")),o.log(`${J.default.dim("Version:")} ${s.version}`),o.log(`${J.default.dim("Rules:")} ${s.rules.length} custom, ${s.ips.length} IP blocking`),o.log(`${J.default.dim("Created:")} ${new Date().toLocaleString()}`),o.log(""),o.log(J.default.dim("To restore this backup later, run:")),o.log(J.default.cyan(`
|
|
50
|
+
`)),t.forEach(i=>{o.log(`${J.default.cyan(i.name)}`),o.log(` ${J.default.dim("Created:")} ${i.created.toLocaleString()}`),o.log(` ${J.default.dim("Size:")} ${(i.size/1024).toFixed(1)} KB`),o.log("")});return}if(n.restore){let t=n.restore.startsWith("/")?n.restore:(0,et.join)(e,n.restore);(0,ue.existsSync)(t)||(o.error(`Backup file not found: ${t}`),process.exit(1));let i=await z(t,"raw"),r=n.config||".doorman.json";if((0,ue.existsSync)(r)&&!await R(`Config file ${r} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(J.default.yellow("Restore cancelled."));return}await F(i,r),o.success(J.default.green(`\u2705 Restored configuration from ${t} to ${r}`));return}await K({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"creating backup"},async({client:t,projectId:i,teamId:r})=>{o.start("Fetching current remote configuration...");let s=await t.fetchFirewallConfig();(0,ue.existsSync)(e)||(0,ue.mkdirSync)(e,{recursive:!0});let a=new Date,l=a.toISOString().split("T")[0]??"unknown-date",c=(a.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",u=`firewall-backup-${`${l}_${c}`}.json`,p=(0,et.join)(e,u),g={...s,backup:{createdAt:new Date().toISOString(),source:"remote",projectId:i,teamId:r,originalVersion:s.version}};await F(g,p),o.success(J.default.green(`\u2705 Backup created: ${p}`)),o.log(""),o.log(J.default.bold("Backup Details:")),o.log(`${J.default.dim("Version:")} ${s.version}`),o.log(`${J.default.dim("Rules:")} ${s.rules.length} custom, ${s.ips.length} IP blocking`),o.log(`${J.default.dim("Created:")} ${new Date().toLocaleString()}`),o.log(""),o.log(J.default.dim("To restore this backup later, run:")),o.log(J.default.cyan(`doorman backup --restore ${u}`))})}catch(e){H(e,"managing backup")}};var kt={};q(kt,{builder:()=>_o,command:()=>ko,desc:()=>So,handler:()=>Lo});var me=P(require("chalk"));T();var W=P(require("chalk")),Pt=P(require("cli-table3"));T();var bt=(n,e,t,i)=>{if(!e.length)throw new Error("minWidths array cannot be empty");if(i.some(c=>c>=e.length))throw new Error("flexColumns contains invalid column index");if(e.length!==t.length)throw new Error("minWidths and maxWidths arrays must have the same length");e.forEach((c,d)=>{let u=t[d];if(u!=null&&c>u)throw new Error(`Column ${d} has minimum width (${c}) greater than maximum width (${u})`)});let r=n||400,s=[...e],a=e.reduce((c,d)=>c+d,0),l=r-a-28;if(l>0&&i.length>0){let c=i.filter(d=>{let u=t[d],p=s[d];return u!==void 0&&p!==void 0&&(u===null||p<u)});for(;l>0&&c.length>0;){let d=Math.floor(l/c.length);if(d===0)break;let u=0;c=c.filter(p=>{let g=t[p],C=s[p];if(g===null)return s[p]!==void 0&&(s[p]+=d),u+=d,!0;{let m=(g??0)-(C??0);if(m<=0)return!1;let w=Math.min(d,m);return s[p]!==void 0&&(s[p]+=w),u+=w,w===d}}),l-=u}if(l>0){let d=i.findLast(u=>t[u]===null);d!==void 0&&d!==void 0&&s[d]!==void 0&&(s[d]+=l)}}return s};var re=P(require("chalk")),hi=n=>{if(n==="permanent")return"permanent";let e=n.slice(0,-1),t=n.slice(-1);return`${e}${{s:"sec",m:"min",h:"hr",d:"day"}[t]||t}`},Po=(n,e)=>e==="60s"?`${n}/min`:e==="3600s"?`${n}/hr`:e==="86400s"?`${n}/day`:`${n}/${hi(e)}`;function yi(n){if(!n.mitigate)return"unknown";let{mitigate:e}=n,t=[];switch(e.action){case"log":t.push(re.default.blue("log"));break;case"deny":t.push(re.default.red("deny"));break;case"challenge":t.push(re.default.yellow("challenge"));break;case"bypass":t.push(re.default.green("bypass"));break;case"rate_limit":t.push(re.default.yellow("rate-limit"));break;case"redirect":t.push(re.default.magenta("redirect"));break;default:t.push(e.action)}return e.rateLimit&&(t[0]=re.default.yellow("rate-limit"),t.push(re.default.yellow(Po(e.rateLimit.requests,e.rateLimit.window)))),e.redirect&&(t[0]=re.default.magenta("redirect"),t.push(re.default.magenta(`\u2192 ${e.redirect.location}`)),e.redirect.permanent!==void 0&&t.push(re.default.gray(`(${e.redirect.permanent?"301":"302"})`))),e.actionDuration&&t.push(re.default.gray(`for ${hi(e.actionDuration)}`)),t.join(" ")}var We=P(require("chalk"));function vt(n){switch(n){case"unchanged":return We.default.bold.gray(" ");case"modified":return We.default.bold.yellow("\u25B4");case"new":return We.default.bold.green("+");case"deleted":return We.default.bold.red("-")}}var qe=P(require("chalk"));function wi(n=[]){return n.map((e,t)=>{let i=e.conditions.map(r=>qe.default.cyan(`${r.type}:${r.neg?qe.default.red(`!${r.op}:`):`${r.op}:`}${qe.default.white(r.value)}`)).join(`
|
|
51
51
|
`);return t>0?`${qe.default.yellow("OR")}
|
|
52
52
|
${i}`:i}).join(`
|
|
53
53
|
`)}var Et=P(require("chalk"));function Ri(n){return{id:n.id||"-",ip:n.ip,hostname:n.hostname,notes:n.notes||"-",status:n.changeStatus?Et.default.yellow(n.changeStatus):Et.default.red("deny")}}var Me=P(require("chalk"));function At(n){switch(n){default:case"unchanged":return Me.default.white;case"modified":return Me.default.yellow;case"new":return Me.default.green;case"deleted":return Me.default.red}}var oe={unchanged:"unchanged",modified:"modified",new:"new",deleted:"deleted"},$t={minWidths:[10,28,20,5,12,35],maxWidths:[15,32,36,8,24,null],flexColumns:[0,1,2,4,5]},Tt={minWidths:[10,28,24,5,12],maxWidths:[20,32,26,null,16],flexColumns:[0,1,3]},Ci=(n,e)=>n==="ip"?bt(e,Tt.minWidths,Tt.maxWidths,Tt.flexColumns):bt(e,$t.minWidths,$t.maxWidths,$t.flexColumns);function Re(n,{showStatus:e}){let t=[W.default.bold.gray("ID"),W.default.bold.gray("Name"),W.default.bold.gray("Conditions"),W.default.bold.gray(" "),W.default.bold.gray("Action"),W.default.bold.gray("Description")],i=["left","left","left","center","left","left"],r=process.stdout.columns||400,s=Ci("rules",r);e&&(t.unshift(W.default.bold.gray("Status")),i.unshift("center"),s.unshift(10));let a=new Pt.default({head:t,colAligns:i,wrapOnWordBoundary:!0,wordWrap:!0,colWidths:s});n.forEach(l=>{let c="changeStatus"in l&&e,d=c?At(l.changeStatus):W.default.white,u=[d(l.id?l.id:"-"),d(l.name),d(wi(l.conditionGroup)),l.active?W.default.bold.green("\u2713"):W.default.bold.red("\u2717"),d(yi(l.action)),d(l.description||"")];c&&u.unshift(d(vt(l.changeStatus))),a.push(u)}),o.log(a.toString())}function Ce(n,{showStatus:e}){let t=[W.default.bold.gray("ID"),W.default.bold.gray("IP"),W.default.bold.gray("Hostname"),W.default.bold.gray("Notes"),W.default.bold.gray("Action")],i=["left","left","left","left","center"],r=process.stdout.columns||400,s=Ci("ip",r);e&&(t.unshift(W.default.bold.gray("Status")),i.unshift("center"),s.unshift(10));let a=new Pt.default({head:t,colAligns:i,wrapOnWordBoundary:!0,wordWrap:!0,colWidths:s});n.forEach(l=>{let c=l.changeStatus!==void 0&&e,d=c?At(l.changeStatus):W.default.white,u=Ri(l),p=[d(u.id),d(u.ip),d(u.hostname),d(u.notes),u.status];c&&p.unshift(d(vt(l.changeStatus))),a.push(p)}),o.log(a.toString())}var ko="diff",So="Show detailed differences between local and remote firewall configuration",_o={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},Lo=async n=>{await K({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"calculating diff"},async({config:e,service:t})=>{o.start("Calculating differences...");let{toAdd:i,toUpdate:r,toDelete:s,ipsToAdd:a,ipsToUpdate:l,ipsToDelete:c,version:d}=await t.getChanges(e),u=i.length>0||r.length>0||s.length>0,p=a.length>0||l.length>0||c.length>0,g=e.version!==d;if(!u&&!p&&!g){o.success(me.default.green("No differences found. Local and remote configurations are in sync."));return}if(n.format==="json"){let m={version:{local:e.version,remote:d,changed:g},customRules:{toAdd:i.map(w=>({...w,status:"add"})),toUpdate:r.map(w=>({...w,status:"update"})),toDelete:s.map(w=>({...w,status:"delete"}))},ipRules:{toAdd:a.map(w=>({...w,status:"add"})),toUpdate:l.map(w=>({...w,status:"update"})),toDelete:c.map(w=>({...w,status:"delete"}))},summary:{hasChanges:u||p||g,customRuleChanges:i.length+r.length+s.length,ipRuleChanges:a.length+l.length+c.length}};o.log(JSON.stringify(m,null,2));return}o.log(me.default.bold(`
|
|
@@ -120,7 +120,7 @@ Remote IP Blocking Rules to Download:
|
|
|
120
120
|
`))},Ho=()=>{o.log(I.default.bold(`
|
|
121
121
|
\u{1F4DA} Helpful Resources:`)),o.log(""),o.log(I.default.cyan("\u{1F517} Find your Project ID:")),o.log(I.default.dim(" https://vercel.com/dashboard \u2192 Select your project \u2192 Settings \u2192 General")),o.log(""),o.log(I.default.cyan("\u{1F517} Find your Team ID:")),o.log(I.default.dim(" https://vercel.com/dashboard \u2192 Team Settings \u2192 General")),o.log(I.default.dim(" (Leave empty if using personal account)")),o.log(""),o.log(I.default.cyan("\u{1F517} Create API Token:")),o.log(I.default.dim(" https://vercel.com/account/tokens \u2192 Create Token")),o.log(I.default.dim(" Scopes needed: Read & Write for your project")),o.log(""),o.log(I.default.cyan("\u{1F517} Documentation:")),o.log(I.default.dim(" https://doorman.griffen.codes/getting-started")),o.log("")},Zo=async n=>{let e=n.projectId,t=n.teamId;return n.interactive&&(o.log(I.default.bold("\u{1F4CB} Project Configuration")),o.log(I.default.dim(`We need your Vercel project details to configure the firewall.
|
|
122
122
|
`)),e||(e=await R("Enter your Vercel Project ID:",{type:"text"})),t||await R("Are you using a Vercel team account?",{type:"confirm"})&&(t=await R("Enter your Vercel Team ID:",{type:"text"})),process.env.VERCEL_TOKEN?o.log(I.default.green("\u2705 VERCEL_TOKEN environment variable found")):(o.log(""),o.log(I.default.yellow("\u26A0\uFE0F VERCEL_TOKEN environment variable not found")),o.log(I.default.dim("You'll need to set this before running sync commands.")),await R("Would you like to see how to create an API token?",{type:"confirm"})&&(o.log(""),o.log(I.default.bold("\u{1F511} Creating a Vercel API Token:")),o.log("1. Go to https://vercel.com/account/tokens"),o.log('2. Click "Create Token"'),o.log('3. Give it a descriptive name (e.g., "Doorman Firewall")'),o.log("4. Set expiration as needed"),o.log("5. Copy the token and set it as an environment variable:"),o.log(""),o.log(I.default.cyan(' export VERCEL_TOKEN="your-token-here"')),o.log(I.default.dim(" # Add this to your ~/.bashrc, ~/.zshrc, or .env file")),o.log("")))),{projectId:e,teamId:t}},Ko=async n=>{try{let e=n.config||".doorman.json";if(n.interactive&&Go(),(0,vi.existsSync)(e)&&!n.force&&!await R(`Config file ${e} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(I.default.yellow("Initialization cancelled."));return}let{projectId:t,teamId:i}=await Zo(n);n.interactive&&(o.log(""),o.log(I.default.bold("\u{1F3A8} Template Selection")),o.log(I.default.dim(`Choose a starting template for your firewall configuration:
|
|
123
|
-
`)),o.log(I.default.cyan("\u{1F4DD} empty")+I.default.dim(" - Minimal config, add your own rules")),o.log(I.default.cyan("\u{1F6E1}\uFE0F basic")+I.default.dim(" - Includes basic bot protection")),o.log(I.default.cyan("\u{1F512} security-focused")+I.default.dim(" - Comprehensive security rules")),o.log(""));let r=n.template;n.interactive&&!n.template&&(r=await R("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),o.start(`Creating ${r} configuration...`);let s=Pe();switch(t&&(s.projectId=t),i&&(s.teamId=i),r){case"basic":s={...s,rules:Ei};break;case"security-focused":s={...s,rules:Ai};break;default:break}await F(s,e),o.success(I.default.green(`\u2705 Created ${e}`)),o.log(""),o.log(I.default.bold("\u{1F4CB} Configuration Summary:")),o.log(`${I.default.dim("File:")} ${e}`),o.log(`${I.default.dim("Template:")} ${r}`),o.log(`${I.default.dim("Project ID:")} ${t||I.default.yellow("Not set - add manually")}`),o.log(`${I.default.dim("Team ID:")} ${i||I.default.dim("None (personal account)")}`),o.log(`${I.default.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),o.log(""),o.log(I.default.bold("\u{1F680} Next Steps:")),t||o.log(`1. ${I.default.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||o.log(`${t?"1":"2"}. ${I.default.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;o.log(`${a}. ${I.default.dim("Review and enable rules in")} ${e}`),o.log(`${a+1}. ${I.default.dim("Run")} ${I.default.cyan("
|
|
123
|
+
`)),o.log(I.default.cyan("\u{1F4DD} empty")+I.default.dim(" - Minimal config, add your own rules")),o.log(I.default.cyan("\u{1F6E1}\uFE0F basic")+I.default.dim(" - Includes basic bot protection")),o.log(I.default.cyan("\u{1F512} security-focused")+I.default.dim(" - Comprehensive security rules")),o.log(""));let r=n.template;n.interactive&&!n.template&&(r=await R("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),o.start(`Creating ${r} configuration...`);let s=Pe();switch(t&&(s.projectId=t),i&&(s.teamId=i),r){case"basic":s={...s,rules:Ei};break;case"security-focused":s={...s,rules:Ai};break;default:break}await F(s,e),o.success(I.default.green(`\u2705 Created ${e}`)),o.log(""),o.log(I.default.bold("\u{1F4CB} Configuration Summary:")),o.log(`${I.default.dim("File:")} ${e}`),o.log(`${I.default.dim("Template:")} ${r}`),o.log(`${I.default.dim("Project ID:")} ${t||I.default.yellow("Not set - add manually")}`),o.log(`${I.default.dim("Team ID:")} ${i||I.default.dim("None (personal account)")}`),o.log(`${I.default.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),o.log(""),o.log(I.default.bold("\u{1F680} Next Steps:")),t||o.log(`1. ${I.default.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||o.log(`${t?"1":"2"}. ${I.default.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;o.log(`${a}. ${I.default.dim("Review and enable rules in")} ${e}`),o.log(`${a+1}. ${I.default.dim("Run")} ${I.default.cyan("doorman validate")} ${I.default.dim("to check your config")}`),o.log(`${a+2}. ${I.default.dim("Run")} ${I.default.cyan("doorman status")} ${I.default.dim("to see sync status")}`),o.log(`${a+3}. ${I.default.dim("Run")} ${I.default.cyan("doorman sync")} ${I.default.dim("to deploy your rules")}`),r!=="empty"&&(o.log(""),o.log(I.default.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),o.log(I.default.dim(" Review each rule and set active: true when ready."))),n.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Ho()}catch(e){H(e,"initializing configuration")}};var Dt={};q(Dt,{builder:()=>Qo,command:()=>Jo,desc:()=>Xo,handler:()=>Yo});var pe=P(require("chalk")),$i=require("zod");T();var Jo="list [configVersion]",Xo="List Vercel Firewall rules, optionally for a specific configuration version",Qo={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},Yo=async n=>{await K({provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({client:e,token:t,projectId:i,teamId:r})=>{if(n.configVersion!==void 0)try{Ze.parse(n.configVersion)}catch(u){throw u instanceof $i.z.ZodError&&(o.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),u}o.start(`Fetching firewall configuration${n.configVersion?` version ${n.configVersion}`:""} ...`),o.verbose(`Token: ${t} projectId: ${i} teamId: ${r}`);let s=await e.fetchFirewallConfig(n.configVersion),a=s.rules,l=s.ips,c=new Date(s.updatedAt),d=new Intl.DateTimeFormat("en-US",{dateStyle:"medium",timeStyle:"medium"}).format(c);if(o.info(`Found ${pe.default.cyan(a.length)} custom rules and ${pe.default.cyan(l.length)} IP blocking rules
|
|
124
124
|
`+pe.default.dim(`Version: ${pe.default.yellow(s.version)} \u2022 Last Updated: ${pe.default.yellow(d)}`)),n.format==="json")o.info(JSON.stringify({version:s.version,updatedAt:s.updatedAt,lastUpdated:d,rules:a,ips:l},null,2));else{if(a.length===0&&l.length===0){o.info(pe.default.yellow("No rules found"));return}a.length>0?(o.log(pe.default.bold.underline(`
|
|
125
125
|
Custom Rules:`),`
|
|
126
126
|
`),Re(a,{showStatus:!1})):o.info(pe.default.cyan("No custom rules found")),l.length>0?(o.log(pe.default.bold.underline(`
|
|
@@ -132,11 +132,11 @@ Dry run \u2014 no changes made.`));return}if(!await Pi(r.length,n.force||!1,n.al
|
|
|
132
132
|
\u26A0\uFE0F Found ${r.length} rules named "${n.name}". All will be removed.`)),Ut(r)),r.length===0){o.error(L.default.red(`No rule found with exact name "${n.name}"`));let l=_i(n.name,i);l.length>0&&(o.log(L.default.yellow(`
|
|
133
133
|
Did you mean one of these?`)),l.forEach((c,d)=>{let u=c.id?` (${c.id})`:"";o.log(L.default.dim(` ${d+1}. ${c.name}${u}`))}),o.log(L.default.dim(`
|
|
134
134
|
Use --id for exact matching, or --interactive to select from a list.`))),process.exit(1)}}else if(n.id&&n.id.length>0){let l=new Set(n.id);r=i.filter(u=>u.id&&l.has(u.id));let c=new Set(r.map(u=>u.id)),d=n.id.filter(u=>!c.has(u));d.length>0&&o.warn(L.default.yellow(`No rules found for IDs: ${d.join(", ")}`)),r.length===0&&(o.error(L.default.red("No matching rules found for the given IDs.")),process.exit(1))}else if(n.disabled){if(r=i.filter(l=>!l.active),r.length===0){o.info("No disabled rules found.");return}}else n.all?r=[...i]:(o.error("Specify --name, --id, --disabled, --all, or --interactive to select rules to remove."),process.exit(1));if(r.length===0){o.info("No rules selected for removal.");return}if(r.length>=5&&!n.dryRun&&!n.force&&o.log(L.default.yellow(`
|
|
135
|
-
\u{1F4A1} Tip: Run ${L.default.cyan("
|
|
135
|
+
\u{1F4A1} Tip: Run ${L.default.cyan("doorman backup")} before bulk removal operations.`)),o.log(L.default.cyan(`
|
|
136
136
|
${n.dryRun?"Would remove":"Removing"} ${r.length} rule(s):`)),Ut(r),n.dryRun){o.log(L.default.dim(`
|
|
137
|
-
Dry run \u2014 no changes made.`));return}if(!await Pi(r.length,n.force||!1,n.all||!1)){o.info("Cancelled.");return}let s=new Set(r),a={...t,rules:i.filter(l=>!s.has(l))};a.rules.length===0&&o.warn(L.default.yellow("\u26A0\uFE0F Configuration will have no rules after removal.")),await F(a,n.config),o.success(L.default.green(`\u2714 Removed ${r.length} rule(s) from configuration`)),Ut(r)}o.log(""),o.log(L.default.dim(`Run ${L.default.cyan("
|
|
137
|
+
Dry run \u2014 no changes made.`));return}if(!await Pi(r.length,n.force||!1,n.all||!1)){o.info("Cancelled.");return}let s=new Set(r),a={...t,rules:i.filter(l=>!s.has(l))};a.rules.length===0&&o.warn(L.default.yellow("\u26A0\uFE0F Configuration will have no rules after removal.")),await F(a,n.config),o.success(L.default.green(`\u2714 Removed ${r.length} rule(s) from configuration`)),Ut(r)}o.log(""),o.log(L.default.dim(`Run ${L.default.cyan("doorman sync")} to deploy these changes.`))}catch(e){H(e,"removing rule")}};var Ot={};q(Ot,{builder:()=>ln,command:()=>sn,desc:()=>an,handler:()=>cn});var k=P(require("chalk"));T();var sn="setup",an="Show setup instructions and helpful links for Vercel Doorman",ln={},cn=async n=>{o.log(""),o.log(k.default.bold.cyan("\u{1F6AA} Vercel Doorman Setup Guide")),o.log(k.default.dim(`Everything you need to get started with firewall management
|
|
138
138
|
`)),o.log(k.default.bold("\u{1F4CB} Prerequisites")),o.log("\u2022 Vercel account with a deployed project"),o.log("\u2022 Project with Pro plan or higher (firewall feature requirement)"),o.log(`\u2022 Admin access to the project/team
|
|
139
|
-
`),o.log(k.default.bold("\u{1F50D} Finding Your Project Information")),o.log(""),o.log(k.default.cyan("1. Project ID:")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Select your project"),o.log(" \u2022 Navigate to Settings \u2192 General"),o.log(" \u2022 Copy the Project ID from the top of the page"),o.log(""),o.log(k.default.cyan("2. Team ID (if using team account):")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Click on your team name in the top-left"),o.log(" \u2022 Go to Team Settings \u2192 General"),o.log(" \u2022 Copy the Team ID"),o.log(" \u2022 Leave empty if using personal account"),o.log(""),o.log(k.default.bold("\u{1F511} Creating API Token")),o.log(""),o.log(k.default.cyan("Steps:")),o.log(" 1. Visit https://vercel.com/account/tokens"),o.log(' 2. Click "Create Token"'),o.log(' 3. Name: "Doorman Firewall Management"'),o.log(" 4. Expiration: Set as needed (90 days recommended)"),o.log(" 5. Scope: Select your project or team"),o.log(" 6. Copy the generated token"),o.log(""),o.log(k.default.cyan("Required Permissions:")),o.log(" \u2022 Read access to project settings"),o.log(" \u2022 Write access to firewall configuration"),o.log(""),o.log(k.default.bold("\u{1F30D} Environment Setup")),o.log(""),o.log(k.default.cyan("Set your API token as environment variable:")),o.log(""),o.log(k.default.green("# For current session:")),o.log('export VERCEL_TOKEN="your-token-here"'),o.log(""),o.log(k.default.green("# For permanent setup (add to ~/.bashrc or ~/.zshrc):")),o.log(`echo 'export VERCEL_TOKEN="your-token-here"' >> ~/.bashrc`),o.log(""),o.log(k.default.green("# Or create a .env file in your project:")),o.log('echo "VERCEL_TOKEN=your-token-here" > .env'),o.log(""),o.log(k.default.bold("\u{1F680} Quick Start Commands")),o.log(""),o.log(k.default.cyan("1. Initialize configuration:")),o.log("
|
|
139
|
+
`),o.log(k.default.bold("\u{1F50D} Finding Your Project Information")),o.log(""),o.log(k.default.cyan("1. Project ID:")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Select your project"),o.log(" \u2022 Navigate to Settings \u2192 General"),o.log(" \u2022 Copy the Project ID from the top of the page"),o.log(""),o.log(k.default.cyan("2. Team ID (if using team account):")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Click on your team name in the top-left"),o.log(" \u2022 Go to Team Settings \u2192 General"),o.log(" \u2022 Copy the Team ID"),o.log(" \u2022 Leave empty if using personal account"),o.log(""),o.log(k.default.bold("\u{1F511} Creating API Token")),o.log(""),o.log(k.default.cyan("Steps:")),o.log(" 1. Visit https://vercel.com/account/tokens"),o.log(' 2. Click "Create Token"'),o.log(' 3. Name: "Doorman Firewall Management"'),o.log(" 4. Expiration: Set as needed (90 days recommended)"),o.log(" 5. Scope: Select your project or team"),o.log(" 6. Copy the generated token"),o.log(""),o.log(k.default.cyan("Required Permissions:")),o.log(" \u2022 Read access to project settings"),o.log(" \u2022 Write access to firewall configuration"),o.log(""),o.log(k.default.bold("\u{1F30D} Environment Setup")),o.log(""),o.log(k.default.cyan("Set your API token as environment variable:")),o.log(""),o.log(k.default.green("# For current session:")),o.log('export VERCEL_TOKEN="your-token-here"'),o.log(""),o.log(k.default.green("# For permanent setup (add to ~/.bashrc or ~/.zshrc):")),o.log(`echo 'export VERCEL_TOKEN="your-token-here"' >> ~/.bashrc`),o.log(""),o.log(k.default.green("# Or create a .env file in your project:")),o.log('echo "VERCEL_TOKEN=your-token-here" > .env'),o.log(""),o.log(k.default.bold("\u{1F680} Quick Start Commands")),o.log(""),o.log(k.default.cyan("1. Initialize configuration:")),o.log(" doorman init --interactive"),o.log(""),o.log(k.default.cyan("2. Validate setup:")),o.log(" doorman validate"),o.log(""),o.log(k.default.cyan("3. Check current status:")),o.log(" doorman status"),o.log(""),o.log(k.default.cyan("4. View existing rules:")),o.log(" doorman list"),o.log(""),o.log(k.default.cyan("5. Sync your configuration:")),o.log(" doorman sync"),o.log(""),o.log(k.default.bold("\u{1F527} Common Issues")),o.log(""),o.log(k.default.yellow('\u274C "Project not found" error:')),o.log(" \u2022 Double-check your Project ID"),o.log(" \u2022 Ensure your token has access to the project"),o.log(" \u2022 Verify the project has Pro plan or higher"),o.log(""),o.log(k.default.yellow('\u274C "Unauthorized" error:')),o.log(" \u2022 Check your VERCEL_TOKEN is set correctly"),o.log(" \u2022 Ensure token hasn't expired"),o.log(" \u2022 Verify token has firewall permissions"),o.log(""),o.log(k.default.yellow('\u274C "Firewall not available" error:')),o.log(" \u2022 Upgrade to Pro plan or higher"),o.log(" \u2022 Contact Vercel support if issue persists"),o.log(""),o.log(k.default.bold("\u{1F4DA} Additional Resources")),o.log(""),o.log(k.default.cyan("Documentation:")),o.log(" \u2022 Doorman Docs: https://doorman.griffen.codes"),o.log(" \u2022 Vercel Firewall: https://vercel.com/docs/security/vercel-firewall"),o.log(" \u2022 API Reference: https://vercel.com/docs/rest-api/endpoints/firewall"),o.log(""),o.log(k.default.cyan("Support:")),o.log(" \u2022 GitHub Issues: https://github.com/gfargo/doorman/issues"),o.log(" \u2022 Vercel Support: https://vercel.com/help"),o.log(""),o.log(k.default.bold("\u{1F512} Security Best Practices")),o.log(""),o.log("\u2022 Store API tokens securely (use environment variables)"),o.log("\u2022 Set token expiration dates"),o.log("\u2022 Use principle of least privilege for token scopes"),o.log("\u2022 Regularly rotate API tokens"),o.log("\u2022 Test rules in staging before production"),o.log("\u2022 Keep backups of working configurations"),o.log(""),o.log(k.default.bold.green("\u2705 Ready to get started?")),o.log(k.default.dim("Run: ")+k.default.cyan("doorman init --interactive")),o.log("")};var Nt={};q(Nt,{builder:()=>pn,command:()=>dn,desc:()=>un,handler:()=>gn});var O=P(require("chalk"));T();var we=P(require("chalk")),Ge=class{static check(e){let t=[],i=[],r=100;return this.checkRuleNaming(e.rules,t),this.checkRuleComplexity(e.rules,t),this.checkSecurityBestPractices(e,t,i),this.checkPerformanceImpact(e,t,i),this.checkMaintainability(e,t,i),t.forEach(s=>{switch(s.severity){case"error":r-=20;break;case"warning":r-=10;break;case"info":r-=5;break}}),{score:Math.max(0,r),issues:t,recommendations:i}}static checkRuleNaming(e,t){e.forEach(i=>{i.id&&!i.id.match(/^rule_[a-z0-9_]+$/)&&t.push({severity:"warning",message:"Rule ID should follow snake_case format: rule_*",rule:i.name,suggestion:`Consider renaming to: rule_${i.name.toLowerCase().replace(/\s+/g,"_")}`}),i.name.length<5&&t.push({severity:"info",message:"Rule name is very short, consider making it more descriptive",rule:i.name}),(!i.description||i.description.length<10)&&t.push({severity:"info",message:"Rule lacks a detailed description",rule:i.name,suggestion:"Add a clear description explaining what this rule does and why"})})}static checkRuleComplexity(e,t){e.forEach(i=>{i.conditionGroup.reduce((s,a)=>s+a.conditions.length,0)>10&&t.push({severity:"warning",message:"Rule has many conditions, consider splitting into multiple rules",rule:i.name,suggestion:"Complex rules can be harder to maintain and debug"}),i.conditionGroup.length>5&&t.push({severity:"warning",message:"Rule has many condition groups, consider simplifying",rule:i.name})})}static checkSecurityBestPractices(e,t,i){let{rules:r,ips:s}=e;r.some(c=>c.action.mitigate.action==="rate_limit")||i.push("Consider adding rate limiting rules for API endpoints"),r.some(c=>c.conditionGroup.some(d=>d.conditions.some(u=>u.type==="user_agent"&&typeof u.value=="string"&&(u.value.toLowerCase().includes("bot")||u.value.toLowerCase().includes("crawler")))))||i.push("Consider adding bot protection rules"),r.forEach(c=>{c.conditionGroup.some(u=>u.conditions.some(p=>p.type==="path"&&p.value==="/"))&&c.action.mitigate.action==="deny"&&t.push({severity:"error",message:"Rule blocks root path - this could block all traffic",rule:c.name,suggestion:"Be more specific with path conditions"})}),s&&s.length>100&&t.push({severity:"warning",message:"Large number of IP blocking rules may impact performance",suggestion:"Consider using CIDR ranges or external IP lists"})}static checkPerformanceImpact(e,t,i){let{rules:r}=e;r.forEach(a=>{a.conditionGroup.some(c=>c.conditions.some(d=>d.op==="re"))&&t.push({severity:"info",message:"Rule uses regex matching, which may impact performance",rule:a.name,suggestion:"Consider using simpler string operations when possible"})}),r.filter(a=>a.active).length>50&&t.push({severity:"warning",message:"Large number of active rules may impact performance",suggestion:"Consider consolidating similar rules or disabling unused ones"})}static checkMaintainability(e,t,i){let{rules:r}=e;r.filter(c=>!c.active).length>r.length*.3&&i.push("Consider removing disabled rules to reduce config complexity");let a=r.map(c=>c.name);a.filter((c,d)=>a.indexOf(c)!==d).forEach(c=>{t.push({severity:"error",message:"Duplicate rule name found",rule:c,suggestion:"Rule names must be unique"})}),e.version||t.push({severity:"warning",message:"Configuration lacks version information",suggestion:"Version tracking helps with change management"})}static formatHealthReport(e){let t="",i=e.score>=80?we.default.green:e.score>=60?we.default.yellow:we.default.red;return t+=we.default.bold(`
|
|
140
140
|
\u{1F3E5} Configuration Health Score: ${i(e.score)}/100
|
|
141
141
|
|
|
142
142
|
`),e.issues.length>0&&(t+=we.default.bold(`Issues Found:
|
|
@@ -162,6 +162,6 @@ AJV Schema Validation:`)),o.log(X.default.green("\u2713 JSON Schema validation p
|
|
|
162
162
|
AJV Schema Validation:`)),o.error(X.default.red("\u2717 JSON Schema validation failed:")),s.forEach(l=>{o.error(X.default.red(` - ${l.instancePath}: ${l.message}`))}));else throw a}if(i.success&&r&&n.verbose){o.log(X.default.bold.underline(`
|
|
163
163
|
Custom Validations:`));let a=i.data,l=new Set,c=new Set;for(let d of a.rules)l.has(d.name)&&c.add(d.name),l.add(d.name);o.log(`${X.default.cyan.dim(`
|
|
164
164
|
Rule Names:`)}`),c.size>0?(o.error(X.default.red("\u2717 Duplicate rule names found:")),c.forEach(d=>o.error(X.default.red(` - "${d}"`)))):o.log(X.default.green("\u2713 All rule names are unique")),o.log("")}if(i.success&&r)o.success(X.default.green("Configuration is valid"));else throw new Error("Configuration validation failed")}catch(e){H(e,"validating configuration")}};var zt={};q(zt,{builder:()=>kn,command:()=>Tn,desc:()=>Pn,handler:()=>Sn});var se=P(require("chalk")),tt=require("fs");T();var Tn="watch",Pn="Watch config file for changes and auto-sync",kn={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},interval:{alias:"i",type:"number",description:"Watch interval in milliseconds",default:1e3},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Sn=async n=>{let e=n.config||".doorman.json";await K({config:e,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"setting up watch mode"},async({service:t})=>{let i=!1,r=0;o.success(se.default.green(`\u{1F440} Watching ${e} for changes...`)),o.log(se.default.dim("Press Ctrl+C to stop watching")),o.log("");let s=async(c,d)=>{if(!(i||c.mtime.getTime()===r)){i=!0,r=c.mtime.getTime();try{o.log(se.default.yellow(`\u{1F4DD} Config file changed at ${new Date().toLocaleTimeString()}`)),o.start("Validating and syncing changes...");let u=await z(e),{toAdd:p,toUpdate:g,toDelete:C,ipsToAdd:m,ipsToUpdate:w,ipsToDelete:_,version:N}=await t.getChanges(u);if(!(p.length>0||g.length>0||C.length>0||m.length>0||w.length>0||_.length>0||u.version!==N)){o.info(se.default.blue("No changes detected, skipping sync"));return}let f=p.length+g.length+C.length+m.length+w.length+_.length;o.log(se.default.cyan(`Syncing ${f} changes...`));let $=await t.syncRules(u,{debug:n.debug});try{let h=await t.validateAndUpdateConfig(u,$,{dryRun:!1});await F(h,e),o.success(se.default.green(`\u2705 Sync completed at ${new Date().toLocaleTimeString()}`))}catch(h){o.warn(se.default.yellow(`\u26A0\uFE0F Sync applied but validation failed: ${h instanceof Error?h.message:String(h)}`)),o.info(se.default.dim("Run `download` to reconcile local config with remote state"))}o.log(se.default.dim("Watching for more changes..."))}catch(u){o.error(se.default.red(`\u274C Sync failed: ${u instanceof Error?u.message:String(u)}`)),o.log(se.default.dim("Continuing to watch for changes..."))}finally{i=!1}}};(0,tt.watchFile)(e,{interval:n.interval},s);let a=()=>{o.log(`
|
|
165
|
-
`),o.info(se.default.yellow("Stopping watch mode...")),(0,tt.unwatchFile)(e),process.exit(0)};process.on("SIGINT",a),process.on("SIGTERM",a);let l=()=>{setTimeout(l,1e3)};l()})};var Fi=[Ot,Lt,gt,Dt,Nt,kt,Ft,jt,St,Bt,xt,It,_t,zt];(0,Vi.config)();async function _n(){let n=(await import("chalk")).default,e=(0,Bi.default)(process.argv.slice(2));e.usage(`${n.bold("\
|
|
165
|
+
`),o.info(se.default.yellow("Stopping watch mode...")),(0,tt.unwatchFile)(e),process.exit(0)};process.on("SIGINT",a),process.on("SIGTERM",a);let l=()=>{setTimeout(l,1e3)};l()})};var Fi=[Ot,Lt,gt,Dt,Nt,kt,Ft,jt,St,Bt,xt,It,_t,zt];(0,Vi.config)();async function _n(){let n=(await import("chalk")).default,e=(0,Bi.default)(process.argv.slice(2));e.usage(`${n.bold("\u{1F6AA} Doorman")}
|
|
166
166
|
|
|
167
|
-
${n.dim("Manage
|
|
167
|
+
${n.dim("Manage firewall rules as code across multiple providers")}`);for(let t of Fi)e.command(t);e.demandCommand(1,"You need at least one command before moving on").help().epilogue(n.dim(`See ${n.bold("https://doorman.griffen.codes/getting-started")} for more info`)).argv}_n().catch(console.error);
|
package/dist/bin/run.mjs
CHANGED
|
@@ -9,14 +9,14 @@ var Xe=Object.defineProperty;var Ri=Object.getOwnPropertyDescriptor;var Ci=Objec
|
|
|
9
9
|
\u{1F4BE} Backup Recommended: ${c.reason}`),c.instructions.forEach(g=>{o.info(` \u2022 ${g}`)}));let d=this.getRollbackGuidance(t);d.available&&(o.info(`
|
|
10
10
|
\u{1F504} Rollback Available: ${d.method}`),d.timeWindow&&o.info(` Time Window: ${d.timeWindow}`));let u=this.getConfirmationMessage(s,t);return await R(u,{type:"confirm"})?(o.info("\u2705 Operation confirmed by user"),!0):(o.info("Operation cancelled by user"),!1)}static async performDryRunValidation(e,t,i){o.info(`\u{1F50D} Performing dry-run validation for ${t}...`);let r=[],s={rulesToAdd:[],rulesToUpdate:[],rulesToDelete:[],ipsToAdd:[],ipsToUpdate:[],ipsToDelete:[],hasChanges:!1};try{this.validateConfigurationStructure(e,r),s=await i(e),this.validateChanges(s,r),this.checkForPotentialIssues(e,s,r);let a=r.length===0;return a?o.info("\u2705 Dry-run validation passed"):(o.warn(`\u26A0\uFE0F Dry-run validation found ${r.length} issues:`),r.forEach(l=>o.warn(` \u2022 ${l}`))),{valid:a,changes:s,issues:r}}catch(a){let l=`Dry-run validation failed: ${a instanceof Error?a.message:String(a)}`;return r.push(l),o.error(l),{valid:!1,changes:s,issues:r}}}static getBackupRecommendation(e,t){return{"sync rules":{recommended:t!=="low",reason:"Rule synchronization can overwrite existing firewall configuration",instructions:['Run "doorman download" to backup current Cloudflare rules',"Save the downloaded configuration file with a timestamp","Consider using version control for configuration files"]},"delete ruleset":{recommended:!0,reason:"Ruleset deletion is irreversible and removes all associated rules",instructions:['Export current ruleset with "doorman export"',"Document the ruleset ID and configuration","Ensure you have the original configuration files"]},"update rules":{recommended:t==="high",reason:"Rule updates can affect traffic flow and security posture",instructions:["Download current configuration as backup","Test changes in a staging environment if possible","Have rollback plan ready"]},"clear all rules":{recommended:!0,reason:"Clearing all rules removes all firewall protection",instructions:['Export complete configuration with "doorman export"',"Save configuration to version control","Document business justification for clearing rules"]}}[e]||{recommended:t!=="low",reason:"Operation may modify existing configuration",instructions:["Download current configuration as backup","Document the changes being made"]}}static getRollbackGuidance(e){return{"sync rules":{available:!0,method:"Restore from backup configuration",instructions:['Use "doorman sync" with your backup configuration file',"Or manually restore rules in Cloudflare dashboard","Check that all rules are functioning as expected"],timeWindow:"No time limit - can rollback anytime"},"delete ruleset":{available:!1,method:"Recreation required",instructions:["Deleted rulesets cannot be restored","Must recreate ruleset and rules from backup",'Use "doorman sync" with backup configuration']},"update rules":{available:!0,method:"Restore previous rule version",instructions:["Sync with previous configuration version","Or use Cloudflare dashboard to revert changes","Verify rule functionality after rollback"],timeWindow:"Immediate - no time restrictions"},"clear all rules":{available:!0,method:"Restore from backup",instructions:["Sync with backup configuration file","Verify all rules are restored correctly","Test firewall functionality"],timeWindow:"No time limit"}}[e]||{available:!0,method:"Restore from backup",instructions:["Use backup configuration to restore previous state","Verify changes are reverted correctly"]}}static formatRiskLevel(e){let t=bi("chalk");switch(e){case"low":return t.green("LOW");case"medium":return t.yellow("MEDIUM");case"high":return t.red("HIGH");default:return String(e).toUpperCase()}}static displayChangeSummary(e){o.info(`
|
|
11
11
|
\u{1F4CA} Change Summary:`),e.rulesToAdd?.length&&o.info(` \u2022 Rules to add: ${e.rulesToAdd.length}`),e.rulesToUpdate?.length&&o.info(` \u2022 Rules to update: ${e.rulesToUpdate.length}`),e.rulesToDelete?.length&&o.info(` \u2022 Rules to delete: ${e.rulesToDelete.length}`),e.ipsToAdd?.length&&o.info(` \u2022 IPs to add: ${e.ipsToAdd.length}`),e.ipsToUpdate?.length&&o.info(` \u2022 IPs to update: ${e.ipsToUpdate.length}`),e.ipsToDelete?.length&&o.info(` \u2022 IPs to delete: ${e.ipsToDelete.length}`),e.hasChanges||o.info(" \u2022 No changes detected")}static displayRiskWarnings(e,t){let i={low:["This operation has minimal risk of disrupting service"],medium:["This operation may temporarily affect traffic filtering","Monitor your application after the change"],high:["\u26A0\uFE0F This operation can significantly impact security and traffic flow","\u26A0\uFE0F Ensure you have tested the configuration in a safe environment","\u26A0\uFE0F Have a rollback plan ready before proceeding"]},r={"clear all rules":["\u{1F6A8} This will remove ALL firewall protection","\u{1F6A8} Your application will be unprotected until new rules are added"],"delete ruleset":["\u{1F6A8} This action is IRREVERSIBLE","\u{1F6A8} All rules in the ruleset will be permanently deleted"]};o.info(`
|
|
12
|
-
\u26A0\uFE0F Warnings:`),i[e]?.forEach(s=>{o.warn(` ${s}`)}),r[t]?.forEach(s=>{o.warn(` ${s}`)})}static getConfirmationMessage(e,t){let i=`Do you want to proceed with ${t}?`;switch(e){case"high":return`${i} (Type 'yes' to confirm high-risk operation)`;case"medium":return`${i} (Proceed with caution)`;default:return i}}static validateConfigurationStructure(e,t){if(!e){t.push("Configuration is null or undefined");return}e.version||t.push("Configuration missing version field"),e.provider||t.push("Configuration missing provider field"),Array.isArray(e.rules)||t.push("Configuration rules field is not an array"),e.ips&&!Array.isArray(e.ips)&&t.push("Configuration ips field is not an array")}static validateChanges(e,t){let i=(e.rulesToDelete?.length||0)+(e.ipsToDelete?.length||0),r=(e.rulesToAdd?.length||0)+(e.rulesToUpdate?.length||0)+(e.ipsToAdd?.length||0)+(e.ipsToUpdate?.length||0)+i;if(i>0&&r>0){let s=i/r;s>.5&&t.push(`High deletion ratio detected: ${Math.round(s*100)}% of changes are deletions`)}r>100&&t.push(`Large number of changes detected: ${r} total changes`)}static checkForPotentialIssues(e,t,i){t.rulesToDelete?.length===e.rules.length&&e.rules.length>0&&i.push("All existing rules will be deleted - this removes all firewall protection");let r=e.rules.filter(l=>l.action.type==="deny"&&l.conditions.some(c=>c.field==="path"&&(c.value==="/"||c.value==="*")));r.length>0&&i.push(`Found ${r.length} rules that may block all traffic`),e.ips&&e.ips.length>1e3&&i.push(`Large IP list detected: ${e.ips.length} IPs may impact performance`);let s=e.rules.map(l=>l.name),a=s.filter((l,c)=>s.indexOf(l)!==c);a.length>0&&i.push(`Duplicate rule names found: ${[...new Set(a)].join(", ")}`)}}});import{config as xn}from"dotenv";import On from"yargs";var at={};z(at,{builder:()=>Ki,command:()=>Hi,desc:()=>Zi,generateRuleId:()=>st,handler:()=>to});T();import U from"chalk";import{LogLevels as Gi}from"consola";import{z as A}from"zod";var Be=A.number().int().positive().optional(),Ai=A.string().ip().or(A.string().regex(/^(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/)),Ut=A.string().optional(),$i=A.enum(["eq","pre","suf","inc","sub","re","ex","nex"]),Ti=A.enum(["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]),Pi=A.object({op:$i,neg:A.boolean().optional(),type:Ti,value:A.union([A.string(),A.array(A.string()),A.array(A.number())]).optional(),key:A.string().optional()}).refine(n=>{switch(n.type){case"ip_address":case"method":case"environment":case"protocol":return n.op==="eq"||n.op==="inc";default:return!0}},"Invalid operator for the given condition type").refine(n=>{let e=["header","cookie"];return!(e.includes(n.type)&&!n.key||e.includes(n.type)&&n.op!=="ex"&&n.op!=="nex"&&!n.value)},"Missing key from condition").refine(n=>!(["ex","nex"].includes(n.op)&&n.neg),"Negation is not supported for the given operator").refine(n=>!(n.op==="inc"&&!Array.isArray(n.value)),"When using `inc` operator, value must be an array"),Qe=A.object({conditions:A.array(Pi).min(1,"Condition group must have at least one condition")}),ki=A.string().regex(/^\d+[smhd]$|^permanent$/),Ye=A.object({requests:A.number().positive().int().refine(n=>n>0,"requests must be positive"),window:A.string().regex(/^\d+[smhd]$/).refine(n=>parseInt(n)>0,"window duration must be positive")}),Si=A.object({location:A.string(),permanent:A.boolean().optional()}),_i=A.enum(["log","deny","challenge","bypass","rate_limit","redirect"]),Li=A.object({action:_i,rateLimit:Ye.nullable().optional(),redirect:Si.nullable().optional(),actionDuration:ki.nullable().optional()}),Di=A.object({mitigate:Li}),et=A.object({id:Ut,name:A.string(),description:A.string().optional(),conditionGroup:A.array(Qe),action:Di,active:A.boolean()});var tt=A.object({id:Ut,ip:Ai,hostname:A.string(),notes:A.string().optional(),action:A.literal("deny")}),Ui=A.object({projectId:A.string().optional(),teamId:A.string().optional()}),ue=Ui.extend({rules:A.array(et),ips:A.array(tt).optional(),version:A.number().optional(),updatedAt:A.string().optional()});ie();T();import{existsSync as Ft,mkdirSync as Bi,readFileSync as ji,writeFileSync as zi}from"fs";import{dirname as Wi}from"path";import Oi from"ajv";var xt="https://doorman.griffen.codes/schema.json",Ot={$schema:"http://json-schema.org/draft-07/schema#",title:"Doorman Config",description:"Schema for
|
|
12
|
+
\u26A0\uFE0F Warnings:`),i[e]?.forEach(s=>{o.warn(` ${s}`)}),r[t]?.forEach(s=>{o.warn(` ${s}`)})}static getConfirmationMessage(e,t){let i=`Do you want to proceed with ${t}?`;switch(e){case"high":return`${i} (Type 'yes' to confirm high-risk operation)`;case"medium":return`${i} (Proceed with caution)`;default:return i}}static validateConfigurationStructure(e,t){if(!e){t.push("Configuration is null or undefined");return}e.version||t.push("Configuration missing version field"),e.provider||t.push("Configuration missing provider field"),Array.isArray(e.rules)||t.push("Configuration rules field is not an array"),e.ips&&!Array.isArray(e.ips)&&t.push("Configuration ips field is not an array")}static validateChanges(e,t){let i=(e.rulesToDelete?.length||0)+(e.ipsToDelete?.length||0),r=(e.rulesToAdd?.length||0)+(e.rulesToUpdate?.length||0)+(e.ipsToAdd?.length||0)+(e.ipsToUpdate?.length||0)+i;if(i>0&&r>0){let s=i/r;s>.5&&t.push(`High deletion ratio detected: ${Math.round(s*100)}% of changes are deletions`)}r>100&&t.push(`Large number of changes detected: ${r} total changes`)}static checkForPotentialIssues(e,t,i){t.rulesToDelete?.length===e.rules.length&&e.rules.length>0&&i.push("All existing rules will be deleted - this removes all firewall protection");let r=e.rules.filter(l=>l.action.type==="deny"&&l.conditions.some(c=>c.field==="path"&&(c.value==="/"||c.value==="*")));r.length>0&&i.push(`Found ${r.length} rules that may block all traffic`),e.ips&&e.ips.length>1e3&&i.push(`Large IP list detected: ${e.ips.length} IPs may impact performance`);let s=e.rules.map(l=>l.name),a=s.filter((l,c)=>s.indexOf(l)!==c);a.length>0&&i.push(`Duplicate rule names found: ${[...new Set(a)].join(", ")}`)}}});import{config as xn}from"dotenv";import On from"yargs";var at={};z(at,{builder:()=>Ki,command:()=>Hi,desc:()=>Zi,generateRuleId:()=>st,handler:()=>to});T();import U from"chalk";import{LogLevels as Gi}from"consola";import{z as A}from"zod";var Be=A.number().int().positive().optional(),Ai=A.string().ip().or(A.string().regex(/^(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/)),Ut=A.string().optional(),$i=A.enum(["eq","pre","suf","inc","sub","re","ex","nex"]),Ti=A.enum(["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]),Pi=A.object({op:$i,neg:A.boolean().optional(),type:Ti,value:A.union([A.string(),A.array(A.string()),A.array(A.number())]).optional(),key:A.string().optional()}).refine(n=>{switch(n.type){case"ip_address":case"method":case"environment":case"protocol":return n.op==="eq"||n.op==="inc";default:return!0}},"Invalid operator for the given condition type").refine(n=>{let e=["header","cookie"];return!(e.includes(n.type)&&!n.key||e.includes(n.type)&&n.op!=="ex"&&n.op!=="nex"&&!n.value)},"Missing key from condition").refine(n=>!(["ex","nex"].includes(n.op)&&n.neg),"Negation is not supported for the given operator").refine(n=>!(n.op==="inc"&&!Array.isArray(n.value)),"When using `inc` operator, value must be an array"),Qe=A.object({conditions:A.array(Pi).min(1,"Condition group must have at least one condition")}),ki=A.string().regex(/^\d+[smhd]$|^permanent$/),Ye=A.object({requests:A.number().positive().int().refine(n=>n>0,"requests must be positive"),window:A.string().regex(/^\d+[smhd]$/).refine(n=>parseInt(n)>0,"window duration must be positive")}),Si=A.object({location:A.string(),permanent:A.boolean().optional()}),_i=A.enum(["log","deny","challenge","bypass","rate_limit","redirect"]),Li=A.object({action:_i,rateLimit:Ye.nullable().optional(),redirect:Si.nullable().optional(),actionDuration:ki.nullable().optional()}),Di=A.object({mitigate:Li}),et=A.object({id:Ut,name:A.string(),description:A.string().optional(),conditionGroup:A.array(Qe),action:Di,active:A.boolean()});var tt=A.object({id:Ut,ip:Ai,hostname:A.string(),notes:A.string().optional(),action:A.literal("deny")}),Ui=A.object({projectId:A.string().optional(),teamId:A.string().optional()}),ue=Ui.extend({rules:A.array(et),ips:A.array(tt).optional(),version:A.number().optional(),updatedAt:A.string().optional()});ie();T();import{existsSync as Ft,mkdirSync as Bi,readFileSync as ji,writeFileSync as zi}from"fs";import{dirname as Wi}from"path";import Oi from"ajv";var xt="https://doorman.griffen.codes/schema.json",Ot={$schema:"http://json-schema.org/draft-07/schema#",title:"Doorman Config",description:"Schema for doorman project configuration files",$ref:"#/definitions/FirewallConfig",definitions:{FirewallConfig:{type:"object",properties:{projectId:{type:"string"},teamId:{type:"string"},$schema:{type:"string"},version:{type:"number"},firewallEnabled:{type:"boolean"},rules:{type:"array",items:{$ref:"#/definitions/CustomRule"}},ips:{type:"array",items:{$ref:"#/definitions/IPBlockingRule"}},updatedAt:{type:"string"}},required:["rules"],additionalProperties:!1,description:"The main configuration type for Vercel Doorman"},CustomRule:{type:"object",properties:{id:{type:"string"},name:{type:"string"},description:{type:"string"},conditionGroup:{type:"array",items:{$ref:"#/definitions/ConditionGroup"}},action:{$ref:"#/definitions/RuleAction"},active:{type:"boolean"}},required:["name","conditionGroup","action","active"],additionalProperties:!1,description:"Rule Types"},ConditionGroup:{type:"object",properties:{conditions:{type:"array",items:{$ref:"#/definitions/RuleCondition"}}},required:["conditions"],additionalProperties:!1},RuleCondition:{type:"object",properties:{op:{$ref:"#/definitions/RuleOperator"},neg:{type:"boolean"},type:{$ref:"#/definitions/RuleType"},key:{type:"string"},value:{anyOf:[{type:"string"},{type:"number"},{type:"array",items:{type:"string"}},{type:"array",items:{type:"number"}}]}},required:["op","type"],additionalProperties:!1,description:"Rule Condition Types"},RuleOperator:{type:"string",enum:["eq","pre","suf","inc","sub","re","ex","nex"]},RuleType:{type:"string",enum:["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"]},RuleAction:{type:"object",properties:{mitigate:{$ref:"#/definitions/MitigationAction"}},required:["mitigate"],additionalProperties:!1},MitigationAction:{type:"object",properties:{action:{$ref:"#/definitions/ActionType"},rateLimit:{anyOf:[{$ref:"#/definitions/RateLimit"},{type:"null"}]},redirect:{anyOf:[{$ref:"#/definitions/Redirect"},{type:"null"}]},actionDuration:{type:["string","null"]}},required:["action"],additionalProperties:!1},ActionType:{type:"string",enum:["log","deny","challenge","bypass","rate_limit","redirect"],description:"Core Types"},RateLimit:{type:"object",properties:{requests:{type:"number"},window:{type:"string"}},required:["requests","window"],additionalProperties:!1,description:"Action Types"},Redirect:{type:"object",properties:{location:{type:"string"},permanent:{type:"boolean"}},required:["location"],additionalProperties:!1},IPBlockingRule:{type:"object",properties:{id:{type:"string"},ip:{type:"string"},hostname:{type:"string"},notes:{type:"string"},action:{type:"string",const:"deny"}},required:["ip","hostname","action"],additionalProperties:!1}}};import oe from"chalk";var ge=class{static formatValidationError(e){let t=e.instancePath||"/",i=oe.cyan(t);switch(e.keyword){case"type":return`${i}: Expected ${oe.green(e.schema)}, got ${oe.red(typeof e.data)}`;case"enum":{let r=e.params.allowedValues;return`${i}: Value must be one of: ${oe.green(r.join(", "))}`}case"required":{let r=e.params;return`${i}: Missing required property ${oe.yellow(r.missingProperty)}`}case"additionalProperties":{let r=e.params;return`${i}: Unknown property ${oe.red(r.additionalProperty)}`}default:return`${i}: ${e.message}`}}static formatCustomError(e,t){return`${oe.cyan(e)}: ${t}`}static wrapErrorBlock(e){let t=oe.red.bold("Validation Errors:"),i=e.map(r=>` ${r}`).join(`
|
|
13
13
|
`);return`${t}
|
|
14
14
|
${i}`}static formatDetailedRuleValidation(e,t){let i=oe.cyan(`Rule: ${e}`),r=t.map(s=>{let a=s.passed?oe.green("\u2713"):oe.red("\u2717"),l=s.passed?oe.green(s.name):oe.red(s.name);return` ${a} ${l}`}).join(`
|
|
15
15
|
`);return`${i}
|
|
16
16
|
${r}`}};var D=class n extends Error{constructor(t,i,r,s=[]){super(t);this.ajvErrors=i;this.zodError=r;this.customErrors=s;this.name="ValidationError"}ajvErrors;zodError;customErrors;static formatErrors(t,i,r=[]){let s=[];return t?.length&&s.push(...t.map(ge.formatValidationError)),i&&s.push(...i.errors.map(a=>`${a.path.join(".")}: ${a.message}`)),r.length&&s.push(...r),ge.wrapErrorBlock(s)}getFormattedMessage(){return n.formatErrors(this.ajvErrors,this.zodError,this.customErrors)}},we=class n{static instance;ajv;schema;constructor(){this.ajv=new Oi({allErrors:!0,verbose:!0}),this.schema=Ot}static getInstance(){return n.instance||(n.instance=new n),n.instance}validateConfig(e){let t=this.ajv.compile(this.schema),i=t(e),r=t.errors,s=ue.safeParse(e),a;s.success||(a=s.error);let l=[];if(i&&s.success){try{this.validateRuleNames(e),this.validateRuleConditionGroup(e),this.validateRuleAction(e)}catch(c){if(c instanceof D)l.push(c.message,...c.customErrors||[]);else throw c}for(let c of e.rules){if(c.action.mitigate?.rateLimit){let d=Ye.safeParse(c.action.mitigate.rateLimit);d.success||l.push(...d.error.errors.map(u=>u.message))}if(c.conditionGroup)for(let d of c.conditionGroup){let u=Qe.safeParse(d);u.success||l.push(...u.error.errors.map(p=>p.message))}}}if(!i||!s.success||l.length>0)throw new D(`Invalid firewall configuration:
|
|
17
|
-
`+D.formatErrors(r,a,l),r,a,l)}validateRuleNames(e){let t=new Set;for(let i of e.rules){if(t.has(i.name))throw new D(`Duplicate rule name found: "${i.name}"`,null);t.add(i.name)}}validateRuleConditionGroup(e){for(let t of e.rules)if(t.conditionGroup)for(let i of t.conditionGroup){if(!i.conditions||i.conditions.length===0)throw new D(`Rule "${t.name}" has an empty condition group`,null);for(let r of i.conditions){if(!r.type||!r.op)throw new D(`Rule "${t.name}" has an invalid condition`,null);let s=r.type==="header"||r.type==="cookie",a=r.op==="ex"||r.op==="nex";if(s){if(!r.key||typeof r.key!="string")throw new D(`Rule "${t.name}" has an invalid condition`,null);if(!a&&(r.value===void 0||r.value===null))throw new D(`Rule "${t.name}" has an invalid condition`,null)}else if(r.value===void 0||r.value===null)throw new D(`Rule "${t.name}" has an invalid condition`,null);let l=Array.isArray(r.value)?r.value:r.value!==void 0?[r.value]:[];if(Array.isArray(r.value)&&l.length===0)throw new D(`Rule "${t.name}" has an invalid value in condition`,null);if(r.type==="ip_address"){for(let c of l)if(typeof c!="string"||!this.isValidIP(c))throw new D(`Rule "${t.name}" has an invalid IP address in condition`,null)}if(r.type==="geo_as_number")for(let c of l){let d=typeof c=="number"?c:parseInt(String(c),10);if(!this.isValidASN(String(d)))throw new D(`Rule "${t.name}" has an invalid ASN in condition`,null)}if(r.type==="path"&&(r.op==="eq"||r.op==="pre")){for(let c of l)if(typeof c!="string"||!this.isValidPath(c))throw new D(`Rule "${t.name}" has an invalid path in condition`,null)}}}else throw new D("Either conditionGroup or type+values must be provided",null)}validateRuleAction(e){for(let t of e.rules)if(typeof t.action=="object"&&"mitigate"in t.action){let i=t.action.mitigate;if(i?.rateLimit){if(i.rateLimit.requests<=0)throw new D("Invalid rate limit configuration: requests must be positive",null);if(!i.rateLimit.window.match(/^\d+[smhd]$/))throw new D("Invalid rate limit configuration: invalid window format",null);if(parseInt(i.rateLimit.window)<=0)throw new D("Invalid rate limit configuration: window duration must be positive",null)}if(i?.actionDuration&&!i.actionDuration.match(/^\d+[smhd]$|^permanent$/))throw new D("Invalid action duration format: "+i.actionDuration,null);if(i?.redirect&&!i.redirect.location)throw new D("Invalid redirect configuration: location is required",null)}}isValidIP(e){let t=e.split("/"),i=t[0]??"",r=t[1],s=d=>{let u=d.split(".");return u.length!==4?!1:u.every(p=>{if(!/^\d{1,3}$/.test(p))return!1;let g=parseInt(p,10);return g>=0&&g<=255})},a=d=>{let u=d.split(":");return u.length!==8?!1:u.every(p=>/^[0-9a-fA-F]{1,4}$/.test(p))},l=s(i),c=a(i);if(!l&&!c)return!1;if(r!==void 0){let d=parseInt(r,10);if(Number.isNaN(d)||l&&(d<0||d>32)||c&&(d<0||d>128))return!1}return!0}isValidASN(e){let t=parseInt(e,10);return!isNaN(t)&&t>=1&&t<=4294967295}isValidPath(e){return e.startsWith("/")&&!e.includes(" ")}};import{existsSync as Ni}from"fs";import{dirname as Fi,resolve as Nt}from"path";var it=[".doorman.json","vercel-firewall.config.json"],Vi=it[0],Re=class{static async findConfig(e){let{findUp:t}=await import("find-up");for(let i of it){let r=await t(i,{cwd:e||process.cwd()});if(r)return r}}static async findProjectRoot(e){let t=await this.findConfig(e);return t?Fi(t):null}static configExists(e){return Ni(Nt(e))}static getDefaultConfigPath(){return Nt(process.cwd(),Vi)}static getSupportedFileNames(){return it}};async function B(n,e="required"){let t=qi(e),i=n||await Re.findConfig();if(!i||!Ft(i)){if(t==="optional")return o.debug("No config file found, returning empty config"),{};let s=Re.getDefaultConfigPath();throw new Error(`No config file found. Run \`vercel-doorman init\` to create one at ${s}, or use --config to specify a custom path.`)}let r;try{let s=ji(i,"utf8");r=JSON.parse(s)}catch(s){throw s instanceof SyntaxError?new Error(`Invalid JSON in config file (${i}): ${s.message}`):s}if(t!=="raw")try{we.getInstance().validateConfig(r)}catch(s){if(t==="lenient")o.warn("Config validation failed:",s);else throw s}return r}async function N(n,e,t={validate:!0,throwOnError:!0}){let i=e||await Re.findConfig()||Re.getDefaultConfigPath();if(t.validate)try{we.getInstance().validateConfig(n)}catch(s){if(o.error("Config validation failed:",s),t.throwOnError)throw s}let r=Wi(i);Ft(r)||Bi(r,{recursive:!0}),zi(i,JSON.stringify(n,null,2)),o.debug(`Config saved to ${i}`)}function qi(n){if(typeof n=="string")return n;let{validate:e=!0,throwOnError:t=!0}=n;return e?t?"required":"lenient":"raw"}T();import Vt from"chalk";import{ZodError as Mi}from"zod";function G(n,e){n instanceof SyntaxError?o.log(ge.wrapErrorBlock(["Invalid JSON format in config file:",` ${n.message}`])):n instanceof Mi?(o.error(Vt.red("Schema validation failed:")),n.errors.forEach(t=>{let i=t.path.join(".");o.error(Vt.red(` - ${i}: ${t.message}`))})):n instanceof Error&&n.name==="ValidationError"?o.error(n):n instanceof Error&&n.message.includes("Forbidden")?o.log(ge.wrapErrorBlock([`Error ${e}:`," Access denied. Check that your token has the correct scope."," If using a team, ensure the token is scoped to that team."])):o.error(ge.wrapErrorBlock([`Error ${e}:`,` ${n instanceof Error?n.message:String(n)}`])),process.exit(1)}var Hi="add [type]",Zi="Add a new firewall rule to your configuration",Ki={type:{type:"string",description:'Rule type: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Guided prompts for rule creation",default:!1},name:{alias:"n",type:"string",description:"Rule name"},description:{type:"string",description:"Rule description"},field:{type:"string",description:"Condition field type (path, method, user_agent, etc.)"},op:{type:"string",description:"Operator (eq, pre, suf, sub, inc, re, ex, nex)"},value:{type:"string",description:"Match value (string or comma-separated for arrays)"},key:{type:"string",description:"Header/query/cookie key (required for header, cookie types)"},neg:{type:"boolean",description:"Negate the condition",default:!1},action:{alias:"a",type:"string",description:"Action type (deny, challenge, rate_limit, redirect, log, bypass)",default:"deny"},active:{type:"boolean",description:"Enable rule immediately",default:!0},requests:{type:"number",description:"Rate limit: max requests"},window:{type:"string",description:'Rate limit: time window (e.g., "60s", "5m")'},duration:{type:"string",description:'Action duration (e.g., "1h", "permanent")'},location:{type:"string",description:"Redirect URL"},permanent:{type:"boolean",description:"Redirect: use 301 instead of 302"},ip:{type:"string",description:"IP address or CIDR for IP blocking rules"},hostname:{type:"string",description:"Hostname for IP blocking rules",default:"*"},notes:{type:"string",description:"Notes for IP blocking rules"},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be added without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}},ot=["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"],nt=["eq","pre","suf","inc","sub","re","ex","nex"],rt=["log","deny","challenge","bypass","rate_limit","redirect"];function st(n){let e=n.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");if(!e)throw new Error("Cannot generate rule ID: name must contain at least one alphanumeric character");return"rule_"+e}function zt(n,e){return e==="inc"?n.split(",").map(t=>t.trim()):n}async function Ji(){let n=await R("Rule name:",{type:"text"});if(!n.trim())throw new Error("Rule name cannot be empty");let e=await R("Description (optional):",{type:"text"}),t=[],i=!0;for(;i;){let u=[],p=!0;for(;p;){let g=await R("Condition type:",{type:"select",options:ot,initial:"path"}),C=await R("Operator:",{type:"select",options:nt,initial:"eq"}),m;(g==="header"||g==="cookie"||g==="query")&&(m=await R(`${g} key:`,{type:"text"}));let w;if(C!=="ex"&&C!=="nex"){let b=await R("Value:",{type:"text"});w=zt(b,C)}let S=C!=="ex"&&C!=="nex"?await R("Negate this condition?",{type:"confirm",initial:!1}):!1,x={type:g,op:C,...w!==void 0&&{value:w},...m&&{key:m},...S&&{neg:S}};u.push(x),p=await R("Add another condition to this group? (AND logic)",{type:"confirm",initial:!1})}t.push({conditions:u}),i=await R("Add another condition group? (OR logic)",{type:"confirm",initial:!1})}let r=await R("Action:",{type:"select",options:rt,initial:"deny"}),s;if(r==="rate_limit"){let u=parseInt(await R("Max requests:",{type:"text",initial:"100"}),10),p=await R('Time window (e.g., "60s", "1m", "1h"):',{type:"text",initial:"60s"});s={requests:u,window:p}}let a;if(r==="redirect"){let u=await R("Redirect URL:",{type:"text"}),p=await R("Permanent redirect (301)?",{type:"confirm",initial:!1});a={location:u,...p&&{permanent:p}}}let l;(r==="deny"||r==="challenge")&&await R("Set action duration? (default: permanent)",{type:"confirm",initial:!1})&&(l=await R('Duration (e.g., "1h", "1d", "permanent"):',{type:"text",initial:"permanent"}));let c=await R("Enable rule immediately?",{type:"confirm",initial:!0});return{id:st(n),name:n.trim(),...e.trim()&&{description:e.trim()},conditionGroup:t,action:{mitigate:{action:r,...s&&{rateLimit:s},...a&&{redirect:a},...l&&{actionDuration:l}}},active:c}}function Xi(n){if(!n.name)throw new Error("--name is required in inline mode. Use --interactive for guided prompts.");if(!n.field)throw new Error("--field is required in inline mode (e.g., --field path)");if(!n.op)throw new Error("--op is required in inline mode (e.g., --op eq)");let e=n.op;if(!nt.includes(e))throw new Error(`Invalid operator "${n.op}". Valid operators: ${nt.join(", ")}`);let t=n.field;if(!ot.includes(t))throw new Error(`Invalid field type "${n.field}". Valid types: ${ot.join(", ")}`);let i=n.action||"deny";if(!rt.includes(i))throw new Error(`Invalid action "${n.action}". Valid actions: ${rt.join(", ")}`);if(e!=="ex"&&e!=="nex"&&!n.value)throw new Error("--value is required for the given operator");if((t==="header"||t==="cookie"||t==="query")&&!n.key)throw new Error(`--key is required for "${t}" condition type`);if(i==="rate_limit"&&(!n.requests||!n.window))throw new Error("--requests and --window are required when action is rate_limit");if(i==="redirect"&&!n.location)throw new Error("--location is required when action is redirect");let r=n.value?zt(n.value,e):void 0,s={type:t,op:e,...r!==void 0&&{value:r},...n.key&&{key:n.key},...n.neg&&{neg:!0}};return{id:st(n.name),name:n.name.trim(),...n.description&&{description:n.description},conditionGroup:[{conditions:[s]}],action:{mitigate:{action:i,...i==="rate_limit"&&n.requests&&n.window&&{rateLimit:{requests:n.requests,window:n.window}},...i==="redirect"&&n.location&&{redirect:{location:n.location,...n.permanent&&{permanent:!0}}},...n.duration&&{actionDuration:n.duration}}},active:n.active!==!1}}async function Qi(){let n=await R("IP address or CIDR (e.g., 192.168.1.100/32):",{type:"text"});if(!n.trim())throw new Error("IP address cannot be empty");let e=await R("Hostname (use * for all):",{type:"text",initial:"*"}),t=await R("Notes (optional):",{type:"text"});return{ip:n.trim(),hostname:e.trim()||"*",...t.trim()&&{notes:t.trim()},action:"deny"}}function Yi(n){if(!n.ip)throw new Error("--ip is required for IP blocking rules");return{ip:n.ip.trim(),hostname:n.hostname||"*",...n.notes&&{notes:n.notes},action:"deny"}}function eo(n,e){if(n.rules.map(r=>r.name.toLowerCase()).includes(e.name.toLowerCase()))return`A rule named "${e.name}" already exists`;let i=n.rules.map(r=>r.id).filter(Boolean);return e.id&&i.includes(e.id)?`A rule with ID "${e.id}" already exists`:null}function Bt(n){o.log(""),o.log(U.bold(` Rule: ${n.name}`)),n.description&&o.log(U.dim(` Description: ${n.description}`)),o.log(U.dim(` ID: ${n.id}`));let e=n.conditionGroup.map(r=>r.conditions.map(s=>{let a=s.neg?"NOT ":"",l=s.key?`[${s.key}] `:"",c=Array.isArray(s.value)?s.value.join(", "):s.value||"";return`${a}${s.type} ${l}${s.op} "${c}"`}).join(" AND "));o.log(U.dim(` Conditions: ${e.join(" OR ")}`));let t=n.action.mitigate.action,i=[];n.action.mitigate.rateLimit&&i.push(`${n.action.mitigate.rateLimit.requests} req/${n.action.mitigate.rateLimit.window}`),n.action.mitigate.redirect&&i.push(`\u2192 ${n.action.mitigate.redirect.location}`),n.action.mitigate.actionDuration&&i.push(`duration: ${n.action.mitigate.actionDuration}`),o.log(U.dim(` Action: ${t}${i.length?` (${i.join(", ")})`:""}`)),o.log(U.dim(` Active: ${n.active?"\u2705":"\u274C"}`))}function jt(n){o.log(""),o.log(U.bold(` IP Rule: ${n.ip}`)),o.log(U.dim(` Hostname: ${n.hostname}`)),n.notes&&o.log(U.dim(` Notes: ${n.notes}`)),o.log(U.dim(` Action: ${n.action}`))}var to=async n=>{try{if(n.debug&&(o.level=Gi.debug),o.debug("Add command arguments:",n),(n.type||"rule")==="ip"){let t=n.interactive?await Qi():Yi(n),i=tt.safeParse(t);if(i.success||(o.error(U.red("Rule validation failed:")),i.error.errors.forEach(a=>{let l=a.path.join(".");o.error(U.red(` - ${l||"ip"}: ${a.message}`))}),process.exit(1)),n.dryRun){o.info(U.cyan(`
|
|
17
|
+
`+D.formatErrors(r,a,l),r,a,l)}validateRuleNames(e){let t=new Set;for(let i of e.rules){if(t.has(i.name))throw new D(`Duplicate rule name found: "${i.name}"`,null);t.add(i.name)}}validateRuleConditionGroup(e){for(let t of e.rules)if(t.conditionGroup)for(let i of t.conditionGroup){if(!i.conditions||i.conditions.length===0)throw new D(`Rule "${t.name}" has an empty condition group`,null);for(let r of i.conditions){if(!r.type||!r.op)throw new D(`Rule "${t.name}" has an invalid condition`,null);let s=r.type==="header"||r.type==="cookie",a=r.op==="ex"||r.op==="nex";if(s){if(!r.key||typeof r.key!="string")throw new D(`Rule "${t.name}" has an invalid condition`,null);if(!a&&(r.value===void 0||r.value===null))throw new D(`Rule "${t.name}" has an invalid condition`,null)}else if(r.value===void 0||r.value===null)throw new D(`Rule "${t.name}" has an invalid condition`,null);let l=Array.isArray(r.value)?r.value:r.value!==void 0?[r.value]:[];if(Array.isArray(r.value)&&l.length===0)throw new D(`Rule "${t.name}" has an invalid value in condition`,null);if(r.type==="ip_address"){for(let c of l)if(typeof c!="string"||!this.isValidIP(c))throw new D(`Rule "${t.name}" has an invalid IP address in condition`,null)}if(r.type==="geo_as_number")for(let c of l){let d=typeof c=="number"?c:parseInt(String(c),10);if(!this.isValidASN(String(d)))throw new D(`Rule "${t.name}" has an invalid ASN in condition`,null)}if(r.type==="path"&&(r.op==="eq"||r.op==="pre")){for(let c of l)if(typeof c!="string"||!this.isValidPath(c))throw new D(`Rule "${t.name}" has an invalid path in condition`,null)}}}else throw new D("Either conditionGroup or type+values must be provided",null)}validateRuleAction(e){for(let t of e.rules)if(typeof t.action=="object"&&"mitigate"in t.action){let i=t.action.mitigate;if(i?.rateLimit){if(i.rateLimit.requests<=0)throw new D("Invalid rate limit configuration: requests must be positive",null);if(!i.rateLimit.window.match(/^\d+[smhd]$/))throw new D("Invalid rate limit configuration: invalid window format",null);if(parseInt(i.rateLimit.window)<=0)throw new D("Invalid rate limit configuration: window duration must be positive",null)}if(i?.actionDuration&&!i.actionDuration.match(/^\d+[smhd]$|^permanent$/))throw new D("Invalid action duration format: "+i.actionDuration,null);if(i?.redirect&&!i.redirect.location)throw new D("Invalid redirect configuration: location is required",null)}}isValidIP(e){let t=e.split("/"),i=t[0]??"",r=t[1],s=d=>{let u=d.split(".");return u.length!==4?!1:u.every(p=>{if(!/^\d{1,3}$/.test(p))return!1;let g=parseInt(p,10);return g>=0&&g<=255})},a=d=>{let u=d.split(":");return u.length!==8?!1:u.every(p=>/^[0-9a-fA-F]{1,4}$/.test(p))},l=s(i),c=a(i);if(!l&&!c)return!1;if(r!==void 0){let d=parseInt(r,10);if(Number.isNaN(d)||l&&(d<0||d>32)||c&&(d<0||d>128))return!1}return!0}isValidASN(e){let t=parseInt(e,10);return!isNaN(t)&&t>=1&&t<=4294967295}isValidPath(e){return e.startsWith("/")&&!e.includes(" ")}};import{existsSync as Ni}from"fs";import{dirname as Fi,resolve as Nt}from"path";var it=[".doorman.json","vercel-firewall.config.json"],Vi=it[0],Re=class{static async findConfig(e){let{findUp:t}=await import("find-up");for(let i of it){let r=await t(i,{cwd:e||process.cwd()});if(r)return r}}static async findProjectRoot(e){let t=await this.findConfig(e);return t?Fi(t):null}static configExists(e){return Ni(Nt(e))}static getDefaultConfigPath(){return Nt(process.cwd(),Vi)}static getSupportedFileNames(){return it}};async function B(n,e="required"){let t=qi(e),i=n||await Re.findConfig();if(!i||!Ft(i)){if(t==="optional")return o.debug("No config file found, returning empty config"),{};let s=Re.getDefaultConfigPath();throw new Error(`No config file found. Run \`doorman init\` to create one at ${s}, or use --config to specify a custom path.`)}let r;try{let s=ji(i,"utf8");r=JSON.parse(s)}catch(s){throw s instanceof SyntaxError?new Error(`Invalid JSON in config file (${i}): ${s.message}`):s}if(t!=="raw")try{we.getInstance().validateConfig(r)}catch(s){if(t==="lenient")o.warn("Config validation failed:",s);else throw s}return r}async function N(n,e,t={validate:!0,throwOnError:!0}){let i=e||await Re.findConfig()||Re.getDefaultConfigPath();if(t.validate)try{we.getInstance().validateConfig(n)}catch(s){if(o.error("Config validation failed:",s),t.throwOnError)throw s}let r=Wi(i);Ft(r)||Bi(r,{recursive:!0}),zi(i,JSON.stringify(n,null,2)),o.debug(`Config saved to ${i}`)}function qi(n){if(typeof n=="string")return n;let{validate:e=!0,throwOnError:t=!0}=n;return e?t?"required":"lenient":"raw"}T();import Vt from"chalk";import{ZodError as Mi}from"zod";function G(n,e){n instanceof SyntaxError?o.log(ge.wrapErrorBlock(["Invalid JSON format in config file:",` ${n.message}`])):n instanceof Mi?(o.error(Vt.red("Schema validation failed:")),n.errors.forEach(t=>{let i=t.path.join(".");o.error(Vt.red(` - ${i}: ${t.message}`))})):n instanceof Error&&n.name==="ValidationError"?o.error(n):n instanceof Error&&n.message.includes("Forbidden")?o.log(ge.wrapErrorBlock([`Error ${e}:`," Access denied. Check that your token has the correct scope."," If using a team, ensure the token is scoped to that team."])):o.error(ge.wrapErrorBlock([`Error ${e}:`,` ${n instanceof Error?n.message:String(n)}`])),process.exit(1)}var Hi="add [type]",Zi="Add a new firewall rule to your configuration",Ki={type:{type:"string",description:'Rule type: "rule" (default) or "ip"',default:"rule",choices:["rule","ip"]},interactive:{alias:"i",type:"boolean",description:"Guided prompts for rule creation",default:!1},name:{alias:"n",type:"string",description:"Rule name"},description:{type:"string",description:"Rule description"},field:{type:"string",description:"Condition field type (path, method, user_agent, etc.)"},op:{type:"string",description:"Operator (eq, pre, suf, sub, inc, re, ex, nex)"},value:{type:"string",description:"Match value (string or comma-separated for arrays)"},key:{type:"string",description:"Header/query/cookie key (required for header, cookie types)"},neg:{type:"boolean",description:"Negate the condition",default:!1},action:{alias:"a",type:"string",description:"Action type (deny, challenge, rate_limit, redirect, log, bypass)",default:"deny"},active:{type:"boolean",description:"Enable rule immediately",default:!0},requests:{type:"number",description:"Rate limit: max requests"},window:{type:"string",description:'Rate limit: time window (e.g., "60s", "5m")'},duration:{type:"string",description:'Action duration (e.g., "1h", "permanent")'},location:{type:"string",description:"Redirect URL"},permanent:{type:"boolean",description:"Redirect: use 301 instead of 302"},ip:{type:"string",description:"IP address or CIDR for IP blocking rules"},hostname:{type:"string",description:"Hostname for IP blocking rules",default:"*"},notes:{type:"string",description:"Notes for IP blocking rules"},config:{alias:"c",type:"string",description:"Config file path"},dryRun:{alias:"d",type:"boolean",description:"Show what would be added without writing",default:!1},debug:{type:"boolean",description:"Enable debug logging",default:!1}},ot=["host","path","method","header","query","cookie","target_path","ip_address","region","protocol","scheme","environment","user_agent","geo_continent","geo_country","geo_country_region","geo_city","geo_as_number","ja4_digest","ja3_digest","rate_limit_api_id"],nt=["eq","pre","suf","inc","sub","re","ex","nex"],rt=["log","deny","challenge","bypass","rate_limit","redirect"];function st(n){let e=n.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"");if(!e)throw new Error("Cannot generate rule ID: name must contain at least one alphanumeric character");return"rule_"+e}function zt(n,e){return e==="inc"?n.split(",").map(t=>t.trim()):n}async function Ji(){let n=await R("Rule name:",{type:"text"});if(!n.trim())throw new Error("Rule name cannot be empty");let e=await R("Description (optional):",{type:"text"}),t=[],i=!0;for(;i;){let u=[],p=!0;for(;p;){let g=await R("Condition type:",{type:"select",options:ot,initial:"path"}),C=await R("Operator:",{type:"select",options:nt,initial:"eq"}),m;(g==="header"||g==="cookie"||g==="query")&&(m=await R(`${g} key:`,{type:"text"}));let w;if(C!=="ex"&&C!=="nex"){let b=await R("Value:",{type:"text"});w=zt(b,C)}let S=C!=="ex"&&C!=="nex"?await R("Negate this condition?",{type:"confirm",initial:!1}):!1,x={type:g,op:C,...w!==void 0&&{value:w},...m&&{key:m},...S&&{neg:S}};u.push(x),p=await R("Add another condition to this group? (AND logic)",{type:"confirm",initial:!1})}t.push({conditions:u}),i=await R("Add another condition group? (OR logic)",{type:"confirm",initial:!1})}let r=await R("Action:",{type:"select",options:rt,initial:"deny"}),s;if(r==="rate_limit"){let u=parseInt(await R("Max requests:",{type:"text",initial:"100"}),10),p=await R('Time window (e.g., "60s", "1m", "1h"):',{type:"text",initial:"60s"});s={requests:u,window:p}}let a;if(r==="redirect"){let u=await R("Redirect URL:",{type:"text"}),p=await R("Permanent redirect (301)?",{type:"confirm",initial:!1});a={location:u,...p&&{permanent:p}}}let l;(r==="deny"||r==="challenge")&&await R("Set action duration? (default: permanent)",{type:"confirm",initial:!1})&&(l=await R('Duration (e.g., "1h", "1d", "permanent"):',{type:"text",initial:"permanent"}));let c=await R("Enable rule immediately?",{type:"confirm",initial:!0});return{id:st(n),name:n.trim(),...e.trim()&&{description:e.trim()},conditionGroup:t,action:{mitigate:{action:r,...s&&{rateLimit:s},...a&&{redirect:a},...l&&{actionDuration:l}}},active:c}}function Xi(n){if(!n.name)throw new Error("--name is required in inline mode. Use --interactive for guided prompts.");if(!n.field)throw new Error("--field is required in inline mode (e.g., --field path)");if(!n.op)throw new Error("--op is required in inline mode (e.g., --op eq)");let e=n.op;if(!nt.includes(e))throw new Error(`Invalid operator "${n.op}". Valid operators: ${nt.join(", ")}`);let t=n.field;if(!ot.includes(t))throw new Error(`Invalid field type "${n.field}". Valid types: ${ot.join(", ")}`);let i=n.action||"deny";if(!rt.includes(i))throw new Error(`Invalid action "${n.action}". Valid actions: ${rt.join(", ")}`);if(e!=="ex"&&e!=="nex"&&!n.value)throw new Error("--value is required for the given operator");if((t==="header"||t==="cookie"||t==="query")&&!n.key)throw new Error(`--key is required for "${t}" condition type`);if(i==="rate_limit"&&(!n.requests||!n.window))throw new Error("--requests and --window are required when action is rate_limit");if(i==="redirect"&&!n.location)throw new Error("--location is required when action is redirect");let r=n.value?zt(n.value,e):void 0,s={type:t,op:e,...r!==void 0&&{value:r},...n.key&&{key:n.key},...n.neg&&{neg:!0}};return{id:st(n.name),name:n.name.trim(),...n.description&&{description:n.description},conditionGroup:[{conditions:[s]}],action:{mitigate:{action:i,...i==="rate_limit"&&n.requests&&n.window&&{rateLimit:{requests:n.requests,window:n.window}},...i==="redirect"&&n.location&&{redirect:{location:n.location,...n.permanent&&{permanent:!0}}},...n.duration&&{actionDuration:n.duration}}},active:n.active!==!1}}async function Qi(){let n=await R("IP address or CIDR (e.g., 192.168.1.100/32):",{type:"text"});if(!n.trim())throw new Error("IP address cannot be empty");let e=await R("Hostname (use * for all):",{type:"text",initial:"*"}),t=await R("Notes (optional):",{type:"text"});return{ip:n.trim(),hostname:e.trim()||"*",...t.trim()&&{notes:t.trim()},action:"deny"}}function Yi(n){if(!n.ip)throw new Error("--ip is required for IP blocking rules");return{ip:n.ip.trim(),hostname:n.hostname||"*",...n.notes&&{notes:n.notes},action:"deny"}}function eo(n,e){if(n.rules.map(r=>r.name.toLowerCase()).includes(e.name.toLowerCase()))return`A rule named "${e.name}" already exists`;let i=n.rules.map(r=>r.id).filter(Boolean);return e.id&&i.includes(e.id)?`A rule with ID "${e.id}" already exists`:null}function Bt(n){o.log(""),o.log(U.bold(` Rule: ${n.name}`)),n.description&&o.log(U.dim(` Description: ${n.description}`)),o.log(U.dim(` ID: ${n.id}`));let e=n.conditionGroup.map(r=>r.conditions.map(s=>{let a=s.neg?"NOT ":"",l=s.key?`[${s.key}] `:"",c=Array.isArray(s.value)?s.value.join(", "):s.value||"";return`${a}${s.type} ${l}${s.op} "${c}"`}).join(" AND "));o.log(U.dim(` Conditions: ${e.join(" OR ")}`));let t=n.action.mitigate.action,i=[];n.action.mitigate.rateLimit&&i.push(`${n.action.mitigate.rateLimit.requests} req/${n.action.mitigate.rateLimit.window}`),n.action.mitigate.redirect&&i.push(`\u2192 ${n.action.mitigate.redirect.location}`),n.action.mitigate.actionDuration&&i.push(`duration: ${n.action.mitigate.actionDuration}`),o.log(U.dim(` Action: ${t}${i.length?` (${i.join(", ")})`:""}`)),o.log(U.dim(` Active: ${n.active?"\u2705":"\u274C"}`))}function jt(n){o.log(""),o.log(U.bold(` IP Rule: ${n.ip}`)),o.log(U.dim(` Hostname: ${n.hostname}`)),n.notes&&o.log(U.dim(` Notes: ${n.notes}`)),o.log(U.dim(` Action: ${n.action}`))}var to=async n=>{try{if(n.debug&&(o.level=Gi.debug),o.debug("Add command arguments:",n),(n.type||"rule")==="ip"){let t=n.interactive?await Qi():Yi(n),i=tt.safeParse(t);if(i.success||(o.error(U.red("Rule validation failed:")),i.error.errors.forEach(a=>{let l=a.path.join(".");o.error(U.red(` - ${l||"ip"}: ${a.message}`))}),process.exit(1)),n.dryRun){o.info(U.cyan(`
|
|
18
18
|
Dry run - The following IP rule would be added:`)),jt(t),o.log(""),o.log(U.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await B(n.config,"raw"),s={...r,ips:[...r.ips||[],t]};await N(s,n.config),o.success(U.green(`\u2714 IP rule "${t.ip}" added to configuration`)),jt(t)}else{let t=n.interactive?await Ji():Xi(n),i=et.safeParse(t);if(i.success||(o.error(U.red("Rule validation failed:")),i.error.errors.forEach(c=>{let d=c.path.join(".")||"rule";o.error(U.red(` - ${d}: ${c.message}`))}),process.exit(1)),n.dryRun){o.info(U.cyan(`
|
|
19
|
-
Dry run - The following rule would be added:`)),Bt(t),o.log(""),o.log(U.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await B(n.config,"raw"),s=r.rules||[],a=eo({...r,rules:s},t);if(a&&(o.warn(U.yellow(`\u26A0\uFE0F ${a}`)),!await R("Proceed anyway?",{type:"confirm",initial:!1}))){o.info("Cancelled.");return}let l={...r,rules:[...s,t]};await N(l,n.config),o.success(U.green(`\u2714 Rule "${t.name}" added to configuration`)),Bt(t)}o.log(""),o.log(U.dim(`Run ${U.cyan("
|
|
19
|
+
Dry run - The following rule would be added:`)),Bt(t),o.log(""),o.log(U.dim(JSON.stringify(t,null,2)));return}o.start("Loading configuration...");let r=await B(n.config,"raw"),s=r.rules||[],a=eo({...r,rules:s},t);if(a&&(o.warn(U.yellow(`\u26A0\uFE0F ${a}`)),!await R("Proceed anyway?",{type:"confirm",initial:!1}))){o.info("Cancelled.");return}let l={...r,rules:[...s,t]};await N(l,n.config),o.success(U.green(`\u2714 Rule "${t.name}" added to configuration`)),Bt(t)}o.log(""),o.log(U.dim(`Run ${U.cyan("doorman sync")} to deploy this rule.`))}catch(e){G(e,"adding rule")}};var ft={};z(ft,{builder:()=>bo,command:()=>Co,desc:()=>Io,handler:()=>vo});T();ie();import J from"chalk";import{existsSync as Ge,mkdirSync as ho,readdirSync as yo,statSync as wo}from"fs";import{join as gt}from"path";import{LogLevels as Ro}from"consola";T();import{LogLevels as mo}from"consola";T();import K from"chalk";import{LogLevels as io}from"consola";function Y(n,e){if(n===e)return!0;if(typeof n!="object"||n===null||typeof e!="object"||e===null||Array.isArray(n)!==Array.isArray(e))return!1;let t=Object.keys(n),i=Object.keys(e);if(t.length!==i.length)return!1;for(let r of t)if(!i.includes(r)||!Y(n[r],e[r]))return!1;return!0}function _(n){let{id:e,...t}=n;return t}T();async function Z(n,e={}){let{maxAttempts:t=3,delayMs:i=1e3,backoff:r=!0}=e,s;for(let a=1;a<=t;a++)try{return await n()}catch(l){if(s=l instanceof Error?l:new Error(String(l)),a===t)break;let c=r?i*a:i;o.debug(`Retry attempt ${a}/${t} failed. Retrying in ${c}ms...`),await new Promise(d=>setTimeout(d,c))}throw new Error(`Operation failed after ${t} attempts. Last error: ${s?.message}`)}function Wt(n){if(!n||n.trim().length===0)return"";let e=n.replace(/([a-z0-9])([A-Z])/g,"$1_$2").replace(/([A-Z])([A-Z][a-z])/g,"$1_$2"),t=["IP","HTTP","XML","API"];return t.forEach(i=>{let r=new RegExp(`\\b${i}\\b`,"g");e=e.replace(r,i.toLowerCase())}),e=e.replace(/[A-Z]{2,}/g,i=>t.includes(i)?i.toLowerCase():i.split("").join("_")).toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_+|_+$/g,"").replace(/_+/g,"_"),t.forEach(i=>{let r=i.toLowerCase();e=e.replace(new RegExp(`([a-z])${r}`,"g"),`$1_${r}`)}),e}var je=class{constructor(e){this.client=e}client;async getChanges(e){try{let t=ue.safeParse(e);if(!t.success)throw new Error(`Invalid firewall configuration: ${t.error.message}`);o.debug("Fetching existing firewall configuration");let i=await this.client.fetchFirewallConfig();o.debug(`Fetched ${i.rules.length} custom rules and ${i.ips.length} IP blocking rules`);let r=e.rules,{toAdd:s,toUpdate:a,toDelete:l}=this.diffRules(r,i.rules);o.debug("Pre Filter IP Rules:",{ips:e.ips,activeConfig:i.ips});let{ipsToAdd:c,ipsToUpdate:d,ipsToDelete:u}=this.diffIPRules(e.ips||[],i.ips);return{version:i.version,toAdd:s,toUpdate:a,toDelete:l,ipsToAdd:c,ipsToUpdate:d,ipsToDelete:u}}catch(t){throw o.error("Error fetching existing firewall configuration:",t),t instanceof Error&&t.message.includes("Invalid firewall configuration")?t:new Error("Failed to fetch existing firewall configuration. Please check your network connection and try again.")}}async syncRules(e,t={}){let{dryRun:i=!1,retryAttempts:r=3,debug:s=!1}=t;s&&(o.level=io.debug);try{let{toAdd:a,toUpdate:l,toDelete:c,ipsToAdd:d,ipsToUpdate:u,ipsToDelete:p}=await this.getChanges(e);if(i)return o.info("Dry run mode. The following changes would be made:"),o.info(`Custom Rules - Add: ${a.length}, Update: ${l.length}, Delete: ${c.length}`),o.info(`IP Rules - Add: ${d.length}, Update: ${u.length}, Delete: ${p.length}`),{addedRules:[],updatedRules:[],deletedRules:[],addedIPRules:[],updatedIPRules:[],deletedIPRules:[],rulesToUpdateLocally:[]};let g=[],C=[],m=[],w=[],S=[],x=[],b=[];for(let f of c)o.debug(`Deleting custom rule: ${f.id}`),await Z(()=>this.client.deleteFirewallRule(f),{maxAttempts:r}),m.push(f),o.debug(`Custom rule deleted: ${f.id}`);for(let f of p)o.debug(`Deleting IP blocking rule: ${f.id}`),await Z(()=>this.client.deleteIPBlockingRule(f),{maxAttempts:r}),x.push(f),o.debug(`IP blocking rule deleted: ${f.id}`);for(let f of a){let $=`rule_${Wt(f.name)}`;o.debug(`Adding new custom rule: ${f.name}`);let h=await Z(()=>this.client.createFirewallRule(f),{maxAttempts:r});g.push(h),o.debug(`New custom rule added: ${h.id}`),f.id!==$&&(b.push({oldId:f.id??"",newId:$,name:f.name}),o.debug(`Rule ID needs update locally: ${f.id} -> ${$}`))}for(let f of d){o.debug(`Adding new IP blocking rule: ${f.ip}`);let $=await Z(()=>this.client.createIPBlockingRule(f),{maxAttempts:r});w.push($),o.debug(`New IP blocking rule added: (hostname): ${$.hostname} (ip): ${$.ip}`)}for(let f of l){o.debug(`Updating custom rule: ${f.id}`);let $=await Z(()=>this.client.updateFirewallRule(f),{maxAttempts:r});C.push($),o.debug(`Custom rule updated: ${$.id}`)}for(let f of u){o.debug(`Updating IP blocking rule: ${f.id}`);let $=await Z(()=>this.client.updateIPBlockingRule(f),{maxAttempts:r});S.push($),o.debug(`IP blocking rule updated: ${$.id}`)}return o.debug(`${K.underline("Custom Rules:")} ${K.green("Added:")} ${K.green(g.length)}, ${K.cyan("Updated:")} ${K.cyan(C.length)}, ${K.red("Deleted:")} ${K.red(m.length)}`),o.debug(`${K.underline("IP Rules:")} ${K.green("Added:")} ${K.green(w.length)}, ${K.cyan("Updated:")} ${K.cyan(S.length)}, ${K.red("Deleted:")} ${K.red(x.length)}`),{addedRules:g,updatedRules:C,deletedRules:m,addedIPRules:w,updatedIPRules:S,deletedIPRules:x,rulesToUpdateLocally:b}}catch(a){throw o.error("Error during sync:",a),new Error("Failed to synchronize firewall rules. Please check the error logs and try again.")}}diffRules(e,t){let i=[],r=[],s=[...t];for(let a of e){let l=t.find(c=>c.id===a.id);if(!l)i.push(a);else{Y(_(a),_(l))||r.push({...a,id:l.id});let c=s.findIndex(d=>d.id===l.id);c!==-1&&s.splice(c,1)}}return{toAdd:i,toUpdate:r,toDelete:s}}diffIPRules(e,t){let i=[],r=[],s=[...t];for(let a of e){let l=t.find(c=>c.id===a.id);if(!l&&!a.id&&(l=t.find(c=>c.ip===a.ip&&c.hostname===a.hostname&&c.action===a.action)),!l)o.debug("Rule not found in existing rules:",{configRule:a}),i.push(a);else{Y(_(a),_(l))||r.push({...l,...a});let c=s.findIndex(d=>d.id===l.id);c!==-1&&s.splice(c,1)}}return{ipsToAdd:i,ipsToUpdate:r,ipsToDelete:s}}async validateAndUpdateConfig(e,t,i={}){if(i.dryRun)return o.info(K.cyan("Dry run: Config metadata would be updated after successful sync")),e;await new Promise(h=>setTimeout(h,2e3));let r=await Z(()=>this.client.fetchFirewallConfig(),{maxAttempts:5,delayMs:1500,backoff:!0}),s=r.rules,a=r.ips,l=new Set(t.addedRules.map(h=>h.id)),c=new Set(t.updatedRules.map(h=>h.id)),d=new Set(t.deletedRules.map(h=>h.id)),u=new Set(t.addedIPRules.map(h=>h.id).filter(Boolean)),p=new Set(t.updatedIPRules.map(h=>h.id).filter(Boolean)),g=new Set(t.deletedIPRules.map(h=>h.id).filter(Boolean)),C=new Map(t.addedRules.map(h=>[h.name,h])),m=t.addedIPRules.map(h=>_(h)),w=s.filter(h=>{if(l.has(h.id)||c.has(h.id))return!1;let v=C.get(h.name);if(v&&Y(_(h),_(v)))return!1;if(d.has(h.id))return!0;let Q=e.rules.find(de=>de.id===h.id);return Q?!Y(_(h),_(Q)):!e.rules.find(Je=>Y(_(h),_(Je)))}),S=a.filter(h=>u.has(h.id)||p.has(h.id)||m.some(de=>Y(_(h),de))?!1:!!(g.has(h.id)||!e.ips?.find(de=>Y(_(h),_(de))))),x=[];if(w.length>0){let h=w.map(v=>` - ${v.name} (${v.id})`).join(`
|
|
20
20
|
`);x.push(`The following custom rules have unexpected state:
|
|
21
21
|
`+h)}if(S.length>0){let h=S.map(v=>` - ${v.ip} (${v.id})`).join(`
|
|
22
22
|
`);x.push(`The following IP blocking rules have unexpected state:
|
|
@@ -47,7 +47,7 @@ ${v}`)}),f.result}),{toAdd:s,toUpdate:a,toDelete:l}=this.diffRules(r,i.rules),c=
|
|
|
47
47
|
`),o.info(" 1. Vercel Firewall"),o.info(` 2. Cloudflare WAF
|
|
48
48
|
`);let n=await R("Select provider (1 or 2):",{type:"text"}),e=String(n).toLowerCase();return e==="1"||e==="vercel"?"vercel":e==="2"||e==="cloudflare"?"cloudflare":(o.warn(`Invalid choice: ${n}, defaulting to Vercel`),"vercel")}async function H(n,e){try{n.debug&&(o.level=mo.debug);let t;if(n.optionalConfig)try{t=await B(n.config,"optional")}catch{t={}}else n.skipValidation?t=await B(n.config,"raw"):t=await B(n.config,"required");let i=await Yt({provider:n.provider,config:t,interactive:!n.ci,token:n.token,projectId:n.projectId,teamId:n.teamId,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId}),r,s,a="",l="",c="";if(i.name==="vercel"){let d=await lt({token:n.token,projectId:n.projectId||t.projectId,teamId:n.teamId||t.teamId});a=d.token,l=d.projectId,c=d.teamId,r=new ze(l,c,a),s=new je(r)}else r={},s={};await e({config:t,provider:i,client:r,service:s,token:a,projectId:l,teamId:c})}catch(t){G(t,n.errorContext)}}var Co="backup",Io="Backup or restore firewall configurations",bo={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},vo=async n=>{try{n.debug&&(o.level=Ro.debug);let e=n.output||"./backups";if(n.list){if(!Ge(e)){o.info(J.yellow("No backup directory found."));return}let t=yo(e).filter(i=>i.endsWith(".json")).map(i=>{let r=gt(e,i),s=wo(r);return{name:i,size:s.size,created:s.mtime}}).sort((i,r)=>r.created.getTime()-i.created.getTime());if(t.length===0){o.info(J.yellow("No backups found."));return}o.log(J.bold(`
|
|
49
49
|
\u{1F4E6} Available Backups:
|
|
50
|
-
`)),t.forEach(i=>{o.log(`${J.cyan(i.name)}`),o.log(` ${J.dim("Created:")} ${i.created.toLocaleString()}`),o.log(` ${J.dim("Size:")} ${(i.size/1024).toFixed(1)} KB`),o.log("")});return}if(n.restore){let t=n.restore.startsWith("/")?n.restore:gt(e,n.restore);Ge(t)||(o.error(`Backup file not found: ${t}`),process.exit(1));let i=await B(t,"raw"),r=n.config||".doorman.json";if(Ge(r)&&!await R(`Config file ${r} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(J.yellow("Restore cancelled."));return}await N(i,r),o.success(J.green(`\u2705 Restored configuration from ${t} to ${r}`));return}await H({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"creating backup"},async({client:t,projectId:i,teamId:r})=>{o.start("Fetching current remote configuration...");let s=await t.fetchFirewallConfig();Ge(e)||ho(e,{recursive:!0});let a=new Date,l=a.toISOString().split("T")[0]??"unknown-date",c=(a.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",u=`firewall-backup-${`${l}_${c}`}.json`,p=gt(e,u),g={...s,backup:{createdAt:new Date().toISOString(),source:"remote",projectId:i,teamId:r,originalVersion:s.version}};await N(g,p),o.success(J.green(`\u2705 Backup created: ${p}`)),o.log(""),o.log(J.bold("Backup Details:")),o.log(`${J.dim("Version:")} ${s.version}`),o.log(`${J.dim("Rules:")} ${s.rules.length} custom, ${s.ips.length} IP blocking`),o.log(`${J.dim("Created:")} ${new Date().toLocaleString()}`),o.log(""),o.log(J.dim("To restore this backup later, run:")),o.log(J.cyan(`
|
|
50
|
+
`)),t.forEach(i=>{o.log(`${J.cyan(i.name)}`),o.log(` ${J.dim("Created:")} ${i.created.toLocaleString()}`),o.log(` ${J.dim("Size:")} ${(i.size/1024).toFixed(1)} KB`),o.log("")});return}if(n.restore){let t=n.restore.startsWith("/")?n.restore:gt(e,n.restore);Ge(t)||(o.error(`Backup file not found: ${t}`),process.exit(1));let i=await B(t,"raw"),r=n.config||".doorman.json";if(Ge(r)&&!await R(`Config file ${r} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(J.yellow("Restore cancelled."));return}await N(i,r),o.success(J.green(`\u2705 Restored configuration from ${t} to ${r}`));return}await H({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"creating backup"},async({client:t,projectId:i,teamId:r})=>{o.start("Fetching current remote configuration...");let s=await t.fetchFirewallConfig();Ge(e)||ho(e,{recursive:!0});let a=new Date,l=a.toISOString().split("T")[0]??"unknown-date",c=(a.toISOString().split("T")[1]??"").split(".")[0]?.replace(/:/g,"-")??"unknown-time",u=`firewall-backup-${`${l}_${c}`}.json`,p=gt(e,u),g={...s,backup:{createdAt:new Date().toISOString(),source:"remote",projectId:i,teamId:r,originalVersion:s.version}};await N(g,p),o.success(J.green(`\u2705 Backup created: ${p}`)),o.log(""),o.log(J.bold("Backup Details:")),o.log(`${J.dim("Version:")} ${s.version}`),o.log(`${J.dim("Rules:")} ${s.rules.length} custom, ${s.ips.length} IP blocking`),o.log(`${J.dim("Created:")} ${new Date().toLocaleString()}`),o.log(""),o.log(J.dim("To restore this backup later, run:")),o.log(J.cyan(`doorman backup --restore ${u}`))})}catch(e){G(e,"managing backup")}};var Ct={};z(Ct,{builder:()=>To,command:()=>Ao,desc:()=>$o,handler:()=>Po});T();import fe from"chalk";T();import M from"chalk";import ri from"cli-table3";var mt=(n,e,t,i)=>{if(!e.length)throw new Error("minWidths array cannot be empty");if(i.some(c=>c>=e.length))throw new Error("flexColumns contains invalid column index");if(e.length!==t.length)throw new Error("minWidths and maxWidths arrays must have the same length");e.forEach((c,d)=>{let u=t[d];if(u!=null&&c>u)throw new Error(`Column ${d} has minimum width (${c}) greater than maximum width (${u})`)});let r=n||400,s=[...e],a=e.reduce((c,d)=>c+d,0),l=r-a-28;if(l>0&&i.length>0){let c=i.filter(d=>{let u=t[d],p=s[d];return u!==void 0&&p!==void 0&&(u===null||p<u)});for(;l>0&&c.length>0;){let d=Math.floor(l/c.length);if(d===0)break;let u=0;c=c.filter(p=>{let g=t[p],C=s[p];if(g===null)return s[p]!==void 0&&(s[p]+=d),u+=d,!0;{let m=(g??0)-(C??0);if(m<=0)return!1;let w=Math.min(d,m);return s[p]!==void 0&&(s[p]+=w),u+=w,w===d}}),l-=u}if(l>0){let d=i.findLast(u=>t[u]===null);d!==void 0&&d!==void 0&&s[d]!==void 0&&(s[d]+=l)}}return s};import se from"chalk";var ei=n=>{if(n==="permanent")return"permanent";let e=n.slice(0,-1),t=n.slice(-1);return`${e}${{s:"sec",m:"min",h:"hr",d:"day"}[t]||t}`},Eo=(n,e)=>e==="60s"?`${n}/min`:e==="3600s"?`${n}/hr`:e==="86400s"?`${n}/day`:`${n}/${ei(e)}`;function ti(n){if(!n.mitigate)return"unknown";let{mitigate:e}=n,t=[];switch(e.action){case"log":t.push(se.blue("log"));break;case"deny":t.push(se.red("deny"));break;case"challenge":t.push(se.yellow("challenge"));break;case"bypass":t.push(se.green("bypass"));break;case"rate_limit":t.push(se.yellow("rate-limit"));break;case"redirect":t.push(se.magenta("redirect"));break;default:t.push(e.action)}return e.rateLimit&&(t[0]=se.yellow("rate-limit"),t.push(se.yellow(Eo(e.rateLimit.requests,e.rateLimit.window)))),e.redirect&&(t[0]=se.magenta("redirect"),t.push(se.magenta(`\u2192 ${e.redirect.location}`)),e.redirect.permanent!==void 0&&t.push(se.gray(`(${e.redirect.permanent?"301":"302"})`))),e.actionDuration&&t.push(se.gray(`for ${ei(e.actionDuration)}`)),t.join(" ")}import He from"chalk";function ht(n){switch(n){case"unchanged":return He.bold.gray(" ");case"modified":return He.bold.yellow("\u25B4");case"new":return He.bold.green("+");case"deleted":return He.bold.red("-")}}import Ze from"chalk";function ii(n=[]){return n.map((e,t)=>{let i=e.conditions.map(r=>Ze.cyan(`${r.type}:${r.neg?Ze.red(`!${r.op}:`):`${r.op}:`}${Ze.white(r.value)}`)).join(`
|
|
51
51
|
`);return t>0?`${Ze.yellow("OR")}
|
|
52
52
|
${i}`:i}).join(`
|
|
53
53
|
`)}import oi from"chalk";function ni(n){return{id:n.id||"-",ip:n.ip,hostname:n.hostname,notes:n.notes||"-",status:n.changeStatus?oi.yellow(n.changeStatus):oi.red("deny")}}import Ke from"chalk";function yt(n){switch(n){default:case"unchanged":return Ke.white;case"modified":return Ke.yellow;case"new":return Ke.green;case"deleted":return Ke.red}}var te={unchanged:"unchanged",modified:"modified",new:"new",deleted:"deleted"},wt={minWidths:[10,28,20,5,12,35],maxWidths:[15,32,36,8,24,null],flexColumns:[0,1,2,4,5]},Rt={minWidths:[10,28,24,5,12],maxWidths:[20,32,26,null,16],flexColumns:[0,1,3]},si=(n,e)=>n==="ip"?mt(e,Rt.minWidths,Rt.maxWidths,Rt.flexColumns):mt(e,wt.minWidths,wt.maxWidths,wt.flexColumns);function me(n,{showStatus:e}){let t=[M.bold.gray("ID"),M.bold.gray("Name"),M.bold.gray("Conditions"),M.bold.gray(" "),M.bold.gray("Action"),M.bold.gray("Description")],i=["left","left","left","center","left","left"],r=process.stdout.columns||400,s=si("rules",r);e&&(t.unshift(M.bold.gray("Status")),i.unshift("center"),s.unshift(10));let a=new ri({head:t,colAligns:i,wrapOnWordBoundary:!0,wordWrap:!0,colWidths:s});n.forEach(l=>{let c="changeStatus"in l&&e,d=c?yt(l.changeStatus):M.white,u=[d(l.id?l.id:"-"),d(l.name),d(ii(l.conditionGroup)),l.active?M.bold.green("\u2713"):M.bold.red("\u2717"),d(ti(l.action)),d(l.description||"")];c&&u.unshift(d(ht(l.changeStatus))),a.push(u)}),o.log(a.toString())}function he(n,{showStatus:e}){let t=[M.bold.gray("ID"),M.bold.gray("IP"),M.bold.gray("Hostname"),M.bold.gray("Notes"),M.bold.gray("Action")],i=["left","left","left","left","center"],r=process.stdout.columns||400,s=si("ip",r);e&&(t.unshift(M.bold.gray("Status")),i.unshift("center"),s.unshift(10));let a=new ri({head:t,colAligns:i,wrapOnWordBoundary:!0,wordWrap:!0,colWidths:s});n.forEach(l=>{let c=l.changeStatus!==void 0&&e,d=c?yt(l.changeStatus):M.white,u=ni(l),p=[d(u.id),d(u.ip),d(u.hostname),d(u.notes),u.status];c&&p.unshift(d(ht(l.changeStatus))),a.push(p)}),o.log(a.toString())}var Ao="diff",$o="Show detailed differences between local and remote firewall configuration",To={config:{alias:"c",type:"string",description:"Path to firewall config file"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},Po=async n=>{await H({config:n.config,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"calculating diff"},async({config:e,service:t})=>{o.start("Calculating differences...");let{toAdd:i,toUpdate:r,toDelete:s,ipsToAdd:a,ipsToUpdate:l,ipsToDelete:c,version:d}=await t.getChanges(e),u=i.length>0||r.length>0||s.length>0,p=a.length>0||l.length>0||c.length>0,g=e.version!==d;if(!u&&!p&&!g){o.success(fe.green("No differences found. Local and remote configurations are in sync."));return}if(n.format==="json"){let m={version:{local:e.version,remote:d,changed:g},customRules:{toAdd:i.map(w=>({...w,status:"add"})),toUpdate:r.map(w=>({...w,status:"update"})),toDelete:s.map(w=>({...w,status:"delete"}))},ipRules:{toAdd:a.map(w=>({...w,status:"add"})),toUpdate:l.map(w=>({...w,status:"update"})),toDelete:c.map(w=>({...w,status:"delete"}))},summary:{hasChanges:u||p||g,customRuleChanges:i.length+r.length+s.length,ipRuleChanges:a.length+l.length+c.length}};o.log(JSON.stringify(m,null,2));return}o.log(fe.bold(`
|
|
@@ -120,7 +120,7 @@ Remote IP Blocking Rules to Download:
|
|
|
120
120
|
`))},Go=()=>{o.log(I.bold(`
|
|
121
121
|
\u{1F4DA} Helpful Resources:`)),o.log(""),o.log(I.cyan("\u{1F517} Find your Project ID:")),o.log(I.dim(" https://vercel.com/dashboard \u2192 Select your project \u2192 Settings \u2192 General")),o.log(""),o.log(I.cyan("\u{1F517} Find your Team ID:")),o.log(I.dim(" https://vercel.com/dashboard \u2192 Team Settings \u2192 General")),o.log(I.dim(" (Leave empty if using personal account)")),o.log(""),o.log(I.cyan("\u{1F517} Create API Token:")),o.log(I.dim(" https://vercel.com/account/tokens \u2192 Create Token")),o.log(I.dim(" Scopes needed: Read & Write for your project")),o.log(""),o.log(I.cyan("\u{1F517} Documentation:")),o.log(I.dim(" https://doorman.griffen.codes/getting-started")),o.log("")},Ho=async n=>{let e=n.projectId,t=n.teamId;return n.interactive&&(o.log(I.bold("\u{1F4CB} Project Configuration")),o.log(I.dim(`We need your Vercel project details to configure the firewall.
|
|
122
122
|
`)),e||(e=await R("Enter your Vercel Project ID:",{type:"text"})),t||await R("Are you using a Vercel team account?",{type:"confirm"})&&(t=await R("Enter your Vercel Team ID:",{type:"text"})),process.env.VERCEL_TOKEN?o.log(I.green("\u2705 VERCEL_TOKEN environment variable found")):(o.log(""),o.log(I.yellow("\u26A0\uFE0F VERCEL_TOKEN environment variable not found")),o.log(I.dim("You'll need to set this before running sync commands.")),await R("Would you like to see how to create an API token?",{type:"confirm"})&&(o.log(""),o.log(I.bold("\u{1F511} Creating a Vercel API Token:")),o.log("1. Go to https://vercel.com/account/tokens"),o.log('2. Click "Create Token"'),o.log('3. Give it a descriptive name (e.g., "Doorman Firewall")'),o.log("4. Set expiration as needed"),o.log("5. Copy the token and set it as an environment variable:"),o.log(""),o.log(I.cyan(' export VERCEL_TOKEN="your-token-here"')),o.log(I.dim(" # Add this to your ~/.bashrc, ~/.zshrc, or .env file")),o.log("")))),{projectId:e,teamId:t}},Zo=async n=>{try{let e=n.config||".doorman.json";if(n.interactive&&Mo(),jo(e)&&!n.force&&!await R(`Config file ${e} already exists. Do you want to overwrite it?`,{type:"confirm"})){o.info(I.yellow("Initialization cancelled."));return}let{projectId:t,teamId:i}=await Ho(n);n.interactive&&(o.log(""),o.log(I.bold("\u{1F3A8} Template Selection")),o.log(I.dim(`Choose a starting template for your firewall configuration:
|
|
123
|
-
`)),o.log(I.cyan("\u{1F4DD} empty")+I.dim(" - Minimal config, add your own rules")),o.log(I.cyan("\u{1F6E1}\uFE0F basic")+I.dim(" - Includes basic bot protection")),o.log(I.cyan("\u{1F512} security-focused")+I.dim(" - Comprehensive security rules")),o.log(""));let r=n.template;n.interactive&&!n.template&&(r=await R("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),o.start(`Creating ${r} configuration...`);let s=ve();switch(t&&(s.projectId=t),i&&(s.teamId=i),r){case"basic":s={...s,rules:ai};break;case"security-focused":s={...s,rules:li};break;default:break}await N(s,e),o.success(I.green(`\u2705 Created ${e}`)),o.log(""),o.log(I.bold("\u{1F4CB} Configuration Summary:")),o.log(`${I.dim("File:")} ${e}`),o.log(`${I.dim("Template:")} ${r}`),o.log(`${I.dim("Project ID:")} ${t||I.yellow("Not set - add manually")}`),o.log(`${I.dim("Team ID:")} ${i||I.dim("None (personal account)")}`),o.log(`${I.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),o.log(""),o.log(I.bold("\u{1F680} Next Steps:")),t||o.log(`1. ${I.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||o.log(`${t?"1":"2"}. ${I.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;o.log(`${a}. ${I.dim("Review and enable rules in")} ${e}`),o.log(`${a+1}. ${I.dim("Run")} ${I.cyan("
|
|
123
|
+
`)),o.log(I.cyan("\u{1F4DD} empty")+I.dim(" - Minimal config, add your own rules")),o.log(I.cyan("\u{1F6E1}\uFE0F basic")+I.dim(" - Includes basic bot protection")),o.log(I.cyan("\u{1F512} security-focused")+I.dim(" - Comprehensive security rules")),o.log(""));let r=n.template;n.interactive&&!n.template&&(r=await R("Select a template:",{type:"select",options:["empty","basic","security-focused"],initial:"empty"})),o.start(`Creating ${r} configuration...`);let s=ve();switch(t&&(s.projectId=t),i&&(s.teamId=i),r){case"basic":s={...s,rules:ai};break;case"security-focused":s={...s,rules:li};break;default:break}await N(s,e),o.success(I.green(`\u2705 Created ${e}`)),o.log(""),o.log(I.bold("\u{1F4CB} Configuration Summary:")),o.log(`${I.dim("File:")} ${e}`),o.log(`${I.dim("Template:")} ${r}`),o.log(`${I.dim("Project ID:")} ${t||I.yellow("Not set - add manually")}`),o.log(`${I.dim("Team ID:")} ${i||I.dim("None (personal account)")}`),o.log(`${I.dim("Rules:")} ${s.rules.length} (all disabled for safety)`),o.log(""),o.log(I.bold("\u{1F680} Next Steps:")),t||o.log(`1. ${I.yellow("Add your projectId")} to ${e}`),process.env.VERCEL_TOKEN||o.log(`${t?"1":"2"}. ${I.yellow("Set VERCEL_TOKEN")} environment variable`);let a=t?process.env.VERCEL_TOKEN?1:2:3;o.log(`${a}. ${I.dim("Review and enable rules in")} ${e}`),o.log(`${a+1}. ${I.dim("Run")} ${I.cyan("doorman validate")} ${I.dim("to check your config")}`),o.log(`${a+2}. ${I.dim("Run")} ${I.cyan("doorman status")} ${I.dim("to see sync status")}`),o.log(`${a+3}. ${I.dim("Run")} ${I.cyan("doorman sync")} ${I.dim("to deploy your rules")}`),r!=="empty"&&(o.log(""),o.log(I.yellow("\u26A0\uFE0F All template rules are disabled by default for safety.")),o.log(I.dim(" Review each rule and set active: true when ready."))),n.interactive&&(!t||!process.env.VERCEL_TOKEN)&&Go()}catch(e){G(e,"initializing configuration")}};var Et={};z(Et,{builder:()=>Qo,command:()=>Jo,desc:()=>Xo,handler:()=>Yo});T();import pe from"chalk";import{z as Ko}from"zod";var Jo="list [configVersion]",Xo="List Vercel Firewall rules, optionally for a specific configuration version",Qo={configVersion:{type:"number",description:"Specific configuration version to fetch (defaults to latest)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},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}},Yo=async n=>{await H({provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,optionalConfig:!0,skipValidation:!0,errorContext:"listing firewall rules"},async({client:e,token:t,projectId:i,teamId:r})=>{if(n.configVersion!==void 0)try{Be.parse(n.configVersion)}catch(u){throw u instanceof Ko.ZodError&&(o.error("Invalid configuration version number. Version must be a positive integer."),process.exit(1)),u}o.start(`Fetching firewall configuration${n.configVersion?` version ${n.configVersion}`:""} ...`),o.verbose(`Token: ${t} projectId: ${i} teamId: ${r}`);let s=await e.fetchFirewallConfig(n.configVersion),a=s.rules,l=s.ips,c=new Date(s.updatedAt),d=new Intl.DateTimeFormat("en-US",{dateStyle:"medium",timeStyle:"medium"}).format(c);if(o.info(`Found ${pe.cyan(a.length)} custom rules and ${pe.cyan(l.length)} IP blocking rules
|
|
124
124
|
`+pe.dim(`Version: ${pe.yellow(s.version)} \u2022 Last Updated: ${pe.yellow(d)}`)),n.format==="json")o.info(JSON.stringify({version:s.version,updatedAt:s.updatedAt,lastUpdated:d,rules:a,ips:l},null,2));else{if(a.length===0&&l.length===0){o.info(pe.yellow("No rules found"));return}a.length>0?(o.log(pe.bold.underline(`
|
|
125
125
|
Custom Rules:`),`
|
|
126
126
|
`),me(a,{showStatus:!1})):o.info(pe.cyan("No custom rules found")),l.length>0?(o.log(pe.bold.underline(`
|
|
@@ -132,11 +132,11 @@ Dry run \u2014 no changes made.`));return}if(!await di(r.length,n.force||!1,n.al
|
|
|
132
132
|
\u26A0\uFE0F Found ${r.length} rules named "${n.name}". All will be removed.`)),At(r)),r.length===0){o.error(L.red(`No rule found with exact name "${n.name}"`));let l=pi(n.name,i);l.length>0&&(o.log(L.yellow(`
|
|
133
133
|
Did you mean one of these?`)),l.forEach((c,d)=>{let u=c.id?` (${c.id})`:"";o.log(L.dim(` ${d+1}. ${c.name}${u}`))}),o.log(L.dim(`
|
|
134
134
|
Use --id for exact matching, or --interactive to select from a list.`))),process.exit(1)}}else if(n.id&&n.id.length>0){let l=new Set(n.id);r=i.filter(u=>u.id&&l.has(u.id));let c=new Set(r.map(u=>u.id)),d=n.id.filter(u=>!c.has(u));d.length>0&&o.warn(L.yellow(`No rules found for IDs: ${d.join(", ")}`)),r.length===0&&(o.error(L.red("No matching rules found for the given IDs.")),process.exit(1))}else if(n.disabled){if(r=i.filter(l=>!l.active),r.length===0){o.info("No disabled rules found.");return}}else n.all?r=[...i]:(o.error("Specify --name, --id, --disabled, --all, or --interactive to select rules to remove."),process.exit(1));if(r.length===0){o.info("No rules selected for removal.");return}if(r.length>=5&&!n.dryRun&&!n.force&&o.log(L.yellow(`
|
|
135
|
-
\u{1F4A1} Tip: Run ${L.cyan("
|
|
135
|
+
\u{1F4A1} Tip: Run ${L.cyan("doorman backup")} before bulk removal operations.`)),o.log(L.cyan(`
|
|
136
136
|
${n.dryRun?"Would remove":"Removing"} ${r.length} rule(s):`)),At(r),n.dryRun){o.log(L.dim(`
|
|
137
|
-
Dry run \u2014 no changes made.`));return}if(!await di(r.length,n.force||!1,n.all||!1)){o.info("Cancelled.");return}let s=new Set(r),a={...t,rules:i.filter(l=>!s.has(l))};a.rules.length===0&&o.warn(L.yellow("\u26A0\uFE0F Configuration will have no rules after removal.")),await N(a,n.config),o.success(L.green(`\u2714 Removed ${r.length} rule(s) from configuration`)),At(r)}o.log(""),o.log(L.dim(`Run ${L.cyan("
|
|
137
|
+
Dry run \u2014 no changes made.`));return}if(!await di(r.length,n.force||!1,n.all||!1)){o.info("Cancelled.");return}let s=new Set(r),a={...t,rules:i.filter(l=>!s.has(l))};a.rules.length===0&&o.warn(L.yellow("\u26A0\uFE0F Configuration will have no rules after removal.")),await N(a,n.config),o.success(L.green(`\u2714 Removed ${r.length} rule(s) from configuration`)),At(r)}o.log(""),o.log(L.dim(`Run ${L.cyan("doorman sync")} to deploy these changes.`))}catch(e){G(e,"removing rule")}};var Tt={};z(Tt,{builder:()=>cn,command:()=>an,desc:()=>ln,handler:()=>dn});T();import P from"chalk";var an="setup",ln="Show setup instructions and helpful links for Vercel Doorman",cn={},dn=async n=>{o.log(""),o.log(P.bold.cyan("\u{1F6AA} Vercel Doorman Setup Guide")),o.log(P.dim(`Everything you need to get started with firewall management
|
|
138
138
|
`)),o.log(P.bold("\u{1F4CB} Prerequisites")),o.log("\u2022 Vercel account with a deployed project"),o.log("\u2022 Project with Pro plan or higher (firewall feature requirement)"),o.log(`\u2022 Admin access to the project/team
|
|
139
|
-
`),o.log(P.bold("\u{1F50D} Finding Your Project Information")),o.log(""),o.log(P.cyan("1. Project ID:")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Select your project"),o.log(" \u2022 Navigate to Settings \u2192 General"),o.log(" \u2022 Copy the Project ID from the top of the page"),o.log(""),o.log(P.cyan("2. Team ID (if using team account):")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Click on your team name in the top-left"),o.log(" \u2022 Go to Team Settings \u2192 General"),o.log(" \u2022 Copy the Team ID"),o.log(" \u2022 Leave empty if using personal account"),o.log(""),o.log(P.bold("\u{1F511} Creating API Token")),o.log(""),o.log(P.cyan("Steps:")),o.log(" 1. Visit https://vercel.com/account/tokens"),o.log(' 2. Click "Create Token"'),o.log(' 3. Name: "Doorman Firewall Management"'),o.log(" 4. Expiration: Set as needed (90 days recommended)"),o.log(" 5. Scope: Select your project or team"),o.log(" 6. Copy the generated token"),o.log(""),o.log(P.cyan("Required Permissions:")),o.log(" \u2022 Read access to project settings"),o.log(" \u2022 Write access to firewall configuration"),o.log(""),o.log(P.bold("\u{1F30D} Environment Setup")),o.log(""),o.log(P.cyan("Set your API token as environment variable:")),o.log(""),o.log(P.green("# For current session:")),o.log('export VERCEL_TOKEN="your-token-here"'),o.log(""),o.log(P.green("# For permanent setup (add to ~/.bashrc or ~/.zshrc):")),o.log(`echo 'export VERCEL_TOKEN="your-token-here"' >> ~/.bashrc`),o.log(""),o.log(P.green("# Or create a .env file in your project:")),o.log('echo "VERCEL_TOKEN=your-token-here" > .env'),o.log(""),o.log(P.bold("\u{1F680} Quick Start Commands")),o.log(""),o.log(P.cyan("1. Initialize configuration:")),o.log("
|
|
139
|
+
`),o.log(P.bold("\u{1F50D} Finding Your Project Information")),o.log(""),o.log(P.cyan("1. Project ID:")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Select your project"),o.log(" \u2022 Navigate to Settings \u2192 General"),o.log(" \u2022 Copy the Project ID from the top of the page"),o.log(""),o.log(P.cyan("2. Team ID (if using team account):")),o.log(" \u2022 Go to https://vercel.com/dashboard"),o.log(" \u2022 Click on your team name in the top-left"),o.log(" \u2022 Go to Team Settings \u2192 General"),o.log(" \u2022 Copy the Team ID"),o.log(" \u2022 Leave empty if using personal account"),o.log(""),o.log(P.bold("\u{1F511} Creating API Token")),o.log(""),o.log(P.cyan("Steps:")),o.log(" 1. Visit https://vercel.com/account/tokens"),o.log(' 2. Click "Create Token"'),o.log(' 3. Name: "Doorman Firewall Management"'),o.log(" 4. Expiration: Set as needed (90 days recommended)"),o.log(" 5. Scope: Select your project or team"),o.log(" 6. Copy the generated token"),o.log(""),o.log(P.cyan("Required Permissions:")),o.log(" \u2022 Read access to project settings"),o.log(" \u2022 Write access to firewall configuration"),o.log(""),o.log(P.bold("\u{1F30D} Environment Setup")),o.log(""),o.log(P.cyan("Set your API token as environment variable:")),o.log(""),o.log(P.green("# For current session:")),o.log('export VERCEL_TOKEN="your-token-here"'),o.log(""),o.log(P.green("# For permanent setup (add to ~/.bashrc or ~/.zshrc):")),o.log(`echo 'export VERCEL_TOKEN="your-token-here"' >> ~/.bashrc`),o.log(""),o.log(P.green("# Or create a .env file in your project:")),o.log('echo "VERCEL_TOKEN=your-token-here" > .env'),o.log(""),o.log(P.bold("\u{1F680} Quick Start Commands")),o.log(""),o.log(P.cyan("1. Initialize configuration:")),o.log(" doorman init --interactive"),o.log(""),o.log(P.cyan("2. Validate setup:")),o.log(" doorman validate"),o.log(""),o.log(P.cyan("3. Check current status:")),o.log(" doorman status"),o.log(""),o.log(P.cyan("4. View existing rules:")),o.log(" doorman list"),o.log(""),o.log(P.cyan("5. Sync your configuration:")),o.log(" doorman sync"),o.log(""),o.log(P.bold("\u{1F527} Common Issues")),o.log(""),o.log(P.yellow('\u274C "Project not found" error:')),o.log(" \u2022 Double-check your Project ID"),o.log(" \u2022 Ensure your token has access to the project"),o.log(" \u2022 Verify the project has Pro plan or higher"),o.log(""),o.log(P.yellow('\u274C "Unauthorized" error:')),o.log(" \u2022 Check your VERCEL_TOKEN is set correctly"),o.log(" \u2022 Ensure token hasn't expired"),o.log(" \u2022 Verify token has firewall permissions"),o.log(""),o.log(P.yellow('\u274C "Firewall not available" error:')),o.log(" \u2022 Upgrade to Pro plan or higher"),o.log(" \u2022 Contact Vercel support if issue persists"),o.log(""),o.log(P.bold("\u{1F4DA} Additional Resources")),o.log(""),o.log(P.cyan("Documentation:")),o.log(" \u2022 Doorman Docs: https://doorman.griffen.codes"),o.log(" \u2022 Vercel Firewall: https://vercel.com/docs/security/vercel-firewall"),o.log(" \u2022 API Reference: https://vercel.com/docs/rest-api/endpoints/firewall"),o.log(""),o.log(P.cyan("Support:")),o.log(" \u2022 GitHub Issues: https://github.com/gfargo/doorman/issues"),o.log(" \u2022 Vercel Support: https://vercel.com/help"),o.log(""),o.log(P.bold("\u{1F512} Security Best Practices")),o.log(""),o.log("\u2022 Store API tokens securely (use environment variables)"),o.log("\u2022 Set token expiration dates"),o.log("\u2022 Use principle of least privilege for token scopes"),o.log("\u2022 Regularly rotate API tokens"),o.log("\u2022 Test rules in staging before production"),o.log("\u2022 Keep backups of working configurations"),o.log(""),o.log(P.bold.green("\u2705 Ready to get started?")),o.log(P.dim("Run: ")+P.cyan("doorman init --interactive")),o.log("")};var Pt={};z(Pt,{builder:()=>gn,command:()=>un,desc:()=>pn,handler:()=>fn});T();import O from"chalk";import ye from"chalk";var Fe=class{static check(e){let t=[],i=[],r=100;return this.checkRuleNaming(e.rules,t),this.checkRuleComplexity(e.rules,t),this.checkSecurityBestPractices(e,t,i),this.checkPerformanceImpact(e,t,i),this.checkMaintainability(e,t,i),t.forEach(s=>{switch(s.severity){case"error":r-=20;break;case"warning":r-=10;break;case"info":r-=5;break}}),{score:Math.max(0,r),issues:t,recommendations:i}}static checkRuleNaming(e,t){e.forEach(i=>{i.id&&!i.id.match(/^rule_[a-z0-9_]+$/)&&t.push({severity:"warning",message:"Rule ID should follow snake_case format: rule_*",rule:i.name,suggestion:`Consider renaming to: rule_${i.name.toLowerCase().replace(/\s+/g,"_")}`}),i.name.length<5&&t.push({severity:"info",message:"Rule name is very short, consider making it more descriptive",rule:i.name}),(!i.description||i.description.length<10)&&t.push({severity:"info",message:"Rule lacks a detailed description",rule:i.name,suggestion:"Add a clear description explaining what this rule does and why"})})}static checkRuleComplexity(e,t){e.forEach(i=>{i.conditionGroup.reduce((s,a)=>s+a.conditions.length,0)>10&&t.push({severity:"warning",message:"Rule has many conditions, consider splitting into multiple rules",rule:i.name,suggestion:"Complex rules can be harder to maintain and debug"}),i.conditionGroup.length>5&&t.push({severity:"warning",message:"Rule has many condition groups, consider simplifying",rule:i.name})})}static checkSecurityBestPractices(e,t,i){let{rules:r,ips:s}=e;r.some(c=>c.action.mitigate.action==="rate_limit")||i.push("Consider adding rate limiting rules for API endpoints"),r.some(c=>c.conditionGroup.some(d=>d.conditions.some(u=>u.type==="user_agent"&&typeof u.value=="string"&&(u.value.toLowerCase().includes("bot")||u.value.toLowerCase().includes("crawler")))))||i.push("Consider adding bot protection rules"),r.forEach(c=>{c.conditionGroup.some(u=>u.conditions.some(p=>p.type==="path"&&p.value==="/"))&&c.action.mitigate.action==="deny"&&t.push({severity:"error",message:"Rule blocks root path - this could block all traffic",rule:c.name,suggestion:"Be more specific with path conditions"})}),s&&s.length>100&&t.push({severity:"warning",message:"Large number of IP blocking rules may impact performance",suggestion:"Consider using CIDR ranges or external IP lists"})}static checkPerformanceImpact(e,t,i){let{rules:r}=e;r.forEach(a=>{a.conditionGroup.some(c=>c.conditions.some(d=>d.op==="re"))&&t.push({severity:"info",message:"Rule uses regex matching, which may impact performance",rule:a.name,suggestion:"Consider using simpler string operations when possible"})}),r.filter(a=>a.active).length>50&&t.push({severity:"warning",message:"Large number of active rules may impact performance",suggestion:"Consider consolidating similar rules or disabling unused ones"})}static checkMaintainability(e,t,i){let{rules:r}=e;r.filter(c=>!c.active).length>r.length*.3&&i.push("Consider removing disabled rules to reduce config complexity");let a=r.map(c=>c.name);a.filter((c,d)=>a.indexOf(c)!==d).forEach(c=>{t.push({severity:"error",message:"Duplicate rule name found",rule:c,suggestion:"Rule names must be unique"})}),e.version||t.push({severity:"warning",message:"Configuration lacks version information",suggestion:"Version tracking helps with change management"})}static formatHealthReport(e){let t="",i=e.score>=80?ye.green:e.score>=60?ye.yellow:ye.red;return t+=ye.bold(`
|
|
140
140
|
\u{1F3E5} Configuration Health Score: ${i(e.score)}/100
|
|
141
141
|
|
|
142
142
|
`),e.issues.length>0&&(t+=ye.bold(`Issues Found:
|
|
@@ -162,6 +162,6 @@ AJV Schema Validation:`)),o.log(X.green("\u2713 JSON Schema validation passed"))
|
|
|
162
162
|
AJV Schema Validation:`)),o.error(X.red("\u2717 JSON Schema validation failed:")),s.forEach(l=>{o.error(X.red(` - ${l.instancePath}: ${l.message}`))}));else throw a}if(i.success&&r&&n.verbose){o.log(X.bold.underline(`
|
|
163
163
|
Custom Validations:`));let a=i.data,l=new Set,c=new Set;for(let d of a.rules)l.has(d.name)&&c.add(d.name),l.add(d.name);o.log(`${X.cyan.dim(`
|
|
164
164
|
Rule Names:`)}`),c.size>0?(o.error(X.red("\u2717 Duplicate rule names found:")),c.forEach(d=>o.error(X.red(` - "${d}"`)))):o.log(X.green("\u2713 All rule names are unique")),o.log("")}if(i.success&&r)o.success(X.green("Configuration is valid"));else throw new Error("Configuration validation failed")}catch(e){G(e,"validating configuration")}};var Dt={};z(Dt,{builder:()=>Dn,command:()=>_n,desc:()=>Ln,handler:()=>Un});T();import ae from"chalk";import{watchFile as kn,unwatchFile as Sn}from"fs";var _n="watch",Ln="Watch config file for changes and auto-sync",Dn={config:{alias:"c",type:"string",description:"Path to firewall config file (defaults to .doorman.json)"},provider:{type:"string",choices:["vercel","cloudflare"],description:"Firewall provider (auto-detected)"},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)"},interval:{alias:"i",type:"number",description:"Watch interval in milliseconds",default:1e3},debug:{type:"boolean",description:"Enable debug logging",default:!1},ci:{type:"boolean",description:"Run in CI mode (non-interactive)",default:!1}},Un=async n=>{let e=n.config||".doorman.json";await H({config:e,provider:n.provider,projectId:n.projectId,teamId:n.teamId,token:n.token,apiToken:n.apiToken,zoneId:n.zoneId,accountId:n.accountId,debug:n.debug,ci:n.ci,errorContext:"setting up watch mode"},async({service:t})=>{let i=!1,r=0;o.success(ae.green(`\u{1F440} Watching ${e} for changes...`)),o.log(ae.dim("Press Ctrl+C to stop watching")),o.log("");let s=async(c,d)=>{if(!(i||c.mtime.getTime()===r)){i=!0,r=c.mtime.getTime();try{o.log(ae.yellow(`\u{1F4DD} Config file changed at ${new Date().toLocaleTimeString()}`)),o.start("Validating and syncing changes...");let u=await B(e),{toAdd:p,toUpdate:g,toDelete:C,ipsToAdd:m,ipsToUpdate:w,ipsToDelete:S,version:x}=await t.getChanges(u);if(!(p.length>0||g.length>0||C.length>0||m.length>0||w.length>0||S.length>0||u.version!==x)){o.info(ae.blue("No changes detected, skipping sync"));return}let f=p.length+g.length+C.length+m.length+w.length+S.length;o.log(ae.cyan(`Syncing ${f} changes...`));let $=await t.syncRules(u,{debug:n.debug});try{let h=await t.validateAndUpdateConfig(u,$,{dryRun:!1});await N(h,e),o.success(ae.green(`\u2705 Sync completed at ${new Date().toLocaleTimeString()}`))}catch(h){o.warn(ae.yellow(`\u26A0\uFE0F Sync applied but validation failed: ${h instanceof Error?h.message:String(h)}`)),o.info(ae.dim("Run `download` to reconcile local config with remote state"))}o.log(ae.dim("Watching for more changes..."))}catch(u){o.error(ae.red(`\u274C Sync failed: ${u instanceof Error?u.message:String(u)}`)),o.log(ae.dim("Continuing to watch for changes..."))}finally{i=!1}}};kn(e,{interval:n.interval},s);let a=()=>{o.log(`
|
|
165
|
-
`),o.info(ae.yellow("Stopping watch mode...")),Sn(e),process.exit(0)};process.on("SIGINT",a),process.on("SIGTERM",a);let l=()=>{setTimeout(l,1e3)};l()})};var wi=[Tt,vt,at,Et,Pt,Ct,kt,Lt,It,_t,$t,ft,bt,Dt];xn();async function Nn(){let n=(await import("chalk")).default,e=On(process.argv.slice(2));e.usage(`${n.bold("\
|
|
165
|
+
`),o.info(ae.yellow("Stopping watch mode...")),Sn(e),process.exit(0)};process.on("SIGINT",a),process.on("SIGTERM",a);let l=()=>{setTimeout(l,1e3)};l()})};var wi=[Tt,vt,at,Et,Pt,Ct,kt,Lt,It,_t,$t,ft,bt,Dt];xn();async function Nn(){let n=(await import("chalk")).default,e=On(process.argv.slice(2));e.usage(`${n.bold("\u{1F6AA} Doorman")}
|
|
166
166
|
|
|
167
|
-
${n.dim("Manage
|
|
167
|
+
${n.dim("Manage firewall rules as code across multiple providers")}`);for(let t of wi)e.command(t);e.demandCommand(1,"You need at least one command before moving on").help().epilogue(n.dim(`See ${n.bold("https://doorman.griffen.codes/getting-started")} for more info`)).argv}Nn().catch(console.error);
|