agent-bootstrap 0.1.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/LICENSE +21 -0
- package/README.md +145 -0
- package/package.json +29 -0
- package/src/cli.cjs +191 -0
- package/src/index.cjs +352 -0
- package/test/run.cjs +189 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeletor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# agent-bootstrap
|
|
2
|
+
|
|
3
|
+
One command to bootstrap an AI agent with Nostr identity, Lightning wallet, ai.wot trust, and DVM service announcement.
|
|
4
|
+
|
|
5
|
+
The "create-react-app" for the agent economy.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://..." --dvm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
In one command:
|
|
14
|
+
1. š **Generates Nostr keys** (nsec/npub) with secure file permissions (600)
|
|
15
|
+
2. š¤ **Publishes profile** to 4 relays (damus, nos.lol, primal, snort)
|
|
16
|
+
3. ā” **Configures Lightning wallet** via NWC (Nostr Wallet Connect)
|
|
17
|
+
4. š¤ **Sets up ai.wot trust** ā ready to receive attestations
|
|
18
|
+
5. š¤ **Registers as a DVM** (kind 31990 service announcement)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g agent-bootstrap
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Bootstrap a new agent
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Minimal ā just identity
|
|
32
|
+
agent-bootstrap init --name "My Agent"
|
|
33
|
+
|
|
34
|
+
# With wallet
|
|
35
|
+
agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://relay.example?secret=..."
|
|
36
|
+
|
|
37
|
+
# Full stack ā identity + wallet + DVM
|
|
38
|
+
agent-bootstrap init --name "My Agent" \
|
|
39
|
+
--about "A helpful AI agent" \
|
|
40
|
+
--nwc "nostr+walletconnect://relay.example?secret=..." \
|
|
41
|
+
--dvm \
|
|
42
|
+
--dvm-price 21
|
|
43
|
+
|
|
44
|
+
# Custom relays
|
|
45
|
+
agent-bootstrap init --name "My Agent" \
|
|
46
|
+
--relays "wss://relay.damus.io,wss://nos.lol"
|
|
47
|
+
|
|
48
|
+
# Generate keys without publishing
|
|
49
|
+
agent-bootstrap init --name "My Agent" --skip-publish
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Check status
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
agent-bootstrap status ./agent-identity
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Verify everything works
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
agent-bootstrap verify ./agent-identity
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Checks:
|
|
65
|
+
- ā
Keys exist and are valid
|
|
66
|
+
- ā
Key file permissions are secure (600)
|
|
67
|
+
- ā
Profile found on relays
|
|
68
|
+
- ā
Wallet config present
|
|
69
|
+
- ā
Trust score reachable via ai.wot API
|
|
70
|
+
|
|
71
|
+
### JSON output
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
agent-bootstrap init --name "My Agent" --json
|
|
75
|
+
agent-bootstrap status ./agent-identity --json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## What it creates
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
agent-identity/
|
|
82
|
+
āāā nostr-keys.json # Nostr keypair (600 permissions)
|
|
83
|
+
āāā wallet-config.json # NWC connection (600 permissions)
|
|
84
|
+
āāā bootstrap-manifest.json # Full bootstrap record
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Programmatic API
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
const { bootstrap, status, verify, generateKeys } = require('agent-bootstrap');
|
|
91
|
+
|
|
92
|
+
// Generate keys only
|
|
93
|
+
const keys = generateKeys();
|
|
94
|
+
console.log(keys.npub, keys.pubkey);
|
|
95
|
+
|
|
96
|
+
// Full bootstrap
|
|
97
|
+
const result = await bootstrap({
|
|
98
|
+
name: 'My Agent',
|
|
99
|
+
about: 'A helpful AI',
|
|
100
|
+
nwcUrl: 'nostr+walletconnect://...',
|
|
101
|
+
dir: './my-agent',
|
|
102
|
+
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
|
103
|
+
dvm: true,
|
|
104
|
+
dvmKind: 5050,
|
|
105
|
+
dvmPrice: 21,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Check status
|
|
109
|
+
const s = await status('./my-agent');
|
|
110
|
+
|
|
111
|
+
// Verify
|
|
112
|
+
const v = await verify('./my-agent');
|
|
113
|
+
console.log(`${v.passed}/${v.total} checks passed`);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## The Agent Economy Stack
|
|
117
|
+
|
|
118
|
+
agent-bootstrap is the entry point. Once bootstrapped, your agent can use:
|
|
119
|
+
|
|
120
|
+
| Package | Purpose |
|
|
121
|
+
|---------|---------|
|
|
122
|
+
| [ai-wot](https://github.com/jeletor/ai-wot) | Trust ā earn reputation through attestations |
|
|
123
|
+
| [agent-discovery](https://github.com/jeletor/agent-discovery) | Discovery ā publish and find agent services |
|
|
124
|
+
| [lightning-agent](https://github.com/jeletor/lightning-agent) | Payments ā send and receive Lightning |
|
|
125
|
+
| [login-with-lightning](https://github.com/jeletor/login-with-lightning) | Auth ā LNURL-auth for agents |
|
|
126
|
+
| [lightning-toll](https://github.com/jeletor/lightning-toll) | Monetize ā L402 paywalls for APIs |
|
|
127
|
+
| [agent-escrow](https://github.com/jeletor/agent-escrow) | Escrow ā safe agent-to-agent transactions |
|
|
128
|
+
| [agent-test-kit](https://github.com/jeletor/agent-test-kit) | Testing ā mock relays and wallets |
|
|
129
|
+
|
|
130
|
+
The flow: **bootstrap** ā discover ā trust ā pay ā deliver ā attest ā repeat.
|
|
131
|
+
|
|
132
|
+
## Dependencies
|
|
133
|
+
|
|
134
|
+
- `nostr-tools` ā Nostr key generation and event signing
|
|
135
|
+
- `ws` ā WebSocket for relay connections
|
|
136
|
+
|
|
137
|
+
Zero other dependencies.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
|
142
|
+
|
|
143
|
+
## Author
|
|
144
|
+
|
|
145
|
+
Built by [Jeletor](https://jeletor.com) š
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-bootstrap",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One command to bootstrap an AI agent with Nostr identity, Lightning wallet, ai.wot trust, DVM announcement, and monitoring",
|
|
5
|
+
"main": "src/index.cjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"agent-bootstrap": "src/cli.cjs"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test/run.cjs"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"ai",
|
|
14
|
+
"agent",
|
|
15
|
+
"nostr",
|
|
16
|
+
"lightning",
|
|
17
|
+
"bootstrap",
|
|
18
|
+
"identity",
|
|
19
|
+
"wallet",
|
|
20
|
+
"trust",
|
|
21
|
+
"dvm"
|
|
22
|
+
],
|
|
23
|
+
"author": "Jeletor <jeletor@jeletor.com>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"nostr-tools": "^2.11.0",
|
|
27
|
+
"ws": "^8.18.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/cli.cjs
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agent-bootstrap CLI ā one command to set up an AI agent on Nostr + Lightning
|
|
3
|
+
|
|
4
|
+
const { bootstrap, status, verify } = require('./index.cjs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
|
|
8
|
+
const HELP = `
|
|
9
|
+
agent-bootstrap ā Set up an AI agent with identity, wallet, trust, and services
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
agent-bootstrap init [options] Bootstrap a new agent
|
|
13
|
+
agent-bootstrap status [dir] Check bootstrap status
|
|
14
|
+
agent-bootstrap verify [dir] Verify all components work
|
|
15
|
+
agent-bootstrap help Show this help
|
|
16
|
+
|
|
17
|
+
Init options:
|
|
18
|
+
--name <name> Agent display name (required)
|
|
19
|
+
--about <bio> Agent bio/description
|
|
20
|
+
--nwc <url> NWC connection string (for Lightning wallet)
|
|
21
|
+
--dir <path> Output directory (default: ./agent-identity)
|
|
22
|
+
--relays <urls> Comma-separated relay URLs
|
|
23
|
+
--dvm Register as a DVM (kind 5050 text generation)
|
|
24
|
+
--dvm-kind <kind> DVM kind number (default: 5050)
|
|
25
|
+
--dvm-price <sats> DVM price in sats (default: 21)
|
|
26
|
+
--skip-publish Generate keys but don't publish to relays
|
|
27
|
+
--json Output results as JSON
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
agent-bootstrap init --name "My Agent" --about "A helpful AI"
|
|
31
|
+
agent-bootstrap init --name "My Agent" --nwc "nostr+walletconnect://..." --dvm
|
|
32
|
+
agent-bootstrap status ./agent-identity
|
|
33
|
+
agent-bootstrap verify ./agent-identity
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
async function main() {
|
|
37
|
+
const args = process.argv.slice(2);
|
|
38
|
+
const cmd = args[0];
|
|
39
|
+
|
|
40
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
|
41
|
+
console.log(HELP);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse flags
|
|
46
|
+
function getFlag(name) {
|
|
47
|
+
const idx = args.indexOf('--' + name);
|
|
48
|
+
if (idx === -1) return undefined;
|
|
49
|
+
return args[idx + 1];
|
|
50
|
+
}
|
|
51
|
+
function hasFlag(name) {
|
|
52
|
+
return args.includes('--' + name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (cmd === 'init') {
|
|
56
|
+
const name = getFlag('name');
|
|
57
|
+
if (!name) {
|
|
58
|
+
console.error('Error: --name is required');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const opts = {
|
|
63
|
+
name,
|
|
64
|
+
about: getFlag('about') || `${name} ā AI agent on Nostr`,
|
|
65
|
+
nwcUrl: getFlag('nwc'),
|
|
66
|
+
dir: getFlag('dir') || './agent-identity',
|
|
67
|
+
relays: getFlag('relays')
|
|
68
|
+
? getFlag('relays').split(',').map(s => s.trim())
|
|
69
|
+
: ['wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.primal.net', 'wss://relay.snort.social'],
|
|
70
|
+
dvm: hasFlag('dvm'),
|
|
71
|
+
dvmKind: parseInt(getFlag('dvm-kind') || '5050', 10),
|
|
72
|
+
dvmPrice: parseInt(getFlag('dvm-price') || '21', 10),
|
|
73
|
+
skipPublish: hasFlag('skip-publish'),
|
|
74
|
+
json: hasFlag('json'),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await bootstrap(opts);
|
|
79
|
+
if (opts.json) {
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
} else {
|
|
82
|
+
printResult(result);
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error('Bootstrap failed:', err.message);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
} else if (cmd === 'status') {
|
|
89
|
+
const dir = args[1] || './agent-identity';
|
|
90
|
+
const result = await status(dir);
|
|
91
|
+
if (hasFlag('json')) {
|
|
92
|
+
console.log(JSON.stringify(result, null, 2));
|
|
93
|
+
} else {
|
|
94
|
+
printStatus(result);
|
|
95
|
+
}
|
|
96
|
+
} else if (cmd === 'verify') {
|
|
97
|
+
const dir = args[1] || './agent-identity';
|
|
98
|
+
const result = await verify(dir);
|
|
99
|
+
if (hasFlag('json')) {
|
|
100
|
+
console.log(JSON.stringify(result, null, 2));
|
|
101
|
+
} else {
|
|
102
|
+
printVerify(result);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
console.error(`Unknown command: ${cmd}`);
|
|
106
|
+
console.log(HELP);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function printResult(r) {
|
|
112
|
+
console.log('');
|
|
113
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
114
|
+
console.log('ā agent-bootstrap ā Setup Complete ā');
|
|
115
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
116
|
+
console.log('');
|
|
117
|
+
console.log(' š Identity');
|
|
118
|
+
console.log(` Name: ${r.identity.name}`);
|
|
119
|
+
console.log(` npub: ${r.identity.npub}`);
|
|
120
|
+
console.log(` Pubkey: ${r.identity.pubkeyHex}`);
|
|
121
|
+
console.log(` Keys: ${r.identity.keysFile}`);
|
|
122
|
+
console.log('');
|
|
123
|
+
|
|
124
|
+
if (r.profile) {
|
|
125
|
+
console.log(' š¤ Profile');
|
|
126
|
+
console.log(` Published to ${r.profile.relays} relay(s)`);
|
|
127
|
+
console.log('');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (r.wallet) {
|
|
131
|
+
console.log(' ā” Wallet');
|
|
132
|
+
console.log(` NWC: Connected`);
|
|
133
|
+
console.log(` Config: ${r.wallet.configFile}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (r.trust) {
|
|
138
|
+
console.log(' š¤ Trust (ai.wot)');
|
|
139
|
+
console.log(` Ready for attestations`);
|
|
140
|
+
console.log(` Score: https://wot.jeletor.cc/v1/score/${r.identity.pubkeyHex}`);
|
|
141
|
+
console.log('');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (r.dvm) {
|
|
145
|
+
console.log(' š¤ DVM');
|
|
146
|
+
console.log(` Kind: ${r.dvm.kind}`);
|
|
147
|
+
console.log(` Price: ${r.dvm.price} sats`);
|
|
148
|
+
console.log(` Published to ${r.dvm.relays} relay(s)`);
|
|
149
|
+
console.log('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log(' š Files saved to: ' + r.dir);
|
|
153
|
+
console.log('');
|
|
154
|
+
console.log(' Next steps:');
|
|
155
|
+
console.log(' 1. Keep your keys file safe (it contains your secret key)');
|
|
156
|
+
if (!r.wallet) {
|
|
157
|
+
console.log(' 2. Connect a wallet: agent-bootstrap init --nwc "nostr+walletconnect://..."');
|
|
158
|
+
}
|
|
159
|
+
console.log(' 3. Start receiving attestations: share your npub with other agents');
|
|
160
|
+
console.log(' 4. Check your trust score: curl https://wot.jeletor.cc/v1/score/' + r.identity.pubkeyHex);
|
|
161
|
+
console.log('');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function printStatus(s) {
|
|
165
|
+
console.log('');
|
|
166
|
+
console.log(' Agent Status: ' + s.dir);
|
|
167
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
168
|
+
console.log(` š Identity: ${s.identity ? 'ā
' + s.identity.npub : 'ā Not found'}`);
|
|
169
|
+
console.log(` š¤ Profile: ${s.profile ? 'ā
Published' : 'ā Not published'}`);
|
|
170
|
+
console.log(` ā” Wallet: ${s.wallet ? 'ā
Configured' : 'ā Not configured'}`);
|
|
171
|
+
console.log(` š¤ DVM: ${s.dvm ? 'ā
Registered' : '⬠Not registered'}`);
|
|
172
|
+
console.log('');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function printVerify(v) {
|
|
176
|
+
console.log('');
|
|
177
|
+
console.log(' Verification Results');
|
|
178
|
+
console.log(' āāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
179
|
+
for (const check of v.checks) {
|
|
180
|
+
const icon = check.ok ? 'ā
' : 'ā';
|
|
181
|
+
console.log(` ${icon} ${check.name}: ${check.message}`);
|
|
182
|
+
}
|
|
183
|
+
console.log('');
|
|
184
|
+
console.log(` ${v.passed}/${v.total} checks passed`);
|
|
185
|
+
console.log('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
main().catch(err => {
|
|
189
|
+
console.error('Fatal:', err.message);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
});
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// agent-bootstrap ā core library
|
|
2
|
+
// Generates Nostr identity, publishes profile, configures wallet, registers DVM, sets up ai.wot
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
// āāā Key Generation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
9
|
+
|
|
10
|
+
function generateKeys() {
|
|
11
|
+
const { generateSecretKey, getPublicKey } = require('nostr-tools/pure');
|
|
12
|
+
const { nip19 } = require('nostr-tools');
|
|
13
|
+
|
|
14
|
+
const secretKey = generateSecretKey();
|
|
15
|
+
const pubkey = getPublicKey(secretKey);
|
|
16
|
+
const secretKeyHex = Buffer.from(secretKey).toString('hex');
|
|
17
|
+
const nsec = nip19.nsecEncode(secretKey);
|
|
18
|
+
const npub = nip19.npubEncode(pubkey);
|
|
19
|
+
|
|
20
|
+
return { secretKey, secretKeyHex, pubkey, nsec, npub };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// āāā Profile Publishing āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
24
|
+
|
|
25
|
+
async function publishProfile(keys, opts) {
|
|
26
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
27
|
+
const { finalizeEvent } = require('nostr-tools/pure');
|
|
28
|
+
const WebSocket = require('ws');
|
|
29
|
+
useWebSocketImplementation(WebSocket);
|
|
30
|
+
|
|
31
|
+
const metadata = {
|
|
32
|
+
name: opts.name,
|
|
33
|
+
about: opts.about || '',
|
|
34
|
+
picture: opts.picture || '',
|
|
35
|
+
nip05: opts.nip05 || '',
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (opts.lightningAddress) {
|
|
39
|
+
metadata.lud16 = opts.lightningAddress;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const event = finalizeEvent({
|
|
43
|
+
kind: 0,
|
|
44
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
45
|
+
tags: [],
|
|
46
|
+
content: JSON.stringify(metadata),
|
|
47
|
+
}, keys.secretKey);
|
|
48
|
+
|
|
49
|
+
let published = 0;
|
|
50
|
+
for (const url of opts.relays) {
|
|
51
|
+
try {
|
|
52
|
+
const relay = await Relay.connect(url);
|
|
53
|
+
await relay.publish(event);
|
|
54
|
+
relay.close();
|
|
55
|
+
published++;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
// relay failed, continue
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { eventId: event.id, published, total: opts.relays.length };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// āāā DVM Announcement āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
65
|
+
|
|
66
|
+
async function publishDVMAnnouncement(keys, opts) {
|
|
67
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
68
|
+
const { finalizeEvent } = require('nostr-tools/pure');
|
|
69
|
+
const WebSocket = require('ws');
|
|
70
|
+
useWebSocketImplementation(WebSocket);
|
|
71
|
+
|
|
72
|
+
const kind = opts.dvmKind || 5050;
|
|
73
|
+
const price = opts.dvmPrice || 21;
|
|
74
|
+
|
|
75
|
+
// Kind 31990 ā DVM service announcement (parameterized replaceable)
|
|
76
|
+
const event = finalizeEvent({
|
|
77
|
+
kind: 31990,
|
|
78
|
+
created_at: Math.floor(Date.now() / 1000),
|
|
79
|
+
tags: [
|
|
80
|
+
['d', `${opts.name}-dvm`],
|
|
81
|
+
['k', String(kind)],
|
|
82
|
+
['t', 'ai'],
|
|
83
|
+
['t', 'agent'],
|
|
84
|
+
['amount', String(price * 1000), 'msats'], // amount in msats
|
|
85
|
+
],
|
|
86
|
+
content: JSON.stringify({
|
|
87
|
+
name: `${opts.name} DVM`,
|
|
88
|
+
about: opts.about || `${opts.name} ā AI agent DVM service`,
|
|
89
|
+
kind,
|
|
90
|
+
price: { amount: price, unit: 'sats' },
|
|
91
|
+
}),
|
|
92
|
+
}, keys.secretKey);
|
|
93
|
+
|
|
94
|
+
let published = 0;
|
|
95
|
+
for (const url of opts.relays) {
|
|
96
|
+
try {
|
|
97
|
+
const relay = await Relay.connect(url);
|
|
98
|
+
await relay.publish(event);
|
|
99
|
+
relay.close();
|
|
100
|
+
published++;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
// relay failed, continue
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return { eventId: event.id, kind, price, published, total: opts.relays.length };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// āāā File Management āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
110
|
+
|
|
111
|
+
function saveKeys(dir, keys) {
|
|
112
|
+
const keysFile = path.join(dir, 'nostr-keys.json');
|
|
113
|
+
const data = {
|
|
114
|
+
publicKeyHex: keys.pubkey,
|
|
115
|
+
secretKeyHex: keys.secretKeyHex,
|
|
116
|
+
npub: keys.npub,
|
|
117
|
+
nsec: keys.nsec,
|
|
118
|
+
createdAt: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
fs.writeFileSync(keysFile, JSON.stringify(data, null, 2));
|
|
121
|
+
fs.chmodSync(keysFile, 0o600); // owner read/write only
|
|
122
|
+
return keysFile;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function saveWalletConfig(dir, nwcUrl) {
|
|
126
|
+
const configFile = path.join(dir, 'wallet-config.json');
|
|
127
|
+
const data = {
|
|
128
|
+
nwcUrl,
|
|
129
|
+
createdAt: new Date().toISOString(),
|
|
130
|
+
};
|
|
131
|
+
fs.writeFileSync(configFile, JSON.stringify(data, null, 2));
|
|
132
|
+
fs.chmodSync(configFile, 0o600);
|
|
133
|
+
return configFile;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function saveBootstrapManifest(dir, result) {
|
|
137
|
+
const manifestFile = path.join(dir, 'bootstrap-manifest.json');
|
|
138
|
+
fs.writeFileSync(manifestFile, JSON.stringify(result, null, 2));
|
|
139
|
+
return manifestFile;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// āāā Bootstrap āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
143
|
+
|
|
144
|
+
async function bootstrap(opts) {
|
|
145
|
+
const dir = path.resolve(opts.dir || './agent-identity');
|
|
146
|
+
|
|
147
|
+
// Create directory
|
|
148
|
+
if (!fs.existsSync(dir)) {
|
|
149
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const result = { dir };
|
|
153
|
+
|
|
154
|
+
// 1. Generate keys
|
|
155
|
+
console.log('š Generating Nostr identity...');
|
|
156
|
+
const keys = generateKeys();
|
|
157
|
+
const keysFile = saveKeys(dir, keys);
|
|
158
|
+
result.identity = {
|
|
159
|
+
name: opts.name,
|
|
160
|
+
npub: keys.npub,
|
|
161
|
+
pubkeyHex: keys.pubkey,
|
|
162
|
+
keysFile,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// 2. Publish profile
|
|
166
|
+
if (!opts.skipPublish) {
|
|
167
|
+
console.log('š¤ Publishing profile to relays...');
|
|
168
|
+
const profile = await publishProfile(keys, {
|
|
169
|
+
name: opts.name,
|
|
170
|
+
about: opts.about,
|
|
171
|
+
relays: opts.relays,
|
|
172
|
+
lightningAddress: opts.lightningAddress,
|
|
173
|
+
});
|
|
174
|
+
result.profile = { relays: profile.published };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 3. Configure wallet
|
|
178
|
+
if (opts.nwcUrl) {
|
|
179
|
+
console.log('ā” Configuring Lightning wallet...');
|
|
180
|
+
const configFile = saveWalletConfig(dir, opts.nwcUrl);
|
|
181
|
+
result.wallet = { configFile };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 4. ai.wot readiness
|
|
185
|
+
result.trust = {
|
|
186
|
+
protocol: 'ai.wot',
|
|
187
|
+
namespace: 'ai.wot',
|
|
188
|
+
ready: true,
|
|
189
|
+
scoreUrl: `https://wot.jeletor.cc/v1/score/${keys.pubkey}`,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// 5. DVM announcement
|
|
193
|
+
if (opts.dvm && !opts.skipPublish) {
|
|
194
|
+
console.log('š¤ Publishing DVM announcement...');
|
|
195
|
+
const dvm = await publishDVMAnnouncement(keys, {
|
|
196
|
+
name: opts.name,
|
|
197
|
+
about: opts.about,
|
|
198
|
+
dvmKind: opts.dvmKind,
|
|
199
|
+
dvmPrice: opts.dvmPrice,
|
|
200
|
+
relays: opts.relays,
|
|
201
|
+
});
|
|
202
|
+
result.dvm = { kind: dvm.kind, price: dvm.price, relays: dvm.published };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 6. Save manifest
|
|
206
|
+
saveBootstrapManifest(dir, result);
|
|
207
|
+
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// āāā Status Check āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
212
|
+
|
|
213
|
+
async function status(dir) {
|
|
214
|
+
dir = path.resolve(dir);
|
|
215
|
+
const result = { dir };
|
|
216
|
+
|
|
217
|
+
// Check keys
|
|
218
|
+
const keysFile = path.join(dir, 'nostr-keys.json');
|
|
219
|
+
if (fs.existsSync(keysFile)) {
|
|
220
|
+
const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
|
|
221
|
+
result.identity = { npub: keys.npub, pubkeyHex: keys.publicKeyHex };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Check wallet
|
|
225
|
+
const walletFile = path.join(dir, 'wallet-config.json');
|
|
226
|
+
if (fs.existsSync(walletFile)) {
|
|
227
|
+
result.wallet = { configured: true };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check manifest
|
|
231
|
+
const manifestFile = path.join(dir, 'bootstrap-manifest.json');
|
|
232
|
+
if (fs.existsSync(manifestFile)) {
|
|
233
|
+
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf-8'));
|
|
234
|
+
result.profile = manifest.profile || null;
|
|
235
|
+
result.dvm = manifest.dvm || null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// āāā Verify āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
242
|
+
|
|
243
|
+
async function verify(dir) {
|
|
244
|
+
dir = path.resolve(dir);
|
|
245
|
+
const checks = [];
|
|
246
|
+
|
|
247
|
+
// 1. Keys exist and are valid
|
|
248
|
+
const keysFile = path.join(dir, 'nostr-keys.json');
|
|
249
|
+
if (fs.existsSync(keysFile)) {
|
|
250
|
+
try {
|
|
251
|
+
const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
|
|
252
|
+
if (keys.publicKeyHex && keys.secretKeyHex && keys.publicKeyHex.length === 64) {
|
|
253
|
+
checks.push({ name: 'Identity', ok: true, message: `Keys valid (${keys.npub.substring(0, 20)}...)` });
|
|
254
|
+
} else {
|
|
255
|
+
checks.push({ name: 'Identity', ok: false, message: 'Keys file is malformed' });
|
|
256
|
+
}
|
|
257
|
+
} catch (e) {
|
|
258
|
+
checks.push({ name: 'Identity', ok: false, message: 'Keys file is not valid JSON' });
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
checks.push({ name: 'Identity', ok: false, message: 'No keys file found' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 2. Keys file permissions
|
|
265
|
+
if (fs.existsSync(keysFile)) {
|
|
266
|
+
const stat = fs.statSync(keysFile);
|
|
267
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
268
|
+
if (mode === '600') {
|
|
269
|
+
checks.push({ name: 'Key Security', ok: true, message: 'Keys file has correct permissions (600)' });
|
|
270
|
+
} else {
|
|
271
|
+
checks.push({ name: 'Key Security', ok: false, message: `Keys file permissions too open: ${mode} (should be 600)` });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 3. Profile on relays
|
|
276
|
+
if (fs.existsSync(keysFile)) {
|
|
277
|
+
try {
|
|
278
|
+
const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
|
|
279
|
+
const { Relay, useWebSocketImplementation } = require('nostr-tools/relay');
|
|
280
|
+
const WebSocket = require('ws');
|
|
281
|
+
useWebSocketImplementation(WebSocket);
|
|
282
|
+
|
|
283
|
+
let found = false;
|
|
284
|
+
const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
|
|
285
|
+
for (const url of relays) {
|
|
286
|
+
try {
|
|
287
|
+
const relay = await Relay.connect(url);
|
|
288
|
+
const events = [];
|
|
289
|
+
await new Promise((resolve) => {
|
|
290
|
+
relay.subscribe([{ kinds: [0], authors: [keys.publicKeyHex], limit: 1 }], {
|
|
291
|
+
onevent(e) { events.push(e); },
|
|
292
|
+
oneose() { resolve(); },
|
|
293
|
+
});
|
|
294
|
+
setTimeout(resolve, 5000);
|
|
295
|
+
});
|
|
296
|
+
relay.close();
|
|
297
|
+
if (events.length > 0) {
|
|
298
|
+
found = true;
|
|
299
|
+
const profile = JSON.parse(events[0].content);
|
|
300
|
+
checks.push({ name: 'Profile', ok: true, message: `Found on ${url}: "${profile.name}"` });
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
// try next relay
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (!found) {
|
|
308
|
+
checks.push({ name: 'Profile', ok: false, message: 'Not found on any relay' });
|
|
309
|
+
}
|
|
310
|
+
} catch (e) {
|
|
311
|
+
checks.push({ name: 'Profile', ok: false, message: e.message });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 4. Wallet config
|
|
316
|
+
const walletFile = path.join(dir, 'wallet-config.json');
|
|
317
|
+
if (fs.existsSync(walletFile)) {
|
|
318
|
+
try {
|
|
319
|
+
const wc = JSON.parse(fs.readFileSync(walletFile, 'utf-8'));
|
|
320
|
+
if (wc.nwcUrl && wc.nwcUrl.startsWith('nostr+walletconnect://')) {
|
|
321
|
+
checks.push({ name: 'Wallet', ok: true, message: 'NWC config present' });
|
|
322
|
+
} else {
|
|
323
|
+
checks.push({ name: 'Wallet', ok: false, message: 'Invalid NWC URL format' });
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {
|
|
326
|
+
checks.push({ name: 'Wallet', ok: false, message: 'Wallet config is not valid JSON' });
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
checks.push({ name: 'Wallet', ok: false, message: 'No wallet config (optional)' });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// 5. Trust score reachable
|
|
333
|
+
if (fs.existsSync(keysFile)) {
|
|
334
|
+
try {
|
|
335
|
+
const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
|
|
336
|
+
const resp = await fetch(`https://wot.jeletor.cc/v1/score/${keys.publicKeyHex}`);
|
|
337
|
+
if (resp.ok) {
|
|
338
|
+
const data = await resp.json();
|
|
339
|
+
checks.push({ name: 'Trust (ai.wot)', ok: true, message: `Score: ${data.score}/100 (${data.attestationCount} attestations)` });
|
|
340
|
+
} else {
|
|
341
|
+
checks.push({ name: 'Trust (ai.wot)', ok: false, message: `API returned ${resp.status}` });
|
|
342
|
+
}
|
|
343
|
+
} catch (e) {
|
|
344
|
+
checks.push({ name: 'Trust (ai.wot)', ok: false, message: 'Could not reach wot.jeletor.cc' });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const passed = checks.filter(c => c.ok).length;
|
|
349
|
+
return { checks, passed, total: checks.length };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = { bootstrap, status, verify, generateKeys, publishProfile, publishDVMAnnouncement };
|
package/test/run.cjs
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// agent-bootstrap tests
|
|
2
|
+
const { generateKeys, bootstrap, status, verify } = require('../src/index.cjs');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
let passed = 0;
|
|
8
|
+
let failed = 0;
|
|
9
|
+
|
|
10
|
+
function test(name, fn) {
|
|
11
|
+
try {
|
|
12
|
+
fn();
|
|
13
|
+
console.log(` ā
${name}`);
|
|
14
|
+
passed++;
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.log(` ā ${name}: ${e.message}`);
|
|
17
|
+
failed++;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function assert(cond, msg) {
|
|
22
|
+
if (!cond) throw new Error(msg || 'Assertion failed');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function asyncTest(name, fn) {
|
|
26
|
+
try {
|
|
27
|
+
await fn();
|
|
28
|
+
console.log(` ā
${name}`);
|
|
29
|
+
passed++;
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.log(` ā ${name}: ${e.message}`);
|
|
32
|
+
failed++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// āāā Key Generation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
37
|
+
|
|
38
|
+
console.log('\nš Key Generation');
|
|
39
|
+
|
|
40
|
+
test('generates valid keys', () => {
|
|
41
|
+
const keys = generateKeys();
|
|
42
|
+
assert(keys.pubkey && keys.pubkey.length === 64, 'pubkey should be 64 hex chars');
|
|
43
|
+
assert(keys.secretKeyHex && keys.secretKeyHex.length === 64, 'secretKeyHex should be 64 hex chars');
|
|
44
|
+
assert(keys.npub.startsWith('npub1'), 'npub should start with npub1');
|
|
45
|
+
assert(keys.nsec.startsWith('nsec1'), 'nsec should start with nsec1');
|
|
46
|
+
assert(keys.secretKey instanceof Uint8Array, 'secretKey should be Uint8Array');
|
|
47
|
+
assert(keys.secretKey.length === 32, 'secretKey should be 32 bytes');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('generates unique keys each time', () => {
|
|
51
|
+
const k1 = generateKeys();
|
|
52
|
+
const k2 = generateKeys();
|
|
53
|
+
assert(k1.pubkey !== k2.pubkey, 'pubkeys should differ');
|
|
54
|
+
assert(k1.secretKeyHex !== k2.secretKeyHex, 'secret keys should differ');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// āāā Bootstrap (local, skip publish) āāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
58
|
+
|
|
59
|
+
console.log('\nš Bootstrap (local)');
|
|
60
|
+
|
|
61
|
+
const tmpDir = path.join(os.tmpdir(), 'agent-bootstrap-test-' + Date.now());
|
|
62
|
+
|
|
63
|
+
asyncTest('bootstrap creates identity files', async () => {
|
|
64
|
+
const result = await bootstrap({
|
|
65
|
+
name: 'Test Agent',
|
|
66
|
+
about: 'A test agent',
|
|
67
|
+
dir: tmpDir,
|
|
68
|
+
relays: [],
|
|
69
|
+
skipPublish: true,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert(result.identity, 'should have identity');
|
|
73
|
+
assert(result.identity.npub.startsWith('npub1'), 'should have valid npub');
|
|
74
|
+
assert(result.identity.pubkeyHex.length === 64, 'should have valid pubkey');
|
|
75
|
+
assert(fs.existsSync(path.join(tmpDir, 'nostr-keys.json')), 'keys file should exist');
|
|
76
|
+
assert(fs.existsSync(path.join(tmpDir, 'bootstrap-manifest.json')), 'manifest should exist');
|
|
77
|
+
}).then(() => {
|
|
78
|
+
|
|
79
|
+
return asyncTest('keys file has correct permissions', async () => {
|
|
80
|
+
const keysFile = path.join(tmpDir, 'nostr-keys.json');
|
|
81
|
+
const stat = fs.statSync(keysFile);
|
|
82
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
83
|
+
assert(mode === '600', `permissions should be 600, got ${mode}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
}).then(() => {
|
|
87
|
+
|
|
88
|
+
return asyncTest('keys file contains valid data', async () => {
|
|
89
|
+
const keysFile = path.join(tmpDir, 'nostr-keys.json');
|
|
90
|
+
const keys = JSON.parse(fs.readFileSync(keysFile, 'utf-8'));
|
|
91
|
+
assert(keys.publicKeyHex.length === 64, 'should have pubkey');
|
|
92
|
+
assert(keys.secretKeyHex.length === 64, 'should have secret key');
|
|
93
|
+
assert(keys.npub.startsWith('npub1'), 'should have npub');
|
|
94
|
+
assert(keys.nsec.startsWith('nsec1'), 'should have nsec');
|
|
95
|
+
assert(keys.createdAt, 'should have timestamp');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
}).then(() => {
|
|
99
|
+
|
|
100
|
+
return asyncTest('manifest records bootstrap result', async () => {
|
|
101
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(tmpDir, 'bootstrap-manifest.json'), 'utf-8'));
|
|
102
|
+
assert(manifest.identity, 'manifest should have identity');
|
|
103
|
+
assert(manifest.trust, 'manifest should have trust config');
|
|
104
|
+
assert(manifest.trust.protocol === 'ai.wot', 'trust protocol should be ai.wot');
|
|
105
|
+
assert(!manifest.profile, 'profile should not be set (skipPublish)');
|
|
106
|
+
assert(!manifest.dvm, 'dvm should not be set');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
}).then(() => {
|
|
110
|
+
|
|
111
|
+
// āāā Bootstrap with wallet āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
112
|
+
|
|
113
|
+
console.log('\nā” Wallet Config');
|
|
114
|
+
const tmpDir2 = path.join(os.tmpdir(), 'agent-bootstrap-test2-' + Date.now());
|
|
115
|
+
|
|
116
|
+
return asyncTest('bootstrap with NWC saves wallet config', async () => {
|
|
117
|
+
const result = await bootstrap({
|
|
118
|
+
name: 'Wallet Agent',
|
|
119
|
+
dir: tmpDir2,
|
|
120
|
+
relays: [],
|
|
121
|
+
skipPublish: true,
|
|
122
|
+
nwcUrl: 'nostr+walletconnect://test-relay?secret=abc123',
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
assert(result.wallet, 'should have wallet');
|
|
126
|
+
const configFile = path.join(tmpDir2, 'wallet-config.json');
|
|
127
|
+
assert(fs.existsSync(configFile), 'wallet config should exist');
|
|
128
|
+
const wc = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
|
|
129
|
+
assert(wc.nwcUrl === 'nostr+walletconnect://test-relay?secret=abc123', 'should save NWC URL');
|
|
130
|
+
|
|
131
|
+
// Permissions
|
|
132
|
+
const stat = fs.statSync(configFile);
|
|
133
|
+
const mode = (stat.mode & 0o777).toString(8);
|
|
134
|
+
assert(mode === '600', `wallet config permissions should be 600, got ${mode}`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
}).then(() => {
|
|
138
|
+
|
|
139
|
+
// āāā Status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
140
|
+
|
|
141
|
+
console.log('\nš Status');
|
|
142
|
+
|
|
143
|
+
return asyncTest('status reads existing bootstrap', async () => {
|
|
144
|
+
const s = await status(tmpDir);
|
|
145
|
+
assert(s.identity, 'should find identity');
|
|
146
|
+
assert(s.identity.npub.startsWith('npub1'), 'should have valid npub');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
}).then(() => {
|
|
150
|
+
|
|
151
|
+
return asyncTest('status on empty dir returns no identity', async () => {
|
|
152
|
+
const emptyDir = path.join(os.tmpdir(), 'agent-bootstrap-empty-' + Date.now());
|
|
153
|
+
fs.mkdirSync(emptyDir, { recursive: true });
|
|
154
|
+
const s = await status(emptyDir);
|
|
155
|
+
assert(!s.identity, 'should not find identity');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
}).then(() => {
|
|
159
|
+
|
|
160
|
+
// āāā Verify (local only) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
161
|
+
|
|
162
|
+
console.log('\nā Verify');
|
|
163
|
+
|
|
164
|
+
return asyncTest('verify checks identity and permissions', async () => {
|
|
165
|
+
const v = await verify(tmpDir);
|
|
166
|
+
assert(v.checks.length >= 2, 'should have at least 2 checks');
|
|
167
|
+
const identityCheck = v.checks.find(c => c.name === 'Identity');
|
|
168
|
+
assert(identityCheck && identityCheck.ok, 'identity check should pass');
|
|
169
|
+
const securityCheck = v.checks.find(c => c.name === 'Key Security');
|
|
170
|
+
assert(securityCheck && securityCheck.ok, 'security check should pass');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
}).then(() => {
|
|
174
|
+
|
|
175
|
+
// āāā Cleanup & Summary āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
176
|
+
|
|
177
|
+
console.log(`\n${'ā'.repeat(40)}`);
|
|
178
|
+
console.log(` ${passed} passed, ${failed} failed, ${passed + failed} total`);
|
|
179
|
+
if (failed > 0) process.exit(1);
|
|
180
|
+
|
|
181
|
+
// Cleanup
|
|
182
|
+
try {
|
|
183
|
+
fs.rmSync(tmpDir, { recursive: true });
|
|
184
|
+
} catch (e) {}
|
|
185
|
+
|
|
186
|
+
}).catch(err => {
|
|
187
|
+
console.error('Test error:', err);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
});
|