mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 23:10:43 +00:00
231 lines
7.3 KiB
TypeScript
231 lines
7.3 KiB
TypeScript
import { describe, expect, test } from "vitest";
|
|
import {
|
|
buildPluginNodeCapabilityScopedHostUrl,
|
|
hasAuthorizedPluginNodeCapability,
|
|
indexPluginNodeCapabilitySurfaces,
|
|
normalizePluginNodeCapabilityScopedUrl,
|
|
refreshClientPluginNodeCapability,
|
|
replacePluginNodeCapabilityInScopedHostUrl,
|
|
setClientPluginNodeCapability,
|
|
} from "./plugin-node-capability.js";
|
|
import type { GatewayWsClient } from "./server/ws-types.js";
|
|
|
|
function makeClient(
|
|
overrides: Partial<GatewayWsClient> & {
|
|
pluginNodeCapabilities?: GatewayWsClient["pluginNodeCapabilities"];
|
|
} = {},
|
|
): GatewayWsClient {
|
|
return {
|
|
socket: {} as GatewayWsClient["socket"],
|
|
connect: {
|
|
role: "node",
|
|
client: {
|
|
mode: "node",
|
|
},
|
|
} as GatewayWsClient["connect"],
|
|
connId: "node-1",
|
|
usesSharedGatewayAuth: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("plugin node capability helpers", () => {
|
|
test("builds scoped host urls from clean base urls", () => {
|
|
expect(
|
|
buildPluginNodeCapabilityScopedHostUrl(
|
|
"http://127.0.0.1:18789/root/?debug=1#hash",
|
|
"token value",
|
|
),
|
|
).toBe("http://127.0.0.1:18789/root/__openclaw__/cap/token%20value");
|
|
expect(buildPluginNodeCapabilityScopedHostUrl("not a url", "token")).toBeUndefined();
|
|
expect(buildPluginNodeCapabilityScopedHostUrl("http://127.0.0.1:18789", " ")).toBeUndefined();
|
|
});
|
|
|
|
test("normalizes scoped urls and moves capability into the query string", () => {
|
|
const normalized = normalizePluginNodeCapabilityScopedUrl(
|
|
"/__openclaw__/cap/token%20value/__openclaw__/canvas/file.txt?download=1",
|
|
);
|
|
expect(normalized).toEqual({
|
|
pathname: "/__openclaw__/canvas/file.txt",
|
|
capability: "token value",
|
|
rewrittenUrl: "/__openclaw__/canvas/file.txt?download=1&oc_cap=token+value",
|
|
scopedPath: true,
|
|
malformedScopedPath: false,
|
|
});
|
|
});
|
|
|
|
test("replaces scoped capability tokens without nesting capability prefixes", () => {
|
|
expect(
|
|
replacePluginNodeCapabilityInScopedHostUrl(
|
|
"http://127.0.0.1:18789/__openclaw__/cap/old-token/__openclaw__/a2ui/",
|
|
"new token",
|
|
),
|
|
).toBe("http://127.0.0.1:18789/__openclaw__/cap/new%20token/__openclaw__/a2ui");
|
|
});
|
|
|
|
test("marks malformed scoped urls without authorizing a path capability", () => {
|
|
const normalized = normalizePluginNodeCapabilityScopedUrl("/__openclaw__/cap/broken");
|
|
expect(normalized.scopedPath).toBe(true);
|
|
expect(normalized.malformedScopedPath).toBe(true);
|
|
expect(normalized.capability).toBeUndefined();
|
|
expect(normalized.rewrittenUrl).toBeUndefined();
|
|
});
|
|
|
|
test("stores capabilities per plugin surface", () => {
|
|
const client = makeClient();
|
|
setClientPluginNodeCapability({
|
|
client,
|
|
surface: { surface: "canvas" },
|
|
capability: "canvas-token",
|
|
expiresAtMs: 100,
|
|
});
|
|
setClientPluginNodeCapability({
|
|
client,
|
|
surface: { surface: "files" },
|
|
capability: "files-token",
|
|
expiresAtMs: 200,
|
|
});
|
|
expect(client.pluginNodeCapabilities).toEqual({
|
|
canvas: { capability: "canvas-token", expiresAtMs: 100 },
|
|
files: { capability: "files-token", expiresAtMs: 200 },
|
|
});
|
|
});
|
|
|
|
test("stores capabilities per plugin-owned surface scope", () => {
|
|
const client = makeClient();
|
|
setClientPluginNodeCapability({
|
|
client,
|
|
surface: { surface: "canvas", scopeKey: "canvas-plugin:canvas" },
|
|
capability: "canvas-token",
|
|
expiresAtMs: 100,
|
|
});
|
|
setClientPluginNodeCapability({
|
|
client,
|
|
surface: { surface: "canvas", scopeKey: "other-plugin:canvas" },
|
|
capability: "other-token",
|
|
expiresAtMs: 200,
|
|
});
|
|
|
|
expect(client.pluginNodeCapabilities).toEqual({
|
|
"canvas\u0000canvas-plugin:canvas": { capability: "canvas-token", expiresAtMs: 100 },
|
|
"canvas\u0000other-plugin:canvas": { capability: "other-token", expiresAtMs: 200 },
|
|
});
|
|
});
|
|
|
|
test("indexes plugin capability surfaces with shortest ttl per surface", () => {
|
|
expect(
|
|
indexPluginNodeCapabilitySurfaces([
|
|
{ surface: "canvas", ttlMs: 5_000 },
|
|
{ surface: " canvas ", ttlMs: 100 },
|
|
{ surface: "files" },
|
|
]),
|
|
).toEqual({
|
|
canvas: { surface: "canvas", ttlMs: 100 },
|
|
files: { surface: "files" },
|
|
});
|
|
});
|
|
|
|
test("refreshes client plugin surface url and stored capability", () => {
|
|
const client = makeClient({
|
|
pluginSurfaceUrls: {
|
|
canvas: "http://127.0.0.1:18789/__openclaw__/cap/old-token",
|
|
},
|
|
pluginNodeCapabilitySurfaces: {
|
|
canvas: { surface: "canvas", ttlMs: 100 },
|
|
},
|
|
});
|
|
const refreshed = refreshClientPluginNodeCapability({
|
|
client,
|
|
surface: { surface: "canvas" },
|
|
nowMs: 1_000,
|
|
});
|
|
expect(refreshed?.surface).toBe("canvas");
|
|
expect(refreshed?.expiresAtMs).toBe(1_100);
|
|
expect(refreshed?.capability).toBeTypeOf("string");
|
|
expect(refreshed?.capability).not.toBe("");
|
|
expect(refreshed?.scopedUrl).toContain("/__openclaw__/cap/");
|
|
expect(refreshed?.scopedUrl).not.toContain("old-token/__openclaw__/cap/");
|
|
expect(client.pluginSurfaceUrls?.canvas).toBe(refreshed?.scopedUrl);
|
|
expect(client.pluginNodeCapabilities?.canvas).toEqual({
|
|
capability: refreshed?.capability,
|
|
expiresAtMs: 1_100,
|
|
});
|
|
});
|
|
|
|
test("authorizes matching plugin surface capabilities and slides expiry", () => {
|
|
const client = makeClient({
|
|
pluginNodeCapabilities: {
|
|
canvas: { capability: "canvas-token", expiresAtMs: 1_500 },
|
|
},
|
|
});
|
|
const clients = new Set([client]);
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients,
|
|
surface: { surface: "canvas", ttlMs: 100 },
|
|
capability: "canvas-token",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(true);
|
|
expect(client.pluginNodeCapabilities?.canvas?.expiresAtMs).toBe(1_100);
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients,
|
|
surface: { surface: "canvas" },
|
|
capability: "wrong",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients,
|
|
surface: { surface: "files" },
|
|
capability: "canvas-token",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
test("does not authorize the same surface token for a different plugin scope", () => {
|
|
const client = makeClient({
|
|
pluginNodeCapabilities: {
|
|
"canvas\u0000canvas-plugin:canvas": { capability: "canvas-token", expiresAtMs: 1_500 },
|
|
},
|
|
});
|
|
const clients = new Set([client]);
|
|
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients,
|
|
surface: { surface: "canvas", scopeKey: "other-plugin:canvas" },
|
|
capability: "canvas-token",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(false);
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients,
|
|
surface: { surface: "canvas", scopeKey: "canvas-plugin:canvas", ttlMs: 100 },
|
|
capability: "canvas-token",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
test("rejects expired capabilities", () => {
|
|
const client = makeClient({
|
|
pluginNodeCapabilities: {
|
|
canvas: { capability: "canvas-token", expiresAtMs: 999 },
|
|
},
|
|
});
|
|
expect(
|
|
hasAuthorizedPluginNodeCapability({
|
|
clients: new Set([client]),
|
|
surface: { surface: "canvas" },
|
|
capability: "canvas-token",
|
|
nowMs: 1_000,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|