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