@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FileSystemCodeStore — node:fs-backed {@link CodeStore}. The OSS dev
|
|
3
|
+
* default for `ggui serve`.
|
|
4
|
+
*
|
|
5
|
+
* Default root is `~/.ggui/code-cache/`; operators can override per
|
|
6
|
+
* `createGguiServer({ codeStore })` for tests or project-local caches.
|
|
7
|
+
*
|
|
8
|
+
* ## Layout
|
|
9
|
+
*
|
|
10
|
+
* Two-level directory sharding by hash prefix to avoid stuffing one
|
|
11
|
+
* directory with thousands of files:
|
|
12
|
+
*
|
|
13
|
+
* <root>/<hash[0..2]>/<hash[2..]>.js
|
|
14
|
+
*
|
|
15
|
+
* e.g. `~/.ggui/code-cache/ab/cdef...0123.js`. The `.js` suffix isn't
|
|
16
|
+
* load-bearing for retrieval — `get(hash)` is the only read API and it
|
|
17
|
+
* derives the path from the hash — but operators occasionally `cat`
|
|
18
|
+
* one for debugging, and the suffix tells their editor to syntax-
|
|
19
|
+
* highlight as JavaScript.
|
|
20
|
+
*
|
|
21
|
+
* ## Atomicity
|
|
22
|
+
*
|
|
23
|
+
* `put` writes via temp-file + rename for atomic-or-throw — readers
|
|
24
|
+
* never see a half-written file. The mkdir-recursive call before write
|
|
25
|
+
* is idempotent and races safely (Linux `mkdir -p` semantics).
|
|
26
|
+
*
|
|
27
|
+
* ## Persistence + cleanup
|
|
28
|
+
*
|
|
29
|
+
* The store grows unbounded. Operators who care can periodically
|
|
30
|
+
* `rm -rf ~/.ggui/code-cache/` — every entry is content-addressable
|
|
31
|
+
* and immutable, so a re-fetch will repopulate from upstream code on
|
|
32
|
+
* next push. There is no eviction policy in the seam itself.
|
|
33
|
+
*
|
|
34
|
+
* ## Permissions
|
|
35
|
+
*
|
|
36
|
+
* Files are written with default umask. The cache contains compiled
|
|
37
|
+
* componentCode the agent generated; nothing more sensitive than what
|
|
38
|
+
* already shipped over the wire to the iframe. Operators who need
|
|
39
|
+
* tighter perms wrap with their own umask.
|
|
40
|
+
*/
|
|
41
|
+
import { createHash } from 'node:crypto';
|
|
42
|
+
import { access, constants as fsConstants, mkdir, readFile, rename, writeFile, } from 'node:fs/promises';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
import { dirname, join } from 'node:path';
|
|
45
|
+
import { CODE_HASH_REGEX, } from '@ggui-ai/mcp-server-core';
|
|
46
|
+
export class FileSystemCodeStore {
|
|
47
|
+
root;
|
|
48
|
+
constructor(opts = {}) {
|
|
49
|
+
this.root = opts.root ?? join(homedir(), '.ggui', 'code-cache');
|
|
50
|
+
}
|
|
51
|
+
/** Absolute on-disk path for a given hash. Two-level sharded layout. */
|
|
52
|
+
absPath(hash) {
|
|
53
|
+
// Sharding: `<root>/<hash[0..2]>/<hash[2..]>.js`. Validation on
|
|
54
|
+
// every put + get below means `hash` is always 64 lowercase hex.
|
|
55
|
+
return join(this.root, hash.slice(0, 2), `${hash.slice(2)}.js`);
|
|
56
|
+
}
|
|
57
|
+
async put(hash, code) {
|
|
58
|
+
if (!CODE_HASH_REGEX.test(hash)) {
|
|
59
|
+
throw new Error(`FileSystemCodeStore.put: hash must match ${CODE_HASH_REGEX.source}, got ${JSON.stringify(hash)}`);
|
|
60
|
+
}
|
|
61
|
+
const absPath = this.absPath(hash);
|
|
62
|
+
// Idempotent: if the file already exists, skip rewrite. Cheap fast
|
|
63
|
+
// path that avoids unnecessary disk churn when the same blueprint
|
|
64
|
+
// is pushed repeatedly.
|
|
65
|
+
if (await fileExists(absPath))
|
|
66
|
+
return;
|
|
67
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
68
|
+
// Temp-file + rename for atomic-or-throw write; readers never see
|
|
69
|
+
// partial bytes.
|
|
70
|
+
const tmpPath = `${absPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
71
|
+
await writeFile(tmpPath, code, { encoding: 'utf-8' });
|
|
72
|
+
await rename(tmpPath, absPath);
|
|
73
|
+
}
|
|
74
|
+
async get(hash) {
|
|
75
|
+
if (!CODE_HASH_REGEX.test(hash))
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
return await readFile(this.absPath(hash), { encoding: 'utf-8' });
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err.code === 'ENOENT')
|
|
82
|
+
return null;
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
hashOf(code) {
|
|
87
|
+
return createHash('sha256').update(code, 'utf-8').digest('hex');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function fileExists(absPath) {
|
|
91
|
+
try {
|
|
92
|
+
await access(absPath, fsConstants.F_OK);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Embedded-ui cookie auth plane.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the THIRD of the three token kinds the server
|
|
5
|
+
* tracks for the embedded UI:
|
|
6
|
+
*
|
|
7
|
+
* 1. bootstrap tokens — short-TTL, single-use, MCP Apps iframes.
|
|
8
|
+
* 2. session tokens — longer-TTL, reusable, post-bootstrap
|
|
9
|
+
* reconnect creds.
|
|
10
|
+
* 3. console cookies — longer-TTL, reusable, same-origin
|
|
11
|
+
* browser-only, issued by this server
|
|
12
|
+
* for ITS OWN console landing/viewer
|
|
13
|
+
* pages. **Scoped narrowly**: consumed
|
|
14
|
+
* ONLY at the live-channel WebSocket
|
|
15
|
+
* upgrade when `cookieAuth` is wired.
|
|
16
|
+
* NEVER authenticates `/mcp`, `/pair`,
|
|
17
|
+
* `/threads`, or any other ingress.
|
|
18
|
+
*
|
|
19
|
+
* Isolation invariant (load-bearing). The cookie uses the SAME HMAC
|
|
20
|
+
* shape as the bootstrap/session tokens but a distinct `kind` claim
|
|
21
|
+
* (`'console-session'`). That makes cross-kind confusion
|
|
22
|
+
* impossible: a cookie value CAN'T verify as a bootstrap or session
|
|
23
|
+
* token. Secrets are shared across kinds because they live in the
|
|
24
|
+
* same trust domain (server-minted same-origin creds) and kind
|
|
25
|
+
* discrimination is sufficient.
|
|
26
|
+
*
|
|
27
|
+
* Rationale for cookie-over-bearer. Browsers can't set `Authorization`
|
|
28
|
+
* on native WebSocket upgrades. A same-origin HTTP-only cookie travels
|
|
29
|
+
* automatically on the upgrade request, is unreadable from JS
|
|
30
|
+
* (blocks XSS exfil), and is scoped to this origin by the browser
|
|
31
|
+
* SameSite rules. The one genuine mismatch case (cookie authentication
|
|
32
|
+
* on cross-origin requests) is defense-in-depth: set `SameSite=Strict`
|
|
33
|
+
* so the cookie never leaves the console's origin in the first
|
|
34
|
+
* place.
|
|
35
|
+
*
|
|
36
|
+
* Lifecycle. Cookie is minted at `POST /ggui/console/session-cookie`
|
|
37
|
+
* (operator posts a shortCode → server resolves → sets Set-Cookie).
|
|
38
|
+
* Cookie is consumed at `GET /ws` upgrade (if the server composition
|
|
39
|
+
* wires `cookieAuth` to the session-channel). Never mutated in
|
|
40
|
+
* between.
|
|
41
|
+
*/
|
|
42
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
43
|
+
/**
|
|
44
|
+
* Cookie name. Chosen deliberately to NOT conflict with common framework
|
|
45
|
+
* cookies (Express session, CSRF, etc.). Change carries a compat
|
|
46
|
+
* concern — the console SPA + the session-channel upgrade read by
|
|
47
|
+
* this exact name. Keep this export as the single source of truth.
|
|
48
|
+
*/
|
|
49
|
+
export declare const CONSOLE_COOKIE_NAME = "ggui_console_session";
|
|
50
|
+
/**
|
|
51
|
+
* Result of minting a cookie. Callers set the cookie on their response
|
|
52
|
+
* (the cookie endpoint) and echo the bound `sessionId`/`appId` back in
|
|
53
|
+
* the response body so the client SPA can bootstrap the viewer without
|
|
54
|
+
* a follow-up round-trip.
|
|
55
|
+
*/
|
|
56
|
+
export interface DevtoolCookieMint {
|
|
57
|
+
readonly cookieValue: string;
|
|
58
|
+
readonly setCookieHeader: string;
|
|
59
|
+
readonly expiresAt: number;
|
|
60
|
+
readonly sessionId: string;
|
|
61
|
+
readonly appId: string;
|
|
62
|
+
}
|
|
63
|
+
export interface MintDevtoolCookieInput {
|
|
64
|
+
readonly sessionId: string;
|
|
65
|
+
readonly appId: string;
|
|
66
|
+
/** HMAC secret. Shared with bootstrap + session tokens by design. */
|
|
67
|
+
readonly secret: string;
|
|
68
|
+
/** Cookie TTL in seconds. Defaults to 8 hours. */
|
|
69
|
+
readonly ttlSec?: number;
|
|
70
|
+
/**
|
|
71
|
+
* When `true`, adds `Secure` to the Set-Cookie attribute string so
|
|
72
|
+
* the cookie is only sent over HTTPS. Operators fronting the server
|
|
73
|
+
* with TLS pass `true`; local dev / HTTP-only deployments leave it
|
|
74
|
+
* falsy. No auto-detect — explicit is safer.
|
|
75
|
+
*/
|
|
76
|
+
readonly secure?: boolean;
|
|
77
|
+
/** Cookie path. Defaults to `/`. */
|
|
78
|
+
readonly path?: string;
|
|
79
|
+
/**
|
|
80
|
+
* SameSite policy. Defaults to `'Strict'` — same-origin operator
|
|
81
|
+
* convenience means we actively want the cookie NOT to travel on
|
|
82
|
+
* cross-origin navigations.
|
|
83
|
+
*/
|
|
84
|
+
readonly sameSite?: 'Strict' | 'Lax' | 'None';
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Mint a cookie value bound to `{ sessionId, appId }` and the formatted
|
|
88
|
+
* `Set-Cookie` header to send on the response. Caller does
|
|
89
|
+
* `res.setHeader('Set-Cookie', result.setCookieHeader)`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function mintDevtoolCookie(input: MintDevtoolCookieInput): DevtoolCookieMint;
|
|
92
|
+
/**
|
|
93
|
+
* Parse a raw `Cookie:` header and extract the console cookie
|
|
94
|
+
* value, or `null` if absent.
|
|
95
|
+
*
|
|
96
|
+
* Minimal manual parser — intentionally avoids pulling in a cookie
|
|
97
|
+
* library for a single cookie name. Handles the shapes we actually
|
|
98
|
+
* see (single cookie, multiple `; `-separated pairs, URL-encoded
|
|
99
|
+
* values); anything exotic returns null rather than trying to be
|
|
100
|
+
* clever.
|
|
101
|
+
*/
|
|
102
|
+
export declare function extractDevtoolCookie(cookieHeader: string | undefined): string | null;
|
|
103
|
+
/**
|
|
104
|
+
* Read the console cookie off incoming headers. Thin wrapper
|
|
105
|
+
* around {@link extractDevtoolCookie} that accepts Node's
|
|
106
|
+
* `IncomingHttpHeaders` directly — the shape both the Express
|
|
107
|
+
* request and the raw `IncomingMessage` use.
|
|
108
|
+
*
|
|
109
|
+
* Node's declared `headers['cookie']` type is `string | undefined`;
|
|
110
|
+
* both Express (post-body-parser) and node:http (the WebSocket
|
|
111
|
+
* upgrade path) consistently deliver a single string. Multi-line
|
|
112
|
+
* `Cookie:` header arrays are not a real shape we see in practice,
|
|
113
|
+
* so we don't try to support them — if a future runtime produces
|
|
114
|
+
* that shape the cookie silently 404s, which is the correct
|
|
115
|
+
* degraded behavior for this auth plane.
|
|
116
|
+
*/
|
|
117
|
+
export declare function readDevtoolCookieFromHeaders(headers: IncomingHttpHeaders): string | null;
|
|
118
|
+
/**
|
|
119
|
+
* Verified claims extracted from an console cookie. Scope is
|
|
120
|
+
* deliberately narrow: just the binding the session-channel upgrade
|
|
121
|
+
* needs to enforce `subscribe.sessionId === cookie.sessionId`.
|
|
122
|
+
*/
|
|
123
|
+
export interface DevtoolCookieClaims {
|
|
124
|
+
readonly sessionId: string;
|
|
125
|
+
readonly appId: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Verify an console cookie value. Returns the claims on success,
|
|
129
|
+
* `null` on any failure (signature, expiry, wrong kind, malformed).
|
|
130
|
+
*
|
|
131
|
+
* Never throws. Failures collapse to `null` so callers don't have to
|
|
132
|
+
* distinguish failure reasons in the hot path — an invalid cookie
|
|
133
|
+
* means "no cookie" for auth purposes. A future refinement could
|
|
134
|
+
* differentiate "cookie expired" (prompt re-mint) from "cookie is
|
|
135
|
+
* for another server" (hard reject), but that distinction is not
|
|
136
|
+
* surfaced today.
|
|
137
|
+
*/
|
|
138
|
+
export declare function verifyDevtoolCookie(cookieValue: string, secret: string): DevtoolCookieClaims | null;
|
|
139
|
+
//# sourceMappingURL=console-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-auth.d.ts","sourceRoot":"","sources":["../src/console-auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAOrD;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CAC/C;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CAqBnB;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAClC,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B,MAAM,GAAG,IAAI,CAcf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,mBAAmB,GAC3B,MAAM,GAAG,IAAI,CAIf;AAED;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,mBAAmB,GAAG,IAAI,CAO5B"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { DEFAULT_DEVTOOL_SESSION_TTL_SEC, mintDevtoolSessionToken, verifyToken, } from '@ggui-ai/mcp-server-core';
|
|
2
|
+
/**
|
|
3
|
+
* Cookie name. Chosen deliberately to NOT conflict with common framework
|
|
4
|
+
* cookies (Express session, CSRF, etc.). Change carries a compat
|
|
5
|
+
* concern — the console SPA + the session-channel upgrade read by
|
|
6
|
+
* this exact name. Keep this export as the single source of truth.
|
|
7
|
+
*/
|
|
8
|
+
export const CONSOLE_COOKIE_NAME = 'ggui_console_session';
|
|
9
|
+
/**
|
|
10
|
+
* Mint a cookie value bound to `{ sessionId, appId }` and the formatted
|
|
11
|
+
* `Set-Cookie` header to send on the response. Caller does
|
|
12
|
+
* `res.setHeader('Set-Cookie', result.setCookieHeader)`.
|
|
13
|
+
*/
|
|
14
|
+
export function mintDevtoolCookie(input) {
|
|
15
|
+
const ttlSec = input.ttlSec ?? DEFAULT_DEVTOOL_SESSION_TTL_SEC;
|
|
16
|
+
const { token, claims } = mintDevtoolSessionToken({ sessionId: input.sessionId, appId: input.appId, ttlSec }, input.secret);
|
|
17
|
+
const attrs = [
|
|
18
|
+
`${CONSOLE_COOKIE_NAME}=${token}`,
|
|
19
|
+
`Path=${input.path ?? '/'}`,
|
|
20
|
+
`Max-Age=${ttlSec}`,
|
|
21
|
+
`SameSite=${input.sameSite ?? 'Strict'}`,
|
|
22
|
+
`HttpOnly`,
|
|
23
|
+
];
|
|
24
|
+
if (input.secure)
|
|
25
|
+
attrs.push('Secure');
|
|
26
|
+
return {
|
|
27
|
+
cookieValue: token,
|
|
28
|
+
setCookieHeader: attrs.join('; '),
|
|
29
|
+
expiresAt: claims.exp * 1000,
|
|
30
|
+
sessionId: input.sessionId,
|
|
31
|
+
appId: input.appId,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse a raw `Cookie:` header and extract the console cookie
|
|
36
|
+
* value, or `null` if absent.
|
|
37
|
+
*
|
|
38
|
+
* Minimal manual parser — intentionally avoids pulling in a cookie
|
|
39
|
+
* library for a single cookie name. Handles the shapes we actually
|
|
40
|
+
* see (single cookie, multiple `; `-separated pairs, URL-encoded
|
|
41
|
+
* values); anything exotic returns null rather than trying to be
|
|
42
|
+
* clever.
|
|
43
|
+
*/
|
|
44
|
+
export function extractDevtoolCookie(cookieHeader) {
|
|
45
|
+
if (!cookieHeader)
|
|
46
|
+
return null;
|
|
47
|
+
const pairs = cookieHeader.split(';');
|
|
48
|
+
for (const raw of pairs) {
|
|
49
|
+
const trimmed = raw.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
const eq = trimmed.indexOf('=');
|
|
53
|
+
if (eq <= 0)
|
|
54
|
+
continue;
|
|
55
|
+
const name = trimmed.slice(0, eq);
|
|
56
|
+
if (name !== CONSOLE_COOKIE_NAME)
|
|
57
|
+
continue;
|
|
58
|
+
const value = trimmed.slice(eq + 1);
|
|
59
|
+
return value ? decodeURIComponent(value) : null;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Read the console cookie off incoming headers. Thin wrapper
|
|
65
|
+
* around {@link extractDevtoolCookie} that accepts Node's
|
|
66
|
+
* `IncomingHttpHeaders` directly — the shape both the Express
|
|
67
|
+
* request and the raw `IncomingMessage` use.
|
|
68
|
+
*
|
|
69
|
+
* Node's declared `headers['cookie']` type is `string | undefined`;
|
|
70
|
+
* both Express (post-body-parser) and node:http (the WebSocket
|
|
71
|
+
* upgrade path) consistently deliver a single string. Multi-line
|
|
72
|
+
* `Cookie:` header arrays are not a real shape we see in practice,
|
|
73
|
+
* so we don't try to support them — if a future runtime produces
|
|
74
|
+
* that shape the cookie silently 404s, which is the correct
|
|
75
|
+
* degraded behavior for this auth plane.
|
|
76
|
+
*/
|
|
77
|
+
export function readDevtoolCookieFromHeaders(headers) {
|
|
78
|
+
const raw = headers['cookie'];
|
|
79
|
+
if (typeof raw === 'string')
|
|
80
|
+
return extractDevtoolCookie(raw);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Verify an console cookie value. Returns the claims on success,
|
|
85
|
+
* `null` on any failure (signature, expiry, wrong kind, malformed).
|
|
86
|
+
*
|
|
87
|
+
* Never throws. Failures collapse to `null` so callers don't have to
|
|
88
|
+
* distinguish failure reasons in the hot path — an invalid cookie
|
|
89
|
+
* means "no cookie" for auth purposes. A future refinement could
|
|
90
|
+
* differentiate "cookie expired" (prompt re-mint) from "cookie is
|
|
91
|
+
* for another server" (hard reject), but that distinction is not
|
|
92
|
+
* surfaced today.
|
|
93
|
+
*/
|
|
94
|
+
export function verifyDevtoolCookie(cookieValue, secret) {
|
|
95
|
+
const result = verifyToken(cookieValue, secret, 'console-session');
|
|
96
|
+
if (!result.ok)
|
|
97
|
+
return null;
|
|
98
|
+
return {
|
|
99
|
+
sessionId: result.claims.sessionId,
|
|
100
|
+
appId: result.claims.appId,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console-facing blueprint-cache trace sink + REST/SSE endpoints
|
|
3
|
+
* powering `/devtools/cache` in the @ggui-ai/console SPA.
|
|
4
|
+
*
|
|
5
|
+
* This is the OSS-default sink the `ggui serve` process registers via
|
|
6
|
+
* {@link setCacheTraceSink}. A hosted closed runtime may swap in a
|
|
7
|
+
* durable sink (e.g. Redis-backed) — the matcher only knows about the
|
|
8
|
+
* {@link CacheTraceSink} contract.
|
|
9
|
+
*
|
|
10
|
+
* **Two surfaces, both admin-gated:**
|
|
11
|
+
* - `GET /ggui/console/cache/recent?limit=<n>` — JSON snapshot of the
|
|
12
|
+
* ring buffer's most recent N events, oldest-first within the page.
|
|
13
|
+
* Used for initial page load.
|
|
14
|
+
* - `GET /ggui/console/cache/stream` — SSE stream of new events as
|
|
15
|
+
* they fire. Heartbeat every 15s to keep proxies awake.
|
|
16
|
+
*
|
|
17
|
+
* **Memory bound.** Default capacity = 200 events. Each event carries
|
|
18
|
+
* a (truncated) intent + up to 5 candidate scores — at ~5KB/event that's
|
|
19
|
+
* ~1MB peak. Operator can override via
|
|
20
|
+
* `createGguiServer({ cacheTrace: { capacity }})` if running on a small
|
|
21
|
+
* box.
|
|
22
|
+
*
|
|
23
|
+
* **Why a separate file from console-llm-trace.** Both share the same
|
|
24
|
+
* shape (bounded buffer + SSE fanout + admin-gated REST), but:
|
|
25
|
+
*
|
|
26
|
+
* 1. The contract differ — `LlmTraceSink` carries provider/model/
|
|
27
|
+
* tokens, `CacheTraceSink` carries scope/intent/decision/
|
|
28
|
+
* candidates. Generic-typing one buffer over both narrows the
|
|
29
|
+
* operator UI's awareness of which fields to format.
|
|
30
|
+
* 2. The two can be swapped independently — a hosted closed runtime
|
|
31
|
+
* may want a durable LLM trace (cost analysis) but a lossy cache
|
|
32
|
+
* trace (ops signal only). One file per sink keeps that swap lean.
|
|
33
|
+
*/
|
|
34
|
+
import type { Express } from 'express';
|
|
35
|
+
import type { CacheTraceEvent, CacheTraceSink } from '@ggui-ai/mcp-server-handlers/session-mutations';
|
|
36
|
+
/** SSE listener — receives one event per cache lookup. */
|
|
37
|
+
type SseListener = (event: CacheTraceEvent) => void;
|
|
38
|
+
/**
|
|
39
|
+
* In-memory ring buffer + listener fanout. Implements
|
|
40
|
+
* {@link CacheTraceSink} so it can be passed to
|
|
41
|
+
* {@link setCacheTraceSink}.
|
|
42
|
+
*
|
|
43
|
+
* **Why a class, not a closure.** Same reasoning as
|
|
44
|
+
* {@link BoundedLlmTraceSink} — tests + operators read state
|
|
45
|
+
* (`recent()`, listener count); subclass is the extension point if a
|
|
46
|
+
* hosted closed runtime wants to pipe events to Redis / DDB / S3 in
|
|
47
|
+
* addition to the ring buffer.
|
|
48
|
+
*/
|
|
49
|
+
export declare class BoundedCacheTraceSink implements CacheTraceSink {
|
|
50
|
+
private readonly capacity;
|
|
51
|
+
private readonly buffer;
|
|
52
|
+
private readonly listeners;
|
|
53
|
+
constructor(opts?: {
|
|
54
|
+
readonly capacity?: number;
|
|
55
|
+
});
|
|
56
|
+
emit(event: CacheTraceEvent): void;
|
|
57
|
+
/**
|
|
58
|
+
* Snapshot of the most-recent `limit` events, oldest-first within
|
|
59
|
+
* the returned slice (so the operator UI can append in chronological
|
|
60
|
+
* order without re-sorting).
|
|
61
|
+
*/
|
|
62
|
+
recent(limit: number): readonly CacheTraceEvent[];
|
|
63
|
+
/** Subscribe to live events. Returns an unsubscribe function. */
|
|
64
|
+
subscribe(listener: SseListener): () => void;
|
|
65
|
+
/** Listener count — for tests + the eventual `/devtools/info` view. */
|
|
66
|
+
listenerCount(): number;
|
|
67
|
+
/** Buffer size — for tests + future bound enforcement assertions. */
|
|
68
|
+
size(): number;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Mount the `/ggui/console/cache/recent` + `/.../stream` routes on
|
|
72
|
+
* `app`. Caller is responsible for installing the admin gate
|
|
73
|
+
* middleware on these paths beforehand — this function does not
|
|
74
|
+
* re-implement auth.
|
|
75
|
+
*/
|
|
76
|
+
export declare function mountConsoleCacheRoutes(app: Express, sink: BoundedCacheTraceSink): void;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=console-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-cache.d.ts","sourceRoot":"","sources":["../src/console-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAqB,OAAO,EAAE,MAAM,SAAS,CAAC;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACf,MAAM,gDAAgD,CAAC;AAGxD,0DAA0D;AAC1D,KAAK,WAAW,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAEpD;;;;;;;;;;GAUG;AACH,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAyB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0B;gBAExC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE;IAUjD,IAAI,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI;IAclC;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,eAAe,EAAE;IAKjD,iEAAiE;IACjE,SAAS,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,IAAI;IAO5C,uEAAuE;IACvE,aAAa,IAAI,MAAM;IAIvB,qEAAqE;IACrE,IAAI,IAAI,MAAM;CAGf;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,qBAAqB,GAC1B,IAAI,CA8CN"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { applyDevtoolSecurityHeaders } from './console-headers.js';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory ring buffer + listener fanout. Implements
|
|
4
|
+
* {@link CacheTraceSink} so it can be passed to
|
|
5
|
+
* {@link setCacheTraceSink}.
|
|
6
|
+
*
|
|
7
|
+
* **Why a class, not a closure.** Same reasoning as
|
|
8
|
+
* {@link BoundedLlmTraceSink} — tests + operators read state
|
|
9
|
+
* (`recent()`, listener count); subclass is the extension point if a
|
|
10
|
+
* hosted closed runtime wants to pipe events to Redis / DDB / S3 in
|
|
11
|
+
* addition to the ring buffer.
|
|
12
|
+
*/
|
|
13
|
+
export class BoundedCacheTraceSink {
|
|
14
|
+
capacity;
|
|
15
|
+
buffer = [];
|
|
16
|
+
listeners = new Set();
|
|
17
|
+
constructor(opts) {
|
|
18
|
+
const cap = opts?.capacity ?? 200;
|
|
19
|
+
if (!Number.isFinite(cap) || cap <= 0) {
|
|
20
|
+
throw new Error(`BoundedCacheTraceSink: capacity must be a positive integer, got ${cap}`);
|
|
21
|
+
}
|
|
22
|
+
this.capacity = Math.floor(cap);
|
|
23
|
+
}
|
|
24
|
+
emit(event) {
|
|
25
|
+
this.buffer.push(event);
|
|
26
|
+
if (this.buffer.length > this.capacity) {
|
|
27
|
+
this.buffer.shift();
|
|
28
|
+
}
|
|
29
|
+
for (const listener of this.listeners) {
|
|
30
|
+
try {
|
|
31
|
+
listener(event);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// One bad listener must not block fan-out to others.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Snapshot of the most-recent `limit` events, oldest-first within
|
|
40
|
+
* the returned slice (so the operator UI can append in chronological
|
|
41
|
+
* order without re-sorting).
|
|
42
|
+
*/
|
|
43
|
+
recent(limit) {
|
|
44
|
+
const n = Math.max(0, Math.min(limit, this.buffer.length));
|
|
45
|
+
return this.buffer.slice(-n);
|
|
46
|
+
}
|
|
47
|
+
/** Subscribe to live events. Returns an unsubscribe function. */
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
this.listeners.add(listener);
|
|
50
|
+
return () => {
|
|
51
|
+
this.listeners.delete(listener);
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Listener count — for tests + the eventual `/devtools/info` view. */
|
|
55
|
+
listenerCount() {
|
|
56
|
+
return this.listeners.size;
|
|
57
|
+
}
|
|
58
|
+
/** Buffer size — for tests + future bound enforcement assertions. */
|
|
59
|
+
size() {
|
|
60
|
+
return this.buffer.length;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Mount the `/ggui/console/cache/recent` + `/.../stream` routes on
|
|
65
|
+
* `app`. Caller is responsible for installing the admin gate
|
|
66
|
+
* middleware on these paths beforehand — this function does not
|
|
67
|
+
* re-implement auth.
|
|
68
|
+
*/
|
|
69
|
+
export function mountConsoleCacheRoutes(app, sink) {
|
|
70
|
+
// GET /ggui/console/cache/recent?limit=<n> — JSON snapshot.
|
|
71
|
+
app.get('/ggui/console/cache/recent', (req, res) => {
|
|
72
|
+
applyDevtoolSecurityHeaders(res);
|
|
73
|
+
const limitRaw = req.query['limit'];
|
|
74
|
+
let limit = 100;
|
|
75
|
+
if (typeof limitRaw === 'string') {
|
|
76
|
+
const parsed = Number.parseInt(limitRaw, 10);
|
|
77
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
78
|
+
limit = Math.min(500, parsed);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
res.json({ events: sink.recent(limit) });
|
|
82
|
+
});
|
|
83
|
+
// GET /ggui/console/cache/stream — SSE live stream.
|
|
84
|
+
// Heartbeat comment every 15s so reverse proxies don't kill the
|
|
85
|
+
// connection on idle. Client cleanup unregisters the listener.
|
|
86
|
+
app.get('/ggui/console/cache/stream', (req, res) => {
|
|
87
|
+
applyDevtoolSecurityHeaders(res);
|
|
88
|
+
res.setHeader('content-type', 'text/event-stream');
|
|
89
|
+
res.setHeader('cache-control', 'no-store');
|
|
90
|
+
res.setHeader('connection', 'keep-alive');
|
|
91
|
+
// Flush headers immediately so the EventSource starts receiving.
|
|
92
|
+
res.flushHeaders?.();
|
|
93
|
+
const heartbeat = setInterval(() => {
|
|
94
|
+
// SSE comment frame — clients ignore but proxies see traffic.
|
|
95
|
+
res.write(': ping\n\n');
|
|
96
|
+
}, 15000);
|
|
97
|
+
const off = sink.subscribe((event) => {
|
|
98
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
99
|
+
});
|
|
100
|
+
req.on('close', () => {
|
|
101
|
+
clearInterval(heartbeat);
|
|
102
|
+
off();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security headers for console responses.
|
|
3
|
+
*
|
|
4
|
+
* Scope: applied ONLY to surfaces the console SPA itself owns —
|
|
5
|
+
* the `/ggui/console/*` API routes + the static mount + its SPA
|
|
6
|
+
* fallback. Deliberately NOT applied to `/mcp`, `/pair`, `/threads`,
|
|
7
|
+
* `/ggui/auth-check`, `/ggui/health` — those are API contract that
|
|
8
|
+
* browsers don't interpret as HTML, and bleeding CSP onto them adds
|
|
9
|
+
* noise without security value.
|
|
10
|
+
*
|
|
11
|
+
* The policy is the NARROWEST that keeps the current viewer working.
|
|
12
|
+
* Lifting any directive carries a real product decision; do NOT widen
|
|
13
|
+
* casually.
|
|
14
|
+
*
|
|
15
|
+
* - `default-src 'none'` — deny by default, opt into every source.
|
|
16
|
+
* - `script-src 'self' blob: data: <ggui-shell-hash>` — Vite emits
|
|
17
|
+
* `<script type="module" src="/assets/...">` with no inline scripts
|
|
18
|
+
* or eval (`'self'` covers that). `blob:` covers the outer dynamic
|
|
19
|
+
* `import(blob:URL)` call in `ReactComponentRenderer`'s
|
|
20
|
+
* `loadModule`: the renderer wraps compiled ESM in a Blob,
|
|
21
|
+
* `URL.createObjectURL`s it, and dynamically imports the resulting
|
|
22
|
+
* blob URL. `data:` covers the INNER bare-specifier rewrite path
|
|
23
|
+
* in `@ggui-ai/design/rendering/rewrite-imports.ts`: `import React
|
|
24
|
+
* from 'react'` inside the generated module becomes `import
|
|
25
|
+
* React from 'data:text/javascript,…shim…'`, and the browser
|
|
26
|
+
* imports that `data:` URL as a script subresource. Stripping
|
|
27
|
+
* either directive silently breaks the renderer — jsdom tests
|
|
28
|
+
* can't detect this because jsdom doesn't enforce CSP; the
|
|
29
|
+
* `live-generation.spec.ts` browser proof is what catches it.
|
|
30
|
+
* Both sources are produced by code running in THIS
|
|
31
|
+
* origin; neither permits third-party script loading, and
|
|
32
|
+
* neither enables `eval` / `new Function` (that needs
|
|
33
|
+
* `'unsafe-eval'`, which is explicitly absent).
|
|
34
|
+
*
|
|
35
|
+
* `<ggui-shell-hash>` is `GGUI_SESSION_SHELL_SCRIPT_HASH` from
|
|
36
|
+
* `mcp-apps-outbound.ts` — the sha-256 source-expression
|
|
37
|
+
* authorising the inline `<script>` block of the production thin
|
|
38
|
+
* shell `<McpAppIframe>` mounts via `srcdoc`. `srcdoc` iframes
|
|
39
|
+
* inherit the parent's CSP, so without the hash the shell's
|
|
40
|
+
* bootstrap script is blocked at parse time and the renderer is
|
|
41
|
+
* never fetched (lifecycle stuck at `mounting`, specs pinning
|
|
42
|
+
* `code-ready` time out). Hash CSP is the right shape because the
|
|
43
|
+
* shell body is static + known at build time; binding the policy
|
|
44
|
+
* to the exact bytes is narrower than `'unsafe-inline'` and
|
|
45
|
+
* narrower than a runtime nonce. Drift between this CSP and the
|
|
46
|
+
* actual script body is caught by `mcp-apps-outbound.test.ts` —
|
|
47
|
+
* edit the script, regenerate the hash.
|
|
48
|
+
*
|
|
49
|
+
* `'unsafe-inline'` is still NEVER allowed for scripts; the hash
|
|
50
|
+
* mechanism preserves the strict-no-inline posture for everything
|
|
51
|
+
* except the one known shell body whose bytes are pinned.
|
|
52
|
+
* - `style-src 'self' 'unsafe-inline'` — React `style={...}` props
|
|
53
|
+
* produce inline `style=""` attributes. `'unsafe-inline'` is
|
|
54
|
+
* required for those attribute-level styles; it does NOT open the
|
|
55
|
+
* door to inline `<script>` or `<style>` blocks (different CSP
|
|
56
|
+
* directive). Scoped, not dangerous.
|
|
57
|
+
* - `connect-src 'self'` — covers both same-origin `fetch()` to
|
|
58
|
+
* `/ggui/console/session-cookie` and the `new WebSocket('/ws')`
|
|
59
|
+
* upgrade. CSP Level 3 treats `'self'` as matching the document's
|
|
60
|
+
* origin across http/https/ws/wss, which is exactly the scope.
|
|
61
|
+
* - `img-src 'self' data:` — no images in the minimal viewer today,
|
|
62
|
+
* but data: URIs are a common future need (inline SVG, favicons).
|
|
63
|
+
* `'self'` alone would reject a minor future surface; adding data:
|
|
64
|
+
* now avoids a churn commit later.
|
|
65
|
+
* - `font-src 'self'` — no custom fonts in-tree, but a future design
|
|
66
|
+
* pass likely adds one. `'self'` matches that future without
|
|
67
|
+
* permissively allowing third-party font CDNs.
|
|
68
|
+
* - `frame-ancestors 'none'` — the console is NOT a frame target.
|
|
69
|
+
* The MCP Apps iframe shell is the framed surface (lives at a
|
|
70
|
+
* different URL with its own CSP); embedding THIS page inside
|
|
71
|
+
* another document would always be a clickjacking concern. Pairs
|
|
72
|
+
* with `X-Frame-Options: DENY` for legacy-browser coverage.
|
|
73
|
+
* - `base-uri 'none'` — no `<base>` tag in `index.html`; deny
|
|
74
|
+
* injection.
|
|
75
|
+
* - `form-action 'self'` — if a future slice adds a form, restrict
|
|
76
|
+
* submissions to same origin. Today there are no forms.
|
|
77
|
+
*
|
|
78
|
+
* What's deliberately NOT here:
|
|
79
|
+
*
|
|
80
|
+
* - `upgrade-insecure-requests` — operators run console on plain
|
|
81
|
+
* HTTP during local dev; forcing HTTPS would break that. TLS is
|
|
82
|
+
* the operator's choice via reverse proxy.
|
|
83
|
+
* - `report-uri` / `report-to` — no CSP telemetry ingestion today.
|
|
84
|
+
* A future hardening slice can add one; keeping this off avoids
|
|
85
|
+
* tying the OSS server to a specific reporting endpoint.
|
|
86
|
+
* - `Strict-Transport-Security` — same reasoning: operator owns HSTS
|
|
87
|
+
* via their reverse proxy / load balancer. The OSS server must
|
|
88
|
+
* stay HTTP-friendly for dev and self-hosted loopback.
|
|
89
|
+
*
|
|
90
|
+
* Header shape (in the order `setHeader` applies):
|
|
91
|
+
*
|
|
92
|
+
* Content-Security-Policy: <policy>
|
|
93
|
+
* X-Content-Type-Options: nosniff
|
|
94
|
+
* X-Frame-Options: DENY
|
|
95
|
+
* Referrer-Policy: strict-origin-when-cross-origin
|
|
96
|
+
* Cross-Origin-Opener-Policy: same-origin
|
|
97
|
+
*/
|
|
98
|
+
import type { Response } from 'express';
|
|
99
|
+
import type { ServerResponse } from 'node:http';
|
|
100
|
+
/**
|
|
101
|
+
* The CSP directive string. Exported so tests can assert against the
|
|
102
|
+
* exact shape without hard-coding the directive order inside the test
|
|
103
|
+
* file. Change this string and a focused header test catches the
|
|
104
|
+
* regression.
|
|
105
|
+
*/
|
|
106
|
+
export declare const DEVTOOL_CSP: string;
|
|
107
|
+
/**
|
|
108
|
+
* Other security headers applied alongside CSP. Split from `DEVTOOL_CSP`
|
|
109
|
+
* so tests can enumerate them separately. Keep this list small +
|
|
110
|
+
* well-justified — every header is a compat risk on the operator's
|
|
111
|
+
* browser matrix.
|
|
112
|
+
*/
|
|
113
|
+
export declare const DEVTOOL_SECURITY_HEADERS: ReadonlyArray<readonly [string, string]>;
|
|
114
|
+
/**
|
|
115
|
+
* Apply the console security header set to a response. Pure
|
|
116
|
+
* side-effect on `res`; returns `void`. Safe to call multiple times on
|
|
117
|
+
* the same response — `setHeader` overwrites.
|
|
118
|
+
*
|
|
119
|
+
* Accepts both Express `Response` and raw Node `ServerResponse` so the
|
|
120
|
+
* `express.static` `setHeaders(res, path, stat)` callback (which passes
|
|
121
|
+
* `ServerResponse`) can reuse the same helper.
|
|
122
|
+
*/
|
|
123
|
+
export declare function applyDevtoolSecurityHeaders(res: Response | ServerResponse): void;
|
|
124
|
+
//# sourceMappingURL=console-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-headers.d.ts","sourceRoot":"","sources":["../src/console-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,EAAE,MAcd,CAAC;AAEb;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,EAAE,aAAa,CAClD,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAO1B,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,QAAQ,GAAG,cAAc,GAC7B,IAAI,CAIN"}
|