@actagent/nostr 2026.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/actagent.plugin.json +17 -0
- package/api.ts +11 -0
- package/channel-plugin-api.ts +2 -0
- package/doctor-contract-api.test.ts +105 -0
- package/doctor-contract-api.ts +297 -0
- package/index.ts +96 -0
- package/npm-shrinkwrap.json +137 -0
- package/package.json +67 -0
- package/runtime-api.ts +6 -0
- package/setup-api.ts +2 -0
- package/setup-entry.ts +10 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +12 -0
- package/src/channel.inbound.test.ts +203 -0
- package/src/channel.lifecycle.test.ts +97 -0
- package/src/channel.outbound.test.ts +175 -0
- package/src/channel.setup.ts +161 -0
- package/src/channel.test.ts +527 -0
- package/src/channel.ts +215 -0
- package/src/config-schema.ts +99 -0
- package/src/default-relays.ts +2 -0
- package/src/gateway.ts +338 -0
- package/src/inbound-direct-dm-runtime.ts +2 -0
- package/src/metrics.ts +454 -0
- package/src/nostr-bus.fuzz.test.ts +383 -0
- package/src/nostr-bus.inbound.test.ts +598 -0
- package/src/nostr-bus.integration.test.ts +491 -0
- package/src/nostr-bus.test.ts +256 -0
- package/src/nostr-bus.ts +799 -0
- package/src/nostr-key-utils.ts +93 -0
- package/src/nostr-profile-core.ts +135 -0
- package/src/nostr-profile-http-runtime.ts +7 -0
- package/src/nostr-profile-http.test.ts +632 -0
- package/src/nostr-profile-http.ts +583 -0
- package/src/nostr-profile-import.test.ts +196 -0
- package/src/nostr-profile-import.ts +273 -0
- package/src/nostr-profile-url-safety.ts +22 -0
- package/src/nostr-profile.fuzz.test.ts +431 -0
- package/src/nostr-profile.test.ts +416 -0
- package/src/nostr-profile.ts +144 -0
- package/src/nostr-state-store.test.ts +172 -0
- package/src/nostr-state-store.ts +132 -0
- package/src/runtime.ts +10 -0
- package/src/seen-tracker.ts +291 -0
- package/src/session-route.ts +26 -0
- package/src/setup-adapter.ts +86 -0
- package/src/setup-surface.ts +204 -0
- package/src/test-fixtures.ts +46 -0
- package/src/types.ts +118 -0
- package/test/setup.ts +5 -0
- package/test-api.ts +2 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Nostr plugin module implements setup adapter behavior.
|
|
2
|
+
import type { ChannelSetupAdapter } from "actagent/plugin-sdk/channel-setup";
|
|
3
|
+
import type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID } from "actagent/plugin-sdk/routing";
|
|
5
|
+
import { patchTopLevelChannelConfigSection, splitSetupEntries } from "actagent/plugin-sdk/setup";
|
|
6
|
+
import { uniqueStrings } from "actagent/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
|
|
8
|
+
const channel = "nostr" as const;
|
|
9
|
+
|
|
10
|
+
export function buildNostrSetupPatch(accountId: string, patch: Record<string, unknown>) {
|
|
11
|
+
return {
|
|
12
|
+
...(accountId !== DEFAULT_ACCOUNT_ID ? { defaultAccount: accountId } : {}),
|
|
13
|
+
...patch,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseRelayUrls(raw: string): { relays: string[]; error?: string } {
|
|
18
|
+
const relays: string[] = [];
|
|
19
|
+
for (const entry of splitSetupEntries(raw)) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = new URL(entry);
|
|
22
|
+
if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
|
|
23
|
+
return { relays: [], error: `Relay must use ws:// or wss:// (${entry})` };
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
return { relays: [], error: `Invalid relay URL: ${entry}` };
|
|
27
|
+
}
|
|
28
|
+
relays.push(entry);
|
|
29
|
+
}
|
|
30
|
+
return { relays: uniqueStrings(relays) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createNostrSetupAdapter(params: {
|
|
34
|
+
resolveAccountId: (cfg: ACTAgentConfig, accountId?: string | null) => string;
|
|
35
|
+
validatePrivateKey: (privateKey: string) => boolean;
|
|
36
|
+
}): ChannelSetupAdapter {
|
|
37
|
+
return {
|
|
38
|
+
resolveAccountId: ({ cfg, accountId }) => params.resolveAccountId(cfg, accountId),
|
|
39
|
+
applyAccountName: ({ cfg, accountId, name }) =>
|
|
40
|
+
patchTopLevelChannelConfigSection({
|
|
41
|
+
cfg,
|
|
42
|
+
channel,
|
|
43
|
+
patch: buildNostrSetupPatch(accountId, name?.trim() ? { name: name.trim() } : {}),
|
|
44
|
+
}),
|
|
45
|
+
validateInput: ({ input }) => {
|
|
46
|
+
const typedInput = input as {
|
|
47
|
+
useEnv?: boolean;
|
|
48
|
+
privateKey?: string;
|
|
49
|
+
relayUrls?: string;
|
|
50
|
+
};
|
|
51
|
+
if (!typedInput.useEnv) {
|
|
52
|
+
const privateKey = typedInput.privateKey?.trim();
|
|
53
|
+
if (!privateKey) {
|
|
54
|
+
return "Nostr requires --private-key or --use-env.";
|
|
55
|
+
}
|
|
56
|
+
if (!params.validatePrivateKey(privateKey)) {
|
|
57
|
+
return "Nostr private key must be valid nsec or 64-character hex.";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (typedInput.relayUrls?.trim()) {
|
|
61
|
+
return parseRelayUrls(typedInput.relayUrls).error ?? null;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
},
|
|
65
|
+
applyAccountConfig: ({ cfg, accountId, input }) => {
|
|
66
|
+
const typedInput = input as {
|
|
67
|
+
useEnv?: boolean;
|
|
68
|
+
privateKey?: string;
|
|
69
|
+
relayUrls?: string;
|
|
70
|
+
};
|
|
71
|
+
const relayResult = typedInput.relayUrls?.trim()
|
|
72
|
+
? parseRelayUrls(typedInput.relayUrls)
|
|
73
|
+
: { relays: [] };
|
|
74
|
+
return patchTopLevelChannelConfigSection({
|
|
75
|
+
cfg,
|
|
76
|
+
channel,
|
|
77
|
+
enabled: true,
|
|
78
|
+
clearFields: typedInput.useEnv ? ["privateKey"] : undefined,
|
|
79
|
+
patch: buildNostrSetupPatch(accountId, {
|
|
80
|
+
...(typedInput.useEnv ? {} : { privateKey: typedInput.privateKey?.trim() }),
|
|
81
|
+
...(relayResult.relays.length > 0 ? { relays: relayResult.relays } : {}),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Nostr plugin module implements setup surface behavior.
|
|
2
|
+
import { DEFAULT_ACCOUNT_ID } from "actagent/plugin-sdk/routing";
|
|
3
|
+
// Nostr plugin module implements setup surface behavior.
|
|
4
|
+
import {
|
|
5
|
+
hasConfiguredSecretInput,
|
|
6
|
+
normalizeSecretInputString,
|
|
7
|
+
} from "actagent/plugin-sdk/secret-input";
|
|
8
|
+
import type { ChannelSetupDmPolicy, ChannelSetupWizard, DmPolicy } from "actagent/plugin-sdk/setup";
|
|
9
|
+
import {
|
|
10
|
+
createSetupTranslator,
|
|
11
|
+
createStandardChannelSetupStatus,
|
|
12
|
+
createTopLevelChannelDmPolicy,
|
|
13
|
+
createTopLevelChannelParsedAllowFromPrompt,
|
|
14
|
+
formatDocsLink,
|
|
15
|
+
mergeAllowFromEntries,
|
|
16
|
+
parseSetupEntriesWithParser,
|
|
17
|
+
patchTopLevelChannelConfigSection,
|
|
18
|
+
} from "actagent/plugin-sdk/setup";
|
|
19
|
+
import { DEFAULT_RELAYS } from "./default-relays.js";
|
|
20
|
+
import { getPublicKeyFromPrivate, normalizePubkey } from "./nostr-key-utils.js";
|
|
21
|
+
import { buildNostrSetupPatch, createNostrSetupAdapter, parseRelayUrls } from "./setup-adapter.js";
|
|
22
|
+
import { resolveDefaultNostrAccountId, resolveNostrAccount } from "./types.js";
|
|
23
|
+
|
|
24
|
+
const t = createSetupTranslator();
|
|
25
|
+
|
|
26
|
+
const channel = "nostr" as const;
|
|
27
|
+
const NOSTR_SETUP_HELP_LINES = [
|
|
28
|
+
t("wizard.nostr.helpPrivateKeyFormat"),
|
|
29
|
+
t("wizard.nostr.helpRelaysOptional"),
|
|
30
|
+
t("wizard.nostr.helpEnvVars"),
|
|
31
|
+
`Docs: ${formatDocsLink("/channels/nostr", "channels/nostr")}`,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const NOSTR_ALLOW_FROM_HELP_LINES = [
|
|
35
|
+
t("wizard.nostr.allowlistIntro"),
|
|
36
|
+
t("wizard.nostr.examples"),
|
|
37
|
+
"- npub1...",
|
|
38
|
+
"- nostr:npub1...",
|
|
39
|
+
"- 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
|
40
|
+
t("wizard.nostr.multipleEntries"),
|
|
41
|
+
`Docs: ${formatDocsLink("/channels/nostr", "channels/nostr")}`,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function parseNostrAllowFrom(raw: string): { entries: string[]; error?: string } {
|
|
45
|
+
return parseSetupEntriesWithParser(raw, (entry) => {
|
|
46
|
+
const cleaned = entry.replace(/^nostr:/i, "").trim();
|
|
47
|
+
try {
|
|
48
|
+
return { value: normalizePubkey(cleaned) };
|
|
49
|
+
} catch {
|
|
50
|
+
return { error: `Invalid Nostr pubkey: ${entry}` };
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const promptNostrAllowFrom = createTopLevelChannelParsedAllowFromPrompt({
|
|
56
|
+
channel,
|
|
57
|
+
defaultAccountId: resolveDefaultNostrAccountId,
|
|
58
|
+
noteTitle: t("wizard.nostr.allowlistTitle"),
|
|
59
|
+
noteLines: NOSTR_ALLOW_FROM_HELP_LINES,
|
|
60
|
+
message: t("wizard.nostr.allowFromPrompt"),
|
|
61
|
+
placeholder: "npub1..., 0123abcd...",
|
|
62
|
+
parseEntries: parseNostrAllowFrom,
|
|
63
|
+
mergeEntries: ({ existing, parsed }) => mergeAllowFromEntries(existing, parsed),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const nostrDmPolicy: ChannelSetupDmPolicy = createTopLevelChannelDmPolicy({
|
|
67
|
+
label: "Nostr",
|
|
68
|
+
channel,
|
|
69
|
+
policyKey: "channels.nostr.dmPolicy",
|
|
70
|
+
allowFromKey: "channels.nostr.allowFrom",
|
|
71
|
+
getCurrent: (cfg) => (cfg.channels?.nostr?.dmPolicy as DmPolicy | undefined) ?? "pairing",
|
|
72
|
+
promptAllowFrom: promptNostrAllowFrom,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const nostrSetupAdapter = createNostrSetupAdapter({
|
|
76
|
+
resolveAccountId: (cfg, accountId) => accountId?.trim() || resolveDefaultNostrAccountId(cfg),
|
|
77
|
+
validatePrivateKey: (privateKey) => {
|
|
78
|
+
try {
|
|
79
|
+
getPublicKeyFromPrivate(privateKey);
|
|
80
|
+
return true;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export const nostrSetupWizard: ChannelSetupWizard = {
|
|
88
|
+
channel,
|
|
89
|
+
resolveAccountIdForConfigure: ({ accountOverride, defaultAccountId }) =>
|
|
90
|
+
accountOverride?.trim() || defaultAccountId,
|
|
91
|
+
resolveShouldPromptAccountIds: () => false,
|
|
92
|
+
status: createStandardChannelSetupStatus({
|
|
93
|
+
channelLabel: "Nostr",
|
|
94
|
+
configuredLabel: t("wizard.channels.statusConfigured"),
|
|
95
|
+
unconfiguredLabel: t("wizard.channels.statusNeedsPrivateKey"),
|
|
96
|
+
configuredHint: t("wizard.channels.statusConfigured"),
|
|
97
|
+
unconfiguredHint: t("wizard.channels.statusNeedsPrivateKey"),
|
|
98
|
+
configuredScore: 1,
|
|
99
|
+
unconfiguredScore: 0,
|
|
100
|
+
includeStatusLine: true,
|
|
101
|
+
resolveConfigured: ({ cfg }) => resolveNostrAccount({ cfg }).configured,
|
|
102
|
+
resolveExtraStatusLines: ({ cfg }) => {
|
|
103
|
+
const account = resolveNostrAccount({ cfg });
|
|
104
|
+
return [`Relays: ${account.relays.length || DEFAULT_RELAYS.length}`];
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
introNote: {
|
|
108
|
+
title: t("wizard.nostr.setupTitle"),
|
|
109
|
+
lines: NOSTR_SETUP_HELP_LINES,
|
|
110
|
+
},
|
|
111
|
+
envShortcut: {
|
|
112
|
+
prompt: t("wizard.nostr.privateKeyEnvPrompt"),
|
|
113
|
+
preferredEnvVar: "NOSTR_PRIVATE_KEY",
|
|
114
|
+
isAvailable: ({ cfg, accountId }) =>
|
|
115
|
+
accountId === DEFAULT_ACCOUNT_ID &&
|
|
116
|
+
Boolean(process.env.NOSTR_PRIVATE_KEY?.trim()) &&
|
|
117
|
+
!hasConfiguredSecretInput(resolveNostrAccount({ cfg, accountId }).config.privateKey),
|
|
118
|
+
apply: async ({ cfg, accountId }) =>
|
|
119
|
+
patchTopLevelChannelConfigSection({
|
|
120
|
+
cfg,
|
|
121
|
+
channel,
|
|
122
|
+
enabled: true,
|
|
123
|
+
clearFields: ["privateKey"],
|
|
124
|
+
patch: buildNostrSetupPatch(accountId, {}),
|
|
125
|
+
}),
|
|
126
|
+
},
|
|
127
|
+
credentials: [
|
|
128
|
+
{
|
|
129
|
+
inputKey: "privateKey",
|
|
130
|
+
providerHint: channel,
|
|
131
|
+
credentialLabel: "private key",
|
|
132
|
+
preferredEnvVar: "NOSTR_PRIVATE_KEY",
|
|
133
|
+
helpTitle: t("wizard.nostr.privateKeyTitle"),
|
|
134
|
+
helpLines: NOSTR_SETUP_HELP_LINES,
|
|
135
|
+
envPrompt: t("wizard.nostr.privateKeyEnvPrompt"),
|
|
136
|
+
keepPrompt: t("wizard.nostr.privateKeyKeep"),
|
|
137
|
+
inputPrompt: t("wizard.nostr.privateKeyInput"),
|
|
138
|
+
allowEnv: ({ accountId }) => accountId === DEFAULT_ACCOUNT_ID,
|
|
139
|
+
inspect: ({ cfg, accountId }) => {
|
|
140
|
+
const account = resolveNostrAccount({ cfg, accountId });
|
|
141
|
+
return {
|
|
142
|
+
accountConfigured: account.configured,
|
|
143
|
+
hasConfiguredValue: hasConfiguredSecretInput(account.config.privateKey),
|
|
144
|
+
resolvedValue: normalizeSecretInputString(account.config.privateKey),
|
|
145
|
+
envValue: process.env.NOSTR_PRIVATE_KEY?.trim(),
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
applyUseEnv: async ({ cfg, accountId }) =>
|
|
149
|
+
patchTopLevelChannelConfigSection({
|
|
150
|
+
cfg,
|
|
151
|
+
channel,
|
|
152
|
+
enabled: true,
|
|
153
|
+
clearFields: ["privateKey"],
|
|
154
|
+
patch: buildNostrSetupPatch(accountId, {}),
|
|
155
|
+
}),
|
|
156
|
+
applySet: async ({ cfg, accountId, resolvedValue }) =>
|
|
157
|
+
patchTopLevelChannelConfigSection({
|
|
158
|
+
cfg,
|
|
159
|
+
channel,
|
|
160
|
+
enabled: true,
|
|
161
|
+
patch: buildNostrSetupPatch(accountId, { privateKey: resolvedValue }),
|
|
162
|
+
}),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
textInputs: [
|
|
166
|
+
{
|
|
167
|
+
inputKey: "relayUrls",
|
|
168
|
+
message: t("wizard.nostr.relayUrlsPrompt"),
|
|
169
|
+
placeholder: DEFAULT_RELAYS.join(", "),
|
|
170
|
+
required: false,
|
|
171
|
+
applyEmptyValue: true,
|
|
172
|
+
helpTitle: t("wizard.nostr.relaysTitle"),
|
|
173
|
+
helpLines: [t("wizard.nostr.relaysWsOnly"), t("wizard.nostr.helpRelaysOptional")],
|
|
174
|
+
currentValue: ({ cfg, accountId }) => {
|
|
175
|
+
const account = resolveNostrAccount({ cfg, accountId });
|
|
176
|
+
const configuredRelays = cfg.channels?.nostr?.relays as string[] | undefined;
|
|
177
|
+
const relays = configuredRelays && configuredRelays.length > 0 ? account.relays : [];
|
|
178
|
+
return relays.join(", ");
|
|
179
|
+
},
|
|
180
|
+
keepPrompt: (value) => t("wizard.nostr.relayUrlsKeep", { value }),
|
|
181
|
+
validate: ({ value }) => parseRelayUrls(value).error,
|
|
182
|
+
applySet: async ({ cfg, accountId, value }) => {
|
|
183
|
+
const relayResult = parseRelayUrls(value);
|
|
184
|
+
return patchTopLevelChannelConfigSection({
|
|
185
|
+
cfg,
|
|
186
|
+
channel,
|
|
187
|
+
enabled: true,
|
|
188
|
+
clearFields: relayResult.relays.length > 0 ? undefined : ["relays"],
|
|
189
|
+
patch: buildNostrSetupPatch(
|
|
190
|
+
accountId,
|
|
191
|
+
relayResult.relays.length > 0 ? { relays: relayResult.relays } : {},
|
|
192
|
+
),
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
dmPolicy: nostrDmPolicy,
|
|
198
|
+
disable: (cfg) =>
|
|
199
|
+
patchTopLevelChannelConfigSection({
|
|
200
|
+
cfg,
|
|
201
|
+
channel,
|
|
202
|
+
patch: { enabled: false },
|
|
203
|
+
}),
|
|
204
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Nostr plugin module implements test fixtures behavior.
|
|
2
|
+
import type { ResolvedNostrAccount } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const TEST_HEX_PRIVATE_KEY =
|
|
5
|
+
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
|
6
|
+
|
|
7
|
+
export const TEST_HEX_PUBLIC_KEY =
|
|
8
|
+
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
|
|
9
|
+
|
|
10
|
+
export const TEST_NSEC = "nsec1qypqxpq9qtpqscx7peytzfwtdjmcv0mrz5rjpej8vjppfkqfqy8skqfv3l";
|
|
11
|
+
|
|
12
|
+
export const TEST_RELAY_URL = "wss://relay.example.com";
|
|
13
|
+
export const TEST_SETUP_RELAY_URLS = ["wss://relay.damus.io", "wss://relay.primal.net"];
|
|
14
|
+
export const TEST_RESOLVED_PRIVATE_KEY = "resolved-nostr-private-key";
|
|
15
|
+
|
|
16
|
+
export const TEST_HEX_PRIVATE_KEY_BYTES = new Uint8Array(
|
|
17
|
+
TEST_HEX_PRIVATE_KEY.match(/.{2}/g)!.map((byte) => Number.parseInt(byte, 16)),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export function createConfiguredNostrCfg(overrides: Record<string, unknown> = {}): {
|
|
21
|
+
channels: { nostr: Record<string, unknown> };
|
|
22
|
+
} {
|
|
23
|
+
return {
|
|
24
|
+
channels: {
|
|
25
|
+
nostr: {
|
|
26
|
+
privateKey: TEST_HEX_PRIVATE_KEY,
|
|
27
|
+
...overrides,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildResolvedNostrAccount(
|
|
34
|
+
overrides: Partial<ResolvedNostrAccount> = {},
|
|
35
|
+
): ResolvedNostrAccount {
|
|
36
|
+
return {
|
|
37
|
+
accountId: "default",
|
|
38
|
+
enabled: true,
|
|
39
|
+
configured: true,
|
|
40
|
+
privateKey: TEST_HEX_PRIVATE_KEY,
|
|
41
|
+
publicKey: TEST_HEX_PUBLIC_KEY,
|
|
42
|
+
relays: [TEST_RELAY_URL],
|
|
43
|
+
config: {},
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Nostr type declarations define plugin contracts.
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_ACCOUNT_ID,
|
|
4
|
+
normalizeAccountId,
|
|
5
|
+
normalizeOptionalAccountId,
|
|
6
|
+
} from "actagent/plugin-sdk/account-id";
|
|
7
|
+
import {
|
|
8
|
+
listCombinedAccountIds,
|
|
9
|
+
resolveListedDefaultAccountId,
|
|
10
|
+
} from "actagent/plugin-sdk/account-resolution";
|
|
11
|
+
import type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
|
|
12
|
+
import { normalizeSecretInputString, type SecretInput } from "actagent/plugin-sdk/secret-input";
|
|
13
|
+
import { normalizeOptionalString } from "actagent/plugin-sdk/string-coerce-runtime";
|
|
14
|
+
import type { NostrProfile } from "./config-schema.js";
|
|
15
|
+
import { DEFAULT_RELAYS } from "./default-relays.js";
|
|
16
|
+
import { getPublicKeyFromPrivate } from "./nostr-key-utils.js";
|
|
17
|
+
|
|
18
|
+
interface NostrAccountConfig {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
name?: string;
|
|
21
|
+
defaultAccount?: string;
|
|
22
|
+
privateKey?: SecretInput;
|
|
23
|
+
relays?: string[];
|
|
24
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
25
|
+
allowFrom?: Array<string | number>;
|
|
26
|
+
profile?: NostrProfile;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ResolvedNostrAccount {
|
|
30
|
+
accountId: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
configured: boolean;
|
|
34
|
+
privateKey: string;
|
|
35
|
+
publicKey: string;
|
|
36
|
+
relays: string[];
|
|
37
|
+
profile?: NostrProfile;
|
|
38
|
+
config: NostrAccountConfig;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveConfiguredDefaultNostrAccountId(cfg: ACTAgentConfig): string | undefined {
|
|
42
|
+
const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
|
43
|
+
| NostrAccountConfig
|
|
44
|
+
| undefined;
|
|
45
|
+
return normalizeOptionalAccountId(nostrCfg?.defaultAccount);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List all configured Nostr account IDs
|
|
50
|
+
*/
|
|
51
|
+
export function listNostrAccountIds(cfg: ACTAgentConfig): string[] {
|
|
52
|
+
const nostrCfg = (cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
|
53
|
+
| NostrAccountConfig
|
|
54
|
+
| undefined;
|
|
55
|
+
const privateKey = normalizeSecretInputString(nostrCfg?.privateKey);
|
|
56
|
+
return listCombinedAccountIds({
|
|
57
|
+
configuredAccountIds: [],
|
|
58
|
+
implicitAccountId: privateKey
|
|
59
|
+
? (resolveConfiguredDefaultNostrAccountId(cfg) ?? DEFAULT_ACCOUNT_ID)
|
|
60
|
+
: undefined,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the default account ID
|
|
66
|
+
*/
|
|
67
|
+
export function resolveDefaultNostrAccountId(cfg: ACTAgentConfig): string {
|
|
68
|
+
return resolveListedDefaultAccountId({
|
|
69
|
+
accountIds: listNostrAccountIds(cfg),
|
|
70
|
+
configuredDefaultAccountId: resolveConfiguredDefaultNostrAccountId(cfg),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a Nostr account from config
|
|
76
|
+
*/
|
|
77
|
+
export function resolveNostrAccount(opts: {
|
|
78
|
+
cfg: ACTAgentConfig;
|
|
79
|
+
accountId?: string | null;
|
|
80
|
+
}): ResolvedNostrAccount {
|
|
81
|
+
const accountId = normalizeAccountId(opts.accountId ?? resolveDefaultNostrAccountId(opts.cfg));
|
|
82
|
+
const nostrCfg = (opts.cfg.channels as Record<string, unknown> | undefined)?.nostr as
|
|
83
|
+
| NostrAccountConfig
|
|
84
|
+
| undefined;
|
|
85
|
+
|
|
86
|
+
const baseEnabled = nostrCfg?.enabled !== false;
|
|
87
|
+
const privateKey = normalizeSecretInputString(nostrCfg?.privateKey) ?? "";
|
|
88
|
+
const configured = Boolean(privateKey);
|
|
89
|
+
|
|
90
|
+
let publicKey = "";
|
|
91
|
+
if (privateKey) {
|
|
92
|
+
try {
|
|
93
|
+
publicKey = getPublicKeyFromPrivate(privateKey);
|
|
94
|
+
} catch {
|
|
95
|
+
// Invalid key - leave publicKey empty, configured will indicate issues
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
accountId,
|
|
101
|
+
name: normalizeOptionalString(nostrCfg?.name),
|
|
102
|
+
enabled: baseEnabled,
|
|
103
|
+
configured,
|
|
104
|
+
privateKey,
|
|
105
|
+
publicKey,
|
|
106
|
+
relays: nostrCfg?.relays ?? DEFAULT_RELAYS,
|
|
107
|
+
profile: nostrCfg?.profile,
|
|
108
|
+
config: {
|
|
109
|
+
enabled: nostrCfg?.enabled,
|
|
110
|
+
name: nostrCfg?.name,
|
|
111
|
+
privateKey: nostrCfg?.privateKey,
|
|
112
|
+
relays: nostrCfg?.relays,
|
|
113
|
+
dmPolicy: nostrCfg?.dmPolicy,
|
|
114
|
+
allowFrom: nostrCfg?.allowFrom,
|
|
115
|
+
profile: nostrCfg?.profile,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
package/test/setup.ts
ADDED
package/test-api.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "."
|
|
5
|
+
},
|
|
6
|
+
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
+
"exclude": [
|
|
8
|
+
"./**/*.test.ts",
|
|
9
|
+
"./dist/**",
|
|
10
|
+
"./node_modules/**",
|
|
11
|
+
"./src/test-support/**",
|
|
12
|
+
"./src/**/*test-helpers.ts",
|
|
13
|
+
"./src/**/*test-harness.ts",
|
|
14
|
+
"./src/**/*test-support.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|