@ggui-ai/mcp-server 0.2.0-alpha.4 → 0.4.0-rc.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.
- package/dist/admin-blueprints-transport.d.ts.map +1 -1
- package/dist/admin-blueprints-transport.js +2 -1
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -1
- package/dist/admin-oauth-providers-transport.js +7 -5
- package/dist/api-renders-routes.d.ts +85 -0
- package/dist/api-renders-routes.d.ts.map +1 -0
- package/dist/api-renders-routes.js +372 -0
- package/dist/build-mcp.d.ts +2 -2
- package/dist/build-mcp.d.ts.map +1 -1
- package/dist/build-mcp.js +64 -5
- package/dist/code-routes.d.ts +47 -0
- package/dist/code-routes.d.ts.map +1 -0
- package/dist/code-routes.js +81 -0
- package/dist/code-store-fs.js +2 -2
- package/dist/console-auth.d.ts +10 -10
- package/dist/console-auth.d.ts.map +1 -1
- package/dist/console-auth.js +5 -5
- package/dist/console-blueprint-routes.d.ts +71 -0
- package/dist/console-blueprint-routes.d.ts.map +1 -0
- package/dist/console-blueprint-routes.js +348 -0
- package/dist/console-chat-routes.d.ts +80 -0
- package/dist/console-chat-routes.d.ts.map +1 -0
- package/dist/console-chat-routes.js +204 -0
- package/dist/console-config-routes.d.ts +37 -0
- package/dist/console-config-routes.d.ts.map +1 -0
- package/dist/console-config-routes.js +91 -0
- package/dist/console-headers.d.ts +1 -1
- package/dist/console-info-routes.d.ts +84 -0
- package/dist/console-info-routes.d.ts.map +1 -0
- package/dist/console-info-routes.js +135 -0
- package/dist/console-keys-routes.d.ts +50 -0
- package/dist/console-keys-routes.d.ts.map +1 -0
- package/dist/console-keys-routes.js +222 -0
- package/dist/console-llm-keys-routes.d.ts +47 -0
- package/dist/console-llm-keys-routes.d.ts.map +1 -0
- package/dist/console-llm-keys-routes.js +443 -0
- package/dist/console-mcp-tools-routes.d.ts +41 -0
- package/dist/console-mcp-tools-routes.d.ts.map +1 -0
- package/dist/console-mcp-tools-routes.js +60 -0
- package/dist/console-registry-routes.d.ts +66 -0
- package/dist/console-registry-routes.d.ts.map +1 -0
- package/dist/console-registry-routes.js +276 -0
- package/dist/console-session-routes.d.ts +89 -0
- package/dist/console-session-routes.d.ts.map +1 -0
- package/dist/console-session-routes.js +385 -0
- package/dist/console-sessions-routes.d.ts +52 -0
- package/dist/console-sessions-routes.d.ts.map +1 -0
- package/dist/console-sessions-routes.js +106 -0
- package/dist/console-static-routes.d.ts +54 -0
- package/dist/console-static-routes.d.ts.map +1 -0
- package/dist/console-static-routes.js +190 -0
- package/dist/console-theme-routes.d.ts +3 -3
- package/dist/console-theme-routes.js +1 -1
- package/dist/console-timeline.d.ts +5 -5
- package/dist/console-timeline.d.ts.map +1 -1
- package/dist/console-timeline.js +27 -26
- package/dist/console-welcome.js +2 -2
- package/dist/email-login.d.ts.map +1 -1
- package/dist/email-login.js +2 -3
- package/dist/ggui-session-channel/action-ingress.d.ts +54 -0
- package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -0
- package/dist/ggui-session-channel/action-ingress.js +228 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts +97 -0
- package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -0
- package/dist/ggui-session-channel/channel-subscriptions.js +224 -0
- package/dist/ggui-session-channel/internal-types.d.ts +102 -0
- package/dist/ggui-session-channel/internal-types.d.ts.map +1 -0
- package/dist/ggui-session-channel/internal-types.js +6 -0
- package/dist/ggui-session-channel/outbound.d.ts +81 -0
- package/dist/ggui-session-channel/outbound.d.ts.map +1 -0
- package/dist/ggui-session-channel/outbound.js +174 -0
- package/dist/ggui-session-channel/socket-router.d.ts +38 -0
- package/dist/ggui-session-channel/socket-router.d.ts.map +1 -0
- package/dist/ggui-session-channel/socket-router.js +213 -0
- package/dist/ggui-session-channel/subscribe.d.ts +165 -0
- package/dist/ggui-session-channel/subscribe.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscribe.js +370 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts +40 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -0
- package/dist/ggui-session-channel/subscriber-lifecycle.js +123 -0
- package/dist/ggui-session-channel.d.ts +425 -0
- package/dist/ggui-session-channel.d.ts.map +1 -0
- package/dist/ggui-session-channel.js +262 -0
- package/dist/health-routes.d.ts +76 -0
- package/dist/health-routes.d.ts.map +1 -0
- package/dist/health-routes.js +145 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -9
- package/dist/instructions-presets.d.ts +3 -3
- package/dist/instructions-presets.js +24 -24
- package/dist/llm-backed-negotiator.d.ts +68 -67
- package/dist/llm-backed-negotiator.d.ts.map +1 -1
- package/dist/llm-backed-negotiator.js +82 -221
- package/dist/mcp-apps-outbound.d.ts +47 -48
- package/dist/mcp-apps-outbound.d.ts.map +1 -1
- package/dist/mcp-apps-outbound.js +154 -177
- package/dist/mcp-endpoint-routes.d.ts +88 -0
- package/dist/mcp-endpoint-routes.d.ts.map +1 -0
- package/dist/mcp-endpoint-routes.js +359 -0
- package/dist/mcp-mounts.d.ts +2 -76
- package/dist/mcp-mounts.d.ts.map +1 -1
- package/dist/mcp-mounts.js +0 -76
- package/dist/oauth-as-routes.d.ts +60 -0
- package/dist/oauth-as-routes.d.ts.map +1 -0
- package/dist/oauth-as-routes.js +82 -0
- package/dist/oauth-clients-routes.d.ts +39 -0
- package/dist/oauth-clients-routes.d.ts.map +1 -0
- package/dist/oauth-clients-routes.js +87 -0
- package/dist/oauth-login-types.d.ts +1 -20
- package/dist/oauth-login-types.d.ts.map +1 -1
- package/dist/oauth-login-types.js +30 -7
- package/dist/oauth-login.d.ts.map +1 -1
- package/dist/oauth-login.js +3 -2
- package/dist/oauth-providers-store.d.ts.map +1 -1
- package/dist/oauth-providers-store.js +5 -5
- package/dist/oauth.d.ts +9 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +41 -19
- package/dist/pairing-transport.d.ts.map +1 -1
- package/dist/pairing-transport.js +2 -1
- package/dist/request-context.d.ts +2 -2
- package/dist/request-context.js +2 -2
- package/dist/reserved-validators.d.ts.map +1 -1
- package/dist/reserved-validators.js +9 -1
- package/dist/route-param.d.ts +9 -0
- package/dist/route-param.d.ts.map +1 -0
- package/dist/route-param.js +10 -0
- package/dist/runtime-bundle-route.d.ts +43 -0
- package/dist/runtime-bundle-route.d.ts.map +1 -0
- package/dist/runtime-bundle-route.js +80 -0
- package/dist/schema-compat.d.ts +64 -62
- package/dist/schema-compat.d.ts.map +1 -1
- package/dist/schema-compat.js +23 -51
- package/dist/server.d.ts +179 -193
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +644 -3759
- package/dist/storage.d.ts +5 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +5 -5
- package/dist/thread-transport.d.ts.map +1 -1
- package/dist/thread-transport.js +4 -3
- package/dist/user-session-auth.d.ts +7 -21
- package/dist/user-session-auth.d.ts.map +1 -1
- package/dist/user-session-auth.js +7 -28
- package/package.json +16 -15
- package/dist/mcp-apps-inbound.d.ts +0 -86
- package/dist/mcp-apps-inbound.d.ts.map +0 -1
- package/dist/mcp-apps-inbound.js +0 -283
- package/dist/render-channel.d.ts +0 -694
- package/dist/render-channel.d.ts.map +0 -1
- package/dist/render-channel.js +0 -1775
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 + PKCE + DCR route family (per MCP spec 2025-06-18+).
|
|
3
|
+
*
|
|
4
|
+
* GET /.well-known/oauth-protected-resource — RFC 9728 metadata
|
|
5
|
+
* GET <pathPrefix>/:appId/.well-known/oauth-protected-resource
|
|
6
|
+
* — per-app metadata (mounted only with `perAppRouting`)
|
|
7
|
+
* GET /.well-known/oauth-authorization-server — RFC 8414 metadata
|
|
8
|
+
* POST /oauth/register — RFC 7591 DCR
|
|
9
|
+
* GET /oauth/authorize — consent page
|
|
10
|
+
* POST /oauth/authorize — consent submit
|
|
11
|
+
* POST /oauth/token — token exchange
|
|
12
|
+
*
|
|
13
|
+
* The handlers themselves live in `./oauth.ts`; this module owns the
|
|
14
|
+
* Express mounting only. Mounted when the composer enables OAuth —
|
|
15
|
+
* without it, pure-bearer clients still work but OAuth-discovery
|
|
16
|
+
* clients (Claude Desktop, claude.ai, Goose, etc.) bail with
|
|
17
|
+
* "couldn't reach".
|
|
18
|
+
*/
|
|
19
|
+
import type { AuthAdapter, PairingService } from "@ggui-ai/mcp-server-core";
|
|
20
|
+
import type { Express } from "express";
|
|
21
|
+
import { type OAuthConfig, type OAuthStorage } from "./oauth.js";
|
|
22
|
+
interface MountOptions {
|
|
23
|
+
/** Express app to mount onto. */
|
|
24
|
+
readonly app: Express;
|
|
25
|
+
/** Resolved OAuth config (issuer override, validateResource, …). */
|
|
26
|
+
readonly oauthConfig: OAuthConfig;
|
|
27
|
+
/** Client / code / token storage the handlers persist into. */
|
|
28
|
+
readonly oauthStorage: OAuthStorage;
|
|
29
|
+
/** Universal MCP endpoint path — the default RFC 9728 resource. */
|
|
30
|
+
readonly universalMcpPath: string;
|
|
31
|
+
/**
|
|
32
|
+
* Per-tenant URL routing shape. When configured, a second
|
|
33
|
+
* well-known endpoint mounts under the same path prefix the
|
|
34
|
+
* per-app `/mcp` handler lives on.
|
|
35
|
+
*/
|
|
36
|
+
readonly perAppRouting?: {
|
|
37
|
+
readonly paramName: string;
|
|
38
|
+
readonly pathPrefix?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Auth adapter the consent-submit handler resolves bearers against. */
|
|
41
|
+
readonly auth: AuthAdapter;
|
|
42
|
+
/**
|
|
43
|
+
* Late-bound pairing-service reference. The pairing service is
|
|
44
|
+
* constructed AFTER these routes mount (declaration-order in the
|
|
45
|
+
* composer), so the consent-submit handler reads through a getter
|
|
46
|
+
* on every request. Safe because requests only arrive after
|
|
47
|
+
* `listen()`, which is strictly after composition completes.
|
|
48
|
+
*/
|
|
49
|
+
readonly getPairingService: () => PairingService | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Mount the OAuth discovery + auth + token endpoints onto the express
|
|
53
|
+
* app. Also flips `trust proxy` on — nginx-ingress + ALB both
|
|
54
|
+
* terminate TLS upstream; without trust-proxy the metadata advertises
|
|
55
|
+
* `http://` instead of `https://` which breaks PKCE (browsers refuse
|
|
56
|
+
* insecure flows). Returns nothing — the routes self-register.
|
|
57
|
+
*/
|
|
58
|
+
export declare function mountOAuthAuthorizationServerRoutes(opts: MountOptions): void;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=oauth-as-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-as-routes.d.ts","sourceRoot":"","sources":["../src/oauth-as-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAOL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,mEAAmE;IACnE,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE;QACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;KAC9B,CAAC;IACF,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,iBAAiB,EAAE,MAAM,cAAc,GAAG,IAAI,CAAC;CACzD;AAED;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAwE5E"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 + PKCE + DCR route family (per MCP spec 2025-06-18+).
|
|
3
|
+
*
|
|
4
|
+
* GET /.well-known/oauth-protected-resource — RFC 9728 metadata
|
|
5
|
+
* GET <pathPrefix>/:appId/.well-known/oauth-protected-resource
|
|
6
|
+
* — per-app metadata (mounted only with `perAppRouting`)
|
|
7
|
+
* GET /.well-known/oauth-authorization-server — RFC 8414 metadata
|
|
8
|
+
* POST /oauth/register — RFC 7591 DCR
|
|
9
|
+
* GET /oauth/authorize — consent page
|
|
10
|
+
* POST /oauth/authorize — consent submit
|
|
11
|
+
* POST /oauth/token — token exchange
|
|
12
|
+
*
|
|
13
|
+
* The handlers themselves live in `./oauth.ts`; this module owns the
|
|
14
|
+
* Express mounting only. Mounted when the composer enables OAuth —
|
|
15
|
+
* without it, pure-bearer clients still work but OAuth-discovery
|
|
16
|
+
* clients (Claude Desktop, claude.ai, Goose, etc.) bail with
|
|
17
|
+
* "couldn't reach".
|
|
18
|
+
*/
|
|
19
|
+
import { handleAuthorizationServerMetadata, handleAuthorizeGet, handleAuthorizePost, handleProtectedResourceMetadata, handleRegister, handleToken, } from "./oauth.js";
|
|
20
|
+
/**
|
|
21
|
+
* Mount the OAuth discovery + auth + token endpoints onto the express
|
|
22
|
+
* app. Also flips `trust proxy` on — nginx-ingress + ALB both
|
|
23
|
+
* terminate TLS upstream; without trust-proxy the metadata advertises
|
|
24
|
+
* `http://` instead of `https://` which breaks PKCE (browsers refuse
|
|
25
|
+
* insecure flows). Returns nothing — the routes self-register.
|
|
26
|
+
*/
|
|
27
|
+
export function mountOAuthAuthorizationServerRoutes(opts) {
|
|
28
|
+
const { app, oauthConfig, oauthStorage, universalMcpPath, perAppRouting, auth, getPairingService, } = opts;
|
|
29
|
+
// `trust proxy` so req.protocol + req.host honor X-Forwarded-Proto +
|
|
30
|
+
// X-Forwarded-Host.
|
|
31
|
+
app.set("trust proxy", true);
|
|
32
|
+
app.get("/.well-known/oauth-protected-resource", (req, res) => handleProtectedResourceMetadata(req, res, oauthConfig, universalMcpPath));
|
|
33
|
+
// Per-app protected-resource metadata (RFC 9728 per-resource
|
|
34
|
+
// discovery). When `perAppRouting` is configured,
|
|
35
|
+
// mount a second well-known endpoint under the same path prefix
|
|
36
|
+
// the per-app `/mcp` handler lives on. Each `appId` gets its own
|
|
37
|
+
// metadata document with `resource: ${issuer}${pathPrefix}/${appId}`
|
|
38
|
+
// so claude.ai's discovery flow against `mcp.ggui.ai/apps/<appId>`
|
|
39
|
+
// sees a per-app resource rather than the universal one. The
|
|
40
|
+
// shared `authorization_servers: [issuer]` lets the auth server
|
|
41
|
+
// (also us) issue tokens bound to either resource via RFC 8707
|
|
42
|
+
// resource indicators.
|
|
43
|
+
if (perAppRouting !== undefined) {
|
|
44
|
+
const { paramName, pathPrefix = "" } = perAppRouting;
|
|
45
|
+
// `path-to-regexp` v8 (express@5) removed the `:param(pattern)`
|
|
46
|
+
// inline-regex route syntax — registering one throws at startup.
|
|
47
|
+
// Per-app routes now mount with a PLAIN named param; `paramPattern`
|
|
48
|
+
// is enforced by a single `app.param` validator (registered with
|
|
49
|
+
// the per-app MCP route in the MCP-endpoint family) that 404s any
|
|
50
|
+
// value failing a full-anchored match. Express resolves `app.param`
|
|
51
|
+
// callbacks at dispatch time for any route declaring the param,
|
|
52
|
+
// regardless of registration order, so this well-known route
|
|
53
|
+
// inherits the check even though the validator is wired elsewhere.
|
|
54
|
+
//
|
|
55
|
+
// `req.params` can't be indexed by the runtime `paramName` under
|
|
56
|
+
// the v8 param-key inference, so pin the params type to the plain
|
|
57
|
+
// string dictionary (`ParamsDictionary`) — the legitimate
|
|
58
|
+
// single-value param shape — so the `paramName` lookup resolves
|
|
59
|
+
// to `string`.
|
|
60
|
+
app.get(`${pathPrefix}/:${paramName}/.well-known/oauth-protected-resource`, (req, res) => {
|
|
61
|
+
const appId = req.params[paramName];
|
|
62
|
+
if (typeof appId !== "string" || appId.length === 0) {
|
|
63
|
+
res.status(404).json({ error: "not_found" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
handleProtectedResourceMetadata(req, res, oauthConfig, `${pathPrefix}/${appId}`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
app.get("/.well-known/oauth-authorization-server", (req, res) => handleAuthorizationServerMetadata(req, res, oauthConfig));
|
|
70
|
+
app.post("/oauth/register", (req, res) => {
|
|
71
|
+
void handleRegister(req, res, oauthConfig, oauthStorage);
|
|
72
|
+
});
|
|
73
|
+
app.get("/oauth/authorize", (req, res) => {
|
|
74
|
+
void handleAuthorizeGet(req, res, oauthConfig, oauthStorage);
|
|
75
|
+
});
|
|
76
|
+
app.post("/oauth/authorize", (req, res) => {
|
|
77
|
+
void handleAuthorizePost(req, res, oauthConfig, oauthStorage, auth, getPairingService());
|
|
78
|
+
});
|
|
79
|
+
app.post("/oauth/token", (req, res) => {
|
|
80
|
+
void handleToken(req, res, oauthStorage);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console OAuth-client management routes (the "Connected Apps"
|
|
3
|
+
* surface). Phase 1 of the Connected Apps slice — list + revoke, no
|
|
4
|
+
* manual create yet.
|
|
5
|
+
*
|
|
6
|
+
* GET /ggui/console/oauth-clients — list (oldest-first by createdAt)
|
|
7
|
+
* DELETE /ggui/console/oauth-clients/:id — revoke (idempotent)
|
|
8
|
+
*
|
|
9
|
+
* Mounted only when both console + OAuth are enabled — there's
|
|
10
|
+
* nothing to manage if OAuth is off, and the management surface
|
|
11
|
+
* belongs to the operator-facing console plane (not the public
|
|
12
|
+
* OAuth metadata endpoints). Same-origin posture: no bearer auth,
|
|
13
|
+
* matches sibling console routes (`/ggui/console/registry`,
|
|
14
|
+
* `/ggui/console/blueprints/cached/...`).
|
|
15
|
+
*
|
|
16
|
+
* Revoke caveat — see OAuthStorage.deleteClient JSDoc: revoke
|
|
17
|
+
* deletes the registration but doesn't invalidate in-flight
|
|
18
|
+
* access tokens (the current paste-key flow has access_token ===
|
|
19
|
+
* paired bearer).
|
|
20
|
+
*/
|
|
21
|
+
import type { Express } from "express";
|
|
22
|
+
import type { Logger } from "./logger.js";
|
|
23
|
+
import type { OAuthStorage } from "./oauth.js";
|
|
24
|
+
interface MountOptions {
|
|
25
|
+
/** Express app to mount onto. */
|
|
26
|
+
readonly app: Express;
|
|
27
|
+
/** OAuth storage the management surface lists/revokes against. */
|
|
28
|
+
readonly oauthStorage: OAuthStorage;
|
|
29
|
+
/** Structured logger. */
|
|
30
|
+
readonly logger: Logger;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mount `GET /ggui/console/oauth-clients` +
|
|
34
|
+
* `DELETE /ggui/console/oauth-clients/:clientId` onto the express
|
|
35
|
+
* app. Returns nothing — the routes self-register.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mountOAuthClientsRoutes(opts: MountOptions): void;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=oauth-clients-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-clients-routes.d.ts","sourceRoot":"","sources":["../src/oauth-clients-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,UAAU,YAAY;IACpB,iCAAiC;IACjC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,kEAAkE;IAClE,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAC;IACpC,yBAAyB;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CA+DhE"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console OAuth-client management routes (the "Connected Apps"
|
|
3
|
+
* surface). Phase 1 of the Connected Apps slice — list + revoke, no
|
|
4
|
+
* manual create yet.
|
|
5
|
+
*
|
|
6
|
+
* GET /ggui/console/oauth-clients — list (oldest-first by createdAt)
|
|
7
|
+
* DELETE /ggui/console/oauth-clients/:id — revoke (idempotent)
|
|
8
|
+
*
|
|
9
|
+
* Mounted only when both console + OAuth are enabled — there's
|
|
10
|
+
* nothing to manage if OAuth is off, and the management surface
|
|
11
|
+
* belongs to the operator-facing console plane (not the public
|
|
12
|
+
* OAuth metadata endpoints). Same-origin posture: no bearer auth,
|
|
13
|
+
* matches sibling console routes (`/ggui/console/registry`,
|
|
14
|
+
* `/ggui/console/blueprints/cached/...`).
|
|
15
|
+
*
|
|
16
|
+
* Revoke caveat — see OAuthStorage.deleteClient JSDoc: revoke
|
|
17
|
+
* deletes the registration but doesn't invalidate in-flight
|
|
18
|
+
* access tokens (the current paste-key flow has access_token ===
|
|
19
|
+
* paired bearer).
|
|
20
|
+
*/
|
|
21
|
+
import { applyDevtoolSecurityHeaders } from "./console-headers.js";
|
|
22
|
+
/**
|
|
23
|
+
* Mount `GET /ggui/console/oauth-clients` +
|
|
24
|
+
* `DELETE /ggui/console/oauth-clients/:clientId` onto the express
|
|
25
|
+
* app. Returns nothing — the routes self-register.
|
|
26
|
+
*/
|
|
27
|
+
export function mountOAuthClientsRoutes(opts) {
|
|
28
|
+
const { app, oauthStorage, logger } = opts;
|
|
29
|
+
app.get("/ggui/console/oauth-clients", async (_req, res) => {
|
|
30
|
+
applyDevtoolSecurityHeaders(res);
|
|
31
|
+
try {
|
|
32
|
+
const clients = await oauthStorage.listClients();
|
|
33
|
+
// Project to a wire shape: explicitly list every field we
|
|
34
|
+
// intend to expose so an unrelated `ClientRecord` field
|
|
35
|
+
// addition doesn't accidentally leak through this endpoint.
|
|
36
|
+
res.json({
|
|
37
|
+
clients: clients.map((c) => ({
|
|
38
|
+
clientId: c.clientId,
|
|
39
|
+
clientName: c.clientName ?? null,
|
|
40
|
+
redirectUris: c.redirectUris,
|
|
41
|
+
createdAt: c.createdAt,
|
|
42
|
+
})),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
logger.warn("console_oauth_clients_list_failed", {
|
|
47
|
+
error: String(err),
|
|
48
|
+
});
|
|
49
|
+
res.status(500).json({
|
|
50
|
+
error: "list_failed",
|
|
51
|
+
message: err instanceof Error
|
|
52
|
+
? `Client list failed — ${err.message}`
|
|
53
|
+
: `Client list failed — ${String(err)}`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
app.delete("/ggui/console/oauth-clients/:clientId", async (req, res) => {
|
|
58
|
+
applyDevtoolSecurityHeaders(res);
|
|
59
|
+
const clientId = req.params["clientId"];
|
|
60
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
61
|
+
res.status(400).json({
|
|
62
|
+
error: "missing_client_id",
|
|
63
|
+
message: "clientId required in path segment (e.g. DELETE /ggui/console/oauth-clients/abc123).",
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await oauthStorage.deleteClient(clientId);
|
|
69
|
+
// 204 whether the id was present or not — `deleteClient`
|
|
70
|
+
// is idempotent at the storage layer (matches DELETE
|
|
71
|
+
// /blueprints/cached/:id semantics).
|
|
72
|
+
res.status(204).end();
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
logger.warn("console_oauth_clients_delete_failed", {
|
|
76
|
+
error: String(err),
|
|
77
|
+
clientId,
|
|
78
|
+
});
|
|
79
|
+
res.status(500).json({
|
|
80
|
+
error: "delete_failed",
|
|
81
|
+
message: err instanceof Error
|
|
82
|
+
? `Client delete failed — ${err.message}`
|
|
83
|
+
: `Client delete failed — ${String(err)}`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
* Provider-subject is the strongest cross-provider primary key the
|
|
28
28
|
* OAuth spec gives us. Future "link account" flows are explicit.
|
|
29
29
|
*/
|
|
30
|
-
import type { AuthResult } from '@ggui-ai/mcp-server-core';
|
|
31
30
|
/**
|
|
32
31
|
* The runtime contract every concrete OAuth provider implements.
|
|
33
32
|
*
|
|
@@ -138,23 +137,5 @@ export interface OAuthProviderConfigRecord {
|
|
|
138
137
|
*/
|
|
139
138
|
readonly enabled: boolean;
|
|
140
139
|
}
|
|
141
|
-
|
|
142
|
-
* The minimal AuthAdapter call shape OAuth callbacks use to mint a
|
|
143
|
-
* bearer. `source: 'oauth'` is the canonical provenance string —
|
|
144
|
-
* audit hooks branch on this to distinguish OAuth identity from
|
|
145
|
-
* pairing-issued bearers.
|
|
146
|
-
*/
|
|
147
|
-
export type OAuthAuthResult = AuthResult & {
|
|
148
|
-
source: 'oauth';
|
|
149
|
-
};
|
|
150
|
-
/**
|
|
151
|
-
* Compose the canonical user-id namespace for an OAuth identity.
|
|
152
|
-
* `userId = '${providerId}:${providerSubject}'`. Pure helper —
|
|
153
|
-
* exported here so every consumer (routes, storage, audit hooks)
|
|
154
|
-
* computes the same thing.
|
|
155
|
-
*/
|
|
156
|
-
export declare function composeOAuthUserId(input: {
|
|
157
|
-
readonly providerId: string;
|
|
158
|
-
readonly providerSubject: string;
|
|
159
|
-
}): string;
|
|
140
|
+
export { composeOAuthUserId } from "@ggui-ai/protocol";
|
|
160
141
|
//# sourceMappingURL=oauth-login-types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-login-types.d.ts","sourceRoot":"","sources":["../src/oauth-login-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;
|
|
1
|
+
{"version":3,"file":"oauth-login-types.d.ts","sourceRoot":"","sources":["../src/oauth-login-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,kBAAkB;IACjC,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAAC;IAC/C;;;;;OAKG;IACH,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACtE;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;OAGG;IACH,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAID,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* OAuth login provider seam.
|
|
3
|
+
*
|
|
4
|
+
* Locks the contract every OAuth-login consumer composes against:
|
|
5
|
+
*
|
|
6
|
+
* - Provider implementations (`oauth-providers/{google,github}.ts`)
|
|
7
|
+
* return one of these per configured provider.
|
|
8
|
+
* - Storage (`oauth-providers-store.ts`) serializes config records
|
|
9
|
+
* and hydrates concrete providers via the registered factory.
|
|
10
|
+
* - Routes (`oauth-login.ts`) iterate the bound providers and call
|
|
11
|
+
* `authorizeUrl` / `exchangeCode` per request.
|
|
12
|
+
* - Console UI (`AdminOAuthProviders.tsx`) lists `providerId` /
|
|
13
|
+
* `displayName` per record so the operator can paste secrets
|
|
14
|
+
* into the right slot.
|
|
15
|
+
*
|
|
16
|
+
* Identity model: a successful OAuth callback mints a bearer for an
|
|
17
|
+
* `Identity` of `{ kind: 'user', userId: '${providerId}:${providerSubject}', roles: [] }`.
|
|
18
|
+
* Email is informational only — providers MAY return it (display in
|
|
19
|
+
* the operator's audit log + the user's `/settings` page), but the
|
|
20
|
+
* identity is always provider-namespaced. No email-based account
|
|
21
|
+
* linking — different providers ⇒ different identities, period.
|
|
22
|
+
*
|
|
23
|
+
* **Why no email-based linking?** A user's OAuth-provider email can
|
|
24
|
+
* change (they switch jobs, providers verify slowly). Linking by
|
|
25
|
+
* email would let an attacker who briefly controls an email at
|
|
26
|
+
* provider X impersonate the same user's account at provider Y.
|
|
27
|
+
* Provider-subject is the strongest cross-provider primary key the
|
|
28
|
+
* OAuth spec gives us. Future "link account" flows are explicit.
|
|
6
29
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
30
|
+
// composeOAuthUserId now lives in @ggui-ai/protocol (shared with the
|
|
31
|
+
// OIDC verify adapter, which can't import upward from mcp-server).
|
|
32
|
+
export { composeOAuthUserId } from "@ggui-ai/protocol";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-login.d.ts","sourceRoot":"","sources":["../src/oauth-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAO1D,OAAO,KAAK,EAEV,SAAS,EACT,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth-login.d.ts","sourceRoot":"","sources":["../src/oauth-login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,KAAK,EAAE,OAAO,EAAqB,MAAM,SAAS,CAAC;AAO1D,OAAO,KAAK,EAEV,SAAS,EACT,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAG1C,eAAO,MAAM,wBAAwB,wCAAwC,CAAC;AAC9E,eAAO,MAAM,2BAA2B,2CACE,CAAC;AAC3C;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,gCACf,CAAC;AAChC,eAAO,MAAM,sBAAsB,oBAAoB,CAAC;AAKxD,MAAM,WAAW,uBAAuB;IACtC;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EACd,aAAa,CAAC,kBAAkB,CAAC,GACjC,CAAC,MAAM,aAAa,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC3F,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kFAAkF;IAClF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,gCAAgC;IAChC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,yCAAyC;IACzC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,wDAAwD;AACxD,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,uBAAuB,GAC5B,IAAI,CAwWN"}
|
package/dist/oauth-login.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, createHmac, randomBytes, timingSafeEqual, } from 'node:crypto';
|
|
2
2
|
import { formatUserSessionCookieHeader } from './user-session-auth.js';
|
|
3
3
|
import { composeOAuthUserId, } from './oauth-login-types.js';
|
|
4
|
+
import { singleParam } from './route-param.js';
|
|
4
5
|
export const DEFAULT_OAUTH_START_PATH = '/ggui/oauth-login/:providerId/start';
|
|
5
6
|
export const DEFAULT_OAUTH_CALLBACK_PATH = '/ggui/oauth-login/:providerId/callback';
|
|
6
7
|
/**
|
|
@@ -81,7 +82,7 @@ export function mountOAuthLoginRoutes(app, opts) {
|
|
|
81
82
|
});
|
|
82
83
|
// --- GET /ggui/oauth-login/:providerId/start ---
|
|
83
84
|
app.get(startPath, async (req, res) => {
|
|
84
|
-
const providerId = req.params['providerId'];
|
|
85
|
+
const providerId = singleParam(req.params['providerId']);
|
|
85
86
|
const reqLogger = opts.logger.child({
|
|
86
87
|
route: 'GET ' + startPath,
|
|
87
88
|
providerId: providerId ?? '<missing>',
|
|
@@ -131,7 +132,7 @@ export function mountOAuthLoginRoutes(app, opts) {
|
|
|
131
132
|
});
|
|
132
133
|
// --- GET /ggui/oauth-login/:providerId/callback ---
|
|
133
134
|
app.get(callbackPath, async (req, res) => {
|
|
134
|
-
const providerId = req.params['providerId'];
|
|
135
|
+
const providerId = singleParam(req.params['providerId']);
|
|
135
136
|
const reqLogger = opts.logger.child({
|
|
136
137
|
route: 'GET ' + callbackPath,
|
|
137
138
|
providerId: providerId ?? '<missing>',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth-providers-store.d.ts","sourceRoot":"","sources":["../src/oauth-providers-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"oauth-providers-store.d.ts","sourceRoot":"","sources":["../src/oauth-providers-store.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACxE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAM1C,MAAM,WAAW,0BAA0B;IACzC,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,qEAAqE;IACrE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAC1D,qEAAqE;IACrE,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,GAAG,IAAI,CAAC,CAAC;IACnE;;;OAGG;IACH,GAAG,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAAC;IACzD,uDAAuD;IACvD,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,mEAAmE;IACnE,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3C;AAsLD,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,0BAA0B,GAC/B,mBAAmB,CA+HrB"}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { promises as fs } from 'node:fs';
|
|
16
16
|
import * as os from 'node:os';
|
|
17
17
|
import * as path from 'node:path';
|
|
18
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
18
19
|
const PROVIDER_ID_RE = /^[a-z][a-z0-9-]*$/;
|
|
19
20
|
const ENV_KEY_RE = /^GGUI_OAUTH_([A-Z0-9_]+)_CLIENT_ID$/;
|
|
20
21
|
const FILE_VERSION = '1';
|
|
@@ -112,15 +113,14 @@ async function readFileShape(filePath, logger) {
|
|
|
112
113
|
});
|
|
113
114
|
return { version: FILE_VERSION, providers: [] };
|
|
114
115
|
}
|
|
115
|
-
if (!parsed
|
|
116
|
+
if (!isRecord(parsed)) {
|
|
116
117
|
logger.warn('oauth_providers_file_corrupt', {
|
|
117
118
|
filePath,
|
|
118
119
|
reason: 'root_not_object',
|
|
119
120
|
});
|
|
120
121
|
return { version: FILE_VERSION, providers: [] };
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
-
if (!Array.isArray(obj.providers)) {
|
|
123
|
+
if (!Array.isArray(parsed['providers'])) {
|
|
124
124
|
logger.warn('oauth_providers_file_corrupt', {
|
|
125
125
|
filePath,
|
|
126
126
|
reason: 'providers_not_array',
|
|
@@ -128,8 +128,8 @@ async function readFileShape(filePath, logger) {
|
|
|
128
128
|
return { version: FILE_VERSION, providers: [] };
|
|
129
129
|
}
|
|
130
130
|
const providers = [];
|
|
131
|
-
for (const row of
|
|
132
|
-
if (!row
|
|
131
|
+
for (const row of parsed['providers']) {
|
|
132
|
+
if (!isRecord(row))
|
|
133
133
|
continue;
|
|
134
134
|
const r = row;
|
|
135
135
|
if (typeof r['providerId'] !== 'string' ||
|
package/dist/oauth.d.ts
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
* ## Storage
|
|
50
50
|
*
|
|
51
51
|
* Auth codes + DCR clients live in-memory ({@link InMemoryOAuthStorage}).
|
|
52
|
-
* For multi-replica deployments (
|
|
53
|
-
* either:
|
|
52
|
+
* For multi-replica deployments (2+ processes behind one load
|
|
53
|
+
* balancer), either:
|
|
54
54
|
* - Use nginx-ingress sticky sessions so the same pod handles both
|
|
55
55
|
* `/oauth/authorize` and `/oauth/token` (current sandbox posture).
|
|
56
56
|
* - Plug a Redis-backed {@link OAuthStorage} via the
|
|
@@ -64,7 +64,7 @@ import type { Request, Response } from 'express';
|
|
|
64
64
|
import type { AuthAdapter, PairingService } from '@ggui-ai/mcp-server-core';
|
|
65
65
|
export interface OAuthConfig {
|
|
66
66
|
/**
|
|
67
|
-
* Public origin of this server (e.g. `https://mcp.
|
|
67
|
+
* Public origin of this server (e.g. `https://mcp.example.com`). Used in
|
|
68
68
|
* discovery metadata, `WWW-Authenticate` headers, and OAuth redirects.
|
|
69
69
|
* Should NOT have a trailing slash. When absent, the server derives
|
|
70
70
|
* it from the request's `Host` header — fine for most deployments,
|
|
@@ -80,7 +80,7 @@ export interface OAuthConfig {
|
|
|
80
80
|
readonly storage?: OAuthStorage;
|
|
81
81
|
/**
|
|
82
82
|
* External consent UI to delegate the user-facing approval step to
|
|
83
|
-
* (e.g. `https://console.
|
|
83
|
+
* (e.g. `https://console.example.com/oauth/consent`). When set,
|
|
84
84
|
* `GET /oauth/authorize` returns a 302 to this URL with every OAuth
|
|
85
85
|
* query param forwarded verbatim — the consent UI then constructs an
|
|
86
86
|
* HTML form that POSTs back to `<issuer>/oauth/authorize` with the
|
|
@@ -104,8 +104,8 @@ export interface OAuthConfig {
|
|
|
104
104
|
* MCP endpoint on this deployment, `false` otherwise.
|
|
105
105
|
*
|
|
106
106
|
* For ggui this gates two shapes:
|
|
107
|
-
* - Universal: `${issuer}` (
|
|
108
|
-
* `${issuer}${universalMcpPath}` (
|
|
107
|
+
* - Universal: `${issuer}` (bare-root mount) or
|
|
108
|
+
* `${issuer}${universalMcpPath}` (the conventional `/mcp`).
|
|
109
109
|
* - Per-app: `${issuer}${perAppRouting.pathPrefix}/<appId>`
|
|
110
110
|
* where `<appId>` matches `perAppRouting.paramPattern`.
|
|
111
111
|
*
|
|
@@ -269,8 +269,9 @@ export declare function buildWwwAuthenticate(issuerUrl: string, resourcePath?: s
|
|
|
269
269
|
* in our case (we host both the resource AND the auth server).
|
|
270
270
|
*
|
|
271
271
|
* `mcpPath` (default `/mcp`) is the deployment's universal MCP route.
|
|
272
|
-
*
|
|
273
|
-
* (the domain already says "mcp");
|
|
272
|
+
* A deployment on a dedicated MCP domain may mount at the bare root
|
|
273
|
+
* `/` so URLs stay short (the domain already says "mcp"); the
|
|
274
|
+
* conventional default is `/mcp`.
|
|
274
275
|
* The trailing slash is normalized off when the path is `/` so the
|
|
275
276
|
* resource URL is `${issuer}` (no trailing slash) rather than
|
|
276
277
|
* `${issuer}/`.
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6DG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.js
CHANGED
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
* ## Storage
|
|
50
50
|
*
|
|
51
51
|
* Auth codes + DCR clients live in-memory ({@link InMemoryOAuthStorage}).
|
|
52
|
-
* For multi-replica deployments (
|
|
53
|
-
* either:
|
|
52
|
+
* For multi-replica deployments (2+ processes behind one load
|
|
53
|
+
* balancer), either:
|
|
54
54
|
* - Use nginx-ingress sticky sessions so the same pod handles both
|
|
55
55
|
* `/oauth/authorize` and `/oauth/token` (current sandbox posture).
|
|
56
56
|
* - Plug a Redis-backed {@link OAuthStorage} via the
|
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
* registrations; clients re-register transparently on next failure.
|
|
62
62
|
*/
|
|
63
63
|
import { createHash, randomBytes } from 'node:crypto';
|
|
64
|
-
import {
|
|
64
|
+
import { isRecord } from '@ggui-ai/protocol';
|
|
65
|
+
import { resolveIdentityFromHeaders, UnauthenticatedError } from './auth.js';
|
|
65
66
|
export class InMemoryOAuthStorage {
|
|
66
67
|
codes = new Map();
|
|
67
68
|
clients = new Map();
|
|
@@ -148,6 +149,27 @@ export function buildWwwAuthenticate(issuerUrl, resourcePath = '') {
|
|
|
148
149
|
// =============================================================================
|
|
149
150
|
// PKCE
|
|
150
151
|
// =============================================================================
|
|
152
|
+
/**
|
|
153
|
+
* Narrow an untrusted params source (express `req.query`, parsed
|
|
154
|
+
* request body) to the string-valued record the OAuth handlers
|
|
155
|
+
* consume. Express query values can be `string | string[] | ParsedQs
|
|
156
|
+
* | ParsedQs[]` (duplicate keys yield arrays under both query
|
|
157
|
+
* parsers) and JSON bodies can carry arbitrary shapes — only single
|
|
158
|
+
* string values are meaningful OAuth params, so anything else is
|
|
159
|
+
* DROPPED at this trust boundary. The downstream required-param
|
|
160
|
+
* validation then rejects the request with a clean 400 instead of
|
|
161
|
+
* flowing a non-string through string-typed code.
|
|
162
|
+
*/
|
|
163
|
+
function toStringParams(source) {
|
|
164
|
+
const out = {};
|
|
165
|
+
if (!isRecord(source))
|
|
166
|
+
return out;
|
|
167
|
+
for (const [key, value] of Object.entries(source)) {
|
|
168
|
+
if (typeof value === 'string')
|
|
169
|
+
out[key] = value;
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
151
173
|
function sha256Base64Url(input) {
|
|
152
174
|
return createHash('sha256').update(input).digest('base64url');
|
|
153
175
|
}
|
|
@@ -164,8 +186,9 @@ function verifyPkce(verifier, challenge) {
|
|
|
164
186
|
* in our case (we host both the resource AND the auth server).
|
|
165
187
|
*
|
|
166
188
|
* `mcpPath` (default `/mcp`) is the deployment's universal MCP route.
|
|
167
|
-
*
|
|
168
|
-
* (the domain already says "mcp");
|
|
189
|
+
* A deployment on a dedicated MCP domain may mount at the bare root
|
|
190
|
+
* `/` so URLs stay short (the domain already says "mcp"); the
|
|
191
|
+
* conventional default is `/mcp`.
|
|
169
192
|
* The trailing slash is normalized off when the path is `/` so the
|
|
170
193
|
* resource URL is `${issuer}` (no trailing slash) rather than
|
|
171
194
|
* `${issuer}/`.
|
|
@@ -206,8 +229,8 @@ export function handleAuthorizationServerMetadata(req, res, config) {
|
|
|
206
229
|
* to do PKCE + paste-key gets registered.
|
|
207
230
|
*/
|
|
208
231
|
export async function handleRegister(req, res, config, storage) {
|
|
209
|
-
const body = (req.body
|
|
210
|
-
const redirectUrisRaw = body
|
|
232
|
+
const body = isRecord(req.body) ? req.body : {};
|
|
233
|
+
const redirectUrisRaw = body['redirect_uris'];
|
|
211
234
|
if (!Array.isArray(redirectUrisRaw) || redirectUrisRaw.length === 0) {
|
|
212
235
|
res.status(400).json({
|
|
213
236
|
error: 'invalid_redirect_uri',
|
|
@@ -224,7 +247,7 @@ export async function handleRegister(req, res, config, storage) {
|
|
|
224
247
|
return;
|
|
225
248
|
}
|
|
226
249
|
const clientId = `mcp_client_${randomBytes(16).toString('base64url')}`;
|
|
227
|
-
const clientName = typeof body
|
|
250
|
+
const clientName = typeof body['client_name'] === 'string' ? body['client_name'] : undefined;
|
|
228
251
|
await storage.putClient({
|
|
229
252
|
clientId,
|
|
230
253
|
redirectUris,
|
|
@@ -256,7 +279,7 @@ export async function handleRegister(req, res, config, storage) {
|
|
|
256
279
|
* with the user's pasted key.
|
|
257
280
|
*/
|
|
258
281
|
export async function handleAuthorizeGet(req, res, config, storage) {
|
|
259
|
-
const params = req.query;
|
|
282
|
+
const params = toStringParams(req.query);
|
|
260
283
|
const issuer = resolveIssuerUrl(req, config.issuerUrl);
|
|
261
284
|
const v = await validateAuthorizeParams(params, storage, config, issuer);
|
|
262
285
|
if ('error' in v) {
|
|
@@ -301,7 +324,7 @@ export async function handleAuthorizeGet(req, res, config, storage) {
|
|
|
301
324
|
* OAuth params).
|
|
302
325
|
*/
|
|
303
326
|
export async function handleAuthorizePost(req, res, config, storage, auth, pairingService) {
|
|
304
|
-
const params = (req.body
|
|
327
|
+
const params = toStringParams(req.body);
|
|
305
328
|
const issuer = resolveIssuerUrl(req, config.issuerUrl);
|
|
306
329
|
const validation = await validateAuthorizeParams(params, storage, config, issuer);
|
|
307
330
|
if ('error' in validation) {
|
|
@@ -338,15 +361,14 @@ export async function handleAuthorizePost(req, res, config, storage, auth, pairi
|
|
|
338
361
|
res.status(400).type('html').send(renderAuthorizePage(params, 'Pair code or API key required'));
|
|
339
362
|
return;
|
|
340
363
|
}
|
|
341
|
-
// Validate the key by running the adapter's auth path.
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
const fakeReq = {
|
|
346
|
-
headers: { authorization: `Bearer ${apiKey}` },
|
|
347
|
-
};
|
|
364
|
+
// Validate the key by running the adapter's auth path. Using the
|
|
365
|
+
// same `resolveIdentityFromHeaders()` the `/mcp` and `/ws` ingress
|
|
366
|
+
// points funnel through keeps the validation semantics
|
|
367
|
+
// byte-identical to /mcp's auth gate — no synthetic Request needed.
|
|
348
368
|
try {
|
|
349
|
-
await
|
|
369
|
+
await resolveIdentityFromHeaders(auth, {
|
|
370
|
+
authorization: `Bearer ${apiKey}`,
|
|
371
|
+
});
|
|
350
372
|
}
|
|
351
373
|
catch (err) {
|
|
352
374
|
if (err instanceof UnauthenticatedError) {
|
|
@@ -430,7 +452,7 @@ export async function handleAuthorizePost(req, res, config, storage, auth, pairi
|
|
|
430
452
|
* 400 status code.
|
|
431
453
|
*/
|
|
432
454
|
export async function handleToken(req, res, storage) {
|
|
433
|
-
const body = (req.body
|
|
455
|
+
const body = toStringParams(req.body);
|
|
434
456
|
if (body['grant_type'] !== 'authorization_code') {
|
|
435
457
|
res.status(400).json({
|
|
436
458
|
error: 'unsupported_grant_type',
|