@zkproofport-app/sdk 0.2.1 → 0.2.3

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 CHANGED
@@ -122,7 +122,23 @@ const relay = await sdk.createRelayRequest('coinbase_country_attestation', {
122
122
  });
123
123
  ```
124
124
 
125
- > The ZKProofport mobile app handles wallet connection and attestation data retrieval automatically. You only provide the inputs above.
125
+ ### `oidc_domain_attestation`
126
+
127
+ Prove email domain affiliation via Google Sign-In. The mobile app handles authentication and proof generation entirely on-device — the user's email is never revealed.
128
+
129
+ | Field | Type | Required | Description |
130
+ |-------|------|----------|-------------|
131
+ | `domain` | `string` | Yes | Target email domain to prove (e.g., `'google.com'`, `'company.com'`) |
132
+ | `scope` | `string` | Yes | dApp scope identifier for proof uniqueness |
133
+
134
+ ```typescript
135
+ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
136
+ domain: 'company.com',
137
+ scope: 'myapp.com',
138
+ });
139
+ ```
140
+
141
+ > The mobile app prompts Google Sign-In, generates the ZK proof locally, and returns the result via relay. The `domain` is a public input — verifiers can confirm which domain was proven.
126
142
 
127
143
  ## Integration Guide
128
144
 
@@ -159,6 +175,8 @@ interface WalletSigner {
159
175
 
160
176
  Any ethers v5/v6 `Signer` is compatible.
161
177
 
178
+ > **OIDC Domain note:** Wallet signer is not required for OIDC Domain proofs. See Step 3 for OIDC-specific usage.
179
+
162
180
  #### About challenge-signature
163
181
 
164
182
  The challenge-signature mechanism was developed **for relay nonce replay prevention**. Each challenge is one-time use and consumed immediately. The signer's recovered address is recorded as `clientId` in relay server logs, which helps the relay operator track requests.
@@ -192,6 +210,21 @@ const relay = await sdk.createRelayRequest('coinbase_attestation', {
192
210
  // relay.pollUrl — Relative URL for HTTP polling
193
211
  ```
194
212
 
213
+ **OIDC Domain Attestation:**
214
+
215
+ ```typescript
216
+ const relay = await sdk.createRelayRequest('oidc_domain_attestation', {
217
+ domain: 'company.com',
218
+ scope: 'myapp.com',
219
+ }, {
220
+ dappName: 'My DApp',
221
+ dappIcon: 'https://myapp.com/icon.png',
222
+ message: 'Verify your email domain',
223
+ });
224
+ ```
225
+
226
+ The mobile app will prompt the user to sign in with Google. The circuit proves the user's email ends with `@company.com` without revealing the full email address. The `domain` field is a **public input** — verifiers can confirm which domain was proven.
227
+
195
228
  ### Step 4: Display QR Code
196
229
 
197
230
  Generate a QR code from the relay deep link for the user to scan with the ZKProofport mobile app:
@@ -294,6 +327,43 @@ if (result.status === 'completed') {
294
327
  const verification = await sdk.verifyResponseOnChain(response);
295
328
  ```
296
329
 
330
+ ### Step 7: Extract Scope and Nullifier
331
+
332
+ After verification, extract the scope and nullifier from the public inputs:
333
+
334
+ ```typescript
335
+ if (result.status === 'completed') {
336
+ // Extract scope — the keccak256 hash of the scope string you provided
337
+ const scope = sdk.extractScope(result.publicInputs, result.circuit);
338
+
339
+ // Extract nullifier — a unique, deterministic hash per user + scope
340
+ // Same user with the same scope always produces the same nullifier
341
+ const nullifier = sdk.extractNullifier(result.publicInputs, result.circuit);
342
+
343
+ console.log('Scope:', scope); // '0x7a6b70726f...'
344
+ console.log('Nullifier:', nullifier); // '0xabc123...'
345
+ }
346
+ ```
347
+
348
+ The **nullifier** serves as a privacy-preserving user identifier:
349
+ - Deterministic: same user + same scope = same nullifier (enables duplicate detection)
350
+ - Privacy-preserving: the wallet address (Coinbase) or email (OIDC) is never revealed
351
+ - Scope-bound: different scopes produce different nullifiers for the same user
352
+
353
+ > **OIDC Domain:** The nullifier is a hash of the user's email and scope. The same email + scope always produces the same nullifier, enabling Sybil resistance without revealing the email address.
354
+
355
+ **Standalone utility functions** are also available for use outside the SDK class:
356
+
357
+ ```typescript
358
+ import {
359
+ extractScopeFromPublicInputs,
360
+ extractNullifierFromPublicInputs,
361
+ } from '@zkproofport-app/sdk';
362
+
363
+ const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
364
+ const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
365
+ ```
366
+
297
367
  ## Complete Example
298
368
 
299
369
  End-to-end integration using the relay flow:
@@ -354,21 +424,7 @@ async function verifyUser() {
354
424
 
355
425
  ## Configuration
356
426
 
357
- `ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed for standard usage.
358
-
359
- For advanced scenarios (e.g., custom verifier deployments), pass a `ProofportConfig`:
360
-
361
- ```typescript
362
- const sdk = new ProofportSDK({
363
- relayUrl: 'https://relay.zkproofport.app',
364
- verifiers: {
365
- coinbase_attestation: {
366
- verifierAddress: '0x...',
367
- chainId: 8453,
368
- },
369
- },
370
- });
371
- ```
427
+ `ProofportSDK.create()` returns a fully configured SDK instance. No manual configuration is needed relay URLs, verifier contracts, and chain settings are all built-in.
372
428
 
373
429
  ## Types Reference
374
430
 
@@ -380,6 +436,7 @@ import type {
380
436
  ProofRequestStatus,
381
437
  CoinbaseKycInputs,
382
438
  CoinbaseCountryInputs,
439
+ OidcDomainInputs,
383
440
  CircuitInputs,
384
441
  ProofRequest,
385
442
  ProofResponse,
@@ -390,27 +447,35 @@ import type {
390
447
  WalletSigner,
391
448
  RelayProofRequest,
392
449
  RelayProofResult,
393
- SDKEnvironment,
394
450
  } from '@zkproofport-app/sdk';
395
451
  ```
396
452
 
397
453
  | Type | Description |
398
454
  |------|-------------|
399
- | `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation'` |
455
+ | `CircuitType` | `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation'` |
400
456
  | `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` |
401
457
  | `CoinbaseKycInputs` | Inputs for `coinbase_attestation` (`{ scope, userAddress?, rawTransaction? }`) |
402
458
  | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation` (`{ scope, countryList, isIncluded, ... }`) |
403
- | `CircuitInputs` | Union: `CoinbaseKycInputs \| CoinbaseCountryInputs` |
459
+ | `OidcDomainInputs` | Inputs for `oidc_domain_attestation` (`{ domain, scope }`) |
460
+ | `CircuitInputs` | Union: `CoinbaseKycInputs \| CoinbaseCountryInputs \| OidcDomainInputs` |
404
461
  | `ProofRequest` | Proof request object with `requestId`, `circuit`, `inputs`, metadata, and expiry |
405
462
  | `ProofResponse` | Proof response with `status`, `proof`, `publicInputs`, `verifierAddress`, `chainId` |
406
463
  | `QRCodeOptions` | QR customization: `width`, `margin`, `darkColor`, `lightColor`, `errorCorrectionLevel` |
407
464
  | `VerifierContract` | Verifier contract info: `{ address, chainId, abi }` |
408
- | `ProofportConfig` | SDK configuration: `{ scheme?, relayUrl?, verifiers? }` |
465
+ | `ProofportConfig` | SDK configuration (internal use `ProofportSDK.create()` handles defaults) |
409
466
  | `ChallengeResponse` | Challenge from relay: `{ challenge, expiresAt }` |
410
467
  | `WalletSigner` | Signer interface: `{ signMessage(msg), getAddress() }` |
411
468
  | `RelayProofRequest` | Relay response: `{ requestId, deepLink, status, pollUrl }` |
412
469
  | `RelayProofResult` | Relay result: `{ requestId, status, proof?, publicInputs?, circuit?, error? }` |
413
- | `SDKEnvironment` | Environment preset (not needed for normal usage) |
470
+
471
+ The `OidcDomainInputs` interface:
472
+
473
+ ```typescript
474
+ interface OidcDomainInputs {
475
+ domain: string; // Target email domain (e.g., 'google.com')
476
+ scope: string; // dApp scope identifier
477
+ }
478
+ ```
414
479
 
415
480
  ## Error Handling
416
481
 
@@ -432,10 +497,7 @@ try {
432
497
 
433
498
  ## Networks
434
499
 
435
- | Network | Chain ID | Status |
436
- |---------|----------|--------|
437
- | Base Mainnet | 8453 | Production |
438
- | Base Sepolia | 84532 | Testnet |
500
+ Proofs are verified on **Base** (Ethereum L2). The SDK handles network configuration automatically — no manual setup required.
439
501
 
440
502
  ## Development
441
503
 
@@ -763,6 +763,7 @@ export declare class ProofportSDK {
763
763
  * const result = await sdk.waitForProof(relay.requestId);
764
764
  * ```
765
765
  */
766
+ private static readonly WALLET_SIGNATURE_CIRCUITS;
766
767
  createRelayRequest(circuit: CircuitType, inputs: CircuitInputs, options?: {
767
768
  message?: string;
768
769
  dappName?: string;
@@ -893,5 +894,27 @@ export declare class ProofportSDK {
893
894
  * ```
894
895
  */
895
896
  extractScope(publicInputs: string[], circuit: CircuitType): string | null;
897
+ /**
898
+ * Extracts the nullifier (bytes32) from proof public inputs.
899
+ *
900
+ * The nullifier is a unique, deterministic hash derived from the user's attestation
901
+ * and scope. It serves as a privacy-preserving user identifier — the same user
902
+ * with the same scope always produces the same nullifier, enabling duplicate
903
+ * detection without revealing the wallet address.
904
+ *
905
+ * @param publicInputs - Array of hex-encoded field elements from proof result
906
+ * @param circuit - Circuit type that produced the public inputs
907
+ * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
908
+ *
909
+ * @example
910
+ * ```typescript
911
+ * const result = await sdk.waitForProof(relay.requestId);
912
+ * if (result.status === 'completed') {
913
+ * const nullifier = sdk.extractNullifier(result.publicInputs, result.circuit);
914
+ * console.log('Nullifier:', nullifier); // '0xabc123...'
915
+ * }
916
+ * ```
917
+ */
918
+ extractNullifier(publicInputs: string[], circuit: CircuitType): string | null;
896
919
  }
897
920
  export default ProofportSDK;
@@ -145,6 +145,8 @@ export declare const COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT: {
145
145
  readonly MERKLE_ROOT_END: 63;
146
146
  readonly SCOPE_START: 64;
147
147
  readonly SCOPE_END: 95;
148
+ readonly NULLIFIER_START: 96;
149
+ readonly NULLIFIER_END: 127;
148
150
  };
149
151
  /**
150
152
  * Coinbase Country Attestation circuit public input layout (byte offsets).
@@ -178,4 +180,34 @@ export declare const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT: {
178
180
  readonly IS_INCLUDED: 85;
179
181
  readonly SCOPE_START: 86;
180
182
  readonly SCOPE_END: 117;
183
+ readonly NULLIFIER_START: 118;
184
+ readonly NULLIFIER_END: 149;
185
+ };
186
+ /**
187
+ * OIDC Domain Attestation circuit public input layout (byte offsets).
188
+ * Defines the byte positions of each field in the flattened public inputs array.
189
+ *
190
+ * Public inputs are packed as individual field elements (one byte per element):
191
+ * - pubkey_modulus_limbs: 18 x u128 = 18 x 16 bytes = 288 bytes → fields 0–287
192
+ * - domain (BoundedVec<u8, 64>): 4-byte length (u32) + 64-byte storage = 68 fields → fields 288–355
193
+ * - scope: 32 bytes → fields 356–387
194
+ * - nullifier: 32 bytes → fields 388–419
195
+ *
196
+ * @example
197
+ * ```typescript
198
+ * const scope = publicInputs.slice(
199
+ * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_START,
200
+ * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_END + 1
201
+ * );
202
+ * ```
203
+ */
204
+ export declare const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT: {
205
+ readonly PUBKEY_MODULUS_START: 0;
206
+ readonly PUBKEY_MODULUS_END: 287;
207
+ readonly DOMAIN_START: 288;
208
+ readonly DOMAIN_END: 355;
209
+ readonly SCOPE_START: 356;
210
+ readonly SCOPE_END: 387;
211
+ readonly NULLIFIER_START: 388;
212
+ readonly NULLIFIER_END: 419;
181
213
  };
package/dist/index.d.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  * ```typescript
8
8
  * import { ProofportSDK } from '@zkproofport-app/sdk';
9
9
  *
10
- * // Initialize with environment preset
11
- * const sdk = ProofportSDK.create('production');
10
+ * // Initialize SDK
11
+ * const sdk = ProofportSDK.create();
12
12
  *
13
13
  * // Set wallet signer
14
14
  * sdk.setSigner(signer);
@@ -26,4 +26,6 @@
26
26
  * ```
27
27
  */
28
28
  export { ProofportSDK, default } from './ProofportSDK';
29
- export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, CoinbaseCountryInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, SDKEnvironment, } from './types';
29
+ export { extractScopeFromPublicInputs, extractNullifierFromPublicInputs, } from './verifier';
30
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from './constants';
31
+ export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, CoinbaseCountryInputs, OidcDomainInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, SDKEnvironment, } from './types';
package/dist/index.esm.js CHANGED
@@ -67,6 +67,12 @@ const CIRCUIT_METADATA = {
67
67
  publicInputsCount: 14,
68
68
  publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'country_list', 'country_list_length', 'is_included'],
69
69
  },
70
+ oidc_domain_attestation: {
71
+ name: 'OIDC Domain',
72
+ description: 'Prove email domain affiliation via OIDC JWT',
73
+ publicInputsCount: 420,
74
+ publicInputNames: ['pubkey_modulus_limbs', 'domain', 'scope', 'nullifier'],
75
+ },
70
76
  };
71
77
  /**
72
78
  * Standard verifier contract ABI shared across all Barretenberg-generated verifiers.
@@ -135,6 +141,97 @@ const DEFAULT_REQUEST_EXPIRY_MS = 10 * 60 * 1000;
135
141
  * ```
136
142
  */
137
143
  const MAX_QR_DATA_SIZE = 2953; // Version 40 with L error correction
144
+ /**
145
+ * Coinbase Attestation circuit public input layout (byte offsets).
146
+ * Defines the byte positions of each field in the flattened public inputs array.
147
+ *
148
+ * Public inputs are packed as bytes32 values:
149
+ * - signal_hash: bytes 0-31
150
+ * - merkle_root: bytes 32-63
151
+ * - scope: bytes 64-95
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * const publicInputs = response.publicInputs;
156
+ * const signalHash = publicInputs.slice(
157
+ * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_START,
158
+ * COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT.SIGNAL_HASH_END + 1
159
+ * );
160
+ * ```
161
+ */
162
+ const COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
163
+ SIGNAL_HASH_START: 0,
164
+ SIGNAL_HASH_END: 31,
165
+ MERKLE_ROOT_START: 32,
166
+ MERKLE_ROOT_END: 63,
167
+ SCOPE_START: 64,
168
+ SCOPE_END: 95,
169
+ NULLIFIER_START: 96,
170
+ NULLIFIER_END: 127,
171
+ };
172
+ /**
173
+ * Coinbase Country Attestation circuit public input layout (byte offsets).
174
+ * Defines the byte positions of each field in the flattened public inputs array.
175
+ *
176
+ * Public inputs are packed as bytes32 values:
177
+ * - signal_hash: bytes 0-31
178
+ * - merkle_root: bytes 32-63
179
+ * - country_list: bytes 64-83 (20 bytes for 10 countries)
180
+ * - country_list_length: byte 84
181
+ * - is_included: byte 85
182
+ * - scope: bytes 86-117
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * const publicInputs = response.publicInputs;
187
+ * const countryList = publicInputs.slice(
188
+ * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_START,
189
+ * COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT.COUNTRY_LIST_END + 1
190
+ * );
191
+ * ```
192
+ */
193
+ const COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT = {
194
+ SIGNAL_HASH_START: 0,
195
+ SIGNAL_HASH_END: 31,
196
+ MERKLE_ROOT_START: 32,
197
+ MERKLE_ROOT_END: 63,
198
+ COUNTRY_LIST_START: 64,
199
+ COUNTRY_LIST_END: 83,
200
+ COUNTRY_LIST_LENGTH: 84,
201
+ IS_INCLUDED: 85,
202
+ SCOPE_START: 86,
203
+ SCOPE_END: 117,
204
+ NULLIFIER_START: 118,
205
+ NULLIFIER_END: 149,
206
+ };
207
+ /**
208
+ * OIDC Domain Attestation circuit public input layout (byte offsets).
209
+ * Defines the byte positions of each field in the flattened public inputs array.
210
+ *
211
+ * Public inputs are packed as individual field elements (one byte per element):
212
+ * - pubkey_modulus_limbs: 18 x u128 = 18 x 16 bytes = 288 bytes → fields 0–287
213
+ * - domain (BoundedVec<u8, 64>): 4-byte length (u32) + 64-byte storage = 68 fields → fields 288–355
214
+ * - scope: 32 bytes → fields 356–387
215
+ * - nullifier: 32 bytes → fields 388–419
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const scope = publicInputs.slice(
220
+ * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_START,
221
+ * OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT.SCOPE_END + 1
222
+ * );
223
+ * ```
224
+ */
225
+ const OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT = {
226
+ PUBKEY_MODULUS_START: 0,
227
+ PUBKEY_MODULUS_END: 287,
228
+ DOMAIN_START: 288,
229
+ DOMAIN_END: 355,
230
+ SCOPE_START: 356,
231
+ SCOPE_END: 387,
232
+ NULLIFIER_START: 388,
233
+ NULLIFIER_END: 419,
234
+ };
138
235
 
139
236
  /**
140
237
  * Deep Link utilities for ZKProofport SDK
@@ -387,7 +484,7 @@ function validateProofRequest(request) {
387
484
  if (!request.circuit) {
388
485
  return { valid: false, error: 'Missing circuit type' };
389
486
  }
390
- if (!['coinbase_attestation', 'coinbase_country_attestation'].includes(request.circuit)) {
487
+ if (!['coinbase_attestation', 'coinbase_country_attestation', 'oidc_domain_attestation'].includes(request.circuit)) {
391
488
  return { valid: false, error: `Invalid circuit type: ${request.circuit}` };
392
489
  }
393
490
  if (!request.callbackUrl) {
@@ -417,6 +514,15 @@ function validateProofRequest(request) {
417
514
  return { valid: false, error: 'isIncluded is required and must be a boolean' };
418
515
  }
419
516
  }
517
+ else if (request.circuit === 'oidc_domain_attestation') {
518
+ const inputs = request.inputs;
519
+ if (!inputs.domain || typeof inputs.domain !== 'string' || inputs.domain.trim() === '') {
520
+ return { valid: false, error: 'domain is required and must be a non-empty string' };
521
+ }
522
+ if (!inputs.scope || typeof inputs.scope !== 'string' || inputs.scope.trim() === '') {
523
+ return { valid: false, error: 'scope is required and must be a non-empty string' };
524
+ }
525
+ }
420
526
  // Check expiry
421
527
  if (request.expiresAt && Date.now() > request.expiresAt) {
422
528
  return { valid: false, error: 'Request has expired' };
@@ -3718,6 +3824,10 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
3718
3824
  start = 86;
3719
3825
  end = 117;
3720
3826
  }
3827
+ else if (circuit === 'oidc_domain_attestation') {
3828
+ start = 356;
3829
+ end = 387;
3830
+ }
3721
3831
  else {
3722
3832
  start = 64;
3723
3833
  end = 95;
@@ -3727,6 +3837,43 @@ function extractScopeFromPublicInputs(publicInputsHex, circuit) {
3727
3837
  const scopeFields = publicInputsHex.slice(start, end + 1);
3728
3838
  return reconstructBytes32FromFields(scopeFields);
3729
3839
  }
3840
+ /**
3841
+ * Extracts the nullifier (bytes32) from public inputs based on circuit type.
3842
+ *
3843
+ * The nullifier is a unique, deterministic hash derived from the user's attestation
3844
+ * and scope. It serves as a privacy-preserving user identifier — the same user
3845
+ * with the same scope always produces the same nullifier, enabling duplicate
3846
+ * detection without revealing the wallet address.
3847
+ *
3848
+ * @param publicInputsHex - Array of hex-encoded field elements
3849
+ * @param circuit - Circuit type (defaults to coinbase_attestation)
3850
+ * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
3851
+ *
3852
+ * @example
3853
+ * ```typescript
3854
+ * const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
3855
+ * console.log(nullifier); // '0xabc123...'
3856
+ * ```
3857
+ */
3858
+ function extractNullifierFromPublicInputs(publicInputsHex, circuit) {
3859
+ let start, end;
3860
+ if (circuit === 'coinbase_country_attestation') {
3861
+ start = 118;
3862
+ end = 149;
3863
+ }
3864
+ else if (circuit === 'oidc_domain_attestation') {
3865
+ start = 388;
3866
+ end = 419;
3867
+ }
3868
+ else {
3869
+ start = 96;
3870
+ end = 127;
3871
+ }
3872
+ if (publicInputsHex.length <= end)
3873
+ return null;
3874
+ const nullifierFields = publicInputsHex.slice(start, end + 1);
3875
+ return reconstructBytes32FromFields(nullifierFields);
3876
+ }
3730
3877
  /** @internal Reconstruct a bytes32 value from 32 individual field elements */
3731
3878
  function reconstructBytes32FromFields(fields) {
3732
3879
  if (fields.length !== 32) {
@@ -4609,53 +4756,26 @@ class ProofportSDK {
4609
4756
  }
4610
4757
  return await response.json();
4611
4758
  }
4612
- /**
4613
- * Creates a proof request through the relay server.
4614
- *
4615
- * This is the recommended way to create proof requests. The relay server:
4616
- * - Issues a server-side requestId (validated by the mobile app)
4617
- * - Tracks request status in Redis
4618
- * - Builds the deep link with relay callback URL
4619
- * - Stores inputs hash for deep link integrity verification
4620
- *
4621
- * @param circuit - Circuit type identifier
4622
- * @param inputs - Circuit-specific inputs
4623
- * @param options - Request options (message, dappName, dappIcon, nonce)
4624
- * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
4625
- * @throws Error if signer not set or relay request fails
4626
- *
4627
- * @example
4628
- * ```typescript
4629
- * const sdk = ProofportSDK.create();
4630
- * sdk.setSigner(signer);
4631
- *
4632
- * const relay = await sdk.createRelayRequest('coinbase_attestation', {
4633
- * scope: 'myapp.com'
4634
- * }, { dappName: 'My DApp' });
4635
- *
4636
- * // Generate QR code from relay deep link
4637
- * const qr = await sdk.generateQRCode(relay.deepLink);
4638
- *
4639
- * // Wait for proof (WebSocket primary, polling fallback)
4640
- * const result = await sdk.waitForProof(relay.requestId);
4641
- * ```
4642
- */
4643
4759
  async createRelayRequest(circuit, inputs, options = {}) {
4644
- if (!this.signer) {
4645
- throw new Error('Signer not set. Call setSigner() first.');
4646
- }
4647
4760
  if (!this.relayUrl) {
4648
4761
  throw new Error('relayUrl is required. Set it in ProofportSDK config.');
4649
4762
  }
4650
- // Get challenge from relay and sign it
4651
- const { challenge } = await this.getChallenge();
4652
- const signature = await this.signer.signMessage(challenge);
4763
+ const needsSignature = ProofportSDK.WALLET_SIGNATURE_CIRCUITS.includes(circuit);
4764
+ if (needsSignature && !this.signer) {
4765
+ throw new Error('Signer not set. Call setSigner() first. Wallet signature is required for this circuit.');
4766
+ }
4767
+ // Get challenge + requestId from relay
4768
+ const { requestId, challenge } = await this.getChallenge();
4653
4769
  const body = {
4770
+ requestId,
4654
4771
  circuitId: circuit,
4655
4772
  inputs,
4656
4773
  challenge,
4657
- signature,
4658
4774
  };
4775
+ // Sign challenge for circuits that require wallet signature
4776
+ if (needsSignature && this.signer) {
4777
+ body.signature = await this.signer.signMessage(challenge);
4778
+ }
4659
4779
  if (options.message)
4660
4780
  body.message = options.message;
4661
4781
  if (options.dappName)
@@ -4907,7 +5027,67 @@ class ProofportSDK {
4907
5027
  extractScope(publicInputs, circuit) {
4908
5028
  return extractScopeFromPublicInputs(publicInputs, circuit);
4909
5029
  }
5030
+ /**
5031
+ * Extracts the nullifier (bytes32) from proof public inputs.
5032
+ *
5033
+ * The nullifier is a unique, deterministic hash derived from the user's attestation
5034
+ * and scope. It serves as a privacy-preserving user identifier — the same user
5035
+ * with the same scope always produces the same nullifier, enabling duplicate
5036
+ * detection without revealing the wallet address.
5037
+ *
5038
+ * @param publicInputs - Array of hex-encoded field elements from proof result
5039
+ * @param circuit - Circuit type that produced the public inputs
5040
+ * @returns Nullifier as hex string (bytes32), or null if publicInputs too short
5041
+ *
5042
+ * @example
5043
+ * ```typescript
5044
+ * const result = await sdk.waitForProof(relay.requestId);
5045
+ * if (result.status === 'completed') {
5046
+ * const nullifier = sdk.extractNullifier(result.publicInputs, result.circuit);
5047
+ * console.log('Nullifier:', nullifier); // '0xabc123...'
5048
+ * }
5049
+ * ```
5050
+ */
5051
+ extractNullifier(publicInputs, circuit) {
5052
+ return extractNullifierFromPublicInputs(publicInputs, circuit);
5053
+ }
4910
5054
  }
5055
+ /**
5056
+ * Creates a proof request through the relay server.
5057
+ *
5058
+ * This is the recommended way to create proof requests. The relay server:
5059
+ * - Issues a server-side requestId (validated by the mobile app)
5060
+ * - Tracks request status in Redis
5061
+ * - Builds the deep link with relay callback URL
5062
+ * - Stores inputs hash for deep link integrity verification
5063
+ *
5064
+ * @param circuit - Circuit type identifier
5065
+ * @param inputs - Circuit-specific inputs
5066
+ * @param options - Request options (message, dappName, dappIcon, nonce)
5067
+ * @returns Promise resolving to RelayProofRequest with requestId, deepLink, pollUrl
5068
+ * @throws Error if signer not set or relay request fails
5069
+ *
5070
+ * @example
5071
+ * ```typescript
5072
+ * const sdk = ProofportSDK.create();
5073
+ * sdk.setSigner(signer);
5074
+ *
5075
+ * const relay = await sdk.createRelayRequest('coinbase_attestation', {
5076
+ * scope: 'myapp.com'
5077
+ * }, { dappName: 'My DApp' });
5078
+ *
5079
+ * // Generate QR code from relay deep link
5080
+ * const qr = await sdk.generateQRCode(relay.deepLink);
5081
+ *
5082
+ * // Wait for proof (WebSocket primary, polling fallback)
5083
+ * const result = await sdk.waitForProof(relay.requestId);
5084
+ * ```
5085
+ */
5086
+ // Circuits that require wallet signature (used as circuit input)
5087
+ ProofportSDK.WALLET_SIGNATURE_CIRCUITS = [
5088
+ 'coinbase_attestation',
5089
+ 'coinbase_country_attestation',
5090
+ ];
4911
5091
 
4912
- export { ProofportSDK, ProofportSDK as default };
5092
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, ProofportSDK, ProofportSDK as default, extractNullifierFromPublicInputs, extractScopeFromPublicInputs };
4913
5093
  //# sourceMappingURL=index.esm.js.map