@holin-work/holin-cli 1.1.2 → 1.2.0

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 +93 -24
  2. package/package.json +1 -1
package/bin/holin-cli.js CHANGED
@@ -119,21 +119,87 @@ async function pingVerify(apiKey) {
119
119
  throw new Error("Unexpected response format from MCP server");
120
120
  }
121
121
 
122
+ /**
123
+ * Call set_holin_credentials on MCP server to establish server-side session.
124
+ * This is required for cli-login mode: Accio sends requests without API key,
125
+ * server reads key from session established by this call.
126
+ */
127
+ async function setCredentials(apiKey) {
128
+ const res = await fetch(UPSTREAM_URL, {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ Accept: "application/json, text/event-stream",
133
+ "X-API-Key": apiKey,
134
+ },
135
+ body: JSON.stringify({
136
+ jsonrpc: "2.0",
137
+ id: 2,
138
+ method: "tools/call",
139
+ params: { name: "set_holin_credentials", arguments: { api_key: apiKey } },
140
+ }),
141
+ signal: AbortSignal.timeout(10000),
142
+ });
143
+
144
+ if (!res.ok) throw new Error(`MCP server responded ${res.status}`);
145
+
146
+ const raw = await res.text();
147
+ for (const line of raw.split("\n")) {
148
+ const trimmed = line.trim();
149
+ if (trimmed.startsWith("data:")) {
150
+ const envelope = JSON.parse(trimmed.slice(5).trim());
151
+ if (envelope?.error) {
152
+ throw new Error(envelope.error.message || "set_holin_credentials failed");
153
+ }
154
+ const text = envelope?.result?.content?.[0]?.text;
155
+ if (text) return JSON.parse(text);
156
+ return { ok: true };
157
+ }
158
+ }
159
+ throw new Error("Unexpected response format from set_holin_credentials");
160
+ }
161
+
122
162
  // ── Proxy Server ─────────────────────────────────────────────────────────────
123
163
 
124
164
  function createProxyServer() {
125
165
  const server = http.createServer((req, res) => {
126
- // Block OAuth discovery probes — return 404 so platform skips OAuth flow
127
- if (req.url.startsWith("/.well-known/")) {
128
- const body = JSON.stringify({ error: "not_found" });
129
- res.writeHead(404, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
166
+ // Collect request body for logging
167
+ const chunks = [];
168
+ req.on("data", (c) => chunks.push(c));
169
+ req.on("end", () => {
170
+ const rawBody = Buffer.concat(chunks).toString("utf8");
171
+ let bodyPreview = rawBody.slice(0, 300);
172
+ try {
173
+ const parsed = JSON.parse(rawBody);
174
+ bodyPreview = JSON.stringify({ method: parsed.method, id: parsed.id, params: parsed.params ? Object.keys(parsed.params) : undefined });
175
+ } catch { /* not JSON */ }
176
+
177
+ log(`[proxy] ${req.method} ${req.url} body=${bodyPreview}`);
178
+
179
+ // Block OAuth discovery probes — return 401 so platform knows auth is required
180
+ if (req.url.startsWith("/.well-known/")) {
181
+ const body = JSON.stringify({ error: "unauthorized" });
182
+ res.writeHead(401, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
183
+ res.end(body);
184
+ return;
185
+ }
186
+
187
+ // Read credentials on every request (hot-reload support)
188
+ const creds = readCredentials();
189
+ if (!creds?.api_key) {
190
+ // No credentials: return 401 so platform knows auth is required (not "no auth needed")
191
+ const body = JSON.stringify({ error: "unauthorized", message: "Run 'holin-cli auth login' first." });
192
+ res.writeHead(401, {
193
+ "Content-Type": "application/json",
194
+ "Content-Length": Buffer.byteLength(body),
195
+ "WWW-Authenticate": "Bearer realm=\"Holin API\"",
196
+ });
130
197
  res.end(body);
131
198
  return;
132
199
  }
133
200
 
134
201
  // Health check endpoint
135
202
  if (req.method === "GET" && req.url === "/health") {
136
- const creds = readCredentials();
137
203
  const body = JSON.stringify({
138
204
  status: "ok",
139
205
  authenticated: !!(creds && creds.api_key),
@@ -150,35 +216,27 @@ function createProxyServer() {
150
216
  return;
151
217
  }
152
218
 
153
- // Read credentials on every request (hot-reload support)
154
- const creds = readCredentials();
155
- if (!creds?.api_key) {
156
- const body = JSON.stringify({ error: "not_authenticated", message: "Run 'holin-cli auth login' first." });
157
- res.writeHead(401, {
158
- "Content-Type": "application/json",
159
- "Content-Length": Buffer.byteLength(body),
160
- });
161
- res.end(body);
162
- return;
163
- }
164
-
165
219
  // Build upstream request
166
220
  // MCP streamable-http has a single endpoint; forward directly to upstream path.
167
221
  // Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
168
222
  const upstreamUrl = new URL(UPSTREAM_URL);
223
+ const bodyBuffer = Buffer.concat(chunks);
224
+ const forwardHeaders = { ...req.headers };
225
+ delete forwardHeaders["transfer-encoding"];
226
+ forwardHeaders["content-length"] = bodyBuffer.length;
227
+ forwardHeaders["host"] = upstreamUrl.hostname;
228
+ forwardHeaders["X-API-Key"] = creds.api_key;
229
+
169
230
  const options = {
170
231
  hostname: upstreamUrl.hostname,
171
232
  port: upstreamUrl.port || 443,
172
233
  path: upstreamUrl.pathname,
173
234
  method: req.method,
174
- headers: {
175
- ...req.headers,
176
- host: upstreamUrl.hostname,
177
- "X-API-Key": creds.api_key,
178
- },
235
+ headers: forwardHeaders,
179
236
  };
180
237
 
181
238
  const proxyReq = https.request(options, (proxyRes) => {
239
+ log(`[proxy] ← ${proxyRes.statusCode} content-type=${proxyRes.headers["content-type"]}`);
182
240
  // Forward response headers (strip content-length for chunked SSE)
183
241
  const headers = { ...proxyRes.headers };
184
242
  delete headers["content-length"];
@@ -210,8 +268,10 @@ function createProxyServer() {
210
268
  }
211
269
  });
212
270
 
213
- req.pipe(proxyReq);
214
- });
271
+ proxyReq.write(bodyBuffer);
272
+ proxyReq.end();
273
+ }); // end req.on("end")
274
+ }); // end http.createServer
215
275
 
216
276
  return server;
217
277
  }
@@ -254,6 +314,15 @@ async function cmdAuthLogin() {
254
314
  writeCredentials(creds);
255
315
  console.log(`[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`);
256
316
 
317
+ // Establish server-side session via set_holin_credentials
318
+ // Accio sends MCP requests without API key; server reads key from this session.
319
+ try {
320
+ await setCredentials(apiKey);
321
+ console.log("[holin-cli] Server-side session established.");
322
+ } catch (err) {
323
+ console.error(`[holin-cli] Warning: Failed to establish server session — ${err.message}`);
324
+ }
325
+
257
326
  // Auto-start proxy in background (detached), wait until it's listening
258
327
  try {
259
328
  await startProxyDaemon();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holin-work/holin-cli",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "Holin CLI — Accio plugin authorization tool for ICBU batch listing",
5
5
  "type": "module",
6
6
  "bin": {