@ophirai/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/dist/__tests__/buyer.test.d.ts +1 -0
- package/dist/__tests__/buyer.test.js +664 -0
- package/dist/__tests__/discovery.test.d.ts +1 -0
- package/dist/__tests__/discovery.test.js +188 -0
- package/dist/__tests__/escrow.test.d.ts +1 -0
- package/dist/__tests__/escrow.test.js +385 -0
- package/dist/__tests__/identity.test.d.ts +1 -0
- package/dist/__tests__/identity.test.js +222 -0
- package/dist/__tests__/integration.test.d.ts +1 -0
- package/dist/__tests__/integration.test.js +681 -0
- package/dist/__tests__/lockstep.test.d.ts +1 -0
- package/dist/__tests__/lockstep.test.js +320 -0
- package/dist/__tests__/messages.test.d.ts +1 -0
- package/dist/__tests__/messages.test.js +976 -0
- package/dist/__tests__/negotiation.test.d.ts +1 -0
- package/dist/__tests__/negotiation.test.js +667 -0
- package/dist/__tests__/seller.test.d.ts +1 -0
- package/dist/__tests__/seller.test.js +767 -0
- package/dist/__tests__/server.test.d.ts +1 -0
- package/dist/__tests__/server.test.js +239 -0
- package/dist/__tests__/signing.test.d.ts +1 -0
- package/dist/__tests__/signing.test.js +713 -0
- package/dist/__tests__/sla.test.d.ts +1 -0
- package/dist/__tests__/sla.test.js +342 -0
- package/dist/__tests__/transport.test.d.ts +1 -0
- package/dist/__tests__/transport.test.js +197 -0
- package/dist/__tests__/x402.test.d.ts +1 -0
- package/dist/__tests__/x402.test.js +141 -0
- package/dist/buyer.d.ts +190 -0
- package/dist/buyer.js +555 -0
- package/dist/discovery.d.ts +47 -0
- package/dist/discovery.js +51 -0
- package/dist/escrow.d.ts +177 -0
- package/dist/escrow.js +434 -0
- package/dist/identity.d.ts +60 -0
- package/dist/identity.js +108 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +43 -0
- package/dist/lockstep.d.ts +94 -0
- package/dist/lockstep.js +127 -0
- package/dist/messages.d.ts +172 -0
- package/dist/messages.js +262 -0
- package/dist/negotiation.d.ts +113 -0
- package/dist/negotiation.js +214 -0
- package/dist/seller.d.ts +127 -0
- package/dist/seller.js +395 -0
- package/dist/server.d.ts +52 -0
- package/dist/server.js +149 -0
- package/dist/signing.d.ts +98 -0
- package/dist/signing.js +165 -0
- package/dist/sla.d.ts +95 -0
- package/dist/sla.js +187 -0
- package/dist/transport.d.ts +41 -0
- package/dist/transport.js +127 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +1 -0
- package/dist/x402.d.ts +25 -0
- package/dist/x402.js +54 -0
- package/package.json +40 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import bs58 from 'bs58';
|
|
3
|
+
import { generateKeyPair, publicKeyToDid, didToPublicKey, generateAgentIdentity, } from '../identity.js';
|
|
4
|
+
import { OphirError, OphirErrorCode } from '@ophirai/protocol';
|
|
5
|
+
describe('generateKeyPair', () => {
|
|
6
|
+
it('returns publicKey of 32 bytes', () => {
|
|
7
|
+
const kp = generateKeyPair();
|
|
8
|
+
expect(kp.publicKey).toHaveLength(32);
|
|
9
|
+
});
|
|
10
|
+
it('returns secretKey of 64 bytes', () => {
|
|
11
|
+
const kp = generateKeyPair();
|
|
12
|
+
expect(kp.secretKey).toHaveLength(64);
|
|
13
|
+
});
|
|
14
|
+
it('publicKey is a Uint8Array', () => {
|
|
15
|
+
const kp = generateKeyPair();
|
|
16
|
+
expect(kp.publicKey).toBeInstanceOf(Uint8Array);
|
|
17
|
+
});
|
|
18
|
+
it('secretKey is a Uint8Array', () => {
|
|
19
|
+
const kp = generateKeyPair();
|
|
20
|
+
expect(kp.secretKey).toBeInstanceOf(Uint8Array);
|
|
21
|
+
});
|
|
22
|
+
it('two calls produce different public keys (entropy check)', () => {
|
|
23
|
+
const kp1 = generateKeyPair();
|
|
24
|
+
const kp2 = generateKeyPair();
|
|
25
|
+
expect(Buffer.from(kp1.publicKey)).not.toEqual(Buffer.from(kp2.publicKey));
|
|
26
|
+
});
|
|
27
|
+
it('two calls produce different secret keys', () => {
|
|
28
|
+
const kp1 = generateKeyPair();
|
|
29
|
+
const kp2 = generateKeyPair();
|
|
30
|
+
expect(Buffer.from(kp1.secretKey)).not.toEqual(Buffer.from(kp2.secretKey));
|
|
31
|
+
});
|
|
32
|
+
it('ten consecutive keypairs are all unique', () => {
|
|
33
|
+
const keys = Array.from({ length: 10 }, () => generateKeyPair());
|
|
34
|
+
const publicKeyHexes = keys.map((kp) => Buffer.from(kp.publicKey).toString('hex'));
|
|
35
|
+
const uniqueKeys = new Set(publicKeyHexes);
|
|
36
|
+
expect(uniqueKeys.size).toBe(10);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('publicKeyToDid', () => {
|
|
40
|
+
it('valid 32-byte key produces a DID starting with did:key:z', () => {
|
|
41
|
+
const kp = generateKeyPair();
|
|
42
|
+
const did = publicKeyToDid(kp.publicKey);
|
|
43
|
+
expect(did.startsWith('did:key:z')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('valid key produces DID matching did:key:z6Mk prefix', () => {
|
|
46
|
+
const kp = generateKeyPair();
|
|
47
|
+
const did = publicKeyToDid(kp.publicKey);
|
|
48
|
+
expect(did).toMatch(/^did:key:z6Mk/);
|
|
49
|
+
});
|
|
50
|
+
it('all-zero 32-byte key produces a valid DID', () => {
|
|
51
|
+
const zeroKey = new Uint8Array(32);
|
|
52
|
+
const did = publicKeyToDid(zeroKey);
|
|
53
|
+
expect(did.startsWith('did:key:z')).toBe(true);
|
|
54
|
+
expect(did.length).toBeGreaterThan('did:key:z'.length);
|
|
55
|
+
});
|
|
56
|
+
it('same key produces same DID (deterministic)', () => {
|
|
57
|
+
const kp = generateKeyPair();
|
|
58
|
+
const did1 = publicKeyToDid(kp.publicKey);
|
|
59
|
+
const did2 = publicKeyToDid(kp.publicKey);
|
|
60
|
+
expect(did1).toBe(did2);
|
|
61
|
+
});
|
|
62
|
+
it('throws OphirError on key shorter than 32 bytes', () => {
|
|
63
|
+
const shortKey = new Uint8Array(16);
|
|
64
|
+
expect(() => publicKeyToDid(shortKey)).toThrow(OphirError);
|
|
65
|
+
expect(() => publicKeyToDid(shortKey)).toThrow('Invalid public key length: expected 32, got 16');
|
|
66
|
+
});
|
|
67
|
+
it('throws OphirError on key longer than 32 bytes', () => {
|
|
68
|
+
const longKey = new Uint8Array(48);
|
|
69
|
+
expect(() => publicKeyToDid(longKey)).toThrow(OphirError);
|
|
70
|
+
expect(() => publicKeyToDid(longKey)).toThrow('Invalid public key length: expected 32, got 48');
|
|
71
|
+
});
|
|
72
|
+
it('throws OphirError on empty key (0 bytes)', () => {
|
|
73
|
+
const emptyKey = new Uint8Array(0);
|
|
74
|
+
expect(() => publicKeyToDid(emptyKey)).toThrow('Invalid public key length: expected 32, got 0');
|
|
75
|
+
});
|
|
76
|
+
it('throws with INVALID_MESSAGE error code', () => {
|
|
77
|
+
const shortKey = new Uint8Array(10);
|
|
78
|
+
try {
|
|
79
|
+
publicKeyToDid(shortKey);
|
|
80
|
+
expect.unreachable('should have thrown');
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
expect(e).toBeInstanceOf(OphirError);
|
|
84
|
+
expect(e.code).toBe(OphirErrorCode.INVALID_MESSAGE);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe('didToPublicKey', () => {
|
|
89
|
+
it('roundtrip: publicKeyToDid then didToPublicKey recovers original key', () => {
|
|
90
|
+
const kp = generateKeyPair();
|
|
91
|
+
const did = publicKeyToDid(kp.publicKey);
|
|
92
|
+
const recovered = didToPublicKey(did);
|
|
93
|
+
expect(Buffer.from(recovered)).toEqual(Buffer.from(kp.publicKey));
|
|
94
|
+
});
|
|
95
|
+
it('recovered key is a Uint8Array of length 32', () => {
|
|
96
|
+
const kp = generateKeyPair();
|
|
97
|
+
const did = publicKeyToDid(kp.publicKey);
|
|
98
|
+
const recovered = didToPublicKey(did);
|
|
99
|
+
expect(recovered).toBeInstanceOf(Uint8Array);
|
|
100
|
+
expect(recovered).toHaveLength(32);
|
|
101
|
+
});
|
|
102
|
+
it('throws on invalid prefix (did:web:...)', () => {
|
|
103
|
+
expect(() => didToPublicKey('did:web:example.com')).toThrow('Invalid did:key format');
|
|
104
|
+
});
|
|
105
|
+
it('throws on invalid prefix (did:ethr:...)', () => {
|
|
106
|
+
expect(() => didToPublicKey('did:ethr:0xabc')).toThrow('Invalid did:key format');
|
|
107
|
+
});
|
|
108
|
+
it('throws on wrong multicodec prefix', () => {
|
|
109
|
+
const fakePayload = new Uint8Array(34);
|
|
110
|
+
fakePayload[0] = 0x00;
|
|
111
|
+
fakePayload[1] = 0x00;
|
|
112
|
+
const fakeDid = `did:key:z${bs58.encode(fakePayload)}`;
|
|
113
|
+
expect(() => didToPublicKey(fakeDid)).toThrow('Invalid multicodec prefix');
|
|
114
|
+
});
|
|
115
|
+
it('throws on truncated key (fewer than 32 bytes after prefix)', () => {
|
|
116
|
+
const truncated = new Uint8Array(12);
|
|
117
|
+
truncated[0] = 0xed;
|
|
118
|
+
truncated[1] = 0x01;
|
|
119
|
+
const fakeDid = `did:key:z${bs58.encode(truncated)}`;
|
|
120
|
+
expect(() => didToPublicKey(fakeDid)).toThrow('Invalid public key length after DID decoding: expected 32, got 10');
|
|
121
|
+
});
|
|
122
|
+
it('throws on oversized key (more than 32 bytes after prefix)', () => {
|
|
123
|
+
const oversized = new Uint8Array(40);
|
|
124
|
+
oversized[0] = 0xed;
|
|
125
|
+
oversized[1] = 0x01;
|
|
126
|
+
const fakeDid = `did:key:z${bs58.encode(oversized)}`;
|
|
127
|
+
expect(() => didToPublicKey(fakeDid)).toThrow('Invalid public key length after DID decoding: expected 32, got 38');
|
|
128
|
+
});
|
|
129
|
+
it('throws on empty string', () => {
|
|
130
|
+
expect(() => didToPublicKey('')).toThrow('DID must be a non-empty string');
|
|
131
|
+
});
|
|
132
|
+
it('throws on non-string input (number)', () => {
|
|
133
|
+
expect(() => didToPublicKey(12345)).toThrow('DID must be a non-empty string');
|
|
134
|
+
});
|
|
135
|
+
it('throws on non-string input (null)', () => {
|
|
136
|
+
expect(() => didToPublicKey(null)).toThrow('DID must be a non-empty string');
|
|
137
|
+
});
|
|
138
|
+
it('throws on non-string input (undefined)', () => {
|
|
139
|
+
expect(() => didToPublicKey(undefined)).toThrow('DID must be a non-empty string');
|
|
140
|
+
});
|
|
141
|
+
it('throws with INVALID_MESSAGE error code for empty string', () => {
|
|
142
|
+
try {
|
|
143
|
+
didToPublicKey('');
|
|
144
|
+
expect.unreachable('should have thrown');
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
expect(e).toBeInstanceOf(OphirError);
|
|
148
|
+
expect(e.code).toBe(OphirErrorCode.INVALID_MESSAGE);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('generateAgentIdentity', () => {
|
|
153
|
+
it('valid HTTPS endpoint returns complete identity bundle', () => {
|
|
154
|
+
const identity = generateAgentIdentity('https://agent.example.com');
|
|
155
|
+
expect(identity.agentId).toMatch(/^did:key:z6Mk/);
|
|
156
|
+
expect(identity.endpoint).toBe('https://agent.example.com');
|
|
157
|
+
expect(identity.keypair.publicKey).toHaveLength(32);
|
|
158
|
+
expect(identity.keypair.secretKey).toHaveLength(64);
|
|
159
|
+
});
|
|
160
|
+
it('valid HTTP endpoint is accepted', () => {
|
|
161
|
+
const identity = generateAgentIdentity('http://localhost:3000');
|
|
162
|
+
expect(identity.agentId).toMatch(/^did:key:z/);
|
|
163
|
+
expect(identity.endpoint).toBe('http://localhost:3000');
|
|
164
|
+
});
|
|
165
|
+
it('agentId matches the keypair public key DID', () => {
|
|
166
|
+
const identity = generateAgentIdentity('https://agent.example.com');
|
|
167
|
+
const expectedDid = publicKeyToDid(identity.keypair.publicKey);
|
|
168
|
+
expect(identity.agentId).toBe(expectedDid);
|
|
169
|
+
});
|
|
170
|
+
it('agentId can be decoded back to the keypair public key', () => {
|
|
171
|
+
const identity = generateAgentIdentity('https://agent.example.com');
|
|
172
|
+
const recovered = didToPublicKey(identity.agentId);
|
|
173
|
+
expect(Buffer.from(recovered)).toEqual(Buffer.from(identity.keypair.publicKey));
|
|
174
|
+
});
|
|
175
|
+
it('throws OphirError on invalid URL string', () => {
|
|
176
|
+
expect(() => generateAgentIdentity('not-a-url')).toThrow(OphirError);
|
|
177
|
+
expect(() => generateAgentIdentity('not-a-url')).toThrow('endpoint is not a valid URL');
|
|
178
|
+
});
|
|
179
|
+
it('throws OphirError on empty string', () => {
|
|
180
|
+
expect(() => generateAgentIdentity('')).toThrow(OphirError);
|
|
181
|
+
expect(() => generateAgentIdentity('')).toThrow('endpoint must be a non-empty string');
|
|
182
|
+
});
|
|
183
|
+
it('throws OphirError on FTP protocol', () => {
|
|
184
|
+
expect(() => generateAgentIdentity('ftp://files.example.com')).toThrow(OphirError);
|
|
185
|
+
expect(() => generateAgentIdentity('ftp://files.example.com')).toThrow('endpoint must use http or https protocol');
|
|
186
|
+
});
|
|
187
|
+
it('throws on non-string input (null)', () => {
|
|
188
|
+
expect(() => generateAgentIdentity(null)).toThrow('endpoint must be a non-empty string');
|
|
189
|
+
});
|
|
190
|
+
it('two calls produce different identities', () => {
|
|
191
|
+
const id1 = generateAgentIdentity('https://agent1.example.com');
|
|
192
|
+
const id2 = generateAgentIdentity('https://agent2.example.com');
|
|
193
|
+
expect(id1.agentId).not.toBe(id2.agentId);
|
|
194
|
+
expect(Buffer.from(id1.keypair.publicKey)).not.toEqual(Buffer.from(id2.keypair.publicKey));
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe('full roundtrip', () => {
|
|
198
|
+
it('generate keypair -> publicKeyToDid -> didToPublicKey -> keys match', () => {
|
|
199
|
+
const kp = generateKeyPair();
|
|
200
|
+
const did = publicKeyToDid(kp.publicKey);
|
|
201
|
+
const recovered = didToPublicKey(did);
|
|
202
|
+
expect(Buffer.from(recovered)).toEqual(Buffer.from(kp.publicKey));
|
|
203
|
+
});
|
|
204
|
+
it('roundtrip preserves all-zero key', () => {
|
|
205
|
+
const zeroKey = new Uint8Array(32);
|
|
206
|
+
const did = publicKeyToDid(zeroKey);
|
|
207
|
+
const recovered = didToPublicKey(did);
|
|
208
|
+
expect(Buffer.from(recovered)).toEqual(Buffer.from(zeroKey));
|
|
209
|
+
});
|
|
210
|
+
it('roundtrip preserves all-ones key (0xFF bytes)', () => {
|
|
211
|
+
const onesKey = new Uint8Array(32).fill(0xff);
|
|
212
|
+
const did = publicKeyToDid(onesKey);
|
|
213
|
+
const recovered = didToPublicKey(did);
|
|
214
|
+
expect(Buffer.from(recovered)).toEqual(Buffer.from(onesKey));
|
|
215
|
+
});
|
|
216
|
+
it('generateAgentIdentity roundtrip through DID', () => {
|
|
217
|
+
const identity = generateAgentIdentity('https://test.example.com');
|
|
218
|
+
const recovered = didToPublicKey(identity.agentId);
|
|
219
|
+
const rederivedDid = publicKeyToDid(recovered);
|
|
220
|
+
expect(rederivedDid).toBe(identity.agentId);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|