()
| 109 | } |
| 110 | |
| 111 | async start() { |
| 112 | // Capture the prior token before clearIfStale wipes it. Daemon restarts |
| 113 | // routinely fall into the stale branch (the previous PID is gone), and |
| 114 | // rotating `proxy.token` on every restart invalidates ANTHROPIC_AUTH_TOKEN |
| 115 | // already exported into long-lived shells (the .bashrc auto-source only |
| 116 | // runs once per terminal). |
| 117 | const priorProxy = readSettings().proxy || {}; |
| 118 | const previous = isValidReusableProxyToken(priorProxy.token) ? priorProxy.token.trim() : null; |
| 119 | // settings.json is operator-edited; previous_tokens may contain non-strings |
| 120 | // (numbers, booleans, objects) that would later crash Buffer.from(cand, 'utf8') |
| 121 | // in _handleRequest as ERR_INVALID_ARG_TYPE — an unhandled rejection that |
| 122 | // takes the daemon down under default --unhandled-rejections=throw. |
| 123 | const priorPreviousTokens = Array.isArray(priorProxy.previous_tokens) |
| 124 | ? priorProxy.previous_tokens |
| 125 | .map((t) => (typeof t === 'string' ? t.trim() : '')) |
| 126 | .filter((t) => isValidReusableProxyToken(t)) |
| 127 | : []; |
| 128 | clearIfStale(); |
| 129 | const clientSettingsOpts = this.clientSettings || {}; |
| 130 | const clientToken = this.clientSettings |
| 131 | ? readReusableClientProxyToken({ ...clientSettingsOpts, port: this.basePort }) |
| 132 | : null; |
| 133 | this.token = previous || clientToken || crypto.randomBytes(32).toString('hex'); |
| 134 | this._priorPreviousTokens = priorPreviousTokens; |
| 135 | this.server = http.createServer((req, res) => this._handleRequest(req, res)); |
| 136 | |
| 137 | let port = this.basePort; |
| 138 | for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) { |
| 139 | const ok = await tryListen(this.server, port); |
| 140 | if (ok) { |
| 141 | this.actualPort = port; |
| 142 | const url = `http://127.0.0.1:${port}`; |
| 143 | const proxyBlock = { |
| 144 | url, |
| 145 | pid: process.pid, |
| 146 | started_at: new Date().toISOString(), |
| 147 | token: this.token, |
| 148 | }; |
| 149 | if (this._priorPreviousTokens && this._priorPreviousTokens.length) { |
| 150 | proxyBlock.previous_tokens = this._priorPreviousTokens; |
| 151 | } |
| 152 | writeSettings({ proxy: proxyBlock }); |
| 153 | if (this.clientSettings) { |
| 154 | try { |
| 155 | const syncResult = syncClaudeProxySettings({ |
| 156 | ...clientSettingsOpts, |
| 157 | url, |
| 158 | port: this.basePort, |
| 159 | token: this.token, |
| 160 | runtimeEnv: process.env, |
| 161 | }); |
| 162 | if (syncResult.synced && syncResult.changed) { |
| 163 | this.logger.log(`[proxy] Synced Claude client settings at ${syncResult.file}`); |
| 164 | } else if (syncResult.reason === 'invalid_settings_json') { |
| 165 | this.logger.warn?.(`[proxy] Skipped Claude client settings sync because ${syncResult.file} is not valid JSON`); |
| 166 | } |
| 167 | } catch (err) { |
| 168 | this.logger.warn?.('[proxy] Claude client settings sync failed: ' + (err && err.message ? err.message : err)); |
nothing calls this directly
no test coverage detected