@neus/sdk 1.0.4 → 1.0.6

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