@@ -5,9 +5,11 @@ import {
55 generateSecret ,
66 isPlaceholder ,
77 isTruthy ,
8+ isUsableSecret ,
89 readEnvFile ,
910 SECRET_KEYS ,
1011 SHARED_KEYS ,
12+ secretRequirement ,
1113 writeEnvValues ,
1214} from './env-files.ts'
1315import { httpHealth , pgProbe , redisPing } from './probes.ts'
@@ -153,11 +155,11 @@ function checkSchema(ctx: CheckContext): Finding[] {
153155 } )
154156 continue
155157 }
156- if ( MIN_32_KEYS . has ( key ) && value . length < 32 ) {
158+ if ( MIN_32_KEYS . has ( key ) && ! isUsableSecret ( key , value ) ) {
157159 findings . push ( {
158160 group : 'schema' ,
159161 status : 'fail' ,
160- message : `${ rel ( file ) } : ${ key } is shorter than 32 chars — the realtime server rejects it ` ,
162+ message : `${ rel ( file ) } : ${ key } ${ secretRequirement ( key ) } ` ,
161163 fix : 'generate a new one with `openssl rand -hex 32` (rotating it invalidates existing sessions/encrypted data)' ,
162164 } )
163165 continue
@@ -436,9 +438,8 @@ function checkCoherence(ctx: CheckContext): Finding[] {
436438 return findings
437439}
438440
439- async function checkLive ( ctx : CheckContext ) : Promise < Finding [ ] > {
441+ async function checkDatabase ( sim : EnvFile ) : Promise < Finding [ ] > {
440442 const findings : Finding [ ] = [ ]
441- const sim = ctx . env . sim
442443 const dsn = sim . vars . get ( 'DATABASE_URL' )
443444 const dsnPassword = ( ( ) => {
444445 try {
@@ -497,53 +498,73 @@ async function checkLive(ctx: CheckContext): Promise<Finding[]> {
497498 } )
498499 }
499500
501+ return findings
502+ }
503+
504+ async function checkRedis ( sim : EnvFile ) : Promise < Finding [ ] > {
500505 const redisUrl = sim . vars . get ( 'REDIS_URL' )
501- if ( redisUrl ) {
502- const ping = await redisPing ( redisUrl )
503- findings . push (
504- ping . ok
505- ? { group : 'live' , status : 'pass' , message : 'redis reachable' }
506- : {
507- group : 'live' ,
508- status : 'fail' ,
509- message : `redis unreachable: ${ ping . error } ` ,
510- fix : 'fix REDIS_URL or remove it (optional for single-replica)' ,
511- }
512- )
513- }
506+ if ( ! redisUrl ) return [ ]
507+ const ping = await redisPing ( redisUrl )
508+ return [
509+ ping . ok
510+ ? { group : 'live' , status : 'pass' , message : 'redis reachable' }
511+ : {
512+ group : 'live' ,
513+ status : 'fail' ,
514+ message : `redis unreachable: ${ ping . error } ` ,
515+ fix : 'fix REDIS_URL or remove it (optional for single-replica)' ,
516+ } ,
517+ ]
518+ }
514519
515- for ( const [ label , port , url ] of [
516- [ 'app' , 3000 , 'http://localhost:3000/api/health' ] ,
517- [ 'realtime' , 3002 , 'http://localhost:3002/health' ] ,
518- ] as const ) {
519- if ( ! ( await portOpen ( port ) ) ) {
520- findings . push ( { group : 'live' , status : 'skip' , message : `${ label } : not running on :${ port } ` } )
521- } else if ( await httpHealth ( url ) ) {
522- findings . push ( { group : 'live' , status : 'pass' , message : `${ label } healthy on :${ port } ` } )
523- } else {
524- findings . push ( {
525- group : 'live' ,
526- status : 'fail' ,
527- message : `${ label } : something is on :${ port } but ${ url } is not answering` ,
528- fix : 'check the dev server logs' ,
529- } )
530- }
520+ async function checkService ( label : string , port : number , url : string ) : Promise < Finding [ ] > {
521+ if ( ! ( await portOpen ( port ) ) ) {
522+ return [ { group : 'live' , status : 'skip' , message : `${ label } : not running on :${ port } ` } ]
523+ }
524+ if ( await httpHealth ( url ) ) {
525+ return [ { group : 'live' , status : 'pass' , message : `${ label } healthy on :${ port } ` } ]
531526 }
527+ return [
528+ {
529+ group : 'live' ,
530+ status : 'fail' ,
531+ message : `${ label } : something is on :${ port } but ${ url } is not answering` ,
532+ fix : 'check the dev server logs' ,
533+ } ,
534+ ]
535+ }
532536
537+ async function checkOllama ( sim : EnvFile ) : Promise < Finding [ ] > {
533538 const ollamaUrl = sim . vars . get ( 'OLLAMA_URL' )
534- if ( ollamaUrl ) {
535- findings . push (
536- ( await httpHealth ( `${ ollamaUrl . replace ( / \/ $ / , '' ) } /api/tags` ) )
537- ? { group : 'live' , status : 'pass' , message : 'ollama reachable' }
538- : {
539- group : 'live' ,
540- status : 'warn' ,
541- message : 'OLLAMA_URL is set but Ollama is not answering' ,
542- fix : 'start Ollama or remove OLLAMA_URL' ,
543- }
544- )
545- }
546- return findings
539+ if ( ! ollamaUrl ) return [ ]
540+ return [
541+ ( await httpHealth ( `${ ollamaUrl . replace ( / \/ $ / , '' ) } /api/tags` ) )
542+ ? { group : 'live' , status : 'pass' , message : 'ollama reachable' }
543+ : {
544+ group : 'live' ,
545+ status : 'warn' ,
546+ message : 'OLLAMA_URL is set but Ollama is not answering' ,
547+ fix : 'start Ollama or remove OLLAMA_URL' ,
548+ } ,
549+ ]
550+ }
551+
552+ /**
553+ * The five probes are independent, so they run concurrently — serially this is
554+ * the sum of every timeout (~17s worst case) on a command whose whole job is to
555+ * tell you what's broken. Results are concatenated in a fixed order so the
556+ * report stays deterministic regardless of which probe settles first.
557+ */
558+ async function checkLive ( ctx : CheckContext ) : Promise < Finding [ ] > {
559+ const sim = ctx . env . sim
560+ const [ database , redis , app , realtime , ollama ] = await Promise . all ( [
561+ checkDatabase ( sim ) ,
562+ checkRedis ( sim ) ,
563+ checkService ( 'app' , 3000 , 'http://localhost:3000/api/health' ) ,
564+ checkService ( 'realtime' , 3002 , 'http://localhost:3002/health' ) ,
565+ checkOllama ( sim ) ,
566+ ] )
567+ return [ ...database , ...redis , ...app , ...realtime , ...ollama ]
547568}
548569
549570export async function runChecks ( ctx : CheckContext , groups ?: CheckGroup [ ] ) : Promise < Finding [ ] > {
0 commit comments