@neus/sdk 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -1,313 +1,306 @@
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
- ]);
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
+ };
32
27
 
33
28
  const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
34
29
 
35
- // Validation for supported verifiers
36
30
  const validateVerifierData = (verifierId, data) => {
37
31
  if (!data || typeof data !== 'object') {
38
32
  return { valid: false, error: 'Data object is required' };
39
33
  }
40
-
41
- // Format validation for supported verifiers
34
+
42
35
  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' };
36
+ case 'ownership-basic':
37
+ if (!data.owner || !validateUniversalAddress(data.owner, typeof data.chain === 'string' ? data.chain : undefined)) {
38
+ return { valid: false, error: 'owner (universal wallet address) is required' };
39
+ }
40
+ if (data.content !== undefined && data.content !== null) {
41
+ if (typeof data.content !== 'string') {
42
+ return { valid: false, error: 'content must be a string when provided' };
49
43
  }
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
- }
44
+ if (data.content.length > 50000) {
45
+ return { valid: false, error: 'content exceeds 50KB inline limit' };
57
46
  }
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
- }
47
+ }
48
+ if (data.contentHash !== undefined && data.contentHash !== null) {
49
+ if (typeof data.contentHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(data.contentHash)) {
50
+ return { valid: false, error: 'contentHash must be a 32-byte hex string (0x + 64 hex chars) when provided' };
62
51
  }
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' };
52
+ }
53
+ if (data.contentType !== undefined && data.contentType !== null) {
54
+ if (typeof data.contentType !== 'string' || data.contentType.length > 100) {
55
+ return { valid: false, error: 'contentType must be a string (max 100 chars) when provided' };
56
+ }
57
+ const base = String(data.contentType).split(';')[0].trim().toLowerCase();
58
+ // Minimal MIME sanity check (server validates more precisely).
59
+ if (!base || base.includes(' ') || !base.includes('/')) {
60
+ return { valid: false, error: 'contentType must be a valid MIME type when provided' };
61
+ }
62
+ }
63
+ if (data.provenance !== undefined && data.provenance !== null) {
64
+ if (!data.provenance || typeof data.provenance !== 'object' || Array.isArray(data.provenance)) {
65
+ return { valid: false, error: 'provenance must be an object when provided' };
66
+ }
67
+ const dk = data.provenance.declaredKind;
68
+ if (dk !== undefined && dk !== null) {
69
+ const allowed = ['human', 'ai', 'mixed', 'unknown'];
70
+ if (typeof dk !== 'string' || !allowed.includes(dk)) {
71
+ return { valid: false, error: `provenance.declaredKind must be one of: ${allowed.join(', ')}` };
71
72
  }
72
73
  }
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' };
74
+ const ai = data.provenance.aiContext;
75
+ if (ai !== undefined && ai !== null) {
76
+ if (typeof ai !== 'object' || Array.isArray(ai)) {
77
+ return { valid: false, error: 'provenance.aiContext must be an object when provided' };
76
78
  }
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(', ')}` };
79
+ if (ai.generatorType !== undefined && ai.generatorType !== null) {
80
+ const allowed = ['local', 'saas', 'agent'];
81
+ if (typeof ai.generatorType !== 'string' || !allowed.includes(ai.generatorType)) {
82
+ return { valid: false, error: `provenance.aiContext.generatorType must be one of: ${allowed.join(', ')}` };
82
83
  }
83
84
  }
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
- }
85
+ if (ai.provider !== undefined && ai.provider !== null) {
86
+ if (typeof ai.provider !== 'string' || ai.provider.length > 64) {
87
+ return { valid: false, error: 'provenance.aiContext.provider must be a string (max 64 chars) when provided' };
109
88
  }
110
89
  }
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' };
90
+ if (ai.model !== undefined && ai.model !== null) {
91
+ if (typeof ai.model !== 'string' || ai.model.length > 128) {
92
+ return { valid: false, error: 'provenance.aiContext.model must be a string (max 128 chars) when provided' };
93
+ }
115
94
  }
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' };
95
+ if (ai.runId !== undefined && ai.runId !== null) {
96
+ if (typeof ai.runId !== 'string' || ai.runId.length > 128) {
97
+ return { valid: false, error: 'provenance.aiContext.runId must be a string (max 128 chars) when provided' };
98
+ }
120
99
  }
121
100
  }
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
- }
101
+ }
102
+ if (data.reference !== undefined) {
103
+ if (!data.reference || typeof data.reference !== 'object') {
104
+ return { valid: false, error: 'reference must be an object when provided' };
132
105
  }
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' };
106
+ if (!data.reference.type || typeof data.reference.type !== 'string') {
107
+ // Only required when reference object is present (or when doing reference-only proofs).
108
+ // Server requires reference.type when reference is used for traceability.
109
+ return { valid: false, error: 'reference.type is required when reference is provided' };
143
110
  }
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
- }
111
+ }
112
+ if (!data.content && !data.contentHash) {
113
+ if (!data.reference || typeof data.reference !== 'object') {
114
+ return { valid: false, error: 'reference is required when neither content nor contentHash is provided' };
149
115
  }
150
- if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
151
- return { valid: false, error: 'blockNumber must be an integer when provided' };
116
+ if (!data.reference.id || typeof data.reference.id !== 'string') {
117
+ return { valid: false, error: 'reference.id is required when neither content nor contentHash is provided' };
152
118
  }
153
- if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
154
- return { valid: false, error: 'Invalid ownerAddress' };
119
+ if (!data.reference.type || typeof data.reference.type !== 'string') {
120
+ return { valid: false, error: 'reference.type is required when neither content nor contentHash is provided' };
155
121
  }
156
- if (!validateWalletAddress(data.contractAddress)) {
157
- return { valid: false, error: 'Invalid contractAddress' };
122
+ }
123
+ break;
124
+ case 'nft-ownership':
125
+ // ownerAddress is optional; server injects from request walletAddress when omitted.
126
+ if (
127
+ !data.contractAddress ||
128
+ data.tokenId === null ||
129
+ data.tokenId === undefined ||
130
+ typeof data.chainId !== 'number'
131
+ ) {
132
+ return { valid: false, error: 'contractAddress, tokenId, and chainId are required' };
133
+ }
134
+ if (data.tokenType !== undefined && data.tokenType !== null) {
135
+ const tt = String(data.tokenType).toLowerCase();
136
+ if (tt !== 'erc721' && tt !== 'erc1155') {
137
+ return { valid: false, error: 'tokenType must be one of: erc721, erc1155' };
158
138
  }
159
- break;
160
- case 'token-holding':
161
- // ownerAddress is optional; server injects from request walletAddress when omitted.
162
- if (
163
- !data.contractAddress ||
139
+ }
140
+ if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
141
+ return { valid: false, error: 'blockNumber must be an integer when provided' };
142
+ }
143
+ if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
144
+ return { valid: false, error: 'Invalid ownerAddress' };
145
+ }
146
+ if (!validateWalletAddress(data.contractAddress)) {
147
+ return { valid: false, error: 'Invalid contractAddress' };
148
+ }
149
+ break;
150
+ case 'token-holding':
151
+ // ownerAddress is optional; server injects from request walletAddress when omitted.
152
+ if (
153
+ !data.contractAddress ||
164
154
  data.minBalance === null ||
165
155
  data.minBalance === undefined ||
166
156
  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.
157
+ ) {
158
+ return { valid: false, error: 'contractAddress, minBalance, and chainId are required' };
159
+ }
160
+ if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
161
+ return { valid: false, error: 'blockNumber must be an integer when provided' };
162
+ }
163
+ if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
164
+ return { valid: false, error: 'Invalid ownerAddress' };
165
+ }
166
+ if (!validateWalletAddress(data.contractAddress)) {
167
+ return { valid: false, error: 'Invalid contractAddress' };
168
+ }
169
+ break;
170
+ case 'ownership-dns-txt':
171
+ if (!data.domain || typeof data.domain !== 'string') {
172
+ return { valid: false, error: 'domain is required' };
173
+ }
174
+ if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
175
+ return { valid: false, error: 'Invalid walletAddress' };
176
+ }
177
+ break;
178
+ case 'wallet-link':
179
+ if (!data.primaryWalletAddress || !validateUniversalAddress(data.primaryWalletAddress, data.chain)) {
180
+ return { valid: false, error: 'primaryWalletAddress is required' };
181
+ }
182
+ if (!data.secondaryWalletAddress || !validateUniversalAddress(data.secondaryWalletAddress, data.chain)) {
183
+ return { valid: false, error: 'secondaryWalletAddress is required' };
184
+ }
185
+ if (!data.signature || typeof data.signature !== 'string') {
186
+ return { valid: false, error: 'signature is required (signed by secondary wallet)' };
187
+ }
188
+ if (typeof data.chain !== 'string' || !/^[a-z0-9]+:[^\s]+$/.test(data.chain)) {
189
+ return { valid: false, error: 'chain is required (namespace:reference)' };
190
+ }
191
+ if (typeof data.signatureMethod !== 'string' || !data.signatureMethod.trim()) {
192
+ return { valid: false, error: 'signatureMethod is required' };
193
+ }
194
+ if (typeof data.signedTimestamp !== 'number') {
195
+ return { valid: false, error: 'signedTimestamp is required' };
196
+ }
197
+ break;
198
+ case 'contract-ownership':
199
+ if (!data.contractAddress || !validateWalletAddress(data.contractAddress)) {
200
+ return { valid: false, error: 'contractAddress is required' };
201
+ }
202
+ if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
203
+ return { valid: false, error: 'Invalid walletAddress' };
204
+ }
205
+ if (typeof data.chainId !== 'number') {
206
+ return { valid: false, error: 'chainId is required' };
207
+ }
208
+ break;
209
+ case 'agent-identity':
210
+ if (!data.agentId || typeof data.agentId !== 'string' || data.agentId.length < 1 || data.agentId.length > 128) {
211
+ return { valid: false, error: 'agentId is required (1-128 chars)' };
212
+ }
213
+ if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
214
+ return { valid: false, error: 'agentWallet is required' };
215
+ }
216
+ if (data.agentType && !['ai', 'bot', 'service', 'automation', 'agent'].includes(data.agentType)) {
217
+ return { valid: false, error: 'agentType must be one of: ai, bot, service, automation, agent' };
218
+ }
219
+ break;
220
+ case 'agent-delegation':
221
+ if (!data.controllerWallet || !validateWalletAddress(data.controllerWallet)) {
222
+ return { valid: false, error: 'controllerWallet is required' };
223
+ }
224
+ if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
225
+ return { valid: false, error: 'agentWallet is required' };
226
+ }
227
+ if (data.scope && (typeof data.scope !== 'string' || data.scope.length > 128)) {
228
+ return { valid: false, error: 'scope must be a string (max 128 chars)' };
229
+ }
230
+ if (data.expiresAt && (typeof data.expiresAt !== 'number' || data.expiresAt < Date.now())) {
231
+ return { valid: false, error: 'expiresAt must be a future timestamp' };
232
+ }
233
+ break;
234
+ case 'ai-content-moderation':
235
+ if (!data.content || typeof data.content !== 'string') {
236
+ return { valid: false, error: 'content is required' };
237
+ }
238
+ if (!data.contentType || typeof data.contentType !== 'string') {
239
+ return { valid: false, error: 'contentType (MIME type) is required' };
240
+ }
241
+ {
242
+ // Only allow content types that are actually moderated (no "verified but skipped" bypass).
243
+ const contentType = String(data.contentType).split(';')[0].trim().toLowerCase();
244
+ const validTypes = [
245
+ 'image/jpeg',
246
+ 'image/png',
247
+ 'image/webp',
248
+ 'image/gif',
249
+ 'text/plain',
250
+ 'text/markdown',
251
+ 'text/x-markdown',
252
+ 'application/json',
253
+ 'application/xml'
254
+ ];
255
+ if (!validTypes.includes(contentType)) {
256
+ return { valid: false, error: `contentType must be one of: ${validTypes.join(', ')}` };
257
+ }
258
+ const isTextual = contentType.startsWith('text/') || contentType.includes('markdown');
259
+ if (isTextual) {
260
+ // Backend enforces 50KB UTF-8 limit for textual moderation payloads.
261
+ try {
262
+ const maxBytes = 50 * 1024;
263
+ const bytes = (typeof TextEncoder !== 'undefined')
264
+ ? new TextEncoder().encode(data.content).length
265
+ : String(data.content).length;
266
+ if (bytes > maxBytes) {
267
+ return {
268
+ valid: false,
269
+ error: `content exceeds ${maxBytes} bytes limit for ai-content-moderation verifier (text)`
270
+ };
281
271
  }
272
+ } catch {
273
+ // If encoding fails, defer to server.
282
274
  }
283
275
  }
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' };
276
+ }
277
+ if (data.content.length > 13653334) {
278
+ return { valid: false, error: 'content exceeds 10MB limit' };
279
+ }
280
+ break;
281
+ case 'ownership-pseudonym':
282
+ if (!data.pseudonymId || typeof data.pseudonymId !== 'string') {
283
+ return { valid: false, error: 'pseudonymId is required' };
284
+ }
285
+ // Validate handle format (3-64 chars, lowercase alphanumeric with ._-)
286
+ if (!/^[a-z0-9._-]{3,64}$/.test(data.pseudonymId.trim().toLowerCase())) {
287
+ return { valid: false, error: 'pseudonymId must be 3-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
288
+ }
289
+ // Validate namespace if provided (1-64 chars)
290
+ if (data.namespace && typeof data.namespace === 'string') {
291
+ if (!/^[a-z0-9._-]{1,64}$/.test(data.namespace.trim().toLowerCase())) {
292
+ return { valid: false, error: 'namespace must be 1-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
307
293
  }
308
- break;
294
+ }
295
+ // Note: signature is not required - envelope signature provides authentication
296
+ break;
297
+ case 'wallet-risk':
298
+ if (data.walletAddress && !validateUniversalAddress(data.walletAddress, data.chain)) {
299
+ return { valid: false, error: 'Invalid walletAddress' };
300
+ }
301
+ break;
309
302
  }
310
-
303
+
311
304
  return { valid: true };
312
305
  };
313
306
 
@@ -319,7 +312,6 @@ export class NeusClient {
319
312
  ...config
320
313
  };
321
314
 
322
- // NEUS Network API
323
315
  this.baseUrl = this.config.apiUrl || 'https://api.neus.network';
324
316
  // Enforce HTTPS for neus.network domains to satisfy CSP and normalize URLs
325
317
  try {
@@ -332,32 +324,25 @@ export class NeusClient {
332
324
  } catch {
333
325
  // If invalid URL string, leave as-is
334
326
  }
335
- // Normalize apiUrl on config
336
327
  this.config.apiUrl = this.baseUrl;
337
- // Default headers for API requests
328
+
329
+ // Default headers
330
+
338
331
  this.defaultHeaders = {
339
332
  'Content-Type': 'application/json',
340
333
  'Accept': 'application/json',
341
334
  'X-Neus-Sdk': 'js'
342
335
  };
343
336
 
344
- // Optional API key (server-side only; do not embed in browser apps)
345
337
  if (typeof this.config.apiKey === 'string' && this.config.apiKey.trim().length > 0) {
346
338
  this.defaultHeaders['Authorization'] = `Bearer ${this.config.apiKey.trim()}`;
347
339
  }
348
- // Public app attribution header (non-secret)
349
340
  if (typeof this.config.appId === 'string' && this.config.appId.trim().length > 0) {
350
341
  this.defaultHeaders['X-Neus-App'] = this.config.appId.trim();
351
342
  }
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.
343
+ if (typeof this.config.paymentSignature === 'string' && this.config.paymentSignature.trim().length > 0) {
344
+ this.defaultHeaders['PAYMENT-SIGNATURE'] = this.config.paymentSignature.trim();
345
+ }
361
346
  if (this.config.extraHeaders && typeof this.config.extraHeaders === 'object') {
362
347
  for (const [k, v] of Object.entries(this.config.extraHeaders)) {
363
348
  if (!k || v === undefined || v === null) continue;
@@ -368,7 +353,6 @@ export class NeusClient {
368
353
  }
369
354
  }
370
355
  try {
371
- // Attach origin in browser environments
372
356
  if (typeof window !== 'undefined' && window.location && window.location.origin) {
373
357
  this.defaultHeaders['X-Client-Origin'] = window.location.origin;
374
358
  }
@@ -506,63 +490,47 @@ export class NeusClient {
506
490
  });
507
491
  }
508
492
 
509
- // ============================================================================
510
- // CORE VERIFICATION METHODS
511
- // ============================================================================
512
-
513
493
  /**
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
- *
494
+ * Create a verification proof.
495
+ *
496
+ * Supports two paths:
497
+ * - **Auto:** Supply `verifier`, `content`, and/or `data` with an optional
498
+ * `wallet` provider. The SDK performs signing in the client.
499
+ * - **Manual:** Supply pre-signed `verifierIds`, `data`, `walletAddress`,
500
+ * `signature`, and `signedTimestamp`.
501
+ *
520
502
  * @param {Object} params - Verification parameters
503
+ * @param {string} [params.verifier] - Verifier ID (auto path, e.g. 'ownership-basic')
504
+ * @param {string} [params.content] - Content to verify (auto path)
505
+ * @param {Object} [params.data] - Structured verification data
506
+ * @param {Object} [params.wallet] - Wallet provider (auto path)
507
+ * @param {Object} [params.options] - Additional options
521
508
  * @param {Array<string>} [params.verifierIds] - Array of verifier IDs (manual path)
522
- * @param {Object} [params.data] - Verification data object (manual path)
523
509
  * @param {string} [params.walletAddress] - Wallet address that signed the request (manual path)
524
510
  * @param {string} [params.signature] - EIP-191 signature (manual path)
525
511
  * @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
- *
512
+ * @param {number} [params.chainId] - EVM signing-context hint; when omitted, resolved to the NEUS protocol primary chain for signing
513
+ * @returns {Promise<Object>} Verification result with proofId
514
+ *
515
+ * @example
516
+ * // Auto path
517
+ * const proof = await client.verify({
518
+ * verifier: 'ownership-basic',
519
+ * content: 'Hello World',
520
+ * wallet: window.ethereum
521
+ * });
522
+ *
533
523
  * @example
524
+ * // Manual path
534
525
  * const proof = await client.verify({
535
526
  * 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
- * },
527
+ * data: { content: "My content", owner: walletAddress },
541
528
  * walletAddress: '0x...',
542
529
  * signature: '0x...',
543
530
  * signedTimestamp: Date.now(),
544
531
  * options: { targetChains: [421614, 11155111] }
545
532
  * });
546
533
  */
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
534
  async verify(params) {
567
535
  // Auto path: if no manual signature fields but auto fields are provided, perform inline wallet flow
568
536
  if ((!params?.signature || !params?.walletAddress) && (params?.verifier || params?.content || params?.data)) {
@@ -573,30 +541,31 @@ export class NeusClient {
573
541
  throw new ValidationError('content is required and must be a string (or use data param with owner + reference)');
574
542
  }
575
543
 
576
- let validVerifiers = FALLBACK_PUBLIC_VERIFIERS;
544
+ let verifierCatalog = FALLBACK_PUBLIC_VERIFIER_CATALOG;
577
545
  try {
578
- const discovered = await this.getVerifiers();
579
- if (Array.isArray(discovered) && discovered.length > 0) {
580
- validVerifiers = discovered;
546
+ const discovered = await this.getVerifierCatalog();
547
+ if (discovered && discovered.metadata && Object.keys(discovered.metadata).length > 0) {
548
+ verifierCatalog = discovered.metadata;
581
549
  }
582
550
  } catch {
583
551
  // Fallback keeps SDK usable if verifier catalog endpoint is temporarily unavailable.
584
552
  }
553
+ const validVerifiers = Object.keys(verifierCatalog);
585
554
  if (!validVerifiers.includes(verifier)) {
586
555
  throw new ValidationError(`Invalid verifier '${verifier}'. Must be one of: ${validVerifiers.join(', ')}.`);
587
556
  }
588
557
 
589
- if (INTERACTIVE_VERIFIERS.has(verifier)) {
558
+ if (verifierCatalog?.[verifier]?.supportsDirectApi === false) {
590
559
  const hostedCheckoutUrl = options?.hostedCheckoutUrl || 'https://neus.network/verify';
591
560
  throw new ValidationError(
592
561
  `${verifier} requires hosted interactive checkout. Use VerifyGate or redirect to ${hostedCheckoutUrl}.`
593
562
  );
594
563
  }
595
-
564
+
596
565
  // These verifiers require explicit data parameter (no auto-path)
597
566
  const requiresDataParam = [
598
- 'ownership-dns-txt',
599
- 'wallet-link',
567
+ 'ownership-dns-txt',
568
+ 'wallet-link',
600
569
  'contract-ownership',
601
570
  'ownership-pseudonym',
602
571
  'wallet-risk',
@@ -739,7 +708,10 @@ export class NeusClient {
739
708
  ...(data?.agentLabel && { agentLabel: data.agentLabel }),
740
709
  ...(data?.agentType && { agentType: data.agentType }),
741
710
  ...(data?.description && { description: data.description }),
742
- ...(data?.capabilities && { capabilities: data.capabilities })
711
+ ...(data?.capabilities && { capabilities: data.capabilities }),
712
+ ...(data?.instructions && { instructions: data.instructions }),
713
+ ...(data?.skills && { skills: data.skills }),
714
+ ...(data?.services && { services: data.services })
743
715
  };
744
716
  } else if (verifier === 'agent-delegation') {
745
717
  if (!data?.agentWallet) {
@@ -752,7 +724,11 @@ export class NeusClient {
752
724
  ...(data?.scope && { scope: data.scope }),
753
725
  ...(data?.permissions && { permissions: data.permissions }),
754
726
  ...(data?.maxSpend && { maxSpend: data.maxSpend }),
755
- ...(data?.expiresAt && { expiresAt: data.expiresAt })
727
+ ...(data?.allowedPaymentTypes && { allowedPaymentTypes: data.allowedPaymentTypes }),
728
+ ...(data?.receiptDisclosure && { receiptDisclosure: data.receiptDisclosure }),
729
+ ...(data?.expiresAt && { expiresAt: data.expiresAt }),
730
+ ...(data?.instructions && { instructions: data.instructions }),
731
+ ...(data?.skills && { skills: data.skills })
756
732
  };
757
733
  } else if (verifier === 'ai-content-moderation') {
758
734
  if (!data?.content) {
@@ -787,7 +763,6 @@ export class NeusClient {
787
763
  ...(data?.includeDetails !== undefined && { includeDetails: data.includeDetails })
788
764
  };
789
765
  } else {
790
- // Default structure for unknown verifiers (should not reach here with validVerifiers check)
791
766
  verificationData = data ? {
792
767
  content,
793
768
  owner: walletAddress,
@@ -810,7 +785,6 @@ export class NeusClient {
810
785
 
811
786
  let signature;
812
787
  try {
813
- // UNIFIED SIGNING: Matches utils/core.ts personalSignUniversal exactly
814
788
  const toHexUtf8 = (s) => {
815
789
  try {
816
790
  const enc = new TextEncoder();
@@ -824,7 +798,7 @@ export class NeusClient {
824
798
  return hex;
825
799
  }
826
800
  };
827
-
801
+
828
802
  // Detect Farcaster wallet - requires hex-encoded messages FIRST
829
803
  const isFarcasterWallet = (() => {
830
804
  if (typeof window === 'undefined') return false;
@@ -841,7 +815,7 @@ export class NeusClient {
841
815
  }
842
816
  return false;
843
817
  })();
844
-
818
+
845
819
  if (isFarcasterWallet) {
846
820
  try {
847
821
  const hexMsg = toHexUtf8(message);
@@ -850,7 +824,7 @@ export class NeusClient {
850
824
  // Fall through
851
825
  }
852
826
  }
853
-
827
+
854
828
  if (!signature) {
855
829
  try {
856
830
  signature = await provider.request({ method: 'personal_sign', params: [message, walletAddress] });
@@ -858,15 +832,17 @@ export class NeusClient {
858
832
  const msg = String(e && (e.message || e.reason) || e || '').toLowerCase();
859
833
  const errCode = (e && (e.code || (e.error && e.error.code))) || null;
860
834
  const needsHex = /byte|bytes|invalid byte sequence|encoding|non-hex/i.test(msg);
861
-
835
+
836
+ const unsupportedRe =
837
+ /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i;
862
838
  const methodUnsupported = (
863
- /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i.test(msg) ||
839
+ unsupportedRe.test(msg) ||
864
840
  errCode === -32601 ||
865
841
  errCode === 4200 ||
866
842
  (msg.includes('personal_sign') && msg.includes('not')) ||
867
843
  (msg.includes('request method') && msg.includes('not supported'))
868
844
  );
869
-
845
+
870
846
  if (methodUnsupported) {
871
847
  this._log('personal_sign not supported; attempting eth_sign fallback');
872
848
  try {
@@ -974,14 +950,15 @@ export class NeusClient {
974
950
  }
975
951
  }
976
952
 
977
- // Build options payload (public surface)
953
+ // Build options payload.
978
954
  const optionsPayload = {
979
955
  ...(options && typeof options === 'object' ? options : {}),
980
956
  targetChains: Array.isArray(options?.targetChains) ? options.targetChains : [],
981
957
  // Privacy and storage options (defaults)
982
958
  privacyLevel: options?.privacyLevel || 'private',
983
959
  publicDisplay: options?.publicDisplay || false,
984
- storeOriginalContent: options?.storeOriginalContent || false
960
+ storeOriginalContent:
961
+ typeof options?.storeOriginalContent === 'boolean' ? options.storeOriginalContent : true
985
962
  };
986
963
  if (typeof options?.enableIpfs === 'boolean') optionsPayload.enableIpfs = options.enableIpfs;
987
964
 
@@ -997,8 +974,6 @@ export class NeusClient {
997
974
  options: optionsPayload
998
975
  };
999
976
 
1000
- // SECURITY: Do not send proof signatures in Authorization headers.
1001
- // Signatures belong in the request body only (they are not bearer tokens).
1002
977
  const response = await this._makeRequest('POST', '/api/v1/verification', requestData);
1003
978
 
1004
979
  if (!response.success) {
@@ -1008,50 +983,44 @@ export class NeusClient {
1008
983
  return this._formatResponse(response);
1009
984
  }
1010
985
 
1011
- // ============================================================================
1012
- // STATUS AND UTILITY METHODS
1013
- // ============================================================================
1014
-
1015
986
  /**
1016
- * Get verification status
987
+ * Get proof record by proof receipt id.
1017
988
  *
1018
- * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1019
- * @returns {Promise<Object>} Verification status and data
989
+ * @param {string} proofId - Proof receipt ID (0x + 64 hex).
990
+ * @returns {Promise<Object>} Proof record and verification state
1020
991
  *
1021
992
  * @example
1022
- * const result = await client.getStatus('0x...');
993
+ * const result = await client.getProof('0x...');
1023
994
  * console.log('Status:', result.status);
1024
995
  */
1025
- async getStatus(proofId) {
996
+ async getProof(proofId) {
1026
997
  if (!proofId || typeof proofId !== 'string') {
1027
998
  throw new ValidationError('proofId is required');
1028
999
  }
1029
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`);
1000
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`);
1030
1001
 
1031
1002
  if (!response.success) {
1032
- throw new ApiError(`Failed to get status: ${response.error?.message || 'Unknown error'}`, response.error);
1003
+ throw new ApiError(`Failed to get proof: ${response.error?.message || 'Unknown error'}`, response.error);
1033
1004
  }
1034
1005
 
1035
1006
  return this._formatResponse(response);
1036
1007
  }
1037
1008
 
1038
1009
  /**
1039
- * Get private proof status with wallet signature
1010
+ * Get private proof record with wallet signature
1040
1011
  *
1041
- * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1012
+ * @param {string} proofId - Proof receipt ID.
1042
1013
  * @param {Object} wallet - Wallet provider (window.ethereum or ethers Wallet)
1043
- * @returns {Promise<Object>} Private verification status and data
1014
+ * @returns {Promise<Object>} Private proof record and verification state
1044
1015
  *
1045
1016
  * @example
1046
- * // Access private proof
1047
- * const privateData = await client.getPrivateStatus(proofId, window.ethereum);
1017
+ * const privateData = await client.getPrivateProof(proofId, window.ethereum);
1048
1018
  */
1049
- async getPrivateStatus(proofId, wallet = null) {
1019
+ async getPrivateProof(proofId, wallet = null) {
1050
1020
  if (!proofId || typeof proofId !== 'string') {
1051
1021
  throw new ValidationError('proofId is required');
1052
1022
  }
1053
1023
 
1054
- // Allow pre-signed universal owner auth (e.g. Solana) to avoid wallet-provider assumptions.
1055
1024
  const isPreSignedAuth = wallet &&
1056
1025
  typeof wallet === 'object' &&
1057
1026
  typeof wallet.walletAddress === 'string' &&
@@ -1066,7 +1035,7 @@ export class NeusClient {
1066
1035
  ...(typeof auth.chain === 'string' && auth.chain.trim() ? { 'x-chain': auth.chain.trim() } : {}),
1067
1036
  ...(typeof auth.signatureMethod === 'string' && auth.signatureMethod.trim() ? { 'x-signature-method': auth.signatureMethod.trim() } : {})
1068
1037
  };
1069
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`, null, headers);
1038
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`, null, headers);
1070
1039
  if (!response.success) {
1071
1040
  throw new ApiError(
1072
1041
  `Failed to access private proof: ${response.error?.message || 'Unauthorized'}`,
@@ -1087,8 +1056,6 @@ export class NeusClient {
1087
1056
 
1088
1057
  const signedTimestamp = Date.now();
1089
1058
 
1090
- // IMPORTANT: This must match the server's Standard Signing String owner-access check.
1091
- // Keep wire payload key `qHash` for backwards compatibility.
1092
1059
  const message = constructVerificationMessage({
1093
1060
  walletAddress,
1094
1061
  signedTimestamp,
@@ -1112,8 +1079,7 @@ export class NeusClient {
1112
1079
  throw new ValidationError(`Failed to sign message: ${error.message}`);
1113
1080
  }
1114
1081
 
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, {
1082
+ const response = await this._makeRequest('GET', `/api/v1/proofs/${proofId}`, null, {
1117
1083
  'x-wallet-address': walletAddress,
1118
1084
  'x-signature': signature,
1119
1085
  'x-signed-timestamp': signedTimestamp.toString(),
@@ -1150,26 +1116,45 @@ export class NeusClient {
1150
1116
  * @returns {Promise<string[]>} Array of verifier IDs
1151
1117
  */
1152
1118
  async getVerifiers() {
1119
+ const catalog = await this.getVerifierCatalog();
1120
+ return Array.isArray(catalog?.data) ? catalog.data : [];
1121
+ }
1122
+
1123
+ /**
1124
+ * Get the public verifier catalog with per-verifier capabilities.
1125
+ * @returns {Promise<{data: string[], metadata: Record<string, { supportsDirectApi?: boolean }>, meta?: object}>}
1126
+ */
1127
+ async getVerifierCatalog() {
1153
1128
  const response = await this._makeRequest('GET', '/api/v1/verification/verifiers');
1154
1129
  if (!response.success) {
1155
1130
  throw new ApiError(`Failed to get verifiers: ${response.error?.message || 'Unknown error'}`, response.error);
1156
1131
  }
1157
- return Array.isArray(response.data) ? response.data : [];
1132
+ return {
1133
+ data: Array.isArray(response.data) ? response.data : [],
1134
+ metadata:
1135
+ response.metadata && typeof response.metadata === 'object' && !Array.isArray(response.metadata)
1136
+ ? response.metadata
1137
+ : {},
1138
+ meta:
1139
+ response.meta && typeof response.meta === 'object' && !Array.isArray(response.meta)
1140
+ ? response.meta
1141
+ : {}
1142
+ };
1158
1143
  }
1159
1144
 
1160
1145
  /**
1161
1146
  * POLL PROOF STATUS - Wait for verification completion
1162
- *
1147
+ *
1163
1148
  * Polls the verification status until it reaches a terminal state (completed or failed).
1164
1149
  * 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).
1150
+ *
1151
+ * @param {string} proofId - Proof ID to poll.
1167
1152
  * @param {Object} [options] - Polling options
1168
1153
  * @param {number} [options.interval=5000] - Polling interval in ms
1169
1154
  * @param {number} [options.timeout=120000] - Total timeout in ms
1170
1155
  * @param {Function} [options.onProgress] - Progress callback function
1171
1156
  * @returns {Promise<Object>} Final verification status
1172
- *
1157
+ *
1173
1158
  * @example
1174
1159
  * const finalStatus = await client.pollProofStatus(proofId, {
1175
1160
  * interval: 3000,
@@ -1195,34 +1180,34 @@ export class NeusClient {
1195
1180
 
1196
1181
  const startTime = Date.now();
1197
1182
  let consecutiveRateLimits = 0;
1198
-
1183
+
1199
1184
  while (Date.now() - startTime < timeout) {
1200
1185
  try {
1201
- const status = await this.getStatus(proofId);
1186
+ const status = await this.getProof(proofId);
1202
1187
  consecutiveRateLimits = 0;
1203
-
1188
+
1204
1189
  // Call progress callback if provided
1205
1190
  if (onProgress && typeof onProgress === 'function') {
1206
1191
  onProgress(status.data || status);
1207
1192
  }
1208
-
1193
+
1209
1194
  // Check for terminal states
1210
1195
  const currentStatus = status.data?.status || status.status;
1211
1196
  if (this._isTerminalStatus(currentStatus)) {
1212
1197
  this._log('Verification completed', { status: currentStatus, duration: Date.now() - startTime });
1213
1198
  return status;
1214
1199
  }
1215
-
1200
+
1216
1201
  // Wait before next poll
1217
1202
  await new Promise(resolve => setTimeout(resolve, interval));
1218
-
1203
+
1219
1204
  } catch (error) {
1220
1205
  this._log('Status poll error', error.message);
1221
1206
  // Continue polling unless it's a validation error
1222
1207
  if (error instanceof ValidationError) {
1223
1208
  throw error;
1224
1209
  }
1225
-
1210
+
1226
1211
  let nextDelay = interval;
1227
1212
  if (error instanceof ApiError && Number(error.statusCode) === 429) {
1228
1213
  consecutiveRateLimits += 1;
@@ -1233,17 +1218,17 @@ export class NeusClient {
1233
1218
  const jitter = Math.floor(backoff * (0.5 + Math.random() * 0.5)); // 50-100%
1234
1219
  nextDelay = jitter;
1235
1220
  }
1236
-
1221
+
1237
1222
  await new Promise(resolve => setTimeout(resolve, nextDelay));
1238
1223
  }
1239
1224
  }
1240
-
1225
+
1241
1226
  throw new NetworkError(`Polling timeout after ${timeout}ms`, 'POLLING_TIMEOUT');
1242
1227
  }
1243
1228
 
1244
1229
  /**
1245
1230
  * DETECT CHAIN ID - Get current wallet chain
1246
- *
1231
+ *
1247
1232
  * @returns {Promise<number>} Current chain ID
1248
1233
  */
1249
1234
  async detectChainId() {
@@ -1277,7 +1262,6 @@ export class NeusClient {
1277
1262
  const message = constructVerificationMessage({
1278
1263
  walletAddress: address,
1279
1264
  signedTimestamp,
1280
- // Keep wire payload key `qHash` for backwards compatibility.
1281
1265
  data: { action: 'revoke_proof', qHash: proofId },
1282
1266
  verifierIds: ['ownership-basic'],
1283
1267
  ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain })
@@ -1298,9 +1282,8 @@ export class NeusClient {
1298
1282
  throw new ValidationError(`Failed to sign revocation: ${error.message}`);
1299
1283
  }
1300
1284
 
1301
- const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/${proofId}/revoke-self`, {
1285
+ const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/revoke-self/${proofId}`, {
1302
1286
  method: 'POST',
1303
- // SECURITY: Do not put proof signatures into Authorization headers.
1304
1287
  headers: { 'Content-Type': 'application/json' },
1305
1288
  body: JSON.stringify({
1306
1289
  walletAddress: address,
@@ -1316,19 +1299,16 @@ export class NeusClient {
1316
1299
  return true;
1317
1300
  }
1318
1301
 
1319
- // ============================================================================
1320
- // PROOFS & GATING METHODS
1321
- // ============================================================================
1322
-
1323
1302
  /**
1324
1303
  * GET PROOFS BY WALLET - Fetch proofs for a wallet address
1325
- *
1304
+ *
1326
1305
  * @param {string} walletAddress - Wallet identity (EVM/Solana/DID)
1327
1306
  * @param {Object} [options] - Filter options
1328
1307
  * @param {number} [options.limit] - Max results (default: 50; higher limits require owner access)
1329
1308
  * @param {number} [options.offset] - Pagination offset (default: 0)
1309
+ * @param {string} [options.qHash] - Filter to single proof by qHash
1330
1310
  * @returns {Promise<Object>} Proofs result
1331
- *
1311
+ *
1332
1312
  * @example
1333
1313
  * const result = await client.getProofsByWallet('0x...', {
1334
1314
  * limit: 50,
@@ -1346,11 +1326,12 @@ export class NeusClient {
1346
1326
  const qs = [];
1347
1327
  if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1348
1328
  if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1329
+ if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1349
1330
 
1350
1331
  const query = qs.length ? `?${qs.join('&')}` : '';
1351
1332
  const response = await this._makeRequest(
1352
1333
  'GET',
1353
- `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`
1334
+ `/api/v1/proofs/by-wallet/${encodeURIComponent(pathId)}${query}`
1354
1335
  );
1355
1336
 
1356
1337
  if (!response.success) {
@@ -1377,6 +1358,7 @@ export class NeusClient {
1377
1358
  * @param {Object} [options]
1378
1359
  * @param {number} [options.limit] - Max results (server enforces caps)
1379
1360
  * @param {number} [options.offset] - Pagination offset
1361
+ * @param {string} [options.qHash] - Filter to single proof by qHash
1380
1362
  * @param {Object} [wallet] - Optional injected wallet/provider (MetaMask/ethers Wallet)
1381
1363
  */
1382
1364
  async getPrivateProofsByWallet(walletAddress, options = {}, wallet = null) {
@@ -1442,9 +1424,10 @@ export class NeusClient {
1442
1424
  const qs = [];
1443
1425
  if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1444
1426
  if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1427
+ if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1445
1428
  const query = qs.length ? `?${qs.join('&')}` : '';
1446
1429
 
1447
- const response = await this._makeRequest('GET', `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`, null, {
1430
+ const response = await this._makeRequest('GET', `/api/v1/proofs/by-wallet/${encodeURIComponent(pathId)}${query}`, null, {
1448
1431
  'x-wallet-address': signerWalletAddress,
1449
1432
  'x-signature': signature,
1450
1433
  'x-signed-timestamp': signedTimestamp.toString(),
@@ -1466,16 +1449,16 @@ export class NeusClient {
1466
1449
  }
1467
1450
 
1468
1451
  /**
1469
- * GATE CHECK (API) - Minimal eligibility check
1452
+ * Gate check (HTTP API) minimal eligibility response.
1470
1453
  *
1471
1454
  * Calls the gate endpoint and returns a **minimal** yes/no response.
1472
- * By default this checks **public + discoverable** proofs only.
1455
+ * By default this checks **public + unlisted** proofs.
1473
1456
  *
1474
1457
  * When `includePrivate=true`, this can perform owner-signed private checks
1475
1458
  * (no full proof payloads returned) by providing a wallet/provider.
1476
1459
  *
1477
- * Prefer this over `checkGate()` for integrations that want the
1478
- * smallest, most stable surface area (and do NOT need full proof payloads).
1460
+ * Prefer this over `checkGate()` when you need the smallest, most stable
1461
+ * response shape and do not need full proof payloads.
1479
1462
  *
1480
1463
  * @param {Object} params - Gate check query params
1481
1464
  * @param {string} params.address - Wallet identity to check (EVM/Solana/DID)
@@ -1550,6 +1533,8 @@ export class NeusClient {
1550
1533
 
1551
1534
  // Wallet filters
1552
1535
  setIfPresent('provider', params.provider);
1536
+ setIfPresent('handle', params.handle);
1537
+ setIfPresent('namespace', params.namespace);
1553
1538
  setIfPresent('ownerAddress', params.ownerAddress);
1554
1539
  setIfPresent('riskLevel', params.riskLevel);
1555
1540
  setBoolIfPresent('sanctioned', params.sanctioned);
@@ -1574,7 +1559,7 @@ export class NeusClient {
1574
1559
  }
1575
1560
  }
1576
1561
  if (!auth) {
1577
- // No signer context available - proceed as public/discoverable gate check.
1562
+ // Without a signer: public and unlisted proofs only.
1578
1563
  } else {
1579
1564
  const normalizedAuthWallet = this._normalizeIdentity(auth.walletAddress);
1580
1565
  const normalizedAddress = this._normalizeIdentity(address);
@@ -1603,11 +1588,17 @@ export class NeusClient {
1603
1588
  }
1604
1589
 
1605
1590
  /**
1606
- * CHECK GATE - Evaluate requirements against existing proofs
1607
- *
1591
+ * CHECK GATE Local preview against proofs you already have in memory or from `getProofsByWallet`.
1592
+ *
1593
+ * **Not authoritative for access control.** For production allow/deny, use {@link NeusClient#gateCheck}
1594
+ * (`GET /api/v1/proofs/check`), which applies the same rules as the NEUS API. This method is useful for
1595
+ * UI previews, offline-ish flows, or when you already fetched proofs and want a quick match without
1596
+ * another round trip — but it can disagree with the server (e.g. `contentHash` matching uses a local
1597
+ * approximation when proof data only has inline `content`; the API uses the standard server-side hash).
1598
+ *
1608
1599
  * Gate-first verification: checks if wallet has valid proofs satisfying requirements.
1609
1600
  * Returns which requirements are missing/expired.
1610
- *
1601
+ *
1611
1602
  * @param {Object} params - Gate check parameters
1612
1603
  * @param {string} params.walletAddress - Target wallet
1613
1604
  * @param {Array<Object>} params.requirements - Array of gate requirements
@@ -1625,7 +1616,7 @@ export class NeusClient {
1625
1616
  * Note: contentHash matching uses approximation in SDK; for exact SHA-256 matching, use backend API
1626
1617
  * @param {Array} [params.proofs] - Pre-fetched proofs (skip API call)
1627
1618
  * @returns {Promise<Object>} Gate result with satisfied, missing, existing
1628
- *
1619
+ *
1629
1620
  * @example
1630
1621
  * // Basic gate check
1631
1622
  * const result = await client.checkGate({
@@ -1682,20 +1673,28 @@ export class NeusClient {
1682
1673
  if (match && typeof match === 'object') {
1683
1674
  const data = verifier.data || {};
1684
1675
  const input = data.input || {}; // NFT/token verifiers store fields in input
1685
-
1686
- for (const [key, expected] of Object.entries(match)) {
1676
+ // Gate format: match as array [{path, op, value}]. Convert to {path: value} for iteration.
1677
+ const matchObj = Array.isArray(match)
1678
+ ? Object.fromEntries(
1679
+ match
1680
+ .filter((m) => m && m.path && String(m.value ?? '').trim() !== '')
1681
+ .map((m) => [String(m.path).trim(), m.value])
1682
+ )
1683
+ : match;
1684
+
1685
+ for (const [key, expected] of Object.entries(matchObj)) {
1687
1686
  let actualValue = null;
1688
-
1687
+
1689
1688
  // Handle nested field access
1690
1689
  if (key.includes('.')) {
1691
1690
  const parts = key.split('.');
1692
1691
  let current = data;
1693
-
1692
+
1694
1693
  if (parts[0] === 'input' && input) {
1695
1694
  current = input;
1696
1695
  parts.shift();
1697
1696
  }
1698
-
1697
+
1699
1698
  for (const part of parts) {
1700
1699
  if (current && typeof current === 'object' && part in current) {
1701
1700
  current = current[part];
@@ -1708,13 +1707,14 @@ export class NeusClient {
1708
1707
  } else {
1709
1708
  actualValue = input[key] || data[key];
1710
1709
  }
1711
-
1710
+
1712
1711
  if (key === 'content' && actualValue === undefined) {
1713
1712
  actualValue = data.reference?.id || data.content;
1714
1713
  }
1715
-
1716
- // Special handling for verifier-specific fields
1714
+
1715
+ // Special handling for verifier-specific fields (claim-based, aligns with proofs/check)
1717
1716
  if (actualValue === undefined) {
1717
+ const claims = data.claims || {};
1718
1718
  if (key === 'contractAddress') {
1719
1719
  actualValue = input.contractAddress || data.contractAddress;
1720
1720
  } else if (key === 'tokenId') {
@@ -1733,9 +1733,28 @@ export class NeusClient {
1733
1733
  actualValue = data.verificationMethod;
1734
1734
  } else if (key === 'domain') {
1735
1735
  actualValue = data.domain;
1736
+ } else if (key === 'handle') {
1737
+ actualValue = data.handle || data.pseudonymId;
1738
+ } else if (key === 'namespace') {
1739
+ actualValue =
1740
+ data.namespace !== undefined && data.namespace !== null && data.namespace !== ''
1741
+ ? data.namespace
1742
+ : 'neus';
1743
+ } else if (key === 'claims.sanctions_passed') {
1744
+ actualValue = claims.sanctions_passed ?? claims.sanctionsPassed;
1745
+ } else if (key === 'claims.age_min') {
1746
+ actualValue = claims.age_min ?? claims.ageMin;
1747
+ } else if (key === 'neusPersonhoodId') {
1748
+ actualValue = data.neusPersonhoodId;
1749
+ } else if (key === 'riskLevel') {
1750
+ actualValue = data.riskLevel;
1751
+ } else if (key === 'sanctioned') {
1752
+ actualValue = data.sanctioned;
1753
+ } else if (key === 'poisoned') {
1754
+ actualValue = data.poisoned;
1736
1755
  }
1737
1756
  }
1738
-
1757
+
1739
1758
  // Content hash check (approximation)
1740
1759
  if (key === 'contentHash' && actualValue === undefined && data.content) {
1741
1760
  try {
@@ -1747,19 +1766,27 @@ export class NeusClient {
1747
1766
  hash = ((hash << 5) - hash) + char;
1748
1767
  hash = hash & hash;
1749
1768
  }
1750
- actualValue = '0x' + Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66);
1769
+ actualValue = `0x${ Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66)}`;
1751
1770
  } catch {
1752
1771
  actualValue = String(data.content);
1753
1772
  }
1754
1773
  }
1755
-
1774
+
1756
1775
  let normalizedActual = actualValue;
1757
1776
  let normalizedExpected = expected;
1758
-
1777
+
1759
1778
  if (actualValue === undefined || actualValue === null) {
1760
1779
  return false;
1761
1780
  }
1762
-
1781
+
1782
+ // Boolean claims (sanctions_passed, sanctioned, poisoned)
1783
+ if (typeof actualValue === 'boolean' || (key && (key.includes('sanctions') || key.includes('sanctioned') || key.includes('poisoned')))) {
1784
+ const bActual = Boolean(actualValue);
1785
+ const bExpected = expected === true || expected === 'true' || String(expected).toLowerCase() === 'true';
1786
+ if (bActual !== bExpected) return false;
1787
+ continue;
1788
+ }
1789
+
1763
1790
  if (key === 'chainId' || (key === 'tokenId' && (typeof actualValue === 'number' || !isNaN(Number(actualValue))))) {
1764
1791
  normalizedActual = Number(actualValue);
1765
1792
  normalizedExpected = Number(expected);
@@ -1773,7 +1800,7 @@ export class NeusClient {
1773
1800
  normalizedActual = actualValue;
1774
1801
  normalizedExpected = expected;
1775
1802
  }
1776
-
1803
+
1777
1804
  if (normalizedActual !== normalizedExpected) {
1778
1805
  return false;
1779
1806
  }
@@ -1799,14 +1826,6 @@ export class NeusClient {
1799
1826
  };
1800
1827
  }
1801
1828
 
1802
- // ============================================================================
1803
- // PRIVATE UTILITY METHODS
1804
- // ============================================================================
1805
-
1806
- /**
1807
- * Get connected wallet address
1808
- * @private
1809
- */
1810
1829
  async _getWalletAddress() {
1811
1830
  if (typeof window === 'undefined' || !window.ethereum) {
1812
1831
  throw new ConfigurationError('No Web3 wallet detected');
@@ -1820,16 +1839,12 @@ export class NeusClient {
1820
1839
  return accounts[0];
1821
1840
  }
1822
1841
 
1823
- /**
1824
- * Make HTTP request to API
1825
- * @private
1826
- */
1827
1842
  async _makeRequest(method, endpoint, data = null, headersOverride = null) {
1828
1843
  const url = `${this.baseUrl}${endpoint}`;
1829
-
1844
+
1830
1845
  const controller = new AbortController();
1831
1846
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1832
-
1847
+
1833
1848
  const options = {
1834
1849
  method,
1835
1850
  headers: { ...this.defaultHeaders, ...(headersOverride || {}) },
@@ -1845,7 +1860,7 @@ export class NeusClient {
1845
1860
  try {
1846
1861
  const response = await fetch(url, options);
1847
1862
  clearTimeout(timeoutId);
1848
-
1863
+
1849
1864
  let responseData;
1850
1865
  try {
1851
1866
  responseData = await response.json();
@@ -1861,23 +1876,19 @@ export class NeusClient {
1861
1876
 
1862
1877
  } catch (error) {
1863
1878
  clearTimeout(timeoutId);
1864
-
1879
+
1865
1880
  if (error.name === 'AbortError') {
1866
1881
  throw new NetworkError(`Request timeout after ${this.config.timeout}ms`);
1867
1882
  }
1868
-
1883
+
1869
1884
  if (error instanceof ApiError) {
1870
1885
  throw error;
1871
1886
  }
1872
-
1887
+
1873
1888
  throw new NetworkError(`Network error: ${error.message}`);
1874
1889
  }
1875
1890
  }
1876
1891
 
1877
- /**
1878
- * Format API response for consistent structure
1879
- * @private
1880
- */
1881
1892
  _formatResponse(response) {
1882
1893
  const proofId = response?.data?.proofId ||
1883
1894
  response?.proofId ||
@@ -1893,9 +1904,9 @@ export class NeusClient {
1893
1904
  response?.data?.id;
1894
1905
  const finalProofId = proofId || qHash || null;
1895
1906
  const finalQHash = qHash || proofId || finalProofId;
1896
-
1897
- const status = response?.data?.status ||
1898
- response?.status ||
1907
+
1908
+ const status = response?.data?.status ||
1909
+ response?.status ||
1899
1910
  response?.data?.resource?.status ||
1900
1911
  (response?.success ? 'completed' : 'unknown');
1901
1912
 
@@ -1907,14 +1918,10 @@ export class NeusClient {
1907
1918
  data: response.data,
1908
1919
  message: response.message,
1909
1920
  timestamp: Date.now(),
1910
- statusUrl: finalProofId ? `${this.baseUrl}/api/v1/verification/status/${finalProofId}` : null
1921
+ proofUrl: finalProofId ? `${this.baseUrl}/api/v1/proofs/${finalProofId}` : null
1911
1922
  };
1912
1923
  }
1913
1924
 
1914
- /**
1915
- * Check if status is terminal (completed or failed)
1916
- * @private
1917
- */
1918
1925
  _isTerminalStatus(status) {
1919
1926
  const terminalStates = [
1920
1927
  'verified',
@@ -1947,5 +1954,5 @@ export class NeusClient {
1947
1954
  }
1948
1955
  }
1949
1956
 
1950
- // Export the constructVerificationMessage function for advanced use
1951
- export { constructVerificationMessage };
1957
+ // Export signing helpers for advanced use
1958
+ export { PORTABLE_PROOF_SIGNER_HEADER, constructVerificationMessage };