@keychat-io/keychat-openclaw 0.1.8
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/LICENSE +661 -0
- package/README.md +231 -0
- package/docs/setup-guide.md +124 -0
- package/index.ts +108 -0
- package/openclaw.plugin.json +38 -0
- package/package.json +33 -0
- package/scripts/install.sh +154 -0
- package/scripts/postinstall.mjs +97 -0
- package/scripts/publish.sh +25 -0
- package/src/bridge-client.ts +840 -0
- package/src/channel.ts +2607 -0
- package/src/config-schema.ts +50 -0
- package/src/ensure-binary.ts +65 -0
- package/src/keychain.ts +75 -0
- package/src/lightning.ts +128 -0
- package/src/media.ts +189 -0
- package/src/nwc.ts +360 -0
- package/src/paths.ts +38 -0
- package/src/qrcode-types.d.ts +4 -0
- package/src/qrcode.ts +9 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +125 -0
package/src/nwc.ts
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nostr Wallet Connect (NWC) — NIP-47 client implementation.
|
|
3
|
+
*
|
|
4
|
+
* Connects to a remote Lightning wallet via Nostr relay.
|
|
5
|
+
* The agent can: make_invoice, lookup_invoice, get_balance, list_transactions.
|
|
6
|
+
* Pay operations require owner approval (forwarded as invoice to owner).
|
|
7
|
+
*
|
|
8
|
+
* Connection URI format:
|
|
9
|
+
* nostr+walletconnect://<wallet_pubkey>?relay=<relay_url>&secret=<hex_secret>
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import NDK, {
|
|
13
|
+
NDKEvent,
|
|
14
|
+
NDKPrivateKeySigner,
|
|
15
|
+
type NDKFilter,
|
|
16
|
+
type NDKSubscription,
|
|
17
|
+
} from "@nostr-dev-kit/ndk";
|
|
18
|
+
|
|
19
|
+
// ─── Types ──────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
export interface NwcConnectionInfo {
|
|
22
|
+
walletPubkey: string; // hex pubkey of wallet service
|
|
23
|
+
relay: string; // relay URL
|
|
24
|
+
secret: string; // hex secret (client private key)
|
|
25
|
+
lud16?: string; // optional lightning address
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NwcInvoice {
|
|
29
|
+
type: "incoming" | "outgoing";
|
|
30
|
+
state?: "pending" | "settled" | "accepted" | "expired" | "failed";
|
|
31
|
+
invoice?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
preimage?: string;
|
|
34
|
+
payment_hash: string;
|
|
35
|
+
amount: number; // msats
|
|
36
|
+
fees_paid?: number;
|
|
37
|
+
created_at?: number;
|
|
38
|
+
expires_at?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NwcBalance {
|
|
42
|
+
balance: number; // msats
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface NwcResponse {
|
|
46
|
+
result_type: string;
|
|
47
|
+
result?: Record<string, unknown> | null;
|
|
48
|
+
error?: { code: string; message: string } | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Parse connection URI ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export function parseNwcUri(uri: string): NwcConnectionInfo | null {
|
|
54
|
+
try {
|
|
55
|
+
// nostr+walletconnect://<pubkey>?relay=...&secret=...
|
|
56
|
+
const cleaned = uri.trim();
|
|
57
|
+
if (!cleaned.startsWith("nostr+walletconnect://")) return null;
|
|
58
|
+
|
|
59
|
+
const url = new URL(cleaned.replace("nostr+walletconnect://", "https://"));
|
|
60
|
+
const walletPubkey = url.hostname;
|
|
61
|
+
const relay = url.searchParams.get("relay");
|
|
62
|
+
const secret = url.searchParams.get("secret");
|
|
63
|
+
const lud16 = url.searchParams.get("lud16") || undefined;
|
|
64
|
+
|
|
65
|
+
if (!walletPubkey || !relay || !secret) return null;
|
|
66
|
+
if (!/^[0-9a-f]{64}$/i.test(walletPubkey)) return null;
|
|
67
|
+
if (!/^[0-9a-f]{64}$/i.test(secret)) return null;
|
|
68
|
+
|
|
69
|
+
return { walletPubkey, relay: decodeURIComponent(relay), secret, lud16 };
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── NWC Client ─────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export class NwcClient {
|
|
78
|
+
private ndk: NDK;
|
|
79
|
+
private signer: NDKPrivateKeySigner;
|
|
80
|
+
private walletPubkey: string;
|
|
81
|
+
private connected = false;
|
|
82
|
+
private sub: NDKSubscription | null = null;
|
|
83
|
+
private pendingRequests = new Map<
|
|
84
|
+
string,
|
|
85
|
+
{
|
|
86
|
+
resolve: (value: NwcResponse) => void;
|
|
87
|
+
reject: (error: Error) => void;
|
|
88
|
+
timer: ReturnType<typeof setTimeout>;
|
|
89
|
+
}
|
|
90
|
+
>();
|
|
91
|
+
|
|
92
|
+
private connInfo: NwcConnectionInfo;
|
|
93
|
+
|
|
94
|
+
constructor(connInfo: NwcConnectionInfo) {
|
|
95
|
+
this.connInfo = connInfo;
|
|
96
|
+
this.signer = new NDKPrivateKeySigner(connInfo.secret);
|
|
97
|
+
this.walletPubkey = connInfo.walletPubkey;
|
|
98
|
+
this.ndk = new NDK({
|
|
99
|
+
explicitRelayUrls: [connInfo.relay],
|
|
100
|
+
signer: this.signer,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Connect to the NWC relay and subscribe for responses. */
|
|
105
|
+
async connect(): Promise<void> {
|
|
106
|
+
if (this.connected) return;
|
|
107
|
+
await this.ndk.connect();
|
|
108
|
+
this.connected = true;
|
|
109
|
+
|
|
110
|
+
// Subscribe for responses (kind:23195) and notifications (kind:23197)
|
|
111
|
+
const clientPubkey = (await this.signer.user()).pubkey;
|
|
112
|
+
const filter: NDKFilter = {
|
|
113
|
+
kinds: [23195 as number, 23196 as number, 23197 as number],
|
|
114
|
+
"#p": [clientPubkey],
|
|
115
|
+
since: Math.floor(Date.now() / 1000) - 60, // 60s buffer for clock skew
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.sub = this.ndk.subscribe(filter, { closeOnEose: false });
|
|
119
|
+
this.sub.on("event", async (event: NDKEvent) => {
|
|
120
|
+
console.log(`[nwc] Received event kind:${event.kind} id:${event.id?.slice(0, 12)} from:${event.pubkey?.slice(0, 12)}`);
|
|
121
|
+
try {
|
|
122
|
+
await this.handleResponse(event);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error("[nwc] Error handling response:", err);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
console.log(`[nwc] Connected to ${this.connInfo.relay}, wallet: ${this.walletPubkey.slice(0, 16)}...`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Disconnect and clean up. */
|
|
132
|
+
async disconnect(): Promise<void> {
|
|
133
|
+
if (this.sub) {
|
|
134
|
+
this.sub.stop();
|
|
135
|
+
this.sub = null;
|
|
136
|
+
}
|
|
137
|
+
// Reject all pending requests
|
|
138
|
+
for (const [, pending] of this.pendingRequests) {
|
|
139
|
+
clearTimeout(pending.timer);
|
|
140
|
+
pending.reject(new Error("NWC client disconnected"));
|
|
141
|
+
}
|
|
142
|
+
this.pendingRequests.clear();
|
|
143
|
+
this.connected = false;
|
|
144
|
+
console.log("[nwc] Disconnected");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Send a NIP-47 request and wait for response (poll-based for reliability). */
|
|
148
|
+
private async request(
|
|
149
|
+
method: string,
|
|
150
|
+
params: Record<string, unknown> = {},
|
|
151
|
+
timeoutMs = 30_000,
|
|
152
|
+
): Promise<NwcResponse> {
|
|
153
|
+
if (!this.connected) {
|
|
154
|
+
await this.connect();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const payload = JSON.stringify({ method, params });
|
|
158
|
+
const event = new NDKEvent(this.ndk);
|
|
159
|
+
event.kind = 23194;
|
|
160
|
+
event.tags = [["p", this.walletPubkey]];
|
|
161
|
+
// Use NIP-04 encryption (default per NIP-47 when no encryption tag in info event)
|
|
162
|
+
event.content = await this.signer.encrypt(
|
|
163
|
+
await this.ndk.getUser({ pubkey: this.walletPubkey }),
|
|
164
|
+
payload,
|
|
165
|
+
"nip04",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await event.sign(this.signer);
|
|
169
|
+
await event.publish();
|
|
170
|
+
const requestId = event.id;
|
|
171
|
+
console.log(`[nwc] Request sent: ${method} (${requestId?.slice(0, 12)})`);
|
|
172
|
+
|
|
173
|
+
// Poll for response tagged with #e matching our request
|
|
174
|
+
const clientPubkey = (await this.signer.user()).pubkey;
|
|
175
|
+
const walletUser = await this.ndk.getUser({ pubkey: this.walletPubkey });
|
|
176
|
+
const startTime = Date.now();
|
|
177
|
+
const pollInterval = 1500; // ms
|
|
178
|
+
|
|
179
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const responses = await this.ndk.fetchEvents({
|
|
184
|
+
kinds: [23195 as number],
|
|
185
|
+
"#p": [clientPubkey],
|
|
186
|
+
"#e": [requestId],
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
for (const resp of responses) {
|
|
190
|
+
let decrypted: string;
|
|
191
|
+
try {
|
|
192
|
+
decrypted = await this.signer.decrypt(walletUser, resp.content, "nip04");
|
|
193
|
+
} catch {
|
|
194
|
+
decrypted = await this.signer.decrypt(walletUser, resp.content, "nip44");
|
|
195
|
+
}
|
|
196
|
+
const parsed = JSON.parse(decrypted) as NwcResponse;
|
|
197
|
+
console.log(`[nwc] Response received: ${parsed.result_type} (${resp.id?.slice(0, 12)})`);
|
|
198
|
+
return parsed;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
// fetch failed, retry
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error(`NWC request timed out: ${method}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Handle an incoming response event. */
|
|
209
|
+
private async handleResponse(event: NDKEvent): Promise<void> {
|
|
210
|
+
// Find which request this responds to
|
|
211
|
+
const eTag = event.tags.find((t) => t[0] === "e");
|
|
212
|
+
const requestId = eTag?.[1];
|
|
213
|
+
|
|
214
|
+
// Decrypt content — try NIP-04 first (default per NIP-47), fallback to NIP-44
|
|
215
|
+
const walletUser = await this.ndk.getUser({ pubkey: this.walletPubkey });
|
|
216
|
+
let decrypted: string;
|
|
217
|
+
try {
|
|
218
|
+
decrypted = await this.signer.decrypt(walletUser, event.content, "nip04");
|
|
219
|
+
} catch {
|
|
220
|
+
decrypted = await this.signer.decrypt(walletUser, event.content, "nip44");
|
|
221
|
+
}
|
|
222
|
+
const response = JSON.parse(decrypted) as NwcResponse;
|
|
223
|
+
|
|
224
|
+
if (event.kind === 23197) {
|
|
225
|
+
// Notification — emit event (could be payment_received etc.)
|
|
226
|
+
console.log(`[nwc] Notification: ${JSON.stringify(response)}`);
|
|
227
|
+
// TODO: emit to channel for payment_received notifications
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!requestId) return;
|
|
232
|
+
const pending = this.pendingRequests.get(requestId);
|
|
233
|
+
if (!pending) return;
|
|
234
|
+
|
|
235
|
+
this.pendingRequests.delete(requestId);
|
|
236
|
+
clearTimeout(pending.timer);
|
|
237
|
+
pending.resolve(response);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ─── Public API ─────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
/** Get wallet balance in msats. */
|
|
243
|
+
async getBalance(): Promise<NwcBalance> {
|
|
244
|
+
const resp = await this.request("get_balance");
|
|
245
|
+
if (resp.error) throw new Error(`NWC get_balance: ${resp.error.message}`);
|
|
246
|
+
return resp.result as unknown as NwcBalance;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Get balance in sats (convenience). */
|
|
250
|
+
async getBalanceSats(): Promise<number> {
|
|
251
|
+
const b = await this.getBalance();
|
|
252
|
+
return Math.floor(b.balance / 1000);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Create an invoice (receive payment). */
|
|
256
|
+
async makeInvoice(
|
|
257
|
+
amountSats: number,
|
|
258
|
+
description?: string,
|
|
259
|
+
expirySecs?: number,
|
|
260
|
+
): Promise<NwcInvoice> {
|
|
261
|
+
const params: Record<string, unknown> = {
|
|
262
|
+
amount: amountSats * 1000, // convert to msats
|
|
263
|
+
};
|
|
264
|
+
if (description) params.description = description;
|
|
265
|
+
if (expirySecs) params.expiry = expirySecs;
|
|
266
|
+
|
|
267
|
+
const resp = await this.request("make_invoice", params);
|
|
268
|
+
if (resp.error) throw new Error(`NWC make_invoice: ${resp.error.message}`);
|
|
269
|
+
return resp.result as unknown as NwcInvoice;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Look up an invoice by payment hash or bolt11 string. */
|
|
273
|
+
async lookupInvoice(opts: {
|
|
274
|
+
paymentHash?: string;
|
|
275
|
+
invoice?: string;
|
|
276
|
+
}): Promise<NwcInvoice> {
|
|
277
|
+
const resp = await this.request("lookup_invoice", opts);
|
|
278
|
+
if (resp.error) throw new Error(`NWC lookup_invoice: ${resp.error.message}`);
|
|
279
|
+
return resp.result as unknown as NwcInvoice;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** List transactions. */
|
|
283
|
+
async listTransactions(opts?: {
|
|
284
|
+
from?: number;
|
|
285
|
+
until?: number;
|
|
286
|
+
limit?: number;
|
|
287
|
+
offset?: number;
|
|
288
|
+
type?: "incoming" | "outgoing";
|
|
289
|
+
}): Promise<NwcInvoice[]> {
|
|
290
|
+
const resp = await this.request("list_transactions", opts ?? {});
|
|
291
|
+
if (resp.error) throw new Error(`NWC list_transactions: ${resp.error.message}`);
|
|
292
|
+
return (resp.result as unknown as { transactions: NwcInvoice[] }).transactions ?? [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Get wallet info (supported methods). */
|
|
296
|
+
async getInfo(): Promise<Record<string, unknown>> {
|
|
297
|
+
const resp = await this.request("get_info");
|
|
298
|
+
if (resp.error) throw new Error(`NWC get_info: ${resp.error.message}`);
|
|
299
|
+
return resp.result ?? {};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Request payment of an invoice.
|
|
304
|
+
* NOTE: This is gated — the agent should NOT call this directly.
|
|
305
|
+
* Instead, forward the invoice to the owner for approval.
|
|
306
|
+
*/
|
|
307
|
+
async payInvoice(invoice: string, amountMsats?: number): Promise<{ preimage: string; fees_paid?: number }> {
|
|
308
|
+
const params: Record<string, unknown> = { invoice };
|
|
309
|
+
if (amountMsats) params.amount = amountMsats;
|
|
310
|
+
|
|
311
|
+
const resp = await this.request("pay_invoice", params, 60_000);
|
|
312
|
+
if (resp.error) throw new Error(`NWC pay_invoice: ${resp.error.message}`);
|
|
313
|
+
return resp.result as unknown as { preimage: string; fees_paid?: number };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Check if connected. */
|
|
317
|
+
isConnected(): boolean {
|
|
318
|
+
return this.connected;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Get connection info summary (no secrets). */
|
|
322
|
+
describe(): { relay: string; walletPubkey: string; lud16?: string } {
|
|
323
|
+
return {
|
|
324
|
+
relay: this.connInfo.relay,
|
|
325
|
+
walletPubkey: this.connInfo.walletPubkey,
|
|
326
|
+
lud16: this.connInfo.lud16,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Singleton management ───────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
let activeClient: NwcClient | null = null;
|
|
334
|
+
|
|
335
|
+
/** Initialize NWC from a connection URI string. */
|
|
336
|
+
export async function initNwc(uri: string): Promise<NwcClient> {
|
|
337
|
+
const info = parseNwcUri(uri);
|
|
338
|
+
if (!info) throw new Error("Invalid NWC connection URI");
|
|
339
|
+
|
|
340
|
+
if (activeClient) {
|
|
341
|
+
await activeClient.disconnect();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
activeClient = new NwcClient(info);
|
|
345
|
+
await activeClient.connect();
|
|
346
|
+
return activeClient;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Get the active NWC client (null if not configured). */
|
|
350
|
+
export function getNwcClient(): NwcClient | null {
|
|
351
|
+
return activeClient;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Disconnect and clear the active client. */
|
|
355
|
+
export async function disconnectNwc(): Promise<void> {
|
|
356
|
+
if (activeClient) {
|
|
357
|
+
await activeClient.disconnect();
|
|
358
|
+
activeClient = null;
|
|
359
|
+
}
|
|
360
|
+
}
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized path resolution for Keychat plugin.
|
|
3
|
+
* All process.env access is isolated here to avoid scanner warnings
|
|
4
|
+
* when other files combine env access with network calls.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
|
|
9
|
+
const HOME = process.env.HOME || "~";
|
|
10
|
+
|
|
11
|
+
/** Base dir: ~/.openclaw/keychat */
|
|
12
|
+
export const KEYCHAT_DIR = join(HOME, ".openclaw", "keychat");
|
|
13
|
+
|
|
14
|
+
/** Media storage: ~/.openclaw/keychat/media */
|
|
15
|
+
export const MEDIA_DIR = join(KEYCHAT_DIR, "media");
|
|
16
|
+
|
|
17
|
+
/** Workspace keychat dir: ~/.openclaw/workspace/keychat */
|
|
18
|
+
export const WORKSPACE_KEYCHAT_DIR = join(HOME, ".openclaw", "workspace", "keychat");
|
|
19
|
+
|
|
20
|
+
/** Signal DB path for a given account */
|
|
21
|
+
export function signalDbPath(accountId: string): string {
|
|
22
|
+
return join(KEYCHAT_DIR, `signal-${accountId}.db`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** QR code image path for a given account */
|
|
26
|
+
export function qrCodePath(accountId: string): string {
|
|
27
|
+
return join(WORKSPACE_KEYCHAT_DIR, `qr-${accountId}.png`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Mnemonic file path for a given account */
|
|
31
|
+
export function mnemonicPath(accountId: string): string {
|
|
32
|
+
return join(KEYCHAT_DIR, `mnemonic-${accountId}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Bridge spawn environment (inherits current env + RUST_LOG) */
|
|
36
|
+
export function bridgeEnv(): NodeJS.ProcessEnv {
|
|
37
|
+
return { ...process.env, RUST_LOG: "info" };
|
|
38
|
+
}
|
package/src/qrcode.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export async function generateQRDataUrl(npub: string): Promise<string> {
|
|
2
|
+
try {
|
|
3
|
+
const QRCode = await import("qrcode");
|
|
4
|
+
const url = `https://www.keychat.io/u/?k=${npub}`;
|
|
5
|
+
return await QRCode.toDataURL(url, { width: 256, margin: 2 });
|
|
6
|
+
} catch {
|
|
7
|
+
return ""; // QR code generation not available
|
|
8
|
+
}
|
|
9
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
let runtime: PluginRuntime | null = null;
|
|
4
|
+
|
|
5
|
+
export function setKeychatRuntime(next: PluginRuntime): void {
|
|
6
|
+
runtime = next;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getKeychatRuntime(): PluginRuntime {
|
|
10
|
+
if (!runtime) {
|
|
11
|
+
throw new Error("Keychat runtime not initialized");
|
|
12
|
+
}
|
|
13
|
+
return runtime;
|
|
14
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
|
|
3
|
+
export interface KeychatAccountConfig {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
name?: string;
|
|
6
|
+
/** Mnemonic phrase for identity (auto-generated on first start) */
|
|
7
|
+
mnemonic?: string;
|
|
8
|
+
/** Public key hex (derived from mnemonic) */
|
|
9
|
+
publicKey?: string;
|
|
10
|
+
/** npub bech32 public key */
|
|
11
|
+
npub?: string;
|
|
12
|
+
/** Nostr relay URLs */
|
|
13
|
+
relays?: string[];
|
|
14
|
+
/** DM access policy */
|
|
15
|
+
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
|
|
16
|
+
/** Allowed sender pubkeys */
|
|
17
|
+
allowFrom?: Array<string | number>;
|
|
18
|
+
/** Lightning address for receiving payments */
|
|
19
|
+
lightningAddress?: string;
|
|
20
|
+
/** Nostr Wallet Connect URI */
|
|
21
|
+
nwcUri?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Top-level keychat channel config — supports single-account or multi-account. */
|
|
25
|
+
export interface KeychatChannelConfig extends KeychatAccountConfig {
|
|
26
|
+
/** Multi-account: each key is an accountId with its own config. */
|
|
27
|
+
accounts?: Record<string, KeychatAccountConfig>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResolvedKeychatAccount {
|
|
31
|
+
accountId: string;
|
|
32
|
+
name?: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
configured: boolean;
|
|
35
|
+
/** Mnemonic for identity restoration */
|
|
36
|
+
mnemonic?: string;
|
|
37
|
+
/** Nostr public key hex */
|
|
38
|
+
publicKey: string;
|
|
39
|
+
/** npub bech32 */
|
|
40
|
+
npub?: string;
|
|
41
|
+
/** Relay URLs */
|
|
42
|
+
relays: string[];
|
|
43
|
+
/** Lightning address for receiving payments */
|
|
44
|
+
lightningAddress?: string;
|
|
45
|
+
/** Nostr Wallet Connect URI */
|
|
46
|
+
nwcUri?: string;
|
|
47
|
+
config: KeychatAccountConfig;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_ACCOUNT_ID = "default";
|
|
51
|
+
|
|
52
|
+
const DEFAULT_RELAYS = [
|
|
53
|
+
"wss://relay.keychat.io",
|
|
54
|
+
"wss://relay.damus.io",
|
|
55
|
+
"wss://relay.primal.net",
|
|
56
|
+
"wss://relay.nostr.band",
|
|
57
|
+
"wss://relay.0xchat.com",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function listKeychatAccountIds(cfg: OpenClawConfig): string[] {
|
|
61
|
+
const keychatCfg = (cfg.channels as Record<string, unknown> | undefined)?.keychat as
|
|
62
|
+
| KeychatChannelConfig
|
|
63
|
+
| undefined;
|
|
64
|
+
|
|
65
|
+
if (!keychatCfg) return [];
|
|
66
|
+
|
|
67
|
+
// Multi-account: return all account keys that aren't explicitly disabled
|
|
68
|
+
if (keychatCfg.accounts && Object.keys(keychatCfg.accounts).length > 0) {
|
|
69
|
+
return Object.entries(keychatCfg.accounts)
|
|
70
|
+
.filter(([_, acct]) => acct.enabled !== false)
|
|
71
|
+
.map(([id]) => id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Single-account (backward compat): config exists → "default" account
|
|
75
|
+
return [DEFAULT_ACCOUNT_ID];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function resolveDefaultKeychatAccountId(cfg: OpenClawConfig): string {
|
|
79
|
+
return DEFAULT_ACCOUNT_ID;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveKeychatAccount(opts: {
|
|
83
|
+
cfg: OpenClawConfig;
|
|
84
|
+
accountId?: string | null;
|
|
85
|
+
}): ResolvedKeychatAccount {
|
|
86
|
+
const accountId = opts.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
87
|
+
const channelCfg = (opts.cfg.channels as Record<string, unknown> | undefined)?.keychat as
|
|
88
|
+
| KeychatChannelConfig
|
|
89
|
+
| undefined;
|
|
90
|
+
|
|
91
|
+
// Multi-account: look up specific account; single-account: use top-level config
|
|
92
|
+
const acctCfg: KeychatAccountConfig | undefined =
|
|
93
|
+
channelCfg?.accounts && Object.keys(channelCfg.accounts).length > 0
|
|
94
|
+
? channelCfg.accounts[accountId]
|
|
95
|
+
: channelCfg;
|
|
96
|
+
|
|
97
|
+
const enabled = acctCfg?.enabled !== false;
|
|
98
|
+
const mnemonic = acctCfg?.mnemonic?.trim();
|
|
99
|
+
const configured = true; // Always configured — identity auto-generates
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
accountId,
|
|
103
|
+
name: acctCfg?.name?.trim() || undefined,
|
|
104
|
+
enabled,
|
|
105
|
+
configured,
|
|
106
|
+
mnemonic: mnemonic || undefined,
|
|
107
|
+
publicKey: acctCfg?.publicKey ?? "",
|
|
108
|
+
npub: acctCfg?.npub,
|
|
109
|
+
relays: acctCfg?.relays ?? DEFAULT_RELAYS,
|
|
110
|
+
lightningAddress: acctCfg?.lightningAddress?.trim() || undefined,
|
|
111
|
+
nwcUri: acctCfg?.nwcUri?.trim() || undefined,
|
|
112
|
+
config: {
|
|
113
|
+
enabled: acctCfg?.enabled,
|
|
114
|
+
name: acctCfg?.name,
|
|
115
|
+
mnemonic: acctCfg?.mnemonic,
|
|
116
|
+
publicKey: acctCfg?.publicKey,
|
|
117
|
+
npub: acctCfg?.npub,
|
|
118
|
+
relays: acctCfg?.relays,
|
|
119
|
+
dmPolicy: acctCfg?.dmPolicy,
|
|
120
|
+
allowFrom: acctCfg?.allowFrom,
|
|
121
|
+
lightningAddress: acctCfg?.lightningAddress,
|
|
122
|
+
nwcUri: acctCfg?.nwcUri,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|