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:
mushuiyu886
2026-07-20 04:21:49 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 8d5ad804a6
commit 7bac63889e
2 changed files with 107 additions and 25 deletions
+60
View File
@@ -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", () => {
+47 -25
View File
@@ -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,
});
/**