mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
fix: address PR #4 review suggestions
- Fix race condition in start() using Promise instead of boolean flag - Document that tunnel intentionally doesn't limit response sizes (streaming) - Improve token upload failure visibility with actionable user message Built [OnSteroids](https://onsteroids.ai) Co-Authored-By: OnSteroids <built@onsteroids.ai>
This commit is contained in:
co-authored by
OnSteroids
parent
e7e95e6970
commit
c3bfa34703
@@ -260,12 +260,24 @@ function uploadTokenToRemoteAsync(tokenPath: string, verbose: boolean): void {
|
|||||||
import('../remote-token-uploader')
|
import('../remote-token-uploader')
|
||||||
.then(({ uploadTokenToRemote, isRemoteUploadEnabled }) => {
|
.then(({ uploadTokenToRemote, isRemoteUploadEnabled }) => {
|
||||||
if (isRemoteUploadEnabled()) {
|
if (isRemoteUploadEnabled()) {
|
||||||
// uploadTokenToRemote handles its own logging for success/failure
|
// uploadTokenToRemote handles its own success/failure logging
|
||||||
uploadTokenToRemote(tokenPath, verbose).catch((err: unknown) => {
|
// On failure, show additional warning so users know local token is still valid
|
||||||
// Unexpected error (not handled by uploadTokenToRemote)
|
uploadTokenToRemote(tokenPath, verbose)
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
.then((success) => {
|
||||||
console.error(`[token-manager] Unexpected upload error: ${message}`);
|
if (!success) {
|
||||||
});
|
console.error(
|
||||||
|
'\n[!] Remote upload failed - token saved locally only. Run "ccs tokens upload" to retry.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
// Unexpected error (not handled by uploadTokenToRemote)
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.error(`[token-manager] Unexpected upload error: ${message}`);
|
||||||
|
console.error(
|
||||||
|
'[!] Token saved locally. Run "ccs tokens upload" to sync to remote server.'
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err: unknown) => {
|
.catch((err: unknown) => {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
*
|
*
|
||||||
* Flow:
|
* Flow:
|
||||||
* Claude CLI --HTTP--> Local Tunnel (port X) --HTTPS--> Remote CLIProxyAPI
|
* Claude CLI --HTTP--> Local Tunnel (port X) --HTTPS--> Remote CLIProxyAPI
|
||||||
|
*
|
||||||
|
* Note: Unlike CodexReasoningProxy, this tunnel does NOT buffer or limit response sizes.
|
||||||
|
* Responses are streamed directly (pipe) which is appropriate for a transparent tunnel.
|
||||||
|
* Socket-level timeouts handle hung connections; size limits are enforced by the remote server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
@@ -31,7 +35,7 @@ export interface HttpsTunnelConfig {
|
|||||||
export class HttpsTunnelProxy {
|
export class HttpsTunnelProxy {
|
||||||
private server: http.Server | null = null;
|
private server: http.Server | null = null;
|
||||||
private port: number | null = null;
|
private port: number | null = null;
|
||||||
private starting = false;
|
private startingPromise: Promise<number> | null = null;
|
||||||
private activeConnections = new Set<Socket>();
|
private activeConnections = new Set<Socket>();
|
||||||
private readonly config: Required<
|
private readonly config: Required<
|
||||||
Pick<
|
Pick<
|
||||||
@@ -75,11 +79,13 @@ export class HttpsTunnelProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<number> {
|
async start(): Promise<number> {
|
||||||
// Prevent race condition with concurrent start() calls
|
// Already started
|
||||||
if (this.server || this.starting) return this.port ?? 0;
|
if (this.server) return this.port ?? 0;
|
||||||
this.starting = true;
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// Prevent race condition: if start() is already in progress, return the same promise
|
||||||
|
if (this.startingPromise) return this.startingPromise;
|
||||||
|
|
||||||
|
this.startingPromise = new Promise((resolve, reject) => {
|
||||||
this.server = http.createServer((req, res) => {
|
this.server = http.createServer((req, res) => {
|
||||||
void this.handleRequest(req, res);
|
void this.handleRequest(req, res);
|
||||||
});
|
});
|
||||||
@@ -93,8 +99,8 @@ export class HttpsTunnelProxy {
|
|||||||
this.server.listen(0, '127.0.0.1', () => {
|
this.server.listen(0, '127.0.0.1', () => {
|
||||||
const address = this.server?.address();
|
const address = this.server?.address();
|
||||||
this.port = typeof address === 'object' && address ? address.port : 0;
|
this.port = typeof address === 'object' && address ? address.port : 0;
|
||||||
this.starting = false;
|
|
||||||
if (this.port === 0) {
|
if (this.port === 0) {
|
||||||
|
this.startingPromise = null;
|
||||||
reject(new Error('Failed to bind to any port'));
|
reject(new Error('Failed to bind to any port'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -105,10 +111,12 @@ export class HttpsTunnelProxy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.server.on('error', (err) => {
|
this.server.on('error', (err) => {
|
||||||
this.starting = false;
|
this.startingPromise = null;
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return this.startingPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
@@ -123,6 +131,7 @@ export class HttpsTunnelProxy {
|
|||||||
this.server.close();
|
this.server.close();
|
||||||
this.server = null;
|
this.server = null;
|
||||||
this.port = null;
|
this.port = null;
|
||||||
|
this.startingPromise = null;
|
||||||
this.log('Stopped');
|
this.log('Stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user