mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(nostr): retain SecretRef-backed accounts (#126934)
This commit is contained in:
committed by
GitHub
parent
718dacc46a
commit
94f042ba86
@@ -119,6 +119,7 @@ The lists below are generated from the source target registry and checked agains
|
||||
- `channels.nextcloud-talk.apiPassword`
|
||||
- `channels.nextcloud-talk.accounts.*.botSecret`
|
||||
- `channels.nextcloud-talk.accounts.*.apiPassword`
|
||||
- `channels.nostr.privateKey`
|
||||
- `channels.zalo.botToken`
|
||||
- `channels.zalo.webhookSecret`
|
||||
- `channels.zalo.accounts.*.botToken`
|
||||
|
||||
@@ -297,6 +297,13 @@
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.nostr.privateKey",
|
||||
"configFile": "openclaw.json",
|
||||
"path": "channels.nostr.privateKey",
|
||||
"secretShape": "secret_input",
|
||||
"optIn": true
|
||||
},
|
||||
{
|
||||
"id": "channels.qqbot.accounts.*.clientSecret",
|
||||
"configFile": "openclaw.json",
|
||||
|
||||
@@ -39,6 +39,10 @@ export default defineBundledChannelEntry({
|
||||
specifier: "./channel-plugin-api.js",
|
||||
exportName: "nostrPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets",
|
||||
},
|
||||
runtime: {
|
||||
specifier: "./api.js",
|
||||
exportName: "setNostrRuntime",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { createResolverContext } from "openclaw/plugin-sdk/secret-ref-runtime";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
channelSecrets,
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./secret-contract-api.js";
|
||||
|
||||
describe("Nostr public secret contract", () => {
|
||||
it("publishes the private-key target for plan, configure, apply, and audit", () => {
|
||||
expect(channelSecrets.secretTargetRegistryEntries).toBe(secretTargetRegistryEntries);
|
||||
expect(channelSecrets.collectRuntimeConfigAssignments).toBe(collectRuntimeConfigAssignments);
|
||||
expect(secretTargetRegistryEntries).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "channels.nostr.privateKey",
|
||||
pathPattern: "channels.nostr.privateKey",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ defaultAccount: undefined, ownerId: "nostr:default" },
|
||||
{ defaultAccount: "Team.A", ownerId: "nostr:team-a" },
|
||||
])(
|
||||
"assigns the configured private key to exact owner $ownerId",
|
||||
({ defaultAccount, ownerId }) => {
|
||||
const sourceConfig = {
|
||||
channels: {
|
||||
nostr: {
|
||||
...(defaultAccount ? { defaultAccount } : {}),
|
||||
relays: ["wss://relay.example"],
|
||||
privateKey: { source: "env", provider: "default", id: "NOSTR_TEST_PRIVATE_KEY" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const config = structuredClone(sourceConfig);
|
||||
const context = createResolverContext({ sourceConfig, env: {} });
|
||||
|
||||
collectRuntimeConfigAssignments({ config, context });
|
||||
|
||||
expect(context.assignments).toEqual([
|
||||
expect.objectContaining({
|
||||
path: "channels.nostr.privateKey",
|
||||
ownerKind: "account",
|
||||
ownerId,
|
||||
requiredForGateway: false,
|
||||
disposition: "isolate",
|
||||
ownerContractDigest: expect.any(String),
|
||||
}),
|
||||
]);
|
||||
context.assignments[0]?.apply("materialized-private-key");
|
||||
expect(config.channels?.nostr?.privateKey).toBe("materialized-private-key");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["file", "exec", "store"] as const)(
|
||||
"does not collect an active $0 provider assignment while Nostr is disabled",
|
||||
(source) => {
|
||||
const sourceConfig = {
|
||||
channels: {
|
||||
nostr: {
|
||||
enabled: false,
|
||||
privateKey: { source, provider: "vault", id: "NOSTR_TEST_PRIVATE_KEY" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const context = createResolverContext({ sourceConfig, env: {} });
|
||||
|
||||
collectRuntimeConfigAssignments({ config: structuredClone(sourceConfig), context });
|
||||
|
||||
expect(context.assignments).toEqual([]);
|
||||
expect(context.warnings).toEqual([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.nostr.privateKey",
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
channelSecrets,
|
||||
collectRuntimeConfigAssignments,
|
||||
secretTargetRegistryEntries,
|
||||
} from "./src/secret-contract.js";
|
||||
@@ -7,4 +7,8 @@ export default defineBundledChannelSetupEntry({
|
||||
specifier: "./setup-plugin-api.js",
|
||||
exportName: "nostrSetupPlugin",
|
||||
},
|
||||
secrets: {
|
||||
specifier: "./secret-contract-api.js",
|
||||
exportName: "channelSecrets",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Nostr tests cover the lightweight setup plugin behavior.
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { withEnv } from "openclaw/plugin-sdk/test-env";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { nostrSetupPlugin } from "./channel.setup.js";
|
||||
import { TEST_HEX_PRIVATE_KEY } from "./test-fixtures.js";
|
||||
@@ -16,4 +17,26 @@ describe("nostr setup plugin", () => {
|
||||
} as never),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps an unresolved named SecretRef account configured without ambient fallback", () => {
|
||||
const cfg = {
|
||||
channels: {
|
||||
nostr: {
|
||||
defaultAccount: "Team.A",
|
||||
privateKey: { source: "env" as const, provider: "default", id: "MISSING_NOSTR_KEY" },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
withEnv({ NOSTR_PRIVATE_KEY: TEST_HEX_PRIVATE_KEY }, () => {
|
||||
expect(nostrSetupPlugin.config.defaultAccountId?.(cfg)).toBe("team-a");
|
||||
expect(nostrSetupPlugin.config.listAccountIds(cfg)).toEqual(["team-a"]);
|
||||
expect(nostrSetupPlugin.config.resolveAccount(cfg, undefined)).toMatchObject({
|
||||
accountId: "team-a",
|
||||
configured: true,
|
||||
privateKey: "",
|
||||
});
|
||||
expect(nostrSetupPlugin.config.resolveAccount(cfg, "Team.A").accountId).toBe("team-a");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Nostr plugin module implements channel.setup behavior.
|
||||
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import {
|
||||
createDelegatedSetupWizardProxy,
|
||||
DEFAULT_ACCOUNT_ID,
|
||||
} from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { createDelegatedSetupWizardProxy } from "openclaw/plugin-sdk/setup-runtime";
|
||||
import { buildChannelConfigSchema, type ChannelPlugin } from "./channel-api.js";
|
||||
import { NostrConfigSchema } from "./config-schema.js";
|
||||
import { DEFAULT_RELAYS } from "./default-relays.js";
|
||||
import { resolveNostrPrivateKey } from "./private-key.js";
|
||||
import { hasConfiguredNostrPrivateKey, resolveNostrPrivateKey } from "./private-key.js";
|
||||
import {
|
||||
createNostrSetupAdapter,
|
||||
createNostrSetupContract,
|
||||
@@ -27,10 +25,7 @@ function getNostrConfig(cfg: OpenClawConfig): NostrAccountConfig | undefined {
|
||||
}
|
||||
|
||||
function resolveDefaultSetupNostrAccountId(cfg: OpenClawConfig): string {
|
||||
const configured = getNostrConfig(cfg)?.defaultAccount;
|
||||
return typeof configured === "string" && configured.trim()
|
||||
? configured.trim()
|
||||
: DEFAULT_ACCOUNT_ID;
|
||||
return normalizeAccountId(getNostrConfig(cfg)?.defaultAccount);
|
||||
}
|
||||
|
||||
function resolveSetupNostrAccount(params: {
|
||||
@@ -38,9 +33,11 @@ function resolveSetupNostrAccount(params: {
|
||||
accountId?: string | null;
|
||||
}): ResolvedNostrAccount {
|
||||
const nostrCfg = getNostrConfig(params.cfg);
|
||||
const accountId = params.accountId?.trim() || resolveDefaultSetupNostrAccountId(params.cfg);
|
||||
const accountId = normalizeAccountId(
|
||||
params.accountId ?? resolveDefaultSetupNostrAccountId(params.cfg),
|
||||
);
|
||||
const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
|
||||
const configured = Boolean(privateKey);
|
||||
const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
|
||||
return {
|
||||
accountId,
|
||||
name: typeof nostrCfg?.name === "string" ? nostrCfg.name : undefined,
|
||||
|
||||
@@ -158,10 +158,11 @@ function requireNostrResolveDmPolicy() {
|
||||
return resolveDmPolicy;
|
||||
}
|
||||
|
||||
function createUnresolvedNostrPrivateKeyCfg() {
|
||||
function createUnresolvedNostrPrivateKeyCfg(defaultAccount?: string) {
|
||||
return {
|
||||
channels: {
|
||||
nostr: {
|
||||
...(defaultAccount ? { defaultAccount } : {}),
|
||||
privateKey: {
|
||||
source: "env" as const,
|
||||
provider: "default",
|
||||
@@ -176,7 +177,7 @@ const unresolvedSecretRefPrivateKeyCases = [
|
||||
{
|
||||
name: "listNostrAccountIds",
|
||||
assert: (cfg: ReturnType<typeof createUnresolvedNostrPrivateKeyCfg>) => {
|
||||
expect(listNostrAccountIds(cfg)).toStrictEqual([]);
|
||||
expect(listNostrAccountIds(cfg)).toStrictEqual(["work"]);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -184,7 +185,8 @@ const unresolvedSecretRefPrivateKeyCases = [
|
||||
assert: (cfg: ReturnType<typeof createUnresolvedNostrPrivateKeyCfg>) => {
|
||||
const account = resolveNostrAccount({ cfg });
|
||||
|
||||
expect(account.configured).toBe(false);
|
||||
expect(account.accountId).toBe("work");
|
||||
expect(account.configured).toBe(true);
|
||||
expect(account.privateKey).toBe("");
|
||||
expect(account.publicKey).toBe("");
|
||||
expect(account.config.privateKey).toEqual(cfg.channels.nostr.privateKey);
|
||||
@@ -447,10 +449,10 @@ describe("nostr setup wizard", () => {
|
||||
|
||||
describe("nostr unresolved SecretRef privateKey", () => {
|
||||
it.each(unresolvedSecretRefPrivateKeyCases)(
|
||||
"$name does not treat unresolved SecretRef privateKey as configured",
|
||||
"$name keeps an unresolved named SecretRef account configured without using ambient credentials",
|
||||
({ assert }) => {
|
||||
withEnv({ NOSTR_PRIVATE_KEY: TEST_HEX_PRIVATE_KEY }, () => {
|
||||
assert(createUnresolvedNostrPrivateKeyCfg());
|
||||
assert(createUnresolvedNostrPrivateKeyCfg("work"));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -597,7 +599,7 @@ describe("nostr account helpers", () => {
|
||||
});
|
||||
|
||||
describe("setup wizard", () => {
|
||||
it("keeps unresolved SecretRef privateKey visible without marking the account configured", () => {
|
||||
it("keeps unresolved SecretRef privateKey configured without exposing a materialized value", () => {
|
||||
const secretRef = {
|
||||
source: "env" as const,
|
||||
provider: "default",
|
||||
@@ -618,7 +620,7 @@ describe("nostr account helpers", () => {
|
||||
expect(
|
||||
withoutNostrPrivateKey(() => credential.inspect({ cfg, accountId: "default" })),
|
||||
).toEqual({
|
||||
accountConfigured: false,
|
||||
accountConfigured: true,
|
||||
hasConfiguredValue: true,
|
||||
resolvedValue: undefined,
|
||||
envValue: undefined,
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
|
||||
export const NOSTR_PRIVATE_KEY_ENV_VAR = "NOSTR_PRIVATE_KEY";
|
||||
|
||||
export function hasConfiguredNostrPrivateKey(value: SecretInput | undefined): boolean {
|
||||
return hasConfiguredSecretInput(value) || Boolean(process.env[NOSTR_PRIVATE_KEY_ENV_VAR]?.trim());
|
||||
}
|
||||
|
||||
export function resolveNostrPrivateKey(value: SecretInput | undefined): string {
|
||||
const configured = normalizeSecretInputString(value);
|
||||
if (configured || hasConfiguredSecretInput(value)) {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import {
|
||||
collectSecretInputAssignment,
|
||||
createChannelSecretTargetRegistryEntries,
|
||||
getChannelRecord,
|
||||
type ResolverContext,
|
||||
type SecretDefaults,
|
||||
} from "openclaw/plugin-sdk/channel-secret-basic-runtime";
|
||||
|
||||
export const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
|
||||
channelKey: "nostr",
|
||||
channel: ["privateKey"],
|
||||
});
|
||||
|
||||
export function collectRuntimeConfigAssignments(params: {
|
||||
config: { channels?: Record<string, unknown> };
|
||||
defaults?: SecretDefaults;
|
||||
context: ResolverContext;
|
||||
}): void {
|
||||
const nostr = getChannelRecord(params.config, "nostr");
|
||||
if (!nostr) {
|
||||
return;
|
||||
}
|
||||
const accountId = normalizeAccountId(
|
||||
typeof nostr.defaultAccount === "string" ? nostr.defaultAccount : undefined,
|
||||
);
|
||||
collectSecretInputAssignment({
|
||||
value: nostr.privateKey,
|
||||
path: "channels.nostr.privateKey",
|
||||
expected: "string",
|
||||
defaults: params.defaults,
|
||||
context: params.context,
|
||||
active: nostr.enabled !== false,
|
||||
inactiveReason: "Nostr channel is disabled.",
|
||||
owner: {
|
||||
ownerKind: "account",
|
||||
ownerId: `nostr:${accountId}`,
|
||||
requiredForGateway: false,
|
||||
disposition: "isolate",
|
||||
contract: nostr,
|
||||
},
|
||||
apply: (value) => {
|
||||
nostr.privateKey = value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const channelSecrets = {
|
||||
secretTargetRegistryEntries,
|
||||
collectRuntimeConfigAssignments,
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
|
||||
import type { NostrProfile } from "./config-schema.js";
|
||||
import { DEFAULT_RELAYS } from "./default-relays.js";
|
||||
import { getPublicKeyFromPrivate } from "./nostr-key-utils.js";
|
||||
import { resolveNostrPrivateKey } from "./private-key.js";
|
||||
import { hasConfiguredNostrPrivateKey, resolveNostrPrivateKey } from "./private-key.js";
|
||||
|
||||
interface NostrAccountConfig {
|
||||
enabled?: boolean;
|
||||
@@ -43,7 +43,7 @@ const {
|
||||
fallbackAccountIdWhenEmpty: false,
|
||||
resolveImplicitAccountId: (cfg) => {
|
||||
const account = cfg.channels?.nostr as NostrAccountConfig | undefined;
|
||||
return resolveNostrPrivateKey(account?.privateKey)
|
||||
return hasConfiguredNostrPrivateKey(account?.privateKey)
|
||||
? (normalizeOptionalAccountId(account?.defaultAccount) ?? DEFAULT_ACCOUNT_ID)
|
||||
: undefined;
|
||||
},
|
||||
@@ -65,7 +65,7 @@ export function resolveNostrAccount(opts: {
|
||||
|
||||
const baseEnabled = nostrCfg?.enabled !== false;
|
||||
const privateKey = resolveNostrPrivateKey(nostrCfg?.privateKey);
|
||||
const configured = Boolean(privateKey);
|
||||
const configured = hasConfiguredNostrPrivateKey(nostrCfg?.privateKey);
|
||||
|
||||
let publicKey = "";
|
||||
if (privateKey) {
|
||||
|
||||
@@ -32,6 +32,9 @@ describe("secrets configure plan helpers", () => {
|
||||
telegram: {
|
||||
botToken: "token", // pragma: allowlist secret
|
||||
},
|
||||
nostr: {
|
||||
privateKey: "nostr-private-key", // pragma: allowlist secret
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
@@ -39,6 +42,15 @@ describe("secrets configure plan helpers", () => {
|
||||
const paths = candidates.map((entry) => entry.path);
|
||||
expect(paths).toContain(TALK_TEST_PROVIDER_API_KEY_PATH);
|
||||
expect(paths).toContain("channels.telegram.botToken");
|
||||
expect(paths).toContain("channels.nostr.privateKey");
|
||||
expect(resolveConfigSecretTargetByPath(["channels", "nostr", "privateKey"])).toMatchObject({
|
||||
entry: {
|
||||
id: "channels.nostr.privateKey",
|
||||
includeInPlan: true,
|
||||
includeInConfigure: true,
|
||||
includeInAudit: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("collects provider upserts and deletes", () => {
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { createTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
import type { SecretRef } from "../config/types.secrets.js";
|
||||
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
|
||||
import {
|
||||
assertSecretOwnerAvailable,
|
||||
SecretSurfaceUnavailableError,
|
||||
} from "./runtime-degraded-state.js";
|
||||
import { activateSecretsRuntimeSnapshotState } from "./runtime-state.js";
|
||||
import { asConfig, setupSecretsRuntimeSnapshotTestHooks } from "./runtime.test-support.ts";
|
||||
import { writeSecretStoreEntry } from "./store/secret-store.js";
|
||||
|
||||
const { prepareSecretsRuntimeSnapshot } = setupSecretsRuntimeSnapshotTestHooks();
|
||||
const tempDirs = createTempDirTracker();
|
||||
const NOSTR_TEST_PRIVATE_KEY = "1".repeat(64);
|
||||
|
||||
afterEach(() => {
|
||||
closeOpenClawStateDatabaseForTest();
|
||||
tempDirs.cleanup();
|
||||
});
|
||||
|
||||
describe("Nostr SecretRef runtime ownership", () => {
|
||||
it.each(["env", "file", "exec", "store"] as const)(
|
||||
"materializes a valid private key from the %s backend",
|
||||
async (source) => {
|
||||
if (source === "exec" && process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
const root = tempDirs.make("openclaw-nostr-secret-");
|
||||
const env = {
|
||||
OPENCLAW_STATE_DIR: path.join(root, "state"),
|
||||
PATH: process.env.PATH ?? "",
|
||||
NOSTR_ENV_KEY: NOSTR_TEST_PRIVATE_KEY,
|
||||
};
|
||||
let ref: SecretRef;
|
||||
let providers: Record<string, unknown> = {};
|
||||
|
||||
if (source === "file") {
|
||||
const filePath = path.join(root, "secrets.json");
|
||||
await fs.writeFile(filePath, JSON.stringify({ nostr: { key: NOSTR_TEST_PRIVATE_KEY } }), {
|
||||
mode: 0o600,
|
||||
});
|
||||
providers = { vault: { source, path: filePath, mode: "json" } };
|
||||
ref = { source, provider: "vault", id: "/nostr/key" };
|
||||
} else if (source === "exec") {
|
||||
const command = path.join(root, "resolve-secret.sh");
|
||||
const response = JSON.stringify({
|
||||
protocolVersion: 1,
|
||||
values: { "nostr/key": NOSTR_TEST_PRIVATE_KEY },
|
||||
});
|
||||
await fs.writeFile(command, `#!/bin/sh\ncat >/dev/null\nprintf '%s' '${response}'\n`, {
|
||||
mode: 0o700,
|
||||
});
|
||||
providers = { vault: { source, command, jsonOnly: true, passEnv: ["PATH"] } };
|
||||
ref = { source, provider: "vault", id: "nostr/key" };
|
||||
} else if (source === "store") {
|
||||
writeSecretStoreEntry({
|
||||
scope: { kind: "team" },
|
||||
name: "NOSTR_STORE_KEY",
|
||||
value: NOSTR_TEST_PRIVATE_KEY,
|
||||
kind: "secret",
|
||||
updatedBy: "test",
|
||||
database: { env },
|
||||
});
|
||||
ref = { source, provider: "default", id: "NOSTR_STORE_KEY" };
|
||||
} else {
|
||||
ref = { source, provider: "default", id: "NOSTR_ENV_KEY" };
|
||||
}
|
||||
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: { providers },
|
||||
channels: { nostr: { defaultAccount: "Team.A", privateKey: ref } },
|
||||
}),
|
||||
env,
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: new Map([["nostr", "bundled"]]),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.nostr?.privateKey).toBe(NOSTR_TEST_PRIVATE_KEY);
|
||||
expect(snapshot.secretOwners).toEqual([
|
||||
expect.objectContaining({ ownerKind: "account", ownerId: "nostr:team-a" }),
|
||||
]);
|
||||
expect(snapshot.degradedOwners).toEqual([]);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps a missing named account cold while its healthy channel sibling remains available", async () => {
|
||||
const missingRef = { source: "env", provider: "default", id: "MISSING_NOSTR_KEY" } as const;
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
channels: {
|
||||
nostr: { defaultAccount: "Team.A", privateKey: missingRef },
|
||||
telegram: {
|
||||
botToken: { source: "env", provider: "default", id: "HEALTHY_TELEGRAM_TOKEN" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
NOSTR_PRIVATE_KEY: NOSTR_TEST_PRIVATE_KEY,
|
||||
HEALTHY_TELEGRAM_TOKEN: "123:healthy-token",
|
||||
},
|
||||
includeAuthStoreRefs: false,
|
||||
allowUnavailableSecretOwners: true,
|
||||
loadablePluginOrigins: new Map([
|
||||
["nostr", "bundled"],
|
||||
["telegram", "bundled"],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.nostr?.privateKey).toEqual(missingRef);
|
||||
expect(snapshot.config.channels?.telegram?.botToken).toBe("123:healthy-token");
|
||||
expect(snapshot.degradedOwners).toEqual([
|
||||
expect.objectContaining({
|
||||
ownerKind: "account",
|
||||
ownerId: "nostr:team-a",
|
||||
state: "unavailable",
|
||||
degradationState: "cold",
|
||||
paths: ["channels.nostr.privateKey"],
|
||||
}),
|
||||
]);
|
||||
|
||||
activateSecretsRuntimeSnapshotState({
|
||||
snapshot,
|
||||
refreshContext: null,
|
||||
refreshHandler: null,
|
||||
});
|
||||
expect(() => assertSecretOwnerAvailable("account", "nostr:team-a")).toThrow(
|
||||
SecretSurfaceUnavailableError,
|
||||
);
|
||||
expect(() => assertSecretOwnerAvailable("account", "telegram:default")).not.toThrow();
|
||||
});
|
||||
|
||||
it("leaves a disabled exec SecretRef inactive without invoking its provider", async () => {
|
||||
const privateKey = { source: "exec", provider: "vault", id: "nostr/key" } as const;
|
||||
const snapshot = await prepareSecretsRuntimeSnapshot({
|
||||
config: asConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
vault: {
|
||||
source: "exec",
|
||||
command: "/definitely/missing/nostr-secret-provider",
|
||||
jsonOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
channels: { nostr: { enabled: false, privateKey } },
|
||||
}),
|
||||
env: {},
|
||||
includeAuthStoreRefs: false,
|
||||
loadablePluginOrigins: new Map([["nostr", "bundled"]]),
|
||||
});
|
||||
|
||||
expect(snapshot.config.channels?.nostr?.privateKey).toEqual(privateKey);
|
||||
expect(snapshot.degradedOwners).toEqual([]);
|
||||
expect(snapshot.warnings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
|
||||
path: "channels.nostr.privateKey",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user