@neus/sdk 1.0.3 → 1.0.5

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/client.js CHANGED
@@ -1,313 +1,312 @@
1
- /**
2
- * NEUS SDK Client
3
- * Create and verify cryptographic proofs across applications
4
- * @license Apache-2.0
5
- */
6
-
7
1
  import { ApiError, ValidationError, NetworkError, ConfigurationError } from './errors.js';
8
- import { constructVerificationMessage, validateWalletAddress, validateUniversalAddress, signMessage, NEUS_CONSTANTS } from './utils.js';
9
-
10
- const FALLBACK_PUBLIC_VERIFIERS = [
11
- 'ownership-basic',
12
- 'ownership-pseudonym',
13
- 'ownership-dns-txt',
14
- 'ownership-social',
15
- 'ownership-org-oauth',
16
- 'contract-ownership',
17
- 'nft-ownership',
18
- 'token-holding',
19
- 'wallet-link',
20
- 'wallet-risk',
21
- 'proof-of-human',
22
- 'agent-identity',
23
- 'agent-delegation',
24
- 'ai-content-moderation'
25
- ];
26
-
27
- const INTERACTIVE_VERIFIERS = new Set([
28
- 'ownership-social',
29
- 'ownership-org-oauth',
30
- 'proof-of-human'
31
- ]);
32
-
33
- const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
34
-
35
- // Validation for supported verifiers
2
+ import {
3
+ PORTABLE_PROOF_SIGNER_HEADER,
4
+ constructVerificationMessage,
5
+ validateWalletAddress,
6
+ validateUniversalAddress,
7
+ signMessage,
8
+ NEUS_CONSTANTS
9
+ } from './utils.js';
10
+
11
+ const FALLBACK_PUBLIC_VERIFIER_CATALOG = {
12
+ 'ownership-basic': { supportsDirectApi: true },
13
+ 'ownership-pseudonym': { supportsDirectApi: true },
14
+ 'ownership-dns-txt': { supportsDirectApi: true },
15
+ 'ownership-social': { supportsDirectApi: false },
16
+ 'ownership-org-oauth': { supportsDirectApi: false },
17
+ 'contract-ownership': { supportsDirectApi: true },
18
+ 'nft-ownership': { supportsDirectApi: true },
19
+ 'token-holding': { supportsDirectApi: true },
20
+ 'wallet-link': { supportsDirectApi: true },
21
+ 'wallet-risk': { supportsDirectApi: true },
22
+ 'proof-of-human': { supportsDirectApi: false },
23
+ 'agent-identity': { supportsDirectApi: true },
24
+ 'agent-delegation': { supportsDirectApi: true },
25
+ 'ai-content-moderation': { supportsDirectApi: true }
26
+ };
27
+
28
+ const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
29
+ const WALLET_LINK_RELATIONSHIP_TYPES = new Set(['primary', 'personal', 'org', 'affiliate', 'agent', 'linked']);
30
+
31
+ function normalizeWalletLinkRelationshipType(value) {
32
+ const normalized = String(value || '').trim().toLowerCase();
33
+ return WALLET_LINK_RELATIONSHIP_TYPES.has(normalized) ? normalized : 'linked';
34
+ }
35
+
36
36
  const validateVerifierData = (verifierId, data) => {
37
37
  if (!data || typeof data !== 'object') {
38
38
  return { valid: false, error: 'Data object is required' };
39
39
  }
40
-
41
- // Format validation for supported verifiers
40
+
42
41
  switch (verifierId) {
43
- case 'ownership-basic':
44
- // Required: owner (must match request walletAddress).
45
- // Reference is optional when content/contentHash is provided.
46
- // If neither content nor contentHash is provided, reference.id is required (reference-only proof).
47
- if (!data.owner || !validateUniversalAddress(data.owner, typeof data.chain === 'string' ? data.chain : undefined)) {
48
- return { valid: false, error: 'owner (universal wallet address) is required' };
42
+ case 'ownership-basic':
43
+ if (!data.owner || !validateUniversalAddress(data.owner, typeof data.chain === 'string' ? data.chain : undefined)) {
44
+ return { valid: false, error: 'owner (universal wallet address) is required' };
45
+ }
46
+ if (data.content !== undefined && data.content !== null) {
47
+ if (typeof data.content !== 'string') {
48
+ return { valid: false, error: 'content must be a string when provided' };
49
49
  }
50
- if (data.content !== undefined && data.content !== null) {
51
- if (typeof data.content !== 'string') {
52
- return { valid: false, error: 'content must be a string when provided' };
53
- }
54
- if (data.content.length > 50000) {
55
- return { valid: false, error: 'content exceeds 50KB inline limit' };
56
- }
50
+ if (data.content.length > 50000) {
51
+ return { valid: false, error: 'content exceeds 50KB inline limit' };
57
52
  }
58
- if (data.contentHash !== undefined && data.contentHash !== null) {
59
- if (typeof data.contentHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(data.contentHash)) {
60
- return { valid: false, error: 'contentHash must be a 32-byte hex string (0x + 64 hex chars) when provided' };
61
- }
53
+ }
54
+ if (data.contentHash !== undefined && data.contentHash !== null) {
55
+ if (typeof data.contentHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(data.contentHash)) {
56
+ return { valid: false, error: 'contentHash must be a 32-byte hex string (0x + 64 hex chars) when provided' };
62
57
  }
63
- if (data.contentType !== undefined && data.contentType !== null) {
64
- if (typeof data.contentType !== 'string' || data.contentType.length > 100) {
65
- return { valid: false, error: 'contentType must be a string (max 100 chars) when provided' };
66
- }
67
- const base = String(data.contentType).split(';')[0].trim().toLowerCase();
68
- // Minimal MIME sanity check (server validates more precisely).
69
- if (!base || base.includes(' ') || !base.includes('/')) {
70
- return { valid: false, error: 'contentType must be a valid MIME type when provided' };
58
+ }
59
+ if (data.contentType !== undefined && data.contentType !== null) {
60
+ if (typeof data.contentType !== 'string' || data.contentType.length > 100) {
61
+ return { valid: false, error: 'contentType must be a string (max 100 chars) when provided' };
62
+ }
63
+ const base = String(data.contentType).split(';')[0].trim().toLowerCase();
64
+ // Minimal MIME sanity check (server validates more precisely).
65
+ if (!base || base.includes(' ') || !base.includes('/')) {
66
+ return { valid: false, error: 'contentType must be a valid MIME type when provided' };
67
+ }
68
+ }
69
+ if (data.provenance !== undefined && data.provenance !== null) {
70
+ if (!data.provenance || typeof data.provenance !== 'object' || Array.isArray(data.provenance)) {
71
+ return { valid: false, error: 'provenance must be an object when provided' };
72
+ }
73
+ const dk = data.provenance.declaredKind;
74
+ if (dk !== undefined && dk !== null) {
75
+ const allowed = ['human', 'ai', 'mixed', 'unknown'];
76
+ if (typeof dk !== 'string' || !allowed.includes(dk)) {
77
+ return { valid: false, error: `provenance.declaredKind must be one of: ${allowed.join(', ')}` };
71
78
  }
72
79
  }
73
- if (data.provenance !== undefined && data.provenance !== null) {
74
- if (!data.provenance || typeof data.provenance !== 'object' || Array.isArray(data.provenance)) {
75
- return { valid: false, error: 'provenance must be an object when provided' };
80
+ const ai = data.provenance.aiContext;
81
+ if (ai !== undefined && ai !== null) {
82
+ if (typeof ai !== 'object' || Array.isArray(ai)) {
83
+ return { valid: false, error: 'provenance.aiContext must be an object when provided' };
76
84
  }
77
- const dk = data.provenance.declaredKind;
78
- if (dk !== undefined && dk !== null) {
79
- const allowed = ['human', 'ai', 'mixed', 'unknown'];
80
- if (typeof dk !== 'string' || !allowed.includes(dk)) {
81
- return { valid: false, error: `provenance.declaredKind must be one of: ${allowed.join(', ')}` };
85
+ if (ai.generatorType !== undefined && ai.generatorType !== null) {
86
+ const allowed = ['local', 'saas', 'agent'];
87
+ if (typeof ai.generatorType !== 'string' || !allowed.includes(ai.generatorType)) {
88
+ return { valid: false, error: `provenance.aiContext.generatorType must be one of: ${allowed.join(', ')}` };
82
89
  }
83
90
  }
84
- const ai = data.provenance.aiContext;
85
- if (ai !== undefined && ai !== null) {
86
- if (typeof ai !== 'object' || Array.isArray(ai)) {
87
- return { valid: false, error: 'provenance.aiContext must be an object when provided' };
88
- }
89
- if (ai.generatorType !== undefined && ai.generatorType !== null) {
90
- const allowed = ['local', 'saas', 'agent'];
91
- if (typeof ai.generatorType !== 'string' || !allowed.includes(ai.generatorType)) {
92
- return { valid: false, error: `provenance.aiContext.generatorType must be one of: ${allowed.join(', ')}` };
93
- }
94
- }
95
- if (ai.provider !== undefined && ai.provider !== null) {
96
- if (typeof ai.provider !== 'string' || ai.provider.length > 64) {
97
- return { valid: false, error: 'provenance.aiContext.provider must be a string (max 64 chars) when provided' };
98
- }
99
- }
100
- if (ai.model !== undefined && ai.model !== null) {
101
- if (typeof ai.model !== 'string' || ai.model.length > 128) {
102
- return { valid: false, error: 'provenance.aiContext.model must be a string (max 128 chars) when provided' };
103
- }
104
- }
105
- if (ai.runId !== undefined && ai.runId !== null) {
106
- if (typeof ai.runId !== 'string' || ai.runId.length > 128) {
107
- return { valid: false, error: 'provenance.aiContext.runId must be a string (max 128 chars) when provided' };
108
- }
91
+ if (ai.provider !== undefined && ai.provider !== null) {
92
+ if (typeof ai.provider !== 'string' || ai.provider.length > 64) {
93
+ return { valid: false, error: 'provenance.aiContext.provider must be a string (max 64 chars) when provided' };
109
94
  }
110
95
  }
111
- }
112
- if (data.reference !== undefined) {
113
- if (!data.reference || typeof data.reference !== 'object') {
114
- return { valid: false, error: 'reference must be an object when provided' };
96
+ if (ai.model !== undefined && ai.model !== null) {
97
+ if (typeof ai.model !== 'string' || ai.model.length > 128) {
98
+ return { valid: false, error: 'provenance.aiContext.model must be a string (max 128 chars) when provided' };
99
+ }
115
100
  }
116
- if (!data.reference.type || typeof data.reference.type !== 'string') {
117
- // Only required when reference object is present (or when doing reference-only proofs).
118
- // Server requires reference.type when reference is used for traceability.
119
- return { valid: false, error: 'reference.type is required when reference is provided' };
101
+ if (ai.runId !== undefined && ai.runId !== null) {
102
+ if (typeof ai.runId !== 'string' || ai.runId.length > 128) {
103
+ return { valid: false, error: 'provenance.aiContext.runId must be a string (max 128 chars) when provided' };
104
+ }
120
105
  }
121
106
  }
122
- if (!data.content && !data.contentHash) {
123
- if (!data.reference || typeof data.reference !== 'object') {
124
- return { valid: false, error: 'reference is required when neither content nor contentHash is provided' };
125
- }
126
- if (!data.reference.id || typeof data.reference.id !== 'string') {
127
- return { valid: false, error: 'reference.id is required when neither content nor contentHash is provided' };
128
- }
129
- if (!data.reference.type || typeof data.reference.type !== 'string') {
130
- return { valid: false, error: 'reference.type is required when neither content nor contentHash is provided' };
131
- }
107
+ }
108
+ if (data.reference !== undefined) {
109
+ if (!data.reference || typeof data.reference !== 'object') {
110
+ return { valid: false, error: 'reference must be an object when provided' };
132
111
  }
133
- break;
134
- case 'nft-ownership':
135
- // ownerAddress is optional; server injects from request walletAddress when omitted.
136
- if (
137
- !data.contractAddress ||
138
- data.tokenId === null ||
139
- data.tokenId === undefined ||
140
- typeof data.chainId !== 'number'
141
- ) {
142
- return { valid: false, error: 'contractAddress, tokenId, and chainId are required' };
112
+ if (!data.reference.type || typeof data.reference.type !== 'string') {
113
+ // Only required when reference object is present (or when doing reference-only proofs).
114
+ // Server requires reference.type when reference is used for traceability.
115
+ return { valid: false, error: 'reference.type is required when reference is provided' };
143
116
  }
144
- if (data.tokenType !== undefined && data.tokenType !== null) {
145
- const tt = String(data.tokenType).toLowerCase();
146
- if (tt !== 'erc721' && tt !== 'erc1155') {
147
- return { valid: false, error: 'tokenType must be one of: erc721, erc1155' };
148
- }
117
+ }
118
+ if (!data.content && !data.contentHash) {
119
+ if (!data.reference || typeof data.reference !== 'object') {
120
+ return { valid: false, error: 'reference is required when neither content nor contentHash is provided' };
149
121
  }
150
- if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
151
- return { valid: false, error: 'blockNumber must be an integer when provided' };
122
+ if (!data.reference.id || typeof data.reference.id !== 'string') {
123
+ return { valid: false, error: 'reference.id is required when neither content nor contentHash is provided' };
152
124
  }
153
- if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
154
- return { valid: false, error: 'Invalid ownerAddress' };
125
+ if (!data.reference.type || typeof data.reference.type !== 'string') {
126
+ return { valid: false, error: 'reference.type is required when neither content nor contentHash is provided' };
155
127
  }
156
- if (!validateWalletAddress(data.contractAddress)) {
157
- return { valid: false, error: 'Invalid contractAddress' };
128
+ }
129
+ break;
130
+ case 'nft-ownership':
131
+ // ownerAddress is optional; server injects from request walletAddress when omitted.
132
+ if (
133
+ !data.contractAddress ||
134
+ data.tokenId === null ||
135
+ data.tokenId === undefined ||
136
+ typeof data.chainId !== 'number'
137
+ ) {
138
+ return { valid: false, error: 'contractAddress, tokenId, and chainId are required' };
139
+ }
140
+ if (data.tokenType !== undefined && data.tokenType !== null) {
141
+ const tt = String(data.tokenType).toLowerCase();
142
+ if (tt !== 'erc721' && tt !== 'erc1155') {
143
+ return { valid: false, error: 'tokenType must be one of: erc721, erc1155' };
158
144
  }
159
- break;
160
- case 'token-holding':
161
- // ownerAddress is optional; server injects from request walletAddress when omitted.
162
- if (
163
- !data.contractAddress ||
145
+ }
146
+ if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
147
+ return { valid: false, error: 'blockNumber must be an integer when provided' };
148
+ }
149
+ if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
150
+ return { valid: false, error: 'Invalid ownerAddress' };
151
+ }
152
+ if (!validateWalletAddress(data.contractAddress)) {
153
+ return { valid: false, error: 'Invalid contractAddress' };
154
+ }
155
+ break;
156
+ case 'token-holding':
157
+ // ownerAddress is optional; server injects from request walletAddress when omitted.
158
+ if (
159
+ !data.contractAddress ||
164
160
  data.minBalance === null ||
165
161
  data.minBalance === undefined ||
166
162
  typeof data.chainId !== 'number'
167
- ) {
168
- return { valid: false, error: 'contractAddress, minBalance, and chainId are required' };
169
- }
170
- if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
171
- return { valid: false, error: 'blockNumber must be an integer when provided' };
172
- }
173
- if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
174
- return { valid: false, error: 'Invalid ownerAddress' };
175
- }
176
- if (!validateWalletAddress(data.contractAddress)) {
177
- return { valid: false, error: 'Invalid contractAddress' };
178
- }
179
- break;
180
- case 'ownership-dns-txt':
181
- if (!data.domain || typeof data.domain !== 'string') {
182
- return { valid: false, error: 'domain is required' };
183
- }
184
- if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
185
- return { valid: false, error: 'Invalid walletAddress' };
186
- }
187
- break;
188
- case 'wallet-link':
189
- if (!data.primaryWalletAddress || !validateUniversalAddress(data.primaryWalletAddress, data.chain)) {
190
- return { valid: false, error: 'primaryWalletAddress is required' };
191
- }
192
- if (!data.secondaryWalletAddress || !validateUniversalAddress(data.secondaryWalletAddress, data.chain)) {
193
- return { valid: false, error: 'secondaryWalletAddress is required' };
194
- }
195
- if (!data.signature || typeof data.signature !== 'string') {
196
- return { valid: false, error: 'signature is required (signed by secondary wallet)' };
197
- }
198
- if (typeof data.chain !== 'string' || !/^[a-z0-9]+:[^\s]+$/.test(data.chain)) {
199
- return { valid: false, error: 'chain is required (namespace:reference)' };
200
- }
201
- if (typeof data.signatureMethod !== 'string' || !data.signatureMethod.trim()) {
202
- return { valid: false, error: 'signatureMethod is required' };
203
- }
204
- if (typeof data.signedTimestamp !== 'number') {
205
- return { valid: false, error: 'signedTimestamp is required' };
206
- }
207
- break;
208
- case 'contract-ownership':
209
- if (!data.contractAddress || !validateWalletAddress(data.contractAddress)) {
210
- return { valid: false, error: 'contractAddress is required' };
211
- }
212
- if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
213
- return { valid: false, error: 'Invalid walletAddress' };
214
- }
215
- if (typeof data.chainId !== 'number') {
216
- return { valid: false, error: 'chainId is required' };
217
- }
218
- break;
219
- case 'agent-identity':
220
- if (!data.agentId || typeof data.agentId !== 'string' || data.agentId.length < 1 || data.agentId.length > 128) {
221
- return { valid: false, error: 'agentId is required (1-128 chars)' };
222
- }
223
- if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
224
- return { valid: false, error: 'agentWallet is required' };
225
- }
226
- if (data.agentType && !['ai', 'bot', 'service', 'automation', 'agent'].includes(data.agentType)) {
227
- return { valid: false, error: 'agentType must be one of: ai, bot, service, automation, agent' };
228
- }
229
- break;
230
- case 'agent-delegation':
231
- if (!data.controllerWallet || !validateWalletAddress(data.controllerWallet)) {
232
- return { valid: false, error: 'controllerWallet is required' };
233
- }
234
- if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
235
- return { valid: false, error: 'agentWallet is required' };
236
- }
237
- if (data.scope && (typeof data.scope !== 'string' || data.scope.length > 128)) {
238
- return { valid: false, error: 'scope must be a string (max 128 chars)' };
239
- }
240
- if (data.expiresAt && (typeof data.expiresAt !== 'number' || data.expiresAt < Date.now())) {
241
- return { valid: false, error: 'expiresAt must be a future timestamp' };
242
- }
243
- break;
244
- case 'ai-content-moderation':
245
- if (!data.content || typeof data.content !== 'string') {
246
- return { valid: false, error: 'content is required' };
247
- }
248
- if (!data.contentType || typeof data.contentType !== 'string') {
249
- return { valid: false, error: 'contentType (MIME type) is required' };
250
- }
251
- {
252
- // Only allow content types that are actually moderated (no "verified but skipped" bypass).
253
- const contentType = String(data.contentType).split(';')[0].trim().toLowerCase();
254
- const validTypes = [
255
- 'image/jpeg',
256
- 'image/png',
257
- 'image/webp',
258
- 'image/gif',
259
- 'text/plain',
260
- 'text/markdown',
261
- 'text/x-markdown',
262
- 'application/json',
263
- 'application/xml'
264
- ];
265
- if (!validTypes.includes(contentType)) {
266
- return { valid: false, error: `contentType must be one of: ${validTypes.join(', ')}` };
267
- }
268
- const isTextual = contentType.startsWith('text/') || contentType.includes('markdown');
269
- if (isTextual) {
270
- // Backend enforces 50KB UTF-8 limit for textual moderation payloads.
271
- try {
272
- const maxBytes = 50 * 1024;
273
- const bytes = (typeof TextEncoder !== 'undefined')
274
- ? new TextEncoder().encode(data.content).length
275
- : String(data.content).length;
276
- if (bytes > maxBytes) {
277
- return { valid: false, error: `content exceeds ${maxBytes} bytes limit for ai-content-moderation verifier (text)` };
278
- }
279
- } catch {
280
- // If encoding fails, defer to server.
163
+ ) {
164
+ return { valid: false, error: 'contractAddress, minBalance, and chainId are required' };
165
+ }
166
+ if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
167
+ return { valid: false, error: 'blockNumber must be an integer when provided' };
168
+ }
169
+ if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
170
+ return { valid: false, error: 'Invalid ownerAddress' };
171
+ }
172
+ if (!validateWalletAddress(data.contractAddress)) {
173
+ return { valid: false, error: 'Invalid contractAddress' };
174
+ }
175
+ break;
176
+ case 'ownership-dns-txt':
177
+ if (!data.domain || typeof data.domain !== 'string') {
178
+ return { valid: false, error: 'domain is required' };
179
+ }
180
+ if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
181
+ return { valid: false, error: 'Invalid walletAddress' };
182
+ }
183
+ break;
184
+ case 'wallet-link':
185
+ if (!data.primaryWalletAddress || !validateUniversalAddress(data.primaryWalletAddress, data.chain)) {
186
+ return { valid: false, error: 'primaryWalletAddress is required' };
187
+ }
188
+ if (!data.secondaryWalletAddress || !validateUniversalAddress(data.secondaryWalletAddress, data.chain)) {
189
+ return { valid: false, error: 'secondaryWalletAddress is required' };
190
+ }
191
+ if (!data.signature || typeof data.signature !== 'string') {
192
+ return { valid: false, error: 'signature is required (signed by secondary wallet)' };
193
+ }
194
+ if (typeof data.chain !== 'string' || !/^[a-z0-9]+:[^\s]+$/.test(data.chain)) {
195
+ return { valid: false, error: 'chain is required (namespace:reference)' };
196
+ }
197
+ if (typeof data.signatureMethod !== 'string' || !data.signatureMethod.trim()) {
198
+ return { valid: false, error: 'signatureMethod is required' };
199
+ }
200
+ if (typeof data.signedTimestamp !== 'number') {
201
+ return { valid: false, error: 'signedTimestamp is required' };
202
+ }
203
+ break;
204
+ case 'contract-ownership':
205
+ if (!data.contractAddress || !validateWalletAddress(data.contractAddress)) {
206
+ return { valid: false, error: 'contractAddress is required' };
207
+ }
208
+ if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
209
+ return { valid: false, error: 'Invalid walletAddress' };
210
+ }
211
+ if (typeof data.chainId !== 'number') {
212
+ return { valid: false, error: 'chainId is required' };
213
+ }
214
+ break;
215
+ case 'agent-identity':
216
+ if (!data.agentId || typeof data.agentId !== 'string' || data.agentId.length < 1 || data.agentId.length > 128) {
217
+ return { valid: false, error: 'agentId is required (1-128 chars)' };
218
+ }
219
+ if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
220
+ return { valid: false, error: 'agentWallet is required' };
221
+ }
222
+ if (data.agentType && !['ai', 'bot', 'service', 'automation', 'agent'].includes(data.agentType)) {
223
+ return { valid: false, error: 'agentType must be one of: ai, bot, service, automation, agent' };
224
+ }
225
+ break;
226
+ case 'agent-delegation':
227
+ if (!data.controllerWallet || !validateWalletAddress(data.controllerWallet)) {
228
+ return { valid: false, error: 'controllerWallet is required' };
229
+ }
230
+ if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
231
+ return { valid: false, error: 'agentWallet is required' };
232
+ }
233
+ if (data.scope && (typeof data.scope !== 'string' || data.scope.length > 128)) {
234
+ return { valid: false, error: 'scope must be a string (max 128 chars)' };
235
+ }
236
+ if (data.expiresAt && (typeof data.expiresAt !== 'number' || data.expiresAt < Date.now())) {
237
+ return { valid: false, error: 'expiresAt must be a future timestamp' };
238
+ }
239
+ break;
240
+ case 'ai-content-moderation':
241
+ if (!data.content || typeof data.content !== 'string') {
242
+ return { valid: false, error: 'content is required' };
243
+ }
244
+ if (!data.contentType || typeof data.contentType !== 'string') {
245
+ return { valid: false, error: 'contentType (MIME type) is required' };
246
+ }
247
+ {
248
+ // Only allow content types that are actually moderated (no "verified but skipped" bypass).
249
+ const contentType = String(data.contentType).split(';')[0].trim().toLowerCase();
250
+ const validTypes = [
251
+ 'image/jpeg',
252
+ 'image/png',
253
+ 'image/webp',
254
+ 'image/gif',
255
+ 'text/plain',
256
+ 'text/markdown',
257
+ 'text/x-markdown',
258
+ 'application/json',
259
+ 'application/xml'
260
+ ];
261
+ if (!validTypes.includes(contentType)) {
262
+ return { valid: false, error: `contentType must be one of: ${validTypes.join(', ')}` };
263
+ }
264
+ const isTextual = contentType.startsWith('text/') || contentType.includes('markdown');
265
+ if (isTextual) {
266
+ // Backend enforces 50KB UTF-8 limit for textual moderation payloads.
267
+ try {
268
+ const maxBytes = 50 * 1024;
269
+ const bytes = (typeof TextEncoder !== 'undefined')
270
+ ? new TextEncoder().encode(data.content).length
271
+ : String(data.content).length;
272
+ if (bytes > maxBytes) {
273
+ return {
274
+ valid: false,
275
+ error: `content exceeds ${maxBytes} bytes limit for ai-content-moderation verifier (text)`
276
+ };
281
277
  }
278
+ } catch {
279
+ // If encoding fails, defer to server.
282
280
  }
283
281
  }
284
- if (data.content.length > 13653334) {
285
- return { valid: false, error: 'content exceeds 10MB limit' };
286
- }
287
- break;
288
- case 'ownership-pseudonym':
289
- if (!data.pseudonymId || typeof data.pseudonymId !== 'string') {
290
- return { valid: false, error: 'pseudonymId is required' };
291
- }
292
- // Validate handle format (3-64 chars, lowercase alphanumeric with ._-)
293
- if (!/^[a-z0-9._-]{3,64}$/.test(data.pseudonymId.trim().toLowerCase())) {
294
- return { valid: false, error: 'pseudonymId must be 3-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
295
- }
296
- // Validate namespace if provided (1-64 chars)
297
- if (data.namespace && typeof data.namespace === 'string') {
298
- if (!/^[a-z0-9._-]{1,64}$/.test(data.namespace.trim().toLowerCase())) {
299
- return { valid: false, error: 'namespace must be 1-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
300
- }
301
- }
302
- // Note: signature is not required - envelope signature provides authentication
303
- break;
304
- case 'wallet-risk':
305
- if (data.walletAddress && !validateUniversalAddress(data.walletAddress, data.chain)) {
306
- return { valid: false, error: 'Invalid walletAddress' };
282
+ }
283
+ if (data.content.length > 13653334) {
284
+ return { valid: false, error: 'content exceeds 10MB limit' };
285
+ }
286
+ break;
287
+ case 'ownership-pseudonym':
288
+ if (!data.pseudonymId || typeof data.pseudonymId !== 'string') {
289
+ return { valid: false, error: 'pseudonymId is required' };
290
+ }
291
+ // Validate handle format (3-64 chars, lowercase alphanumeric with ._-)
292
+ if (!/^[a-z0-9._-]{3,64}$/.test(data.pseudonymId.trim().toLowerCase())) {
293
+ return { valid: false, error: 'pseudonymId must be 3-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
294
+ }
295
+ // Validate namespace if provided (1-64 chars)
296
+ if (data.namespace && typeof data.namespace === 'string') {
297
+ if (!/^[a-z0-9._-]{1,64}$/.test(data.namespace.trim().toLowerCase())) {
298
+ return { valid: false, error: 'namespace must be 1-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
307
299
  }
308
- break;
300
+ }
301
+ // Note: signature is not required - envelope signature provides authentication
302
+ break;
303
+ case 'wallet-risk':
304
+ if (data.walletAddress && !validateUniversalAddress(data.walletAddress, data.chain)) {
305
+ return { valid: false, error: 'Invalid walletAddress' };
306
+ }
307
+ break;
309
308
  }
310
-
309
+
311
310
  return { valid: true };
312
311
  };
313
312
 
@@ -319,7 +318,6 @@ export class NeusClient {
319
318
  ...config
320
319
  };
321
320
 
322
- // NEUS Network API
323
321
  this.baseUrl = this.config.apiUrl || 'https://api.neus.network';
324
322
  // Enforce HTTPS for neus.network domains to satisfy CSP and normalize URLs
325
323
  try {
@@ -332,32 +330,25 @@ export class NeusClient {
332
330
  } catch {
333
331
  // If invalid URL string, leave as-is
334
332
  }
335
- // Normalize apiUrl on config
336
333
  this.config.apiUrl = this.baseUrl;
337
- // Default headers for API requests
334
+
335
+ // Default headers
336
+
338
337
  this.defaultHeaders = {
339
338
  'Content-Type': 'application/json',
340
339
  'Accept': 'application/json',
341
340
  'X-Neus-Sdk': 'js'
342
341
  };
343
342
 
344
- // Optional API key (server-side only; do not embed in browser apps)
345
343
  if (typeof this.config.apiKey === 'string' && this.config.apiKey.trim().length > 0) {
346
344
  this.defaultHeaders['Authorization'] = `Bearer ${this.config.apiKey.trim()}`;
347
345
  }
348
- // Public app attribution header (non-secret)
349
346
  if (typeof this.config.appId === 'string' && this.config.appId.trim().length > 0) {
350
347
  this.defaultHeaders['X-Neus-App'] = this.config.appId.trim();
351
348
  }
352
- // Ephemeral sponsor capability token
353
- if (typeof this.config.sponsorGrant === 'string' && this.config.sponsorGrant.trim().length > 0) {
354
- this.defaultHeaders['X-Sponsor-Grant'] = this.config.sponsorGrant.trim();
355
- }
356
- // x402 retry receipt header
357
- if (typeof this.config.paymentSignature === 'string' && this.config.paymentSignature.trim().length > 0) {
358
- this.defaultHeaders['PAYMENT-SIGNATURE'] = this.config.paymentSignature.trim();
359
- }
360
- // Optional caller-supplied passthrough headers.
349
+ if (typeof this.config.paymentSignature === 'string' && this.config.paymentSignature.trim().length > 0) {
350
+ this.defaultHeaders['PAYMENT-SIGNATURE'] = this.config.paymentSignature.trim();
351
+ }
361
352
  if (this.config.extraHeaders && typeof this.config.extraHeaders === 'object') {
362
353
  for (const [k, v] of Object.entries(this.config.extraHeaders)) {
363
354
  if (!k || v === undefined || v === null) continue;
@@ -368,7 +359,6 @@ export class NeusClient {
368
359
  }
369
360
  }
370
361
  try {
371
- // Attach origin in browser environments
372
362
  if (typeof window !== 'undefined' && window.location && window.location.origin) {
373
363
  this.defaultHeaders['X-Client-Origin'] = window.location.origin;
374
364
  }
@@ -493,76 +483,136 @@ export class NeusClient {
493
483
  };
494
484
  }
495
485
 
496
- async createGatePrivateAuth(params = {}) {
497
- const address = (params.address || '').toString();
498
- if (!validateUniversalAddress(address, params.chain)) {
499
- throw new ValidationError('Valid address is required');
500
- }
486
+ async createGatePrivateAuth(params = {}) {
487
+ const address = (params.address || '').toString();
488
+ if (!validateUniversalAddress(address, params.chain)) {
489
+ throw new ValidationError('Valid address is required');
490
+ }
501
491
  return this._buildPrivateGateAuth({
502
492
  address,
503
493
  wallet: params.wallet,
504
494
  chain: params.chain,
505
- signatureMethod: params.signatureMethod
506
- });
507
- }
508
-
509
- // ============================================================================
510
- // CORE VERIFICATION METHODS
511
- // ============================================================================
512
-
513
- /**
514
- * VERIFY - Standard verification (auto or manual)
515
- *
516
- * Create proofs with complete control over the verification process.
517
- * If signature and walletAddress are omitted but verifier/content are provided,
518
- * this method performs the wallet flow inline (no aliases, no secondary methods).
519
- *
495
+ signatureMethod: params.signatureMethod
496
+ });
497
+ }
498
+
499
+ async createWalletLinkData(params = {}) {
500
+ const normalizedPrimary = this._normalizeIdentity(params.primaryWalletAddress);
501
+ const normalizedSecondary = this._normalizeIdentity(params.secondaryWalletAddress);
502
+
503
+ if (!EVM_ADDRESS_RE.test(normalizedPrimary)) {
504
+ throw new ValidationError('wallet-link requires a valid primaryWalletAddress');
505
+ }
506
+ if (!EVM_ADDRESS_RE.test(normalizedSecondary)) {
507
+ throw new ValidationError('wallet-link requires a valid secondaryWalletAddress');
508
+ }
509
+ if (normalizedPrimary === normalizedSecondary) {
510
+ throw new ValidationError('wallet-link secondaryWalletAddress must differ from primaryWalletAddress');
511
+ }
512
+
513
+ const providerWallet = params.wallet || this._getDefaultBrowserWallet();
514
+ const { signerWalletAddress, provider } = await this._resolveWalletSigner(providerWallet);
515
+ const normalizedSigner = this._normalizeIdentity(signerWalletAddress);
516
+ if (!EVM_ADDRESS_RE.test(normalizedSigner)) {
517
+ throw new ValidationError('wallet-link requires an EVM wallet/provider for the secondary wallet');
518
+ }
519
+ if (normalizedSigner !== normalizedSecondary) {
520
+ throw new ValidationError('wallet-link wallet/provider must be connected to secondaryWalletAddress');
521
+ }
522
+
523
+ const resolvedChain = (() => {
524
+ const raw = String(params.chain || '').trim();
525
+ if (!raw) return `eip155:${this._getHubChainId()}`;
526
+ if (!/^eip155:\d+$/.test(raw)) {
527
+ throw new ValidationError('wallet-link requires chain in the form eip155:<chainId>');
528
+ }
529
+ return raw;
530
+ })();
531
+ const signedTimestamp = Number.isFinite(Number(params.signedTimestamp)) && Number(params.signedTimestamp) > 0
532
+ ? Number(params.signedTimestamp)
533
+ : Date.now();
534
+ const linkData = {
535
+ primaryAccountId: `${resolvedChain}:${normalizedPrimary}`,
536
+ secondaryAccountId: `${resolvedChain}:${normalizedSecondary}`,
537
+ primaryWalletAddress: normalizedPrimary,
538
+ secondaryWalletAddress: normalizedSecondary
539
+ };
540
+ const message = constructVerificationMessage({
541
+ walletAddress: normalizedSecondary,
542
+ signedTimestamp,
543
+ data: linkData,
544
+ verifierIds: ['wallet-link'],
545
+ chain: resolvedChain
546
+ });
547
+
548
+ let signature;
549
+ try {
550
+ signature = await signMessage({
551
+ provider,
552
+ message,
553
+ walletAddress: signerWalletAddress
554
+ });
555
+ } catch (error) {
556
+ if (error?.code === 4001) {
557
+ throw new ValidationError('User rejected wallet-link signature request');
558
+ }
559
+ throw new ValidationError(`Failed to sign wallet-link message: ${error?.message || String(error)}`);
560
+ }
561
+
562
+ const label = String(params.label || '').trim().slice(0, 64);
563
+ return {
564
+ primaryWalletAddress: normalizedPrimary,
565
+ secondaryWalletAddress: normalizedSecondary,
566
+ signature,
567
+ chain: resolvedChain,
568
+ signatureMethod: 'eip191',
569
+ signedTimestamp,
570
+ relationshipType: normalizeWalletLinkRelationshipType(params.relationshipType),
571
+ ...(label ? { label } : {})
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Create a verification proof.
577
+ *
578
+ * Supports two paths:
579
+ * - **Auto:** Supply `verifier`, `content`, and/or `data` with an optional
580
+ * `wallet` provider. The SDK performs signing in the client.
581
+ * - **Manual:** Supply pre-signed `verifierIds`, `data`, `walletAddress`,
582
+ * `signature`, and `signedTimestamp`.
583
+ *
520
584
  * @param {Object} params - Verification parameters
585
+ * @param {string} [params.verifier] - Verifier ID (auto path, e.g. 'ownership-basic')
586
+ * @param {string} [params.content] - Content to verify (auto path)
587
+ * @param {Object} [params.data] - Structured verification data
588
+ * @param {Object} [params.wallet] - Wallet provider (auto path)
589
+ * @param {Object} [params.options] - Additional options
521
590
  * @param {Array<string>} [params.verifierIds] - Array of verifier IDs (manual path)
522
- * @param {Object} [params.data] - Verification data object (manual path)
523
591
  * @param {string} [params.walletAddress] - Wallet address that signed the request (manual path)
524
592
  * @param {string} [params.signature] - EIP-191 signature (manual path)
525
593
  * @param {number} [params.signedTimestamp] - Unix timestamp when signature was created (manual path)
526
- * @param {number} [params.chainId] - Chain ID for verification context (optional, managed by protocol)
527
- * @param {Object} [params.options] - Additional options
528
- * @param {string} [params.verifier] - Verifier ID (auto path)
529
- * @param {string} [params.content] - Content/description (auto path)
530
- * @param {Object} [params.wallet] - Optional injected wallet/provider (auto path)
531
- * @returns {Promise<Object>} Verification result with proofId (qHash is a deprecated alias)
532
- *
594
+ * @param {number} [params.chainId] - EVM signing-context hint; when omitted, resolved to the NEUS protocol primary chain for signing
595
+ * @returns {Promise<Object>} Verification result with proofId
596
+ *
597
+ * @example
598
+ * // Auto path
599
+ * const proof = await client.verify({
600
+ * verifier: 'ownership-basic',
601
+ * content: 'Hello World',
602
+ * wallet: window.ethereum
603
+ * });
604
+ *
533
605
  * @example
606
+ * // Manual path
534
607
  * const proof = await client.verify({
535
608
  * verifierIds: ['ownership-basic'],
536
- * data: {
537
- * content: "My content",
538
- * owner: walletAddress, // or ownerAddress for nft-ownership/token-holding
539
- * reference: { type: 'other', id: 'my-unique-identifier' }
540
- * },
609
+ * data: { content: "My content", owner: walletAddress },
541
610
  * walletAddress: '0x...',
542
611
  * signature: '0x...',
543
612
  * signedTimestamp: Date.now(),
544
613
  * options: { targetChains: [421614, 11155111] }
545
614
  * });
546
615
  */
547
- /**
548
- * Create a verification proof
549
- *
550
- * @param {Object} params - Verification parameters
551
- * @param {string} [params.verifier] - Verifier ID (e.g., 'ownership-basic')
552
- * @param {string} [params.content] - Content to verify
553
- * @param {Object} [params.data] - Structured verification data
554
- * @param {Object} [params.wallet] - Wallet provider
555
- * @param {Object} [params.options] - Additional options
556
- * @returns {Promise<Object>} Verification result with proofId (qHash is a deprecated alias)
557
- *
558
- * @example
559
- * // Simple ownership proof
560
- * const proof = await client.verify({
561
- * verifier: 'ownership-basic',
562
- * content: 'Hello World',
563
- * wallet: window.ethereum
564
- * });
565
- */
566
616
  async verify(params) {
567
617
  // Auto path: if no manual signature fields but auto fields are provided, perform inline wallet flow
568
618
  if ((!params?.signature || !params?.walletAddress) && (params?.verifier || params?.content || params?.data)) {
@@ -573,30 +623,31 @@ export class NeusClient {
573
623
  throw new ValidationError('content is required and must be a string (or use data param with owner + reference)');
574
624
  }
575
625
 
576
- let validVerifiers = FALLBACK_PUBLIC_VERIFIERS;
626
+ let verifierCatalog = FALLBACK_PUBLIC_VERIFIER_CATALOG;
577
627
  try {
578
- const discovered = await this.getVerifiers();
579
- if (Array.isArray(discovered) && discovered.length > 0) {
580
- validVerifiers = discovered;
628
+ const discovered = await this.getVerifierCatalog();
629
+ if (discovered && discovered.metadata && Object.keys(discovered.metadata).length > 0) {
630
+ verifierCatalog = discovered.metadata;
581
631
  }
582
632
  } catch {
583
633
  // Fallback keeps SDK usable if verifier catalog endpoint is temporarily unavailable.
584
634
  }
635
+ const validVerifiers = Object.keys(verifierCatalog);
585
636
  if (!validVerifiers.includes(verifier)) {
586
637
  throw new ValidationError(`Invalid verifier '${verifier}'. Must be one of: ${validVerifiers.join(', ')}.`);
587
638
  }
588
639
 
589
- if (INTERACTIVE_VERIFIERS.has(verifier)) {
640
+ if (verifierCatalog?.[verifier]?.supportsDirectApi === false) {
590
641
  const hostedCheckoutUrl = options?.hostedCheckoutUrl || 'https://neus.network/verify';
591
642
  throw new ValidationError(
592
643
  `${verifier} requires hosted interactive checkout. Use VerifyGate or redirect to ${hostedCheckoutUrl}.`
593
644
  );
594
645
  }
595
-
646
+
596
647
  // These verifiers require explicit data parameter (no auto-path)
597
648
  const requiresDataParam = [
598
- 'ownership-dns-txt',
599
- 'wallet-link',
649
+ 'ownership-dns-txt',
650
+ 'wallet-link',
600
651
  'contract-ownership',
601
652
  'ownership-pseudonym',
602
653
  'wallet-risk',
@@ -739,7 +790,10 @@ export class NeusClient {
739
790
  ...(data?.agentLabel && { agentLabel: data.agentLabel }),
740
791
  ...(data?.agentType && { agentType: data.agentType }),
741
792
  ...(data?.description && { description: data.description }),
742
- ...(data?.capabilities && { capabilities: data.capabilities })
793
+ ...(data?.capabilities && { capabilities: data.capabilities }),
794
+ ...(data?.instructions && { instructions: data.instructions }),
795
+ ...(data?.skills && { skills: data.skills }),
796
+ ...(data?.services && { services: data.services })
743
797
  };
744
798
  } else if (verifier === 'agent-delegation') {
745
799
  if (!data?.agentWallet) {
@@ -752,7 +806,11 @@ export class NeusClient {
752
806
  ...(data?.scope && { scope: data.scope }),
753
807
  ...(data?.permissions && { permissions: data.permissions }),
754
808
  ...(data?.maxSpend && { maxSpend: data.maxSpend }),
755
- ...(data?.expiresAt && { expiresAt: data.expiresAt })
809
+ ...(data?.allowedPaymentTypes && { allowedPaymentTypes: data.allowedPaymentTypes }),
810
+ ...(data?.receiptDisclosure && { receiptDisclosure: data.receiptDisclosure }),
811
+ ...(data?.expiresAt && { expiresAt: data.expiresAt }),
812
+ ...(data?.instructions && { instructions: data.instructions }),
813
+ ...(data?.skills && { skills: data.skills })
756
814
  };
757
815
  } else if (verifier === 'ai-content-moderation') {
758
816
  if (!data?.content) {
@@ -787,7 +845,6 @@ export class NeusClient {
787
845
  ...(data?.includeDetails !== undefined && { includeDetails: data.includeDetails })
788
846
  };
789
847
  } else {
790
- // Default structure for unknown verifiers (should not reach here with validVerifiers check)
791
848
  verificationData = data ? {
792
849
  content,
793
850
  owner: walletAddress,
@@ -810,7 +867,6 @@ export class NeusClient {
810
867
 
811
868
  let signature;
812
869
  try {
813
- // UNIFIED SIGNING: Matches utils/core.ts personalSignUniversal exactly
814
870
  const toHexUtf8 = (s) => {
815
871
  try {
816
872
  const enc = new TextEncoder();
@@ -824,7 +880,7 @@ export class NeusClient {
824
880
  return hex;
825
881
  }
826
882
  };
827
-
883
+
828
884
  // Detect Farcaster wallet - requires hex-encoded messages FIRST
829
885
  const isFarcasterWallet = (() => {
830
886
  if (typeof window === 'undefined') return false;
@@ -841,7 +897,7 @@ export class NeusClient {
841
897
  }
842
898
  return false;
843
899
  })();
844
-
900
+
845
901
  if (isFarcasterWallet) {
846
902
  try {
847
903
  const hexMsg = toHexUtf8(message);
@@ -850,7 +906,7 @@ export class NeusClient {
850
906
  // Fall through
851
907
  }
852
908
  }
853
-
909
+
854
910
  if (!signature) {
855
911
  try {
856
912
  signature = await provider.request({ method: 'personal_sign', params: [message, walletAddress] });
@@ -858,15 +914,17 @@ export class NeusClient {
858
914
  const msg = String(e && (e.message || e.reason) || e || '').toLowerCase();
859
915
  const errCode = (e && (e.code || (e.error && e.error.code))) || null;
860
916
  const needsHex = /byte|bytes|invalid byte sequence|encoding|non-hex/i.test(msg);
861
-
917
+
918
+ const unsupportedRe =
919
+ /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i;
862
920
  const methodUnsupported = (
863
- /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i.test(msg) ||
921
+ unsupportedRe.test(msg) ||
864
922
  errCode === -32601 ||
865
923
  errCode === 4200 ||
866
924
  (msg.includes('personal_sign') && msg.includes('not')) ||
867
925
  (msg.includes('request method') && msg.includes('not supported'))
868
926
  );
869
-
927
+
870
928
  if (methodUnsupported) {
871
929
  this._log('personal_sign not supported; attempting eth_sign fallback');
872
930
  try {
@@ -974,14 +1032,15 @@ export class NeusClient {
974
1032
  }
975
1033
  }
976
1034
 
977
- // Build options payload (public surface)
1035
+ // Build options payload.
978
1036
  const optionsPayload = {
979
1037
  ...(options && typeof options === 'object' ? options : {}),
980
1038
  targetChains: Array.isArray(options?.targetChains) ? options.targetChains : [],
981
1039
  // Privacy and storage options (defaults)
982
1040
  privacyLevel: options?.privacyLevel || 'private',
983
1041
  publicDisplay: options?.publicDisplay || false,
984
- storeOriginalContent: options?.storeOriginalContent || false
1042
+ storeOriginalContent:
1043
+ typeof options?.storeOriginalContent === 'boolean' ? options.storeOriginalContent : true
985
1044
  };
986
1045
  if (typeof options?.enableIpfs === 'boolean') optionsPayload.enableIpfs = options.enableIpfs;
987
1046
 
@@ -997,8 +1056,6 @@ export class NeusClient {
997
1056
  options: optionsPayload
998
1057
  };
999
1058
 
1000
- // SECURITY: Do not send proof signatures in Authorization headers.
1001
- // Signatures belong in the request body only (they are not bearer tokens).
1002
1059
  const response = await this._makeRequest('POST', '/api/v1/verification', requestData);
1003
1060
 
1004
1061
  if (!response.success) {
@@ -1008,50 +1065,44 @@ export class NeusClient {
1008
1065
  return this._formatResponse(response);
1009
1066
  }
1010
1067
 
1011
- // ============================================================================
1012
- // STATUS AND UTILITY METHODS
1013
- // ============================================================================
1014
-
1015
1068
  /**
1016
- * Get verification status
1069
+ * Get proof record by proof receipt id.
1017
1070
  *
1018
- * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1019
- * @returns {Promise<Object>} Verification status and data
1071
+ * @param {string} proofId - Proof receipt ID (0x + 64 hex).
1072
+ * @returns {Promise<Object>} Proof record and verification state
1020
1073
  *
1021
1074
  * @example
1022
- * const result = await client.getStatus('0x...');
1075
+ * const result = await client.getProof('0x...');
1023
1076
  * console.log('Status:', result.status);
1024
1077
  */
1025
- async getStatus(proofId) {
1078
+ async getProof(proofId) {
1026
1079
  if (!proofId || typeof proofId !== 'string') {
1027
1080
  throw new ValidationError('proofId is required');
1028
1081
  }
1029
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`);
1082
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`);
1030
1083
 
1031
1084
  if (!response.success) {
1032
- throw new ApiError(`Failed to get status: ${response.error?.message || 'Unknown error'}`, response.error);
1085
+ throw new ApiError(`Failed to get proof: ${response.error?.message || 'Unknown error'}`, response.error);
1033
1086
  }
1034
1087
 
1035
1088
  return this._formatResponse(response);
1036
1089
  }
1037
1090
 
1038
1091
  /**
1039
- * Get private proof status with wallet signature
1092
+ * Get private proof record with wallet signature
1040
1093
  *
1041
- * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1094
+ * @param {string} proofId - Proof receipt ID.
1042
1095
  * @param {Object} wallet - Wallet provider (window.ethereum or ethers Wallet)
1043
- * @returns {Promise<Object>} Private verification status and data
1096
+ * @returns {Promise<Object>} Private proof record and verification state
1044
1097
  *
1045
1098
  * @example
1046
- * // Access private proof
1047
- * const privateData = await client.getPrivateStatus(proofId, window.ethereum);
1099
+ * const privateData = await client.getPrivateProof(proofId, window.ethereum);
1048
1100
  */
1049
- async getPrivateStatus(proofId, wallet = null) {
1101
+ async getPrivateProof(proofId, wallet = null) {
1050
1102
  if (!proofId || typeof proofId !== 'string') {
1051
1103
  throw new ValidationError('proofId is required');
1052
1104
  }
1053
1105
 
1054
- // Allow pre-signed universal owner auth (e.g. Solana) to avoid wallet-provider assumptions.
1055
1106
  const isPreSignedAuth = wallet &&
1056
1107
  typeof wallet === 'object' &&
1057
1108
  typeof wallet.walletAddress === 'string' &&
@@ -1066,7 +1117,7 @@ export class NeusClient {
1066
1117
  ...(typeof auth.chain === 'string' && auth.chain.trim() ? { 'x-chain': auth.chain.trim() } : {}),
1067
1118
  ...(typeof auth.signatureMethod === 'string' && auth.signatureMethod.trim() ? { 'x-signature-method': auth.signatureMethod.trim() } : {})
1068
1119
  };
1069
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`, null, headers);
1120
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`, null, headers);
1070
1121
  if (!response.success) {
1071
1122
  throw new ApiError(
1072
1123
  `Failed to access private proof: ${response.error?.message || 'Unauthorized'}`,
@@ -1087,8 +1138,6 @@ export class NeusClient {
1087
1138
 
1088
1139
  const signedTimestamp = Date.now();
1089
1140
 
1090
- // IMPORTANT: This must match the server's Standard Signing String owner-access check.
1091
- // Keep wire payload key `qHash` for backwards compatibility.
1092
1141
  const message = constructVerificationMessage({
1093
1142
  walletAddress,
1094
1143
  signedTimestamp,
@@ -1112,8 +1161,7 @@ export class NeusClient {
1112
1161
  throw new ValidationError(`Failed to sign message: ${error.message}`);
1113
1162
  }
1114
1163
 
1115
- // Make request with signature headers (server reads x-wallet-address/x-signature/x-signed-timestamp)
1116
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`, null, {
1164
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`, null, {
1117
1165
  'x-wallet-address': walletAddress,
1118
1166
  'x-signature': signature,
1119
1167
  'x-signed-timestamp': signedTimestamp.toString(),
@@ -1150,26 +1198,45 @@ export class NeusClient {
1150
1198
  * @returns {Promise<string[]>} Array of verifier IDs
1151
1199
  */
1152
1200
  async getVerifiers() {
1201
+ const catalog = await this.getVerifierCatalog();
1202
+ return Array.isArray(catalog?.data) ? catalog.data : [];
1203
+ }
1204
+
1205
+ /**
1206
+ * Get the public verifier catalog with per-verifier capabilities.
1207
+ * @returns {Promise<{data: string[], metadata: Record<string, { supportsDirectApi?: boolean }>, meta?: object}>}
1208
+ */
1209
+ async getVerifierCatalog() {
1153
1210
  const response = await this._makeRequest('GET', '/api/v1/verification/verifiers');
1154
1211
  if (!response.success) {
1155
1212
  throw new ApiError(`Failed to get verifiers: ${response.error?.message || 'Unknown error'}`, response.error);
1156
1213
  }
1157
- return Array.isArray(response.data) ? response.data : [];
1214
+ return {
1215
+ data: Array.isArray(response.data) ? response.data : [],
1216
+ metadata:
1217
+ response.metadata && typeof response.metadata === 'object' && !Array.isArray(response.metadata)
1218
+ ? response.metadata
1219
+ : {},
1220
+ meta:
1221
+ response.meta && typeof response.meta === 'object' && !Array.isArray(response.meta)
1222
+ ? response.meta
1223
+ : {}
1224
+ };
1158
1225
  }
1159
1226
 
1160
1227
  /**
1161
1228
  * POLL PROOF STATUS - Wait for verification completion
1162
- *
1229
+ *
1163
1230
  * Polls the verification status until it reaches a terminal state (completed or failed).
1164
1231
  * Useful for providing real-time feedback to users during verification.
1165
- *
1166
- * @param {string} proofId - Proof ID to poll (standard). `qHash` is a deprecated alias (same value).
1232
+ *
1233
+ * @param {string} proofId - Proof ID to poll.
1167
1234
  * @param {Object} [options] - Polling options
1168
1235
  * @param {number} [options.interval=5000] - Polling interval in ms
1169
1236
  * @param {number} [options.timeout=120000] - Total timeout in ms
1170
1237
  * @param {Function} [options.onProgress] - Progress callback function
1171
1238
  * @returns {Promise<Object>} Final verification status
1172
- *
1239
+ *
1173
1240
  * @example
1174
1241
  * const finalStatus = await client.pollProofStatus(proofId, {
1175
1242
  * interval: 3000,
@@ -1195,34 +1262,34 @@ export class NeusClient {
1195
1262
 
1196
1263
  const startTime = Date.now();
1197
1264
  let consecutiveRateLimits = 0;
1198
-
1265
+
1199
1266
  while (Date.now() - startTime < timeout) {
1200
1267
  try {
1201
- const status = await this.getStatus(proofId);
1268
+ const status = await this.getProof(proofId);
1202
1269
  consecutiveRateLimits = 0;
1203
-
1270
+
1204
1271
  // Call progress callback if provided
1205
1272
  if (onProgress && typeof onProgress === 'function') {
1206
1273
  onProgress(status.data || status);
1207
1274
  }
1208
-
1275
+
1209
1276
  // Check for terminal states
1210
1277
  const currentStatus = status.data?.status || status.status;
1211
1278
  if (this._isTerminalStatus(currentStatus)) {
1212
1279
  this._log('Verification completed', { status: currentStatus, duration: Date.now() - startTime });
1213
1280
  return status;
1214
1281
  }
1215
-
1282
+
1216
1283
  // Wait before next poll
1217
1284
  await new Promise(resolve => setTimeout(resolve, interval));
1218
-
1285
+
1219
1286
  } catch (error) {
1220
1287
  this._log('Status poll error', error.message);
1221
1288
  // Continue polling unless it's a validation error
1222
1289
  if (error instanceof ValidationError) {
1223
1290
  throw error;
1224
1291
  }
1225
-
1292
+
1226
1293
  let nextDelay = interval;
1227
1294
  if (error instanceof ApiError && Number(error.statusCode) === 429) {
1228
1295
  consecutiveRateLimits += 1;
@@ -1233,17 +1300,17 @@ export class NeusClient {
1233
1300
  const jitter = Math.floor(backoff * (0.5 + Math.random() * 0.5)); // 50-100%
1234
1301
  nextDelay = jitter;
1235
1302
  }
1236
-
1303
+
1237
1304
  await new Promise(resolve => setTimeout(resolve, nextDelay));
1238
1305
  }
1239
1306
  }
1240
-
1307
+
1241
1308
  throw new NetworkError(`Polling timeout after ${timeout}ms`, 'POLLING_TIMEOUT');
1242
1309
  }
1243
1310
 
1244
1311
  /**
1245
1312
  * DETECT CHAIN ID - Get current wallet chain
1246
- *
1313
+ *
1247
1314
  * @returns {Promise<number>} Current chain ID
1248
1315
  */
1249
1316
  async detectChainId() {
@@ -1277,7 +1344,6 @@ export class NeusClient {
1277
1344
  const message = constructVerificationMessage({
1278
1345
  walletAddress: address,
1279
1346
  signedTimestamp,
1280
- // Keep wire payload key `qHash` for backwards compatibility.
1281
1347
  data: { action: 'revoke_proof', qHash: proofId },
1282
1348
  verifierIds: ['ownership-basic'],
1283
1349
  ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain })
@@ -1298,9 +1364,8 @@ export class NeusClient {
1298
1364
  throw new ValidationError(`Failed to sign revocation: ${error.message}`);
1299
1365
  }
1300
1366
 
1301
- const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/${proofId}/revoke-self`, {
1367
+ const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/revoke-self/${proofId}`, {
1302
1368
  method: 'POST',
1303
- // SECURITY: Do not put proof signatures into Authorization headers.
1304
1369
  headers: { 'Content-Type': 'application/json' },
1305
1370
  body: JSON.stringify({
1306
1371
  walletAddress: address,
@@ -1316,19 +1381,16 @@ export class NeusClient {
1316
1381
  return true;
1317
1382
  }
1318
1383
 
1319
- // ============================================================================
1320
- // PROOFS & GATING METHODS
1321
- // ============================================================================
1322
-
1323
1384
  /**
1324
1385
  * GET PROOFS BY WALLET - Fetch proofs for a wallet address
1325
- *
1386
+ *
1326
1387
  * @param {string} walletAddress - Wallet identity (EVM/Solana/DID)
1327
1388
  * @param {Object} [options] - Filter options
1328
1389
  * @param {number} [options.limit] - Max results (default: 50; higher limits require owner access)
1329
1390
  * @param {number} [options.offset] - Pagination offset (default: 0)
1391
+ * @param {string} [options.qHash] - Filter to single proof by qHash
1330
1392
  * @returns {Promise<Object>} Proofs result
1331
- *
1393
+ *
1332
1394
  * @example
1333
1395
  * const result = await client.getProofsByWallet('0x...', {
1334
1396
  * limit: 50,
@@ -1346,11 +1408,12 @@ export class NeusClient {
1346
1408
  const qs = [];
1347
1409
  if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1348
1410
  if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1411
+ if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1349
1412
 
1350
1413
  const query = qs.length ? `?${qs.join('&')}` : '';
1351
1414
  const response = await this._makeRequest(
1352
1415
  'GET',
1353
- `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`
1416
+ `/api/v1/proofs/by-wallet/${encodeURIComponent(pathId)}${query}`
1354
1417
  );
1355
1418
 
1356
1419
  if (!response.success) {
@@ -1377,6 +1440,7 @@ export class NeusClient {
1377
1440
  * @param {Object} [options]
1378
1441
  * @param {number} [options.limit] - Max results (server enforces caps)
1379
1442
  * @param {number} [options.offset] - Pagination offset
1443
+ * @param {string} [options.qHash] - Filter to single proof by qHash
1380
1444
  * @param {Object} [wallet] - Optional injected wallet/provider (MetaMask/ethers Wallet)
1381
1445
  */
1382
1446
  async getPrivateProofsByWallet(walletAddress, options = {}, wallet = null) {
@@ -1442,9 +1506,10 @@ export class NeusClient {
1442
1506
  const qs = [];
1443
1507
  if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1444
1508
  if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1509
+ if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1445
1510
  const query = qs.length ? `?${qs.join('&')}` : '';
1446
1511
 
1447
- const response = await this._makeRequest('GET', `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`, null, {
1512
+ const response = await this._makeRequest('GET', `/api/v1/proofs/by-wallet/${encodeURIComponent(pathId)}${query}`, null, {
1448
1513
  'x-wallet-address': signerWalletAddress,
1449
1514
  'x-signature': signature,
1450
1515
  'x-signed-timestamp': signedTimestamp.toString(),
@@ -1466,16 +1531,16 @@ export class NeusClient {
1466
1531
  }
1467
1532
 
1468
1533
  /**
1469
- * GATE CHECK (API) - Minimal eligibility check
1534
+ * Gate check (HTTP API) minimal eligibility response.
1470
1535
  *
1471
1536
  * Calls the gate endpoint and returns a **minimal** yes/no response.
1472
- * By default this checks **public + discoverable** proofs only.
1537
+ * By default this checks **public + unlisted** proofs.
1473
1538
  *
1474
1539
  * When `includePrivate=true`, this can perform owner-signed private checks
1475
1540
  * (no full proof payloads returned) by providing a wallet/provider.
1476
1541
  *
1477
- * Prefer this over `checkGate()` for integrations that want the
1478
- * smallest, most stable surface area (and do NOT need full proof payloads).
1542
+ * Prefer this over `checkGate()` when you need the smallest, most stable
1543
+ * response shape and do not need full proof payloads.
1479
1544
  *
1480
1545
  * @param {Object} params - Gate check query params
1481
1546
  * @param {string} params.address - Wallet identity to check (EVM/Solana/DID)
@@ -1550,6 +1615,8 @@ export class NeusClient {
1550
1615
 
1551
1616
  // Wallet filters
1552
1617
  setIfPresent('provider', params.provider);
1618
+ setIfPresent('handle', params.handle);
1619
+ setIfPresent('namespace', params.namespace);
1553
1620
  setIfPresent('ownerAddress', params.ownerAddress);
1554
1621
  setIfPresent('riskLevel', params.riskLevel);
1555
1622
  setBoolIfPresent('sanctioned', params.sanctioned);
@@ -1574,7 +1641,7 @@ export class NeusClient {
1574
1641
  }
1575
1642
  }
1576
1643
  if (!auth) {
1577
- // No signer context available - proceed as public/discoverable gate check.
1644
+ // Without a signer: public and unlisted proofs only.
1578
1645
  } else {
1579
1646
  const normalizedAuthWallet = this._normalizeIdentity(auth.walletAddress);
1580
1647
  const normalizedAddress = this._normalizeIdentity(address);
@@ -1603,11 +1670,17 @@ export class NeusClient {
1603
1670
  }
1604
1671
 
1605
1672
  /**
1606
- * CHECK GATE - Evaluate requirements against existing proofs
1607
- *
1673
+ * CHECK GATE Local preview against proofs you already have in memory or from `getProofsByWallet`.
1674
+ *
1675
+ * **Not authoritative for access control.** For production allow/deny, use {@link NeusClient#gateCheck}
1676
+ * (`GET /api/v1/proofs/check`), which applies the same rules as the NEUS API. This method is useful for
1677
+ * UI previews, offline-ish flows, or when you already fetched proofs and want a quick match without
1678
+ * another round trip — but it can disagree with the server (e.g. `contentHash` matching uses a local
1679
+ * approximation when proof data only has inline `content`; the API uses the standard server-side hash).
1680
+ *
1608
1681
  * Gate-first verification: checks if wallet has valid proofs satisfying requirements.
1609
1682
  * Returns which requirements are missing/expired.
1610
- *
1683
+ *
1611
1684
  * @param {Object} params - Gate check parameters
1612
1685
  * @param {string} params.walletAddress - Target wallet
1613
1686
  * @param {Array<Object>} params.requirements - Array of gate requirements
@@ -1625,7 +1698,7 @@ export class NeusClient {
1625
1698
  * Note: contentHash matching uses approximation in SDK; for exact SHA-256 matching, use backend API
1626
1699
  * @param {Array} [params.proofs] - Pre-fetched proofs (skip API call)
1627
1700
  * @returns {Promise<Object>} Gate result with satisfied, missing, existing
1628
- *
1701
+ *
1629
1702
  * @example
1630
1703
  * // Basic gate check
1631
1704
  * const result = await client.checkGate({
@@ -1682,20 +1755,28 @@ export class NeusClient {
1682
1755
  if (match && typeof match === 'object') {
1683
1756
  const data = verifier.data || {};
1684
1757
  const input = data.input || {}; // NFT/token verifiers store fields in input
1685
-
1686
- for (const [key, expected] of Object.entries(match)) {
1758
+ // Gate format: match as array [{path, op, value}]. Convert to {path: value} for iteration.
1759
+ const matchObj = Array.isArray(match)
1760
+ ? Object.fromEntries(
1761
+ match
1762
+ .filter((m) => m && m.path && String(m.value ?? '').trim() !== '')
1763
+ .map((m) => [String(m.path).trim(), m.value])
1764
+ )
1765
+ : match;
1766
+
1767
+ for (const [key, expected] of Object.entries(matchObj)) {
1687
1768
  let actualValue = null;
1688
-
1769
+
1689
1770
  // Handle nested field access
1690
1771
  if (key.includes('.')) {
1691
1772
  const parts = key.split('.');
1692
1773
  let current = data;
1693
-
1774
+
1694
1775
  if (parts[0] === 'input' && input) {
1695
1776
  current = input;
1696
1777
  parts.shift();
1697
1778
  }
1698
-
1779
+
1699
1780
  for (const part of parts) {
1700
1781
  if (current && typeof current === 'object' && part in current) {
1701
1782
  current = current[part];
@@ -1708,13 +1789,14 @@ export class NeusClient {
1708
1789
  } else {
1709
1790
  actualValue = input[key] || data[key];
1710
1791
  }
1711
-
1792
+
1712
1793
  if (key === 'content' && actualValue === undefined) {
1713
1794
  actualValue = data.reference?.id || data.content;
1714
1795
  }
1715
-
1716
- // Special handling for verifier-specific fields
1796
+
1797
+ // Special handling for verifier-specific fields (claim-based, aligns with proofs/check)
1717
1798
  if (actualValue === undefined) {
1799
+ const claims = data.claims || {};
1718
1800
  if (key === 'contractAddress') {
1719
1801
  actualValue = input.contractAddress || data.contractAddress;
1720
1802
  } else if (key === 'tokenId') {
@@ -1733,9 +1815,28 @@ export class NeusClient {
1733
1815
  actualValue = data.verificationMethod;
1734
1816
  } else if (key === 'domain') {
1735
1817
  actualValue = data.domain;
1818
+ } else if (key === 'handle') {
1819
+ actualValue = data.handle || data.pseudonymId;
1820
+ } else if (key === 'namespace') {
1821
+ actualValue =
1822
+ data.namespace !== undefined && data.namespace !== null && data.namespace !== ''
1823
+ ? data.namespace
1824
+ : 'neus';
1825
+ } else if (key === 'claims.sanctions_passed') {
1826
+ actualValue = claims.sanctions_passed ?? claims.sanctionsPassed;
1827
+ } else if (key === 'claims.age_min') {
1828
+ actualValue = claims.age_min ?? claims.ageMin;
1829
+ } else if (key === 'neusPersonhoodId') {
1830
+ actualValue = data.neusPersonhoodId;
1831
+ } else if (key === 'riskLevel') {
1832
+ actualValue = data.riskLevel;
1833
+ } else if (key === 'sanctioned') {
1834
+ actualValue = data.sanctioned;
1835
+ } else if (key === 'poisoned') {
1836
+ actualValue = data.poisoned;
1736
1837
  }
1737
1838
  }
1738
-
1839
+
1739
1840
  // Content hash check (approximation)
1740
1841
  if (key === 'contentHash' && actualValue === undefined && data.content) {
1741
1842
  try {
@@ -1747,19 +1848,27 @@ export class NeusClient {
1747
1848
  hash = ((hash << 5) - hash) + char;
1748
1849
  hash = hash & hash;
1749
1850
  }
1750
- actualValue = '0x' + Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66);
1851
+ actualValue = `0x${ Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66)}`;
1751
1852
  } catch {
1752
1853
  actualValue = String(data.content);
1753
1854
  }
1754
1855
  }
1755
-
1856
+
1756
1857
  let normalizedActual = actualValue;
1757
1858
  let normalizedExpected = expected;
1758
-
1859
+
1759
1860
  if (actualValue === undefined || actualValue === null) {
1760
1861
  return false;
1761
1862
  }
1762
-
1863
+
1864
+ // Boolean claims (sanctions_passed, sanctioned, poisoned)
1865
+ if (typeof actualValue === 'boolean' || (key && (key.includes('sanctions') || key.includes('sanctioned') || key.includes('poisoned')))) {
1866
+ const bActual = Boolean(actualValue);
1867
+ const bExpected = expected === true || expected === 'true' || String(expected).toLowerCase() === 'true';
1868
+ if (bActual !== bExpected) return false;
1869
+ continue;
1870
+ }
1871
+
1763
1872
  if (key === 'chainId' || (key === 'tokenId' && (typeof actualValue === 'number' || !isNaN(Number(actualValue))))) {
1764
1873
  normalizedActual = Number(actualValue);
1765
1874
  normalizedExpected = Number(expected);
@@ -1773,7 +1882,7 @@ export class NeusClient {
1773
1882
  normalizedActual = actualValue;
1774
1883
  normalizedExpected = expected;
1775
1884
  }
1776
-
1885
+
1777
1886
  if (normalizedActual !== normalizedExpected) {
1778
1887
  return false;
1779
1888
  }
@@ -1799,14 +1908,6 @@ export class NeusClient {
1799
1908
  };
1800
1909
  }
1801
1910
 
1802
- // ============================================================================
1803
- // PRIVATE UTILITY METHODS
1804
- // ============================================================================
1805
-
1806
- /**
1807
- * Get connected wallet address
1808
- * @private
1809
- */
1810
1911
  async _getWalletAddress() {
1811
1912
  if (typeof window === 'undefined' || !window.ethereum) {
1812
1913
  throw new ConfigurationError('No Web3 wallet detected');
@@ -1820,16 +1921,12 @@ export class NeusClient {
1820
1921
  return accounts[0];
1821
1922
  }
1822
1923
 
1823
- /**
1824
- * Make HTTP request to API
1825
- * @private
1826
- */
1827
1924
  async _makeRequest(method, endpoint, data = null, headersOverride = null) {
1828
1925
  const url = `${this.baseUrl}${endpoint}`;
1829
-
1926
+
1830
1927
  const controller = new AbortController();
1831
1928
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1832
-
1929
+
1833
1930
  const options = {
1834
1931
  method,
1835
1932
  headers: { ...this.defaultHeaders, ...(headersOverride || {}) },
@@ -1845,7 +1942,7 @@ export class NeusClient {
1845
1942
  try {
1846
1943
  const response = await fetch(url, options);
1847
1944
  clearTimeout(timeoutId);
1848
-
1945
+
1849
1946
  let responseData;
1850
1947
  try {
1851
1948
  responseData = await response.json();
@@ -1861,23 +1958,19 @@ export class NeusClient {
1861
1958
 
1862
1959
  } catch (error) {
1863
1960
  clearTimeout(timeoutId);
1864
-
1961
+
1865
1962
  if (error.name === 'AbortError') {
1866
1963
  throw new NetworkError(`Request timeout after ${this.config.timeout}ms`);
1867
1964
  }
1868
-
1965
+
1869
1966
  if (error instanceof ApiError) {
1870
1967
  throw error;
1871
1968
  }
1872
-
1969
+
1873
1970
  throw new NetworkError(`Network error: ${error.message}`);
1874
1971
  }
1875
1972
  }
1876
1973
 
1877
- /**
1878
- * Format API response for consistent structure
1879
- * @private
1880
- */
1881
1974
  _formatResponse(response) {
1882
1975
  const proofId = response?.data?.proofId ||
1883
1976
  response?.proofId ||
@@ -1893,9 +1986,9 @@ export class NeusClient {
1893
1986
  response?.data?.id;
1894
1987
  const finalProofId = proofId || qHash || null;
1895
1988
  const finalQHash = qHash || proofId || finalProofId;
1896
-
1897
- const status = response?.data?.status ||
1898
- response?.status ||
1989
+
1990
+ const status = response?.data?.status ||
1991
+ response?.status ||
1899
1992
  response?.data?.resource?.status ||
1900
1993
  (response?.success ? 'completed' : 'unknown');
1901
1994
 
@@ -1907,14 +2000,10 @@ export class NeusClient {
1907
2000
  data: response.data,
1908
2001
  message: response.message,
1909
2002
  timestamp: Date.now(),
1910
- statusUrl: finalProofId ? `${this.baseUrl}/api/v1/verification/status/${finalProofId}` : null
2003
+ proofUrl: finalProofId ? `${this.baseUrl}/api/v1/proofs/${finalProofId}` : null
1911
2004
  };
1912
2005
  }
1913
2006
 
1914
- /**
1915
- * Check if status is terminal (completed or failed)
1916
- * @private
1917
- */
1918
2007
  _isTerminalStatus(status) {
1919
2008
  const terminalStates = [
1920
2009
  'verified',
@@ -1947,5 +2036,5 @@ export class NeusClient {
1947
2036
  }
1948
2037
  }
1949
2038
 
1950
- // Export the constructVerificationMessage function for advanced use
1951
- export { constructVerificationMessage };
2039
+ // Export signing helpers for advanced use
2040
+ export { PORTABLE_PROOF_SIGNER_HEADER, constructVerificationMessage };