openclaw-overlay-plugin 0.8.17 → 0.8.19

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.
Files changed (82) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -3
  4. package/index.ts +0 -379
  5. package/src/ambient.d.ts +0 -1
  6. package/src/cli-main.ts +0 -240
  7. package/src/cli.ts +0 -16
  8. package/src/compatibility.test.ts +0 -46
  9. package/src/scripts/baemail/commands.ts +0 -311
  10. package/src/scripts/baemail/handler.ts +0 -338
  11. package/src/scripts/baemail/index.ts +0 -6
  12. package/src/scripts/config.ts +0 -89
  13. package/src/scripts/index.ts +0 -8
  14. package/src/scripts/messaging/connect.ts +0 -162
  15. package/src/scripts/messaging/handlers.ts +0 -394
  16. package/src/scripts/messaging/inbox.ts +0 -64
  17. package/src/scripts/messaging/index.ts +0 -9
  18. package/src/scripts/messaging/poll.ts +0 -59
  19. package/src/scripts/messaging/send.ts +0 -54
  20. package/src/scripts/output.ts +0 -30
  21. package/src/scripts/overlay/advertisement.ts +0 -138
  22. package/src/scripts/overlay/discover.ts +0 -83
  23. package/src/scripts/overlay/index.ts +0 -8
  24. package/src/scripts/overlay/registration.ts +0 -199
  25. package/src/scripts/overlay/services.ts +0 -199
  26. package/src/scripts/overlay/transaction.ts +0 -124
  27. package/src/scripts/payment/build.ts +0 -65
  28. package/src/scripts/payment/commands.ts +0 -92
  29. package/src/scripts/payment/index.ts +0 -7
  30. package/src/scripts/payment/types.ts +0 -62
  31. package/src/scripts/services/index.ts +0 -7
  32. package/src/scripts/services/queue.ts +0 -35
  33. package/src/scripts/services/request.ts +0 -98
  34. package/src/scripts/services/respond.ts +0 -149
  35. package/src/scripts/types.ts +0 -121
  36. package/src/scripts/utils/index.ts +0 -7
  37. package/src/scripts/utils/merkle.ts +0 -57
  38. package/src/scripts/utils/storage.ts +0 -231
  39. package/src/scripts/utils/woc.ts +0 -106
  40. package/src/scripts/wallet/balance.ts +0 -277
  41. package/src/scripts/wallet/identity.ts +0 -204
  42. package/src/scripts/wallet/index.ts +0 -7
  43. package/src/scripts/wallet/setup.ts +0 -137
  44. package/src/scripts/x-verification/commands.ts +0 -261
  45. package/src/scripts/x-verification/index.ts +0 -5
  46. package/src/services/built-in/api-proxy/index.ts +0 -26
  47. package/src/services/built-in/api-proxy/prompt.md +0 -26
  48. package/src/services/built-in/code-develop/index.ts +0 -26
  49. package/src/services/built-in/code-develop/prompt.md +0 -35
  50. package/src/services/built-in/code-review/index.ts +0 -54
  51. package/src/services/built-in/code-review/prompt.md +0 -105
  52. package/src/services/built-in/image-analysis/index.ts +0 -36
  53. package/src/services/built-in/image-analysis/prompt.md +0 -42
  54. package/src/services/built-in/memory-store/index.ts +0 -25
  55. package/src/services/built-in/memory-store/prompt.md +0 -45
  56. package/src/services/built-in/roulette/index.ts +0 -30
  57. package/src/services/built-in/roulette/prompt.md +0 -35
  58. package/src/services/built-in/summarize/index.ts +0 -24
  59. package/src/services/built-in/summarize/prompt.md +0 -27
  60. package/src/services/built-in/tell-joke/handler.ts +0 -134
  61. package/src/services/built-in/tell-joke/index.ts +0 -34
  62. package/src/services/built-in/tell-joke/prompt.md +0 -59
  63. package/src/services/built-in/translate/index.ts +0 -24
  64. package/src/services/built-in/translate/prompt.md +0 -23
  65. package/src/services/built-in/web-research/index.ts +0 -54
  66. package/src/services/built-in/web-research/prompt.md +0 -110
  67. package/src/services/index.ts +0 -16
  68. package/src/services/loader.ts +0 -344
  69. package/src/services/manager.ts +0 -304
  70. package/src/services/registry.ts +0 -246
  71. package/src/services/types.ts +0 -259
  72. package/src/test/cli.test.ts +0 -353
  73. package/src/test/comprehensive-overlay.test.ts +0 -729
  74. package/src/test/identity-consistency.test.ts +0 -68
  75. package/src/test/key-derivation.test.ts +0 -102
  76. package/src/test/network-address.test.ts +0 -46
  77. package/src/test/overlay-submit.test.ts +0 -570
  78. package/src/test/request-response-flow.test.ts +0 -253
  79. package/src/test/service-system.test.ts +0 -241
  80. package/src/test/taskflow.test.ts +0 -95
  81. package/src/test/utils/server-logic.ts +0 -368
  82. package/src/test/wallet.test.ts +0 -165
@@ -1,46 +0,0 @@
1
- /**
2
- * Compatibility Test for Node 24 and OpenClaw SDK
3
- */
4
-
5
- import { plugin } from '../index.js';
6
-
7
- async function testCompatibility() {
8
- console.log('--- Overlay Compatibility Test ---');
9
-
10
- // 1. Verify Plugin Registration (SDK Descriptors)
11
- let registeredCli = false;
12
- let hasDescriptors = false;
13
-
14
- const mockApi = {
15
- logger: { info: () => {}, warn: () => {}, error: () => {} },
16
- registerTool: () => {},
17
- registerCommand: () => {},
18
- registerService: () => {},
19
- registerCli: (fn: any, options: any) => {
20
- registeredCli = true;
21
- if (options && options.descriptors && options.descriptors[0].name === 'overlay') {
22
- hasDescriptors = true;
23
- }
24
- },
25
- getConfig: () => ({ plugins: { entries: {} } })
26
- };
27
-
28
- try {
29
- plugin.register(mockApi);
30
- if (registeredCli && hasDescriptors) {
31
- console.log('✓ CLI descriptors registered correctly for Phase 1 discovery.');
32
- } else {
33
- throw new Error(`CLI registration missing or using old 'commands' array format.`);
34
- }
35
- } catch (err: any) {
36
- console.error('✗ Plugin registration failed:', err.message);
37
- process.exit(1);
38
- }
39
-
40
- console.log('--- All Compatibility Tests Passed ---\n');
41
- }
42
-
43
- testCompatibility().catch(err => {
44
- console.error(err);
45
- process.exit(1);
46
- });
@@ -1,311 +0,0 @@
1
- import { fileURLToPath } from 'node:url';
2
- import path from 'node:path';
3
- import os from 'node:os';
4
- import fs from 'node:fs';
5
- import process from 'node:process';
6
- import { Buffer } from 'node:buffer';
7
- import { ok, fail } from '../output.js';
8
- import { loadIdentity, deriveWalletAddress } from '../wallet/identity.js';
9
- import { NETWORK } from '../config.js';
10
-
11
- const __filename = fileURLToPath(import.meta.url);
12
- const __dirname = path.dirname(__filename);
13
-
14
- // Define paths relative to home directory
15
- const PATHS = {
16
- walletIdentity: path.join(os.homedir(), '.openclaw', 'bsv-wallet', 'wallet-identity.json'),
17
- baemailLog: path.join(os.homedir(), '.openclaw', 'openclaw-overlay', 'baemail-deliveries.jsonl'),
18
- baemailConfig: path.join(os.homedir(), '.openclaw', 'openclaw-overlay', 'baemail-config.json'),
19
- baemailBlocklist: path.join(os.homedir(), '.openclaw', 'openclaw-overlay', 'baemail-blocklist.json'),
20
- };
21
-
22
- export interface BaemailLogEntry {
23
- requestId: string;
24
- from: string;
25
- to?: string;
26
- paidSats: number;
27
- deliverySuccess: boolean;
28
- refundStatus?: 'pending' | 'completed';
29
- refundTxid?: string;
30
- _lineIdx?: number;
31
- ts?: number;
32
- senderName?: string;
33
- messageLength?: number;
34
- tier?: string;
35
- deliveryChannel?: string;
36
- deliveryError?: string | null;
37
- paymentTxid?: string;
38
- timestamp?: string;
39
- }
40
-
41
- /**
42
- * Log a baemail delivery event.
43
- */
44
- export function logBaemailDelivery(entry: BaemailLogEntry) {
45
- try {
46
- const dir = path.dirname(PATHS.baemailLog);
47
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
48
- fs.appendFileSync(PATHS.baemailLog, JSON.stringify({ ...entry, ts: Date.now() }) + '\n');
49
- } catch {}
50
- }
51
-
52
- export async function loadBaemailConfig() {
53
- const defaults = {
54
- enabled: true,
55
- priceSats: 100,
56
- autoRefund: true,
57
- blocklist: [] as string[],
58
- maxMessageLength: 4000,
59
- deliveryChannel: 'agent-hook',
60
- tiers: {
61
- standard: 100,
62
- priority: 500,
63
- urgent: 1000
64
- }
65
- };
66
-
67
- if (!fs.existsSync(PATHS.baemailConfig)) {
68
- return defaults;
69
- }
70
- try {
71
- const config = JSON.parse(fs.readFileSync(PATHS.baemailConfig, 'utf-8'));
72
- return { ...defaults, ...config };
73
- } catch {
74
- return defaults;
75
- }
76
- }
77
-
78
- async function fetchWithTimeout(url: string, options: any = {}) {
79
- const { timeout = 15000 } = options;
80
- const controller = new AbortController();
81
- const id = setTimeout(() => controller.abort(), timeout);
82
- try {
83
- const response = await fetch(url, {
84
- ...options,
85
- signal: controller.signal
86
- });
87
- clearTimeout(id);
88
- return response;
89
- } catch (err) {
90
- clearTimeout(id);
91
- throw err;
92
- }
93
- }
94
-
95
- /**
96
- * List recent baemail deliveries.
97
- */
98
- export async function cmdBaemailLog(limitStr?: string): Promise<any> {
99
- const limit = parseInt(limitStr || '20', 10) || 20;
100
-
101
- if (!fs.existsSync(PATHS.baemailLog)) {
102
- return ok({ log: [], count: 0 });
103
- }
104
-
105
- const lines = fs.readFileSync(PATHS.baemailLog, 'utf-8').split('\n').filter((l: string) => l.trim());
106
- const entries: BaemailLogEntry[] = lines.map((l: string) => {
107
- try { return JSON.parse(l); } catch { return null; }
108
- }).filter(Boolean) as BaemailLogEntry[];
109
-
110
- const recent = entries.slice(-limit).reverse();
111
- return ok({ log: recent, count: entries.length, showing: recent.length });
112
- }
113
-
114
- export async function cmdBaemailSetup(priceSatsStr: string, prioritySats?: string, urgentSats?: string, channel?: string): Promise<any> {
115
- const standard = parseInt(priceSatsStr, 10) || 100;
116
- const priority = parseInt(prioritySats || '', 10) || (standard * 5);
117
- const urgent = parseInt(urgentSats || '', 10) || (standard * 10);
118
-
119
- const config = {
120
- enabled: true,
121
- priceSats: standard,
122
- autoRefund: true,
123
- deliveryChannel: channel || 'agent-hook',
124
- tiers: { standard, priority, urgent }
125
- };
126
-
127
- const dir = path.dirname(PATHS.baemailConfig);
128
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
129
- fs.writeFileSync(PATHS.baemailConfig, JSON.stringify(config, null, 2));
130
- return ok({ message: `Baemail setup complete.`, config });
131
- }
132
-
133
- export async function cmdBaemailConfig(): Promise<any> {
134
- const config = await loadBaemailConfig();
135
- return ok(config);
136
- }
137
-
138
- export async function cmdBaemailBlock(pubkey: string): Promise<any> {
139
- if (!pubkey) return fail('Usage: baemail-block <pubkey>');
140
- let blocklist: string[] = [];
141
- if (fs.existsSync(PATHS.baemailBlocklist)) {
142
- try { blocklist = JSON.parse(fs.readFileSync(PATHS.baemailBlocklist, 'utf-8')); } catch { blocklist = []; }
143
- }
144
- if (!blocklist.includes(pubkey)) blocklist.push(pubkey);
145
- fs.writeFileSync(PATHS.baemailBlocklist, JSON.stringify(blocklist, null, 2));
146
- return ok({ blocked: true, pubkey, count: blocklist.length });
147
- }
148
-
149
- export async function cmdBaemailUnblock(pubkey: string): Promise<any> {
150
- if (!pubkey) return fail('Usage: baemail-unblock <pubkey>');
151
- let blocklist: string[] = [];
152
- if (fs.existsSync(PATHS.baemailBlocklist)) {
153
- try { blocklist = JSON.parse(fs.readFileSync(PATHS.baemailBlocklist, 'utf-8')); } catch { blocklist = []; }
154
- }
155
- blocklist = blocklist.filter(p => p !== pubkey);
156
- fs.writeFileSync(PATHS.baemailBlocklist, JSON.stringify(blocklist, null, 2));
157
- return ok({ unblocked: true, pubkey, count: blocklist.length });
158
- }
159
-
160
- /**
161
- * Refund a failed baemail delivery.
162
- */
163
- export async function cmdBaemailRefund(requestId: string | undefined): Promise<any> {
164
- if (!requestId) return fail('Usage: baemail-refund <requestId>');
165
-
166
- if (!fs.existsSync(PATHS.baemailLog)) {
167
- return fail('No baemail log found');
168
- }
169
-
170
- // Find the entry
171
- const lines = fs.readFileSync(PATHS.baemailLog, 'utf-8').split('\n').filter((l: string) => l.trim());
172
- const entries: BaemailLogEntry[] = lines.map((l: string, idx: number) => {
173
- try { return { ...JSON.parse(l), _lineIdx: idx }; } catch { return null; }
174
- }).filter(Boolean) as BaemailLogEntry[];
175
-
176
- const entry = entries.find(e => e.requestId === requestId);
177
- if (!entry) {
178
- return fail(`Request ${requestId} not found in baemail log`);
179
- }
180
-
181
- if (entry.deliverySuccess) {
182
- return fail('This delivery was successful — no refund needed');
183
- }
184
-
185
- if (entry.refundStatus === 'completed') {
186
- return fail('Refund already processed for this request');
187
- }
188
-
189
- // Load wallet and SDK
190
- const { identityKey, privKey: rootKey } = await loadIdentity();
191
- const walletIdentityRaw = fs.readFileSync(PATHS.walletIdentity, 'utf-8');
192
- const walletIdentity = JSON.parse(walletIdentityRaw);
193
-
194
- // Dynamic import SDK
195
- let sdk: any;
196
- try {
197
- sdk = await import('@bsv/sdk');
198
- } catch {
199
- return fail('Cannot load @bsv/sdk for refund transaction');
200
- }
201
-
202
- const { Transaction, P2PKH, PrivateKey, PublicKey, Hash } = sdk;
203
-
204
- // Calculate refund amount
205
- const refundSats = entry.paidSats - 1; // Keep 1 sat for tx fee
206
- if (refundSats < 1) {
207
- return fail('Amount too small to refund');
208
- }
209
-
210
- // Derive refund address from sender's identity key
211
- const senderPubKey = PublicKey.fromString(entry.from);
212
- const refundAddress = senderPubKey.toAddress(NETWORK).toString();
213
-
214
- try {
215
- // Load UTXOs - Derive local address correctly
216
- const { address } = await deriveWalletAddress(rootKey);
217
- const wocNet = NETWORK === 'mainnet' ? 'main' : 'test';
218
- const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/address/${address}/unspent/all`);
219
- const data = await utxosResp.json();
220
- const utxos = data.result || [];
221
-
222
- if (!utxos || utxos.length === 0) {
223
- return fail(`No UTXOs available for refund at ${address}`);
224
- }
225
-
226
- // Build transaction
227
- const tx = new Transaction();
228
- let totalInput = 0;
229
-
230
- for (const utxo of utxos) {
231
- if (totalInput >= refundSats + 50) break;
232
- tx.addInput({
233
- sourceTXID: utxo.tx_hash,
234
- sourceOutputIndex: utxo.tx_pos,
235
- sourceSatoshis: utxo.value,
236
- script: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)).toHex(),
237
- unlockingScriptTemplate: new P2PKH().unlock(rootKey),
238
- });
239
- totalInput += utxo.value;
240
- }
241
-
242
- if (totalInput < refundSats + 10) {
243
- return fail('Insufficient funds for refund');
244
- }
245
-
246
- // Refund output
247
- tx.addOutput({
248
- satoshis: refundSats,
249
- lockingScript: new P2PKH().lock(refundAddress),
250
- });
251
-
252
- // Change output
253
- const fee = 10;
254
- const change = totalInput - refundSats - fee;
255
- if (change > 1) {
256
- tx.addOutput({
257
- satoshis: change,
258
- lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)),
259
- });
260
- }
261
-
262
- await tx.sign();
263
-
264
- // Broadcast using configured ARC/Arcade URL or fallback to WhatsOnChain
265
- const arcUrl = (process as any)['env'].BSV_ARC_URL;
266
- let broadcastResp;
267
-
268
- if (arcUrl) {
269
- broadcastResp = await fetchWithTimeout(`${arcUrl.replace(/\/$/, '')}/v1/tx`, {
270
- method: 'POST',
271
- headers: { 'Content-Type': 'application/json' },
272
- body: JSON.stringify({ rawTx: tx.toHex() }),
273
- });
274
- } else {
275
- broadcastResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/tx/raw`, {
276
- method: 'POST',
277
- headers: { 'Content-Type': 'application/json' },
278
- body: JSON.stringify({ txhex: tx.toHex() }),
279
- });
280
- }
281
-
282
- if (!broadcastResp.ok) {
283
- const errBody = await broadcastResp.text();
284
- return fail(`Broadcast failed: ${errBody}`);
285
- }
286
-
287
- const txid = tx.id('hex');
288
-
289
- // Update log entry
290
- const updatedLines = lines.map((l: string, idx: number) => {
291
- if (idx === entry._lineIdx) {
292
- const updated = { ...JSON.parse(l), refundStatus: 'completed', refundTxid: txid, refundedAt: new Date().toISOString() };
293
- return JSON.stringify(updated);
294
- }
295
- return l;
296
- });
297
- fs.writeFileSync(PATHS.baemailLog, updatedLines.join('\n') + '\n');
298
-
299
- return ok({
300
- refunded: true,
301
- requestId,
302
- refundSats,
303
- refundAddress,
304
- txid,
305
- note: `Refunded ${refundSats} sats to sender`,
306
- });
307
-
308
- } catch (err) {
309
- return fail(`Refund failed: ${(err as Error).message}`);
310
- }
311
- }
@@ -1,338 +0,0 @@
1
- /**
2
- * Baemail service handler - processes incoming paid messages.
3
- */
4
-
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
- import os from 'node:os';
8
- import { OVERLAY_URL, PATHS } from '../config.js';
9
- import { loadBaemailConfig, BaemailLogEntry } from './commands.js';
10
- import { signRelayMessage } from '../wallet/identity.js';
11
- import { verifyAndAcceptPayment } from '../messaging/handlers.js';
12
- import { fetchWithTimeout } from '../utils/woc.js';
13
- import { ensureStateDir } from '../utils/storage.js';
14
-
15
- // Dynamic SDK import
16
- let _sdk: any = null;
17
-
18
- async function getSdk(): Promise<any> {
19
- if (_sdk) return _sdk;
20
- try {
21
- _sdk = await import('@bsv/sdk');
22
- return _sdk;
23
- } catch {
24
- throw new Error('Cannot load @bsv/sdk');
25
- }
26
- }
27
-
28
- interface BaemailInput {
29
- message?: string;
30
- senderName?: string;
31
- replyIdentityKey?: string;
32
- }
33
-
34
- interface ServiceMessage {
35
- id: string;
36
- from: string;
37
- payload?: {
38
- input?: BaemailInput;
39
- payment?: any;
40
- };
41
- }
42
-
43
- interface ProcessResult {
44
- id: string;
45
- type: string;
46
- serviceId: string;
47
- action: string;
48
- tier?: string;
49
- deliverySuccess?: boolean;
50
- deliveryError?: string | null | undefined;
51
- paymentAccepted?: boolean;
52
- paymentTxid?: string;
53
- satoshisReceived?: number;
54
- from: string;
55
- ack: boolean;
56
- reason?: string | null;
57
- }
58
-
59
- /**
60
- * Process incoming baemail service request.
61
- */
62
- export async function processBaemail(
63
- msg: ServiceMessage,
64
- identityKey: string,
65
- privKey: any
66
- ): Promise<ProcessResult> {
67
- const input = (msg.payload?.input || msg.payload) as BaemailInput;
68
- const payment = msg.payload?.payment;
69
-
70
- // Load config
71
- const config = await loadBaemailConfig();
72
- if (!config) {
73
- const rejectPayload = {
74
- requestId: msg.id,
75
- serviceId: 'baemail',
76
- status: 'rejected',
77
- reason: 'Baemail service not configured on this agent.',
78
- };
79
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
80
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
81
- method: 'POST',
82
- headers: { 'Content-Type': 'application/json' },
83
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
84
- });
85
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'not configured', from: msg.from, ack: true };
86
- }
87
-
88
- // Check blocklist
89
- if (config.blocklist?.includes(msg.from)) {
90
- const rejectPayload = {
91
- requestId: msg.id,
92
- serviceId: 'baemail',
93
- status: 'rejected',
94
- reason: 'Sender is blocked.',
95
- };
96
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
97
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
98
- method: 'POST',
99
- headers: { 'Content-Type': 'application/json' },
100
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
101
- });
102
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'blocked', from: msg.from, ack: true };
103
- }
104
-
105
- // Validate message
106
- const message = input?.message;
107
- if (!message || typeof message !== 'string' || message.trim().length === 0) {
108
- const rejectPayload = {
109
- requestId: msg.id,
110
- serviceId: 'baemail',
111
- status: 'rejected',
112
- reason: 'Missing or empty message. Send {message: "your message"}',
113
- };
114
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
115
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
116
- method: 'POST',
117
- headers: { 'Content-Type': 'application/json' },
118
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
119
- });
120
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'missing message', from: msg.from, ack: true };
121
- }
122
-
123
- if (message.length > (config.maxMessageLength || 4000)) {
124
- const rejectPayload = {
125
- requestId: msg.id,
126
- serviceId: 'baemail',
127
- status: 'rejected',
128
- reason: `Message too long. Max ${config.maxMessageLength || 4000} characters.`,
129
- };
130
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
131
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
132
- method: 'POST',
133
- headers: { 'Content-Type': 'application/json' },
134
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
135
- });
136
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'message too long', from: msg.from, ack: true };
137
- }
138
-
139
- // Load wallet identity
140
- const sdk = await getSdk();
141
- const { PrivateKey, Hash } = sdk;
142
-
143
- let walletIdentity: any;
144
- try {
145
- walletIdentity = JSON.parse(fs.readFileSync(PATHS.walletIdentity, 'utf-8'));
146
- } catch (err) {
147
- const rejectPayload = {
148
- requestId: msg.id,
149
- serviceId: 'baemail',
150
- status: 'rejected',
151
- reason: 'Service temporarily unavailable (wallet error)',
152
- };
153
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
154
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
155
- method: 'POST',
156
- headers: { 'Content-Type': 'application/json' },
157
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
158
- });
159
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'wallet error', from: msg.from, ack: true };
160
- }
161
-
162
- // Sender info
163
- const senderName = input?.senderName || 'Anonymous';
164
- const replyKey = input?.replyIdentityKey || msg.from;
165
-
166
- // Check hooks configured
167
- let hookToken: string | null = null;
168
- let hookPort = 18789;
169
- const openclawConfigPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
170
-
171
- if (fs.existsSync(openclawConfigPath)) {
172
- try {
173
- const openclawConfig = JSON.parse(fs.readFileSync(openclawConfigPath, 'utf-8'));
174
- hookToken = openclawConfig?.hooks?.token;
175
- hookPort = openclawConfig?.gateway?.port || 18789;
176
- } catch {
177
- // Ignore parse errors
178
- }
179
- }
180
-
181
- if (!hookToken) {
182
- const rejectPayload = {
183
- requestId: msg.id,
184
- serviceId: 'baemail',
185
- status: 'rejected',
186
- reason: 'OpenClaw hooks not configured. Payment NOT accepted.',
187
- };
188
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
189
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
190
- method: 'POST',
191
- headers: { 'Content-Type': 'application/json' },
192
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
193
- });
194
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'hooks not configured', from: msg.from, ack: true };
195
- }
196
-
197
- // Verify and accept payment
198
- const ourHash160 = Hash.hash160(PrivateKey.fromHex(walletIdentity.rootKeyHex).toPublicKey().encode(true));
199
- const minPrice = config.tiers.standard;
200
-
201
- const payResult = await verifyAndAcceptPayment(payment, minPrice, msg.from, 'baemail', ourHash160);
202
-
203
- if (!payResult.accepted) {
204
- const rejectPayload = {
205
- requestId: msg.id,
206
- serviceId: 'baemail',
207
- status: 'rejected',
208
- reason: `Payment rejected: ${payResult.error}. Minimum: ${minPrice} sats.`,
209
- };
210
- const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
211
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
212
- method: 'POST',
213
- headers: { 'Content-Type': 'application/json' },
214
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
215
- });
216
- return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: payResult.error, from: msg.from, ack: true };
217
- }
218
-
219
- // Determine tier
220
- const paidSats = payResult.satoshis;
221
- let tier = 'standard';
222
- let tierEmoji = '📧';
223
- if (paidSats >= config.tiers.urgent) {
224
- tier = 'urgent';
225
- tierEmoji = '🚨';
226
- } else if (paidSats >= config.tiers.priority) {
227
- tier = 'priority';
228
- tierEmoji = '⚡';
229
- }
230
-
231
- // Format message
232
- const formattedMessage = `${tierEmoji} **Baemail** (${tier.toUpperCase()})
233
-
234
- **From:** ${senderName}
235
- **Paid:** ${paidSats} sats
236
- **Reply to:** \`${replyKey.slice(0, 16)}...\`
237
-
238
- ---
239
-
240
- ${message}
241
-
242
- ---
243
- _Reply via overlay: \`cli send ${replyKey} ping "your reply"\`_`;
244
-
245
- // Deliver via hooks
246
- let deliverySuccess = false;
247
- let deliveryError: string | null = null;
248
-
249
- try {
250
- const hookHost = (process as any)['en' + 'v'].OPENCLAW_HOST || (process as any)['en' + 'v'].OPENCLAW_HOST || '127.0.0.1';
251
- const hookUrl = `http://${hookHost}:${hookPort}/hooks/agent`;
252
- const hookResp = await fetchWithTimeout(hookUrl, {
253
- method: 'POST',
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- 'Authorization': `Bearer ${hookToken}`,
257
- 'x-openclaw-token': hookToken,
258
- },
259
- body: JSON.stringify({
260
- message: formattedMessage,
261
- name: 'Baemail',
262
- sessionKey: `baemail:${msg.id}`,
263
- wakeMode: 'now',
264
- deliver: true,
265
- channel: config.deliveryChannel,
266
- }),
267
- });
268
-
269
- if (hookResp.ok) {
270
- deliverySuccess = true;
271
- } else {
272
- const body = await hookResp.text().catch(() => '');
273
- deliveryError = `Hook failed: ${hookResp.status} ${body}`;
274
- }
275
- } catch (err) {
276
- deliveryError = (err as Error).message;
277
- }
278
-
279
- // Log delivery
280
- ensureStateDir();
281
- const logEntry: BaemailLogEntry = {
282
- requestId: msg.id,
283
- from: msg.from,
284
- senderName,
285
- tier,
286
- paidSats,
287
- messageLength: message.length,
288
- deliveryChannel: config.deliveryChannel,
289
- deliverySuccess,
290
- deliveryError: deliveryError ?? null,
291
- paymentTxid: payResult.txid || '',
292
- refundStatus: deliverySuccess ? undefined : 'pending',
293
- timestamp: new Date().toISOString(),
294
- };
295
- fs.appendFileSync(PATHS.baemailLog, JSON.stringify(logEntry) + '\n');
296
-
297
- // Send response
298
- const responsePayload = {
299
- requestId: msg.id,
300
- serviceId: 'baemail',
301
- status: deliverySuccess ? 'fulfilled' : 'delivery_failed',
302
- result: {
303
- delivered: deliverySuccess,
304
- tier,
305
- channel: config.deliveryChannel,
306
- paidSats,
307
- error: deliveryError,
308
- replyTo: identityKey,
309
- refundable: !deliverySuccess,
310
- note: deliverySuccess ? undefined : 'Delivery failed. Run: baemail-refund ' + msg.id,
311
- },
312
- paymentAccepted: true,
313
- paymentTxid: payResult.txid,
314
- satoshisReceived: payResult.satoshis,
315
- };
316
-
317
- const respSig = signRelayMessage(privKey, msg.from, 'service-response', responsePayload);
318
- await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
319
- method: 'POST',
320
- headers: { 'Content-Type': 'application/json' },
321
- body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: responsePayload, signature: respSig }),
322
- });
323
-
324
- return {
325
- id: msg.id,
326
- type: 'service-request',
327
- serviceId: 'baemail',
328
- action: deliverySuccess ? 'fulfilled' : 'delivery_failed',
329
- tier,
330
- deliverySuccess,
331
- deliveryError: deliveryError === null ? undefined : deliveryError,
332
- paymentAccepted: true,
333
- paymentTxid: payResult.txid || undefined,
334
- satoshisReceived: payResult.satoshis,
335
- from: msg.from,
336
- ack: true,
337
- };
338
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Baemail module exports - paid message forwarding service.
3
- */
4
-
5
- export * from './commands.js';
6
- export * from './handler.js';