@zkproofport-app/sdk 0.5.0-beta.2 → 0.5.0-beta.4

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
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@zkproofport-app/sdk)](https://www.npmjs.com/package/@zkproofport-app/sdk)
4
4
  [![license](https://img.shields.io/npm/l/@zkproofport-app/sdk)](./LICENSE)
5
5
 
6
- TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain. Three circuits are officially supported today — Coinbase KYC, Coinbase country attestation, and OIDC email-domain attestation (Google, Microsoft 365). Two more prove today against a testnet deployment and are marked experimental: GIWA attestation and Dojang Playground. The three Korean mobile ID (mDL) identifiers are reserved and planned. [Circuit Identifiers](#circuit-identifiers) has the full list and what each status means.
6
+ TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https://zkproofport.com) mobile app and verifying them on-chain. Three circuits are officially supported today — Coinbase KYC, Coinbase country attestation, and OIDC email-domain attestation (Google, Microsoft 365). Two more prove today against a testnet deployment and are marked experimental: GIWA attestation and Dojang Playground. [Circuit Identifiers](#circuit-identifiers) has the full list and what each status means.
7
7
 
8
8
  ## How It Works
9
9
 
@@ -33,7 +33,7 @@ TypeScript SDK for requesting zero-knowledge proofs from the [ZKProofport](https
33
33
  ```
34
34
 
35
35
  1. Your app creates a proof request through the SDK
36
- 2. The SDK asks the relay for a one-time challenge and a tracked request ID. For the Coinbase circuits it also signs that challenge with your wallet signer (EIP-191); the OIDC and mDL circuits need no signature
36
+ 2. The SDK asks the relay for a one-time challenge and a tracked request ID. For the Coinbase circuits it also signs that challenge with your wallet signer (EIP-191); the OIDC circuit needs no signature
37
37
  3. Your app shows the returned deep link as a QR code (desktop) or navigates to it (mobile)
38
38
  4. The user opens the ZKProofport app, which generates the ZK proof on-device
39
39
  5. The proof result flows back through the relay to your app over WebSocket (HTTP polling as fallback)
@@ -90,6 +90,7 @@ if (result.status === 'completed') {
90
90
  publicInputs: result.publicInputs,
91
91
  verifierAddress: result.verifierAddress,
92
92
  chainId: result.chainId,
93
+ signerVariant: result.signerVariant,
93
94
  };
94
95
 
95
96
  const verification = await sdk.verifyResponseOnChain(response);
@@ -99,7 +100,7 @@ if (result.status === 'completed') {
99
100
 
100
101
  ## Circuit Identifiers
101
102
 
102
- Circuit IDs are canonical, lowercase and case-sensitive: each one is the `name` field of that circuit's `Nargo.toml`, verbatim. Hyphenated spellings such as `coinbase-kyc` or `mdl-kr-age` are route and directory names from elsewhere — they are not circuit IDs, and passing one does not fail with a clear error. It produces a nullifier mismatch or a verifier lookup that finds nothing.
103
+ Circuit IDs are canonical, lowercase and case-sensitive: each one is the `name` field of that circuit's `Nargo.toml`, verbatim. Hyphenated spellings such as `coinbase-kyc` or `oidc-domain` are route and directory names from elsewhere — they are not circuit IDs, and passing one does not fail with a clear error. It produces a nullifier mismatch or a verifier lookup that finds nothing.
103
104
 
104
105
  This SDK is the single source of truth for that list. Import the constants rather than typing the strings, and a rename becomes a compile error instead of a runtime surprise:
105
106
 
@@ -126,28 +127,23 @@ import { CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circui
126
127
  | `CIRCUIT_IDS.OIDC_DOMAIN_ATTESTATION` | `oidc_domain_attestation` | **Supported** |
127
128
  | `CIRCUIT_IDS.GIWA_ATTESTATION` | `giwa_attestation` | _Experimental_ |
128
129
  | `CIRCUIT_IDS.DOJANG_PLAYGROUND` | `dojang_playground` | _Experimental_ |
129
- | `CIRCUIT_IDS.MDL_KR_OWNERSHIP` | `mdl_kr_ownership` | Planned |
130
- | `CIRCUIT_IDS.MDL_KR_AGE` | `mdl_kr_age` | Planned |
131
- | `CIRCUIT_IDS.MDL_KR_REGION` | `mdl_kr_region` | Planned |
132
130
 
133
131
  **Supported** means generally available — build a product on it.
134
132
 
135
133
  **Experimental** means you can prove it today and it verifies, but only against a testnet deployment. Inputs, public-input layout and the verifier address can change without a major version bump, and there is no mainnet to point at. Try it; do not ship on it.
136
134
 
137
- **Planned** means the identifier is reserved and the circuit exists, but it is not officially supported yet: availability, inputs and public-input layout can change without a major version bump.
135
+ **Planned** marks an identifier reserved ahead of official support; inputs and public-input layout can change without a major version bump. No circuit has this status today.
138
136
 
139
- Every ID appears in `CircuitType` and in `sdk.getSupportedCircuits()`, planned ones included, so being assignable is not a support guarantee. Gate on the status, not on the type:
137
+ Every ID appears in `CircuitType` and in `sdk.getSupportedCircuits()`, experimental ones included, so being assignable is not a support guarantee. Gate on the status, not on the type:
140
138
 
141
139
  ```typescript
142
140
  import { CIRCUIT_SUPPORT_STATUS, isSupportedCircuitId } from '@zkproofport-app/sdk';
143
141
 
144
142
  CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'
145
143
  CIRCUIT_SUPPORT_STATUS.giwa_attestation; // 'experimental'
146
- CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'
147
144
 
148
145
  isSupportedCircuitId('oidc_domain_attestation'); // true
149
146
  isSupportedCircuitId('giwa_attestation'); // false — experimental is not supported
150
- isSupportedCircuitId('mdl_kr_age'); // false
151
147
  isSupportedCircuitId('coinbase-kyc'); // false — not an ID at all
152
148
  ```
153
149
 
@@ -163,14 +159,14 @@ isSupportedCircuitId('coinbase-kyc'); // false — not an ID at all
163
159
  | `isCircuitId(value)` | `value is CircuitId` | Narrows an unknown value; `false` for hyphenated names |
164
160
  | `isSupportedCircuitId(value)` | `value is CircuitId` | The same, and additionally requires `supported` |
165
161
  | `getCircuitSupportStatus(id)` | `CircuitSupportStatus` | Status of one ID; throws on an unknown one |
166
- | `CircuitId` | type | Union of the eight IDs. `CircuitType` is an alias of it |
167
- | `CircuitSupportStatus` | type | `'supported' \| 'planned'` |
162
+ | `CircuitId` | type | Union of the five IDs. `CircuitType` is an alias of it |
163
+ | `CircuitSupportStatus` | type | `'supported' \| 'experimental' \| 'planned'` |
168
164
 
169
165
  Both objects and all three arrays are frozen. `ALL_CIRCUIT_IDS`, `SUPPORTED_CIRCUIT_IDS` and `PLANNED_CIRCUIT_IDS` are derived from `CIRCUIT_SUPPORT_STATUS` at load time, so they cannot disagree with it.
170
166
 
171
167
  ## Supported Circuits
172
168
 
173
- The circuits below are officially supported. Pass their IDs exactly as written, or use the `CIRCUIT_IDS` constants. `sdk.getSupportedCircuits()` returns every known ID at runtime, planned ones included — see [Circuit Identifiers](#circuit-identifiers).
169
+ The circuits below are officially supported. Pass their IDs exactly as written, or use the `CIRCUIT_IDS` constants. `sdk.getSupportedCircuits()` returns every known ID at runtime, experimental ones included — see [Circuit Identifiers](#circuit-identifiers).
174
170
 
175
171
  ### `coinbase_attestation`
176
172
 
@@ -334,7 +330,7 @@ circuit names are special:
334
330
  | What the circuit does with an action | Circuits |
335
331
  |---|---|
336
332
  | Signs it when present, signs the signal hash when not | `giwa_attestation`, `dojang_playground` |
337
- | Has no action inputs — sending one is refused, not ignored | Coinbase KYC, Coinbase country, OIDC domain, the three mDL circuits |
333
+ | Has no action inputs — sending one is refused, not ignored | Coinbase KYC, Coinbase country, OIDC domain |
338
334
 
339
335
  ```typescript
340
336
  import { circuitActionBinding } from '@zkproofport-app/sdk';
@@ -347,62 +343,6 @@ Sending an action to a circuit that cannot prove one is an error rather than a
347
343
  silent drop: a dropped action would leave you believing a proof authorized
348
344
  something the circuit never saw.
349
345
 
350
- ## Planned Circuits
351
-
352
- ZKProofport is registered with the Ministry of the Interior and Safety as a Korea Mobile ID integration service (모바일 신분증 연계서비스). The three circuits below are in beta: `CIRCUIT_SUPPORT_STATUS` reports each of them as `planned`, and their input shape and public-input layout can change without a major version bump.
353
-
354
- ### `mdl_kr_ownership`
355
-
356
- Proves the user holds a valid Korean mobile driver's license (모바일 신분증). The license data stays on-device; only the attributes selected by `discloseFlags` are revealed. No wallet signer is required.
357
-
358
- | Field | Type | Required | Description |
359
- |-------|------|----------|-------------|
360
- | `scope` | `string` | Yes | Application-specific identifier for proof uniqueness. Non-empty, 256 characters or fewer, no control characters. |
361
- | `discloseFlags` | `number` | No | Attribute disclosure bitmask, integer 0–15 (`0x01` name, `0x02` birth, `0x04` sex, `0x08` phone). Omit or `0` for a fully anonymous ownership proof. |
362
-
363
- ```typescript
364
- // Anonymous "holds a valid Korean mobile ID" proof
365
- const relay = await sdk.createRelayRequest('mdl_kr_ownership', {
366
- scope: 'myapp.com',
367
- });
368
- ```
369
-
370
- ### `mdl_kr_age`
371
-
372
- Proves the user is at least `ageThreshold` years old according to their Korean mobile driver's license, without revealing the birth date. No wallet signer is required.
373
-
374
- | Field | Type | Required | Description |
375
- |-------|------|----------|-------------|
376
- | `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
377
- | `ageThreshold` | `number` | Yes | Minimum age to prove — integer between 1 and 150 (e.g. `19` for Korean adult verification) |
378
-
379
- ```typescript
380
- const relay = await sdk.createRelayRequest('mdl_kr_age', {
381
- scope: 'myapp.com',
382
- ageThreshold: 19,
383
- });
384
- ```
385
-
386
- ### `mdl_kr_region`
387
-
388
- Proves the user's registered address is in the specified si/do region without revealing the full address. No wallet signer is required.
389
-
390
- | Field | Type | Required | Description |
391
- |-------|------|----------|-------------|
392
- | `scope` | `string` | Yes | Application-specific identifier (same rules as above) |
393
- | `targetRegion` | `string` | Yes | Region (si/do) to prove residency in, in Korean (e.g. `'경기도'`, `'서울특별시'`). Non-empty, 256 characters or fewer, no control characters. |
394
-
395
- ```typescript
396
- const relay = await sdk.createRelayRequest('mdl_kr_region', {
397
- scope: 'myapp.com',
398
- targetRegion: '경기도',
399
- });
400
- ```
401
-
402
- > mDL inputs are checked before the request leaves your process: a missing `scope`, an out-of-range `discloseFlags`, a non-integer `ageThreshold` or an empty `targetRegion` throws locally instead of coming back as a relay error.
403
-
404
- > The mDL circuits do not require `setSigner()` — the proof is bound to the license via an on-device nullifier (`nullifier_value` public input), not a wallet address. Use `sdk.extractNullifier(publicInputs, circuit)` for sybil-resistant user identification.
405
-
406
346
  ## Integration Guide
407
347
 
408
348
  ### Step 1: Initialize
@@ -438,7 +378,7 @@ interface WalletSigner {
438
378
 
439
379
  Any ethers v5/v6 `Signer` is compatible.
440
380
 
441
- > **When you can skip this step:** the OIDC Domain and the three mDL circuits never sign a challenge. Calling `createRelayRequest` for those circuits without a signer works. Calling it for a Coinbase circuit without one throws `Signer not set. Call setSigner() first. Wallet signature is required for this circuit.`
381
+ > **When you can skip this step:** the OIDC Domain circuit never signs a challenge. Calling `createRelayRequest` for it without a signer works. Calling it for a Coinbase circuit without one throws `Signer not set. Call setSigner() first. Wallet signature is required for this circuit.`
442
382
 
443
383
  #### About challenge-signature
444
384
 
@@ -625,7 +565,7 @@ if (ProofportSDK.isMobile()) {
625
565
 
626
566
  ```typescript
627
567
  const result = await sdk.waitForProof(relay.requestId, {
628
- timeoutMs: 300000, // 5 minutes (default)
568
+ timeoutMs: 600000, // 10 minutes (default)
629
569
  onStatusChange: (update) => {
630
570
  console.log('Status:', update.status);
631
571
  },
@@ -658,7 +598,7 @@ const once = await sdk.pollResult(relay.requestId);
658
598
  // Poll until terminal state
659
599
  const result = await sdk.waitForResult(relay.requestId, {
660
600
  intervalMs: 2000,
661
- timeoutMs: 300000,
601
+ timeoutMs: 600000,
662
602
  onStatusChange: (result) => console.log(result.status),
663
603
  });
664
604
  ```
@@ -738,7 +678,7 @@ switch (result.status) {
738
678
 
739
679
  `isTerminalRelayStatus(status)` and `RELAY_TERMINAL_STATUSES` are exported for code that drives `pollResult()` in a loop of its own.
740
680
 
741
- The relay answers `'expired'` before the SDK's default `timeoutMs` (300 000 ms) runs out, so with the defaults you get a result to branch on rather than a thrown timeout. If you pass a much shorter `timeoutMs`, the SDK's own timeout can still fire first and the promise rejects.
681
+ The relay answers `'expired'` before the SDK's default `timeoutMs` (600 000 ms) runs out, so with the defaults you get a result to branch on rather than a thrown timeout. If you pass a much shorter `timeoutMs`, the SDK's own timeout can still fire first and the promise rejects.
742
682
 
743
683
  #### What is not on the result
744
684
 
@@ -772,6 +712,7 @@ The relay answers `'expired'` before the SDK's default `timeoutMs` (300 000 ms)
772
712
  | `publicInputs` | `string[]` | When completed | `result.publicInputs` |
773
713
  | `verifierAddress` | `string` | No | `result.verifierAddress`. Optional. When present it must match the SDK's own verifier for the circuit, or verification is refused |
774
714
  | `chainId` | `number` | No | `result.chainId`. Optional. When present it must match the network the SDK verifies the circuit on, or verification is refused |
715
+ | `signerVariant` | `string` | No | `result.signerVariant`. Copy it unchanged whenever the relay sends it: some circuits are proved by more than one package, each with its own verifier, and this field says which one made the proof (`standard`, `android` or `android_factory`). When it is absent the circuit's standard verifier is used, and a proof that has another package's size is refused; an unrecognised value is refused by name |
775
716
  | `numPublicInputs` | `number` | No | Never delivered by the relay, and never read during verification — `publicInputs.length` is used instead. Omit it |
776
717
  | `timestamp` | `number` | No | Not delivered by the relay. Omit it |
777
718
  | `error` | `string` | On failure | `result.error`. Carried for your own logging; verification ignores it |
@@ -784,11 +725,13 @@ Verify the proof cryptographically by calling the deployed Solidity verifier con
784
725
 
785
726
  `verifyResponseOnChain` checks the proof against the verifier contract the SDK ships with for that circuit. Which contract judges a proof is never taken from the proof's own response: the SDK carries the deployed verifiers, generated from the circuits' deployment records, and a response whose `verifierAddress` or `chainId` differs from them is refused rather than checked elsewhere. If you configure your own verifier in the SDK's `verifiers` option, that one is used instead.
786
727
 
787
- For OIDC domain proofs the SDK also checks that the proof was made with a signing key Google or Microsoft actually publishes. That key is part of the proof's public inputs, so a proof made with a self-generated key would otherwise pass the contract. The SDK fetches the provider's published keys, caches them as the provider's own cache headers allow, and returns `{ valid: false, error }` when the key is not among them or the keys cannot be fetched. Every verify method does this; there is nothing to call separately.
728
+ For OIDC domain proofs the SDK first checks what the proof says about the sign-in token: it must have been issued to a ZKProofport sign-in client (not to some other website), the proof must have been made within the last 15 minutes (and not more than 5 minutes ahead of your clock), and a proof made by the ZKProofport app must carry the nonce of **your** request — so a proof made for another request cannot be replayed as the answer to yours. For a request you created with `createRelayRequest` on the same SDK instance this happens automatically. If you verify in a different process (or after a restart), pass the request back: `sdk.verifyResponseOnChain(response, provider, { request: { scope, requestId } })`, with the scope you requested and the `requestId` the relay returned; without it an OIDC response is refused. `verifyOnChain` and `verifyResponseOffChain` take the same `request` option.
729
+
730
+ The SDK also checks that the proof was made with a signing key Google or Microsoft actually publishes. That key is part of the proof's public inputs, so a proof made with a self-generated key would otherwise pass the contract. The SDK fetches the provider's published keys, caches them as the provider's own cache headers allow, and returns `{ valid: false, error }` when the key is not among them or the keys cannot be fetched. Every verify method does this; there is nothing to call separately.
788
731
 
789
732
  For Coinbase KYC, Coinbase country and GIWA proofs the SDK checks that the attestation's signer belongs to the published signer set: the proof carries the root of the signer list it was made against, and a proof made against any other list is refused with `{ valid: false, error }`. This too runs in every verify method.
790
733
 
791
- The one limit: a proof made with a key the provider has already withdrawn, which this process never saw while it was published, cannot be checked and is refused. Keys seen while published stay acceptable for 24 hours after they disappear.
734
+ The one limit: a proof made with a key the provider has already withdrawn, which this process never saw while it was published, cannot be checked and is refused. Keys seen while published stay acceptable for 1 hour after they were last listed.
792
735
 
793
736
 
794
737
 
@@ -804,6 +747,7 @@ if (result.status === 'completed') {
804
747
  publicInputs: result.publicInputs,
805
748
  verifierAddress: result.verifierAddress,
806
749
  chainId: result.chainId,
750
+ signerVariant: result.signerVariant,
807
751
  };
808
752
 
809
753
  const verification = await sdk.verifyResponseOnChain(response);
@@ -879,7 +823,7 @@ if (sdk.extractScope(publicInputs, circuit) !== expectedScope) {
879
823
  The nullifier is a deterministic hash over the user's credential and the scope:
880
824
 
881
825
  - **Deterministic** — same user, same scope, same nullifier, every time. That is what makes duplicate detection possible.
882
- - **Privacy-preserving** — the wallet address (Coinbase), the email (OIDC) and the licence (mDL) stay hidden. There is no way back from the nullifier to the person.
826
+ - **Privacy-preserving** — the wallet address (Coinbase) and the email (OIDC) stay hidden. There is no way back from the nullifier to the person.
883
827
  - **Scope-bound** — the same user proving to a different scope produces an unrelated nullifier, so your users cannot be correlated across apps.
884
828
 
885
829
  On your side, it is the primary key of a verified user. Store it once verification succeeds, with a unique constraint, and check it before granting anything:
@@ -927,18 +871,15 @@ import {
927
871
  extractDomainFromPublicInputs,
928
872
  } from '@zkproofport-app/sdk';
929
873
 
930
- // Scope and nullifier work for all six circuits — the layout is picked from the
874
+ // Scope and nullifier work for all five circuits — the layout is picked from the
931
875
  // circuit id, so always pass it:
932
876
  const scope = extractScopeFromPublicInputs(publicInputs, 'coinbase_attestation');
933
877
  const nullifier = extractNullifierFromPublicInputs(publicInputs, 'coinbase_attestation');
934
878
 
935
- // OIDC domain attestation uses a different public input layout (148 fields)
879
+ // OIDC domain attestation uses a different public input layout (214 fields)
936
880
  const oidcScope = extractScopeFromPublicInputs(publicInputs, 'oidc_domain_attestation');
937
881
  const oidcNullifier = extractNullifierFromPublicInputs(publicInputs, 'oidc_domain_attestation');
938
882
 
939
- // The three mDL circuits share one layout
940
- const mdlNullifier = extractNullifierFromPublicInputs(publicInputs, 'mdl_kr_age');
941
-
942
883
  // Extract domain from OIDC Domain Attestation
943
884
  const domain = extractDomainFromPublicInputs(publicInputs, 'oidc_domain_attestation');
944
885
  // domain: 'example.com', or null if the circuit doesn't match or inputs are insufficient
@@ -1049,9 +990,6 @@ import type {
1049
990
  OidcDomainInputs,
1050
991
  ActionBoundInputs,
1051
992
  TypedAction,
1052
- MdlKrOwnershipInputs,
1053
- MdlKrAgeInputs,
1054
- MdlKrRegionInputs,
1055
993
  CircuitInputs,
1056
994
  ProofRequest,
1057
995
  ProofResponse,
@@ -1067,20 +1005,17 @@ import type {
1067
1005
 
1068
1006
  | Type | Description |
1069
1007
  |------|-------------|
1070
- | `CircuitId` | Union of the eight canonical circuit IDs — `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation' \| 'giwa_attestation' \| 'dojang_playground' \| 'mdl_kr_ownership' \| 'mdl_kr_age' \| 'mdl_kr_region'`. Also exported from `@zkproofport-app/sdk/circuits` |
1008
+ | `CircuitId` | Union of the five canonical circuit IDs — `'coinbase_attestation' \| 'coinbase_country_attestation' \| 'oidc_domain_attestation' \| 'giwa_attestation' \| 'dojang_playground'`. Also exported from `@zkproofport-app/sdk/circuits` |
1071
1009
  | `TypedAction` | The EIP-712 action a `giwa_attestation` proof binds to: `{ domain, types, primaryType, message }` — the same object `eth_signTypedData_v4` takes |
1072
- | `CircuitSupportStatus` | `'supported' \| 'planned'` — see [Circuit Identifiers](#circuit-identifiers) |
1010
+ | `CircuitSupportStatus` | `'supported' \| 'experimental' \| 'planned'` — see [Circuit Identifiers](#circuit-identifiers) |
1073
1011
  | `CircuitType` | Alias of `CircuitId`. Kept as the name used throughout `ProofRequest`, `ProofResponse` and the SDK methods |
1074
1012
  | `ProofRequestStatus` | `'pending' \| 'completed' \| 'error' \| 'cancelled'` — the status on a `ProofResponse` |
1075
1013
  | `CoinbaseKycInputs` | Inputs for `coinbase_attestation`: `{ scope, userAddress?, rawTransaction? }` |
1076
1014
  | `CoinbaseCountryInputs` | Inputs for `coinbase_country_attestation`: `{ scope, countryList, isIncluded, userAddress?, rawTransaction? }` |
1077
1015
  | `OidcDomainInputs` | Inputs for `oidc_domain_attestation`: `{ domain, scope, provider? }` |
1078
- | `MdlKrOwnershipInputs` | Inputs for `mdl_kr_ownership`: `{ scope, discloseFlags? }` |
1079
1016
  | `ActionBoundInputs` | Inputs for the circuits that can bind an EIP-712 action: `{ scope, action?, userAddress?, rawTransaction? }`. Whether the action is required is the circuit's property, in `CIRCUIT_ACTION_BINDING` — optional for `giwa_attestation` and `dojang_playground` |
1080
1017
  | `GiwaAttestationInputs` | Alias of `ActionBoundInputs`, for `giwa_attestation` |
1081
1018
  | `DojangPlaygroundInputs` | Alias of `ActionBoundInputs`, for `dojang_playground` |
1082
- | `MdlKrAgeInputs` | Inputs for `mdl_kr_age`: `{ scope, ageThreshold }` |
1083
- | `MdlKrRegionInputs` | Inputs for `mdl_kr_region`: `{ scope, targetRegion }` |
1084
1019
  | `CircuitInputs` | Union of every input type above (plus an empty-input form for circuits that need nothing from the dApp) |
1085
1020
  | `ProofRequest` | Request object: `requestId`, `circuit`, `inputs`, `createdAt`, plus optional `message`, `dappName`, `dappIcon`, `returnScheme` (a bare scheme such as `mydapp://`), `expiresAt` |
1086
1021
  | `ProofResponse` | What `verifyResponseOnChain()` takes: `requestId`, `circuit`, `status`, and — when completed — `proof`, `publicInputs`, `verifierAddress`, `chainId`; `error` when it failed. `numPublicInputs` and `timestamp` also exist but the relay never fills them, so omit them. Field by field in [Step 6](#step-6-read-the-result) |
@@ -1114,7 +1049,6 @@ import {
1114
1049
  COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT,
1115
1050
  COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT,
1116
1051
  OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT,
1117
- MDL_KR_PUBLIC_INPUT_LAYOUT,
1118
1052
  } from '@zkproofport-app/sdk';
1119
1053
  ```
1120
1054
 
@@ -1156,7 +1090,7 @@ const signalHash = toBytes32(publicInputs.slice(L.SIGNAL_HASH_START, L.SIGNAL_HA
1156
1090
 
1157
1091
  The country list and the `is_included` flag are public inputs, which means the proof is only as strong as the list it was proved against. Read indices 64–85 back and confirm they are the list and the direction you asked for; a valid proof against a different list is still a valid proof.
1158
1092
 
1159
- ### `oidc_domain_attestation` — 148 entries
1093
+ ### `oidc_domain_attestation` — 214 entries
1160
1094
 
1161
1095
  | Index | Constants | Circuit field | What it is |
1162
1096
  |-------|-----------|---------------|------------|
@@ -1166,39 +1100,17 @@ The country list and the `is_included` flag are public inputs, which means the p
1166
1100
  | 83–114 | `SCOPE_START` / `_END` | `scope` | `bytes32` scope hash |
1167
1101
  | 115–146 | `NULLIFIER_START` / `_END` | `nullifier` | `bytes32` over the user's email and the scope |
1168
1102
  | 147 | `PROVIDER` | `provider` | `0` = Google, `1` = Microsoft. The circuit accepts no other value |
1103
+ | 148–179 | `AUD_HASH_START` / `_END` | `aud_hash` | keccak256 of the ID token's `aud` (the sign-in client it was issued to) |
1104
+ | 180 | `NONCE_MODE` | `nonce_mode` | `1` = the token's nonce is bound to your request, `0` = no nonce (agent device sign-in) |
1105
+ | 181–212 | `NONCE_BINDING_START` / `_END` | `nonce_binding` | In mode `1`, keccak256("zkproofport-oidc-v1" ‖ keccak256(scope) ‖ requestId); all zero in mode `0` |
1106
+ | 213 | `NOW` | `now` | Unix seconds the proof was made at; the circuit proves the token was valid then |
1107
+
1108
+ Proofs from the earlier 148-entry version of this circuit bind no audience, expiry or nonce and are refused by every verify method.
1169
1109
 
1170
1110
  `DOMAIN_START` and `DOMAIN_END` are kept as deprecated aliases of `DOMAIN_STORAGE_START` and `DOMAIN_LEN`. Use the current names — the old pair reads as a range and is not one.
1171
1111
 
1172
1112
  Index 147 tells you which issuer signed the token, not whether the account belongs to an organisation. Workspace and Microsoft 365 membership is checked on-device only when you set `provider` in the request; if you did not set it, do not read a membership guarantee out of this field.
1173
1113
 
1174
- ### Korea Mobile ID (mDL) — one layout, three circuits
1175
-
1176
- All three mDL circuits share the same first 64 entries and differ after that. Entries are single bytes, except `age_threshold` and `current_year`, which each hold their whole value in one entry.
1177
-
1178
- | Index | Constants | Circuit field | Applies to |
1179
- |-------|-----------|---------------|------------|
1180
- | 0–31 | `SCOPE_START` / `_END` | `scope` | all three |
1181
- | 32–63 | `NULLIFIER_START` / `_END` | `nullifier_value` | all three |
1182
- | 64 | `OWNERSHIP_DISCLOSE_FLAGS` | `disclose_flags` | `mdl_kr_ownership` (97 entries) — the bitmask that was actually proved (`0x01` name, `0x02` birth, `0x04` sex, `0x08` phone) |
1183
- | 65–96 | `OWNERSHIP_OWNER_COMMIT_START` / `_END` | `owner_commit` | `mdl_kr_ownership` — `bytes32` commitment to the disclosed attributes. All zero when `disclose_flags` is `0`, which is the anonymous case |
1184
- | 64 | `AGE_THRESHOLD` | `age_threshold` | `mdl_kr_age` (66 entries) — the age the proof actually clears. Read it back; it is a public input, so confirm it is the threshold you asked for |
1185
- | 65 | `AGE_CURRENT_YEAR` | `current_year` | `mdl_kr_age` — the year the comparison was made in |
1186
- | 64–95 | `REGION_CODE_START` / `_END` | `region_code` | `mdl_kr_region` (96 entries) — `bytes32`, not a readable region name. See below |
1187
-
1188
- `region_code` is `keccak256` over the region name, UTF-8 and zero-padded to 64 bytes. The circuit proves that the region on the licence hashes to this value, but the value itself is a public input — so recompute it from the region you asked for and compare, exactly as you do with the scope:
1189
-
1190
- ```typescript
1191
- import { keccak256 } from 'ethers';
1192
-
1193
- const regionCodeFor = (region: string): string => {
1194
- const padded = new Uint8Array(64);
1195
- padded.set(new TextEncoder().encode(region));
1196
- return keccak256(padded);
1197
- };
1198
-
1199
- // regionCodeFor('경기도') — compare against indices 64–95, rebuilt with toBytes32()
1200
- ```
1201
-
1202
1114
  ## Error Handling
1203
1115
 
1204
1116
  Async SDK methods throw standard `Error` objects. The messages are stable enough to log, not to switch on:
@@ -1212,9 +1124,9 @@ try {
1212
1124
  }
1213
1125
 
1214
1126
  try {
1215
- await sdk.createRelayRequest('mdl_kr_age', { scope: 'app.com', ageThreshold: 0 });
1127
+ await sdk.createRelayRequest('coinbase-kyc', { scope: 'app.com' });
1216
1128
  } catch (err) {
1217
- // "ageThreshold is required and must be an integer between 1 and 150 (circuit: mdl_kr_age)"
1129
+ // "Unknown circuit 'coinbase-kyc'. Expected: coinbase_attestation, ..."
1218
1130
  }
1219
1131
 
1220
1132
  try {
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ProofRequest, ProofResponse, CircuitType, CircuitInputs, ProofportConfig, QRCodeOptions, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult } from './types';
5
5
  import { type OffChainVerifyOptions } from './offChainVerifier';
6
+ import { type OidcRequestContext } from './verifier';
6
7
  import type { SDKEnvironment } from './types';
7
8
  /**
8
9
  * Main SDK class for interacting with the ZKProofport mobile app.
@@ -41,6 +42,15 @@ import type { SDKEnvironment } from './types';
41
42
  export declare class ProofportSDK {
42
43
  private config;
43
44
  private pendingRequests;
45
+ /**
46
+ * requestId -> scope of the relay requests this instance created, so that
47
+ * verifyResponse* can check an OIDC proof's request-bound nonce without the
48
+ * dApp passing the request back in. Oldest entries go first past
49
+ * {@link ProofportSDK.MAX_REMEMBERED_RELAY_REQUESTS}.
50
+ */
51
+ private relayRequests;
52
+ /** Bound on {@link relayRequests}; a dApp that verifies later can pass the request explicitly. */
53
+ static readonly MAX_REMEMBERED_RELAY_REQUESTS = 1000;
44
54
  private signer;
45
55
  private relayUrl;
46
56
  private socket;
@@ -179,22 +189,6 @@ export declare class ProofportSDK {
179
189
  * falling through to another circuit's builder.
180
190
  */
181
191
  private static readonly REQUEST_BUILDERS;
182
- /**
183
- * @internal
184
- * Creates a Korea Mobile ID (mDL) proof request for any of the three
185
- * mDL predicate circuits (ownership / age / region).
186
- *
187
- * The mobile app collects the license data on-device; the dapp only
188
- * supplies the predicate parameters validated here. No wallet signature
189
- * is required for mDL circuits.
190
- *
191
- * @param circuit - 'mdl_kr_ownership' | 'mdl_kr_age' | 'mdl_kr_region'
192
- * @param inputs - Predicate inputs (see MdlKr*Inputs types)
193
- * @param options - Request configuration options
194
- *
195
- * @throws Error if the predicate inputs fail validation
196
- */
197
- private createMdlKrRequest;
198
192
  /**
199
193
  * @internal
200
194
  * Generates a deep link URL for a proof request.
@@ -497,10 +491,24 @@ export declare class ProofportSDK {
497
491
  * }
498
492
  * ```
499
493
  */
500
- verifyOnChain(circuit: CircuitType, proof: string, publicInputs: string[], providerOrSigner?: any): Promise<{
494
+ verifyOnChain(circuit: CircuitType, proof: string, publicInputs: string[], providerOrSigner?: any, options?: {
495
+ request?: OidcRequestContext;
496
+ signerVariant?: string;
497
+ }): Promise<{
501
498
  valid: boolean;
502
499
  error?: string;
503
500
  }>;
501
+ /**
502
+ * The request an `oidc_domain_attestation` response answers: the one the
503
+ * caller passed, else the relay request this instance created under the
504
+ * response's requestId. `{ error }` when there is none, or when the two
505
+ * disagree -- an OIDC proof made with a request-bound nonce is only
506
+ * meaningful against the request it was bound to, so a response this SDK
507
+ * cannot place is refused rather than checked without it.
508
+ */
509
+ private oidcRequestFor;
510
+ /** @internal Remember a relay request's scope for verifyResponse*. */
511
+ private rememberRelayRequest;
504
512
  /**
505
513
  * The verifier for a circuit: the one the dApp configured, otherwise the
506
514
  * pinned deployment on the chain this SDK's environment verifies that
@@ -516,6 +524,9 @@ export declare class ProofportSDK {
516
524
  *
517
525
  * @param response - ProofResponse object from parseResponse()
518
526
  * @param providerOrSigner - ethers v6 Provider or Signer (defaults to base mainnet public RPC)
527
+ * @param options.request - For `oidc_domain_attestation`: the `{ scope, requestId }`
528
+ * the response answers. Not needed for a relay request this SDK instance
529
+ * created; an OIDC response this instance cannot place is refused without it.
519
530
  *
520
531
  * @returns Promise resolving to verification result with valid boolean and optional error message
521
532
  *
@@ -542,7 +553,9 @@ export declare class ProofportSDK {
542
553
  * });
543
554
  * ```
544
555
  */
545
- verifyResponseOnChain(response: ProofResponse, providerOrSigner?: any): Promise<{
556
+ verifyResponseOnChain(response: ProofResponse, providerOrSigner?: any, options?: {
557
+ request?: OidcRequestContext;
558
+ }): Promise<{
546
559
  valid: boolean;
547
560
  error?: string;
548
561
  }>;
@@ -896,7 +909,7 @@ export declare class ProofportSDK {
896
909
  * @param requestId - The relay-issued request ID
897
910
  * @param options - Polling options
898
911
  * @param options.intervalMs - Polling interval in milliseconds (default: 2000)
899
- * @param options.timeoutMs - Maximum polling time in milliseconds (default: 300000 = 5 min)
912
+ * @param options.timeoutMs - Maximum polling time in milliseconds (default: 600000 = 10 min, `DEFAULT_RELAY_WAIT_MS`)
900
913
  * @param options.onStatusChange - Callback when status changes
901
914
  * @returns Promise resolving to final RelayProofResult
902
915
  * @throws Error if timeout or relay error
@@ -961,7 +974,7 @@ export declare class ProofportSDK {
961
974
  *
962
975
  * @param requestId - The relay-issued request ID
963
976
  * @param options - Configuration options
964
- * @param options.timeoutMs - Maximum wait time in ms (default: 300000 = 5 min)
977
+ * @param options.timeoutMs - Maximum wait time in ms (default: 600000 = 10 min, `DEFAULT_RELAY_WAIT_MS`)
965
978
  * @param options.onStatusChange - Callback for status updates
966
979
  * @returns Promise resolving to final RelayProofResult
967
980
  */
@@ -20,7 +20,7 @@ export declare const RELAY_URLS: Record<SDKEnvironment, string>;
20
20
  * Mirrors the mobile app's `CIRCUIT_NETWORKS` (proofport-app
21
21
  * src/config/contracts.ts), because a proof is checked where the app said its
22
22
  * verifier is: Base for the Coinbase and OIDC circuits, GIWA Sepolia for GIWA
23
- * and Dojang (testnet-only), Base Sepolia for Korea Mobile ID (testnet-only).
23
+ * and Dojang (testnet-only).
24
24
  * The ADDRESS on that chain always comes from core's pinned table, never from
25
25
  * a proof response.
26
26
  */
@@ -31,9 +31,6 @@ export declare const VERIFICATION_CHAINS: {
31
31
  readonly oidc_domain_attestation: 8453;
32
32
  readonly giwa_attestation: 91342;
33
33
  readonly dojang_playground: 91342;
34
- readonly mdl_kr_ownership: 84532;
35
- readonly mdl_kr_age: 84532;
36
- readonly mdl_kr_region: 84532;
37
34
  };
38
35
  readonly staging: {
39
36
  readonly coinbase_attestation: 84532;
@@ -41,9 +38,6 @@ export declare const VERIFICATION_CHAINS: {
41
38
  readonly oidc_domain_attestation: 84532;
42
39
  readonly giwa_attestation: 91342;
43
40
  readonly dojang_playground: 91342;
44
- readonly mdl_kr_ownership: 84532;
45
- readonly mdl_kr_age: 84532;
46
- readonly mdl_kr_region: 84532;
47
41
  };
48
42
  readonly local: {
49
43
  readonly coinbase_attestation: 84532;
@@ -51,9 +45,6 @@ export declare const VERIFICATION_CHAINS: {
51
45
  readonly oidc_domain_attestation: 84532;
52
46
  readonly giwa_attestation: 91342;
53
47
  readonly dojang_playground: 91342;
54
- readonly mdl_kr_ownership: 84532;
55
- readonly mdl_kr_age: 84532;
56
- readonly mdl_kr_region: 84532;
57
48
  };
58
49
  };
59
50
  /**
@@ -132,4 +123,4 @@ export declare const DEFAULT_REQUEST_EXPIRY_MS: number;
132
123
  * ```
133
124
  */
134
125
  export declare const MAX_QR_DATA_SIZE = 2953;
135
- export { VERIFIER_ABI, RPC_ENDPOINTS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from '@zkproofport-app/core';
126
+ export { VERIFIER_ABI, RPC_ENDPOINTS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from '@zkproofport-app/core';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Deep Link utilities for ZKProofport SDK
3
3
  */
4
- import type { ProofRequest, ProofResponse, CircuitInputs, DeepLinkComponents } from './types';
4
+ import type { ProofRequest, ProofResponse, DeepLinkComponents } from './types';
5
5
  /**
6
6
  * Generates a unique request ID for proof requests.
7
7
  *
@@ -170,23 +170,6 @@ export declare function parseDeepLink(url: string): DeepLinkComponents | null;
170
170
  * ```
171
171
  */
172
172
  export declare function isProofportDeepLink(url: string, scheme?: string): boolean;
173
- /**
174
- * Validates dapp-supplied inputs for the Korea Mobile ID (mDL) circuits.
175
- * Shared by deep-link request validation and relay request creation so the
176
- * same guards apply on every request path.
177
- *
178
- * Rules:
179
- * - `scope` — required non-empty string, ≤256 chars, no control characters
180
- * - `mdl_kr_ownership.discloseFlags` — optional integer 0–15 (0x0F bitmask)
181
- * - `mdl_kr_age.ageThreshold` — required integer 1–150
182
- * - `mdl_kr_region.targetRegion` — required non-empty string (Korean si/do),
183
- * ≤256 chars, no control characters
184
- *
185
- * @param circuit - One of the three mDL circuit identifiers
186
- * @param inputs - Dapp-supplied circuit inputs
187
- * @returns Error message string, or null when the inputs are valid
188
- */
189
- export declare function validateMdlInputs(circuit: 'mdl_kr_ownership' | 'mdl_kr_age' | 'mdl_kr_region', inputs: CircuitInputs): string | null;
190
173
  /**
191
174
  * Longest accepted `returnScheme` value.
192
175
  * Kept in sync with proofport-relay `src/returnScheme.ts`.
@@ -273,6 +256,22 @@ export declare function validateReturnScheme(value: unknown): string | null;
273
256
  * @internal
274
257
  */
275
258
  export declare function detectReturnScheme(userAgent?: string): string | undefined;
259
+ /**
260
+ * @internal
261
+ * Validates a proof request for completeness and correctness.
262
+ *
263
+ * Performs comprehensive validation including:
264
+ * - Required fields (requestId, circuit, callbackUrl)
265
+ * - Circuit type validity (must be a supported circuit)
266
+ * - Circuit-specific input validation (e.g., userAddress format, countryList structure)
267
+ * - Expiration check (if expiresAt is provided)
268
+ *
269
+ * Note: userAddress is optional for both circuits - the mobile app will prompt
270
+ * for wallet connection if not provided.
271
+ *
272
+ * @param request - Proof request to validate
273
+ * @returns Validation result with `valid` flag and optional `error` message
274
+ */
276
275
  export declare function validateProofRequest(request: ProofRequest): {
277
276
  valid: boolean;
278
277
  error?: string;
package/dist/index.d.ts CHANGED
@@ -27,15 +27,17 @@
27
27
  */
28
28
  export { ProofportSDK, default } from './ProofportSDK';
29
29
  export { extractScopeFromPublicInputs, extractNullifierFromPublicInputs, extractDomainFromPublicInputs, } from './verifier';
30
- export { CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, ALL_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, isCircuitId, isSupportedCircuitId, getCircuitSupportStatus, CIRCUIT_VK_PATHS, } from './circuits';
30
+ export { CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, ALL_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, isCircuitId, isSupportedCircuitId, getCircuitSupportStatus, CIRCUIT_VK_PATHS, CIRCUIT_PACKAGES, SIGNER_VARIANTS, isSignerVariant, signerVariantsOf, getCircuitPackage, resolveSignerVariant, evmProofByteLength, } from './circuits';
31
31
  export { verifyProofOffChain } from './offChainVerifier';
32
32
  export type { OffChainVerifyResult, OffChainVerifyOptions, } from './offChainVerifier';
33
- export { PINNED_VERIFIERS, getPinnedVerifier } from './verifier';
33
+ export { PINNED_VERIFIERS, PINNED_VARIANT_VERIFIERS, getPinnedVerifier, getPinnedVariantVerifier } from './verifier';
34
34
  export type { PinnedVerifier } from './verifier';
35
- export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, } from './constants';
36
- export type { CircuitId, CircuitSupportStatus } from './circuits';
35
+ export { checkOidcPolicy, deriveOidcNonceBinding, oidcAudienceHash, OIDC_ALLOWED_AUDIENCES, OIDC_NONCE_DOMAIN, OIDC_NOW_MAX_AGE_SECONDS, OIDC_NOW_MAX_FUTURE_SECONDS, OIDC_V1_PUBLIC_INPUT_COUNT, OIDC_V2_PUBLIC_INPUT_COUNT, } from './verifier';
36
+ export type { OidcRequestContext, OidcAudience, OidcNonceMode } from './verifier';
37
+ export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, } from './constants';
38
+ export type { CircuitId, CircuitSupportStatus, SignerVariant, CircuitPackage } from './circuits';
37
39
  export { CIRCUIT_ACTION_BINDING, circuitActionBinding, } from './circuits';
38
40
  export type { CircuitActionBinding } from './circuits';
39
- export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, ActionBoundInputs, GiwaAttestationInputs, DojangPlaygroundInputs, TypedAction, CoinbaseCountryInputs, OidcDomainInputs, MdlKrOwnershipInputs, MdlKrAgeInputs, MdlKrRegionInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, RelayProofStatus, RelayTerminalStatus, SDKEnvironment, } from './types';
40
- export { RELAY_TERMINAL_STATUSES, isTerminalRelayStatus } from './relayStatus';
41
+ export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, ActionBoundInputs, GiwaAttestationInputs, DojangPlaygroundInputs, TypedAction, CoinbaseCountryInputs, OidcDomainInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, RelayProofStatus, RelayTerminalStatus, SDKEnvironment, } from './types';
42
+ export { RELAY_TERMINAL_STATUSES, isTerminalRelayStatus, DEFAULT_RELAY_WAIT_MS } from './relayStatus';
41
43
  export { validateTypedAction, parseTypedAction } from './typedAction';