@ggui-ai/mcp-server 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +48 -0
- package/dist/admin-blueprints-transport.d.ts +114 -0
- package/dist/admin-blueprints-transport.d.ts.map +1 -0
- package/dist/admin-blueprints-transport.js +118 -0
- package/dist/admin-oauth-providers-transport.d.ts +40 -0
- package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
- package/dist/admin-oauth-providers-transport.js +263 -0
- package/dist/auth.d.ts +39 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +75 -0
- package/dist/build-mcp.d.ts +128 -0
- package/dist/build-mcp.d.ts.map +1 -0
- package/dist/build-mcp.js +113 -0
- package/dist/code-store-fs.d.ts +19 -0
- package/dist/code-store-fs.d.ts.map +1 -0
- package/dist/code-store-fs.js +98 -0
- package/dist/console-auth.d.ts +139 -0
- package/dist/console-auth.d.ts.map +1 -0
- package/dist/console-auth.js +102 -0
- package/dist/console-cache.d.ts +78 -0
- package/dist/console-cache.d.ts.map +1 -0
- package/dist/console-cache.js +105 -0
- package/dist/console-headers.d.ts +124 -0
- package/dist/console-headers.d.ts.map +1 -0
- package/dist/console-headers.js +49 -0
- package/dist/console-llm-trace.d.ts +66 -0
- package/dist/console-llm-trace.d.ts.map +1 -0
- package/dist/console-llm-trace.js +105 -0
- package/dist/console-payloads.d.ts +67 -0
- package/dist/console-payloads.d.ts.map +1 -0
- package/dist/console-payloads.js +105 -0
- package/dist/console-theme-routes.d.ts +111 -0
- package/dist/console-theme-routes.d.ts.map +1 -0
- package/dist/console-theme-routes.js +202 -0
- package/dist/console-timeline.d.ts +45 -0
- package/dist/console-timeline.d.ts.map +1 -0
- package/dist/console-timeline.js +169 -0
- package/dist/console-validator.d.ts +67 -0
- package/dist/console-validator.d.ts.map +1 -0
- package/dist/console-validator.js +105 -0
- package/dist/console-welcome.d.ts +7 -0
- package/dist/console-welcome.d.ts.map +1 -0
- package/dist/console-welcome.js +221 -0
- package/dist/csrf-middleware.d.ts +55 -0
- package/dist/csrf-middleware.d.ts.map +1 -0
- package/dist/csrf-middleware.js +138 -0
- package/dist/email-login.d.ts +174 -0
- package/dist/email-login.d.ts.map +1 -0
- package/dist/email-login.js +254 -0
- package/dist/email-resend.d.ts +29 -0
- package/dist/email-resend.d.ts.map +1 -0
- package/dist/email-resend.js +71 -0
- package/dist/email-sender-from-env.d.ts +34 -0
- package/dist/email-sender-from-env.d.ts.map +1 -0
- package/dist/email-sender-from-env.js +112 -0
- package/dist/email-smtp.d.ts +42 -0
- package/dist/email-smtp.d.ts.map +1 -0
- package/dist/email-smtp.js +81 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +122 -0
- package/dist/instructions-presets.d.ts +112 -0
- package/dist/instructions-presets.d.ts.map +1 -0
- package/dist/instructions-presets.js +195 -0
- package/dist/llm-backed-negotiator.d.ts +178 -0
- package/dist/llm-backed-negotiator.d.ts.map +1 -0
- package/dist/llm-backed-negotiator.js +579 -0
- package/dist/logger.d.ts +23 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/mcp-apps-inbound.d.ts +86 -0
- package/dist/mcp-apps-inbound.d.ts.map +1 -0
- package/dist/mcp-apps-inbound.js +278 -0
- package/dist/mcp-apps-outbound.d.ts +448 -0
- package/dist/mcp-apps-outbound.d.ts.map +1 -0
- package/dist/mcp-apps-outbound.js +1163 -0
- package/dist/mcp-mounts.d.ts +239 -0
- package/dist/mcp-mounts.d.ts.map +1 -0
- package/dist/mcp-mounts.js +222 -0
- package/dist/oauth-login-types.d.ts +160 -0
- package/dist/oauth-login-types.d.ts.map +1 -0
- package/dist/oauth-login-types.js +9 -0
- package/dist/oauth-login.d.ts +77 -0
- package/dist/oauth-login.d.ts.map +1 -0
- package/dist/oauth-login.js +455 -0
- package/dist/oauth-providers/github.d.ts +17 -0
- package/dist/oauth-providers/github.d.ts.map +1 -0
- package/dist/oauth-providers/github.js +89 -0
- package/dist/oauth-providers/google.d.ts +18 -0
- package/dist/oauth-providers/google.d.ts.map +1 -0
- package/dist/oauth-providers/google.js +59 -0
- package/dist/oauth-providers-store.d.ts +32 -0
- package/dist/oauth-providers-store.d.ts.map +1 -0
- package/dist/oauth-providers-store.js +291 -0
- package/dist/oauth.d.ts +347 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +686 -0
- package/dist/pairing-transport.d.ts +99 -0
- package/dist/pairing-transport.d.ts.map +1 -0
- package/dist/pairing-transport.js +223 -0
- package/dist/rate-limit-middleware.d.ts +36 -0
- package/dist/rate-limit-middleware.d.ts.map +1 -0
- package/dist/rate-limit-middleware.js +57 -0
- package/dist/render-gate.d.ts +87 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +77 -0
- package/dist/render-rate-limit.d.ts +59 -0
- package/dist/render-rate-limit.d.ts.map +1 -0
- package/dist/render-rate-limit.js +73 -0
- package/dist/render-signing.d.ts +98 -0
- package/dist/render-signing.d.ts.map +1 -0
- package/dist/render-signing.js +113 -0
- package/dist/request-context.d.ts +113 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +154 -0
- package/dist/reserved-validators.d.ts +22 -0
- package/dist/reserved-validators.d.ts.map +1 -0
- package/dist/reserved-validators.js +101 -0
- package/dist/schema-compat.d.ts +167 -0
- package/dist/schema-compat.d.ts.map +1 -0
- package/dist/schema-compat.js +187 -0
- package/dist/security-headers-middleware.d.ts +38 -0
- package/dist/security-headers-middleware.d.ts.map +1 -0
- package/dist/security-headers-middleware.js +30 -0
- package/dist/server.d.ts +2060 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +6338 -0
- package/dist/session-channel.d.ts +651 -0
- package/dist/session-channel.d.ts.map +1 -0
- package/dist/session-channel.js +1756 -0
- package/dist/storage.d.ts +89 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +171 -0
- package/dist/thread-transport.d.ts +118 -0
- package/dist/thread-transport.d.ts.map +1 -0
- package/dist/thread-transport.js +478 -0
- package/dist/user-session-auth.d.ts +167 -0
- package/dist/user-session-auth.d.ts.map +1 -0
- package/dist/user-session-auth.js +148 -0
- package/package.json +76 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-mounts — aggregate external MCP tool handler bundles onto the
|
|
3
|
+
* ggui `/mcp` surface so one session can see ggui-native tools (e.g.
|
|
4
|
+
* `ggui_push`) AND mounted tools (e.g. `tasks_*`) through a single
|
|
5
|
+
* MCP connection.
|
|
6
|
+
*
|
|
7
|
+
* Scope lock: a **mount** is a named bundle of `SharedHandler`
|
|
8
|
+
* instances. That's the same runtime contract ggui's own default
|
|
9
|
+
* handlers already satisfy — no proxy, no in-memory MCP wire, no
|
|
10
|
+
* extra registry. A fixture (or any host package) that wants to
|
|
11
|
+
* expose its tools on the OSS path builds `SharedHandler[]` against
|
|
12
|
+
* the same seams ggui's native handlers use, wraps them in a mount,
|
|
13
|
+
* and passes the mount to {@link CreateGguiServerOptions.mcpMounts}.
|
|
14
|
+
*
|
|
15
|
+
* The narrower "proxy an external MCP server whose source you can't
|
|
16
|
+
* modify" path (connector registry + `tools/list` forwarding) is
|
|
17
|
+
* deliberately NOT handled here — it is a separate seam. The bundle
|
|
18
|
+
* seam is the smallest honest shape for in-process mounts: it reuses
|
|
19
|
+
* the exact same handler interface, so collision checks,
|
|
20
|
+
* `buildMcpServer` registration, `toolCount`, telemetry, and logging
|
|
21
|
+
* all Just Work.
|
|
22
|
+
*/
|
|
23
|
+
import type { ZodRawShape } from 'zod';
|
|
24
|
+
import type { HandlerContext, SharedHandler } from '@ggui-ai/mcp-server-handlers';
|
|
25
|
+
import type { WiredActionContext, WiredActionRouter } from './session-channel.js';
|
|
26
|
+
/**
|
|
27
|
+
* Runtime ctx the mount-router hands the mount handler. Structurally a
|
|
28
|
+
* superset of `HandlerContext` (so the mount's `handler(input, ctx)`
|
|
29
|
+
* signature stays unchanged) PLUS the wired-action-only fields from
|
|
30
|
+
* {@link WiredActionContext}.
|
|
31
|
+
*
|
|
32
|
+
* Why the type is exported: TS-authored mount tools that want to read
|
|
33
|
+
* `ctx.sendPropsUpdate` / `ctx.stackItemId` import this and narrow their
|
|
34
|
+
* `handler` parameter (e.g. `async handler(input, ctx) { const wired =
|
|
35
|
+
* ctx as WiredMountContext; … }`). JS-authored mounts (.mjs) read the
|
|
36
|
+
* fields structurally — they're present on the runtime object whether
|
|
37
|
+
* or not the static type knows about them.
|
|
38
|
+
*
|
|
39
|
+
* The static `SharedHandler.handler` signature deliberately stays
|
|
40
|
+
* narrow on `HandlerContext`. Widening that type would force every
|
|
41
|
+
* shared handler (ggui-native + mounted) to acknowledge a wired-only
|
|
42
|
+
* surface, even handlers that never run through the wired-action path
|
|
43
|
+
* (e.g. `ggui_push`, blueprint search). The structural superset here
|
|
44
|
+
* keeps the canonical contract narrow without sacrificing access for
|
|
45
|
+
* mount tools that opt in.
|
|
46
|
+
*/
|
|
47
|
+
/**
|
|
48
|
+
* Intersection (not interface-extension) because `HandlerContext` declares
|
|
49
|
+
* `sessionId?` / `stackItemId?` as optional — the canonical context shape any
|
|
50
|
+
* handler may see — whereas `WiredActionContext` declares them as required
|
|
51
|
+
* (the wired-action dispatcher always knows the active session + stack
|
|
52
|
+
* frame at invocation time). Interface-extends rejects "narrowing
|
|
53
|
+
* optional → required" via TS2320 ("cannot simultaneously extend"), but
|
|
54
|
+
* an intersection composes the two perfectly: optional ∧ required ≡ required.
|
|
55
|
+
*
|
|
56
|
+
* Surface for consumers stays identical — a TS-authored mount that types
|
|
57
|
+
* its `handler` parameter as `WiredMountContext` reads `sessionId: string`
|
|
58
|
+
* + `stackItemId: string` (no `| undefined`) + every `HandlerContext` field
|
|
59
|
+
* (`appId`, `requestId`, optional `apiKeyHash`).
|
|
60
|
+
*/
|
|
61
|
+
export type WiredMountContext = Omit<HandlerContext, 'sessionId' | 'stackItemId'> & WiredActionContext;
|
|
62
|
+
/**
|
|
63
|
+
* One named bundle of tool handlers the server should aggregate onto
|
|
64
|
+
* its MCP surface.
|
|
65
|
+
*
|
|
66
|
+
* `name` is diagnostic-only — it shows up in collision-error messages
|
|
67
|
+
* and composition telemetry so an operator running `ggui serve` with
|
|
68
|
+
* three mounts can tell which one is misconfigured. It does NOT appear
|
|
69
|
+
* on the wire; tool names stay whatever each handler declares.
|
|
70
|
+
*/
|
|
71
|
+
export interface McpServerMount {
|
|
72
|
+
/**
|
|
73
|
+
* Human-readable mount identifier. Surfaced in collision errors and
|
|
74
|
+
* `server.composed` telemetry. No uniqueness constraint across mounts
|
|
75
|
+
* (two mounts named `"tasks"` compose fine as long as their tool-name
|
|
76
|
+
* sets don't collide).
|
|
77
|
+
*/
|
|
78
|
+
readonly name: string;
|
|
79
|
+
/**
|
|
80
|
+
* Tool handler bundle. Each entry is a `SharedHandler` — the same
|
|
81
|
+
* shape ggui-native handlers (blueprint family, session-mutations,
|
|
82
|
+
* threads) already use. The server calls {@link SharedHandler.handler}
|
|
83
|
+
* through `buildMcpServer`'s regular registration path; validation,
|
|
84
|
+
* logging, and output-schema parsing all happen uniformly.
|
|
85
|
+
*/
|
|
86
|
+
readonly handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Branded HTTP path for an isolated MCP service. Mint one from a
|
|
90
|
+
* raw string via {@link validateServicePath}.
|
|
91
|
+
*/
|
|
92
|
+
export type ServicePath = string & {
|
|
93
|
+
readonly __brand: 'ServicePath';
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Validate + brand a service HTTP path. Throws on malformed input or
|
|
97
|
+
* collision with a reserved built-in route.
|
|
98
|
+
*
|
|
99
|
+
* Rules:
|
|
100
|
+
* - Must start with `/`.
|
|
101
|
+
* - May contain letters, digits, `-`, `_`, and `/` (no whitespace,
|
|
102
|
+
* no `.`, no path traversal).
|
|
103
|
+
* - Must not end with `/` (prevents trailing-slash variant collisions).
|
|
104
|
+
* - Must not equal a reserved built-in path (`/mcp`, `/protocol`, ...).
|
|
105
|
+
*/
|
|
106
|
+
export declare function validateServicePath(p: string): ServicePath;
|
|
107
|
+
/**
|
|
108
|
+
* One isolated MCP server mounted at its own HTTP path. Unlike a
|
|
109
|
+
* {@link McpServerMount} (which contributes tools to the shared
|
|
110
|
+
* audience-filtered routes), a service is a complete, self-contained
|
|
111
|
+
* MCP server with its own tool namespace.
|
|
112
|
+
*
|
|
113
|
+
* Use a **service** when the handler set is conceptually a distinct
|
|
114
|
+
* MCP server (`mcp.ggui.ai/docs`, `mcp.ggui.ai/playground/todos`).
|
|
115
|
+
* Use a **mount** when the handlers should appear alongside
|
|
116
|
+
* ggui-native tools on the shared `/mcp` surface (fixtures, external
|
|
117
|
+
* MCPs aggregated for one session's view).
|
|
118
|
+
*
|
|
119
|
+
* Why two concepts and not one: audience tags carry caller-class
|
|
120
|
+
* semantics (`agent` vs `runtime`) that don't map 1:1 to URL paths
|
|
121
|
+
* — agent + runtime both live on `/mcp` today. Collapsing audience
|
|
122
|
+
* and path into a single "mount at path" model would lose that
|
|
123
|
+
* caller-class distinction. Mounts and services are honest about the
|
|
124
|
+
* two different things the framework actually does.
|
|
125
|
+
*
|
|
126
|
+
* Compose-time invariants (validated by {@link validateMcpServices}):
|
|
127
|
+
* - `path` passes {@link validateServicePath}.
|
|
128
|
+
* - Each handler declares a non-empty `outputSchema` (matching the
|
|
129
|
+
* mount rule — empty schemas silently strip `structuredContent`).
|
|
130
|
+
* - Tool names are unique within a service. Cross-service collisions
|
|
131
|
+
* ARE allowed — services are isolated namespaces; a client connects
|
|
132
|
+
* to one path and only ever sees that path's tools.
|
|
133
|
+
* - Service handlers MUST NOT set `audience`. Services bypass
|
|
134
|
+
* audience filtering entirely (the path IS the audience), so an
|
|
135
|
+
* explicit tag is silently meaningless. Reject loudly.
|
|
136
|
+
* - Service paths are unique across the whole `mcpServices` array.
|
|
137
|
+
*/
|
|
138
|
+
export interface McpService {
|
|
139
|
+
/**
|
|
140
|
+
* Human-readable service identifier. Surfaced in validation errors
|
|
141
|
+
* and telemetry. No uniqueness constraint across services — only
|
|
142
|
+
* {@link path} must be unique.
|
|
143
|
+
*/
|
|
144
|
+
readonly name: string;
|
|
145
|
+
/**
|
|
146
|
+
* HTTP path the service mounts at (e.g. `/docs`,
|
|
147
|
+
* `/playground/todos`). Validated via {@link validateServicePath} at
|
|
148
|
+
* compose time.
|
|
149
|
+
*/
|
|
150
|
+
readonly path: string;
|
|
151
|
+
/**
|
|
152
|
+
* Tool handler bundle. Same shape ggui-native handlers and mount
|
|
153
|
+
* handlers use. See {@link McpService} for the service-specific
|
|
154
|
+
* rules layered on top of the canonical contract.
|
|
155
|
+
*/
|
|
156
|
+
readonly handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
157
|
+
/**
|
|
158
|
+
* When `true`, this service skips the auth chain — unauthenticated
|
|
159
|
+
* requests are let through with a synthesized identity
|
|
160
|
+
* (`{kind: 'builder'}`, `source: 'anonymous'`). Default `false`
|
|
161
|
+
* (auth required, same posture as `/mcp` / `/protocol` / `/ops`).
|
|
162
|
+
*
|
|
163
|
+
* Use for first-touch public surfaces (docs MCP, landing-agent
|
|
164
|
+
* demos) where requiring a bearer token would block the use case.
|
|
165
|
+
* Handlers that need to distinguish anonymous from authenticated
|
|
166
|
+
* traffic read `ctx` for the synthesized identity OR check the
|
|
167
|
+
* underlying `source` via the broader request context.
|
|
168
|
+
*
|
|
169
|
+
* Anonymous services SHOULD plug into the `RateLimiter` seam
|
|
170
|
+
* (per-IP + per-session keys) to prevent unbounded compute; the
|
|
171
|
+
* seam is wired the same way for authenticated and anonymous
|
|
172
|
+
* paths — this flag does not change rate-limit composition.
|
|
173
|
+
*/
|
|
174
|
+
readonly anonymous?: boolean;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Validate every service in the array: path well-formed + non-reserved
|
|
178
|
+
* + unique across services, every handler's `outputSchema` non-empty,
|
|
179
|
+
* no `audience` tags on service handlers, no within-service tool-name
|
|
180
|
+
* collisions. Returns the input list when valid; throws with the
|
|
181
|
+
* offending service name + path embedded in the message otherwise.
|
|
182
|
+
*
|
|
183
|
+
* Cross-service tool-name collisions are deliberately NOT checked —
|
|
184
|
+
* services are isolated namespaces by design.
|
|
185
|
+
*
|
|
186
|
+
* No side effects — safe to call multiple times. Returns the input
|
|
187
|
+
* reference unchanged on the empty path.
|
|
188
|
+
*/
|
|
189
|
+
export declare function validateMcpServices(services: ReadonlyArray<McpService> | undefined): ReadonlyArray<McpService>;
|
|
190
|
+
/**
|
|
191
|
+
* Compose a single handler list from ggui's base handlers + every
|
|
192
|
+
* mount's handlers. Throws on tool-name collision so misconfiguration
|
|
193
|
+
* surfaces at server-construction time, NOT on the first `tools/call`.
|
|
194
|
+
*
|
|
195
|
+
* Error messages intentionally mention the offending mount name so an
|
|
196
|
+
* operator with several mounts can tell which bundle introduced the
|
|
197
|
+
* collision without grepping their code.
|
|
198
|
+
*
|
|
199
|
+
* No side effects — the returned list is a fresh array even when
|
|
200
|
+
* `mounts` is empty, so callers never mutate the input reference.
|
|
201
|
+
*/
|
|
202
|
+
export declare function composeHandlersWithMounts(baseHandlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>, mounts: ReadonlyArray<McpServerMount> | undefined): ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
|
|
203
|
+
/**
|
|
204
|
+
* Build a {@link WiredActionRouter} that dispatches wired-action
|
|
205
|
+
* hits to the matching mount handler's `handler(input, ctx)`. Zero-config composition for
|
|
206
|
+
* OSS `ggui serve` — when the operator declares `ggui.json#mcpMounts`,
|
|
207
|
+
* every mounted tool automatically becomes wire-dispatchable from
|
|
208
|
+
* a generated UI's `useAction` without additional glue.
|
|
209
|
+
*
|
|
210
|
+
* Ownership + scoping:
|
|
211
|
+
* - Only MOUNT handlers participate. ggui-native handlers
|
|
212
|
+
* (`ggui_push`, `ggui_handshake`, etc.) are platform tools and
|
|
213
|
+
* deliberately NOT exposed as wire-dispatchable actions — a
|
|
214
|
+
* component that tried to dispatch `ggui_push` would bypass the
|
|
215
|
+
* agentic-loop contract.
|
|
216
|
+
* - Name collisions across mounts are prevented at aggregation
|
|
217
|
+
* time by {@link composeHandlersWithMounts}, so the first-match
|
|
218
|
+
* lookup here is safe.
|
|
219
|
+
*
|
|
220
|
+
* Context synthesis:
|
|
221
|
+
* - Each invocation gets a fresh {@link HandlerContext} with the
|
|
222
|
+
* caller-supplied `appId` (typically the session's appId) + a
|
|
223
|
+
* fresh request id. Mount handlers that read from storage scope
|
|
224
|
+
* to this appId, matching the `/mcp` ingress behavior.
|
|
225
|
+
* - The session-channel dispatcher additionally hands a
|
|
226
|
+
* {@link WiredActionContext}. The runtime ctx the mount handler
|
|
227
|
+
* sees is a structural superset
|
|
228
|
+
* ({@link WiredMountContext}) so a JS-authored mount can call
|
|
229
|
+
* `ctx.sendPropsUpdate(ctx.stackItemId, {...})` directly. The static
|
|
230
|
+
* `SharedHandler.handler(input, ctx: HandlerContext)` shape stays
|
|
231
|
+
* untouched — tooling that doesn't read the wired fields keeps its
|
|
232
|
+
* existing types.
|
|
233
|
+
*
|
|
234
|
+
* Returns `null` when `mounts` is empty/absent, signaling the
|
|
235
|
+
* composer to OMIT the `wiredActionRouter` opt entirely so servers
|
|
236
|
+
* with no mounts behave as if the router did not exist.
|
|
237
|
+
*/
|
|
238
|
+
export declare function composeWiredActionRouterFromMounts(mounts: ReadonlyArray<McpServerMount> | undefined, resolveContext: () => HandlerContext): WiredActionRouter | null;
|
|
239
|
+
//# sourceMappingURL=mcp-mounts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-mounts.d.ts","sourceRoot":"","sources":["../src/mcp-mounts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,KAAK,CAAC;AACvC,OAAO,KAAK,EACV,cAAc,EACd,aAAa,EACd,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EACV,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,iBAAiB,GAAG,IAAI,CAAC,cAAc,EAAE,WAAW,GAAG,aAAa,CAAC,GAC/E,kBAAkB,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAA;CAAE,CAAC;AAsBvE;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAiB1D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,UAAU;IACzB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;IAC1E;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,SAAS,GAC9C,aAAa,CAAC,UAAU,CAAC,CAyC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,EACpE,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,GAChD,aAAa,CAAC,aAAa,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAyDxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,EACjD,cAAc,EAAE,MAAM,cAAc,GACnC,iBAAiB,GAAG,IAAI,CA2C1B"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in routes the multi-service mount loop must not shadow.
|
|
3
|
+
* Reserved at validation time so a typo in a host config can never
|
|
4
|
+
* silently swallow OAuth discovery / health / per-app traffic.
|
|
5
|
+
*/
|
|
6
|
+
const RESERVED_SERVICE_PATHS = new Set([
|
|
7
|
+
'/',
|
|
8
|
+
'/mcp',
|
|
9
|
+
'/protocol',
|
|
10
|
+
'/ops',
|
|
11
|
+
'/ws',
|
|
12
|
+
'/health',
|
|
13
|
+
'/.well-known',
|
|
14
|
+
'/oauth',
|
|
15
|
+
'/_ggui',
|
|
16
|
+
'/ggui',
|
|
17
|
+
]);
|
|
18
|
+
const SERVICE_PATH_REGEX = /^\/[a-zA-Z0-9_/-]+$/;
|
|
19
|
+
/**
|
|
20
|
+
* Validate + brand a service HTTP path. Throws on malformed input or
|
|
21
|
+
* collision with a reserved built-in route.
|
|
22
|
+
*
|
|
23
|
+
* Rules:
|
|
24
|
+
* - Must start with `/`.
|
|
25
|
+
* - May contain letters, digits, `-`, `_`, and `/` (no whitespace,
|
|
26
|
+
* no `.`, no path traversal).
|
|
27
|
+
* - Must not end with `/` (prevents trailing-slash variant collisions).
|
|
28
|
+
* - Must not equal a reserved built-in path (`/mcp`, `/protocol`, ...).
|
|
29
|
+
*/
|
|
30
|
+
export function validateServicePath(p) {
|
|
31
|
+
if (!SERVICE_PATH_REGEX.test(p)) {
|
|
32
|
+
throw new Error(`createGguiServer: mcpServices path "${p}" is malformed — must start with "/" and contain only letters, digits, "-", "_", and "/".`);
|
|
33
|
+
}
|
|
34
|
+
if (p.length > 1 && p.endsWith('/')) {
|
|
35
|
+
throw new Error(`createGguiServer: mcpServices path "${p}" must not end with "/" (use "${p.slice(0, -1)}" instead).`);
|
|
36
|
+
}
|
|
37
|
+
if (RESERVED_SERVICE_PATHS.has(p)) {
|
|
38
|
+
throw new Error(`createGguiServer: mcpServices path "${p}" collides with a reserved built-in route. Pick a distinct prefix (e.g. "/docs", "/playground/...").`);
|
|
39
|
+
}
|
|
40
|
+
return p;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Validate every service in the array: path well-formed + non-reserved
|
|
44
|
+
* + unique across services, every handler's `outputSchema` non-empty,
|
|
45
|
+
* no `audience` tags on service handlers, no within-service tool-name
|
|
46
|
+
* collisions. Returns the input list when valid; throws with the
|
|
47
|
+
* offending service name + path embedded in the message otherwise.
|
|
48
|
+
*
|
|
49
|
+
* Cross-service tool-name collisions are deliberately NOT checked —
|
|
50
|
+
* services are isolated namespaces by design.
|
|
51
|
+
*
|
|
52
|
+
* No side effects — safe to call multiple times. Returns the input
|
|
53
|
+
* reference unchanged on the empty path.
|
|
54
|
+
*/
|
|
55
|
+
export function validateMcpServices(services) {
|
|
56
|
+
if (!services || services.length === 0)
|
|
57
|
+
return services ?? [];
|
|
58
|
+
const seenPaths = new Set();
|
|
59
|
+
for (const svc of services) {
|
|
60
|
+
if (typeof svc.name !== 'string' || svc.name.length === 0) {
|
|
61
|
+
throw new Error('createGguiServer: every `mcpServices` entry must carry a non-empty string `name` (for validation-error clarity).');
|
|
62
|
+
}
|
|
63
|
+
validateServicePath(svc.path);
|
|
64
|
+
if (seenPaths.has(svc.path)) {
|
|
65
|
+
throw new Error(`createGguiServer: mcpServices path "${svc.path}" is declared by more than one service. Each service must mount at a distinct path.`);
|
|
66
|
+
}
|
|
67
|
+
seenPaths.add(svc.path);
|
|
68
|
+
const seenTools = new Set();
|
|
69
|
+
for (const h of svc.handlers) {
|
|
70
|
+
if (typeof h.outputSchema !== 'object' ||
|
|
71
|
+
h.outputSchema === null ||
|
|
72
|
+
Object.keys(h.outputSchema).length === 0) {
|
|
73
|
+
throw new Error(`createGguiServer: service "${svc.name}" handler "${h.name}" declares an empty \`outputSchema\` — this silently strips \`structuredContent\` at the MCP SDK boundary. Declare the fields the handler returns (e.g. \`outputSchema: { items: z.array(...) }\`).`);
|
|
74
|
+
}
|
|
75
|
+
if (h.audience !== undefined) {
|
|
76
|
+
throw new Error(`createGguiServer: service "${svc.name}" handler "${h.name}" sets \`audience\` (${JSON.stringify(h.audience)}). Services bypass audience filtering — the path "${svc.path}" IS the audience. Remove the \`audience\` field or move the handler to an aggregate mount.`);
|
|
77
|
+
}
|
|
78
|
+
if (seenTools.has(h.name)) {
|
|
79
|
+
throw new Error(`createGguiServer: service "${svc.name}" registers tool "${h.name}" twice. Tool names must be unique within a service (cross-service collisions ARE allowed).`);
|
|
80
|
+
}
|
|
81
|
+
seenTools.add(h.name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return services;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Compose a single handler list from ggui's base handlers + every
|
|
88
|
+
* mount's handlers. Throws on tool-name collision so misconfiguration
|
|
89
|
+
* surfaces at server-construction time, NOT on the first `tools/call`.
|
|
90
|
+
*
|
|
91
|
+
* Error messages intentionally mention the offending mount name so an
|
|
92
|
+
* operator with several mounts can tell which bundle introduced the
|
|
93
|
+
* collision without grepping their code.
|
|
94
|
+
*
|
|
95
|
+
* No side effects — the returned list is a fresh array even when
|
|
96
|
+
* `mounts` is empty, so callers never mutate the input reference.
|
|
97
|
+
*/
|
|
98
|
+
export function composeHandlersWithMounts(baseHandlers, mounts) {
|
|
99
|
+
if (!mounts || mounts.length === 0) {
|
|
100
|
+
return baseHandlers;
|
|
101
|
+
}
|
|
102
|
+
// Track ownership of every tool name so the error message can name
|
|
103
|
+
// the offending source. 'ggui' = one of ggui's native handlers;
|
|
104
|
+
// any other string = the mount's `name` that first claimed the tool.
|
|
105
|
+
const owner = new Map();
|
|
106
|
+
for (const h of baseHandlers)
|
|
107
|
+
owner.set(h.name, 'ggui');
|
|
108
|
+
const aggregated = [
|
|
109
|
+
...baseHandlers,
|
|
110
|
+
];
|
|
111
|
+
for (const mount of mounts) {
|
|
112
|
+
if (typeof mount.name !== 'string' || mount.name.length === 0) {
|
|
113
|
+
throw new Error('createGguiServer: every `mcpMounts` entry must carry a non-empty string `name` (for diagnostic telemetry + collision-error clarity).');
|
|
114
|
+
}
|
|
115
|
+
for (const mh of mount.handlers) {
|
|
116
|
+
// `outputSchema: {}` (empty `ZodRawShape`) silently strips
|
|
117
|
+
// `structuredContent` at the MCP SDK boundary — the handler
|
|
118
|
+
// can return `{ items: [...] }` and the wire answer is `{}`.
|
|
119
|
+
// Operators hitting this see success-looking responses with
|
|
120
|
+
// missing data and no diagnostic. Reject it at compose time
|
|
121
|
+
// so the failure arrives with the mount + tool names
|
|
122
|
+
// attached instead of showing up as a mystery at tools/call.
|
|
123
|
+
//
|
|
124
|
+
// Scope: mounted handlers only. ggui-native handlers are
|
|
125
|
+
// under repo ownership and already correctly shaped. If a
|
|
126
|
+
// legitimate no-output tool ever lands in a mount, declare
|
|
127
|
+
// `{ ok: z.literal(true) }` or equivalent — the zero-field
|
|
128
|
+
// case is never what you want over the wire.
|
|
129
|
+
if (typeof mh.outputSchema !== 'object' ||
|
|
130
|
+
mh.outputSchema === null ||
|
|
131
|
+
Object.keys(mh.outputSchema).length === 0) {
|
|
132
|
+
throw new Error(`createGguiServer: mount "${mount.name}" handler "${mh.name}" declares an empty \`outputSchema\` — this silently strips \`structuredContent\` at the MCP SDK boundary. Declare the fields the handler returns (e.g. \`outputSchema: { items: z.array(...) }\`). If the tool genuinely returns nothing, declare a sentinel like \`{ ok: z.literal(true) }\`.`);
|
|
133
|
+
}
|
|
134
|
+
const prior = owner.get(mh.name);
|
|
135
|
+
if (prior !== undefined) {
|
|
136
|
+
const priorLabel = prior === 'ggui'
|
|
137
|
+
? 'a ggui-native tool'
|
|
138
|
+
: `mount "${prior}"`;
|
|
139
|
+
throw new Error(`createGguiServer: mount "${mount.name}" registers tool "${mh.name}" which collides with ${priorLabel}. Rename the tool or drop the duplicate mount.`);
|
|
140
|
+
}
|
|
141
|
+
owner.set(mh.name, mount.name);
|
|
142
|
+
aggregated.push(mh);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return aggregated;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Build a {@link WiredActionRouter} that dispatches wired-action
|
|
149
|
+
* hits to the matching mount handler's `handler(input, ctx)`. Zero-config composition for
|
|
150
|
+
* OSS `ggui serve` — when the operator declares `ggui.json#mcpMounts`,
|
|
151
|
+
* every mounted tool automatically becomes wire-dispatchable from
|
|
152
|
+
* a generated UI's `useAction` without additional glue.
|
|
153
|
+
*
|
|
154
|
+
* Ownership + scoping:
|
|
155
|
+
* - Only MOUNT handlers participate. ggui-native handlers
|
|
156
|
+
* (`ggui_push`, `ggui_handshake`, etc.) are platform tools and
|
|
157
|
+
* deliberately NOT exposed as wire-dispatchable actions — a
|
|
158
|
+
* component that tried to dispatch `ggui_push` would bypass the
|
|
159
|
+
* agentic-loop contract.
|
|
160
|
+
* - Name collisions across mounts are prevented at aggregation
|
|
161
|
+
* time by {@link composeHandlersWithMounts}, so the first-match
|
|
162
|
+
* lookup here is safe.
|
|
163
|
+
*
|
|
164
|
+
* Context synthesis:
|
|
165
|
+
* - Each invocation gets a fresh {@link HandlerContext} with the
|
|
166
|
+
* caller-supplied `appId` (typically the session's appId) + a
|
|
167
|
+
* fresh request id. Mount handlers that read from storage scope
|
|
168
|
+
* to this appId, matching the `/mcp` ingress behavior.
|
|
169
|
+
* - The session-channel dispatcher additionally hands a
|
|
170
|
+
* {@link WiredActionContext}. The runtime ctx the mount handler
|
|
171
|
+
* sees is a structural superset
|
|
172
|
+
* ({@link WiredMountContext}) so a JS-authored mount can call
|
|
173
|
+
* `ctx.sendPropsUpdate(ctx.stackItemId, {...})` directly. The static
|
|
174
|
+
* `SharedHandler.handler(input, ctx: HandlerContext)` shape stays
|
|
175
|
+
* untouched — tooling that doesn't read the wired fields keeps its
|
|
176
|
+
* existing types.
|
|
177
|
+
*
|
|
178
|
+
* Returns `null` when `mounts` is empty/absent, signaling the
|
|
179
|
+
* composer to OMIT the `wiredActionRouter` opt entirely so servers
|
|
180
|
+
* with no mounts behave as if the router did not exist.
|
|
181
|
+
*/
|
|
182
|
+
export function composeWiredActionRouterFromMounts(mounts, resolveContext) {
|
|
183
|
+
if (!mounts || mounts.length === 0)
|
|
184
|
+
return null;
|
|
185
|
+
const byName = new Map();
|
|
186
|
+
for (const mount of mounts) {
|
|
187
|
+
for (const h of mount.handlers) {
|
|
188
|
+
// First-write-wins matches `composeHandlersWithMounts`'s
|
|
189
|
+
// collision-rejection order; the compose path throws before
|
|
190
|
+
// reaching this builder, so a duplicate here is dead code.
|
|
191
|
+
if (!byName.has(h.name))
|
|
192
|
+
byName.set(h.name, h);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
has(toolName) {
|
|
197
|
+
return byName.has(toolName);
|
|
198
|
+
},
|
|
199
|
+
async invoke(toolName, input, wiredCtx) {
|
|
200
|
+
const handler = byName.get(toolName);
|
|
201
|
+
if (!handler) {
|
|
202
|
+
// Unreachable in normal use — the session channel has()-gates
|
|
203
|
+
// before calling invoke. Thrown errors surface as TOOL_THREW
|
|
204
|
+
// envelopes, so the caller still gets a canonical shape.
|
|
205
|
+
throw new Error(`wiredActionRouter(mounts): no handler registered for '${toolName}'`);
|
|
206
|
+
}
|
|
207
|
+
// Synthesize the runtime ctx — structural superset of
|
|
208
|
+
// HandlerContext + WiredActionContext. The static
|
|
209
|
+
// `SharedHandler.handler` accepts `HandlerContext`; mounts that
|
|
210
|
+
// need the wired fields read them off the same `ctx` argument
|
|
211
|
+
// (TS via `WiredMountContext`, JS structurally).
|
|
212
|
+
const baseCtx = resolveContext();
|
|
213
|
+
const ctx = {
|
|
214
|
+
...baseCtx,
|
|
215
|
+
sessionId: wiredCtx.sessionId,
|
|
216
|
+
stackItemId: wiredCtx.stackItemId,
|
|
217
|
+
sendPropsUpdate: wiredCtx.sendPropsUpdate,
|
|
218
|
+
};
|
|
219
|
+
return handler.handler(input, ctx);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
import type { AuthResult } from '@ggui-ai/mcp-server-core';
|
|
31
|
+
/**
|
|
32
|
+
* The runtime contract every concrete OAuth provider implements.
|
|
33
|
+
*
|
|
34
|
+
* Stateless by design — `authorizeUrl` builds the redirect URL from
|
|
35
|
+
* the caller-supplied state + PKCE challenge; `exchangeCode` swaps
|
|
36
|
+
* the callback `code` for the user's `providerSubject` (and optional
|
|
37
|
+
* email). All provider-specific config (client_id, client_secret,
|
|
38
|
+
* scopes, endpoint URLs) is captured at construction time inside the
|
|
39
|
+
* concrete impl — the routes don't need to know.
|
|
40
|
+
*
|
|
41
|
+
* **Implementation expectations:**
|
|
42
|
+
*
|
|
43
|
+
* - `providerId` is operator-stable (`'google'`, `'github'`, ...).
|
|
44
|
+
* The route `GET /ggui/oauth-login/:providerId/start` matches
|
|
45
|
+
* this value, so changing it breaks bookmarks.
|
|
46
|
+
* - `authorizeUrl` MUST URL-encode every query parameter — the
|
|
47
|
+
* state token is HMAC-bound and may contain `+` / `=` characters.
|
|
48
|
+
* - `exchangeCode` MUST send the `code_verifier` and the same
|
|
49
|
+
* `redirect_uri` the authorize step used. PKCE `S256` is the
|
|
50
|
+
* only method we support; providers that only support `plain`
|
|
51
|
+
* are out of scope (Google, GitHub both support S256).
|
|
52
|
+
* - `exchangeCode` MUST throw on any non-2xx provider response.
|
|
53
|
+
* The route catches and 400s the callback (state mismatch and
|
|
54
|
+
* network error are both "abort the flow"; finer-grained UX is
|
|
55
|
+
* a follow-up).
|
|
56
|
+
* - `providerSubject` MUST be the provider's stable user ID
|
|
57
|
+
* (Google `sub`, GitHub `id`), NEVER the email. Anyone changing
|
|
58
|
+
* this to email opens session fixation across email rotation.
|
|
59
|
+
*/
|
|
60
|
+
export interface OAuthLoginProvider {
|
|
61
|
+
/** Stable URL slug for this provider — `'google'`, `'github'`, ... */
|
|
62
|
+
readonly providerId: string;
|
|
63
|
+
/**
|
|
64
|
+
* Human-readable label for the operator's `/admin/oauth-providers`
|
|
65
|
+
* page and the end-user's `/login` button. May change per release;
|
|
66
|
+
* `providerId` is the stable wire field.
|
|
67
|
+
*/
|
|
68
|
+
readonly displayName: string;
|
|
69
|
+
/**
|
|
70
|
+
* Build the provider's authorize URL. Caller supplies the HMAC-bound
|
|
71
|
+
* `state` token and the PKCE `codeChallenge` (S256-base64url-encoded
|
|
72
|
+
* SHA-256 of the verifier). The provider implementation appends its
|
|
73
|
+
* own `client_id`, `redirect_uri`, `scope`, and `code_challenge_method=S256`.
|
|
74
|
+
*/
|
|
75
|
+
authorizeUrl(input: AuthorizeUrlInput): string;
|
|
76
|
+
/**
|
|
77
|
+
* Exchange the callback `code` for the user's identity. `codeVerifier`
|
|
78
|
+
* is the original PKCE plaintext the route stamped during /start.
|
|
79
|
+
* `redirectUri` is repeated here because some providers strict-match
|
|
80
|
+
* it against the authorize call (Google does; GitHub doesn't).
|
|
81
|
+
*/
|
|
82
|
+
exchangeCode(input: ExchangeCodeInput): Promise<OAuthExchangeResult>;
|
|
83
|
+
}
|
|
84
|
+
export interface AuthorizeUrlInput {
|
|
85
|
+
readonly state: string;
|
|
86
|
+
readonly codeChallenge: string;
|
|
87
|
+
readonly redirectUri: string;
|
|
88
|
+
}
|
|
89
|
+
export interface ExchangeCodeInput {
|
|
90
|
+
readonly code: string;
|
|
91
|
+
readonly codeVerifier: string;
|
|
92
|
+
readonly redirectUri: string;
|
|
93
|
+
}
|
|
94
|
+
export interface OAuthExchangeResult {
|
|
95
|
+
/**
|
|
96
|
+
* Provider-stable user identifier. NEVER the email; that's a UX
|
|
97
|
+
* field. Used to mint `userId = '${providerId}:${providerSubject}'`.
|
|
98
|
+
*/
|
|
99
|
+
readonly providerSubject: string;
|
|
100
|
+
/**
|
|
101
|
+
* Optional display email. If present, lands in audit metadata +
|
|
102
|
+
* `/settings` UI. Absent providers (or users who hide their email)
|
|
103
|
+
* are still authenticated; subject alone is sufficient identity.
|
|
104
|
+
*/
|
|
105
|
+
readonly email?: string;
|
|
106
|
+
/**
|
|
107
|
+
* Optional display name. Same rules as `email` — UX only, never
|
|
108
|
+
* load-bearing for identity decisions.
|
|
109
|
+
*/
|
|
110
|
+
readonly displayName?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Storage-layer record. The serialized form sitting in
|
|
114
|
+
* `~/.ggui/oauth-providers.json`. Contains the per-provider client
|
|
115
|
+
* credentials the operator pasted at `/admin/oauth-providers` (or
|
|
116
|
+
* the env-override values if `GGUI_OAUTH_<PROVIDERID>_CLIENT_ID` /
|
|
117
|
+
* `GGUI_OAUTH_<PROVIDERID>_CLIENT_SECRET` are set).
|
|
118
|
+
*
|
|
119
|
+
* Storage is responsible for: file mode 0600, atomic writes, env
|
|
120
|
+
* override (env wins), and surfacing `enabled: false` records so the
|
|
121
|
+
* `/admin/oauth-providers` UI can render the slot without it being
|
|
122
|
+
* consumable by the routes.
|
|
123
|
+
*/
|
|
124
|
+
export interface OAuthProviderConfigRecord {
|
|
125
|
+
readonly providerId: string;
|
|
126
|
+
readonly clientId: string;
|
|
127
|
+
readonly clientSecret: string;
|
|
128
|
+
/**
|
|
129
|
+
* Where the values came from. UI surfaces env-overridden providers
|
|
130
|
+
* as read-only ("configured (env)"); file-backed records are
|
|
131
|
+
* editable.
|
|
132
|
+
*/
|
|
133
|
+
readonly source: 'file' | 'env';
|
|
134
|
+
/**
|
|
135
|
+
* Operator can disable a record without deleting it. `false` keeps
|
|
136
|
+
* the slot visible in the admin UI but excludes it from `/login`
|
|
137
|
+
* button rendering and route lookups.
|
|
138
|
+
*/
|
|
139
|
+
readonly enabled: boolean;
|
|
140
|
+
}
|
|
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;
|
|
160
|
+
//# sourceMappingURL=oauth-login-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-login-types.d.ts","sourceRoot":"","sources":["../src/oauth-login-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAED;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC,GAAG,MAAM,CAET"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose the canonical user-id namespace for an OAuth identity.
|
|
3
|
+
* `userId = '${providerId}:${providerSubject}'`. Pure helper —
|
|
4
|
+
* exported here so every consumer (routes, storage, audit hooks)
|
|
5
|
+
* computes the same thing.
|
|
6
|
+
*/
|
|
7
|
+
export function composeOAuthUserId(input) {
|
|
8
|
+
return `${input.providerId}:${input.providerSubject}`;
|
|
9
|
+
}
|