bb-signer 0.3.10 → 0.5.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/cli.js +267 -63
- package/identity.js +83 -18
- package/index.js +20 -5
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -31,7 +31,7 @@ import { execSync } from 'child_process';
|
|
|
31
31
|
import { homedir } from 'os';
|
|
32
32
|
import { join, dirname } from 'path';
|
|
33
33
|
import { fileURLToPath } from 'url';
|
|
34
|
-
import { identityExists, initIdentity, loadIdentity, getOrCreateIdentity, loadConfig, saveConfig, getProxyUrl } from './identity.js';
|
|
34
|
+
import { identityExists, initIdentity, loadIdentity, getOrCreateIdentity, loadConfig, saveConfig, getProxyUrl, validateProfileName, listProfiles, deleteProfile } from './identity.js';
|
|
35
35
|
import { signEvent, cleanEvent, signMessage } from './crypto.js';
|
|
36
36
|
import { submitToRelay } from './submit.js';
|
|
37
37
|
|
|
@@ -118,6 +118,25 @@ const EDITOR_ALIASES = {
|
|
|
118
118
|
|
|
119
119
|
const SUPPORTED_EDITORS = Object.keys(EDITORS).join(', ');
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Resolve the active profile from --profile flag or BB_PROFILE env var.
|
|
123
|
+
* Returns null for the default profile.
|
|
124
|
+
*/
|
|
125
|
+
function resolveProfile() {
|
|
126
|
+
const idx = process.argv.indexOf('--profile');
|
|
127
|
+
if (idx !== -1 && process.argv[idx + 1]) {
|
|
128
|
+
const name = process.argv[idx + 1];
|
|
129
|
+
validateProfileName(name);
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
132
|
+
if (process.env.BB_PROFILE) {
|
|
133
|
+
const name = process.env.BB_PROFILE;
|
|
134
|
+
validateProfileName(name);
|
|
135
|
+
return name;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
121
140
|
// BB MCP config templates by style
|
|
122
141
|
const BB_CONFIGS = {
|
|
123
142
|
claude: {
|
|
@@ -241,6 +260,58 @@ function fallbackToSettingsFile(ed, mcpConfig) {
|
|
|
241
260
|
console.log(` ✅ ${ed.label}: Configured`);
|
|
242
261
|
}
|
|
243
262
|
|
|
263
|
+
function configureClaudeJson() {
|
|
264
|
+
// Claude Code stores MCP servers per-project in ~/.claude.json
|
|
265
|
+
// Format: { projects: { "/path/to/cwd": { mcpServers: { ... } } } }
|
|
266
|
+
// The bb server uses HTTP transport (direct URL, no mcp-remote needed).
|
|
267
|
+
// The bb_signer server uses stdio transport (local process).
|
|
268
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
269
|
+
const cwd = process.cwd();
|
|
270
|
+
|
|
271
|
+
console.log('\nConfiguring Claude Code...');
|
|
272
|
+
|
|
273
|
+
let claudeJson = {};
|
|
274
|
+
try {
|
|
275
|
+
claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
276
|
+
} catch {
|
|
277
|
+
// File doesn't exist or invalid JSON — start fresh
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
281
|
+
if (!claudeJson.projects[cwd]) claudeJson.projects[cwd] = {};
|
|
282
|
+
if (!claudeJson.projects[cwd].mcpServers) claudeJson.projects[cwd].mcpServers = {};
|
|
283
|
+
|
|
284
|
+
// HTTP transport for the remote proxy (no mcp-remote wrapper needed)
|
|
285
|
+
claudeJson.projects[cwd].mcpServers.bb = {
|
|
286
|
+
type: "http",
|
|
287
|
+
url: "https://mcp.bb.org.ai/mcp"
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// Stdio transport for the local signer
|
|
291
|
+
claudeJson.projects[cwd].mcpServers.bb_signer = {
|
|
292
|
+
type: "stdio",
|
|
293
|
+
command: "npx",
|
|
294
|
+
args: ["-y", `bb-signer@${VERSION}`, "server"],
|
|
295
|
+
env: {}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Also register for user scope (mcpServers at top level) so it works from ANY directory
|
|
299
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
300
|
+
claudeJson.mcpServers.bb = {
|
|
301
|
+
type: "http",
|
|
302
|
+
url: "https://mcp.bb.org.ai/mcp"
|
|
303
|
+
};
|
|
304
|
+
claudeJson.mcpServers.bb_signer = {
|
|
305
|
+
type: "stdio",
|
|
306
|
+
command: "npx",
|
|
307
|
+
args: ["-y", `bb-signer@${VERSION}`, "server"],
|
|
308
|
+
env: {}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + '\n');
|
|
312
|
+
console.log(` ✅ Claude Code: Configured (${cwd})`);
|
|
313
|
+
}
|
|
314
|
+
|
|
244
315
|
async function resolveEditorFilter() {
|
|
245
316
|
// Look for a non-flag argument after "install", e.g. `install gemini --yes`
|
|
246
317
|
const installIdx = process.argv.indexOf('install');
|
|
@@ -290,15 +361,16 @@ async function install() {
|
|
|
290
361
|
}
|
|
291
362
|
|
|
292
363
|
// Step 1: Create identity
|
|
364
|
+
const profile = resolveProfile();
|
|
293
365
|
let identity;
|
|
294
366
|
let isNew = false;
|
|
295
|
-
if (identityExists()) {
|
|
296
|
-
identity = loadIdentity();
|
|
297
|
-
console.log(` ✅ Identity: ${identity.publicKeyBase58} (existing)`);
|
|
367
|
+
if (identityExists(profile)) {
|
|
368
|
+
identity = loadIdentity(profile);
|
|
369
|
+
console.log(` ✅ Identity: ${identity.publicKeyBase58} (existing${profile ? `, profile: ${profile}` : ''})`);
|
|
298
370
|
} else {
|
|
299
|
-
identity = getOrCreateIdentity();
|
|
371
|
+
identity = getOrCreateIdentity(profile);
|
|
300
372
|
isNew = true;
|
|
301
|
-
console.log(` ✅ Identity: ${identity.publicKeyBase58} (created)`);
|
|
373
|
+
console.log(` ✅ Identity: ${identity.publicKeyBase58} (created${profile ? `, profile: ${profile}` : ''})`);
|
|
302
374
|
}
|
|
303
375
|
|
|
304
376
|
// Step 2: Save default proxy URL if not already configured
|
|
@@ -312,24 +384,12 @@ async function install() {
|
|
|
312
384
|
const ed = EDITORS[editorFilter];
|
|
313
385
|
const mcpConfig = getMcpConfig(ed);
|
|
314
386
|
|
|
315
|
-
// For Claude Code:
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
installViaClaude(mcpConfig);
|
|
322
|
-
console.log(` ✅ ${ed.label}: Configured via \`claude mcp add\``);
|
|
323
|
-
} catch (e) {
|
|
324
|
-
console.error(` ⚠️ \`claude mcp add\` failed: ${e.message}`);
|
|
325
|
-
}
|
|
326
|
-
// Always also write settings.json — belt and suspenders
|
|
327
|
-
const plan = planEditorConfig(ed.label, ed.paths, mcpConfig, ed.detectDirs);
|
|
328
|
-
if (plan) {
|
|
329
|
-
applyEditorConfig(plan);
|
|
330
|
-
} else {
|
|
331
|
-
fallbackToSettingsFile(ed, mcpConfig);
|
|
332
|
-
}
|
|
387
|
+
// For Claude Code: write directly to ~/.claude.json (the actual config file).
|
|
388
|
+
// Claude Code stores MCP servers per-project in ~/.claude.json under projects[cwd].mcpServers.
|
|
389
|
+
// ~/.claude/settings.json does NOT configure MCP servers (common misconception).
|
|
390
|
+
// `claude mcp add` can't run from within a nested Claude Code session.
|
|
391
|
+
if (editorFilter === 'claude') {
|
|
392
|
+
configureClaudeJson(mcpConfig);
|
|
333
393
|
} else {
|
|
334
394
|
// All other editors: write to config file
|
|
335
395
|
const plans = [planEditorConfig(ed.label, ed.paths, mcpConfig, ed.detectDirs)].filter(Boolean);
|
|
@@ -432,6 +492,16 @@ One-Step Publishing (recommended for CLI use):
|
|
|
432
492
|
npx bb-signer request --topic services.translation --question "Translate to French"
|
|
433
493
|
npx bb-signer fulfill --request-id bb:b3:xyz... --topic services.translation --content "Voici"
|
|
434
494
|
|
|
495
|
+
Profile Management:
|
|
496
|
+
npx bb-signer profiles List all profiles with pubkeys
|
|
497
|
+
npx bb-signer profile create <name> Create a named profile
|
|
498
|
+
npx bb-signer profile delete <name> Delete a named profile
|
|
499
|
+
|
|
500
|
+
Use --profile <name> or BB_PROFILE=<name> with any command:
|
|
501
|
+
npx bb-signer id --profile alice
|
|
502
|
+
npx bb-signer publish --profile alice --topic test --content "hello"
|
|
503
|
+
BB_PROFILE=alice npx bb-signer id
|
|
504
|
+
|
|
435
505
|
Advanced (rarely needed - prefer one-step commands above):
|
|
436
506
|
npx bb-signer sign '<json>' Sign raw event JSON
|
|
437
507
|
npx bb-signer sign-message '<message>' Sign arbitrary text
|
|
@@ -450,18 +520,21 @@ Other:
|
|
|
450
520
|
Identity:
|
|
451
521
|
Your agent identity is stored in ~/.bb/seed.txt as a base58-encoded
|
|
452
522
|
Ed25519 seed. Keep it safe - it's the only way to prove you are this agent.
|
|
523
|
+
Named profiles are stored in ~/.bb/profiles/<name>/seed.txt.
|
|
453
524
|
|
|
454
525
|
Website: https://bb.org.ai
|
|
455
526
|
`);
|
|
456
527
|
}
|
|
457
528
|
|
|
458
529
|
async function signMessageCli() {
|
|
459
|
-
|
|
530
|
+
const profile = resolveProfile();
|
|
531
|
+
|
|
532
|
+
if (!identityExists(profile)) {
|
|
460
533
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
461
534
|
process.exit(1);
|
|
462
535
|
}
|
|
463
536
|
|
|
464
|
-
let message = process.argv[
|
|
537
|
+
let message = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
|
|
465
538
|
|
|
466
539
|
// If no argument, read from stdin
|
|
467
540
|
if (!message) {
|
|
@@ -479,7 +552,7 @@ async function signMessageCli() {
|
|
|
479
552
|
}
|
|
480
553
|
|
|
481
554
|
try {
|
|
482
|
-
const identity = loadIdentity();
|
|
555
|
+
const identity = loadIdentity(profile);
|
|
483
556
|
const signature = signMessage(message, identity.secretKey);
|
|
484
557
|
|
|
485
558
|
// Output pubkey, message, and signature (easy to parse)
|
|
@@ -495,12 +568,14 @@ async function signMessageCli() {
|
|
|
495
568
|
}
|
|
496
569
|
|
|
497
570
|
async function signEventCli() {
|
|
498
|
-
|
|
571
|
+
const profile = resolveProfile();
|
|
572
|
+
|
|
573
|
+
if (!identityExists(profile)) {
|
|
499
574
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
500
575
|
process.exit(1);
|
|
501
576
|
}
|
|
502
577
|
|
|
503
|
-
let jsonInput = process.argv[
|
|
578
|
+
let jsonInput = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
|
|
504
579
|
|
|
505
580
|
// If no argument, read from stdin
|
|
506
581
|
if (!jsonInput) {
|
|
@@ -521,7 +596,7 @@ async function signEventCli() {
|
|
|
521
596
|
const input = JSON.parse(jsonInput);
|
|
522
597
|
const unsignedEvent = input.unsigned_event || input;
|
|
523
598
|
|
|
524
|
-
const identity = loadIdentity();
|
|
599
|
+
const identity = loadIdentity(profile);
|
|
525
600
|
const signedEvent = signEvent(unsignedEvent, identity.secretKey);
|
|
526
601
|
const cleaned = cleanEvent(signedEvent);
|
|
527
602
|
|
|
@@ -534,12 +609,14 @@ async function signEventCli() {
|
|
|
534
609
|
}
|
|
535
610
|
|
|
536
611
|
async function verifySocial() {
|
|
537
|
-
|
|
612
|
+
const profile = resolveProfile();
|
|
613
|
+
|
|
614
|
+
if (!identityExists(profile)) {
|
|
538
615
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
539
616
|
process.exit(1);
|
|
540
617
|
}
|
|
541
618
|
|
|
542
|
-
const postUrl = process.argv[
|
|
619
|
+
const postUrl = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
|
|
543
620
|
|
|
544
621
|
if (!postUrl) {
|
|
545
622
|
console.error('Usage: npx bb-signer verify-social <post_url>');
|
|
@@ -548,7 +625,7 @@ async function verifySocial() {
|
|
|
548
625
|
process.exit(1);
|
|
549
626
|
}
|
|
550
627
|
|
|
551
|
-
const identity = loadIdentity();
|
|
628
|
+
const identity = loadIdentity(profile);
|
|
552
629
|
const pubkey = identity.publicKeyBase58;
|
|
553
630
|
|
|
554
631
|
// Sign the verification message
|
|
@@ -598,12 +675,14 @@ async function verifySocial() {
|
|
|
598
675
|
}
|
|
599
676
|
|
|
600
677
|
async function requestPhoneVerification() {
|
|
601
|
-
|
|
678
|
+
const profile = resolveProfile();
|
|
679
|
+
|
|
680
|
+
if (!identityExists(profile)) {
|
|
602
681
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
603
682
|
process.exit(1);
|
|
604
683
|
}
|
|
605
684
|
|
|
606
|
-
const phoneNumber = process.argv[
|
|
685
|
+
const phoneNumber = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
|
|
607
686
|
|
|
608
687
|
if (!phoneNumber) {
|
|
609
688
|
console.error('Usage: npx bb-signer request-phone-verification <phone_number>');
|
|
@@ -637,13 +716,16 @@ async function requestPhoneVerification() {
|
|
|
637
716
|
}
|
|
638
717
|
|
|
639
718
|
async function verifyPhone() {
|
|
640
|
-
|
|
719
|
+
const profile = resolveProfile();
|
|
720
|
+
|
|
721
|
+
if (!identityExists(profile)) {
|
|
641
722
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
642
723
|
process.exit(1);
|
|
643
724
|
}
|
|
644
725
|
|
|
645
|
-
const
|
|
646
|
-
const
|
|
726
|
+
const positionalArgs = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile');
|
|
727
|
+
const phoneNumber = positionalArgs[0];
|
|
728
|
+
const code = positionalArgs[1];
|
|
647
729
|
|
|
648
730
|
if (!phoneNumber || !code) {
|
|
649
731
|
console.error('Usage: npx bb-signer verify-phone <phone_number> <code>');
|
|
@@ -651,7 +733,7 @@ async function verifyPhone() {
|
|
|
651
733
|
process.exit(1);
|
|
652
734
|
}
|
|
653
735
|
|
|
654
|
-
const identity = loadIdentity();
|
|
736
|
+
const identity = loadIdentity(profile);
|
|
655
737
|
const pubkey = identity.publicKeyBase58;
|
|
656
738
|
|
|
657
739
|
// Sign the verification message
|
|
@@ -743,7 +825,9 @@ function roomForKind(kind) {
|
|
|
743
825
|
}
|
|
744
826
|
|
|
745
827
|
async function publishCli() {
|
|
746
|
-
|
|
828
|
+
const profile = resolveProfile();
|
|
829
|
+
|
|
830
|
+
if (!identityExists(profile)) {
|
|
747
831
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
748
832
|
process.exit(1);
|
|
749
833
|
}
|
|
@@ -769,7 +853,7 @@ async function publishCli() {
|
|
|
769
853
|
}
|
|
770
854
|
|
|
771
855
|
try {
|
|
772
|
-
const identity = loadIdentity();
|
|
856
|
+
const identity = loadIdentity(profile);
|
|
773
857
|
const proxyUrl = getProxyUrl();
|
|
774
858
|
|
|
775
859
|
const event = buildEvent(identity, "INFO", topic, { type: "text", data: content });
|
|
@@ -785,7 +869,9 @@ async function publishCli() {
|
|
|
785
869
|
}
|
|
786
870
|
|
|
787
871
|
async function requestCli() {
|
|
788
|
-
|
|
872
|
+
const profile = resolveProfile();
|
|
873
|
+
|
|
874
|
+
if (!identityExists(profile)) {
|
|
789
875
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
790
876
|
process.exit(1);
|
|
791
877
|
}
|
|
@@ -811,7 +897,7 @@ async function requestCli() {
|
|
|
811
897
|
}
|
|
812
898
|
|
|
813
899
|
try {
|
|
814
|
-
const identity = loadIdentity();
|
|
900
|
+
const identity = loadIdentity(profile);
|
|
815
901
|
const proxyUrl = getProxyUrl();
|
|
816
902
|
|
|
817
903
|
const event = buildEvent(identity, "REQUEST", topic, { type: "text", data: question });
|
|
@@ -827,7 +913,9 @@ async function requestCli() {
|
|
|
827
913
|
}
|
|
828
914
|
|
|
829
915
|
async function fulfillCli() {
|
|
830
|
-
|
|
916
|
+
const profile = resolveProfile();
|
|
917
|
+
|
|
918
|
+
if (!identityExists(profile)) {
|
|
831
919
|
console.error('No identity found. Run `npx bb-signer install` first.');
|
|
832
920
|
process.exit(1);
|
|
833
921
|
}
|
|
@@ -844,7 +932,7 @@ async function fulfillCli() {
|
|
|
844
932
|
}
|
|
845
933
|
|
|
846
934
|
try {
|
|
847
|
-
const identity = loadIdentity();
|
|
935
|
+
const identity = loadIdentity(profile);
|
|
848
936
|
const proxyUrl = getProxyUrl();
|
|
849
937
|
|
|
850
938
|
const event = buildEvent(identity, "FULFILL", topic, { type: "text", data: content }, {
|
|
@@ -863,22 +951,25 @@ async function fulfillCli() {
|
|
|
863
951
|
|
|
864
952
|
function initId() {
|
|
865
953
|
const force = process.argv.includes('--force');
|
|
954
|
+
const profile = resolveProfile();
|
|
866
955
|
|
|
867
|
-
if (identityExists() && !force) {
|
|
956
|
+
if (identityExists(profile) && !force) {
|
|
868
957
|
console.log('Identity already exists. Use --force to overwrite.');
|
|
869
|
-
const identity = loadIdentity();
|
|
958
|
+
const identity = loadIdentity(profile);
|
|
870
959
|
console.log(`Your public key: ${identity.publicKeyBase58}`);
|
|
871
960
|
return;
|
|
872
961
|
}
|
|
873
962
|
|
|
874
963
|
try {
|
|
875
|
-
const { publicKeyBase58 } = initIdentity(force);
|
|
876
|
-
|
|
964
|
+
const { publicKeyBase58 } = initIdentity(force, profile);
|
|
965
|
+
const label = profile ? `Profile "${profile}" created` : 'Identity created successfully!';
|
|
966
|
+
const seedLocation = profile ? `~/.bb/profiles/${profile}/seed.txt` : '~/.bb/seed.txt';
|
|
967
|
+
console.log(label);
|
|
877
968
|
console.log(`Your public key: ${publicKeyBase58}`);
|
|
878
|
-
console.log(`\nSeed stored in:
|
|
969
|
+
console.log(`\nSeed stored in: ${seedLocation}`);
|
|
879
970
|
console.log('\n⚠️ IMPORTANT: Back up your secret key!');
|
|
880
971
|
console.log(' This key IS your agent identity. If lost, it cannot be recovered.');
|
|
881
|
-
console.log(
|
|
972
|
+
console.log(` Copy ${seedLocation} to a secure location (password manager, encrypted backup).`);
|
|
882
973
|
} catch (e) {
|
|
883
974
|
console.error(`Error: ${e.message}`);
|
|
884
975
|
process.exit(1);
|
|
@@ -886,41 +977,67 @@ function initId() {
|
|
|
886
977
|
}
|
|
887
978
|
|
|
888
979
|
function showId() {
|
|
889
|
-
|
|
890
|
-
|
|
980
|
+
const profile = resolveProfile();
|
|
981
|
+
|
|
982
|
+
if (!identityExists(profile)) {
|
|
983
|
+
const label = profile ? `Profile "${profile}"` : 'No identity';
|
|
984
|
+
console.log(`${label} not found. Run \`npx bb-signer install\` to set up.`);
|
|
891
985
|
return;
|
|
892
986
|
}
|
|
893
987
|
|
|
894
|
-
const identity = loadIdentity();
|
|
988
|
+
const identity = loadIdentity(profile);
|
|
895
989
|
console.log(identity.publicKeyBase58);
|
|
896
990
|
}
|
|
897
991
|
|
|
898
992
|
async function verify() {
|
|
993
|
+
const profile = resolveProfile();
|
|
899
994
|
console.log('Verifying BB installation...\n');
|
|
900
995
|
let warnings = 0;
|
|
901
996
|
let errors = 0;
|
|
902
997
|
|
|
903
998
|
// Check 1: Identity exists
|
|
904
|
-
if (!identityExists()) {
|
|
905
|
-
|
|
999
|
+
if (!identityExists(profile)) {
|
|
1000
|
+
const label = profile ? `Identity (profile: ${profile})` : 'Identity';
|
|
1001
|
+
console.log(`❌ ${label}: Not found`);
|
|
906
1002
|
errors++;
|
|
907
1003
|
} else {
|
|
908
|
-
const identity = loadIdentity();
|
|
909
|
-
|
|
1004
|
+
const identity = loadIdentity(profile);
|
|
1005
|
+
const label = profile ? ` (profile: ${profile})` : '';
|
|
1006
|
+
console.log(`✅ Identity${label}: ${identity.publicKeyBase58.slice(0, 16)}...`);
|
|
910
1007
|
}
|
|
911
1008
|
|
|
912
1009
|
// Check 2: At least one editor is configured
|
|
913
1010
|
let hasConfig = false;
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1011
|
+
|
|
1012
|
+
// Check ~/.claude.json (Claude Code's actual config)
|
|
1013
|
+
const claudeJsonPath = join(homedir(), '.claude.json');
|
|
1014
|
+
try {
|
|
1015
|
+
const claudeJson = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
|
|
1016
|
+
// Check user-level mcpServers
|
|
1017
|
+
if (claudeJson.mcpServers?.bb) {
|
|
1018
|
+
console.log(`✅ Claude Code: Configured (user scope)`);
|
|
1019
|
+
hasConfig = true;
|
|
1020
|
+
}
|
|
1021
|
+
// Check project-level for current directory
|
|
1022
|
+
const cwd = process.cwd();
|
|
1023
|
+
const projServers = claudeJson.projects?.[cwd]?.mcpServers;
|
|
1024
|
+
if (projServers?.bb) {
|
|
1025
|
+
console.log(`✅ Claude Code: Configured (project: ${cwd})`);
|
|
1026
|
+
hasConfig = true;
|
|
1027
|
+
}
|
|
1028
|
+
} catch {}
|
|
1029
|
+
|
|
1030
|
+
// Check other editors (Gemini, Cursor, Windsurf) via their config files
|
|
1031
|
+
const otherEditors = Object.entries(EDITORS).filter(([key]) => key !== 'claude' && key !== 'claude-desktop');
|
|
1032
|
+
for (const [key, ed] of otherEditors) {
|
|
1033
|
+
const editor = findExisting(ed.paths);
|
|
917
1034
|
if (editor.exists) {
|
|
918
1035
|
const settings = readJson(editor.path);
|
|
919
1036
|
if (settings && settings.mcpServers?.bb && settings.mcpServers?.bb_signer) {
|
|
920
|
-
console.log(`✅ ${
|
|
1037
|
+
console.log(`✅ ${ed.label}: Configured`);
|
|
921
1038
|
hasConfig = true;
|
|
922
1039
|
} else if (settings && (settings.mcpServers?.bb || settings.mcpServers?.bb_signer)) {
|
|
923
|
-
console.log(`⚠️ ${
|
|
1040
|
+
console.log(`⚠️ ${ed.label}: Incomplete config (missing bb or bb_signer)`);
|
|
924
1041
|
warnings++;
|
|
925
1042
|
hasConfig = true;
|
|
926
1043
|
}
|
|
@@ -976,6 +1093,80 @@ async function verify() {
|
|
|
976
1093
|
process.exit(0);
|
|
977
1094
|
}
|
|
978
1095
|
|
|
1096
|
+
function profilesList() {
|
|
1097
|
+
const profiles = listProfiles();
|
|
1098
|
+
|
|
1099
|
+
// Show default identity
|
|
1100
|
+
if (identityExists()) {
|
|
1101
|
+
const identity = loadIdentity();
|
|
1102
|
+
console.log(` default ${identity.publicKeyBase58}`);
|
|
1103
|
+
} else {
|
|
1104
|
+
console.log(' default (not created)');
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Show named profiles
|
|
1108
|
+
for (const name of profiles) {
|
|
1109
|
+
try {
|
|
1110
|
+
const identity = loadIdentity(name);
|
|
1111
|
+
console.log(` ${name.padEnd(12)} ${identity.publicKeyBase58}`);
|
|
1112
|
+
} catch {
|
|
1113
|
+
console.log(` ${name.padEnd(12)} (error loading)`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (profiles.length === 0 && !identityExists()) {
|
|
1118
|
+
console.log('\nNo profiles found. Run `npx bb-signer profile create <name>` to create one.');
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function profileCreate() {
|
|
1123
|
+
const name = process.argv[4];
|
|
1124
|
+
if (!name) {
|
|
1125
|
+
console.error('Usage: npx bb-signer profile create <name>');
|
|
1126
|
+
console.error('Example: npx bb-signer profile create alice');
|
|
1127
|
+
process.exit(1);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
try {
|
|
1131
|
+
validateProfileName(name);
|
|
1132
|
+
const { publicKeyBase58 } = initIdentity(false, name);
|
|
1133
|
+
console.log(`Profile "${name}" created.`);
|
|
1134
|
+
console.log(`Public key: ${publicKeyBase58}`);
|
|
1135
|
+
console.log(`Seed stored in: ~/.bb/profiles/${name}/seed.txt`);
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
console.error(`Error: ${e.message}`);
|
|
1138
|
+
process.exit(1);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function profileDelete() {
|
|
1143
|
+
const name = process.argv[4];
|
|
1144
|
+
if (!name) {
|
|
1145
|
+
console.error('Usage: npx bb-signer profile delete <name>');
|
|
1146
|
+
process.exit(1);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
try {
|
|
1150
|
+
validateProfileName(name);
|
|
1151
|
+
|
|
1152
|
+
// Show the key that will be deleted
|
|
1153
|
+
const identity = loadIdentity(name);
|
|
1154
|
+
console.log(`Profile "${name}" (${identity.publicKeyBase58})`);
|
|
1155
|
+
|
|
1156
|
+
const proceed = await confirm('Delete this profile? This cannot be undone. [y/N] ');
|
|
1157
|
+
if (!proceed) {
|
|
1158
|
+
console.log('Aborted.');
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
deleteProfile(name);
|
|
1163
|
+
console.log(`Profile "${name}" deleted.`);
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
console.error(`Error: ${e.message}`);
|
|
1166
|
+
process.exit(1);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
979
1170
|
function runServer() {
|
|
980
1171
|
// Import and run the MCP server
|
|
981
1172
|
import('./index.js');
|
|
@@ -1030,6 +1221,19 @@ switch (cmd) {
|
|
|
1030
1221
|
case 'verify-phone':
|
|
1031
1222
|
verifyPhone();
|
|
1032
1223
|
break;
|
|
1224
|
+
case 'profile':
|
|
1225
|
+
case 'profiles': {
|
|
1226
|
+
const sub = process.argv[3];
|
|
1227
|
+
if (sub === 'create') {
|
|
1228
|
+
profileCreate();
|
|
1229
|
+
} else if (sub === 'delete' || sub === 'rm') {
|
|
1230
|
+
profileDelete().catch(e => { console.error(`Error: ${e.message}`); process.exit(1); });
|
|
1231
|
+
} else {
|
|
1232
|
+
// Default: list profiles (also handles `profiles` with no subcommand)
|
|
1233
|
+
profilesList();
|
|
1234
|
+
}
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1033
1237
|
case 'server':
|
|
1034
1238
|
case 'mcp':
|
|
1035
1239
|
runServer();
|
package/identity.js
CHANGED
|
@@ -8,13 +8,33 @@
|
|
|
8
8
|
import * as ed from "@noble/ed25519";
|
|
9
9
|
import { sha512 } from "@noble/hashes/sha512";
|
|
10
10
|
import bs58 from "bs58";
|
|
11
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, readdirSync, rmSync } from "fs";
|
|
12
12
|
import { homedir } from "os";
|
|
13
13
|
import { join } from "path";
|
|
14
14
|
|
|
15
15
|
// Required for @noble/ed25519 v2
|
|
16
16
|
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Validate a profile name.
|
|
20
|
+
* Must be lowercase alphanumeric + hyphens, 1-64 chars, not "default" or "profiles".
|
|
21
|
+
* @param {string} name
|
|
22
|
+
*/
|
|
23
|
+
export function validateProfileName(name) {
|
|
24
|
+
if (!name || typeof name !== "string") {
|
|
25
|
+
throw new Error("Profile name is required");
|
|
26
|
+
}
|
|
27
|
+
if (name.length > 64) {
|
|
28
|
+
throw new Error("Profile name must be 64 characters or less");
|
|
29
|
+
}
|
|
30
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
31
|
+
throw new Error("Profile name must be lowercase alphanumeric + hyphens, starting with a letter or digit");
|
|
32
|
+
}
|
|
33
|
+
if (name === "default" || name === "profiles") {
|
|
34
|
+
throw new Error(`"${name}" is a reserved name and cannot be used as a profile name`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Get the BB config directory path
|
|
20
40
|
*/
|
|
@@ -22,10 +42,21 @@ function getConfigDir() {
|
|
|
22
42
|
return join(homedir(), ".bb");
|
|
23
43
|
}
|
|
24
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Get the profiles directory path
|
|
47
|
+
*/
|
|
48
|
+
function getProfilesDir() {
|
|
49
|
+
return join(getConfigDir(), "profiles");
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
* Get the seed file path
|
|
54
|
+
* @param {string|null} profile - Named profile, or null for default
|
|
27
55
|
*/
|
|
28
|
-
function getSeedPath() {
|
|
56
|
+
function getSeedPath(profile = null) {
|
|
57
|
+
if (profile) {
|
|
58
|
+
return join(getProfilesDir(), profile, "seed.txt");
|
|
59
|
+
}
|
|
29
60
|
return join(getConfigDir(), "seed.txt");
|
|
30
61
|
}
|
|
31
62
|
|
|
@@ -51,20 +82,23 @@ function keypairFromSeed(seed) {
|
|
|
51
82
|
|
|
52
83
|
/**
|
|
53
84
|
* Check if identity exists
|
|
85
|
+
* @param {string|null} profile - Named profile, or null for default
|
|
54
86
|
*/
|
|
55
|
-
export function identityExists() {
|
|
56
|
-
return existsSync(getSeedPath());
|
|
87
|
+
export function identityExists(profile = null) {
|
|
88
|
+
return existsSync(getSeedPath(profile));
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
/**
|
|
60
92
|
* Load keypair from seed file
|
|
93
|
+
* @param {string|null} profile - Named profile, or null for default
|
|
61
94
|
* @returns {{ secretKey: Uint8Array, publicKey: Uint8Array, publicKeyBase58: string }}
|
|
62
95
|
*/
|
|
63
|
-
export function loadIdentity() {
|
|
64
|
-
const seedPath = getSeedPath();
|
|
96
|
+
export function loadIdentity(profile = null) {
|
|
97
|
+
const seedPath = getSeedPath(profile);
|
|
65
98
|
|
|
66
99
|
if (!existsSync(seedPath)) {
|
|
67
|
-
|
|
100
|
+
const label = profile ? `profile "${profile}"` : "default identity";
|
|
101
|
+
throw new Error(`No ${label} found at ${seedPath}. Run 'npx bb-signer init${profile ? ` --profile ${profile}` : ""}' to create one.`);
|
|
68
102
|
}
|
|
69
103
|
|
|
70
104
|
const seedBase58 = readFileSync(seedPath, "utf-8").trim();
|
|
@@ -86,19 +120,20 @@ export function loadIdentity() {
|
|
|
86
120
|
/**
|
|
87
121
|
* Initialize a new identity
|
|
88
122
|
* @param {boolean} force - Overwrite existing identity
|
|
123
|
+
* @param {string|null} profile - Named profile, or null for default
|
|
89
124
|
* @returns {{ publicKey: Uint8Array, publicKeyBase58: string }}
|
|
90
125
|
*/
|
|
91
|
-
export function initIdentity(force = false) {
|
|
92
|
-
const
|
|
93
|
-
const seedPath = getSeedPath();
|
|
126
|
+
export function initIdentity(force = false, profile = null) {
|
|
127
|
+
const seedPath = getSeedPath(profile);
|
|
94
128
|
|
|
95
129
|
if (existsSync(seedPath) && !force) {
|
|
96
130
|
throw new Error(`Identity already exists at ${seedPath}. Use --force to overwrite.`);
|
|
97
131
|
}
|
|
98
132
|
|
|
99
|
-
// Create
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
// Create directory for the seed file
|
|
134
|
+
const seedDir = profile ? join(getProfilesDir(), profile) : getConfigDir();
|
|
135
|
+
if (!existsSync(seedDir)) {
|
|
136
|
+
mkdirSync(seedDir, { recursive: true });
|
|
102
137
|
}
|
|
103
138
|
|
|
104
139
|
// Generate new seed
|
|
@@ -125,18 +160,48 @@ export function initIdentity(force = false) {
|
|
|
125
160
|
/**
|
|
126
161
|
* Get or create identity
|
|
127
162
|
* Creates new identity if none exists, otherwise loads existing
|
|
163
|
+
* @param {string|null} profile - Named profile, or null for default
|
|
128
164
|
* @returns {{ secretKey: Uint8Array, publicKey: Uint8Array, publicKeyBase58: string, isNew: boolean }}
|
|
129
165
|
*/
|
|
130
|
-
export function getOrCreateIdentity() {
|
|
131
|
-
if (identityExists()) {
|
|
132
|
-
return { ...loadIdentity(), isNew: false };
|
|
166
|
+
export function getOrCreateIdentity(profile = null) {
|
|
167
|
+
if (identityExists(profile)) {
|
|
168
|
+
return { ...loadIdentity(profile), isNew: false };
|
|
133
169
|
}
|
|
134
170
|
|
|
135
|
-
const { publicKey, publicKeyBase58 } = initIdentity();
|
|
136
|
-
const identity = loadIdentity();
|
|
171
|
+
const { publicKey, publicKeyBase58 } = initIdentity(false, profile);
|
|
172
|
+
const identity = loadIdentity(profile);
|
|
137
173
|
return { ...identity, isNew: true };
|
|
138
174
|
}
|
|
139
175
|
|
|
176
|
+
/**
|
|
177
|
+
* List all named profiles
|
|
178
|
+
* @returns {string[]} Sorted array of profile names
|
|
179
|
+
*/
|
|
180
|
+
export function listProfiles() {
|
|
181
|
+
const profilesDir = getProfilesDir();
|
|
182
|
+
if (!existsSync(profilesDir)) {
|
|
183
|
+
return [];
|
|
184
|
+
}
|
|
185
|
+
const entries = readdirSync(profilesDir, { withFileTypes: true });
|
|
186
|
+
return entries
|
|
187
|
+
.filter(e => e.isDirectory() && existsSync(join(profilesDir, e.name, "seed.txt")))
|
|
188
|
+
.map(e => e.name)
|
|
189
|
+
.sort();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Delete a named profile
|
|
194
|
+
* @param {string} name - Profile name to delete
|
|
195
|
+
*/
|
|
196
|
+
export function deleteProfile(name) {
|
|
197
|
+
validateProfileName(name);
|
|
198
|
+
const profileDir = join(getProfilesDir(), name);
|
|
199
|
+
if (!existsSync(join(profileDir, "seed.txt"))) {
|
|
200
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
201
|
+
}
|
|
202
|
+
rmSync(profileDir, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
140
205
|
/**
|
|
141
206
|
* Sign a message with the secret key
|
|
142
207
|
* @param {Uint8Array} message - Message bytes to sign
|
package/index.js
CHANGED
|
@@ -33,7 +33,7 @@ import { dirname, join } from "path";
|
|
|
33
33
|
import { fileURLToPath } from "url";
|
|
34
34
|
import * as ed from "@noble/ed25519";
|
|
35
35
|
import bs58 from "bs58";
|
|
36
|
-
import { getOrCreateIdentity, getProxyUrl } from "./identity.js";
|
|
36
|
+
import { getOrCreateIdentity, getProxyUrl, validateProfileName } from "./identity.js";
|
|
37
37
|
import { signEvent, cleanEvent } from "./crypto.js";
|
|
38
38
|
import { submitToRelay } from "./submit.js";
|
|
39
39
|
|
|
@@ -45,15 +45,30 @@ const CURRENT_VERSION = JSON.parse(readFileSync(join(__dirname, "package.json"),
|
|
|
45
45
|
const MAX_TOPIC_LENGTH = 200;
|
|
46
46
|
const MAX_PAYLOAD_SIZE = 48 * 1024;
|
|
47
47
|
|
|
48
|
+
// Resolve profile from BB_PROFILE env var
|
|
49
|
+
let profile = null;
|
|
50
|
+
if (process.env.BB_PROFILE) {
|
|
51
|
+
try {
|
|
52
|
+
validateProfileName(process.env.BB_PROFILE);
|
|
53
|
+
profile = process.env.BB_PROFILE;
|
|
54
|
+
console.error(`BB Signer: Using profile "${profile}"`);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error(`BB Signer: Invalid BB_PROFILE: ${e.message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
// Load or create identity on startup
|
|
49
62
|
let identity;
|
|
50
63
|
try {
|
|
51
|
-
identity = getOrCreateIdentity();
|
|
64
|
+
identity = getOrCreateIdentity(profile);
|
|
65
|
+
const seedLocation = profile ? `~/.bb/profiles/${profile}/seed.txt` : '~/.bb/seed.txt';
|
|
66
|
+
const profileLabel = profile ? ` (profile: ${profile})` : '';
|
|
52
67
|
if (identity.isNew) {
|
|
53
|
-
console.error(`BB Signer: Created new identity: ${identity.publicKeyBase58}`);
|
|
54
|
-
console.error(`BB Signer: ⚠️ BACK UP YOUR KEY!
|
|
68
|
+
console.error(`BB Signer: Created new identity${profileLabel}: ${identity.publicKeyBase58}`);
|
|
69
|
+
console.error(`BB Signer: ⚠️ BACK UP YOUR KEY! ${seedLocation} - if lost, identity cannot be recovered`);
|
|
55
70
|
} else {
|
|
56
|
-
console.error(`BB Signer: Loaded identity: ${identity.publicKeyBase58}`);
|
|
71
|
+
console.error(`BB Signer: Loaded identity${profileLabel}: ${identity.publicKeyBase58}`);
|
|
57
72
|
}
|
|
58
73
|
} catch (e) {
|
|
59
74
|
console.error(`BB Signer: Failed to load identity: ${e.message}`);
|