Skip to content

Commit cf65c23

Browse files
docs: agent instructions and llms.txt for AI discoverability
1 parent 68a381a commit cf65c23

5 files changed

Lines changed: 282 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# AGENTS.md — private-equality
2+
3+
Instructions in this file apply to the entire repository.
4+
5+
## Project Summary
6+
7+
`@forgesworn/private-equality` decides "do two parties hold the same secret —
8+
yes/no?" via the Socialist Millionaires' Protocol (OTR variant) over
9+
Ristretto255, revealing nothing else on mismatch. Pure four-message state
10+
machine; the consumer supplies the authenticated channel and ferries opaque
11+
bytes.
12+
13+
## Commands
14+
15+
- Build: `npm run build`
16+
- Test: `npm test`
17+
- Coverage gate: `npm run coverage`
18+
- Typecheck: `npm run typecheck`
19+
20+
## Rules
21+
22+
- British English in all prose. ESM-only; imports use `.js` extensions.
23+
- Commits: `type: description`. No `Co-Authored-By` lines.
24+
- Do not modify domain separators, tag bytes, the `sessionBinding` folding,
25+
identity-point rejection, or the zero-scalar guard without crypto review —
26+
see the "Crypto Safety" section of `CLAUDE.md`.
27+
- Bump `PRIVATE_EQUALITY_VERSION` (`src/types.ts`) together with
28+
`package.json` and add a `CHANGELOG.md` entry for any release.
29+
- Anvil auto-releases on push to main — work on branches.

CLAUDE.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# CLAUDE.md — @forgesworn/private-equality
2+
3+
AI agent instructions for working on this codebase.
4+
5+
## Build & Test
6+
7+
```bash
8+
npm run build # tsc → dist/
9+
npm test # vitest (single run)
10+
npm run coverage # vitest with the per-file coverage gate
11+
npm run typecheck # tsc --noEmit
12+
```
13+
14+
Always run `npm test` after changes. Always run `npm run typecheck` before committing.
15+
16+
## Architecture
17+
18+
| File | Purpose |
19+
|------|---------|
20+
| `src/group.ts` | Isolates `@noble/curves` ristretto255 — the ONLY file importing it. Point/scalar codecs, `hashToScalar`, `randomScalar`. `Pt` is a structural interface (dodges TS4094 on @noble's anonymous class). |
21+
| `src/zkp.ts` | Three sigma proofs: PoK of a discrete log, representation (`P = g3^r`, `Q = g1^r · g2^y`), equality of two discrete logs. All Fiat–Shamir with domain separation + per-proof tag byte. |
22+
| `src/smp.ts` | The four-message OTR-SMP state machine + fixed-layout wire codec. `initiate`/`respond` entry points. |
23+
| `src/types.ts` | Public types: `Secret`, `SmpResult`, `SmpStep`, `SmpSession`, `SmpError`, `PRIVATE_EQUALITY_VERSION`. |
24+
| `src/index.ts` | Public API re-exports. |
25+
26+
### Data flow
27+
28+
Pure state machine — no transport. The consumer ferries opaque `Uint8Array`
29+
messages over an authenticated channel: `initiate(secret, binding)`
30+
`{ session, first }`; each side calls `session.next(incoming)` until
31+
`{ done: true, result: { match } }`. Four messages total.
32+
33+
## Crypto Safety — Do Not Change Without Expert Review
34+
35+
- **Domain separators** (`private-equality/pok-v1`, `private-equality/repr-v1`,
36+
`private-equality/eq-v1`, `private-equality/secret-v1`) are protocol
37+
constants. Changing them breaks interoperability between versions.
38+
- **Per-proof tag bytes** separate the distinct proofs within a session. Do not
39+
renumber or merge them — two proofs sharing a challenge domain enables proof
40+
transplantation.
41+
- **`sessionBinding` is folded into every Fiat–Shamir challenge.** Never make it
42+
optional and never drop it from a challenge — it is the MITM defence.
43+
- **Identity points are rejected on the wire** (decode-time). Do not relax this —
44+
it closes the degenerate `QaQb = identity` case.
45+
- **The zero secret-scalar guard** in secret derivation is intentional. Do not
46+
remove it.
47+
- **The wire codec is fixed-layout** (32-byte points and scalars, strict length
48+
checks). Do not add variable-length fields without redesigning the codec.
49+
- **`PRIVATE_EQUALITY_VERSION` in `src/types.ts` is pinned by
50+
`src/smoke.test.ts` as a literal string.** Anvil bumps `package.json` from
51+
commit prefixes; the constant does not update itself. Any releasing change
52+
(`feat:`/`fix:`) must update `src/types.ts` and `src/smoke.test.ts` to the
53+
new version in the same PR.
54+
55+
## Conventions
56+
57+
- **British English** in all prose (colour, initialise, behaviour, licence).
58+
- **ESM-only** — all imports use `.js` extensions.
59+
- **Commit messages**: `type: description` (e.g. `feat: add subpath export`, `fix: reject oversized message`).
60+
- **No `Co-Authored-By`** lines in commits.
61+
- **Anvil auto-release on main** — every push to main runs `forgesworn/anvil@v0` and can auto-publish. Work on branches; merge to main only when a logical chunk is complete.
62+
63+
## Release & Versioning
64+
65+
**Automated via [forgesworn/anvil](https://github.com/forgesworn/anvil)**
66+
`auto-release.yml` reads conventional commits on push to `main`, bumps the
67+
version, and creates a GitHub Release; `release.yml` then runs the pre-publish
68+
gates and publishes to npm via OIDC trusted publishing.
69+
70+
| Type | Version Bump |
71+
|------|--------------|
72+
| `fix:` | Patch (0.1.x) |
73+
| `feat:` | Minor (0.x.0) |
74+
| `BREAKING CHANGE:` (in commit body) | Major (x.0.0) |
75+
| `chore:`, `docs:`, `refactor:`, `ci:` | None |
76+
77+
Every release needs a matching `CHANGELOG.md` entry (anvil extracts the release
78+
body from it) and a synchronised `PRIVATE_EQUALITY_VERSION`.
79+
80+
## Testing
81+
82+
Tests live in `src/*.test.ts`. Vitest with a per-file coverage gate
83+
(`vitest.config.ts`). Coverage floor: `group`/`zkp` at 100%, `smp` at 100%
84+
lines / 87% branch. Tests cover the full protocol round-trip, mismatch
85+
non-leakage, binding mismatch aborts, malformed/replayed messages, and
86+
tampered proofs. PRs touching `zkp.ts` or `smp.ts` require crypto review.

GEMINI.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# GEMINI.md — private-equality
2+
3+
Private equality of a secret, revealing one bit — Socialist Millionaires'
4+
Protocol over Ristretto255.
5+
6+
## Commands
7+
8+
- `npm run build` / `npm test` / `npm run coverage` / `npm run typecheck`
9+
10+
## Key facts
11+
12+
- Pure state machine: `initiate`/`respond``session.next(incoming)` → one
13+
boolean. Four messages, ferried by the consumer over an authenticated channel.
14+
- `sessionBinding` (the channel transcript hash) is mandatory and folded into
15+
every Fiat–Shamir challenge — it is the MITM defence.
16+
- Crypto-sensitive files: `src/zkp.ts`, `src/smp.ts`, `src/group.ts`. Do not
17+
change constants or validation there without expert review (see `CLAUDE.md`).
18+
- British English, ESM-only, `type: description` commits, no `Co-Authored-By`.

llms-full.txt

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# @forgesworn/private-equality
2+
3+
> Decide "do two parties hold the same secret — yes/no?" via the Socialist Millionaires' Protocol (OTR variant) over Ristretto255, revealing nothing else on mismatch.
4+
5+
## Getting Started
6+
7+
Install from npm:
8+
9+
```bash
10+
npm install @forgesworn/private-equality
11+
```
12+
13+
The library is ESM-only with a single entry point:
14+
15+
```typescript
16+
import { initiate, respond, SmpError } from '@forgesworn/private-equality'
17+
```
18+
19+
It is a pure state machine — no transport. You ferry opaque `Uint8Array`
20+
messages over YOUR authenticated channel (e.g. Noise), and you MUST supply a
21+
`sessionBinding` — that channel's transcript hash — identically on both sides.
22+
23+
## Core flow (four messages)
24+
25+
```typescript
26+
const binding = /* your authenticated channel's transcript hash */
27+
const alice = initiate(secret, binding) // { session, first }
28+
const bob = respond(secret, binding) // { session }
29+
30+
const s1 = bob.session.next(alice.first) // { send }
31+
const s2 = alice.session.next(s1.send!) // { send }
32+
const s3 = bob.session.next(s2.send!) // { send, done: true, result }
33+
const s4 = alice.session.next(s3.send!) // { done: true, result }
34+
// s3.result.match === s4.result.match — the single revealed bit
35+
```
36+
37+
## API
38+
39+
- `initiate(secret: Uint8Array | string, sessionBinding: Uint8Array)` → `{ session: SmpSession; first: Uint8Array }`
40+
- `respond(secret: Uint8Array | string, sessionBinding: Uint8Array)` → `{ session: SmpSession }`
41+
- `SmpSession.next(incoming: Uint8Array)` → `{ send: Uint8Array }` or `{ send?: Uint8Array; done: true; result: { match: boolean } }`
42+
- `SmpError` — thrown on invalid proof, malformed message, wrong-state call, or binding mismatch
43+
- `PRIVATE_EQUALITY_VERSION` — library version string
44+
45+
## Security notes
46+
47+
- On mismatch, neither side learns anything beyond "not equal".
48+
- Requires an authenticated channel; the binding defeats cross-channel relays.
49+
- Standard published construction (OTR SMP); NOT third-party audited — no
50+
production privacy claims until audited.
51+
- DDH-based (Ristretto255) — not post-quantum.
52+
53+
## Types
54+
55+
```typescript
56+
type Secret = Uint8Array | string // hashed to a scalar internally
57+
interface SmpResult { match: boolean } // the one revealed bit
58+
type SmpStep =
59+
| { send: Uint8Array; done?: false } // intermediate
60+
| { send?: Uint8Array; done: true; result: SmpResult } // terminal
61+
interface SmpSession { next(incoming: Uint8Array): SmpStep }
62+
class SmpError extends Error {}
63+
```
64+
65+
## Protocol detail
66+
67+
The OTR variant of the Socialist Millionaires' Protocol, ported to the
68+
Ristretto255 prime-order group. Four fixed-layout messages; every message
69+
carries sigma proofs (proof-of-knowledge of a discrete log, a representation
70+
proof, and equality-of-two-discrete-logs) verified before the state machine
71+
advances. All Fiat–Shamir challenges are domain-separated
72+
(`private-equality/pok-v1`, `.../repr-v1`, `.../eq-v1`, `.../secret-v1` for
73+
secret derivation) with a per-proof tag byte, and fold in the `sessionBinding`.
74+
75+
Wire format: fixed-layout concatenation of 32-byte Ristretto255 point and
76+
scalar encodings with strict length checks. Identity points are rejected on
77+
decode. Message sizes are deterministic per step.
78+
79+
## Error handling
80+
81+
All failures throw `SmpError`: invalid or transplanted proofs, malformed or
82+
wrong-length messages, out-of-order `next()` calls, and mismatched
83+
`sessionBinding` values (the MITM case). Treat any `SmpError` as a protocol
84+
abort — do not retry within the same session; start a fresh one.
85+
86+
## What the consumer must provide
87+
88+
1. **The secret derivation** — a canonical byte encoding of the value being
89+
compared (the library hashes it to a scalar with a domain-separated hash).
90+
2. **An authenticated channel** — e.g. a Noise session; the library never
91+
opens sockets.
92+
3. **The `sessionBinding`** — the channel's transcript hash, identical on both
93+
sides. Mismatch aborts by design.
94+
95+
## Dependencies
96+
97+
`@noble/curves` (ristretto255) and `@noble/hashes` only. `@noble/curves` is
98+
isolated behind `src/group.ts`.

llms.txt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# @forgesworn/private-equality
2+
3+
> Decide "do two parties hold the same secret — yes/no?" via the Socialist Millionaires' Protocol (OTR variant) over Ristretto255, revealing nothing else on mismatch.
4+
5+
## Getting Started
6+
7+
Install from npm:
8+
9+
```bash
10+
npm install @forgesworn/private-equality
11+
```
12+
13+
The library is ESM-only with a single entry point:
14+
15+
```typescript
16+
import { initiate, respond, SmpError } from '@forgesworn/private-equality'
17+
```
18+
19+
It is a pure state machine — no transport. You ferry opaque `Uint8Array`
20+
messages over YOUR authenticated channel (e.g. Noise), and you MUST supply a
21+
`sessionBinding` — that channel's transcript hash — identically on both sides.
22+
23+
## Core flow (four messages)
24+
25+
```typescript
26+
const binding = /* your authenticated channel's transcript hash */
27+
const alice = initiate(secret, binding) // { session, first }
28+
const bob = respond(secret, binding) // { session }
29+
30+
const s1 = bob.session.next(alice.first) // { send }
31+
const s2 = alice.session.next(s1.send!) // { send }
32+
const s3 = bob.session.next(s2.send!) // { send, done: true, result }
33+
const s4 = alice.session.next(s3.send!) // { done: true, result }
34+
// s3.result.match === s4.result.match — the single revealed bit
35+
```
36+
37+
## API
38+
39+
- `initiate(secret: Uint8Array | string, sessionBinding: Uint8Array)` → `{ session: SmpSession; first: Uint8Array }`
40+
- `respond(secret: Uint8Array | string, sessionBinding: Uint8Array)` → `{ session: SmpSession }`
41+
- `SmpSession.next(incoming: Uint8Array)` → `{ send: Uint8Array }` or `{ send?: Uint8Array; done: true; result: { match: boolean } }`
42+
- `SmpError` — thrown on invalid proof, malformed message, wrong-state call, or binding mismatch
43+
- `PRIVATE_EQUALITY_VERSION` — library version string
44+
45+
## Security notes
46+
47+
- On mismatch, neither side learns anything beyond "not equal".
48+
- Requires an authenticated channel; the binding defeats cross-channel relays.
49+
- Standard published construction (OTR SMP); NOT third-party audited — no
50+
production privacy claims until audited.
51+
- DDH-based (Ristretto255) — not post-quantum.

0 commit comments

Comments
 (0)