@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 +2 -1
- package/src/auth.js +16 -1
- package/src/oauth.js +46 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.21.
|
|
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
|
|
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
|
}
|