@holin-work/holin-cli 1.1.3 → 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 +92 -24
  2. package/package.json +1 -1
package/bin/holin-cli.js CHANGED
@@ -119,28 +119,81 @@ 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 401 so platform knows auth is required
127
- if (req.url.startsWith("/.well-known/")) {
128
- const body = JSON.stringify({ error: "unauthorized" });
129
- res.writeHead(401, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body) });
130
- res.end(body);
131
- return;
132
- }
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
+ }
133
186
 
134
- // Read credentials on every request (hot-reload support)
135
- const creds = readCredentials();
136
- if (!creds?.api_key) {
137
- // No credentials: return 401 so platform knows auth is required (not "no auth needed")
138
- const body = JSON.stringify({ error: "unauthorized", message: "Run 'holin-cli auth login' first." });
139
- res.writeHead(401, {
140
- "Content-Type": "application/json",
141
- "Content-Length": Buffer.byteLength(body),
142
- "WWW-Authenticate": "Bearer realm=\"Holin API\"",
143
- });
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
+ });
144
197
  res.end(body);
145
198
  return;
146
199
  }
@@ -167,19 +220,23 @@ function createProxyServer() {
167
220
  // MCP streamable-http has a single endpoint; forward directly to upstream path.
168
221
  // Accio requests http://127.0.0.1:18787/mcp → upstream https://api.holin.work/icbu/mcp
169
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
+
170
230
  const options = {
171
231
  hostname: upstreamUrl.hostname,
172
232
  port: upstreamUrl.port || 443,
173
233
  path: upstreamUrl.pathname,
174
234
  method: req.method,
175
- headers: {
176
- ...req.headers,
177
- host: upstreamUrl.hostname,
178
- "X-API-Key": creds.api_key,
179
- },
235
+ headers: forwardHeaders,
180
236
  };
181
237
 
182
238
  const proxyReq = https.request(options, (proxyRes) => {
239
+ log(`[proxy] ← ${proxyRes.statusCode} content-type=${proxyRes.headers["content-type"]}`);
183
240
  // Forward response headers (strip content-length for chunked SSE)
184
241
  const headers = { ...proxyRes.headers };
185
242
  delete headers["content-length"];
@@ -211,8 +268,10 @@ function createProxyServer() {
211
268
  }
212
269
  });
213
270
 
214
- req.pipe(proxyReq);
215
- });
271
+ proxyReq.write(bodyBuffer);
272
+ proxyReq.end();
273
+ }); // end req.on("end")
274
+ }); // end http.createServer
216
275
 
217
276
  return server;
218
277
  }
@@ -255,6 +314,15 @@ async function cmdAuthLogin() {
255
314
  writeCredentials(creds);
256
315
  console.log(`[holin-cli] Connected: ${creds.customer_name || creds.customer_id} (${creds.plan})`);
257
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
+
258
326
  // Auto-start proxy in background (detached), wait until it's listening
259
327
  try {
260
328
  await startProxyDaemon();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holin-work/holin-cli",
3
- "version": "1.1.3",
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": {