@ophirai/sdk 0.1.0
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 +139 -0
- package/dist/__tests__/buyer.test.d.ts +1 -0
- package/dist/__tests__/buyer.test.js +664 -0
- package/dist/__tests__/discovery.test.d.ts +1 -0
- package/dist/__tests__/discovery.test.js +188 -0
- package/dist/__tests__/escrow.test.d.ts +1 -0
- package/dist/__tests__/escrow.test.js +385 -0
- package/dist/__tests__/identity.test.d.ts +1 -0
- package/dist/__tests__/identity.test.js +222 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +681 -0
- package/dist/__tests__/lockstep.test.d.ts +1 -0
- package/dist/__tests__/lockstep.test.js +320 -0
- package/dist/__tests__/messages.test.d.ts +1 -0
- package/dist/__tests__/messages.test.js +976 -0
- package/dist/__tests__/negotiation.test.d.ts +1 -0
- package/dist/__tests__/negotiation.test.js +667 -0
- package/dist/__tests__/seller.test.d.ts +1 -0
- package/dist/__tests__/seller.test.js +767 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +239 -0
- package/dist/__tests__/signing.test.d.ts +1 -0
- package/dist/__tests__/signing.test.js +713 -0
- package/dist/__tests__/sla.test.d.ts +1 -0
- package/dist/__tests__/sla.test.js +342 -0
- package/dist/__tests__/transport.test.d.ts +1 -0
- package/dist/__tests__/transport.test.js +197 -0
- package/dist/__tests__/x402.test.d.ts +1 -0
- package/dist/__tests__/x402.test.js +141 -0
- package/dist/buyer.d.ts +190 -0
- package/dist/buyer.js +555 -0
- package/dist/discovery.d.ts +47 -0
- package/dist/discovery.js +51 -0
- package/dist/escrow.d.ts +177 -0
- package/dist/escrow.js +434 -0
- package/dist/identity.d.ts +60 -0
- package/dist/identity.js +108 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +43 -0
- package/dist/lockstep.d.ts +94 -0
- package/dist/lockstep.js +127 -0
- package/dist/messages.d.ts +172 -0
- package/dist/messages.js +262 -0
- package/dist/negotiation.d.ts +113 -0
- package/dist/negotiation.js +214 -0
- package/dist/seller.d.ts +127 -0
- package/dist/seller.js +395 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.js +149 -0
- package/dist/signing.d.ts +98 -0
- package/dist/signing.js +165 -0
- package/dist/sla.d.ts +95 -0
- package/dist/sla.js +187 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.js +127 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/dist/x402.d.ts +25 -0
- package/dist/x402.js +54 -0
- package/package.json +40 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { agreementToLockstepSpec, LockstepMonitor, } from '../lockstep.js';
|
|
3
|
+
function makeAgreement(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
agreement_id: 'agr_001',
|
|
6
|
+
rfq_id: 'rfq_001',
|
|
7
|
+
accepting_message_id: 'quote_001',
|
|
8
|
+
final_terms: {
|
|
9
|
+
price_per_unit: '0.005',
|
|
10
|
+
currency: 'USDC',
|
|
11
|
+
unit: 'request',
|
|
12
|
+
sla: {
|
|
13
|
+
metrics: [
|
|
14
|
+
{
|
|
15
|
+
name: 'uptime_pct',
|
|
16
|
+
target: 99.9,
|
|
17
|
+
comparison: 'gte',
|
|
18
|
+
measurement_method: 'rolling_average',
|
|
19
|
+
measurement_window: '24h',
|
|
20
|
+
penalty_per_violation: {
|
|
21
|
+
amount: '10.0',
|
|
22
|
+
currency: 'USDC',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'p99_latency_ms',
|
|
27
|
+
target: 200,
|
|
28
|
+
comparison: 'lte',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'accuracy_pct',
|
|
32
|
+
target: 95,
|
|
33
|
+
comparison: 'gte',
|
|
34
|
+
measurement_method: 'sampled',
|
|
35
|
+
measurement_window: '1h',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
agreement_hash: 'abc123def456',
|
|
41
|
+
buyer_signature: 'buyer_sig_base64',
|
|
42
|
+
seller_signature: 'seller_sig_base64',
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe('agreementToLockstepSpec', () => {
|
|
47
|
+
it('produces valid structure with spec_version and verification_mode', () => {
|
|
48
|
+
const agreement = makeAgreement();
|
|
49
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
50
|
+
expect(spec.spec_version).toBe('1.0');
|
|
51
|
+
expect(spec.verification_mode).toBe('continuous');
|
|
52
|
+
expect(spec.agent_id).toBe('agr_001');
|
|
53
|
+
});
|
|
54
|
+
it('maps all SLA metrics to behavioral requirements', () => {
|
|
55
|
+
const agreement = makeAgreement();
|
|
56
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
57
|
+
expect(spec.behavioral_requirements).toHaveLength(3);
|
|
58
|
+
const [uptime, latency, accuracy] = spec.behavioral_requirements;
|
|
59
|
+
expect(uptime.metric).toBe('uptime_pct');
|
|
60
|
+
expect(uptime.operator).toBe('gte');
|
|
61
|
+
expect(uptime.threshold).toBe(99.9);
|
|
62
|
+
expect(uptime.measurement_method).toBe('rolling_average');
|
|
63
|
+
expect(uptime.measurement_window).toBe('24h');
|
|
64
|
+
expect(latency.metric).toBe('p99_latency_ms');
|
|
65
|
+
expect(latency.operator).toBe('lte');
|
|
66
|
+
expect(latency.threshold).toBe(200);
|
|
67
|
+
expect(latency.measurement_method).toBe('rolling_average'); // default
|
|
68
|
+
expect(latency.measurement_window).toBe('1h'); // default
|
|
69
|
+
expect(accuracy.metric).toBe('accuracy_pct');
|
|
70
|
+
expect(accuracy.operator).toBe('gte');
|
|
71
|
+
expect(accuracy.threshold).toBe(95);
|
|
72
|
+
expect(accuracy.measurement_method).toBe('sampled');
|
|
73
|
+
expect(accuracy.measurement_window).toBe('1h');
|
|
74
|
+
});
|
|
75
|
+
it('uses custom_name for custom metrics', () => {
|
|
76
|
+
const agreement = makeAgreement({
|
|
77
|
+
final_terms: {
|
|
78
|
+
price_per_unit: '0.01',
|
|
79
|
+
currency: 'USDC',
|
|
80
|
+
unit: 'request',
|
|
81
|
+
sla: {
|
|
82
|
+
metrics: [
|
|
83
|
+
{
|
|
84
|
+
name: 'custom',
|
|
85
|
+
custom_name: 'gpu_utilization_pct',
|
|
86
|
+
target: 80,
|
|
87
|
+
comparison: 'gte',
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
94
|
+
expect(spec.behavioral_requirements).toHaveLength(1);
|
|
95
|
+
expect(spec.behavioral_requirements[0].metric).toBe('gpu_utilization_pct');
|
|
96
|
+
});
|
|
97
|
+
it('sets on_violation with escrow address when present', () => {
|
|
98
|
+
const agreement = makeAgreement({
|
|
99
|
+
escrow: {
|
|
100
|
+
address: 'EscrowPDA123abc',
|
|
101
|
+
txSignature: 'tx_sig_123',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
105
|
+
expect(spec.on_violation.action).toBe('trigger_dispute');
|
|
106
|
+
expect(spec.on_violation.escrow_address).toBe('EscrowPDA123abc');
|
|
107
|
+
});
|
|
108
|
+
it('sets penalty_rate from first metric penalty_per_violation', () => {
|
|
109
|
+
const agreement = makeAgreement();
|
|
110
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
111
|
+
expect(spec.on_violation.penalty_rate).toBe(10.0);
|
|
112
|
+
});
|
|
113
|
+
it('omits penalty_rate when no penalty_per_violation', () => {
|
|
114
|
+
const agreement = makeAgreement({
|
|
115
|
+
final_terms: {
|
|
116
|
+
price_per_unit: '0.01',
|
|
117
|
+
currency: 'USDC',
|
|
118
|
+
unit: 'request',
|
|
119
|
+
sla: {
|
|
120
|
+
metrics: [
|
|
121
|
+
{ name: 'uptime_pct', target: 99, comparison: 'gte' },
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
127
|
+
expect(spec.on_violation.penalty_rate).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
it('handles agreement with no SLA metrics', () => {
|
|
130
|
+
const agreement = makeAgreement({
|
|
131
|
+
final_terms: {
|
|
132
|
+
price_per_unit: '0.01',
|
|
133
|
+
currency: 'USDC',
|
|
134
|
+
unit: 'request',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
138
|
+
expect(spec.behavioral_requirements).toHaveLength(0);
|
|
139
|
+
expect(spec.on_violation.penalty_rate).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
it('omits escrow_address when no escrow on agreement', () => {
|
|
142
|
+
const agreement = makeAgreement();
|
|
143
|
+
delete agreement.escrow;
|
|
144
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
145
|
+
expect(spec.on_violation.escrow_address).toBeUndefined();
|
|
146
|
+
});
|
|
147
|
+
it('maps all 8 standard SLA metric names', () => {
|
|
148
|
+
const metricNames = [
|
|
149
|
+
'uptime_pct',
|
|
150
|
+
'p50_latency_ms',
|
|
151
|
+
'p99_latency_ms',
|
|
152
|
+
'accuracy_pct',
|
|
153
|
+
'throughput_rpm',
|
|
154
|
+
'error_rate_pct',
|
|
155
|
+
'time_to_first_byte_ms',
|
|
156
|
+
'custom',
|
|
157
|
+
];
|
|
158
|
+
const metrics = metricNames.map((name) => ({
|
|
159
|
+
name,
|
|
160
|
+
target: 99,
|
|
161
|
+
comparison: 'gte',
|
|
162
|
+
...(name === 'custom' ? { custom_name: 'my_metric' } : {}),
|
|
163
|
+
}));
|
|
164
|
+
const agreement = makeAgreement({
|
|
165
|
+
final_terms: {
|
|
166
|
+
price_per_unit: '0.01',
|
|
167
|
+
currency: 'USDC',
|
|
168
|
+
unit: 'request',
|
|
169
|
+
sla: { metrics },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const spec = agreementToLockstepSpec(agreement);
|
|
173
|
+
expect(spec.behavioral_requirements).toHaveLength(8);
|
|
174
|
+
const mapped = spec.behavioral_requirements.map((r) => r.metric);
|
|
175
|
+
expect(mapped).toContain('uptime_pct');
|
|
176
|
+
expect(mapped).toContain('p50_latency_ms');
|
|
177
|
+
expect(mapped).toContain('p99_latency_ms');
|
|
178
|
+
expect(mapped).toContain('accuracy_pct');
|
|
179
|
+
expect(mapped).toContain('throughput_rpm');
|
|
180
|
+
expect(mapped).toContain('error_rate_pct');
|
|
181
|
+
expect(mapped).toContain('time_to_first_byte_ms');
|
|
182
|
+
expect(mapped).toContain('my_metric'); // custom resolved to custom_name
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe('LockstepMonitor', () => {
|
|
186
|
+
const originalFetch = globalThis.fetch;
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
189
|
+
});
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
globalThis.fetch = originalFetch;
|
|
192
|
+
vi.restoreAllMocks();
|
|
193
|
+
});
|
|
194
|
+
it('defaults to https://api.lockstep.dev/v1 endpoint', () => {
|
|
195
|
+
const monitor = new LockstepMonitor();
|
|
196
|
+
// Verify by observing fetch URL during startMonitoring
|
|
197
|
+
const agreement = makeAgreement();
|
|
198
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('skip'));
|
|
199
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
200
|
+
monitor.startMonitoring(agreement);
|
|
201
|
+
expect(mockFetch).toHaveBeenCalledWith('https://api.lockstep.dev/v1/monitors', expect.any(Object));
|
|
202
|
+
});
|
|
203
|
+
it('uses custom verification endpoint', () => {
|
|
204
|
+
const monitor = new LockstepMonitor({
|
|
205
|
+
verificationEndpoint: 'http://localhost:9000',
|
|
206
|
+
});
|
|
207
|
+
const agreement = makeAgreement();
|
|
208
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('skip'));
|
|
209
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
210
|
+
monitor.startMonitoring(agreement);
|
|
211
|
+
expect(mockFetch).toHaveBeenCalledWith('http://localhost:9000/monitors', expect.any(Object));
|
|
212
|
+
});
|
|
213
|
+
it('startMonitoring returns a monitoringId', async () => {
|
|
214
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
215
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
216
|
+
const monitor = new LockstepMonitor();
|
|
217
|
+
const agreement = makeAgreement();
|
|
218
|
+
const result = await monitor.startMonitoring(agreement);
|
|
219
|
+
expect(result.monitoringId).toBe('mon_agr_001');
|
|
220
|
+
});
|
|
221
|
+
it('startMonitoring succeeds even when endpoint is unreachable', async () => {
|
|
222
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
223
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
224
|
+
const monitor = new LockstepMonitor();
|
|
225
|
+
const agreement = makeAgreement();
|
|
226
|
+
const result = await monitor.startMonitoring(agreement);
|
|
227
|
+
expect(result.monitoringId).toBe('mon_agr_001');
|
|
228
|
+
});
|
|
229
|
+
it('checkCompliance returns remote data when available', async () => {
|
|
230
|
+
const remoteResult = {
|
|
231
|
+
compliant: false,
|
|
232
|
+
violations: [
|
|
233
|
+
{ metric: 'uptime_pct', threshold: 99.9, observed: 98.5, timestamp: '2026-03-04T00:00:00Z' },
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
237
|
+
ok: true,
|
|
238
|
+
json: () => Promise.resolve(remoteResult),
|
|
239
|
+
});
|
|
240
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
241
|
+
const monitor = new LockstepMonitor();
|
|
242
|
+
const result = await monitor.checkCompliance('mon_agr_001');
|
|
243
|
+
expect(result.compliant).toBe(false);
|
|
244
|
+
expect(result.violations).toHaveLength(1);
|
|
245
|
+
expect(result.violations[0].metric).toBe('uptime_pct');
|
|
246
|
+
});
|
|
247
|
+
it('checkCompliance reports non-compliant when endpoint fails (safe default)', async () => {
|
|
248
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('timeout'));
|
|
249
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
250
|
+
const monitor = new LockstepMonitor();
|
|
251
|
+
const result = await monitor.checkCompliance('mon_agr_001');
|
|
252
|
+
// When the verification endpoint is unreachable, compliance is unknown.
|
|
253
|
+
// The safe default is to report non-compliant with no specific violations,
|
|
254
|
+
// so callers don't mistakenly treat unverified state as verified compliance.
|
|
255
|
+
expect(result.compliant).toBe(false);
|
|
256
|
+
expect(result.violations).toHaveLength(0);
|
|
257
|
+
});
|
|
258
|
+
it('checkCompliance reports non-compliant when response is not ok', async () => {
|
|
259
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: false, status: 500 });
|
|
260
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
261
|
+
const monitor = new LockstepMonitor();
|
|
262
|
+
const result = await monitor.checkCompliance('mon_agr_001');
|
|
263
|
+
expect(result.compliant).toBe(false);
|
|
264
|
+
expect(result.violations).toHaveLength(0);
|
|
265
|
+
});
|
|
266
|
+
it('triggerDispute returns remote result when available', async () => {
|
|
267
|
+
const remoteDispute = {
|
|
268
|
+
dispute_id: 'dispute_remote_123',
|
|
269
|
+
outcome: 'penalty_applied',
|
|
270
|
+
txSignature: 'tx_abc123',
|
|
271
|
+
};
|
|
272
|
+
const mockFetch = vi.fn()
|
|
273
|
+
.mockResolvedValueOnce({ ok: true }) // startMonitoring
|
|
274
|
+
.mockResolvedValueOnce({
|
|
275
|
+
ok: true,
|
|
276
|
+
json: () => Promise.resolve(remoteDispute),
|
|
277
|
+
});
|
|
278
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
279
|
+
const monitor = new LockstepMonitor();
|
|
280
|
+
await monitor.startMonitoring(makeAgreement());
|
|
281
|
+
const result = await monitor.triggerDispute('mon_agr_001', {
|
|
282
|
+
metric: 'uptime_pct',
|
|
283
|
+
threshold: 99.9,
|
|
284
|
+
observed: 98.5,
|
|
285
|
+
});
|
|
286
|
+
expect(result.dispute_id).toBe('dispute_remote_123');
|
|
287
|
+
expect(result.outcome).toBe('penalty_applied');
|
|
288
|
+
});
|
|
289
|
+
it('triggerDispute returns pending fallback when endpoint fails', async () => {
|
|
290
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error('unreachable'));
|
|
291
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
292
|
+
const monitor = new LockstepMonitor();
|
|
293
|
+
const result = await monitor.triggerDispute('mon_agr_001', {
|
|
294
|
+
metric: 'uptime_pct',
|
|
295
|
+
threshold: 99.9,
|
|
296
|
+
observed: 98.5,
|
|
297
|
+
});
|
|
298
|
+
expect(result.dispute_id).toMatch(/^dispute_mon_agr_001_/);
|
|
299
|
+
expect(result.outcome).toBe('pending');
|
|
300
|
+
});
|
|
301
|
+
it('triggerDispute sends spec and violation in body', async () => {
|
|
302
|
+
const mockFetch = vi.fn()
|
|
303
|
+
.mockResolvedValueOnce({ ok: true }) // startMonitoring
|
|
304
|
+
.mockResolvedValueOnce({ ok: false }); // triggerDispute fails, that's OK
|
|
305
|
+
vi.stubGlobal('fetch', mockFetch);
|
|
306
|
+
const monitor = new LockstepMonitor({
|
|
307
|
+
verificationEndpoint: 'http://local:8080',
|
|
308
|
+
});
|
|
309
|
+
const agreement = makeAgreement();
|
|
310
|
+
await monitor.startMonitoring(agreement);
|
|
311
|
+
const violation = { metric: 'p99_latency_ms', threshold: 200, observed: 500 };
|
|
312
|
+
await monitor.triggerDispute('mon_agr_001', violation);
|
|
313
|
+
const disputeCall = mockFetch.mock.calls[1];
|
|
314
|
+
expect(disputeCall[0]).toBe('http://local:8080/monitors/mon_agr_001/dispute');
|
|
315
|
+
const body = JSON.parse(disputeCall[1].body);
|
|
316
|
+
expect(body.violation).toEqual(violation);
|
|
317
|
+
expect(body.spec).toBeDefined();
|
|
318
|
+
expect(body.spec.agent_id).toBe('agr_001');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|