@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,141 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { agreementToX402Headers, parseX402Response } from '../x402.js';
3
+ function makeAgreement(overrides = {}) {
4
+ return {
5
+ agreement_id: 'agr_x402_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
+ },
13
+ agreement_hash: 'abcdef1234567890',
14
+ buyer_signature: 'buyer_sig',
15
+ seller_signature: 'seller_sig',
16
+ ...overrides,
17
+ };
18
+ }
19
+ describe('agreementToX402Headers', () => {
20
+ it('includes agreement ID in headers', () => {
21
+ const agreement = makeAgreement();
22
+ const headers = agreementToX402Headers(agreement);
23
+ expect(headers['X-Payment-Agreement-Id']).toBe('agr_x402_001');
24
+ });
25
+ it('includes price, currency, unit, and agreement hash', () => {
26
+ const agreement = makeAgreement();
27
+ const headers = agreementToX402Headers(agreement);
28
+ expect(headers['X-Payment-Amount']).toBe('0.005');
29
+ expect(headers['X-Payment-Currency']).toBe('USDC');
30
+ expect(headers['X-Payment-Unit']).toBe('request');
31
+ expect(headers['X-Payment-Agreement-Hash']).toBe('abcdef1234567890');
32
+ });
33
+ it('includes escrow network and deposit when final_terms has escrow', () => {
34
+ const agreement = makeAgreement({
35
+ final_terms: {
36
+ price_per_unit: '0.01',
37
+ currency: 'USDC',
38
+ unit: 'request',
39
+ escrow: {
40
+ network: 'solana',
41
+ deposit_amount: '100.0',
42
+ release_condition: 'sla_met',
43
+ },
44
+ },
45
+ });
46
+ const headers = agreementToX402Headers(agreement);
47
+ expect(headers['X-Payment-Network']).toBe('solana');
48
+ expect(headers['X-Payment-Escrow-Deposit']).toBe('100.0');
49
+ });
50
+ it('includes escrow address when agreement has escrow PDA', () => {
51
+ const agreement = makeAgreement({
52
+ escrow: {
53
+ address: 'EscrowPDA_base58address',
54
+ txSignature: 'tx_sig_123',
55
+ },
56
+ });
57
+ const headers = agreementToX402Headers(agreement);
58
+ expect(headers['X-Payment-Escrow-Address']).toBe('EscrowPDA_base58address');
59
+ });
60
+ it('omits escrow headers when no escrow configured', () => {
61
+ const agreement = makeAgreement();
62
+ const headers = agreementToX402Headers(agreement);
63
+ expect(headers['X-Payment-Network']).toBeUndefined();
64
+ expect(headers['X-Payment-Escrow-Deposit']).toBeUndefined();
65
+ expect(headers['X-Payment-Escrow-Address']).toBeUndefined();
66
+ });
67
+ it('returns all expected header keys', () => {
68
+ const agreement = makeAgreement({
69
+ final_terms: {
70
+ price_per_unit: '0.01',
71
+ currency: 'USDC',
72
+ unit: 'token',
73
+ escrow: {
74
+ network: 'solana',
75
+ deposit_amount: '50',
76
+ release_condition: 'job_complete',
77
+ },
78
+ },
79
+ escrow: {
80
+ address: 'PDA_addr',
81
+ txSignature: 'tx123',
82
+ },
83
+ });
84
+ const headers = agreementToX402Headers(agreement);
85
+ const keys = Object.keys(headers);
86
+ expect(keys).toContain('X-Payment-Amount');
87
+ expect(keys).toContain('X-Payment-Currency');
88
+ expect(keys).toContain('X-Payment-Agreement-Id');
89
+ expect(keys).toContain('X-Payment-Agreement-Hash');
90
+ expect(keys).toContain('X-Payment-Unit');
91
+ expect(keys).toContain('X-Payment-Network');
92
+ expect(keys).toContain('X-Payment-Escrow-Deposit');
93
+ expect(keys).toContain('X-Payment-Escrow-Address');
94
+ });
95
+ });
96
+ describe('parseX402Response', () => {
97
+ it('parses standard cased headers', () => {
98
+ const result = parseX402Response({
99
+ 'X-Payment-Amount': '0.005',
100
+ 'X-Payment-Currency': 'USDC',
101
+ 'X-Payment-Address': 'addr_123',
102
+ });
103
+ expect(result.price).toBe('0.005');
104
+ expect(result.currency).toBe('USDC');
105
+ expect(result.paymentAddress).toBe('addr_123');
106
+ });
107
+ it('parses lowercase headers', () => {
108
+ const result = parseX402Response({
109
+ 'x-payment-amount': '0.01',
110
+ 'x-payment-currency': 'SOL',
111
+ 'x-payment-address': 'sol_addr',
112
+ });
113
+ expect(result.price).toBe('0.01');
114
+ expect(result.currency).toBe('SOL');
115
+ expect(result.paymentAddress).toBe('sol_addr');
116
+ });
117
+ it('parses mixed-case headers', () => {
118
+ const result = parseX402Response({
119
+ 'X-PAYMENT-AMOUNT': '1.5',
120
+ 'X-Payment-currency': 'ETH',
121
+ 'x-Payment-Address': 'eth_addr',
122
+ });
123
+ expect(result.price).toBe('1.5');
124
+ expect(result.currency).toBe('ETH');
125
+ expect(result.paymentAddress).toBe('eth_addr');
126
+ });
127
+ it('returns defaults when headers are missing', () => {
128
+ const result = parseX402Response({});
129
+ expect(result.price).toBe('0');
130
+ expect(result.currency).toBe('USDC');
131
+ expect(result.paymentAddress).toBe('');
132
+ });
133
+ it('returns partial defaults for partially present headers', () => {
134
+ const result = parseX402Response({
135
+ 'X-Payment-Amount': '5.0',
136
+ });
137
+ expect(result.price).toBe('5.0');
138
+ expect(result.currency).toBe('USDC');
139
+ expect(result.paymentAddress).toBe('');
140
+ });
141
+ });
@@ -0,0 +1,190 @@
1
+ import type { QuoteParams, ServiceRequirement, BudgetConstraint, SLARequirement, ViolationEvidence } from '@ophirai/protocol';
2
+ import { NegotiationSession } from './negotiation.js';
3
+ import type { EscrowConfig, RankingFunction, SellerInfo, Agreement, DisputeResult } from './types.js';
4
+ /** Configuration for creating a BuyerAgent. */
5
+ export interface BuyerAgentConfig {
6
+ /** Optional Ed25519 keypair; auto-generated if omitted. */
7
+ keypair?: {
8
+ publicKey: Uint8Array;
9
+ secretKey: Uint8Array;
10
+ };
11
+ /** HTTP endpoint URL where this buyer listens for incoming quotes and counter-offers. */
12
+ endpoint: string;
13
+ /** Optional Solana escrow configuration for payment enforcement. */
14
+ escrowConfig?: EscrowConfig;
15
+ /** Optional Lockstep verification endpoint for SLA compliance monitoring. */
16
+ lockstepEndpoint?: string;
17
+ }
18
+ /**
19
+ * Buy-side negotiation agent. Sends RFQs, collects quotes, ranks them,
20
+ * and accepts/counters/rejects offers. Verifies seller signatures on all
21
+ * incoming messages.
22
+ */
23
+ export declare class BuyerAgent {
24
+ private keypair;
25
+ private agentId;
26
+ private endpoint;
27
+ private transport;
28
+ private server;
29
+ private sessions;
30
+ private quoteListeners;
31
+ /** Tracks processed message IDs within the replay window to reject duplicate/replayed messages. */
32
+ private seenMessageIds;
33
+ constructor(config: BuyerAgentConfig);
34
+ /** Check if a message ID has already been processed (replay protection).
35
+ * Records the ID if new; throws DUPLICATE_MESSAGE if already seen.
36
+ * Periodically evicts entries older than the replay protection window. */
37
+ private enforceNoDuplicate;
38
+ /** Register JSON-RPC handlers for Quote, Counter, and Accept methods.
39
+ * Each handler validates the message schema, verifies the sender's Ed25519
40
+ * signature, checks expiration, enforces replay protection, and updates the session state. */
41
+ private registerHandlers;
42
+ /** Resolve all pending waitForQuotes() promises for the given RFQ and clear the listener queue. */
43
+ private notifyQuoteListeners;
44
+ /** Discover seller agents matching a service category. Currently returns empty; use direct endpoints.
45
+ * @param _query - Search criteria containing a service category and optional requirements
46
+ * @returns An empty array (placeholder for future discovery integration)
47
+ * @example
48
+ * ```typescript
49
+ * const sellers = await buyer.discover({ category: 'llm-inference' });
50
+ * ```
51
+ */
52
+ discover(_query: {
53
+ category: string;
54
+ requirements?: Record<string, unknown>;
55
+ }): Promise<SellerInfo[]>;
56
+ /** Send an RFQ to one or more sellers and return the negotiation session.
57
+ * @param params - RFQ parameters including seller targets, service requirements, budget, and SLA
58
+ * @param params.sellers - Seller endpoints (strings) or SellerInfo objects to receive the RFQ
59
+ * @param params.service - The service requirement describing what the buyer needs
60
+ * @param params.budget - Budget constraint with maximum price and currency
61
+ * @param params.sla - Optional SLA requirements for the service
62
+ * @param params.maxRounds - Optional maximum number of negotiation rounds
63
+ * @param params.timeout - Optional TTL in milliseconds for the RFQ
64
+ * @returns The newly created NegotiationSession tracking this RFQ
65
+ * @throws {OphirError} When a non-network error occurs sending to a seller
66
+ * @example
67
+ * ```typescript
68
+ * const session = await buyer.requestQuotes({
69
+ * sellers: ['http://seller:3000'],
70
+ * service: { category: 'llm-inference', params: {} },
71
+ * budget: { max_price_per_unit: '0.01', currency: 'USDC' },
72
+ * });
73
+ * ```
74
+ */
75
+ requestQuotes(params: {
76
+ sellers: string[] | SellerInfo[];
77
+ service: ServiceRequirement;
78
+ budget: BudgetConstraint;
79
+ sla?: SLARequirement;
80
+ maxRounds?: number;
81
+ timeout?: number;
82
+ }): Promise<NegotiationSession>;
83
+ /** Wait for quotes to arrive, resolving when minQuotes are received or timeout elapses.
84
+ * @param session - The negotiation session to wait on
85
+ * @param options - Optional wait configuration
86
+ * @param options.minQuotes - Minimum number of quotes before resolving (default: 1)
87
+ * @param options.timeout - Maximum time to wait in milliseconds (default: 30000)
88
+ * @returns Array of quotes received so far when the condition is met or timeout elapses
89
+ * @example
90
+ * ```typescript
91
+ * const quotes = await buyer.waitForQuotes(session, { minQuotes: 2, timeout: 10000 });
92
+ * ```
93
+ */
94
+ waitForQuotes(session: NegotiationSession, options?: {
95
+ minQuotes?: number;
96
+ timeout?: number;
97
+ }): Promise<QuoteParams[]>;
98
+ /** Sort quotes by a ranking strategy (cheapest, fastest, best_sla, or custom function).
99
+ * @param quotes - Array of quotes to rank
100
+ * @param strategy - Ranking strategy: 'cheapest', 'fastest', 'best_sla', or a custom comparator (default: 'cheapest')
101
+ * @returns A new sorted array of quotes (best first)
102
+ * @example
103
+ * ```typescript
104
+ * const ranked = buyer.rankQuotes(quotes, 'fastest');
105
+ * const best = ranked[0];
106
+ * ```
107
+ */
108
+ rankQuotes(quotes: QuoteParams[], strategy?: 'cheapest' | 'fastest' | 'best_sla' | RankingFunction): QuoteParams[];
109
+ /** Compute a composite SLA quality score for ranking. Higher-is-better metrics (uptime, accuracy) add directly; lower-is-better metrics (latency, error rate) are inverted. */
110
+ private scoreSLA;
111
+ /** Accept a quote, creating a signed agreement with the seller. Verifies the seller's signature first.
112
+ * @param quote - The quote to accept, as received from a seller
113
+ * @returns A dual-signed Agreement containing final terms and both party signatures
114
+ * @throws {OphirError} When the seller's signature on the quote is invalid
115
+ * @throws {OphirError} When the seller's counter-signature on the accept is invalid
116
+ * @example
117
+ * ```typescript
118
+ * const agreement = await buyer.acceptQuote(quotes[0]);
119
+ * console.log(agreement.agreement_id);
120
+ * ```
121
+ */
122
+ acceptQuote(quote: QuoteParams): Promise<Agreement>;
123
+ /** Send a counter-offer proposing modified terms for a quote.
124
+ * @param quote - The original quote to counter
125
+ * @param modifications - Key-value map of proposed term changes (e.g., price, SLA targets)
126
+ * @param justification - Optional human-readable reason for the counter-offer
127
+ * @returns The updated NegotiationSession reflecting the new counter round
128
+ * @throws {OphirError} When no active session exists for the quote's RFQ ID
129
+ * @example
130
+ * ```typescript
131
+ * const session = await buyer.counter(quote, { price_per_unit: '0.008' }, 'Volume discount');
132
+ * ```
133
+ */
134
+ counter(quote: QuoteParams, modifications: Record<string, unknown>, justification?: string): Promise<NegotiationSession>;
135
+ /** Reject all quotes in a session and notify sellers.
136
+ * @param session - The negotiation session whose quotes should be rejected
137
+ * @param reason - Optional rejection reason sent to all sellers (default: 'Rejected by buyer')
138
+ * @returns Resolves when all rejection messages have been sent
139
+ * @throws {OphirError} When a non-network error occurs notifying a seller
140
+ * @example
141
+ * ```typescript
142
+ * await buyer.reject(session, 'Budget exceeded');
143
+ * ```
144
+ */
145
+ reject(session: NegotiationSession, reason?: string): Promise<void>;
146
+ /** File an SLA violation dispute against a seller for a given agreement.
147
+ * @param agreement - The agreement under which the violation occurred
148
+ * @param violation - Evidence of the SLA violation including metric name and observed value
149
+ * @returns A DisputeResult with the dispute ID and initial 'pending' outcome
150
+ * @throws {OphirError} When a non-network error occurs notifying the seller
151
+ * @example
152
+ * ```typescript
153
+ * const result = await buyer.dispute(agreement, {
154
+ * metric: 'uptime_pct', observed: 95.0, threshold: 99.9,
155
+ * });
156
+ * ```
157
+ */
158
+ dispute(agreement: Agreement, violation: ViolationEvidence): Promise<DisputeResult>;
159
+ /** Get a negotiation session by its RFQ ID.
160
+ * @param rfqId - The RFQ identifier to look up
161
+ * @returns The matching NegotiationSession, or undefined if not found
162
+ */
163
+ getSession(rfqId: string): NegotiationSession | undefined;
164
+ /** Get all active negotiation sessions.
165
+ * @returns Array of all NegotiationSession instances tracked by this agent
166
+ */
167
+ getSessions(): NegotiationSession[];
168
+ /** Start the HTTP server to receive quotes and counter-offers.
169
+ * @param port - Port number to listen on (default: 3001). Pass 0 for a random available port.
170
+ * @returns Resolves when the server is listening
171
+ * @example
172
+ * ```typescript
173
+ * await buyer.listen(0);
174
+ * console.log(buyer.getEndpoint());
175
+ * ```
176
+ */
177
+ listen(port?: number): Promise<void>;
178
+ /** Stop the HTTP server and close all connections.
179
+ * @returns Resolves when the server has been shut down
180
+ */
181
+ close(): Promise<void>;
182
+ /** Get this agent's did:key identifier.
183
+ * @returns The agent's decentralized identifier (did:key)
184
+ */
185
+ getAgentId(): string;
186
+ /** Get this agent's HTTP endpoint URL.
187
+ * @returns The endpoint URL string (updated after listen() binds a port)
188
+ */
189
+ getEndpoint(): string;
190
+ }