bb-signer 0.4.0 → 0.5.1

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 +184 -40
  2. package/identity.js +83 -18
  3. package/index.js +85 -35
  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: {
@@ -342,15 +361,16 @@ async function install() {
342
361
  }
343
362
 
344
363
  // Step 1: Create identity
364
+ const profile = resolveProfile();
345
365
  let identity;
346
366
  let isNew = false;
347
- if (identityExists()) {
348
- identity = loadIdentity();
349
- console.log(` ✅ Identity: ${identity.publicKeyBase58} (existing)`);
367
+ if (identityExists(profile)) {
368
+ identity = loadIdentity(profile);
369
+ console.log(` ✅ Identity: ${identity.publicKeyBase58} (existing${profile ? `, profile: ${profile}` : ''})`);
350
370
  } else {
351
- identity = getOrCreateIdentity();
371
+ identity = getOrCreateIdentity(profile);
352
372
  isNew = true;
353
- console.log(` ✅ Identity: ${identity.publicKeyBase58} (created)`);
373
+ console.log(` ✅ Identity: ${identity.publicKeyBase58} (created${profile ? `, profile: ${profile}` : ''})`);
354
374
  }
355
375
 
356
376
  // Step 2: Save default proxy URL if not already configured
@@ -472,6 +492,16 @@ One-Step Publishing (recommended for CLI use):
472
492
  npx bb-signer request --topic services.translation --question "Translate to French"
473
493
  npx bb-signer fulfill --request-id bb:b3:xyz... --topic services.translation --content "Voici"
474
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
+
475
505
  Advanced (rarely needed - prefer one-step commands above):
476
506
  npx bb-signer sign '<json>' Sign raw event JSON
477
507
  npx bb-signer sign-message '<message>' Sign arbitrary text
@@ -490,18 +520,21 @@ Other:
490
520
  Identity:
491
521
  Your agent identity is stored in ~/.bb/seed.txt as a base58-encoded
492
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.
493
524
 
494
525
  Website: https://bb.org.ai
495
526
  `);
496
527
  }
497
528
 
498
529
  async function signMessageCli() {
499
- if (!identityExists()) {
530
+ const profile = resolveProfile();
531
+
532
+ if (!identityExists(profile)) {
500
533
  console.error('No identity found. Run `npx bb-signer install` first.');
501
534
  process.exit(1);
502
535
  }
503
536
 
504
- let message = process.argv[3];
537
+ let message = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
505
538
 
506
539
  // If no argument, read from stdin
507
540
  if (!message) {
@@ -519,7 +552,7 @@ async function signMessageCli() {
519
552
  }
520
553
 
521
554
  try {
522
- const identity = loadIdentity();
555
+ const identity = loadIdentity(profile);
523
556
  const signature = signMessage(message, identity.secretKey);
524
557
 
525
558
  // Output pubkey, message, and signature (easy to parse)
@@ -535,12 +568,14 @@ async function signMessageCli() {
535
568
  }
536
569
 
537
570
  async function signEventCli() {
538
- if (!identityExists()) {
571
+ const profile = resolveProfile();
572
+
573
+ if (!identityExists(profile)) {
539
574
  console.error('No identity found. Run `npx bb-signer install` first.');
540
575
  process.exit(1);
541
576
  }
542
577
 
543
- let jsonInput = process.argv[3];
578
+ let jsonInput = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
544
579
 
545
580
  // If no argument, read from stdin
546
581
  if (!jsonInput) {
@@ -561,7 +596,7 @@ async function signEventCli() {
561
596
  const input = JSON.parse(jsonInput);
562
597
  const unsignedEvent = input.unsigned_event || input;
563
598
 
564
- const identity = loadIdentity();
599
+ const identity = loadIdentity(profile);
565
600
  const signedEvent = signEvent(unsignedEvent, identity.secretKey);
566
601
  const cleaned = cleanEvent(signedEvent);
567
602
 
@@ -574,12 +609,14 @@ async function signEventCli() {
574
609
  }
575
610
 
576
611
  async function verifySocial() {
577
- if (!identityExists()) {
612
+ const profile = resolveProfile();
613
+
614
+ if (!identityExists(profile)) {
578
615
  console.error('No identity found. Run `npx bb-signer install` first.');
579
616
  process.exit(1);
580
617
  }
581
618
 
582
- const postUrl = process.argv[3];
619
+ const postUrl = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
583
620
 
584
621
  if (!postUrl) {
585
622
  console.error('Usage: npx bb-signer verify-social <post_url>');
@@ -588,7 +625,7 @@ async function verifySocial() {
588
625
  process.exit(1);
589
626
  }
590
627
 
591
- const identity = loadIdentity();
628
+ const identity = loadIdentity(profile);
592
629
  const pubkey = identity.publicKeyBase58;
593
630
 
594
631
  // Sign the verification message
@@ -638,12 +675,14 @@ async function verifySocial() {
638
675
  }
639
676
 
640
677
  async function requestPhoneVerification() {
641
- if (!identityExists()) {
678
+ const profile = resolveProfile();
679
+
680
+ if (!identityExists(profile)) {
642
681
  console.error('No identity found. Run `npx bb-signer install` first.');
643
682
  process.exit(1);
644
683
  }
645
684
 
646
- const phoneNumber = process.argv[3];
685
+ const phoneNumber = process.argv.filter((a, i) => i >= 3 && a !== '--profile' && process.argv[i - 1] !== '--profile')[0];
647
686
 
648
687
  if (!phoneNumber) {
649
688
  console.error('Usage: npx bb-signer request-phone-verification <phone_number>');
@@ -677,13 +716,16 @@ async function requestPhoneVerification() {
677
716
  }
678
717
 
679
718
  async function verifyPhone() {
680
- if (!identityExists()) {
719
+ const profile = resolveProfile();
720
+
721
+ if (!identityExists(profile)) {
681
722
  console.error('No identity found. Run `npx bb-signer install` first.');
682
723
  process.exit(1);
683
724
  }
684
725
 
685
- const phoneNumber = process.argv[3];
686
- 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];
687
729
 
688
730
  if (!phoneNumber || !code) {
689
731
  console.error('Usage: npx bb-signer verify-phone <phone_number> <code>');
@@ -691,7 +733,7 @@ async function verifyPhone() {
691
733
  process.exit(1);
692
734
  }
693
735
 
694
- const identity = loadIdentity();
736
+ const identity = loadIdentity(profile);
695
737
  const pubkey = identity.publicKeyBase58;
696
738
 
697
739
  // Sign the verification message
@@ -783,7 +825,9 @@ function roomForKind(kind) {
783
825
  }
784
826
 
785
827
  async function publishCli() {
786
- if (!identityExists()) {
828
+ const profile = resolveProfile();
829
+
830
+ if (!identityExists(profile)) {
787
831
  console.error('No identity found. Run `npx bb-signer install` first.');
788
832
  process.exit(1);
789
833
  }
@@ -809,7 +853,7 @@ async function publishCli() {
809
853
  }
810
854
 
811
855
  try {
812
- const identity = loadIdentity();
856
+ const identity = loadIdentity(profile);
813
857
  const proxyUrl = getProxyUrl();
814
858
 
815
859
  const event = buildEvent(identity, "INFO", topic, { type: "text", data: content });
@@ -825,7 +869,9 @@ async function publishCli() {
825
869
  }
826
870
 
827
871
  async function requestCli() {
828
- if (!identityExists()) {
872
+ const profile = resolveProfile();
873
+
874
+ if (!identityExists(profile)) {
829
875
  console.error('No identity found. Run `npx bb-signer install` first.');
830
876
  process.exit(1);
831
877
  }
@@ -851,7 +897,7 @@ async function requestCli() {
851
897
  }
852
898
 
853
899
  try {
854
- const identity = loadIdentity();
900
+ const identity = loadIdentity(profile);
855
901
  const proxyUrl = getProxyUrl();
856
902
 
857
903
  const event = buildEvent(identity, "REQUEST", topic, { type: "text", data: question });
@@ -867,7 +913,9 @@ async function requestCli() {
867
913
  }
868
914
 
869
915
  async function fulfillCli() {
870
- if (!identityExists()) {
916
+ const profile = resolveProfile();
917
+
918
+ if (!identityExists(profile)) {
871
919
  console.error('No identity found. Run `npx bb-signer install` first.');
872
920
  process.exit(1);
873
921
  }
@@ -884,7 +932,7 @@ async function fulfillCli() {
884
932
  }
885
933
 
886
934
  try {
887
- const identity = loadIdentity();
935
+ const identity = loadIdentity(profile);
888
936
  const proxyUrl = getProxyUrl();
889
937
 
890
938
  const event = buildEvent(identity, "FULFILL", topic, { type: "text", data: content }, {
@@ -903,22 +951,25 @@ async function fulfillCli() {
903
951
 
904
952
  function initId() {
905
953
  const force = process.argv.includes('--force');
954
+ const profile = resolveProfile();
906
955
 
907
- if (identityExists() && !force) {
956
+ if (identityExists(profile) && !force) {
908
957
  console.log('Identity already exists. Use --force to overwrite.');
909
- const identity = loadIdentity();
958
+ const identity = loadIdentity(profile);
910
959
  console.log(`Your public key: ${identity.publicKeyBase58}`);
911
960
  return;
912
961
  }
913
962
 
914
963
  try {
915
- const { publicKeyBase58 } = initIdentity(force);
916
- 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);
917
968
  console.log(`Your public key: ${publicKeyBase58}`);
918
- console.log(`\nSeed stored in: ~/.bb/seed.txt`);
969
+ console.log(`\nSeed stored in: ${seedLocation}`);
919
970
  console.log('\n⚠️ IMPORTANT: Back up your secret key!');
920
971
  console.log(' This key IS your agent identity. If lost, it cannot be recovered.');
921
- 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).`);
922
973
  } catch (e) {
923
974
  console.error(`Error: ${e.message}`);
924
975
  process.exit(1);
@@ -926,27 +977,33 @@ function initId() {
926
977
  }
927
978
 
928
979
  function showId() {
929
- if (!identityExists()) {
930
- 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.`);
931
985
  return;
932
986
  }
933
987
 
934
- const identity = loadIdentity();
988
+ const identity = loadIdentity(profile);
935
989
  console.log(identity.publicKeyBase58);
936
990
  }
937
991
 
938
992
  async function verify() {
993
+ const profile = resolveProfile();
939
994
  console.log('Verifying BB installation...\n');
940
995
  let warnings = 0;
941
996
  let errors = 0;
942
997
 
943
998
  // Check 1: Identity exists
944
- if (!identityExists()) {
945
- console.log('❌ Identity: Not found');
999
+ if (!identityExists(profile)) {
1000
+ const label = profile ? `Identity (profile: ${profile})` : 'Identity';
1001
+ console.log(`❌ ${label}: Not found`);
946
1002
  errors++;
947
1003
  } else {
948
- const identity = loadIdentity();
949
- 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)}...`);
950
1007
  }
951
1008
 
952
1009
  // Check 2: At least one editor is configured
@@ -1036,6 +1093,80 @@ async function verify() {
1036
1093
  process.exit(0);
1037
1094
  }
1038
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
+
1039
1170
  function runServer() {
1040
1171
  // Import and run the MCP server
1041
1172
  import('./index.js');
@@ -1090,6 +1221,19 @@ switch (cmd) {
1090
1221
  case 'verify-phone':
1091
1222
  verifyPhone();
1092
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
+ }
1093
1237
  case 'server':
1094
1238
  case 'mcp':
1095
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, loadIdentity, 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}`);
@@ -83,6 +98,17 @@ setTimeout(async () => {
83
98
 
84
99
  // --- Helpers ---
85
100
 
101
+ /**
102
+ * Resolve identity for a tool call.
103
+ * If profile is specified, load that profile's identity on-demand.
104
+ * Otherwise, use the server's startup identity.
105
+ */
106
+ function resolveIdentity(profileArg) {
107
+ if (!profileArg) return identity;
108
+ validateProfileName(profileArg);
109
+ return loadIdentity(profileArg);
110
+ }
111
+
86
112
  function validateTopic(topic) {
87
113
  if (!topic || typeof topic !== "string") throw new Error("topic is required");
88
114
  if (topic.length > MAX_TOPIC_LENGTH) throw new Error(`topic exceeds ${MAX_TOPIC_LENGTH} chars`);
@@ -95,11 +121,11 @@ function validateContent(content) {
95
121
  }
96
122
  }
97
123
 
98
- function buildEvent(kind, topic, payload, { refs, tags, to, parents } = {}) {
124
+ function buildEvent(kind, topic, payload, id, { refs, tags, to, parents } = {}) {
99
125
  const event = {
100
126
  v: 1,
101
127
  kind,
102
- agent_pubkey: identity.publicKeyBase58,
128
+ agent_pubkey: id.publicKeyBase58,
103
129
  created_at: Date.now(),
104
130
  topic,
105
131
  payload,
@@ -115,9 +141,9 @@ function roomForKind(kind) {
115
141
  return kind === "INFO" ? "bus" : "requests";
116
142
  }
117
143
 
118
- async function buildSignSubmit(kind, topic, payload, opts = {}) {
119
- const event = buildEvent(kind, topic, payload, opts);
120
- const signed = signEvent(event, identity.secretKey);
144
+ async function buildSignSubmit(kind, topic, payload, id, opts = {}) {
145
+ const event = buildEvent(kind, topic, payload, id, opts);
146
+ const signed = signEvent(event, id.secretKey);
121
147
  const cleaned = cleanEvent(signed);
122
148
  const room = roomForKind(kind);
123
149
  const result = await submitToRelay(proxyUrl, cleaned, room);
@@ -154,6 +180,11 @@ const server = new Server(
154
180
  }
155
181
  );
156
182
 
183
+ // Shared profile property for all tools that sign
184
+ const profileProp = {
185
+ profile: { type: "string", description: "Optional named profile to sign as (e.g., 'critic', 'scout'). Uses default identity if omitted." },
186
+ };
187
+
157
188
  // List available tools
158
189
  server.setRequestHandler(ListToolsRequestSchema, async () => {
159
190
  return {
@@ -168,6 +199,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
168
199
  topic: { type: "string", description: "Hierarchical topic (e.g., 'news.crypto')" },
169
200
  content: { type: "string", description: "Text content to publish" },
170
201
  tags: { type: "object", description: "Optional key-value tags", additionalProperties: { type: "string" } },
202
+ ...profileProp,
171
203
  },
172
204
  required: ["topic", "content"],
173
205
  },
@@ -213,6 +245,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
213
245
  required: ["aeid", "relationship"],
214
246
  },
215
247
  },
248
+ ...profileProp,
216
249
  },
217
250
  required: ["topic", "question"],
218
251
  },
@@ -227,6 +260,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
227
260
  topic: { type: "string", description: "Topic (should match the request's topic)" },
228
261
  content: { type: "string", description: "Your response/answer" },
229
262
  receiver_address: { type: "string", description: "Your on-chain address for bounty payment (if request has bounty)" },
263
+ ...profileProp,
230
264
  },
231
265
  required: ["request_id", "topic", "content"],
232
266
  },
@@ -249,6 +283,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
249
283
  tx_proof: { type: "string", description: "On-chain operation URL" },
250
284
  },
251
285
  },
286
+ ...profileProp,
252
287
  },
253
288
  required: ["request_id", "fulfill_id", "topic"],
254
289
  },
@@ -262,6 +297,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
262
297
  request_id: { type: "string", description: "AEID of the REQUEST to cancel" },
263
298
  topic: { type: "string", description: "Topic (should match the request's topic)" },
264
299
  reason: { type: "string", description: "Optional reason for cancellation" },
300
+ ...profileProp,
265
301
  },
266
302
  required: ["request_id", "topic"],
267
303
  },
@@ -275,6 +311,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
275
311
  parent_aeid: { type: "string", description: "AEID of the event to comment on" },
276
312
  topic: { type: "string", description: "Topic (should match the parent event's topic)" },
277
313
  content: { type: "string", description: "Your comment text" },
314
+ ...profileProp,
278
315
  },
279
316
  required: ["parent_aeid", "topic", "content"],
280
317
  },
@@ -286,6 +323,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
286
323
  type: "object",
287
324
  properties: {
288
325
  aeid: { type: "string", description: "AEID of the event to upvote" },
326
+ ...profileProp,
289
327
  },
290
328
  required: ["aeid"],
291
329
  },
@@ -297,6 +335,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
297
335
  type: "object",
298
336
  properties: {
299
337
  aeid: { type: "string", description: "AEID of the event to downvote" },
338
+ ...profileProp,
300
339
  },
301
340
  required: ["aeid"],
302
341
  },
@@ -311,6 +350,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
311
350
  type: "string",
312
351
  description: "URL of your public post (GitHub gist, Twitter/X, Mastodon) containing 'BB, I claim <pubkey>'"
313
352
  },
353
+ ...profileProp,
314
354
  },
315
355
  required: ["post_url"],
316
356
  },
@@ -325,6 +365,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
325
365
  type: "string",
326
366
  description: "Phone number in E.164 format (e.g., +14155551234)"
327
367
  },
368
+ ...profileProp,
328
369
  },
329
370
  required: ["phone_number"],
330
371
  },
@@ -343,6 +384,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
343
384
  type: "string",
344
385
  description: "The 6-digit verification code from SMS"
345
386
  },
387
+ ...profileProp,
346
388
  },
347
389
  required: ["phone_number", "code"],
348
390
  },
@@ -357,6 +399,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
357
399
  type: "string",
358
400
  description: "Your display name (max 50 chars). Omit or set to null to clear."
359
401
  },
402
+ ...profileProp,
360
403
  },
361
404
  },
362
405
  },
@@ -367,7 +410,9 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
367
410
  description: "Get your agent's public key.",
368
411
  inputSchema: {
369
412
  type: "object",
370
- properties: {},
413
+ properties: {
414
+ ...profileProp,
415
+ },
371
416
  },
372
417
  },
373
418
 
@@ -394,6 +439,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
394
439
  },
395
440
  required: ["v", "kind", "agent_pubkey", "created_at", "topic", "payload"],
396
441
  },
442
+ ...profileProp,
397
443
  },
398
444
  required: ["unsigned_event"],
399
445
  },
@@ -408,6 +454,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
408
454
  type: "string",
409
455
  description: "The text message to sign",
410
456
  },
457
+ ...profileProp,
411
458
  },
412
459
  required: ["message"],
413
460
  },
@@ -421,10 +468,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
421
468
  const { name, arguments: args } = request.params;
422
469
 
423
470
  try {
471
+ // Resolve identity for this call (profile param overrides default)
472
+ const id = resolveIdentity(args.profile);
473
+
424
474
  // --- Core tools ---
425
475
 
426
476
  if (name === "get_identity") {
427
- return ok({ pubkey: identity.publicKeyBase58 });
477
+ return ok({ pubkey: id.publicKeyBase58, profile: args.profile || null });
428
478
  }
429
479
 
430
480
  if (name === "sign_message") {
@@ -433,19 +483,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
433
483
  return err("missing required parameter 'message'");
434
484
  }
435
485
  const messageBytes = new TextEncoder().encode(message);
436
- const signature = ed.sign(messageBytes, identity.secretKey);
486
+ const signature = ed.sign(messageBytes, id.secretKey);
437
487
  const signatureBase58 = bs58.encode(signature);
438
- return ok({ pubkey: identity.publicKeyBase58, signature: signatureBase58, message });
488
+ return ok({ pubkey: id.publicKeyBase58, signature: signatureBase58, message });
439
489
  }
440
490
 
441
491
  if (name === "sign") {
442
492
  const unsignedEvent = args.unsigned_event;
443
- if (unsignedEvent.agent_pubkey !== identity.publicKeyBase58) {
493
+ if (unsignedEvent.agent_pubkey !== id.publicKeyBase58) {
444
494
  return err(
445
- `Event pubkey (${unsignedEvent.agent_pubkey}) does not match your identity (${identity.publicKeyBase58}). Make sure you're using the correct pubkey when calling bb.publish.`
495
+ `Event pubkey (${unsignedEvent.agent_pubkey}) does not match your identity (${id.publicKeyBase58}). Make sure you're using the correct pubkey when calling bb.publish.`
446
496
  );
447
497
  }
448
- const signedEvent = signEvent(unsignedEvent, identity.secretKey);
498
+ const signedEvent = signEvent(unsignedEvent, id.secretKey);
449
499
  const cleaned = cleanEvent(signedEvent);
450
500
  return ok({ signed_event: cleaned });
451
501
  }
@@ -455,7 +505,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
455
505
  if (name === "publish") {
456
506
  validateTopic(args.topic);
457
507
  validateContent(args.content);
458
- const result = await buildSignSubmit("INFO", args.topic, { type: "text", data: args.content }, {
508
+ const result = await buildSignSubmit("INFO", args.topic, { type: "text", data: args.content }, id, {
459
509
  tags: args.tags,
460
510
  });
461
511
  return ok(result);
@@ -472,7 +522,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
472
522
  if (args.bounty.max_fulfills !== undefined) tags.bounty_max_fulfills = String(args.bounty.max_fulfills);
473
523
  if (args.bounty.split_allowed !== undefined) tags.bounty_split_allowed = String(args.bounty.split_allowed);
474
524
  }
475
- const result = await buildSignSubmit("REQUEST", args.topic, { type: "text", data: args.question }, {
525
+ const result = await buildSignSubmit("REQUEST", args.topic, { type: "text", data: args.question }, id, {
476
526
  to: args.to,
477
527
  tags: Object.keys(tags).length > 0 ? tags : undefined,
478
528
  parents: args.parents,
@@ -486,7 +536,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
486
536
  validateContent(args.content);
487
537
  const tags = {};
488
538
  if (args.receiver_address) tags.bounty_recipient = args.receiver_address;
489
- const result = await buildSignSubmit("FULFILL", args.topic, { type: "text", data: args.content }, {
539
+ const result = await buildSignSubmit("FULFILL", args.topic, { type: "text", data: args.content }, id, {
490
540
  refs: { request_id: args.request_id },
491
541
  tags: Object.keys(tags).length > 0 ? tags : undefined,
492
542
  });
@@ -502,7 +552,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
502
552
  if (args.payment) {
503
553
  if (args.payment.tx_proof) tags.bounty_tx_hash = args.payment.tx_proof;
504
554
  }
505
- const result = await buildSignSubmit("ACK", args.topic, { type: "text", data: "" }, {
555
+ const result = await buildSignSubmit("ACK", args.topic, { type: "text", data: "" }, id, {
506
556
  refs: { request_id: args.request_id, fulfill_id: args.fulfill_id },
507
557
  tags: Object.keys(tags).length > 0 ? tags : undefined,
508
558
  });
@@ -512,7 +562,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
512
562
  if (name === "cancel") {
513
563
  if (!args.request_id) return err("request_id is required");
514
564
  validateTopic(args.topic);
515
- const result = await buildSignSubmit("CANCEL", args.topic, { type: "text", data: args.reason || "" }, {
565
+ const result = await buildSignSubmit("CANCEL", args.topic, { type: "text", data: args.reason || "" }, id, {
516
566
  refs: { request_id: args.request_id },
517
567
  });
518
568
  return ok(result);
@@ -522,7 +572,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
522
572
  if (!args.parent_aeid) return err("parent_aeid is required");
523
573
  validateTopic(args.topic);
524
574
  validateContent(args.content);
525
- const result = await buildSignSubmit("COMMENT", args.topic, { type: "text", data: args.content }, {
575
+ const result = await buildSignSubmit("COMMENT", args.topic, { type: "text", data: args.content }, id, {
526
576
  refs: { parent_aeid: args.parent_aeid },
527
577
  });
528
578
  return ok(result);
@@ -533,7 +583,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
533
583
  const reaction = name === "upvote" ? "like" : "dislike";
534
584
  const message = `REACT:${args.aeid}:${reaction}`;
535
585
  const messageBytes = new TextEncoder().encode(message);
536
- const signature = ed.sign(messageBytes, identity.secretKey);
586
+ const signature = ed.sign(messageBytes, id.secretKey);
537
587
  const signatureBase58 = bs58.encode(signature);
538
588
 
539
589
  const resp = await fetch(`${proxyUrl}/react`, {
@@ -542,7 +592,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
542
592
  body: JSON.stringify({
543
593
  aeid: args.aeid,
544
594
  reaction,
545
- reactor_pubkey: identity.publicKeyBase58,
595
+ reactor_pubkey: id.publicKeyBase58,
546
596
  signature: signatureBase58,
547
597
  }),
548
598
  });
@@ -556,9 +606,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
556
606
  const postUrl = args.post_url;
557
607
 
558
608
  // Sign the verification message
559
- const message = `VERIFY_SOCIAL:${identity.publicKeyBase58}:${postUrl}`;
609
+ const message = `VERIFY_SOCIAL:${id.publicKeyBase58}:${postUrl}`;
560
610
  const messageBytes = new TextEncoder().encode(message);
561
- const signature = ed.sign(messageBytes, identity.secretKey);
611
+ const signature = ed.sign(messageBytes, id.secretKey);
562
612
  const signatureBase58 = bs58.encode(signature);
563
613
 
564
614
  // Get indexer URL (derive from proxy URL)
@@ -568,7 +618,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
568
618
  method: "POST",
569
619
  headers: { "Content-Type": "application/json" },
570
620
  body: JSON.stringify({
571
- pubkey: identity.publicKeyBase58,
621
+ pubkey: id.publicKeyBase58,
572
622
  post_url: postUrl,
573
623
  signature: signatureBase58,
574
624
  }),
@@ -616,9 +666,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
616
666
  const code = args.code;
617
667
 
618
668
  // Sign the verification message
619
- const message = `VERIFY_PHONE:${identity.publicKeyBase58}:${phoneNumber}`;
669
+ const message = `VERIFY_PHONE:${id.publicKeyBase58}:${phoneNumber}`;
620
670
  const messageBytes = new TextEncoder().encode(message);
621
- const signature = ed.sign(messageBytes, identity.secretKey);
671
+ const signature = ed.sign(messageBytes, id.secretKey);
622
672
  const signatureBase58 = bs58.encode(signature);
623
673
 
624
674
  // Get indexer URL (derive from proxy URL)
@@ -630,7 +680,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
630
680
  body: JSON.stringify({
631
681
  phone_number: phoneNumber,
632
682
  code: code,
633
- pubkey: identity.publicKeyBase58,
683
+ pubkey: id.publicKeyBase58,
634
684
  signature: signatureBase58,
635
685
  }),
636
686
  });
@@ -654,9 +704,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
654
704
 
655
705
  // Sign the message: SET_DISPLAY_NAME:{pubkey}:{display_name}
656
706
  const displayNameStr = displayName || "";
657
- const message = `SET_DISPLAY_NAME:${identity.publicKeyBase58}:${displayNameStr}`;
707
+ const message = `SET_DISPLAY_NAME:${id.publicKeyBase58}:${displayNameStr}`;
658
708
  const messageBytes = new TextEncoder().encode(message);
659
- const signature = ed.sign(messageBytes, identity.secretKey);
709
+ const signature = ed.sign(messageBytes, id.secretKey);
660
710
  const signatureBase58 = bs58.encode(signature);
661
711
 
662
712
  // Get indexer URL (derive from proxy URL)
@@ -666,7 +716,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
666
716
  method: "POST",
667
717
  headers: { "Content-Type": "application/json" },
668
718
  body: JSON.stringify({
669
- pubkey: identity.publicKeyBase58,
719
+ pubkey: id.publicKeyBase58,
670
720
  display_name: displayName,
671
721
  signature: signatureBase58,
672
722
  }),
@@ -676,7 +726,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
676
726
 
677
727
  return ok({
678
728
  success: true,
679
- pubkey: identity.publicKeyBase58,
729
+ pubkey: id.publicKeyBase58,
680
730
  display_name: result.display_name,
681
731
  });
682
732
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bb-signer",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Minimal local signer for BB - signs events for the agent collaboration network",
5
5
  "type": "module",
6
6
  "main": "index.js",