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.
Files changed (4) hide show
  1. package/cli.js +267 -63
  2. package/identity.js +83 -18
  3. package/index.js +20 -5
  4. 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: try `claude mcp add`, always also write settings.json as fallback.
316
- // `claude mcp add` can hang when run from within a Claude Code bash session,
317
- // so the settings.json write ensures config is always persisted.
318
- if (ed.usesCli && hasClaude()) {
319
- console.log('\nConfiguring via Claude CLI...');
320
- try {
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
- if (!identityExists()) {
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[3];
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
- if (!identityExists()) {
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[3];
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
- if (!identityExists()) {
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[3];
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
- if (!identityExists()) {
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[3];
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
- if (!identityExists()) {
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 phoneNumber = process.argv[3];
646
- const code = process.argv[4];
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
- if (!identityExists()) {
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
- if (!identityExists()) {
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
- if (!identityExists()) {
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
- console.log('Identity created successfully!');
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: ~/.bb/seed.txt`);
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(' Copy ~/.bb/seed.txt to a secure location (password manager, encrypted backup).');
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
- if (!identityExists()) {
890
- console.log('No identity found. Run `npx bb-signer install` to set up.');
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
- console.log('❌ Identity: Not found');
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
- console.log(`✅ Identity: ${identity.publicKeyBase58.slice(0, 16)}...`);
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
- const editorChecks = Object.entries(EDITORS).map(([key, ed]) => [ed.label, ed.paths]);
915
- for (const [name, paths] of editorChecks) {
916
- const editor = findExisting(paths);
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(`✅ ${name}: Configured`);
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(`⚠️ ${name}: Incomplete config (missing bb or bb_signer)`);
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
- throw new Error(`No identity found at ${seedPath}. Run 'bb-signer init' to create one.`);
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 configDir = getConfigDir();
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 config directory
100
- if (!existsSync(configDir)) {
101
- mkdirSync(configDir, { recursive: true });
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! ~/.bb/seed.txt - if lost, identity cannot be recovered`);
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}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bb-signer",
3
- "version": "0.3.10",
3
+ "version": "0.5.0",
4
4
  "description": "Minimal local signer for BB - signs events for the agent collaboration network",
5
5
  "type": "module",
6
6
  "main": "index.js",