@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
package/dist/server.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { OphirError } from '@ophirai/protocol';
|
|
3
|
+
/**
|
|
4
|
+
* Express-based JSON-RPC 2.0 server for receiving Ophir negotiation messages.
|
|
5
|
+
* Dispatches incoming requests to registered method handlers.
|
|
6
|
+
*/
|
|
7
|
+
export class NegotiationServer {
|
|
8
|
+
app;
|
|
9
|
+
handlers = new Map();
|
|
10
|
+
server;
|
|
11
|
+
boundPort;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.app = express();
|
|
14
|
+
this.app.use(express.json());
|
|
15
|
+
this.app.post('/', async (req, res) => {
|
|
16
|
+
if (!req.body || typeof req.body !== 'object') {
|
|
17
|
+
res.status(400).json({
|
|
18
|
+
jsonrpc: '2.0',
|
|
19
|
+
id: null,
|
|
20
|
+
error: { code: -32700, message: 'Parse error: request body must be a JSON object' },
|
|
21
|
+
});
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { jsonrpc, method, params, id } = req.body;
|
|
25
|
+
if (jsonrpc !== '2.0') {
|
|
26
|
+
res.json({
|
|
27
|
+
jsonrpc: '2.0',
|
|
28
|
+
id: id ?? null,
|
|
29
|
+
error: { code: -32600, message: 'Invalid Request: jsonrpc must be "2.0"' },
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!method || typeof method !== 'string') {
|
|
34
|
+
res.json({
|
|
35
|
+
jsonrpc: '2.0',
|
|
36
|
+
id: id ?? null,
|
|
37
|
+
error: { code: -32600, message: 'Invalid Request: method must be a string' },
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const handler = this.handlers.get(method);
|
|
42
|
+
if (!handler) {
|
|
43
|
+
// Notifications (no id) get no response
|
|
44
|
+
if (id === undefined) {
|
|
45
|
+
res.status(204).end();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
res.json({
|
|
49
|
+
jsonrpc: '2.0',
|
|
50
|
+
id,
|
|
51
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const result = await handler(params);
|
|
57
|
+
// Notifications get no response
|
|
58
|
+
if (id === undefined) {
|
|
59
|
+
res.status(204).end();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
res.json({ jsonrpc: '2.0', id, result });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (id === undefined) {
|
|
66
|
+
res.status(204).end();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const isOphirError = err instanceof OphirError;
|
|
70
|
+
res.json({
|
|
71
|
+
jsonrpc: '2.0',
|
|
72
|
+
id,
|
|
73
|
+
error: {
|
|
74
|
+
code: isOphirError ? -32000 : -32603,
|
|
75
|
+
message: err instanceof Error ? err.message : 'Internal error',
|
|
76
|
+
data: isOphirError
|
|
77
|
+
? { ophir_code: err.code, ...err.data }
|
|
78
|
+
: undefined,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** Register an async handler for a JSON-RPC method name.
|
|
85
|
+
* @param method - The JSON-RPC method name to handle (e.g. "ophir.propose")
|
|
86
|
+
* @param handler - Async function that receives params and returns the result
|
|
87
|
+
* @returns void
|
|
88
|
+
* @example
|
|
89
|
+
* ```typescript
|
|
90
|
+
* server.handle('ophir.propose', async (params) => {
|
|
91
|
+
* return { accepted: true };
|
|
92
|
+
* });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
handle(method, handler) {
|
|
96
|
+
this.handlers.set(method, handler);
|
|
97
|
+
}
|
|
98
|
+
/** Start listening for JSON-RPC requests on the given port.
|
|
99
|
+
* @param port - The TCP port to bind to; pass 0 for an OS-assigned port
|
|
100
|
+
* @returns Resolves when the server is ready to accept connections
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* const server = new NegotiationServer();
|
|
104
|
+
* await server.listen(3000);
|
|
105
|
+
* console.log('Listening on port', server.getPort());
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
async listen(port) {
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
this.server = this.app.listen(port, () => {
|
|
111
|
+
if (this.server) {
|
|
112
|
+
const addr = this.server.address();
|
|
113
|
+
if (addr && typeof addr === 'object') {
|
|
114
|
+
this.boundPort = addr.port;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Get the actual bound port (useful when port 0 was passed to listen).
|
|
122
|
+
* @returns The bound port number, or undefined if the server is not listening
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* await server.listen(0);
|
|
126
|
+
* const port = server.getPort(); // e.g. 49152
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
getPort() {
|
|
130
|
+
return this.boundPort;
|
|
131
|
+
}
|
|
132
|
+
/** Stop the server and close all connections.
|
|
133
|
+
* @returns Resolves when the server has fully shut down
|
|
134
|
+
* @throws {Error} When the underlying HTTP server fails to close
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* await server.close();
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async close() {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
if (!this.server) {
|
|
143
|
+
resolve();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.server.close((err) => (err ? reject(err) : resolve()));
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { FinalTerms } from '@ophirai/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* JCS (RFC 8785) canonicalization using json-stable-stringify.
|
|
4
|
+
* Produces deterministic JSON output regardless of key insertion order.
|
|
5
|
+
* Handles nested objects, arrays, nulls, numbers, and booleans.
|
|
6
|
+
* Undefined values are excluded (standard JSON.stringify behavior).
|
|
7
|
+
*
|
|
8
|
+
* @param obj - The value to canonicalize. Must be a JSON-serializable value
|
|
9
|
+
* (object, array, string, number, boolean, or null). Throws on undefined,
|
|
10
|
+
* functions, or symbols.
|
|
11
|
+
* @returns A deterministic JSON string with sorted keys.
|
|
12
|
+
* @throws {OphirError} INVALID_MESSAGE if the input cannot be serialized
|
|
13
|
+
* (undefined, function, symbol, or circular reference).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const canonical = canonicalize({ z: 1, a: 2 });
|
|
18
|
+
* // '{"a":2,"z":1}'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare function canonicalize(obj: unknown): string;
|
|
22
|
+
/**
|
|
23
|
+
* Ed25519 sign canonical bytes. Returns base64-encoded signature.
|
|
24
|
+
*
|
|
25
|
+
* @param canonicalBytes - The data to sign as a Uint8Array.
|
|
26
|
+
* @param secretKey - The 64-byte Ed25519 secret key.
|
|
27
|
+
* @throws {OphirError} if canonicalBytes is not a Uint8Array.
|
|
28
|
+
* @throws {OphirError} if secretKey is not 64 bytes.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* const data = new TextEncoder().encode('hello ophir');
|
|
33
|
+
* const signature = sign(data, keypair.secretKey);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export declare function sign(canonicalBytes: Uint8Array, secretKey: Uint8Array): string;
|
|
37
|
+
/**
|
|
38
|
+
* Verify base64-encoded Ed25519 signature against public key.
|
|
39
|
+
* Returns false (never throws) on any invalid input.
|
|
40
|
+
*
|
|
41
|
+
* @param canonicalBytes - The data that was signed as a Uint8Array.
|
|
42
|
+
* @param signature - The base64-encoded signature string.
|
|
43
|
+
* @param publicKey - The 32-byte Ed25519 public key.
|
|
44
|
+
* @throws never
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const data = new TextEncoder().encode('hello ophir');
|
|
49
|
+
* const valid = verify(data, signature, keypair.publicKey);
|
|
50
|
+
* // true or false
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function verify(canonicalBytes: Uint8Array, signature: string, publicKey: Uint8Array): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* SHA-256 hash of canonicalized final terms, returned as hex string.
|
|
56
|
+
* Used as the agreement_hash that both parties commit to.
|
|
57
|
+
*
|
|
58
|
+
* Validates that finalTerms contains the required fields: price_per_unit, currency, and unit.
|
|
59
|
+
*
|
|
60
|
+
* @throws {OphirError} if finalTerms is missing required fields.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const hash = agreementHash({ price_per_unit: '0.01', currency: 'USDC', unit: 'request' });
|
|
65
|
+
* // '3a7f...' (64-char hex string)
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function agreementHash(finalTerms: FinalTerms): string;
|
|
69
|
+
/**
|
|
70
|
+
* Canonicalize params object, sign with Ed25519, return base64 signature.
|
|
71
|
+
*
|
|
72
|
+
* @param params - The object to canonicalize and sign. Must not be null or undefined.
|
|
73
|
+
* @param secretKey - The 64-byte Ed25519 secret key.
|
|
74
|
+
* @throws {OphirError} if params is null or undefined.
|
|
75
|
+
* @throws {OphirError} if secretKey is not 64 bytes.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const sig = signMessage({ rfq_id: 'rfq-001', price: '10.00' }, keypair.secretKey);
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function signMessage(params: unknown, secretKey: Uint8Array): string;
|
|
83
|
+
/**
|
|
84
|
+
* Canonicalize params object, verify Ed25519 signature.
|
|
85
|
+
* Returns false (never throws) on invalid signature or null/undefined params.
|
|
86
|
+
*
|
|
87
|
+
* @param params - The object that was signed.
|
|
88
|
+
* @param signature - The base64-encoded signature string.
|
|
89
|
+
* @param publicKey - The 32-byte Ed25519 public key.
|
|
90
|
+
* @throws never
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const valid = verifyMessage({ rfq_id: 'rfq-001', price: '10.00' }, sig, keypair.publicKey);
|
|
95
|
+
* // true or false
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export declare function verifyMessage(params: unknown, signature: string, publicKey: Uint8Array): boolean;
|
package/dist/signing.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import nacl from 'tweetnacl';
|
|
2
|
+
import stringify from 'json-stable-stringify';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { OphirError, OphirErrorCode } from '@ophirai/protocol';
|
|
5
|
+
/**
|
|
6
|
+
* JCS (RFC 8785) canonicalization using json-stable-stringify.
|
|
7
|
+
* Produces deterministic JSON output regardless of key insertion order.
|
|
8
|
+
* Handles nested objects, arrays, nulls, numbers, and booleans.
|
|
9
|
+
* Undefined values are excluded (standard JSON.stringify behavior).
|
|
10
|
+
*
|
|
11
|
+
* @param obj - The value to canonicalize. Must be a JSON-serializable value
|
|
12
|
+
* (object, array, string, number, boolean, or null). Throws on undefined,
|
|
13
|
+
* functions, or symbols.
|
|
14
|
+
* @returns A deterministic JSON string with sorted keys.
|
|
15
|
+
* @throws {OphirError} INVALID_MESSAGE if the input cannot be serialized
|
|
16
|
+
* (undefined, function, symbol, or circular reference).
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const canonical = canonicalize({ z: 1, a: 2 });
|
|
21
|
+
* // '{"a":2,"z":1}'
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function canonicalize(obj) {
|
|
25
|
+
if (obj === undefined) {
|
|
26
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'Cannot canonicalize undefined — value must be JSON-serializable');
|
|
27
|
+
}
|
|
28
|
+
if (typeof obj === 'function' || typeof obj === 'symbol') {
|
|
29
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Cannot canonicalize ${typeof obj} — value must be JSON-serializable`);
|
|
30
|
+
}
|
|
31
|
+
const result = stringify(obj);
|
|
32
|
+
if (result === undefined) {
|
|
33
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'Canonicalization failed — input may contain circular references');
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ed25519 sign canonical bytes. Returns base64-encoded signature.
|
|
39
|
+
*
|
|
40
|
+
* @param canonicalBytes - The data to sign as a Uint8Array.
|
|
41
|
+
* @param secretKey - The 64-byte Ed25519 secret key.
|
|
42
|
+
* @throws {OphirError} if canonicalBytes is not a Uint8Array.
|
|
43
|
+
* @throws {OphirError} if secretKey is not 64 bytes.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const data = new TextEncoder().encode('hello ophir');
|
|
48
|
+
* const signature = sign(data, keypair.secretKey);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export function sign(canonicalBytes, secretKey) {
|
|
52
|
+
if (!(canonicalBytes instanceof Uint8Array)) {
|
|
53
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'canonicalBytes must be a Uint8Array');
|
|
54
|
+
}
|
|
55
|
+
if (secretKey.length !== nacl.sign.secretKeyLength) {
|
|
56
|
+
throw new OphirError(OphirErrorCode.INVALID_SIGNATURE, `Invalid secret key length: expected ${nacl.sign.secretKeyLength}, got ${secretKey.length}`);
|
|
57
|
+
}
|
|
58
|
+
const sig = nacl.sign.detached(canonicalBytes, secretKey);
|
|
59
|
+
return Buffer.from(sig).toString('base64');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Verify base64-encoded Ed25519 signature against public key.
|
|
63
|
+
* Returns false (never throws) on any invalid input.
|
|
64
|
+
*
|
|
65
|
+
* @param canonicalBytes - The data that was signed as a Uint8Array.
|
|
66
|
+
* @param signature - The base64-encoded signature string.
|
|
67
|
+
* @param publicKey - The 32-byte Ed25519 public key.
|
|
68
|
+
* @throws never
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const data = new TextEncoder().encode('hello ophir');
|
|
73
|
+
* const valid = verify(data, signature, keypair.publicKey);
|
|
74
|
+
* // true or false
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function verify(canonicalBytes, signature, publicKey) {
|
|
78
|
+
try {
|
|
79
|
+
if (!(canonicalBytes instanceof Uint8Array)) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (publicKey.length !== nacl.sign.publicKeyLength) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const sigBytes = Buffer.from(signature, 'base64');
|
|
86
|
+
if (sigBytes.length !== nacl.sign.signatureLength) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
return nacl.sign.detached.verify(canonicalBytes, sigBytes, publicKey);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* SHA-256 hash of canonicalized final terms, returned as hex string.
|
|
97
|
+
* Used as the agreement_hash that both parties commit to.
|
|
98
|
+
*
|
|
99
|
+
* Validates that finalTerms contains the required fields: price_per_unit, currency, and unit.
|
|
100
|
+
*
|
|
101
|
+
* @throws {OphirError} if finalTerms is missing required fields.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```typescript
|
|
105
|
+
* const hash = agreementHash({ price_per_unit: '0.01', currency: 'USDC', unit: 'request' });
|
|
106
|
+
* // '3a7f...' (64-char hex string)
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function agreementHash(finalTerms) {
|
|
110
|
+
if (!finalTerms || typeof finalTerms !== 'object') {
|
|
111
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'finalTerms must be a non-null object');
|
|
112
|
+
}
|
|
113
|
+
if (!finalTerms.price_per_unit || !finalTerms.currency || !finalTerms.unit) {
|
|
114
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'finalTerms must contain price_per_unit, currency, and unit');
|
|
115
|
+
}
|
|
116
|
+
const canonical = canonicalize(finalTerms);
|
|
117
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Canonicalize params object, sign with Ed25519, return base64 signature.
|
|
121
|
+
*
|
|
122
|
+
* @param params - The object to canonicalize and sign. Must not be null or undefined.
|
|
123
|
+
* @param secretKey - The 64-byte Ed25519 secret key.
|
|
124
|
+
* @throws {OphirError} if params is null or undefined.
|
|
125
|
+
* @throws {OphirError} if secretKey is not 64 bytes.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const sig = signMessage({ rfq_id: 'rfq-001', price: '10.00' }, keypair.secretKey);
|
|
130
|
+
* ```
|
|
131
|
+
*/
|
|
132
|
+
export function signMessage(params, secretKey) {
|
|
133
|
+
if (params === null || params === undefined) {
|
|
134
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'params must not be null or undefined');
|
|
135
|
+
}
|
|
136
|
+
const canonical = canonicalize(params);
|
|
137
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
138
|
+
return sign(bytes, secretKey);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Canonicalize params object, verify Ed25519 signature.
|
|
142
|
+
* Returns false (never throws) on invalid signature or null/undefined params.
|
|
143
|
+
*
|
|
144
|
+
* @param params - The object that was signed.
|
|
145
|
+
* @param signature - The base64-encoded signature string.
|
|
146
|
+
* @param publicKey - The 32-byte Ed25519 public key.
|
|
147
|
+
* @throws never
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const valid = verifyMessage({ rfq_id: 'rfq-001', price: '10.00' }, sig, keypair.publicKey);
|
|
152
|
+
* // true or false
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
export function verifyMessage(params, signature, publicKey) {
|
|
156
|
+
if (!signature || typeof signature !== 'string') {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (params === null || params === undefined) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const canonical = canonicalize(params);
|
|
163
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
164
|
+
return verify(bytes, signature, publicKey);
|
|
165
|
+
}
|
package/dist/sla.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { SLARequirement } from '@ophirai/protocol';
|
|
2
|
+
/** Pre-built SLA templates for common AI service categories. */
|
|
3
|
+
export declare const SLA_TEMPLATES: {
|
|
4
|
+
readonly inference_realtime: () => SLARequirement;
|
|
5
|
+
readonly inference_batch: () => SLARequirement;
|
|
6
|
+
readonly data_processing: () => SLARequirement;
|
|
7
|
+
readonly code_generation: () => SLARequirement;
|
|
8
|
+
readonly translation: () => SLARequirement;
|
|
9
|
+
};
|
|
10
|
+
/** Per-metric comparison detail between two SLA offers. */
|
|
11
|
+
export interface SLAComparisonDetail {
|
|
12
|
+
metric: string;
|
|
13
|
+
a_value: number;
|
|
14
|
+
b_value: number;
|
|
15
|
+
better: 'a' | 'b' | 'tie';
|
|
16
|
+
}
|
|
17
|
+
/** Overall result of comparing two SLA requirements. */
|
|
18
|
+
export interface SLAComparisonResult {
|
|
19
|
+
winner: 'a' | 'b' | 'tie';
|
|
20
|
+
details: SLAComparisonDetail[];
|
|
21
|
+
}
|
|
22
|
+
/** Compare two SLA requirements metric-by-metric and determine which is better overall.
|
|
23
|
+
* @param a - First SLA requirement to compare
|
|
24
|
+
* @param b - Second SLA requirement to compare
|
|
25
|
+
* @returns Comparison result with per-metric details and an overall winner
|
|
26
|
+
* @throws {OphirError} When either SLA requirement is missing a metrics array
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* const result = compareSLAs(sellerSLA, buyerSLA);
|
|
30
|
+
* if (result.winner === 'a') console.log('Seller offers better terms');
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function compareSLAs(a: SLARequirement, b: SLARequirement): SLAComparisonResult;
|
|
34
|
+
/** An unmet SLA metric requirement with the gap between offered and required values. */
|
|
35
|
+
export interface SLAGap {
|
|
36
|
+
metric: string;
|
|
37
|
+
required: number;
|
|
38
|
+
offered: number;
|
|
39
|
+
gap: number;
|
|
40
|
+
}
|
|
41
|
+
/** Result of checking whether an offered SLA meets required thresholds. */
|
|
42
|
+
export interface SLAMeetsResult {
|
|
43
|
+
meets: boolean;
|
|
44
|
+
gaps: SLAGap[];
|
|
45
|
+
}
|
|
46
|
+
/** Check if an offered SLA meets all required metric thresholds.
|
|
47
|
+
* @param offered - The SLA being offered by the seller
|
|
48
|
+
* @param required - The SLA requirements demanded by the buyer
|
|
49
|
+
* @returns Whether all requirements are met, with detailed gaps for any unmet metrics
|
|
50
|
+
* @throws {OphirError} When either SLA requirement is missing a metrics array
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const { meets, gaps } = meetsSLARequirements(sellerSLA, buyerSLA);
|
|
54
|
+
* if (!meets) gaps.forEach(g => console.log(`${g.metric} off by ${g.gap}`));
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export declare function meetsSLARequirements(offered: SLARequirement, required: SLARequirement): SLAMeetsResult;
|
|
58
|
+
/** A Lockstep behavioral check derived from an SLA metric. */
|
|
59
|
+
export interface LockstepBehavioralCheck {
|
|
60
|
+
metric: string;
|
|
61
|
+
operator: string;
|
|
62
|
+
threshold: number;
|
|
63
|
+
measurement_method: string;
|
|
64
|
+
measurement_window: string;
|
|
65
|
+
}
|
|
66
|
+
/** Lockstep verification spec derived from SLA terms. */
|
|
67
|
+
export interface LockstepVerificationSpec {
|
|
68
|
+
version: string;
|
|
69
|
+
agreement_id: string;
|
|
70
|
+
agreement_hash: string;
|
|
71
|
+
behavioral_checks: LockstepBehavioralCheck[];
|
|
72
|
+
dispute_resolution: {
|
|
73
|
+
method: string;
|
|
74
|
+
timeout_hours?: number;
|
|
75
|
+
arbitrator?: string;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** Convert an SLA requirement into a Lockstep behavioral verification spec.
|
|
79
|
+
* @param sla - The SLA requirement containing metrics and dispute resolution terms
|
|
80
|
+
* @param agreement - The agreement identifiers to bind the spec to
|
|
81
|
+
* @param agreement.agreement_id - Unique identifier for the agreement
|
|
82
|
+
* @param agreement.agreement_hash - Content hash of the agreement for integrity verification
|
|
83
|
+
* @returns A Lockstep verification spec with behavioral checks derived from each SLA metric
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const spec = slaToLockstepSpec(sla, {
|
|
87
|
+
* agreement_id: 'agr_123',
|
|
88
|
+
* agreement_hash: '0xabc...',
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function slaToLockstepSpec(sla: SLARequirement, agreement: {
|
|
93
|
+
agreement_id: string;
|
|
94
|
+
agreement_hash: string;
|
|
95
|
+
}): LockstepVerificationSpec;
|
package/dist/sla.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { OphirError, OphirErrorCode } from '@ophirai/protocol';
|
|
2
|
+
/** Pre-built SLA templates for common AI service categories. */
|
|
3
|
+
export const SLA_TEMPLATES = {
|
|
4
|
+
inference_realtime: () => ({
|
|
5
|
+
metrics: [
|
|
6
|
+
{ name: 'p99_latency_ms', target: 500, comparison: 'lte' },
|
|
7
|
+
{ name: 'uptime_pct', target: 99.9, comparison: 'gte' },
|
|
8
|
+
{ name: 'accuracy_pct', target: 95, comparison: 'gte' },
|
|
9
|
+
],
|
|
10
|
+
dispute_resolution: { method: 'lockstep_verification', timeout_hours: 24 },
|
|
11
|
+
}),
|
|
12
|
+
inference_batch: () => ({
|
|
13
|
+
metrics: [
|
|
14
|
+
{ name: 'throughput_rpm', target: 1000, comparison: 'gte' },
|
|
15
|
+
{ name: 'accuracy_pct', target: 97, comparison: 'gte' },
|
|
16
|
+
{ name: 'error_rate_pct', target: 1, comparison: 'lte' },
|
|
17
|
+
],
|
|
18
|
+
dispute_resolution: { method: 'lockstep_verification', timeout_hours: 48 },
|
|
19
|
+
}),
|
|
20
|
+
data_processing: () => ({
|
|
21
|
+
metrics: [
|
|
22
|
+
{ name: 'throughput_rpm', target: 500, comparison: 'gte' },
|
|
23
|
+
{ name: 'uptime_pct', target: 99.5, comparison: 'gte' },
|
|
24
|
+
{ name: 'error_rate_pct', target: 2, comparison: 'lte' },
|
|
25
|
+
],
|
|
26
|
+
dispute_resolution: { method: 'automatic_escrow', timeout_hours: 24 },
|
|
27
|
+
}),
|
|
28
|
+
code_generation: () => ({
|
|
29
|
+
metrics: [
|
|
30
|
+
{ name: 'p99_latency_ms', target: 5000, comparison: 'lte' },
|
|
31
|
+
{ name: 'accuracy_pct', target: 90, comparison: 'gte' },
|
|
32
|
+
{ name: 'uptime_pct', target: 99, comparison: 'gte' },
|
|
33
|
+
],
|
|
34
|
+
dispute_resolution: { method: 'lockstep_verification', timeout_hours: 24 },
|
|
35
|
+
}),
|
|
36
|
+
translation: () => ({
|
|
37
|
+
metrics: [
|
|
38
|
+
{ name: 'accuracy_pct', target: 95, comparison: 'gte' },
|
|
39
|
+
{ name: 'p99_latency_ms', target: 3000, comparison: 'lte' },
|
|
40
|
+
{ name: 'uptime_pct', target: 99, comparison: 'gte' },
|
|
41
|
+
],
|
|
42
|
+
dispute_resolution: { method: 'lockstep_verification', timeout_hours: 24 },
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
function metricKey(m) {
|
|
46
|
+
return m.name === 'custom' && m.custom_name ? m.custom_name : m.name;
|
|
47
|
+
}
|
|
48
|
+
/** Compare two SLA requirements metric-by-metric and determine which is better overall.
|
|
49
|
+
* @param a - First SLA requirement to compare
|
|
50
|
+
* @param b - Second SLA requirement to compare
|
|
51
|
+
* @returns Comparison result with per-metric details and an overall winner
|
|
52
|
+
* @throws {OphirError} When either SLA requirement is missing a metrics array
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const result = compareSLAs(sellerSLA, buyerSLA);
|
|
56
|
+
* if (result.winner === 'a') console.log('Seller offers better terms');
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function compareSLAs(a, b) {
|
|
60
|
+
if (!a?.metrics || !b?.metrics) {
|
|
61
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'Both SLA requirements must have a metrics array');
|
|
62
|
+
}
|
|
63
|
+
const aMap = new Map(a.metrics.map((m) => [metricKey(m), m]));
|
|
64
|
+
const bMap = new Map(b.metrics.map((m) => [metricKey(m), m]));
|
|
65
|
+
const allKeys = new Set([...aMap.keys(), ...bMap.keys()]);
|
|
66
|
+
const details = [];
|
|
67
|
+
let aWins = 0;
|
|
68
|
+
let bWins = 0;
|
|
69
|
+
for (const key of allKeys) {
|
|
70
|
+
const am = aMap.get(key);
|
|
71
|
+
const bm = bMap.get(key);
|
|
72
|
+
if (!am || !bm)
|
|
73
|
+
continue;
|
|
74
|
+
const aVal = am.target;
|
|
75
|
+
const bVal = bm.target;
|
|
76
|
+
let better;
|
|
77
|
+
if (aVal === bVal) {
|
|
78
|
+
better = 'tie';
|
|
79
|
+
}
|
|
80
|
+
else if (am.comparison === 'lte') {
|
|
81
|
+
// Lower is better for lte metrics
|
|
82
|
+
better = aVal < bVal ? 'a' : 'b';
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Higher is better for gte/eq metrics
|
|
86
|
+
better = aVal > bVal ? 'a' : 'b';
|
|
87
|
+
}
|
|
88
|
+
if (better === 'a')
|
|
89
|
+
aWins++;
|
|
90
|
+
if (better === 'b')
|
|
91
|
+
bWins++;
|
|
92
|
+
details.push({ metric: key, a_value: aVal, b_value: bVal, better });
|
|
93
|
+
}
|
|
94
|
+
const winner = aWins > bWins ? 'a' : bWins > aWins ? 'b' : 'tie';
|
|
95
|
+
return { winner, details };
|
|
96
|
+
}
|
|
97
|
+
/** Check if an offered SLA meets all required metric thresholds.
|
|
98
|
+
* @param offered - The SLA being offered by the seller
|
|
99
|
+
* @param required - The SLA requirements demanded by the buyer
|
|
100
|
+
* @returns Whether all requirements are met, with detailed gaps for any unmet metrics
|
|
101
|
+
* @throws {OphirError} When either SLA requirement is missing a metrics array
|
|
102
|
+
* @example
|
|
103
|
+
* ```typescript
|
|
104
|
+
* const { meets, gaps } = meetsSLARequirements(sellerSLA, buyerSLA);
|
|
105
|
+
* if (!meets) gaps.forEach(g => console.log(`${g.metric} off by ${g.gap}`));
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function meetsSLARequirements(offered, required) {
|
|
109
|
+
if (!offered?.metrics || !required?.metrics) {
|
|
110
|
+
throw new OphirError(OphirErrorCode.INVALID_MESSAGE, 'Both SLA requirements must have a metrics array');
|
|
111
|
+
}
|
|
112
|
+
const offeredMap = new Map(offered.metrics.map((m) => [metricKey(m), m]));
|
|
113
|
+
const gaps = [];
|
|
114
|
+
for (const req of required.metrics) {
|
|
115
|
+
const key = metricKey(req);
|
|
116
|
+
const off = offeredMap.get(key);
|
|
117
|
+
if (!off) {
|
|
118
|
+
gaps.push({ metric: key, required: req.target, offered: 0, gap: req.target });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (req.comparison === 'lte') {
|
|
122
|
+
// Offered must be <= required target
|
|
123
|
+
if (off.target > req.target) {
|
|
124
|
+
gaps.push({
|
|
125
|
+
metric: key,
|
|
126
|
+
required: req.target,
|
|
127
|
+
offered: off.target,
|
|
128
|
+
gap: off.target - req.target,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else if (req.comparison === 'gte') {
|
|
133
|
+
// Offered must be >= required target
|
|
134
|
+
if (off.target < req.target) {
|
|
135
|
+
gaps.push({
|
|
136
|
+
metric: key,
|
|
137
|
+
required: req.target,
|
|
138
|
+
offered: off.target,
|
|
139
|
+
gap: req.target - off.target,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
else if (req.comparison === 'eq') {
|
|
144
|
+
if (off.target !== req.target) {
|
|
145
|
+
gaps.push({
|
|
146
|
+
metric: key,
|
|
147
|
+
required: req.target,
|
|
148
|
+
offered: off.target,
|
|
149
|
+
gap: Math.abs(req.target - off.target),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { meets: gaps.length === 0, gaps };
|
|
155
|
+
}
|
|
156
|
+
/** Convert an SLA requirement into a Lockstep behavioral verification spec.
|
|
157
|
+
* @param sla - The SLA requirement containing metrics and dispute resolution terms
|
|
158
|
+
* @param agreement - The agreement identifiers to bind the spec to
|
|
159
|
+
* @param agreement.agreement_id - Unique identifier for the agreement
|
|
160
|
+
* @param agreement.agreement_hash - Content hash of the agreement for integrity verification
|
|
161
|
+
* @returns A Lockstep verification spec with behavioral checks derived from each SLA metric
|
|
162
|
+
* @example
|
|
163
|
+
* ```typescript
|
|
164
|
+
* const spec = slaToLockstepSpec(sla, {
|
|
165
|
+
* agreement_id: 'agr_123',
|
|
166
|
+
* agreement_hash: '0xabc...',
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export function slaToLockstepSpec(sla, agreement) {
|
|
171
|
+
return {
|
|
172
|
+
version: '1.0',
|
|
173
|
+
agreement_id: agreement.agreement_id,
|
|
174
|
+
agreement_hash: agreement.agreement_hash,
|
|
175
|
+
behavioral_checks: sla.metrics.map((m) => ({
|
|
176
|
+
metric: metricKey(m),
|
|
177
|
+
operator: m.comparison,
|
|
178
|
+
threshold: m.target,
|
|
179
|
+
measurement_method: m.measurement_method ?? 'rolling_average',
|
|
180
|
+
measurement_window: m.measurement_window ?? '1h',
|
|
181
|
+
})),
|
|
182
|
+
dispute_resolution: sla.dispute_resolution ?? {
|
|
183
|
+
method: 'automatic_escrow',
|
|
184
|
+
timeout_hours: 24,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|