@cofhe/sdk 0.4.0 → 0.5.1

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 (95) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/adapters/{ethers5.test.ts → test/ethers5.test.ts} +2 -2
  3. package/adapters/{ethers6.test.ts → test/ethers6.test.ts} +2 -2
  4. package/adapters/{hardhat.hh2.test.ts → test/hardhat.hh2.test.ts} +2 -2
  5. package/adapters/{index.test.ts → test/index.test.ts} +1 -1
  6. package/adapters/{wagmi.test.ts → test/wagmi.test.ts} +1 -1
  7. package/chains/{chains.test.ts → test/chains.test.ts} +1 -1
  8. package/core/client.ts +11 -1
  9. package/core/clientTypes.ts +3 -1
  10. package/core/consts.ts +9 -0
  11. package/core/decrypt/cofheMocksDecryptForTx.ts +14 -3
  12. package/core/decrypt/decryptForTxBuilder.ts +16 -2
  13. package/core/decrypt/decryptForViewBuilder.ts +14 -7
  14. package/core/decrypt/polling.ts +14 -0
  15. package/core/decrypt/tnDecryptV2.ts +250 -110
  16. package/core/decrypt/tnSealOutputV2.ts +245 -104
  17. package/core/decrypt/verifyDecryptResult.ts +65 -0
  18. package/core/encrypt/cofheMocksZkVerifySign.ts +6 -6
  19. package/core/encrypt/zkPackProveVerify.ts +10 -19
  20. package/core/fetchKeys.ts +0 -2
  21. package/core/index.ts +9 -1
  22. package/core/keyStore.ts +5 -2
  23. package/core/permits.ts +5 -0
  24. package/core/{client.test.ts → test/client.test.ts} +7 -7
  25. package/core/{config.test.ts → test/config.test.ts} +1 -1
  26. package/core/test/decrypt.test.ts +252 -0
  27. package/core/test/decryptBuilders.test.ts +390 -0
  28. package/core/{encrypt → test}/encryptInputsBuilder.test.ts +61 -6
  29. package/core/{fetchKeys.test.ts → test/fetchKeys.test.ts} +3 -3
  30. package/core/{keyStore.test.ts → test/keyStore.test.ts} +5 -3
  31. package/core/{permits.test.ts → test/permits.test.ts} +42 -1
  32. package/core/test/pollCallbacks.test.ts +563 -0
  33. package/core/types.ts +13 -0
  34. package/dist/chains.d.cts +2 -2
  35. package/dist/chains.d.ts +2 -2
  36. package/dist/chunk-4FP4V35O.js +13 -0
  37. package/dist/{chunk-NWDKXBIP.js → chunk-MRCKUMOS.js} +62 -22
  38. package/dist/{chunk-MXND5SVN.js → chunk-S7OKGLFD.js} +485 -207
  39. package/dist/{clientTypes-kkrRdawm.d.ts → clientTypes-BSbwairE.d.cts} +23 -6
  40. package/dist/{clientTypes-ACVWbrXL.d.cts → clientTypes-DDmcgZ0a.d.ts} +23 -6
  41. package/dist/core.cjs +561 -244
  42. package/dist/core.d.cts +24 -6
  43. package/dist/core.d.ts +24 -6
  44. package/dist/core.js +3 -2
  45. package/dist/node.cjs +566 -246
  46. package/dist/node.d.cts +3 -3
  47. package/dist/node.d.ts +3 -3
  48. package/dist/node.js +14 -7
  49. package/dist/{permit-MZ502UBl.d.cts → permit-DnVMDT5h.d.cts} +34 -4
  50. package/dist/{permit-MZ502UBl.d.ts → permit-DnVMDT5h.d.ts} +34 -4
  51. package/dist/permits.cjs +66 -29
  52. package/dist/permits.d.cts +18 -13
  53. package/dist/permits.d.ts +18 -13
  54. package/dist/permits.js +2 -1
  55. package/dist/web.cjs +604 -256
  56. package/dist/web.d.cts +8 -4
  57. package/dist/web.d.ts +8 -4
  58. package/dist/web.js +49 -14
  59. package/dist/zkProve.worker.cjs +72 -64
  60. package/dist/zkProve.worker.js +71 -64
  61. package/node/index.ts +13 -4
  62. package/node/test/client.test.ts +25 -0
  63. package/node/test/config.test.ts +16 -0
  64. package/node/test/inherited.test.ts +244 -0
  65. package/node/test/tfheinit.test.ts +56 -0
  66. package/package.json +24 -22
  67. package/permits/permit.ts +31 -5
  68. package/permits/sealing.ts +1 -1
  69. package/permits/{localstorage.test.ts → test/localstorage.test.ts} +2 -2
  70. package/permits/{permit.test.ts → test/permit.test.ts} +35 -1
  71. package/permits/{sealing.test.ts → test/sealing.test.ts} +1 -1
  72. package/permits/{store.test.ts → test/store.test.ts} +2 -2
  73. package/permits/{validation.test.ts → test/validation.test.ts} +82 -6
  74. package/permits/types.ts +1 -1
  75. package/permits/validation.ts +42 -2
  76. package/web/const.ts +2 -0
  77. package/web/index.ts +40 -11
  78. package/web/storage.ts +18 -3
  79. package/web/{client.web.test.ts → test/client.web.test.ts} +13 -1
  80. package/web/test/config.web.test.ts +16 -0
  81. package/web/test/inherited.web.test.ts +245 -0
  82. package/web/test/tfheinit.web.test.ts +62 -0
  83. package/web/{worker.config.web.test.ts → test/worker.config.web.test.ts} +1 -1
  84. package/web/{worker.output.web.test.ts → test/worker.output.web.test.ts} +1 -1
  85. package/web/{workerManager.test.ts → test/workerManager.test.ts} +1 -1
  86. package/web/{workerManager.web.test.ts → test/workerManager.web.test.ts} +1 -1
  87. package/web/zkProve.worker.ts +94 -84
  88. package/node/client.test.ts +0 -147
  89. package/node/config.test.ts +0 -68
  90. package/node/encryptInputs.test.ts +0 -155
  91. package/web/config.web.test.ts +0 -69
  92. package/web/encryptInputs.web.test.ts +0 -172
  93. package/web/worker.builder.web.test.ts +0 -148
  94. /package/dist/{types-YiAC4gig.d.cts → types-C07FK-cL.d.cts} +0 -0
  95. /package/dist/{types-YiAC4gig.d.ts → types-C07FK-cL.d.ts} +0 -0
@@ -0,0 +1,244 @@
1
+ import { Encryptable, FheTypes, type CofheClient } from '@/core';
2
+ import { arbSepolia as cofheArbSepolia, getChainById } from '@/chains';
3
+ import {
4
+ TEST_PRIVATE_KEY,
5
+ PRIMARY_TEST_CHAIN,
6
+ primaryTestChainRegistry,
7
+ isPrimaryTestChainReady,
8
+ } from '@cofhe/test-setup';
9
+
10
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
11
+ import type { Chain, PublicClient, WalletClient } from 'viem';
12
+ import { createPublicClient, createWalletClient, http } from 'viem';
13
+ import { privateKeyToAccount } from 'viem/accounts';
14
+ import { arbitrumSepolia, baseSepolia, sepolia } from 'viem/chains';
15
+ import { createCofheClient, createCofheConfig } from '../index.js';
16
+
17
+ const DEFAULT_TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
18
+ const BOB_PRIVATE_KEY = (TEST_PRIVATE_KEY || DEFAULT_TEST_PRIVATE_KEY) as `0x${string}`;
19
+ const ALICE_PRIVATE_KEY = '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d';
20
+
21
+ const bobAccount = privateKeyToAccount(BOB_PRIVATE_KEY);
22
+ const aliceAccount = privateKeyToAccount(ALICE_PRIVATE_KEY);
23
+
24
+ const VIEM_CHAINS: Record<number, Chain> = {
25
+ 421614: arbitrumSepolia,
26
+ 84532: baseSepolia,
27
+ 11155111: sepolia,
28
+ };
29
+
30
+ describe('@cofhe/node - Inherited Client Tests', () => {
31
+ let cofheClient: CofheClient;
32
+ let publicClient: PublicClient;
33
+ let bobWalletClient: WalletClient;
34
+ let aliceWalletClient: WalletClient;
35
+
36
+ beforeAll(() => {
37
+ publicClient = createPublicClient({
38
+ chain: arbitrumSepolia,
39
+ transport: http(),
40
+ });
41
+
42
+ bobWalletClient = createWalletClient({
43
+ chain: arbitrumSepolia,
44
+ transport: http(),
45
+ account: bobAccount,
46
+ });
47
+
48
+ aliceWalletClient = createWalletClient({
49
+ chain: arbitrumSepolia,
50
+ transport: http(),
51
+ account: aliceAccount,
52
+ });
53
+ });
54
+
55
+ beforeEach(() => {
56
+ const config = createCofheConfig({
57
+ supportedChains: [cofheArbSepolia],
58
+ });
59
+ cofheClient = createCofheClient(config);
60
+ });
61
+
62
+ describe('Client Creation', () => {
63
+ it('should create a client with expected surface', () => {
64
+ expect(cofheClient).toBeDefined();
65
+ expect(cofheClient.config).toBeDefined();
66
+ expect(cofheClient.connected).toBe(false);
67
+ expect(typeof cofheClient.connect).toBe('function');
68
+ expect(typeof cofheClient.disconnect).toBe('function');
69
+ expect(typeof cofheClient.encryptInputs).toBe('function');
70
+ expect(typeof cofheClient.decryptForView).toBe('function');
71
+ expect(typeof cofheClient.decryptForTx).toBe('function');
72
+ expect(typeof cofheClient.getSnapshot).toBe('function');
73
+ expect(typeof cofheClient.subscribe).toBe('function');
74
+ expect(cofheClient.permits).toBeDefined();
75
+ });
76
+ });
77
+
78
+ describe('Connection', () => {
79
+ it('should connect to a real chain', async () => {
80
+ await cofheClient.connect(publicClient, bobWalletClient);
81
+
82
+ expect(cofheClient.connected).toBe(true);
83
+
84
+ const snapshot = cofheClient.getSnapshot();
85
+ expect(snapshot.connected).toBe(true);
86
+ expect(snapshot.chainId).toBe(cofheArbSepolia.id);
87
+ expect(snapshot.account).toBe(bobAccount.address);
88
+ }, 30000);
89
+ });
90
+
91
+ describe('Encrypt Input', () => {
92
+ it('should encrypt a uint128 value', async () => {
93
+ await cofheClient.connect(publicClient, bobWalletClient);
94
+
95
+ const encrypted = await cofheClient.encryptInputs([Encryptable.uint128(100n)]).execute();
96
+
97
+ expect(encrypted).toBeDefined();
98
+ expect(encrypted.length).toBe(1);
99
+ expect(encrypted[0].utype).toBe(FheTypes.Uint128);
100
+ expect(encrypted[0].ctHash).toBeDefined();
101
+ expect(typeof encrypted[0].ctHash).toBe('bigint');
102
+ expect(encrypted[0].signature).toBeDefined();
103
+ expect(typeof encrypted[0].signature).toBe('string');
104
+ expect(encrypted[0].securityZone).toBe(0);
105
+ }, 60000);
106
+ });
107
+
108
+ describe('Self Permit', () => {
109
+ it('should create a self permit', async () => {
110
+ await cofheClient.connect(publicClient, bobWalletClient);
111
+
112
+ const permit = await cofheClient.permits.createSelf({
113
+ issuer: bobAccount.address,
114
+ name: 'Test Self Permit',
115
+ });
116
+
117
+ expect(permit).toBeDefined();
118
+ expect(permit.type).toBe('self');
119
+ expect(permit.name).toBe('Test Self Permit');
120
+ expect(permit.issuer).toBe(bobAccount.address);
121
+ expect(permit.issuerSignature).not.toBe('0x');
122
+ expect(permit.sealingPair).toBeDefined();
123
+ expect(permit.sealingPair.publicKey).toBeDefined();
124
+
125
+ const activePermit = cofheClient.permits.getActivePermit();
126
+ expect(activePermit).toBeDefined();
127
+ expect(activePermit!.hash).toBe(permit.hash);
128
+ }, 30000);
129
+ });
130
+
131
+ describe('Sharing Permit', () => {
132
+ it('should create a sharing permit, export it, and import it as another user', async () => {
133
+ await cofheClient.connect(publicClient, bobWalletClient);
134
+
135
+ const sharingPermit = await cofheClient.permits.createSharing({
136
+ issuer: bobAccount.address,
137
+ recipient: aliceAccount.address,
138
+ name: 'Test Sharing Permit',
139
+ });
140
+
141
+ expect(sharingPermit).toBeDefined();
142
+ expect(sharingPermit.type).toBe('sharing');
143
+ expect(sharingPermit.issuer).toBe(bobAccount.address);
144
+ expect(sharingPermit.recipient).toBe(aliceAccount.address);
145
+ expect(sharingPermit.issuerSignature).not.toBe('0x');
146
+
147
+ const exported = cofheClient.permits.export(sharingPermit);
148
+ expect(exported).toBeDefined();
149
+ const parsed = JSON.parse(exported);
150
+ expect(parsed.type).toBe('sharing');
151
+ expect(parsed.issuer).toBe(bobAccount.address);
152
+ expect(parsed.recipient).toBe(aliceAccount.address);
153
+ expect(parsed.issuerSignature).toBeDefined();
154
+ expect(parsed).not.toHaveProperty('sealingPair');
155
+
156
+ const aliceConfig = createCofheConfig({
157
+ supportedChains: [cofheArbSepolia],
158
+ });
159
+ const aliceClient = createCofheClient(aliceConfig);
160
+ await aliceClient.connect(publicClient, aliceWalletClient);
161
+
162
+ const importedPermit = await aliceClient.permits.importShared(exported);
163
+
164
+ expect(importedPermit).toBeDefined();
165
+ expect(importedPermit.type).toBe('recipient');
166
+ expect(importedPermit.issuer).toBe(bobAccount.address);
167
+ expect(importedPermit.recipient).toBe(aliceAccount.address);
168
+ expect(importedPermit.recipientSignature).not.toBe('0x');
169
+ expect(importedPermit.sealingPair).toBeDefined();
170
+ }, 30000);
171
+ });
172
+
173
+ describe('Decrypt (read-only, pre-stored values)', () => {
174
+ let decryptClient: CofheClient;
175
+ let decryptPublicClient: PublicClient;
176
+ let decryptWalletClient: WalletClient;
177
+
178
+ let privateCtHash: `0x${string}`;
179
+ let privateValue: bigint;
180
+ let publicCtHash: `0x${string}`;
181
+ let publicValue: bigint;
182
+
183
+ beforeAll(() => {
184
+ if (!isPrimaryTestChainReady(primaryTestChainRegistry)) {
185
+ throw new Error('Primary test chain registry not initialized. Run `pnpm test:setup` first.');
186
+ }
187
+
188
+ const reg = primaryTestChainRegistry;
189
+ const viemChain = VIEM_CHAINS[reg.chainId];
190
+ if (!viemChain) throw new Error(`No viem chain mapping for chain ${reg.chainId}`);
191
+
192
+ const cofheChain = getChainById(reg.chainId);
193
+ if (!cofheChain) throw new Error(`No cofhe chain config for chain ${reg.chainId}`);
194
+
195
+ privateCtHash = reg.privateValue.ctHash as `0x${string}`;
196
+ privateValue = BigInt(reg.privateValue.value);
197
+ publicCtHash = reg.publicValue.ctHash as `0x${string}`;
198
+ publicValue = BigInt(reg.publicValue.value);
199
+
200
+ decryptPublicClient = createPublicClient({ chain: viemChain, transport: http() });
201
+ decryptWalletClient = createWalletClient({ chain: viemChain, transport: http(), account: bobAccount });
202
+
203
+ const config = createCofheConfig({ supportedChains: [cofheChain] });
204
+ decryptClient = createCofheClient(config);
205
+ });
206
+
207
+ it('decryptForView — private value with permit', async () => {
208
+ await decryptClient.connect(decryptPublicClient, decryptWalletClient);
209
+
210
+ await decryptClient.permits.createSelf({
211
+ issuer: bobAccount.address,
212
+ name: 'Decrypt View Permit',
213
+ });
214
+
215
+ const result = await decryptClient.decryptForView(privateCtHash, FheTypes.Uint32).execute();
216
+ expect(result).toBe(privateValue);
217
+ }, 180000);
218
+
219
+ it('decryptForTx — public value without permit', async () => {
220
+ await decryptClient.connect(decryptPublicClient, decryptWalletClient);
221
+
222
+ const result = await decryptClient.decryptForTx(publicCtHash).withoutPermit().execute();
223
+
224
+ expect(BigInt(result.ctHash)).toBe(BigInt(publicCtHash));
225
+ expect(result.decryptedValue).toBe(publicValue);
226
+ expect(result.signature).toMatch(/^0x[0-9a-fA-F]+$/);
227
+ }, 180000);
228
+
229
+ it('decryptForTx — private value with permit', async () => {
230
+ await decryptClient.connect(decryptPublicClient, decryptWalletClient);
231
+
232
+ const permit = await decryptClient.permits.createSelf({
233
+ issuer: bobAccount.address,
234
+ name: 'Decrypt Tx Permit',
235
+ });
236
+
237
+ const result = await decryptClient.decryptForTx(privateCtHash).withPermit(permit).execute();
238
+
239
+ expect(BigInt(result.ctHash)).toBe(BigInt(privateCtHash));
240
+ expect(result.decryptedValue).toBe(privateValue);
241
+ expect(result.signature).toBeDefined();
242
+ }, 180000);
243
+ });
244
+ });
@@ -0,0 +1,56 @@
1
+ import { Encryptable, type CofheClient } from '@/core';
2
+ import { arbSepolia as cofheArbSepolia } from '@/chains';
3
+
4
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
5
+ import type { PublicClient, WalletClient } from 'viem';
6
+ import { createPublicClient, createWalletClient, http } from 'viem';
7
+ import { privateKeyToAccount } from 'viem/accounts';
8
+ import { arbitrumSepolia as viemArbitrumSepolia } from 'viem/chains';
9
+ import { createCofheClient, createCofheConfig } from '../index.js';
10
+
11
+ const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
12
+
13
+ describe('@cofhe/node - TFHE Initialization Tests', () => {
14
+ let cofheClient: CofheClient;
15
+ let publicClient: PublicClient;
16
+ let walletClient: WalletClient;
17
+
18
+ beforeAll(() => {
19
+ publicClient = createPublicClient({
20
+ chain: viemArbitrumSepolia,
21
+ transport: http(),
22
+ });
23
+
24
+ const account = privateKeyToAccount(TEST_PRIVATE_KEY);
25
+ walletClient = createWalletClient({
26
+ chain: viemArbitrumSepolia,
27
+ transport: http(),
28
+ account,
29
+ });
30
+ });
31
+
32
+ beforeEach(() => {
33
+ const config = createCofheConfig({
34
+ supportedChains: [cofheArbSepolia],
35
+ });
36
+ cofheClient = createCofheClient(config);
37
+ });
38
+
39
+ describe('Node TFHE Initialization', () => {
40
+ it('should initialize node-tfhe on first encryption', async () => {
41
+ await cofheClient.connect(publicClient, walletClient);
42
+
43
+ const result = await cofheClient.encryptInputs([Encryptable.uint128(100n)]).execute();
44
+
45
+ expect(result).toBeDefined();
46
+ }, 60000);
47
+
48
+ it('should handle multiple encryptions without re-initializing', async () => {
49
+ await cofheClient.connect(publicClient, walletClient);
50
+
51
+ await cofheClient.encryptInputs([Encryptable.uint128(100n)]).execute();
52
+
53
+ await cofheClient.encryptInputs([Encryptable.uint64(50n)]).execute();
54
+ }, 120000);
55
+ });
56
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cofhe/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "SDK for Fhenix COFHE coprocessor interaction",
6
6
  "main": "./dist/core.cjs",
@@ -57,14 +57,14 @@
57
57
  "CHANGELOG.md"
58
58
  ],
59
59
  "dependencies": {
60
- "iframe-shared-storage": "^1.0.34",
61
- "immer": "^10.1.1",
62
- "node-tfhe": "0.11.1",
63
- "tfhe": "0.11.1",
64
- "tweetnacl": "^1.0.3",
65
- "viem": "^2.38.6",
66
- "zod": "^4.0.0",
67
- "zustand": "^5.0.1"
60
+ "iframe-shared-storage": "1.0.34",
61
+ "immer": "10.1.1",
62
+ "node-tfhe": "1.5.3",
63
+ "tfhe": "1.5.3",
64
+ "tweetnacl": "1.0.3",
65
+ "viem": "2.38.6",
66
+ "zod": "4.0.0",
67
+ "zustand": "5.0.1"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@nomicfoundation/hardhat-ethers": "^3.0.0",
@@ -88,24 +88,26 @@
88
88
  }
89
89
  },
90
90
  "devDependencies": {
91
- "@nomicfoundation/hardhat-ethers": "^3.0.0",
92
- "@types/node": "^20.0.0",
93
- "@vitest/browser": "^3.0.0",
94
- "@vitest/coverage-v8": "^3.0.0",
95
- "eslint": "^8.57.0",
96
- "ethers5": "npm:ethers@^5.7.2",
97
- "ethers6": "npm:ethers@^6.13.0",
98
- "happy-dom": "^15.0.0",
99
- "hardhat": "^2.19.0",
100
- "playwright": "^1.55.0",
101
- "tsup": "^8.0.2",
91
+ "@nomicfoundation/hardhat-ethers": "3.1.0",
92
+ "@types/node": "20.19.15",
93
+ "@vitest/browser": "3.2.4",
94
+ "@vitest/coverage-v8": "3.2.4",
95
+ "eslint": "8.57.1",
96
+ "ethers5": "npm:ethers@5.8.0",
97
+ "ethers6": "npm:ethers@6.15.0",
98
+ "happy-dom": "15.11.7",
99
+ "hardhat": "2.26.3",
100
+ "playwright": "1.55.0",
101
+ "tsup": "8.0.2",
102
102
  "typescript": "5.5.4",
103
- "vitest": "^3.0.0",
103
+ "vitest": "3.2.4",
104
104
  "@cofhe/eslint-config": "0.2.1",
105
+ "@cofhe/test-setup": "0.0.0",
105
106
  "@cofhe/tsconfig": "0.1.2"
106
107
  },
107
108
  "publishConfig": {
108
- "access": "public"
109
+ "access": "public",
110
+ "registry": "https://registry.npmjs.org/"
109
111
  },
110
112
  "scripts": {
111
113
  "build": "tsup",
package/permits/permit.ts CHANGED
@@ -22,7 +22,6 @@ import {
22
22
  validateImportPermit,
23
23
  ValidationUtils,
24
24
  } from './validation.js';
25
- import * as z from 'zod';
26
25
  import { SignatureUtils } from './signature.js';
27
26
  import { GenerateSealingKey, SealingKey } from './sealing.js';
28
27
  import { checkPermitValidityOnChain, getAclEIP712Domain } from './onchain-utils.js';
@@ -217,9 +216,9 @@ export const PermitUtils = {
217
216
  },
218
217
 
219
218
  /**
220
- * Validate a permit
219
+ * Validate a permit (schema-level validation)
221
220
  */
222
- validate: (permit: Permit) => {
221
+ validateSchema: (permit: Permit) => {
223
222
  if (permit.type === 'self') {
224
223
  return validateSelfPermit(permit);
225
224
  } else if (permit.type === 'sharing') {
@@ -231,12 +230,28 @@ export const PermitUtils = {
231
230
  }
232
231
  },
233
232
 
233
+ /**
234
+ * Validate a permit (holistic validation).
235
+ *
236
+ * This validates:
237
+ * - Permit schema (shape + invariants)
238
+ * - Permit is signed
239
+ * - Permit is not expired
240
+ *
241
+ * For schema-only validation, use `validateSchema(permit)`.
242
+ */
243
+ validate: (permit: Permit) => {
244
+ const validated = PermitUtils.validateSchema(permit);
245
+ ValidationUtils.assertSignedAndNotExpired(validated as Permit);
246
+ return validated;
247
+ },
248
+
234
249
  /**
235
250
  * Get the permission object from a permit (for use in contracts)
236
251
  */
237
252
  getPermission: (permit: Permit, skipValidation = false): Permission => {
238
253
  if (!skipValidation) {
239
- PermitUtils.validate(permit);
254
+ PermitUtils.validateSchema(permit);
240
255
  }
241
256
 
242
257
  return {
@@ -308,8 +323,19 @@ export const PermitUtils = {
308
323
  },
309
324
 
310
325
  /**
311
- * Check if permit is valid
326
+ * Check if permit is signed and not expired
312
327
  */
328
+ isSignedAndNotExpired: (permit: Permit) => {
329
+ return ValidationUtils.isSignedAndNotExpired(permit);
330
+ },
331
+
332
+ /**
333
+ * Assert that permit is signed and not expired
334
+ */
335
+ assertSignedAndNotExpired: (permit: Permit): void => {
336
+ return ValidationUtils.assertSignedAndNotExpired(permit);
337
+ },
338
+
313
339
  isValid: (permit: Permit) => {
314
340
  return ValidationUtils.isValid(permit);
315
341
  },
@@ -1,4 +1,4 @@
1
- import * as nacl from 'tweetnacl';
1
+ import nacl from 'tweetnacl';
2
2
  import { fromHexString, toBeArray, toBigInt, toHexString, isBigIntOrNumber, isString } from './utils.js';
3
3
 
4
4
  const PRIVATE_KEY_LENGTH = 64;
@@ -11,8 +11,8 @@ import {
11
11
  setActivePermitHash,
12
12
  PermitUtils,
13
13
  permitStore,
14
- } from './index.js';
15
- import { createMockPermit } from './test-utils.js';
14
+ } from '../index.js';
15
+ import { createMockPermit } from '../test-utils.js';
16
16
 
17
17
  // Type declarations for happy-dom environment
18
18
  declare const localStorage: {
@@ -4,7 +4,7 @@ import {
4
4
  type CreateSelfPermitOptions,
5
5
  type CreateSharingPermitOptions,
6
6
  type ImportSharedPermitOptions,
7
- } from './index.js';
7
+ } from '../index.js';
8
8
  import { createPublicClient, createWalletClient, http, type PublicClient, type WalletClient } from 'viem';
9
9
  import { arbitrumSepolia } from 'viem/chains';
10
10
  import { privateKeyToAccount } from 'viem/accounts';
@@ -399,6 +399,29 @@ describe('PermitUtils Tests', () => {
399
399
  expect(parsed).not.toHaveProperty('sealingPair');
400
400
  expect(parsed).not.toHaveProperty('issuerSignature');
401
401
  });
402
+
403
+ it('should export sharing permit data with recipient and issuerSignature', async () => {
404
+ const permit = await PermitUtils.createSharingAndSign(
405
+ {
406
+ issuer: bobAddress,
407
+ recipient: aliceAddress,
408
+ name: 'Test Sharing Permit',
409
+ },
410
+ publicClient,
411
+ bobWalletClient
412
+ );
413
+
414
+ const exported = PermitUtils.export(permit);
415
+ const parsed = JSON.parse(exported);
416
+
417
+ expect(parsed.name).toBe('Test Sharing Permit');
418
+ expect(parsed.type).toBe('sharing');
419
+ expect(parsed.issuer).toBe(bobAddress);
420
+ expect(parsed.recipient).toBe(aliceAddress);
421
+ expect(parsed.issuerSignature).toBeDefined();
422
+ expect(parsed.issuerSignature).not.toBe('0x');
423
+ expect(parsed).not.toHaveProperty('sealingPair');
424
+ });
402
425
  });
403
426
 
404
427
  describe('updateName', () => {
@@ -459,6 +482,17 @@ describe('PermitUtils Tests', () => {
459
482
  expect(validation.valid).toBe(true);
460
483
  expect(validation.error).toBeNull();
461
484
  });
485
+
486
+ it('should throw on validate() for expired signed permit', async () => {
487
+ const expiredPermit = PermitUtils.createSelf({
488
+ issuer: bobAddress,
489
+ name: 'Expired Permit',
490
+ expiration: Math.floor(Date.now() / 1000) - 3600,
491
+ });
492
+
493
+ const signedExpiredPermit = await PermitUtils.sign(expiredPermit, publicClient, bobWalletClient);
494
+ expect(() => PermitUtils.validate(signedExpiredPermit)).toThrow('Permit is expired');
495
+ });
462
496
  });
463
497
 
464
498
  describe('real contract interactions', () => {
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { SealingKey, GenerateSealingKey } from './index.js';
2
+ import { SealingKey, GenerateSealingKey } from '../index.js';
3
3
 
4
4
  describe('SealingKey', () => {
5
5
  it('should create a SealingKey with valid keys', () => {
@@ -13,9 +13,9 @@ import {
13
13
  getActivePermitHash,
14
14
  setActivePermitHash,
15
15
  PermitUtils,
16
- } from './index.js';
16
+ } from '../index.js';
17
17
 
18
- import { createMockPermit } from './test-utils.js';
18
+ import { createMockPermit } from '../test-utils.js';
19
19
 
20
20
  describe('Storage Tests', () => {
21
21
  const chainId = 1;
@@ -11,8 +11,8 @@ import {
11
11
  type CreateSelfPermitOptions,
12
12
  type CreateSharingPermitOptions,
13
13
  type ImportSharedPermitOptions,
14
- } from './index.js';
15
- import { createMockPermit } from './test-utils.js';
14
+ } from '../index.js';
15
+ import { createMockPermit } from '../test-utils.js';
16
16
 
17
17
  describe('Validation Tests', () => {
18
18
  describe('validateSelfPermitOptions', () => {
@@ -247,14 +247,14 @@ describe('Validation Tests', () => {
247
247
  });
248
248
  });
249
249
 
250
- describe('isValid', () => {
250
+ describe('isSignedAndNotExpired', () => {
251
251
  it('should return valid for valid permit', async () => {
252
252
  const permit = {
253
253
  ...(await createMockPermit()),
254
254
  expiration: Math.floor(Date.now() / 1000) + 3600,
255
255
  issuerSignature: '0x1234567890abcdef' as `0x${string}`,
256
256
  };
257
- const result = ValidationUtils.isValid(permit);
257
+ const result = ValidationUtils.isSignedAndNotExpired(permit);
258
258
  expect(result.valid).toBe(true);
259
259
  expect(result.error).toBeNull();
260
260
  });
@@ -265,7 +265,7 @@ describe('Validation Tests', () => {
265
265
  expiration: Math.floor(Date.now() / 1000) - 3600,
266
266
  issuerSignature: '0x1234567890abcdef' as `0x${string}`,
267
267
  };
268
- const result = ValidationUtils.isValid(permit);
268
+ const result = ValidationUtils.isSignedAndNotExpired(permit);
269
269
  expect(result.valid).toBe(false);
270
270
  expect(result.error).toBe('expired');
271
271
  });
@@ -276,10 +276,86 @@ describe('Validation Tests', () => {
276
276
  expiration: Math.floor(Date.now() / 1000) + 3600,
277
277
  issuerSignature: '0x' as `0x${string}`,
278
278
  };
279
- const result = ValidationUtils.isValid(permit);
279
+ const result = ValidationUtils.isSignedAndNotExpired(permit);
280
280
  expect(result.valid).toBe(false);
281
281
  expect(result.error).toBe('not-signed');
282
282
  });
283
283
  });
284
+
285
+ describe('assertSignedAndNotExpired', () => {
286
+ it('should not throw for valid permit', async () => {
287
+ const permit = {
288
+ ...(await createMockPermit()),
289
+ expiration: Math.floor(Date.now() / 1000) + 3600,
290
+ issuerSignature: '0x1234567890abcdef' as `0x${string}`,
291
+ };
292
+ expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).not.toThrow();
293
+ });
294
+
295
+ it('should throw for expired permit', async () => {
296
+ const permit = {
297
+ ...(await createMockPermit()),
298
+ expiration: Math.floor(Date.now() / 1000) - 3600,
299
+ issuerSignature: '0x1234567890abcdef' as `0x${string}`,
300
+ };
301
+ expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).toThrow('Permit is expired');
302
+ });
303
+
304
+ it('should throw for unsigned permit', async () => {
305
+ const permit = {
306
+ ...(await createMockPermit()),
307
+ expiration: Math.floor(Date.now() / 1000) + 3600,
308
+ issuerSignature: '0x' as `0x${string}`,
309
+ };
310
+ expect(() => ValidationUtils.assertSignedAndNotExpired(permit)).toThrow('Permit is not signed');
311
+ });
312
+ });
313
+
314
+ describe('isValid', () => {
315
+ it('should return invalid-schema for schema-invalid permit', async () => {
316
+ const permit = {
317
+ ...(await createMockPermit()),
318
+ type: 'self' as const,
319
+ expiration: Math.floor(Date.now() / 1000) + 3600,
320
+ issuerSignature: '0x1234567890abcdef' as `0x${string}`,
321
+ // Self permits must have recipient == zeroAddress per schema.
322
+ recipient: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as `0x${string}`,
323
+ };
324
+
325
+ const result = ValidationUtils.isValid(permit as unknown as Permit);
326
+ expect(result.valid).toBe(false);
327
+ expect(result.error).toBe('invalid-schema');
328
+ });
329
+
330
+ it('should return expired for expired but otherwise schema-valid permit', async () => {
331
+ const permit = {
332
+ ...(await createMockPermit()),
333
+ type: 'self' as const,
334
+ expiration: Math.floor(Date.now() / 1000) - 3600,
335
+ issuerSignature: '0x1234567890abcdef' as `0x${string}`,
336
+ recipient: '0x0000000000000000000000000000000000000000' as `0x${string}`,
337
+ recipientSignature: '0x' as `0x${string}`,
338
+ };
339
+
340
+ const result = ValidationUtils.isValid(permit as unknown as Permit);
341
+ expect(result.valid).toBe(false);
342
+ expect(result.error).toBe('expired');
343
+ });
344
+
345
+ it('should return valid for schema-valid, signed, non-expired permit', async () => {
346
+ const permit = {
347
+ ...(await createMockPermit()),
348
+ type: 'self' as const,
349
+ expiration: Math.floor(Date.now() / 1000) + 3600,
350
+ issuerSignature: '0x1234567890abcdef' as `0x${string}`,
351
+ recipient: '0x0000000000000000000000000000000000000000' as `0x${string}`,
352
+ recipientSignature: '0x' as `0x${string}`,
353
+ };
354
+
355
+ const result = ValidationUtils.isValid(permit as unknown as Permit);
356
+ expect(result.valid).toBe(true);
357
+ expect(result.error).toBeNull();
358
+ });
359
+ });
284
360
  });
285
361
  });
package/permits/types.ts CHANGED
@@ -189,7 +189,7 @@ export type PermitHashFields = Pick<
189
189
  */
190
190
  export interface ValidationResult {
191
191
  valid: boolean;
192
- error: string | null;
192
+ error: 'invalid-schema' | 'expired' | 'not-signed' | null;
193
193
  }
194
194
 
195
195
  /**