@superblocksteam/gateway 2.0.155-next.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.
- package/LICENSE.txt +87 -0
- package/README.md +170 -0
- package/dist/agents/resolve-orchestrator-url.d.ts +22 -0
- package/dist/agents/resolve-orchestrator-url.js +38 -0
- package/dist/agents/resolve-orchestrator-url.js.map +1 -0
- package/dist/capabilities/import-prompt.d.ts +11 -0
- package/dist/capabilities/import-prompt.js +96 -0
- package/dist/capabilities/import-prompt.js.map +1 -0
- package/dist/capabilities/lifecycle.d.ts +73 -0
- package/dist/capabilities/lifecycle.js +2531 -0
- package/dist/capabilities/lifecycle.js.map +1 -0
- package/dist/capabilities/persisted-progress.d.ts +46 -0
- package/dist/capabilities/persisted-progress.js +246 -0
- package/dist/capabilities/persisted-progress.js.map +1 -0
- package/dist/capabilities/types.d.ts +446 -0
- package/dist/capabilities/types.js +47 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capture/browser-contract.d.ts +36 -0
- package/dist/capture/browser-contract.js +24 -0
- package/dist/capture/browser-contract.js.map +1 -0
- package/dist/capture/browser-instructions.d.ts +9 -0
- package/dist/capture/browser-instructions.js +17 -0
- package/dist/capture/browser-instructions.js.map +1 -0
- package/dist/capture/capture-library-probe.d.ts +55 -0
- package/dist/capture/capture-library-probe.js +177 -0
- package/dist/capture/capture-library-probe.js.map +1 -0
- package/dist/capture/capture-library.d.ts +52 -0
- package/dist/capture/capture-library.js +249 -0
- package/dist/capture/capture-library.js.map +1 -0
- package/dist/capture/classify-library-paint.d.ts +21 -0
- package/dist/capture/classify-library-paint.js +13 -0
- package/dist/capture/classify-library-paint.js.map +1 -0
- package/dist/capture/host-mcp-scan.d.ts +16 -0
- package/dist/capture/host-mcp-scan.js +64 -0
- package/dist/capture/host-mcp-scan.js.map +1 -0
- package/dist/capture/library-parent-script.d.ts +34 -0
- package/dist/capture/library-parent-script.js +82 -0
- package/dist/capture/library-parent-script.js.map +1 -0
- package/dist/capture/mode.d.ts +22 -0
- package/dist/capture/mode.js +45 -0
- package/dist/capture/mode.js.map +1 -0
- package/dist/config.d.ts +141 -0
- package/dist/config.js +109 -0
- package/dist/config.js.map +1 -0
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +9 -0
- package/dist/debug.js.map +1 -0
- package/dist/deps.d.ts +36 -0
- package/dist/deps.js +2 -0
- package/dist/deps.js.map +1 -0
- package/dist/events/cursor.d.ts +43 -0
- package/dist/events/cursor.js +78 -0
- package/dist/events/cursor.js.map +1 -0
- package/dist/events/memory-event-store.d.ts +34 -0
- package/dist/events/memory-event-store.js +110 -0
- package/dist/events/memory-event-store.js.map +1 -0
- package/dist/events/merge.d.ts +23 -0
- package/dist/events/merge.js +97 -0
- package/dist/events/merge.js.map +1 -0
- package/dist/events/normalized-collector.d.ts +62 -0
- package/dist/events/normalized-collector.js +156 -0
- package/dist/events/normalized-collector.js.map +1 -0
- package/dist/events/schema.d.ts +9 -0
- package/dist/events/schema.js +93 -0
- package/dist/events/schema.js.map +1 -0
- package/dist/events/snapshot.d.ts +32 -0
- package/dist/events/snapshot.js +57 -0
- package/dist/events/snapshot.js.map +1 -0
- package/dist/events/stream-key.d.ts +2 -0
- package/dist/events/stream-key.js +31 -0
- package/dist/events/stream-key.js.map +1 -0
- package/dist/events/types.d.ts +179 -0
- package/dist/events/types.js +66 -0
- package/dist/events/types.js.map +1 -0
- package/dist/health/readiness.d.ts +99 -0
- package/dist/health/readiness.js +202 -0
- package/dist/health/readiness.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/filters.d.ts +8 -0
- package/dist/integrations/filters.js +40 -0
- package/dist/integrations/filters.js.map +1 -0
- package/dist/integrations/map.d.ts +26 -0
- package/dist/integrations/map.js +43 -0
- package/dist/integrations/map.js.map +1 -0
- package/dist/integrations/profile-key.d.ts +19 -0
- package/dist/integrations/profile-key.js +35 -0
- package/dist/integrations/profile-key.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +5 -0
- package/dist/main.js.map +1 -0
- package/dist/orchestrator/client.d.ts +62 -0
- package/dist/orchestrator/client.js +160 -0
- package/dist/orchestrator/client.js.map +1 -0
- package/dist/playwright/ensure-chromium.d.ts +26 -0
- package/dist/playwright/ensure-chromium.js +60 -0
- package/dist/playwright/ensure-chromium.js.map +1 -0
- package/dist/preview/capture-screenshot.d.ts +52 -0
- package/dist/preview/capture-screenshot.js +158 -0
- package/dist/preview/capture-screenshot.js.map +1 -0
- package/dist/preview/viewer-url.d.ts +82 -0
- package/dist/preview/viewer-url.js +128 -0
- package/dist/preview/viewer-url.js.map +1 -0
- package/dist/process/fault-barrier.d.ts +55 -0
- package/dist/process/fault-barrier.js +113 -0
- package/dist/process/fault-barrier.js.map +1 -0
- package/dist/resume/memory-progress-store.d.ts +39 -0
- package/dist/resume/memory-progress-store.js +82 -0
- package/dist/resume/memory-progress-store.js.map +1 -0
- package/dist/resume/memory-recent-app-store.d.ts +14 -0
- package/dist/resume/memory-recent-app-store.js +27 -0
- package/dist/resume/memory-recent-app-store.js.map +1 -0
- package/dist/resume/memory-turn-store.d.ts +18 -0
- package/dist/resume/memory-turn-store.js +73 -0
- package/dist/resume/memory-turn-store.js.map +1 -0
- package/dist/resume/progress-key.d.ts +21 -0
- package/dist/resume/progress-key.js +58 -0
- package/dist/resume/progress-key.js.map +1 -0
- package/dist/resume/stores.d.ts +14 -0
- package/dist/resume/stores.js +18 -0
- package/dist/resume/stores.js.map +1 -0
- package/dist/resume/types.d.ts +124 -0
- package/dist/resume/types.js +13 -0
- package/dist/resume/types.js.map +1 -0
- package/dist/sabs/agent-facing-text.d.ts +33 -0
- package/dist/sabs/agent-facing-text.js +125 -0
- package/dist/sabs/agent-facing-text.js.map +1 -0
- package/dist/sabs/browser-context.d.ts +53 -0
- package/dist/sabs/browser-context.js +91 -0
- package/dist/sabs/browser-context.js.map +1 -0
- package/dist/sabs/editor-client-methods.d.ts +147 -0
- package/dist/sabs/editor-client-methods.js +715 -0
- package/dist/sabs/editor-client-methods.js.map +1 -0
- package/dist/sabs/recent-tool-runs.d.ts +14 -0
- package/dist/sabs/recent-tool-runs.js +61 -0
- package/dist/sabs/recent-tool-runs.js.map +1 -0
- package/dist/sabs/session-peer.d.ts +351 -0
- package/dist/sabs/session-peer.js +2 -0
- package/dist/sabs/session-peer.js.map +1 -0
- package/dist/sabs/turn-collector.d.ts +152 -0
- package/dist/sabs/turn-collector.js +573 -0
- package/dist/sabs/turn-collector.js.map +1 -0
- package/dist/sabs/websocket-session-peer.d.ts +514 -0
- package/dist/sabs/websocket-session-peer.js +2487 -0
- package/dist/sabs/websocket-session-peer.js.map +1 -0
- package/dist/security/constant-time.d.ts +9 -0
- package/dist/security/constant-time.js +13 -0
- package/dist/security/constant-time.js.map +1 -0
- package/dist/server/client.d.ts +371 -0
- package/dist/server/client.js +572 -0
- package/dist/server/client.js.map +1 -0
- package/dist/server/identity-reads.d.ts +76 -0
- package/dist/server/identity-reads.js +209 -0
- package/dist/server/identity-reads.js.map +1 -0
- package/dist/start.d.ts +14 -0
- package/dist/start.js +75 -0
- package/dist/start.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +32 -0
- package/dist/telemetry/metrics.js +57 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/transports/mcp/admin-tools.d.ts +47 -0
- package/dist/transports/mcp/admin-tools.js +164 -0
- package/dist/transports/mcp/admin-tools.js.map +1 -0
- package/dist/transports/mcp/app-status-html.d.ts +15 -0
- package/dist/transports/mcp/app-status-html.js +563 -0
- package/dist/transports/mcp/app-status-html.js.map +1 -0
- package/dist/transports/mcp/decision-elicitation.d.ts +74 -0
- package/dist/transports/mcp/decision-elicitation.js +233 -0
- package/dist/transports/mcp/decision-elicitation.js.map +1 -0
- package/dist/transports/mcp/dispatch-probe.d.ts +24 -0
- package/dist/transports/mcp/dispatch-probe.js +48 -0
- package/dist/transports/mcp/dispatch-probe.js.map +1 -0
- package/dist/transports/mcp/format-tool-content.d.ts +25 -0
- package/dist/transports/mcp/format-tool-content.js +48 -0
- package/dist/transports/mcp/format-tool-content.js.map +1 -0
- package/dist/transports/mcp/mount.d.ts +9 -0
- package/dist/transports/mcp/mount.js +469 -0
- package/dist/transports/mcp/mount.js.map +1 -0
- package/dist/transports/mcp/progress-notifier.d.ts +39 -0
- package/dist/transports/mcp/progress-notifier.js +52 -0
- package/dist/transports/mcp/progress-notifier.js.map +1 -0
- package/package.json +72 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless capture of a preview viewer URL for MCP image content.
|
|
3
|
+
*
|
|
4
|
+
* Best-effort: callers must succeed with URLs alone when this returns
|
|
5
|
+
* undefined. Output is capped for Claude Desktop tool-result size limits.
|
|
6
|
+
*/
|
|
7
|
+
import { isLoopbackUrl } from "./viewer-url.js";
|
|
8
|
+
/** Soft cap on decoded bytes before base64 (~256 KiB). */
|
|
9
|
+
export const PREVIEW_SCREENSHOT_MAX_BYTES = 256 * 1024;
|
|
10
|
+
/** How long to keep waiting for the shell to subscribe before handshaking anyway. */
|
|
11
|
+
const HANDSHAKE_FALLBACK_MS = 2_000;
|
|
12
|
+
/**
|
|
13
|
+
* Settle time after the app's first element becomes visible, covering the
|
|
14
|
+
* layout and data-bound widgets that land just behind it. The app publishes
|
|
15
|
+
* no "finished rendering" signal an embedder could await.
|
|
16
|
+
*/
|
|
17
|
+
const APP_PAINT_SETTLE_MS = 2_500;
|
|
18
|
+
/**
|
|
19
|
+
* Page source that hands the deployed shell its bearer, and the whole reason a
|
|
20
|
+
* headless capture can paint a real app.
|
|
21
|
+
*
|
|
22
|
+
* `ui-deployed-shell` only accepts a bearer through an `INITIALIZE_EMBED`
|
|
23
|
+
* message from its embedder, and ~300ms after mount it gives up and tries
|
|
24
|
+
* Auth0 silent login instead — which has no session to find here. At top level
|
|
25
|
+
* the shell's embedder port has no parent window to check messages against, so
|
|
26
|
+
* it accepts one posted by the page itself.
|
|
27
|
+
*
|
|
28
|
+
* Delivery happens exactly once: each `INITIALIZE_EMBED` replaces the shell's
|
|
29
|
+
* auth state and restarts its bootstrap, so a repeating handshake would keep
|
|
30
|
+
* the app from ever settling.
|
|
31
|
+
*
|
|
32
|
+
* This is source text rather than a function handed to `addInitScript` because
|
|
33
|
+
* Playwright ships a function by serializing it, which drags in whatever
|
|
34
|
+
* helpers the bundler injected into its body (`__name` under tsx) and throws a
|
|
35
|
+
* ReferenceError in the page.
|
|
36
|
+
*
|
|
37
|
+
* The result is only ever evaluated as script source (Playwright forwards it
|
|
38
|
+
* to CDP `addScriptToEvaluateOnNewDocument`), never interpolated into HTML, so
|
|
39
|
+
* `JSON.stringify` is sufficient to contain the token. Putting it in a
|
|
40
|
+
* `<script>` element would not be — the token would need `</script>` escaped.
|
|
41
|
+
*/
|
|
42
|
+
export function buildEmbedHandshakeScript(token) {
|
|
43
|
+
return `(function () {
|
|
44
|
+
// The app iframe gets its own credentials from the shell; a handshake there
|
|
45
|
+
// would be rejected as not-from-parent anyway.
|
|
46
|
+
if (window.parent !== window) return;
|
|
47
|
+
var token = ${JSON.stringify(token)};
|
|
48
|
+
var sent = false;
|
|
49
|
+
function send() {
|
|
50
|
+
if (sent) return;
|
|
51
|
+
sent = true;
|
|
52
|
+
// Addressed to this window only. "*" would expose the bearer to every
|
|
53
|
+
// listener on the page, including third-party scripts the shell loads.
|
|
54
|
+
window.postMessage(
|
|
55
|
+
{ data: { token: token }, type: "INITIALIZE_EMBED" },
|
|
56
|
+
window.location.origin,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
// Handshake the moment the shell subscribes, so its very first render
|
|
60
|
+
// already has the bearer and no bootstrap is wasted on the Auth0 path.
|
|
61
|
+
var addEventListener = window.addEventListener;
|
|
62
|
+
window.addEventListener = function (type, listener, options) {
|
|
63
|
+
addEventListener.call(window, type, listener, options);
|
|
64
|
+
if (type === "message") send();
|
|
65
|
+
};
|
|
66
|
+
// Safety net for a shell that subscribes some other way: a late handshake
|
|
67
|
+
// still recovers, at the cost of one discarded bootstrap.
|
|
68
|
+
window.setTimeout(send, ${HANDSHAKE_FALLBACK_MS});
|
|
69
|
+
})();`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Captures a JPEG screenshot via Playwright when available. Soft-fails when
|
|
73
|
+
* Chromium is missing or the page never becomes ready — callers must still
|
|
74
|
+
* return preview URLs when this returns undefined.
|
|
75
|
+
*/
|
|
76
|
+
export const capturePreviewScreenshotWithPlaywright = async (input) => {
|
|
77
|
+
let chromium;
|
|
78
|
+
try {
|
|
79
|
+
// Optional runtime dep — demos without browsers still return preview URLs.
|
|
80
|
+
const mod = (await import(/* webpackIgnore: true */ "playwright"));
|
|
81
|
+
chromium = mod.chromium;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
console.warn("gateway preview screenshot: playwright unavailable", {
|
|
85
|
+
message: error instanceof Error ? error.message : String(error),
|
|
86
|
+
});
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const timeoutMs = input.timeoutMs ?? 45_000;
|
|
90
|
+
let browser;
|
|
91
|
+
try {
|
|
92
|
+
// launch() must stay inside this try: a missing Chromium binary throws
|
|
93
|
+
// here, and that used to fail the whole get_app / preview_app capability.
|
|
94
|
+
browser = await chromium.launch({ headless: true });
|
|
95
|
+
const page = await browser.newPage();
|
|
96
|
+
if (input.embedToken) {
|
|
97
|
+
// The bearer would reach the app iframe on hosts where the shell hands
|
|
98
|
+
// the runtime token straight through, so it stays on the local stack
|
|
99
|
+
// this capture path exists for.
|
|
100
|
+
if (isLoopbackUrl(input.url)) {
|
|
101
|
+
await page.addInitScript({
|
|
102
|
+
content: buildEmbedHandshakeScript(input.embedToken),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
console.warn("gateway preview screenshot: shell is not local, capturing unauthenticated", { url: input.url });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await page.setViewportSize({ width: 1280, height: 800 });
|
|
110
|
+
// Not "networkidle": the shell holds a dev-server HMR socket open and
|
|
111
|
+
// the app streams telemetry, so idle never arrives and goto() would
|
|
112
|
+
// spend the entire timeout before throwing on an app that had painted.
|
|
113
|
+
await page.goto(input.url, {
|
|
114
|
+
timeout: timeoutMs,
|
|
115
|
+
waitUntil: "domcontentloaded",
|
|
116
|
+
});
|
|
117
|
+
// Wait on content *inside* the app frame, not the frame element: the
|
|
118
|
+
// shell attaches an about:blank iframe within ~50ms and only points it
|
|
119
|
+
// at the app once auth, metadata, and the CDN cookie prime land, so the
|
|
120
|
+
// element alone photographs a loading spinner.
|
|
121
|
+
await page
|
|
122
|
+
.frameLocator("iframe")
|
|
123
|
+
.locator("body > *")
|
|
124
|
+
.first()
|
|
125
|
+
.waitFor({ state: "visible", timeout: timeoutMs })
|
|
126
|
+
// A frame that never paints still deserves a screenshot: the error
|
|
127
|
+
// page it captures is what tells the caller what went wrong.
|
|
128
|
+
.catch(() => undefined);
|
|
129
|
+
await page.waitForTimeout(APP_PAINT_SETTLE_MS);
|
|
130
|
+
let quality = 80;
|
|
131
|
+
let buffer = await page.screenshot({ type: "jpeg", quality });
|
|
132
|
+
while (buffer.byteLength > PREVIEW_SCREENSHOT_MAX_BYTES && quality > 30) {
|
|
133
|
+
quality -= 10;
|
|
134
|
+
buffer = await page.screenshot({ type: "jpeg", quality });
|
|
135
|
+
}
|
|
136
|
+
if (buffer.byteLength > PREVIEW_SCREENSHOT_MAX_BYTES) {
|
|
137
|
+
console.warn("gateway preview screenshot exceeds size cap; skipping", {
|
|
138
|
+
bytes: buffer.byteLength,
|
|
139
|
+
});
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
dataBase64: buffer.toString("base64"),
|
|
144
|
+
mimeType: "image/jpeg",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
console.warn("gateway preview screenshot failed", {
|
|
149
|
+
message: error instanceof Error ? error.message : String(error),
|
|
150
|
+
url: input.url,
|
|
151
|
+
});
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await browser?.close().catch(() => undefined);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=capture-screenshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture-screenshot.js","sourceRoot":"","sources":["../../src/preview/capture-screenshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the URL that loads a code-mode preview without a Superblocks UI login.
|
|
3
|
+
*
|
|
4
|
+
* Deployed-shell contract: the CDN/shell accepts `commitId` + `jwt` as query
|
|
5
|
+
* params on the first request so it can set a `viewer-jwt` cookie (see
|
|
6
|
+
* csb-mock `deployed-cdn` and production shell). That first-hop query JWT is
|
|
7
|
+
* unavoidable for the shell itself today; scoped `apps:preview` tokens are
|
|
8
|
+
* issued by the Server with the platform default TTL (typically 24h).
|
|
9
|
+
*
|
|
10
|
+
* Gateway-facing openLink URLs put the JWT in the URL fragment instead, so
|
|
11
|
+
* tunnel/proxy access logs and Referer headers do not record the bearer. An
|
|
12
|
+
* interstitial page reads the fragment and redirects to the shell query form.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isHttpsUrl(url: string): boolean;
|
|
15
|
+
export declare function isLoopbackUrl(url: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The embed shell route for a preview commit.
|
|
18
|
+
*
|
|
19
|
+
* Unlike {@link buildDeployedShellViewerUrl}, which hands the browser the app
|
|
20
|
+
* bundle straight off the CDN, this loads `ui-deployed-shell`: the piece that
|
|
21
|
+
* resolves auth, fetches app metadata, primes the CDN cookie, and feeds the
|
|
22
|
+
* app iframe. A bare bundle has no way to authenticate its APIs, so only this
|
|
23
|
+
* URL can paint a working app. It carries no token — the shell takes its
|
|
24
|
+
* bearer from an `INITIALIZE_EMBED` message — and the commit comes from the
|
|
25
|
+
* application's current version.
|
|
26
|
+
*
|
|
27
|
+
* `deployedShellBaseUrl` is forwarded as `deployedUrl` because the shell reads
|
|
28
|
+
* its CDN origin from `/env.js`, which the local stack does not publish; it
|
|
29
|
+
* then falls back to a hardcoded `localhost:4041` that only matches slot 0.
|
|
30
|
+
* On every other slot the cookie prime is refused and the shell renders its
|
|
31
|
+
* error page instead of the app.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildEmbedShellPreviewUrl(input: {
|
|
34
|
+
applicationId: string;
|
|
35
|
+
deployedShellBaseUrl?: string;
|
|
36
|
+
uiBaseUrl: string;
|
|
37
|
+
}): string;
|
|
38
|
+
/**
|
|
39
|
+
* Replaces the origin of `url` with `publicBaseUrl`'s origin, keeping path and
|
|
40
|
+
* query. Used so Claude Desktop can `openLink` an https URL while the actual
|
|
41
|
+
* app still lives on a slot-local host.
|
|
42
|
+
*/
|
|
43
|
+
export declare function rewriteUrlOrigin(url: string, publicBaseUrl: string | undefined): string;
|
|
44
|
+
/**
|
|
45
|
+
* Deployed-shell URL for a preview commit, with the scoped JWT in the query.
|
|
46
|
+
*
|
|
47
|
+
* `deployedShellBaseUrl` may include `__SB_APP_ID` (production CDN pattern) or
|
|
48
|
+
* be a plain origin (local csb-mock CDN on :4041).
|
|
49
|
+
*/
|
|
50
|
+
export declare function buildDeployedShellViewerUrl(input: {
|
|
51
|
+
applicationId: string;
|
|
52
|
+
commitId: string;
|
|
53
|
+
deployedShellBaseUrl: string;
|
|
54
|
+
jwt: string;
|
|
55
|
+
}): string;
|
|
56
|
+
export type OpenablePreviewUrls = {
|
|
57
|
+
/**
|
|
58
|
+
* True when `previewUrl` is https and Claude Desktop can call `openLink`.
|
|
59
|
+
*/
|
|
60
|
+
openableViaHttps: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* URL returned to MCP clients / Open preview button.
|
|
63
|
+
*/
|
|
64
|
+
previewUrl: string;
|
|
65
|
+
/**
|
|
66
|
+
* Direct deployed-shell (or UI) URL used for headless screenshot capture.
|
|
67
|
+
* May be http localhost even when `previewUrl` is an https gateway redirect.
|
|
68
|
+
*/
|
|
69
|
+
viewerUrl: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Picks the best preview URL for Claude: prefer an https front door, fall back
|
|
73
|
+
* to the local viewer / UI preview URL for copy-paste + screenshot.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildOpenablePreviewUrls(input: {
|
|
76
|
+
applicationId: string;
|
|
77
|
+
commitId: string;
|
|
78
|
+
deployedShellBaseUrl: string | undefined;
|
|
79
|
+
jwt: string | undefined;
|
|
80
|
+
publicViewerBaseUrl: string | undefined;
|
|
81
|
+
uiPreviewUrl: string;
|
|
82
|
+
}): OpenablePreviewUrls;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds the URL that loads a code-mode preview without a Superblocks UI login.
|
|
3
|
+
*
|
|
4
|
+
* Deployed-shell contract: the CDN/shell accepts `commitId` + `jwt` as query
|
|
5
|
+
* params on the first request so it can set a `viewer-jwt` cookie (see
|
|
6
|
+
* csb-mock `deployed-cdn` and production shell). That first-hop query JWT is
|
|
7
|
+
* unavoidable for the shell itself today; scoped `apps:preview` tokens are
|
|
8
|
+
* issued by the Server with the platform default TTL (typically 24h).
|
|
9
|
+
*
|
|
10
|
+
* Gateway-facing openLink URLs put the JWT in the URL fragment instead, so
|
|
11
|
+
* tunnel/proxy access logs and Referer headers do not record the bearer. An
|
|
12
|
+
* interstitial page reads the fragment and redirects to the shell query form.
|
|
13
|
+
*/
|
|
14
|
+
export function isHttpsUrl(url) {
|
|
15
|
+
try {
|
|
16
|
+
return new URL(url).protocol === "https:";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
23
|
+
export function isLoopbackUrl(url) {
|
|
24
|
+
try {
|
|
25
|
+
return LOOPBACK_HOSTNAMES.has(new URL(url).hostname);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The embed shell route for a preview commit.
|
|
33
|
+
*
|
|
34
|
+
* Unlike {@link buildDeployedShellViewerUrl}, which hands the browser the app
|
|
35
|
+
* bundle straight off the CDN, this loads `ui-deployed-shell`: the piece that
|
|
36
|
+
* resolves auth, fetches app metadata, primes the CDN cookie, and feeds the
|
|
37
|
+
* app iframe. A bare bundle has no way to authenticate its APIs, so only this
|
|
38
|
+
* URL can paint a working app. It carries no token — the shell takes its
|
|
39
|
+
* bearer from an `INITIALIZE_EMBED` message — and the commit comes from the
|
|
40
|
+
* application's current version.
|
|
41
|
+
*
|
|
42
|
+
* `deployedShellBaseUrl` is forwarded as `deployedUrl` because the shell reads
|
|
43
|
+
* its CDN origin from `/env.js`, which the local stack does not publish; it
|
|
44
|
+
* then falls back to a hardcoded `localhost:4041` that only matches slot 0.
|
|
45
|
+
* On every other slot the cookie prime is refused and the shell renders its
|
|
46
|
+
* error page instead of the app.
|
|
47
|
+
*/
|
|
48
|
+
export function buildEmbedShellPreviewUrl(input) {
|
|
49
|
+
const base = input.uiBaseUrl.trim().replace(/\/$/, "");
|
|
50
|
+
const url = `${base}/code-mode/embed/applications/preview/${encodeURIComponent(input.applicationId)}`;
|
|
51
|
+
const cdnBase = input.deployedShellBaseUrl?.trim();
|
|
52
|
+
if (!cdnBase) {
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
// Passed verbatim, `__SB_APP_ID` and all: this is the same template shape
|
|
56
|
+
// the shell expects from env.js, and it expands the placeholder itself.
|
|
57
|
+
return `${url}?deployedUrl=${encodeURIComponent(cdnBase)}`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Replaces the origin of `url` with `publicBaseUrl`'s origin, keeping path and
|
|
61
|
+
* query. Used so Claude Desktop can `openLink` an https URL while the actual
|
|
62
|
+
* app still lives on a slot-local host.
|
|
63
|
+
*/
|
|
64
|
+
export function rewriteUrlOrigin(url, publicBaseUrl) {
|
|
65
|
+
if (!publicBaseUrl?.trim()) {
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const original = new URL(url);
|
|
70
|
+
const publicBase = new URL(publicBaseUrl);
|
|
71
|
+
// Build from the public origin so a slot-local port (e.g. :4141) is not
|
|
72
|
+
// carried onto an https tunnel that listens on 443.
|
|
73
|
+
return new URL(`${original.pathname}${original.search}${original.hash}`, publicBase).toString();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return url;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Deployed-shell URL for a preview commit, with the scoped JWT in the query.
|
|
81
|
+
*
|
|
82
|
+
* `deployedShellBaseUrl` may include `__SB_APP_ID` (production CDN pattern) or
|
|
83
|
+
* be a plain origin (local csb-mock CDN on :4041).
|
|
84
|
+
*/
|
|
85
|
+
export function buildDeployedShellViewerUrl(input) {
|
|
86
|
+
const base = input.deployedShellBaseUrl
|
|
87
|
+
.trim()
|
|
88
|
+
.replace(/\/$/, "")
|
|
89
|
+
.replaceAll("__SB_APP_ID", input.applicationId);
|
|
90
|
+
const url = new URL(base.includes("://") ? base : `http://${base}`);
|
|
91
|
+
// Ensure we hit the shell root when the base is only an origin.
|
|
92
|
+
if (!url.pathname || url.pathname === "") {
|
|
93
|
+
url.pathname = "/";
|
|
94
|
+
}
|
|
95
|
+
url.searchParams.set("commitId", input.commitId);
|
|
96
|
+
url.searchParams.set("jwt", input.jwt);
|
|
97
|
+
return url.toString();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Picks the best preview URL for Claude: prefer an https front door, fall back
|
|
101
|
+
* to the local viewer / UI preview URL for copy-paste + screenshot.
|
|
102
|
+
*/
|
|
103
|
+
export function buildOpenablePreviewUrls(input) {
|
|
104
|
+
const viewerUrl = input.deployedShellBaseUrl && input.jwt
|
|
105
|
+
? buildDeployedShellViewerUrl({
|
|
106
|
+
applicationId: input.applicationId,
|
|
107
|
+
commitId: input.commitId,
|
|
108
|
+
deployedShellBaseUrl: input.deployedShellBaseUrl,
|
|
109
|
+
jwt: input.jwt,
|
|
110
|
+
})
|
|
111
|
+
: input.uiPreviewUrl;
|
|
112
|
+
if (input.publicViewerBaseUrl && isHttpsUrl(input.publicViewerBaseUrl)) {
|
|
113
|
+
const previewUrl = rewriteUrlOrigin(viewerUrl, input.publicViewerBaseUrl);
|
|
114
|
+
return {
|
|
115
|
+
openableViaHttps: isHttpsUrl(previewUrl),
|
|
116
|
+
previewUrl,
|
|
117
|
+
viewerUrl,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// Stdio Gateway does not bind HTTP, so never advertise a gateway /viewer
|
|
121
|
+
// path as openable. Preview URLs are the editor or the deployed shell.
|
|
122
|
+
return {
|
|
123
|
+
openableViaHttps: isHttpsUrl(viewerUrl),
|
|
124
|
+
previewUrl: viewerUrl,
|
|
125
|
+
viewerUrl,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=viewer-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"viewer-url.js","sourceRoot":"","sources":["../../src/preview/viewer-url.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit code the gateway uses for a fault it decided not to survive.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from 0 (an operator asked it to stop) and from 1 (it never got far
|
|
5
|
+
* enough to serve), so a supervisor can tell "restart this" from "stop trying".
|
|
6
|
+
* `superviseGateway` in the CLI reads it; the two MUST stay in agreement.
|
|
7
|
+
*/
|
|
8
|
+
export declare const GATEWAY_FAULT_EXIT_CODE = 97;
|
|
9
|
+
export type GatewayFaultKind = "uncaught_exception" | "unhandled_rejection";
|
|
10
|
+
export type GatewayFault = {
|
|
11
|
+
at: number;
|
|
12
|
+
/** Whether the gateway gave up its process over this. */
|
|
13
|
+
fatal: boolean;
|
|
14
|
+
kind: GatewayFaultKind;
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The slice of `process` this needs, so a test can emit a fault without arming
|
|
19
|
+
* the real handlers and taking the runner down with them.
|
|
20
|
+
*/
|
|
21
|
+
export type FaultBarrierListener = (...args: unknown[]) => void;
|
|
22
|
+
export type FaultBarrierHost = {
|
|
23
|
+
listenerCount: (event: string) => number;
|
|
24
|
+
off: (event: string, handler: FaultBarrierListener) => void;
|
|
25
|
+
on: (event: string, handler: FaultBarrierListener) => void;
|
|
26
|
+
};
|
|
27
|
+
export type FaultBarrierOptions = {
|
|
28
|
+
drainDeadlineMs?: number;
|
|
29
|
+
exit?: (code: number) => void;
|
|
30
|
+
log?: (message: string, fault: GatewayFault) => void;
|
|
31
|
+
/** Where the fault is recorded so `/health` can stop claiming to be well. */
|
|
32
|
+
onFault: (fault: GatewayFault) => void;
|
|
33
|
+
process?: FaultBarrierHost;
|
|
34
|
+
/** Best-effort release of listeners and sockets before a fatal exit. */
|
|
35
|
+
shutdown?: () => Promise<void>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Node's default for an unhandled rejection is to kill the process. That is
|
|
39
|
+
* wrong for this gateway: its fire-and-forget paths (a live-edit turn, a
|
|
40
|
+
* liveness beat, a progress notification) belong to one caller, and a
|
|
41
|
+
* dependency that stopped answering must cost that caller its call rather than
|
|
42
|
+
* cost every concurrent caller the process. See ENG-5809, where ~8 concurrent
|
|
43
|
+
* agents lost the gateway to rejections that trailed a 120s upstream timeout.
|
|
44
|
+
*
|
|
45
|
+
* An uncaught exception is treated differently on purpose. A rejection that
|
|
46
|
+
* escaped a promise is a call that failed; an exception nobody caught means an
|
|
47
|
+
* invariant broke somewhere the code did not expect to be, and the heap it left
|
|
48
|
+
* behind is not worth serving from. Those exit deterministically on
|
|
49
|
+
* {@link GATEWAY_FAULT_EXIT_CODE} so a supervisor restarts at once, which is
|
|
50
|
+
* the other half of ENG-5809: recovery took between 80 seconds and never.
|
|
51
|
+
*
|
|
52
|
+
* Returns a disposer that releases the handlers, for tests and for a second
|
|
53
|
+
* gateway in the same process.
|
|
54
|
+
*/
|
|
55
|
+
export declare function installFaultBarrier(options: FaultBarrierOptions): () => void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { recordGatewayFault } from "../telemetry/metrics.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exit code the gateway uses for a fault it decided not to survive.
|
|
4
|
+
*
|
|
5
|
+
* Distinct from 0 (an operator asked it to stop) and from 1 (it never got far
|
|
6
|
+
* enough to serve), so a supervisor can tell "restart this" from "stop trying".
|
|
7
|
+
* `superviseGateway` in the CLI reads it; the two MUST stay in agreement.
|
|
8
|
+
*/
|
|
9
|
+
export const GATEWAY_FAULT_EXIT_CODE = 97;
|
|
10
|
+
/** How long a fatal fault waits for in-flight work before the process goes. */
|
|
11
|
+
const DEFAULT_DRAIN_DEADLINE_MS = 2_000;
|
|
12
|
+
/** Hosts this module has already armed, so a second install is a no-op. */
|
|
13
|
+
const armed = new WeakSet();
|
|
14
|
+
function messageOf(error) {
|
|
15
|
+
if (error instanceof Error) {
|
|
16
|
+
return error.message;
|
|
17
|
+
}
|
|
18
|
+
return String(error);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Node's default for an unhandled rejection is to kill the process. That is
|
|
22
|
+
* wrong for this gateway: its fire-and-forget paths (a live-edit turn, a
|
|
23
|
+
* liveness beat, a progress notification) belong to one caller, and a
|
|
24
|
+
* dependency that stopped answering must cost that caller its call rather than
|
|
25
|
+
* cost every concurrent caller the process. See ENG-5809, where ~8 concurrent
|
|
26
|
+
* agents lost the gateway to rejections that trailed a 120s upstream timeout.
|
|
27
|
+
*
|
|
28
|
+
* An uncaught exception is treated differently on purpose. A rejection that
|
|
29
|
+
* escaped a promise is a call that failed; an exception nobody caught means an
|
|
30
|
+
* invariant broke somewhere the code did not expect to be, and the heap it left
|
|
31
|
+
* behind is not worth serving from. Those exit deterministically on
|
|
32
|
+
* {@link GATEWAY_FAULT_EXIT_CODE} so a supervisor restarts at once, which is
|
|
33
|
+
* the other half of ENG-5809: recovery took between 80 seconds and never.
|
|
34
|
+
*
|
|
35
|
+
* Returns a disposer that releases the handlers, for tests and for a second
|
|
36
|
+
* gateway in the same process.
|
|
37
|
+
*/
|
|
38
|
+
export function installFaultBarrier(options) {
|
|
39
|
+
const host = options.process ?? process;
|
|
40
|
+
const exit = options.exit ?? ((code) => process.exit(code));
|
|
41
|
+
const log = options.log ??
|
|
42
|
+
((message, fault) => {
|
|
43
|
+
console.error(message, fault);
|
|
44
|
+
});
|
|
45
|
+
// Installed twice, one gateway would report every fault twice and race two
|
|
46
|
+
// exits. `gateway serve` boots through `startStdioGateway`, so this is
|
|
47
|
+
// reachable rather than theoretical. The MCP host owns process lifecycle;
|
|
48
|
+
// this barrier records the fault and exits instead of wrapping a grandchild.
|
|
49
|
+
//
|
|
50
|
+
// Keyed on our own installs rather than on how many listeners the host has.
|
|
51
|
+
// Counting foreign listeners looks equivalent and is not: any library that
|
|
52
|
+
// registers one - a test runner, an instrumentation SDK - would silently
|
|
53
|
+
// suppress this barrier, and the gateway would go back to dying on a
|
|
54
|
+
// rejection with nothing recording why.
|
|
55
|
+
if (armed.has(host)) {
|
|
56
|
+
return () => undefined;
|
|
57
|
+
}
|
|
58
|
+
armed.add(host);
|
|
59
|
+
const record = (kind, error, fatal) => {
|
|
60
|
+
const fault = {
|
|
61
|
+
at: Date.now(),
|
|
62
|
+
fatal,
|
|
63
|
+
kind,
|
|
64
|
+
message: messageOf(error),
|
|
65
|
+
};
|
|
66
|
+
recordGatewayFault(fault);
|
|
67
|
+
log(fatal
|
|
68
|
+
? "gateway fatal fault; exiting for a supervised restart"
|
|
69
|
+
: "gateway survived an unhandled rejection", fault);
|
|
70
|
+
options.onFault(fault);
|
|
71
|
+
return fault;
|
|
72
|
+
};
|
|
73
|
+
const onRejection = (reason) => {
|
|
74
|
+
record("unhandled_rejection", reason, false);
|
|
75
|
+
};
|
|
76
|
+
const onException = (error) => {
|
|
77
|
+
record("uncaught_exception", error, true);
|
|
78
|
+
void drainThenExit();
|
|
79
|
+
};
|
|
80
|
+
let exiting = false;
|
|
81
|
+
const drainThenExit = async () => {
|
|
82
|
+
// A second exception during the drain must not start a second drain, or the
|
|
83
|
+
// deadline below stops being a deadline.
|
|
84
|
+
if (exiting) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
exiting = true;
|
|
88
|
+
const deadlineMs = options.drainDeadlineMs ?? DEFAULT_DRAIN_DEADLINE_MS;
|
|
89
|
+
try {
|
|
90
|
+
// The race is the point: a drain that hangs is the failure mode being
|
|
91
|
+
// fixed, so it never decides when the process leaves.
|
|
92
|
+
await Promise.race([
|
|
93
|
+
options.shutdown?.() ?? Promise.resolve(),
|
|
94
|
+
new Promise((resolve) => {
|
|
95
|
+
const timer = setTimeout(resolve, deadlineMs);
|
|
96
|
+
timer.unref?.();
|
|
97
|
+
}),
|
|
98
|
+
]);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// A shutdown that threw changes nothing about needing to exit.
|
|
102
|
+
}
|
|
103
|
+
exit(GATEWAY_FAULT_EXIT_CODE);
|
|
104
|
+
};
|
|
105
|
+
host.on("unhandledRejection", onRejection);
|
|
106
|
+
host.on("uncaughtException", onException);
|
|
107
|
+
return () => {
|
|
108
|
+
armed.delete(host);
|
|
109
|
+
host.off("unhandledRejection", onRejection);
|
|
110
|
+
host.off("uncaughtException", onException);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=fault-barrier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fault-barrier.js","sourceRoot":"","sources":["../../src/process/fault-barrier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type AppScopeKey, type ProgressCursorKey, type ProgressCursorRecord, type ProgressStore } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Cap on how many cursors and pointers a single process will hold.
|
|
4
|
+
*
|
|
5
|
+
* Redis expires keys on its own; a plain Map does not, and a gateway that ran
|
|
6
|
+
* for a week would hold one entry per (caller, application) it had ever
|
|
7
|
+
* served. Insertion order is iteration order for a Map, so the oldest write
|
|
8
|
+
* is the one dropped — a caller evicted this way re-reads the retained
|
|
9
|
+
* history rather than losing anything, which is the same outcome as its
|
|
10
|
+
* cursor having expired.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_MEMORY_PROGRESS_ENTRIES = 2048;
|
|
13
|
+
/**
|
|
14
|
+
* Local-only fallback, matching the other in-memory stores.
|
|
15
|
+
*
|
|
16
|
+
* Not a second source of truth: two replicas with private maps would hand the
|
|
17
|
+
* same caller two different positions in the same stream, which is exactly
|
|
18
|
+
* the divergence Redis is required outside local to prevent.
|
|
19
|
+
*/
|
|
20
|
+
export declare class MemoryProgressStore implements ProgressStore {
|
|
21
|
+
private readonly cursors;
|
|
22
|
+
private readonly lastActivity;
|
|
23
|
+
private readonly liveEdits;
|
|
24
|
+
getCursor(key: ProgressCursorKey): Promise<ProgressCursorRecord | undefined>;
|
|
25
|
+
getLastActivityAtMs(key: AppScopeKey): Promise<number | undefined>;
|
|
26
|
+
getLiveEditId(key: AppScopeKey): Promise<string | undefined>;
|
|
27
|
+
setCursor(input: ProgressCursorKey & {
|
|
28
|
+
record: ProgressCursorRecord;
|
|
29
|
+
ttlMs?: number;
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
setLastActivityAtMs(input: AppScopeKey & {
|
|
32
|
+
atMs: number;
|
|
33
|
+
ttlMs?: number;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
setLiveEditId(input: AppScopeKey & {
|
|
36
|
+
liveEditId: string;
|
|
37
|
+
ttlMs?: number;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
}
|