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 +16 -1
- package/dist/commands/id/index.js +191 -0
- package/dist/compat-matrix.js +0 -0
- package/dist/core/config.js +19 -2
- package/dist/core/http.js +27 -0
- package/dist/core/identity-protocol.js +51 -0
- package/dist/core/identity-wallet.js +166 -0
- package/dist/index.js +11 -0
- package/package.json +1 -1
- package/dist/commands/identity/index.js +0 -120
- package/dist/core/identity.js +0 -87
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** —
|
|
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
|
+
}
|
package/dist/compat-matrix.js
CHANGED
|
File without changes
|
package/dist/core/config.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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,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
|
-
}
|
package/dist/core/identity.js
DELETED
|
@@ -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
|
-
}
|