@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,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"}
|