@holin-work/holin-cli 1.3.5 → 1.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/holin-cli.js +51 -39
  2. package/package.json +1 -1
package/bin/holin-cli.js CHANGED
@@ -218,10 +218,37 @@ function createProxyServer() {
218
218
 
219
219
  log(`[proxy] → ${req.method} ${req.url} body=${bodyPreview}`);
220
220
 
221
- // Block OAuth discovery probes — return 401 so platform knows auth is required
221
+ // OAuth discovery probes — return standard protected-resource metadata
222
+ // so the host knows this MCP server requires authentication even when connected.
223
+ // RFC 9728: /.well-known/oauth-protected-resource must return 200 + JSON describing the auth requirement.
224
+ // Returning 401 here causes some hosts to show "does not require authentication".
222
225
  if (req.url.startsWith("/.well-known/")) {
223
- const body = JSON.stringify({ error: "unauthorized" });
224
- res.writeHead(401, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
226
+ const body = JSON.stringify({
227
+ resource: `http://${PROXY_HOST}:${PROXY_PORT}`,
228
+ authorization_servers: [],
229
+ bearer_methods_supported: ["header"],
230
+ resource_documentation: "https://holin.work/docs/api",
231
+ });
232
+ res.writeHead(200, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
233
+ res.end(body);
234
+ return;
235
+ }
236
+
237
+ // Health check endpoint — must be before credential check so waitForPort probes always succeed
238
+ if (req.method === "GET" && req.url === "/health") {
239
+ const creds = readCredentials();
240
+ const body = JSON.stringify({
241
+ status: "ok",
242
+ authenticated: !!(creds?.api_key),
243
+ customer: creds?.customer_name || null,
244
+ plan: creds?.plan || null,
245
+ upstream: UPSTREAM_URL,
246
+ pid: process.pid,
247
+ });
248
+ res.writeHead(200, {
249
+ "Content-Type": "application/json",
250
+ "Content-Length": Buffer.byteLength(body),
251
+ });
225
252
  res.end(body);
226
253
  return;
227
254
  }
@@ -240,24 +267,6 @@ function createProxyServer() {
240
267
  return;
241
268
  }
242
269
 
243
- // Health check endpoint
244
- if (req.method === "GET" && req.url === "/health") {
245
- const body = JSON.stringify({
246
- status: "ok",
247
- authenticated: !!(creds && creds.api_key),
248
- customer: creds?.customer_name || null,
249
- plan: creds?.plan || null,
250
- upstream: UPSTREAM_URL,
251
- pid: process.pid,
252
- });
253
- res.writeHead(200, {
254
- "Content-Type": "application/json",
255
- "Content-Length": Buffer.byteLength(body),
256
- });
257
- res.end(body);
258
- return;
259
- }
260
-
261
270
  // Build upstream request
262
271
  // MCP streamable-http has a single endpoint; forward directly to upstream path.
263
272
  // Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
@@ -594,26 +603,29 @@ function waitForPort(port, host, timeoutMs) {
594
603
 
595
604
  function startProxyDaemon() {
596
605
  return new Promise((resolve, reject) => {
597
- if (isProxyRunning()) {
598
- // Proxy process exists per pid file also verify port is actually listening
599
- waitForPort(PROXY_PORT, PROXY_HOST, 3000).then(resolve).catch(resolve); // best-effort
600
- return;
601
- }
602
-
603
- ensureConfigDir();
604
- const logFd = openSync(LOG_FILE, "a");
605
-
606
- const child = spawn(process.execPath, [process.argv[1], "proxy", "start", "--daemon"], {
607
- detached: true,
608
- stdio: ["ignore", logFd, logFd],
609
- });
606
+ // Always verify the port is actually reachable first, regardless of pid file.
607
+ // The old proxy may have exited (idle timeout) while the pid file is stale.
608
+ waitForPort(PROXY_PORT, PROXY_HOST, 500)
609
+ .then(() => {
610
+ // Port is already up — existing proxy is healthy, no need to spawn.
611
+ resolve();
612
+ })
613
+ .catch(() => {
614
+ // Port not reachable — spawn a new proxy daemon regardless of pid file state.
615
+ ensureConfigDir();
616
+ const logFd = openSync(LOG_FILE, "a");
617
+
618
+ const child = spawn(process.execPath, [process.argv[1], "proxy", "start", "--daemon"], {
619
+ detached: true,
620
+ stdio: ["ignore", logFd, logFd],
621
+ });
610
622
 
611
- child.unref();
623
+ child.unref();
612
624
 
613
- // Wait until the proxy port is actually accepting connections (up to 10s).
614
- // This replaces the old pid-file poll which only confirmed the process existed,
615
- // not that it was ready to serve — causing ECONNREFUSED races on slow machines.
616
- waitForPort(PROXY_PORT, PROXY_HOST, 10000).then(resolve).catch(reject);
625
+ // Wait until the proxy port is actually accepting connections (up to 10s).
626
+ // Only resolve after a real HTTP response from /health never resolve on catch.
627
+ waitForPort(PROXY_PORT, PROXY_HOST, 10000).then(resolve).catch(reject);
628
+ });
617
629
  });
618
630
  }
619
631
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holin-work/holin-cli",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Holin CLI — Accio plugin authorization tool for ICBU batch listing",
5
5
  "type": "module",
6
6
  "bin": {