@neus/sdk 1.0.0 → 1.0.1

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