@ggui-ai/mcp-server 0.24.0 → 0.26.0

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 (70) hide show
  1. package/README.md +2 -2
  2. package/dist/api-renders-routes.d.ts +46 -5
  3. package/dist/api-renders-routes.d.ts.map +1 -1
  4. package/dist/api-renders-routes.js +98 -39
  5. package/dist/api-renders-stream-route.d.ts +2 -1
  6. package/dist/api-renders-stream-route.d.ts.map +1 -1
  7. package/dist/api-renders-stream-route.js +14 -2
  8. package/dist/build-mcp.d.ts +16 -0
  9. package/dist/build-mcp.d.ts.map +1 -1
  10. package/dist/build-mcp.js +82 -12
  11. package/dist/console-auth.d.ts +10 -10
  12. package/dist/console-auth.d.ts.map +1 -1
  13. package/dist/control-service.d.ts.map +1 -1
  14. package/dist/control-service.js +0 -3
  15. package/dist/first-seen.d.ts +14 -0
  16. package/dist/first-seen.d.ts.map +1 -0
  17. package/dist/first-seen.js +19 -0
  18. package/dist/ggui-session-channel/action-ingress.d.ts.map +1 -1
  19. package/dist/ggui-session-channel/action-ingress.js +96 -8
  20. package/dist/ggui-session-channel/channel-subscriptions.d.ts +0 -1
  21. package/dist/ggui-session-channel/channel-subscriptions.d.ts.map +1 -1
  22. package/dist/ggui-session-channel/channel-subscriptions.js +4 -1
  23. package/dist/ggui-session-channel/internal-types.d.ts +20 -12
  24. package/dist/ggui-session-channel/internal-types.d.ts.map +1 -1
  25. package/dist/ggui-session-channel/outbound.d.ts.map +1 -1
  26. package/dist/ggui-session-channel/outbound.js +18 -6
  27. package/dist/ggui-session-channel/stream-epoch.d.ts +61 -0
  28. package/dist/ggui-session-channel/stream-epoch.d.ts.map +1 -0
  29. package/dist/ggui-session-channel/stream-epoch.js +47 -0
  30. package/dist/ggui-session-channel/subscribe.d.ts +40 -53
  31. package/dist/ggui-session-channel/subscribe.d.ts.map +1 -1
  32. package/dist/ggui-session-channel/subscribe.js +288 -124
  33. package/dist/ggui-session-channel/subscriber-lifecycle.d.ts.map +1 -1
  34. package/dist/ggui-session-channel/subscriber-lifecycle.js +17 -7
  35. package/dist/ggui-session-channel.d.ts +43 -7
  36. package/dist/ggui-session-channel.d.ts.map +1 -1
  37. package/dist/ggui-session-channel.js +7 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/instructions-presets.d.ts +22 -21
  42. package/dist/instructions-presets.d.ts.map +1 -1
  43. package/dist/instructions-presets.js +38 -38
  44. package/dist/llm-backed-negotiator.d.ts +4 -37
  45. package/dist/llm-backed-negotiator.d.ts.map +1 -1
  46. package/dist/llm-backed-negotiator.js +28 -210
  47. package/dist/mcp-apps-outbound.d.ts +40 -91
  48. package/dist/mcp-apps-outbound.d.ts.map +1 -1
  49. package/dist/mcp-apps-outbound.js +100 -15
  50. package/dist/mcp-endpoint-routes.d.ts +1 -0
  51. package/dist/mcp-endpoint-routes.d.ts.map +1 -1
  52. package/dist/mcp-endpoint-routes.js +38 -4
  53. package/dist/runtime-bundle-hash.d.ts +7 -0
  54. package/dist/runtime-bundle-hash.d.ts.map +1 -1
  55. package/dist/runtime-bundle-hash.js +8 -2
  56. package/dist/runtime-bundle-route.d.ts.map +1 -1
  57. package/dist/runtime-bundle-route.js +15 -0
  58. package/dist/server.d.ts +143 -85
  59. package/dist/server.d.ts.map +1 -1
  60. package/dist/server.js +224 -110
  61. package/dist/storage.d.ts +9 -1
  62. package/dist/storage.d.ts.map +1 -1
  63. package/dist/storage.js +4 -3
  64. package/dist/view-proof-gate.d.ts +96 -0
  65. package/dist/view-proof-gate.d.ts.map +1 -0
  66. package/dist/view-proof-gate.js +167 -0
  67. package/package.json +14 -15
  68. package/dist/render-read-gate.d.ts +0 -50
  69. package/dist/render-read-gate.d.ts.map +0 -1
  70. package/dist/render-read-gate.js +0 -36
package/dist/server.js CHANGED
@@ -39,7 +39,7 @@
39
39
  */
40
40
  import { CONSOLE_DIST_DIR } from "@ggui-ai/console/server";
41
41
  import { RUNTIME_BUNDLE_FILE, RUNTIME_BUNDLE_URL_PATH, RUNTIME_SHIMS_DIR, RUNTIME_SHIMS_URL_PREFIX, } from "@ggui-ai/iframe-runtime/server";
42
- import { createDeterministicBlueprintSelector, isTokenRegisteringAuthAdapter, mintSessionToken, mintWsToken, refreshWsToken, verifyToken, } from "@ggui-ai/mcp-server-core";
42
+ import { isTokenRegisteringAuthAdapter, DEFAULT_WS_TOKEN_REFRESH_WINDOW_MULTIPLIER, DEFAULT_WS_TOKEN_TTL_SEC, mintViewRoot, mintWsToken, verifyWsTokenSignature, verifyToken, } from "@ggui-ai/mcp-server-core";
43
43
  import { createInMemoryBlueprintSearch, createInMemoryGeneratorRegistry, FixedWindowRateLimiter, InMemoryActiveConsumerRegistry, InMemoryAppMetadataStore, InMemoryAuthAdapter, InMemoryBlueprintIndex, InMemoryBlueprintStore, InMemoryKeyValueStore, InMemoryPairingService, InMemoryPendingEventConsumer, InMemoryQuotaStore, InMemoryGguiSessionStore, InMemoryGguiSessionStreamBuffer, InMemoryVectorStore, MockEmbeddingProvider, NoopAuditSink, NoopRateLimiter, NoopTelemetrySink, } from "@ggui-ai/mcp-server-core/in-memory";
44
44
  import { createGguiListGadgetsHandler, createGguiListThemesHandler, } from "@ggui-ai/mcp-server-handlers/app-discovery";
45
45
  import { createDescribeBlueprintFormatHandler, createDescribeDataContractFormatHandler, createGetBlueprintBoilerplateHandler, createGetExampleBlueprintsHandler, createListAvailablePrimitivesHandler, createListFeaturedBlueprintsHandler, createRenderBlueprintHandler, createSearchBlueprintsHandler, createValidateBlueprintHandler, } from "@ggui-ai/mcp-server-handlers/blueprints";
@@ -62,20 +62,17 @@ import { mountDevtoolThemeRoutes, } from "./console-theme-routes.js";
62
62
  import { mountConsoleTimelineRoutes } from "./console-timeline.js";
63
63
  import { buildInlineRenderShellHtml, } from "./mcp-apps-outbound.js";
64
64
  import { BoundedValidatorTraceSink, mountConsoleValidatorRoutes } from "./console-validator.js";
65
- // Operator-class MCP handlers — twelve `ggui_ops_*` handlers across
66
- // four domains (apps / orgs / connector-keys / coupon). Every factory
67
- // binds a deps seam; deployments that don't wire the seam simply
68
- // don't register the tool (matching `ggui_ops_get_credit_balance`'s
69
- // pattern).
65
+ // Operator-class MCP handlers — the six `ggui_ops_*` apps handlers.
66
+ // Every factory binds a deps seam; deployments that don't wire the
67
+ // seam simply don't register the tool (matching
68
+ // `ggui_ops_get_credit_balance`'s pattern).
70
69
  import { createCreateAppHandler, createDeleteAppHandler, createListAppsHandler, createSetAppThemeHandler, createSetDefaultAppHandler, createUpdateAppHandler, } from "@ggui-ai/mcp-server-handlers/ops-apps";
71
70
  import { fontFaceRules, validateOverlayCoverage } from "@ggui-ai/design/themes";
72
- import { createIssueConnectorKeyHandler, createListConnectorKeysHandler, createRevokeConnectorKeyHandler, } from "@ggui-ai/mcp-server-handlers/ops-connector-keys";
73
- import { createRedeemCouponHandler, } from "@ggui-ai/mcp-server-handlers/ops-coupon";
74
- import { createCreateOrgHandler, createGetOrgBalanceHandler, createInviteToOrgHandler, createListOrgsHandler, createRemoveOrgMemberHandler, createRenameOrgHandler, createRevokeInviteHandler, } from "@ggui-ai/mcp-server-handlers/ops-orgs";
75
- import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetRenderSourceHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiRuntimePullHandler, createGguiRuntimeTelemetryHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, createGguiAmendHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
71
+ import { createGguiConsumeHandler, createGguiDeclareToolCatalogHandler, createGguiEmitHandler, createGguiGetRenderSourceHandler, createGguiGetSessionHandler, createGguiHandshakeHandler, createGguiListSessionsHandler, createGguiRefreshWsTokenHandler, createGguiRenderHandler, createGguiReportRenderFailureHandler, createGguiRuntimePullHandler, createGguiRuntimeTelemetryHandler, createGguiSubmitActionHandler, createGguiSyncContextHandler, createGguiUpdateHandler, createGguiAmendHandler, InMemoryToolIdentityCatalogStore, createInMemoryProvisionalPreviewRegistry, } from "@ggui-ai/mcp-server-handlers/renders";
76
72
  import { DEFAULT_ADMIN_BLUEPRINTS_PATH, mountAdminBlueprintsTransport, } from "./admin-blueprints-transport.js";
77
73
  import { mountAdminOAuthProvidersTransport } from "./admin-oauth-providers-transport.js";
78
74
  import { DEFAULT_BUILDER_APP_ID, defaultAppIdFromIdentity } from "./auth.js";
75
+ import { assertViewProofDeclarations, createViewProofGate, unverifiableViewProofTools, } from "./view-proof-gate.js";
79
76
  import { mountMcpEndpoints } from "./mcp-endpoint-routes.js";
80
77
  import { mountApiRendersRoutes } from "./api-renders-routes.js";
81
78
  import { mountApiRendersStreamRoute } from "./api-renders-stream-route.js";
@@ -100,7 +97,7 @@ import { computeRuntimeBundleHash, insertRuntimeBundleHash, } from "./runtime-bu
100
97
  import { createCsrfMiddleware, mountCsrfTokenRoute } from "./csrf-middleware.js";
101
98
  import { mountEmailLoginRoutes } from "./email-login.js";
102
99
  import { resolveMcpInstructions } from "./instructions-presets.js";
103
- import { buildLlmCaller, createLlmBackedHandshakeNegotiator } from "./llm-backed-negotiator.js";
100
+ import { createLlmBackedHandshakeNegotiator } from "./llm-backed-negotiator.js";
104
101
  import { createConsoleLogger } from "./logger.js";
105
102
  import { buildControlService, CONTROL_PATH } from "./control-service.js";
106
103
  import { createBrowserCorsMiddleware, createPreflightFallback } from "./browser-cors.js";
@@ -234,6 +231,9 @@ export function buildOpsBlueprintHandlers(input) {
234
231
  }));
235
232
  handlers.push(createGguiOpsDeleteBlueprintHandler({
236
233
  blueprintStore: bundle.blueprintStore,
234
+ // ggui#1541 — the registry generate and register mirror into, so a
235
+ // delete takes the blueprint out of the handshake's reuse too.
236
+ ...(bundle.cacheRegistry ? { cacheRegistry: bundle.cacheRegistry } : {}),
237
237
  authorizeAppAccess: bundle.authorizeAppAccess,
238
238
  }));
239
239
  return handlers;
@@ -366,15 +366,25 @@ export function defaultHandlers(deps) {
366
366
  handlers.push(createGguiRuntimeTelemetryHandler({
367
367
  ...(deps.logger ? { logger: deps.logger } : {}),
368
368
  }));
369
- // `ggui_runtime_refresh_ws_token` — G14 (2026-05-23) signed-
370
- // envelope refresh tool. Registered only when a refresh seam is
371
- // wired (typically `channelBootstrap.refresh` from the
372
- // mcpAppsEnabled branch). Without the seam, the tool would always
373
- // return BOOTSTRAP_NOT_SUPPORTED, which is honest but useless on
374
- // tools/list — skip registration entirely.
375
- if (deps.render.bootstrapRefresh) {
369
+ // `ggui_runtime_report_render_failure` (ggui#1609) — a card whose render
370
+ // failed says so, for the session its view proof binds. Unlike
371
+ // telemetry it is recorded, so its session is proven, never claimed;
372
+ // this server names each proven report (`render_failed`) and stores
373
+ // nothing — a deployment that keeps a per-session mark supplies
374
+ // `recordRenderFailure` when it composes its own handler set.
375
+ handlers.push(createGguiReportRenderFailureHandler({
376
+ renderStore: deps.render.renderStore,
377
+ }));
378
+ // `ggui_runtime_refresh_ws_token` — the authorized re-mint of a
379
+ // view's live credential (ggui#1496 part B). Registered only when the
380
+ // deployment can both verify an envelope and mint a credential;
381
+ // without either, the tool would answer BOOTSTRAP_NOT_SUPPORTED on
382
+ // every call, which is honest but useless on tools/list.
383
+ if (deps.render.wsTokenVerify && deps.render.mintBootstrap) {
376
384
  handlers.push(createGguiRefreshWsTokenHandler({
377
- refreshSeam: deps.render.bootstrapRefresh,
385
+ renderStore: deps.render.renderStore,
386
+ verify: deps.render.wsTokenVerify,
387
+ mint: deps.render.mintBootstrap,
378
388
  }));
379
389
  }
380
390
  // Phase B (flatten-render-identity): the session lifetime entry
@@ -453,7 +463,13 @@ export function defaultHandlers(deps) {
453
463
  // Bootstrap-emission deps mirror render so MCP Apps hosts that
454
464
  // forward `ui/notifications/tool-result` via postMessage can
455
465
  // re-apply patched props on the live mount without a WS round-trip.
456
- ...(deps.update.mintBootstrap ? { mintWsToken: deps.update.mintBootstrap } : {}),
466
+ // The result door issues a view key when a key-issuing minter is
467
+ // wired (ggui#1415); otherwise it mints the live credential alone.
468
+ ...(deps.update.mintViewRoot
469
+ ? { mintWsToken: deps.update.mintViewRoot }
470
+ : deps.update.mintBootstrap
471
+ ? { mintWsToken: deps.update.mintBootstrap }
472
+ : {}),
457
473
  ...(deps.update.runtimeUrl !== undefined ? { runtimeUrl: deps.update.runtimeUrl } : {}),
458
474
  ...(deps.update.sessionApiBaseUrl !== undefined
459
475
  ? { sessionApiBaseUrl: deps.update.sessionApiBaseUrl }
@@ -637,7 +653,13 @@ export function defaultHandlers(deps) {
637
653
  ...(deps.render.streamWebSocketLocalTools !== undefined
638
654
  ? { streamWebSocketLocalTools: deps.render.streamWebSocketLocalTools }
639
655
  : {}),
640
- ...(deps.render.mintBootstrap ? { mintWsToken: deps.render.mintBootstrap } : {}),
656
+ // The result door issues a view key when a key-issuing minter is
657
+ // wired (ggui#1415); otherwise it mints the live credential alone.
658
+ ...(deps.render.mintViewRoot
659
+ ? { mintWsToken: deps.render.mintViewRoot }
660
+ : deps.render.mintBootstrap
661
+ ? { mintWsToken: deps.render.mintBootstrap }
662
+ : {}),
641
663
  ...(deps.render.runtimeUrl !== undefined ? { runtimeUrl: deps.render.runtimeUrl } : {}),
642
664
  ...(deps.render.sessionApiBaseUrl !== undefined
643
665
  ? { sessionApiBaseUrl: deps.render.sessionApiBaseUrl }
@@ -719,8 +741,8 @@ export function defaultHandlers(deps) {
719
741
  return handlers;
720
742
  }
721
743
  /**
722
- * Build the operator-class handlers for the apps / orgs /
723
- * connector-keys / coupon / blueprint domains. Each domain materializes
744
+ * Build the operator-class handlers for the apps and blueprint
745
+ * domains. Each domain materializes
724
746
  * independently when its deps seam is bound; deployments that wired
725
747
  * none get an empty list.
726
748
  *
@@ -742,19 +764,6 @@ export function buildOpsBundleHandlers(deps) {
742
764
  // REST/AppSync doors refuse the same overlay for the same reason.
743
765
  createSetAppThemeHandler({ apps, overlayCoverage: (overlay) => validateOverlayCoverage(overlay) }), createDeleteAppHandler({ apps, userDefaultApp }), createSetDefaultAppHandler({ apps, userDefaultApp }));
744
766
  }
745
- if (deps.opsOrgs) {
746
- const { orgs, invites } = deps.opsOrgs;
747
- handlers.push(createListOrgsHandler({ orgs }), createCreateOrgHandler({ orgs }), createRenameOrgHandler({ orgs }), createRemoveOrgMemberHandler({ orgs }), createGetOrgBalanceHandler({ orgs }), createInviteToOrgHandler({ invites }), createRevokeInviteHandler({ invites }));
748
- }
749
- if (deps.opsConnectorKeys) {
750
- const { connectorKeys } = deps.opsConnectorKeys;
751
- handlers.push(createListConnectorKeysHandler({ connectorKeys }), createIssueConnectorKeyHandler({ connectorKeys }), createRevokeConnectorKeyHandler({ connectorKeys }));
752
- }
753
- if (deps.opsCoupon) {
754
- handlers.push(createRedeemCouponHandler({
755
- coupons: deps.opsCoupon.coupons,
756
- }));
757
- }
758
767
  if (deps.opsBlueprint) {
759
768
  // No server-level `appMetadataStore` fallback on this path — the
760
769
  // bundle's own field governs, since a deployment that binds its own
@@ -766,12 +775,49 @@ export function buildOpsBundleHandlers(deps) {
766
775
  }
767
776
  return handlers;
768
777
  }
778
+ /**
779
+ * How long {@link GguiServer.close} lets active connections finish before
780
+ * it ends them (ggui#1631), in milliseconds.
781
+ */
782
+ export const DEFAULT_CLOSE_GRACE_MS = 5000;
783
+ /**
784
+ * The ws-token TTL and refresh window a server mints with (ggui#1496 part
785
+ * B). Both are positive whole seconds, and the window is at least the TTL:
786
+ * then every token this server mints, root and renewal alike, is past its
787
+ * `exp` by `rootIat + window`, so every gate refuses it in the same second.
788
+ * (A token minted while the window was longer, or by a server sharing the
789
+ * secret with a longer window, can outlive that second on the other gates;
790
+ * only `/state` refuses it.)
791
+ */
792
+ function resolveWsTokenLifetimes(opts) {
793
+ const wholePositive = (v) => Number.isInteger(v) && v > 0;
794
+ const ttlSec = opts.wsTokenTtlSec ?? DEFAULT_WS_TOKEN_TTL_SEC;
795
+ if (!wholePositive(ttlSec)) {
796
+ throw new Error(`createGguiServer: wsTokenTtlSec must be a positive whole number of seconds (got ${String(ttlSec)}).`);
797
+ }
798
+ const windowSec = opts.wsTokenRefreshWindowSec ?? DEFAULT_WS_TOKEN_REFRESH_WINDOW_MULTIPLIER * ttlSec;
799
+ if (!wholePositive(windowSec)) {
800
+ throw new Error(`createGguiServer: wsTokenRefreshWindowSec must be a positive whole number of seconds (got ${String(windowSec)}).`);
801
+ }
802
+ if (windowSec < ttlSec) {
803
+ throw new Error(`createGguiServer: wsTokenRefreshWindowSec (${windowSec}) must be at least wsTokenTtlSec (${ttlSec}); a shorter window would let a root token outlive its own renewal chain.`);
804
+ }
805
+ return { ttlSec, windowSec };
806
+ }
769
807
  /**
770
808
  * Build a runnable OSS MCP server. Every option has a sensible default
771
809
  * so `createGguiServer()` with no arguments boots a working in-memory
772
810
  * server on demand.
773
811
  */
774
812
  export function createGguiServer(opts = {}) {
813
+ if ((opts.perAppRouting?.perAppOnlySources?.length ?? 0) > 0 && !opts.perAppRouting?.authorize) {
814
+ throw new Error("createGguiServer: perAppRouting.perAppOnlySources lists credential sources that are valid only at their app's endpoint, but perAppRouting.authorize is not wired");
815
+ }
816
+ const { ttlSec: wsTokenTtlSec, windowSec: wsTokenRefreshWindowSec } = resolveWsTokenLifetimes(opts);
817
+ const keyedSince = opts.viewProof?.keyedSince;
818
+ if (keyedSince !== undefined && (!Number.isFinite(keyedSince) || keyedSince < 0)) {
819
+ throw new Error(`createGguiServer: viewProof.keyedSince must be a finite, non-negative epoch in milliseconds (got ${String(keyedSince)}).`);
820
+ }
775
821
  const info = { ...DEFAULT_INFO, ...opts.info };
776
822
  const logger = opts.logger ?? createConsoleLogger({ server: info.name });
777
823
  const bodyLimit = opts.bodyLimit ?? "4mb";
@@ -986,9 +1032,23 @@ export function createGguiServer(opts = {}) {
986
1032
  // (which always call this on the live-mode path) get clean strings
987
1033
  // without per-callsite narrowing.
988
1034
  let mintBootstrap;
1035
+ // The key-issuing doors' minters (ggui#1415): a root ws token stamped
1036
+ // with the secret's key id and the door, and the view key rooted in it.
1037
+ // Only the render/update results (`result`) and the data-plane read door
1038
+ // (`read`), on a read that issues the key, use them; every bearer door,
1039
+ // and every read that issues no key, mints through `mintBootstrap`.
1040
+ let mintResultViewRoot;
1041
+ let mintReadViewRoot;
1042
+ // `/state`'s one minter (ggui#1496 part B, slice 2): a possession renewal
1043
+ // that carries its chain's root forward. Every other door mints a root
1044
+ // through `mintBootstrap`.
1045
+ let mintChainedBootstrap;
989
1046
  let channelBootstrap;
990
- // Shared HMAC secret for server-minted creds (bootstrap tokens,
991
- // session tokens, console cookies). Distinct `kind` claims
1047
+ // The authorized refresh's envelope check (ggui#1496 part B): signature,
1048
+ // shape and kind at ANY age, over the same secret the minter signs with.
1049
+ let wsTokenVerify;
1050
+ // Shared HMAC secret for server-minted creds (ws bootstrap tokens,
1051
+ // console cookies). Distinct `kind` claims
992
1052
  // prevent cross-kind confusion; sharing the secret keeps the
993
1053
  // config surface small and means operators rotate ONE value.
994
1054
  // Resolved here so both the MCP Apps block and the console
@@ -1009,7 +1069,7 @@ export function createGguiServer(opts = {}) {
1009
1069
  // from `@ggui-ai/mcp-server-core` for callers that need explicit
1010
1070
  // single-use semantics (one-time-link share, etc.).
1011
1071
  const syncMinter = (sessionId, appId) => {
1012
- const { token, claims } = mintWsToken({ sessionId, appId }, secret);
1072
+ const { token, claims } = mintWsToken({ sessionId, appId, ttlSec: wsTokenTtlSec }, secret);
1013
1073
  return {
1014
1074
  wsUrl,
1015
1075
  token,
@@ -1017,6 +1077,36 @@ export function createGguiServer(opts = {}) {
1017
1077
  };
1018
1078
  };
1019
1079
  mintBootstrap = syncMinter;
1080
+ const viewRootMinter = (src) => (sessionId, appId) => {
1081
+ const minted = mintViewRoot({ sessionId, appId, ttlSec: wsTokenTtlSec, src }, secret);
1082
+ if (minted.viewKeyNotIssued !== undefined) {
1083
+ logger.info("view_key_not_issued", { sessionId, src, reason: minted.viewKeyNotIssued });
1084
+ }
1085
+ return {
1086
+ wsUrl,
1087
+ token: minted.token,
1088
+ expiresAt: new Date(minted.claims.exp * 1000).toISOString(),
1089
+ viewKey: minted.viewKey,
1090
+ };
1091
+ };
1092
+ mintResultViewRoot = viewRootMinter("result");
1093
+ mintReadViewRoot = viewRootMinter("read");
1094
+ // The renewal's `exp` is capped at `rootIat + window` by the mint
1095
+ // itself (`notAfter`), from the same clock read that stamps its `iat`.
1096
+ // No second left means nothing to renew (`undefined`, answered as the
1097
+ // ordinary expiry). That happens when the bound's second arrives between
1098
+ // the route's expiry check and this mint (the route awaits the store in
1099
+ // between), or for a token minted while the window was longer.
1100
+ mintChainedBootstrap = (sessionId, appId, rootIat) => {
1101
+ const { token, claims } = mintWsToken({ sessionId, appId, ttlSec: wsTokenTtlSec, rootIat, notAfter: rootIat + wsTokenRefreshWindowSec }, secret);
1102
+ if (claims.exp <= claims.iat)
1103
+ return undefined;
1104
+ return {
1105
+ wsUrl,
1106
+ token,
1107
+ expiresAt: new Date(claims.exp * 1000).toISOString(),
1108
+ };
1109
+ };
1020
1110
  channelBootstrap = {
1021
1111
  verify: (token) => {
1022
1112
  const result = verifyToken(token, secret, "ws");
@@ -1035,28 +1125,17 @@ export function createGguiServer(opts = {}) {
1035
1125
  }
1036
1126
  return { ok: false, reason: "invalid" };
1037
1127
  },
1038
- issueSessionToken: (sessionId, appId) => {
1039
- const { token } = mintSessionToken({ sessionId, appId }, secret);
1040
- return token;
1041
- },
1042
- refresh: (token) => {
1043
- const result = refreshWsToken(token, secret);
1044
- if (result.ok) {
1045
- return {
1046
- ok: true,
1047
- token: result.token,
1048
- expiresAt: new Date(result.claims.exp * 1000).toISOString(),
1049
- };
1050
- }
1051
- if (result.reason === "refresh_window_closed") {
1052
- return { ok: false, reason: "window_closed" };
1128
+ };
1129
+ wsTokenVerify = (envelope) => {
1130
+ const result = verifyWsTokenSignature(envelope, secret);
1131
+ return result.ok
1132
+ ? {
1133
+ ok: true,
1134
+ sessionId: result.claims.sessionId,
1135
+ appId: result.claims.appId,
1136
+ rootIat: result.claims.rootIat ?? result.claims.iat,
1053
1137
  }
1054
- // Tamper / format / kind / shape failures collapse into a
1055
- // single `invalid` — the iframe MUST re-handshake; the exact
1056
- // breakage type is logged server-side, not surfaced on the
1057
- // wire (would be useful only to attackers probing the surface).
1058
- return { ok: false, reason: "invalid" };
1059
- },
1138
+ : { ok: false };
1060
1139
  };
1061
1140
  }
1062
1141
  // Live-channel render endpoint reference. Declared here (not at the
@@ -1162,7 +1241,6 @@ export function createGguiServer(opts = {}) {
1162
1241
  new InMemoryBlueprintStore({
1163
1242
  embeddingProvider: opts.embedding,
1164
1243
  });
1165
- const blueprintSelector = opts.blueprintSelector ?? createDeterministicBlueprintSelector();
1166
1244
  // Auto-seed the multi-axis search against the resolved store.
1167
1245
  // Type-narrowed at the call site: when the caller passed
1168
1246
  // their own store we can't assume it implements `listAllForApp`,
@@ -1445,15 +1523,11 @@ export function createGguiServer(opts = {}) {
1445
1523
  ? { postFailureHook: opts.renderPostFailureHook }
1446
1524
  : {}),
1447
1525
  ...(mintBootstrap ? { mintBootstrap } : {}),
1448
- // G14 (2026-05-23) refresh seam. Same `channelBootstrap`
1449
- // the WS upgrade path uses — sharing it means one HMAC
1450
- // secret + one refresh-window policy across the verify
1451
- // path and the `ggui_runtime_refresh_ws_token` tool.
1452
- // Absent when MCP Apps isn't enabled or no bootstrap
1453
- // secret is wired; the tool isn't registered in that case.
1454
- ...(channelBootstrap
1455
- ? { bootstrapRefresh: { refresh: channelBootstrap.refresh } }
1456
- : {}),
1526
+ ...(mintResultViewRoot ? { mintViewRoot: mintResultViewRoot } : {}),
1527
+ // The authorized refresh's envelope check (ggui#1496 part B),
1528
+ // over the same secret as `mintBootstrap`. Absent when MCP
1529
+ // Apps isn't enabled; the tool isn't registered then.
1530
+ ...(wsTokenVerify ? { wsTokenVerify } : {}),
1457
1531
  // Iframe-runtime bundle URL — padded onto the
1458
1532
  // `ai.ggui/render.runtimeUrl` slice field by the render
1459
1533
  // handler's resultMeta. C8 made this required on
@@ -1526,25 +1600,7 @@ export function createGguiServer(opts = {}) {
1526
1600
  // every story-path render + emits `cache.hit /
1527
1601
  // similarity / cachedBlueprintId / llmCallsAvoided` on
1528
1602
  // structuredContent.
1529
- ...(generationWithCache
1530
- ? {
1531
- generation: {
1532
- ...generationWithCache,
1533
- // Thread an LLMCaller resolver into
1534
- // render so the registry-based three-tier matcher
1535
- // can fire its rerank step. Per-call resolution
1536
- // because BYOK creds depend on ctx.
1537
- resolveLlmCaller: async (ctx) => {
1538
- if (!generationWithCache.resolveLlm)
1539
- return null;
1540
- const creds = await generationWithCache.resolveLlm(ctx);
1541
- if (!creds)
1542
- return null;
1543
- return buildLlmCaller(creds.selection, creds.providerKey);
1544
- },
1545
- },
1546
- }
1547
- : {}),
1603
+ ...(generationWithCache ? { generation: generationWithCache } : {}),
1548
1604
  // Live-subscriber notifier. Late-binds to the
1549
1605
  // GguiSessionChannelServer created further down (`channel`
1550
1606
  // / `channelForHealth`) — the channel doesn't exist yet
@@ -1678,6 +1734,7 @@ export function createGguiServer(opts = {}) {
1678
1734
  // resolvers keeps the two transports byte-identical at the
1679
1735
  // bootstrap-projection boundary.
1680
1736
  ...(mintBootstrap ? { mintBootstrap } : {}),
1737
+ ...(mintResultViewRoot ? { mintViewRoot: mintResultViewRoot } : {}),
1681
1738
  // Function form: matches render so ggui_update emits the
1682
1739
  // same absolute URL when the server sits behind a tunnel
1683
1740
  // / reverse proxy.
@@ -1771,12 +1828,12 @@ export function createGguiServer(opts = {}) {
1771
1828
  // same sink as the rest of the server.
1772
1829
  logger,
1773
1830
  });
1774
- // Operator-class domain handlers (apps / orgs / connector-keys /
1775
- // coupon / blueprints). Built on EVERY path — they hang off their own
1776
- // explicit options, so a deployment that supplies a custom base
1777
- // handler list still gets the domains it wired. A name already claimed
1778
- // by the base list wins: that's how a hosted deployment ships its own
1779
- // `ggui_ops_create_app` while still picking up the rest of the family,
1831
+ // Operator-class domain handlers (apps / blueprints). Built on EVERY
1832
+ // path — they hang off their own explicit options, so a deployment
1833
+ // that supplies a custom base handler list still gets the domains it
1834
+ // wired. A name already claimed by the base list wins: that's how a
1835
+ // deployment ships its own `ggui_ops_create_app` while still picking
1836
+ // up the rest of the family,
1780
1837
  // and it is what keeps the blueprint family single-registered when the
1781
1838
  // default handler set already built it from the same bundle.
1782
1839
  const baseHandlerNames = new Set(baseHandlers.map((h) => h.name));
@@ -1789,6 +1846,30 @@ export function createGguiServer(opts = {}) {
1789
1846
  // construction instead of at first `tools/call`. The actual route
1790
1847
  // mounting happens below alongside the canonical routes.
1791
1848
  const mcpServices = validateMcpServices(opts.mcpServices);
1849
+ // ggui#1415 — the view-origin proof's measuring gate, over every handler
1850
+ // this server serves (the data and control planes, mounts, and isolated
1851
+ // services). Every declared proof must be one a view can sign (refused at
1852
+ // boot otherwise). The gate runs where views are keyed (MCP Apps on);
1853
+ // without it, a declared call's line says `viewProofUnverifiable`, named
1854
+ // once here.
1855
+ const servedHandlers = [...handlers, ...mcpServices.flatMap((service) => service.handlers)];
1856
+ assertViewProofDeclarations(servedHandlers);
1857
+ const viewProofGate = mcpAppsEnabled && sharedTokenSecret !== undefined
1858
+ ? createViewProofGate({
1859
+ secret: sharedTokenSecret,
1860
+ sessionStore: renderStore,
1861
+ ...(keyedSince !== undefined ? { keyedSince } : {}),
1862
+ })
1863
+ : undefined;
1864
+ if (viewProofGate === undefined) {
1865
+ const unverifiable = unverifiableViewProofTools(servedHandlers);
1866
+ if (unverifiable.length > 0) {
1867
+ logger.warn("view_proof_unverifiable", {
1868
+ tools: unverifiable,
1869
+ hint: "These tools declare a view proof. MCP Apps is off, so this server keys no view and does not verify proofs.",
1870
+ });
1871
+ }
1872
+ }
1792
1873
  // The control plane — every `protocol`- and `ops`-tagged handler,
1793
1874
  // projected onto one anonymous-capable route with per-tool auth and
1794
1875
  // confirmation gates. Built from the SAME composed `handlers` array
@@ -1887,10 +1968,12 @@ export function createGguiServer(opts = {}) {
1887
1968
  // pass a stable `csrfSecret` so tokens survive a restart.
1888
1969
  app.use(createSecurityHeadersMiddleware());
1889
1970
  // Resolved once at boot — every per-request `buildMcpServer` call
1890
- // receives the same string, so `tools/list` + `initialize` stay
1891
- // consistent across the lifetime of the process. Operators tune
1892
- // via `mcpInstructions: 'default' | 'aggressive' | 'minimal' | 'off'`
1893
- // or a custom string. See `instructions-presets.ts` for full copy.
1971
+ // receives the same string, so every `initialize` result carries the
1972
+ // same instructions for the life of the process. Operators opt in via
1973
+ // `mcpInstructions: 'default' | 'aggressive' | 'always' | 'minimal'` or
1974
+ // a custom string; unset or `'off'` sends none (ggui#1579). The boot
1975
+ // line says what is SENT, so it can never again name text no host got.
1976
+ // See `instructions-presets.ts` for the copy.
1894
1977
  const resolvedInstructions = resolveMcpInstructions(opts.mcpInstructions);
1895
1978
  if (resolvedInstructions) {
1896
1979
  logger.info("mcp_instructions_set", {
@@ -1901,15 +1984,19 @@ export function createGguiServer(opts = {}) {
1901
1984
  opts.mcpInstructions === "minimal" ||
1902
1985
  opts.mcpInstructions === "off")
1903
1986
  ? opts.mcpInstructions
1904
- : opts.mcpInstructions === undefined
1905
- ? "default (no-preset fallback)"
1906
- : "custom",
1987
+ : "custom",
1907
1988
  length: resolvedInstructions.length,
1989
+ sentInInitialize: true,
1908
1990
  });
1909
1991
  }
1910
1992
  else {
1911
1993
  logger.info("mcp_instructions_off", {
1912
- reason: opts.mcpInstructions === "off" ? "preset=off" : "empty custom string",
1994
+ reason: opts.mcpInstructions === undefined
1995
+ ? "unset (the default is off)"
1996
+ : opts.mcpInstructions === "off"
1997
+ ? "preset=off"
1998
+ : "empty custom string",
1999
+ sentInInitialize: false,
1913
2000
  });
1914
2001
  }
1915
2002
  const csrfSecret = opts.csrfSecret && opts.csrfSecret.length >= 32
@@ -2053,6 +2140,7 @@ export function createGguiServer(opts = {}) {
2053
2140
  // endpoint family spreads a fresh copy per request.
2054
2141
  const buildMcpOptions = {
2055
2142
  mcpAppsOutbound: mcpAppsEnabled,
2143
+ ...(viewProofGate !== undefined ? { viewProofGate } : {}),
2056
2144
  // Caller-provided `shellHtml` overrides the default (and the
2057
2145
  // inline-runtime shell); `installMcpAppsOutbound` falls back to
2058
2146
  // its baked `GGUI_RENDER_SHELL_HTML` constant when both are
@@ -2225,6 +2313,10 @@ export function createGguiServer(opts = {}) {
2225
2313
  // Mirrors the per-tool `mintBootstrap` plumbed into
2226
2314
  // the handler factory above.
2227
2315
  ...(mintBootstrap ? { mintWsToken: mintBootstrap } : {}),
2316
+ // The data-plane read door issues a view key (ggui#1415); every
2317
+ // other mount, and an anonymous caller, mints through
2318
+ // `mintBootstrap` alone.
2319
+ ...(mintReadViewRoot ? { mintViewRoot: mintReadViewRoot } : {}),
2228
2320
  },
2229
2321
  }
2230
2322
  : {}),
@@ -2294,9 +2386,10 @@ export function createGguiServer(opts = {}) {
2294
2386
  // R6 /state snapshot + R7 /events cursor-replay reads — see
2295
2387
  // `./api-renders-routes.ts` for the wsToken auth posture, binding +
2296
2388
  // app-scope gates, and response taxonomy. Mounted only when MCP Apps is on,
2297
- // a render store is resolved, and a token secret exists (the same
2298
- // preconditions the credential minter needs).
2299
- if (mcpAppsEnabled && renderStore && sharedTokenSecret !== undefined) {
2389
+ // a render store is resolved, and a token secret exists. MCP Apps being on
2390
+ // is also what assigns the two minters, so the last conjunct only narrows
2391
+ // the type: it never keeps a deployment from mounting.
2392
+ if (mcpAppsEnabled && renderStore && sharedTokenSecret !== undefined && mintChainedBootstrap) {
2300
2393
  mountApiRendersRoutes({
2301
2394
  app,
2302
2395
  renderStore,
@@ -2314,7 +2407,7 @@ export function createGguiServer(opts = {}) {
2314
2407
  // composes (ggui#522) — session-API URLs keep the public origin.
2315
2408
  ...(opts.codeBaseUrl !== undefined ? { codeBaseUrl: opts.codeBaseUrl } : {}),
2316
2409
  ...(mintCodeModuleUrl !== undefined ? { mintCodeModuleUrl } : {}),
2317
- ...(mintBootstrap ? { mintBootstrap } : {}),
2410
+ mintChainedBootstrap,
2318
2411
  resolveRuntimeUrl: resolveRuntimeUrlForResultMeta,
2319
2412
  logger,
2320
2413
  });
@@ -3031,6 +3124,14 @@ export function createGguiServer(opts = {}) {
3031
3124
  // above — subscribes that omit `payload.appId` resolve their
3032
3125
  // identity-default through the identical rule (SPEC §12.2).
3033
3126
  appIdFromIdentity,
3127
+ // ggui#1480 — a subscribe that declares an app other than its
3128
+ // identity's own passes the same per-app authorization the
3129
+ // `/mcp` endpoint runs on a URL-addressed app.
3130
+ ...(opts.perAppRouting?.authorize ? { authorizeApp: opts.perAppRouting.authorize } : {}),
3131
+ // ggui#1482 — a listed source is authorized on every subscribe.
3132
+ ...(opts.perAppRouting?.perAppOnlySources
3133
+ ? { perAppOnlySources: opts.perAppRouting.perAppOnlySources }
3134
+ : {}),
3034
3135
  logger: logger.child({ component: "render-channel" }),
3035
3136
  path: typeof opts.renderChannel === "object" ? opts.renderChannel.path : undefined,
3036
3137
  streamBuffer: streamBuffer ??
@@ -3157,7 +3258,6 @@ export function createGguiServer(opts = {}) {
3157
3258
  adminToken: resolvedAdminToken,
3158
3259
  generators: generators ?? null,
3159
3260
  blueprintStore,
3160
- blueprintSelector,
3161
3261
  blueprintSearch,
3162
3262
  async listen(port = 0, host = opts.host ?? "127.0.0.1") {
3163
3263
  // The Origin/Host validation policy (ggui#438a) was built above
@@ -3241,14 +3341,28 @@ export function createGguiServer(opts = {}) {
3241
3341
  }
3242
3342
  });
3243
3343
  },
3244
- async close() {
3344
+ async close(options = {}) {
3245
3345
  if (channel)
3246
3346
  await channel.close();
3247
3347
  const server = httpServer;
3248
3348
  if (!server)
3249
3349
  return;
3350
+ const graceMs = options.graceMs ?? DEFAULT_CLOSE_GRACE_MS;
3250
3351
  await new Promise((resolve, reject) => {
3251
- server.close((err) => (err ? reject(err) : resolve()));
3352
+ // ggui#1631: after the grace, end what is still open. The timer is
3353
+ // unref'd so it never keeps the process alive on its own.
3354
+ const deadline = Number.isFinite(graceMs)
3355
+ ? setTimeout(() => server.closeAllConnections(), Math.max(0, graceMs))
3356
+ : undefined;
3357
+ deadline?.unref();
3358
+ server.close((err) => {
3359
+ if (deadline !== undefined)
3360
+ clearTimeout(deadline);
3361
+ if (err)
3362
+ reject(err);
3363
+ else
3364
+ resolve();
3365
+ });
3252
3366
  });
3253
3367
  httpServer = null;
3254
3368
  },
package/dist/storage.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GguiSessionStore, ThreadStore, VectorStore } from '@ggui-ai/mcp-server-core';
1
+ import type { GguiSessionStore, GguiSessionStreamBuffer, 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
  /**
@@ -49,6 +49,14 @@ export interface ResolvedStorageStores {
49
49
  * `/ggui/health` advertisement matches the active store.
50
50
  */
51
51
  readonly threadDurability?: 'durable' | 'ephemeral';
52
+ /**
53
+ * The live channel's stream buffer, present iff `renders` declares
54
+ * sqlite: a `SqliteGguiSessionStreamBuffer` on the same database file,
55
+ * so a session that survives a restart also keeps its stream `seq`, its
56
+ * epoch and its retained envelopes (ggui#1534). Absent otherwise, and
57
+ * `createGguiServer` keeps its in-memory buffer.
58
+ */
59
+ readonly streamBuffer?: GguiSessionStreamBuffer;
52
60
  }
53
61
  /**
54
62
  * Instantiate the concrete storage adapters declared in a parsed
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAqDA,OAAO,KAAK,EACV,gBAAgB,EAChB,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,gBAAgB,CAAC;IACxC;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,gBAAgB,EAChB,uBAAuB,EACvB,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,gBAAgB,CAAC;IACxC;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;IACpD;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,aAAa,GAAG,SAAS,EACjC,IAAI,GAAE,+BAAoC,GACzC,OAAO,CAAC,qBAAqB,CAAC,CAkDhC"}
package/dist/storage.js CHANGED
@@ -41,9 +41,9 @@
41
41
  * who want storage from config write:
42
42
  *
43
43
  * ```ts
44
- * const { renderStore, vectors } =
44
+ * const { renderStore, vectors, streamBuffer } =
45
45
  * await resolveStorageFromConfig(manifest.storage, { baseDir: projectRoot });
46
- * const server = createGguiServer({ renderStore, vectors });
46
+ * const server = createGguiServer({ renderStore, vectors, streamBuffer });
47
47
  * ```
48
48
  *
49
49
  * Explicit instances passed to `createGguiServer` still win — this
@@ -113,11 +113,12 @@ export async function resolveStorageFromConfig(config, opts = {}) {
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 { SqliteGguiSessionStore, SqliteVectorStore, SqliteThreadStore } = await import('@ggui-ai/mcp-server-core/sqlite');
116
+ const { SqliteGguiSessionStore, SqliteGguiSessionStreamBuffer, SqliteVectorStore, SqliteThreadStore } = await import('@ggui-ai/mcp-server-core/sqlite');
117
117
  if (config.renders && config.renders.driver === 'sqlite') {
118
118
  const filename = resolveStoragePath(config.renders.path, opts.baseDir);
119
119
  ensureParentDir(filename);
120
120
  result.renderStore = new SqliteGguiSessionStore({ filename });
121
+ result.streamBuffer = new SqliteGguiSessionStreamBuffer({ filename });
121
122
  }
122
123
  if (config.vectors && config.vectors.driver === 'sqlite') {
123
124
  const filename = resolveStoragePath(config.vectors.path, opts.baseDir);