bb-signer 0.4.0 → 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 +184 -40
  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: {
@@ -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, 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.4.0",
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",