@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
@@ -0,0 +1,41 @@
1
+ /** Configuration options for the JSON-RPC HTTP client. */
2
+ export interface JsonRpcClientConfig {
3
+ /** Request timeout in milliseconds (default: 30000). */
4
+ timeout?: number;
5
+ }
6
+ /**
7
+ * JSON-RPC 2.0 HTTP client for Ophir agent-to-agent communication.
8
+ * Handles request/response lifecycle, timeouts, and error mapping.
9
+ */
10
+ export declare class JsonRpcClient {
11
+ private timeout;
12
+ constructor(config?: JsonRpcClientConfig);
13
+ /** Send a JSON-RPC 2.0 request and return the typed result.
14
+ * @param endpoint - The URL of the remote JSON-RPC server
15
+ * @param method - The JSON-RPC method name to invoke
16
+ * @param params - The parameters object to include in the request
17
+ * @param id - Optional request ID; a random UUID is generated if omitted
18
+ * @returns The parsed result field from the JSON-RPC response
19
+ * @throws {OphirError} SELLER_UNREACHABLE on network/timeout/HTTP errors
20
+ * @throws {OphirError} INVALID_MESSAGE on malformed JSON or JSON-RPC error responses
21
+ * @example
22
+ * ```typescript
23
+ * const client = new JsonRpcClient();
24
+ * const result = await client.send<Quote>('https://agent.example/rpc', 'ophir.propose', { terms });
25
+ * ```
26
+ */
27
+ send<T>(endpoint: string, method: string, params: object, id?: string): Promise<T>;
28
+ /** Send a JSON-RPC 2.0 notification (fire-and-forget, no response expected).
29
+ * @param endpoint - The URL of the remote JSON-RPC server
30
+ * @param method - The JSON-RPC method name to invoke
31
+ * @param params - The parameters object to include in the notification
32
+ * @returns Resolves when the request has been sent
33
+ * @throws {OphirError} SELLER_UNREACHABLE on network or timeout errors
34
+ * @example
35
+ * ```typescript
36
+ * const client = new JsonRpcClient();
37
+ * await client.sendNotification('https://agent.example/rpc', 'ophir.cancel', { agreement_id });
38
+ * ```
39
+ */
40
+ sendNotification(endpoint: string, method: string, params: object): Promise<void>;
41
+ }
@@ -0,0 +1,127 @@
1
+ import { OphirError, OphirErrorCode } from '@ophirai/protocol';
2
+ const DEFAULT_TIMEOUT = 30_000;
3
+ const USER_AGENT = 'ophir-sdk/0.1.0';
4
+ /**
5
+ * JSON-RPC 2.0 HTTP client for Ophir agent-to-agent communication.
6
+ * Handles request/response lifecycle, timeouts, and error mapping.
7
+ */
8
+ export class JsonRpcClient {
9
+ timeout;
10
+ constructor(config) {
11
+ this.timeout = config?.timeout ?? DEFAULT_TIMEOUT;
12
+ }
13
+ /** Send a JSON-RPC 2.0 request and return the typed result.
14
+ * @param endpoint - The URL of the remote JSON-RPC server
15
+ * @param method - The JSON-RPC method name to invoke
16
+ * @param params - The parameters object to include in the request
17
+ * @param id - Optional request ID; a random UUID is generated if omitted
18
+ * @returns The parsed result field from the JSON-RPC response
19
+ * @throws {OphirError} SELLER_UNREACHABLE on network/timeout/HTTP errors
20
+ * @throws {OphirError} INVALID_MESSAGE on malformed JSON or JSON-RPC error responses
21
+ * @example
22
+ * ```typescript
23
+ * const client = new JsonRpcClient();
24
+ * const result = await client.send<Quote>('https://agent.example/rpc', 'ophir.propose', { terms });
25
+ * ```
26
+ */
27
+ async send(endpoint, method, params, id) {
28
+ if (!endpoint || typeof endpoint !== 'string') {
29
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, 'Endpoint must be a non-empty string');
30
+ }
31
+ const requestId = id ?? crypto.randomUUID();
32
+ const body = JSON.stringify({
33
+ jsonrpc: '2.0',
34
+ method,
35
+ params,
36
+ id: requestId,
37
+ });
38
+ const controller = new AbortController();
39
+ const timer = setTimeout(() => controller.abort(), this.timeout);
40
+ let response;
41
+ try {
42
+ response = await fetch(endpoint, {
43
+ method: 'POST',
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ 'User-Agent': USER_AGENT,
47
+ },
48
+ body,
49
+ signal: controller.signal,
50
+ });
51
+ }
52
+ catch (err) {
53
+ if (err instanceof Error && err.name === 'AbortError') {
54
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, `Request to ${endpoint} timed out after ${this.timeout}ms`);
55
+ }
56
+ const message = err instanceof Error ? err.message : String(err);
57
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, `Network error reaching ${endpoint}: ${message}`);
58
+ }
59
+ finally {
60
+ clearTimeout(timer);
61
+ }
62
+ if (!response.ok) {
63
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, `HTTP ${response.status} from ${endpoint}`, { status: response.status, statusText: response.statusText });
64
+ }
65
+ let json;
66
+ try {
67
+ json = await response.json();
68
+ }
69
+ catch {
70
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Invalid JSON response from ${endpoint}`);
71
+ }
72
+ // Validate response ID matches request ID to prevent response spoofing
73
+ if (json.id !== undefined && json.id !== requestId) {
74
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `JSON-RPC response ID mismatch: expected ${requestId}, got ${String(json.id)}`);
75
+ }
76
+ if (json.error && typeof json.error === 'object') {
77
+ const err = json.error;
78
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, err.message ?? 'JSON-RPC error', { code: err.code, data: err.data });
79
+ }
80
+ if (!('result' in json)) {
81
+ throw new OphirError(OphirErrorCode.INVALID_MESSAGE, `Missing 'result' field in JSON-RPC response from ${endpoint}`);
82
+ }
83
+ return json.result;
84
+ }
85
+ /** Send a JSON-RPC 2.0 notification (fire-and-forget, no response expected).
86
+ * @param endpoint - The URL of the remote JSON-RPC server
87
+ * @param method - The JSON-RPC method name to invoke
88
+ * @param params - The parameters object to include in the notification
89
+ * @returns Resolves when the request has been sent
90
+ * @throws {OphirError} SELLER_UNREACHABLE on network or timeout errors
91
+ * @example
92
+ * ```typescript
93
+ * const client = new JsonRpcClient();
94
+ * await client.sendNotification('https://agent.example/rpc', 'ophir.cancel', { agreement_id });
95
+ * ```
96
+ */
97
+ async sendNotification(endpoint, method, params) {
98
+ const body = JSON.stringify({
99
+ jsonrpc: '2.0',
100
+ method,
101
+ params,
102
+ });
103
+ const controller = new AbortController();
104
+ const timer = setTimeout(() => controller.abort(), this.timeout);
105
+ try {
106
+ await fetch(endpoint, {
107
+ method: 'POST',
108
+ headers: {
109
+ 'Content-Type': 'application/json',
110
+ 'User-Agent': USER_AGENT,
111
+ },
112
+ body,
113
+ signal: controller.signal,
114
+ });
115
+ }
116
+ catch (err) {
117
+ if (err instanceof Error && err.name === 'AbortError') {
118
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, `Notification to ${endpoint} timed out after ${this.timeout}ms`);
119
+ }
120
+ const message = err instanceof Error ? err.message : String(err);
121
+ throw new OphirError(OphirErrorCode.SELLER_UNREACHABLE, `Network error sending notification to ${endpoint}: ${message}`);
122
+ }
123
+ finally {
124
+ clearTimeout(timer);
125
+ }
126
+ }
127
+ }
@@ -0,0 +1,86 @@
1
+ import type { QuoteParams, FinalTerms } from '@ophirai/protocol';
2
+ /** Configuration for connecting to the Solana escrow program. */
3
+ export interface EscrowConfig {
4
+ /** Solana RPC endpoint URL (defaults to devnet). */
5
+ rpcUrl?: string;
6
+ /** Anchor program ID for the Ophir escrow program. */
7
+ programId?: string;
8
+ }
9
+ /** A service offering advertised by a seller agent. */
10
+ export interface ServiceOffering {
11
+ /** Service category (e.g. 'inference', 'translation', 'data_processing'). */
12
+ category: string;
13
+ /** Human-readable description of the service. */
14
+ description: string;
15
+ /** Base price per unit as a decimal string (e.g. '0.005'). */
16
+ base_price: string;
17
+ /** Payment currency (e.g. 'USDC'). */
18
+ currency: string;
19
+ /** Pricing unit (e.g. 'request', 'token', 'MB'). */
20
+ unit: string;
21
+ /** Maximum concurrent capacity, if limited. */
22
+ capacity?: number;
23
+ }
24
+ /** Pricing strategy for automated quote generation by seller agents. */
25
+ export interface PricingStrategy {
26
+ /** Strategy type: fixed base price, competitive (undercut), or dynamic. */
27
+ type: 'fixed' | 'competitive' | 'dynamic';
28
+ /** Margin adjustment factor (e.g. 0.1 for 10% margin). */
29
+ margin?: number;
30
+ }
31
+ /** Custom comparator function for ranking quotes. Return negative if `a` is preferred. */
32
+ export type RankingFunction = (a: QuoteParams, b: QuoteParams) => number;
33
+ /** Registration info for a known seller agent. */
34
+ export interface SellerInfo {
35
+ /** Seller's did:key identifier. */
36
+ agentId: string;
37
+ /** Seller's HTTP endpoint for receiving JSON-RPC messages. */
38
+ endpoint: string;
39
+ /** Services offered by this seller. */
40
+ services: ServiceOffering[];
41
+ }
42
+ /** Finalized agreement between buyer and seller, signed by both parties. */
43
+ export interface Agreement {
44
+ /** Unique agreement identifier. */
45
+ agreement_id: string;
46
+ /** RFQ that initiated this negotiation. */
47
+ rfq_id: string;
48
+ /** The quote or counter ID that was accepted. Stored so signatures can be independently verified. */
49
+ accepting_message_id: string;
50
+ /** Final agreed terms (price, currency, SLA, escrow). */
51
+ final_terms: FinalTerms;
52
+ /** SHA-256 hash of the canonicalized final terms. */
53
+ agreement_hash: string;
54
+ /** Buyer's Ed25519 signature over the unsigned accept payload (base64). */
55
+ buyer_signature: string;
56
+ /**
57
+ * Seller's Ed25519 counter-signature over the same unsigned accept payload (base64).
58
+ * Absent until the seller counter-signs the accept message.
59
+ */
60
+ seller_signature?: string;
61
+ /** On-chain escrow details, if funded. */
62
+ escrow?: {
63
+ /** Solana escrow PDA address (base58). */
64
+ address: string;
65
+ /** Transaction signature from escrow creation. */
66
+ txSignature: string;
67
+ };
68
+ }
69
+ /** Result of a dispute resolution. */
70
+ export interface DisputeResult {
71
+ /** Unique dispute identifier. */
72
+ dispute_id: string;
73
+ /** Current dispute outcome. */
74
+ outcome: 'penalty_applied' | 'dismissed' | 'pending';
75
+ /** Transaction signature if an on-chain action was taken. */
76
+ txSignature?: string;
77
+ }
78
+ /** Result of a completed job execution. */
79
+ export interface JobResult {
80
+ /** Agreement this job was executed under. */
81
+ agreement_id: string;
82
+ /** Job completion status. */
83
+ status: 'completed' | 'failed';
84
+ /** Observed SLA metrics from job execution. */
85
+ metrics?: Record<string, number>;
86
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/x402.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Agreement } from './types.js';
2
+ /** Generate x402-compatible payment headers from agreed terms.
3
+ * @param agreement - The finalized agreement containing price, currency, and escrow details
4
+ * @returns A record of X-Payment-* headers suitable for HTTP requests
5
+ * @example
6
+ * ```typescript
7
+ * const headers = agreementToX402Headers(agreement);
8
+ * const response = await fetch(url, { headers });
9
+ * ```
10
+ */
11
+ export declare function agreementToX402Headers(agreement: Agreement): Record<string, string>;
12
+ /** Parse x402 payment response headers into structured data.
13
+ * @param headers - The HTTP response headers (lookup is case-insensitive per HTTP spec)
14
+ * @returns Parsed payment details with price, currency, and paymentAddress
15
+ * @example
16
+ * ```typescript
17
+ * const payment = parseX402Response(response.headers);
18
+ * console.log(payment.price, payment.currency); // "0.01" "USDC"
19
+ * ```
20
+ */
21
+ export declare function parseX402Response(headers: Record<string, string>): {
22
+ price: string;
23
+ currency: string;
24
+ paymentAddress: string;
25
+ };
package/dist/x402.js ADDED
@@ -0,0 +1,54 @@
1
+ /** Generate x402-compatible payment headers from agreed terms.
2
+ * @param agreement - The finalized agreement containing price, currency, and escrow details
3
+ * @returns A record of X-Payment-* headers suitable for HTTP requests
4
+ * @example
5
+ * ```typescript
6
+ * const headers = agreementToX402Headers(agreement);
7
+ * const response = await fetch(url, { headers });
8
+ * ```
9
+ */
10
+ export function agreementToX402Headers(agreement) {
11
+ const terms = agreement.final_terms;
12
+ const headers = {
13
+ 'X-Payment-Amount': terms.price_per_unit,
14
+ 'X-Payment-Currency': terms.currency,
15
+ 'X-Payment-Agreement-Id': agreement.agreement_id,
16
+ 'X-Payment-Agreement-Hash': agreement.agreement_hash,
17
+ 'X-Payment-Unit': terms.unit,
18
+ };
19
+ if (terms.escrow) {
20
+ headers['X-Payment-Network'] = terms.escrow.network;
21
+ headers['X-Payment-Escrow-Deposit'] = terms.escrow.deposit_amount;
22
+ }
23
+ if (agreement.escrow?.address) {
24
+ headers['X-Payment-Escrow-Address'] = agreement.escrow.address;
25
+ }
26
+ return headers;
27
+ }
28
+ /**
29
+ * Look up a header value case-insensitively.
30
+ */
31
+ function getHeader(headers, name) {
32
+ const lower = name.toLowerCase();
33
+ for (const key of Object.keys(headers)) {
34
+ if (key.toLowerCase() === lower)
35
+ return headers[key];
36
+ }
37
+ return undefined;
38
+ }
39
+ /** Parse x402 payment response headers into structured data.
40
+ * @param headers - The HTTP response headers (lookup is case-insensitive per HTTP spec)
41
+ * @returns Parsed payment details with price, currency, and paymentAddress
42
+ * @example
43
+ * ```typescript
44
+ * const payment = parseX402Response(response.headers);
45
+ * console.log(payment.price, payment.currency); // "0.01" "USDC"
46
+ * ```
47
+ */
48
+ export function parseX402Response(headers) {
49
+ return {
50
+ price: getHeader(headers, 'X-Payment-Amount') ?? '0',
51
+ currency: getHeader(headers, 'X-Payment-Currency') ?? 'USDC',
52
+ paymentAddress: getHeader(headers, 'X-Payment-Address') ?? '',
53
+ };
54
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@ophirai/sdk",
3
+ "version": "0.1.0",
4
+ "description": "SDK for the Ophir Agent Negotiation Protocol — BuyerAgent, SellerAgent, Ed25519 signing, Solana escrow, and SLA enforcement",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/ophirai/ophir",
12
+ "directory": "packages/sdk"
13
+ },
14
+ "keywords": ["ai-agents", "negotiation", "sdk", "ed25519", "solana", "escrow", "sla", "agent-to-agent", "ophir", "did-key"],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "test": "vitest run",
18
+ "lint": "tsc --noEmit",
19
+ "clean": "rm -rf dist"
20
+ },
21
+ "files": ["dist", "README.md"],
22
+ "dependencies": {
23
+ "@ophirai/protocol": "*",
24
+ "@solana/web3.js": "^1.95.0",
25
+ "tweetnacl": "^1.0.3",
26
+ "bs58": "^6.0.0",
27
+ "json-stable-stringify": "^1.1.0",
28
+ "zod": "^3.23.0",
29
+ "express": "^4.18.0",
30
+ "uuid": "^9.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.5.0",
34
+ "vitest": "^2.0.0",
35
+ "@types/express": "^4.17.0",
36
+ "@types/json-stable-stringify": "^1.0.0",
37
+ "@types/uuid": "^10.0.0",
38
+ "tsx": "^4.0.0"
39
+ }
40
+ }