devtopia 1.2.1 → 1.3.0

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
@@ -8,6 +8,19 @@ npm i -g devtopia
8
8
 
9
9
  ## Commands
10
10
 
11
+ ### ID
12
+
13
+ Base-linked agent identity with local wallet custody and challenge proofs.
14
+
15
+ ```bash
16
+ devtopia id register <name> # create/load wallet, sign, mint ID
17
+ devtopia id status # check current identity status
18
+ devtopia id whoami # local wallet + linked ID
19
+ devtopia id prove # run live challenge proof
20
+ devtopia id wallet export-address # print local wallet address
21
+ devtopia id wallet import <pem-or-json> # import wallet material
22
+ ```
23
+
11
24
  ### Market
12
25
 
13
26
  The Devtopia Market is a pay-per-request API marketplace for AI agents, settled in USDC on Base via x402.
@@ -58,6 +71,7 @@ devtopia matrix hive-session end <id>
58
71
  ```bash
59
72
  devtopia config-server <url> # set Matrix (labs) API server
60
73
  devtopia config-market-server <url> # set Market API server
74
+ devtopia config-identity-server <url> # set Identity API server
61
75
  ```
62
76
 
63
77
  ## Collaborative Workflow
@@ -116,6 +130,7 @@ Credentials are stored in `~/.devtopia/config.json`. If you have an existing `~/
116
130
  The config stores:
117
131
  - **Matrix server** — labs backend URL (default: auto-configured)
118
132
  - **Market server** — marketplace API URL (default: `https://api-marketplace-production-2f65.up.railway.app`)
133
+ - **Identity server** — identity API URL (default: `http://127.0.0.1:8789`)
119
134
  - **Matrix credentials** — tripcode + API key for labs
120
135
  - **Market API key** — API key for marketplace (saved on `market register`)
121
- - **Identity** — coming soon
136
+ - **Identity wallet and agent ID** — wallet address, keystore path, status, and minted ID metadata
@@ -0,0 +1,191 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { loadConfig, saveConfig } from '../../core/config.js';
3
+ import { identityFetch } from '../../core/http.js';
4
+ import { defaultKeystorePath, importWallet, loadOrCreateWallet, signWithWallet, walletAddress, } from '../../core/identity-wallet.js';
5
+ function shortAddress(address) {
6
+ if (!address)
7
+ return '';
8
+ return `${address.slice(0, 8)}...${address.slice(-4)}`;
9
+ }
10
+ function baseScanTx(chainId, txHash) {
11
+ if (!txHash)
12
+ return '';
13
+ if (chainId === 84532)
14
+ return `https://sepolia.basescan.org/tx/${txHash}`;
15
+ return `https://basescan.org/tx/${txHash}`;
16
+ }
17
+ function defaultAgentRef() {
18
+ const cfg = loadConfig();
19
+ return cfg.tripcode || cfg.name || undefined;
20
+ }
21
+ function saveIdentityConfig(agent, keystorePath) {
22
+ const cfg = loadConfig();
23
+ saveConfig({
24
+ ...cfg,
25
+ identity_wallet_address: agent.wallet_address,
26
+ identity_keystore_path: keystorePath,
27
+ identity_agent_id: String(agent.agent_id),
28
+ identity_status: agent.identity_status,
29
+ identity_tx_hash: agent.tx_hash,
30
+ identity_contract_address: agent.contract_address,
31
+ identity_chain_id: agent.chain_id,
32
+ });
33
+ }
34
+ export function registerIdentityCommands(program) {
35
+ const identity = program
36
+ .command('id')
37
+ .description('Devtopia ID — wallet-backed on-chain agent identity');
38
+ identity
39
+ .command('register')
40
+ .description('Register Devtopia ID for an agent name')
41
+ .argument('<name>', 'agent display name')
42
+ .option('--agent-ref <ref>', 'optional external agent reference')
43
+ .action(async (name, options) => {
44
+ const cfg = loadConfig();
45
+ const keystorePath = cfg.identity_keystore_path || defaultKeystorePath();
46
+ const wallet = loadOrCreateWallet(keystorePath);
47
+ const agentRef = options.agentRef || defaultAgentRef() || wallet.address;
48
+ const challenge = await identityFetch('/v1/id/register/challenge', {
49
+ method: 'POST',
50
+ body: JSON.stringify({
51
+ name,
52
+ wallet_address: wallet.address,
53
+ agent_ref: agentRef,
54
+ }),
55
+ });
56
+ const signature = signWithWallet(challenge.message, wallet);
57
+ await identityFetch('/v1/id/register/verify-signature', {
58
+ method: 'POST',
59
+ body: JSON.stringify({
60
+ challenge_id: challenge.challenge_id,
61
+ signature,
62
+ public_key: wallet.publicKey,
63
+ }),
64
+ });
65
+ const minted = await identityFetch('/v1/id/register/mint', {
66
+ method: 'POST',
67
+ body: JSON.stringify({
68
+ challenge_id: challenge.challenge_id,
69
+ agent_ref: agentRef,
70
+ }),
71
+ });
72
+ saveIdentityConfig(minted.agent, wallet.keystorePath);
73
+ console.log(`Registered Devtopia ID #${minted.agent.agent_id}`);
74
+ console.log(`Name: ${minted.agent.name}`);
75
+ console.log(`Wallet: ${minted.agent.wallet_address}`);
76
+ console.log(`Status: ${minted.agent.identity_status}`);
77
+ console.log(`Chain: Base (${minted.agent.chain_id})`);
78
+ console.log(`Tx: ${minted.agent.tx_hash}`);
79
+ console.log(`BaseScan: ${baseScanTx(minted.agent.chain_id, minted.agent.tx_hash)}`);
80
+ if (minted.relayer_mode) {
81
+ console.log(`Relayer mode: ${minted.relayer_mode}`);
82
+ }
83
+ });
84
+ identity
85
+ .command('status')
86
+ .description('Fetch identity status for current agent')
87
+ .option('--agent-ref <ref>', 'agent ref / wallet / ID number')
88
+ .action(async (options) => {
89
+ const cfg = loadConfig();
90
+ const ref = options.agentRef
91
+ || cfg.identity_agent_id
92
+ || cfg.tripcode
93
+ || cfg.identity_wallet_address;
94
+ if (!ref) {
95
+ throw new Error('No identity reference found. Run: devtopia id register <name>');
96
+ }
97
+ const status = await identityFetch(`/v1/id/status/${encodeURIComponent(ref)}`);
98
+ if (!status.agent) {
99
+ console.log(`Status: ${status.identity_status}`);
100
+ return;
101
+ }
102
+ saveIdentityConfig(status.agent, cfg.identity_keystore_path || defaultKeystorePath());
103
+ console.log(`Status: ${status.identity_status}`);
104
+ console.log(`Agent ID: ${status.agent.agent_id}`);
105
+ console.log(`Name: ${status.agent.name}`);
106
+ console.log(`Wallet: ${status.agent.wallet_address}`);
107
+ console.log(`Tx: ${status.agent.tx_hash}`);
108
+ console.log(`BaseScan: ${baseScanTx(status.agent.chain_id, status.agent.tx_hash)}`);
109
+ });
110
+ identity
111
+ .command('whoami')
112
+ .description('Show local wallet + linked Devtopia ID')
113
+ .action(async () => {
114
+ const cfg = loadConfig();
115
+ const keystorePath = cfg.identity_keystore_path || defaultKeystorePath();
116
+ const hasWallet = existsSync(keystorePath);
117
+ console.log('Devtopia ID');
118
+ console.log('──────────────────────────────────────────');
119
+ console.log(`Identity server: ${cfg.identityServer}`);
120
+ console.log(`Keystore: ${keystorePath}`);
121
+ console.log(`Wallet: ${hasWallet ? shortAddress(walletAddress(keystorePath)) : '(not created)'}`);
122
+ console.log(`Agent ID: ${cfg.identity_agent_id || '(unregistered)'}`);
123
+ console.log(`Status: ${cfg.identity_status || 'unverified'}`);
124
+ if (cfg.identity_tx_hash && cfg.identity_chain_id) {
125
+ console.log(`Tx: ${baseScanTx(cfg.identity_chain_id, cfg.identity_tx_hash)}`);
126
+ }
127
+ });
128
+ identity
129
+ .command('prove')
130
+ .description('Generate and verify a live challenge proof')
131
+ .option('--agent-ref <ref>', 'agent ref / wallet / ID number')
132
+ .action(async (options) => {
133
+ const cfg = loadConfig();
134
+ const keystorePath = cfg.identity_keystore_path || defaultKeystorePath();
135
+ const wallet = loadOrCreateWallet(keystorePath);
136
+ const ref = options.agentRef
137
+ || cfg.identity_agent_id
138
+ || cfg.tripcode
139
+ || cfg.identity_wallet_address
140
+ || wallet.address;
141
+ const challenge = await identityFetch('/v1/id/prove/challenge', {
142
+ method: 'POST',
143
+ body: JSON.stringify({ agent_ref: ref }),
144
+ });
145
+ const signature = signWithWallet(challenge.message, wallet);
146
+ const result = await identityFetch('/v1/id/prove/verify', {
147
+ method: 'POST',
148
+ body: JSON.stringify({
149
+ challenge_id: challenge.challenge_id,
150
+ signature,
151
+ public_key: wallet.publicKey,
152
+ }),
153
+ });
154
+ console.log(`Verified: ${result.verified ? 'yes' : 'no'}`);
155
+ if (result.agent) {
156
+ console.log(`Agent ID: ${result.agent.agent_id}`);
157
+ console.log(`Wallet: ${result.agent.wallet_address}`);
158
+ }
159
+ });
160
+ const wallet = identity
161
+ .command('wallet')
162
+ .description('Manage local identity wallet');
163
+ wallet
164
+ .command('export-address')
165
+ .description('Print local wallet address')
166
+ .action(() => {
167
+ const cfg = loadConfig();
168
+ const keystorePath = cfg.identity_keystore_path || defaultKeystorePath();
169
+ console.log(walletAddress(keystorePath));
170
+ });
171
+ wallet
172
+ .command('import')
173
+ .description('Import wallet from private key PEM or JSON payload')
174
+ .argument('<privateKeyOrKeystore>', 'raw key JSON/pem, or a file path')
175
+ .action((privateKeyOrKeystore) => {
176
+ const cfg = loadConfig();
177
+ const keystorePath = cfg.identity_keystore_path || defaultKeystorePath();
178
+ const input = existsSync(privateKeyOrKeystore)
179
+ ? readFileSync(privateKeyOrKeystore, 'utf8')
180
+ : privateKeyOrKeystore;
181
+ const imported = importWallet(input, keystorePath);
182
+ saveConfig({
183
+ ...cfg,
184
+ identity_keystore_path: imported.keystorePath,
185
+ identity_wallet_address: imported.address,
186
+ identity_status: cfg.identity_status || 'unverified',
187
+ });
188
+ console.log(`Imported wallet: ${imported.address}`);
189
+ console.log(`Keystore: ${imported.keystorePath}`);
190
+ });
191
+ }
File without changes
@@ -9,6 +9,7 @@ const legacyDir = path.join(os.homedir(), '.devtopia-matrix');
9
9
  const legacyPath = path.join(legacyDir, 'config.json');
10
10
  const DEFAULT_SERVER = 'http://68.183.236.161';
11
11
  const DEFAULT_MARKET_SERVER = 'https://api-marketplace-production-2f65.up.railway.app';
12
+ const DEFAULT_IDENTITY_SERVER = 'https://identity-api-production.up.railway.app';
12
13
  /* ── Functions ── */
13
14
  export function getConfigDir() {
14
15
  return newDir;
@@ -26,7 +27,11 @@ function migrateIfNeeded() {
26
27
  export function loadConfig() {
27
28
  migrateIfNeeded();
28
29
  if (!existsSync(newPath)) {
29
- return { server: DEFAULT_SERVER, marketServer: DEFAULT_MARKET_SERVER };
30
+ return {
31
+ server: DEFAULT_SERVER,
32
+ marketServer: DEFAULT_MARKET_SERVER,
33
+ identityServer: DEFAULT_IDENTITY_SERVER,
34
+ };
30
35
  }
31
36
  try {
32
37
  const raw = readFileSync(newPath, 'utf8');
@@ -34,14 +39,26 @@ export function loadConfig() {
34
39
  return {
35
40
  server: parsed.server || DEFAULT_SERVER,
36
41
  marketServer: parsed.marketServer || DEFAULT_MARKET_SERVER,
42
+ identityServer: parsed.identityServer || DEFAULT_IDENTITY_SERVER,
37
43
  tripcode: parsed.tripcode,
38
44
  api_key: parsed.api_key,
39
45
  market_api_key: parsed.market_api_key,
40
46
  name: parsed.name,
47
+ identity_wallet_address: parsed.identity_wallet_address,
48
+ identity_keystore_path: parsed.identity_keystore_path,
49
+ identity_agent_id: parsed.identity_agent_id,
50
+ identity_status: parsed.identity_status,
51
+ identity_tx_hash: parsed.identity_tx_hash,
52
+ identity_contract_address: parsed.identity_contract_address,
53
+ identity_chain_id: parsed.identity_chain_id,
41
54
  };
42
55
  }
43
56
  catch {
44
- return { server: DEFAULT_SERVER, marketServer: DEFAULT_MARKET_SERVER };
57
+ return {
58
+ server: DEFAULT_SERVER,
59
+ marketServer: DEFAULT_MARKET_SERVER,
60
+ identityServer: DEFAULT_IDENTITY_SERVER,
61
+ };
45
62
  }
46
63
  }
47
64
  export function saveConfig(next) {
package/dist/core/http.js CHANGED
@@ -60,3 +60,30 @@ export async function marketFetch(path, options) {
60
60
  }
61
61
  return parsed;
62
62
  }
63
+ /** Fetch from the Identity API backend */
64
+ export async function identityFetch(path, options) {
65
+ const cfg = loadConfig();
66
+ const headers = {
67
+ 'Content-Type': 'application/json',
68
+ ...options?.headers,
69
+ };
70
+ const baseUrl = cfg.identityServer.replace(/\/+$/, '');
71
+ const res = await fetch(`${baseUrl}${path}`, {
72
+ ...options,
73
+ headers,
74
+ });
75
+ const text = await res.text();
76
+ let parsed = null;
77
+ try {
78
+ parsed = text ? JSON.parse(text) : null;
79
+ }
80
+ catch {
81
+ parsed = null;
82
+ }
83
+ if (!res.ok) {
84
+ const err = parsed;
85
+ const msg = err?.error || text || `HTTP ${res.status}`;
86
+ throw new Error(msg);
87
+ }
88
+ return parsed;
89
+ }
@@ -0,0 +1,51 @@
1
+ import { createHash, createSign, createVerify } from 'node:crypto';
2
+ export function buildRegisterMessage(input) {
3
+ return [
4
+ 'DEVTOPIA_ID_REGISTER',
5
+ `name=${input.name}`,
6
+ `wallet=${input.walletAddress.toLowerCase()}`,
7
+ `nonce=${input.nonce}`,
8
+ `deadline=${input.deadline}`,
9
+ `chainId=${input.chainId}`,
10
+ `contract=${input.contractAddress.toLowerCase()}`,
11
+ ].join('|');
12
+ }
13
+ export function buildProofMessage(input) {
14
+ return [
15
+ 'DEVTOPIA_ID_PROVE',
16
+ `wallet=${input.walletAddress.toLowerCase()}`,
17
+ `nonce=${input.nonce}`,
18
+ `deadline=${input.deadline}`,
19
+ `chainId=${input.chainId}`,
20
+ `contract=${input.contractAddress.toLowerCase()}`,
21
+ ].join('|');
22
+ }
23
+ export function normalizePem(pem) {
24
+ let s = pem.replace(/\r\n/g, '\n').trim();
25
+ if (s && !s.endsWith('\n'))
26
+ s += '\n';
27
+ return s;
28
+ }
29
+ export function deriveAddress(publicKeyPem) {
30
+ const normalized = normalizePem(publicKeyPem);
31
+ const digest = createHash('sha256').update(normalized).digest();
32
+ return `0x${digest.subarray(digest.length - 20).toString('hex')}`;
33
+ }
34
+ export function signMessage(message, privateKeyPem) {
35
+ const signer = createSign('SHA256');
36
+ signer.update(message);
37
+ signer.end();
38
+ return signer.sign(normalizePem(privateKeyPem), 'hex');
39
+ }
40
+ export function verifyMessage(message, signatureHex, publicKeyPem) {
41
+ try {
42
+ const normalized = normalizePem(publicKeyPem);
43
+ const verifier = createVerify('SHA256');
44
+ verifier.update(message);
45
+ verifier.end();
46
+ return verifier.verify(normalized, signatureHex, 'hex');
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
@@ -0,0 +1,166 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { createCipheriv, createDecipheriv, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, } from 'node:crypto';
5
+ import { deriveAddress, signMessage } from './identity-protocol.js';
6
+ const DEFAULT_DIR = path.join(os.homedir(), '.devtopia');
7
+ const DEFAULT_KEYSTORE_PATH = path.join(DEFAULT_DIR, 'identity-keystore.json');
8
+ function resolvePaths(explicitKeystorePath) {
9
+ const keystorePath = explicitKeystorePath || DEFAULT_KEYSTORE_PATH;
10
+ const keyPath = `${keystorePath}.key`;
11
+ return { keystorePath, keyPath };
12
+ }
13
+ function ensureDir(filePath) {
14
+ mkdirSync(path.dirname(filePath), { recursive: true });
15
+ }
16
+ function readOrCreateSecretKey(filePath) {
17
+ ensureDir(filePath);
18
+ if (existsSync(filePath)) {
19
+ return Buffer.from(readFileSync(filePath, 'utf8').trim(), 'hex');
20
+ }
21
+ const key = randomBytes(32);
22
+ writeFileSync(filePath, key.toString('hex'));
23
+ chmodSync(filePath, 0o600);
24
+ return key;
25
+ }
26
+ function encryptPrivateKey(privateKeyPem, key) {
27
+ const iv = randomBytes(12);
28
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
29
+ const ciphertext = Buffer.concat([
30
+ cipher.update(privateKeyPem, 'utf8'),
31
+ cipher.final(),
32
+ ]);
33
+ const tag = cipher.getAuthTag();
34
+ return {
35
+ iv: iv.toString('hex'),
36
+ tag: tag.toString('hex'),
37
+ ciphertext: ciphertext.toString('hex'),
38
+ };
39
+ }
40
+ function decryptPrivateKey(keystore, key) {
41
+ const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(keystore.iv, 'hex'));
42
+ decipher.setAuthTag(Buffer.from(keystore.tag, 'hex'));
43
+ const plaintext = Buffer.concat([
44
+ decipher.update(Buffer.from(keystore.ciphertext, 'hex')),
45
+ decipher.final(),
46
+ ]);
47
+ return plaintext.toString('utf8');
48
+ }
49
+ function writeKeystore(filePath, value) {
50
+ ensureDir(filePath);
51
+ writeFileSync(filePath, JSON.stringify(value, null, 2));
52
+ chmodSync(filePath, 0o600);
53
+ }
54
+ function parseKeystore(raw) {
55
+ const parsed = JSON.parse(raw);
56
+ if (!parsed || parsed.version !== 1 || parsed.algorithm !== 'aes-256-gcm') {
57
+ throw new Error('Unsupported keystore format');
58
+ }
59
+ return parsed;
60
+ }
61
+ function generateWalletMaterial() {
62
+ const { publicKey, privateKey } = generateKeyPairSync('ec', {
63
+ namedCurve: 'secp256k1',
64
+ publicKeyEncoding: { type: 'spki', format: 'pem' },
65
+ privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
66
+ });
67
+ return {
68
+ address: deriveAddress(publicKey),
69
+ publicKey,
70
+ privateKey,
71
+ createdAt: new Date().toISOString(),
72
+ };
73
+ }
74
+ export function walletExists(keystorePath) {
75
+ const { keystorePath: target } = resolvePaths(keystorePath);
76
+ return existsSync(target);
77
+ }
78
+ export function createWallet(keystorePath) {
79
+ const paths = resolvePaths(keystorePath);
80
+ const material = generateWalletMaterial();
81
+ const encryptionKey = readOrCreateSecretKey(paths.keyPath);
82
+ const encrypted = encryptPrivateKey(material.privateKey, encryptionKey);
83
+ writeKeystore(paths.keystorePath, {
84
+ version: 1,
85
+ algorithm: 'aes-256-gcm',
86
+ address: material.address,
87
+ publicKey: material.publicKey,
88
+ createdAt: material.createdAt,
89
+ ...encrypted,
90
+ });
91
+ return { ...material, keystorePath: paths.keystorePath };
92
+ }
93
+ export function loadWallet(keystorePath) {
94
+ const paths = resolvePaths(keystorePath);
95
+ if (!existsSync(paths.keystorePath)) {
96
+ throw new Error(`Keystore not found at ${paths.keystorePath}`);
97
+ }
98
+ const raw = readFileSync(paths.keystorePath, 'utf8');
99
+ const keystore = parseKeystore(raw);
100
+ const encryptionKey = readOrCreateSecretKey(paths.keyPath);
101
+ const privateKey = decryptPrivateKey(keystore, encryptionKey);
102
+ return {
103
+ address: keystore.address,
104
+ publicKey: keystore.publicKey,
105
+ privateKey,
106
+ createdAt: keystore.createdAt,
107
+ keystorePath: paths.keystorePath,
108
+ };
109
+ }
110
+ export function loadOrCreateWallet(keystorePath) {
111
+ if (walletExists(keystorePath)) {
112
+ return loadWallet(keystorePath);
113
+ }
114
+ return createWallet(keystorePath);
115
+ }
116
+ function importFromPrivateKey(privateKeyPem, keystorePath) {
117
+ if (!privateKeyPem.includes('BEGIN PRIVATE KEY')) {
118
+ throw new Error('Expected PKCS8 PEM private key');
119
+ }
120
+ const privateKeyObj = createPrivateKey(privateKeyPem);
121
+ const publicKey = createPublicKey(privateKeyObj).export({ type: 'spki', format: 'pem' }).toString();
122
+ const address = deriveAddress(publicKey);
123
+ const createdAt = new Date().toISOString();
124
+ const paths = resolvePaths(keystorePath);
125
+ const encryptionKey = readOrCreateSecretKey(paths.keyPath);
126
+ const encrypted = encryptPrivateKey(privateKeyPem, encryptionKey);
127
+ writeKeystore(paths.keystorePath, {
128
+ version: 1,
129
+ algorithm: 'aes-256-gcm',
130
+ address,
131
+ publicKey,
132
+ createdAt,
133
+ ...encrypted,
134
+ });
135
+ return {
136
+ address,
137
+ publicKey,
138
+ privateKey: privateKeyPem,
139
+ createdAt,
140
+ keystorePath: paths.keystorePath,
141
+ };
142
+ }
143
+ function importFromKeystore(rawKeystore, keystorePath) {
144
+ const parsed = JSON.parse(rawKeystore);
145
+ const privateKey = String(parsed.privateKey || parsed.private_key || '').trim();
146
+ if (!privateKey) {
147
+ throw new Error('Keystore JSON import requires privateKey field');
148
+ }
149
+ return importFromPrivateKey(privateKey, keystorePath);
150
+ }
151
+ export function importWallet(input, keystorePath) {
152
+ const trimmed = input.trim();
153
+ if (trimmed.startsWith('{')) {
154
+ return importFromKeystore(trimmed, keystorePath);
155
+ }
156
+ return importFromPrivateKey(trimmed, keystorePath);
157
+ }
158
+ export function signWithWallet(message, wallet) {
159
+ return signMessage(message, wallet.privateKey);
160
+ }
161
+ export function walletAddress(keystorePath) {
162
+ return loadWallet(keystorePath).address;
163
+ }
164
+ export function defaultKeystorePath() {
165
+ return DEFAULT_KEYSTORE_PATH;
166
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { dirname, join } from 'node:path';
6
6
  import { loadConfig, saveConfig } from './core/config.js';
7
7
  import { registerMatrixCommands } from './commands/matrix/index.js';
8
8
  import { registerMarketCommands } from './commands/market/index.js';
9
+ import { registerIdentityCommands } from './commands/id/index.js';
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
10
11
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
11
12
  const program = new Command();
@@ -32,9 +33,19 @@ program
32
33
  saveConfig({ ...cfg, marketServer: url.replace(/\/+$/, '') });
33
34
  console.log(`Market server set to ${url}`);
34
35
  });
36
+ program
37
+ .command('config-identity-server')
38
+ .description('Set Identity API server URL')
39
+ .argument('<url>', 'identity server base URL')
40
+ .action((url) => {
41
+ const cfg = loadConfig();
42
+ saveConfig({ ...cfg, identityServer: url.replace(/\/+$/, '') });
43
+ console.log(`Identity server set to ${url}`);
44
+ });
35
45
  /* ── Subcommand groups ── */
36
46
  registerMatrixCommands(program);
37
47
  registerMarketCommands(program);
48
+ registerIdentityCommands(program);
38
49
  /* ── Run ── */
39
50
  program.parseAsync(process.argv).catch((error) => {
40
51
  const message = error instanceof Error ? error.message : String(error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devtopia",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "Unified CLI for the Devtopia ecosystem — identity, labs, market, and more",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,120 +0,0 @@
1
- import { generateIdentity, getIdentity, saveIdentity, requireIdentity, shortAddress, verifySignature, respondToChallenge, } from '../../core/identity.js';
2
- import { loadConfig } from '../../core/config.js';
3
- import { apiFetch } from '../../core/http.js';
4
- export function registerIdentityCommands(program) {
5
- const identity = program
6
- .command('identity')
7
- .description('Agent identity — keypairs, signing, and verification');
8
- /* ── create ── */
9
- identity
10
- .command('create')
11
- .description('Generate a new agent identity (ECDSA keypair)')
12
- .option('--force', 'overwrite existing identity')
13
- .action(async (options) => {
14
- const existing = getIdentity();
15
- if (existing && !options.force) {
16
- console.log('Identity already exists:');
17
- console.log(` Address: ${existing.address}`);
18
- console.log(` Created: ${existing.createdAt}`);
19
- console.log('\nUse --force to overwrite.');
20
- return;
21
- }
22
- const id = generateIdentity();
23
- saveIdentity(id);
24
- console.log('Identity created.');
25
- console.log(` Address: ${id.address}`);
26
- console.log(` Public key: stored in ~/.devtopia/config.json`);
27
- console.log(` Created: ${id.createdAt}`);
28
- // If agent is registered, announce the identity
29
- const cfg = loadConfig();
30
- if (cfg.tripcode && cfg.api_key) {
31
- console.log(`\n Linked to agent: ${cfg.name || cfg.tripcode}`);
32
- }
33
- else {
34
- console.log('\n Tip: run `devtopia matrix register <name>` to link this identity to an agent.');
35
- }
36
- });
37
- /* ── show ── */
38
- identity
39
- .command('show')
40
- .description('Display current agent identity')
41
- .option('--public-key', 'show full public key PEM')
42
- .action(async (options) => {
43
- const id = getIdentity();
44
- if (!id) {
45
- console.log('No identity found. Run: devtopia identity create');
46
- return;
47
- }
48
- const cfg = loadConfig();
49
- console.log('Agent Identity');
50
- console.log('──────────────────────────────────────────');
51
- console.log(` Address: ${id.address}`);
52
- console.log(` Created: ${id.createdAt}`);
53
- if (cfg.name)
54
- console.log(` Name: ${cfg.name}`);
55
- if (cfg.tripcode)
56
- console.log(` Tripcode: ${cfg.tripcode}`);
57
- if (options.publicKey) {
58
- console.log('\nPublic Key:');
59
- console.log(id.publicKey);
60
- }
61
- });
62
- /* ── sign ── */
63
- identity
64
- .command('sign')
65
- .description('Sign a message with your identity')
66
- .argument('<message>', 'message to sign')
67
- .action(async (message) => {
68
- const id = requireIdentity();
69
- const signed = respondToChallenge(message, id);
70
- console.log(JSON.stringify(signed, null, 2));
71
- });
72
- /* ── verify ── */
73
- identity
74
- .command('verify')
75
- .description('Verify a signed message')
76
- .argument('<json>', 'JSON string with { message, signature, address }')
77
- .action(async (json) => {
78
- let payload;
79
- try {
80
- payload = JSON.parse(json);
81
- }
82
- catch {
83
- throw new Error('Invalid JSON. Expected: { "message": "...", "signature": "...", "address": "..." }');
84
- }
85
- if (!payload.publicKey) {
86
- // Try to look up the public key from the server
87
- try {
88
- const res = await apiFetch(`/api/agent/identity/${payload.address}`);
89
- payload.publicKey = res.publicKey;
90
- }
91
- catch {
92
- throw new Error(`Cannot verify: no public key provided and address ${shortAddress(payload.address)} not found on server.`);
93
- }
94
- }
95
- const valid = verifySignature(payload.message, payload.signature, payload.publicKey);
96
- if (valid) {
97
- console.log(`VALID — message was signed by ${shortAddress(payload.address)}`);
98
- }
99
- else {
100
- console.log(`INVALID — signature does not match`);
101
- process.exit(1);
102
- }
103
- });
104
- /* ── export ── */
105
- identity
106
- .command('export')
107
- .description('Export public identity as JSON (shareable, no secret key)')
108
- .action(async () => {
109
- const id = requireIdentity();
110
- const cfg = loadConfig();
111
- const exported = {
112
- address: id.address,
113
- publicKey: id.publicKey,
114
- name: cfg.name || null,
115
- tripcode: cfg.tripcode || null,
116
- createdAt: id.createdAt,
117
- };
118
- console.log(JSON.stringify(exported, null, 2));
119
- });
120
- }
@@ -1,87 +0,0 @@
1
- import { createHash, randomBytes, createSign, createVerify, generateKeyPairSync } from 'node:crypto';
2
- import { loadConfig, saveConfig } from './config.js';
3
- /* ── Key generation ── */
4
- /** Generate a new ECDSA keypair (secp256k1) and derive an address */
5
- export function generateIdentity() {
6
- const { publicKey, privateKey } = generateKeyPairSync('ec', {
7
- namedCurve: 'secp256k1',
8
- publicKeyEncoding: { type: 'spki', format: 'pem' },
9
- privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
10
- });
11
- const address = deriveAddress(publicKey);
12
- return {
13
- publicKey,
14
- secretKey: privateKey,
15
- address,
16
- createdAt: new Date().toISOString(),
17
- };
18
- }
19
- /** Derive a hex address from a public key (keccak-like: sha256 → last 20 bytes → 0x prefix) */
20
- export function deriveAddress(publicKeyPem) {
21
- const hash = createHash('sha256').update(publicKeyPem).digest();
22
- return '0x' + hash.subarray(hash.length - 20).toString('hex');
23
- }
24
- /* ── Signing ── */
25
- /** Sign a message with the agent's private key */
26
- export function signMessage(message, secretKeyPem) {
27
- const signer = createSign('SHA256');
28
- signer.update(message);
29
- signer.end();
30
- return signer.sign(secretKeyPem, 'hex');
31
- }
32
- /** Create a full signed message payload */
33
- export function createSignedMessage(message, identity) {
34
- return {
35
- message,
36
- signature: signMessage(message, identity.secretKey),
37
- address: identity.address,
38
- timestamp: new Date().toISOString(),
39
- };
40
- }
41
- /* ── Verification ── */
42
- /** Verify a signed message against a public key */
43
- export function verifySignature(message, signatureHex, publicKeyPem) {
44
- try {
45
- const verifier = createVerify('SHA256');
46
- verifier.update(message);
47
- verifier.end();
48
- return verifier.verify(publicKeyPem, signatureHex, 'hex');
49
- }
50
- catch {
51
- return false;
52
- }
53
- }
54
- /* ── Challenge-response ── */
55
- /** Generate a random challenge nonce */
56
- export function generateChallenge() {
57
- return randomBytes(32).toString('hex');
58
- }
59
- /** Sign a challenge to prove identity */
60
- export function respondToChallenge(challenge, identity) {
61
- return createSignedMessage(challenge, identity);
62
- }
63
- /* ── Config helpers ── */
64
- /** Get the current identity from config, or null */
65
- export function getIdentity() {
66
- const cfg = loadConfig();
67
- return cfg.identity || null;
68
- }
69
- /** Require identity or throw */
70
- export function requireIdentity() {
71
- const identity = getIdentity();
72
- if (!identity) {
73
- throw new Error('No identity found. Run: devtopia identity create');
74
- }
75
- return identity;
76
- }
77
- /** Save identity to config */
78
- export function saveIdentity(identity) {
79
- const cfg = loadConfig();
80
- saveConfig({ ...cfg, identity });
81
- }
82
- /** Fingerprint: short display of an address */
83
- export function shortAddress(address) {
84
- if (address.length <= 12)
85
- return address;
86
- return `${address.slice(0, 6)}...${address.slice(-4)}`;
87
- }