@ggui-ai/mcp-server 0.1.0-rc.3 → 0.2.0-alpha.3

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 (59) hide show
  1. package/README.md +1 -1
  2. package/dist/build-mcp.d.ts +8 -8
  3. package/dist/build-mcp.d.ts.map +1 -1
  4. package/dist/build-mcp.js +53 -12
  5. package/dist/console-auth.d.ts +7 -7
  6. package/dist/console-auth.d.ts.map +1 -1
  7. package/dist/console-auth.js +4 -4
  8. package/dist/console-cache.d.ts +2 -2
  9. package/dist/console-cache.d.ts.map +1 -1
  10. package/dist/console-cache.js +10 -10
  11. package/dist/console-headers.d.ts +2 -2
  12. package/dist/console-headers.js +2 -2
  13. package/dist/console-payloads.d.ts +3 -3
  14. package/dist/console-payloads.d.ts.map +1 -1
  15. package/dist/console-payloads.js +10 -10
  16. package/dist/console-timeline.d.ts +12 -30
  17. package/dist/console-timeline.d.ts.map +1 -1
  18. package/dist/console-timeline.js +44 -45
  19. package/dist/index.d.ts +7 -7
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +5 -5
  22. package/dist/instructions-presets.d.ts +1 -1
  23. package/dist/instructions-presets.d.ts.map +1 -1
  24. package/dist/instructions-presets.js +40 -22
  25. package/dist/llm-backed-negotiator.d.ts +6 -6
  26. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  27. package/dist/llm-backed-negotiator.js +92 -91
  28. package/dist/mcp-apps-inbound.d.ts +3 -3
  29. package/dist/mcp-apps-inbound.d.ts.map +1 -1
  30. package/dist/mcp-apps-inbound.js +28 -23
  31. package/dist/mcp-apps-outbound.d.ts +136 -121
  32. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  33. package/dist/mcp-apps-outbound.js +384 -414
  34. package/dist/mcp-mounts.d.ts +21 -20
  35. package/dist/mcp-mounts.d.ts.map +1 -1
  36. package/dist/mcp-mounts.js +25 -29
  37. package/dist/{session-channel.d.ts → render-channel.d.ts} +145 -102
  38. package/dist/render-channel.d.ts.map +1 -0
  39. package/dist/{session-channel.js → render-channel.js} +481 -462
  40. package/dist/schema-compat.d.ts +11 -11
  41. package/dist/schema-compat.d.ts.map +1 -1
  42. package/dist/schema-compat.js +6 -6
  43. package/dist/server.d.ts +224 -219
  44. package/dist/server.d.ts.map +1 -1
  45. package/dist/server.js +1498 -1963
  46. package/dist/storage.d.ts +7 -7
  47. package/dist/storage.d.ts.map +1 -1
  48. package/dist/storage.js +12 -12
  49. package/package.json +12 -11
  50. package/dist/render-gate.d.ts +0 -87
  51. package/dist/render-gate.d.ts.map +0 -1
  52. package/dist/render-gate.js +0 -77
  53. package/dist/render-rate-limit.d.ts +0 -59
  54. package/dist/render-rate-limit.d.ts.map +0 -1
  55. package/dist/render-rate-limit.js +0 -73
  56. package/dist/render-signing.d.ts +0 -98
  57. package/dist/render-signing.d.ts.map +0 -1
  58. package/dist/render-signing.js +0 -113
  59. package/dist/session-channel.d.ts.map +0 -1
package/dist/storage.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { SessionStore, ThreadStore, VectorStore } from '@ggui-ai/mcp-server-core';
1
+ import type { RenderStore, ThreadStore, VectorStore } from '@ggui-ai/mcp-server-core';
2
2
  import type { StorageConfig } from '@ggui-ai/project-config';
3
3
  export interface ResolveStorageFromConfigOptions {
4
4
  /**
5
5
  * Directory used to resolve relative `path` values in the storage
6
6
  * config. Typically the directory containing `ggui.json` so a
7
- * manifest saying `"path": "./ggui-sessions.sqlite"` lands next to
7
+ * manifest saying `"path": "./ggui-renders.sqlite"` lands next to
8
8
  * the manifest (not CWD, which would silently create a file wherever
9
9
  * the process happened to be started).
10
10
  *
@@ -16,9 +16,9 @@ export interface ResolveStorageFromConfigOptions {
16
16
  readonly baseDir?: string;
17
17
  }
18
18
  export interface ResolvedStorageStores {
19
- /** Concrete SessionStore, iff the config declared one. Undefined =
19
+ /** Concrete RenderStore, iff the config declared one. Undefined =
20
20
  * caller falls back to createGguiServer's in-memory default. */
21
- readonly sessionStore?: SessionStore;
21
+ readonly renderStore?: RenderStore;
22
22
  /** Concrete VectorStore, iff the config declared one. Undefined =
23
23
  * caller falls back to createGguiServer's in-memory default. */
24
24
  readonly vectors?: VectorStore;
@@ -33,7 +33,7 @@ export interface ResolvedStorageStores {
33
33
  * real; routes mount and work until restart).
34
34
  * - `driver: 'sqlite'` → `SqliteThreadStore` (durable).
35
35
  *
36
- * This is the one semantic deviation from sessions/vectors, which
36
+ * This is the one semantic deviation from renders/vectors, which
37
37
  * treat `driver: 'memory'` as a no-op because `createGguiServer`
38
38
  * already defaults those to in-memory stores internally. Threads
39
39
  * don't have an implicit default — the whole route family is
@@ -60,13 +60,13 @@ export interface ResolvedStorageStores {
60
60
  * case of `threads:`, no thread
61
61
  * routes at all).
62
62
  *
63
- * - `sessions` / `vectors`:
63
+ * - `renders` / `vectors`:
64
64
  * - `driver: 'memory'` → omitted from the bundle
65
65
  * (same fallback — declaring
66
66
  * memory is the same outcome
67
67
  * as omitting it; present
68
68
  * for intent visibility).
69
- * - `driver: 'sqlite'` → `SqliteSessionStore` /
69
+ * - `driver: 'sqlite'` → `SqliteRenderStore` /
70
70
  * `SqliteVectorStore`.
71
71
  *
72
72
  * - `threads`:
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAqDA,OAAO,KAAK,EACV,YAAY,EACZ,WAAW,EACX,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,WAAW,+BAA+B;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC;oEACgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;IACrC;oEACgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;IAC/B;;;;;;;;;;;;;;;;;yCAiBqC;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,IAAI,GAAE,+BAAoC,GACzC,OAAO,CAAC,qBAAqB,CAAC,CAgDhC"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAqDA,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,WAAW,EACZ,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,WAAW,+BAA+B;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC;oEACgE;IAChE,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;oEACgE;IAChE,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;IAC/B;;;;;;;;;;;;;;;;;yCAiBqC;IACrC,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,GAAG,WAAW,CAAC;CACrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,IAAI,GAAE,+BAAoC,GACzC,OAAO,CAAC,qBAAqB,CAAC,CAgDhC"}
package/dist/storage.js CHANGED
@@ -27,12 +27,12 @@
27
27
  * ## Why `better-sqlite3` gets imported dynamically
28
28
  *
29
29
  * `better-sqlite3` is an optional peer dep of `@ggui-ai/mcp-server-core`.
30
- * If we `import { SqliteSessionStore } from '@ggui-ai/mcp-server-core/sqlite'`
30
+ * If we `import { SqliteRenderStore } from '@ggui-ai/mcp-server-core/sqlite'`
31
31
  * at the top of this file, any consumer that doesn't opt into SQLite
32
32
  * storage still pays the peer-dep cost (the module graph resolves the
33
33
  * subpath at import time, which tries to load better-sqlite3's N-API
34
34
  * binary). Dynamic `await import(...)` keeps the cost truly optional:
35
- * SQLite is loaded only when `storage.sessions.driver === 'sqlite'` or
35
+ * SQLite is loaded only when `storage.renders.driver === 'sqlite'` or
36
36
  * `storage.vectors.driver === 'sqlite'`.
37
37
  *
38
38
  * ## Why this is async + returns a bundle instead of augmenting createGguiServer
@@ -41,9 +41,9 @@
41
41
  * who want storage from config write:
42
42
  *
43
43
  * ```ts
44
- * const { sessionStore, vectors } =
44
+ * const { renderStore, vectors } =
45
45
  * await resolveStorageFromConfig(manifest.storage, { baseDir: projectRoot });
46
- * const server = createGguiServer({ sessionStore, vectors });
46
+ * const server = createGguiServer({ renderStore, vectors });
47
47
  * ```
48
48
  *
49
49
  * Explicit instances passed to `createGguiServer` still win — this
@@ -68,13 +68,13 @@ import { InMemoryThreadStore } from '@ggui-ai/mcp-server-core/in-memory';
68
68
  * case of `threads:`, no thread
69
69
  * routes at all).
70
70
  *
71
- * - `sessions` / `vectors`:
71
+ * - `renders` / `vectors`:
72
72
  * - `driver: 'memory'` → omitted from the bundle
73
73
  * (same fallback — declaring
74
74
  * memory is the same outcome
75
75
  * as omitting it; present
76
76
  * for intent visibility).
77
- * - `driver: 'sqlite'` → `SqliteSessionStore` /
77
+ * - `driver: 'sqlite'` → `SqliteRenderStore` /
78
78
  * `SqliteVectorStore`.
79
79
  *
80
80
  * - `threads`:
@@ -96,7 +96,7 @@ import { InMemoryThreadStore } from '@ggui-ai/mcp-server-core/in-memory';
96
96
  export async function resolveStorageFromConfig(config, opts = {}) {
97
97
  if (!config)
98
98
  return {};
99
- const sessionsSqlite = config.sessions?.driver === 'sqlite';
99
+ const rendersSqlite = config.renders?.driver === 'sqlite';
100
100
  const vectorsSqlite = config.vectors?.driver === 'sqlite';
101
101
  const threadsSqlite = config.threads?.driver === 'sqlite';
102
102
  const threadsMemory = config.threads?.driver === 'memory';
@@ -108,16 +108,16 @@ export async function resolveStorageFromConfig(config, opts = {}) {
108
108
  result.threadStore = new InMemoryThreadStore();
109
109
  result.threadDurability = 'ephemeral';
110
110
  }
111
- if (!sessionsSqlite && !vectorsSqlite && !threadsSqlite)
111
+ if (!rendersSqlite && !vectorsSqlite && !threadsSqlite)
112
112
  return result;
113
113
  // Single dynamic import serves every sqlite adapter — better-sqlite3's
114
114
  // N-API binding loads once even if the import lands twice; the
115
115
  // subpath barrel is cached by Node's module loader.
116
- const { SqliteSessionStore, SqliteVectorStore, SqliteThreadStore } = await import('@ggui-ai/mcp-server-core/sqlite');
117
- if (config.sessions && config.sessions.driver === 'sqlite') {
118
- const filename = resolveStoragePath(config.sessions.path, opts.baseDir);
116
+ const { SqliteRenderStore, SqliteVectorStore, SqliteThreadStore } = await import('@ggui-ai/mcp-server-core/sqlite');
117
+ if (config.renders && config.renders.driver === 'sqlite') {
118
+ const filename = resolveStoragePath(config.renders.path, opts.baseDir);
119
119
  ensureParentDir(filename);
120
- result.sessionStore = new SqliteSessionStore({ filename });
120
+ result.renderStore = new SqliteRenderStore({ filename });
121
121
  }
122
122
  if (config.vectors && config.vectors.driver === 'sqlite') {
123
123
  const filename = resolveStoragePath(config.vectors.path, opts.baseDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/mcp-server",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.2.0-alpha.3",
4
4
  "description": "Self-hosted MCP server for the ggui protocol. An HTTP/MCP binding layer over @ggui-ai/mcp-server-handlers and the reference adapters from @ggui-ai/mcp-server-core — run it via `ggui serve`.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -25,22 +25,23 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
+ "@modelcontextprotocol/ext-apps": "^1.7.2",
28
29
  "@modelcontextprotocol/sdk": "^1.27.0",
29
30
  "express": "^4.21.0",
30
31
  "nodemailer": "^8.0.7",
31
32
  "resend": "^6.12.2",
32
33
  "ws": "^8.20.1",
33
34
  "zod": "^4.3.6",
34
- "@ggui-ai/iframe-runtime": "0.1.0-rc.3",
35
- "@ggui-ai/mcp-server-core": "0.1.0-rc.3",
36
- "@ggui-ai/mcp-server-handlers": "0.1.0-rc.3",
37
- "@ggui-ai/console": "0.1.0-rc.3",
38
- "@ggui-ai/negotiator": "0.1.0-rc.3",
39
- "@ggui-ai/project-config": "0.1.0-rc.3",
40
- "@ggui-ai/preview-a2ui": "0.1.0-rc.3",
41
- "@ggui-ai/protocol": "0.1.0-rc.3",
42
- "@ggui-ai/ui-gen": "0.1.0-rc.3",
43
- "@ggui-ai/ui-registry": "0.1.0-rc.3"
35
+ "@ggui-ai/console": "0.2.0-alpha.3",
36
+ "@ggui-ai/mcp-server-core": "0.2.0-alpha.3",
37
+ "@ggui-ai/mcp-server-handlers": "0.2.0-alpha.3",
38
+ "@ggui-ai/iframe-runtime": "0.2.0-alpha.3",
39
+ "@ggui-ai/negotiator": "0.2.0-alpha.3",
40
+ "@ggui-ai/preview-a2ui": "0.2.0-alpha.3",
41
+ "@ggui-ai/project-config": "0.2.0-alpha.3",
42
+ "@ggui-ai/protocol": "0.2.0-alpha.3",
43
+ "@ggui-ai/ui-gen": "0.2.0-alpha.3",
44
+ "@ggui-ai/ui-registry": "0.2.0-alpha.3"
44
45
  },
45
46
  "devDependencies": {
46
47
  "@types/express": "^4.17.21",
@@ -1,87 +0,0 @@
1
- /**
2
- * `gateShortCode` — the single chokepoint for capability-URL routes.
3
- *
4
- * `/r/<code>` and `/api/bootstrap/<code>` are sibling surfaces that
5
- * expose the same render state via different content types (HTML
6
- * shell vs JSON bootstrap envelope). Before this gate, each route
7
- * implemented its own lookup + session resolve + 404 mapping, which
8
- * meant: hardening one route (revoke checks, sig verify, rate limit,
9
- * audit log) silently left the other unhardened. Capability URLs are
10
- * the credential — drift across the two routes is a defense gap.
11
- *
12
- * This module centralizes the gate logic. Routes ask the gate for
13
- * an outcome; content-type rendering stays on the route.
14
- *
15
- * Outcomes today:
16
- * - `ok` — binding lookup + session resolve both succeeded
17
- * - `not_found` — shortCode unknown OR revoked (lookup returned null)
18
- * - `session_missing` — binding present but session record absent
19
- *
20
- * Outcomes reserved for future slices (sig verify, rate limit, etc.):
21
- * - `invalid_signature` (HMAC slice)
22
- * - `expired` (TTL slice)
23
- * - `rate_limited` (per-code throttle slice)
24
- *
25
- * Each future slice extends the union; routes can map new codes to
26
- * appropriate status codes (403, 410, 429) without re-implementing
27
- * the lookup.
28
- */
29
- import type { Session } from '@ggui-ai/protocol';
30
- import type { ShortCodeIndex } from '@ggui-ai/mcp-server-core';
31
- import type { RenderSigner } from './render-signing.js';
32
- /** Minimal SessionStore surface the gate uses — narrower than the
33
- * full interface so test fakes stay tiny. */
34
- interface GateSessionStore {
35
- get(id: string): Promise<Session | null>;
36
- }
37
- /** The single failure surface the gate emits. New codes added here
38
- * grow the discriminated union; consumers exhaustively match. */
39
- export type RenderGateFailureCode = 'not_found' | 'session_missing' | 'invalid_signature' | 'expired' | 'malformed_signature';
40
- export type RenderGateOutcome = {
41
- readonly ok: true;
42
- readonly binding: NonNullable<Awaited<ReturnType<ShortCodeIndex['lookup']>>>;
43
- readonly session: Session;
44
- } | {
45
- readonly ok: false;
46
- readonly code: RenderGateFailureCode;
47
- /** Optional context that's safe to pass to a structured logger
48
- * but MUST NOT leak to the wire — error responses stay generic
49
- * ("shortCode not recognised") to avoid info-leak amplification
50
- * on brute-force attempts. */
51
- readonly logContext?: Record<string, unknown>;
52
- };
53
- export interface GateShortCodeInput {
54
- readonly shortCode: string;
55
- readonly shortCodeIndex: ShortCodeIndex;
56
- readonly sessionStore: GateSessionStore;
57
- /**
58
- * Optional render-URL signer. When wired, the gate verifies the
59
- * incoming `sig`+`exp` query pair BEFORE lookup; an invalid or
60
- * expired signature short-circuits with a tagged failure code so
61
- * the route can map to an appropriate status (403/410). Absent =
62
- * signing disabled (legacy or `--no-render-signing` boot).
63
- */
64
- readonly signer?: RenderSigner;
65
- /**
66
- * Raw `sig`+`exp` from the request query string. When `signer` is
67
- * wired but these are missing, the gate fails with
68
- * `malformed_signature`. When `signer` is absent, both fields are
69
- * ignored.
70
- */
71
- readonly signedQuery?: {
72
- readonly sig?: string | undefined;
73
- readonly exp?: string | undefined;
74
- };
75
- }
76
- /**
77
- * Look up a shortCode + resolve its session. Idempotent + side-
78
- * effect-free; safe to call from any HTTP handler. Returns a tagged
79
- * outcome the caller maps to its content-type-appropriate response.
80
- *
81
- * Empty input MUST be caught upstream (path-param validation) — the
82
- * gate assumes a non-empty shortCode arrives because the route
83
- * pattern requires it.
84
- */
85
- export declare function gateShortCode(input: GateShortCodeInput): Promise<RenderGateOutcome>;
86
- export {};
87
- //# sourceMappingURL=render-gate.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"render-gate.d.ts","sourceRoot":"","sources":["../src/render-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD;8CAC8C;AAC9C,UAAU,gBAAgB;IACxB,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC;CAC1C;AAED;kEACkE;AAClE,MAAM,MAAM,qBAAqB,GAC7B,WAAW,GACX,iBAAiB,GACjB,mBAAmB,GACnB,SAAS,GACT,qBAAqB,CAAC;AAE1B,MAAM,MAAM,iBAAiB,GACzB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAC3B,OAAO,CAAC,UAAU,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAC9C,CAAC;IACF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC;;;mCAG+B;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/C,CAAC;AAEN,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;IACxC,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAClC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;KACnC,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,iBAAiB,CAAC,CAwC5B"}
@@ -1,77 +0,0 @@
1
- /**
2
- * `gateShortCode` — the single chokepoint for capability-URL routes.
3
- *
4
- * `/r/<code>` and `/api/bootstrap/<code>` are sibling surfaces that
5
- * expose the same render state via different content types (HTML
6
- * shell vs JSON bootstrap envelope). Before this gate, each route
7
- * implemented its own lookup + session resolve + 404 mapping, which
8
- * meant: hardening one route (revoke checks, sig verify, rate limit,
9
- * audit log) silently left the other unhardened. Capability URLs are
10
- * the credential — drift across the two routes is a defense gap.
11
- *
12
- * This module centralizes the gate logic. Routes ask the gate for
13
- * an outcome; content-type rendering stays on the route.
14
- *
15
- * Outcomes today:
16
- * - `ok` — binding lookup + session resolve both succeeded
17
- * - `not_found` — shortCode unknown OR revoked (lookup returned null)
18
- * - `session_missing` — binding present but session record absent
19
- *
20
- * Outcomes reserved for future slices (sig verify, rate limit, etc.):
21
- * - `invalid_signature` (HMAC slice)
22
- * - `expired` (TTL slice)
23
- * - `rate_limited` (per-code throttle slice)
24
- *
25
- * Each future slice extends the union; routes can map new codes to
26
- * appropriate status codes (403, 410, 429) without re-implementing
27
- * the lookup.
28
- */
29
- /**
30
- * Look up a shortCode + resolve its session. Idempotent + side-
31
- * effect-free; safe to call from any HTTP handler. Returns a tagged
32
- * outcome the caller maps to its content-type-appropriate response.
33
- *
34
- * Empty input MUST be caught upstream (path-param validation) — the
35
- * gate assumes a non-empty shortCode arrives because the route
36
- * pattern requires it.
37
- */
38
- export async function gateShortCode(input) {
39
- // Signature verification runs BEFORE the lookup so a flood of
40
- // requests with garbage shortCodes can be rejected without hitting
41
- // the index. The signer also gates information-leak: revealing
42
- // "this code doesn't exist" vs "your sig is wrong" lets an
43
- // attacker probe shortCodes via timing differences. Rejecting on
44
- // signature first keeps the response timing uniform for any code
45
- // an attacker hasn't seen a valid sig for.
46
- if (input.signer) {
47
- const verify = input.signer.verify({
48
- shortCode: input.shortCode,
49
- sig: input.signedQuery?.sig,
50
- exp: input.signedQuery?.exp,
51
- });
52
- if (!verify.ok) {
53
- return {
54
- ok: false,
55
- code: verify.code === 'malformed'
56
- ? 'malformed_signature'
57
- : verify.code,
58
- };
59
- }
60
- }
61
- const binding = await input.shortCodeIndex.lookup(input.shortCode);
62
- if (!binding) {
63
- return { ok: false, code: 'not_found' };
64
- }
65
- const session = await input.sessionStore.get(binding.sessionId);
66
- if (!session) {
67
- return {
68
- ok: false,
69
- code: 'session_missing',
70
- logContext: {
71
- shortCode: input.shortCode,
72
- sessionId: binding.sessionId,
73
- },
74
- };
75
- }
76
- return { ok: true, binding, session };
77
- }
@@ -1,59 +0,0 @@
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
- /** Configuration knobs. Operators tune via boot options. */
24
- export interface RenderRateLimiterConfig {
25
- /** Window size in seconds. Each shortCode gets `limit` hits per
26
- * rolling window. Default 60s. */
27
- readonly windowSeconds?: number;
28
- /** Per-shortCode hit cap inside the window. Default 30. */
29
- readonly limit?: number;
30
- /** Clock seam for tests. */
31
- readonly now?: () => number;
32
- }
33
- export type RenderRateLimitResult = {
34
- readonly allowed: true;
35
- } | {
36
- readonly allowed: false;
37
- readonly retryAfterSeconds: number;
38
- };
39
- export interface RenderRateLimiter {
40
- /** Record a hit on `shortCode`. Returns the gate decision + (when
41
- * rejected) a Retry-After hint in seconds. */
42
- check(shortCode: string): RenderRateLimitResult;
43
- }
44
- /**
45
- * In-memory reference implementation. Cleanup runs lazily on each
46
- * call — entries past their window-end are reaped before the count is
47
- * read. Worst-case heap: bounded by the number of distinct shortCodes
48
- * hit within the last `windowSeconds`; pre-launch OSS workloads stay
49
- * tiny enough that the periodic pass is sufficient.
50
- */
51
- export declare function createInMemoryRenderRateLimiter(cfg?: RenderRateLimiterConfig): RenderRateLimiter;
52
- /**
53
- * Mask a shortCode for log output. The full code is the credential —
54
- * log it verbatim and a leaked log line becomes a leaked URL. Show
55
- * just enough (first 3 chars) to correlate within a session without
56
- * giving the credential away.
57
- */
58
- export declare function maskShortCode(shortCode: string): string;
59
- //# sourceMappingURL=render-rate-limit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"render-rate-limit.d.ts","sourceRoot":"","sources":["../src/render-rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,4DAA4D;AAC5D,MAAM,WAAW,uBAAuB;IACtC;uCACmC;IACnC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4BAA4B;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAC1B;IAAE,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC;mDAC+C;IAC/C,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAAC;CACjD;AAED;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAC7C,GAAG,GAAE,uBAA4B,GAChC,iBAAiB,CAmCnB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIvD"}
@@ -1,73 +0,0 @@
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
- }
@@ -1,98 +0,0 @@
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
@@ -1 +0,0 @@
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"}