fix(gateway): type portal open credentials

This commit is contained in:
Peter Steinberger
2026-08-12 14:54:45 -07:00
parent 1efa308d0b
commit 283b58cbc8
6 changed files with 40 additions and 20 deletions
@@ -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
}
@@ -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;
@@ -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() });
+6 -3
View File
@@ -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<PortalSummary>;
open: (params: GatewayPortalOpenParams) => Promise<PortalOpenResult>;
list: () => PortalSummary[];
close: (id: string) => Promise<void>;
closeAll: () => Promise<void>;
@@ -79,7 +82,7 @@ export function createGatewayPortalService(params: {
const operations = new Map<string, Promise<void>>();
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");
+6 -3
View File
@@ -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();
+2 -2
View File
@@ -17,7 +17,7 @@ type PortalsPageTestElement = HTMLElement & {
updateComplete: Promise<boolean>;
};
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[],