@openclaw/nostr 2026.3.12 → 2026.5.1-beta.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 +6 -0
- package/api.ts +10 -0
- package/channel-plugin-api.ts +1 -0
- package/index.ts +60 -36
- package/openclaw.plugin.json +190 -1
- package/package.json +41 -9
- package/runtime-api.ts +6 -0
- package/setup-api.ts +1 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/src/channel-api.ts +15 -0
- package/src/channel.inbound.test.ts +176 -0
- package/src/channel.outbound.test.ts +89 -49
- package/src/channel.setup.ts +231 -0
- package/src/channel.test.ts +439 -71
- package/src/channel.ts +146 -284
- package/src/config-schema.ts +18 -12
- package/src/default-relays.ts +1 -0
- package/src/gateway.ts +302 -0
- package/src/inbound-direct-dm-runtime.ts +1 -0
- package/src/metrics.ts +6 -6
- package/src/nostr-bus.fuzz.test.ts +74 -247
- package/src/nostr-bus.inbound.test.ts +526 -0
- package/src/nostr-bus.integration.test.ts +88 -64
- package/src/nostr-bus.test.ts +22 -31
- package/src/nostr-bus.ts +206 -136
- package/src/nostr-key-utils.ts +94 -0
- package/src/nostr-profile-core.ts +134 -0
- package/src/nostr-profile-http-runtime.ts +6 -0
- package/src/nostr-profile-http.test.ts +310 -192
- package/src/nostr-profile-http.ts +51 -36
- package/src/nostr-profile-import.ts +3 -3
- package/src/nostr-profile-url-safety.ts +21 -0
- package/src/nostr-profile.fuzz.test.ts +7 -57
- package/src/nostr-profile.test.ts +16 -14
- package/src/nostr-profile.ts +13 -146
- package/src/nostr-state-store.test.ts +106 -2
- package/src/nostr-state-store.ts +46 -49
- package/src/runtime.ts +6 -3
- package/src/seen-tracker.ts +1 -1
- package/src/session-route.ts +25 -0
- package/src/setup-surface.ts +265 -0
- package/src/test-fixtures.ts +45 -0
- package/src/types.ts +26 -25
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -110
- package/src/types.test.ts +0 -175
package/src/channel.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
+
import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
|
|
2
|
+
import {
|
|
3
|
+
createScopedDmSecurityResolver,
|
|
4
|
+
createTopLevelChannelConfigAdapter,
|
|
5
|
+
} from "openclaw/plugin-sdk/channel-config-helpers";
|
|
6
|
+
import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
|
|
7
|
+
import {
|
|
8
|
+
buildPassiveChannelStatusSummary,
|
|
9
|
+
buildTrafficStatusSummary,
|
|
10
|
+
} from "openclaw/plugin-sdk/extension-shared";
|
|
11
|
+
import { createComputedAccountStatusAdapter } from "openclaw/plugin-sdk/status-helpers";
|
|
1
12
|
import {
|
|
2
13
|
buildChannelConfigSchema,
|
|
3
14
|
collectStatusIssuesFromLastError,
|
|
4
15
|
createDefaultChannelRuntimeState,
|
|
5
16
|
DEFAULT_ACCOUNT_ID,
|
|
6
17
|
formatPairingApproveHint,
|
|
7
|
-
mapAllowFromEntries,
|
|
8
18
|
type ChannelPlugin,
|
|
9
|
-
} from "
|
|
19
|
+
} from "./channel-api.js";
|
|
10
20
|
import type { NostrProfile } from "./config-schema.js";
|
|
11
21
|
import { NostrConfigSchema } from "./config-schema.js";
|
|
12
|
-
import
|
|
13
|
-
|
|
22
|
+
import {
|
|
23
|
+
getActiveNostrBuses,
|
|
24
|
+
nostrOutboundAdapter,
|
|
25
|
+
nostrPairingTextAdapter,
|
|
26
|
+
startNostrGatewayAccount,
|
|
27
|
+
} from "./gateway.js";
|
|
28
|
+
import { normalizePubkey } from "./nostr-key-utils.js";
|
|
14
29
|
import type { ProfilePublishResult } from "./nostr-profile.js";
|
|
15
|
-
import {
|
|
30
|
+
import { resolveNostrOutboundSessionRoute } from "./session-route.js";
|
|
31
|
+
import { nostrSetupAdapter, nostrSetupWizard } from "./setup-surface.js";
|
|
16
32
|
import {
|
|
17
33
|
listNostrAccountIds,
|
|
18
34
|
resolveDefaultNostrAccountId,
|
|
@@ -20,293 +36,139 @@ import {
|
|
|
20
36
|
type ResolvedNostrAccount,
|
|
21
37
|
} from "./types.js";
|
|
22
38
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
docsLabel: "nostr",
|
|
37
|
-
blurb: "Decentralized DMs via Nostr relays (NIP-04)",
|
|
38
|
-
order: 100,
|
|
39
|
-
},
|
|
40
|
-
capabilities: {
|
|
41
|
-
chatTypes: ["direct"], // DMs only for MVP
|
|
42
|
-
media: false, // No media for MVP
|
|
43
|
-
},
|
|
44
|
-
reload: { configPrefixes: ["channels.nostr"] },
|
|
45
|
-
configSchema: buildChannelConfigSchema(NostrConfigSchema),
|
|
46
|
-
|
|
47
|
-
config: {
|
|
48
|
-
listAccountIds: (cfg) => listNostrAccountIds(cfg),
|
|
49
|
-
resolveAccount: (cfg, accountId) => resolveNostrAccount({ cfg, accountId }),
|
|
50
|
-
defaultAccountId: (cfg) => resolveDefaultNostrAccountId(cfg),
|
|
51
|
-
isConfigured: (account) => account.configured,
|
|
52
|
-
describeAccount: (account) => ({
|
|
53
|
-
accountId: account.accountId,
|
|
54
|
-
name: account.name,
|
|
55
|
-
enabled: account.enabled,
|
|
56
|
-
configured: account.configured,
|
|
57
|
-
publicKey: account.publicKey,
|
|
58
|
-
}),
|
|
59
|
-
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
60
|
-
mapAllowFromEntries(resolveNostrAccount({ cfg, accountId }).config.allowFrom),
|
|
61
|
-
formatAllowFrom: ({ allowFrom }) =>
|
|
62
|
-
allowFrom
|
|
63
|
-
.map((entry) => String(entry).trim())
|
|
64
|
-
.filter(Boolean)
|
|
65
|
-
.map((entry) => {
|
|
66
|
-
if (entry === "*") {
|
|
67
|
-
return "*";
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
return normalizePubkey(entry);
|
|
71
|
-
} catch {
|
|
72
|
-
return entry; // Keep as-is if normalization fails
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
.filter(Boolean),
|
|
39
|
+
const resolveNostrDmPolicy = createScopedDmSecurityResolver<ResolvedNostrAccount>({
|
|
40
|
+
channelKey: "nostr",
|
|
41
|
+
resolvePolicy: (account) => account.config.dmPolicy,
|
|
42
|
+
resolveAllowFrom: (account) => account.config.allowFrom,
|
|
43
|
+
policyPathSuffix: "dmPolicy",
|
|
44
|
+
defaultPolicy: "pairing",
|
|
45
|
+
approveHint: formatPairingApproveHint("nostr"),
|
|
46
|
+
normalizeEntry: (raw) => {
|
|
47
|
+
try {
|
|
48
|
+
return normalizePubkey(raw.trim().replace(/^nostr:/i, ""));
|
|
49
|
+
} catch {
|
|
50
|
+
return raw.trim();
|
|
51
|
+
}
|
|
76
52
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const nostrConfigAdapter = createTopLevelChannelConfigAdapter<ResolvedNostrAccount>({
|
|
56
|
+
sectionKey: "nostr",
|
|
57
|
+
resolveAccount: (cfg) => resolveNostrAccount({ cfg }),
|
|
58
|
+
listAccountIds: listNostrAccountIds,
|
|
59
|
+
defaultAccountId: resolveDefaultNostrAccountId,
|
|
60
|
+
deleteMode: "clear-fields",
|
|
61
|
+
clearBaseFields: [
|
|
62
|
+
"name",
|
|
63
|
+
"defaultAccount",
|
|
64
|
+
"privateKey",
|
|
65
|
+
"relays",
|
|
66
|
+
"dmPolicy",
|
|
67
|
+
"allowFrom",
|
|
68
|
+
"profile",
|
|
69
|
+
],
|
|
70
|
+
resolveAllowFrom: (account) => account.config.allowFrom,
|
|
71
|
+
formatAllowFrom: (allowFrom) =>
|
|
72
|
+
allowFrom
|
|
73
|
+
.map((entry) => String(entry).trim())
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.map((entry) => {
|
|
76
|
+
if (entry === "*") {
|
|
77
|
+
return "*";
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return normalizePubkey(entry);
|
|
81
|
+
} catch {
|
|
82
|
+
return entry;
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
.filter(Boolean),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = createChatChannelPlugin({
|
|
89
|
+
base: {
|
|
90
|
+
id: "nostr",
|
|
91
|
+
meta: {
|
|
92
|
+
id: "nostr",
|
|
93
|
+
label: "Nostr",
|
|
94
|
+
selectionLabel: "Nostr",
|
|
95
|
+
docsPath: "/channels/nostr",
|
|
96
|
+
docsLabel: "nostr",
|
|
97
|
+
blurb: "Decentralized DMs via Nostr relays (NIP-04)",
|
|
98
|
+
order: 100,
|
|
93
99
|
},
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
resolveDmPolicy: ({ account }) => {
|
|
98
|
-
return {
|
|
99
|
-
policy: account.config.dmPolicy ?? "pairing",
|
|
100
|
-
allowFrom: account.config.allowFrom ?? [],
|
|
101
|
-
policyPath: "channels.nostr.dmPolicy",
|
|
102
|
-
allowFromPath: "channels.nostr.allowFrom",
|
|
103
|
-
approveHint: formatPairingApproveHint("nostr"),
|
|
104
|
-
normalizeEntry: (raw) => {
|
|
105
|
-
try {
|
|
106
|
-
return normalizePubkey(raw.replace(/^nostr:/i, "").trim());
|
|
107
|
-
} catch {
|
|
108
|
-
return raw.trim();
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
};
|
|
100
|
+
capabilities: {
|
|
101
|
+
chatTypes: ["direct"], // DMs only for MVP
|
|
102
|
+
media: false, // No media for MVP
|
|
112
103
|
},
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
104
|
+
reload: { configPrefixes: ["channels.nostr"] },
|
|
105
|
+
configSchema: buildChannelConfigSchema(NostrConfigSchema),
|
|
106
|
+
setup: nostrSetupAdapter,
|
|
107
|
+
setupWizard: nostrSetupWizard,
|
|
108
|
+
config: {
|
|
109
|
+
...nostrConfigAdapter,
|
|
110
|
+
isConfigured: (account) => account.configured,
|
|
111
|
+
describeAccount: (account) =>
|
|
112
|
+
describeAccountSnapshot({
|
|
113
|
+
account,
|
|
114
|
+
configured: account.configured,
|
|
115
|
+
extra: {
|
|
116
|
+
publicKey: account.publicKey,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
124
119
|
},
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
120
|
+
messaging: {
|
|
121
|
+
normalizeTarget: (target) => {
|
|
122
|
+
// Strip nostr: prefix if present
|
|
123
|
+
const cleaned = target.trim().replace(/^nostr:/i, "");
|
|
124
|
+
try {
|
|
125
|
+
return normalizePubkey(cleaned);
|
|
126
|
+
} catch {
|
|
127
|
+
return cleaned;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
targetResolver: {
|
|
131
|
+
looksLikeId: (input) => {
|
|
132
|
+
const trimmed = input.trim();
|
|
133
|
+
return trimmed.startsWith("npub1") || /^[0-9a-fA-F]{64}$/.test(trimmed);
|
|
134
|
+
},
|
|
135
|
+
hint: "<npub|hex pubkey|nostr:npub...>",
|
|
129
136
|
},
|
|
130
|
-
|
|
137
|
+
resolveOutboundSessionRoute: (params) => resolveNostrOutboundSessionRoute(params),
|
|
131
138
|
},
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
messageId: `nostr-${Date.now()}`,
|
|
156
|
-
};
|
|
139
|
+
status: {
|
|
140
|
+
...createComputedAccountStatusAdapter<ResolvedNostrAccount>({
|
|
141
|
+
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
142
|
+
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts),
|
|
143
|
+
buildChannelSummary: ({ snapshot }) =>
|
|
144
|
+
buildPassiveChannelStatusSummary(snapshot, {
|
|
145
|
+
publicKey: snapshot.publicKey ?? null,
|
|
146
|
+
}),
|
|
147
|
+
resolveAccountSnapshot: ({ account, runtime }) => ({
|
|
148
|
+
accountId: account.accountId,
|
|
149
|
+
name: account.name,
|
|
150
|
+
enabled: account.enabled,
|
|
151
|
+
configured: account.configured,
|
|
152
|
+
extra: {
|
|
153
|
+
publicKey: account.publicKey,
|
|
154
|
+
profile: account.profile,
|
|
155
|
+
...buildTrafficStatusSummary(runtime),
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
}),
|
|
159
|
+
},
|
|
160
|
+
gateway: {
|
|
161
|
+
startAccount: startNostrGatewayAccount,
|
|
157
162
|
},
|
|
158
163
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID),
|
|
162
|
-
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("nostr", accounts),
|
|
163
|
-
buildChannelSummary: ({ snapshot }) => ({
|
|
164
|
-
configured: snapshot.configured ?? false,
|
|
165
|
-
publicKey: snapshot.publicKey ?? null,
|
|
166
|
-
running: snapshot.running ?? false,
|
|
167
|
-
lastStartAt: snapshot.lastStartAt ?? null,
|
|
168
|
-
lastStopAt: snapshot.lastStopAt ?? null,
|
|
169
|
-
lastError: snapshot.lastError ?? null,
|
|
170
|
-
}),
|
|
171
|
-
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
172
|
-
accountId: account.accountId,
|
|
173
|
-
name: account.name,
|
|
174
|
-
enabled: account.enabled,
|
|
175
|
-
configured: account.configured,
|
|
176
|
-
publicKey: account.publicKey,
|
|
177
|
-
profile: account.profile,
|
|
178
|
-
running: runtime?.running ?? false,
|
|
179
|
-
lastStartAt: runtime?.lastStartAt ?? null,
|
|
180
|
-
lastStopAt: runtime?.lastStopAt ?? null,
|
|
181
|
-
lastError: runtime?.lastError ?? null,
|
|
182
|
-
lastInboundAt: runtime?.lastInboundAt ?? null,
|
|
183
|
-
lastOutboundAt: runtime?.lastOutboundAt ?? null,
|
|
184
|
-
}),
|
|
164
|
+
pairing: {
|
|
165
|
+
text: nostrPairingTextAdapter,
|
|
185
166
|
},
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
startAccount: async (ctx) => {
|
|
189
|
-
const account = ctx.account;
|
|
190
|
-
ctx.setStatus({
|
|
191
|
-
accountId: account.accountId,
|
|
192
|
-
publicKey: account.publicKey,
|
|
193
|
-
});
|
|
194
|
-
ctx.log?.info(
|
|
195
|
-
`[${account.accountId}] starting Nostr provider (pubkey: ${account.publicKey})`,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
if (!account.configured) {
|
|
199
|
-
throw new Error("Nostr private key not configured");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const runtime = getNostrRuntime();
|
|
203
|
-
|
|
204
|
-
// Track bus handle for metrics callback
|
|
205
|
-
let busHandle: NostrBusHandle | null = null;
|
|
206
|
-
|
|
207
|
-
const bus = await startNostrBus({
|
|
208
|
-
accountId: account.accountId,
|
|
209
|
-
privateKey: account.privateKey,
|
|
210
|
-
relays: account.relays,
|
|
211
|
-
onMessage: async (senderPubkey, text, reply) => {
|
|
212
|
-
ctx.log?.debug?.(
|
|
213
|
-
`[${account.accountId}] DM from ${senderPubkey}: ${text.slice(0, 50)}...`,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
// Forward to OpenClaw's message pipeline
|
|
217
|
-
await (
|
|
218
|
-
runtime.channel.reply as { handleInboundMessage?: (params: unknown) => Promise<void> }
|
|
219
|
-
).handleInboundMessage?.({
|
|
220
|
-
channel: "nostr",
|
|
221
|
-
accountId: account.accountId,
|
|
222
|
-
senderId: senderPubkey,
|
|
223
|
-
chatType: "direct",
|
|
224
|
-
chatId: senderPubkey, // For DMs, chatId is the sender's pubkey
|
|
225
|
-
text,
|
|
226
|
-
reply: async (responseText: string) => {
|
|
227
|
-
await reply(responseText);
|
|
228
|
-
},
|
|
229
|
-
});
|
|
230
|
-
},
|
|
231
|
-
onError: (error, context) => {
|
|
232
|
-
ctx.log?.error?.(`[${account.accountId}] Nostr error (${context}): ${error.message}`);
|
|
233
|
-
},
|
|
234
|
-
onConnect: (relay) => {
|
|
235
|
-
ctx.log?.debug?.(`[${account.accountId}] Connected to relay: ${relay}`);
|
|
236
|
-
},
|
|
237
|
-
onDisconnect: (relay) => {
|
|
238
|
-
ctx.log?.debug?.(`[${account.accountId}] Disconnected from relay: ${relay}`);
|
|
239
|
-
},
|
|
240
|
-
onEose: (relays) => {
|
|
241
|
-
ctx.log?.debug?.(`[${account.accountId}] EOSE received from relays: ${relays}`);
|
|
242
|
-
},
|
|
243
|
-
onMetric: (event: MetricEvent) => {
|
|
244
|
-
// Log significant metrics at appropriate levels
|
|
245
|
-
if (event.name.startsWith("event.rejected.")) {
|
|
246
|
-
ctx.log?.debug?.(
|
|
247
|
-
`[${account.accountId}] Metric: ${event.name} ${JSON.stringify(event.labels)}`,
|
|
248
|
-
);
|
|
249
|
-
} else if (event.name === "relay.circuit_breaker.open") {
|
|
250
|
-
ctx.log?.warn?.(
|
|
251
|
-
`[${account.accountId}] Circuit breaker opened for relay: ${event.labels?.relay}`,
|
|
252
|
-
);
|
|
253
|
-
} else if (event.name === "relay.circuit_breaker.close") {
|
|
254
|
-
ctx.log?.info?.(
|
|
255
|
-
`[${account.accountId}] Circuit breaker closed for relay: ${event.labels?.relay}`,
|
|
256
|
-
);
|
|
257
|
-
} else if (event.name === "relay.error") {
|
|
258
|
-
ctx.log?.debug?.(`[${account.accountId}] Relay error: ${event.labels?.relay}`);
|
|
259
|
-
}
|
|
260
|
-
// Update cached metrics snapshot
|
|
261
|
-
if (busHandle) {
|
|
262
|
-
metricsSnapshots.set(account.accountId, busHandle.getMetrics());
|
|
263
|
-
}
|
|
264
|
-
},
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
busHandle = bus;
|
|
268
|
-
|
|
269
|
-
// Store the bus handle
|
|
270
|
-
activeBuses.set(account.accountId, bus);
|
|
271
|
-
|
|
272
|
-
ctx.log?.info(
|
|
273
|
-
`[${account.accountId}] Nostr provider started, connected to ${account.relays.length} relay(s)`,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
// Return cleanup function
|
|
277
|
-
return {
|
|
278
|
-
stop: () => {
|
|
279
|
-
bus.close();
|
|
280
|
-
activeBuses.delete(account.accountId);
|
|
281
|
-
metricsSnapshots.delete(account.accountId);
|
|
282
|
-
ctx.log?.info(`[${account.accountId}] Nostr provider stopped`);
|
|
283
|
-
},
|
|
284
|
-
};
|
|
285
|
-
},
|
|
167
|
+
security: {
|
|
168
|
+
resolveDmPolicy: resolveNostrDmPolicy,
|
|
286
169
|
},
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Get metrics snapshot for a Nostr account.
|
|
291
|
-
* Returns undefined if account is not running.
|
|
292
|
-
*/
|
|
293
|
-
export function getNostrMetrics(
|
|
294
|
-
accountId: string = DEFAULT_ACCOUNT_ID,
|
|
295
|
-
): MetricsSnapshot | undefined {
|
|
296
|
-
const bus = activeBuses.get(accountId);
|
|
297
|
-
if (bus) {
|
|
298
|
-
return bus.getMetrics();
|
|
299
|
-
}
|
|
300
|
-
return metricsSnapshots.get(accountId);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Get all active Nostr bus handles.
|
|
305
|
-
* Useful for debugging and status reporting.
|
|
306
|
-
*/
|
|
307
|
-
export function getActiveNostrBuses(): Map<string, NostrBusHandle> {
|
|
308
|
-
return new Map(activeBuses);
|
|
309
|
-
}
|
|
170
|
+
outbound: nostrOutboundAdapter,
|
|
171
|
+
});
|
|
310
172
|
|
|
311
173
|
/**
|
|
312
174
|
* Publish a profile (kind:0) for a Nostr account.
|
|
@@ -319,7 +181,7 @@ export async function publishNostrProfile(
|
|
|
319
181
|
accountId: string = DEFAULT_ACCOUNT_ID,
|
|
320
182
|
profile: NostrProfile,
|
|
321
183
|
): Promise<ProfilePublishResult> {
|
|
322
|
-
const bus =
|
|
184
|
+
const bus = getActiveNostrBuses().get(accountId);
|
|
323
185
|
if (!bus) {
|
|
324
186
|
throw new Error(`Nostr bus not running for account ${accountId}`);
|
|
325
187
|
}
|
|
@@ -336,7 +198,7 @@ export async function getNostrProfileState(accountId: string = DEFAULT_ACCOUNT_I
|
|
|
336
198
|
lastPublishedEventId: string | null;
|
|
337
199
|
lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
|
|
338
200
|
} | null> {
|
|
339
|
-
const bus =
|
|
201
|
+
const bus = getActiveNostrBuses().get(accountId);
|
|
340
202
|
if (!bus) {
|
|
341
203
|
return null;
|
|
342
204
|
}
|
package/src/config-schema.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
AllowFromListSchema,
|
|
3
|
+
DmPolicySchema,
|
|
4
|
+
MarkdownConfigSchema,
|
|
5
|
+
} from "openclaw/plugin-sdk/channel-config-primitives";
|
|
6
|
+
import { buildSecretInputSchema } from "openclaw/plugin-sdk/secret-input";
|
|
7
|
+
import { z } from "openclaw/plugin-sdk/zod";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Validates https:// URLs only (no javascript:, data:, file:, etc.)
|
|
@@ -50,7 +54,16 @@ export const NostrProfileSchema = z.object({
|
|
|
50
54
|
lud16: z.string().optional(),
|
|
51
55
|
});
|
|
52
56
|
|
|
53
|
-
export
|
|
57
|
+
export interface NostrProfile {
|
|
58
|
+
name?: string;
|
|
59
|
+
displayName?: string;
|
|
60
|
+
about?: string;
|
|
61
|
+
picture?: string;
|
|
62
|
+
banner?: string;
|
|
63
|
+
website?: string;
|
|
64
|
+
nip05?: string;
|
|
65
|
+
lud16?: string;
|
|
66
|
+
}
|
|
54
67
|
|
|
55
68
|
/**
|
|
56
69
|
* Zod schema for channels.nostr.* configuration
|
|
@@ -69,7 +82,7 @@ export const NostrConfigSchema = z.object({
|
|
|
69
82
|
markdown: MarkdownConfigSchema,
|
|
70
83
|
|
|
71
84
|
/** Private key in hex or nsec bech32 format */
|
|
72
|
-
privateKey:
|
|
85
|
+
privateKey: buildSecretInputSchema().optional(),
|
|
73
86
|
|
|
74
87
|
/** WebSocket relay URLs to connect to */
|
|
75
88
|
relays: z.array(z.string()).optional(),
|
|
@@ -83,10 +96,3 @@ export const NostrConfigSchema = z.object({
|
|
|
83
96
|
/** Profile metadata (NIP-01 kind:0 content) */
|
|
84
97
|
profile: NostrProfileSchema.optional(),
|
|
85
98
|
});
|
|
86
|
-
|
|
87
|
-
export type NostrConfig = z.infer<typeof NostrConfigSchema>;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* JSON Schema for Control UI (converted from Zod)
|
|
91
|
-
*/
|
|
92
|
-
export const nostrChannelConfigSchema = buildChannelConfigSchema(NostrConfigSchema);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const DEFAULT_RELAYS = ["wss://relay.damus.io", "wss://nos.lol"];
|