@neus/sdk 1.0.2 → 1.0.3

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,1729 +1,1951 @@
1
- /**
2
- * NEUS SDK Client
3
- * Create and verify cryptographic proofs across applications
4
- * @license Apache-2.0
5
- */
6
-
7
- import { ApiError, ValidationError, NetworkError, ConfigurationError } from './errors.js';
8
- import { constructVerificationMessage, validateWalletAddress, NEUS_CONSTANTS } from './utils.js';
9
-
10
- // Validation for supported verifiers
11
- const validateVerifierData = (verifierId, data) => {
12
- if (!data || typeof data !== 'object') {
13
- return { valid: false, error: 'Data object is required' };
14
- }
15
-
16
- // Validate wallet address if present
17
- // Validate owner/ownerAddress fields based on verifier type
18
- const ownerField = (verifierId === 'nft-ownership' || verifierId === 'token-holding') ? 'ownerAddress' : 'owner';
19
- if (data[ownerField] && !validateWalletAddress(data[ownerField])) {
20
- return { valid: false, error: `Invalid ${ownerField} address` };
21
- }
22
-
23
- // Format validation for supported verifiers
24
- switch (verifierId) {
25
- case 'ownership-basic':
26
- // Required: owner (must match request walletAddress).
27
- // Reference is optional when content/contentHash is provided.
28
- // If neither content nor contentHash is provided, reference.id is required (reference-only proof).
29
- if (!data.owner || !validateWalletAddress(data.owner)) {
30
- return { valid: false, error: 'owner (wallet address) is required' };
31
- }
32
- if (data.content !== undefined && data.content !== null) {
33
- if (typeof data.content !== 'string') {
34
- return { valid: false, error: 'content must be a string when provided' };
35
- }
36
- if (data.content.length > 50000) {
37
- return { valid: false, error: 'content exceeds 50KB inline limit' };
38
- }
39
- }
40
- if (data.contentHash !== undefined && data.contentHash !== null) {
41
- if (typeof data.contentHash !== 'string' || !/^0x[a-fA-F0-9]{64}$/.test(data.contentHash)) {
42
- return { valid: false, error: 'contentHash must be a 32-byte hex string (0x + 64 hex chars) when provided' };
43
- }
44
- }
45
- if (data.contentType !== undefined && data.contentType !== null) {
46
- if (typeof data.contentType !== 'string' || data.contentType.length > 100) {
47
- return { valid: false, error: 'contentType must be a string (max 100 chars) when provided' };
48
- }
49
- const base = String(data.contentType).split(';')[0].trim().toLowerCase();
50
- // Minimal MIME sanity check (server validates more precisely).
51
- if (!base || base.includes(' ') || !base.includes('/')) {
52
- return { valid: false, error: 'contentType must be a valid MIME type when provided' };
53
- }
54
- }
55
- if (data.provenance !== undefined && data.provenance !== null) {
56
- if (!data.provenance || typeof data.provenance !== 'object' || Array.isArray(data.provenance)) {
57
- return { valid: false, error: 'provenance must be an object when provided' };
58
- }
59
- const dk = data.provenance.declaredKind;
60
- if (dk !== undefined && dk !== null) {
61
- const allowed = ['human', 'ai', 'mixed', 'unknown'];
62
- if (typeof dk !== 'string' || !allowed.includes(dk)) {
63
- return { valid: false, error: `provenance.declaredKind must be one of: ${allowed.join(', ')}` };
64
- }
65
- }
66
- const ai = data.provenance.aiContext;
67
- if (ai !== undefined && ai !== null) {
68
- if (typeof ai !== 'object' || Array.isArray(ai)) {
69
- return { valid: false, error: 'provenance.aiContext must be an object when provided' };
70
- }
71
- if (ai.generatorType !== undefined && ai.generatorType !== null) {
72
- const allowed = ['local', 'saas', 'agent'];
73
- if (typeof ai.generatorType !== 'string' || !allowed.includes(ai.generatorType)) {
74
- return { valid: false, error: `provenance.aiContext.generatorType must be one of: ${allowed.join(', ')}` };
75
- }
76
- }
77
- if (ai.provider !== undefined && ai.provider !== null) {
78
- if (typeof ai.provider !== 'string' || ai.provider.length > 64) {
79
- return { valid: false, error: 'provenance.aiContext.provider must be a string (max 64 chars) when provided' };
80
- }
81
- }
82
- if (ai.model !== undefined && ai.model !== null) {
83
- if (typeof ai.model !== 'string' || ai.model.length > 128) {
84
- return { valid: false, error: 'provenance.aiContext.model must be a string (max 128 chars) when provided' };
85
- }
86
- }
87
- if (ai.runId !== undefined && ai.runId !== null) {
88
- if (typeof ai.runId !== 'string' || ai.runId.length > 128) {
89
- return { valid: false, error: 'provenance.aiContext.runId must be a string (max 128 chars) when provided' };
90
- }
91
- }
92
- }
93
- }
94
- if (data.reference !== undefined) {
95
- if (!data.reference || typeof data.reference !== 'object') {
96
- return { valid: false, error: 'reference must be an object when provided' };
97
- }
98
- if (!data.reference.type || typeof data.reference.type !== 'string') {
99
- // Only required when reference object is present (or when doing reference-only proofs).
100
- // Server requires reference.type when reference is used for traceability.
101
- return { valid: false, error: 'reference.type is required when reference is provided' };
102
- }
103
- }
104
- if (!data.content && !data.contentHash) {
105
- if (!data.reference || typeof data.reference !== 'object') {
106
- return { valid: false, error: 'reference is required when neither content nor contentHash is provided' };
107
- }
108
- if (!data.reference.id || typeof data.reference.id !== 'string') {
109
- return { valid: false, error: 'reference.id is required when neither content nor contentHash is provided' };
110
- }
111
- if (!data.reference.type || typeof data.reference.type !== 'string') {
112
- return { valid: false, error: 'reference.type is required when neither content nor contentHash is provided' };
113
- }
114
- }
115
- break;
116
- case 'nft-ownership':
117
- // ownerAddress is optional; server injects from request walletAddress when omitted.
118
- if (
119
- !data.contractAddress ||
120
- data.tokenId === null ||
121
- data.tokenId === undefined ||
122
- typeof data.chainId !== 'number'
123
- ) {
124
- return { valid: false, error: 'contractAddress, tokenId, and chainId are required' };
125
- }
126
- if (data.tokenType !== undefined && data.tokenType !== null) {
127
- const tt = String(data.tokenType).toLowerCase();
128
- if (tt !== 'erc721' && tt !== 'erc1155') {
129
- return { valid: false, error: 'tokenType must be one of: erc721, erc1155' };
130
- }
131
- }
132
- if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
133
- return { valid: false, error: 'blockNumber must be an integer when provided' };
134
- }
135
- if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
136
- return { valid: false, error: 'Invalid ownerAddress' };
137
- }
138
- if (!validateWalletAddress(data.contractAddress)) {
139
- return { valid: false, error: 'Invalid contractAddress' };
140
- }
141
- break;
142
- case 'token-holding':
143
- // ownerAddress is optional; server injects from request walletAddress when omitted.
144
- if (
145
- !data.contractAddress ||
146
- data.minBalance === null ||
147
- data.minBalance === undefined ||
148
- typeof data.chainId !== 'number'
149
- ) {
150
- return { valid: false, error: 'contractAddress, minBalance, and chainId are required' };
151
- }
152
- if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
153
- return { valid: false, error: 'blockNumber must be an integer when provided' };
154
- }
155
- if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
156
- return { valid: false, error: 'Invalid ownerAddress' };
157
- }
158
- if (!validateWalletAddress(data.contractAddress)) {
159
- return { valid: false, error: 'Invalid contractAddress' };
160
- }
161
- break;
162
- case 'ownership-dns-txt':
163
- if (!data.domain || typeof data.domain !== 'string') {
164
- return { valid: false, error: 'domain is required' };
165
- }
166
- if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
167
- return { valid: false, error: 'Invalid walletAddress' };
168
- }
169
- break;
170
- case 'wallet-link':
171
- if (!data.primaryWalletAddress || !validateWalletAddress(data.primaryWalletAddress)) {
172
- return { valid: false, error: 'primaryWalletAddress is required' };
173
- }
174
- if (!data.secondaryWalletAddress || !validateWalletAddress(data.secondaryWalletAddress)) {
175
- return { valid: false, error: 'secondaryWalletAddress is required' };
176
- }
177
- if (!data.signature || typeof data.signature !== 'string') {
178
- return { valid: false, error: 'signature is required (signed by secondary wallet)' };
179
- }
180
- if (typeof data.chainId !== 'number') {
181
- return { valid: false, error: 'chainId is required' };
182
- }
183
- if (typeof data.signedTimestamp !== 'number') {
184
- return { valid: false, error: 'signedTimestamp is required' };
185
- }
186
- break;
187
- case 'contract-ownership':
188
- if (!data.contractAddress || !validateWalletAddress(data.contractAddress)) {
189
- return { valid: false, error: 'contractAddress is required' };
190
- }
191
- if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
192
- return { valid: false, error: 'Invalid walletAddress' };
193
- }
194
- if (typeof data.chainId !== 'number') {
195
- return { valid: false, error: 'chainId is required' };
196
- }
197
- break;
198
- case 'agent-identity':
199
- if (!data.agentId || typeof data.agentId !== 'string' || data.agentId.length < 1 || data.agentId.length > 128) {
200
- return { valid: false, error: 'agentId is required (1-128 chars)' };
201
- }
202
- if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
203
- return { valid: false, error: 'agentWallet is required' };
204
- }
205
- if (data.agentType && !['ai', 'bot', 'service', 'automation', 'agent'].includes(data.agentType)) {
206
- return { valid: false, error: 'agentType must be one of: ai, bot, service, automation, agent' };
207
- }
208
- break;
209
- case 'agent-delegation':
210
- if (!data.controllerWallet || !validateWalletAddress(data.controllerWallet)) {
211
- return { valid: false, error: 'controllerWallet is required' };
212
- }
213
- if (!data.agentWallet || !validateWalletAddress(data.agentWallet)) {
214
- return { valid: false, error: 'agentWallet is required' };
215
- }
216
- if (data.scope && (typeof data.scope !== 'string' || data.scope.length > 128)) {
217
- return { valid: false, error: 'scope must be a string (max 128 chars)' };
218
- }
219
- if (data.expiresAt && (typeof data.expiresAt !== 'number' || data.expiresAt < Date.now())) {
220
- return { valid: false, error: 'expiresAt must be a future timestamp' };
221
- }
222
- break;
223
- case 'ai-content-moderation':
224
- if (!data.content || typeof data.content !== 'string') {
225
- return { valid: false, error: 'content is required' };
226
- }
227
- if (!data.contentType || typeof data.contentType !== 'string') {
228
- return { valid: false, error: 'contentType (MIME type) is required' };
229
- }
230
- {
231
- // Only allow content types that are actually moderated (no "verified but skipped" bypass).
232
- const contentType = String(data.contentType).split(';')[0].trim().toLowerCase();
233
- const validTypes = [
234
- 'image/jpeg',
235
- 'image/png',
236
- 'image/webp',
237
- 'image/gif',
238
- 'text/plain',
239
- 'text/markdown',
240
- 'text/x-markdown',
241
- 'application/json',
242
- 'application/xml'
243
- ];
244
- if (!validTypes.includes(contentType)) {
245
- return { valid: false, error: `contentType must be one of: ${validTypes.join(', ')}` };
246
- }
247
- const isTextual = contentType.startsWith('text/') || contentType.includes('markdown');
248
- if (isTextual) {
249
- // Backend enforces 50KB UTF-8 limit for textual moderation payloads.
250
- try {
251
- const maxBytes = 50 * 1024;
252
- const bytes = (typeof TextEncoder !== 'undefined')
253
- ? new TextEncoder().encode(data.content).length
254
- : String(data.content).length;
255
- if (bytes > maxBytes) {
256
- return { valid: false, error: `content exceeds ${maxBytes} bytes limit for ai-content-moderation verifier (text)` };
257
- }
258
- } catch {
259
- // If encoding fails, defer to server.
260
- }
261
- }
262
- }
263
- if (data.content.length > 13653334) {
264
- return { valid: false, error: 'content exceeds 10MB limit' };
265
- }
266
- break;
267
- case 'ownership-pseudonym':
268
- if (!data.pseudonymId || typeof data.pseudonymId !== 'string') {
269
- return { valid: false, error: 'pseudonymId is required' };
270
- }
271
- // Validate handle format (3-64 chars, lowercase alphanumeric with ._-)
272
- if (!/^[a-z0-9._-]{3,64}$/.test(data.pseudonymId.trim().toLowerCase())) {
273
- return { valid: false, error: 'pseudonymId must be 3-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
274
- }
275
- // Validate namespace if provided (1-64 chars)
276
- if (data.namespace && typeof data.namespace === 'string') {
277
- if (!/^[a-z0-9._-]{1,64}$/.test(data.namespace.trim().toLowerCase())) {
278
- return { valid: false, error: 'namespace must be 1-64 characters, lowercase alphanumeric with dots, underscores, or hyphens' };
279
- }
280
- }
281
- // Note: signature is not required - envelope signature provides authentication
282
- break;
283
- case 'wallet-risk':
284
- if (data.walletAddress && !validateWalletAddress(data.walletAddress)) {
285
- return { valid: false, error: 'Invalid walletAddress' };
286
- }
287
- break;
288
- }
289
-
290
- return { valid: true };
291
- };
292
-
293
- export class NeusClient {
294
- constructor(config = {}) {
295
- this.config = {
296
- timeout: 30000,
297
- enableLogging: false,
298
- ...config
299
- };
300
-
301
- // NEUS Network API
302
- this.baseUrl = this.config.apiUrl || 'https://api.neus.network';
303
- // Enforce HTTPS for neus.network domains to satisfy CSP and normalize URLs
304
- try {
305
- const url = new URL(this.baseUrl);
306
- if (url.hostname.endsWith('neus.network') && url.protocol === 'http:') {
307
- url.protocol = 'https:';
308
- }
309
- // Always remove trailing slash for consistency
310
- this.baseUrl = url.toString().replace(/\/$/, '');
311
- } catch {
312
- // If invalid URL string, leave as-is
313
- }
314
- // Normalize apiUrl on config
315
- this.config.apiUrl = this.baseUrl;
316
- // Default headers for API requests
317
- this.defaultHeaders = {
318
- 'Content-Type': 'application/json',
319
- 'Accept': 'application/json',
320
- 'X-Neus-Sdk': 'js'
321
- };
322
-
323
- // Optional API key (server-side only; do not embed in browser apps)
324
- if (typeof this.config.apiKey === 'string' && this.config.apiKey.trim().length > 0) {
325
- this.defaultHeaders['Authorization'] = `Bearer ${this.config.apiKey.trim()}`;
326
- }
327
- try {
328
- // Attach origin in browser environments
329
- if (typeof window !== 'undefined' && window.location && window.location.origin) {
330
- this.defaultHeaders['X-Client-Origin'] = window.location.origin;
331
- }
332
- } catch {
333
- // ignore: optional browser metadata header
334
- }
335
- }
336
-
337
- // ============================================================================
338
- // CORE VERIFICATION METHODS
339
- // ============================================================================
340
-
341
- /**
342
- * VERIFY - Standard verification (auto or manual)
343
- *
344
- * Create proofs with complete control over the verification process.
345
- * If signature and walletAddress are omitted but verifier/content are provided,
346
- * this method performs the wallet flow inline (no aliases, no secondary methods).
347
- *
348
- * @param {Object} params - Verification parameters
349
- * @param {Array<string>} [params.verifierIds] - Array of verifier IDs (manual path)
350
- * @param {Object} [params.data] - Verification data object (manual path)
351
- * @param {string} [params.walletAddress] - Wallet address that signed the request (manual path)
352
- * @param {string} [params.signature] - EIP-191 signature (manual path)
353
- * @param {number} [params.signedTimestamp] - Unix timestamp when signature was created (manual path)
354
- * @param {number} [params.chainId] - Chain ID for verification context (optional, managed by protocol)
355
- * @param {Object} [params.options] - Additional options
356
- * @param {string} [params.verifier] - Verifier ID (auto path)
357
- * @param {string} [params.content] - Content/description (auto path)
358
- * @param {Object} [params.wallet] - Optional injected wallet/provider (auto path)
359
- * @returns {Promise<Object>} Verification result with qHash
360
- *
361
- * @example
362
- * const proof = await client.verify({
363
- * verifierIds: ['ownership-basic'],
364
- * data: {
365
- * content: "My content",
366
- * owner: walletAddress, // or ownerAddress for nft-ownership/token-holding
367
- * reference: { type: 'other', id: 'my-unique-identifier' }
368
- * },
369
- * walletAddress: '0x...',
370
- * signature: '0x...',
371
- * signedTimestamp: Date.now(),
372
- * options: { targetChains: [421614, 11155111] }
373
- * });
374
- */
375
- /**
376
- * Create a verification proof
377
- *
378
- * @param {Object} params - Verification parameters
379
- * @param {string} [params.verifier] - Verifier ID (e.g., 'ownership-basic')
380
- * @param {string} [params.content] - Content to verify
381
- * @param {Object} [params.data] - Structured verification data
382
- * @param {Object} [params.wallet] - Wallet provider
383
- * @param {Object} [params.options] - Additional options
384
- * @returns {Promise<Object>} Verification result with qHash
385
- *
386
- * @example
387
- * // Simple ownership proof
388
- * const proof = await client.verify({
389
- * verifier: 'ownership-basic',
390
- * content: 'Hello World',
391
- * wallet: window.ethereum
392
- * });
393
- */
394
- async verify(params) {
395
- // Auto path: if no manual signature fields but auto fields are provided, perform inline wallet flow
396
- if ((!params?.signature || !params?.walletAddress) && (params?.verifier || params?.content || params?.data)) {
397
- const { content, verifier = 'ownership-basic', data = null, wallet = null, options = {} } = params;
398
-
399
- // ownership-basic: content required for simple mode, but data param mode allows contentHash or reference
400
- if (verifier === 'ownership-basic' && !data && (!content || typeof content !== 'string')) {
401
- throw new ValidationError('content is required and must be a string (or use data param with owner + reference)');
402
- }
403
-
404
- const validVerifiers = [
405
- 'ownership-basic',
406
- 'ownership-pseudonym', // Pseudonymous identity (public)
407
- 'nft-ownership',
408
- 'token-holding',
409
- 'ownership-dns-txt',
410
- 'wallet-link',
411
- 'contract-ownership',
412
- 'wallet-risk', // Wallet risk assessment (public)
413
- // AI & Agent verifiers (ERC-8004 aligned)
414
- 'agent-identity',
415
- 'agent-delegation',
416
- 'ai-content-moderation'
417
- ];
418
- if (!validVerifiers.includes(verifier)) {
419
- throw new ValidationError(`Invalid verifier '${verifier}'. Must be one of: ${validVerifiers.join(', ')}.`);
420
- }
421
-
422
- // These verifiers require explicit data parameter (no auto-path)
423
- const requiresDataParam = [
424
- 'ownership-dns-txt',
425
- 'wallet-link',
426
- 'contract-ownership',
427
- 'ownership-pseudonym',
428
- 'wallet-risk',
429
- 'agent-identity',
430
- 'agent-delegation',
431
- 'ai-content-moderation'
432
- ];
433
- if (requiresDataParam.includes(verifier)) {
434
- if (!data || typeof data !== 'object') {
435
- throw new ValidationError(`${verifier} requires explicit data parameter. Cannot use auto-path.`);
436
- }
437
- }
438
-
439
- // Auto-detect wallet and get address
440
- let walletAddress, provider;
441
- if (wallet) {
442
- walletAddress = wallet.address || wallet.selectedAddress;
443
- provider = wallet.provider || wallet;
444
- } else {
445
- if (typeof window === 'undefined' || !window.ethereum) {
446
- throw new ConfigurationError('No Web3 wallet detected. Please install MetaMask or provide wallet parameter.');
447
- }
448
- await window.ethereum.request({ method: 'eth_requestAccounts' });
449
- provider = window.ethereum;
450
- const accounts = await provider.request({ method: 'eth_accounts' });
451
- walletAddress = accounts[0];
452
- }
453
-
454
- // Prepare verification data based on verifier type
455
- let verificationData;
456
- if (verifier === 'ownership-basic') {
457
- if (data && typeof data === 'object') {
458
- // Data param mode: use provided data, inject owner if missing
459
- verificationData = {
460
- owner: data.owner || walletAddress,
461
- reference: data.reference,
462
- ...(data.content && { content: data.content }),
463
- ...(data.contentHash && { contentHash: data.contentHash }),
464
- ...(data.contentType && { contentType: data.contentType }),
465
- ...(data.provenance && { provenance: data.provenance })
466
- };
467
- } else {
468
- // Simple content mode: derive reference from content
469
- verificationData = {
470
- content: content,
471
- owner: walletAddress,
472
- reference: { type: 'other' }
473
- };
474
- }
475
- } else if (verifier === 'token-holding') {
476
- if (!data?.contractAddress) {
477
- throw new ValidationError('token-holding requires contractAddress in data parameter');
478
- }
479
- if (data?.minBalance === null || data?.minBalance === undefined) {
480
- throw new ValidationError('token-holding requires minBalance in data parameter');
481
- }
482
- if (typeof data?.chainId !== 'number') {
483
- throw new ValidationError('token-holding requires chainId (number) in data parameter');
484
- }
485
- verificationData = {
486
- ownerAddress: walletAddress,
487
- contractAddress: data.contractAddress,
488
- minBalance: data.minBalance,
489
- chainId: data.chainId
490
- };
491
- } else if (verifier === 'nft-ownership') {
492
- if (!data?.contractAddress) {
493
- throw new ValidationError('nft-ownership requires contractAddress in data parameter');
494
- }
495
- if (data?.tokenId === null || data?.tokenId === undefined) {
496
- throw new ValidationError('nft-ownership requires tokenId in data parameter');
497
- }
498
- if (typeof data?.chainId !== 'number') {
499
- throw new ValidationError('nft-ownership requires chainId (number) in data parameter');
500
- }
501
- verificationData = {
502
- ownerAddress: walletAddress,
503
- contractAddress: data.contractAddress,
504
- tokenId: data.tokenId,
505
- chainId: data.chainId,
506
- tokenType: data?.tokenType || 'erc721'
507
- };
508
- } else if (verifier === 'ownership-dns-txt') {
509
- if (!data?.domain) {
510
- throw new ValidationError('ownership-dns-txt requires domain in data parameter');
511
- }
512
- verificationData = {
513
- domain: data.domain,
514
- walletAddress: walletAddress
515
- };
516
- } else if (verifier === 'wallet-link') {
517
- if (!data?.secondaryWalletAddress) {
518
- throw new ValidationError('wallet-link requires secondaryWalletAddress in data parameter');
519
- }
520
- if (!data?.signature) {
521
- throw new ValidationError('wallet-link requires signature in data parameter (signed by secondary wallet)');
522
- }
523
- if (typeof data?.chainId !== 'number') {
524
- throw new ValidationError('wallet-link requires chainId (number) in data parameter');
525
- }
526
- verificationData = {
527
- primaryWalletAddress: walletAddress,
528
- secondaryWalletAddress: data.secondaryWalletAddress,
529
- signature: data.signature,
530
- chainId: data.chainId,
531
- signedTimestamp: data?.signedTimestamp || Date.now()
532
- };
533
- } else if (verifier === 'contract-ownership') {
534
- if (!data?.contractAddress) {
535
- throw new ValidationError('contract-ownership requires contractAddress in data parameter');
536
- }
537
- if (typeof data?.chainId !== 'number') {
538
- throw new ValidationError('contract-ownership requires chainId (number) in data parameter');
539
- }
540
- verificationData = {
541
- contractAddress: data.contractAddress,
542
- walletAddress: walletAddress,
543
- chainId: data.chainId,
544
- ...(data?.method && { method: data.method })
545
- };
546
- } else if (verifier === 'agent-identity') {
547
- if (!data?.agentId) {
548
- throw new ValidationError('agent-identity requires agentId in data parameter');
549
- }
550
- verificationData = {
551
- agentId: data.agentId,
552
- agentWallet: data?.agentWallet || walletAddress,
553
- ...(data?.agentLabel && { agentLabel: data.agentLabel }),
554
- ...(data?.agentType && { agentType: data.agentType }),
555
- ...(data?.description && { description: data.description }),
556
- ...(data?.capabilities && { capabilities: data.capabilities })
557
- };
558
- } else if (verifier === 'agent-delegation') {
559
- if (!data?.agentWallet) {
560
- throw new ValidationError('agent-delegation requires agentWallet in data parameter');
561
- }
562
- verificationData = {
563
- controllerWallet: data?.controllerWallet || walletAddress,
564
- agentWallet: data.agentWallet,
565
- ...(data?.agentId && { agentId: data.agentId }),
566
- ...(data?.scope && { scope: data.scope }),
567
- ...(data?.permissions && { permissions: data.permissions }),
568
- ...(data?.maxSpend && { maxSpend: data.maxSpend }),
569
- ...(data?.expiresAt && { expiresAt: data.expiresAt })
570
- };
571
- } else if (verifier === 'ai-content-moderation') {
572
- if (!data?.content) {
573
- throw new ValidationError('ai-content-moderation requires content (base64) in data parameter');
574
- }
575
- if (!data?.contentType) {
576
- throw new ValidationError('ai-content-moderation requires contentType (MIME type) in data parameter');
577
- }
578
- verificationData = {
579
- content: data.content,
580
- contentType: data.contentType,
581
- ...(data?.provider && { provider: data.provider })
582
- };
583
- } else if (verifier === 'ownership-pseudonym') {
584
- if (!data?.pseudonymId) {
585
- throw new ValidationError('ownership-pseudonym requires pseudonymId in data parameter');
586
- }
587
- verificationData = {
588
- pseudonymId: data.pseudonymId,
589
- ...(data?.namespace && { namespace: data.namespace }),
590
- ...(data?.displayName && { displayName: data.displayName }),
591
- ...(data?.metadata && { metadata: data.metadata })
592
- };
593
- } else if (verifier === 'wallet-risk') {
594
- // wallet-risk defaults to verifying the connected wallet
595
- verificationData = {
596
- walletAddress: data?.walletAddress || walletAddress,
597
- ...(data?.provider && { provider: data.provider }),
598
- // Mainnet-first semantics: if caller doesn't provide chainId, default to Ethereum mainnet (1).
599
- // This avoids accidental testnet semantics for risk providers.
600
- chainId: (typeof data?.chainId === 'number' && Number.isFinite(data.chainId)) ? data.chainId : 1,
601
- ...(data?.includeDetails !== undefined && { includeDetails: data.includeDetails })
602
- };
603
- } else {
604
- // Default structure for unknown verifiers (should not reach here with validVerifiers check)
605
- verificationData = data ? {
606
- content,
607
- owner: walletAddress,
608
- ...data
609
- } : {
610
- content,
611
- owner: walletAddress
612
- };
613
- }
614
-
615
- const signedTimestamp = Date.now();
616
- const verifierIds = [verifier];
617
- const message = constructVerificationMessage({
618
- walletAddress,
619
- signedTimestamp,
620
- data: verificationData,
621
- verifierIds,
622
- chainId: NEUS_CONSTANTS.HUB_CHAIN_ID // Protocol-managed chain
623
- });
624
-
625
- let signature;
626
- try {
627
- // UNIFIED SIGNING: Matches utils/core.ts personalSignUniversal exactly
628
- const toHexUtf8 = (s) => {
629
- try {
630
- const enc = new TextEncoder();
631
- const bytes = enc.encode(s);
632
- let hex = '0x';
633
- for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, '0');
634
- return hex;
635
- } catch {
636
- let hex = '0x';
637
- for (let i = 0; i < s.length; i++) hex += s.charCodeAt(i).toString(16).padStart(2, '0');
638
- return hex;
639
- }
640
- };
641
-
642
- // Detect Farcaster wallet - requires hex-encoded messages FIRST
643
- const isFarcasterWallet = (() => {
644
- if (typeof window === 'undefined') return false;
645
- try {
646
- const w = window;
647
- const fc = w.farcaster;
648
- if (!fc || !fc.context) return false;
649
- const fcProvider = fc.provider || fc.walletProvider || (fc.context && fc.context.walletProvider);
650
- if (fcProvider === provider) return true;
651
- if (w.mini && w.mini.wallet === provider && fc && fc.context) return true;
652
- if (w.ethereum === provider && fc && fc.context) return true;
653
- } catch {
654
- // ignore: optional Farcaster detection
655
- }
656
- return false;
657
- })();
658
-
659
- if (isFarcasterWallet) {
660
- try {
661
- const hexMsg = toHexUtf8(message);
662
- signature = await provider.request({ method: 'personal_sign', params: [hexMsg, walletAddress] });
663
- } catch (e) {
664
- // Fall through
665
- }
666
- }
667
-
668
- if (!signature) {
669
- try {
670
- signature = await provider.request({ method: 'personal_sign', params: [message, walletAddress] });
671
- } catch (e) {
672
- const msg = String(e && (e.message || e.reason) || e || '').toLowerCase();
673
- const errCode = (e && (e.code || (e.error && e.error.code))) || null;
674
- const needsHex = /byte|bytes|invalid byte sequence|encoding|non-hex/i.test(msg);
675
-
676
- const methodUnsupported = (
677
- /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i.test(msg) ||
678
- errCode === -32601 ||
679
- errCode === 4200 ||
680
- (msg.includes('personal_sign') && msg.includes('not')) ||
681
- (msg.includes('request method') && msg.includes('not supported'))
682
- );
683
-
684
- if (methodUnsupported) {
685
- this._log('personal_sign not supported; attempting eth_sign fallback');
686
- try {
687
- const enc = new TextEncoder();
688
- const bytes = enc.encode(message);
689
- const prefix = `\x19Ethereum Signed Message:\n${bytes.length}`;
690
- const full = new Uint8Array(prefix.length + bytes.length);
691
- for (let i = 0; i < prefix.length; i++) full[i] = prefix.charCodeAt(i);
692
- full.set(bytes, prefix.length);
693
- let payloadHex = '0x';
694
- for (let i = 0; i < full.length; i++) payloadHex += full[i].toString(16).padStart(2, '0');
695
- try {
696
- if (typeof window !== 'undefined') window.__NEUS_ALLOW_ETH_SIGN__ = true;
697
- } catch {
698
- // ignore
699
- }
700
- signature = await provider.request({ method: 'eth_sign', params: [walletAddress, payloadHex], neusAllowEthSign: true });
701
- try {
702
- if (typeof window !== 'undefined') delete window.__NEUS_ALLOW_ETH_SIGN__;
703
- } catch {
704
- // ignore
705
- }
706
- } catch (fallbackErr) {
707
- this._log('eth_sign fallback failed', { message: fallbackErr?.message || String(fallbackErr) });
708
- try {
709
- if (typeof window !== 'undefined') delete window.__NEUS_ALLOW_ETH_SIGN__;
710
- } catch {
711
- // ignore
712
- }
713
- throw e;
714
- }
715
- } else if (needsHex) {
716
- this._log('Retrying personal_sign with hex-encoded message');
717
- const hexMsg = toHexUtf8(message);
718
- signature = await provider.request({ method: 'personal_sign', params: [hexMsg, walletAddress] });
719
- } else {
720
- throw e;
721
- }
722
- }
723
- }
724
- } catch (error) {
725
- if (error.code === 4001) {
726
- throw new ValidationError('User rejected the signature request. Signature is required to create proofs.');
727
- }
728
- throw new ValidationError(`Failed to sign verification message: ${error.message}`);
729
- }
730
-
731
- return this.verify({
732
- verifierIds,
733
- data: verificationData,
734
- walletAddress,
735
- signature,
736
- signedTimestamp,
737
- options
738
- });
739
- }
740
-
741
- const {
742
- verifierIds,
743
- data,
744
- walletAddress,
745
- signature,
746
- signedTimestamp,
747
- chainId,
748
- chain,
749
- signatureMethod,
750
- options = {}
751
- } = params;
752
-
753
- const resolvedChainId = chainId || (chain ? null : NEUS_CONSTANTS.HUB_CHAIN_ID);
754
-
755
- // Normalize verifier IDs
756
- const normalizeVerifierId = (id) => {
757
- if (typeof id !== 'string') return id;
758
- const match = id.match(/^(.*)@\d+$/);
759
- return match ? match[1] : id;
760
- };
761
- const normalizedVerifierIds = Array.isArray(verifierIds) ? verifierIds.map(normalizeVerifierId) : [];
762
-
763
- // Validate required parameters
764
- if (!normalizedVerifierIds || normalizedVerifierIds.length === 0) {
765
- throw new ValidationError('verifierIds array is required');
766
- }
767
- if (!data || typeof data !== 'object') {
768
- throw new ValidationError('data object is required');
769
- }
770
- if (!walletAddress || typeof walletAddress !== 'string') {
771
- throw new ValidationError('walletAddress is required');
772
- }
773
- if (!signature) {
774
- throw new ValidationError('signature is required');
775
- }
776
- if (!signedTimestamp || typeof signedTimestamp !== 'number') {
777
- throw new ValidationError('signedTimestamp is required');
778
- }
779
- if (resolvedChainId !== null && typeof resolvedChainId !== 'number') {
780
- throw new ValidationError('chainId must be a number');
781
- }
782
-
783
- // Validate verifier data
784
- for (const verifierId of normalizedVerifierIds) {
785
- const validation = validateVerifierData(verifierId, data);
786
- if (!validation.valid) {
787
- throw new ValidationError(`Validation failed for verifier '${verifierId}': ${validation.error}`);
788
- }
789
- }
790
-
791
- // Build options payload (public surface)
792
- const optionsPayload = {
793
- ...(options && typeof options === 'object' ? options : {}),
794
- targetChains: Array.isArray(options?.targetChains) ? options.targetChains : [],
795
- // Privacy and storage options (defaults)
796
- privacyLevel: options?.privacyLevel || 'private',
797
- publicDisplay: options?.publicDisplay || false,
798
- storeOriginalContent: options?.storeOriginalContent || false
799
- };
800
- if (typeof options?.enableIpfs === 'boolean') optionsPayload.enableIpfs = options.enableIpfs;
801
-
802
- const requestData = {
803
- verifierIds: normalizedVerifierIds,
804
- data,
805
- walletAddress,
806
- signature,
807
- signedTimestamp,
808
- ...(resolvedChainId !== null && { chainId: resolvedChainId }),
809
- ...(chain && { chain }),
810
- ...(signatureMethod && { signatureMethod }),
811
- options: optionsPayload
812
- };
813
-
814
- // SECURITY: Do not send proof signatures in Authorization headers.
815
- // Signatures belong in the request body only (they are not bearer tokens).
816
- const response = await this._makeRequest('POST', '/api/v1/verification', requestData);
817
-
818
- if (!response.success) {
819
- throw new ApiError(`Verification failed: ${response.error?.message || 'Unknown error'}`, response.error);
820
- }
821
-
822
- return this._formatResponse(response);
823
- }
824
-
825
- // ============================================================================
826
- // STATUS AND UTILITY METHODS
827
- // ============================================================================
828
-
829
- /**
830
- * Get verification status
831
- *
832
- * @param {string} qHash - Verification ID (qHash or proofId)
833
- * @returns {Promise<Object>} Verification status and data
834
- *
835
- * @example
836
- * const result = await client.getStatus('0x...');
837
- * console.log('Status:', result.status);
838
- */
839
- async getStatus(qHash) {
840
- if (!qHash || typeof qHash !== 'string') {
841
- throw new ValidationError('qHash is required');
842
- }
843
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${qHash}`);
844
-
845
- if (!response.success) {
846
- throw new ApiError(`Failed to get status: ${response.error?.message || 'Unknown error'}`, response.error);
847
- }
848
-
849
- return this._formatResponse(response);
850
- }
851
-
852
- /**
853
- * Get private proof status with wallet signature
854
- *
855
- * @param {string} qHash - Verification ID
856
- * @param {Object} wallet - Wallet provider (window.ethereum or ethers Wallet)
857
- * @returns {Promise<Object>} Private verification status and data
858
- *
859
- * @example
860
- * // Access private proof
861
- * const privateData = await client.getPrivateStatus(qHash, window.ethereum);
862
- */
863
- async getPrivateStatus(qHash, wallet = null) {
864
- if (!qHash || typeof qHash !== 'string') {
865
- throw new ValidationError('qHash is required');
866
- }
867
-
868
- // Auto-detect wallet if not provided
869
- if (!wallet) {
870
- if (typeof window === 'undefined' || !window.ethereum) {
871
- throw new ConfigurationError('No wallet provider available');
872
- }
873
- wallet = window.ethereum;
874
- }
875
-
876
- let walletAddress, provider;
877
-
878
- // Handle different wallet types
879
- if (wallet.address) {
880
- // ethers Wallet
881
- walletAddress = wallet.address;
882
- provider = wallet;
883
- } else if (wallet.selectedAddress || wallet.request) {
884
- // Browser provider (MetaMask, etc.)
885
- provider = wallet;
886
- if (wallet.selectedAddress) {
887
- walletAddress = wallet.selectedAddress;
888
- } else {
889
- const accounts = await provider.request({ method: 'eth_accounts' });
890
- if (!accounts || accounts.length === 0) {
891
- throw new ConfigurationError('No wallet accounts available');
892
- }
893
- walletAddress = accounts[0];
894
- }
895
- } else {
896
- throw new ConfigurationError('Invalid wallet provider');
897
- }
898
-
899
- const signedTimestamp = Date.now();
900
-
901
- // IMPORTANT: This must match the server's Standard Signing String owner-access check:
902
- // data.action='access_private_proof' + data.qHash, verifierIds=['ownership-basic'], chainId=default chainId.
903
- const message = constructVerificationMessage({
904
- walletAddress,
905
- signedTimestamp,
906
- data: { action: 'access_private_proof', qHash },
907
- verifierIds: ['ownership-basic'],
908
- chainId: NEUS_CONSTANTS.HUB_CHAIN_ID
909
- });
910
-
911
- let signature;
912
- try {
913
- if (provider.signMessage) {
914
- // ethers Wallet
915
- signature = await provider.signMessage(message);
916
- } else {
917
- // Browser provider
918
- signature = await provider.request({
919
- method: 'personal_sign',
920
- params: [message, walletAddress]
921
- });
922
- }
923
- } catch (error) {
924
- if (error.code === 4001) {
925
- throw new ValidationError('User rejected signature request');
926
- }
927
- throw new ValidationError(`Failed to sign message: ${error.message}`);
928
- }
929
-
930
- // Make request with signature headers (server reads x-wallet-address/x-signature/x-signed-timestamp)
931
- const response = await this._makeRequest('GET', `/api/v1/verification/status/${qHash}`, null, {
932
- 'x-wallet-address': walletAddress,
933
- 'x-signature': signature,
934
- 'x-signed-timestamp': signedTimestamp.toString()
935
- });
936
-
937
- if (!response.success) {
938
- throw new ApiError(
939
- `Failed to access private proof: ${response.error?.message || 'Unauthorized'}`,
940
- response.error
941
- );
942
- }
943
-
944
- return this._formatResponse(response);
945
- }
946
-
947
- /**
948
- * Check API health
949
- *
950
- * @returns {Promise<boolean>} True if API is healthy
951
- */
952
- async isHealthy() {
953
- try {
954
- const response = await this._makeRequest('GET', '/api/v1/health');
955
- return response.success === true;
956
- } catch {
957
- return false;
958
- }
959
- }
960
-
961
- /**
962
- * List available verifiers
963
- *
964
- * @returns {Promise<string[]>} Array of verifier IDs
965
- */
966
- async getVerifiers() {
967
- const response = await this._makeRequest('GET', '/api/v1/verification/verifiers');
968
- if (!response.success) {
969
- throw new ApiError(`Failed to get verifiers: ${response.error?.message || 'Unknown error'}`, response.error);
970
- }
971
- return Array.isArray(response.data) ? response.data : [];
972
- }
973
-
974
- /**
975
- * POLL PROOF STATUS - Wait for verification completion
976
- *
977
- * Polls the verification status until it reaches a terminal state (completed or failed).
978
- * Useful for providing real-time feedback to users during verification.
979
- *
980
- * @param {string} qHash - Verification ID to poll
981
- * @param {Object} [options] - Polling options
982
- * @param {number} [options.interval=5000] - Polling interval in ms
983
- * @param {number} [options.timeout=120000] - Total timeout in ms
984
- * @param {Function} [options.onProgress] - Progress callback function
985
- * @returns {Promise<Object>} Final verification status
986
- *
987
- * @example
988
- * const finalStatus = await client.pollProofStatus(qHash, {
989
- * interval: 3000,
990
- * timeout: 60000,
991
- * onProgress: (status) => {
992
- * console.log('Current status:', status.status);
993
- * if (status.crosschain) {
994
- * console.log(`Cross-chain: ${status.crosschain.finalized}/${status.crosschain.totalChains}`);
995
- * }
996
- * }
997
- * });
998
- */
999
- async pollProofStatus(qHash, options = {}) {
1000
- const {
1001
- interval = 5000,
1002
- timeout = 120000,
1003
- onProgress
1004
- } = options;
1005
-
1006
- if (!qHash || typeof qHash !== 'string') {
1007
- throw new ValidationError('qHash is required');
1008
- }
1009
-
1010
- const startTime = Date.now();
1011
- let consecutiveRateLimits = 0;
1012
-
1013
- while (Date.now() - startTime < timeout) {
1014
- try {
1015
- const status = await this.getStatus(qHash);
1016
- consecutiveRateLimits = 0;
1017
-
1018
- // Call progress callback if provided
1019
- if (onProgress && typeof onProgress === 'function') {
1020
- onProgress(status.data || status);
1021
- }
1022
-
1023
- // Check for terminal states
1024
- const currentStatus = status.data?.status || status.status;
1025
- if (this._isTerminalStatus(currentStatus)) {
1026
- this._log('Verification completed', { status: currentStatus, duration: Date.now() - startTime });
1027
- return status;
1028
- }
1029
-
1030
- // Wait before next poll
1031
- await new Promise(resolve => setTimeout(resolve, interval));
1032
-
1033
- } catch (error) {
1034
- this._log('Status poll error', error.message);
1035
- // Continue polling unless it's a validation error
1036
- if (error instanceof ValidationError) {
1037
- throw error;
1038
- }
1039
-
1040
- let nextDelay = interval;
1041
- if (error instanceof ApiError && Number(error.statusCode) === 429) {
1042
- consecutiveRateLimits += 1;
1043
- const exp = Math.min(6, consecutiveRateLimits); // cap growth
1044
- const base = Math.max(500, Number(interval) || 0);
1045
- const max = 30000; // 30s cap to keep UX responsive
1046
- const backoff = Math.min(max, base * Math.pow(2, exp));
1047
- const jitter = Math.floor(backoff * (0.5 + Math.random() * 0.5)); // 50-100%
1048
- nextDelay = jitter;
1049
- }
1050
-
1051
- await new Promise(resolve => setTimeout(resolve, nextDelay));
1052
- }
1053
- }
1054
-
1055
- throw new NetworkError(`Polling timeout after ${timeout}ms`, 'POLLING_TIMEOUT');
1056
- }
1057
-
1058
- /**
1059
- * DETECT CHAIN ID - Get current wallet chain
1060
- *
1061
- * @returns {Promise<number>} Current chain ID
1062
- */
1063
- async detectChainId() {
1064
- if (typeof window === 'undefined' || !window.ethereum) {
1065
- throw new ConfigurationError('No Web3 wallet detected');
1066
- }
1067
-
1068
- try {
1069
- const chainId = await window.ethereum.request({ method: 'eth_chainId' });
1070
- return parseInt(chainId, 16);
1071
- } catch (error) {
1072
- throw new NetworkError(`Failed to detect chain ID: ${error.message}`);
1073
- }
1074
- }
1075
-
1076
- /** Revoke your own proof (owner-signed) */
1077
- async revokeOwnProof(qHash, wallet) {
1078
- if (!qHash || typeof qHash !== 'string') {
1079
- throw new ValidationError('qHash is required');
1080
- }
1081
- const address = wallet?.address || await this._getWalletAddress();
1082
- const signedTimestamp = Date.now();
1083
- const hubChainId = NEUS_CONSTANTS.HUB_CHAIN_ID;
1084
-
1085
- const message = constructVerificationMessage({
1086
- walletAddress: address,
1087
- signedTimestamp,
1088
- data: { action: 'revoke_proof', qHash },
1089
- verifierIds: ['ownership-basic'],
1090
- chainId: hubChainId
1091
- });
1092
-
1093
- let signature;
1094
- try {
1095
- // UNIFIED SIGNING: Match utils/core.ts fallback order
1096
- const toHexUtf8 = (s) => {
1097
- const enc = new TextEncoder();
1098
- const bytes = enc.encode(s);
1099
- let hex = '0x';
1100
- for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, '0');
1101
- return hex;
1102
- };
1103
-
1104
- // Detect Farcaster wallet - requires hex-encoded messages FIRST
1105
- const isFarcasterWallet = (() => {
1106
- if (typeof window === 'undefined') return false;
1107
- try {
1108
- const w = window;
1109
- const fc = w.farcaster;
1110
- if (!fc || !fc.context) return false;
1111
- const fcProvider = fc.provider || fc.walletProvider || (fc.context && fc.context.walletProvider);
1112
- if (fcProvider === w.ethereum) return true;
1113
- if (w.mini && w.mini.wallet === w.ethereum && fc && fc.context) return true;
1114
- if (w.ethereum && fc && fc.context) return true;
1115
- } catch {
1116
- // ignore: optional Farcaster detection
1117
- }
1118
- return false;
1119
- })();
1120
-
1121
- if (isFarcasterWallet) {
1122
- try {
1123
- const hexMsg = toHexUtf8(message);
1124
- signature = await window.ethereum.request({ method: 'personal_sign', params: [hexMsg, address] });
1125
- } catch {
1126
- // ignore: fall through to standard signing
1127
- }
1128
- }
1129
-
1130
- if (!signature) {
1131
- signature = await window.ethereum.request({ method: 'personal_sign', params: [message, address] });
1132
- }
1133
- } catch (error) {
1134
- if (error.code === 4001) {
1135
- throw new ValidationError('User rejected revocation signature');
1136
- }
1137
- throw new ValidationError(`Failed to sign revocation: ${error.message}`);
1138
- }
1139
-
1140
- const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/${qHash}/revoke-self`, {
1141
- method: 'POST',
1142
- // SECURITY: Do not put proof signatures into Authorization headers.
1143
- headers: { 'Content-Type': 'application/json' },
1144
- body: JSON.stringify({ walletAddress: address, signature, signedTimestamp })
1145
- });
1146
- const json = await res.json();
1147
- if (!json.success) {
1148
- throw new ApiError(json.error?.message || 'Failed to revoke proof', json.error);
1149
- }
1150
- return true;
1151
- }
1152
-
1153
- // ============================================================================
1154
- // PROOFS & GATING METHODS
1155
- // ============================================================================
1156
-
1157
- /**
1158
- * GET PROOFS BY WALLET - Fetch proofs for a wallet address
1159
- *
1160
- * @param {string} walletAddress - Wallet address (0x...) or DID (did:pkh:...)
1161
- * @param {Object} [options] - Filter options
1162
- * @param {number} [options.limit] - Max results (default: 50; higher limits require owner access)
1163
- * @param {number} [options.offset] - Pagination offset (default: 0)
1164
- * @returns {Promise<Object>} Proofs result
1165
- *
1166
- * @example
1167
- * const result = await client.getProofsByWallet('0x...', {
1168
- * limit: 50,
1169
- * offset: 0
1170
- * });
1171
- */
1172
- async getProofsByWallet(walletAddress, options = {}) {
1173
- if (!walletAddress || typeof walletAddress !== 'string') {
1174
- throw new ValidationError('walletAddress is required');
1175
- }
1176
-
1177
- const id = walletAddress.trim();
1178
- const pathId = /^0x[a-fA-F0-9]{40}$/i.test(id) ? id.toLowerCase() : id;
1179
-
1180
- const qs = [];
1181
- if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1182
- if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1183
-
1184
- const query = qs.length ? `?${qs.join('&')}` : '';
1185
- const response = await this._makeRequest(
1186
- 'GET',
1187
- `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`
1188
- );
1189
-
1190
- if (!response.success) {
1191
- throw new ApiError(`Failed to get proofs: ${response.error?.message || 'Unknown error'}`, response.error);
1192
- }
1193
-
1194
- // Normalize response structure
1195
- const proofs = response.data?.proofs || response.data || response.proofs || [];
1196
- return {
1197
- success: true,
1198
- proofs: Array.isArray(proofs) ? proofs : [],
1199
- totalCount: response.data?.totalCount ?? proofs.length,
1200
- hasMore: Boolean(response.data?.hasMore),
1201
- nextOffset: response.data?.nextOffset ?? null
1202
- };
1203
- }
1204
-
1205
- /**
1206
- * Get proofs by wallet (owner access)
1207
- *
1208
- * Signs an owner-access intent and requests private proofs via signature headers.
1209
- *
1210
- * @param {string} walletAddress - Wallet address (0x...) or DID (did:pkh:...)
1211
- * @param {Object} [options]
1212
- * @param {number} [options.limit] - Max results (server enforces caps)
1213
- * @param {number} [options.offset] - Pagination offset
1214
- * @param {Object} [wallet] - Optional injected wallet/provider (MetaMask/ethers Wallet)
1215
- */
1216
- async getPrivateProofsByWallet(walletAddress, options = {}, wallet = null) {
1217
- if (!walletAddress || typeof walletAddress !== 'string') {
1218
- throw new ValidationError('walletAddress is required');
1219
- }
1220
-
1221
- const id = walletAddress.trim();
1222
- const pathId = /^0x[a-fA-F0-9]{40}$/i.test(id) ? id.toLowerCase() : id;
1223
-
1224
- // Auto-detect wallet if not provided
1225
- if (!wallet) {
1226
- if (typeof window === 'undefined' || !window.ethereum) {
1227
- throw new ConfigurationError('No wallet provider available');
1228
- }
1229
- wallet = window.ethereum;
1230
- }
1231
-
1232
- let signerWalletAddress, provider;
1233
- if (wallet.address) {
1234
- signerWalletAddress = wallet.address;
1235
- provider = wallet;
1236
- } else if (wallet.selectedAddress || wallet.request) {
1237
- provider = wallet;
1238
- if (wallet.selectedAddress) {
1239
- signerWalletAddress = wallet.selectedAddress;
1240
- } else {
1241
- const accounts = await provider.request({ method: 'eth_accounts' });
1242
- if (!accounts || accounts.length === 0) {
1243
- throw new ConfigurationError('No wallet accounts available');
1244
- }
1245
- signerWalletAddress = accounts[0];
1246
- }
1247
- } else {
1248
- throw new ConfigurationError('Invalid wallet provider');
1249
- }
1250
-
1251
- const signedTimestamp = Date.now();
1252
- const message = constructVerificationMessage({
1253
- walletAddress: signerWalletAddress,
1254
- signedTimestamp,
1255
- data: { action: 'access_private_proofs_by_wallet', walletAddress: signerWalletAddress.toLowerCase() },
1256
- verifierIds: ['ownership-basic'],
1257
- chainId: NEUS_CONSTANTS.HUB_CHAIN_ID
1258
- });
1259
-
1260
- let signature;
1261
- try {
1262
- if (provider.signMessage) {
1263
- signature = await provider.signMessage(message);
1264
- } else {
1265
- signature = await provider.request({
1266
- method: 'personal_sign',
1267
- params: [message, signerWalletAddress]
1268
- });
1269
- }
1270
- } catch (error) {
1271
- if (error.code === 4001) {
1272
- throw new ValidationError('User rejected signature request');
1273
- }
1274
- throw new ValidationError(`Failed to sign message: ${error.message}`);
1275
- }
1276
-
1277
- const qs = [];
1278
- if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1279
- if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1280
- const query = qs.length ? `?${qs.join('&')}` : '';
1281
-
1282
- const response = await this._makeRequest('GET', `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`, null, {
1283
- 'x-wallet-address': signerWalletAddress,
1284
- 'x-signature': signature,
1285
- 'x-signed-timestamp': signedTimestamp.toString()
1286
- });
1287
-
1288
- if (!response.success) {
1289
- throw new ApiError(`Failed to get proofs: ${response.error?.message || 'Unauthorized'}`, response.error);
1290
- }
1291
-
1292
- const proofs = response.data?.proofs || response.data || response.proofs || [];
1293
- return {
1294
- success: true,
1295
- proofs: Array.isArray(proofs) ? proofs : [],
1296
- totalCount: response.data?.totalCount ?? proofs.length,
1297
- hasMore: Boolean(response.data?.hasMore),
1298
- nextOffset: response.data?.nextOffset ?? null
1299
- };
1300
- }
1301
-
1302
- /**
1303
- * GATE CHECK (API) - Minimal eligibility check
1304
- *
1305
- * Calls the public gate endpoint and returns a **minimal** yes/no response
1306
- * against **public + discoverable** proofs only.
1307
- *
1308
- * Prefer this over `checkGate()` for server-side integrations that want the
1309
- * smallest, most stable surface area (and do NOT need full proof payloads).
1310
- *
1311
- * @param {Object} params - Gate check query params
1312
- * @param {string} params.address - Wallet address to check (0x...)
1313
- * @param {Array<string>|string} [params.verifierIds] - Verifier IDs to match (array or comma-separated)
1314
- * @param {boolean} [params.requireAll] - Require all verifierIds on a single proof
1315
- * @param {number} [params.minCount] - Minimum number of matching proofs
1316
- * @param {number} [params.sinceDays] - Optional time window in days
1317
- * @param {number} [params.since] - Optional unix timestamp in ms (lower bound)
1318
- * @param {number} [params.limit] - Max rows to scan (server may clamp)
1319
- * @returns {Promise<Object>} API response ({ success, data })
1320
- */
1321
- async gateCheck(params = {}) {
1322
- const address = (params.address || '').toString();
1323
- if (!address || !/^0x[a-fA-F0-9]{40}$/i.test(address)) {
1324
- throw new ValidationError('Valid address is required');
1325
- }
1326
-
1327
- // Build query string safely (stringify all values; allow arrays for common fields)
1328
- const qs = new URLSearchParams();
1329
- qs.set('address', address);
1330
-
1331
- const setIfPresent = (key, value) => {
1332
- if (value === undefined || value === null) return;
1333
- const str = typeof value === 'string' ? value : String(value);
1334
- if (str.length === 0) return;
1335
- qs.set(key, str);
1336
- };
1337
-
1338
- const setBoolIfPresent = (key, value) => {
1339
- if (value === undefined || value === null) return;
1340
- qs.set(key, value ? 'true' : 'false');
1341
- };
1342
-
1343
- const setCsvIfPresent = (key, value) => {
1344
- if (value === undefined || value === null) return;
1345
- if (Array.isArray(value)) {
1346
- const items = value.map(v => String(v).trim()).filter(Boolean);
1347
- if (items.length) qs.set(key, items.join(','));
1348
- return;
1349
- }
1350
- setIfPresent(key, value);
1351
- };
1352
-
1353
- // Common filters
1354
- setCsvIfPresent('verifierIds', params.verifierIds);
1355
- setBoolIfPresent('requireAll', params.requireAll);
1356
- setIfPresent('minCount', params.minCount);
1357
- setIfPresent('sinceDays', params.sinceDays);
1358
- setIfPresent('since', params.since);
1359
- setIfPresent('limit', params.limit);
1360
-
1361
- // Common match filters (optional)
1362
- setIfPresent('referenceType', params.referenceType);
1363
- setIfPresent('referenceId', params.referenceId);
1364
- setIfPresent('tag', params.tag);
1365
- setCsvIfPresent('tags', params.tags);
1366
- setIfPresent('contentType', params.contentType);
1367
- setIfPresent('content', params.content);
1368
- setIfPresent('contentHash', params.contentHash);
1369
-
1370
- // Asset/ownership filters
1371
- setIfPresent('contractAddress', params.contractAddress);
1372
- setIfPresent('tokenId', params.tokenId);
1373
- setIfPresent('chainId', params.chainId);
1374
- setIfPresent('domain', params.domain);
1375
- setIfPresent('minBalance', params.minBalance);
1376
-
1377
- // Wallet filters
1378
- setIfPresent('provider', params.provider);
1379
- setIfPresent('ownerAddress', params.ownerAddress);
1380
- setIfPresent('riskLevel', params.riskLevel);
1381
- setBoolIfPresent('sanctioned', params.sanctioned);
1382
- setBoolIfPresent('poisoned', params.poisoned);
1383
- setIfPresent('primaryWalletAddress', params.primaryWalletAddress);
1384
- setIfPresent('secondaryWalletAddress', params.secondaryWalletAddress);
1385
- setIfPresent('verificationMethod', params.verificationMethod);
1386
-
1387
- const response = await this._makeRequest('GET', `/api/v1/proofs/gate/check?${qs.toString()}`);
1388
- if (!response.success) {
1389
- throw new ApiError(`Gate check failed: ${response.error?.message || 'Unknown error'}`, response.error);
1390
- }
1391
- return response;
1392
- }
1393
-
1394
- /**
1395
- * CHECK GATE - Evaluate requirements against existing proofs
1396
- *
1397
- * Gate-first verification: checks if wallet has valid proofs satisfying requirements.
1398
- * Returns which requirements are missing/expired.
1399
- *
1400
- * @param {Object} params - Gate check parameters
1401
- * @param {string} params.walletAddress - Target wallet
1402
- * @param {Array<Object>} params.requirements - Array of gate requirements
1403
- * @param {string} params.requirements[].verifierId - Required verifier ID
1404
- * @param {number} [params.requirements[].maxAgeMs] - Max proof age in ms (TTL)
1405
- * @param {boolean} [params.requirements[].optional] - If true, not required for gate satisfaction
1406
- * @param {number} [params.requirements[].minCount] - Minimum proofs needed (default: 1)
1407
- * @param {Object} [params.requirements[].match] - Verifier data match criteria
1408
- * Supports nested fields: 'reference.type', 'reference.id', 'content', 'contentHash', 'input.*', 'license.*'
1409
- * Supports verifier-specific:
1410
- * - NFT/Token: 'contractAddress', 'tokenId', 'chainId', 'ownerAddress', 'minBalance'
1411
- * - DNS: 'domain', 'walletAddress'
1412
- * - Wallet-link: 'primaryWalletAddress', 'secondaryWalletAddress', 'chainId'
1413
- * - Contract-ownership: 'contractAddress', 'chainId', 'owner', 'verificationMethod'
1414
- * Note: contentHash matching uses approximation in SDK; for exact SHA-256 matching, use backend API
1415
- * @param {Array} [params.proofs] - Pre-fetched proofs (skip API call)
1416
- * @returns {Promise<Object>} Gate result with satisfied, missing, existing
1417
- *
1418
- * @example
1419
- * // Basic gate check
1420
- * const result = await client.checkGate({
1421
- * walletAddress: '0x...',
1422
- * requirements: [
1423
- * { verifierId: 'nft-ownership', match: { contractAddress: '0x...' } }
1424
- * ]
1425
- * });
1426
- */
1427
- async checkGate(params) {
1428
- const { walletAddress, requirements, proofs: preloadedProofs } = params;
1429
-
1430
- if (!walletAddress || !/^0x[a-fA-F0-9]{40}$/i.test(walletAddress)) {
1431
- throw new ValidationError('Valid walletAddress is required');
1432
- }
1433
- if (!Array.isArray(requirements) || requirements.length === 0) {
1434
- throw new ValidationError('requirements array is required and must not be empty');
1435
- }
1436
-
1437
- // Use preloaded proofs or fetch from API
1438
- let proofs = preloadedProofs;
1439
- if (!proofs) {
1440
- const result = await this.getProofsByWallet(walletAddress, { limit: 100 });
1441
- proofs = result.proofs;
1442
- }
1443
-
1444
- const now = Date.now();
1445
- const existing = {};
1446
- const missing = [];
1447
-
1448
- for (const req of requirements) {
1449
- const { verifierId, maxAgeMs, optional = false, minCount = 1, match } = req;
1450
-
1451
- // Find matching proofs for this verifier
1452
- const matchingProofs = (proofs || []).filter(proof => {
1453
- // Must have this verifier and be verified
1454
- const verifiedVerifiers = proof.verifiedVerifiers || [];
1455
- const verifier = verifiedVerifiers.find(
1456
- v => v.verifierId === verifierId && v.verified === true
1457
- );
1458
- if (!verifier) return false;
1459
-
1460
- // Check proof is not revoked
1461
- if (proof.revokedAt) return false;
1462
-
1463
- // Check TTL if specified
1464
- if (maxAgeMs) {
1465
- const proofTimestamp = proof.createdAt || proof.signedTimestamp || 0;
1466
- const proofAge = now - proofTimestamp;
1467
- if (proofAge > maxAgeMs) return false;
1468
- }
1469
-
1470
- // Check custom match criteria if specified
1471
- if (match && typeof match === 'object') {
1472
- const data = verifier.data || {};
1473
- const input = data.input || {}; // NFT/token verifiers store fields in input
1474
-
1475
- for (const [key, expected] of Object.entries(match)) {
1476
- let actualValue = null;
1477
-
1478
- // Handle nested field access
1479
- if (key.includes('.')) {
1480
- const parts = key.split('.');
1481
- let current = data;
1482
-
1483
- if (parts[0] === 'input' && input) {
1484
- current = input;
1485
- parts.shift();
1486
- }
1487
-
1488
- for (const part of parts) {
1489
- if (current && typeof current === 'object' && part in current) {
1490
- current = current[part];
1491
- } else {
1492
- current = undefined;
1493
- break;
1494
- }
1495
- }
1496
- actualValue = current;
1497
- } else {
1498
- actualValue = input[key] || data[key];
1499
- }
1500
-
1501
- if (key === 'content' && actualValue === undefined) {
1502
- actualValue = data.reference?.id || data.content;
1503
- }
1504
-
1505
- // Special handling for verifier-specific fields
1506
- if (actualValue === undefined) {
1507
- if (key === 'contractAddress') {
1508
- actualValue = input.contractAddress || data.contractAddress;
1509
- } else if (key === 'tokenId') {
1510
- actualValue = input.tokenId || data.tokenId;
1511
- } else if (key === 'chainId') {
1512
- actualValue = input.chainId || data.chainId;
1513
- } else if (key === 'ownerAddress') {
1514
- actualValue = input.ownerAddress || data.owner || data.walletAddress;
1515
- } else if (key === 'minBalance') {
1516
- actualValue = input.minBalance || data.onChainData?.requiredMinBalance || data.minBalance;
1517
- } else if (key === 'primaryWalletAddress') {
1518
- actualValue = data.primaryWalletAddress;
1519
- } else if (key === 'secondaryWalletAddress') {
1520
- actualValue = data.secondaryWalletAddress;
1521
- } else if (key === 'verificationMethod') {
1522
- actualValue = data.verificationMethod;
1523
- } else if (key === 'domain') {
1524
- actualValue = data.domain;
1525
- }
1526
- }
1527
-
1528
- // Content hash check (approximation)
1529
- if (key === 'contentHash' && actualValue === undefined && data.content) {
1530
- try {
1531
- // Simple hash approximation for non-crypto envs
1532
- let hash = 0;
1533
- const str = String(data.content);
1534
- for (let i = 0; i < str.length; i++) {
1535
- const char = str.charCodeAt(i);
1536
- hash = ((hash << 5) - hash) + char;
1537
- hash = hash & hash;
1538
- }
1539
- actualValue = '0x' + Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66);
1540
- } catch {
1541
- actualValue = String(data.content);
1542
- }
1543
- }
1544
-
1545
- let normalizedActual = actualValue;
1546
- let normalizedExpected = expected;
1547
-
1548
- if (actualValue === undefined || actualValue === null) {
1549
- return false;
1550
- }
1551
-
1552
- if (key === 'chainId' || (key === 'tokenId' && (typeof actualValue === 'number' || !isNaN(Number(actualValue))))) {
1553
- normalizedActual = Number(actualValue);
1554
- normalizedExpected = Number(expected);
1555
- if (isNaN(normalizedActual) || isNaN(normalizedExpected)) return false;
1556
- }
1557
- else if (typeof actualValue === 'string' && (actualValue.startsWith('0x') || actualValue.length > 20)) {
1558
- normalizedActual = actualValue.toLowerCase();
1559
- normalizedExpected = typeof expected === 'string' ? String(expected).toLowerCase() : expected;
1560
- }
1561
- else {
1562
- normalizedActual = actualValue;
1563
- normalizedExpected = expected;
1564
- }
1565
-
1566
- if (normalizedActual !== normalizedExpected) {
1567
- return false;
1568
- }
1569
- }
1570
- }
1571
-
1572
- return true;
1573
- });
1574
-
1575
- if (matchingProofs.length >= minCount) {
1576
- const sorted = matchingProofs.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
1577
- existing[verifierId] = sorted[0];
1578
- } else if (!optional) {
1579
- missing.push(req);
1580
- }
1581
- }
1582
-
1583
- return {
1584
- satisfied: missing.length === 0,
1585
- missing,
1586
- existing,
1587
- allProofs: proofs
1588
- };
1589
- }
1590
-
1591
- // ============================================================================
1592
- // PRIVATE UTILITY METHODS
1593
- // ============================================================================
1594
-
1595
- /**
1596
- * Get connected wallet address
1597
- * @private
1598
- */
1599
- async _getWalletAddress() {
1600
- if (typeof window === 'undefined' || !window.ethereum) {
1601
- throw new ConfigurationError('No Web3 wallet detected');
1602
- }
1603
-
1604
- const accounts = await window.ethereum.request({ method: 'eth_accounts' });
1605
- if (!accounts || accounts.length === 0) {
1606
- throw new ConfigurationError('No wallet accounts available');
1607
- }
1608
-
1609
- return accounts[0];
1610
- }
1611
-
1612
- /**
1613
- * Make HTTP request to API
1614
- * @private
1615
- */
1616
- async _makeRequest(method, endpoint, data = null, headersOverride = null) {
1617
- const url = `${this.baseUrl}${endpoint}`;
1618
-
1619
- const controller = new AbortController();
1620
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1621
-
1622
- const options = {
1623
- method,
1624
- headers: { ...this.defaultHeaders, ...(headersOverride || {}) },
1625
- signal: controller.signal
1626
- };
1627
-
1628
- if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
1629
- options.body = JSON.stringify(data);
1630
- }
1631
-
1632
- this._log(`${method} ${endpoint}`, data ? { requestBodyKeys: Object.keys(data) } : {});
1633
-
1634
- try {
1635
- const response = await fetch(url, options);
1636
- clearTimeout(timeoutId);
1637
-
1638
- let responseData;
1639
- try {
1640
- responseData = await response.json();
1641
- } catch {
1642
- responseData = { error: { message: 'Invalid JSON response' } };
1643
- }
1644
-
1645
- if (!response.ok) {
1646
- throw ApiError.fromResponse(response, responseData);
1647
- }
1648
-
1649
- return responseData;
1650
-
1651
- } catch (error) {
1652
- clearTimeout(timeoutId);
1653
-
1654
- if (error.name === 'AbortError') {
1655
- throw new NetworkError(`Request timeout after ${this.config.timeout}ms`);
1656
- }
1657
-
1658
- if (error instanceof ApiError) {
1659
- throw error;
1660
- }
1661
-
1662
- throw new NetworkError(`Network error: ${error.message}`);
1663
- }
1664
- }
1665
-
1666
- /**
1667
- * Format API response for consistent structure
1668
- * @private
1669
- */
1670
- _formatResponse(response) {
1671
- const qHash = response?.data?.qHash ||
1672
- response?.qHash ||
1673
- response?.data?.resource?.qHash ||
1674
- response?.data?.id;
1675
-
1676
- const status = response?.data?.status ||
1677
- response?.status ||
1678
- response?.data?.resource?.status ||
1679
- (response?.success ? 'completed' : 'unknown');
1680
-
1681
- return {
1682
- success: response.success,
1683
- qHash,
1684
- status,
1685
- data: response.data,
1686
- message: response.message,
1687
- timestamp: Date.now(),
1688
- statusUrl: qHash ? `${this.baseUrl}/api/v1/verification/status/${qHash}` : null
1689
- };
1690
- }
1691
-
1692
- /**
1693
- * Check if status is terminal (completed or failed)
1694
- * @private
1695
- */
1696
- _isTerminalStatus(status) {
1697
- const terminalStates = [
1698
- 'verified',
1699
- 'verified_crosschain_propagated',
1700
- 'completed_all_successful',
1701
- 'failed',
1702
- 'error',
1703
- 'rejected',
1704
- 'cancelled'
1705
- ];
1706
- return typeof status === 'string' && terminalStates.some(state => status.includes(state));
1707
- }
1708
-
1709
- /** SDK logging (opt-in via config.enableLogging) */
1710
- _log(message, data = {}) {
1711
- if (this.config.enableLogging) {
1712
- try {
1713
- const prefix = '[neus/sdk]';
1714
- if (data && Object.keys(data).length > 0) {
1715
- // eslint-disable-next-line no-console
1716
- console.log(prefix, message, data);
1717
- } else {
1718
- // eslint-disable-next-line no-console
1719
- console.log(prefix, message);
1720
- }
1721
- } catch {
1722
- // ignore logging failures
1723
- }
1724
- }
1725
- }
1726
- }
1727
-
1728
- // Export the constructVerificationMessage function for advanced use
1729
- export { constructVerificationMessage };
1
+ /**
2
+ * NEUS SDK Client
3
+ * Create and verify cryptographic proofs across applications
4
+ * @license Apache-2.0
5
+ */
6
+
7
+ 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
36
+ const validateVerifierData = (verifierId, data) => {
37
+ if (!data || typeof data !== 'object') {
38
+ return { valid: false, error: 'Data object is required' };
39
+ }
40
+
41
+ // Format validation for supported verifiers
42
+ 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' };
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
+ }
57
+ }
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
+ }
62
+ }
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' };
71
+ }
72
+ }
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' };
76
+ }
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(', ')}` };
82
+ }
83
+ }
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
+ }
109
+ }
110
+ }
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' };
115
+ }
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' };
120
+ }
121
+ }
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
+ }
132
+ }
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' };
143
+ }
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
+ }
149
+ }
150
+ if (data.blockNumber !== undefined && data.blockNumber !== null && !Number.isInteger(data.blockNumber)) {
151
+ return { valid: false, error: 'blockNumber must be an integer when provided' };
152
+ }
153
+ if (data.ownerAddress && !validateWalletAddress(data.ownerAddress)) {
154
+ return { valid: false, error: 'Invalid ownerAddress' };
155
+ }
156
+ if (!validateWalletAddress(data.contractAddress)) {
157
+ return { valid: false, error: 'Invalid contractAddress' };
158
+ }
159
+ break;
160
+ case 'token-holding':
161
+ // ownerAddress is optional; server injects from request walletAddress when omitted.
162
+ if (
163
+ !data.contractAddress ||
164
+ data.minBalance === null ||
165
+ data.minBalance === undefined ||
166
+ 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.
281
+ }
282
+ }
283
+ }
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' };
307
+ }
308
+ break;
309
+ }
310
+
311
+ return { valid: true };
312
+ };
313
+
314
+ export class NeusClient {
315
+ constructor(config = {}) {
316
+ this.config = {
317
+ timeout: 30000,
318
+ enableLogging: false,
319
+ ...config
320
+ };
321
+
322
+ // NEUS Network API
323
+ this.baseUrl = this.config.apiUrl || 'https://api.neus.network';
324
+ // Enforce HTTPS for neus.network domains to satisfy CSP and normalize URLs
325
+ try {
326
+ const url = new URL(this.baseUrl);
327
+ if (url.hostname.endsWith('neus.network') && url.protocol === 'http:') {
328
+ url.protocol = 'https:';
329
+ }
330
+ // Always remove trailing slash for consistency
331
+ this.baseUrl = url.toString().replace(/\/$/, '');
332
+ } catch {
333
+ // If invalid URL string, leave as-is
334
+ }
335
+ // Normalize apiUrl on config
336
+ this.config.apiUrl = this.baseUrl;
337
+ // Default headers for API requests
338
+ this.defaultHeaders = {
339
+ 'Content-Type': 'application/json',
340
+ 'Accept': 'application/json',
341
+ 'X-Neus-Sdk': 'js'
342
+ };
343
+
344
+ // Optional API key (server-side only; do not embed in browser apps)
345
+ if (typeof this.config.apiKey === 'string' && this.config.apiKey.trim().length > 0) {
346
+ this.defaultHeaders['Authorization'] = `Bearer ${this.config.apiKey.trim()}`;
347
+ }
348
+ // Public app attribution header (non-secret)
349
+ if (typeof this.config.appId === 'string' && this.config.appId.trim().length > 0) {
350
+ this.defaultHeaders['X-Neus-App'] = this.config.appId.trim();
351
+ }
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.
361
+ if (this.config.extraHeaders && typeof this.config.extraHeaders === 'object') {
362
+ for (const [k, v] of Object.entries(this.config.extraHeaders)) {
363
+ if (!k || v === undefined || v === null) continue;
364
+ const key = String(k).trim();
365
+ const value = String(v).trim();
366
+ if (!key || !value) continue;
367
+ this.defaultHeaders[key] = value;
368
+ }
369
+ }
370
+ try {
371
+ // Attach origin in browser environments
372
+ if (typeof window !== 'undefined' && window.location && window.location.origin) {
373
+ this.defaultHeaders['X-Client-Origin'] = window.location.origin;
374
+ }
375
+ } catch {
376
+ // ignore: optional browser metadata header
377
+ }
378
+ }
379
+
380
+ _getHubChainId() {
381
+ const configured = Number(this.config?.hubChainId);
382
+ if (Number.isFinite(configured) && configured > 0) return Math.floor(configured);
383
+ return NEUS_CONSTANTS.HUB_CHAIN_ID;
384
+ }
385
+
386
+ _normalizeIdentity(value) {
387
+ let raw = String(value || '').trim();
388
+ if (!raw) return '';
389
+ const didMatch = raw.match(/^did:pkh:([^:]+):([^:]+):(.+)$/i);
390
+ if (didMatch && didMatch[3]) {
391
+ raw = String(didMatch[3]).trim();
392
+ }
393
+ return EVM_ADDRESS_RE.test(raw) ? raw.toLowerCase() : raw;
394
+ }
395
+
396
+ _inferChainForAddress(address, explicitChain) {
397
+ if (typeof explicitChain === 'string' && explicitChain.includes(':')) return explicitChain.trim();
398
+ const raw = String(address || '').trim();
399
+ const didMatch = raw.match(/^did:pkh:([^:]+):([^:]+):(.+)$/i);
400
+ if (didMatch && didMatch[1] && didMatch[2]) {
401
+ return `${didMatch[1]}:${didMatch[2]}`;
402
+ }
403
+ if (EVM_ADDRESS_RE.test(raw)) {
404
+ return `eip155:${this._getHubChainId()}`;
405
+ }
406
+ // Default non-EVM chain for universal wallet paths when caller omits chain.
407
+ return 'solana:mainnet';
408
+ }
409
+
410
+ async _resolveWalletSigner(wallet) {
411
+ if (!wallet) {
412
+ throw new ConfigurationError('No wallet provider available');
413
+ }
414
+
415
+ if (wallet.address) {
416
+ return { signerWalletAddress: wallet.address, provider: wallet };
417
+ }
418
+ if (wallet.publicKey && typeof wallet.publicKey.toBase58 === 'function') {
419
+ return { signerWalletAddress: wallet.publicKey.toBase58(), provider: wallet };
420
+ }
421
+ if (typeof wallet.getAddress === 'function') {
422
+ const signerWalletAddress = await wallet.getAddress().catch(() => null);
423
+ return { signerWalletAddress, provider: wallet };
424
+ }
425
+ if (wallet.selectedAddress || wallet.request) {
426
+ const provider = wallet;
427
+ if (wallet.selectedAddress) {
428
+ return { signerWalletAddress: wallet.selectedAddress, provider };
429
+ }
430
+ const accounts = await provider.request({ method: 'eth_accounts' });
431
+ if (!accounts || accounts.length === 0) {
432
+ throw new ConfigurationError('No wallet accounts available');
433
+ }
434
+ return { signerWalletAddress: accounts[0], provider };
435
+ }
436
+
437
+ throw new ConfigurationError('Invalid wallet provider');
438
+ }
439
+
440
+ _getDefaultBrowserWallet() {
441
+ if (typeof window === 'undefined') return null;
442
+ return window.ethereum || window.solana || (window.phantom && window.phantom.solana) || null;
443
+ }
444
+
445
+ async _buildPrivateGateAuth({ address, wallet, chain, signatureMethod } = {}) {
446
+ const providerWallet = wallet || this._getDefaultBrowserWallet();
447
+ const { signerWalletAddress, provider } = await this._resolveWalletSigner(providerWallet);
448
+ if (!signerWalletAddress || typeof signerWalletAddress !== 'string') {
449
+ throw new ConfigurationError('No wallet accounts available');
450
+ }
451
+
452
+ const normalizedSigner = this._normalizeIdentity(signerWalletAddress);
453
+ const normalizedAddress = this._normalizeIdentity(address);
454
+ if (!normalizedSigner || normalizedSigner !== normalizedAddress) {
455
+ throw new ValidationError('wallet must match address when includePrivate=true');
456
+ }
457
+
458
+ const signerIsEvm = EVM_ADDRESS_RE.test(normalizedSigner);
459
+ const resolvedChain = this._inferChainForAddress(normalizedSigner, chain);
460
+ const resolvedSignatureMethod = (typeof signatureMethod === 'string' && signatureMethod.trim())
461
+ ? signatureMethod.trim()
462
+ : (signerIsEvm ? 'eip191' : 'ed25519');
463
+
464
+ const signedTimestamp = Date.now();
465
+ const message = constructVerificationMessage({
466
+ walletAddress: signerWalletAddress,
467
+ signedTimestamp,
468
+ data: { action: 'gate_check_private_proofs', walletAddress: normalizedAddress },
469
+ verifierIds: ['ownership-basic'],
470
+ ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain: resolvedChain })
471
+ });
472
+
473
+ let signature;
474
+ try {
475
+ signature = await signMessage({
476
+ provider,
477
+ message,
478
+ walletAddress: signerWalletAddress,
479
+ ...(signerIsEvm ? {} : { chain: resolvedChain })
480
+ });
481
+ } catch (error) {
482
+ if (error.code === 4001) {
483
+ throw new ValidationError('User rejected signature request');
484
+ }
485
+ throw new ValidationError(`Failed to sign message: ${error.message}`);
486
+ }
487
+
488
+ return {
489
+ walletAddress: signerWalletAddress,
490
+ signature,
491
+ signedTimestamp,
492
+ ...(signerIsEvm ? {} : { chain: resolvedChain, signatureMethod: resolvedSignatureMethod })
493
+ };
494
+ }
495
+
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
+ }
501
+ return this._buildPrivateGateAuth({
502
+ address,
503
+ wallet: params.wallet,
504
+ 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
+ *
520
+ * @param {Object} params - Verification parameters
521
+ * @param {Array<string>} [params.verifierIds] - Array of verifier IDs (manual path)
522
+ * @param {Object} [params.data] - Verification data object (manual path)
523
+ * @param {string} [params.walletAddress] - Wallet address that signed the request (manual path)
524
+ * @param {string} [params.signature] - EIP-191 signature (manual path)
525
+ * @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
+ *
533
+ * @example
534
+ * const proof = await client.verify({
535
+ * 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
+ * },
541
+ * walletAddress: '0x...',
542
+ * signature: '0x...',
543
+ * signedTimestamp: Date.now(),
544
+ * options: { targetChains: [421614, 11155111] }
545
+ * });
546
+ */
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
+ async verify(params) {
567
+ // Auto path: if no manual signature fields but auto fields are provided, perform inline wallet flow
568
+ if ((!params?.signature || !params?.walletAddress) && (params?.verifier || params?.content || params?.data)) {
569
+ const { content, verifier = 'ownership-basic', data = null, wallet = null, options = {} } = params;
570
+
571
+ // ownership-basic: content required for simple mode, but data param mode allows contentHash or reference
572
+ if (verifier === 'ownership-basic' && !data && (!content || typeof content !== 'string')) {
573
+ throw new ValidationError('content is required and must be a string (or use data param with owner + reference)');
574
+ }
575
+
576
+ let validVerifiers = FALLBACK_PUBLIC_VERIFIERS;
577
+ try {
578
+ const discovered = await this.getVerifiers();
579
+ if (Array.isArray(discovered) && discovered.length > 0) {
580
+ validVerifiers = discovered;
581
+ }
582
+ } catch {
583
+ // Fallback keeps SDK usable if verifier catalog endpoint is temporarily unavailable.
584
+ }
585
+ if (!validVerifiers.includes(verifier)) {
586
+ throw new ValidationError(`Invalid verifier '${verifier}'. Must be one of: ${validVerifiers.join(', ')}.`);
587
+ }
588
+
589
+ if (INTERACTIVE_VERIFIERS.has(verifier)) {
590
+ const hostedCheckoutUrl = options?.hostedCheckoutUrl || 'https://neus.network/verify';
591
+ throw new ValidationError(
592
+ `${verifier} requires hosted interactive checkout. Use VerifyGate or redirect to ${hostedCheckoutUrl}.`
593
+ );
594
+ }
595
+
596
+ // These verifiers require explicit data parameter (no auto-path)
597
+ const requiresDataParam = [
598
+ 'ownership-dns-txt',
599
+ 'wallet-link',
600
+ 'contract-ownership',
601
+ 'ownership-pseudonym',
602
+ 'wallet-risk',
603
+ 'agent-identity',
604
+ 'agent-delegation',
605
+ 'ai-content-moderation'
606
+ ];
607
+ if (requiresDataParam.includes(verifier)) {
608
+ if (!data || typeof data !== 'object') {
609
+ throw new ValidationError(`${verifier} requires explicit data parameter. Cannot use auto-path.`);
610
+ }
611
+ }
612
+
613
+ // Auto-detect wallet and get address
614
+ let walletAddress, provider;
615
+ if (wallet) {
616
+ walletAddress =
617
+ wallet.address ||
618
+ wallet.selectedAddress ||
619
+ wallet.walletAddress ||
620
+ (typeof wallet.getAddress === 'function' ? await wallet.getAddress() : null);
621
+ provider = wallet.provider || wallet;
622
+ if (!walletAddress && provider && typeof provider.request === 'function') {
623
+ const accounts = await provider.request({ method: 'eth_accounts' });
624
+ walletAddress = Array.isArray(accounts) ? accounts[0] : null;
625
+ }
626
+ } else {
627
+ if (typeof window === 'undefined' || !window.ethereum) {
628
+ throw new ConfigurationError('No Web3 wallet detected. Please install MetaMask or provide wallet parameter.');
629
+ }
630
+ await window.ethereum.request({ method: 'eth_requestAccounts' });
631
+ provider = window.ethereum;
632
+ const accounts = await provider.request({ method: 'eth_accounts' });
633
+ walletAddress = accounts[0];
634
+ }
635
+
636
+ // Prepare verification data based on verifier type
637
+ let verificationData;
638
+ if (verifier === 'ownership-basic') {
639
+ if (data && typeof data === 'object') {
640
+ // Data param mode: use provided data, inject owner if missing
641
+ verificationData = {
642
+ owner: data.owner || walletAddress,
643
+ reference: data.reference,
644
+ ...(data.content && { content: data.content }),
645
+ ...(data.contentHash && { contentHash: data.contentHash }),
646
+ ...(data.contentType && { contentType: data.contentType }),
647
+ ...(data.provenance && { provenance: data.provenance })
648
+ };
649
+ } else {
650
+ // Simple content mode: derive reference from content
651
+ verificationData = {
652
+ content: content,
653
+ owner: walletAddress,
654
+ reference: { type: 'other' }
655
+ };
656
+ }
657
+ } else if (verifier === 'token-holding') {
658
+ if (!data?.contractAddress) {
659
+ throw new ValidationError('token-holding requires contractAddress in data parameter');
660
+ }
661
+ if (data?.minBalance === null || data?.minBalance === undefined) {
662
+ throw new ValidationError('token-holding requires minBalance in data parameter');
663
+ }
664
+ if (typeof data?.chainId !== 'number') {
665
+ throw new ValidationError('token-holding requires chainId (number) in data parameter');
666
+ }
667
+ verificationData = {
668
+ ownerAddress: walletAddress,
669
+ contractAddress: data.contractAddress,
670
+ minBalance: data.minBalance,
671
+ chainId: data.chainId
672
+ };
673
+ } else if (verifier === 'nft-ownership') {
674
+ if (!data?.contractAddress) {
675
+ throw new ValidationError('nft-ownership requires contractAddress in data parameter');
676
+ }
677
+ if (data?.tokenId === null || data?.tokenId === undefined) {
678
+ throw new ValidationError('nft-ownership requires tokenId in data parameter');
679
+ }
680
+ if (typeof data?.chainId !== 'number') {
681
+ throw new ValidationError('nft-ownership requires chainId (number) in data parameter');
682
+ }
683
+ verificationData = {
684
+ ownerAddress: walletAddress,
685
+ contractAddress: data.contractAddress,
686
+ tokenId: data.tokenId,
687
+ chainId: data.chainId,
688
+ tokenType: data?.tokenType || 'erc721'
689
+ };
690
+ } else if (verifier === 'ownership-dns-txt') {
691
+ if (!data?.domain) {
692
+ throw new ValidationError('ownership-dns-txt requires domain in data parameter');
693
+ }
694
+ verificationData = {
695
+ domain: data.domain,
696
+ walletAddress: walletAddress
697
+ };
698
+ } else if (verifier === 'wallet-link') {
699
+ if (!data?.secondaryWalletAddress) {
700
+ throw new ValidationError('wallet-link requires secondaryWalletAddress in data parameter');
701
+ }
702
+ if (!data?.signature) {
703
+ throw new ValidationError('wallet-link requires signature in data parameter (signed by secondary wallet)');
704
+ }
705
+ if (typeof data?.chain !== 'string' || !/^[a-z0-9]+:[^\s]+$/.test(data.chain)) {
706
+ throw new ValidationError('wallet-link requires chain (namespace:reference) in data parameter');
707
+ }
708
+ if (typeof data?.signatureMethod !== 'string' || !data.signatureMethod.trim()) {
709
+ throw new ValidationError('wallet-link requires signatureMethod in data parameter');
710
+ }
711
+ verificationData = {
712
+ primaryWalletAddress: walletAddress,
713
+ secondaryWalletAddress: data.secondaryWalletAddress,
714
+ signature: data.signature,
715
+ chain: data.chain,
716
+ signatureMethod: data.signatureMethod,
717
+ signedTimestamp: data?.signedTimestamp || Date.now()
718
+ };
719
+ } else if (verifier === 'contract-ownership') {
720
+ if (!data?.contractAddress) {
721
+ throw new ValidationError('contract-ownership requires contractAddress in data parameter');
722
+ }
723
+ if (typeof data?.chainId !== 'number') {
724
+ throw new ValidationError('contract-ownership requires chainId (number) in data parameter');
725
+ }
726
+ verificationData = {
727
+ contractAddress: data.contractAddress,
728
+ walletAddress: walletAddress,
729
+ chainId: data.chainId,
730
+ ...(data?.method && { method: data.method })
731
+ };
732
+ } else if (verifier === 'agent-identity') {
733
+ if (!data?.agentId) {
734
+ throw new ValidationError('agent-identity requires agentId in data parameter');
735
+ }
736
+ verificationData = {
737
+ agentId: data.agentId,
738
+ agentWallet: data?.agentWallet || walletAddress,
739
+ ...(data?.agentLabel && { agentLabel: data.agentLabel }),
740
+ ...(data?.agentType && { agentType: data.agentType }),
741
+ ...(data?.description && { description: data.description }),
742
+ ...(data?.capabilities && { capabilities: data.capabilities })
743
+ };
744
+ } else if (verifier === 'agent-delegation') {
745
+ if (!data?.agentWallet) {
746
+ throw new ValidationError('agent-delegation requires agentWallet in data parameter');
747
+ }
748
+ verificationData = {
749
+ controllerWallet: data?.controllerWallet || walletAddress,
750
+ agentWallet: data.agentWallet,
751
+ ...(data?.agentId && { agentId: data.agentId }),
752
+ ...(data?.scope && { scope: data.scope }),
753
+ ...(data?.permissions && { permissions: data.permissions }),
754
+ ...(data?.maxSpend && { maxSpend: data.maxSpend }),
755
+ ...(data?.expiresAt && { expiresAt: data.expiresAt })
756
+ };
757
+ } else if (verifier === 'ai-content-moderation') {
758
+ if (!data?.content) {
759
+ throw new ValidationError('ai-content-moderation requires content (base64) in data parameter');
760
+ }
761
+ if (!data?.contentType) {
762
+ throw new ValidationError('ai-content-moderation requires contentType (MIME type) in data parameter');
763
+ }
764
+ verificationData = {
765
+ content: data.content,
766
+ contentType: data.contentType,
767
+ ...(data?.provider && { provider: data.provider })
768
+ };
769
+ } else if (verifier === 'ownership-pseudonym') {
770
+ if (!data?.pseudonymId) {
771
+ throw new ValidationError('ownership-pseudonym requires pseudonymId in data parameter');
772
+ }
773
+ verificationData = {
774
+ pseudonymId: data.pseudonymId,
775
+ ...(data?.namespace && { namespace: data.namespace }),
776
+ ...(data?.displayName && { displayName: data.displayName }),
777
+ ...(data?.metadata && { metadata: data.metadata })
778
+ };
779
+ } else if (verifier === 'wallet-risk') {
780
+ // wallet-risk defaults to verifying the connected wallet
781
+ verificationData = {
782
+ walletAddress: data?.walletAddress || walletAddress,
783
+ ...(data?.provider && { provider: data.provider }),
784
+ // Mainnet-first semantics: if caller doesn't provide chainId, default to Ethereum mainnet (1).
785
+ // This avoids accidental testnet semantics for risk providers.
786
+ chainId: (typeof data?.chainId === 'number' && Number.isFinite(data.chainId)) ? data.chainId : 1,
787
+ ...(data?.includeDetails !== undefined && { includeDetails: data.includeDetails })
788
+ };
789
+ } else {
790
+ // Default structure for unknown verifiers (should not reach here with validVerifiers check)
791
+ verificationData = data ? {
792
+ content,
793
+ owner: walletAddress,
794
+ ...data
795
+ } : {
796
+ content,
797
+ owner: walletAddress
798
+ };
799
+ }
800
+
801
+ const signedTimestamp = Date.now();
802
+ const verifierIds = [verifier];
803
+ const message = constructVerificationMessage({
804
+ walletAddress,
805
+ signedTimestamp,
806
+ data: verificationData,
807
+ verifierIds,
808
+ chainId: this._getHubChainId() // Protocol-managed chain
809
+ });
810
+
811
+ let signature;
812
+ try {
813
+ // UNIFIED SIGNING: Matches utils/core.ts personalSignUniversal exactly
814
+ const toHexUtf8 = (s) => {
815
+ try {
816
+ const enc = new TextEncoder();
817
+ const bytes = enc.encode(s);
818
+ let hex = '0x';
819
+ for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, '0');
820
+ return hex;
821
+ } catch {
822
+ let hex = '0x';
823
+ for (let i = 0; i < s.length; i++) hex += s.charCodeAt(i).toString(16).padStart(2, '0');
824
+ return hex;
825
+ }
826
+ };
827
+
828
+ // Detect Farcaster wallet - requires hex-encoded messages FIRST
829
+ const isFarcasterWallet = (() => {
830
+ if (typeof window === 'undefined') return false;
831
+ try {
832
+ const w = window;
833
+ const fc = w.farcaster;
834
+ if (!fc || !fc.context) return false;
835
+ const fcProvider = fc.provider || fc.walletProvider || (fc.context && fc.context.walletProvider);
836
+ if (fcProvider === provider) return true;
837
+ if (w.mini && w.mini.wallet === provider && fc && fc.context) return true;
838
+ if (w.ethereum === provider && fc && fc.context) return true;
839
+ } catch {
840
+ // ignore: optional Farcaster detection
841
+ }
842
+ return false;
843
+ })();
844
+
845
+ if (isFarcasterWallet) {
846
+ try {
847
+ const hexMsg = toHexUtf8(message);
848
+ signature = await provider.request({ method: 'personal_sign', params: [hexMsg, walletAddress] });
849
+ } catch (e) {
850
+ // Fall through
851
+ }
852
+ }
853
+
854
+ if (!signature) {
855
+ try {
856
+ signature = await provider.request({ method: 'personal_sign', params: [message, walletAddress] });
857
+ } catch (e) {
858
+ const msg = String(e && (e.message || e.reason) || e || '').toLowerCase();
859
+ const errCode = (e && (e.code || (e.error && e.error.code))) || null;
860
+ const needsHex = /byte|bytes|invalid byte sequence|encoding|non-hex/i.test(msg);
861
+
862
+ const methodUnsupported = (
863
+ /method.*not.*supported|unsupported|not implemented|method not found|unknown method|does not support/i.test(msg) ||
864
+ errCode === -32601 ||
865
+ errCode === 4200 ||
866
+ (msg.includes('personal_sign') && msg.includes('not')) ||
867
+ (msg.includes('request method') && msg.includes('not supported'))
868
+ );
869
+
870
+ if (methodUnsupported) {
871
+ this._log('personal_sign not supported; attempting eth_sign fallback');
872
+ try {
873
+ const enc = new TextEncoder();
874
+ const bytes = enc.encode(message);
875
+ const prefix = `\x19Ethereum Signed Message:\n${bytes.length}`;
876
+ const full = new Uint8Array(prefix.length + bytes.length);
877
+ for (let i = 0; i < prefix.length; i++) full[i] = prefix.charCodeAt(i);
878
+ full.set(bytes, prefix.length);
879
+ let payloadHex = '0x';
880
+ for (let i = 0; i < full.length; i++) payloadHex += full[i].toString(16).padStart(2, '0');
881
+ try {
882
+ if (typeof window !== 'undefined') window.__NEUS_ALLOW_ETH_SIGN__ = true;
883
+ } catch {
884
+ // ignore
885
+ }
886
+ signature = await provider.request({ method: 'eth_sign', params: [walletAddress, payloadHex], neusAllowEthSign: true });
887
+ try {
888
+ if (typeof window !== 'undefined') delete window.__NEUS_ALLOW_ETH_SIGN__;
889
+ } catch {
890
+ // ignore
891
+ }
892
+ } catch (fallbackErr) {
893
+ this._log('eth_sign fallback failed', { message: fallbackErr?.message || String(fallbackErr) });
894
+ try {
895
+ if (typeof window !== 'undefined') delete window.__NEUS_ALLOW_ETH_SIGN__;
896
+ } catch {
897
+ // ignore
898
+ }
899
+ throw e;
900
+ }
901
+ } else if (needsHex) {
902
+ this._log('Retrying personal_sign with hex-encoded message');
903
+ const hexMsg = toHexUtf8(message);
904
+ signature = await provider.request({ method: 'personal_sign', params: [hexMsg, walletAddress] });
905
+ } else {
906
+ throw e;
907
+ }
908
+ }
909
+ }
910
+ } catch (error) {
911
+ if (error.code === 4001) {
912
+ throw new ValidationError('User rejected the signature request. Signature is required to create proofs.');
913
+ }
914
+ throw new ValidationError(`Failed to sign verification message: ${error.message}`);
915
+ }
916
+
917
+ return this.verify({
918
+ verifierIds,
919
+ data: verificationData,
920
+ walletAddress,
921
+ signature,
922
+ signedTimestamp,
923
+ options
924
+ });
925
+ }
926
+
927
+ const {
928
+ verifierIds,
929
+ data,
930
+ walletAddress,
931
+ signature,
932
+ signedTimestamp,
933
+ chainId,
934
+ chain,
935
+ signatureMethod,
936
+ options = {}
937
+ } = params;
938
+
939
+ const resolvedChainId = chainId || (chain ? null : NEUS_CONSTANTS.HUB_CHAIN_ID);
940
+
941
+ // Normalize verifier IDs
942
+ const normalizeVerifierId = (id) => {
943
+ if (typeof id !== 'string') return id;
944
+ const match = id.match(/^(.*)@\d+$/);
945
+ return match ? match[1] : id;
946
+ };
947
+ const normalizedVerifierIds = Array.isArray(verifierIds) ? verifierIds.map(normalizeVerifierId) : [];
948
+
949
+ // Validate required parameters
950
+ if (!normalizedVerifierIds || normalizedVerifierIds.length === 0) {
951
+ throw new ValidationError('verifierIds array is required');
952
+ }
953
+ if (!data || typeof data !== 'object') {
954
+ throw new ValidationError('data object is required');
955
+ }
956
+ if (!walletAddress || typeof walletAddress !== 'string') {
957
+ throw new ValidationError('walletAddress is required');
958
+ }
959
+ if (!signature) {
960
+ throw new ValidationError('signature is required');
961
+ }
962
+ if (!signedTimestamp || typeof signedTimestamp !== 'number') {
963
+ throw new ValidationError('signedTimestamp is required');
964
+ }
965
+ if (resolvedChainId !== null && typeof resolvedChainId !== 'number') {
966
+ throw new ValidationError('chainId must be a number');
967
+ }
968
+
969
+ // Validate verifier data
970
+ for (const verifierId of normalizedVerifierIds) {
971
+ const validation = validateVerifierData(verifierId, data);
972
+ if (!validation.valid) {
973
+ throw new ValidationError(`Validation failed for verifier '${verifierId}': ${validation.error}`);
974
+ }
975
+ }
976
+
977
+ // Build options payload (public surface)
978
+ const optionsPayload = {
979
+ ...(options && typeof options === 'object' ? options : {}),
980
+ targetChains: Array.isArray(options?.targetChains) ? options.targetChains : [],
981
+ // Privacy and storage options (defaults)
982
+ privacyLevel: options?.privacyLevel || 'private',
983
+ publicDisplay: options?.publicDisplay || false,
984
+ storeOriginalContent: options?.storeOriginalContent || false
985
+ };
986
+ if (typeof options?.enableIpfs === 'boolean') optionsPayload.enableIpfs = options.enableIpfs;
987
+
988
+ const requestData = {
989
+ verifierIds: normalizedVerifierIds,
990
+ data,
991
+ walletAddress,
992
+ signature,
993
+ signedTimestamp,
994
+ ...(resolvedChainId !== null && { chainId: resolvedChainId }),
995
+ ...(chain && { chain }),
996
+ ...(signatureMethod && { signatureMethod }),
997
+ options: optionsPayload
998
+ };
999
+
1000
+ // SECURITY: Do not send proof signatures in Authorization headers.
1001
+ // Signatures belong in the request body only (they are not bearer tokens).
1002
+ const response = await this._makeRequest('POST', '/api/v1/verification', requestData);
1003
+
1004
+ if (!response.success) {
1005
+ throw new ApiError(`Verification failed: ${response.error?.message || 'Unknown error'}`, response.error);
1006
+ }
1007
+
1008
+ return this._formatResponse(response);
1009
+ }
1010
+
1011
+ // ============================================================================
1012
+ // STATUS AND UTILITY METHODS
1013
+ // ============================================================================
1014
+
1015
+ /**
1016
+ * Get verification status
1017
+ *
1018
+ * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1019
+ * @returns {Promise<Object>} Verification status and data
1020
+ *
1021
+ * @example
1022
+ * const result = await client.getStatus('0x...');
1023
+ * console.log('Status:', result.status);
1024
+ */
1025
+ async getStatus(proofId) {
1026
+ if (!proofId || typeof proofId !== 'string') {
1027
+ throw new ValidationError('proofId is required');
1028
+ }
1029
+ const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`);
1030
+
1031
+ if (!response.success) {
1032
+ throw new ApiError(`Failed to get status: ${response.error?.message || 'Unknown error'}`, response.error);
1033
+ }
1034
+
1035
+ return this._formatResponse(response);
1036
+ }
1037
+
1038
+ /**
1039
+ * Get private proof status with wallet signature
1040
+ *
1041
+ * @param {string} proofId - Proof ID (standard). `qHash` is a deprecated alias (same value).
1042
+ * @param {Object} wallet - Wallet provider (window.ethereum or ethers Wallet)
1043
+ * @returns {Promise<Object>} Private verification status and data
1044
+ *
1045
+ * @example
1046
+ * // Access private proof
1047
+ * const privateData = await client.getPrivateStatus(proofId, window.ethereum);
1048
+ */
1049
+ async getPrivateStatus(proofId, wallet = null) {
1050
+ if (!proofId || typeof proofId !== 'string') {
1051
+ throw new ValidationError('proofId is required');
1052
+ }
1053
+
1054
+ // Allow pre-signed universal owner auth (e.g. Solana) to avoid wallet-provider assumptions.
1055
+ const isPreSignedAuth = wallet &&
1056
+ typeof wallet === 'object' &&
1057
+ typeof wallet.walletAddress === 'string' &&
1058
+ typeof wallet.signature === 'string' &&
1059
+ typeof wallet.signedTimestamp === 'number';
1060
+ if (isPreSignedAuth) {
1061
+ const auth = wallet;
1062
+ const headers = {
1063
+ 'x-wallet-address': String(auth.walletAddress),
1064
+ 'x-signature': String(auth.signature),
1065
+ 'x-signed-timestamp': String(auth.signedTimestamp),
1066
+ ...(typeof auth.chain === 'string' && auth.chain.trim() ? { 'x-chain': auth.chain.trim() } : {}),
1067
+ ...(typeof auth.signatureMethod === 'string' && auth.signatureMethod.trim() ? { 'x-signature-method': auth.signatureMethod.trim() } : {})
1068
+ };
1069
+ const response = await this._makeRequest('GET', `/api/v1/verification/status/${proofId}`, null, headers);
1070
+ if (!response.success) {
1071
+ throw new ApiError(
1072
+ `Failed to access private proof: ${response.error?.message || 'Unauthorized'}`,
1073
+ response.error
1074
+ );
1075
+ }
1076
+ return this._formatResponse(response);
1077
+ }
1078
+
1079
+ const providerWallet = wallet || this._getDefaultBrowserWallet();
1080
+ const { signerWalletAddress: walletAddress, provider } = await this._resolveWalletSigner(providerWallet);
1081
+ if (!walletAddress || typeof walletAddress !== 'string') {
1082
+ throw new ConfigurationError('No wallet accounts available');
1083
+ }
1084
+ const signerIsEvm = EVM_ADDRESS_RE.test(this._normalizeIdentity(walletAddress));
1085
+ const chain = this._inferChainForAddress(walletAddress);
1086
+ const signatureMethod = signerIsEvm ? 'eip191' : 'ed25519';
1087
+
1088
+ const signedTimestamp = Date.now();
1089
+
1090
+ // IMPORTANT: This must match the server's Standard Signing String owner-access check.
1091
+ // Keep wire payload key `qHash` for backwards compatibility.
1092
+ const message = constructVerificationMessage({
1093
+ walletAddress,
1094
+ signedTimestamp,
1095
+ data: { action: 'access_private_proof', qHash: proofId },
1096
+ verifierIds: ['ownership-basic'],
1097
+ ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain })
1098
+ });
1099
+
1100
+ let signature;
1101
+ try {
1102
+ signature = await signMessage({
1103
+ provider,
1104
+ message,
1105
+ walletAddress,
1106
+ ...(signerIsEvm ? {} : { chain })
1107
+ });
1108
+ } catch (error) {
1109
+ if (error.code === 4001) {
1110
+ throw new ValidationError('User rejected signature request');
1111
+ }
1112
+ throw new ValidationError(`Failed to sign message: ${error.message}`);
1113
+ }
1114
+
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, {
1117
+ 'x-wallet-address': walletAddress,
1118
+ 'x-signature': signature,
1119
+ 'x-signed-timestamp': signedTimestamp.toString(),
1120
+ ...(signerIsEvm ? {} : { 'x-chain': chain, 'x-signature-method': signatureMethod })
1121
+ });
1122
+
1123
+ if (!response.success) {
1124
+ throw new ApiError(
1125
+ `Failed to access private proof: ${response.error?.message || 'Unauthorized'}`,
1126
+ response.error
1127
+ );
1128
+ }
1129
+
1130
+ return this._formatResponse(response);
1131
+ }
1132
+
1133
+ /**
1134
+ * Check API health
1135
+ *
1136
+ * @returns {Promise<boolean>} True if API is healthy
1137
+ */
1138
+ async isHealthy() {
1139
+ try {
1140
+ const response = await this._makeRequest('GET', '/api/v1/health');
1141
+ return response.success === true;
1142
+ } catch {
1143
+ return false;
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * List available verifiers
1149
+ *
1150
+ * @returns {Promise<string[]>} Array of verifier IDs
1151
+ */
1152
+ async getVerifiers() {
1153
+ const response = await this._makeRequest('GET', '/api/v1/verification/verifiers');
1154
+ if (!response.success) {
1155
+ throw new ApiError(`Failed to get verifiers: ${response.error?.message || 'Unknown error'}`, response.error);
1156
+ }
1157
+ return Array.isArray(response.data) ? response.data : [];
1158
+ }
1159
+
1160
+ /**
1161
+ * POLL PROOF STATUS - Wait for verification completion
1162
+ *
1163
+ * Polls the verification status until it reaches a terminal state (completed or failed).
1164
+ * 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).
1167
+ * @param {Object} [options] - Polling options
1168
+ * @param {number} [options.interval=5000] - Polling interval in ms
1169
+ * @param {number} [options.timeout=120000] - Total timeout in ms
1170
+ * @param {Function} [options.onProgress] - Progress callback function
1171
+ * @returns {Promise<Object>} Final verification status
1172
+ *
1173
+ * @example
1174
+ * const finalStatus = await client.pollProofStatus(proofId, {
1175
+ * interval: 3000,
1176
+ * timeout: 60000,
1177
+ * onProgress: (status) => {
1178
+ * console.log('Current status:', status.status);
1179
+ * if (status.crosschain) {
1180
+ * console.log(`Cross-chain: ${status.crosschain.finalized}/${status.crosschain.totalChains}`);
1181
+ * }
1182
+ * }
1183
+ * });
1184
+ */
1185
+ async pollProofStatus(proofId, options = {}) {
1186
+ const {
1187
+ interval = 5000,
1188
+ timeout = 120000,
1189
+ onProgress
1190
+ } = options;
1191
+
1192
+ if (!proofId || typeof proofId !== 'string') {
1193
+ throw new ValidationError('proofId is required');
1194
+ }
1195
+
1196
+ const startTime = Date.now();
1197
+ let consecutiveRateLimits = 0;
1198
+
1199
+ while (Date.now() - startTime < timeout) {
1200
+ try {
1201
+ const status = await this.getStatus(proofId);
1202
+ consecutiveRateLimits = 0;
1203
+
1204
+ // Call progress callback if provided
1205
+ if (onProgress && typeof onProgress === 'function') {
1206
+ onProgress(status.data || status);
1207
+ }
1208
+
1209
+ // Check for terminal states
1210
+ const currentStatus = status.data?.status || status.status;
1211
+ if (this._isTerminalStatus(currentStatus)) {
1212
+ this._log('Verification completed', { status: currentStatus, duration: Date.now() - startTime });
1213
+ return status;
1214
+ }
1215
+
1216
+ // Wait before next poll
1217
+ await new Promise(resolve => setTimeout(resolve, interval));
1218
+
1219
+ } catch (error) {
1220
+ this._log('Status poll error', error.message);
1221
+ // Continue polling unless it's a validation error
1222
+ if (error instanceof ValidationError) {
1223
+ throw error;
1224
+ }
1225
+
1226
+ let nextDelay = interval;
1227
+ if (error instanceof ApiError && Number(error.statusCode) === 429) {
1228
+ consecutiveRateLimits += 1;
1229
+ const exp = Math.min(6, consecutiveRateLimits); // cap growth
1230
+ const base = Math.max(500, Number(interval) || 0);
1231
+ const max = 30000; // 30s cap to keep UX responsive
1232
+ const backoff = Math.min(max, base * Math.pow(2, exp));
1233
+ const jitter = Math.floor(backoff * (0.5 + Math.random() * 0.5)); // 50-100%
1234
+ nextDelay = jitter;
1235
+ }
1236
+
1237
+ await new Promise(resolve => setTimeout(resolve, nextDelay));
1238
+ }
1239
+ }
1240
+
1241
+ throw new NetworkError(`Polling timeout after ${timeout}ms`, 'POLLING_TIMEOUT');
1242
+ }
1243
+
1244
+ /**
1245
+ * DETECT CHAIN ID - Get current wallet chain
1246
+ *
1247
+ * @returns {Promise<number>} Current chain ID
1248
+ */
1249
+ async detectChainId() {
1250
+ if (typeof window === 'undefined' || !window.ethereum) {
1251
+ throw new ConfigurationError('No Web3 wallet detected');
1252
+ }
1253
+
1254
+ try {
1255
+ const chainId = await window.ethereum.request({ method: 'eth_chainId' });
1256
+ return parseInt(chainId, 16);
1257
+ } catch (error) {
1258
+ throw new NetworkError(`Failed to detect chain ID: ${error.message}`);
1259
+ }
1260
+ }
1261
+
1262
+ /** Revoke your own proof (owner-signed) */
1263
+ async revokeOwnProof(proofId, wallet) {
1264
+ if (!proofId || typeof proofId !== 'string') {
1265
+ throw new ValidationError('proofId is required');
1266
+ }
1267
+ const providerWallet = wallet || this._getDefaultBrowserWallet();
1268
+ const { signerWalletAddress: address, provider } = await this._resolveWalletSigner(providerWallet);
1269
+ if (!address || typeof address !== 'string') {
1270
+ throw new ConfigurationError('No wallet accounts available');
1271
+ }
1272
+ const signerIsEvm = EVM_ADDRESS_RE.test(this._normalizeIdentity(address));
1273
+ const chain = this._inferChainForAddress(address);
1274
+ const signatureMethod = signerIsEvm ? 'eip191' : 'ed25519';
1275
+ const signedTimestamp = Date.now();
1276
+
1277
+ const message = constructVerificationMessage({
1278
+ walletAddress: address,
1279
+ signedTimestamp,
1280
+ // Keep wire payload key `qHash` for backwards compatibility.
1281
+ data: { action: 'revoke_proof', qHash: proofId },
1282
+ verifierIds: ['ownership-basic'],
1283
+ ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain })
1284
+ });
1285
+
1286
+ let signature;
1287
+ try {
1288
+ signature = await signMessage({
1289
+ provider,
1290
+ message,
1291
+ walletAddress: address,
1292
+ ...(signerIsEvm ? {} : { chain })
1293
+ });
1294
+ } catch (error) {
1295
+ if (error.code === 4001) {
1296
+ throw new ValidationError('User rejected revocation signature');
1297
+ }
1298
+ throw new ValidationError(`Failed to sign revocation: ${error.message}`);
1299
+ }
1300
+
1301
+ const res = await fetch(`${this.config.apiUrl}/api/v1/proofs/${proofId}/revoke-self`, {
1302
+ method: 'POST',
1303
+ // SECURITY: Do not put proof signatures into Authorization headers.
1304
+ headers: { 'Content-Type': 'application/json' },
1305
+ body: JSON.stringify({
1306
+ walletAddress: address,
1307
+ signature,
1308
+ signedTimestamp,
1309
+ ...(signerIsEvm ? {} : { chain, signatureMethod })
1310
+ })
1311
+ });
1312
+ const json = await res.json();
1313
+ if (!json.success) {
1314
+ throw new ApiError(json.error?.message || 'Failed to revoke proof', json.error);
1315
+ }
1316
+ return true;
1317
+ }
1318
+
1319
+ // ============================================================================
1320
+ // PROOFS & GATING METHODS
1321
+ // ============================================================================
1322
+
1323
+ /**
1324
+ * GET PROOFS BY WALLET - Fetch proofs for a wallet address
1325
+ *
1326
+ * @param {string} walletAddress - Wallet identity (EVM/Solana/DID)
1327
+ * @param {Object} [options] - Filter options
1328
+ * @param {number} [options.limit] - Max results (default: 50; higher limits require owner access)
1329
+ * @param {number} [options.offset] - Pagination offset (default: 0)
1330
+ * @returns {Promise<Object>} Proofs result
1331
+ *
1332
+ * @example
1333
+ * const result = await client.getProofsByWallet('0x...', {
1334
+ * limit: 50,
1335
+ * offset: 0
1336
+ * });
1337
+ */
1338
+ async getProofsByWallet(walletAddress, options = {}) {
1339
+ if (!walletAddress || typeof walletAddress !== 'string') {
1340
+ throw new ValidationError('walletAddress is required');
1341
+ }
1342
+
1343
+ const id = walletAddress.trim();
1344
+ const pathId = /^0x[a-fA-F0-9]{40}$/i.test(id) ? id.toLowerCase() : id;
1345
+
1346
+ const qs = [];
1347
+ if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1348
+ if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1349
+
1350
+ const query = qs.length ? `?${qs.join('&')}` : '';
1351
+ const response = await this._makeRequest(
1352
+ 'GET',
1353
+ `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`
1354
+ );
1355
+
1356
+ if (!response.success) {
1357
+ throw new ApiError(`Failed to get proofs: ${response.error?.message || 'Unknown error'}`, response.error);
1358
+ }
1359
+
1360
+ // Normalize response structure
1361
+ const proofs = response.data?.proofs || response.data || response.proofs || [];
1362
+ return {
1363
+ success: true,
1364
+ proofs: Array.isArray(proofs) ? proofs : [],
1365
+ totalCount: response.data?.totalCount ?? proofs.length,
1366
+ hasMore: Boolean(response.data?.hasMore),
1367
+ nextOffset: response.data?.nextOffset ?? null
1368
+ };
1369
+ }
1370
+
1371
+ /**
1372
+ * Get proofs by wallet (owner access)
1373
+ *
1374
+ * Signs an owner-access intent and requests private proofs via signature headers.
1375
+ *
1376
+ * @param {string} walletAddress - Wallet identity (EVM/Solana/DID)
1377
+ * @param {Object} [options]
1378
+ * @param {number} [options.limit] - Max results (server enforces caps)
1379
+ * @param {number} [options.offset] - Pagination offset
1380
+ * @param {Object} [wallet] - Optional injected wallet/provider (MetaMask/ethers Wallet)
1381
+ */
1382
+ async getPrivateProofsByWallet(walletAddress, options = {}, wallet = null) {
1383
+ if (!walletAddress || typeof walletAddress !== 'string') {
1384
+ throw new ValidationError('walletAddress is required');
1385
+ }
1386
+
1387
+ const id = walletAddress.trim();
1388
+ const pathId = /^0x[a-fA-F0-9]{40}$/i.test(id) ? id.toLowerCase() : id;
1389
+
1390
+ const requestedIdentity = this._normalizeIdentity(id);
1391
+
1392
+ // Auto-detect wallet if not provided
1393
+ if (!wallet) {
1394
+ const defaultWallet = this._getDefaultBrowserWallet();
1395
+ if (!defaultWallet) {
1396
+ throw new ConfigurationError('No wallet provider available');
1397
+ }
1398
+ wallet = defaultWallet;
1399
+ }
1400
+
1401
+ const { signerWalletAddress, provider } = await this._resolveWalletSigner(wallet);
1402
+
1403
+ if (!signerWalletAddress || typeof signerWalletAddress !== 'string') {
1404
+ throw new ConfigurationError('No wallet accounts available');
1405
+ }
1406
+
1407
+ const normalizedSigner = this._normalizeIdentity(signerWalletAddress);
1408
+ if (!normalizedSigner || normalizedSigner !== requestedIdentity) {
1409
+ throw new ValidationError('wallet must match walletAddress for private proof access');
1410
+ }
1411
+
1412
+ const signerIsEvm = EVM_ADDRESS_RE.test(normalizedSigner);
1413
+ const chain = this._inferChainForAddress(normalizedSigner, options?.chain);
1414
+ const signatureMethod = (typeof options?.signatureMethod === 'string' && options.signatureMethod.trim())
1415
+ ? options.signatureMethod.trim()
1416
+ : (signerIsEvm ? 'eip191' : 'ed25519');
1417
+
1418
+ const signedTimestamp = Date.now();
1419
+ const message = constructVerificationMessage({
1420
+ walletAddress: signerWalletAddress,
1421
+ signedTimestamp,
1422
+ data: { action: 'access_private_proofs_by_wallet', walletAddress: normalizedSigner },
1423
+ verifierIds: ['ownership-basic'],
1424
+ ...(signerIsEvm ? { chainId: this._getHubChainId() } : { chain })
1425
+ });
1426
+
1427
+ let signature;
1428
+ try {
1429
+ signature = await signMessage({
1430
+ provider,
1431
+ message,
1432
+ walletAddress: signerWalletAddress,
1433
+ ...(signerIsEvm ? {} : { chain })
1434
+ });
1435
+ } catch (error) {
1436
+ if (error.code === 4001) {
1437
+ throw new ValidationError('User rejected signature request');
1438
+ }
1439
+ throw new ValidationError(`Failed to sign message: ${error.message}`);
1440
+ }
1441
+
1442
+ const qs = [];
1443
+ if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1444
+ if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1445
+ const query = qs.length ? `?${qs.join('&')}` : '';
1446
+
1447
+ const response = await this._makeRequest('GET', `/api/v1/proofs/byWallet/${encodeURIComponent(pathId)}${query}`, null, {
1448
+ 'x-wallet-address': signerWalletAddress,
1449
+ 'x-signature': signature,
1450
+ 'x-signed-timestamp': signedTimestamp.toString(),
1451
+ ...(signerIsEvm ? {} : { 'x-chain': chain, 'x-signature-method': signatureMethod })
1452
+ });
1453
+
1454
+ if (!response.success) {
1455
+ throw new ApiError(`Failed to get proofs: ${response.error?.message || 'Unauthorized'}`, response.error);
1456
+ }
1457
+
1458
+ const proofs = response.data?.proofs || response.data || response.proofs || [];
1459
+ return {
1460
+ success: true,
1461
+ proofs: Array.isArray(proofs) ? proofs : [],
1462
+ totalCount: response.data?.totalCount ?? proofs.length,
1463
+ hasMore: Boolean(response.data?.hasMore),
1464
+ nextOffset: response.data?.nextOffset ?? null
1465
+ };
1466
+ }
1467
+
1468
+ /**
1469
+ * GATE CHECK (API) - Minimal eligibility check
1470
+ *
1471
+ * Calls the gate endpoint and returns a **minimal** yes/no response.
1472
+ * By default this checks **public + discoverable** proofs only.
1473
+ *
1474
+ * When `includePrivate=true`, this can perform owner-signed private checks
1475
+ * (no full proof payloads returned) by providing a wallet/provider.
1476
+ *
1477
+ * Prefer this over `checkGate()` for integrations that want the
1478
+ * smallest, most stable surface area (and do NOT need full proof payloads).
1479
+ *
1480
+ * @param {Object} params - Gate check query params
1481
+ * @param {string} params.address - Wallet identity to check (EVM/Solana/DID)
1482
+ * @param {Array<string>|string} [params.verifierIds] - Verifier IDs to match (array or comma-separated)
1483
+ * @param {boolean} [params.requireAll] - Require all verifierIds on a single proof
1484
+ * @param {number} [params.minCount] - Minimum number of matching proofs
1485
+ * @param {number} [params.sinceDays] - Optional time window in days
1486
+ * @param {number} [params.since] - Optional unix timestamp in ms (lower bound)
1487
+ * @param {number} [params.limit] - Max rows to scan (server may clamp)
1488
+ * @param {boolean} [params.includePrivate] - Include private proofs for owner-authenticated requests
1489
+ * @param {boolean} [params.includeQHashes] - Include matched qHashes in response (minimal IDs only)
1490
+ * @param {Object} [params.wallet] - Optional wallet/provider used to sign includePrivate owner checks
1491
+ * @returns {Promise<Object>} API response ({ success, data })
1492
+ */
1493
+ async gateCheck(params = {}) {
1494
+ const address = (params.address || '').toString();
1495
+ if (!validateUniversalAddress(address, params.chain)) {
1496
+ throw new ValidationError('Valid address is required');
1497
+ }
1498
+
1499
+ // Build query string safely (stringify all values; allow arrays for common fields)
1500
+ const qs = new URLSearchParams();
1501
+ qs.set('address', address);
1502
+
1503
+ const setIfPresent = (key, value) => {
1504
+ if (value === undefined || value === null) return;
1505
+ const str = typeof value === 'string' ? value : String(value);
1506
+ if (str.length === 0) return;
1507
+ qs.set(key, str);
1508
+ };
1509
+
1510
+ const setBoolIfPresent = (key, value) => {
1511
+ if (value === undefined || value === null) return;
1512
+ qs.set(key, value ? 'true' : 'false');
1513
+ };
1514
+
1515
+ const setCsvIfPresent = (key, value) => {
1516
+ if (value === undefined || value === null) return;
1517
+ if (Array.isArray(value)) {
1518
+ const items = value.map(v => String(v).trim()).filter(Boolean);
1519
+ if (items.length) qs.set(key, items.join(','));
1520
+ return;
1521
+ }
1522
+ setIfPresent(key, value);
1523
+ };
1524
+
1525
+ // Common filters
1526
+ setCsvIfPresent('verifierIds', params.verifierIds);
1527
+ setBoolIfPresent('requireAll', params.requireAll);
1528
+ setIfPresent('minCount', params.minCount);
1529
+ setIfPresent('sinceDays', params.sinceDays);
1530
+ setIfPresent('since', params.since);
1531
+ setIfPresent('limit', params.limit);
1532
+ setBoolIfPresent('includePrivate', params.includePrivate);
1533
+ setBoolIfPresent('includeQHashes', params.includeQHashes);
1534
+
1535
+ // Common match filters (optional)
1536
+ setIfPresent('referenceType', params.referenceType);
1537
+ setIfPresent('referenceId', params.referenceId);
1538
+ setIfPresent('tag', params.tag);
1539
+ setCsvIfPresent('tags', params.tags);
1540
+ setIfPresent('contentType', params.contentType);
1541
+ setIfPresent('content', params.content);
1542
+ setIfPresent('contentHash', params.contentHash);
1543
+
1544
+ // Asset/ownership filters
1545
+ setIfPresent('contractAddress', params.contractAddress);
1546
+ setIfPresent('tokenId', params.tokenId);
1547
+ setIfPresent('chainId', params.chainId);
1548
+ setIfPresent('domain', params.domain);
1549
+ setIfPresent('minBalance', params.minBalance);
1550
+
1551
+ // Wallet filters
1552
+ setIfPresent('provider', params.provider);
1553
+ setIfPresent('ownerAddress', params.ownerAddress);
1554
+ setIfPresent('riskLevel', params.riskLevel);
1555
+ setBoolIfPresent('sanctioned', params.sanctioned);
1556
+ setBoolIfPresent('poisoned', params.poisoned);
1557
+ setIfPresent('primaryWalletAddress', params.primaryWalletAddress);
1558
+ setIfPresent('secondaryWalletAddress', params.secondaryWalletAddress);
1559
+ setIfPresent('verificationMethod', params.verificationMethod);
1560
+
1561
+ let headersOverride = null;
1562
+ if (params.includePrivate === true) {
1563
+ const provided = params.privateAuth && typeof params.privateAuth === 'object' ? params.privateAuth : null;
1564
+ let auth = provided;
1565
+ if (!auth) {
1566
+ const walletCandidate = params.wallet || this._getDefaultBrowserWallet();
1567
+ if (walletCandidate) {
1568
+ auth = await this._buildPrivateGateAuth({
1569
+ address,
1570
+ wallet: walletCandidate,
1571
+ chain: params.chain,
1572
+ signatureMethod: params.signatureMethod
1573
+ });
1574
+ }
1575
+ }
1576
+ if (!auth) {
1577
+ // No signer context available - proceed as public/discoverable gate check.
1578
+ } else {
1579
+ const normalizedAuthWallet = this._normalizeIdentity(auth.walletAddress);
1580
+ const normalizedAddress = this._normalizeIdentity(address);
1581
+ if (!normalizedAuthWallet || normalizedAuthWallet !== normalizedAddress) {
1582
+ throw new ValidationError('privateAuth.walletAddress must match address when includePrivate=true');
1583
+ }
1584
+ const authChain = (typeof auth.chain === 'string' && auth.chain.includes(':')) ? auth.chain.trim() : null;
1585
+ const authSignatureMethod = (typeof auth.signatureMethod === 'string' && auth.signatureMethod.trim())
1586
+ ? auth.signatureMethod.trim()
1587
+ : null;
1588
+ headersOverride = {
1589
+ 'x-wallet-address': String(auth.walletAddress),
1590
+ 'x-signature': String(auth.signature),
1591
+ 'x-signed-timestamp': String(auth.signedTimestamp),
1592
+ ...(authChain ? { 'x-chain': authChain } : {}),
1593
+ ...(authSignatureMethod ? { 'x-signature-method': authSignatureMethod } : {})
1594
+ };
1595
+ }
1596
+ }
1597
+
1598
+ const response = await this._makeRequest('GET', `/api/v1/proofs/check?${qs.toString()}`, null, headersOverride);
1599
+ if (!response.success) {
1600
+ throw new ApiError(`Gate check failed: ${response.error?.message || 'Unknown error'}`, response.error);
1601
+ }
1602
+ return response;
1603
+ }
1604
+
1605
+ /**
1606
+ * CHECK GATE - Evaluate requirements against existing proofs
1607
+ *
1608
+ * Gate-first verification: checks if wallet has valid proofs satisfying requirements.
1609
+ * Returns which requirements are missing/expired.
1610
+ *
1611
+ * @param {Object} params - Gate check parameters
1612
+ * @param {string} params.walletAddress - Target wallet
1613
+ * @param {Array<Object>} params.requirements - Array of gate requirements
1614
+ * @param {string} params.requirements[].verifierId - Required verifier ID
1615
+ * @param {number} [params.requirements[].maxAgeMs] - Max proof age in ms (TTL)
1616
+ * @param {boolean} [params.requirements[].optional] - If true, not required for gate satisfaction
1617
+ * @param {number} [params.requirements[].minCount] - Minimum proofs needed (default: 1)
1618
+ * @param {Object} [params.requirements[].match] - Verifier data match criteria
1619
+ * Supports nested fields: 'reference.type', 'reference.id', 'content', 'contentHash', 'input.*', 'license.*'
1620
+ * Supports verifier-specific:
1621
+ * - NFT/Token: 'contractAddress', 'tokenId', 'chainId', 'ownerAddress', 'minBalance'
1622
+ * - DNS: 'domain', 'walletAddress'
1623
+ * - Wallet-link: 'primaryWalletAddress', 'secondaryWalletAddress', 'chain', 'signatureMethod'
1624
+ * - Contract-ownership: 'contractAddress', 'chainId', 'owner', 'verificationMethod'
1625
+ * Note: contentHash matching uses approximation in SDK; for exact SHA-256 matching, use backend API
1626
+ * @param {Array} [params.proofs] - Pre-fetched proofs (skip API call)
1627
+ * @returns {Promise<Object>} Gate result with satisfied, missing, existing
1628
+ *
1629
+ * @example
1630
+ * // Basic gate check
1631
+ * const result = await client.checkGate({
1632
+ * walletAddress: '0x...',
1633
+ * requirements: [
1634
+ * { verifierId: 'nft-ownership', match: { contractAddress: '0x...' } }
1635
+ * ]
1636
+ * });
1637
+ */
1638
+ async checkGate(params) {
1639
+ const { walletAddress, requirements, proofs: preloadedProofs } = params;
1640
+
1641
+ if (!validateUniversalAddress(walletAddress)) {
1642
+ throw new ValidationError('Valid walletAddress is required');
1643
+ }
1644
+ if (!Array.isArray(requirements) || requirements.length === 0) {
1645
+ throw new ValidationError('requirements array is required and must not be empty');
1646
+ }
1647
+
1648
+ // Use preloaded proofs or fetch from API
1649
+ let proofs = preloadedProofs;
1650
+ if (!proofs) {
1651
+ const result = await this.getProofsByWallet(walletAddress, { limit: 100 });
1652
+ proofs = result.proofs;
1653
+ }
1654
+
1655
+ const now = Date.now();
1656
+ const existing = {};
1657
+ const missing = [];
1658
+
1659
+ for (const req of requirements) {
1660
+ const { verifierId, maxAgeMs, optional = false, minCount = 1, match } = req;
1661
+
1662
+ // Find matching proofs for this verifier
1663
+ const matchingProofs = (proofs || []).filter(proof => {
1664
+ // Must have this verifier and be verified
1665
+ const verifiedVerifiers = proof.verifiedVerifiers || [];
1666
+ const verifier = verifiedVerifiers.find(
1667
+ v => v.verifierId === verifierId && v.verified === true
1668
+ );
1669
+ if (!verifier) return false;
1670
+
1671
+ // Check proof is not revoked
1672
+ if (proof.revokedAt) return false;
1673
+
1674
+ // Check TTL if specified
1675
+ if (maxAgeMs) {
1676
+ const proofTimestamp = proof.createdAt || proof.signedTimestamp || 0;
1677
+ const proofAge = now - proofTimestamp;
1678
+ if (proofAge > maxAgeMs) return false;
1679
+ }
1680
+
1681
+ // Check custom match criteria if specified
1682
+ if (match && typeof match === 'object') {
1683
+ const data = verifier.data || {};
1684
+ const input = data.input || {}; // NFT/token verifiers store fields in input
1685
+
1686
+ for (const [key, expected] of Object.entries(match)) {
1687
+ let actualValue = null;
1688
+
1689
+ // Handle nested field access
1690
+ if (key.includes('.')) {
1691
+ const parts = key.split('.');
1692
+ let current = data;
1693
+
1694
+ if (parts[0] === 'input' && input) {
1695
+ current = input;
1696
+ parts.shift();
1697
+ }
1698
+
1699
+ for (const part of parts) {
1700
+ if (current && typeof current === 'object' && part in current) {
1701
+ current = current[part];
1702
+ } else {
1703
+ current = undefined;
1704
+ break;
1705
+ }
1706
+ }
1707
+ actualValue = current;
1708
+ } else {
1709
+ actualValue = input[key] || data[key];
1710
+ }
1711
+
1712
+ if (key === 'content' && actualValue === undefined) {
1713
+ actualValue = data.reference?.id || data.content;
1714
+ }
1715
+
1716
+ // Special handling for verifier-specific fields
1717
+ if (actualValue === undefined) {
1718
+ if (key === 'contractAddress') {
1719
+ actualValue = input.contractAddress || data.contractAddress;
1720
+ } else if (key === 'tokenId') {
1721
+ actualValue = input.tokenId || data.tokenId;
1722
+ } else if (key === 'chainId') {
1723
+ actualValue = input.chainId || data.chainId;
1724
+ } else if (key === 'ownerAddress') {
1725
+ actualValue = input.ownerAddress || data.owner || data.walletAddress;
1726
+ } else if (key === 'minBalance') {
1727
+ actualValue = input.minBalance || data.onChainData?.requiredMinBalance || data.minBalance;
1728
+ } else if (key === 'primaryWalletAddress') {
1729
+ actualValue = data.primaryWalletAddress;
1730
+ } else if (key === 'secondaryWalletAddress') {
1731
+ actualValue = data.secondaryWalletAddress;
1732
+ } else if (key === 'verificationMethod') {
1733
+ actualValue = data.verificationMethod;
1734
+ } else if (key === 'domain') {
1735
+ actualValue = data.domain;
1736
+ }
1737
+ }
1738
+
1739
+ // Content hash check (approximation)
1740
+ if (key === 'contentHash' && actualValue === undefined && data.content) {
1741
+ try {
1742
+ // Simple hash approximation for non-crypto envs
1743
+ let hash = 0;
1744
+ const str = String(data.content);
1745
+ for (let i = 0; i < str.length; i++) {
1746
+ const char = str.charCodeAt(i);
1747
+ hash = ((hash << 5) - hash) + char;
1748
+ hash = hash & hash;
1749
+ }
1750
+ actualValue = '0x' + Math.abs(hash).toString(16).padStart(64, '0').substring(0, 66);
1751
+ } catch {
1752
+ actualValue = String(data.content);
1753
+ }
1754
+ }
1755
+
1756
+ let normalizedActual = actualValue;
1757
+ let normalizedExpected = expected;
1758
+
1759
+ if (actualValue === undefined || actualValue === null) {
1760
+ return false;
1761
+ }
1762
+
1763
+ if (key === 'chainId' || (key === 'tokenId' && (typeof actualValue === 'number' || !isNaN(Number(actualValue))))) {
1764
+ normalizedActual = Number(actualValue);
1765
+ normalizedExpected = Number(expected);
1766
+ if (isNaN(normalizedActual) || isNaN(normalizedExpected)) return false;
1767
+ }
1768
+ else if (typeof actualValue === 'string' && (actualValue.startsWith('0x') || actualValue.length > 20)) {
1769
+ normalizedActual = actualValue.toLowerCase();
1770
+ normalizedExpected = typeof expected === 'string' ? String(expected).toLowerCase() : expected;
1771
+ }
1772
+ else {
1773
+ normalizedActual = actualValue;
1774
+ normalizedExpected = expected;
1775
+ }
1776
+
1777
+ if (normalizedActual !== normalizedExpected) {
1778
+ return false;
1779
+ }
1780
+ }
1781
+ }
1782
+
1783
+ return true;
1784
+ });
1785
+
1786
+ if (matchingProofs.length >= minCount) {
1787
+ const sorted = matchingProofs.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
1788
+ existing[verifierId] = sorted[0];
1789
+ } else if (!optional) {
1790
+ missing.push(req);
1791
+ }
1792
+ }
1793
+
1794
+ return {
1795
+ satisfied: missing.length === 0,
1796
+ missing,
1797
+ existing,
1798
+ allProofs: proofs
1799
+ };
1800
+ }
1801
+
1802
+ // ============================================================================
1803
+ // PRIVATE UTILITY METHODS
1804
+ // ============================================================================
1805
+
1806
+ /**
1807
+ * Get connected wallet address
1808
+ * @private
1809
+ */
1810
+ async _getWalletAddress() {
1811
+ if (typeof window === 'undefined' || !window.ethereum) {
1812
+ throw new ConfigurationError('No Web3 wallet detected');
1813
+ }
1814
+
1815
+ const accounts = await window.ethereum.request({ method: 'eth_accounts' });
1816
+ if (!accounts || accounts.length === 0) {
1817
+ throw new ConfigurationError('No wallet accounts available');
1818
+ }
1819
+
1820
+ return accounts[0];
1821
+ }
1822
+
1823
+ /**
1824
+ * Make HTTP request to API
1825
+ * @private
1826
+ */
1827
+ async _makeRequest(method, endpoint, data = null, headersOverride = null) {
1828
+ const url = `${this.baseUrl}${endpoint}`;
1829
+
1830
+ const controller = new AbortController();
1831
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
1832
+
1833
+ const options = {
1834
+ method,
1835
+ headers: { ...this.defaultHeaders, ...(headersOverride || {}) },
1836
+ signal: controller.signal
1837
+ };
1838
+
1839
+ if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
1840
+ options.body = JSON.stringify(data);
1841
+ }
1842
+
1843
+ this._log(`${method} ${endpoint}`, data ? { requestBodyKeys: Object.keys(data) } : {});
1844
+
1845
+ try {
1846
+ const response = await fetch(url, options);
1847
+ clearTimeout(timeoutId);
1848
+
1849
+ let responseData;
1850
+ try {
1851
+ responseData = await response.json();
1852
+ } catch {
1853
+ responseData = { error: { message: 'Invalid JSON response' } };
1854
+ }
1855
+
1856
+ if (!response.ok) {
1857
+ throw ApiError.fromResponse(response, responseData);
1858
+ }
1859
+
1860
+ return responseData;
1861
+
1862
+ } catch (error) {
1863
+ clearTimeout(timeoutId);
1864
+
1865
+ if (error.name === 'AbortError') {
1866
+ throw new NetworkError(`Request timeout after ${this.config.timeout}ms`);
1867
+ }
1868
+
1869
+ if (error instanceof ApiError) {
1870
+ throw error;
1871
+ }
1872
+
1873
+ throw new NetworkError(`Network error: ${error.message}`);
1874
+ }
1875
+ }
1876
+
1877
+ /**
1878
+ * Format API response for consistent structure
1879
+ * @private
1880
+ */
1881
+ _formatResponse(response) {
1882
+ const proofId = response?.data?.proofId ||
1883
+ response?.proofId ||
1884
+ response?.data?.resource?.proofId ||
1885
+ response?.data?.qHash ||
1886
+ response?.qHash ||
1887
+ response?.data?.resource?.qHash ||
1888
+ response?.data?.id;
1889
+ const qHash = response?.data?.qHash ||
1890
+ response?.qHash ||
1891
+ response?.data?.resource?.qHash ||
1892
+ proofId ||
1893
+ response?.data?.id;
1894
+ const finalProofId = proofId || qHash || null;
1895
+ const finalQHash = qHash || proofId || finalProofId;
1896
+
1897
+ const status = response?.data?.status ||
1898
+ response?.status ||
1899
+ response?.data?.resource?.status ||
1900
+ (response?.success ? 'completed' : 'unknown');
1901
+
1902
+ return {
1903
+ success: response.success,
1904
+ proofId: finalProofId,
1905
+ qHash: finalQHash,
1906
+ status,
1907
+ data: response.data,
1908
+ message: response.message,
1909
+ timestamp: Date.now(),
1910
+ statusUrl: finalProofId ? `${this.baseUrl}/api/v1/verification/status/${finalProofId}` : null
1911
+ };
1912
+ }
1913
+
1914
+ /**
1915
+ * Check if status is terminal (completed or failed)
1916
+ * @private
1917
+ */
1918
+ _isTerminalStatus(status) {
1919
+ const terminalStates = [
1920
+ 'verified',
1921
+ 'verified_crosschain_propagated',
1922
+ 'completed_all_successful',
1923
+ 'failed',
1924
+ 'error',
1925
+ 'rejected',
1926
+ 'cancelled'
1927
+ ];
1928
+ return typeof status === 'string' && terminalStates.some(state => status.includes(state));
1929
+ }
1930
+
1931
+ /** SDK logging (opt-in via config.enableLogging) */
1932
+ _log(message, data = {}) {
1933
+ if (this.config.enableLogging) {
1934
+ try {
1935
+ const prefix = '[neus/sdk]';
1936
+ if (data && Object.keys(data).length > 0) {
1937
+ // eslint-disable-next-line no-console
1938
+ console.log(prefix, message, data);
1939
+ } else {
1940
+ // eslint-disable-next-line no-console
1941
+ console.log(prefix, message);
1942
+ }
1943
+ } catch {
1944
+ // ignore logging failures
1945
+ }
1946
+ }
1947
+ }
1948
+ }
1949
+
1950
+ // Export the constructVerificationMessage function for advanced use
1951
+ export { constructVerificationMessage };