clawntenna 0.1.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.
- package/README.md +74 -0
- package/dist/cli/index.js +3695 -0
- package/dist/index.cjs +697 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +301 -0
- package/dist/index.d.ts +301 -0
- package/dist/index.js +635 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ACCESS_PRIVATE: () => ACCESS_PRIVATE,
|
|
24
|
+
ACCESS_PUBLIC: () => ACCESS_PUBLIC,
|
|
25
|
+
ACCESS_PUBLIC_LIMITED: () => ACCESS_PUBLIC_LIMITED,
|
|
26
|
+
AccessLevel: () => AccessLevel,
|
|
27
|
+
CHAINS: () => CHAINS,
|
|
28
|
+
CHAIN_IDS: () => CHAIN_IDS,
|
|
29
|
+
Clawntenna: () => Clawntenna,
|
|
30
|
+
KEY_MANAGER_ABI: () => KEY_MANAGER_ABI,
|
|
31
|
+
PERMISSION_ADMIN: () => PERMISSION_ADMIN,
|
|
32
|
+
PERMISSION_NONE: () => PERMISSION_NONE,
|
|
33
|
+
PERMISSION_READ: () => PERMISSION_READ,
|
|
34
|
+
PERMISSION_READ_WRITE: () => PERMISSION_READ_WRITE,
|
|
35
|
+
PERMISSION_WRITE: () => PERMISSION_WRITE,
|
|
36
|
+
Permission: () => Permission,
|
|
37
|
+
REGISTRY_ABI: () => REGISTRY_ABI,
|
|
38
|
+
ROLE_ADMIN: () => ROLE_ADMIN,
|
|
39
|
+
ROLE_MEMBER: () => ROLE_MEMBER,
|
|
40
|
+
ROLE_OWNER_DELEGATE: () => ROLE_OWNER_DELEGATE,
|
|
41
|
+
ROLE_SUPPORT_MANAGER: () => ROLE_SUPPORT_MANAGER,
|
|
42
|
+
ROLE_TOPIC_MANAGER: () => ROLE_TOPIC_MANAGER,
|
|
43
|
+
Role: () => Role,
|
|
44
|
+
bytesToHex: () => bytesToHex,
|
|
45
|
+
computeSharedSecret: () => computeSharedSecret,
|
|
46
|
+
decrypt: () => decrypt,
|
|
47
|
+
decryptMessage: () => decryptMessage,
|
|
48
|
+
decryptTopicKey: () => decryptTopicKey,
|
|
49
|
+
deriveAESKeyFromSecret: () => deriveAESKeyFromSecret,
|
|
50
|
+
deriveKeyFromPassphrase: () => deriveKeyFromPassphrase,
|
|
51
|
+
deriveKeypairFromSignature: () => deriveKeypairFromSignature,
|
|
52
|
+
derivePublicTopicKey: () => derivePublicTopicKey,
|
|
53
|
+
encrypt: () => encrypt,
|
|
54
|
+
encryptMessage: () => encryptMessage,
|
|
55
|
+
encryptTopicKeyForUser: () => encryptTopicKeyForUser,
|
|
56
|
+
getChain: () => getChain,
|
|
57
|
+
hexToBytes: () => hexToBytes,
|
|
58
|
+
keypairFromPrivateKey: () => keypairFromPrivateKey
|
|
59
|
+
});
|
|
60
|
+
module.exports = __toCommonJS(index_exports);
|
|
61
|
+
|
|
62
|
+
// src/client.ts
|
|
63
|
+
var import_ethers = require("ethers");
|
|
64
|
+
|
|
65
|
+
// src/chains.ts
|
|
66
|
+
var CHAINS = {
|
|
67
|
+
base: {
|
|
68
|
+
chainId: 8453,
|
|
69
|
+
name: "Base",
|
|
70
|
+
shortName: "Base",
|
|
71
|
+
rpc: "https://base.publicnode.com",
|
|
72
|
+
explorer: "https://basescan.org",
|
|
73
|
+
registry: "0x5fF6BF04F1B5A78ae884D977a3C80A0D8E2072bF",
|
|
74
|
+
keyManager: "0xdc302ff43a34F6aEa19426D60C9D150e0661E4f4"
|
|
75
|
+
},
|
|
76
|
+
avalanche: {
|
|
77
|
+
chainId: 43114,
|
|
78
|
+
name: "Avalanche C-Chain",
|
|
79
|
+
shortName: "Avalanche",
|
|
80
|
+
rpc: "https://api.avax.network/ext/bc/C/rpc",
|
|
81
|
+
explorer: "https://snowtrace.io",
|
|
82
|
+
registry: "0x3Ca2FF0bD1b3633513299EB5d3e2d63e058b0713",
|
|
83
|
+
keyManager: "0x5a5ea9D408FBA984fFf6e243Dcc71ff6E00C73E4"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var CHAIN_IDS = {
|
|
87
|
+
8453: "base",
|
|
88
|
+
43114: "avalanche"
|
|
89
|
+
};
|
|
90
|
+
function getChain(nameOrId) {
|
|
91
|
+
if (typeof nameOrId === "number") {
|
|
92
|
+
const name = CHAIN_IDS[nameOrId];
|
|
93
|
+
if (!name) throw new Error(`Unsupported chain ID: ${nameOrId}`);
|
|
94
|
+
return CHAINS[name];
|
|
95
|
+
}
|
|
96
|
+
const chain = CHAINS[nameOrId];
|
|
97
|
+
if (!chain) throw new Error(`Unsupported chain: ${nameOrId}`);
|
|
98
|
+
return chain;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/contracts.ts
|
|
102
|
+
var REGISTRY_ABI = [
|
|
103
|
+
// ===== READ FUNCTIONS =====
|
|
104
|
+
// Applications
|
|
105
|
+
"function applications(uint256) view returns (uint256 id, string name, string description, string frontendUrl, address owner, uint64 createdAt, uint32 memberCount, uint32 topicCount, bool active, bool allowPublicTopicCreation)",
|
|
106
|
+
"function getApplication(uint256 appId) view returns (tuple(uint256 id, string name, string description, string frontendUrl, address owner, uint64 createdAt, uint32 memberCount, uint32 topicCount, bool active, bool allowPublicTopicCreation, address topicCreationFeeToken, uint256 topicCreationFeeAmount))",
|
|
107
|
+
"function applicationCount() view returns (uint256)",
|
|
108
|
+
"function applicationNames(string) view returns (uint256)",
|
|
109
|
+
// Topics
|
|
110
|
+
"function topics(uint256) view returns (uint256 id, uint256 applicationId, string name, string description, address owner, address creator, uint64 createdAt, uint64 lastMessageAt, uint256 messageCount, uint8 accessLevel, bool active)",
|
|
111
|
+
"function getTopic(uint256 topicId) view returns (tuple(uint256 id, uint256 applicationId, string name, string description, address owner, address creator, uint64 createdAt, uint64 lastMessageAt, uint256 messageCount, uint8 accessLevel, bool active))",
|
|
112
|
+
"function topicCount() view returns (uint256)",
|
|
113
|
+
"function getApplicationTopics(uint256 appId) view returns (uint256[])",
|
|
114
|
+
// Members
|
|
115
|
+
"function members(uint256 appId, address user) view returns (address account, string nickname, uint8 roles, uint64 joinedAt)",
|
|
116
|
+
"function getMember(uint256 appId, address account) view returns (tuple(address account, string nickname, uint8 roles, uint64 joinedAt))",
|
|
117
|
+
"function isMember(uint256 appId, address account) view returns (bool)",
|
|
118
|
+
"function getApplicationMembers(uint256 appId) view returns (address[])",
|
|
119
|
+
// Permissions
|
|
120
|
+
"function canReadTopic(uint256 topicId, address user) view returns (bool)",
|
|
121
|
+
"function canWriteToTopic(uint256 topicId, address user) view returns (bool)",
|
|
122
|
+
"function getTopicPermission(uint256 topicId, address user) view returns (uint8)",
|
|
123
|
+
"function topicPermissions(uint256, address) view returns (uint8)",
|
|
124
|
+
// Nicknames
|
|
125
|
+
"function getNickname(uint256 appId, address user) view returns (string)",
|
|
126
|
+
"function hasNickname(uint256 appId, address user) view returns (bool)",
|
|
127
|
+
"function canChangeNickname(uint256 appId, address user) view returns (bool canChange, uint256 timeRemaining)",
|
|
128
|
+
"function appNicknameCooldown(uint256 appId) view returns (uint256)",
|
|
129
|
+
// Fees
|
|
130
|
+
"function getTopicMessageFee(uint256 topicId) view returns (address token, uint256 amount)",
|
|
131
|
+
// ===== WRITE FUNCTIONS =====
|
|
132
|
+
// Applications
|
|
133
|
+
"function createApplication(string name, string description, string frontendUrl, bool allowPublicTopicCreation) returns (uint256)",
|
|
134
|
+
"function updateApplicationFrontendUrl(uint256 appId, string frontendUrl)",
|
|
135
|
+
// Topics
|
|
136
|
+
"function createTopic(uint256 appId, string name, string description, uint8 accessLevel) returns (uint256)",
|
|
137
|
+
"function setTopicPermission(uint256 topicId, address user, uint8 permission)",
|
|
138
|
+
// Members
|
|
139
|
+
"function addMember(uint256 appId, address member, string nickname, uint8 roles)",
|
|
140
|
+
"function removeMember(uint256 appId, address member)",
|
|
141
|
+
"function updateMemberRoles(uint256 appId, address member, uint8 roles)",
|
|
142
|
+
"function updateMemberNickname(uint256 appId, string nickname)",
|
|
143
|
+
// Nicknames (V3)
|
|
144
|
+
"function setNickname(uint256 appId, string nickname)",
|
|
145
|
+
"function clearNickname(uint256 appId)",
|
|
146
|
+
"function setNicknameCooldown(uint256 appId, uint256 cooldownSeconds)",
|
|
147
|
+
// Messaging
|
|
148
|
+
"function sendMessage(uint256 topicId, bytes payload)",
|
|
149
|
+
// Fees
|
|
150
|
+
"function setTopicCreationFee(uint256 appId, address feeTokenAddr, uint256 feeAmount)",
|
|
151
|
+
"function setTopicMessageFee(uint256 topicId, address feeTokenAddr, uint256 feeAmount)",
|
|
152
|
+
// ===== EVENTS =====
|
|
153
|
+
"event ApplicationCreated(uint256 indexed applicationId, string name, address indexed owner)",
|
|
154
|
+
"event TopicCreated(uint256 indexed topicId, uint256 indexed applicationId, string name, address indexed creator, uint8 accessLevel)",
|
|
155
|
+
"event MemberAdded(uint256 indexed applicationId, address indexed member, string nickname, uint8 roles)",
|
|
156
|
+
"event MemberRemoved(uint256 indexed applicationId, address indexed member)",
|
|
157
|
+
"event MemberRolesUpdated(uint256 indexed applicationId, address indexed member, uint8 roles)",
|
|
158
|
+
"event NicknameUpdated(uint256 indexed applicationId, address indexed member, string nickname)",
|
|
159
|
+
"event UserNicknameSet(uint256 indexed applicationId, address indexed user, string nickname)",
|
|
160
|
+
"event TopicPermissionSet(uint256 indexed topicId, address indexed user, uint8 permission)",
|
|
161
|
+
"event MessageSent(uint256 indexed topicId, address indexed sender, bytes payload, uint256 timestamp)",
|
|
162
|
+
"event TopicMessageFeeUpdated(uint256 indexed topicId, address token, uint256 amount)"
|
|
163
|
+
];
|
|
164
|
+
var KEY_MANAGER_ABI = [
|
|
165
|
+
// ===== READ FUNCTIONS =====
|
|
166
|
+
"function hasPublicKey(address user) view returns (bool)",
|
|
167
|
+
"function getPublicKey(address user) view returns (bytes)",
|
|
168
|
+
"function publicKeys(address) view returns (bytes)",
|
|
169
|
+
"function hasKeyAccess(uint256 topicId, address user) view returns (bool)",
|
|
170
|
+
"function getMyKey(uint256 topicId) view returns (bytes encryptedKey, bytes granterPublicKey, address granter, uint256 keyVersion, uint256 currentVersion)",
|
|
171
|
+
"function getKeyGrant(uint256 topicId, address user) view returns (tuple(bytes encryptedKey, bytes granterPublicKey, address granter, uint256 keyVersion, uint64 grantedAt))",
|
|
172
|
+
"function keyVersions(uint256 topicId) view returns (uint256)",
|
|
173
|
+
// ===== WRITE FUNCTIONS =====
|
|
174
|
+
"function registerPublicKey(bytes publicKey)",
|
|
175
|
+
"function grantKeyAccess(uint256 topicId, address user, bytes encryptedKey)",
|
|
176
|
+
"function batchGrantKeyAccess(uint256 topicId, address[] users, bytes[] encryptedKeys)",
|
|
177
|
+
"function revokeKeyAccess(uint256 topicId, address user)",
|
|
178
|
+
"function rotateKey(uint256 topicId)",
|
|
179
|
+
// ===== EVENTS =====
|
|
180
|
+
"event PublicKeyRegistered(address indexed user, bytes publicKey)",
|
|
181
|
+
"event PublicKeyUpdated(address indexed user, bytes publicKey)",
|
|
182
|
+
"event KeyAccessGranted(uint256 indexed topicId, address indexed user, address indexed granter, uint256 version)",
|
|
183
|
+
"event KeyAccessRevoked(uint256 indexed topicId, address indexed user)",
|
|
184
|
+
"event KeyRotated(uint256 indexed topicId, uint256 newVersion)"
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
// src/crypto/encrypt.ts
|
|
188
|
+
var import_pbkdf2 = require("@noble/hashes/pbkdf2");
|
|
189
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
190
|
+
var import_aes = require("@noble/ciphers/aes");
|
|
191
|
+
var import_utils = require("@noble/hashes/utils");
|
|
192
|
+
|
|
193
|
+
// src/types.ts
|
|
194
|
+
var AccessLevel = /* @__PURE__ */ ((AccessLevel2) => {
|
|
195
|
+
AccessLevel2[AccessLevel2["PUBLIC"] = 0] = "PUBLIC";
|
|
196
|
+
AccessLevel2[AccessLevel2["PUBLIC_LIMITED"] = 1] = "PUBLIC_LIMITED";
|
|
197
|
+
AccessLevel2[AccessLevel2["PRIVATE"] = 2] = "PRIVATE";
|
|
198
|
+
return AccessLevel2;
|
|
199
|
+
})(AccessLevel || {});
|
|
200
|
+
var Permission = /* @__PURE__ */ ((Permission2) => {
|
|
201
|
+
Permission2[Permission2["NONE"] = 0] = "NONE";
|
|
202
|
+
Permission2[Permission2["READ"] = 1] = "READ";
|
|
203
|
+
Permission2[Permission2["WRITE"] = 2] = "WRITE";
|
|
204
|
+
Permission2[Permission2["READ_WRITE"] = 3] = "READ_WRITE";
|
|
205
|
+
Permission2[Permission2["ADMIN"] = 4] = "ADMIN";
|
|
206
|
+
return Permission2;
|
|
207
|
+
})(Permission || {});
|
|
208
|
+
var Role = /* @__PURE__ */ ((Role2) => {
|
|
209
|
+
Role2[Role2["MEMBER"] = 1] = "MEMBER";
|
|
210
|
+
Role2[Role2["SUPPORT_MANAGER"] = 2] = "SUPPORT_MANAGER";
|
|
211
|
+
Role2[Role2["TOPIC_MANAGER"] = 4] = "TOPIC_MANAGER";
|
|
212
|
+
Role2[Role2["ADMIN"] = 8] = "ADMIN";
|
|
213
|
+
Role2[Role2["OWNER_DELEGATE"] = 16] = "OWNER_DELEGATE";
|
|
214
|
+
return Role2;
|
|
215
|
+
})(Role || {});
|
|
216
|
+
|
|
217
|
+
// src/constants.ts
|
|
218
|
+
var ACCESS_PUBLIC = 0 /* PUBLIC */;
|
|
219
|
+
var ACCESS_PUBLIC_LIMITED = 1 /* PUBLIC_LIMITED */;
|
|
220
|
+
var ACCESS_PRIVATE = 2 /* PRIVATE */;
|
|
221
|
+
var PERMISSION_NONE = 0 /* NONE */;
|
|
222
|
+
var PERMISSION_READ = 1 /* READ */;
|
|
223
|
+
var PERMISSION_WRITE = 2 /* WRITE */;
|
|
224
|
+
var PERMISSION_READ_WRITE = 3 /* READ_WRITE */;
|
|
225
|
+
var PERMISSION_ADMIN = 4 /* ADMIN */;
|
|
226
|
+
var ROLE_MEMBER = 1 /* MEMBER */;
|
|
227
|
+
var ROLE_SUPPORT_MANAGER = 2 /* SUPPORT_MANAGER */;
|
|
228
|
+
var ROLE_TOPIC_MANAGER = 4 /* TOPIC_MANAGER */;
|
|
229
|
+
var ROLE_ADMIN = 8 /* ADMIN */;
|
|
230
|
+
var ROLE_OWNER_DELEGATE = 16 /* OWNER_DELEGATE */;
|
|
231
|
+
var PUBLIC_KEY_MATERIAL_PREFIX = "antenna-public-topic-";
|
|
232
|
+
var SALT_PREFIX = "antenna-v2-salt-";
|
|
233
|
+
var PBKDF2_ITERATIONS = 1e5;
|
|
234
|
+
var ECDH_HKDF_SALT = "antenna-ecdh-v1";
|
|
235
|
+
var ECDH_HKDF_INFO = "topic-key-encryption";
|
|
236
|
+
var ECDH_DERIVATION_MESSAGE = (address, appId) => `Clawntenna ECDH Key Derivation
|
|
237
|
+
|
|
238
|
+
This signature generates your encryption key.
|
|
239
|
+
It never leaves your device.
|
|
240
|
+
|
|
241
|
+
Wallet: ${address}
|
|
242
|
+
App: ${appId}
|
|
243
|
+
Chain: Base (8453)`;
|
|
244
|
+
|
|
245
|
+
// src/crypto/encrypt.ts
|
|
246
|
+
var encoder = new TextEncoder();
|
|
247
|
+
var decoder = new TextDecoder();
|
|
248
|
+
function derivePublicTopicKey(topicId) {
|
|
249
|
+
const keyMaterial = PUBLIC_KEY_MATERIAL_PREFIX + topicId;
|
|
250
|
+
const salt = encoder.encode(SALT_PREFIX + topicId);
|
|
251
|
+
return (0, import_pbkdf2.pbkdf2)(import_sha256.sha256, keyMaterial, salt, { c: PBKDF2_ITERATIONS, dkLen: 32 });
|
|
252
|
+
}
|
|
253
|
+
function deriveKeyFromPassphrase(passphrase, topicId) {
|
|
254
|
+
const salt = encoder.encode(SALT_PREFIX + topicId);
|
|
255
|
+
return (0, import_pbkdf2.pbkdf2)(import_sha256.sha256, passphrase, salt, { c: PBKDF2_ITERATIONS, dkLen: 32 });
|
|
256
|
+
}
|
|
257
|
+
function encrypt(plaintext, key) {
|
|
258
|
+
const iv = (0, import_utils.randomBytes)(12);
|
|
259
|
+
const aes = (0, import_aes.gcm)(key, iv);
|
|
260
|
+
const ciphertext = aes.encrypt(encoder.encode(plaintext));
|
|
261
|
+
const payload = {
|
|
262
|
+
e: true,
|
|
263
|
+
v: 2,
|
|
264
|
+
iv: toBase64(iv),
|
|
265
|
+
ct: toBase64(ciphertext)
|
|
266
|
+
};
|
|
267
|
+
return JSON.stringify(payload);
|
|
268
|
+
}
|
|
269
|
+
function decrypt(jsonStr, key) {
|
|
270
|
+
try {
|
|
271
|
+
const data = JSON.parse(jsonStr);
|
|
272
|
+
if (!data.e) {
|
|
273
|
+
return jsonStr;
|
|
274
|
+
}
|
|
275
|
+
const iv = fromBase64(data.iv);
|
|
276
|
+
const ct = fromBase64(data.ct);
|
|
277
|
+
const aes = (0, import_aes.gcm)(key, iv);
|
|
278
|
+
const decrypted = aes.decrypt(ct);
|
|
279
|
+
return decoder.decode(decrypted);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function encryptMessage(text, key, options) {
|
|
285
|
+
const content = { text };
|
|
286
|
+
if (options?.replyTo) content.replyTo = options.replyTo;
|
|
287
|
+
if (options?.mentions) content.mentions = options.mentions;
|
|
288
|
+
return encrypt(JSON.stringify(content), key);
|
|
289
|
+
}
|
|
290
|
+
function decryptMessage(jsonStr, key) {
|
|
291
|
+
const decrypted = decrypt(jsonStr, key);
|
|
292
|
+
if (!decrypted) return null;
|
|
293
|
+
try {
|
|
294
|
+
const content = JSON.parse(decrypted);
|
|
295
|
+
if (typeof content === "object" && content.text) {
|
|
296
|
+
return {
|
|
297
|
+
text: content.text,
|
|
298
|
+
replyTo: content.replyTo || null,
|
|
299
|
+
mentions: content.mentions || null
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
return { text: decrypted, replyTo: null, mentions: null };
|
|
303
|
+
} catch {
|
|
304
|
+
return { text: decrypted, replyTo: null, mentions: null };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function toBase64(bytes) {
|
|
308
|
+
if (typeof Buffer !== "undefined") {
|
|
309
|
+
return Buffer.from(bytes).toString("base64");
|
|
310
|
+
}
|
|
311
|
+
return btoa(String.fromCharCode(...bytes));
|
|
312
|
+
}
|
|
313
|
+
function fromBase64(str) {
|
|
314
|
+
if (typeof Buffer !== "undefined") {
|
|
315
|
+
return new Uint8Array(Buffer.from(str, "base64"));
|
|
316
|
+
}
|
|
317
|
+
return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/crypto/ecdh.ts
|
|
321
|
+
var import_secp256k1 = require("@noble/curves/secp256k1");
|
|
322
|
+
var import_hkdf = require("@noble/hashes/hkdf");
|
|
323
|
+
var import_sha2562 = require("@noble/hashes/sha256");
|
|
324
|
+
var import_aes2 = require("@noble/ciphers/aes");
|
|
325
|
+
var import_utils2 = require("@noble/hashes/utils");
|
|
326
|
+
var encoder2 = new TextEncoder();
|
|
327
|
+
async function deriveKeypairFromSignature(walletAddress, signMessage, appId = 1) {
|
|
328
|
+
const message = ECDH_DERIVATION_MESSAGE(walletAddress, appId);
|
|
329
|
+
const signature = await signMessage(message);
|
|
330
|
+
const sigBytes = encoder2.encode(signature);
|
|
331
|
+
const hashBuffer = (0, import_sha2562.sha256)(sigBytes);
|
|
332
|
+
const privateKey = new Uint8Array(hashBuffer);
|
|
333
|
+
const publicKey = import_secp256k1.secp256k1.getPublicKey(privateKey, true);
|
|
334
|
+
return { privateKey, publicKey };
|
|
335
|
+
}
|
|
336
|
+
function keypairFromPrivateKey(privateKeyHex) {
|
|
337
|
+
const cleaned = privateKeyHex.startsWith("0x") ? privateKeyHex.slice(2) : privateKeyHex;
|
|
338
|
+
const privateKey = hexToBytes(cleaned);
|
|
339
|
+
const publicKey = import_secp256k1.secp256k1.getPublicKey(privateKey, true);
|
|
340
|
+
return { privateKey, publicKey };
|
|
341
|
+
}
|
|
342
|
+
function computeSharedSecret(ourPrivateKey, theirPublicKey) {
|
|
343
|
+
const sharedPoint = import_secp256k1.secp256k1.getSharedSecret(ourPrivateKey, theirPublicKey);
|
|
344
|
+
return sharedPoint.slice(1, 33);
|
|
345
|
+
}
|
|
346
|
+
function deriveAESKeyFromSecret(sharedSecret, info = ECDH_HKDF_INFO) {
|
|
347
|
+
return (0, import_hkdf.hkdf)(import_sha2562.sha256, sharedSecret, encoder2.encode(ECDH_HKDF_SALT), info, 32);
|
|
348
|
+
}
|
|
349
|
+
function encryptTopicKeyForUser(topicKey, ourPrivateKey, recipientPublicKey) {
|
|
350
|
+
const shared = computeSharedSecret(ourPrivateKey, recipientPublicKey);
|
|
351
|
+
const aesKey = deriveAESKeyFromSecret(shared);
|
|
352
|
+
const iv = (0, import_utils2.randomBytes)(12);
|
|
353
|
+
const aes = (0, import_aes2.gcm)(aesKey, iv);
|
|
354
|
+
const ciphertext = aes.encrypt(topicKey);
|
|
355
|
+
const result = new Uint8Array(iv.length + ciphertext.length);
|
|
356
|
+
result.set(iv);
|
|
357
|
+
result.set(ciphertext, iv.length);
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
function decryptTopicKey(encryptedKey, ourPrivateKey, granterPublicKey) {
|
|
361
|
+
const shared = computeSharedSecret(ourPrivateKey, granterPublicKey);
|
|
362
|
+
const aesKey = deriveAESKeyFromSecret(shared);
|
|
363
|
+
const iv = encryptedKey.slice(0, 12);
|
|
364
|
+
const ciphertext = encryptedKey.slice(12);
|
|
365
|
+
const aes = (0, import_aes2.gcm)(aesKey, iv);
|
|
366
|
+
return aes.decrypt(ciphertext);
|
|
367
|
+
}
|
|
368
|
+
function bytesToHex(bytes) {
|
|
369
|
+
return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
370
|
+
}
|
|
371
|
+
function hexToBytes(hex) {
|
|
372
|
+
const cleaned = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
373
|
+
return new Uint8Array(cleaned.match(/.{1,2}/g).map((b) => parseInt(b, 16)));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/client.ts
|
|
377
|
+
var Clawntenna = class {
|
|
378
|
+
provider;
|
|
379
|
+
wallet;
|
|
380
|
+
registry;
|
|
381
|
+
keyManager;
|
|
382
|
+
chainName;
|
|
383
|
+
// In-memory ECDH state
|
|
384
|
+
ecdhPrivateKey = null;
|
|
385
|
+
ecdhPublicKey = null;
|
|
386
|
+
topicKeys = /* @__PURE__ */ new Map();
|
|
387
|
+
constructor(options = {}) {
|
|
388
|
+
const chainName = options.chain ?? "base";
|
|
389
|
+
const chain = CHAINS[chainName];
|
|
390
|
+
if (!chain) throw new Error(`Unsupported chain: ${chainName}`);
|
|
391
|
+
this.chainName = chainName;
|
|
392
|
+
const rpcUrl = options.rpcUrl ?? chain.rpc;
|
|
393
|
+
this.provider = new import_ethers.ethers.JsonRpcProvider(rpcUrl);
|
|
394
|
+
const registryAddr = options.registryAddress ?? chain.registry;
|
|
395
|
+
const keyManagerAddr = options.keyManagerAddress ?? chain.keyManager;
|
|
396
|
+
if (options.privateKey) {
|
|
397
|
+
this.wallet = new import_ethers.ethers.Wallet(options.privateKey, this.provider);
|
|
398
|
+
this.registry = new import_ethers.ethers.Contract(registryAddr, REGISTRY_ABI, this.wallet);
|
|
399
|
+
this.keyManager = new import_ethers.ethers.Contract(keyManagerAddr, KEY_MANAGER_ABI, this.wallet);
|
|
400
|
+
} else {
|
|
401
|
+
this.wallet = null;
|
|
402
|
+
this.registry = new import_ethers.ethers.Contract(registryAddr, REGISTRY_ABI, this.provider);
|
|
403
|
+
this.keyManager = new import_ethers.ethers.Contract(keyManagerAddr, KEY_MANAGER_ABI, this.provider);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
get address() {
|
|
407
|
+
return this.wallet?.address ?? null;
|
|
408
|
+
}
|
|
409
|
+
// ===== MESSAGING =====
|
|
410
|
+
/**
|
|
411
|
+
* Send an encrypted message to a topic.
|
|
412
|
+
* Automatically determines encryption key based on topic access level.
|
|
413
|
+
*/
|
|
414
|
+
async sendMessage(topicId, text, options) {
|
|
415
|
+
if (!this.wallet) throw new Error("Wallet required to send messages");
|
|
416
|
+
const key = await this.getEncryptionKey(topicId);
|
|
417
|
+
const encrypted = encryptMessage(text, key, {
|
|
418
|
+
replyTo: options?.replyTo,
|
|
419
|
+
mentions: options?.mentions
|
|
420
|
+
});
|
|
421
|
+
return this.registry.sendMessage(topicId, import_ethers.ethers.toUtf8Bytes(encrypted));
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Read and decrypt recent messages from a topic.
|
|
425
|
+
*/
|
|
426
|
+
async readMessages(topicId, options) {
|
|
427
|
+
const limit = options?.limit ?? 50;
|
|
428
|
+
const fromBlock = options?.fromBlock ?? -1e5;
|
|
429
|
+
const key = await this.getEncryptionKey(topicId);
|
|
430
|
+
const filter = this.registry.filters.MessageSent(topicId);
|
|
431
|
+
const events = await this.registry.queryFilter(filter, fromBlock);
|
|
432
|
+
const messages = [];
|
|
433
|
+
const recent = events.slice(-limit);
|
|
434
|
+
for (const event of recent) {
|
|
435
|
+
const log = event;
|
|
436
|
+
const payloadStr = import_ethers.ethers.toUtf8String(log.args.payload);
|
|
437
|
+
const parsed = decryptMessage(payloadStr, key);
|
|
438
|
+
messages.push({
|
|
439
|
+
topicId: BigInt(topicId),
|
|
440
|
+
sender: log.args.sender,
|
|
441
|
+
text: parsed?.text ?? "[decryption failed]",
|
|
442
|
+
replyTo: parsed?.replyTo ?? null,
|
|
443
|
+
mentions: parsed?.mentions ?? null,
|
|
444
|
+
timestamp: log.args.timestamp,
|
|
445
|
+
txHash: log.transactionHash,
|
|
446
|
+
blockNumber: log.blockNumber
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return messages;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Subscribe to real-time messages on a topic.
|
|
453
|
+
* Returns an unsubscribe function.
|
|
454
|
+
*/
|
|
455
|
+
onMessage(topicId, callback) {
|
|
456
|
+
let key = null;
|
|
457
|
+
this.getEncryptionKey(topicId).then((k) => {
|
|
458
|
+
key = k;
|
|
459
|
+
});
|
|
460
|
+
const handler = (tId, sender, payload, timestamp, event) => {
|
|
461
|
+
if (!key) return;
|
|
462
|
+
const payloadStr = import_ethers.ethers.toUtf8String(payload);
|
|
463
|
+
const parsed = decryptMessage(payloadStr, key);
|
|
464
|
+
callback({
|
|
465
|
+
topicId: tId,
|
|
466
|
+
sender,
|
|
467
|
+
text: parsed?.text ?? "[decryption failed]",
|
|
468
|
+
replyTo: parsed?.replyTo ?? null,
|
|
469
|
+
mentions: parsed?.mentions ?? null,
|
|
470
|
+
timestamp,
|
|
471
|
+
txHash: event.transactionHash,
|
|
472
|
+
blockNumber: event.blockNumber
|
|
473
|
+
});
|
|
474
|
+
};
|
|
475
|
+
this.registry.on(this.registry.filters.MessageSent(topicId), handler);
|
|
476
|
+
return () => {
|
|
477
|
+
this.registry.off(this.registry.filters.MessageSent(topicId), handler);
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
// ===== NICKNAMES =====
|
|
481
|
+
async setNickname(appId, nickname) {
|
|
482
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
483
|
+
return this.registry.setNickname(appId, nickname);
|
|
484
|
+
}
|
|
485
|
+
async getNickname(appId, address) {
|
|
486
|
+
return this.registry.getNickname(appId, address);
|
|
487
|
+
}
|
|
488
|
+
// ===== TOPICS =====
|
|
489
|
+
async createTopic(appId, name, description, accessLevel) {
|
|
490
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
491
|
+
return this.registry.createTopic(appId, name, description, accessLevel);
|
|
492
|
+
}
|
|
493
|
+
async getTopic(topicId) {
|
|
494
|
+
const t = await this.registry.getTopic(topicId);
|
|
495
|
+
return {
|
|
496
|
+
id: t.id,
|
|
497
|
+
applicationId: t.applicationId,
|
|
498
|
+
name: t.name,
|
|
499
|
+
description: t.description,
|
|
500
|
+
owner: t.owner,
|
|
501
|
+
creator: t.creator,
|
|
502
|
+
createdAt: t.createdAt,
|
|
503
|
+
lastMessageAt: t.lastMessageAt,
|
|
504
|
+
messageCount: t.messageCount,
|
|
505
|
+
accessLevel: Number(t.accessLevel),
|
|
506
|
+
active: t.active
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
async getApplicationTopics(appId) {
|
|
510
|
+
return this.registry.getApplicationTopics(appId);
|
|
511
|
+
}
|
|
512
|
+
async setTopicPermission(topicId, user, permission) {
|
|
513
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
514
|
+
return this.registry.setTopicPermission(topicId, user, permission);
|
|
515
|
+
}
|
|
516
|
+
// ===== MEMBERS =====
|
|
517
|
+
async addMember(appId, address, nickname, roles) {
|
|
518
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
519
|
+
return this.registry.addMember(appId, address, nickname, roles);
|
|
520
|
+
}
|
|
521
|
+
async removeMember(appId, address) {
|
|
522
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
523
|
+
return this.registry.removeMember(appId, address);
|
|
524
|
+
}
|
|
525
|
+
async updateMemberRoles(appId, address, roles) {
|
|
526
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
527
|
+
return this.registry.updateMemberRoles(appId, address, roles);
|
|
528
|
+
}
|
|
529
|
+
async getMember(appId, address) {
|
|
530
|
+
const m = await this.registry.getMember(appId, address);
|
|
531
|
+
return {
|
|
532
|
+
account: m.account,
|
|
533
|
+
nickname: m.nickname,
|
|
534
|
+
roles: Number(m.roles),
|
|
535
|
+
joinedAt: m.joinedAt
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
async isMember(appId, address) {
|
|
539
|
+
return this.registry.isMember(appId, address);
|
|
540
|
+
}
|
|
541
|
+
async getApplicationMembers(appId) {
|
|
542
|
+
return this.registry.getApplicationMembers(appId);
|
|
543
|
+
}
|
|
544
|
+
// ===== ACCESS CHECKS =====
|
|
545
|
+
async canRead(topicId, address) {
|
|
546
|
+
return this.registry.canReadTopic(topicId, address);
|
|
547
|
+
}
|
|
548
|
+
async canWrite(topicId, address) {
|
|
549
|
+
return this.registry.canWriteToTopic(topicId, address);
|
|
550
|
+
}
|
|
551
|
+
// ===== APPLICATIONS =====
|
|
552
|
+
async createApplication(name, description, frontendUrl, allowPublicTopicCreation) {
|
|
553
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
554
|
+
return this.registry.createApplication(name, description, frontendUrl, allowPublicTopicCreation);
|
|
555
|
+
}
|
|
556
|
+
async getApplication(appId) {
|
|
557
|
+
const a = await this.registry.getApplication(appId);
|
|
558
|
+
return {
|
|
559
|
+
id: a.id,
|
|
560
|
+
name: a.name,
|
|
561
|
+
description: a.description,
|
|
562
|
+
frontendUrl: a.frontendUrl,
|
|
563
|
+
owner: a.owner,
|
|
564
|
+
createdAt: a.createdAt,
|
|
565
|
+
memberCount: Number(a.memberCount),
|
|
566
|
+
topicCount: Number(a.topicCount),
|
|
567
|
+
active: a.active,
|
|
568
|
+
allowPublicTopicCreation: a.allowPublicTopicCreation,
|
|
569
|
+
topicCreationFeeToken: a.topicCreationFeeToken,
|
|
570
|
+
topicCreationFeeAmount: a.topicCreationFeeAmount
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
// ===== FEES =====
|
|
574
|
+
async getTopicMessageFee(topicId) {
|
|
575
|
+
const [token, amount] = await this.registry.getTopicMessageFee(topicId);
|
|
576
|
+
return { token, amount };
|
|
577
|
+
}
|
|
578
|
+
// ===== ECDH (Private Topics) =====
|
|
579
|
+
/**
|
|
580
|
+
* Derive ECDH keypair from wallet signature (deterministic).
|
|
581
|
+
* Requires a signer capable of signing messages.
|
|
582
|
+
*/
|
|
583
|
+
async deriveECDHFromWallet(appId = 1) {
|
|
584
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
585
|
+
const { privateKey, publicKey } = await deriveKeypairFromSignature(
|
|
586
|
+
this.wallet.address,
|
|
587
|
+
(msg) => this.wallet.signMessage(msg),
|
|
588
|
+
appId
|
|
589
|
+
);
|
|
590
|
+
this.ecdhPrivateKey = privateKey;
|
|
591
|
+
this.ecdhPublicKey = publicKey;
|
|
592
|
+
return { publicKey };
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Load ECDH keypair from a hex private key (e.g. from credentials file).
|
|
596
|
+
*/
|
|
597
|
+
loadECDHKeypair(privateKeyHex) {
|
|
598
|
+
const { privateKey, publicKey } = keypairFromPrivateKey(privateKeyHex);
|
|
599
|
+
this.ecdhPrivateKey = privateKey;
|
|
600
|
+
this.ecdhPublicKey = publicKey;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Register ECDH public key on-chain.
|
|
604
|
+
*/
|
|
605
|
+
async registerPublicKey() {
|
|
606
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
607
|
+
if (!this.ecdhPublicKey) throw new Error("ECDH key not derived yet");
|
|
608
|
+
const hasKey = await this.keyManager.hasPublicKey(this.wallet.address);
|
|
609
|
+
if (hasKey) {
|
|
610
|
+
throw new Error("Public key already registered on-chain");
|
|
611
|
+
}
|
|
612
|
+
return this.keyManager.registerPublicKey(this.ecdhPublicKey);
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Fetch and decrypt the topic symmetric key from an on-chain ECDH grant.
|
|
616
|
+
*/
|
|
617
|
+
async fetchAndDecryptTopicKey(topicId) {
|
|
618
|
+
if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
|
|
619
|
+
const grant = await this.keyManager.getMyKey(topicId);
|
|
620
|
+
const encryptedKey = import_ethers.ethers.getBytes(grant.encryptedKey);
|
|
621
|
+
const granterPubKey = import_ethers.ethers.getBytes(grant.granterPublicKey);
|
|
622
|
+
const topicKey = decryptTopicKey(encryptedKey, this.ecdhPrivateKey, granterPubKey);
|
|
623
|
+
this.topicKeys.set(topicId, topicKey);
|
|
624
|
+
return topicKey;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Grant a user access to a private topic's symmetric key.
|
|
628
|
+
*/
|
|
629
|
+
async grantKeyAccess(topicId, userAddress, topicKey) {
|
|
630
|
+
if (!this.wallet) throw new Error("Wallet required");
|
|
631
|
+
if (!this.ecdhPrivateKey) throw new Error("ECDH key not derived yet");
|
|
632
|
+
const userPubKeyBytes = import_ethers.ethers.getBytes(await this.keyManager.getPublicKey(userAddress));
|
|
633
|
+
const encrypted = encryptTopicKeyForUser(topicKey, this.ecdhPrivateKey, userPubKeyBytes);
|
|
634
|
+
return this.keyManager.grantKeyAccess(topicId, userAddress, encrypted);
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Store a pre-known topic key (e.g. loaded from credentials).
|
|
638
|
+
*/
|
|
639
|
+
setTopicKey(topicId, key) {
|
|
640
|
+
this.topicKeys.set(topicId, key);
|
|
641
|
+
}
|
|
642
|
+
// ===== INTERNAL =====
|
|
643
|
+
/**
|
|
644
|
+
* Get the encryption key for a topic, determining the type automatically.
|
|
645
|
+
*/
|
|
646
|
+
async getEncryptionKey(topicId) {
|
|
647
|
+
const storedKey = this.topicKeys.get(topicId);
|
|
648
|
+
if (storedKey) return storedKey;
|
|
649
|
+
const topic = await this.getTopic(topicId);
|
|
650
|
+
if (topic.accessLevel === 2 /* PRIVATE */) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
`Topic ${topicId} is PRIVATE. Call fetchAndDecryptTopicKey() or setTopicKey() first.`
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
return derivePublicTopicKey(topicId);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
659
|
+
0 && (module.exports = {
|
|
660
|
+
ACCESS_PRIVATE,
|
|
661
|
+
ACCESS_PUBLIC,
|
|
662
|
+
ACCESS_PUBLIC_LIMITED,
|
|
663
|
+
AccessLevel,
|
|
664
|
+
CHAINS,
|
|
665
|
+
CHAIN_IDS,
|
|
666
|
+
Clawntenna,
|
|
667
|
+
KEY_MANAGER_ABI,
|
|
668
|
+
PERMISSION_ADMIN,
|
|
669
|
+
PERMISSION_NONE,
|
|
670
|
+
PERMISSION_READ,
|
|
671
|
+
PERMISSION_READ_WRITE,
|
|
672
|
+
PERMISSION_WRITE,
|
|
673
|
+
Permission,
|
|
674
|
+
REGISTRY_ABI,
|
|
675
|
+
ROLE_ADMIN,
|
|
676
|
+
ROLE_MEMBER,
|
|
677
|
+
ROLE_OWNER_DELEGATE,
|
|
678
|
+
ROLE_SUPPORT_MANAGER,
|
|
679
|
+
ROLE_TOPIC_MANAGER,
|
|
680
|
+
Role,
|
|
681
|
+
bytesToHex,
|
|
682
|
+
computeSharedSecret,
|
|
683
|
+
decrypt,
|
|
684
|
+
decryptMessage,
|
|
685
|
+
decryptTopicKey,
|
|
686
|
+
deriveAESKeyFromSecret,
|
|
687
|
+
deriveKeyFromPassphrase,
|
|
688
|
+
deriveKeypairFromSignature,
|
|
689
|
+
derivePublicTopicKey,
|
|
690
|
+
encrypt,
|
|
691
|
+
encryptMessage,
|
|
692
|
+
encryptTopicKeyForUser,
|
|
693
|
+
getChain,
|
|
694
|
+
hexToBytes,
|
|
695
|
+
keypairFromPrivateKey
|
|
696
|
+
});
|
|
697
|
+
//# sourceMappingURL=index.cjs.map
|