@neus/sdk 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/neus.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from 'node:child_process';
2
+ import { exec, spawnSync } from 'node:child_process';
3
3
  import { createHash, randomBytes } from 'node:crypto';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
@@ -12,6 +12,13 @@ import {
12
12
  } from '../mcp-hosts.js';
13
13
 
14
14
  const __cliDir = path.dirname(fileURLToPath(import.meta.url));
15
+ const CLI_PACKAGE_VERSION = (() => {
16
+ try {
17
+ return JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8')).version;
18
+ } catch {
19
+ return '0.0.0';
20
+ }
21
+ })();
15
22
 
16
23
  const NEUS_APP_URL = 'https://neus.network';
17
24
  const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
@@ -100,7 +107,7 @@ function emitCliBanner(cliOptions = {}) {
100
107
  if (!shouldEmitCliBanner(cliOptions)) return;
101
108
  const version = readCliVersion();
102
109
  const title = paint('NEUS', 'green');
103
- const meta = `${paint(`v${version}`, 'dim')}${paint(' | trust receipts', 'dim')}`;
110
+ const meta = `${paint(`v${version}`, 'dim')}${paint(' | trust that travels', 'dim')}`;
104
111
  writeCliLine('');
105
112
  writeCliLine(` ${title} ${meta}`);
106
113
  writeCliLine('');
@@ -137,21 +144,20 @@ function describeClientResult(command, result) {
137
144
  }
138
145
  if (result.changed) return 'updated';
139
146
  if (result.authConfigured) return 'signed in';
147
+ if (result.configured) return 'ready';
140
148
  return 'ready';
141
149
  }
142
150
 
143
151
  function printBuilderGuidance(command, results) {
144
- if (!['setup', 'auth'].includes(command)) return;
152
+ if (!['setup', 'auth', 'check'].includes(command)) return;
145
153
  const hasCodex = results.some(result => result.client === 'codex');
146
154
  writeCliLine('');
147
- writeCliLine(paint('Builder notes', 'cyan'));
148
- writeGuidanceLine('Use from any shell without a global install: `npx -y -p @neus/sdk neus ...`.');
155
+ writeCliLine(paint('Next steps', 'cyan'));
156
+ writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for assistant prompts.');
149
157
  if (hasCodex) {
150
- writeGuidanceLine('Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.');
158
+ writeGuidanceLine('Codex OAuth: `neus auth --client codex` or `codex mcp login neus`.');
151
159
  }
152
- writeGuidanceLine(
153
- 'Claude plugin commands run inside Claude Code chat, not as `claude install`: `/plugin marketplace add https://github.com/neus/network`.'
154
- );
160
+ writeGuidanceLine('Ask your assistant: "Use NEUS Verify before taking sensitive actions."');
155
161
  }
156
162
 
157
163
  function selectedClientNames(results) {
@@ -178,7 +184,7 @@ function printStatusGuidance(results) {
178
184
  writeGuidanceLine(NEUS_MCP_URL);
179
185
  writeCliLine(paint('Profile connection', 'cyan'));
180
186
  if (results.some(result => result.configured)) {
181
- writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus doctor --live` to confirm live Profile context.');
187
+ writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus check` to confirm live connection.');
182
188
  } else {
183
189
  writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(results)}\`.`);
184
190
  }
@@ -298,6 +304,7 @@ function envAccessKey() {
298
304
 
299
305
  /** --access-key flag, else NEUS_ACCESS_KEY from the environment, else browser sign-in. */
300
306
  function resolveAccessKey(options) {
307
+ if (options?.oauth) return '';
301
308
  const explicit = String(options.accessKey || '').trim();
302
309
  if (explicit) return explicit;
303
310
  return envAccessKey();
@@ -309,6 +316,7 @@ function resolveLiveAccessKey(options, scope, cwd) {
309
316
  if (explicit) return explicit;
310
317
  const installed = readInstalledAccessKey(scope, cwd);
311
318
  if (installed) return installed;
319
+ if (options?.oauth) return '';
312
320
  return envAccessKey();
313
321
  }
314
322
 
@@ -581,7 +589,8 @@ function parseArgs(argv) {
581
589
  live: false,
582
590
  json: false,
583
591
  dryRun: false,
584
- project: false
592
+ project: false,
593
+ oauth: false
585
594
  };
586
595
 
587
596
  for (let index = 1; index < argv.length; index += 1) {
@@ -637,6 +646,10 @@ function parseArgs(argv) {
637
646
  index += 1;
638
647
  continue;
639
648
  }
649
+ if (token === '--oauth') {
650
+ options.oauth = true;
651
+ continue;
652
+ }
640
653
  if (token === '--help' || token === '-h') {
641
654
  return { command: 'help', options };
642
655
  }
@@ -659,6 +672,8 @@ function printUsage(exitCode = 0) {
659
672
  ' auth Sign in (browser, or NEUS_ACCESS_KEY / --access-key when set)',
660
673
  ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
661
674
  ' status Show current NEUS MCP setup',
675
+ ' check Confirm setup and live NEUS connection (alias for doctor --live)',
676
+ ' examples Show assistant prompts to try after install',
662
677
  ' doctor Deep check: config status, profile connection, and live MCP context',
663
678
  ' import Detect and package supported assistant context for NEUS portability',
664
679
  ' export Export the latest local NEUS portable agent manifest',
@@ -668,6 +683,7 @@ function printUsage(exitCode = 0) {
668
683
  ' --client <name[,name]> Limit setup to claude, codex, cursor, or vscode',
669
684
  ' --project Write shared project config instead of user config',
670
685
  ' --access-key <npk_...> Override profile access key (else uses NEUS_ACCESS_KEY if set)',
686
+ ' --oauth Force browser OAuth (ignore NEUS_ACCESS_KEY in the environment)',
671
687
  ' --from <source> Import source: auto, cursor, claude-code, or claude-desktop',
672
688
  ' --to <format> Export format: manifest or json',
673
689
  ' --output <path> Write exported manifest to a specific path',
@@ -1105,7 +1121,7 @@ function createEmptyManifest(source) {
1105
1121
  proofHints: {
1106
1122
  status: 'not-issued',
1107
1123
  qHashes: [],
1108
- next: ['neus setup', 'neus auth', 'neus doctor --live']
1124
+ next: ['neus setup', 'neus auth', 'neus check']
1109
1125
  }
1110
1126
  };
1111
1127
  }
@@ -1383,7 +1399,7 @@ async function runLiveMcpDiagnostics(accessKey) {
1383
1399
  params: {
1384
1400
  protocolVersion: '2025-11-25',
1385
1401
  capabilities: {},
1386
- clientInfo: { name: 'neus-cli', version: '1.0.0' }
1402
+ clientInfo: { name: 'neus-cli', version: CLI_PACKAGE_VERSION }
1387
1403
  },
1388
1404
  accessKey,
1389
1405
  signal: controller.signal
@@ -1424,6 +1440,9 @@ async function runLiveMcpDiagnostics(accessKey) {
1424
1440
  signal: controller.signal
1425
1441
  });
1426
1442
  const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1443
+ const profileCtx = context.ok ? context.payload?.profileContext : null;
1444
+ const principal = profileCtx?.principal || null;
1445
+ const proofsTotal = profileCtx?.profileSummary?.proofsSummary?.total;
1427
1446
  return {
1428
1447
  live: true,
1429
1448
  reachable: true,
@@ -1431,6 +1450,9 @@ async function runLiveMcpDiagnostics(accessKey) {
1431
1450
  toolsCount: toolNames.length,
1432
1451
  tools: toolNames,
1433
1452
  contextMode: mode,
1453
+ sessionWallet: context.ok ? context.payload?.sessionWallet || principal?.primaryAccount || null : null,
1454
+ profileHandle: principal?.handle || null,
1455
+ proofsTotal: Number.isFinite(Number(proofsTotal)) ? Number(proofsTotal) : null,
1434
1456
  checks: [
1435
1457
  {
1436
1458
  name: 'initialize',
@@ -1693,9 +1715,12 @@ async function runAuthBrowser(options) {
1693
1715
  logStep('next', 'wait', 'finish sign-in in the browser');
1694
1716
  }
1695
1717
 
1696
- const { exec } = require('node:child_process');
1697
- const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1698
- exec(`${openCmd} "${authUrl}"`, err => {
1718
+ const openCommand = process.platform === 'win32'
1719
+ ? `cmd /c start "" "${authUrl.replace(/"/g, '\\"')}"`
1720
+ : process.platform === 'darwin'
1721
+ ? `open "${authUrl.replace(/"/g, '\\"')}"`
1722
+ : `xdg-open "${authUrl.replace(/"/g, '\\"')}"`;
1723
+ exec(openCommand, { shell: true }, err => {
1699
1724
  if (err && !options.json) {
1700
1725
  logStep('warn', 'browser', 'open the URL above manually');
1701
1726
  }
@@ -1810,12 +1835,18 @@ async function runSetup(options) {
1810
1835
  }
1811
1836
 
1812
1837
  if (options.json) {
1838
+ payload.authRequired = !accessKey && !options.dryRun;
1839
+ if (payload.authRequired) {
1840
+ payload.nextCommand = clients.length === 1 && clients[0] === 'codex'
1841
+ ? 'neus auth --client codex'
1842
+ : 'neus auth';
1843
+ }
1813
1844
  printJson(payload);
1814
1845
  return payload;
1815
1846
  }
1816
1847
 
1817
1848
  printFlowSummary('setup', scope, initResults, {
1818
- nextStep: accessKey ? 'Open your MCP client and ask the assistant to use NEUS Trust.' : '',
1849
+ nextStep: accessKey ? 'Run `neus examples`, then ask your assistant to use NEUS Verify.' : '',
1819
1850
  cliOptions: options
1820
1851
  });
1821
1852
 
@@ -1823,7 +1854,7 @@ async function runSetup(options) {
1823
1854
  const authResult = await runAuth(options);
1824
1855
  if (authResult && !authResult.hasErrors) {
1825
1856
  printFlowSummary('auth', authResult.scope, authResult.results, {
1826
- nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
1857
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
1827
1858
  cliOptions: options
1828
1859
  });
1829
1860
  }
@@ -1910,7 +1941,39 @@ function runExport(options) {
1910
1941
  printExportSummary(payload, options);
1911
1942
  }
1912
1943
 
1944
+ const ASSISTANT_EXAMPLE_PROMPTS = [
1945
+ 'Use NEUS Verify before taking sensitive actions.',
1946
+ 'Check whether I already have the required trust receipt.',
1947
+ 'Verify this agent is trusted before it runs tools.',
1948
+ 'Use NEUS Vault before storing or using secrets.',
1949
+ 'Show the receipt for this verification.'
1950
+ ];
1951
+
1952
+ function runExamples(options) {
1953
+ const payload = {
1954
+ command: 'examples',
1955
+ intro: 'Try this in your assistant:',
1956
+ prompts: ASSISTANT_EXAMPLE_PROMPTS
1957
+ };
1958
+
1959
+ if (options.json) {
1960
+ printJson(payload);
1961
+ return;
1962
+ }
1963
+
1964
+ emitCliBanner(options);
1965
+ writeCliLine(paint('examples', 'green'));
1966
+ writeCliLine('');
1967
+ writeCliLine(` ${paint(payload.intro, 'dim')}`);
1968
+ writeCliLine('');
1969
+ ASSISTANT_EXAMPLE_PROMPTS.forEach((prompt, index) => {
1970
+ writeCliLine(` ${paint(String(index + 1) + '.', 'cyan')} ${prompt}`);
1971
+ });
1972
+ writeCliLine('');
1973
+ }
1974
+
1913
1975
  async function runDoctor(options) {
1976
+ const displayCommand = options.displayCommand || 'doctor';
1914
1977
  const scope = resolveScope(options);
1915
1978
  const cwd = process.cwd();
1916
1979
  const clients = resolveClients(scope, options.clients);
@@ -1922,7 +1985,7 @@ async function runDoctor(options) {
1922
1985
  const configuredClients = inspected.filter(r => r.configured);
1923
1986
  const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
1924
1987
  const payload = {
1925
- command: 'doctor',
1988
+ command: displayCommand,
1926
1989
  scope,
1927
1990
  clients: inspected,
1928
1991
  configuredCount: configuredClients.length,
@@ -1951,7 +2014,7 @@ async function runDoctor(options) {
1951
2014
 
1952
2015
  if (configuredClients.length === 0) {
1953
2016
  emitCliBanner(options);
1954
- writeCliLine(paint('doctor', 'green'));
2017
+ writeCliLine(paint(displayCommand, 'green'));
1955
2018
  for (const result of inspected) {
1956
2019
  if (result.error) {
1957
2020
  logStep('warn', result.client, result.error);
@@ -1966,13 +2029,13 @@ async function runDoctor(options) {
1966
2029
  writeGuidanceLine(NEUS_MCP_URL);
1967
2030
  writeCliLine(paint('Profile connection', 'cyan'));
1968
2031
  writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(inspected)}\`.`);
1969
- writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus doctor --live\`.`);
2032
+ writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus check\`.`);
1970
2033
  writeCliLine('');
1971
2034
  process.exitCode = 1;
1972
2035
  return;
1973
2036
  }
1974
2037
 
1975
- printFlowSummary('doctor', scope, inspected, { cliOptions: options });
2038
+ printFlowSummary(displayCommand, scope, inspected, { cliOptions: options });
1976
2039
  const hasCodex = inspected.some(result => result.client === 'codex');
1977
2040
  writeCliLine(paint('Profile connection', 'cyan'));
1978
2041
  if (options.live && payload.mcp) {
@@ -1983,16 +2046,19 @@ async function runDoctor(options) {
1983
2046
  : 'No account credential found for the configured MCP clients. Run `neus auth`.'
1984
2047
  );
1985
2048
  } else {
1986
- logStep(
1987
- payload.mcp.authenticated ? 'ok' : 'warn',
1988
- 'profile',
1989
- payload.mcp.authenticated
1990
- ? `live MCP context confirmed; ${payload.mcp.toolsCount || 0} tools discovered`
1991
- : 'live MCP context was not confirmed'
1992
- );
2049
+ if (payload.mcp.authenticated) {
2050
+ const handle = payload.mcp.profileHandle ? ` as ${payload.mcp.profileHandle}` : '';
2051
+ const receipts =
2052
+ payload.mcp.proofsTotal != null ? ` · ${payload.mcp.proofsTotal} trust receipts on file` : '';
2053
+ logStep('ok', 'profile', `connected${handle}${receipts}`);
2054
+ writeGuidanceLine('NEUS Verify is ready. Ask your assistant to verify trust before sensitive actions.');
2055
+ writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for starter prompts.');
2056
+ } else {
2057
+ logStep('warn', 'profile', 'live connection was not confirmed — run `neus auth`');
2058
+ }
1993
2059
  }
1994
2060
  } else if (liveAccessKey) {
1995
- writeGuidanceLine('Saved credential found. Run `neus doctor --live` to confirm Profile context.');
2061
+ writeGuidanceLine('Saved credential found. Run `neus check` to confirm live connection.');
1996
2062
  } else {
1997
2063
  writeGuidanceLine(
1998
2064
  hasCodex
@@ -2096,12 +2162,12 @@ async function main() {
2096
2162
  printJson(result);
2097
2163
  } else if (result.authMethod !== 'browser') {
2098
2164
  printFlowSummary('auth', result.scope, result.results, {
2099
- nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
2165
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2100
2166
  cliOptions: options
2101
2167
  });
2102
2168
  } else {
2103
2169
  printFlowSummary('auth', result.scope, result.results, {
2104
- nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
2170
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2105
2171
  cliOptions: options
2106
2172
  });
2107
2173
  }
@@ -2122,10 +2188,18 @@ async function main() {
2122
2188
  }
2123
2189
  return;
2124
2190
  }
2191
+ if (command === 'check') {
2192
+ await runDoctor({ ...options, live: true, displayCommand: 'check' });
2193
+ return;
2194
+ }
2125
2195
  if (command === 'doctor') {
2126
2196
  await runDoctor(options);
2127
2197
  return;
2128
2198
  }
2199
+ if (command === 'examples') {
2200
+ runExamples(options);
2201
+ return;
2202
+ }
2129
2203
  if (command === 'import') {
2130
2204
  runImport(options);
2131
2205
  return;
package/client.js CHANGED
@@ -11,19 +11,19 @@ import {
11
11
 
12
12
  const FALLBACK_PUBLIC_VERIFIER_CATALOG = {
13
13
  'ownership-basic': { supportsDirectApi: true },
14
+ 'ownership-social': { supportsDirectApi: false },
14
15
  'ownership-pseudonym': { supportsDirectApi: true },
15
16
  'ownership-dns-txt': { supportsDirectApi: true },
16
- 'ownership-social': { supportsDirectApi: false },
17
17
  'ownership-org-oauth': { supportsDirectApi: false },
18
18
  'contract-ownership': { supportsDirectApi: true },
19
+ 'proof-of-human': { supportsDirectApi: false },
19
20
  'nft-ownership': { supportsDirectApi: true },
20
21
  'token-holding': { supportsDirectApi: true },
21
- 'wallet-link': { supportsDirectApi: true },
22
22
  'wallet-risk': { supportsDirectApi: true },
23
- 'proof-of-human': { supportsDirectApi: false },
23
+ 'wallet-link': { supportsDirectApi: true },
24
+ 'ai-content-moderation': { supportsDirectApi: true },
24
25
  'agent-identity': { supportsDirectApi: true },
25
- 'agent-delegation': { supportsDirectApi: true },
26
- 'ai-content-moderation': { supportsDirectApi: true }
26
+ 'agent-delegation': { supportsDirectApi: true }
27
27
  };
28
28
 
29
29
  const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
@@ -1204,6 +1204,12 @@ export class NeusClient {
1204
1204
  typeof options?.storeOriginalContent === 'boolean' ? options.storeOriginalContent : true
1205
1205
  };
1206
1206
  if (typeof options?.enableIpfs === 'boolean') optionsPayload.enableIpfs = options.enableIpfs;
1207
+ // Receipts persist offchain by default; hub registry anchoring is explicit opt-in.
1208
+ if (options?.publishToHub === true) {
1209
+ optionsPayload.publishToHub = true;
1210
+ } else {
1211
+ delete optionsPayload.publishToHub;
1212
+ }
1207
1213
 
1208
1214
  const requestData = {
1209
1215
  verifierIds: normalizedVerifierIds,
@@ -1471,6 +1477,33 @@ export class NeusClient {
1471
1477
  return true;
1472
1478
  }
1473
1479
 
1480
+ _buildProofsByWalletQuery(options = {}) {
1481
+ const qs = [];
1482
+ if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1483
+ const cursorRaw = options.cursor !== null && options.cursor !== undefined ? String(options.cursor).trim() : '';
1484
+ if (cursorRaw) qs.push(`cursor=${encodeURIComponent(cursorRaw)}`);
1485
+ else if (options.offset !== undefined && options.offset !== null) {
1486
+ qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1487
+ }
1488
+ if (options.q) qs.push(`q=${encodeURIComponent(String(options.q))}`);
1489
+ if (options.qHash) qs.push(`qHash=${encodeURIComponent(String(options.qHash).toLowerCase())}`);
1490
+ if (options.verifierId) qs.push(`verifierId=${encodeURIComponent(String(options.verifierId))}`);
1491
+ if (options.verifierIds) qs.push(`verifierIds=${encodeURIComponent(String(options.verifierIds))}`);
1492
+ if (options.tags) qs.push(`tags=${encodeURIComponent(String(options.tags))}`);
1493
+ if (options.tagPrefix) qs.push(`tagPrefix=${encodeURIComponent(String(options.tagPrefix))}`);
1494
+ if (options.tagContains) qs.push(`tagContains=${encodeURIComponent(String(options.tagContains))}`);
1495
+ if (options.tagPrefixesAll) qs.push(`tagPrefixesAll=${encodeURIComponent(String(options.tagPrefixesAll))}`);
1496
+ if (options.status) qs.push(`status=${encodeURIComponent(String(options.status))}`);
1497
+ if (options.appId) qs.push(`appId=${encodeURIComponent(String(options.appId))}`);
1498
+ if (options.chainCoverage) qs.push(`chainCoverage=${encodeURIComponent(String(options.chainCoverage))}`);
1499
+ if (options.privacyLevel) qs.push(`privacyLevel=${encodeURIComponent(String(options.privacyLevel))}`);
1500
+ if (options.includeHistory) qs.push('includeHistory=1');
1501
+ if (options.includeFacets) qs.push(`includeFacets=${encodeURIComponent(String(options.includeFacets))}`);
1502
+ if (options.visibility) qs.push(`visibility=${encodeURIComponent(String(options.visibility))}`);
1503
+ if (options.isPublicRead) qs.push('isPublicRead=1');
1504
+ return qs;
1505
+ }
1506
+
1474
1507
  async getProofsByWallet(walletAddress, options = {}) {
1475
1508
  if (!walletAddress || typeof walletAddress !== 'string') {
1476
1509
  throw new ValidationError('walletAddress is required');
@@ -1479,12 +1512,7 @@ export class NeusClient {
1479
1512
  const id = walletAddress.trim();
1480
1513
  const pathId = /^0x[a-fA-F0-9]{40}$/i.test(id) ? id.toLowerCase() : id;
1481
1514
 
1482
- const qs = [];
1483
- if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1484
- const cursorRaw = options.cursor !== null && options.cursor !== undefined ? String(options.cursor).trim() : '';
1485
- if (cursorRaw) qs.push(`cursor=${encodeURIComponent(cursorRaw)}`);
1486
- else if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1487
- if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1515
+ const qs = this._buildProofsByWalletQuery(options);
1488
1516
 
1489
1517
  const query = qs.length ? `?${qs.join('&')}` : '';
1490
1518
  const response = await this._makeRequest(
@@ -1500,13 +1528,14 @@ export class NeusClient {
1500
1528
  return {
1501
1529
  success: true,
1502
1530
  proofs: Array.isArray(proofs) ? proofs : [],
1503
- totalCount: response.data?.totalCount ?? proofs.length,
1531
+ totalCount: typeof response.data?.totalCount === 'number' ? response.data.totalCount : null,
1504
1532
  hasMore: Boolean(response.data?.hasMore),
1505
1533
  nextOffset: response.data?.nextOffset ?? null,
1506
1534
  nextCursor:
1507
1535
  typeof response.data?.nextCursor === 'string' && response.data.nextCursor.trim()
1508
1536
  ? response.data.nextCursor.trim()
1509
- : null
1537
+ : null,
1538
+ facets: response.data?.facets || null,
1510
1539
  };
1511
1540
  }
1512
1541
 
@@ -1569,12 +1598,7 @@ export class NeusClient {
1569
1598
  throw new ValidationError(`Failed to sign message: ${error.message}`);
1570
1599
  }
1571
1600
 
1572
- const qs = [];
1573
- if (options.limit) qs.push(`limit=${encodeURIComponent(String(options.limit))}`);
1574
- const cursorRaw = options.cursor !== null && options.cursor !== undefined ? String(options.cursor).trim() : '';
1575
- if (cursorRaw) qs.push(`cursor=${encodeURIComponent(cursorRaw)}`);
1576
- else if (options.offset) qs.push(`offset=${encodeURIComponent(String(options.offset))}`);
1577
- if (options.qHash) qs.push(`qHash=${encodeURIComponent(options.qHash.toLowerCase())}`);
1601
+ const qs = this._buildProofsByWalletQuery(options);
1578
1602
  const query = qs.length ? `?${qs.join('&')}` : '';
1579
1603
 
1580
1604
  const response = await this._makeRequest('GET', `/api/v1/proofs/by-wallet/${encodeURIComponent(pathId)}${query}`, null, {
@@ -1592,7 +1616,7 @@ export class NeusClient {
1592
1616
  return {
1593
1617
  success: true,
1594
1618
  proofs: Array.isArray(proofs) ? proofs : [],
1595
- totalCount: response.data?.totalCount ?? proofs.length,
1619
+ totalCount: typeof response.data?.totalCount === 'number' ? response.data.totalCount : null,
1596
1620
  hasMore: Boolean(response.data?.hasMore),
1597
1621
  nextOffset: response.data?.nextOffset ?? null,
1598
1622
  nextCursor:
@@ -1730,6 +1754,67 @@ export class NeusClient {
1730
1754
  return response;
1731
1755
  }
1732
1756
 
1757
+ /**
1758
+ * Get the public snapshot of a published gate: requirements, charge, schedule,
1759
+ * checkout plan, and reward presence. Never returns the secret reward value —
1760
+ * that is delivered post-verify via fulfillGate().
1761
+ *
1762
+ * @param {string} gateId Published gate handle
1763
+ * @returns {Promise<object>} Public gate snapshot
1764
+ */
1765
+ async getGate(gateId) {
1766
+ const id = String(gateId || '').trim();
1767
+ if (!id || id.length > 80 || !/^[a-zA-Z0-9:_-]+$/.test(id)) {
1768
+ throw new ValidationError('Valid gateId is required');
1769
+ }
1770
+ const response = await this._makeRequest('GET', `/api/v1/gates/${encodeURIComponent(id)}`);
1771
+ if (!response.success || !response.data?.gate) {
1772
+ throw new ApiError(`Gate lookup failed: ${response.error?.message || 'Gate not found'}`, response.error);
1773
+ }
1774
+ return response.data.gate;
1775
+ }
1776
+
1777
+ /**
1778
+ * Post-verify reward delivery for hosted gate checkout. Requires a verified
1779
+ * proof (qHash) for the gate; paid gates also require payment evidence
1780
+ * (paymentCheckoutSessionId for card, or paymentTxHash for USDC).
1781
+ *
1782
+ * @param {object} params
1783
+ * @param {string} params.gateId Published gate handle
1784
+ * @param {string} params.qHash Verified proof receipt id
1785
+ * @param {string} [params.walletAddress] Wallet bound to the proof (required without a session cookie)
1786
+ * @param {string} [params.paymentCheckoutSessionId] Stripe checkout session id (card rail)
1787
+ * @param {string} [params.paymentTxHash] USDC payment transaction hash (wallet rail)
1788
+ * @returns {Promise<object>} `{ success, data: { gateId, qHash, fulfillment, successReturnUrl? } }`
1789
+ */
1790
+ async fulfillGate(params = {}) {
1791
+ const gateId = String(params.gateId || '').trim();
1792
+ if (!gateId || gateId.length > 80 || !/^[a-zA-Z0-9:_-]+$/.test(gateId)) {
1793
+ throw new ValidationError('Valid gateId is required');
1794
+ }
1795
+ const qHash = String(params.qHash || '').trim();
1796
+ if (!/^0x[a-fA-F0-9]{64}$/.test(qHash)) {
1797
+ throw new ValidationError('Valid qHash is required');
1798
+ }
1799
+ const body = { qHash };
1800
+ const walletAddress = String(params.walletAddress || '').trim();
1801
+ if (walletAddress) body.walletAddress = walletAddress;
1802
+ const paymentCheckoutSessionId = String(params.paymentCheckoutSessionId || '').trim();
1803
+ if (paymentCheckoutSessionId) body.paymentCheckoutSessionId = paymentCheckoutSessionId;
1804
+ const paymentTxHash = String(params.paymentTxHash || '').trim();
1805
+ if (paymentTxHash) body.paymentTxHash = paymentTxHash;
1806
+
1807
+ const response = await this._makeRequest(
1808
+ 'POST',
1809
+ `/api/v1/gates/${encodeURIComponent(gateId)}/fulfill`,
1810
+ body
1811
+ );
1812
+ if (!response.success) {
1813
+ throw new ApiError(`Gate fulfillment failed: ${response.error?.message || 'Unknown error'}`, response.error);
1814
+ }
1815
+ return response;
1816
+ }
1817
+
1733
1818
  async checkGate(params) {
1734
1819
  const { walletAddress, requirements, proofs: preloadedProofs } = params;
1735
1820