11// @effect -diagnostics nodeBuiltinImport:off - the synchronous preload bootstrap needs a synchronous, fail-closed credential read.
2+ import * as NodeCrypto from "node:crypto" ;
23import * as NodeFS from "node:fs" ;
34import * as NodePath from "node:path" ;
45
@@ -43,16 +44,14 @@ function parseTokenEnvironmentFile(contents: string): string | null {
4344 return null ;
4445}
4546
47+ const credentialFile = ( homeDirectory : string ) =>
48+ NodePath . join ( homeDirectory , ".config" , "t3code" , "mcp-gateway.env" ) ;
49+
4650export function readMcpGatewayBridgeToken ( source : McpGatewayCredentialSource ) : string | null {
4751 const environmentToken = validateToken ( source . env . T3_MCP_BRIDGE_TOKEN ) ;
4852 if ( environmentToken !== null ) return environmentToken ;
4953
50- const credentialPath = NodePath . join (
51- source . homeDirectory ,
52- ".config" ,
53- "t3code" ,
54- "mcp-gateway.env" ,
55- ) ;
54+ const credentialPath = credentialFile ( source . homeDirectory ) ;
5655 try {
5756 const file = source . statFile ( credentialPath ) ;
5857 if ( ! file . isFile ( ) || ( source . userId !== null && file . uid !== source . userId ) ) return null ;
@@ -63,12 +62,49 @@ export function readMcpGatewayBridgeToken(source: McpGatewayCredentialSource): s
6362 }
6463}
6564
65+ export interface McpGatewayCredentialStore extends McpGatewayCredentialSource {
66+ readonly makeDirectory : ( path : string ) => void ;
67+ /** Creates the file only if it does not exist yet. */
68+ readonly createFileExclusive : ( path : string , contents : string ) => void ;
69+ readonly newToken : ( ) => string ;
70+ }
71+
72+ /**
73+ * The token, creating the protected env file when there is none, so an enabled gateway keeps
74+ * the same token across restarts. A file that exists but is unusable is left for its owner to fix.
75+ */
76+ export function ensureMcpGatewayBridgeToken ( store : McpGatewayCredentialStore ) : string | null {
77+ const existing = readMcpGatewayBridgeToken ( store ) ;
78+ if ( existing !== null ) return existing ;
79+ const credentialPath = credentialFile ( store . homeDirectory ) ;
80+ try {
81+ store . makeDirectory ( NodePath . dirname ( credentialPath ) ) ;
82+ store . createFileExclusive (
83+ credentialPath ,
84+ `${ TOKEN_ENVIRONMENT_VARIABLE } =${ store . newToken ( ) } \n` ,
85+ ) ;
86+ } catch {
87+ // Another window created it first, or it cannot be written; the read below decides.
88+ }
89+ return readMcpGatewayBridgeToken ( store ) ;
90+ }
91+
92+ const processCredentialSource = ( homeDirectory : string ) : McpGatewayCredentialStore => ( {
93+ env : process . env ,
94+ homeDirectory,
95+ userId : typeof process . getuid === "function" ? process . getuid ( ) : null ,
96+ statFile : NodeFS . statSync ,
97+ readFileString : ( path ) => NodeFS . readFileSync ( path , "utf8" ) ,
98+ makeDirectory : ( path ) => NodeFS . mkdirSync ( path , { recursive : true , mode : 0o700 } ) ,
99+ createFileExclusive : ( path , contents ) =>
100+ NodeFS . writeFileSync ( path , contents , { flag : "wx" , mode : 0o600 } ) ,
101+ newToken : ( ) => NodeCrypto . randomBytes ( 32 ) . toString ( "hex" ) ,
102+ } ) ;
103+
66104export function readMcpGatewayBridgeTokenFromProcess ( homeDirectory : string ) : string | null {
67- return readMcpGatewayBridgeToken ( {
68- env : process . env ,
69- homeDirectory,
70- userId : typeof process . getuid === "function" ? process . getuid ( ) : null ,
71- statFile : NodeFS . statSync ,
72- readFileString : ( path ) => NodeFS . readFileSync ( path , "utf8" ) ,
73- } ) ;
105+ return readMcpGatewayBridgeToken ( processCredentialSource ( homeDirectory ) ) ;
106+ }
107+
108+ export function ensureMcpGatewayBridgeTokenFromProcess ( homeDirectory : string ) : string | null {
109+ return ensureMcpGatewayBridgeToken ( processCredentialSource ( homeDirectory ) ) ;
74110}
0 commit comments