fix(runtime): accept camelcase miniapp bridge methods

This commit is contained in:
2026-08-06 01:20:41 +08:00
parent c4613ce14e
commit c7c18168b6
3 changed files with 29 additions and 11 deletions
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { CachedVerifiedSurfaceDocumentResolver, parseReadyMessage, productionSurfaceDocument, surfaceDocument } from "@/runtime/surfaces/isolated-surface-host";
import { CachedVerifiedSurfaceDocumentResolver, parseBridgeMessage, parseReadyMessage, productionSurfaceDocument, surfaceDocument } from "@/runtime/surfaces/isolated-surface-host";
import { VerifiedSurfaceBundleCache, type SurfaceBundleDigest, type SurfaceManifestSignatureVerifier } from "@/runtime/surfaces/surface-bundle-cache";
describe("isolated Surface bridge contract", () => {
@@ -19,6 +19,10 @@ describe("isolated Surface bridge contract", () => {
expect(document).not.toContain("https://");
});
it("accepts the SDK camelCase reportProgress bridge method", () => {
expect(parseBridgeMessage({ v: 1, type: "lineup.miniapp.v1.request", instance_id: "task:001", request_id: "req_1", method: "tools.reportProgress", payload: { call_id: "call-1", progress: { percent: 10 } } })).toMatchObject({ method: "tools.reportProgress" });
});
it("wraps only an exact verified production bundle in the host default-deny CSP", async () => {
// Digest mismatch behavior belongs to the cache suite. This test isolates
// the resolver's exact-version lookup and Host-owned CSP wrapping.
@@ -89,6 +89,12 @@ export class IsolatedSurfaceHost {
}
private readonly receiveMessage = (event: MessageEvent<unknown>): void => {
if (event.data && typeof event.data === "object") {
const candidate = event.data as { type?: unknown; instance_id?: unknown; method?: unknown };
if (candidate.type === BRIDGE_REQUEST_TYPE || candidate.type === READY_TYPE) {
console.info("[LineUp Surface] bridge.raw", { type: String(candidate.type), instance_id: String(candidate.instance_id ?? ""), method: typeof candidate.method === "string" ? candidate.method : "" });
}
}
const message = parseBridgeMessage(event.data);
if (!message) return;
const hosted = this.surfaces.get(message.instance_id);
@@ -104,9 +110,17 @@ export class IsolatedSurfaceHost {
}
if (message.type === BRIDGE_REQUEST_TYPE) {
if (!hosted.ready || !this.callbacks.request) return;
console.info("[LineUp Surface] bridge.request", { instance_id: message.instance_id, method: message.method });
void this.callbacks.request(hosted.instance.instance_id, message.method, message.payload)
.then(result => this.sendResponse(hosted, message.request_id, result))
.catch(error => this.sendError(hosted, message.request_id, error instanceof Error ? error.message : "bridge_request_failed"));
.then(result => {
console.info("[LineUp Surface] bridge.response", { instance_id: message.instance_id, method: message.method, disposition: typeof result?.disposition === "string" ? result.disposition : "data" });
this.sendResponse(hosted, message.request_id, result);
})
.catch(error => {
const reason = error instanceof Error ? error.message : "bridge_request_failed";
console.info("[LineUp Surface] bridge.error", { instance_id: message.instance_id, method: message.method, reason });
this.sendError(hosted, message.request_id, reason);
});
return;
}
if (!hosted.ready || hosted.cancelSent) return;
@@ -157,13 +171,13 @@ export function parseReadyMessage(value: unknown): SurfaceReadyMessage | undefin
return parsed?.type === READY_TYPE ? parsed : undefined;
}
function parseBridgeMessage(value: unknown): SurfaceReadyMessage | SurfaceEventMessage | BridgeRequestMessage | undefined {
export function parseBridgeMessage(value: unknown): SurfaceReadyMessage | SurfaceEventMessage | BridgeRequestMessage | undefined {
if (!isPlainObject(value) || value.v !== BRIDGE_VERSION || (value.type !== READY_TYPE && value.type !== "lineup.surface.v1.event" && value.type !== BRIDGE_REQUEST_TYPE) || typeof value.instance_id !== "string"
|| !/^[A-Za-z0-9._:-]{1,128}$/.test(value.instance_id)) return undefined;
if (value.type === READY_TYPE && Object.keys(value).every(key => key === "v" || key === "type" || key === "instance_id")) return { v: 1, type: READY_TYPE, instance_id: value.instance_id };
if (value.type === "lineup.surface.v1.event" && value.event === "cancel" && Object.keys(value).every(key => key === "v" || key === "type" || key === "instance_id" || key === "event")) return { v: 1, type: "lineup.surface.v1.event", instance_id: value.instance_id, event: "cancel" };
if (value.type === BRIDGE_REQUEST_TYPE && typeof value.request_id === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value.request_id)
&& typeof value.method === "string" && /^[a-z][a-z0-9._-]{0,63}$/.test(value.method) && isPlainObject(value.payload)
&& typeof value.method === "string" && /^[a-z][A-Za-z0-9._-]{0,63}$/.test(value.method) && isPlainObject(value.payload)
&& Object.keys(value).every(key => key === "v" || key === "type" || key === "instance_id" || key === "request_id" || key === "method" || key === "payload")) {
return { v: 1, type: BRIDGE_REQUEST_TYPE, instance_id: value.instance_id, request_id: value.request_id, method: value.method, payload: cloneObject(value.payload) };
}