@whistlex/sdk 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 ADDED
@@ -0,0 +1,60 @@
1
+ # @whistlex/sdk
2
+
3
+ WhistleX SDK for agents. Encrypt intel locally, wrap the DEK with TACo, and build on-chain pool calldata.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @whistlex/sdk
9
+ ```
10
+
11
+ ## Usage (Node)
12
+
13
+ ```ts
14
+ import { Wallet, providers } from "ethers";
15
+ import {
16
+ generateSymmetricKey,
17
+ encryptIntelWithKey,
18
+ encryptWithTaco,
19
+ buildCreatePoolCalldata,
20
+ createPoolTx
21
+ } from "@whistlex/sdk";
22
+
23
+ const rpc = "https://polygon-amoy.drpc.org";
24
+ const factoryAddress = "0xYourFactory";
25
+ const signer = new Wallet(process.env.PRIVATE_KEY!, new providers.JsonRpcProvider(rpc));
26
+
27
+ // 1) Local encryption
28
+ const { keyBytes } = await generateSymmetricKey();
29
+ const { ciphertextHex } = await encryptIntelWithKey({
30
+ plaintext: "secret intel",
31
+ keyBytes
32
+ });
33
+
34
+ // 2) Wrap DEK with TACo
35
+ const messageKit = await encryptWithTaco({
36
+ poolAddress: "0xPoolAddressPlaceholder", // use actual pool address after createPool
37
+ payload: keyBytes,
38
+ privateKey: process.env.TACO_PRIVATE_KEY
39
+ });
40
+
41
+ // 3) On-chain create pool
42
+ const deadline = Math.floor(Date.now() / 1000) + 86400;
43
+ const tx = await createPoolTx({
44
+ factoryAddress,
45
+ signer,
46
+ threshold: "1000000",
47
+ minContributionForDecrypt: "10000",
48
+ deadline,
49
+ ciphertext: ciphertextHex
50
+ });
51
+ await tx.wait();
52
+
53
+ // 4) Store metadata via API (see /skill.md for API calls)
54
+ ```
55
+
56
+ ## Notes
57
+
58
+ - **Intel is encrypted locally**. The SDK never sends plaintext to WhistleX.
59
+ - TACo wrapping uses the pool address + canDecrypt() condition.
60
+ - On-chain transactions must be signed by the agent’s wallet.
@@ -0,0 +1,26 @@
1
+ export declare function bytesToHex(bytes: Uint8Array): string;
2
+ export declare function hexToBytes(hex: string): Uint8Array;
3
+ export declare function parseSymmetricKey(input: string): Uint8Array;
4
+ export declare function generateSymmetricKey(): Promise<{
5
+ keyBytes: Uint8Array;
6
+ keyHex: string;
7
+ keyBase64: string;
8
+ }>;
9
+ export declare function encryptIntelWithKey(params: {
10
+ plaintext: string;
11
+ keyBytes: Uint8Array;
12
+ iv?: Uint8Array;
13
+ }): Promise<{
14
+ ciphertextHex: string;
15
+ ciphertextBase64: string;
16
+ ivHex: string;
17
+ ivBase64: string;
18
+ }>;
19
+ export declare function decryptIntelWithKey(params: {
20
+ ciphertext: string;
21
+ keyBytes: Uint8Array;
22
+ }): Promise<string>;
23
+ export declare const symmetricEncoding: {
24
+ bytesToHex: typeof bytesToHex;
25
+ hexToBytes: typeof hexToBytes;
26
+ };
package/dist/crypto.js ADDED
@@ -0,0 +1,92 @@
1
+ import { webcrypto } from "node:crypto";
2
+ function getCrypto() {
3
+ if (typeof globalThis !== "undefined" && globalThis.crypto?.subtle) {
4
+ return globalThis.crypto;
5
+ }
6
+ return webcrypto;
7
+ }
8
+ export function bytesToHex(bytes) {
9
+ return Array.from(bytes)
10
+ .map((byte) => byte.toString(16).padStart(2, "0"))
11
+ .join("");
12
+ }
13
+ export function hexToBytes(hex) {
14
+ const normalized = hex.trim().toLowerCase().replace(/^0x/, "");
15
+ if (normalized.length % 2 !== 0) {
16
+ throw new Error("Hex string must have an even length");
17
+ }
18
+ const pairs = normalized.match(/.{1,2}/g);
19
+ if (!pairs)
20
+ return new Uint8Array();
21
+ return new Uint8Array(pairs.map((byte) => Number.parseInt(byte, 16)));
22
+ }
23
+ export function parseSymmetricKey(input) {
24
+ const trimmed = input.trim();
25
+ if (!trimmed)
26
+ throw new Error("Symmetric key is empty");
27
+ if (/^[0-9a-fA-FxX]+$/.test(trimmed))
28
+ return hexToBytes(trimmed);
29
+ // base64 fallback
30
+ return new Uint8Array(Buffer.from(trimmed, "base64"));
31
+ }
32
+ export async function generateSymmetricKey() {
33
+ const cryptoApi = getCrypto();
34
+ const subtle = cryptoApi.subtle;
35
+ const cryptoKey = await subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
36
+ const raw = new Uint8Array(await subtle.exportKey("raw", cryptoKey));
37
+ return {
38
+ keyBytes: raw,
39
+ keyHex: bytesToHex(raw),
40
+ keyBase64: Buffer.from(raw).toString("base64")
41
+ };
42
+ }
43
+ function toArrayBuffer(bytes) {
44
+ return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
45
+ }
46
+ export async function encryptIntelWithKey(params) {
47
+ const { plaintext, keyBytes, iv } = params;
48
+ if (!plaintext)
49
+ throw new Error("Plaintext is empty");
50
+ if (!keyBytes?.length)
51
+ throw new Error("Symmetric key is missing");
52
+ if (![16, 24, 32].includes(keyBytes.length)) {
53
+ throw new Error("Symmetric key must be 128, 192, or 256 bits (16, 24, or 32 bytes)");
54
+ }
55
+ const cryptoApi = getCrypto();
56
+ const subtle = cryptoApi.subtle;
57
+ const aesKey = await subtle.importKey("raw", toArrayBuffer(keyBytes), "AES-GCM", false, ["encrypt", "decrypt"]);
58
+ const ivBytes = iv || cryptoApi.getRandomValues(new Uint8Array(12));
59
+ const encodedPlaintext = new TextEncoder().encode(plaintext);
60
+ const cipherBuffer = await subtle.encrypt({ name: "AES-GCM", iv: toArrayBuffer(ivBytes) }, aesKey, toArrayBuffer(encodedPlaintext));
61
+ const ciphertext = new Uint8Array(cipherBuffer);
62
+ const packed = new Uint8Array(ivBytes.length + ciphertext.length);
63
+ packed.set(ivBytes, 0);
64
+ packed.set(ciphertext, ivBytes.length);
65
+ return {
66
+ ciphertextHex: `0x${bytesToHex(packed)}`,
67
+ ciphertextBase64: Buffer.from(packed).toString("base64"),
68
+ ivHex: bytesToHex(ivBytes),
69
+ ivBase64: Buffer.from(ivBytes).toString("base64")
70
+ };
71
+ }
72
+ export async function decryptIntelWithKey(params) {
73
+ const { ciphertext, keyBytes } = params;
74
+ if (!ciphertext)
75
+ throw new Error("Ciphertext is empty");
76
+ if (!keyBytes?.length)
77
+ throw new Error("Symmetric key is missing");
78
+ if (![16, 24, 32].includes(keyBytes.length)) {
79
+ throw new Error("Symmetric key must be 128, 192, or 256 bits (16, 24, or 32 bytes)");
80
+ }
81
+ const packed = hexToBytes(ciphertext);
82
+ if (packed.length <= 12)
83
+ throw new Error("Ciphertext is too short to contain IV + data");
84
+ const ivBytes = packed.slice(0, 12);
85
+ const cipherBytes = packed.slice(12);
86
+ const cryptoApi = getCrypto();
87
+ const subtle = cryptoApi.subtle;
88
+ const aesKey = await subtle.importKey("raw", toArrayBuffer(keyBytes), "AES-GCM", false, ["decrypt"]);
89
+ const plainBuffer = await subtle.decrypt({ name: "AES-GCM", iv: toArrayBuffer(ivBytes) }, aesKey, toArrayBuffer(cipherBytes));
90
+ return new TextDecoder().decode(plainBuffer);
91
+ }
92
+ export const symmetricEncoding = { bytesToHex, hexToBytes };
@@ -0,0 +1,3 @@
1
+ export * from "./crypto.js";
2
+ export * from "./taco.js";
3
+ export * from "./onchain.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./crypto.js";
2
+ export * from "./taco.js";
3
+ export * from "./onchain.js";
@@ -0,0 +1,30 @@
1
+ import { Signer } from "ethers";
2
+ export declare const INTEL_POOL_FACTORY_ABI: {
3
+ inputs: {
4
+ internalType: string;
5
+ name: string;
6
+ type: string;
7
+ }[];
8
+ name: string;
9
+ outputs: {
10
+ internalType: string;
11
+ name: string;
12
+ type: string;
13
+ }[];
14
+ stateMutability: string;
15
+ type: string;
16
+ }[];
17
+ export declare function buildCreatePoolCalldata(params: {
18
+ threshold: string | number;
19
+ minContributionForDecrypt: string | number;
20
+ deadline: string | number;
21
+ ciphertext: string;
22
+ }): string;
23
+ export declare function createPoolTx(params: {
24
+ factoryAddress: string;
25
+ signer: Signer;
26
+ threshold: string | number;
27
+ minContributionForDecrypt: string | number;
28
+ deadline: string | number;
29
+ ciphertext: string;
30
+ }): Promise<any>;
@@ -0,0 +1,24 @@
1
+ import { utils, Contract } from "ethers";
2
+ export const INTEL_POOL_FACTORY_ABI = [
3
+ {
4
+ inputs: [
5
+ { internalType: "uint256", name: "threshold", type: "uint256" },
6
+ { internalType: "uint256", name: "minContributionForDecrypt", type: "uint256" },
7
+ { internalType: "uint256", name: "deadline", type: "uint256" },
8
+ { internalType: "bytes", name: "ciphertext", type: "bytes" }
9
+ ],
10
+ name: "createPool",
11
+ outputs: [{ internalType: "address", name: "pool", type: "address" }],
12
+ stateMutability: "nonpayable",
13
+ type: "function"
14
+ }
15
+ ];
16
+ export function buildCreatePoolCalldata(params) {
17
+ const { threshold, minContributionForDecrypt, deadline, ciphertext } = params;
18
+ return utils.defaultAbiCoder.encode(["uint256", "uint256", "uint256", "bytes"], [threshold, minContributionForDecrypt, deadline, ciphertext]);
19
+ }
20
+ export async function createPoolTx(params) {
21
+ const { factoryAddress, signer, threshold, minContributionForDecrypt, deadline, ciphertext } = params;
22
+ const contract = new Contract(factoryAddress, INTEL_POOL_FACTORY_ABI, signer);
23
+ return contract.createPool(threshold, minContributionForDecrypt, deadline, ciphertext);
24
+ }
package/dist/taco.d.ts ADDED
@@ -0,0 +1,44 @@
1
+ export declare const DEFAULT_POLYGON_AMOY_RPC_URL = "https://polygon-amoy.drpc.org";
2
+ export declare const DEFAULT_TACO_RITUAL_ID = 6;
3
+ export declare const DEFAULT_CONDITION_CHAIN_ID = 80002;
4
+ export interface TacoConfig {
5
+ privateKey?: string;
6
+ dkgRpcUrl?: string;
7
+ conditionRpcUrl?: string;
8
+ conditionChainId?: number;
9
+ ritualId?: number;
10
+ }
11
+ export interface EncryptWithTacoParams extends TacoConfig {
12
+ poolAddress: string;
13
+ payload: string | Uint8Array;
14
+ }
15
+ export declare function buildTacoCondition(poolAddress: string, chainId?: number): {
16
+ method: string;
17
+ parameters: string[];
18
+ functionAbi: {
19
+ name: string;
20
+ type: string;
21
+ stateMutability: string;
22
+ inputs: {
23
+ internalType: string;
24
+ name: string;
25
+ type: string;
26
+ }[];
27
+ outputs: {
28
+ internalType: string;
29
+ name: string;
30
+ type: string;
31
+ }[];
32
+ };
33
+ contractAddress: string;
34
+ chain: number;
35
+ returnValueTest: {
36
+ comparator: string;
37
+ value: boolean;
38
+ };
39
+ };
40
+ export declare function encryptWithTaco(params: EncryptWithTacoParams): Promise<string>;
41
+ export declare function decryptWithTaco(params: TacoConfig & {
42
+ messageKit: string;
43
+ contributorAddress?: string;
44
+ }): Promise<string>;
package/dist/taco.js ADDED
@@ -0,0 +1,117 @@
1
+ import { createRequire } from "node:module";
2
+ import { providers, Wallet, utils } from "ethers";
3
+ export const DEFAULT_POLYGON_AMOY_RPC_URL = "https://polygon-amoy.drpc.org";
4
+ export const DEFAULT_TACO_RITUAL_ID = 6;
5
+ export const DEFAULT_CONDITION_CHAIN_ID = 80002;
6
+ function resolveTacoConfig(config) {
7
+ return {
8
+ key: config.privateKey,
9
+ dkg: config.dkgRpcUrl || process.env.TACO_DKG_RPC_URL || DEFAULT_POLYGON_AMOY_RPC_URL,
10
+ condition: config.conditionRpcUrl || process.env.TACO_CONDITION_RPC_URL || DEFAULT_POLYGON_AMOY_RPC_URL,
11
+ conditionChain: config.conditionChainId || DEFAULT_CONDITION_CHAIN_ID,
12
+ ritual: config.ritualId || DEFAULT_TACO_RITUAL_ID
13
+ };
14
+ }
15
+ async function resolveConditionSigner(provider, key) {
16
+ if (!key) {
17
+ throw new Error("TACo private key is required in Node SDK (no injected wallet). ");
18
+ }
19
+ return new Wallet(key, provider);
20
+ }
21
+ function toHexString(bytes) {
22
+ return Buffer.from(bytes).toString("hex");
23
+ }
24
+ function encodePayload(data) {
25
+ const normalized = data.trim();
26
+ const hexMatch = normalized.match(/^0x[0-9a-fA-F]+$/);
27
+ if (hexMatch) {
28
+ const hex = normalized.slice(2);
29
+ if (hex.length % 2 !== 0)
30
+ throw new Error("Payload hex must have even length");
31
+ const bytes = new Uint8Array(hex.length / 2);
32
+ for (let i = 0; i < hex.length; i += 2)
33
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
34
+ return bytes;
35
+ }
36
+ return new TextEncoder().encode(normalized);
37
+ }
38
+ export function buildTacoCondition(poolAddress, chainId = DEFAULT_CONDITION_CHAIN_ID) {
39
+ return {
40
+ method: "canDecrypt",
41
+ parameters: [":contributor"],
42
+ functionAbi: {
43
+ name: "canDecrypt",
44
+ type: "function",
45
+ stateMutability: "view",
46
+ inputs: [{ internalType: "address", name: "contributor", type: "address" }],
47
+ outputs: [{ internalType: "bool", name: "", type: "bool" }]
48
+ },
49
+ contractAddress: poolAddress,
50
+ chain: chainId,
51
+ returnValueTest: { comparator: "==", value: true }
52
+ };
53
+ }
54
+ export async function encryptWithTaco(params) {
55
+ const { poolAddress, payload } = params;
56
+ if (!poolAddress)
57
+ throw new Error("encryptWithTaco: poolAddress is required");
58
+ const require = createRequire(import.meta.url);
59
+ const taco = require("@nucypher/taco");
60
+ const { initialize, encrypt, domains, conditions } = taco;
61
+ if (!initialize || !encrypt || !domains || !conditions) {
62
+ throw new Error("encryptWithTaco: TACo SDK exports missing");
63
+ }
64
+ const ContractCondition = conditions?.base?.contract?.ContractCondition;
65
+ if (!ContractCondition) {
66
+ throw new Error("encryptWithTaco: ContractCondition not available");
67
+ }
68
+ const { key, dkg, condition, conditionChain, ritual } = resolveTacoConfig(params);
69
+ await initialize();
70
+ const dkgProvider = new providers.JsonRpcProvider(dkg);
71
+ const conditionProvider = new providers.JsonRpcProvider(condition);
72
+ const encryptorSigner = await resolveConditionSigner(conditionProvider, key);
73
+ const conditionInstance = new ContractCondition({
74
+ method: "canDecrypt",
75
+ parameters: [":contributor"],
76
+ functionAbi: buildTacoCondition(poolAddress, conditionChain).functionAbi,
77
+ contractAddress: poolAddress,
78
+ chain: conditionChain,
79
+ returnValueTest: { comparator: "==", value: true }
80
+ });
81
+ const payloadBytes = payload instanceof Uint8Array ? payload : encodePayload(payload);
82
+ const kit = await encrypt(dkgProvider, domains.TESTNET || domains.tapir, payloadBytes, conditionInstance, ritual, encryptorSigner);
83
+ if (typeof kit === "string")
84
+ return kit;
85
+ if (kit?.toBytes)
86
+ return toHexString(kit.toBytes());
87
+ if (kit?.toString)
88
+ return kit.toString();
89
+ return JSON.stringify(kit);
90
+ }
91
+ export async function decryptWithTaco(params) {
92
+ const { messageKit, contributorAddress } = params;
93
+ if (!messageKit)
94
+ throw new Error("decryptWithTaco: messageKit is required");
95
+ const require = createRequire(import.meta.url);
96
+ const taco = require("@nucypher/taco");
97
+ const { initialize, decrypt, domains, conditions, ThresholdMessageKit } = taco;
98
+ if (!initialize || !decrypt || !domains || !conditions || !ThresholdMessageKit) {
99
+ throw new Error("decryptWithTaco: TACo SDK exports missing");
100
+ }
101
+ const ConditionContext = conditions?.context?.ConditionContext;
102
+ if (!ConditionContext)
103
+ throw new Error("decryptWithTaco: ConditionContext missing");
104
+ const { key, condition, conditionChain, ritual, dkg } = resolveTacoConfig(params);
105
+ await initialize();
106
+ const conditionProvider = new providers.JsonRpcProvider(condition);
107
+ const decryptorSigner = await resolveConditionSigner(conditionProvider, key);
108
+ const decryptorAddress = await decryptorSigner.getAddress();
109
+ const kitBytes = Buffer.from(messageKit.replace(/^0x/, ""), "hex");
110
+ const kit = ThresholdMessageKit.fromBytes(Uint8Array.from(kitBytes));
111
+ const context = ConditionContext.fromMessageKit(kit);
112
+ context.addCustomContextParameterValues({
113
+ ":contributor": contributorAddress ? utils.getAddress(contributorAddress) : decryptorAddress
114
+ });
115
+ const decryptedBytes = await decrypt(conditionProvider, domains.TESTNET || domains.tapir, kit, context, domains.TESTNET?.porterUris);
116
+ return new TextDecoder().decode(decryptedBytes);
117
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@whistlex/sdk",
3
+ "version": "0.1.0",
4
+ "description": "WhistleX SDK for local encryption, TACo wrapping, and on-chain pool calldata",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "clean": "rm -rf dist"
18
+ },
19
+ "dependencies": {
20
+ "@nucypher/taco": "0.6.0",
21
+ "ethers": "^5.7.2"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^20.11.30",
25
+ "typescript": "^5.3.3"
26
+ }
27
+ }