@blindmarket/sdk 0.1.4

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 (244) hide show
  1. package/README.md +72 -0
  2. package/biome.json +19 -0
  3. package/dist/BlindEscrowClient-CyPuKMge.d.cts +54 -0
  4. package/dist/BlindEscrowClient-R7iGpE-n.d.ts +54 -0
  5. package/dist/KeyStore-Be_SSpCt.d.cts +21 -0
  6. package/dist/KeyStore-k1JgYPJY.d.ts +21 -0
  7. package/dist/Signer-BnqH_qew.d.cts +38 -0
  8. package/dist/Signer-LColE0Uo.d.ts +38 -0
  9. package/dist/Storage-4QOrh7rm.d.ts +17 -0
  10. package/dist/Storage-Cf4IJRMk.d.cts +17 -0
  11. package/dist/TaskRegistryClient-Ch6lYqM_.d.ts +24 -0
  12. package/dist/TaskRegistryClient-CrQjddDc.d.cts +24 -0
  13. package/dist/aes-J-7HrHze.d.cts +17 -0
  14. package/dist/aes-J-7HrHze.d.ts +17 -0
  15. package/dist/agent/Agent.d.ts +59 -0
  16. package/dist/agent/Agent.js +73 -0
  17. package/dist/agent/index.cjs +425 -0
  18. package/dist/agent/index.cjs.map +1 -0
  19. package/dist/agent/index.d.cts +63 -0
  20. package/dist/agent/index.d.ts +1 -0
  21. package/dist/agent/index.js +1 -0
  22. package/dist/agent/index.js.map +1 -0
  23. package/dist/api/index.cjs +4 -0
  24. package/dist/api/index.cjs.map +1 -0
  25. package/dist/api/index.d.cts +2 -0
  26. package/dist/api/index.d.ts +1 -0
  27. package/dist/api/index.js +1 -0
  28. package/dist/api/index.js.map +1 -0
  29. package/dist/chain/BlindEscrowClient.d.ts +51 -0
  30. package/dist/chain/BlindEscrowClient.js +142 -0
  31. package/dist/chain/BlindReputationClient.d.ts +16 -0
  32. package/dist/chain/BlindReputationClient.js +45 -0
  33. package/dist/chain/ChainClient.d.ts +26 -0
  34. package/dist/chain/ChainClient.js +46 -0
  35. package/dist/chain/TaskRegistryClient.d.ts +21 -0
  36. package/dist/chain/TaskRegistryClient.js +70 -0
  37. package/dist/chain/abi/BlindEscrow.json +1113 -0
  38. package/dist/chain/abi/BlindReputation.json +447 -0
  39. package/dist/chain/abi/TaskRegistry.json +586 -0
  40. package/dist/chain/abi/index.d.ts +4 -0
  41. package/dist/chain/abi/index.js +4 -0
  42. package/dist/chain/errors.d.ts +8 -0
  43. package/dist/chain/errors.js +32 -0
  44. package/dist/chain/index.cjs +2484 -0
  45. package/dist/chain/index.cjs.map +1 -0
  46. package/dist/chain/index.d.cts +2305 -0
  47. package/dist/chain/index.d.ts +6 -0
  48. package/dist/chain/index.js +6 -0
  49. package/dist/chain/index.js.map +1 -0
  50. package/dist/compute/index.cjs +4 -0
  51. package/dist/compute/index.cjs.map +1 -0
  52. package/dist/compute/index.d.cts +2 -0
  53. package/dist/compute/index.d.ts +1 -0
  54. package/dist/compute/index.js +1 -0
  55. package/dist/compute/index.js.map +1 -0
  56. package/dist/crypto/aes.d.ts +15 -0
  57. package/dist/crypto/aes.js +81 -0
  58. package/dist/crypto/constants.d.ts +7 -0
  59. package/dist/crypto/constants.js +7 -0
  60. package/dist/crypto/ecies.d.ts +8 -0
  61. package/dist/crypto/ecies.js +51 -0
  62. package/dist/crypto/envelope.d.ts +35 -0
  63. package/dist/crypto/envelope.js +103 -0
  64. package/dist/crypto/hkdf.d.ts +5 -0
  65. package/dist/crypto/hkdf.js +34 -0
  66. package/dist/crypto/index.cjs +424 -0
  67. package/dist/crypto/index.cjs.map +1 -0
  68. package/dist/crypto/index.d.cts +74 -0
  69. package/dist/crypto/index.d.ts +6 -0
  70. package/dist/crypto/index.js +6 -0
  71. package/dist/crypto/index.js.map +1 -0
  72. package/dist/crypto/keys.d.ts +12 -0
  73. package/dist/crypto/keys.js +42 -0
  74. package/dist/errors/BlindBountyError.d.ts +14 -0
  75. package/dist/errors/BlindBountyError.js +30 -0
  76. package/dist/errors/codes.d.ts +19 -0
  77. package/dist/errors/codes.js +48 -0
  78. package/dist/errors/index.cjs +136 -0
  79. package/dist/errors/index.cjs.map +1 -0
  80. package/dist/errors/index.d.cts +60 -0
  81. package/dist/errors/index.d.ts +3 -0
  82. package/dist/errors/index.js +3 -0
  83. package/dist/errors/index.js.map +1 -0
  84. package/dist/errors/subclasses.d.ts +26 -0
  85. package/dist/errors/subclasses.js +41 -0
  86. package/dist/events/EventBus.d.ts +13 -0
  87. package/dist/events/EventBus.js +41 -0
  88. package/dist/events/index.cjs +41 -0
  89. package/dist/events/index.cjs.map +1 -0
  90. package/dist/events/index.d.cts +61 -0
  91. package/dist/events/index.d.ts +2 -0
  92. package/dist/events/index.js +2 -0
  93. package/dist/events/index.js.map +1 -0
  94. package/dist/events/types.d.ts +45 -0
  95. package/dist/events/types.js +1 -0
  96. package/dist/index.cjs +4 -0
  97. package/dist/index.cjs.map +1 -0
  98. package/dist/index.d.cts +2 -0
  99. package/dist/index.d.ts +129 -0
  100. package/dist/index.js +154 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/keystore/InMemoryKeyStore.d.ts +21 -0
  103. package/dist/keystore/InMemoryKeyStore.js +111 -0
  104. package/dist/keystore/KeyStore.d.ts +18 -0
  105. package/dist/keystore/KeyStore.js +1 -0
  106. package/dist/keystore/index.cjs +335 -0
  107. package/dist/keystore/index.cjs.map +1 -0
  108. package/dist/keystore/index.d.cts +23 -0
  109. package/dist/keystore/index.d.ts +2 -0
  110. package/dist/keystore/index.js +2 -0
  111. package/dist/keystore/index.js.map +1 -0
  112. package/dist/network/index.cjs +74 -0
  113. package/dist/network/index.cjs.map +1 -0
  114. package/dist/network/index.d.cts +36 -0
  115. package/dist/network/index.d.ts +3 -0
  116. package/dist/network/index.js +3 -0
  117. package/dist/network/index.js.map +1 -0
  118. package/dist/network/presets.d.ts +24 -0
  119. package/dist/network/presets.js +23 -0
  120. package/dist/network/resolve.d.ts +7 -0
  121. package/dist/network/resolve.js +17 -0
  122. package/dist/network/types.d.ts +16 -0
  123. package/dist/network/types.js +1 -0
  124. package/dist/signer/EthersSigner.d.ts +24 -0
  125. package/dist/signer/EthersSigner.js +68 -0
  126. package/dist/signer/PrivateKeySigner.d.ts +14 -0
  127. package/dist/signer/PrivateKeySigner.js +18 -0
  128. package/dist/signer/Signer.d.ts +35 -0
  129. package/dist/signer/Signer.js +1 -0
  130. package/dist/signer/index.cjs +121 -0
  131. package/dist/signer/index.cjs.map +1 -0
  132. package/dist/signer/index.d.cts +40 -0
  133. package/dist/signer/index.d.ts +3 -0
  134. package/dist/signer/index.js +3 -0
  135. package/dist/signer/index.js.map +1 -0
  136. package/dist/storage/MemoryStorage.d.ts +18 -0
  137. package/dist/storage/MemoryStorage.js +40 -0
  138. package/dist/storage/Storage.d.ts +14 -0
  139. package/dist/storage/Storage.js +1 -0
  140. package/dist/storage/ZgStorage.d.ts +24 -0
  141. package/dist/storage/ZgStorage.js +102 -0
  142. package/dist/storage/index.cjs +221 -0
  143. package/dist/storage/index.cjs.map +1 -0
  144. package/dist/storage/index.d.cts +44 -0
  145. package/dist/storage/index.d.ts +3 -0
  146. package/dist/storage/index.js +3 -0
  147. package/dist/storage/index.js.map +1 -0
  148. package/dist/testing/index.cjs +4 -0
  149. package/dist/testing/index.cjs.map +1 -0
  150. package/dist/testing/index.d.cts +2 -0
  151. package/dist/testing/index.d.ts +1 -0
  152. package/dist/testing/index.js +1 -0
  153. package/dist/testing/index.js.map +1 -0
  154. package/dist/types-BOG5Ub1z.d.cts +31 -0
  155. package/dist/types-BOG5Ub1z.d.ts +31 -0
  156. package/dist/types-V7_ZmUb-.d.cts +19 -0
  157. package/dist/types-hAsITUQ7.d.ts +19 -0
  158. package/dist/types.d.ts +58 -0
  159. package/dist/types.js +1 -0
  160. package/dist/verifier/index.cjs +4 -0
  161. package/dist/verifier/index.cjs.map +1 -0
  162. package/dist/verifier/index.d.cts +2 -0
  163. package/dist/verifier/index.d.ts +1 -0
  164. package/dist/verifier/index.js +1 -0
  165. package/dist/verifier/index.js.map +1 -0
  166. package/dist/worker/Worker.d.ts +53 -0
  167. package/dist/worker/Worker.js +53 -0
  168. package/dist/worker/index.cjs +382 -0
  169. package/dist/worker/index.cjs.map +1 -0
  170. package/dist/worker/index.d.cts +58 -0
  171. package/dist/worker/index.d.ts +1 -0
  172. package/dist/worker/index.js +1 -0
  173. package/dist/worker/index.js.map +1 -0
  174. package/examples/agent-demo.ts +121 -0
  175. package/package.json +46 -0
  176. package/scripts/gen-crypto-fixtures.mjs +72 -0
  177. package/src/agent/Agent.ts +122 -0
  178. package/src/agent/index.ts +1 -0
  179. package/src/api/index.ts +1 -0
  180. package/src/chain/BlindEscrowClient.ts +182 -0
  181. package/src/chain/BlindReputationClient.ts +55 -0
  182. package/src/chain/ChainClient.ts +59 -0
  183. package/src/chain/TaskRegistryClient.ts +82 -0
  184. package/src/chain/abi/BlindEscrow.json +1113 -0
  185. package/src/chain/abi/BlindReputation.json +447 -0
  186. package/src/chain/abi/TaskRegistry.json +586 -0
  187. package/src/chain/abi/index.ts +5 -0
  188. package/src/chain/errors.ts +43 -0
  189. package/src/chain/index.ts +6 -0
  190. package/src/compute/index.ts +1 -0
  191. package/src/crypto/aes.ts +104 -0
  192. package/src/crypto/constants.ts +7 -0
  193. package/src/crypto/ecies.ts +60 -0
  194. package/src/crypto/envelope.ts +131 -0
  195. package/src/crypto/hkdf.ts +45 -0
  196. package/src/crypto/index.ts +6 -0
  197. package/src/crypto/keys.ts +56 -0
  198. package/src/errors/BlindMarketError.ts +38 -0
  199. package/src/errors/codes.ts +67 -0
  200. package/src/errors/index.ts +3 -0
  201. package/src/errors/subclasses.ts +59 -0
  202. package/src/events/EventBus.ts +45 -0
  203. package/src/events/index.ts +2 -0
  204. package/src/events/types.ts +13 -0
  205. package/src/index.ts +222 -0
  206. package/src/keystore/InMemoryKeyStore.ts +146 -0
  207. package/src/keystore/KeyStore.ts +19 -0
  208. package/src/keystore/index.ts +2 -0
  209. package/src/network/index.ts +3 -0
  210. package/src/network/presets.ts +27 -0
  211. package/src/network/resolve.ts +19 -0
  212. package/src/network/types.ts +18 -0
  213. package/src/signer/EthersSigner.ts +82 -0
  214. package/src/signer/PrivateKeySigner.ts +19 -0
  215. package/src/signer/Signer.ts +40 -0
  216. package/src/signer/index.ts +3 -0
  217. package/src/storage/MemoryStorage.ts +49 -0
  218. package/src/storage/Storage.ts +15 -0
  219. package/src/storage/ZgStorage.ts +124 -0
  220. package/src/storage/index.ts +3 -0
  221. package/src/testing/index.ts +1 -0
  222. package/src/types.ts +76 -0
  223. package/src/verifier/index.ts +1 -0
  224. package/src/worker/Worker.ts +98 -0
  225. package/src/worker/index.ts +1 -0
  226. package/test/agent.test.ts +193 -0
  227. package/test/chain.test.ts +145 -0
  228. package/test/crypto.aes.test.ts +81 -0
  229. package/test/crypto.compat.test.ts +33 -0
  230. package/test/crypto.ecies.test.ts +64 -0
  231. package/test/envelope.test.ts +82 -0
  232. package/test/errors.test.ts +64 -0
  233. package/test/events.test.ts +59 -0
  234. package/test/fixtures/crypto.json +16 -0
  235. package/test/keystore.test.ts +103 -0
  236. package/test/network.test.ts +53 -0
  237. package/test/signer.test.ts +66 -0
  238. package/test/smoke.test.ts +7 -0
  239. package/test/storage.test.ts +55 -0
  240. package/test/worker.test.ts +181 -0
  241. package/tsconfig.build.json +1 -0
  242. package/tsconfig.json +13 -0
  243. package/tsup.config.ts +32 -0
  244. package/vitest.config.ts +17 -0
@@ -0,0 +1,81 @@
1
+ import * as fc from 'fast-check';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { aesDecrypt, aesEncrypt, generateAesKey, sha256 } from '../src/crypto/index.js';
4
+ import { CryptoError } from '../src/errors/index.js';
5
+
6
+ describe('aes-256-gcm', () => {
7
+ it('encrypts then decrypts arbitrary bytes', async () => {
8
+ const key = await generateAesKey();
9
+ const pt = new TextEncoder().encode('hello bounty');
10
+ const ct = await aesEncrypt(pt, key);
11
+ expect(ct.byteLength).toBe(pt.byteLength + 12 + 16);
12
+ const rt = await aesDecrypt(ct, key);
13
+ expect(new TextDecoder().decode(rt)).toBe('hello bounty');
14
+ });
15
+
16
+ it('generates distinct keys each call', async () => {
17
+ const a = await generateAesKey();
18
+ const b = await generateAesKey();
19
+ expect(a.byteLength).toBe(32);
20
+ expect(b.byteLength).toBe(32);
21
+ expect(a).not.toEqual(b);
22
+ });
23
+
24
+ it('emits distinct ciphertexts for the same plaintext+key (random IV)', async () => {
25
+ const key = await generateAesKey();
26
+ const pt = new TextEncoder().encode('same text');
27
+ const c1 = await aesEncrypt(pt, key);
28
+ const c2 = await aesEncrypt(pt, key);
29
+ expect(c1).not.toEqual(c2);
30
+ });
31
+
32
+ it('rejects non-32-byte keys on encrypt', async () => {
33
+ const key = new Uint8Array(16);
34
+ await expect(aesEncrypt(new Uint8Array(4), key)).rejects.toBeInstanceOf(CryptoError);
35
+ });
36
+
37
+ it('rejects non-32-byte keys on decrypt', async () => {
38
+ const key = new Uint8Array(16);
39
+ await expect(aesDecrypt(new Uint8Array(32), key)).rejects.toBeInstanceOf(CryptoError);
40
+ });
41
+
42
+ it('rejects truncated blobs', async () => {
43
+ const key = await generateAesKey();
44
+ await expect(aesDecrypt(new Uint8Array(10), key)).rejects.toBeInstanceOf(CryptoError);
45
+ });
46
+
47
+ it('detects tampering (auth tag check)', async () => {
48
+ const key = await generateAesKey();
49
+ const ct = await aesEncrypt(new TextEncoder().encode('data'), key);
50
+ // biome-ignore lint/style/noNonNullAssertion: we just created the array
51
+ ct[ct.length - 1] = (ct[ct.length - 1]! ^ 0x01) & 0xff;
52
+ await expect(aesDecrypt(ct, key)).rejects.toBeInstanceOf(CryptoError);
53
+ });
54
+
55
+ it('round-trips zero-byte plaintext', async () => {
56
+ const key = await generateAesKey();
57
+ const ct = await aesEncrypt(new Uint8Array(0), key);
58
+ expect(ct.byteLength).toBe(28); // 12 IV + 16 tag + 0 ct
59
+ const rt = await aesDecrypt(ct, key);
60
+ expect(rt.byteLength).toBe(0);
61
+ });
62
+
63
+ it('property: arbitrary plaintext round-trips', async () => {
64
+ await fc.assert(
65
+ fc.asyncProperty(fc.uint8Array({ minLength: 0, maxLength: 4096 }), async (pt) => {
66
+ const key = await generateAesKey();
67
+ const rt = await aesDecrypt(await aesEncrypt(pt, key), key);
68
+ expect(Array.from(rt)).toEqual(Array.from(pt));
69
+ }),
70
+ { numRuns: 50 },
71
+ );
72
+ });
73
+ });
74
+
75
+ describe('sha256', () => {
76
+ it('matches a known vector', async () => {
77
+ const digest = await sha256(new TextEncoder().encode('abc'));
78
+ const hex = Array.from(digest, (b) => b.toString(16).padStart(2, '0')).join('');
79
+ expect(hex).toBe('ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
80
+ });
81
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Wire-format compatibility: the SDK (Web Crypto) must decrypt blobs produced by
3
+ * backend/src/services/crypto.ts (Node crypto). The fixture at test/fixtures/crypto.json
4
+ * is generated by scripts/gen-crypto-fixtures.mjs using the same Node primitives
5
+ * as the backend — if this test fails, the SDK has diverged from the backend's
6
+ * byte layout and will break cross-stack encryption.
7
+ */
8
+ import { describe, expect, it } from 'vitest';
9
+ import fixture from './fixtures/crypto.json' with { type: 'json' };
10
+ import { aesDecrypt, eciesDecrypt } from '../src/crypto/index.js';
11
+
12
+ function hexToBytes(hex: string): Uint8Array {
13
+ const out = new Uint8Array(hex.length / 2);
14
+ for (let i = 0; i < out.length; i++) {
15
+ out[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
16
+ }
17
+ return out;
18
+ }
19
+
20
+ describe('crypto cross-compat with backend wire format', () => {
21
+ it('SDK decrypts backend-produced AES blob', async () => {
22
+ const key = hexToBytes(fixture.aes.key);
23
+ const blob = hexToBytes(fixture.aes.blob);
24
+ const pt = await aesDecrypt(blob, key);
25
+ expect(new TextDecoder().decode(pt)).toBe(fixture.plaintext);
26
+ });
27
+
28
+ it('SDK decrypts backend-produced ECIES blob', async () => {
29
+ const blob = hexToBytes(fixture.ecies.blob);
30
+ const pt = await eciesDecrypt(blob, fixture.keypair.privateKey);
31
+ expect(new TextDecoder().decode(pt)).toBe(fixture.plaintext);
32
+ });
33
+ });
@@ -0,0 +1,64 @@
1
+ import * as fc from 'fast-check';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ derivePublicKey,
5
+ eciesDecrypt,
6
+ eciesEncrypt,
7
+ generateKeyPair,
8
+ } from '../src/crypto/index.js';
9
+ import { CryptoError } from '../src/errors/index.js';
10
+
11
+ describe('ecies secp256k1', () => {
12
+ it('round-trips', async () => {
13
+ const { privateKey, publicKey } = generateKeyPair();
14
+ const pt = new TextEncoder().encode('secret instructions');
15
+ const ct = await eciesEncrypt(pt, publicKey);
16
+ const rt = await eciesDecrypt(ct, privateKey);
17
+ expect(new TextDecoder().decode(rt)).toBe('secret instructions');
18
+ });
19
+
20
+ it('wire format: [65 ephemeral pubkey][12 IV][16 tag][ciphertext]', async () => {
21
+ const { publicKey } = generateKeyPair();
22
+ const ct = await eciesEncrypt(new Uint8Array([1, 2, 3]), publicKey);
23
+ expect(ct.byteLength).toBe(65 + 12 + 16 + 3);
24
+ // First byte of ephemeral pubkey must be 0x04 (uncompressed marker)
25
+ expect(ct[0]).toBe(0x04);
26
+ });
27
+
28
+ it('wrong private key fails', async () => {
29
+ const good = generateKeyPair();
30
+ const bad = generateKeyPair();
31
+ const ct = await eciesEncrypt(new Uint8Array([9]), good.publicKey);
32
+ await expect(eciesDecrypt(ct, bad.privateKey)).rejects.toBeInstanceOf(CryptoError);
33
+ });
34
+
35
+ it('rejects truncated blobs', async () => {
36
+ const { privateKey } = generateKeyPair();
37
+ await expect(eciesDecrypt(new Uint8Array(30), privateKey)).rejects.toBeInstanceOf(CryptoError);
38
+ });
39
+
40
+ it('rejects compressed / malformed recipient pubkey', async () => {
41
+ const bogus = '02' + '00'.repeat(32); // compressed form (33 bytes), not supported
42
+ await expect(eciesEncrypt(new Uint8Array([0]), bogus)).rejects.toBeInstanceOf(CryptoError);
43
+ });
44
+
45
+ it('derivePublicKey recovers the same pubkey used for encryption', async () => {
46
+ const { privateKey, publicKey } = generateKeyPair();
47
+ const recovered = derivePublicKey(privateKey);
48
+ expect(recovered.toLowerCase()).toBe(publicKey.toLowerCase());
49
+ const pt = new TextEncoder().encode('x');
50
+ const ct = await eciesEncrypt(pt, recovered);
51
+ expect(new TextDecoder().decode(await eciesDecrypt(ct, privateKey))).toBe('x');
52
+ });
53
+
54
+ it('property: arbitrary plaintext round-trips', async () => {
55
+ await fc.assert(
56
+ fc.asyncProperty(fc.uint8Array({ minLength: 0, maxLength: 1024 }), async (pt) => {
57
+ const { privateKey, publicKey } = generateKeyPair();
58
+ const rt = await eciesDecrypt(await eciesEncrypt(pt, publicKey), privateKey);
59
+ expect(Array.from(rt)).toEqual(Array.from(pt));
60
+ }),
61
+ { numRuns: 25 },
62
+ );
63
+ });
64
+ });
@@ -0,0 +1,82 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ aesDecrypt,
4
+ eciesDecrypt,
5
+ generateKeyPair,
6
+ openEvidenceAsAgent,
7
+ sealEvidence,
8
+ __internals,
9
+ } from '../src/crypto/index.js';
10
+ import { CryptoError } from '../src/errors/index.js';
11
+
12
+ describe('evidence envelope', () => {
13
+ it('seal → agent-open round-trips', async () => {
14
+ const agent = generateKeyPair();
15
+ const enclave = generateKeyPair();
16
+ const evidence = new TextEncoder().encode('I did the photography task');
17
+ const { envelope } = await sealEvidence({
18
+ evidence,
19
+ agentPubKey: agent.publicKey,
20
+ enclavePubKey: enclave.publicKey,
21
+ });
22
+ const recovered = await openEvidenceAsAgent(envelope, agent.privateKey);
23
+ expect(new TextDecoder().decode(recovered)).toBe('I did the photography task');
24
+ });
25
+
26
+ it('enclave-wrapped key also decrypts to the same AES key', async () => {
27
+ const agent = generateKeyPair();
28
+ const enclave = generateKeyPair();
29
+ const evidence = new TextEncoder().encode('enclave-path');
30
+ const { envelope, aesKey } = await sealEvidence({
31
+ evidence,
32
+ agentPubKey: agent.publicKey,
33
+ enclavePubKey: enclave.publicKey,
34
+ });
35
+ const parsed = __internals.parse(envelope);
36
+ const keyFromEnclave = await eciesDecrypt(parsed.wrappedForEnclave, enclave.privateKey);
37
+ const keyFromAgent = await eciesDecrypt(parsed.wrappedForAgent, agent.privateKey);
38
+ expect(Array.from(keyFromEnclave)).toEqual(Array.from(aesKey));
39
+ expect(Array.from(keyFromAgent)).toEqual(Array.from(aesKey));
40
+ // And the key decrypts the AES portion independently
41
+ const pt = await aesDecrypt(parsed.aesCiphertext, keyFromEnclave);
42
+ expect(new TextDecoder().decode(pt)).toBe('enclave-path');
43
+ });
44
+
45
+ it('wrong magic bytes throw CRYPTO/INTEGRITY_CHECK', async () => {
46
+ const agent = generateKeyPair();
47
+ const enclave = generateKeyPair();
48
+ const { envelope } = await sealEvidence({
49
+ evidence: new Uint8Array([1, 2, 3]),
50
+ agentPubKey: agent.publicKey,
51
+ enclavePubKey: enclave.publicKey,
52
+ });
53
+ envelope[0] = 0xff;
54
+ await expect(openEvidenceAsAgent(envelope, agent.privateKey)).rejects.toBeInstanceOf(CryptoError);
55
+ });
56
+
57
+ it('unknown version throws CRYPTO/UNSUPPORTED_CIPHER', async () => {
58
+ const agent = generateKeyPair();
59
+ const enclave = generateKeyPair();
60
+ const { envelope } = await sealEvidence({
61
+ evidence: new Uint8Array([1, 2, 3]),
62
+ agentPubKey: agent.publicKey,
63
+ enclavePubKey: enclave.publicKey,
64
+ });
65
+ envelope[2] = 0x99;
66
+ await expect(openEvidenceAsAgent(envelope, agent.privateKey)).rejects.toMatchObject({
67
+ code: 'CRYPTO/UNSUPPORTED_CIPHER',
68
+ });
69
+ });
70
+
71
+ it('truncated envelope throws CRYPTO/INTEGRITY_CHECK', async () => {
72
+ const agent = generateKeyPair();
73
+ const enclave = generateKeyPair();
74
+ const { envelope } = await sealEvidence({
75
+ evidence: new Uint8Array([1, 2, 3]),
76
+ agentPubKey: agent.publicKey,
77
+ enclavePubKey: enclave.publicKey,
78
+ });
79
+ const short = envelope.slice(0, 10);
80
+ await expect(openEvidenceAsAgent(short, agent.privateKey)).rejects.toBeInstanceOf(CryptoError);
81
+ });
82
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ BlindMarketError,
4
+ ConfigError,
5
+ CryptoError,
6
+ StorageError,
7
+ ChainError,
8
+ ComputeError,
9
+ ApiError,
10
+ LifecycleError,
11
+ TimeoutError,
12
+ } from '../src/errors/index.js';
13
+
14
+ describe('BlindMarketError', () => {
15
+ it('preserves code, message, retriable, context, cause', () => {
16
+ const cause = new Error('root');
17
+ const err = new StorageError('STORAGE/UPLOAD_FAILED', 'upload blew up', {
18
+ retriable: true,
19
+ context: { attempt: 1 },
20
+ cause,
21
+ });
22
+ expect(err).toBeInstanceOf(BlindMarketError);
23
+ expect(err.name).toBe('StorageError');
24
+ expect(err.code).toBe('STORAGE/UPLOAD_FAILED');
25
+ expect(err.message).toBe('upload blew up');
26
+ expect(err.retriable).toBe(true);
27
+ expect(err.context).toEqual({ attempt: 1 });
28
+ expect(err.cause).toBe(cause);
29
+ });
30
+
31
+ it('defaults retriable to false', () => {
32
+ const err = new ConfigError('CONFIG/MISSING_SIGNER', 'no signer');
33
+ expect(err.retriable).toBe(false);
34
+ });
35
+
36
+ it('exposes a stable toJSON for telemetry (no secrets)', () => {
37
+ const err = new ApiError('API/VALIDATION', 'bad input', { context: { field: 'reward' } });
38
+ const j = err.toJSON();
39
+ expect(j).toMatchObject({
40
+ name: 'ApiError',
41
+ code: 'API/VALIDATION',
42
+ message: 'bad input',
43
+ retriable: false,
44
+ context: { field: 'reward' },
45
+ });
46
+ expect(j).not.toHaveProperty('cause');
47
+ });
48
+
49
+ it.each([
50
+ [ConfigError, 'CONFIG/MISSING_SIGNER'],
51
+ [CryptoError, 'CRYPTO/DECRYPT_FAILED'],
52
+ [StorageError, 'STORAGE/NOT_FOUND'],
53
+ [ChainError, 'CHAIN/TX_REVERTED'],
54
+ [ComputeError, 'COMPUTE/BROKER_UNREACHABLE'],
55
+ [ApiError, 'API/RATE_LIMITED'],
56
+ [LifecycleError, 'LIFECYCLE/ILLEGAL_TRANSITION'],
57
+ [TimeoutError, 'TIMEOUT'],
58
+ ] as const)('subclass %p accepts its canonical code', (Ctor, code) => {
59
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic ctor call for table test
60
+ const err = new (Ctor as any)(code, 'msg');
61
+ expect(err.code).toBe(code);
62
+ expect(err).toBeInstanceOf(BlindMarketError);
63
+ });
64
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { EventBus, type SdkEvents } from '../src/events/index.js';
3
+
4
+ const txPayload = { hash: '0xabc' as `0x${string}`, from: '0x00' as `0x${string}`, to: '0x01' as `0x${string}` };
5
+
6
+ describe('EventBus', () => {
7
+ it('delivers typed events to subscribers', () => {
8
+ const bus = new EventBus<SdkEvents>();
9
+ const fn = vi.fn();
10
+ bus.on('tx.sent', fn);
11
+ bus.emit('tx.sent', txPayload);
12
+ expect(fn).toHaveBeenCalledWith(txPayload);
13
+ });
14
+
15
+ it('once fires only once', () => {
16
+ const bus = new EventBus<SdkEvents>();
17
+ const fn = vi.fn();
18
+ bus.once('tx.sent', fn);
19
+ bus.emit('tx.sent', txPayload);
20
+ bus.emit('tx.sent', txPayload);
21
+ expect(fn).toHaveBeenCalledTimes(1);
22
+ });
23
+
24
+ it('off removes a specific listener', () => {
25
+ const bus = new EventBus<SdkEvents>();
26
+ const fn = vi.fn();
27
+ bus.on('tx.sent', fn);
28
+ bus.off('tx.sent', fn);
29
+ bus.emit('tx.sent', txPayload);
30
+ expect(fn).not.toHaveBeenCalled();
31
+ });
32
+
33
+ it('on returns an unsubscribe function', () => {
34
+ const bus = new EventBus<SdkEvents>();
35
+ const fn = vi.fn();
36
+ const unsubscribe = bus.on('tx.sent', fn);
37
+ unsubscribe();
38
+ bus.emit('tx.sent', txPayload);
39
+ expect(fn).not.toHaveBeenCalled();
40
+ });
41
+
42
+ it('isolates listener exceptions (all other listeners still fire)', () => {
43
+ const bus = new EventBus<SdkEvents>();
44
+ const bad = vi.fn(() => {
45
+ throw new Error('boom');
46
+ });
47
+ const good = vi.fn();
48
+ bus.on('tx.sent', bad);
49
+ bus.on('tx.sent', good);
50
+ bus.emit('tx.sent', txPayload);
51
+ expect(bad).toHaveBeenCalled();
52
+ expect(good).toHaveBeenCalled();
53
+ });
54
+
55
+ it('emit with no subscribers is a no-op', () => {
56
+ const bus = new EventBus<SdkEvents>();
57
+ expect(() => bus.emit('tx.sent', txPayload)).not.toThrow();
58
+ });
59
+ });
@@ -0,0 +1,16 @@
1
+ {
2
+ "_note": "Generated by scripts/gen-crypto-fixtures.mjs using Node built-in crypto, matching backend/src/services/crypto.ts. If the SDK decrypts these back to the expected plaintext, wire-format compatibility is proven. Regenerate only on intentional format changes.",
3
+ "hkdf_info": "BlindMarket-ECIES-v1",
4
+ "keypair": {
5
+ "privateKey": "0d1ab8008ec2d77282500acb8e2c8cb237a64e0d2a474ab8f2a8415723e5fb87",
6
+ "publicKey": "04f797b44d169c83d53b444ff690df5233616f4d93bb234e49c48a84d8b256e8291701bdcf4fc608aa987029ddad03957957f97e2cc408a4e9f1fa7d8286cdbcaf"
7
+ },
8
+ "plaintext": "hello cross-compat",
9
+ "aes": {
10
+ "key": "5516bb862924cf891c6071c11a07b7286e149b4886db5beb392cca1181f565d9",
11
+ "blob": "70baf194291fcac086eb0e0ce5cce9a7af3ec485776d1605aaa7732cf5e15b5d4fbd79a4549b44ec9dd266462619"
12
+ },
13
+ "ecies": {
14
+ "blob": "0480f4dbacb4a2bb8575b9c7a7e5f5d47f6f8dc9c804a8d01cb3e9f7101e7040f5937dee7d7e8787033af1259e00d877ab785dcd89f8fc79d62f8f4bf6b51fac8ea72518b2d7845ca6c054e7fd152774852583fca8b71185f418ce6aa29d6d10a7d51c85d02dcfbaeea534d346c27c"
15
+ }
16
+ }
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { generateAesKey } from '../src/crypto/index.js';
3
+ import { CryptoError } from '../src/errors/index.js';
4
+ import { InMemoryKeyStore } from '../src/keystore/index.js';
5
+ import type { Address, Hex, TaskKey } from '../src/types.js';
6
+
7
+ async function mkKey(): Promise<TaskKey> {
8
+ return { aesKey: await generateAesKey(), createdAt: new Date() };
9
+ }
10
+
11
+ describe('InMemoryKeyStore', () => {
12
+ it('round-trips task keys', async () => {
13
+ const ks = new InMemoryKeyStore();
14
+ const tk = await mkKey();
15
+ await ks.putTaskKey(42n, tk);
16
+ const got = await ks.getTaskKey(42n);
17
+ expect(got).not.toBeNull();
18
+ expect(got?.aesKey).toEqual(tk.aesKey);
19
+ expect(got?.createdAt.getTime()).toBe(tk.createdAt.getTime());
20
+ });
21
+
22
+ it('returns null for missing task keys', async () => {
23
+ const ks = new InMemoryKeyStore();
24
+ expect(await ks.getTaskKey(999n)).toBeNull();
25
+ });
26
+
27
+ it('list enumerates stored task IDs', async () => {
28
+ const ks = new InMemoryKeyStore();
29
+ await ks.putTaskKey(1n, await mkKey());
30
+ await ks.putTaskKey(2n, await mkKey());
31
+ const all = await ks.list();
32
+ expect(all.map((r) => r.taskId).sort()).toEqual([1n, 2n]);
33
+ });
34
+
35
+ it('delete removes a task key', async () => {
36
+ const ks = new InMemoryKeyStore();
37
+ await ks.putTaskKey(7n, await mkKey());
38
+ await ks.delete(7n);
39
+ expect(await ks.getTaskKey(7n)).toBeNull();
40
+ });
41
+
42
+ it('peer pubkey cache', async () => {
43
+ const ks = new InMemoryKeyStore();
44
+ const addr = '0x0000000000000000000000000000000000000001' as Address;
45
+ const pub = ('0x04' + '11'.repeat(64)) as Hex;
46
+ expect(await ks.getPeerPubKey(addr)).toBeNull();
47
+ await ks.putPeerPubKey(addr, pub);
48
+ expect(await ks.getPeerPubKey(addr)).toBe(pub);
49
+ });
50
+
51
+ it('returned AES key is a copy (mutations do not leak back)', async () => {
52
+ const ks = new InMemoryKeyStore();
53
+ await ks.putTaskKey(1n, await mkKey());
54
+ const a = await ks.getTaskKey(1n);
55
+ const b = await ks.getTaskKey(1n);
56
+ // biome-ignore lint/style/noNonNullAssertion: returned by an earlier non-null assertion
57
+ a!.aesKey[0] = (a!.aesKey[0]! ^ 0xff) & 0xff;
58
+ expect(b?.aesKey).not.toEqual(a?.aesKey);
59
+ });
60
+
61
+ it('export/import round-trips with the same passphrase', async () => {
62
+ const src = new InMemoryKeyStore();
63
+ await src.putTaskKey(1n, await mkKey());
64
+ await src.putTaskKey(2n, await mkKey());
65
+ const addr = '0x0000000000000000000000000000000000000009' as Address;
66
+ const pub = ('0x04' + 'ab'.repeat(64)) as Hex;
67
+ await src.putPeerPubKey(addr, pub);
68
+
69
+ const exported = await src.export('correct-horse-battery-staple');
70
+ const dst = new InMemoryKeyStore();
71
+ await dst.import(exported, 'correct-horse-battery-staple');
72
+
73
+ const srcList = (await src.list()).map((r) => r.taskId).sort();
74
+ const dstList = (await dst.list()).map((r) => r.taskId).sort();
75
+ expect(dstList).toEqual(srcList);
76
+ expect((await dst.getTaskKey(1n))?.aesKey).toEqual((await src.getTaskKey(1n))?.aesKey);
77
+ expect(await dst.getPeerPubKey(addr)).toBe(pub);
78
+ });
79
+
80
+ it('import with wrong passphrase fails', async () => {
81
+ const src = new InMemoryKeyStore();
82
+ await src.putTaskKey(1n, await mkKey());
83
+ const exported = await src.export('goodpassphrase');
84
+ const dst = new InMemoryKeyStore();
85
+ await expect(dst.import(exported, 'wrongpassphrase')).rejects.toBeInstanceOf(CryptoError);
86
+ });
87
+
88
+ it('export rejects short passphrases', async () => {
89
+ const ks = new InMemoryKeyStore();
90
+ await expect(ks.export('short')).rejects.toBeInstanceOf(CryptoError);
91
+ });
92
+
93
+ it('import rejects invalid JSON', async () => {
94
+ const ks = new InMemoryKeyStore();
95
+ await expect(ks.import('not-json', 'any-passphrase')).rejects.toBeInstanceOf(CryptoError);
96
+ });
97
+
98
+ it('import rejects unknown format', async () => {
99
+ const ks = new InMemoryKeyStore();
100
+ const fake = JSON.stringify({ format: 'other-format', salt: '00', blob: '00' });
101
+ await expect(ks.import(fake, 'any-passphrase')).rejects.toBeInstanceOf(CryptoError);
102
+ });
103
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ConfigError } from '../src/errors/index.js';
3
+ import { networks, resolveNetwork, type Network } from '../src/network/index.js';
4
+
5
+ describe('networks', () => {
6
+ it('testnet preset matches 0G Galileo', () => {
7
+ expect(networks.testnet.chainId).toBe(16602n);
8
+ expect(networks.testnet.contracts.escrow).toMatch(/^0x[a-fA-F0-9]{40}$/);
9
+ expect(networks.testnet.contracts.registry).toMatch(/^0x[a-fA-F0-9]{40}$/);
10
+ expect(networks.testnet.contracts.reputation).toMatch(/^0x[a-fA-F0-9]{40}$/);
11
+ expect(networks.testnet.contracts.usdc).toMatch(/^0x[a-fA-F0-9]{40}$/);
12
+ expect(networks.testnet.rpc.length).toBeGreaterThan(0);
13
+ expect(networks.testnet.indexer).toBeTruthy();
14
+ expect(networks.testnet.broker).toBeTruthy();
15
+ });
16
+
17
+ it('testnet addresses match contracts/deployments/0g-testnet.json', () => {
18
+ expect(networks.testnet.contracts.escrow).toBe('0xFd4F93F5A7BE144c405D1D8fbEC63Fb776207681');
19
+ expect(networks.testnet.contracts.registry).toBe('0xeE52d780A47F77E8a4a1cEb236e3C65A48FbD828');
20
+ expect(networks.testnet.contracts.reputation).toBe('0x4A6374Fae37E19E69ba43E7cf6994AC15F63256e');
21
+ expect(networks.testnet.contracts.usdc).toBe('0x317227efcA18D004E12CA8046AEf7E1597458F25');
22
+ });
23
+
24
+ it('resolveNetwork accepts a preset name', () => {
25
+ const n = resolveNetwork('testnet');
26
+ expect(n.chainId).toBe(16602n);
27
+ expect(n.name).toBe('0g-galileo-testnet');
28
+ });
29
+
30
+ it('resolveNetwork rejects unknown preset with ConfigError', () => {
31
+ // biome-ignore lint/suspicious/noExplicitAny: intentional type escape to test runtime error path
32
+ expect(() => resolveNetwork('nope' as any)).toThrow(ConfigError);
33
+ });
34
+
35
+ it('resolveNetwork passes through a custom network object', () => {
36
+ const custom: Network = {
37
+ name: 'localhost',
38
+ chainId: 1337n,
39
+ rpc: ['http://localhost:8545'],
40
+ contracts: {
41
+ escrow: '0x0000000000000000000000000000000000000001',
42
+ registry: '0x0000000000000000000000000000000000000002',
43
+ reputation: '0x0000000000000000000000000000000000000003',
44
+ usdc: '0x0000000000000000000000000000000000000004',
45
+ },
46
+ indexer: '',
47
+ broker: '',
48
+ };
49
+ const resolved = resolveNetwork(custom);
50
+ expect(resolved).toBe(custom);
51
+ expect(resolved.chainId).toBe(1337n);
52
+ });
53
+ });
@@ -0,0 +1,66 @@
1
+ import { ethers } from 'ethers';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { eciesDecrypt, eciesEncrypt } from '../src/crypto/index.js';
4
+ import { EthersSigner, PrivateKeySigner } from '../src/signer/index.js';
5
+
6
+ const FIXED_PK = '0x0123456789012345678901234567890123456789012345678901234567890123';
7
+ const FIXED_ADDR = '0x14791697260E4c9A71f18484C9f997B308e59325';
8
+
9
+ describe('PrivateKeySigner', () => {
10
+ it('derives the correct address from a known private key', async () => {
11
+ const signer = new PrivateKeySigner(FIXED_PK);
12
+ expect((await signer.getAddress()).toLowerCase()).toBe(FIXED_ADDR.toLowerCase());
13
+ });
14
+
15
+ it('accepts the private key with or without 0x prefix', async () => {
16
+ const a = new PrivateKeySigner(FIXED_PK);
17
+ const b = new PrivateKeySigner(FIXED_PK.slice(2));
18
+ expect(await a.getAddress()).toBe(await b.getAddress());
19
+ });
20
+
21
+ it('signMessage produces a signature that recovers to the signer address', async () => {
22
+ const signer = new PrivateKeySigner(FIXED_PK);
23
+ const msg = 'hello';
24
+ const sig = await signer.signMessage(msg);
25
+ const recovered = ethers.verifyMessage(msg, sig);
26
+ expect(recovered.toLowerCase()).toBe(FIXED_ADDR.toLowerCase());
27
+ });
28
+
29
+ it('getPublicKey returns the uncompressed key (65 bytes / 130 hex + 0x)', async () => {
30
+ const signer = new PrivateKeySigner(FIXED_PK);
31
+ const pub = await signer.getPublicKey();
32
+ expect(pub.startsWith('0x04')).toBe(true);
33
+ expect(pub.length).toBe(2 + 130);
34
+ });
35
+
36
+ it('pubkey from signer round-trips through ECIES', async () => {
37
+ const signer = new PrivateKeySigner(FIXED_PK);
38
+ const pub = await signer.getPublicKey();
39
+ const plaintext = new TextEncoder().encode('direct-to-signer-pubkey');
40
+ const blob = await eciesEncrypt(plaintext, pub);
41
+ const pt = await eciesDecrypt(blob, FIXED_PK);
42
+ expect(new TextDecoder().decode(pt)).toBe('direct-to-signer-pubkey');
43
+ });
44
+
45
+ it('getPublicKey is cached (returns identical string instance)', async () => {
46
+ const signer = new PrivateKeySigner(FIXED_PK);
47
+ const a = await signer.getPublicKey();
48
+ const b = await signer.getPublicKey();
49
+ expect(a).toBe(b);
50
+ });
51
+ });
52
+
53
+ describe('EthersSigner wrapping an arbitrary ethers.Signer', () => {
54
+ it('wraps a provider-less Wallet and passes through signMessage', async () => {
55
+ const wallet = new ethers.Wallet(FIXED_PK);
56
+ const signer = new EthersSigner(wallet);
57
+ const sig = await signer.signMessage('x');
58
+ expect(ethers.verifyMessage('x', sig).toLowerCase()).toBe(FIXED_ADDR.toLowerCase());
59
+ });
60
+
61
+ it('chainId throws ConfigError when no provider is attached', async () => {
62
+ const wallet = new ethers.Wallet(FIXED_PK);
63
+ const signer = new EthersSigner(wallet);
64
+ await expect(signer.chainId()).rejects.toMatchObject({ code: 'CONFIG/BAD_CONFIG' });
65
+ });
66
+ });
@@ -0,0 +1,7 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ describe('smoke', () => {
4
+ it('vitest runs', () => {
5
+ expect(1 + 1).toBe(2);
6
+ });
7
+ });