11import type { DevframeNodeContext , DevframeNodeRpcSession } from 'devframe/types'
2+ import type { ColorFn } from 'devframe/utils/colors'
23import type { DevframeAuthHandler } from '../node/auth'
34import { colors } from 'devframe/utils/colors'
45import { s } from 'devframe/utils/simple-schema'
@@ -18,11 +19,12 @@ export interface CreateInteractiveAuthOptions {
1819 /**
1920 * Print the current code + magic-link URL. Devframe stays headless, so
2021 * there is no default banner printed automatically; call
21- * `auth.printBanner()` yourself once the server is listening. Override
22- * this to customize the format; defaults to a small boxed message on
23- * stdout.
22+ * `auth.printBanner()` yourself once the server is listening. Defaults to
23+ * {@link createAuthBanner}'s output; pass its result here directly to
24+ * rebrand the box (title / colors), or your own function to replace the
25+ * format outright.
2426 */
25- banner ?: ( info : { code : string , url : string } ) => void
27+ banner ?: AuthBannerFunction
2628 /**
2729 * Called once a code exchange succeeds, so a host rendering its own
2830 * banner can retract it. Fires after the rotated code is printed, so
@@ -38,9 +40,68 @@ export interface CreateInteractiveAuthOptions {
3840 serverUrl ?: ( ) => string
3941}
4042
41- function defaultBanner ( info : { code : string , url : string } ) : void {
42- // eslint-disable-next-line no-console
43- console . log ( `\n ${ colors . dim ( 'devframe auth code' ) } ${ colors . bold ( info . code ) } \n ${ colors . dim ( 'or open' ) } ${ colors . cyan ( info . url ) } \n` )
43+ /** Signature of `options.banner`: render the current auth code + magic-link URL. */
44+ export type AuthBannerFunction = ( info : { code : string , url : string } ) => void
45+
46+ /** Palette for {@link createAuthBanner}'s box - one color per part, so a host can rebrand a subset. */
47+ export interface CreateAuthBannerColorsOptions {
48+ border : ColorFn
49+ title : ColorFn
50+ label : ColorFn
51+ code : ColorFn
52+ url : ColorFn
53+ }
54+
55+ export interface CreateAuthBannerOptions {
56+ /** Box title. Defaults to `'Devframe'` - set to your product name for branding. */
57+ title ?: string
58+ /** Palette overrides; unset colors fall back to dim/bold/cyan defaults. */
59+ colors ?: Partial < CreateAuthBannerColorsOptions >
60+ }
61+
62+ /**
63+ * Build a {@link AuthBannerFunction} that renders the auth code + magic-link
64+ * URL as a small bordered box, its two rows label-aligned. `createInteractiveAuth`
65+ * falls back to `createAuthBanner()` when no `banner` is given; call this
66+ * yourself to rebrand the box (`title` / `colors`) and pass the result as
67+ * `options.banner`.
68+ */
69+ export function createAuthBanner ( options : CreateAuthBannerOptions = { } ) : AuthBannerFunction {
70+ const title = options . title ?? 'Devframe'
71+ const palette : CreateAuthBannerColorsOptions = {
72+ border : colors . dim ,
73+ title : colors . bold ,
74+ label : colors . dim ,
75+ code : colors . bold ,
76+ url : colors . cyan ,
77+ ...options . colors ,
78+ }
79+
80+ return ( info ) => {
81+ const rows : [ label : string , value : string , color : ColorFn ] [ ] = [
82+ [ 'auth code' , info . code , palette . code ] ,
83+ [ 'or open' , info . url , palette . url ] ,
84+ ]
85+ const labelWidth = Math . max ( ...rows . map ( ( [ label ] ) => label . length ) )
86+ const contentWidth = Math . max ( ...rows . map ( ( [ , value ] ) => labelWidth + 2 + value . length ) )
87+ const titleBarLength = title . length + 3
88+ const lineWidth = Math . max ( contentWidth , titleBarLength - 2 )
89+
90+ const top = [
91+ palette . border ( `╭─` ) ,
92+ palette . title ( title ) ,
93+ palette . border ( `${ '─' . repeat ( Math . max ( lineWidth + 2 - titleBarLength , 0 ) ) } ╮` ) ,
94+ ] . join ( ' ' )
95+ const bottom = `╰${ '─' . repeat ( lineWidth + 2 ) } ╯`
96+ const body = rows . map ( ( [ label , value , color ] ) => {
97+ const plain = `${ label . padEnd ( labelWidth ) } ${ value } `
98+ const pad = ' ' . repeat ( lineWidth - plain . length )
99+ return `${ palette . border ( '│' ) } ${ palette . label ( label . padEnd ( labelWidth ) ) } ${ color ( value ) } ${ pad } ${ palette . border ( '│' ) } `
100+ } )
101+
102+ // eslint-disable-next-line no-console
103+ console . log ( `\n${ palette . border ( top ) } \n${ body . join ( '\n' ) } \n${ palette . border ( bottom ) } \n` )
104+ }
44105}
45106
46107/**
@@ -80,14 +141,16 @@ export function createInteractiveAuth(
80141 return options . serverUrl ?.( ) ?? context . host . resolveOrigin ( )
81142 }
82143
144+ const banner = options . banner ?? createAuthBanner ( )
145+
83146 let bannerPrintedForCode : string | undefined
84147 function printBanner ( ) : void {
85148 const code = getTempAuthCode ( )
86149 if ( code === bannerPrintedForCode )
87150 return
88151 bannerPrintedForCode = code
89152 const url = buildOtpAuthUrl ( resolveServerUrl ( ) , code )
90- ; ( options . banner ?? defaultBanner ) ( { code, url } )
153+ banner ( { code, url } )
91154 }
92155
93156 const anonymousAuth = defineRpcFunction ( {
0 commit comments