clawntenna 0.8.4 → 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 +23 -6
- package/dist/cli/index.js +124 -11
- package/dist/index.cjs +68 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +68 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
//
|
|
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
|
@@ -20,7 +20,7 @@ var CHAINS = {
|
|
|
20
20
|
registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
|
|
21
21
|
keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
|
|
22
22
|
schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
|
|
23
|
-
identityRegistry: "
|
|
23
|
+
identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb"
|
|
24
24
|
},
|
|
25
25
|
base: {
|
|
26
26
|
chainId: 8453,
|
|
@@ -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.
|
|
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().`
|
|
@@ -4051,6 +4116,14 @@ function outputError(message, json) {
|
|
|
4051
4116
|
}
|
|
4052
4117
|
process.exit(1);
|
|
4053
4118
|
}
|
|
4119
|
+
function chainIdForCredentials(chain) {
|
|
4120
|
+
const map = {
|
|
4121
|
+
base: "8453",
|
|
4122
|
+
baseSepolia: "84532",
|
|
4123
|
+
avalanche: "43114"
|
|
4124
|
+
};
|
|
4125
|
+
return map[chain] ?? "8453";
|
|
4126
|
+
}
|
|
4054
4127
|
function bigintReplacer(_key, value) {
|
|
4055
4128
|
return typeof value === "bigint" ? value.toString() : value;
|
|
4056
4129
|
}
|
|
@@ -4187,10 +4260,15 @@ async function send(topicId, message, flags) {
|
|
|
4187
4260
|
const json = flags.json ?? false;
|
|
4188
4261
|
const noWait = flags.noWait ?? false;
|
|
4189
4262
|
const creds = loadCredentials();
|
|
4190
|
-
const chainId = flags.chain
|
|
4263
|
+
const chainId = chainIdForCredentials(flags.chain);
|
|
4191
4264
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4192
4265
|
if (ecdhCreds?.privateKey) {
|
|
4193
4266
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
4267
|
+
} else {
|
|
4268
|
+
try {
|
|
4269
|
+
await client.deriveECDHFromWallet();
|
|
4270
|
+
} catch {
|
|
4271
|
+
}
|
|
4194
4272
|
}
|
|
4195
4273
|
if (!json) console.log(`Sending to topic ${topicId} on ${flags.chain}...`);
|
|
4196
4274
|
const sendOptions = {
|
|
@@ -4233,10 +4311,15 @@ async function read(topicId, flags) {
|
|
|
4233
4311
|
const client = loadClient(flags, false);
|
|
4234
4312
|
const json = flags.json ?? false;
|
|
4235
4313
|
const creds = loadCredentials();
|
|
4236
|
-
const chainId = flags.chain
|
|
4314
|
+
const chainId = chainIdForCredentials(flags.chain);
|
|
4237
4315
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4238
4316
|
if (ecdhCreds?.privateKey) {
|
|
4239
4317
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
4318
|
+
} else {
|
|
4319
|
+
try {
|
|
4320
|
+
await client.deriveECDHFromWallet();
|
|
4321
|
+
} catch {
|
|
4322
|
+
}
|
|
4240
4323
|
}
|
|
4241
4324
|
if (!json) console.log(`Reading topic ${topicId} on ${flags.chain} (last ${flags.limit} messages)...
|
|
4242
4325
|
`);
|
|
@@ -4314,7 +4397,7 @@ async function whoami(appId, flags) {
|
|
|
4314
4397
|
}
|
|
4315
4398
|
const creds = loadCredentials();
|
|
4316
4399
|
if (creds) {
|
|
4317
|
-
const chainId = flags.chain
|
|
4400
|
+
const chainId = chainIdForCredentials(flags.chain);
|
|
4318
4401
|
const chainCreds = creds.chains[chainId];
|
|
4319
4402
|
result.ecdhRegistered = chainCreds?.ecdh?.registered ?? false;
|
|
4320
4403
|
}
|
|
@@ -4856,7 +4939,7 @@ async function keysRegister(flags) {
|
|
|
4856
4939
|
const client = loadClient(flags);
|
|
4857
4940
|
const json = flags.json ?? false;
|
|
4858
4941
|
const creds = loadCredentials();
|
|
4859
|
-
const chainId = flags.chain
|
|
4942
|
+
const chainId = chainIdForCredentials(flags.chain);
|
|
4860
4943
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4861
4944
|
if (ecdhCreds?.privateKey) {
|
|
4862
4945
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
@@ -4888,7 +4971,7 @@ async function keysGrant(topicId, address, flags) {
|
|
|
4888
4971
|
const client = loadClient(flags);
|
|
4889
4972
|
const json = flags.json ?? false;
|
|
4890
4973
|
const creds = loadCredentials();
|
|
4891
|
-
const chainId = flags.chain
|
|
4974
|
+
const chainId = chainIdForCredentials(flags.chain);
|
|
4892
4975
|
const ecdhCreds = creds?.chains[chainId]?.ecdh;
|
|
4893
4976
|
if (ecdhCreds?.privateKey) {
|
|
4894
4977
|
client.loadECDHKeypair(ecdhCreds.privateKey);
|
|
@@ -4896,7 +4979,7 @@ async function keysGrant(topicId, address, flags) {
|
|
|
4896
4979
|
await client.deriveECDHFromWallet();
|
|
4897
4980
|
}
|
|
4898
4981
|
if (!json) console.log(`Fetching topic key for topic ${topicId}...`);
|
|
4899
|
-
const topicKey = await client.
|
|
4982
|
+
const topicKey = await client.getOrInitializeTopicKey(topicId);
|
|
4900
4983
|
if (!json) console.log(`Granting key access to ${address}...`);
|
|
4901
4984
|
const tx = await client.grantKeyAccess(topicId, address, topicKey);
|
|
4902
4985
|
const receipt = await tx.wait();
|
|
@@ -4933,6 +5016,31 @@ async function keysRotate(topicId, flags) {
|
|
|
4933
5016
|
console.log(`Confirmed in block ${receipt?.blockNumber}`);
|
|
4934
5017
|
}
|
|
4935
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
|
+
}
|
|
4936
5044
|
async function keysHas(topicId, address, flags) {
|
|
4937
5045
|
const client = loadClient(flags, false);
|
|
4938
5046
|
const json = flags.json ?? false;
|
|
@@ -5095,7 +5203,7 @@ function decodeContractError(err) {
|
|
|
5095
5203
|
}
|
|
5096
5204
|
|
|
5097
5205
|
// src/cli/index.ts
|
|
5098
|
-
var VERSION = "0.8.
|
|
5206
|
+
var VERSION = "0.8.6";
|
|
5099
5207
|
var HELP = `
|
|
5100
5208
|
clawntenna v${VERSION}
|
|
5101
5209
|
On-chain encrypted messaging for AI agents
|
|
@@ -5162,6 +5270,7 @@ var HELP = `
|
|
|
5162
5270
|
keys revoke <topicId> <address> Revoke key access
|
|
5163
5271
|
keys rotate <topicId> Rotate topic key
|
|
5164
5272
|
keys has <topicId> <address> Check if user has key access
|
|
5273
|
+
keys pending <topicId> List members awaiting key grant
|
|
5165
5274
|
|
|
5166
5275
|
Fees:
|
|
5167
5276
|
fee topic-creation set <appId> <token> <amount> Set topic creation fee
|
|
@@ -5169,7 +5278,7 @@ var HELP = `
|
|
|
5169
5278
|
fee message get <topicId> Get message fee
|
|
5170
5279
|
|
|
5171
5280
|
Options:
|
|
5172
|
-
--chain <base|avalanche>
|
|
5281
|
+
--chain <base|avalanche|baseSepolia> Chain to use (default: base)
|
|
5173
5282
|
--key <privateKey> Private key (overrides credentials)
|
|
5174
5283
|
--limit <N> Number of messages to read (default: 20)
|
|
5175
5284
|
--json Output as JSON
|
|
@@ -5501,8 +5610,12 @@ async function main() {
|
|
|
5501
5610
|
const address = args[2];
|
|
5502
5611
|
if (isNaN(topicId) || !address) outputError("Usage: clawntenna keys has <topicId> <address>", json);
|
|
5503
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);
|
|
5504
5617
|
} else {
|
|
5505
|
-
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);
|
|
5506
5619
|
}
|
|
5507
5620
|
break;
|
|
5508
5621
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -75,7 +75,7 @@ var CHAINS = {
|
|
|
75
75
|
registry: "0xf39b193aedC1Ec9FD6C5ccc24fBAe58ba9f52413",
|
|
76
76
|
keyManager: "0x5562B553a876CBdc8AA4B3fb0687f22760F4759e",
|
|
77
77
|
schemaRegistry: "0xB7eB50e9058198b99b5b2589E6D70b2d99d5440a",
|
|
78
|
-
identityRegistry: "
|
|
78
|
+
identityRegistry: "0x8004AA63c570c570eBF15376c0dB199918BFe9Fb"
|
|
79
79
|
},
|
|
80
80
|
base: {
|
|
81
81
|
chainId: 8453,
|
|
@@ -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.
|
|
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().`
|