diff --git a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift index 672557879533..b8d355e0ac42 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawProtocol/GatewayModels.swift @@ -19277,10 +19277,10 @@ public struct PortalSummary: Codable, Sendable { public struct PortalListParams: Codable, Sendable {} public struct PortalListResult: Codable, Sendable { - public let portals: [PortalOpenResult] + public let portals: [PortalSummary] public init( - portals: [PortalOpenResult]) + portals: [PortalSummary]) { self.portals = portals } @@ -19321,8 +19321,8 @@ public struct PortalOpenResult: Codable, Sendable { public let title: String public let port: Int public let listenport: Int - public let tokenquery: String? - public let url: String? + public let tokenquery: String + public let url: String public let publicurl: String public let path: String? public let description: String? @@ -19333,8 +19333,8 @@ public struct PortalOpenResult: Codable, Sendable { title: String, port: Int, listenport: Int, - tokenquery: String? = nil, - url: String? = nil, + tokenquery: String, + url: String, publicurl: String, path: String? = nil, description: String? = nil, @@ -19395,10 +19395,10 @@ public struct PortalCloseResult: Codable, Sendable { } public struct PortalChangedEvent: Codable, Sendable { - public let portals: [PortalOpenResult] + public let portals: [PortalSummary] public init( - portals: [PortalOpenResult]) + portals: [PortalSummary]) { self.portals = portals } diff --git a/packages/gateway-protocol/src/schema/portals.test.ts b/packages/gateway-protocol/src/schema/portals.test.ts index bbf2f95bc244..c6ba93fcdae3 100644 --- a/packages/gateway-protocol/src/schema/portals.test.ts +++ b/packages/gateway-protocol/src/schema/portals.test.ts @@ -45,6 +45,7 @@ describe("portal protocol schemas", () => { expect(Value.Check(PortalListResultSchema, { portals: [portal] })).toBe(true); const { tokenQuery: _tokenQuery, url: _url, ...redactedPortal } = portal; expect(Value.Check(PortalSummarySchema, redactedPortal)).toBe(true); + expect(Value.Check(PortalOpenResultSchema, redactedPortal)).toBe(false); expect(Value.Check(PortalCloseResultSchema, { closed: true })).toBe(true); expect(Value.Check(PortalChangedEventSchema, { portals: [portal] })).toBe(true); const { publicUrl: _publicUrl, ...missingPublicUrl } = portal; diff --git a/packages/gateway-protocol/src/schema/portals.ts b/packages/gateway-protocol/src/schema/portals.ts index e94fb604904f..6eee3cc4dd60 100644 --- a/packages/gateway-protocol/src/schema/portals.ts +++ b/packages/gateway-protocol/src/schema/portals.ts @@ -2,17 +2,25 @@ import { Type, type Static } from "typebox"; import { closedObject } from "./closed-object.js"; import { NonEmptyString } from "./primitives.js"; -export const PortalSummarySchema = closedObject({ +const PortalSummaryIdentityFields = { id: NonEmptyString, title: NonEmptyString, port: Type.Integer({ minimum: 1, maximum: 65_535 }), listenPort: Type.Integer({ minimum: 1, maximum: 65_535 }), - tokenQuery: Type.Optional(NonEmptyString), - url: Type.Optional(NonEmptyString), +}; + +const PortalSummaryMetadataFields = { publicUrl: NonEmptyString, path: Type.Optional(Type.String({ pattern: "^/" })), description: Type.Optional(Type.String()), createdAtMs: Type.Integer({ minimum: 0 }), +}; + +export const PortalSummarySchema = closedObject({ + ...PortalSummaryIdentityFields, + tokenQuery: Type.Optional(NonEmptyString), + url: Type.Optional(NonEmptyString), + ...PortalSummaryMetadataFields, }); export const PortalListParamsSchema = closedObject({}); @@ -26,7 +34,12 @@ export const PortalOpenParamsSchema = closedObject({ description: Type.Optional(Type.String()), path: Type.Optional(Type.String({ pattern: "^/" })), }); -export const PortalOpenResultSchema = PortalSummarySchema; +export const PortalOpenResultSchema = closedObject({ + ...PortalSummaryIdentityFields, + tokenQuery: NonEmptyString, + url: NonEmptyString, + ...PortalSummaryMetadataFields, +}); export const PortalCloseParamsSchema = closedObject({ id: NonEmptyString }); export const PortalCloseResultSchema = closedObject({ closed: Type.Boolean() }); diff --git a/src/gateway/portals/portal-service.ts b/src/gateway/portals/portal-service.ts index fc5e0112bcad..09bf8f5af711 100644 --- a/src/gateway/portals/portal-service.ts +++ b/src/gateway/portals/portal-service.ts @@ -4,7 +4,10 @@ import { createServer as createHttpsServer } from "node:https"; import type { AddressInfo } from "node:net"; import type { Duplex } from "node:stream"; import type { TlsOptions } from "node:tls"; -import type { PortalSummary } from "../../../packages/gateway-protocol/src/index.js"; +import type { + PortalOpenResult, + PortalSummary, +} from "../../../packages/gateway-protocol/src/index.js"; import { listenGatewayHttpServer } from "../server/http-listen.js"; import { handlePortalProxyRequest, handlePortalProxyUpgrade } from "./portal-http-proxy.js"; @@ -33,7 +36,7 @@ type GatewayPortalOpenParams = { }; export type GatewayPortalService = { - open: (params: GatewayPortalOpenParams) => Promise; + open: (params: GatewayPortalOpenParams) => Promise; list: () => PortalSummary[]; close: (id: string) => Promise; closeAll: () => Promise; @@ -79,7 +82,7 @@ export function createGatewayPortalService(params: { const operations = new Map>(); let closed = false; - const summarize = (portal: PortalEntry): PortalSummary => { + const summarize = (portal: PortalEntry): PortalOpenResult => { const host = params.httpBindHosts[0]; if (!host) { throw new Error("Gateway listener must start before opening a portal"); diff --git a/src/gateway/server-methods/portals.test.ts b/src/gateway/server-methods/portals.test.ts index f0bc842523f7..f5c9028d98b9 100644 --- a/src/gateway/server-methods/portals.test.ts +++ b/src/gateway/server-methods/portals.test.ts @@ -1,12 +1,15 @@ import { describe, expect, it, vi } from "vitest"; -import type { PortalSummary } from "../../../packages/gateway-protocol/src/index.js"; +import type { + PortalOpenResult, + PortalSummary, +} from "../../../packages/gateway-protocol/src/index.js"; import { resolveCoreOperatorGatewayMethodScope } from "../methods/core-descriptors.js"; import type { GatewayPortalService } from "../portals/portal-service.js"; import { createGatewayBroadcaster } from "../server-broadcast.js"; import type { GatewayWsClient } from "../server/ws-types.js"; import { portalHandlers } from "./portals.js"; -const portal: PortalSummary = { +const portal = { id: "p3000", title: "App", port: 3000, @@ -15,7 +18,7 @@ const portal: PortalSummary = { url: `http://127.0.0.1:43123/?openclaw_portal=${"a".repeat(64)}`, publicUrl: "http://127.0.0.1:43123/", createdAtMs: 1, -}; +} satisfies PortalOpenResult; function harness(service?: GatewayPortalService, scopes = ["operator.write"]) { const broadcast = vi.fn(); diff --git a/ui/src/pages/portals/portals-page.test.ts b/ui/src/pages/portals/portals-page.test.ts index fd2e176d167d..ed774aaf041e 100644 --- a/ui/src/pages/portals/portals-page.test.ts +++ b/ui/src/pages/portals/portals-page.test.ts @@ -17,7 +17,7 @@ type PortalsPageTestElement = HTMLElement & { updateComplete: Promise; }; -const portal: PortalSummary = { +const portal = { id: "p3000", title: "Seeded app", port: 3000, @@ -28,7 +28,7 @@ const portal: PortalSummary = { path: "/app", description: "Use the seeded test account.", createdAtMs: 1_000, -}; +} satisfies PortalSummary; function createContext( methods: string[],