openclaw-overlay-plugin 0.7.22
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 +406 -0
- package/SKILL.md +78 -0
- package/clawdbot.plugin.json +106 -0
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +192 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +14 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +13 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.js +26 -0
- package/dist/core/payment.d.ts +16 -0
- package/dist/core/payment.js +94 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/types.js +4 -0
- package/dist/core/verify.d.ts +28 -0
- package/dist/core/verify.js +104 -0
- package/dist/core/wallet.d.ts +99 -0
- package/dist/core/wallet.js +219 -0
- package/dist/scripts/baemail/commands.d.ts +64 -0
- package/dist/scripts/baemail/commands.js +258 -0
- package/dist/scripts/baemail/handler.d.ts +36 -0
- package/dist/scripts/baemail/handler.js +284 -0
- package/dist/scripts/baemail/index.d.ts +5 -0
- package/dist/scripts/baemail/index.js +5 -0
- package/dist/scripts/config.d.ts +48 -0
- package/dist/scripts/config.js +68 -0
- package/dist/scripts/index.d.ts +7 -0
- package/dist/scripts/index.js +7 -0
- package/dist/scripts/messaging/connect.d.ts +8 -0
- package/dist/scripts/messaging/connect.js +114 -0
- package/dist/scripts/messaging/handlers.d.ts +21 -0
- package/dist/scripts/messaging/handlers.js +334 -0
- package/dist/scripts/messaging/inbox.d.ts +11 -0
- package/dist/scripts/messaging/inbox.js +51 -0
- package/dist/scripts/messaging/index.d.ts +8 -0
- package/dist/scripts/messaging/index.js +8 -0
- package/dist/scripts/messaging/poll.d.ts +7 -0
- package/dist/scripts/messaging/poll.js +52 -0
- package/dist/scripts/messaging/send.d.ts +7 -0
- package/dist/scripts/messaging/send.js +43 -0
- package/dist/scripts/output.d.ts +12 -0
- package/dist/scripts/output.js +19 -0
- package/dist/scripts/overlay/discover.d.ts +7 -0
- package/dist/scripts/overlay/discover.js +72 -0
- package/dist/scripts/overlay/index.d.ts +7 -0
- package/dist/scripts/overlay/index.js +7 -0
- package/dist/scripts/overlay/registration.d.ts +19 -0
- package/dist/scripts/overlay/registration.js +176 -0
- package/dist/scripts/overlay/services.d.ts +29 -0
- package/dist/scripts/overlay/services.js +167 -0
- package/dist/scripts/overlay/transaction.d.ts +42 -0
- package/dist/scripts/overlay/transaction.js +103 -0
- package/dist/scripts/payment/build.d.ts +24 -0
- package/dist/scripts/payment/build.js +54 -0
- package/dist/scripts/payment/commands.d.ts +15 -0
- package/dist/scripts/payment/commands.js +73 -0
- package/dist/scripts/payment/index.d.ts +6 -0
- package/dist/scripts/payment/index.js +6 -0
- package/dist/scripts/payment/types.d.ts +56 -0
- package/dist/scripts/payment/types.js +4 -0
- package/dist/scripts/services/index.d.ts +6 -0
- package/dist/scripts/services/index.js +6 -0
- package/dist/scripts/services/queue.d.ts +11 -0
- package/dist/scripts/services/queue.js +28 -0
- package/dist/scripts/services/request.d.ts +7 -0
- package/dist/scripts/services/request.js +82 -0
- package/dist/scripts/services/respond.d.ts +11 -0
- package/dist/scripts/services/respond.js +132 -0
- package/dist/scripts/types.d.ts +107 -0
- package/dist/scripts/types.js +4 -0
- package/dist/scripts/utils/index.d.ts +6 -0
- package/dist/scripts/utils/index.js +6 -0
- package/dist/scripts/utils/merkle.d.ts +12 -0
- package/dist/scripts/utils/merkle.js +47 -0
- package/dist/scripts/utils/storage.d.ts +66 -0
- package/dist/scripts/utils/storage.js +211 -0
- package/dist/scripts/utils/woc.d.ts +26 -0
- package/dist/scripts/utils/woc.js +91 -0
- package/dist/scripts/wallet/balance.d.ts +22 -0
- package/dist/scripts/wallet/balance.js +240 -0
- package/dist/scripts/wallet/identity.d.ts +70 -0
- package/dist/scripts/wallet/identity.js +151 -0
- package/dist/scripts/wallet/index.d.ts +6 -0
- package/dist/scripts/wallet/index.js +6 -0
- package/dist/scripts/wallet/setup.d.ts +15 -0
- package/dist/scripts/wallet/setup.js +105 -0
- package/dist/scripts/x-verification/commands.d.ts +27 -0
- package/dist/scripts/x-verification/commands.js +222 -0
- package/dist/scripts/x-verification/index.d.ts +4 -0
- package/dist/scripts/x-verification/index.js +4 -0
- package/dist/services/built-in/api-proxy/index.d.ts +6 -0
- package/dist/services/built-in/api-proxy/index.js +23 -0
- package/dist/services/built-in/code-develop/index.d.ts +6 -0
- package/dist/services/built-in/code-develop/index.js +23 -0
- package/dist/services/built-in/code-review/index.d.ts +10 -0
- package/dist/services/built-in/code-review/index.js +51 -0
- package/dist/services/built-in/image-analysis/index.d.ts +6 -0
- package/dist/services/built-in/image-analysis/index.js +33 -0
- package/dist/services/built-in/memory-store/index.d.ts +6 -0
- package/dist/services/built-in/memory-store/index.js +22 -0
- package/dist/services/built-in/roulette/index.d.ts +6 -0
- package/dist/services/built-in/roulette/index.js +27 -0
- package/dist/services/built-in/summarize/index.d.ts +6 -0
- package/dist/services/built-in/summarize/index.js +21 -0
- package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
- package/dist/services/built-in/tell-joke/handler.js +122 -0
- package/dist/services/built-in/tell-joke/index.d.ts +9 -0
- package/dist/services/built-in/tell-joke/index.js +31 -0
- package/dist/services/built-in/translate/index.d.ts +6 -0
- package/dist/services/built-in/translate/index.js +21 -0
- package/dist/services/built-in/web-research/index.d.ts +9 -0
- package/dist/services/built-in/web-research/index.js +51 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +14 -0
- package/dist/services/loader.d.ts +77 -0
- package/dist/services/loader.js +292 -0
- package/dist/services/manager.d.ts +86 -0
- package/dist/services/manager.js +255 -0
- package/dist/services/registry.d.ts +98 -0
- package/dist/services/registry.js +204 -0
- package/dist/services/types.d.ts +230 -0
- package/dist/services/types.js +30 -0
- package/dist/test/cli.test.d.ts +7 -0
- package/dist/test/cli.test.js +329 -0
- package/dist/test/comprehensive-overlay.test.d.ts +13 -0
- package/dist/test/comprehensive-overlay.test.js +593 -0
- package/dist/test/key-derivation.test.d.ts +12 -0
- package/dist/test/key-derivation.test.js +86 -0
- package/dist/test/overlay-submit.test.d.ts +10 -0
- package/dist/test/overlay-submit.test.js +460 -0
- package/dist/test/request-response-flow.test.d.ts +5 -0
- package/dist/test/request-response-flow.test.js +209 -0
- package/dist/test/service-system.test.d.ts +5 -0
- package/dist/test/service-system.test.js +190 -0
- package/dist/test/utils/server-logic.d.ts +98 -0
- package/dist/test/utils/server-logic.js +286 -0
- package/dist/test/wallet.test.d.ts +7 -0
- package/dist/test/wallet.test.js +146 -0
- package/index.ts +1965 -0
- package/openclaw.plugin.json +106 -0
- package/package.json +73 -0
- package/src/cli-main.ts +230 -0
- package/src/cli.ts +16 -0
- package/src/core/README.md +246 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +42 -0
- package/src/core/payment.ts +111 -0
- package/src/core/types.ts +102 -0
- package/src/core/verify.ts +119 -0
- package/src/core/wallet.ts +282 -0
- package/src/scripts/baemail/commands.ts +326 -0
- package/src/scripts/baemail/handler.ts +338 -0
- package/src/scripts/baemail/index.ts +6 -0
- package/src/scripts/config.ts +81 -0
- package/src/scripts/index.ts +8 -0
- package/src/scripts/messaging/connect.ts +121 -0
- package/src/scripts/messaging/handlers.ts +394 -0
- package/src/scripts/messaging/inbox.ts +64 -0
- package/src/scripts/messaging/index.ts +9 -0
- package/src/scripts/messaging/poll.ts +59 -0
- package/src/scripts/messaging/send.ts +54 -0
- package/src/scripts/output.ts +21 -0
- package/src/scripts/overlay/discover.ts +81 -0
- package/src/scripts/overlay/index.ts +8 -0
- package/src/scripts/overlay/registration.ts +199 -0
- package/src/scripts/overlay/services.ts +199 -0
- package/src/scripts/overlay/transaction.ts +124 -0
- package/src/scripts/payment/build.ts +65 -0
- package/src/scripts/payment/commands.ts +92 -0
- package/src/scripts/payment/index.ts +7 -0
- package/src/scripts/payment/types.ts +62 -0
- package/src/scripts/services/index.ts +7 -0
- package/src/scripts/services/queue.ts +35 -0
- package/src/scripts/services/request.ts +98 -0
- package/src/scripts/services/respond.ts +149 -0
- package/src/scripts/types.ts +121 -0
- package/src/scripts/utils/index.ts +7 -0
- package/src/scripts/utils/merkle.ts +57 -0
- package/src/scripts/utils/storage.ts +231 -0
- package/src/scripts/utils/woc.ts +106 -0
- package/src/scripts/wallet/balance.ts +277 -0
- package/src/scripts/wallet/identity.ts +203 -0
- package/src/scripts/wallet/index.ts +7 -0
- package/src/scripts/wallet/setup.ts +121 -0
- package/src/scripts/x-verification/commands.ts +256 -0
- package/src/scripts/x-verification/index.ts +5 -0
- package/src/services/built-in/api-proxy/index.ts +26 -0
- package/src/services/built-in/api-proxy/prompt.md +26 -0
- package/src/services/built-in/code-develop/index.ts +26 -0
- package/src/services/built-in/code-develop/prompt.md +35 -0
- package/src/services/built-in/code-review/index.ts +54 -0
- package/src/services/built-in/code-review/prompt.md +105 -0
- package/src/services/built-in/image-analysis/index.ts +36 -0
- package/src/services/built-in/image-analysis/prompt.md +42 -0
- package/src/services/built-in/memory-store/index.ts +25 -0
- package/src/services/built-in/memory-store/prompt.md +45 -0
- package/src/services/built-in/roulette/index.ts +30 -0
- package/src/services/built-in/roulette/prompt.md +35 -0
- package/src/services/built-in/summarize/index.ts +24 -0
- package/src/services/built-in/summarize/prompt.md +27 -0
- package/src/services/built-in/tell-joke/handler.ts +134 -0
- package/src/services/built-in/tell-joke/index.ts +34 -0
- package/src/services/built-in/tell-joke/prompt.md +59 -0
- package/src/services/built-in/translate/index.ts +24 -0
- package/src/services/built-in/translate/prompt.md +23 -0
- package/src/services/built-in/web-research/index.ts +54 -0
- package/src/services/built-in/web-research/prompt.md +110 -0
- package/src/services/index.ts +16 -0
- package/src/services/loader.ts +344 -0
- package/src/services/manager.ts +304 -0
- package/src/services/registry.ts +246 -0
- package/src/services/types.ts +259 -0
- package/src/test/cli.test.ts +352 -0
- package/src/test/comprehensive-overlay.test.ts +729 -0
- package/src/test/key-derivation.test.ts +102 -0
- package/src/test/overlay-submit.test.ts +570 -0
- package/src/test/request-response-flow.test.ts +252 -0
- package/src/test/service-system.test.ts +241 -0
- package/src/test/utils/server-logic.ts +368 -0
- package/src/test/wallet.test.ts +166 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service system comprehensive tests.
|
|
3
|
+
* Tests service registration, loading, validation, and execution.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { serviceRegistry, serviceLoader, serviceManager, initializeServiceSystem } from '../services/index.js';
|
|
9
|
+
import { ServiceCategory } from '../services/types.js';
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const projectRoot = path.resolve(__dirname, '..', '..');
|
|
12
|
+
// Test helper functions
|
|
13
|
+
function assert(condition, message) {
|
|
14
|
+
if (!condition) {
|
|
15
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function assertEquals(actual, expected, message) {
|
|
19
|
+
if (actual !== expected) {
|
|
20
|
+
throw new Error(`Assertion failed: ${message || ''} - Expected ${expected}, got ${actual}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
console.log('\n=== Service System Tests ===\n');
|
|
24
|
+
// Test 1: Service Registry Basic Operations
|
|
25
|
+
console.log('Testing service registry basic operations...');
|
|
26
|
+
// Create test service
|
|
27
|
+
const testService = {
|
|
28
|
+
id: 'test-service',
|
|
29
|
+
name: 'Test Service',
|
|
30
|
+
description: 'A test service for unit testing',
|
|
31
|
+
defaultPrice: 10,
|
|
32
|
+
category: ServiceCategory.UTILITY,
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
message: { type: 'string', description: 'Test message' }
|
|
37
|
+
},
|
|
38
|
+
required: ['message']
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
// Clear registry for clean test
|
|
42
|
+
serviceRegistry.clear();
|
|
43
|
+
// Test registration
|
|
44
|
+
serviceRegistry.register(testService);
|
|
45
|
+
assert(serviceRegistry.get('test-service') !== undefined, 'Service should be registered');
|
|
46
|
+
assertEquals(serviceRegistry.get('test-service')?.name, 'Test Service', 'Service name should match');
|
|
47
|
+
// Test listing
|
|
48
|
+
const services = serviceRegistry.list();
|
|
49
|
+
assert(services.length === 1, 'Should have one registered service');
|
|
50
|
+
assertEquals(services[0].id, 'test-service', 'Listed service should match registered service');
|
|
51
|
+
console.log('✅ Registry: basic operations work correctly');
|
|
52
|
+
// Test 2: Service Registry Validation
|
|
53
|
+
console.log('Testing service registry validation...');
|
|
54
|
+
// Test duplicate registration
|
|
55
|
+
try {
|
|
56
|
+
serviceRegistry.register(testService);
|
|
57
|
+
throw new Error('Should have thrown error for duplicate registration');
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
assert(error.message.includes('already registered'), 'Should reject duplicate registration');
|
|
61
|
+
}
|
|
62
|
+
// Test invalid service (missing required fields)
|
|
63
|
+
try {
|
|
64
|
+
serviceRegistry.register({
|
|
65
|
+
id: '',
|
|
66
|
+
name: 'Invalid',
|
|
67
|
+
description: 'Missing ID',
|
|
68
|
+
defaultPrice: 10
|
|
69
|
+
});
|
|
70
|
+
throw new Error('Should have thrown error for invalid service');
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
assert(error.message.includes('Service ID is required'), 'Should reject service without ID');
|
|
74
|
+
}
|
|
75
|
+
console.log('✅ Registry: validation works correctly');
|
|
76
|
+
// Test 3: Service Loader Directory Scanning
|
|
77
|
+
console.log('Testing service loader directory scanning...');
|
|
78
|
+
// Clear registry for clean test
|
|
79
|
+
serviceRegistry.clear();
|
|
80
|
+
// Load built-in services
|
|
81
|
+
const builtInServices = await serviceLoader.loadBuiltInServices();
|
|
82
|
+
assert(builtInServices.length > 0, 'Should load built-in services');
|
|
83
|
+
// Check that specific services are loaded
|
|
84
|
+
const expectedServices = ['tell-joke', 'api-proxy', 'summarize', 'memory-store', 'roulette', 'code-develop', 'image-analysis'];
|
|
85
|
+
for (const serviceId of expectedServices) {
|
|
86
|
+
const service = builtInServices.find(s => s.id === serviceId);
|
|
87
|
+
assert(service !== undefined, `Should load ${serviceId} service`);
|
|
88
|
+
assert(service.name.length > 0, `${serviceId} should have a name`);
|
|
89
|
+
assert(service.description.length > 0, `${serviceId} should have a description`);
|
|
90
|
+
assert(service.defaultPrice > 0, `${serviceId} should have a valid price`);
|
|
91
|
+
}
|
|
92
|
+
console.log('✅ Loader: directory scanning works correctly');
|
|
93
|
+
// Test 4: Service Manager Integration
|
|
94
|
+
console.log('Testing service manager integration...');
|
|
95
|
+
// Initialize service system
|
|
96
|
+
await initializeServiceSystem();
|
|
97
|
+
// Test service discovery
|
|
98
|
+
const joke = serviceManager.registry.get('tell-joke');
|
|
99
|
+
assert(joke !== undefined, 'Should find tell-joke service');
|
|
100
|
+
assertEquals(joke.name, 'Random Joke', 'Service name should match');
|
|
101
|
+
// Test validation with valid input
|
|
102
|
+
const validationResult = serviceManager.validate('tell-joke', { topic: 'programming' });
|
|
103
|
+
assert(validationResult.valid, 'Valid input should pass validation');
|
|
104
|
+
// Tell-joke service has no required fields, so empty object is valid
|
|
105
|
+
const emptyInputResult = serviceManager.validate('tell-joke', {});
|
|
106
|
+
assert(emptyInputResult.valid, 'Empty input should be valid for tell-joke service');
|
|
107
|
+
// Test with truly invalid input (wrong type)
|
|
108
|
+
const invalidResult = serviceManager.validate('tell-joke', { topic: 123 });
|
|
109
|
+
assert(!invalidResult.valid, 'Invalid input type should fail validation');
|
|
110
|
+
console.log('✅ Manager: integration works correctly');
|
|
111
|
+
// Test 5: Service Input Schema Validation
|
|
112
|
+
console.log('Testing service input schema validation...');
|
|
113
|
+
// Test api-proxy service validation
|
|
114
|
+
const apiProxy = serviceManager.registry.get('api-proxy');
|
|
115
|
+
assert(apiProxy !== undefined, 'Should find api-proxy service');
|
|
116
|
+
const validApiInput = { url: 'https://api.example.com/test', method: 'GET' };
|
|
117
|
+
const validApiResult = serviceManager.validate('api-proxy', validApiInput);
|
|
118
|
+
assert(validApiResult.valid, 'Valid api-proxy input should pass');
|
|
119
|
+
const invalidApiInput = { method: 'GET' }; // missing required url
|
|
120
|
+
const invalidApiResult = serviceManager.validate('api-proxy', invalidApiInput);
|
|
121
|
+
assert(!invalidApiResult.valid, 'Invalid api-proxy input should fail');
|
|
122
|
+
console.log('✅ Validation: input schema validation works correctly');
|
|
123
|
+
// Test 6: Service Categories
|
|
124
|
+
console.log('Testing service categories...');
|
|
125
|
+
// Check that services have appropriate categories
|
|
126
|
+
const categorizedServices = serviceManager.registry.list();
|
|
127
|
+
const categories = new Set(categorizedServices.map(s => s.category).filter(Boolean));
|
|
128
|
+
assert(categories.size > 0, 'Should have services with categories');
|
|
129
|
+
// Verify known categories
|
|
130
|
+
const expectedCategories = ['utility', 'ai', 'entertainment', 'development'];
|
|
131
|
+
for (const category of expectedCategories) {
|
|
132
|
+
const servicesInCategory = categorizedServices.filter(s => s.category === category);
|
|
133
|
+
assert(servicesInCategory.length > 0, `Should have services in ${category} category`);
|
|
134
|
+
}
|
|
135
|
+
console.log('✅ Categories: service categorization works correctly');
|
|
136
|
+
// Test 7: Service Loading Error Handling
|
|
137
|
+
console.log('Testing service loading error handling...');
|
|
138
|
+
// Test loading from non-existent directory
|
|
139
|
+
try {
|
|
140
|
+
await serviceLoader.loadFromDirectory('/non/existent/directory');
|
|
141
|
+
// Should not throw, but return empty array
|
|
142
|
+
console.log('✅ Loader: graceful handling of non-existent directory');
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// This is also acceptable behavior
|
|
146
|
+
console.log('✅ Loader: appropriate error handling for non-existent directory');
|
|
147
|
+
}
|
|
148
|
+
// Test 8: Service Prompt Files
|
|
149
|
+
console.log('Testing service prompt files...');
|
|
150
|
+
// Check that services with prompt files can be found
|
|
151
|
+
const servicesWithPrompts = categorizedServices.filter(s => s.promptFile);
|
|
152
|
+
assert(servicesWithPrompts.length > 0, 'Should have services with prompt files');
|
|
153
|
+
// Verify prompt files exist
|
|
154
|
+
for (const service of servicesWithPrompts.slice(0, 3)) { // Test first 3 to avoid excessive file I/O
|
|
155
|
+
if (service.promptFile) {
|
|
156
|
+
const promptPath = path.resolve(projectRoot, service.promptFile);
|
|
157
|
+
assert(fs.existsSync(promptPath), `Prompt file should exist: ${service.promptFile}`);
|
|
158
|
+
const content = fs.readFileSync(promptPath, 'utf-8');
|
|
159
|
+
assert(content.includes(service.id), `Prompt should reference service ID: ${service.id}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
console.log('✅ Prompts: service prompt files work correctly');
|
|
163
|
+
// Test 9: Service Registry Search and Filter
|
|
164
|
+
console.log('Testing service registry search and filter...');
|
|
165
|
+
// Test filtering by category
|
|
166
|
+
const aiServices = categorizedServices.filter(s => s.category === ServiceCategory.AI);
|
|
167
|
+
assert(aiServices.length > 0, 'Should have AI services');
|
|
168
|
+
const utilityServices = categorizedServices.filter(s => s.category === ServiceCategory.UTILITY);
|
|
169
|
+
assert(utilityServices.length > 0, 'Should have utility services');
|
|
170
|
+
// Test name search
|
|
171
|
+
const jokeServices = categorizedServices.filter(s => s.name.toLowerCase().includes('joke'));
|
|
172
|
+
assert(jokeServices.length > 0, 'Should find joke-related services');
|
|
173
|
+
console.log('✅ Search: service filtering works correctly');
|
|
174
|
+
// Test 10: Service System State Consistency
|
|
175
|
+
console.log('Testing service system state consistency...');
|
|
176
|
+
// Verify that registry and manager are in sync
|
|
177
|
+
const registryServices = serviceManager.registry.list();
|
|
178
|
+
const registryIds = new Set(registryServices.map(s => s.id));
|
|
179
|
+
// All services should be unique
|
|
180
|
+
assertEquals(registryIds.size, registryServices.length, 'All service IDs should be unique');
|
|
181
|
+
// All services should have valid pricing
|
|
182
|
+
for (const service of registryServices) {
|
|
183
|
+
assert(service.defaultPrice > 0, `Service ${service.id} should have valid price`);
|
|
184
|
+
assert(service.name.length > 0, `Service ${service.id} should have a name`);
|
|
185
|
+
assert(service.description.length > 0, `Service ${service.id} should have a description`);
|
|
186
|
+
}
|
|
187
|
+
console.log('✅ Consistency: service system state is consistent');
|
|
188
|
+
console.log('\n========================================');
|
|
189
|
+
console.log('Service System Tests completed: All tests passed!');
|
|
190
|
+
console.log('========================================\n');
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server validation logic mirrored from openclaw-overlay.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the exact same parsing and validation logic
|
|
5
|
+
* used by the server's topic managers, allowing us to validate
|
|
6
|
+
* client output before submission.
|
|
7
|
+
*
|
|
8
|
+
* Updated to use PushDrop tokens instead of plain OP_RETURN.
|
|
9
|
+
*/
|
|
10
|
+
import { Script, LockingScript } from '@bsv/sdk';
|
|
11
|
+
export declare const PROTOCOL_ID = "openclaw-overlay-v1";
|
|
12
|
+
export interface OpenclawIdentityData {
|
|
13
|
+
protocol: string;
|
|
14
|
+
type: 'identity';
|
|
15
|
+
identityKey: string;
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
channels: Record<string, string>;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
timestamp: string;
|
|
21
|
+
}
|
|
22
|
+
export interface OpenclawIdentityRevocationData {
|
|
23
|
+
protocol: string;
|
|
24
|
+
type: 'identity-revocation';
|
|
25
|
+
identityKey: string;
|
|
26
|
+
reason?: string;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
}
|
|
29
|
+
export interface OpenclawServiceData {
|
|
30
|
+
protocol: string;
|
|
31
|
+
type: 'service';
|
|
32
|
+
identityKey: string;
|
|
33
|
+
serviceId: string;
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
pricing: {
|
|
37
|
+
model: string;
|
|
38
|
+
amountSats: number;
|
|
39
|
+
};
|
|
40
|
+
timestamp: string;
|
|
41
|
+
}
|
|
42
|
+
export type OpenclawPayload = OpenclawIdentityData | OpenclawIdentityRevocationData | OpenclawServiceData;
|
|
43
|
+
export interface AdmittanceResult {
|
|
44
|
+
outputsToAdmit: number[];
|
|
45
|
+
coinsToRetain: number[];
|
|
46
|
+
}
|
|
47
|
+
export interface STEAKResponse {
|
|
48
|
+
[topic: string]: AdmittanceResult;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Extract data fields from a PushDrop script using the SDK's decode method.
|
|
52
|
+
* Returns the fields array or null if not a valid PushDrop script.
|
|
53
|
+
*/
|
|
54
|
+
export declare function extractPushDropFields(script: Script | LockingScript): number[][] | null;
|
|
55
|
+
/**
|
|
56
|
+
* Legacy function for backwards compatibility - extracts from OP_RETURN scripts.
|
|
57
|
+
* @deprecated Use extractPushDropFields instead
|
|
58
|
+
*/
|
|
59
|
+
export declare function extractOpReturnPushes(script: Script): Uint8Array[] | null;
|
|
60
|
+
/**
|
|
61
|
+
* Parse identity payload from a PushDrop script.
|
|
62
|
+
* The first field contains the JSON payload.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseIdentityOutput(script: Script | LockingScript): OpenclawIdentityData | null;
|
|
65
|
+
/**
|
|
66
|
+
* Parse identity revocation payload from a PushDrop script.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseRevocationOutput(script: Script | LockingScript): OpenclawIdentityRevocationData | null;
|
|
69
|
+
/**
|
|
70
|
+
* Parse service payload from a PushDrop script.
|
|
71
|
+
*/
|
|
72
|
+
export declare function parseServiceOutput(script: Script | LockingScript): OpenclawServiceData | null;
|
|
73
|
+
/**
|
|
74
|
+
* Simulate the identity topic manager's identifyAdmissibleOutputs.
|
|
75
|
+
*/
|
|
76
|
+
export declare function identifyIdentityOutputs(beef: number[]): AdmittanceResult;
|
|
77
|
+
/**
|
|
78
|
+
* Simulate the services topic manager's identifyAdmissibleOutputs.
|
|
79
|
+
*/
|
|
80
|
+
export declare function identifyServiceOutputs(beef: number[]): AdmittanceResult;
|
|
81
|
+
/**
|
|
82
|
+
* Validate BEEF format and structure.
|
|
83
|
+
*/
|
|
84
|
+
export declare function validateBeef(beef: number[]): {
|
|
85
|
+
valid: boolean;
|
|
86
|
+
error?: string;
|
|
87
|
+
version?: number;
|
|
88
|
+
txCount?: number;
|
|
89
|
+
hasProofs?: boolean;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Validate BEEF has proper ancestry chain.
|
|
93
|
+
*/
|
|
94
|
+
export declare function validateBeefAncestry(beef: number[]): {
|
|
95
|
+
valid: boolean;
|
|
96
|
+
error?: string;
|
|
97
|
+
chain?: string[];
|
|
98
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server validation logic mirrored from openclaw-overlay.
|
|
3
|
+
*
|
|
4
|
+
* This module contains the exact same parsing and validation logic
|
|
5
|
+
* used by the server's topic managers, allowing us to validate
|
|
6
|
+
* client output before submission.
|
|
7
|
+
*
|
|
8
|
+
* Updated to use PushDrop tokens instead of plain OP_RETURN.
|
|
9
|
+
*/
|
|
10
|
+
import { OP, Beef, PushDrop } from '@bsv/sdk';
|
|
11
|
+
export const PROTOCOL_ID = 'openclaw-overlay-v1';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Script Parsing using PushDrop.decode()
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Extract data fields from a PushDrop script using the SDK's decode method.
|
|
17
|
+
* Returns the fields array or null if not a valid PushDrop script.
|
|
18
|
+
*/
|
|
19
|
+
export function extractPushDropFields(script) {
|
|
20
|
+
try {
|
|
21
|
+
const decoded = PushDrop.decode(script);
|
|
22
|
+
return decoded.fields;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Legacy function for backwards compatibility - extracts from OP_RETURN scripts.
|
|
30
|
+
* @deprecated Use extractPushDropFields instead
|
|
31
|
+
*/
|
|
32
|
+
export function extractOpReturnPushes(script) {
|
|
33
|
+
const chunks = script.chunks;
|
|
34
|
+
// Legacy 4+ chunk format: OP_FALSE OP_RETURN <data> <data> ...
|
|
35
|
+
if (chunks.length >= 4 &&
|
|
36
|
+
chunks[0].op === OP.OP_FALSE &&
|
|
37
|
+
chunks[1].op === OP.OP_RETURN) {
|
|
38
|
+
const pushes = [];
|
|
39
|
+
for (let i = 2; i < chunks.length; i++) {
|
|
40
|
+
if (chunks[i].data) {
|
|
41
|
+
pushes.push(new Uint8Array(chunks[i].data));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return pushes;
|
|
45
|
+
}
|
|
46
|
+
// Collapsed 2-chunk format: OP_FALSE OP_RETURN with data blob
|
|
47
|
+
if (chunks.length === 2 &&
|
|
48
|
+
chunks[0].op === OP.OP_FALSE &&
|
|
49
|
+
chunks[1].op === OP.OP_RETURN &&
|
|
50
|
+
chunks[1].data) {
|
|
51
|
+
const blob = chunks[1].data;
|
|
52
|
+
const pushes = [];
|
|
53
|
+
let pos = 0;
|
|
54
|
+
while (pos < blob.length) {
|
|
55
|
+
const op = blob[pos++];
|
|
56
|
+
if (op > 0 && op <= 75) {
|
|
57
|
+
const end = Math.min(pos + op, blob.length);
|
|
58
|
+
pushes.push(new Uint8Array(blob.slice(pos, end)));
|
|
59
|
+
pos = end;
|
|
60
|
+
}
|
|
61
|
+
else if (op === 0x4c) {
|
|
62
|
+
const len = blob[pos++] ?? 0;
|
|
63
|
+
const end = Math.min(pos + len, blob.length);
|
|
64
|
+
pushes.push(new Uint8Array(blob.slice(pos, end)));
|
|
65
|
+
pos = end;
|
|
66
|
+
}
|
|
67
|
+
else if (op === 0x4d) {
|
|
68
|
+
const len = (blob[pos] ?? 0) | ((blob[pos + 1] ?? 0) << 8);
|
|
69
|
+
pos += 2;
|
|
70
|
+
const end = Math.min(pos + len, blob.length);
|
|
71
|
+
pushes.push(new Uint8Array(blob.slice(pos, end)));
|
|
72
|
+
pos = end;
|
|
73
|
+
}
|
|
74
|
+
else if (op === 0x4e) {
|
|
75
|
+
const len = ((blob[pos] ?? 0) |
|
|
76
|
+
((blob[pos + 1] ?? 0) << 8) |
|
|
77
|
+
((blob[pos + 2] ?? 0) << 16) |
|
|
78
|
+
((blob[pos + 3] ?? 0) << 24)) >>> 0;
|
|
79
|
+
pos += 4;
|
|
80
|
+
const end = Math.min(pos + len, blob.length);
|
|
81
|
+
pushes.push(new Uint8Array(blob.slice(pos, end)));
|
|
82
|
+
pos = end;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return pushes.length >= 2 ? pushes : null;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Payload Parsing - Updated for PushDrop
|
|
94
|
+
// ============================================================================
|
|
95
|
+
/**
|
|
96
|
+
* Parse identity payload from a PushDrop script.
|
|
97
|
+
* The first field contains the JSON payload.
|
|
98
|
+
*/
|
|
99
|
+
export function parseIdentityOutput(script) {
|
|
100
|
+
const fields = extractPushDropFields(script);
|
|
101
|
+
if (!fields || fields.length < 1)
|
|
102
|
+
return null;
|
|
103
|
+
try {
|
|
104
|
+
const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
105
|
+
// Server validation rules
|
|
106
|
+
if (payload.protocol !== PROTOCOL_ID)
|
|
107
|
+
return null;
|
|
108
|
+
if (payload.type !== 'identity')
|
|
109
|
+
return null;
|
|
110
|
+
if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
|
|
111
|
+
return null;
|
|
112
|
+
if (typeof payload.name !== 'string' || payload.name.length === 0)
|
|
113
|
+
return null;
|
|
114
|
+
if (!Array.isArray(payload.capabilities))
|
|
115
|
+
return null;
|
|
116
|
+
if (typeof payload.timestamp !== 'string')
|
|
117
|
+
return null;
|
|
118
|
+
return payload;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse identity revocation payload from a PushDrop script.
|
|
126
|
+
*/
|
|
127
|
+
export function parseRevocationOutput(script) {
|
|
128
|
+
const fields = extractPushDropFields(script);
|
|
129
|
+
if (!fields || fields.length < 1)
|
|
130
|
+
return null;
|
|
131
|
+
try {
|
|
132
|
+
const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
133
|
+
if (payload.protocol !== PROTOCOL_ID)
|
|
134
|
+
return null;
|
|
135
|
+
if (payload.type !== 'identity-revocation')
|
|
136
|
+
return null;
|
|
137
|
+
if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
|
|
138
|
+
return null;
|
|
139
|
+
if (typeof payload.timestamp !== 'string')
|
|
140
|
+
return null;
|
|
141
|
+
return payload;
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse service payload from a PushDrop script.
|
|
149
|
+
*/
|
|
150
|
+
export function parseServiceOutput(script) {
|
|
151
|
+
const fields = extractPushDropFields(script);
|
|
152
|
+
if (!fields || fields.length < 1)
|
|
153
|
+
return null;
|
|
154
|
+
try {
|
|
155
|
+
const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
156
|
+
if (payload.protocol !== PROTOCOL_ID)
|
|
157
|
+
return null;
|
|
158
|
+
if (payload.type !== 'service')
|
|
159
|
+
return null;
|
|
160
|
+
if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
|
|
161
|
+
return null;
|
|
162
|
+
if (typeof payload.serviceId !== 'string' || payload.serviceId.length === 0)
|
|
163
|
+
return null;
|
|
164
|
+
if (typeof payload.name !== 'string' || payload.name.length === 0)
|
|
165
|
+
return null;
|
|
166
|
+
if (!payload.pricing || typeof payload.pricing.amountSats !== 'number')
|
|
167
|
+
return null;
|
|
168
|
+
if (typeof payload.timestamp !== 'string')
|
|
169
|
+
return null;
|
|
170
|
+
return payload;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// ============================================================================
|
|
177
|
+
// Topic Manager Simulation
|
|
178
|
+
// ============================================================================
|
|
179
|
+
/**
|
|
180
|
+
* Simulate the identity topic manager's identifyAdmissibleOutputs.
|
|
181
|
+
*/
|
|
182
|
+
export function identifyIdentityOutputs(beef) {
|
|
183
|
+
const parsedBeef = Beef.fromBinary(beef);
|
|
184
|
+
const subjectTx = parsedBeef.txs[0]?._tx;
|
|
185
|
+
if (!subjectTx) {
|
|
186
|
+
return { outputsToAdmit: [], coinsToRetain: [] };
|
|
187
|
+
}
|
|
188
|
+
const outputsToAdmit = [];
|
|
189
|
+
for (let i = 0; i < subjectTx.outputs.length; i++) {
|
|
190
|
+
const output = subjectTx.outputs[i];
|
|
191
|
+
if (output.lockingScript) {
|
|
192
|
+
// Check identity
|
|
193
|
+
const identity = parseIdentityOutput(output.lockingScript);
|
|
194
|
+
if (identity !== null) {
|
|
195
|
+
outputsToAdmit.push(i);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// Check revocation
|
|
199
|
+
const revocation = parseRevocationOutput(output.lockingScript);
|
|
200
|
+
if (revocation !== null) {
|
|
201
|
+
outputsToAdmit.push(i);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return { outputsToAdmit, coinsToRetain: [] };
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Simulate the services topic manager's identifyAdmissibleOutputs.
|
|
209
|
+
*/
|
|
210
|
+
export function identifyServiceOutputs(beef) {
|
|
211
|
+
const parsedBeef = Beef.fromBinary(beef);
|
|
212
|
+
const subjectTx = parsedBeef.txs[0]?._tx;
|
|
213
|
+
if (!subjectTx) {
|
|
214
|
+
return { outputsToAdmit: [], coinsToRetain: [] };
|
|
215
|
+
}
|
|
216
|
+
const outputsToAdmit = [];
|
|
217
|
+
for (let i = 0; i < subjectTx.outputs.length; i++) {
|
|
218
|
+
const output = subjectTx.outputs[i];
|
|
219
|
+
if (output.lockingScript) {
|
|
220
|
+
const service = parseServiceOutput(output.lockingScript);
|
|
221
|
+
if (service !== null) {
|
|
222
|
+
outputsToAdmit.push(i);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return { outputsToAdmit, coinsToRetain: [] };
|
|
227
|
+
}
|
|
228
|
+
// ============================================================================
|
|
229
|
+
// BEEF Validation
|
|
230
|
+
// ============================================================================
|
|
231
|
+
/**
|
|
232
|
+
* Validate BEEF format and structure.
|
|
233
|
+
*/
|
|
234
|
+
export function validateBeef(beef) {
|
|
235
|
+
try {
|
|
236
|
+
// Check magic bytes
|
|
237
|
+
if (beef.length < 4) {
|
|
238
|
+
return { valid: false, error: 'BEEF too short' };
|
|
239
|
+
}
|
|
240
|
+
const magic = beef.slice(0, 4);
|
|
241
|
+
const magicHex = magic.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
242
|
+
if (magicHex !== '0100beef' && magicHex !== '0200beef') {
|
|
243
|
+
return { valid: false, error: `Invalid magic bytes: ${magicHex}` };
|
|
244
|
+
}
|
|
245
|
+
const version = magicHex === '0100beef' ? 1 : 2;
|
|
246
|
+
// Parse BEEF
|
|
247
|
+
const parsed = Beef.fromBinary(beef);
|
|
248
|
+
if (!parsed.txs || parsed.txs.length === 0) {
|
|
249
|
+
return { valid: false, error: 'BEEF contains no transactions' };
|
|
250
|
+
}
|
|
251
|
+
// Check for merkle proofs
|
|
252
|
+
const hasProofs = parsed.bumps && parsed.bumps.length > 0;
|
|
253
|
+
return {
|
|
254
|
+
valid: true,
|
|
255
|
+
version,
|
|
256
|
+
txCount: parsed.txs.length,
|
|
257
|
+
hasProofs,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
return { valid: false, error: e instanceof Error ? e.message : 'Unknown error' };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Validate BEEF has proper ancestry chain.
|
|
266
|
+
*/
|
|
267
|
+
export function validateBeefAncestry(beef) {
|
|
268
|
+
try {
|
|
269
|
+
const parsed = Beef.fromBinary(beef);
|
|
270
|
+
const chain = [];
|
|
271
|
+
for (const btx of parsed.txs) {
|
|
272
|
+
const txid = btx.txid || btx._tx?.id('hex');
|
|
273
|
+
if (txid)
|
|
274
|
+
chain.push(txid);
|
|
275
|
+
}
|
|
276
|
+
// Use Beef's built-in validation
|
|
277
|
+
const isValid = parsed.isValid(false);
|
|
278
|
+
if (!isValid) {
|
|
279
|
+
return { valid: false, error: 'BEEF ancestry chain is invalid', chain };
|
|
280
|
+
}
|
|
281
|
+
return { valid: true, chain };
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
return { valid: false, error: e instanceof Error ? e.message : 'Unknown error' };
|
|
285
|
+
}
|
|
286
|
+
}
|