npm package: @prxvt/hook
Description: Privacy layer for the Agentic Commerce Protocol (ERC-8183) — AES-256-GCM encryption, ECDH key wrapping, ZK proof generation, and on-chain envelope construction for PrivacyHook
Latest version: 0.2.0

# @prxvt/hook

Privacy SDK for [ERC-8183](https://eips.ethereum.org/EIPS/eip-8183) PrivacyHook. Handles AES-256-GCM encryption, ECDH key wrapping, ZK proof generation, IPFS upload, and on-chain envelope construction.

## Install

```bash
npm install @prxvt/hook ethers
```

## Quick Start

### Provider: Encrypt and Submit

```ts
import {
  encryptAndPrepareSubmission,
  buildConfigParams,
  PinataAdapter,
} from "@prxvt/hook";

// 1. Configure privacy on-chain (at setBudget time)
const configParams = buildConfigParams(
  "0x0000000000000000000000000000000000000000", // no ZK verifier
  2 // require 2 wrapped keys (client + evaluator)
);
await core.connect(provider).setBudget(jobId, budget, configParams);

// 2. Encrypt and prepare submission
const ipfs = new PinataAdapter({ apiKey: "...", secretApiKey: "..." });
const plaintext = new TextEncoder().encode("Private job output");

const { deliverable, optParams } = await encryptAndPrepareSubmission(
  plaintext,
  [
    { publicKey: clientPubKey, label: "client" },
    { publicKey: evaluatorPubKey, label: "evaluator" },
  ],
  ipfs
);

// 3. Submit on-chain
await core.connect(provider).submit(jobId, deliverable, optParams);
```

### Recipient: Fetch and Decrypt

```ts
import { fetchAndDecrypt, PinataAdapter } from "@prxvt/hook";

const ipfs = new PinataAdapter({ apiKey: "...", secretApiKey: "..." });

const plaintext = await fetchAndDecrypt(
  cid,                // IPFS CID from EncryptedSubmission event
  wrappedKey,         // your wrapped key entry from the event
  recipientPrivateKey,
  ipfs
);
```

### Standalone Key Wrapping

```ts
import { wrapKey, unwrapKey } from "@prxvt/hook";

// Wrap a data key for a recipient
const wrappedKey = wrapKey(dataKey, recipientPublicKey);

// Unwrap using the recipient's private key
const recoveredKey = unwrapKey(wrappedKey, recipientPrivateKey);
```

### ZK Proof Generation

```ts
import { generateGroth16Proof, buildConfigParams } from "@prxvt/hook";

// Configure with ZK verifier and 3 public inputs (for selective disclosure)
const configParams = buildConfigParams(zkVerifierAddress, 2, 3);

// Generate proof
const proof = await generateGroth16Proof(secretPreimage, jobId, {
  wasmPath: "./circuits/build/privacy_preimage_js/privacy_preimage.wasm",
  zkeyPath: "./circuits/build/privacy_preimage_final.zkey",
});
```

## API Reference

### Encryption

| Function | Description |
|----------|-------------|
| `encryptForRecipients(plaintext, recipients)` | Encrypt data with AES-256-GCM, wrap key per recipient via ECDH |
| `decryptWithPrivateKey(ciphertext, wrappedKey, privateKey)` | Unwrap key and decrypt ciphertext |

### Key Wrapping

| Function | Description |
|----------|-------------|
| `wrapKey(dataKey, recipientPubKey, ephemeralPrivKey?)` | Wrap a 32-byte key for a single recipient (returns 94-byte v1 key) |
| `unwrapKey(wrappedKey, recipientPrivateKey)` | Unwrap a v1 wrapped key to recover the data key |

### Envelope Construction

| Function | Description |
|----------|-------------|
| `buildConfigParams(zkVerifier, minWrappedKeys, numPublicInputs?)` | ABI-encode setBudget optParams |
| `buildEnvelopeParams(cid, wrappedKeys, zkProof?)` | ABI-encode submit optParams |
| `bytesToHex(bytes)` | Convert Uint8Array to 0x-prefixed hex string |

### CID Utilities

| Function | Description |
|----------|-------------|
| `cidToBytes32(cid)` | Convert IPFS CID string to bytes32 (keccak256) |
| `verifyCidMatch(cid, deliverable)` | Check CID maps to expected deliverable hash |

### High-Level

| Function | Description |
|----------|-------------|
| `encryptAndPrepareSubmission(plaintext, recipients, ipfs, zkProof?)` | Full flow: encrypt → IPFS upload → build submit params |
| `fetchAndDecrypt(cid, wrappedKey, privateKey, ipfs)` | Full flow: IPFS download → decrypt |

### ZK Proving

| Function | Description |
|----------|-------------|
| `generateGroth16Proof(preimage, jobId, options)` | Generate Groth16 proof for privacy_preimage circuit |
| `poseidonHash(preimage)` | Compute Poseidon hash matching the circuit |
| `formatProofForChain(snarkjsProof)` | ABI-encode proof for on-chain submission |

### IPFS Adapters

| Adapter | Description |
|---------|-------------|
| `PinataAdapter({ apiKey, secretApiKey, gateway? })` | Pinata pinning service |
| `LocalNodeAdapter({ url? })` | Local IPFS node (Kubo HTTP API, default `localhost:5001`) |

Implement `IpfsAdapter` interface for custom backends:

```ts
interface IpfsAdapter {
  upload(data: Uint8Array): Promise<string>;   // returns CID
  download(cid: string): Promise<Uint8Array>;
}
```

### Constants

| Constant | Value | Description |
|----------|-------|-------------|
| `WRAPPED_KEY_VERSION` | `0x01` | Current wrapped key format version |
| `WRAPPED_KEY_V1_LENGTH` | `94` | Byte length of a v1 wrapped key |

## Wrapped Key Format (v1)

Each recipient gets a 94-byte wrapped key entry:

```
Offset  Length  Field
──────  ──────  ────────────────────────────
0       1       Version byte (0x01)
1       33      Ephemeral secp256k1 public key (compressed)
34      12      AES-GCM wrapping nonce
46      48      Encrypted AES key (32) + GCM auth tag (16)
──────  ──────  ────────────────────────────
Total   94 bytes
```

### Key Derivation

1. Generate ephemeral secp256k1 key pair (one per envelope)
2. For each recipient's public key:
   - Compute ECDH shared secret
   - Derive 256-bit wrapping key via HKDF-SHA256 (info=`"erc8183-privacy-wrap"`)
   - Encrypt AES data key with AES-256-GCM using the wrapping key

### Versioning

The version byte enables future format evolution without breaking existing integrations. The on-chain PrivacyHook validates both the length (94 bytes) and version byte (0x01) of each wrapped key.

## Crypto Details

| Component | Algorithm |
|-----------|-----------|
| Data encryption | AES-256-GCM (random 96-bit nonce) |
| Key wrapping | ECDH on secp256k1 → HKDF-SHA256 → AES-256-GCM |
| Key derivation | HKDF-SHA256 with info = `"erc8183-privacy-wrap"` |
| Ephemeral keys | One secp256k1 key pair per envelope |
| ZK proofs | Groth16 on BN128 (snarkjs) |

### Ciphertext Format

```
dataNonce (12) || AES-GCM ciphertext || authTag (16)
```