@neus/sdk 1.0.4 → 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 CHANGED
@@ -16,7 +16,22 @@ npm install @neus/sdk
16
16
  npx -y -p @neus/sdk neus init
17
17
  ```
18
18
 
19
- Prints the hosted MCP URL and documentation links in your terminal - fast setup in IDEs and agent clients.
19
+ Configures supported MCP clients automatically. By default the command installs NEUS into user-level Claude Code, Cursor, and VS Code MCP config when those clients are detected.
20
+
21
+ ## CLI
22
+
23
+ ```bash
24
+ # Autopilot setup for detected clients
25
+ npx -y -p @neus/sdk neus init
26
+
27
+ # Enable personal account tools such as neus_me and private reads
28
+ npx -y -p @neus/sdk neus auth --access-key <npk_...>
29
+
30
+ # Inspect current NEUS MCP setup
31
+ npx -y -p @neus/sdk neus status --json
32
+ ```
33
+
34
+ Use `neus init --project` when you want shared repo config instead of personal user-scope setup. Access keys stay user-scope only so secrets do not land in checked-in config.
20
35
 
21
36
  ## Minimal working example
22
37
 
@@ -51,6 +66,7 @@ const check = await client.gateCheck({
51
66
  | `client.gateCheck()` | **Server eligibility** (use for real gates) |
52
67
  | `client.checkGate()` | Local preview only |
53
68
  | `getHostedCheckoutUrl()` | Hosted verify URL |
69
+ | `client.createWalletLinkData()` | Build advanced direct wallet-link payload |
54
70
 
55
71
  ## VerifyGate (React)
56
72
 
@@ -76,6 +92,27 @@ const client = new NeusClient({
76
92
  });
77
93
  ```
78
94
 
95
+ ## Wallet-link
96
+
97
+ For user-facing browser flows, prefer Hosted Verify so the user can select a secondary wallet, sign once, see the linked state, and only then continue to proof creation.
98
+
99
+ Use `client.createWalletLinkData()` only for advanced direct/API flows where your app already controls the secondary-wallet provider:
100
+
101
+ ```javascript
102
+ const walletLinkData = await client.createWalletLinkData({
103
+ primaryWalletAddress: '0xprimary...',
104
+ secondaryWalletAddress: '0xsecondary...',
105
+ wallet: window.ethereum,
106
+ relationshipType: 'linked',
107
+ label: 'ops-wallet'
108
+ });
109
+
110
+ await client.verify({
111
+ verifier: 'wallet-link',
112
+ data: walletLinkData
113
+ });
114
+ ```
115
+
79
116
  ## Docs
80
117
 
81
118
  - [Quickstart](https://docs.neus.network/quickstart)
package/cjs/client.cjs CHANGED
@@ -411,6 +411,11 @@ var FALLBACK_PUBLIC_VERIFIER_CATALOG = {
411
411
  "ai-content-moderation": { supportsDirectApi: true }
412
412
  };
413
413
  var EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
414
+ var WALLET_LINK_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["primary", "personal", "org", "affiliate", "agent", "linked"]);
415
+ function normalizeWalletLinkRelationshipType(value) {
416
+ const normalized = String(value || "").trim().toLowerCase();
417
+ return WALLET_LINK_RELATIONSHIP_TYPES.has(normalized) ? normalized : "linked";
418
+ }
414
419
  var validateVerifierData = (verifierId, data) => {
415
420
  if (!data || typeof data !== "object") {
416
421
  return { valid: false, error: "Data object is required" };
@@ -821,6 +826,74 @@ var NeusClient = class {
821
826
  signatureMethod: params.signatureMethod
822
827
  });
823
828
  }
829
+ async createWalletLinkData(params = {}) {
830
+ const normalizedPrimary = this._normalizeIdentity(params.primaryWalletAddress);
831
+ const normalizedSecondary = this._normalizeIdentity(params.secondaryWalletAddress);
832
+ if (!EVM_ADDRESS_RE.test(normalizedPrimary)) {
833
+ throw new ValidationError("wallet-link requires a valid primaryWalletAddress");
834
+ }
835
+ if (!EVM_ADDRESS_RE.test(normalizedSecondary)) {
836
+ throw new ValidationError("wallet-link requires a valid secondaryWalletAddress");
837
+ }
838
+ if (normalizedPrimary === normalizedSecondary) {
839
+ throw new ValidationError("wallet-link secondaryWalletAddress must differ from primaryWalletAddress");
840
+ }
841
+ const providerWallet = params.wallet || this._getDefaultBrowserWallet();
842
+ const { signerWalletAddress, provider } = await this._resolveWalletSigner(providerWallet);
843
+ const normalizedSigner = this._normalizeIdentity(signerWalletAddress);
844
+ if (!EVM_ADDRESS_RE.test(normalizedSigner)) {
845
+ throw new ValidationError("wallet-link requires an EVM wallet/provider for the secondary wallet");
846
+ }
847
+ if (normalizedSigner !== normalizedSecondary) {
848
+ throw new ValidationError("wallet-link wallet/provider must be connected to secondaryWalletAddress");
849
+ }
850
+ const resolvedChain = (() => {
851
+ const raw = String(params.chain || "").trim();
852
+ if (!raw) return `eip155:${this._getHubChainId()}`;
853
+ if (!/^eip155:\d+$/.test(raw)) {
854
+ throw new ValidationError("wallet-link requires chain in the form eip155:<chainId>");
855
+ }
856
+ return raw;
857
+ })();
858
+ const signedTimestamp = Number.isFinite(Number(params.signedTimestamp)) && Number(params.signedTimestamp) > 0 ? Number(params.signedTimestamp) : Date.now();
859
+ const linkData = {
860
+ primaryAccountId: `${resolvedChain}:${normalizedPrimary}`,
861
+ secondaryAccountId: `${resolvedChain}:${normalizedSecondary}`,
862
+ primaryWalletAddress: normalizedPrimary,
863
+ secondaryWalletAddress: normalizedSecondary
864
+ };
865
+ const message = constructVerificationMessage({
866
+ walletAddress: normalizedSecondary,
867
+ signedTimestamp,
868
+ data: linkData,
869
+ verifierIds: ["wallet-link"],
870
+ chain: resolvedChain
871
+ });
872
+ let signature;
873
+ try {
874
+ signature = await signMessage({
875
+ provider,
876
+ message,
877
+ walletAddress: signerWalletAddress
878
+ });
879
+ } catch (error) {
880
+ if (error?.code === 4001) {
881
+ throw new ValidationError("User rejected wallet-link signature request");
882
+ }
883
+ throw new ValidationError(`Failed to sign wallet-link message: ${error?.message || String(error)}`);
884
+ }
885
+ const label = String(params.label || "").trim().slice(0, 64);
886
+ return {
887
+ primaryWalletAddress: normalizedPrimary,
888
+ secondaryWalletAddress: normalizedSecondary,
889
+ signature,
890
+ chain: resolvedChain,
891
+ signatureMethod: "eip191",
892
+ signedTimestamp,
893
+ relationshipType: normalizeWalletLinkRelationshipType(params.relationshipType),
894
+ ...label ? { label } : {}
895
+ };
896
+ }
824
897
  /**
825
898
  * Create a verification proof.
826
899
  *
package/cjs/index.cjs CHANGED
@@ -1073,7 +1073,11 @@ __export(client_exports, {
1073
1073
  PORTABLE_PROOF_SIGNER_HEADER: () => PORTABLE_PROOF_SIGNER_HEADER,
1074
1074
  constructVerificationMessage: () => constructVerificationMessage
1075
1075
  });
1076
- var FALLBACK_PUBLIC_VERIFIER_CATALOG, EVM_ADDRESS_RE, validateVerifierData, NeusClient;
1076
+ function normalizeWalletLinkRelationshipType(value) {
1077
+ const normalized = String(value || "").trim().toLowerCase();
1078
+ return WALLET_LINK_RELATIONSHIP_TYPES.has(normalized) ? normalized : "linked";
1079
+ }
1080
+ var FALLBACK_PUBLIC_VERIFIER_CATALOG, EVM_ADDRESS_RE, WALLET_LINK_RELATIONSHIP_TYPES, validateVerifierData, NeusClient;
1077
1081
  var init_client = __esm({
1078
1082
  "client.js"() {
1079
1083
  "use strict";
@@ -1096,6 +1100,7 @@ var init_client = __esm({
1096
1100
  "ai-content-moderation": { supportsDirectApi: true }
1097
1101
  };
1098
1102
  EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
1103
+ WALLET_LINK_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["primary", "personal", "org", "affiliate", "agent", "linked"]);
1099
1104
  validateVerifierData = (verifierId, data) => {
1100
1105
  if (!data || typeof data !== "object") {
1101
1106
  return { valid: false, error: "Data object is required" };
@@ -1506,6 +1511,74 @@ var init_client = __esm({
1506
1511
  signatureMethod: params.signatureMethod
1507
1512
  });
1508
1513
  }
1514
+ async createWalletLinkData(params = {}) {
1515
+ const normalizedPrimary = this._normalizeIdentity(params.primaryWalletAddress);
1516
+ const normalizedSecondary = this._normalizeIdentity(params.secondaryWalletAddress);
1517
+ if (!EVM_ADDRESS_RE.test(normalizedPrimary)) {
1518
+ throw new ValidationError("wallet-link requires a valid primaryWalletAddress");
1519
+ }
1520
+ if (!EVM_ADDRESS_RE.test(normalizedSecondary)) {
1521
+ throw new ValidationError("wallet-link requires a valid secondaryWalletAddress");
1522
+ }
1523
+ if (normalizedPrimary === normalizedSecondary) {
1524
+ throw new ValidationError("wallet-link secondaryWalletAddress must differ from primaryWalletAddress");
1525
+ }
1526
+ const providerWallet = params.wallet || this._getDefaultBrowserWallet();
1527
+ const { signerWalletAddress, provider } = await this._resolveWalletSigner(providerWallet);
1528
+ const normalizedSigner = this._normalizeIdentity(signerWalletAddress);
1529
+ if (!EVM_ADDRESS_RE.test(normalizedSigner)) {
1530
+ throw new ValidationError("wallet-link requires an EVM wallet/provider for the secondary wallet");
1531
+ }
1532
+ if (normalizedSigner !== normalizedSecondary) {
1533
+ throw new ValidationError("wallet-link wallet/provider must be connected to secondaryWalletAddress");
1534
+ }
1535
+ const resolvedChain = (() => {
1536
+ const raw = String(params.chain || "").trim();
1537
+ if (!raw) return `eip155:${this._getHubChainId()}`;
1538
+ if (!/^eip155:\d+$/.test(raw)) {
1539
+ throw new ValidationError("wallet-link requires chain in the form eip155:<chainId>");
1540
+ }
1541
+ return raw;
1542
+ })();
1543
+ const signedTimestamp = Number.isFinite(Number(params.signedTimestamp)) && Number(params.signedTimestamp) > 0 ? Number(params.signedTimestamp) : Date.now();
1544
+ const linkData = {
1545
+ primaryAccountId: `${resolvedChain}:${normalizedPrimary}`,
1546
+ secondaryAccountId: `${resolvedChain}:${normalizedSecondary}`,
1547
+ primaryWalletAddress: normalizedPrimary,
1548
+ secondaryWalletAddress: normalizedSecondary
1549
+ };
1550
+ const message = constructVerificationMessage({
1551
+ walletAddress: normalizedSecondary,
1552
+ signedTimestamp,
1553
+ data: linkData,
1554
+ verifierIds: ["wallet-link"],
1555
+ chain: resolvedChain
1556
+ });
1557
+ let signature;
1558
+ try {
1559
+ signature = await signMessage({
1560
+ provider,
1561
+ message,
1562
+ walletAddress: signerWalletAddress
1563
+ });
1564
+ } catch (error) {
1565
+ if (error?.code === 4001) {
1566
+ throw new ValidationError("User rejected wallet-link signature request");
1567
+ }
1568
+ throw new ValidationError(`Failed to sign wallet-link message: ${error?.message || String(error)}`);
1569
+ }
1570
+ const label = String(params.label || "").trim().slice(0, 64);
1571
+ return {
1572
+ primaryWalletAddress: normalizedPrimary,
1573
+ secondaryWalletAddress: normalizedSecondary,
1574
+ signature,
1575
+ chain: resolvedChain,
1576
+ signatureMethod: "eip191",
1577
+ signedTimestamp,
1578
+ relationshipType: normalizeWalletLinkRelationshipType(params.relationshipType),
1579
+ ...label ? { label } : {}
1580
+ };
1581
+ }
1509
1582
  /**
1510
1583
  * Create a verification proof.
1511
1584
  *
package/cli/neus.mjs CHANGED
@@ -1,58 +1,606 @@
1
1
  #!/usr/bin/env node
2
- const argv = process.argv.slice(2);
3
- const sub = argv[0];
2
+ import { spawnSync } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
4
6
 
5
- function usage() {
6
- process.stderr.write("Usage: neus init\n");
7
- process.stderr.write("Prints hosted MCP config and doc URLs. Does not write files.\n");
8
- process.exit(sub && sub !== "help" && sub !== "--help" && sub !== "-h" ? 1 : 0);
7
+ const NEUS_SERVER_NAME = 'neus';
8
+ const NEUS_MCP_URL = 'https://mcp.neus.network/mcp';
9
+ const NEUS_ACCESS_KEYS_URL = 'https://neus.network/profile?tab=account';
10
+ const SUPPORTED_CLIENTS = ['claude', 'cursor', 'vscode'];
11
+
12
+ function fileExists(targetPath) {
13
+ try {
14
+ fs.accessSync(targetPath);
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
9
19
  }
10
20
 
11
- function printInit() {
12
- const mcpBlock = {
13
- mcpServers: {
14
- neus: {
15
- type: "streamableHttp",
16
- url: "https://mcp.neus.network/mcp",
21
+ function jsonStringify(value) {
22
+ return `${JSON.stringify(value, null, 2)}\n`;
23
+ }
24
+
25
+ function readJsonFile(targetPath, fallback) {
26
+ if (!fileExists(targetPath)) return fallback;
27
+ const raw = fs.readFileSync(targetPath, 'utf8');
28
+ const parsed = JSON.parse(raw);
29
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
30
+ }
31
+
32
+ function writeJsonFile(targetPath, nextValue, dryRun) {
33
+ const serialized = jsonStringify(nextValue);
34
+ const hadExistingFile = fileExists(targetPath);
35
+ const previous = hadExistingFile ? fs.readFileSync(targetPath, 'utf8') : null;
36
+ const changed = previous !== serialized;
37
+ const backupPath = hadExistingFile && changed ? `${targetPath}.bak` : null;
38
+
39
+ if (!dryRun && changed) {
40
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
41
+ if (backupPath) {
42
+ fs.copyFileSync(targetPath, backupPath);
43
+ }
44
+ fs.writeFileSync(targetPath, serialized, 'utf8');
45
+ }
46
+
47
+ return {
48
+ changed,
49
+ targetPath,
50
+ backupPath,
51
+ dryRun,
52
+ };
53
+ }
54
+
55
+ function resolveCommand(command) {
56
+ const checker = process.platform === 'win32' ? 'where' : 'which';
57
+ const result = spawnSync(checker, [command], {
58
+ encoding: 'utf8',
59
+ stdio: ['ignore', 'pipe', 'pipe'],
60
+ });
61
+ if (result.status !== 0) return null;
62
+ const firstMatch = result.stdout
63
+ .split(/\r?\n/)
64
+ .map((line) => line.trim())
65
+ .find(Boolean);
66
+ return firstMatch || null;
67
+ }
68
+
69
+ function runCommand(command, args, cwd, tolerateFailure = false) {
70
+ const resolvedCommand = resolveCommand(command) || command;
71
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
72
+ const result = isWindowsScript
73
+ ? spawnSync(
74
+ process.env.ComSpec || 'cmd.exe',
75
+ ['/d', '/s', '/c', resolvedCommand, ...args],
76
+ {
77
+ cwd,
78
+ encoding: 'utf8',
79
+ stdio: ['ignore', 'pipe', 'pipe'],
80
+ },
81
+ )
82
+ : spawnSync(resolvedCommand, args, {
83
+ cwd,
84
+ encoding: 'utf8',
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ });
87
+
88
+ if (result.error && !tolerateFailure) {
89
+ throw result.error;
90
+ }
91
+
92
+ if (result.status !== 0 && !tolerateFailure) {
93
+ const detail = [result.stderr, result.stdout].find((value) => typeof value === 'string' && value.trim()) || '';
94
+ throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ function commandExists(command) {
101
+ return Boolean(resolveCommand(command));
102
+ }
103
+
104
+ function cursorInstalled() {
105
+ const homeDir = os.homedir();
106
+ const appData = process.env.APPDATA || '';
107
+ const localAppData = process.env.LOCALAPPDATA || '';
108
+ return [
109
+ path.join(homeDir, '.cursor'),
110
+ path.join(appData, 'Cursor'),
111
+ path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe'),
112
+ ].some(fileExists);
113
+ }
114
+
115
+ function defaultUserClients() {
116
+ const detected = [];
117
+ if (commandExists('claude')) detected.push('claude');
118
+ if (cursorInstalled()) detected.push('cursor');
119
+ if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code'))) detected.push('vscode');
120
+ return detected;
121
+ }
122
+
123
+ function parseClientOption(raw) {
124
+ return String(raw || '')
125
+ .split(',')
126
+ .map((value) => value.trim().toLowerCase())
127
+ .filter(Boolean);
128
+ }
129
+
130
+ function parseArgs(argv) {
131
+ if (argv.length === 0) {
132
+ return {
133
+ command: 'help',
134
+ options: {
135
+ accessKey: process.env.NEUS_ACCESS_KEY || '',
136
+ clients: [],
137
+ json: false,
138
+ dryRun: false,
139
+ project: false,
17
140
  },
141
+ };
142
+ }
143
+
144
+ const command = argv[0];
145
+ const options = {
146
+ accessKey: process.env.NEUS_ACCESS_KEY || '',
147
+ clients: [],
148
+ json: false,
149
+ dryRun: false,
150
+ project: false,
151
+ };
152
+
153
+ for (let index = 1; index < argv.length; index += 1) {
154
+ const token = argv[index];
155
+ if (token === '--json') {
156
+ options.json = true;
157
+ continue;
158
+ }
159
+ if (token === '--dry-run') {
160
+ options.dryRun = true;
161
+ continue;
162
+ }
163
+ if (token === '--project') {
164
+ options.project = true;
165
+ continue;
166
+ }
167
+ if (token === '--client') {
168
+ const value = argv[index + 1];
169
+ if (!value) throw new Error('--client requires a value');
170
+ options.clients.push(...parseClientOption(value));
171
+ index += 1;
172
+ continue;
173
+ }
174
+ if (token === '--access-key') {
175
+ const value = argv[index + 1];
176
+ if (!value) throw new Error('--access-key requires a value');
177
+ options.accessKey = value;
178
+ index += 1;
179
+ continue;
180
+ }
181
+ if (token === '--help' || token === '-h') {
182
+ return { command: 'help', options };
183
+ }
184
+ throw new Error(`Unknown option: ${token}`);
185
+ }
186
+
187
+ options.accessKey = String(options.accessKey || '').trim();
188
+ options.clients = [...new Set(options.clients)];
189
+
190
+ return { command, options };
191
+ }
192
+
193
+ function printUsage(exitCode = 0) {
194
+ const lines = [
195
+ 'Usage: neus <command> [options]',
196
+ '',
197
+ 'Commands:',
198
+ ' init Configure supported MCP clients automatically',
199
+ ' auth Add or update a personal access key for NEUS MCP',
200
+ ' status Show current NEUS MCP setup',
201
+ ' help Show this message',
202
+ '',
203
+ 'Options:',
204
+ ' --client <name[,name]> Limit setup to claude, cursor, or vscode',
205
+ ' --project Write shared project config instead of user config',
206
+ ' --access-key <npk_...> Configure Bearer auth for personal account tools',
207
+ ' --json Emit machine-readable output',
208
+ ' --dry-run Preview changes without writing files',
209
+ ];
210
+ const stream = exitCode === 0 ? process.stdout : process.stderr;
211
+ stream.write(`${lines.join('\n')}\n`);
212
+ process.exit(exitCode);
213
+ }
214
+
215
+ function assertValidClients(clients) {
216
+ for (const client of clients) {
217
+ if (!SUPPORTED_CLIENTS.includes(client)) {
218
+ throw new Error(`Unsupported client: ${client}`);
219
+ }
220
+ }
221
+ }
222
+
223
+ function resolveScope(options) {
224
+ return options.project ? 'project' : 'user';
225
+ }
226
+
227
+ function resolveClients(scope, requestedClients) {
228
+ assertValidClients(requestedClients);
229
+ if (requestedClients.length > 0) return requestedClients;
230
+ if (scope === 'project') return [...SUPPORTED_CLIENTS];
231
+ return defaultUserClients();
232
+ }
233
+
234
+ function ensureClientSelection(scope, clients) {
235
+ if (clients.length > 0) return;
236
+ if (scope === 'project') return;
237
+ throw new Error('No supported clients detected. Re-run with --project or use --client to target a specific client.');
238
+ }
239
+
240
+ function ensureSafeAuth(command, scope, accessKey) {
241
+ if (command === 'auth' && scope !== 'user') {
242
+ throw new Error('`neus auth` only supports user scope so access keys never land in shared project config.');
243
+ }
244
+ if (scope === 'project' && accessKey) {
245
+ throw new Error('Access keys are only supported in user scope. Remove --project or omit --access-key.');
246
+ }
247
+ }
248
+
249
+ function buildCursorServer(accessKey) {
250
+ return {
251
+ type: 'streamableHttp',
252
+ url: NEUS_MCP_URL,
253
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
254
+ };
255
+ }
256
+
257
+ function buildVsCodeServer(accessKey) {
258
+ return {
259
+ type: 'http',
260
+ url: NEUS_MCP_URL,
261
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
262
+ };
263
+ }
264
+
265
+ function buildClaudeServer(accessKey) {
266
+ return {
267
+ type: 'http',
268
+ url: NEUS_MCP_URL,
269
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
270
+ };
271
+ }
272
+
273
+ function cursorConfigPath(scope, cwd) {
274
+ return scope === 'user'
275
+ ? path.join(os.homedir(), '.cursor', 'mcp.json')
276
+ : path.join(cwd, '.cursor', 'mcp.json');
277
+ }
278
+
279
+ function vscodeConfigPath(scope, cwd) {
280
+ return scope === 'user'
281
+ ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Code', 'User', 'mcp.json')
282
+ : path.join(cwd, '.vscode', 'mcp.json');
283
+ }
284
+
285
+ function claudeProjectConfigPath(cwd) {
286
+ return path.join(cwd, '.mcp.json');
287
+ }
288
+
289
+ function installCursor(scope, accessKey, dryRun, cwd) {
290
+ const targetPath = cursorConfigPath(scope, cwd);
291
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
292
+ const next = {
293
+ ...doc,
294
+ mcpServers: {
295
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
296
+ [NEUS_SERVER_NAME]: buildCursorServer(accessKey),
297
+ },
298
+ };
299
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
300
+ return {
301
+ client: 'cursor',
302
+ scope,
303
+ configured: true,
304
+ authConfigured: Boolean(accessKey),
305
+ changed: writeResult.changed,
306
+ targetPath,
307
+ backupPath: writeResult.backupPath,
308
+ dryRun,
309
+ };
310
+ }
311
+
312
+ function installVsCode(scope, accessKey, dryRun, cwd) {
313
+ const targetPath = vscodeConfigPath(scope, cwd);
314
+ const doc = readJsonFile(targetPath, { servers: {} });
315
+ const next = {
316
+ ...doc,
317
+ servers: {
318
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers) ? doc.servers : {}),
319
+ [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey),
18
320
  },
19
321
  };
322
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
323
+ return {
324
+ client: 'vscode',
325
+ scope,
326
+ configured: true,
327
+ authConfigured: Boolean(accessKey),
328
+ changed: writeResult.changed,
329
+ targetPath,
330
+ backupPath: writeResult.backupPath,
331
+ dryRun,
332
+ };
333
+ }
334
+
335
+ function installClaudeProject(scope, accessKey, dryRun, cwd) {
336
+ const targetPath = claudeProjectConfigPath(cwd);
337
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
338
+ const next = {
339
+ ...doc,
340
+ mcpServers: {
341
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
342
+ [NEUS_SERVER_NAME]: buildClaudeServer(accessKey),
343
+ },
344
+ };
345
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
346
+ return {
347
+ client: 'claude',
348
+ scope,
349
+ configured: true,
350
+ authConfigured: Boolean(accessKey),
351
+ changed: writeResult.changed,
352
+ targetPath,
353
+ backupPath: writeResult.backupPath,
354
+ dryRun,
355
+ };
356
+ }
357
+
358
+ function installClaudeUser(scope, accessKey, dryRun, cwd) {
359
+ if (!commandExists('claude')) {
360
+ throw new Error('Claude Code CLI is not installed or not on PATH.');
361
+ }
362
+
363
+ if (!dryRun) {
364
+ runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_SERVER_NAME], cwd, true);
365
+ const addArgs = [
366
+ 'mcp',
367
+ 'add',
368
+ '--transport',
369
+ 'http',
370
+ '--scope',
371
+ 'user',
372
+ NEUS_SERVER_NAME,
373
+ NEUS_MCP_URL,
374
+ ];
375
+ if (accessKey) {
376
+ addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
377
+ }
378
+ runCommand('claude', addArgs, cwd);
379
+ }
380
+
381
+ return {
382
+ client: 'claude',
383
+ scope,
384
+ configured: true,
385
+ authConfigured: Boolean(accessKey),
386
+ changed: true,
387
+ targetPath: '~/.claude.json',
388
+ backupPath: null,
389
+ dryRun,
390
+ };
391
+ }
392
+
393
+ function installClaude(scope, accessKey, dryRun, cwd) {
394
+ if (scope === 'project') {
395
+ return installClaudeProject(scope, accessKey, dryRun, cwd);
396
+ }
397
+ return installClaudeUser(scope, accessKey, dryRun, cwd);
398
+ }
20
399
 
400
+ function installClient(client, scope, accessKey, dryRun, cwd) {
401
+ if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
402
+ if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
403
+ if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
404
+ throw new Error(`Unsupported client: ${client}`);
405
+ }
406
+
407
+ function inspectCursor(scope, cwd) {
408
+ const targetPath = cursorConfigPath(scope, cwd);
409
+ if (!fileExists(targetPath)) {
410
+ return { client: 'cursor', scope, configured: false, authConfigured: false, targetPath };
411
+ }
412
+ const doc = readJsonFile(targetPath, {});
413
+ const server = doc.mcpServers?.[NEUS_SERVER_NAME];
414
+ return {
415
+ client: 'cursor',
416
+ scope,
417
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
418
+ authConfigured: Boolean(server?.headers?.Authorization),
419
+ targetPath,
420
+ };
421
+ }
422
+
423
+ function inspectVsCode(scope, cwd) {
424
+ const targetPath = vscodeConfigPath(scope, cwd);
425
+ if (!fileExists(targetPath)) {
426
+ return { client: 'vscode', scope, configured: false, authConfigured: false, targetPath };
427
+ }
428
+ const doc = readJsonFile(targetPath, {});
429
+ const server = doc.servers?.[NEUS_SERVER_NAME];
430
+ return {
431
+ client: 'vscode',
432
+ scope,
433
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
434
+ authConfigured: Boolean(server?.headers?.Authorization),
435
+ targetPath,
436
+ };
437
+ }
438
+
439
+ function inspectClaude(scope, cwd) {
440
+ if (scope === 'project') {
441
+ const targetPath = claudeProjectConfigPath(cwd);
442
+ if (!fileExists(targetPath)) {
443
+ return { client: 'claude', scope, configured: false, authConfigured: false, targetPath };
444
+ }
445
+ const doc = readJsonFile(targetPath, {});
446
+ const server = doc.mcpServers?.[NEUS_SERVER_NAME];
447
+ return {
448
+ client: 'claude',
449
+ scope,
450
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
451
+ authConfigured: Boolean(server?.headers?.Authorization),
452
+ targetPath,
453
+ };
454
+ }
455
+
456
+ if (!commandExists('claude')) {
457
+ return { client: 'claude', scope, configured: false, authConfigured: null, targetPath: '~/.claude.json' };
458
+ }
459
+
460
+ const result = runCommand('claude', ['mcp', 'list'], cwd, true);
461
+ const configured = result.status === 0 && result.stdout.split(/\r?\n/).some((line) => line.trim() === NEUS_SERVER_NAME);
462
+ return {
463
+ client: 'claude',
464
+ scope,
465
+ configured,
466
+ authConfigured: null,
467
+ targetPath: '~/.claude.json',
468
+ };
469
+ }
470
+
471
+ function inspectClient(client, scope, cwd) {
472
+ if (client === 'cursor') return inspectCursor(scope, cwd);
473
+ if (client === 'vscode') return inspectVsCode(scope, cwd);
474
+ if (client === 'claude') return inspectClaude(scope, cwd);
475
+ throw new Error(`Unsupported client: ${client}`);
476
+ }
477
+
478
+ function printJson(payload) {
479
+ process.stdout.write(jsonStringify(payload));
480
+ }
481
+
482
+ function printResultSummary(command, scope, results, accessKey) {
483
+ const changedCount = results.filter((result) => result.changed).length;
484
+ const configuredClients = results.map((result) => result.client).join(', ');
21
485
  const lines = [
22
- "# NEUS",
23
- "",
24
- "## MCP",
25
- JSON.stringify(mcpBlock, null, 2),
26
- "",
27
- "## URLs",
28
- "MCP: https://mcp.neus.network/mcp",
29
- "Verify: https://neus.network/verify",
30
- "Explorer: https://neus.network/hub",
31
- "",
32
- "## Docs",
33
- "MCP setup: https://docs.neus.network/mcp/setup",
34
- "LLM / assistants: https://docs.neus.network/platform/llm-docs",
35
- "Agents: https://docs.neus.network/agents/overview",
36
- "Agent identity: https://docs.neus.network/agents/agent-identity",
37
- "Agent delegation: https://docs.neus.network/agents/agent-delegation",
38
- "Integration: https://docs.neus.network/integration",
39
- "Quickstart: https://docs.neus.network/quickstart",
40
- "Machine route map: https://neus.network/llms.txt",
41
- "",
42
- "This command only prints to stdout; it does not modify files or create accounts.",
43
- "Use Authorization: Bearer <access key> when a tool returns auth_required (see MCP auth).",
486
+ `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
487
+ `Configured: ${configuredClients || 'none'}.`,
44
488
  ];
45
489
 
46
- process.stdout.write(`${lines.join("\n")}\n`);
490
+ if (changedCount > 0) {
491
+ lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
492
+ }
493
+
494
+ if (command === 'init' && !accessKey) {
495
+ lines.push(`Account tools stay optional. Add personal auth later with: neus auth --access-key <npk_...>`);
496
+ }
497
+ if ((command === 'init' || command === 'auth') && accessKey) {
498
+ lines.push('Personal account tools are enabled where the client supports user-scope auth setup.');
499
+ }
500
+ if (command === 'status') {
501
+ const enabled = results.filter((result) => result.configured).map((result) => result.client);
502
+ lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
503
+ }
504
+
505
+ process.stdout.write(`${lines.join('\n')}\n`);
47
506
  }
48
507
 
49
- if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
50
- usage();
508
+ function runInit(options) {
509
+ const scope = resolveScope(options);
510
+ ensureSafeAuth('init', scope, options.accessKey);
511
+
512
+ const clients = resolveClients(scope, options.clients);
513
+ ensureClientSelection(scope, clients);
514
+
515
+ const results = clients.map((client) => installClient(client, scope, options.accessKey, options.dryRun, process.cwd()));
516
+ const payload = {
517
+ command: 'init',
518
+ scope,
519
+ detectedClients: defaultUserClients(),
520
+ clients,
521
+ accessKeyConfigured: Boolean(options.accessKey),
522
+ results,
523
+ };
524
+
525
+ if (options.json) {
526
+ printJson(payload);
527
+ return;
528
+ }
529
+ printResultSummary('init', scope, results, options.accessKey);
530
+ }
531
+
532
+ function runAuth(options) {
533
+ const scope = resolveScope(options);
534
+ ensureSafeAuth('auth', scope, options.accessKey);
535
+ if (!options.accessKey) {
536
+ throw new Error(`Missing access key. Create one at ${NEUS_ACCESS_KEYS_URL} and rerun neus auth --access-key <npk_...>.`);
537
+ }
538
+
539
+ const clients = resolveClients(scope, options.clients);
540
+ ensureClientSelection(scope, clients);
541
+
542
+ const results = clients.map((client) => installClient(client, scope, options.accessKey, options.dryRun, process.cwd()));
543
+ const payload = {
544
+ command: 'auth',
545
+ scope,
546
+ clients,
547
+ accessKeyConfigured: true,
548
+ results,
549
+ };
550
+
551
+ if (options.json) {
552
+ printJson(payload);
553
+ return;
554
+ }
555
+ printResultSummary('auth', scope, results, options.accessKey);
51
556
  }
52
557
 
53
- if (sub === "init") {
54
- printInit();
55
- process.exit(0);
558
+ function runStatus(options) {
559
+ const scope = resolveScope(options);
560
+ const clients = resolveClients(scope, options.clients);
561
+ ensureClientSelection(scope, clients);
562
+
563
+ const inspected = clients.map((client) => inspectClient(client, scope, process.cwd()));
564
+ const payload = {
565
+ command: 'status',
566
+ scope,
567
+ clients: inspected,
568
+ };
569
+
570
+ if (options.json) {
571
+ printJson(payload);
572
+ return;
573
+ }
574
+ printResultSummary('status', scope, inspected, '');
575
+ }
576
+
577
+ function main() {
578
+ try {
579
+ const { command, options } = parseArgs(process.argv.slice(2));
580
+
581
+ if (command === 'help') {
582
+ printUsage(0);
583
+ return;
584
+ }
585
+ if (command === 'init') {
586
+ runInit(options);
587
+ return;
588
+ }
589
+ if (command === 'auth') {
590
+ runAuth(options);
591
+ return;
592
+ }
593
+ if (command === 'status') {
594
+ runStatus(options);
595
+ return;
596
+ }
597
+
598
+ process.stderr.write(`Unknown subcommand: ${command}\n`);
599
+ printUsage(1);
600
+ } catch (error) {
601
+ process.stderr.write(`${error?.message || 'Unknown error'}\n`);
602
+ process.exit(1);
603
+ }
56
604
  }
57
605
 
58
- usage();
606
+ main();
package/client.js CHANGED
@@ -25,7 +25,13 @@ const FALLBACK_PUBLIC_VERIFIER_CATALOG = {
25
25
  'ai-content-moderation': { supportsDirectApi: true }
26
26
  };
27
27
 
28
- const EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
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
+ }
29
35
 
30
36
  const validateVerifierData = (verifierId, data) => {
31
37
  if (!data || typeof data !== 'object') {
@@ -477,22 +483,98 @@ export class NeusClient {
477
483
  };
478
484
  }
479
485
 
480
- async createGatePrivateAuth(params = {}) {
481
- const address = (params.address || '').toString();
482
- if (!validateUniversalAddress(address, params.chain)) {
483
- throw new ValidationError('Valid address is required');
484
- }
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
+ }
485
491
  return this._buildPrivateGateAuth({
486
492
  address,
487
493
  wallet: params.wallet,
488
494
  chain: params.chain,
489
- signatureMethod: params.signatureMethod
490
- });
491
- }
492
-
493
- /**
494
- * Create a verification proof.
495
- *
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
+ *
496
578
  * Supports two paths:
497
579
  * - **Auto:** Supply `verifier`, `content`, and/or `data` with an optional
498
580
  * `wallet` provider. The SDK performs signing in the client.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@neus/sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Portable trust for people, apps, and agents. Store a proof ID; check gates across surfaces.",
5
5
  "bin": {
6
- "neus": "./cli/neus.mjs"
6
+ "neus": "cli/neus.mjs"
7
7
  },
8
8
  "main": "index.js",
9
9
  "type": "module",
package/types.d.ts CHANGED
@@ -54,6 +54,20 @@ declare module '@neus/sdk' {
54
54
 
55
55
  /** Get the public verifier catalog, including per-verifier capabilities. */
56
56
  getVerifierCatalog(): Promise<VerifierCatalog>;
57
+
58
+ /**
59
+ * Build and sign a wallet-link verifier payload with the secondary wallet.
60
+ * Use this for advanced direct/API flows; browser user-facing flows should still prefer hosted `/verify`.
61
+ */
62
+ createWalletLinkData(params: {
63
+ primaryWalletAddress: string;
64
+ secondaryWalletAddress: string;
65
+ wallet?: WalletLike;
66
+ chain?: string;
67
+ signedTimestamp?: number;
68
+ relationshipType?: 'primary' | 'personal' | 'org' | 'affiliate' | 'agent' | 'linked';
69
+ label?: string;
70
+ }): Promise<WalletLinkData>;
57
71
 
58
72
  /**
59
73
  * Poll verification status until completion
@@ -548,7 +548,7 @@ function VerifyGate({
548
548
  if (verifierId === "wallet-link") {
549
549
  if (!explicit?.secondaryWalletAddress || !explicit?.signature || !explicit?.chain || !explicit?.signatureMethod) {
550
550
  throw new Error(
551
- "wallet-link requires verifierData: { secondaryWalletAddress, signature, chain, signatureMethod }"
551
+ "wallet-link direct mode requires verifierData: { secondaryWalletAddress, signature, chain, signatureMethod }. For user-facing flows, prefer hosted checkout."
552
552
  );
553
553
  }
554
554
  return explicit;