@cloudgrid-io/mcp 0.21.12 → 0.21.14

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudgrid-io/mcp",
3
- "version": "0.21.12",
3
+ "version": "0.21.14",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -73,6 +73,7 @@
73
73
  "test:web-restart-recovery": "node --test test/web-restart-recovery.test.mjs",
74
74
  "test:oauth-client-persistence": "node --test test/oauth-client-persistence.test.mjs",
75
75
  "test:oauth-token-exchange": "node --test test/oauth-token-exchange.test.mjs",
76
+ "test:oauth-observability": "node --test test/oauth-flow-observability.test.mjs",
76
77
  "test:web-capabilities-log": "node --test test/web-capabilities-log.test.mjs",
77
78
  "test:internal-refs": "node --test test/no-internal-refs.test.mjs",
78
79
  "test:login-app": "node test/login-app.test.mjs",
package/src/auth.js CHANGED
@@ -45,8 +45,23 @@ export function newLoginCode() {
45
45
  }
46
46
 
47
47
  // The sign-in URL the user opens in a browser — always the public host.
48
+ //
49
+ // Two independent params, matching what the CLI sends (cloudgrid packages/cli
50
+ // commands/login.ts, #2979):
51
+ //
52
+ // source=mcp signup ATTRIBUTION. Stamped as `signup_source` on the
53
+ // user row, and only when this login CREATES the user.
54
+ // arrival=ai_tool the onboarding PATH selector. Nothing to do with source.
55
+ //
56
+ // `arrival` is sent explicitly rather than left to the server. The API defaults
57
+ // a missing arrival to `website`, which is the seven-screen browser onboarding
58
+ // instead of the two-screen path that ends back in the tool — that default is
59
+ // exactly what bit the CLI before #2979 added it there. The API does currently
60
+ // infer `ai_tool` for source=mcp, so this changes nothing observable today; it
61
+ // stops the flow depending on an inference we do not own and would not notice
62
+ // losing.
48
63
  export function buildLoginUrl(code) {
49
- return `${PUBLIC_API_BASE}/auth/login?code=${encodeURIComponent(code)}&source=mcp`;
64
+ return `${PUBLIC_API_BASE}/auth/login?code=${encodeURIComponent(code)}&source=mcp&arrival=ai_tool`;
50
65
  }
51
66
 
52
67
  const CERT_ERROR_GUIDANCE =
package/src/oauth.js CHANGED
@@ -33,6 +33,31 @@ function b64url(buf) {
33
33
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
34
  }
35
35
 
36
+ // Observability helpers (#345). The OAuth flow was only observable on the paths
37
+ // it did NOT take — a refused token exchange logs (denyToken), but a successful
38
+ // one and the authorize arrival logged nothing, so "no log line" meant both
39
+ // "never happened" and "worked fine". These emit SHAPE and PRESENCE, never
40
+ // values: this is an auth path and a log aggregator is not private. See the
41
+ // issue's redaction list — code, verifier, code_challenge value, JWT, secret,
42
+ // the full client_id, and the full redirect_uri all stay out of the log.
43
+
44
+ // A ~12-char prefix correlates lines without printing the signed client_id,
45
+ // which embeds the redirect set.
46
+ function clientIdPrefix(clientId) {
47
+ return clientId ? `${String(clientId).slice(0, 12)}…` : "absent";
48
+ }
49
+
50
+ // The HOST is the diagnostic value; the path can carry a per-install identifier,
51
+ // so it is dropped.
52
+ function hostOf(uri) {
53
+ if (!uri) return "absent";
54
+ try {
55
+ return new URL(String(uri)).host;
56
+ } catch {
57
+ return "invalid";
58
+ }
59
+ }
60
+
36
61
  // --- Stateless client registration (#3060) -----------------------------------
37
62
  // A `client_id` is a signed token that CARRIES its own redirect-URI set, not a
38
63
  // random handle into a process-local Map. Registration signs the (sorted,
@@ -278,6 +303,16 @@ export function mountOAuth(app, publicBase, opts = {}) {
278
303
  app.get("/oauth/authorize", (req, res) => {
279
304
  sweep(authSessions);
280
305
  const { client_id, redirect_uri, state, code_challenge, code_challenge_method, response_type } = req.query;
306
+ // Observability (#345): the request SHAPE the client actually sent, logged
307
+ // before any check so a rejected request is still visible. Presence, not
308
+ // values — nothing here is reusable in a log aggregator.
309
+ console.error(
310
+ `[oauth] authorize request: client_id=${clientIdPrefix(client_id)} ` +
311
+ `response_type=${response_type ? String(response_type) : "absent"} ` +
312
+ `state=${state ? "present" : "absent"} ` +
313
+ `pkce=${code_challenge ? (code_challenge_method ? String(code_challenge_method) : "present(no-method)") : "absent"} ` +
314
+ `redirect_host=${hostOf(redirect_uri)}`,
315
+ );
281
316
  const client = verifyClientId(client_id, clientSecrets.secrets);
282
317
  if (!client || !client.redirect_uris.includes(String(redirect_uri))) {
283
318
  res.status(400).send("Unknown client or redirect_uri. Re-add the connector and try again.");
@@ -331,6 +366,10 @@ export function mountOAuth(app, publicBase, opts = {}) {
331
366
  console.error("[oauth] authorization code issued after sign-in; awaiting token exchange");
332
367
  const sep = sess.redirect_uri.includes("?") ? "&" : "?";
333
368
  const redirect = `${sess.redirect_uri}${sep}code=${encodeURIComponent(code)}${sess.state ? `&state=${encodeURIComponent(sess.state)}` : ""}`;
369
+ // Observability (#345): we handed control back to the client, and to
370
+ // where. The host only — the full redirect carries the code and a
371
+ // per-install path.
372
+ console.error(`[oauth] redirect handed back to client; redirect_host=${hostOf(sess.redirect_uri)}`);
334
373
  res.json({ status: "ready", redirect });
335
374
  return;
336
375
  }
@@ -386,13 +425,15 @@ export function mountOAuth(app, publicBase, opts = {}) {
386
425
  denyToken(res, "unsupported_grant_type", "grant_type must be authorization_code", `(got ${JSON.stringify(grant_type ?? null)})`);
387
426
  return;
388
427
  }
389
- const rec = authCodes.get(String(code));
428
+ const codeKey = String(code);
429
+ const rec = authCodes.get(codeKey);
390
430
  if (!rec) {
391
431
  // Also the retry case: codes are single-use, so a second exchange of a
392
432
  // code that already succeeded lands here.
393
433
  denyToken(res, "invalid_grant", "authorization code is unknown, expired or already exchanged");
394
434
  return;
395
435
  }
436
+ authCodes.delete(codeKey); // single use — consume before validation so a failed attempt cannot be retried
396
437
  if (rec.client_id !== String(client_id)) {
397
438
  denyToken(
398
439
  res,
@@ -412,9 +453,12 @@ export function mountOAuth(app, publicBase, opts = {}) {
412
453
  denyToken(res, "invalid_grant", code_verifier === undefined ? "code_verifier is required (PKCE)" : "PKCE verification failed");
413
454
  return;
414
455
  }
415
- authCodes.delete(String(code)); // single use
416
456
  const claims = decodeJwt(rec.jwt);
417
457
  const expiresIn = claims.exp ? Math.max(60, claims.exp - Math.floor(Date.now() / 1000)) : 30 * 86400;
458
+ // Observability (#345): the success path. Without this, a successful
459
+ // exchange and total silence were the same observation. The token is never
460
+ // logged; client_id is a prefix only.
461
+ console.error(`[oauth] token exchange succeeded; access token issued (client_id=${clientIdPrefix(client_id)})`);
418
462
  res.json({ access_token: rec.jwt, token_type: "Bearer", expires_in: expiresIn, scope: "cloudgrid" });
419
463
  });
420
464
  }