@taceo/oprf-client 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TACEO
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,244 @@
1
+ # @taceo/oprf-client
2
+
3
+ WebSocket client for distributed threshold OPRF over a network of TACEO service nodes.
4
+
5
+ This package provides high-level APIs for interacting with threshold OPRF services, handling the full protocol flow including session management, challenge generation, proof verification, and output finalization.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pnpm add @taceo/oprf-client
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Full Distributed OPRF
16
+
17
+ Services must be pre-built WebSocket URLs. Use `toOprfUri` to construct them from base URLs.
18
+
19
+ ```ts
20
+ import { distributedOprf, toOprfUri } from '@taceo/oprf-client';
21
+
22
+ const bases = [
23
+ 'http://node1.example.com',
24
+ 'http://node2.example.com',
25
+ 'http://node3.example.com',
26
+ ];
27
+
28
+ const services = bases.map((s) => toOprfUri(s, 'my-module'));
29
+
30
+ const result = await distributedOprf(
31
+ services,
32
+ 2, // threshold
33
+ 12345n, // query
34
+ 0n, // domain separator
35
+ { auth: { api_key: 'secret' } }
36
+ );
37
+
38
+ console.log(result.output); // OPRF output (bigint)
39
+ console.log(result.dlogProof); // Chaum-Pedersen proof
40
+ console.log(result.epoch); // Key epoch from servers
41
+ ```
42
+
43
+ ### Building Service URLs
44
+
45
+ ```ts
46
+ import { toOprfUri } from '@taceo/oprf-client';
47
+
48
+ // http → ws, https → wss; appends /api/{module}/oprf?version={protocolVersion}
49
+ const url = toOprfUri('https://node1.example.com', 'my-module');
50
+ // → 'wss://node1.example.com/api/my-module/oprf?version=1.0.0'
51
+
52
+ // Custom protocol version
53
+ const urlV2 = toOprfUri('https://node1.example.com', 'my-module', '2.0.0');
54
+ ```
55
+
56
+ ### Step-by-Step Protocol
57
+
58
+ ```ts
59
+ import {
60
+ initSessions,
61
+ finishSessions,
62
+ generateChallengeRequest,
63
+ verifyDlogEquality,
64
+ toOprfUri,
65
+ } from '@taceo/oprf-client';
66
+ import {
67
+ blindQuery,
68
+ unblindResponse,
69
+ finalizeOutput,
70
+ randomBlindingFactor,
71
+ prepareBlindingFactor,
72
+ } from '@taceo/oprf-core';
73
+
74
+ const services = bases.map((s) => toOprfUri(s, module));
75
+
76
+ // 1. Blind the query
77
+ const beta = randomBlindingFactor();
78
+ const blindedRequest = blindQuery(query, beta);
79
+
80
+ // 2. Initialize sessions with service nodes
81
+ const sessions = await initSessions(services, threshold, {
82
+ request_id: crypto.randomUUID(),
83
+ blinded_query: blindedRequest,
84
+ auth: {},
85
+ });
86
+
87
+ // 3. Generate challenge from commitments
88
+ const challenge = generateChallengeRequest(sessions);
89
+
90
+ // 4. Finish sessions to get proof shares
91
+ const proofShares = await finishSessions(sessions, challenge);
92
+
93
+ // 5. Verify the combined DLog proof
94
+ const proof = verifyDlogEquality(
95
+ requestId,
96
+ sessions.oprfPublicKeys[0],
97
+ blindedRequest,
98
+ proofShares,
99
+ challenge
100
+ );
101
+
102
+ // 6. Unblind and finalize
103
+ const blindedResponse = challenge.blindedResponse();
104
+ const unblinded = unblindResponse(blindedResponse, prepareBlindingFactor(beta));
105
+ const output = finalizeOutput(domainSeparator, query, unblinded);
106
+ ```
107
+
108
+ ## API
109
+
110
+ ### Main Functions
111
+
112
+ - **`distributedOprf(services, threshold, query, domainSeparator, options?): Promise<VerifiableOprfOutput>`**
113
+
114
+ End-to-end distributed OPRF: blind → init sessions → challenge → finish → verify → unblind → finalize. Services must be pre-built WS URLs (use `toOprfUri`).
115
+
116
+ - **`toOprfUri(service, auth, protocolVersion?): string`**
117
+
118
+ Build a WebSocket URL for a single OPRF service. Converts `http://` → `ws://`, `https://` → `wss://`. Appends `/api/{auth}/oprf?version={protocolVersion}`.
119
+
120
+ - **`initSessions(services, threshold, request): Promise<OprfSessions>`**
121
+
122
+ Connect to service nodes via WebSocket (pre-built WS URLs), send blinded query, receive commitments. On threshold failure throws `NodeError[]` (use `aggregateError` to convert).
123
+
124
+ - **`finishSessions(sessions, challenge): Promise<DLogProofShareShamir[]>`**
125
+
126
+ Send challenge to nodes, receive proof shares.
127
+
128
+ - **`generateChallengeRequest(sessions): DLogCommitmentsShamir`**
129
+
130
+ Combine commitments from sessions into a challenge request.
131
+
132
+ - **`verifyDlogEquality(requestId, publicKey, blindedRequest, proofShares, challenge): DLogEqualityProof`**
133
+
134
+ Combine proof shares and verify the DLog equality proof.
135
+
136
+ - **`aggregateError(threshold, errors): OprfClientError`**
137
+
138
+ Aggregate an array of `NodeError` into a single protocol-level `OprfClientError`.
139
+
140
+ ### Types
141
+
142
+ ```ts
143
+ interface VerifiableOprfOutput {
144
+ output: bigint; // Final OPRF output
145
+ dlogProof: DLogEqualityProof; // Combined Chaum-Pedersen proof
146
+ blindedRequest: AffinePoint<bigint>; // Client's blinded query
147
+ blindedResponse: AffinePoint<bigint>; // Combined blinded response
148
+ unblindedResponse: AffinePoint<bigint>;
149
+ oprfPublicKey: AffinePoint<bigint>; // Service public key
150
+ epoch: number; // Key epoch
151
+ }
152
+
153
+ interface DistributedOprfOptions<Auth = unknown> {
154
+ auth?: Auth; // Auth payload for OprfRequest
155
+ }
156
+ ```
157
+
158
+ ### Error Handling
159
+
160
+ The library uses a two-tier error model:
161
+
162
+ - **`NodeError`** — per-node error (WebSocket / service level)
163
+ - **`OprfClientError`** — protocol-level error (aggregated or logical)
164
+
165
+ ```ts
166
+ import {
167
+ OprfClientError,
168
+ isOprfClientError,
169
+ NodeError,
170
+ isNodeError,
171
+ ServiceError,
172
+ } from '@taceo/oprf-client';
173
+
174
+ try {
175
+ const result = await distributedOprf(...);
176
+ } catch (err) {
177
+ if (isOprfClientError(err)) {
178
+ switch (err.code) {
179
+ case 'NonUniqueServices':
180
+ // Duplicate service URLs provided
181
+ break;
182
+ case 'ThresholdServiceError':
183
+ // >= threshold nodes returned the same application-level error
184
+ console.log(err.details?.serviceError?.errorCode);
185
+ break;
186
+ case 'Networking':
187
+ // >= threshold nodes had WebSocket / networking errors
188
+ console.log(err.details?.networkingErrors);
189
+ break;
190
+ case 'UnexpectedMessage':
191
+ // >= threshold nodes reported unexpected message format
192
+ break;
193
+ case 'InvalidDLogProof':
194
+ // Proof verification failed
195
+ break;
196
+ case 'InconsistentOprfPublicKeys':
197
+ // Nodes returned different public keys
198
+ break;
199
+ case 'CannotFinishSession':
200
+ // Failed to finish a session with a node
201
+ break;
202
+ case 'NodeErrorDisagreement':
203
+ // Nodes returned differing errors — no consensus reached
204
+ console.log(err.details?.nodeErrors);
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ ```
210
+
211
+ ### Error Codes
212
+
213
+ #### `OprfClientErrorCode` (protocol-level)
214
+
215
+ | Code | Description |
216
+ | ---------------------------- | ----------------------------------------------------- |
217
+ | `NonUniqueServices` | Duplicate service URLs provided |
218
+ | `ThresholdServiceError` | ≥ threshold nodes returned the same application error |
219
+ | `Networking` | ≥ threshold nodes had WebSocket / networking errors |
220
+ | `UnexpectedMessage` | ≥ threshold nodes reported unexpected message format |
221
+ | `InvalidDLogProof` | DLog proof verification failed |
222
+ | `InconsistentOprfPublicKeys` | Nodes returned different public keys |
223
+ | `CannotFinishSession` | Failed to finish session after init |
224
+ | `NodeErrorDisagreement` | Nodes returned differing errors, no consensus |
225
+ | `Unknown` | Unexpected error |
226
+
227
+ #### `NodeErrorCode` (per-node)
228
+
229
+ | Code | Description |
230
+ | ------------------- | ------------------------------------------------ |
231
+ | `ServiceError` | Application-level error in WebSocket close frame |
232
+ | `WsError` | WebSocket connection or transport error |
233
+ | `UnexpectedMessage` | Unexpected message format from a node |
234
+ | `Unknown` | Unclassified per-node error |
235
+
236
+ ## Wire Protocol
237
+
238
+ - WebSocket endpoint: `/api/{module}/oprf?version={protocolVersion}`
239
+ - Use `toOprfUri` to build URLs — `http://` → `ws://`, `https://` → `wss://`
240
+ - Messages use JSON with string-serialized BigInts for affine points
241
+
242
+ ## License
243
+
244
+ MIT