@neus/sdk 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -140
- package/cjs/client.cjs +634 -223
- package/cjs/errors.cjs +1 -0
- package/cjs/gates.cjs +1 -0
- package/cjs/index.cjs +840 -236
- package/cjs/utils.cjs +408 -12
- package/client.js +502 -244
- package/index.js +75 -64
- package/neus-logo.svg +3 -0
- package/package.json +9 -5
- package/types.d.ts +379 -82
- package/utils.js +443 -14
- package/widgets/README.md +64 -53
- package/widgets/index.js +9 -9
- package/widgets/verify-gate/dist/ProofBadge.js +51 -38
- package/widgets/verify-gate/dist/VerifyGate.js +284 -59
- package/widgets/verify-gate/index.js +13 -13
package/utils.js
CHANGED
|
@@ -3,7 +3,58 @@
|
|
|
3
3
|
* Core utility functions for proof creation and verification
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { SDKError } from './errors.js';
|
|
6
|
+
import { SDKError, ApiError, ValidationError } from './errors.js';
|
|
7
|
+
|
|
8
|
+
const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
9
|
+
|
|
10
|
+
function encodeBase58Bytes(input) {
|
|
11
|
+
let source;
|
|
12
|
+
if (input instanceof Uint8Array) {
|
|
13
|
+
source = input;
|
|
14
|
+
} else if (input instanceof ArrayBuffer) {
|
|
15
|
+
source = new Uint8Array(input);
|
|
16
|
+
} else if (ArrayBuffer.isView(input)) {
|
|
17
|
+
source = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
18
|
+
} else if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(input)) {
|
|
19
|
+
source = new Uint8Array(input);
|
|
20
|
+
} else {
|
|
21
|
+
throw new SDKError('Unsupported non-EVM signature byte format', 'INVALID_SIGNATURE_FORMAT');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (source.length === 0) return '';
|
|
25
|
+
|
|
26
|
+
let zeroes = 0;
|
|
27
|
+
while (zeroes < source.length && source[zeroes] === 0) {
|
|
28
|
+
zeroes++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const iFactor = Math.log(256) / Math.log(58);
|
|
32
|
+
const size = (((source.length - zeroes) * iFactor) + 1) >>> 0;
|
|
33
|
+
const b58 = new Uint8Array(size);
|
|
34
|
+
|
|
35
|
+
let length = 0;
|
|
36
|
+
for (let i = zeroes; i < source.length; i++) {
|
|
37
|
+
let carry = source[i];
|
|
38
|
+
let j = 0;
|
|
39
|
+
for (let k = size - 1; (carry !== 0 || j < length) && k >= 0; k--, j++) {
|
|
40
|
+
carry += 256 * b58[k];
|
|
41
|
+
b58[k] = carry % 58;
|
|
42
|
+
carry = (carry / 58) | 0;
|
|
43
|
+
}
|
|
44
|
+
length = j;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let it = size - length;
|
|
48
|
+
while (it < size && b58[it] === 0) {
|
|
49
|
+
it++;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let out = BASE58_ALPHABET[0].repeat(zeroes);
|
|
53
|
+
for (; it < size; it++) {
|
|
54
|
+
out += BASE58_ALPHABET[b58[it]];
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
7
58
|
|
|
8
59
|
/**
|
|
9
60
|
* Deterministic JSON stringification for consistent serialization
|
|
@@ -58,11 +109,11 @@ export function constructVerificationMessage({ walletAddress, signedTimestamp, d
|
|
|
58
109
|
throw new SDKError('verifierIds is required and must be a non-empty array', 'INVALID_VERIFIER_IDS');
|
|
59
110
|
}
|
|
60
111
|
|
|
61
|
-
// Chain context: prefer explicit `chain` when provided (
|
|
112
|
+
// Chain context: prefer explicit `chain` when provided (e.g. "solana:mainnet"),
|
|
62
113
|
// otherwise use numeric `chainId` (EVM-first public surface).
|
|
63
114
|
const chainContext = (typeof chain === 'string' && chain.length > 0) ? chain : chainId;
|
|
64
115
|
if (!chainContext) {
|
|
65
|
-
throw new SDKError('chainId is required (or provide chain for
|
|
116
|
+
throw new SDKError('chainId is required (or provide chain for universal mode)', 'INVALID_CHAIN_CONTEXT');
|
|
66
117
|
}
|
|
67
118
|
if (chainContext === chainId && typeof chainId !== 'number') {
|
|
68
119
|
throw new SDKError('chainId must be a number when provided', 'INVALID_CHAIN_ID');
|
|
@@ -108,6 +159,41 @@ export function validateWalletAddress(address) {
|
|
|
108
159
|
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
109
160
|
}
|
|
110
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Validate universal wallet address format.
|
|
164
|
+
* Uses chain namespace when provided; otherwise applies conservative multi-chain checks.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} address - Address to validate
|
|
167
|
+
* @param {string} [chain] - Optional CAIP-2 chain reference (namespace:reference)
|
|
168
|
+
* @returns {boolean} True if valid universal wallet address
|
|
169
|
+
*/
|
|
170
|
+
export function validateUniversalAddress(address, chain) {
|
|
171
|
+
if (!address || typeof address !== 'string') return false;
|
|
172
|
+
const value = address.trim();
|
|
173
|
+
if (!value) return false;
|
|
174
|
+
|
|
175
|
+
const chainRef = typeof chain === 'string' ? chain.trim().toLowerCase() : '';
|
|
176
|
+
const namespace = chainRef.includes(':') ? chainRef.split(':')[0] : '';
|
|
177
|
+
|
|
178
|
+
if (validateWalletAddress(value)) return true;
|
|
179
|
+
if (namespace === 'eip155') return false;
|
|
180
|
+
|
|
181
|
+
if (namespace === 'solana') {
|
|
182
|
+
return /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (namespace === 'bip122') {
|
|
186
|
+
return /^(bc1|tb1|bcrt1)[a-z0-9]{11,87}$/.test(value.toLowerCase()) || /^[13mn2][a-km-zA-HJ-NP-Z1-9]{25,62}$/.test(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (namespace === 'near') {
|
|
190
|
+
return /^[a-z0-9._-]{2,64}$/.test(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generic fallback for universal-address style identifiers.
|
|
194
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{1,127}$/.test(value);
|
|
195
|
+
}
|
|
196
|
+
|
|
111
197
|
/**
|
|
112
198
|
* Validate timestamp freshness
|
|
113
199
|
*
|
|
@@ -151,7 +237,7 @@ export function createVerificationData(content, owner, reference = null) {
|
|
|
151
237
|
|
|
152
238
|
return {
|
|
153
239
|
content,
|
|
154
|
-
owner: owner.toLowerCase(),
|
|
240
|
+
owner: validateWalletAddress(owner) ? owner.toLowerCase() : owner,
|
|
155
241
|
reference: reference || {
|
|
156
242
|
// Must be a valid backend enum value; 'content' is not supported.
|
|
157
243
|
type: 'other',
|
|
@@ -179,11 +265,328 @@ export function deriveDid(address, chainIdOrChain) {
|
|
|
179
265
|
} else {
|
|
180
266
|
if (typeof chainContext !== 'number') {
|
|
181
267
|
throw new SDKError('deriveDid: chainId (number) or chain (namespace:reference string) is required', 'INVALID_ARGUMENT');
|
|
182
|
-
|
|
268
|
+
}
|
|
183
269
|
return `did:pkh:eip155:${chainContext}:${address.toLowerCase()}`;
|
|
184
270
|
}
|
|
185
271
|
}
|
|
186
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Resolve DID from wallet identity via API endpoint
|
|
275
|
+
*
|
|
276
|
+
* @param {Object} params - DID resolution parameters
|
|
277
|
+
* @param {string} params.walletAddress - Wallet address to resolve
|
|
278
|
+
* @param {number} [params.chainId] - EVM chain ID
|
|
279
|
+
* @param {string} [params.chain] - Universal chain context (namespace:reference)
|
|
280
|
+
* @param {Object} [options] - Request options
|
|
281
|
+
* @param {string} [options.endpoint='/api/v1/profile/did/resolve'] - DID resolve endpoint
|
|
282
|
+
* @param {string} [options.apiUrl] - Absolute API base URL for non-relative endpoints
|
|
283
|
+
* @param {RequestCredentials} [options.credentials] - Fetch credentials mode
|
|
284
|
+
* @param {Record<string, string>} [options.headers] - Extra request headers
|
|
285
|
+
* @returns {Promise<{did: string, data: any, raw: any}>}
|
|
286
|
+
*/
|
|
287
|
+
export async function resolveDID(params, options = {}) {
|
|
288
|
+
const endpointPath = options.endpoint || '/api/v1/profile/did/resolve';
|
|
289
|
+
const apiUrl = typeof options.apiUrl === 'string' ? options.apiUrl.trim() : '';
|
|
290
|
+
|
|
291
|
+
const resolveEndpoint = (path) => {
|
|
292
|
+
if (!path || typeof path !== 'string') return null;
|
|
293
|
+
const trimmedPath = path.trim();
|
|
294
|
+
if (!trimmedPath) return null;
|
|
295
|
+
if (/^https?:\/\//i.test(trimmedPath)) return trimmedPath;
|
|
296
|
+
if (trimmedPath.startsWith('/')) {
|
|
297
|
+
if (!apiUrl) return trimmedPath;
|
|
298
|
+
try {
|
|
299
|
+
return new URL(trimmedPath, apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`).toString();
|
|
300
|
+
} catch {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
const base = apiUrl || NEUS_CONSTANTS.API_BASE_URL;
|
|
305
|
+
if (!base || typeof base !== 'string') return null;
|
|
306
|
+
try {
|
|
307
|
+
return new URL(trimmedPath, base.endsWith('/') ? base : `${base}/`).toString();
|
|
308
|
+
} catch {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const endpoint = resolveEndpoint(endpointPath);
|
|
314
|
+
if (!endpoint) {
|
|
315
|
+
throw new SDKError('resolveDID requires a valid endpoint', 'INVALID_ENDPOINT');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const payload = {
|
|
319
|
+
walletAddress: params?.walletAddress,
|
|
320
|
+
chainId: params?.chainId,
|
|
321
|
+
chain: params?.chain
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const isRelative = endpoint.startsWith('/') || !/^https?:\/\//i.test(endpoint);
|
|
325
|
+
const credentialsMode = options.credentials !== undefined
|
|
326
|
+
? options.credentials
|
|
327
|
+
: (isRelative ? 'same-origin' : 'omit');
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const response = await fetch(endpoint, {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: {
|
|
333
|
+
'Content-Type': 'application/json',
|
|
334
|
+
Accept: 'application/json',
|
|
335
|
+
...(options.headers || {})
|
|
336
|
+
},
|
|
337
|
+
body: JSON.stringify(payload),
|
|
338
|
+
credentials: credentialsMode
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const json = await response.json().catch(() => null);
|
|
342
|
+
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const msg = json?.error?.message || json?.error || json?.message || 'DID resolution failed';
|
|
345
|
+
throw new SDKError(msg, 'DID_RESOLVE_FAILED', json);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const did = json?.data?.did || json?.did;
|
|
349
|
+
if (!did || typeof did !== 'string') {
|
|
350
|
+
throw new SDKError('DID resolution missing DID', 'DID_RESOLVE_MISSING', json);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return { did, data: json?.data || null, raw: json };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (error instanceof SDKError) throw error;
|
|
356
|
+
throw new SDKError(`DID resolution failed: ${error?.message || error}`, 'DID_RESOLVE_FAILED');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Standardize verification request via backend signer-string endpoint
|
|
362
|
+
*
|
|
363
|
+
* @param {Object} params - Verification request payload
|
|
364
|
+
* @param {Object} [options] - Request options
|
|
365
|
+
* @param {string} [options.endpoint='/api/v1/verification/standardize'] - Standardize endpoint
|
|
366
|
+
* @param {string} [options.apiUrl] - Absolute API base URL for non-relative endpoints
|
|
367
|
+
* @param {RequestCredentials} [options.credentials] - Fetch credentials mode
|
|
368
|
+
* @param {Record<string, string>} [options.headers] - Extra request headers
|
|
369
|
+
* @returns {Promise<any>}
|
|
370
|
+
*/
|
|
371
|
+
export async function standardizeVerificationRequest(params, options = {}) {
|
|
372
|
+
const endpointPath = options.endpoint || '/api/v1/verification/standardize';
|
|
373
|
+
const apiUrl = typeof options.apiUrl === 'string' ? options.apiUrl.trim() : '';
|
|
374
|
+
|
|
375
|
+
const resolveEndpoint = (path) => {
|
|
376
|
+
if (!path || typeof path !== 'string') return null;
|
|
377
|
+
const trimmedPath = path.trim();
|
|
378
|
+
if (!trimmedPath) return null;
|
|
379
|
+
if (/^https?:\/\//i.test(trimmedPath)) return trimmedPath;
|
|
380
|
+
if (trimmedPath.startsWith('/')) {
|
|
381
|
+
if (!apiUrl) return trimmedPath;
|
|
382
|
+
try {
|
|
383
|
+
return new URL(trimmedPath, apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`).toString();
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const base = apiUrl || NEUS_CONSTANTS.API_BASE_URL;
|
|
389
|
+
if (!base || typeof base !== 'string') return null;
|
|
390
|
+
try {
|
|
391
|
+
return new URL(trimmedPath, base.endsWith('/') ? base : `${base}/`).toString();
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const endpoint = resolveEndpoint(endpointPath);
|
|
398
|
+
if (!endpoint) {
|
|
399
|
+
throw new SDKError('standardizeVerificationRequest requires a valid endpoint', 'INVALID_ENDPOINT');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const isRelative = endpoint.startsWith('/') || !/^https?:\/\//i.test(endpoint);
|
|
403
|
+
const credentialsMode = options.credentials !== undefined
|
|
404
|
+
? options.credentials
|
|
405
|
+
: (isRelative ? 'same-origin' : 'omit');
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const response = await fetch(endpoint, {
|
|
409
|
+
method: 'POST',
|
|
410
|
+
headers: {
|
|
411
|
+
'Content-Type': 'application/json',
|
|
412
|
+
Accept: 'application/json',
|
|
413
|
+
...(options.headers || {})
|
|
414
|
+
},
|
|
415
|
+
body: JSON.stringify(params || {}),
|
|
416
|
+
credentials: credentialsMode
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const json = await response.json().catch(() => null);
|
|
420
|
+
if (!response.ok) {
|
|
421
|
+
const msg = json?.error?.message || json?.error || json?.message || 'Standardize request failed';
|
|
422
|
+
throw new SDKError(msg, 'STANDARDIZE_FAILED', json);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return json?.data || json;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (error instanceof SDKError) throw error;
|
|
428
|
+
throw new SDKError(`Standardize request failed: ${error?.message || error}`, 'STANDARDIZE_FAILED');
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Resolve default ZK Passport configuration values.
|
|
434
|
+
* Kept as an SDK utility to preserve existing app integrations.
|
|
435
|
+
*
|
|
436
|
+
* @param {Object} [overrides] - Caller-provided config overrides
|
|
437
|
+
* @returns {Object}
|
|
438
|
+
*/
|
|
439
|
+
export function resolveZkPassportConfig(overrides = {}) {
|
|
440
|
+
const defaults = {
|
|
441
|
+
provider: 'zkpassport',
|
|
442
|
+
scope: 'basic_kyc',
|
|
443
|
+
checkSanctions: true,
|
|
444
|
+
requireFaceMatch: true,
|
|
445
|
+
faceMatchMode: 'strict'
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
...defaults,
|
|
450
|
+
...(overrides && typeof overrides === 'object' ? overrides : {})
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Convert a UTF-8 string to `0x`-prefixed hex.
|
|
456
|
+
*
|
|
457
|
+
* @param {string} value
|
|
458
|
+
* @returns {string}
|
|
459
|
+
*/
|
|
460
|
+
export function toHexUtf8(value) {
|
|
461
|
+
const input = typeof value === 'string' ? value : String(value || '');
|
|
462
|
+
const bytes = new TextEncoder().encode(input);
|
|
463
|
+
return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Sign an arbitrary message with the provided wallet/provider.
|
|
468
|
+
* Supports EIP-1193 wallets and signer-like objects.
|
|
469
|
+
*
|
|
470
|
+
* @param {Object} params
|
|
471
|
+
* @param {Object} [params.provider] - Wallet provider/signer
|
|
472
|
+
* @param {string} params.message - Message to sign
|
|
473
|
+
* @param {string} [params.walletAddress] - Explicit signer address (recommended)
|
|
474
|
+
* @param {string} [params.chain] - Chain context (`namespace:reference`)
|
|
475
|
+
* @returns {Promise<string>}
|
|
476
|
+
*/
|
|
477
|
+
export async function signMessage({ provider, message, walletAddress, chain } = {}) {
|
|
478
|
+
const msg = typeof message === 'string' ? message : String(message || '');
|
|
479
|
+
if (!msg) {
|
|
480
|
+
throw new SDKError('signMessage: message is required', 'INVALID_ARGUMENT');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const resolvedProvider = provider || (
|
|
484
|
+
typeof window !== 'undefined' && window?.ethereum ? window.ethereum : null
|
|
485
|
+
);
|
|
486
|
+
if (!resolvedProvider) {
|
|
487
|
+
throw new SDKError('signMessage: provider is required', 'SIGNER_UNAVAILABLE');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const chainStr = typeof chain === 'string' && chain.trim().length > 0 ? chain.trim() : 'eip155';
|
|
491
|
+
const namespace = chainStr.includes(':') ? chainStr.split(':')[0] || 'eip155' : 'eip155';
|
|
492
|
+
|
|
493
|
+
const resolveAddress = async () => {
|
|
494
|
+
if (typeof walletAddress === 'string' && walletAddress.trim().length > 0) return walletAddress;
|
|
495
|
+
if (namespace === 'solana') {
|
|
496
|
+
if (resolvedProvider?.publicKey && typeof resolvedProvider.publicKey.toBase58 === 'function') {
|
|
497
|
+
const pk = resolvedProvider.publicKey.toBase58();
|
|
498
|
+
if (typeof pk === 'string' && pk) return pk;
|
|
499
|
+
}
|
|
500
|
+
if (typeof resolvedProvider.getAddress === 'function') {
|
|
501
|
+
const addr = await resolvedProvider.getAddress().catch(() => null);
|
|
502
|
+
if (typeof addr === 'string' && addr) return addr;
|
|
503
|
+
}
|
|
504
|
+
if (typeof resolvedProvider.address === 'string' && resolvedProvider.address) return resolvedProvider.address;
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
if (typeof resolvedProvider.address === 'string' && resolvedProvider.address) return resolvedProvider.address;
|
|
508
|
+
if (typeof resolvedProvider.getAddress === 'function') return await resolvedProvider.getAddress();
|
|
509
|
+
if (typeof resolvedProvider.request === 'function') {
|
|
510
|
+
let accounts = await resolvedProvider.request({ method: 'eth_accounts' }).catch(() => []);
|
|
511
|
+
if (!Array.isArray(accounts) || accounts.length === 0) {
|
|
512
|
+
accounts = await resolvedProvider.request({ method: 'eth_requestAccounts' }).catch(() => []);
|
|
513
|
+
}
|
|
514
|
+
if (Array.isArray(accounts) && accounts[0]) return accounts[0];
|
|
515
|
+
}
|
|
516
|
+
return null;
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
if (namespace !== 'eip155') {
|
|
520
|
+
if (typeof resolvedProvider.signMessage === 'function') {
|
|
521
|
+
const encoded = typeof msg === 'string' ? new TextEncoder().encode(msg) : msg;
|
|
522
|
+
const result = await resolvedProvider.signMessage(encoded);
|
|
523
|
+
if (typeof result === 'string' && result) return result;
|
|
524
|
+
if (result instanceof Uint8Array) return encodeBase58Bytes(result);
|
|
525
|
+
if (result instanceof ArrayBuffer) return encodeBase58Bytes(new Uint8Array(result));
|
|
526
|
+
if (ArrayBuffer.isView(result)) return encodeBase58Bytes(result);
|
|
527
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.isBuffer === 'function' && Buffer.isBuffer(result)) return encodeBase58Bytes(result);
|
|
528
|
+
}
|
|
529
|
+
throw new SDKError('Non-EVM signing requires provider.signMessage', 'SIGNER_UNAVAILABLE');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const address = await resolveAddress();
|
|
533
|
+
|
|
534
|
+
if (typeof resolvedProvider.request === 'function' && address) {
|
|
535
|
+
let firstPersonalSignError = null;
|
|
536
|
+
try {
|
|
537
|
+
const sig = await resolvedProvider.request({ method: 'personal_sign', params: [msg, address] });
|
|
538
|
+
if (typeof sig === 'string' && sig) return sig;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
firstPersonalSignError = error;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
let secondPersonalSignError = null;
|
|
544
|
+
try {
|
|
545
|
+
const sig = await resolvedProvider.request({ method: 'personal_sign', params: [address, msg] });
|
|
546
|
+
if (typeof sig === 'string' && sig) return sig;
|
|
547
|
+
} catch (error) {
|
|
548
|
+
secondPersonalSignError = error;
|
|
549
|
+
const signatureErrorMessage = String(
|
|
550
|
+
error?.message ||
|
|
551
|
+
error?.reason ||
|
|
552
|
+
firstPersonalSignError?.message ||
|
|
553
|
+
firstPersonalSignError?.reason ||
|
|
554
|
+
''
|
|
555
|
+
).toLowerCase();
|
|
556
|
+
const needsHex = /byte|bytes|invalid byte sequence|encoding|non-hex/i.test(signatureErrorMessage);
|
|
557
|
+
if (needsHex) {
|
|
558
|
+
try {
|
|
559
|
+
const hexMsg = toHexUtf8(msg);
|
|
560
|
+
const sig = await resolvedProvider.request({ method: 'personal_sign', params: [hexMsg, address] });
|
|
561
|
+
if (typeof sig === 'string' && sig) return sig;
|
|
562
|
+
} catch {
|
|
563
|
+
// Continue to additional fallbacks.
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
const sig = await resolvedProvider.request({ method: 'eth_sign', params: [address, msg] });
|
|
570
|
+
if (typeof sig === 'string' && sig) return sig;
|
|
571
|
+
} catch { /* try next method */ }
|
|
572
|
+
|
|
573
|
+
if (secondPersonalSignError || firstPersonalSignError) {
|
|
574
|
+
const lastError = secondPersonalSignError || firstPersonalSignError;
|
|
575
|
+
const isUserRejection = [4001, 'ACTION_REJECTED'].includes(lastError?.code);
|
|
576
|
+
if (isUserRejection) {
|
|
577
|
+
throw lastError;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (typeof resolvedProvider.signMessage === 'function') {
|
|
583
|
+
const result = await resolvedProvider.signMessage(msg);
|
|
584
|
+
if (typeof result === 'string' && result) return result;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
throw new SDKError('Unable to sign message with provided wallet/provider', 'SIGNER_UNAVAILABLE');
|
|
588
|
+
}
|
|
589
|
+
|
|
187
590
|
/**
|
|
188
591
|
* Determine if a verification status is terminal (completed or failed)
|
|
189
592
|
* @param {string} status - The verification status
|
|
@@ -442,10 +845,28 @@ export class StatusPoller {
|
|
|
442
845
|
setTimeout(pollAttempt, this.currentInterval);
|
|
443
846
|
|
|
444
847
|
} catch (error) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
848
|
+
if (error instanceof ValidationError) {
|
|
849
|
+
reject(error);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if ((error instanceof ApiError && error.statusCode === 429) || error?.isRetryable === true) {
|
|
854
|
+
if (this.options.exponentialBackoff) {
|
|
855
|
+
const next = Math.min(this.currentInterval * 2, this.options.maxInterval);
|
|
856
|
+
const jitter = next * (0.5 + Math.random() * 0.5);
|
|
857
|
+
this.currentInterval = Math.max(250, Math.floor(jitter));
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (this.attempt >= this.options.maxAttempts) {
|
|
861
|
+
reject(new SDKError('Verification polling timeout', 'POLLING_TIMEOUT'));
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
setTimeout(pollAttempt, this.currentInterval);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
reject(new SDKError(`Polling failed: ${error.message}`, 'POLLING_ERROR'));
|
|
449
870
|
}
|
|
450
871
|
};
|
|
451
872
|
|
|
@@ -554,19 +975,27 @@ export function validateVerifierPayload(verifierId, data) {
|
|
|
554
975
|
if (!(key in data)) result.missing.push(key);
|
|
555
976
|
});
|
|
556
977
|
if (!('ownerAddress' in data)) {
|
|
557
|
-
result.warnings.push('ownerAddress omitted (
|
|
978
|
+
result.warnings.push('ownerAddress omitted (defaults to the signed walletAddress)');
|
|
558
979
|
}
|
|
559
980
|
} else if (id === 'token-holding') {
|
|
560
981
|
['contractAddress', 'minBalance', 'chainId'].forEach((key) => {
|
|
561
982
|
if (!(key in data)) result.missing.push(key);
|
|
562
983
|
});
|
|
563
984
|
if (!('ownerAddress' in data)) {
|
|
564
|
-
result.warnings.push('ownerAddress omitted (
|
|
985
|
+
result.warnings.push('ownerAddress omitted (defaults to the signed walletAddress)');
|
|
565
986
|
}
|
|
566
987
|
} else if (id === 'ownership-basic') {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
988
|
+
// ownership-basic requires an owner, and needs at least one binding:
|
|
989
|
+
// - content (inline), or
|
|
990
|
+
// - contentHash (recommended for large content), or
|
|
991
|
+
// - reference.id (reference-only proofs)
|
|
992
|
+
if (!('owner' in data)) result.missing.push('owner');
|
|
993
|
+
const hasContent = typeof data.content === 'string' && data.content.length > 0;
|
|
994
|
+
const hasContentHash = typeof data.contentHash === 'string' && data.contentHash.length > 0;
|
|
995
|
+
const hasRefId = typeof data.reference?.id === 'string' && data.reference.id.length > 0;
|
|
996
|
+
if (!hasContent && !hasContentHash && !hasRefId) {
|
|
997
|
+
result.missing.push('content (or contentHash or reference.id)');
|
|
998
|
+
}
|
|
570
999
|
}
|
|
571
1000
|
|
|
572
1001
|
if (result.missing.length > 0) {
|
package/widgets/README.md
CHANGED
|
@@ -1,53 +1,64 @@
|
|
|
1
|
-
# NEUS Widgets (VerifyGate + ProofBadge)
|
|
2
|
-
|
|
3
|
-
React components for NEUS verification and access gating.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @neus/sdk react react-dom
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## VerifyGate
|
|
12
|
-
|
|
13
|
-
Gate UI behind verification requirements.
|
|
14
|
-
|
|
15
|
-
```jsx
|
|
16
|
-
import { VerifyGate } from '@neus/sdk/widgets';
|
|
17
|
-
|
|
18
|
-
export function Page() {
|
|
19
|
-
return (
|
|
20
|
-
<VerifyGate
|
|
21
|
-
requiredVerifiers={['nft-ownership']}
|
|
22
|
-
verifierData={{
|
|
23
|
-
'nft-ownership': { contractAddress: '0x...', tokenId: '1', chainId:
|
|
24
|
-
}}
|
|
25
|
-
>
|
|
26
|
-
<div>Unlocked</div>
|
|
27
|
-
</VerifyGate>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### Key props
|
|
33
|
-
|
|
34
|
-
- `requiredVerifiers`: `string[]` (default: `['ownership-basic']`)
|
|
35
|
-
- `verifierData`: object keyed by verifier id
|
|
36
|
-
- `strategy`: `'reuse-or-create' | 'reuse' | 'fresh'` (default: `'reuse-or-create'`)
|
|
37
|
-
- `proofOptions`: `{ privacyLevel, publicDisplay, storeOriginalContent, enableIpfs? }` (defaults: private)
|
|
38
|
-
- `mode`: `'create' | 'access'` (default: `'create'`)
|
|
39
|
-
- `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
1
|
+
# NEUS Widgets (VerifyGate + ProofBadge)
|
|
2
|
+
|
|
3
|
+
React components for NEUS verification and access gating.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @neus/sdk react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## VerifyGate
|
|
12
|
+
|
|
13
|
+
Gate UI behind verification requirements.
|
|
14
|
+
|
|
15
|
+
```jsx
|
|
16
|
+
import { VerifyGate } from '@neus/sdk/widgets';
|
|
17
|
+
|
|
18
|
+
export function Page() {
|
|
19
|
+
return (
|
|
20
|
+
<VerifyGate
|
|
21
|
+
requiredVerifiers={['nft-ownership']}
|
|
22
|
+
verifierData={{
|
|
23
|
+
'nft-ownership': { contractAddress: '0x...', tokenId: '1', chainId: 8453 }
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<div>Unlocked</div>
|
|
27
|
+
</VerifyGate>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Key props
|
|
33
|
+
|
|
34
|
+
- `requiredVerifiers`: `string[]` (default: `['ownership-basic']`)
|
|
35
|
+
- `verifierData`: object keyed by verifier id
|
|
36
|
+
- `strategy`: `'reuse-or-create' | 'reuse' | 'fresh'` (default: `'reuse-or-create'`)
|
|
37
|
+
- `proofOptions`: `{ privacyLevel, publicDisplay, storeOriginalContent, enableIpfs? }` (defaults: private)
|
|
38
|
+
- `mode`: `'create' | 'access'` (default: `'create'`)
|
|
39
|
+
- `proofId`: string (required for `mode="access"`)
|
|
40
|
+
- `maxProofAgeMs`: `number` (optional) - max proof age override in milliseconds for reuse checks
|
|
41
|
+
- `onError`: `(error: Error) => void` (optional) - called when verification/gating errors occur
|
|
42
|
+
- `apiUrl`: protocol API base URL (optional)
|
|
43
|
+
- `hostedCheckoutUrl`: hosted verify page URL (optional, recommended when `apiUrl` is custom)
|
|
44
|
+
|
|
45
|
+
Notes:
|
|
46
|
+
- Reuse without prompting can only see **public + discoverable** proofs.
|
|
47
|
+
- Reusing private proofs requires an **owner signature** (wallet grants read access).
|
|
48
|
+
- Interactive verifiers (`ownership-social`, `ownership-org-oauth`, `proof-of-human`) use NEUS hosted checkout automatically when required.
|
|
49
|
+
- If you set a custom `apiUrl`, also set `hostedCheckoutUrl` to your hosted verify UI (for example `https://neus.network/verify`).
|
|
50
|
+
`apiUrl` is used for API calls; `hostedCheckoutUrl` is used for popup checkout routing.
|
|
51
|
+
|
|
52
|
+
## ProofBadge
|
|
53
|
+
|
|
54
|
+
Display verification status by Proof ID (`proofId`).
|
|
55
|
+
|
|
56
|
+
```jsx
|
|
57
|
+
import { ProofBadge } from '@neus/sdk/widgets';
|
|
58
|
+
|
|
59
|
+
<ProofBadge proofId="0x..." showChains />
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Notes:
|
|
63
|
+
- Widgets default to a bundled NEUS logo asset (inlined at build time), so no external logo fetch is required.
|
|
64
|
+
- You can override with `logoUrl` in `ProofBadge`, `SimpleProofBadge`, `NeusPillLink`, and `VerifiedIcon`.
|
package/widgets/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NEUS Widgets
|
|
3
|
-
*
|
|
4
|
-
* Core Widgets:
|
|
5
|
-
* - VerifyGate: Universal verification gate component
|
|
6
|
-
* - ProofBadge: Display verification status
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export * from './verify-gate/index.js';
|
|
1
|
+
/**
|
|
2
|
+
* NEUS Widgets
|
|
3
|
+
*
|
|
4
|
+
* Core Widgets:
|
|
5
|
+
* - VerifyGate: Universal verification gate component
|
|
6
|
+
* - ProofBadge: Display verification status
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export * from './verify-gate/index.js';
|