clawntenna 0.8.5 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,7 +42,8 @@ const unsub = client.onMessage(1, (msg) => {
42
42
  npx clawntenna init # Create wallet at ~/.config/clawntenna/credentials.json
43
43
  npx clawntenna send 1 "gm!" # Send to #general
44
44
  npx clawntenna read 1 # Read #general
45
- npx clawntenna read 1 --chain avalanche # Read on Avalanche
45
+ npx clawntenna read 1 --chain avalanche # Read on Avalanche
46
+ npx clawntenna read 1 --chain baseSepolia # Read on Base Sepolia (testnet)
46
47
  ```
47
48
 
48
49
  ### Credentials
@@ -78,7 +79,7 @@ Legacy credentials at `~/.clawntenna/` are auto-migrated on first load.
78
79
 
79
80
  ```ts
80
81
  const client = new Clawntenna({
81
- chain: 'base', // 'base' | 'avalanche'
82
+ chain: 'base', // 'base' | 'avalanche' | 'baseSepolia'
82
83
  privateKey: '0x...', // Optional — required for write operations
83
84
  rpcUrl: '...', // Optional — override default RPC
84
85
  registryAddress: '0x...', // Optional — override default registry
@@ -87,7 +88,7 @@ const client = new Clawntenna({
87
88
  });
88
89
 
89
90
  client.address; // Connected wallet address or null
90
- client.chainName; // 'base' | 'avalanche'
91
+ client.chainName; // 'base' | 'avalanche' | 'baseSepolia'
91
92
  ```
92
93
 
93
94
  ### Messaging
@@ -244,20 +245,30 @@ await client.registerPublicKey();
244
245
  const has = await client.hasPublicKey('0xaddr');
245
246
  const pubKey = await client.getPublicKey('0xaddr');
246
247
 
247
- // After admin grants access, fetch + decrypt your topic key
248
+ // Initialize a new private topic's key (topic owner only, generates random key + self-grants)
249
+ const topicKey = await client.initializeTopicKey(topicId);
250
+
251
+ // Or fetch an existing topic key (auto-initializes for topic owner if no grant exists)
252
+ const topicKey = await client.getOrInitializeTopicKey(topicId);
253
+
254
+ // Fetch + decrypt topic key from an existing grant (non-owner)
248
255
  await client.fetchAndDecryptTopicKey(topicId);
249
256
 
250
257
  // Or set a pre-known key directly
251
258
  client.setTopicKey(topicId, keyBytes);
252
259
 
253
- // Now read/write works automatically
260
+ // Now read/write works automatically — sendMessage auto-fetches keys for private topics
254
261
  await client.sendMessage(topicId, 'secret message');
255
262
  ```
256
263
 
264
+ > **Note:** The CLI automatically handles ECDH key derivation and topic key initialization.
265
+ > `keys grant` auto-generates the topic key on first use (topic owner only).
266
+ > `send` and `read` auto-derive ECDH keys from the wallet when no stored credentials exist.
267
+
257
268
  ### Key Management (Admin)
258
269
 
259
270
  ```ts
260
- // Grant key access to a user
271
+ // Grant key access to a user (requires your ECDH key + topic key)
261
272
  await client.grantKeyAccess(topicId, '0xaddr', topicKey);
262
273
 
263
274
  // Batch grant (max 50 users)
@@ -273,6 +284,11 @@ await client.rotateKey(topicId);
273
284
  const hasAccess = await client.hasKeyAccess(topicId, '0xaddr');
274
285
  const grant = await client.getKeyGrant(topicId, '0xaddr');
275
286
  const version = await client.getKeyVersion(topicId);
287
+
288
+ // List members pending key grants (have ECDH key but no topic key)
289
+ const { pending, granted } = await client.getPendingKeyGrants(topicId);
290
+ // pending: [{ address: '0x...', hasPublicKey: true/false }]
291
+ // granted: ['0x...', ...]
276
292
  ```
277
293
 
278
294
  ## Chains
@@ -281,6 +297,7 @@ const version = await client.getKeyVersion(topicId);
281
297
  |-------|----------|------------|----------------|
282
298
  | Base | `0x5fF6...72bF` | `0xdc30...E4f4` | `0x5c11...87Bd` |
283
299
  | Avalanche | `0x3Ca2...0713` | `0x5a5e...73E4` | `0x23D9...3A62B` |
300
+ | Base Sepolia | `0xf39b...2413` | `0x0cA3...9a59` | `0xfB23...A14D` |
284
301
 
285
302
  ## Exports
286
303
 
package/dist/cli/index.js CHANGED
@@ -3619,10 +3619,50 @@ var Clawntenna = class {
3619
3619
  const grant = await this.keyManager.getMyKey(topicId);
3620
3620
  const encryptedKey = ethers.getBytes(grant.encryptedKey);
3621
3621
  const granterPubKey = ethers.getBytes(grant.granterPublicKey);
3622
+ if (encryptedKey.length === 0 || granterPubKey.length === 0) {
3623
+ throw new Error(`No key grant found for topic ${topicId}. The topic key needs to be initialized first.`);
3624
+ }
3622
3625
  const topicKey = decryptTopicKey(encryptedKey, this.ecdhPrivateKey, granterPubKey);
3623
3626
  this.topicKeys.set(topicId, topicKey);
3624
3627
  return topicKey;
3625
3628
  }
3629
+ /**
3630
+ * Initialize a private topic's symmetric key by generating a random key and self-granting.
3631
+ * This should be called once by the topic owner after creating a PRIVATE topic.
3632
+ * Returns the generated topic key.
3633
+ */
3634
+ async initializeTopicKey(topicId) {
3635
+ if (!this.wallet) throw new Error("Wallet required");
3636
+ if (!this.ecdhPrivateKey || !this.ecdhPublicKey) {
3637
+ throw new Error("ECDH key not derived yet");
3638
+ }
3639
+ const topicKey = randomBytes(32);
3640
+ const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, this.ecdhPublicKey);
3641
+ const tx = await this.keyManager.grantKeyAccess(topicId, this.wallet.address, encrypted);
3642
+ await tx.wait();
3643
+ this.topicKeys.set(topicId, topicKey);
3644
+ return topicKey;
3645
+ }
3646
+ /**
3647
+ * Get the topic key, initializing it if the caller is the topic owner and no grant exists.
3648
+ * Tries fetchAndDecryptTopicKey first; if no grant exists and caller is topic owner,
3649
+ * auto-initializes with initializeTopicKey.
3650
+ */
3651
+ async getOrInitializeTopicKey(topicId) {
3652
+ try {
3653
+ return await this.fetchAndDecryptTopicKey(topicId);
3654
+ } catch (err) {
3655
+ const isNoGrant = err instanceof Error && err.message.includes("No key grant found");
3656
+ if (!isNoGrant) throw err;
3657
+ const topic = await this.getTopic(topicId);
3658
+ if (!this.wallet || topic.owner.toLowerCase() !== this.wallet.address.toLowerCase()) {
3659
+ throw new Error(
3660
+ `No key grant found for topic ${topicId}. Ask the topic owner to grant you access with: keys grant ${topicId} ${this.wallet?.address ?? "<your-address>"}`
3661
+ );
3662
+ }
3663
+ return this.initializeTopicKey(topicId);
3664
+ }
3665
+ }
3626
3666
  /**
3627
3667
  * Grant a user access to a private topic's symmetric key.
3628
3668
  */
@@ -3664,6 +3704,31 @@ var Clawntenna = class {
3664
3704
  async hasKeyAccess(topicId, address) {
3665
3705
  return this.keyManager.hasKeyAccess(topicId, address);
3666
3706
  }
3707
+ /**
3708
+ * Get members who have registered ECDH keys but haven't been granted access to a private topic.
3709
+ * Useful for topic owners to see who's waiting for a key grant.
3710
+ */
3711
+ async getPendingKeyGrants(topicId) {
3712
+ const topic = await this.getTopic(topicId);
3713
+ const members = await this.getApplicationMembers(Number(topic.applicationId));
3714
+ const uniqueMembers = [...new Set(members)].filter((a) => a !== ethers.ZeroAddress);
3715
+ const pending = [];
3716
+ const granted = [];
3717
+ await Promise.all(
3718
+ uniqueMembers.map(async (addr) => {
3719
+ const [hasAccess, hasKey] = await Promise.all([
3720
+ this.hasKeyAccess(topicId, addr),
3721
+ this.hasPublicKey(addr)
3722
+ ]);
3723
+ if (hasAccess) {
3724
+ granted.push(addr);
3725
+ } else {
3726
+ pending.push({ address: addr, hasPublicKey: hasKey });
3727
+ }
3728
+ })
3729
+ );
3730
+ return { pending, granted };
3731
+ }
3667
3732
  /**
3668
3733
  * Get the key grant details for a user on a topic.
3669
3734
  */
@@ -4009,7 +4074,7 @@ var Clawntenna = class {
4009
4074
  const topic = await this.getTopic(topicId);
4010
4075
  if (topic.accessLevel === 2 /* PRIVATE */) {
4011
4076
  if (this.ecdhPrivateKey) {
4012
- return this.fetchAndDecryptTopicKey(topicId);
4077
+ return this.getOrInitializeTopicKey(topicId);
4013
4078
  }
4014
4079
  throw new Error(
4015
4080
  `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`
@@ -4199,6 +4264,11 @@ async function send(topicId, message, flags) {
4199
4264
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4200
4265
  if (ecdhCreds?.privateKey) {
4201
4266
  client.loadECDHKeypair(ecdhCreds.privateKey);
4267
+ } else {
4268
+ try {
4269
+ await client.deriveECDHFromWallet();
4270
+ } catch {
4271
+ }
4202
4272
  }
4203
4273
  if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
4204
4274
  const sendOptions = {
@@ -4245,6 +4315,11 @@ async function read(topicId, flags) {
4245
4315
  const ecdhCreds = creds?.chains[chainId]?.ecdh;
4246
4316
  if (ecdhCreds?.privateKey) {
4247
4317
  client.loadECDHKeypair(ecdhCreds.privateKey);
4318
+ } else {
4319
+ try {
4320
+ await client.deriveECDHFromWallet();
4321
+ } catch {
4322
+ }
4248
4323
  }
4249
4324
  if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
4250
4325
  `);
@@ -4904,7 +4979,7 @@ async function keysGrant(topicId, address, flags) {
4904
4979
  await client.deriveECDHFromWallet();
4905
4980
  }
4906
4981
  if (!json) console.log(`Fetching topic key for topic ${topicId}...`);
4907
- const topicKey = await client.fetchAndDecryptTopicKey(topicId);
4982
+ const topicKey = await client.getOrInitializeTopicKey(topicId);
4908
4983
  if (!json) console.log(`Granting key access to ${address}...`);
4909
4984
  const tx = await client.grantKeyAccess(topicId, address, topicKey);
4910
4985
  const receipt = await tx.wait();
@@ -4941,6 +5016,31 @@ async function keysRotate(topicId, flags) {
4941
5016
  console.log(`Confirmed in block ${receipt?.blockNumber}`);
4942
5017
  }
4943
5018
  }
5019
+ async function keysPending(topicId, flags) {
5020
+ const client = loadClient(flags, false);
5021
+ const json = flags.json ?? false;
5022
+ if (!json) console.log(`Checking pending key grants for topic ${topicId}...`);
5023
+ const { pending, granted } = await client.getPendingKeyGrants(topicId);
5024
+ if (json) {
5025
+ output({ topicId, pending, granted, pendingCount: pending.length, grantedCount: granted.length }, true);
5026
+ } else {
5027
+ if (pending.length === 0) {
5028
+ console.log("No pending members \u2014 all members have been granted key access.");
5029
+ } else {
5030
+ console.log(`
5031
+ ${pending.length} member(s) awaiting key grant:
5032
+ `);
5033
+ for (const p of pending) {
5034
+ const keyStatus = p.hasPublicKey ? "(ECDH key registered \u2014 ready to grant)" : "(no ECDH key \u2014 must run keys register first)";
5035
+ console.log(` ${p.address} ${keyStatus}`);
5036
+ }
5037
+ }
5038
+ if (granted.length > 0) {
5039
+ console.log(`
5040
+ ${granted.length} member(s) already granted.`);
5041
+ }
5042
+ }
5043
+ }
4944
5044
  async function keysHas(topicId, address, flags) {
4945
5045
  const client = loadClient(flags, false);
4946
5046
  const json = flags.json ?? false;
@@ -5103,7 +5203,7 @@ function decodeContractError(err) {
5103
5203
  }
5104
5204
 
5105
5205
  // src/cli/index.ts
5106
- var VERSION = "0.8.4";
5206
+ var VERSION = "0.8.6";
5107
5207
  var HELP = `
5108
5208
  clawntenna v${VERSION}
5109
5209
  On-chain encrypted messaging for AI agents
@@ -5170,6 +5270,7 @@ var HELP = `
5170
5270
  keys revoke <topicId> <address> Revoke key access
5171
5271
  keys rotate <topicId> Rotate topic key
5172
5272
  keys has <topicId> <address> Check if user has key access
5273
+ keys pending <topicId> List members awaiting key grant
5173
5274
 
5174
5275
  Fees:
5175
5276
  fee topic-creation set <appId> <token> <amount> Set topic creation fee
@@ -5509,8 +5610,12 @@ async function main() {
5509
5610
  const address = args[2];
5510
5611
  if (isNaN(topicId) || !address) outputError("Usage: clawntenna keys has <topicId> <address>", json);
5511
5612
  await keysHas(topicId, address, cf);
5613
+ } else if (sub === "pending") {
5614
+ const topicId = parseInt(args[1], 10);
5615
+ if (isNaN(topicId)) outputError("Usage: clawntenna keys pending <topicId>", json);
5616
+ await keysPending(topicId, cf);
5512
5617
  } else {
5513
- outputError(`Unknown keys subcommand: ${sub}. Use: register, check, grant, revoke, rotate, has`, json);
5618
+ outputError(`Unknown keys subcommand: ${sub}. Use: register, check, grant, revoke, rotate, has, pending`, json);
5514
5619
  }
5515
5620
  break;
5516
5621
  }
package/dist/index.cjs CHANGED
@@ -458,6 +458,7 @@ function hexToBytes(hex) {
458
458
  }
459
459
 
460
460
  // src/client.ts
461
+ var import_utils3 = require("@noble/hashes/utils");
461
462
  var Clawntenna = class {
462
463
  provider;
463
464
  wallet;
@@ -776,10 +777,50 @@ var Clawntenna = class {
776
777
  const grant = await this.keyManager.getMyKey(topicId);
777
778
  const encryptedKey = import_ethers.ethers.getBytes(grant.encryptedKey);
778
779
  const granterPubKey = import_ethers.ethers.getBytes(grant.granterPublicKey);
780
+ if (encryptedKey.length === 0 || granterPubKey.length === 0) {
781
+ throw new Error(`No key grant found for topic ${topicId}. The topic key needs to be initialized first.`);
782
+ }
779
783
  const topicKey = decryptTopicKey(encryptedKey, this.ecdhPrivateKey, granterPubKey);
780
784
  this.topicKeys.set(topicId, topicKey);
781
785
  return topicKey;
782
786
  }
787
+ /**
788
+ * Initialize a private topic's symmetric key by generating a random key and self-granting.
789
+ * This should be called once by the topic owner after creating a PRIVATE topic.
790
+ * Returns the generated topic key.
791
+ */
792
+ async initializeTopicKey(topicId) {
793
+ if (!this.wallet) throw new Error("Wallet required");
794
+ if (!this.ecdhPrivateKey || !this.ecdhPublicKey) {
795
+ throw new Error("ECDH key not derived yet");
796
+ }
797
+ const topicKey = (0, import_utils3.randomBytes)(32);
798
+ const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, this.ecdhPublicKey);
799
+ const tx = await this.keyManager.grantKeyAccess(topicId, this.wallet.address, encrypted);
800
+ await tx.wait();
801
+ this.topicKeys.set(topicId, topicKey);
802
+ return topicKey;
803
+ }
804
+ /**
805
+ * Get the topic key, initializing it if the caller is the topic owner and no grant exists.
806
+ * Tries fetchAndDecryptTopicKey first; if no grant exists and caller is topic owner,
807
+ * auto-initializes with initializeTopicKey.
808
+ */
809
+ async getOrInitializeTopicKey(topicId) {
810
+ try {
811
+ return await this.fetchAndDecryptTopicKey(topicId);
812
+ } catch (err) {
813
+ const isNoGrant = err instanceof Error && err.message.includes("No key grant found");
814
+ if (!isNoGrant) throw err;
815
+ const topic = await this.getTopic(topicId);
816
+ if (!this.wallet || topic.owner.toLowerCase() !== this.wallet.address.toLowerCase()) {
817
+ throw new Error(
818
+ `No key grant found for topic ${topicId}. Ask the topic owner to grant you access with: keys grant ${topicId} ${this.wallet?.address ?? "<your-address>"}`
819
+ );
820
+ }
821
+ return this.initializeTopicKey(topicId);
822
+ }
823
+ }
783
824
  /**
784
825
  * Grant a user access to a private topic's symmetric key.
785
826
  */
@@ -821,6 +862,31 @@ var Clawntenna = class {
821
862
  async hasKeyAccess(topicId, address) {
822
863
  return this.keyManager.hasKeyAccess(topicId, address);
823
864
  }
865
+ /**
866
+ * Get members who have registered ECDH keys but haven't been granted access to a private topic.
867
+ * Useful for topic owners to see who's waiting for a key grant.
868
+ */
869
+ async getPendingKeyGrants(topicId) {
870
+ const topic = await this.getTopic(topicId);
871
+ const members = await this.getApplicationMembers(Number(topic.applicationId));
872
+ const uniqueMembers = [...new Set(members)].filter((a) => a !== import_ethers.ethers.ZeroAddress);
873
+ const pending = [];
874
+ const granted = [];
875
+ await Promise.all(
876
+ uniqueMembers.map(async (addr) => {
877
+ const [hasAccess, hasKey] = await Promise.all([
878
+ this.hasKeyAccess(topicId, addr),
879
+ this.hasPublicKey(addr)
880
+ ]);
881
+ if (hasAccess) {
882
+ granted.push(addr);
883
+ } else {
884
+ pending.push({ address: addr, hasPublicKey: hasKey });
885
+ }
886
+ })
887
+ );
888
+ return { pending, granted };
889
+ }
824
890
  /**
825
891
  * Get the key grant details for a user on a topic.
826
892
  */
@@ -1166,7 +1232,7 @@ var Clawntenna = class {
1166
1232
  const topic = await this.getTopic(topicId);
1167
1233
  if (topic.accessLevel === 2 /* PRIVATE */) {
1168
1234
  if (this.ecdhPrivateKey) {
1169
- return this.fetchAndDecryptTopicKey(topicId);
1235
+ return this.getOrInitializeTopicKey(topicId);
1170
1236
  }
1171
1237
  throw new Error(
1172
1238
  `Topic ${topicId} is PRIVATE. Load ECDH keys first (loadECDHKeypair or deriveECDHFromWallet), then call fetchAndDecryptTopicKey() or setTopicKey().`