@@ -51,6 +51,8 @@ function fail(message: string): never {
5151 process . exit ( 1 ) ;
5252}
5353
54+ const RESERVED_EMAIL_DOMAINS = new Set ( [ "example.com" , "example.org" , "example.net" ] ) ;
55+
5456function info ( message : string ) : void {
5557 console . log ( chalk . cyan ( `${ PREFIX } : ${ message } ` ) ) ;
5658}
@@ -59,6 +61,19 @@ function success(message: string): void {
5961 console . log ( chalk . green ( `${ PREFIX } : ${ message } ` ) ) ;
6062}
6163
64+ function validateEmail ( email : string , fieldName : string ) : string {
65+ const normalized = email . trim ( ) ;
66+ const match = normalized . match ( / ^ [ ^ @ \s ] + @ ( [ ^ @ \s ] + ) $ / ) ;
67+ if ( ! match ) {
68+ fail ( `${ fieldName } must be a valid email address` ) ;
69+ }
70+ const domain = match [ 1 ] . toLowerCase ( ) ;
71+ if ( RESERVED_EMAIL_DOMAINS . has ( domain ) ) {
72+ fail ( `${ fieldName } must not use reserved example.* domains because ACME rejects them` ) ;
73+ }
74+ return normalized ;
75+ }
76+
6277function requireRoot ( ) : void {
6378 if ( typeof process . getuid === "function" && process . getuid ( ) !== 0 ) {
6479 fail ( "run as root" ) ;
@@ -193,6 +208,24 @@ async function promptText(message: string, defaultValue = ""): Promise<string> {
193208 } ) ;
194209}
195210
211+ async function promptEmail ( message : string , defaultValue = "" , fieldName = "email" ) : Promise < string > {
212+ return await input ( {
213+ message,
214+ default : defaultValue ,
215+ validate : ( value ) => {
216+ const normalized = value . trim ( ) ;
217+ const match = normalized . match ( / ^ [ ^ @ \s ] + @ ( [ ^ @ \s ] + ) $ / ) ;
218+ if ( ! match ) {
219+ return "Enter a valid email address" ;
220+ }
221+ if ( RESERVED_EMAIL_DOMAINS . has ( match [ 1 ] . toLowerCase ( ) ) ) {
222+ return "Reserved example.* domains are not accepted" ;
223+ }
224+ return true ;
225+ }
226+ } ) . then ( ( value ) => validateEmail ( value , fieldName ) ) ;
227+ }
228+
196229async function promptConfirm ( message : string , defaultValue : boolean , assumeYes : boolean ) : Promise < boolean > {
197230 if ( assumeYes ) {
198231 return true ;
@@ -204,7 +237,7 @@ async function interactiveConfig(options: InstallOptions): Promise<void> {
204237 options . publicIp = await detectPublicIp ( options . publicIp ) ;
205238 const dashed = dashedIp ( options . publicIp ) ;
206239
207- options . email = options . email || ( await promptText ( "Email for ACME/notifications" , `admin@${ options . publicIp } .nip.io` ) ) ;
240+ options . email = options . email || ( await promptEmail ( "Email for ACME/notifications" , `admin@${ options . publicIp } .nip.io` , "--email" ) ) ;
208241 options . zitadelAdminEmail = options . zitadelAdminEmail || options . email ;
209242
210243 if ( ! options . domain && ! options . manageDomain ) {
@@ -237,7 +270,11 @@ async function interactiveConfig(options: InstallOptions): Promise<void> {
237270 options . authDomain ||
238271 ( options . domain ? `auth.${ options . domain } ` : `auth.${ dashed } .traefik.me` ) ;
239272 options . authDomain = await promptText ( "ZITADEL auth domain" , options . authDomain ) ;
240- options . zitadelAdminEmail = await promptText ( "ZITADEL bootstrap admin email" , options . zitadelAdminEmail || options . email ) ;
273+ options . zitadelAdminEmail = await promptEmail (
274+ "ZITADEL bootstrap admin email" ,
275+ options . zitadelAdminEmail || options . email ,
276+ "--zitadel-admin-email"
277+ ) ;
241278 } else {
242279 options . idpMode = "none" ;
243280 options . authDomain = "" ;
@@ -314,10 +351,12 @@ function validateNonInteractive(options: InstallOptions): void {
314351 if ( ! options . email ) {
315352 fail ( "--email is required in non-interactive mode" ) ;
316353 }
354+ options . email = validateEmail ( options . email , "--email" ) ;
317355
318356 if ( options . idpMode === "zitadel_self_hosted" ) {
319357 options . authDomain = options . authDomain || ( options . domain ? `auth.${ options . domain } ` : `auth.${ dashed } .traefik.me` ) ;
320358 options . zitadelAdminEmail = options . zitadelAdminEmail || options . email ;
359+ options . zitadelAdminEmail = validateEmail ( options . zitadelAdminEmail , "--zitadel-admin-email" ) ;
321360 } else {
322361 options . authDomain = "" ;
323362 }
0 commit comments