@@ -8,13 +8,14 @@ import { AuthManager } from "./auth/manager.js";
88import { startServer , type ProxyServer } from "./server/server.js" ;
99import { startControlListener , LogBuffer , type ControlState } from "./android/control.js" ;
1010import { loadCredential , saveCredential , clearCredential , getStorePath } from "./auth/store.js" ;
11- import { ZaiOAuthClient , BigmodelOAuthClient } from "./auth/oauth.js" ;
11+ import { ZaiOAuthClient , BigmodelOAuthClient , LOGIN_TIMEOUT_MS , parsePastedCallbackUrl , type OAuthResult } from "./auth/oauth.js" ;
1212import { KeyResolver } from "./auth/resolver.js" ;
1313import type { Credential } from "./auth/types.js" ;
1414import type { ProviderId } from "./provider/types.js" ;
1515import type { ProxyConfig } from "./config/types.js" ;
1616import { updateConfigYaml } from "./config/edit.js" ;
1717import { openBrowser } from "./runtime/open-browser.js" ;
18+ import { pasteLoginInstructions , readPastedLine , boldIfTTY } from "./runtime/paste-login.js" ;
1819import { buildServerOptions } from "./server/server-options.js" ;
1920import { readFileSync , existsSync , writeFileSync } from "node:fs" ;
2021import { join } from "node:path" ;
@@ -405,22 +406,31 @@ async function claimCommand(args: string[]): Promise<void> {
405406async function authLogin ( args : string [ ] ) : Promise < void > {
406407 const provider = args [ 0 ] as ProviderId | undefined ;
407408 const importMode = args . includes ( "--import" ) ;
409+ // Headless paste login: --paste flag or ZCODE_OAUTH_PASTE=1 (docker-friendly).
410+ const pasteMode =
411+ args . includes ( "--paste" ) || / ^ ( 1 | t r u e | y e s ) $ / i. test ( process . env . ZCODE_OAUTH_PASTE ?? "" ) ;
408412
409413 if ( ! provider || ( provider !== "zai" && provider !== "bigmodel" ) ) {
410- console . error ( "Usage: zcode-proxy auth login <zai|bigmodel> [--import]" ) ;
414+ console . error ( "Usage: zcode-proxy auth login <zai|bigmodel> [--import] [--paste]" ) ;
415+ process . exit ( 1 ) ;
416+ }
417+ if ( pasteMode && provider !== "bigmodel" ) {
418+ console . error ( "--paste applies to the bigmodel auth-code flow only." ) ;
419+ console . error ( "zai login is server-mediated (no localhost callback) and already works headless." ) ;
411420 process . exit ( 1 ) ;
412421 }
413422
414423 ensureConfigWithDeviceMid ( ) ;
415424
416- console . log ( `Logging in: ${ provider } ${ importMode ? " (import)" : " (OAuth)" } \n` ) ;
425+ const mode = importMode ? "(import)" : pasteMode ? "(OAuth, paste)" : "(OAuth)" ;
426+ console . log ( `Logging in: ${ provider } ${ mode } \n` ) ;
417427
418428 let cred : Credential ;
419429
420430 if ( importMode ) {
421431 cred = importFromZCodeConfig ( provider ) ;
422432 } else {
423- const { accessToken, userId, jwt } = await runOAuth ( provider ) ;
433+ const { accessToken, userId, jwt } = await runOAuth ( provider , pasteMode ) ;
424434 console . log ( "\nResolving API key..." ) ;
425435 const resolver = new KeyResolver ( ) ;
426436 cred = await resolver . resolveCodingPlanCredential ( accessToken , provider , userId ) ;
@@ -506,16 +516,21 @@ async function authStatus(): Promise<void> {
506516 console . log ( ` Store: ${ getStorePath ( ) } ` ) ;
507517}
508518
509- async function runOAuth ( provider : ProviderId ) : Promise < { accessToken : string ; userId ?: string ; jwt ?: string } > {
519+ async function runOAuth ( provider : ProviderId , pasteMode : boolean ) : Promise < OAuthResult > {
510520 if ( provider === "bigmodel" ) {
511521 const oauth = new BigmodelOAuthClient ( ) ;
522+ if ( pasteMode ) return runPasteLogin ( oauth ) ;
512523 const result = await oauth . authorize ( ( url ) => {
513524 console . log ( "Open this URL to authorize:\n" ) ;
514525 console . log ( ` ${ url } \n` ) ;
515526 console . log ( "Waiting for authorization... (expires in 300s)\n" ) ;
527+ console . log (
528+ "Headless/Docker? The callback page will NOT load here — Ctrl-C and " +
529+ "re-run with `auth login bigmodel --paste` to paste the redirected URL instead.\n" ,
530+ ) ;
516531 openBrowser ( url ) ;
517532 } ) ;
518- return { accessToken : result . accessToken , userId : result . userId , jwt : result . jwt } ;
533+ return result ;
519534 }
520535
521536 const oauth = new ZaiOAuthClient ( ) ;
@@ -525,7 +540,32 @@ async function runOAuth(provider: ProviderId): Promise<{ accessToken: string; us
525540 console . log ( "Waiting for authorization... (expires in 300s)\n" ) ;
526541 openBrowser ( url ) ;
527542 } ) ;
528- return { accessToken : result . accessToken , userId : result . userId , jwt : result . jwt } ;
543+ return result ;
544+ }
545+
546+ /**
547+ * Headless bigmodel login (`auth login bigmodel --paste`): the localhost
548+ * callback server is still bound — it defines the redirect port and the
549+ * browser can never reach it from inside a container anyway — but instead of
550+ * waiting on it, the user pastes the redirected URL back. The exact
551+ * `started.callbackUrl` string is used BOTH as the authorize `redirect` param
552+ * and as the exchange `redirect_uri` (the token endpoint requires them to
553+ * match), so the pair stays consistent by construction.
554+ */
555+ async function runPasteLogin ( oauth : BigmodelOAuthClient ) : Promise < OAuthResult > {
556+ const started = await oauth . start ( ) ;
557+ try {
558+ console . log ( pasteLoginInstructions ( started . authorizeUrl , started . callbackUrl , LOGIN_TIMEOUT_MS ) ) ;
559+ openBrowser ( started . authorizeUrl ) ;
560+ process . stdout . write ( "\n" + boldIfTTY ( "Paste the FULL redirected URL here, then press Enter:" ) + "\n> " ) ;
561+ const pasted = await readPastedLine ( LOGIN_TIMEOUT_MS ) ;
562+ const code = parsePastedCallbackUrl ( pasted , started . state ) ;
563+ console . log ( "\nExchanging authorization code..." ) ;
564+ const tokens = await oauth . exchangeCode ( code , started . callbackUrl , started . state ) ;
565+ return { accessToken : tokens . accessToken , provider : "bigmodel" , userId : tokens . userId , jwt : tokens . jwt } ;
566+ } finally {
567+ await oauth . close ( ) ;
568+ }
529569}
530570
531571function importFromZCodeConfig ( provider : ProviderId ) : Credential {
0 commit comments