@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.
Files changed (141) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +48 -0
  3. package/dist/admin-blueprints-transport.d.ts +114 -0
  4. package/dist/admin-blueprints-transport.d.ts.map +1 -0
  5. package/dist/admin-blueprints-transport.js +118 -0
  6. package/dist/admin-oauth-providers-transport.d.ts +40 -0
  7. package/dist/admin-oauth-providers-transport.d.ts.map +1 -0
  8. package/dist/admin-oauth-providers-transport.js +263 -0
  9. package/dist/auth.d.ts +39 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +75 -0
  12. package/dist/build-mcp.d.ts +128 -0
  13. package/dist/build-mcp.d.ts.map +1 -0
  14. package/dist/build-mcp.js +113 -0
  15. package/dist/code-store-fs.d.ts +19 -0
  16. package/dist/code-store-fs.d.ts.map +1 -0
  17. package/dist/code-store-fs.js +98 -0
  18. package/dist/console-auth.d.ts +139 -0
  19. package/dist/console-auth.d.ts.map +1 -0
  20. package/dist/console-auth.js +102 -0
  21. package/dist/console-cache.d.ts +78 -0
  22. package/dist/console-cache.d.ts.map +1 -0
  23. package/dist/console-cache.js +105 -0
  24. package/dist/console-headers.d.ts +124 -0
  25. package/dist/console-headers.d.ts.map +1 -0
  26. package/dist/console-headers.js +49 -0
  27. package/dist/console-llm-trace.d.ts +66 -0
  28. package/dist/console-llm-trace.d.ts.map +1 -0
  29. package/dist/console-llm-trace.js +105 -0
  30. package/dist/console-payloads.d.ts +67 -0
  31. package/dist/console-payloads.d.ts.map +1 -0
  32. package/dist/console-payloads.js +105 -0
  33. package/dist/console-theme-routes.d.ts +111 -0
  34. package/dist/console-theme-routes.d.ts.map +1 -0
  35. package/dist/console-theme-routes.js +202 -0
  36. package/dist/console-timeline.d.ts +45 -0
  37. package/dist/console-timeline.d.ts.map +1 -0
  38. package/dist/console-timeline.js +169 -0
  39. package/dist/console-validator.d.ts +67 -0
  40. package/dist/console-validator.d.ts.map +1 -0
  41. package/dist/console-validator.js +105 -0
  42. package/dist/console-welcome.d.ts +7 -0
  43. package/dist/console-welcome.d.ts.map +1 -0
  44. package/dist/console-welcome.js +221 -0
  45. package/dist/csrf-middleware.d.ts +55 -0
  46. package/dist/csrf-middleware.d.ts.map +1 -0
  47. package/dist/csrf-middleware.js +138 -0
  48. package/dist/email-login.d.ts +174 -0
  49. package/dist/email-login.d.ts.map +1 -0
  50. package/dist/email-login.js +254 -0
  51. package/dist/email-resend.d.ts +29 -0
  52. package/dist/email-resend.d.ts.map +1 -0
  53. package/dist/email-resend.js +71 -0
  54. package/dist/email-sender-from-env.d.ts +34 -0
  55. package/dist/email-sender-from-env.d.ts.map +1 -0
  56. package/dist/email-sender-from-env.js +112 -0
  57. package/dist/email-smtp.d.ts +42 -0
  58. package/dist/email-smtp.d.ts.map +1 -0
  59. package/dist/email-smtp.js +81 -0
  60. package/dist/index.d.ts +102 -0
  61. package/dist/index.d.ts.map +1 -0
  62. package/dist/index.js +122 -0
  63. package/dist/instructions-presets.d.ts +112 -0
  64. package/dist/instructions-presets.d.ts.map +1 -0
  65. package/dist/instructions-presets.js +195 -0
  66. package/dist/llm-backed-negotiator.d.ts +178 -0
  67. package/dist/llm-backed-negotiator.d.ts.map +1 -0
  68. package/dist/llm-backed-negotiator.js +579 -0
  69. package/dist/logger.d.ts +23 -0
  70. package/dist/logger.d.ts.map +1 -0
  71. package/dist/logger.js +41 -0
  72. package/dist/mcp-apps-inbound.d.ts +86 -0
  73. package/dist/mcp-apps-inbound.d.ts.map +1 -0
  74. package/dist/mcp-apps-inbound.js +278 -0
  75. package/dist/mcp-apps-outbound.d.ts +448 -0
  76. package/dist/mcp-apps-outbound.d.ts.map +1 -0
  77. package/dist/mcp-apps-outbound.js +1163 -0
  78. package/dist/mcp-mounts.d.ts +239 -0
  79. package/dist/mcp-mounts.d.ts.map +1 -0
  80. package/dist/mcp-mounts.js +222 -0
  81. package/dist/oauth-login-types.d.ts +160 -0
  82. package/dist/oauth-login-types.d.ts.map +1 -0
  83. package/dist/oauth-login-types.js +9 -0
  84. package/dist/oauth-login.d.ts +77 -0
  85. package/dist/oauth-login.d.ts.map +1 -0
  86. package/dist/oauth-login.js +455 -0
  87. package/dist/oauth-providers/github.d.ts +17 -0
  88. package/dist/oauth-providers/github.d.ts.map +1 -0
  89. package/dist/oauth-providers/github.js +89 -0
  90. package/dist/oauth-providers/google.d.ts +18 -0
  91. package/dist/oauth-providers/google.d.ts.map +1 -0
  92. package/dist/oauth-providers/google.js +59 -0
  93. package/dist/oauth-providers-store.d.ts +32 -0
  94. package/dist/oauth-providers-store.d.ts.map +1 -0
  95. package/dist/oauth-providers-store.js +291 -0
  96. package/dist/oauth.d.ts +347 -0
  97. package/dist/oauth.d.ts.map +1 -0
  98. package/dist/oauth.js +686 -0
  99. package/dist/pairing-transport.d.ts +99 -0
  100. package/dist/pairing-transport.d.ts.map +1 -0
  101. package/dist/pairing-transport.js +223 -0
  102. package/dist/rate-limit-middleware.d.ts +36 -0
  103. package/dist/rate-limit-middleware.d.ts.map +1 -0
  104. package/dist/rate-limit-middleware.js +57 -0
  105. package/dist/render-gate.d.ts +87 -0
  106. package/dist/render-gate.d.ts.map +1 -0
  107. package/dist/render-gate.js +77 -0
  108. package/dist/render-rate-limit.d.ts +59 -0
  109. package/dist/render-rate-limit.d.ts.map +1 -0
  110. package/dist/render-rate-limit.js +73 -0
  111. package/dist/render-signing.d.ts +98 -0
  112. package/dist/render-signing.d.ts.map +1 -0
  113. package/dist/render-signing.js +113 -0
  114. package/dist/request-context.d.ts +113 -0
  115. package/dist/request-context.d.ts.map +1 -0
  116. package/dist/request-context.js +154 -0
  117. package/dist/reserved-validators.d.ts +22 -0
  118. package/dist/reserved-validators.d.ts.map +1 -0
  119. package/dist/reserved-validators.js +101 -0
  120. package/dist/schema-compat.d.ts +167 -0
  121. package/dist/schema-compat.d.ts.map +1 -0
  122. package/dist/schema-compat.js +187 -0
  123. package/dist/security-headers-middleware.d.ts +38 -0
  124. package/dist/security-headers-middleware.d.ts.map +1 -0
  125. package/dist/security-headers-middleware.js +30 -0
  126. package/dist/server.d.ts +2060 -0
  127. package/dist/server.d.ts.map +1 -0
  128. package/dist/server.js +6338 -0
  129. package/dist/session-channel.d.ts +651 -0
  130. package/dist/session-channel.d.ts.map +1 -0
  131. package/dist/session-channel.js +1756 -0
  132. package/dist/storage.d.ts +89 -0
  133. package/dist/storage.d.ts.map +1 -0
  134. package/dist/storage.js +171 -0
  135. package/dist/thread-transport.d.ts +118 -0
  136. package/dist/thread-transport.d.ts.map +1 -0
  137. package/dist/thread-transport.js +478 -0
  138. package/dist/user-session-auth.d.ts +167 -0
  139. package/dist/user-session-auth.d.ts.map +1 -0
  140. package/dist/user-session-auth.js +148 -0
  141. package/package.json +76 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Per-shortCode rate limiter.
3
+ *
4
+ * Brute-force attempts on `/r/<code>` or `/api/bootstrap/<code>` are
5
+ * trivially detected at the wire by per-shortCode call counts. Even
6
+ * with high-entropy shortCodes and HMAC-signed render URLs, the gate
7
+ * still hits a real backend on every request — so DoS resistance +
8
+ * abuse-signal logging matter independently of entropy.
9
+ *
10
+ * **Design choice — rate-limit by shortCode, not by peer.**
11
+ * Cross-origin iframes (claude.ai) NAT every user through the host's
12
+ * outbound proxy, so per-peer limits would either rate-limit the whole
13
+ * host (false-positive flood) or accept every peer (no signal). Per-
14
+ * shortCode is the right granularity: 30 hits/minute on a single
15
+ * code is abuse no matter who sent them; 30 hits/minute spread across
16
+ * 30 unique codes is normal traffic.
17
+ *
18
+ * In-memory + per-process by default. Operator-grade deployments
19
+ * back this with a shared store (Redis bucket counter) — wire via the
20
+ * {@link RenderRateLimiter} interface; the OSS reference uses a
21
+ * `Map<shortCode, {windowStart, count}>` with periodic cleanup.
22
+ */
23
+ /**
24
+ * In-memory reference implementation. Cleanup runs lazily on each
25
+ * call — entries past their window-end are reaped before the count is
26
+ * read. Worst-case heap: bounded by the number of distinct shortCodes
27
+ * hit within the last `windowSeconds`; pre-launch OSS workloads stay
28
+ * tiny enough that the periodic pass is sufficient.
29
+ */
30
+ export function createInMemoryRenderRateLimiter(cfg = {}) {
31
+ const windowMs = (cfg.windowSeconds ?? 60) * 1000;
32
+ const limit = cfg.limit ?? 30;
33
+ const now = cfg.now ?? (() => Date.now());
34
+ const buckets = new Map();
35
+ return {
36
+ check(shortCode) {
37
+ if (!shortCode) {
38
+ // Empty shortCode is upstream's path-validation problem; we
39
+ // still return allowed:true to keep the contract uniform.
40
+ return { allowed: true };
41
+ }
42
+ const t = now();
43
+ const existing = buckets.get(shortCode);
44
+ if (!existing || t - existing.windowStart >= windowMs) {
45
+ // Window expired or never opened — start a fresh window.
46
+ buckets.set(shortCode, { windowStart: t, count: 1 });
47
+ return { allowed: true };
48
+ }
49
+ if (existing.count >= limit) {
50
+ const retryMs = windowMs - (t - existing.windowStart);
51
+ return {
52
+ allowed: false,
53
+ retryAfterSeconds: Math.max(1, Math.ceil(retryMs / 1000)),
54
+ };
55
+ }
56
+ existing.count += 1;
57
+ return { allowed: true };
58
+ },
59
+ };
60
+ }
61
+ /**
62
+ * Mask a shortCode for log output. The full code is the credential —
63
+ * log it verbatim and a leaked log line becomes a leaked URL. Show
64
+ * just enough (first 3 chars) to correlate within a session without
65
+ * giving the credential away.
66
+ */
67
+ export function maskShortCode(shortCode) {
68
+ if (!shortCode || shortCode.length === 0)
69
+ return '<empty>';
70
+ if (shortCode.length <= 3)
71
+ return `${shortCode}***`;
72
+ return `${shortCode.slice(0, 3)}***`;
73
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * HMAC-signed render URL helpers (capability-URL hardening).
3
+ *
4
+ * The shortCode IS the credential under the capability-URL model.
5
+ * Signed render URLs add three properties on top of the base model
6
+ * (entropy + revoke + parity gate):
7
+ *
8
+ * 1. **Time-bound.** Each minted URL carries `?exp=<unix>`. Past
9
+ * that timestamp, the gate refuses even if the binding still
10
+ * exists. A leaked URL stops working without operator action.
11
+ * 2. **Tamper-evident.** Each minted URL carries `?sig=<hmac>` over
12
+ * `(shortCode, exp)`. Changing either invalidates the sig.
13
+ * Attackers who learn ONE valid shortCode can't generate URLs
14
+ * for shortCodes they're guessing.
15
+ * 3. **Nuclear revoke (operational).** Rotating the signing secret
16
+ * invalidates every outstanding URL across the whole server in
17
+ * one move. Useful for incident response.
18
+ *
19
+ * **Threat model — what this is NOT.** The signature does not bind
20
+ * the URL to a user, IP, or session. Anyone with the URL still has
21
+ * full access until it expires. That's the Google-Docs-share-link
22
+ * model. It's the only viable model when the URL must be embeddable
23
+ * in cross-origin iframes (claude.ai), which can't attach auth
24
+ * headers — see plan audit dated 2026-05-15.
25
+ *
26
+ * **Key lifecycle.** Operators set `--render-signing-secret <hex>` to
27
+ * pin a stable key (URLs survive restarts; rotation is opt-in). When
28
+ * omitted, the secret is auto-generated at boot — restart = every
29
+ * outstanding URL dies. Document both behaviors so operators choose
30
+ * deliberately.
31
+ *
32
+ * **Opt-out.** `--no-render-signing` (operator boots without a signer)
33
+ * disables the layer entirely. The gate skips the verify step and
34
+ * behavior reverts to plain (unsigned) capability URLs. Useful for
35
+ * legacy hosts that strip query strings or tooling that pre-records
36
+ * URLs.
37
+ */
38
+ export type RenderSignerVerifyResult = {
39
+ readonly ok: true;
40
+ } | {
41
+ readonly ok: false;
42
+ readonly code: 'invalid_signature' | 'expired' | 'malformed';
43
+ };
44
+ export interface RenderSigner {
45
+ /**
46
+ * Mint a `{sig, exp}` pair for `shortCode`. `expSeconds` overrides
47
+ * the configured TTL. The returned `exp` is a unix-epoch second
48
+ * timestamp so URLs stay short.
49
+ */
50
+ sign(shortCode: string, expSecondsOverride?: number): {
51
+ readonly sig: string;
52
+ readonly exp: number;
53
+ };
54
+ /**
55
+ * Verify a `sig` + `exp` against `shortCode`. Returns `{ok: true}`
56
+ * on success; tagged failure otherwise. `malformed` covers missing
57
+ * fields, non-numeric `exp`, sig-length mismatches — distinguishing
58
+ * those from `invalid_signature` keeps the audit log informative
59
+ * without leaking detail to the wire (the response code stays
60
+ * uniform).
61
+ */
62
+ verify(args: {
63
+ readonly shortCode: string;
64
+ readonly sig: string | undefined;
65
+ readonly exp: string | undefined;
66
+ }): RenderSignerVerifyResult;
67
+ /** Encode `{sig, exp}` as a `?sig=...&exp=...` query string suitable
68
+ * for appending to a render URL. Returns the suffix WITHOUT a
69
+ * leading `?` or `&` — callers join with whatever separator their
70
+ * URL already has. */
71
+ toQuerySuffix(args: {
72
+ readonly sig: string;
73
+ readonly exp: number;
74
+ }): string;
75
+ }
76
+ export interface CreateRenderSignerInput {
77
+ /**
78
+ * 32-byte hex key. When omitted, a fresh random key is minted —
79
+ * stable for the lifetime of this process, lost on restart.
80
+ * Operators wanting URLs that survive restarts MUST pass an
81
+ * explicit key (typically from a sealed-secret store).
82
+ */
83
+ readonly secret?: string;
84
+ /**
85
+ * Default URL lifetime in seconds. Override per-call via
86
+ * {@link RenderSigner.sign}'s `expSecondsOverride`.
87
+ */
88
+ readonly ttlSeconds?: number;
89
+ /** Clock seam for tests. Defaults to `Date.now()`. */
90
+ readonly now?: () => number;
91
+ }
92
+ /**
93
+ * Build a {@link RenderSigner}. Memoize the result on the server-boot
94
+ * scope; the secret rotates only when the operator restarts (or sets
95
+ * a new explicit `--render-signing-secret`).
96
+ */
97
+ export declare function createRenderSigner(input?: CreateRenderSignerInput): RenderSigner;
98
+ //# sourceMappingURL=render-signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-signing.d.ts","sourceRoot":"","sources":["../src/render-signing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAUH,MAAM,MAAM,wBAAwB,GAChC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,mBAAmB,GAAG,SAAS,GAAG,WAAW,CAAC;CAC9D,CAAC;AAEN,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,kBAAkB,CAAC,EAAE,MAAM,GAAG;QACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;KACtB,CAAC;IAEF;;;;;;;OAOG;IACH,MAAM,CAAC,IAAI,EAAE;QACX,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;QACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;KAClC,GAAG,wBAAwB,CAAC;IAE7B;;;2BAGuB;IACvB,aAAa,CAAC,IAAI,EAAE;QAClB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;KACtB,GAAG,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,sDAAsD;IACtD,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,GAAE,uBAA4B,GAClC,YAAY,CA0Ed"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * HMAC-signed render URL helpers (capability-URL hardening).
3
+ *
4
+ * The shortCode IS the credential under the capability-URL model.
5
+ * Signed render URLs add three properties on top of the base model
6
+ * (entropy + revoke + parity gate):
7
+ *
8
+ * 1. **Time-bound.** Each minted URL carries `?exp=<unix>`. Past
9
+ * that timestamp, the gate refuses even if the binding still
10
+ * exists. A leaked URL stops working without operator action.
11
+ * 2. **Tamper-evident.** Each minted URL carries `?sig=<hmac>` over
12
+ * `(shortCode, exp)`. Changing either invalidates the sig.
13
+ * Attackers who learn ONE valid shortCode can't generate URLs
14
+ * for shortCodes they're guessing.
15
+ * 3. **Nuclear revoke (operational).** Rotating the signing secret
16
+ * invalidates every outstanding URL across the whole server in
17
+ * one move. Useful for incident response.
18
+ *
19
+ * **Threat model — what this is NOT.** The signature does not bind
20
+ * the URL to a user, IP, or session. Anyone with the URL still has
21
+ * full access until it expires. That's the Google-Docs-share-link
22
+ * model. It's the only viable model when the URL must be embeddable
23
+ * in cross-origin iframes (claude.ai), which can't attach auth
24
+ * headers — see plan audit dated 2026-05-15.
25
+ *
26
+ * **Key lifecycle.** Operators set `--render-signing-secret <hex>` to
27
+ * pin a stable key (URLs survive restarts; rotation is opt-in). When
28
+ * omitted, the secret is auto-generated at boot — restart = every
29
+ * outstanding URL dies. Document both behaviors so operators choose
30
+ * deliberately.
31
+ *
32
+ * **Opt-out.** `--no-render-signing` (operator boots without a signer)
33
+ * disables the layer entirely. The gate skips the verify step and
34
+ * behavior reverts to plain (unsigned) capability URLs. Useful for
35
+ * legacy hosts that strip query strings or tooling that pre-records
36
+ * URLs.
37
+ */
38
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
39
+ /** Default TTL when the operator doesn't override. 24h matches the
40
+ * cloud pod's ShortCode row TTL — long enough for a back-to-the-tab
41
+ * user pattern, short enough that a leaked URL doesn't outlive a
42
+ * reasonable incident-response window. */
43
+ const DEFAULT_TTL_SECONDS = 24 * 60 * 60;
44
+ /**
45
+ * Build a {@link RenderSigner}. Memoize the result on the server-boot
46
+ * scope; the secret rotates only when the operator restarts (or sets
47
+ * a new explicit `--render-signing-secret`).
48
+ */
49
+ export function createRenderSigner(input = {}) {
50
+ const secret = input.secret !== undefined && input.secret.length > 0
51
+ ? input.secret
52
+ : randomBytes(32).toString('hex');
53
+ const ttl = input.ttlSeconds !== undefined && input.ttlSeconds > 0
54
+ ? Math.floor(input.ttlSeconds)
55
+ : DEFAULT_TTL_SECONDS;
56
+ const now = input.now ?? (() => Date.now());
57
+ const secretBuffer = Buffer.from(secret, 'utf8');
58
+ function computeSig(shortCode, exp) {
59
+ // HMAC-SHA256 over `<shortCode>.<exp>`. Fixed separator + numeric
60
+ // exp keeps the input unambiguous (no length-extension or
61
+ // collision risk via overloaded encoding). Hex output stays
62
+ // URL-safe without percent-encoding.
63
+ return createHmac('sha256', secretBuffer)
64
+ .update(`${shortCode}.${exp}`)
65
+ .digest('hex');
66
+ }
67
+ return {
68
+ sign(shortCode, expSecondsOverride) {
69
+ const ttlForCall = expSecondsOverride !== undefined && expSecondsOverride > 0
70
+ ? Math.floor(expSecondsOverride)
71
+ : ttl;
72
+ const exp = Math.floor(now() / 1000) + ttlForCall;
73
+ return { sig: computeSig(shortCode, exp), exp };
74
+ },
75
+ verify({ shortCode, sig, exp }) {
76
+ if (typeof sig !== 'string' ||
77
+ typeof exp !== 'string' ||
78
+ sig.length === 0 ||
79
+ exp.length === 0) {
80
+ return { ok: false, code: 'malformed' };
81
+ }
82
+ const expNum = Number.parseInt(exp, 10);
83
+ if (!Number.isFinite(expNum) || expNum <= 0) {
84
+ return { ok: false, code: 'malformed' };
85
+ }
86
+ // Expired? Check BEFORE the HMAC compare. Expiry is a cheap
87
+ // numeric compare; bailing here on stale URLs avoids the more
88
+ // expensive constant-time sig compare on every drive-by stale
89
+ // hit. Information-leak concern: an attacker can probe `exp`
90
+ // independently of sig — but `exp` is already in the URL and
91
+ // not secret.
92
+ const nowSeconds = Math.floor(now() / 1000);
93
+ if (expNum < nowSeconds) {
94
+ return { ok: false, code: 'expired' };
95
+ }
96
+ const expected = computeSig(shortCode, expNum);
97
+ // Constant-time compare. Buffer length mismatch → fail without
98
+ // exposing length information via `timingSafeEqual` (which
99
+ // throws on length mismatch).
100
+ const a = Buffer.from(sig, 'hex');
101
+ const b = Buffer.from(expected, 'hex');
102
+ if (a.length !== b.length || a.length === 0) {
103
+ return { ok: false, code: 'invalid_signature' };
104
+ }
105
+ return timingSafeEqual(a, b)
106
+ ? { ok: true }
107
+ : { ok: false, code: 'invalid_signature' };
108
+ },
109
+ toQuerySuffix({ sig, exp }) {
110
+ return `sig=${sig}&exp=${exp}`;
111
+ },
112
+ };
113
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Per-request context plumbing — AsyncLocalStorage backed.
3
+ *
4
+ * The capability-URL routes (`/r/<code>`, `/api/bootstrap/<code>`) and
5
+ * the push/update tool result-meta builders all want to know the
6
+ * absolute public base URL of THIS server as seen by THIS client.
7
+ * That can't come from a static config in two common dev/OSS scenarios:
8
+ *
9
+ * 1. Local dev behind cloudflared / ngrok: operator runs the MCP on
10
+ * `localhost:6781`, the tunnel rewrites the public host. The
11
+ * server doesn't know its tunnel host at boot; cloudflared
12
+ * connects over loopback and adds `X-Forwarded-Host: <tunnel>`.
13
+ * 2. Co-located reverse proxy (Nginx, Caddy on the same box): same
14
+ * pattern, peer is loopback, host is in `X-Forwarded-Host`.
15
+ *
16
+ * Without auto-derive, every dev hits the "Runtime bundle failed to
17
+ * load" trap: `_meta.ggui.bootstrap.runtimeUrl` ships the relative
18
+ * `/_ggui/iframe-runtime.js`, the iframe boots inside an opaque
19
+ * srcdoc origin (claude.ai), the relative path doesn't resolve.
20
+ *
21
+ * ## Trust model — read this BEFORE adding fields here
22
+ *
23
+ * `X-Forwarded-Host` is trivially spoofable when reachable from the
24
+ * public internet (anyone with curl can send arbitrary headers). The
25
+ * only safe trust signal is **TCP peer is loopback** (127.0.0.1, ::1,
26
+ * IPv4-mapped loopback). Loopback means: this header was attached by
27
+ * a co-located process the operator deployed (cloudflared, ngrok,
28
+ * Nginx). Off-machine attackers can't reach the server via loopback;
29
+ * a remote-bind would itself be the operator's deploy choice.
30
+ *
31
+ * Auto-derive applies ONLY to data the relevant route really should
32
+ * derive from the request:
33
+ * - runtimeUrl on push/update bootstrap meta (this slice).
34
+ *
35
+ * Auto-derive MUST NOT apply to:
36
+ * - OAuth callback URLs (operator-config'd one-time per provider;
37
+ * leaking spoofed host into an OAuth flow opens redirect-attack
38
+ * vectors).
39
+ * - Email magic-link URLs (long-lived credentials; spoofed host
40
+ * makes an attacker the "trusted" destination).
41
+ * - WebSocket token base URLs.
42
+ *
43
+ * Each callsite that wants auto-derive opts in explicitly via
44
+ * `resolvePublicBaseUrl(configuredOrUndefined)`. Everything not opting
45
+ * in stays static-config-only.
46
+ */
47
+ import type { NextFunction, Request, Response } from 'express';
48
+ import { AsyncLocalStorage } from 'node:async_hooks';
49
+ /** Inferred public-facing identity of this request. */
50
+ export interface RequestContext {
51
+ /** `http` or `https` — derived from X-Forwarded-Proto when peer is
52
+ * local, else from `req.protocol`. */
53
+ readonly proto: 'http' | 'https';
54
+ /** Host:port the client sees us at. Either `req.host`, or
55
+ * `X-Forwarded-Host` (first entry of a comma-separated list)
56
+ * when the TCP peer is loopback. */
57
+ readonly host: string;
58
+ /** True iff the TCP peer is loopback. Auto-derive logic gates on
59
+ * this — see file header for the trust rationale. */
60
+ readonly peerIsLocal: boolean;
61
+ /** True iff this request carried an `X-Forwarded-Host` header AND
62
+ * the peer was loopback. This is the "I am behind a proxy" signal.
63
+ * Auto-derive (`resolvePublicBaseUrl` without an explicit configured
64
+ * value) requires both — direct localhost browser hits don't get
65
+ * their URLs rewritten, only proxy-fronted ones do. */
66
+ readonly forwardedHostHonored: boolean;
67
+ }
68
+ export declare const requestContextStore: AsyncLocalStorage<RequestContext>;
69
+ /**
70
+ * Build the Express middleware that runs every request inside an ALS
71
+ * scope. Add this near the top of the middleware chain — before any
72
+ * route handler or downstream `app.use` that may call `getRequestContext()`.
73
+ *
74
+ * Reads `req.socket.remoteAddress` directly (not `req.ip`) so a
75
+ * prior `app.set('trust proxy', ...)` configuration doesn't widen
76
+ * the loopback gate behind our back.
77
+ */
78
+ export declare function buildRequestContextMiddleware(): (req: Request, _res: Response, next: NextFunction) => void;
79
+ /** Read the request context out of the ALS store. Returns undefined
80
+ * when called outside any request (background workers, boot setup). */
81
+ export declare function getRequestContext(): RequestContext | undefined;
82
+ /**
83
+ * Resolve the public base URL for a request.
84
+ *
85
+ * Order:
86
+ * 1. Explicit configured value wins — operators ALWAYS get the last
87
+ * word. Trailing slash trimmed for join-safety.
88
+ * 2. If a request context exists AND its TCP peer is loopback AND
89
+ * the request carries a usable host, return `<proto>://<host>`.
90
+ * 3. Otherwise undefined — callers must fall back to their own
91
+ * default (typically a relative URL or skip auto-prefixing).
92
+ *
93
+ * Returns undefined (not an empty string) so callers can distinguish
94
+ * "I have a base, use it" from "I don't have a base, ship the value
95
+ * as-is" with a simple `if (base) ...` check.
96
+ */
97
+ export declare function resolvePublicBaseUrl(configured?: string): string | undefined;
98
+ /**
99
+ * Resolve `runtimeUrl` against the request. If the configured/static
100
+ * value is already absolute, return it verbatim. Otherwise prefix it
101
+ * with the public base URL
102
+ * derived from {@link resolvePublicBaseUrl}; if no base is available,
103
+ * return the raw (still-relative) value so the caller's existing
104
+ * fall-back paths kick in.
105
+ *
106
+ * Both `runtimeUrl` arguments tolerate `undefined` to keep callsites
107
+ * tidy when the dep is optional.
108
+ */
109
+ export declare function resolveRuntimeUrl(args: {
110
+ readonly configuredPublicBaseUrl?: string;
111
+ readonly runtimeUrl?: string;
112
+ }): string | undefined;
113
+ //# sourceMappingURL=request-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-context.d.ts","sourceRoot":"","sources":["../src/request-context.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,uDAAuD;AACvD,MAAM,WAAW,cAAc;IAC7B;2CACuC;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IACjC;;yCAEqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;0DACsD;IACtD,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;4DAIwD;IACxD,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACxC;AAED,eAAO,MAAM,mBAAmB,mCAA0C,CAAC;AAW3E;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,KACnC,KAAK,OAAO,EAAE,MAAM,QAAQ,EAAE,MAAM,YAAY,UAoCzD;AAED;wEACwE;AACxE,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAa5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAC1C,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,GAAG,MAAM,GAAG,SAAS,CAOrB"}
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Per-request context plumbing — AsyncLocalStorage backed.
3
+ *
4
+ * The capability-URL routes (`/r/<code>`, `/api/bootstrap/<code>`) and
5
+ * the push/update tool result-meta builders all want to know the
6
+ * absolute public base URL of THIS server as seen by THIS client.
7
+ * That can't come from a static config in two common dev/OSS scenarios:
8
+ *
9
+ * 1. Local dev behind cloudflared / ngrok: operator runs the MCP on
10
+ * `localhost:6781`, the tunnel rewrites the public host. The
11
+ * server doesn't know its tunnel host at boot; cloudflared
12
+ * connects over loopback and adds `X-Forwarded-Host: <tunnel>`.
13
+ * 2. Co-located reverse proxy (Nginx, Caddy on the same box): same
14
+ * pattern, peer is loopback, host is in `X-Forwarded-Host`.
15
+ *
16
+ * Without auto-derive, every dev hits the "Runtime bundle failed to
17
+ * load" trap: `_meta.ggui.bootstrap.runtimeUrl` ships the relative
18
+ * `/_ggui/iframe-runtime.js`, the iframe boots inside an opaque
19
+ * srcdoc origin (claude.ai), the relative path doesn't resolve.
20
+ *
21
+ * ## Trust model — read this BEFORE adding fields here
22
+ *
23
+ * `X-Forwarded-Host` is trivially spoofable when reachable from the
24
+ * public internet (anyone with curl can send arbitrary headers). The
25
+ * only safe trust signal is **TCP peer is loopback** (127.0.0.1, ::1,
26
+ * IPv4-mapped loopback). Loopback means: this header was attached by
27
+ * a co-located process the operator deployed (cloudflared, ngrok,
28
+ * Nginx). Off-machine attackers can't reach the server via loopback;
29
+ * a remote-bind would itself be the operator's deploy choice.
30
+ *
31
+ * Auto-derive applies ONLY to data the relevant route really should
32
+ * derive from the request:
33
+ * - runtimeUrl on push/update bootstrap meta (this slice).
34
+ *
35
+ * Auto-derive MUST NOT apply to:
36
+ * - OAuth callback URLs (operator-config'd one-time per provider;
37
+ * leaking spoofed host into an OAuth flow opens redirect-attack
38
+ * vectors).
39
+ * - Email magic-link URLs (long-lived credentials; spoofed host
40
+ * makes an attacker the "trusted" destination).
41
+ * - WebSocket token base URLs.
42
+ *
43
+ * Each callsite that wants auto-derive opts in explicitly via
44
+ * `resolvePublicBaseUrl(configuredOrUndefined)`. Everything not opting
45
+ * in stays static-config-only.
46
+ */
47
+ import { AsyncLocalStorage } from 'node:async_hooks';
48
+ export const requestContextStore = new AsyncLocalStorage();
49
+ /** Loopback TCP-peer addresses we accept as the "co-located reverse
50
+ * proxy" trust signal. Includes the IPv4-mapped IPv6 form Node uses
51
+ * for IPv4 connections on a dual-stack socket. */
52
+ const LOOPBACK_PEERS = new Set([
53
+ '127.0.0.1',
54
+ '::1',
55
+ '::ffff:127.0.0.1',
56
+ ]);
57
+ /**
58
+ * Build the Express middleware that runs every request inside an ALS
59
+ * scope. Add this near the top of the middleware chain — before any
60
+ * route handler or downstream `app.use` that may call `getRequestContext()`.
61
+ *
62
+ * Reads `req.socket.remoteAddress` directly (not `req.ip`) so a
63
+ * prior `app.set('trust proxy', ...)` configuration doesn't widen
64
+ * the loopback gate behind our back.
65
+ */
66
+ export function buildRequestContextMiddleware() {
67
+ return (req, _res, next) => {
68
+ const peer = req.socket.remoteAddress ?? '';
69
+ const peerIsLocal = LOOPBACK_PEERS.has(peer);
70
+ let proto = req.protocol === 'https' ? 'https' : 'http';
71
+ let host = req.get('host') ?? '';
72
+ let forwardedHostHonored = false;
73
+ if (peerIsLocal) {
74
+ const xfHost = req.get('x-forwarded-host');
75
+ const xfProto = req.get('x-forwarded-proto');
76
+ if (typeof xfHost === 'string' && xfHost.length > 0) {
77
+ // X-Forwarded-Host may be a comma-separated chain; the
78
+ // outermost (first) entry is the public-facing host.
79
+ const first = xfHost.split(',')[0];
80
+ if (typeof first === 'string' && first.trim().length > 0) {
81
+ host = first.trim();
82
+ forwardedHostHonored = true;
83
+ }
84
+ }
85
+ if (xfProto === 'http' || xfProto === 'https') {
86
+ proto = xfProto;
87
+ }
88
+ else if (typeof xfProto === 'string' && xfProto.includes(',')) {
89
+ // Same chain semantics — first entry wins.
90
+ const first = xfProto.split(',')[0]?.trim();
91
+ if (first === 'http' || first === 'https') {
92
+ proto = first;
93
+ }
94
+ }
95
+ }
96
+ requestContextStore.run({ proto, host, peerIsLocal, forwardedHostHonored }, next);
97
+ };
98
+ }
99
+ /** Read the request context out of the ALS store. Returns undefined
100
+ * when called outside any request (background workers, boot setup). */
101
+ export function getRequestContext() {
102
+ return requestContextStore.getStore();
103
+ }
104
+ /**
105
+ * Resolve the public base URL for a request.
106
+ *
107
+ * Order:
108
+ * 1. Explicit configured value wins — operators ALWAYS get the last
109
+ * word. Trailing slash trimmed for join-safety.
110
+ * 2. If a request context exists AND its TCP peer is loopback AND
111
+ * the request carries a usable host, return `<proto>://<host>`.
112
+ * 3. Otherwise undefined — callers must fall back to their own
113
+ * default (typically a relative URL or skip auto-prefixing).
114
+ *
115
+ * Returns undefined (not an empty string) so callers can distinguish
116
+ * "I have a base, use it" from "I don't have a base, ship the value
117
+ * as-is" with a simple `if (base) ...` check.
118
+ */
119
+ export function resolvePublicBaseUrl(configured) {
120
+ if (typeof configured === 'string' && configured.length > 0) {
121
+ return configured.replace(/\/$/, '');
122
+ }
123
+ const ctx = getRequestContext();
124
+ if (!ctx || !ctx.forwardedHostHonored || !ctx.host) {
125
+ // No explicit `X-Forwarded-Host` from a trusted (loopback) peer
126
+ // means there's no proxy signal to act on. Return undefined so
127
+ // callers ship their static value as-is — direct browser hits
128
+ // resolve relative URLs against the page origin just fine.
129
+ return undefined;
130
+ }
131
+ return `${ctx.proto}://${ctx.host}`;
132
+ }
133
+ /**
134
+ * Resolve `runtimeUrl` against the request. If the configured/static
135
+ * value is already absolute, return it verbatim. Otherwise prefix it
136
+ * with the public base URL
137
+ * derived from {@link resolvePublicBaseUrl}; if no base is available,
138
+ * return the raw (still-relative) value so the caller's existing
139
+ * fall-back paths kick in.
140
+ *
141
+ * Both `runtimeUrl` arguments tolerate `undefined` to keep callsites
142
+ * tidy when the dep is optional.
143
+ */
144
+ export function resolveRuntimeUrl(args) {
145
+ const raw = args.runtimeUrl;
146
+ if (raw === undefined)
147
+ return undefined;
148
+ if (/^https?:\/\//i.test(raw))
149
+ return raw;
150
+ const base = resolvePublicBaseUrl(args.configuredPublicBaseUrl);
151
+ if (!base)
152
+ return raw;
153
+ return raw.startsWith('/') ? base + raw : `${base}/${raw}`;
154
+ }
@@ -0,0 +1,22 @@
1
+ import { type ReservedChannelValidator } from '@ggui-ai/protocol';
2
+ /**
3
+ * Returns a reserved-validator map binding `_ggui:preview` to the A2UI
4
+ * adapter. Single-entry by design — each reserved channel gets its own
5
+ * validator; consumers that want to compose more should call
6
+ * {@link mergeReservedValidators}.
7
+ *
8
+ * No parameters today. When the A2UI V1 subset widens (e.g.
9
+ * `updateDataModel`), the adapter follows `parseServerMessage` without
10
+ * touching this export.
11
+ */
12
+ export declare function composePreviewReservedValidator(): ReadonlyMap<string, ReservedChannelValidator>;
13
+ /**
14
+ * Merge two reserved-validator maps into one. Keys present in
15
+ * `override` WIN on conflict — the pattern is "server supplies
16
+ * defaults (A2UI), caller may replace by key".
17
+ *
18
+ * Returns a `ReadonlyMap` so the composed result has the same
19
+ * immutability guarantee as the individual inputs.
20
+ */
21
+ export declare function mergeReservedValidators(base: ReadonlyMap<string, ReservedChannelValidator> | undefined, override: ReadonlyMap<string, ReservedChannelValidator> | undefined): ReadonlyMap<string, ReservedChannelValidator> | undefined;
22
+ //# sourceMappingURL=reserved-validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reserved-validators.d.ts","sourceRoot":"","sources":["../src/reserved-validators.ts"],"names":[],"mappings":"AA6BA,OAAO,EAGL,KAAK,wBAAwB,EAE9B,MAAM,mBAAmB,CAAC;AA0C3B;;;;;;;;;GASG;AACH,wBAAgB,+BAA+B,IAAI,WAAW,CAC5D,MAAM,EACN,wBAAwB,CACzB,CAEA;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,GAAG,SAAS,EAC/D,QAAQ,EAAE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,GAAG,SAAS,GAClE,WAAW,CAAC,MAAM,EAAE,wBAAwB,CAAC,GAAG,SAAS,CAS3D"}