Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Architecture Decisions

## ADR-001: AnonVoteClient SDK — Subpath Export (Option B)

**Status:** Accepted
**Date:** 2026-07-28

### Context

`@anonvote/crypto` exports five low-level cryptographic primitives. A higher-level
`AnonVoteClient` SDK needed to be added. Two placement options were considered:

**Option A** — Add `src/client.ts` to the existing package and export `AnonVoteClient`
alongside the primitives from `src/index.ts`. One package, one entry point.

**Option B** — Create `src/client/` with its own entry point and expose it as the
subpath export `@anonvote/crypto/client`. Primitives and client are imported separately.

### Decision

**Option B — subpath export** was chosen.

Rationale:

- **Tree-shaking.** Consumers who only need the raw primitives (`encryptVote`,
`hashToken`, etc.) do not pay the cost of importing the client code. The subpath
makes the import graph explicit.
- **Separation of concerns.** The SDK layer has different stability guarantees and
a different change cadence than the primitives. A separate entry point makes that
boundary clear.
- **Node.js 12+ subpath exports** are already a standard pattern and the package is
already in a TypeScript + CommonJS configuration that supports them with no extra
tooling.
- **Explicit API surface.** Developers importing `@anonvote/crypto/client` signal
intent — they want the SDK, not just the primitives.

### Consequences

`package.json` gains an `exports` field:

```json
{
"exports": {
".": "./dist/index.js",
"./client": "./dist/client/index.js"
}
}
```

`tsconfig.json` `include` must cover `src/client/`.

New files created:
- `src/client/types.ts` — domain-level SDK types
- `src/client/index.ts` — `AnonVoteClient` class

The existing `src/client.ts` (lower-level, retry-focused) is preserved and continues
to be exported from the root entry point. The new `src/client/index.ts` is the
developer-facing SDK.
67 changes: 46 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,35 +75,54 @@ const optionId = decryptVote(encrypted, BALLOT_KEY);

---

## Usage: AnonVoteClient
## Usage: AnonVoteClient SDK

The AnonVoteClient SDK is the recommended way to integrate AnonVote into your application. It lives at the `@anonvote/crypto/client` subpath so consumers of only the raw primitives don't pay the import cost.

```bash
npm install @anonvote/crypto
```

```typescript
import { AnonVoteClient } from "@anonvote/crypto";
import { randomBytes } from "crypto";
import { AnonVoteClient } from "@anonvote/crypto/client";

const client = new AnonVoteClient({
encryptionKey: process.env.BALLOT_ENCRYPTION_KEY!,
});
// Generate a fresh key per ballot — never reuse across ballots
const ballotKey = randomBytes(32).toString("hex");

// Create an election
const client = new AnonVoteClient({ ballotKey });

// 1. Create an election (pure client-side, no network)
const election = client.createElection({
title: "Board Election 2024",
description: "Elect the new board members",
options: ["Alice", "Bob", "Charlie"],
startTime: Date.now(),
endTime: Date.now() + 7 * 24 * 60 * 60 * 1000,
title: "Board Election 2026",
description: "Elect two new board members.",
options: ["Alice", "Bob", "Abstain"],
startTime: new Date(),
endTime: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
});

// Cast a vote using the election returned above
const receipt = client.castVote({
ballotId: election.id,
voteOption: election.options[0].text,
});
// 2. Cast a vote — pass the option UUID, not the label
const ballot = client.castVote(election, election.options[0].id);

// 3. Verify locally before submitting
const result = client.verifyVote(ballot);
console.log(result.confirmed); // true

// Verify the receipt returned by castVote
const isValid = client.verifyVote(receipt.encryptedPayload);
console.log(isValid); // true
// 4. Serialize for server submission — optionId is intentionally excluded
const json = client.serialize(ballot);
await fetch("/api/votes", { method: "POST", body: json });

// 5. Deserialize a stored ballot
const restored = client.deserialize(json);
```

### Key guarantees

- The constructor throws immediately if `ballotKey` is not a valid 64-character hex string — misconfigured clients fail at construction, not at the first crypto operation.
- `castVote` never logs the `optionId`. The option the voter chose stays local.
- `serialize` omits `optionId` — only the encrypted payload reaches the server.
- `verifyVote` propagates decryption errors rather than silently returning `false`. A corrupted payload is a different failure mode from an option mismatch.

---

## Environment variables
Expand Down Expand Up @@ -249,15 +268,21 @@ The `no-console` rule is enforced as an error. If lint flags a `console.*` in `s
```
js/
├── src/
│ ├── crypto.ts # Core cryptographic functions
│ ├── client.ts # AnonVoteClient SDK
│ ├── client/
│ │ ├── index.ts # AnonVoteClient SDK (@anonvote/crypto/client)
│ │ └── types.ts # Domain-level SDK types
│ ├── crypto.ts # Core cryptographic primitives
│ ├── client.ts # Low-level retry-aware client (root export)
│ ├── errors.ts # Error classes
│ ├── retry.ts # Exponential backoff retry utility
│ ├── types.ts # Shared TypeScript types
│ └── index.ts # Public API re-exports
├── tests/
│ ├── crypto.test.ts
│ ├── client.test.ts
│ ├── sdk-client.test.ts # AnonVoteClient SDK tests (issue #42)
│ └── errors.test.ts
├── DECISIONS.md # Architecture decision records
├── package.json
└── tsconfig.json
```
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
"import": "./dist/index.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./client": {
"import": "./dist/client/index.js",
"require": "./dist/client/index.js",
"types": "./dist/client/index.d.ts"
}
},
"files": [
Expand Down
Loading
Loading