mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(nostr): prefixed targets fail to send (#110881)
* fix(nostr): prefixed targets send successfully * fix(nostr): address target normalization review * fix(nostr): preserve invalid prefixed allowlist tokens * test(nostr): cover prefixed npub outbound targets Co-authored-by: 杨浩宇0668001029 <yang.haoyu@xydigit.com> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
Peter Steinberger
parent
8d5ad804a6
commit
7bac63889e
@@ -7,9 +7,12 @@ import {
|
||||
import type { WizardPrompter } from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import { nostrPlugin } from "./channel.js";
|
||||
import { normalizePubkey } from "./nostr-key-utils.js";
|
||||
import { nostrSetupWizard } from "./setup-surface.js";
|
||||
import {
|
||||
TEST_HEX_PRIVATE_KEY,
|
||||
TEST_HEX_PUBLIC_KEY,
|
||||
TEST_SETUP_RELAY_URLS,
|
||||
buildResolvedNostrAccount,
|
||||
createConfiguredNostrCfg,
|
||||
@@ -201,6 +204,25 @@ describe("nostrPlugin", () => {
|
||||
const ids = nostrTestPlugin.config.listAccountIds(cfg);
|
||||
expect(ids).toContain("default");
|
||||
});
|
||||
|
||||
it("normalizes prefixed npub allowlist entries", () => {
|
||||
const npub = "npub140x77qfrg4ncn27dauqjx3t83x4ummcpydzk0zdtehhszg69v7ystddknj";
|
||||
const formatted = nostrPlugin.config.formatAllowFrom?.({
|
||||
cfg: createConfiguredNostrCfg() as OpenClawConfig,
|
||||
allowFrom: [`nostr:${npub}`],
|
||||
});
|
||||
|
||||
expect(formatted).toEqual([normalizePubkey(npub)]);
|
||||
});
|
||||
|
||||
it("preserves invalid prefixed allowlist entries instead of promoting them to wildcards", () => {
|
||||
const formatted = nostrPlugin.config.formatAllowFrom?.({
|
||||
cfg: createConfiguredNostrCfg() as OpenClawConfig,
|
||||
allowFrom: ["nostr:*"],
|
||||
});
|
||||
|
||||
expect(formatted).toEqual(["nostr:*"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("messaging", () => {
|
||||
@@ -229,6 +251,44 @@ describe("nostrPlugin", () => {
|
||||
expect(normalize(`nostr:${TEST_HEX_PRIVATE_KEY}`)).toBe(TEST_HEX_PRIVATE_KEY);
|
||||
expect(normalize(` nostr:${TEST_HEX_PRIVATE_KEY} `)).toBe(TEST_HEX_PRIVATE_KEY);
|
||||
});
|
||||
|
||||
it("recognizes prefixed targets after provider normalization", () => {
|
||||
const raw = `nostr:${TEST_HEX_PUBLIC_KEY}`;
|
||||
const normalized = nostrPlugin.messaging?.normalizeTarget?.(raw);
|
||||
|
||||
expect(nostrPlugin.messaging?.targetResolver?.looksLikeId?.(raw, normalized)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "hex", target: TEST_HEX_PUBLIC_KEY },
|
||||
{
|
||||
name: "npub",
|
||||
target: "npub140x77qfrg4ncn27dauqjx3t83x4ummcpydzk0zdtehhszg69v7ystddknj",
|
||||
},
|
||||
])("normalizes prefixed $name targets for direct outbound sends", ({ target }) => {
|
||||
const result = nostrPlugin.outbound?.resolveTarget?.({
|
||||
cfg: createConfiguredNostrCfg() as OpenClawConfig,
|
||||
to: `nostr:${target}`,
|
||||
mode: "explicit",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true, to: normalizePubkey(target) });
|
||||
});
|
||||
|
||||
it("preserves the missing-target hint when no outbound target is supplied", () => {
|
||||
const result = nostrPlugin.outbound?.resolveTarget?.({
|
||||
cfg: createConfiguredNostrCfg() as OpenClawConfig,
|
||||
mode: "explicit",
|
||||
});
|
||||
|
||||
expect(result?.ok).toBe(false);
|
||||
if (!result || result.ok) {
|
||||
throw new Error("expected blank Nostr target to fail");
|
||||
}
|
||||
expect(result.error.message).toBe(
|
||||
"Delivering to Nostr requires target <npub|hex pubkey|nostr:npub...>",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("outbound", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createTopLevelChannelConfigAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-config-helpers";
|
||||
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
||||
import { missingTargetError } from "openclaw/plugin-sdk/channel-feedback";
|
||||
import { createChannelMessageAdapterFromOutbound } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import {
|
||||
buildPassiveChannelStatusSummary,
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
createDefaultChannelRuntimeState,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
formatPairingApproveHint,
|
||||
type ChannelOutboundAdapter,
|
||||
type ChannelPlugin,
|
||||
} from "./channel-api.js";
|
||||
import type { NostrProfile } from "./config-schema.js";
|
||||
@@ -39,6 +41,22 @@ import {
|
||||
type ResolvedNostrAccount,
|
||||
} from "./types.js";
|
||||
|
||||
const NOSTR_TARGET_HINT = "<npub|hex pubkey|nostr:npub...>";
|
||||
|
||||
function stripNostrTargetPrefix(target: string): string {
|
||||
return target.trim().replace(/^nostr:/i, "");
|
||||
}
|
||||
|
||||
function normalizeNostrTarget(target: string): string {
|
||||
const cleaned = stripNostrTargetPrefix(target);
|
||||
try {
|
||||
return normalizePubkey(cleaned);
|
||||
} catch {
|
||||
// Invalid prefixed tokens must stay distinct from "*" so formatting cannot widen access.
|
||||
return target.trim();
|
||||
}
|
||||
}
|
||||
|
||||
const resolveNostrDmPolicy = createScopedDmSecurityResolver<ResolvedNostrAccount>({
|
||||
channelKey: "nostr",
|
||||
resolvePolicy: (account) => account.config.dmPolicy,
|
||||
@@ -46,13 +64,7 @@ const resolveNostrDmPolicy = createScopedDmSecurityResolver<ResolvedNostrAccount
|
||||
policyPathSuffix: "dmPolicy",
|
||||
defaultPolicy: "pairing",
|
||||
approveHint: formatPairingApproveHint("nostr"),
|
||||
normalizeEntry: (raw) => {
|
||||
try {
|
||||
return normalizePubkey(raw.trim().replace(/^nostr:/i, ""));
|
||||
} catch {
|
||||
return raw.trim();
|
||||
}
|
||||
},
|
||||
normalizeEntry: normalizeNostrTarget,
|
||||
});
|
||||
|
||||
const nostrConfigAdapter = createTopLevelChannelConfigAdapter<ResolvedNostrAccount>({
|
||||
@@ -77,11 +89,7 @@ const nostrConfigAdapter = createTopLevelChannelConfigAdapter<ResolvedNostrAccou
|
||||
if (entry === "*") {
|
||||
return "*";
|
||||
}
|
||||
try {
|
||||
return normalizePubkey(entry);
|
||||
} catch {
|
||||
return entry;
|
||||
}
|
||||
return normalizeNostrTarget(entry);
|
||||
})
|
||||
.filter(Boolean),
|
||||
});
|
||||
@@ -91,6 +99,28 @@ const nostrMessageAdapter = createChannelMessageAdapterFromOutbound({
|
||||
outbound: nostrOutboundAdapter,
|
||||
});
|
||||
|
||||
const nostrPluginOutboundAdapter: ChannelOutboundAdapter = {
|
||||
...nostrOutboundAdapter,
|
||||
resolveTarget: ({ to }) => {
|
||||
const trimmed = to?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
return {
|
||||
ok: false,
|
||||
error: missingTargetError("Nostr", NOSTR_TARGET_HINT),
|
||||
};
|
||||
}
|
||||
const normalized = normalizeNostrTarget(trimmed);
|
||||
try {
|
||||
return { ok: true, to: normalizePubkey(normalized) };
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
error: new Error("Nostr target must be a 64-character hex pubkey or npub value"),
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChannelPlugin({
|
||||
base: {
|
||||
id: "nostr",
|
||||
@@ -125,25 +155,17 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChanne
|
||||
},
|
||||
messaging: {
|
||||
targetPrefixes: ["nostr"],
|
||||
normalizeTarget: (target) => {
|
||||
// Strip nostr: prefix if present
|
||||
const cleaned = target.trim().replace(/^nostr:/i, "");
|
||||
try {
|
||||
return normalizePubkey(cleaned);
|
||||
} catch {
|
||||
return cleaned;
|
||||
}
|
||||
},
|
||||
normalizeTarget: normalizeNostrTarget,
|
||||
targetResolver: {
|
||||
looksLikeId: (input) => {
|
||||
const trimmed = input.trim();
|
||||
looksLikeId: (input, normalized) => {
|
||||
const trimmed = normalized?.trim() || stripNostrTargetPrefix(input);
|
||||
return (
|
||||
trimmed.startsWith("npub1") ||
|
||||
trimmed.startsWith("NPUB1") ||
|
||||
/^[0-9a-fA-F]{64}$/.test(trimmed)
|
||||
);
|
||||
},
|
||||
hint: "<npub|hex pubkey|nostr:npub...>",
|
||||
hint: NOSTR_TARGET_HINT,
|
||||
},
|
||||
resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params),
|
||||
},
|
||||
@@ -179,7 +201,7 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChanne
|
||||
security: {
|
||||
resolveDmPolicy: resolveNostrDmPolicy,
|
||||
},
|
||||
outbound: nostrOutboundAdapter,
|
||||
outbound: nostrPluginOutboundAdapter,
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user