mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 20:16:28 +01:00
170 lines
5.6 KiB
TypeScript
170 lines
5.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
createDirectDmPreCryptoGuardPolicy,
|
|
createPreCryptoDirectDmAuthorizer,
|
|
dispatchInboundDirectDmWithRuntime,
|
|
resolveInboundDirectDmAccessWithRuntime,
|
|
} from "./direct-dm.js";
|
|
|
|
const baseCfg = {
|
|
commands: { useAccessGroups: true },
|
|
} as unknown as OpenClawConfig;
|
|
|
|
describe("plugin-sdk/direct-dm", () => {
|
|
it("resolves inbound DM access and command auth through one helper", async () => {
|
|
const result = await resolveInboundDirectDmAccessWithRuntime({
|
|
cfg: baseCfg,
|
|
channel: "nostr",
|
|
accountId: "default",
|
|
dmPolicy: "pairing",
|
|
allowFrom: [],
|
|
senderId: "paired-user",
|
|
rawBody: "/status",
|
|
isSenderAllowed: (senderId, allowFrom) => allowFrom.includes(senderId),
|
|
readStoreAllowFrom: async () => ["paired-user"],
|
|
runtime: {
|
|
shouldComputeCommandAuthorized: () => true,
|
|
resolveCommandAuthorizedFromAuthorizers: ({ authorizers }) =>
|
|
authorizers.some((entry) => entry.configured && entry.allowed),
|
|
},
|
|
modeWhenAccessGroupsOff: "configured",
|
|
});
|
|
|
|
expect(result.access.decision).toBe("allow");
|
|
expect(result.access.effectiveAllowFrom).toEqual(["paired-user"]);
|
|
expect(result.senderAllowedForCommands).toBe(true);
|
|
expect(result.commandAuthorized).toBe(true);
|
|
});
|
|
|
|
it("creates a pre-crypto authorizer that issues pairing and blocks unknown senders", async () => {
|
|
const issuePairingChallenge = vi.fn(async () => {});
|
|
const onBlocked = vi.fn();
|
|
const authorizer = createPreCryptoDirectDmAuthorizer({
|
|
resolveAccess: async (senderId) => ({
|
|
access:
|
|
senderId === "pair-me"
|
|
? {
|
|
decision: "pairing" as const,
|
|
reasonCode: "dm_policy_pairing_required",
|
|
reason: "dmPolicy=pairing (not allowlisted)",
|
|
effectiveAllowFrom: [],
|
|
}
|
|
: {
|
|
decision: "block" as const,
|
|
reasonCode: "dm_policy_disabled",
|
|
reason: "dmPolicy=disabled",
|
|
effectiveAllowFrom: [],
|
|
},
|
|
}),
|
|
issuePairingChallenge,
|
|
onBlocked,
|
|
});
|
|
|
|
await expect(
|
|
authorizer({
|
|
senderId: "pair-me",
|
|
reply: async () => {},
|
|
}),
|
|
).resolves.toBe("pairing");
|
|
await expect(
|
|
authorizer({
|
|
senderId: "blocked",
|
|
reply: async () => {},
|
|
}),
|
|
).resolves.toBe("block");
|
|
|
|
expect(issuePairingChallenge).toHaveBeenCalledTimes(1);
|
|
expect(onBlocked).toHaveBeenCalledWith({
|
|
senderId: "blocked",
|
|
reason: "dmPolicy=disabled",
|
|
reasonCode: "dm_policy_disabled",
|
|
});
|
|
});
|
|
|
|
it("builds a shared pre-crypto guard policy with partial overrides", () => {
|
|
const policy = createDirectDmPreCryptoGuardPolicy({
|
|
maxFutureSkewSec: 30,
|
|
rateLimit: {
|
|
maxPerSenderPerWindow: 5,
|
|
},
|
|
});
|
|
|
|
expect(policy.allowedKinds).toEqual([4]);
|
|
expect(policy.maxFutureSkewSec).toBe(30);
|
|
expect(policy.maxCiphertextBytes).toBe(16 * 1024);
|
|
expect(policy.rateLimit.maxPerSenderPerWindow).toBe(5);
|
|
expect(policy.rateLimit.maxGlobalPerWindow).toBe(200);
|
|
});
|
|
|
|
it("dispatches direct DMs through the standard route/session/reply pipeline", async () => {
|
|
const recordInboundSession = vi.fn(async () => {});
|
|
const dispatchReplyWithBufferedBlockDispatcher = vi.fn(async ({ dispatcherOptions }) => {
|
|
await dispatcherOptions.deliver({ text: "reply text" });
|
|
});
|
|
const deliver = vi.fn(async () => {});
|
|
|
|
const result = await dispatchInboundDirectDmWithRuntime({
|
|
cfg: {
|
|
session: { store: { type: "jsonl" } },
|
|
} as never,
|
|
runtime: {
|
|
channel: {
|
|
routing: {
|
|
resolveAgentRoute: vi.fn(({ accountId, peer }) => ({
|
|
agentId: "agent-main",
|
|
accountId,
|
|
sessionKey: `dm:${peer.id}`,
|
|
})),
|
|
},
|
|
session: {
|
|
resolveStorePath: vi.fn(() => "/tmp/direct-dm-session-store"),
|
|
readSessionUpdatedAt: vi.fn(() => 1234),
|
|
recordInboundSession,
|
|
},
|
|
reply: {
|
|
resolveEnvelopeFormatOptions: vi.fn(() => ({ mode: "agent" })),
|
|
formatAgentEnvelope: vi.fn(({ body }) => `env:${body}`),
|
|
finalizeInboundContext: vi.fn((ctx) => ctx),
|
|
dispatchReplyWithBufferedBlockDispatcher,
|
|
},
|
|
},
|
|
} as never,
|
|
channel: "nostr",
|
|
channelLabel: "Nostr",
|
|
accountId: "default",
|
|
peer: { kind: "direct", id: "sender-1" },
|
|
senderId: "sender-1",
|
|
senderAddress: "nostr:sender-1",
|
|
recipientAddress: "nostr:bot-1",
|
|
conversationLabel: "sender-1",
|
|
rawBody: "hello world",
|
|
messageId: "event-123",
|
|
timestamp: 1_710_000_000_000,
|
|
commandAuthorized: true,
|
|
deliver,
|
|
onRecordError: () => {},
|
|
onDispatchError: () => {},
|
|
});
|
|
|
|
expect(result.route).toMatchObject({
|
|
agentId: "agent-main",
|
|
accountId: "default",
|
|
sessionKey: "dm:sender-1",
|
|
});
|
|
expect(result.storePath).toBe("/tmp/direct-dm-session-store");
|
|
expect(result.ctxPayload).toMatchObject({
|
|
Body: "env:hello world",
|
|
BodyForAgent: "hello world",
|
|
From: "nostr:sender-1",
|
|
To: "nostr:bot-1",
|
|
SenderId: "sender-1",
|
|
MessageSid: "event-123",
|
|
CommandAuthorized: true,
|
|
});
|
|
expect(recordInboundSession).toHaveBeenCalledTimes(1);
|
|
expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(1);
|
|
expect(deliver).toHaveBeenCalledWith({ text: "reply text" });
|
|
});
|
|
});
|