fix(config): redact Nostr privateKey in config views (#58177)

* wip(config): preserve nostr redaction progress

* fix(config): add private key redaction fallback

* fix(config): align nostr privateKey secret input handling

* fix(config): require resolved nostr private keys
This commit is contained in:
Vincent Koc
2026-03-31 19:55:03 +09:00
committed by GitHub
parent efe9183f9d
commit 57700d716f
10 changed files with 200 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ import {
} from "../../../test/helpers/plugins/setup-wizard.js";
import type { OpenClawConfig } from "../runtime-api.js";
import { nostrPlugin } from "./channel.js";
import { nostrSetupWizard } from "./setup-surface.js";
import {
TEST_HEX_PRIVATE_KEY,
TEST_SETUP_RELAY_URLS,
@@ -225,6 +226,21 @@ describe("nostr account helpers", () => {
const cfg = createConfiguredNostrCfg({ defaultAccount: "work" });
expect(listNostrAccountIds(cfg)).toEqual(["work"]);
});
it("does not treat unresolved SecretRef privateKey as configured", () => {
const cfg = {
channels: {
nostr: {
privateKey: {
source: "env",
provider: "default",
id: "NOSTR_PRIVATE_KEY",
},
},
},
};
expect(listNostrAccountIds(cfg)).toEqual([]);
});
});
describe("resolveDefaultNostrAccountId", () => {
@@ -313,6 +329,27 @@ describe("nostr account helpers", () => {
expect(account.publicKey).toBe("");
});
it("does not treat unresolved SecretRef privateKey as configured", () => {
const secretRef = {
source: "env" as const,
provider: "default",
id: "NOSTR_PRIVATE_KEY",
};
const cfg = {
channels: {
nostr: {
privateKey: secretRef,
},
},
};
const account = resolveNostrAccount({ cfg });
expect(account.configured).toBe(false);
expect(account.privateKey).toBe("");
expect(account.publicKey).toBe("");
expect(account.config.privateKey).toEqual(secretRef);
});
it("preserves all config options", () => {
const cfg = createConfiguredNostrCfg({
name: "Bot",
@@ -333,4 +370,32 @@ describe("nostr account helpers", () => {
});
});
});
describe("setup wizard", () => {
it("keeps unresolved SecretRef privateKey visible without marking the account configured", () => {
const secretRef = {
source: "env" as const,
provider: "default",
id: "NOSTR_PRIVATE_KEY",
};
const cfg = {
channels: {
nostr: {
privateKey: secretRef,
},
},
};
const credential = nostrSetupWizard.credentials?.[0];
if (!credential?.inspect) {
throw new Error("nostr setup credential inspect missing");
}
expect(credential.inspect({ cfg, accountId: "default" })).toEqual({
accountConfigured: false,
hasConfiguredValue: true,
resolvedValue: undefined,
envValue: undefined,
});
});
});
});

View File

@@ -4,6 +4,7 @@ import {
DmPolicySchema,
MarkdownConfigSchema,
} from "openclaw/plugin-sdk/channel-config-primitives";
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
import { z } from "openclaw/plugin-sdk/zod";
/**
@@ -73,7 +74,7 @@ export const NostrConfigSchema = z.object({
markdown: MarkdownConfigSchema,
/** Private key in hex or nsec bech32 format */
privateKey: z.string().optional(),
privateKey: buildSecretInputSchema().optional(),
/** WebSocket relay URLs to connect to */
relays: z.array(z.string()).optional(),

View File

@@ -1,6 +1,10 @@
import type { ChannelSetupAdapter } from "openclaw/plugin-sdk/channel-setup";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import {
hasConfiguredSecretInput,
normalizeSecretInputString,
} from "openclaw/plugin-sdk/secret-input";
import {
createTopLevelChannelParsedAllowFromPrompt,
createTopLevelChannelDmPolicy,
@@ -165,7 +169,7 @@ export const nostrSetupWizard: ChannelSetupWizard = {
isAvailable: ({ cfg, accountId }) =>
accountId === DEFAULT_ACCOUNT_ID &&
Boolean(process.env.NOSTR_PRIVATE_KEY?.trim()) &&
!resolveNostrAccount({ cfg, accountId }).config.privateKey?.trim(),
!hasConfiguredSecretInput(resolveNostrAccount({ cfg, accountId }).config.privateKey),
apply: async ({ cfg }) =>
patchTopLevelChannelConfigSection({
cfg,
@@ -191,8 +195,8 @@ export const nostrSetupWizard: ChannelSetupWizard = {
const account = resolveNostrAccount({ cfg, accountId });
return {
accountConfigured: account.configured,
hasConfiguredValue: Boolean(account.config.privateKey?.trim()),
resolvedValue: account.config.privateKey?.trim(),
hasConfiguredValue: hasConfiguredSecretInput(account.config.privateKey),
resolvedValue: normalizeSecretInputString(account.config.privateKey),
envValue: process.env.NOSTR_PRIVATE_KEY?.trim(),
};
},

View File

@@ -7,6 +7,7 @@ import {
listCombinedAccountIds,
resolveListedDefaultAccountId,
} from "openclaw/plugin-sdk/account-resolution";
import { normalizeSecretInputString, type SecretInput } from "openclaw/plugin-sdk/secret-input";
import type { OpenClawConfig } from "../api.js";
import type { NostrProfile } from "./config-schema.js";
import { DEFAULT_RELAYS } from "./default-relays.js";
@@ -16,7 +17,7 @@ export interface NostrAccountConfig {
enabled?: boolean;
name?: string;
defaultAccount?: string;
privateKey?: string;
privateKey?: SecretInput;
relays?: string[];
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
allowFrom?: Array<string | number>;
@@ -49,9 +50,10 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
| NostrAccountConfig
| undefined;
const privateKey = normalizeSecretInputString(nostrCfg?.privateKey);
return listCombinedAccountIds({
configuredAccountIds: [],
implicitAccountId: nostrCfg?.privateKey
implicitAccountId: privateKey
? (resolveConfiguredDefaultNostrAccountId(cfg) ?? DEFAULT_ACCOUNT_ID)
: undefined,
});
@@ -80,11 +82,11 @@ export function resolveNostrAccount(opts: {
| undefined;
const baseEnabled = nostrCfg?.enabled !== false;
const privateKey = nostrCfg?.privateKey ?? "";
const configured = Boolean(privateKey.trim());
const privateKey = normalizeSecretInputString(nostrCfg?.privateKey) ?? "";
const configured = Boolean(privateKey);
let publicKey = "";
if (configured) {
if (privateKey) {
try {
publicKey = getPublicKeyFromPrivate(privateKey);
} catch {