fix(nostr): accept uppercase Bech32 keys in setup and DM targeting (#109878)

* fix(nostr): accept uppercase NSEC and NPUB inputs

* test(nostr): satisfy target resolver type check
This commit is contained in:
mushuiyu886
2026-07-18 08:13:11 +01:00
committed by GitHub
parent cad4e395d2
commit bfc94ad3c5
6 changed files with 55 additions and 4 deletions
@@ -158,6 +158,10 @@ describe("nostr outbound cfg threading", () => {
await cleanup.stop();
});
it("recognizes uppercase npub targets", () => {
expect(nostrPlugin.messaging?.targetResolver?.looksLikeId?.("NPUB1XYZ123")).toBe(true);
});
it("backs declared message adapter capabilities with outbound sends", async () => {
installOutboundRuntime();
const { cleanup, sendDm } = await startOutboundAccount();
@@ -0,0 +1,19 @@
// Nostr tests cover the lightweight setup plugin behavior.
import { nip19 } from "nostr-tools";
import { describe, expect, it } from "vitest";
import { nostrSetupPlugin } from "./channel.setup.js";
import { TEST_HEX_PRIVATE_KEY } from "./test-fixtures.js";
describe("nostr setup plugin", () => {
it("accepts uppercase bech32 private keys", () => {
const nsec = nip19.nsecEncode(Buffer.from(TEST_HEX_PRIVATE_KEY, "hex")).toUpperCase();
expect(
nostrSetupPlugin.setup?.validateInput?.({
cfg: {},
accountId: "default",
input: { privateKey: nsec },
} as never),
).toBeNull();
});
});
+5 -1
View File
@@ -91,7 +91,11 @@ function resolveSetupNostrAccount(params: {
}
function looksLikeNostrPrivateKey(privateKey: string): boolean {
return privateKey.startsWith("nsec1") || /^[0-9a-fA-F]{64}$/.test(privateKey);
return (
privateKey.startsWith("nsec1") ||
privateKey.startsWith("NSEC1") ||
/^[0-9a-fA-F]{64}$/.test(privateKey)
);
}
const nostrSetupAdapter = createNostrSetupAdapter({
+5 -1
View File
@@ -137,7 +137,11 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChanne
targetResolver: {
looksLikeId: (input) => {
const trimmed = input.trim();
return trimmed.startsWith("npub1") || /^[0-9a-fA-F]{64}$/.test(trimmed);
return (
trimmed.startsWith("npub1") ||
trimmed.startsWith("NPUB1") ||
/^[0-9a-fA-F]{64}$/.test(trimmed)
);
},
hint: "<npub|hex pubkey|nostr:npub...>",
},
+20
View File
@@ -62,6 +62,18 @@ describe("validatePrivateKey", () => {
});
describe("nsec format", () => {
it("accepts uppercase bech32 private keys", () => {
const nsec = nip19.nsecEncode(Buffer.from(TEST_HEX_PRIVATE_KEY, "hex"));
expect(validatePrivateKey(nsec.toUpperCase())).toEqual(validatePrivateKey(nsec));
});
it("rejects mixed-case bech32 private keys", () => {
const nsec = nip19.nsecEncode(Buffer.from(TEST_HEX_PRIVATE_KEY, "hex"));
expectThrowsError(() => validatePrivateKey(`N${nsec.slice(1)}`));
});
it("rejects invalid nsec (wrong checksum)", () => {
const badNsec = "nsec1invalidinvalidinvalidinvalidinvalidinvalidinvalidinvalid";
expectThrowsError(() => validatePrivateKey(badNsec));
@@ -113,6 +125,14 @@ describe("normalizePubkey", () => {
it("trims surrounding whitespace before decoding", () => {
expect(normalizePubkey(` ${NPUB} `)).toBe(HEX);
});
it("decodes uppercase bech32 public keys", () => {
expect(normalizePubkey(NPUB.toUpperCase())).toBe(HEX);
});
it("rejects mixed-case bech32 public keys", () => {
expectThrowsError(() => normalizePubkey(`N${NPUB.slice(1)}`));
});
});
});
+2 -2
View File
@@ -8,7 +8,7 @@ export function validatePrivateKey(key: string): Uint8Array {
const trimmed = key.trim();
// Handle nsec (bech32) format
if (trimmed.startsWith("nsec1")) {
if (trimmed.startsWith("nsec1") || trimmed.startsWith("NSEC1")) {
const decoded = nip19.decode(trimmed);
if (decoded.type !== "nsec") {
throw new Error("Invalid nsec key: wrong type");
@@ -44,7 +44,7 @@ export function normalizePubkey(input: string): string {
const trimmed = input.trim();
// npub format - decode to hex
if (trimmed.startsWith("npub1")) {
if (trimmed.startsWith("npub1") || trimmed.startsWith("NPUB1")) {
const decoded = nip19.decode(trimmed);
if (decoded.type !== "npub" || typeof decoded.data !== "string") {
throw new Error("Invalid npub key");