@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.
Files changed (60) hide show
  1. package/README.md +139 -0
  2. package/dist/__tests__/buyer.test.d.ts +1 -0
  3. package/dist/__tests__/buyer.test.js +664 -0
  4. package/dist/__tests__/discovery.test.d.ts +1 -0
  5. package/dist/__tests__/discovery.test.js +188 -0
  6. package/dist/__tests__/escrow.test.d.ts +1 -0
  7. package/dist/__tests__/escrow.test.js +385 -0
  8. package/dist/__tests__/identity.test.d.ts +1 -0
  9. package/dist/__tests__/identity.test.js +222 -0
  10. package/dist/__tests__/integration.test.d.ts +1 -0
  11. package/dist/__tests__/integration.test.js +681 -0
  12. package/dist/__tests__/lockstep.test.d.ts +1 -0
  13. package/dist/__tests__/lockstep.test.js +320 -0
  14. package/dist/__tests__/messages.test.d.ts +1 -0
  15. package/dist/__tests__/messages.test.js +976 -0
  16. package/dist/__tests__/negotiation.test.d.ts +1 -0
  17. package/dist/__tests__/negotiation.test.js +667 -0
  18. package/dist/__tests__/seller.test.d.ts +1 -0
  19. package/dist/__tests__/seller.test.js +767 -0
  20. package/dist/__tests__/server.test.d.ts +1 -0
  21. package/dist/__tests__/server.test.js +239 -0
  22. package/dist/__tests__/signing.test.d.ts +1 -0
  23. package/dist/__tests__/signing.test.js +713 -0
  24. package/dist/__tests__/sla.test.d.ts +1 -0
  25. package/dist/__tests__/sla.test.js +342 -0
  26. package/dist/__tests__/transport.test.d.ts +1 -0
  27. package/dist/__tests__/transport.test.js +197 -0
  28. package/dist/__tests__/x402.test.d.ts +1 -0
  29. package/dist/__tests__/x402.test.js +141 -0
  30. package/dist/buyer.d.ts +190 -0
  31. package/dist/buyer.js +555 -0
  32. package/dist/discovery.d.ts +47 -0
  33. package/dist/discovery.js +51 -0
  34. package/dist/escrow.d.ts +177 -0
  35. package/dist/escrow.js +434 -0
  36. package/dist/identity.d.ts +60 -0
  37. package/dist/identity.js +108 -0
  38. package/dist/index.d.ts +122 -0
  39. package/dist/index.js +43 -0
  40. package/dist/lockstep.d.ts +94 -0
  41. package/dist/lockstep.js +127 -0
  42. package/dist/messages.d.ts +172 -0
  43. package/dist/messages.js +262 -0
  44. package/dist/negotiation.d.ts +113 -0
  45. package/dist/negotiation.js +214 -0
  46. package/dist/seller.d.ts +127 -0
  47. package/dist/seller.js +395 -0
  48. package/dist/server.d.ts +52 -0
  49. package/dist/server.js +149 -0
  50. package/dist/signing.d.ts +98 -0
  51. package/dist/signing.js +165 -0
  52. package/dist/sla.d.ts +95 -0
  53. package/dist/sla.js +187 -0
  54. package/dist/transport.d.ts +41 -0
  55. package/dist/transport.js +127 -0
  56. package/dist/types.d.ts +86 -0
  57. package/dist/types.js +1 -0
  58. package/dist/x402.d.ts +25 -0
  59. package/dist/x402.js +54 -0
  60. 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;
@@ -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
+ }