@zkproofport-app/sdk 0.3.0 → 0.3.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/dist/circuits.mjs CHANGED
@@ -44,7 +44,7 @@ const CIRCUIT_IDS = Object.freeze({
44
44
  COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
45
45
  /** OIDC email-domain attestation. Officially supported. */
46
46
  OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
47
- /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
47
+ /** Arc eligibility: exact EIP-712 action authorization. Experimental on Arc Testnet. */
48
48
  ARC_ELIGIBILITY: 'arc_eligibility',
49
49
  /** GIWA attestation. Planned — not officially supported yet. */
50
50
  GIWA_ATTESTATION: 'giwa_attestation',
@@ -75,7 +75,11 @@ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
75
75
  coinbase_country_attestation: 'supported',
76
76
  oidc_domain_attestation: 'supported',
77
77
  arc_eligibility: 'experimental',
78
- giwa_attestation: 'planned',
78
+ // Experimental, not planned: the circuit is built, its verifier is
79
+ // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
80
+ // 'supported' is that it is testnet-only, so the app offers it behind
81
+ // Developer Mode and reads its files from main rather than a release tag.
82
+ giwa_attestation: 'experimental',
79
83
  mdl_kr_ownership: 'planned',
80
84
  mdl_kr_age: 'planned',
81
85
  mdl_kr_region: 'planned',
@@ -187,18 +191,44 @@ function getCircuitSupportStatus(circuit) {
187
191
  }
188
192
  return CIRCUIT_SUPPORT_STATUS[circuit];
189
193
  }
194
+ const CIRCUIT_ACTION_BINDING = {
195
+ coinbase_attestation: 'none',
196
+ coinbase_country_attestation: 'none',
197
+ // The signature lives inside the identity token; no wallet, no action.
198
+ oidc_domain_attestation: 'none',
199
+ // Both branch in-circuit: with an action the wallet signs the typed data,
200
+ // without one it personal_signs the request's signal hash. Arc read
201
+ // 'required' until 2026-09-22 because its circuit had no second path -- a
202
+ // request without an action built a vector 64 bytes short and died inside
203
+ // the prover. The circuit gained the branch; the table follows it.
204
+ arc_eligibility: 'optional',
205
+ giwa_attestation: 'optional',
206
+ mdl_kr_ownership: 'none',
207
+ mdl_kr_age: 'none',
208
+ mdl_kr_region: 'none',
209
+ };
210
+ /*
211
+ * The union is declared above rather than derived from the table's values.
212
+ *
213
+ * Deriving it narrowed the type to whatever the table happens to hold today --
214
+ * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
215
+ * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
216
+ * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
217
+ * and nothing else), so the union says so and the table is checked against it.
218
+ */
190
219
  /**
191
- * Whether a circuit's relay request must carry a wallet signature.
192
- *
193
- * The SDK checks this to fail fast — "Signer not set" locally beats a 401 from
194
- * the relay two round trips later, with nothing saying which circuit needed
195
- * what. The relay declares its OWN copy on purpose and does not import this
196
- * one: it is a security boundary, and an auth policy that a published package
197
- * can change is not a policy. A test in the relay asserts the two agree.
220
+ * How this circuit treats an action, or an error naming the id that is not one.
198
221
  *
199
- * `satisfies` and not an annotation, so a circuit added above is a compile
200
- * error here until somebody decides which it is.
222
+ * No default: a circuit nobody has classified must not inherit 'none', which
223
+ * would silently drop an action a dapp meant to bind.
201
224
  */
225
+ function circuitActionBinding(circuit) {
226
+ const binding = CIRCUIT_ACTION_BINDING[circuit];
227
+ if (!binding) {
228
+ throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
229
+ }
230
+ return binding;
231
+ }
202
232
  const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
203
233
  coinbase_attestation: true,
204
234
  coinbase_country_attestation: true,
@@ -211,8 +241,12 @@ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
211
241
  mdl_kr_ownership: false,
212
242
  mdl_kr_age: false,
213
243
  mdl_kr_region: false,
214
- giwa_attestation: false,
244
+ // A GIWA-attested wallet signing an EIP-712 action, the same shape as Arc.
245
+ // This read `false` while the circuit had always verified a wallet signature
246
+ // -- it sat under the Web2 comment above and was never revisited. A request
247
+ // for it would have reached the relay with no signer.
248
+ giwa_attestation: true,
215
249
  };
216
250
 
217
- export { ALL_CIRCUIT_IDS, CIRCUIT_IDS, CIRCUIT_NEEDS_WALLET_SIGNATURE, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId };
251
+ export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_NEEDS_WALLET_SIGNATURE, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, EXPERIMENTAL_CIRCUIT_IDS, PLANNED_CIRCUIT_IDS, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId };
218
252
  //# sourceMappingURL=circuits.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"circuits.mjs","sources":["../src/circuits.ts"],"sourcesContent":["/**\n * Canonical ZKProofport circuit identifiers.\n *\n * This module is the single source of truth for circuit IDs. Every other layer\n * — the mobile app, the demo, the relay, server-side provers — reads its list\n * from here instead of keeping a private copy that silently drifts.\n *\n * It has **no imports and no dependencies**, so it can be pulled in on its own:\n *\n * ```typescript\n * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';\n * ```\n *\n * The same names are also re-exported from the package root\n * (`@zkproofport-app/sdk`) for callers that already import the SDK.\n *\n * ## Identifiers are canonical and case-sensitive\n *\n * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is\n * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,\n * `mdl-kr-age`) are directory or UI route names in other repositories — they\n * are **not** circuit IDs, and passing one produces a nullifier mismatch or a\n * failed on-chain lookup rather than a clear error.\n */\n\n/**\n * Whether a circuit is officially supported today or still on the roadmap.\n *\n * - `supported` — generally available. Safe to build a product on.\n * - `planned` — the identifier is reserved and the circuit exists, but it is\n * not officially supported yet. Availability, inputs and public-input layout\n * may change without a major version bump.\n */\nexport type CircuitSupportStatus = 'supported' | 'experimental' | 'planned';\n\n/**\n * Canonical circuit identifiers, keyed by a stable constant name.\n *\n * Prefer these constants over string literals so a rename is a compile error\n * rather than a runtime surprise.\n *\n * @example\n * ```typescript\n * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';\n *\n * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {\n * scope: 'myapp.com',\n * });\n * ```\n */\nexport const CIRCUIT_IDS = Object.freeze({\n /** Coinbase KYC attestation. Officially supported. */\n COINBASE_ATTESTATION: 'coinbase_attestation',\n /** Coinbase country attestation (inclusion / exclusion). Officially supported. */\n COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',\n /** OIDC email-domain attestation. Officially supported. */\n OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',\n /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */\n ARC_ELIGIBILITY: 'arc_eligibility',\n /** GIWA attestation. Planned — not officially supported yet. */\n GIWA_ATTESTATION: 'giwa_attestation',\n /** Korea Mobile ID ownership. Planned — not officially supported yet. */\n MDL_KR_OWNERSHIP: 'mdl_kr_ownership',\n /** Korea Mobile ID age threshold. Planned — not officially supported yet. */\n MDL_KR_AGE: 'mdl_kr_age',\n /** Korea Mobile ID si/do region. Planned — not officially supported yet. */\n MDL_KR_REGION: 'mdl_kr_region',\n} as const);\n\n/**\n * Union of every canonical circuit identifier.\n *\n * `CircuitType` (exported from the package root) is an alias of this type — the\n * two names refer to the same union, and this module is where it is defined.\n */\nexport type CircuitId = (typeof CIRCUIT_IDS)[keyof typeof CIRCUIT_IDS];\n\n/**\n * Support status for every circuit.\n *\n * The record is exhaustive over {@link CircuitId}: adding an identifier without\n * giving it a status is a compile error, so this list cannot drift from\n * {@link CIRCUIT_IDS}.\n *\n * @example\n * ```typescript\n * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';\n *\n * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'\n * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'\n * ```\n */\nexport const CIRCUIT_SUPPORT_STATUS: Readonly<Record<CircuitId, CircuitSupportStatus>> =\n Object.freeze({\n coinbase_attestation: 'supported',\n coinbase_country_attestation: 'supported',\n oidc_domain_attestation: 'supported',\n arc_eligibility: 'experimental',\n giwa_attestation: 'planned',\n mdl_kr_ownership: 'planned',\n mdl_kr_age: 'planned',\n mdl_kr_region: 'planned',\n } as const);\n\n/**\n * Where each circuit's verification key FILE lives inside the\n * `zkproofport/circuits` repository, relative to its root.\n *\n * Off-chain verification needs the verification key and nothing else — no\n * circuit bytecode, no witness. The key is public by nature: it is what anyone\n * checking a proof must already have, and the Solidity verifiers deployed\n * on-chain embed the same key.\n *\n * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY\n * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.\n * These paths were first copied here from the mobile app, which keeps the\n * directory and the filename as two fields and joins them — so taking only the\n * directory produced a URL that answers 404, and the first browser to try\n * off-chain verification said \"Could not fetch the verification key (HTTP 404)\".\n *\n * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is\n * not listed here fails to compile rather than failing at verification time,\n * where the only symptom would be a proof that cannot be checked.\n */\nexport const CIRCUIT_VK_PATHS: Readonly<Record<CircuitId, string>> = Object.freeze({\n coinbase_attestation: 'coinbase-attestation/target/vk/vk',\n coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',\n oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',\n arc_eligibility: 'arc-eligibility/target/vk/vk',\n giwa_attestation: 'giwa-attestation/target/vk/vk',\n mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',\n mdl_kr_age: 'mdl/kr-age/target/vk/vk',\n mdl_kr_region: 'mdl/kr-region/target/vk/vk',\n} as const);\n\n/**\n * Every canonical circuit identifier, in declaration order.\n *\n * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}\n * when you only want the ones that are officially supported today.\n */\nexport const ALL_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n Object.values(CIRCUIT_IDS) as CircuitId[]\n);\n\n/**\n * Circuits that are officially supported today: the two Coinbase circuits and\n * OIDC domain attestation.\n *\n * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.\n */\nexport const SUPPORTED_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported')\n);\n\n/**\n * Circuits whose identifiers are reserved but which are not officially\n * supported yet: GIWA attestation and the three Korea Mobile ID circuits.\n *\n * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.\n */\n/**\n * Circuits you can prove today, on a testnet, with everything subject to change.\n *\n * `arc_eligibility` is the reason this status exists. Calling it `planned`\n * would be wrong — the circuit compiles, the verifier is deployed and a proof\n * verifies against it — and calling it `supported` would be worse, because the\n * only deployment is Arc Testnet, Circle has not published a mainnet chain id,\n * and the public-input layout is still open. Neither existing word was true, so\n * the honest move was a third word rather than picking the closer lie.\n */\nexport const EXPERIMENTAL_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'),\n);\n\nexport const PLANNED_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned')\n);\n\n/**\n * Narrows an unknown value to a canonical circuit identifier.\n *\n * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which\n * are not circuit IDs.\n *\n * @example\n * ```typescript\n * isCircuitId('coinbase_attestation'); // true\n * isCircuitId('coinbase-kyc'); // false\n * ```\n */\nexport function isCircuitId(value: unknown): value is CircuitId {\n return typeof value === 'string' && (ALL_CIRCUIT_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Narrows an unknown value to a circuit identifier that is officially supported\n * today. Planned circuits return `false`.\n *\n * @example\n * ```typescript\n * isSupportedCircuitId('oidc_domain_attestation'); // true\n * isSupportedCircuitId('mdl_kr_age'); // false — planned\n * ```\n */\nexport function isSupportedCircuitId(value: unknown): value is CircuitId {\n return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';\n}\n\n/**\n * Returns the support status of a circuit.\n *\n * @param circuit - A canonical circuit identifier.\n * @throws Error when the value is not a canonical circuit identifier.\n *\n * @example\n * ```typescript\n * getCircuitSupportStatus('giwa_attestation'); // 'planned'\n * ```\n */\nexport function getCircuitSupportStatus(circuit: CircuitId): CircuitSupportStatus {\n if (!isCircuitId(circuit)) {\n throw new Error(\n `Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`\n );\n }\n return CIRCUIT_SUPPORT_STATUS[circuit];\n}\n\n/**\n * Whether a circuit's relay request must carry a wallet signature.\n *\n * The SDK checks this to fail fast — \"Signer not set\" locally beats a 401 from\n * the relay two round trips later, with nothing saying which circuit needed\n * what. The relay declares its OWN copy on purpose and does not import this\n * one: it is a security boundary, and an auth policy that a published package\n * can change is not a policy. A test in the relay asserts the two agree.\n *\n * `satisfies` and not an annotation, so a circuit added above is a compile\n * error here until somebody decides which it is.\n */\nexport const CIRCUIT_NEEDS_WALLET_SIGNATURE = {\n coinbase_attestation: true,\n coinbase_country_attestation: true,\n // Same Coinbase-attested wallet as above; what changes is that it signs an\n // EIP-712 action rather than signal_hash.\n arc_eligibility: true,\n // The signature is inside the OIDC identity token, checked by the prover.\n oidc_domain_attestation: false,\n // Web2 (OmniOne CX) flows with no wallet binding at all.\n mdl_kr_ownership: false,\n mdl_kr_age: false,\n mdl_kr_region: false,\n giwa_attestation: false,\n} satisfies Record<CircuitId, boolean>;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"circuits.mjs","sources":["../src/circuits.ts"],"sourcesContent":["/**\n * Canonical ZKProofport circuit identifiers.\n *\n * This module is the single source of truth for circuit IDs. Every other layer\n * — the mobile app, the demo, the relay, server-side provers — reads its list\n * from here instead of keeping a private copy that silently drifts.\n *\n * It has **no imports and no dependencies**, so it can be pulled in on its own:\n *\n * ```typescript\n * import { CIRCUIT_IDS, isCircuitId } from '@zkproofport-app/sdk/circuits';\n * ```\n *\n * The same names are also re-exported from the package root\n * (`@zkproofport-app/sdk`) for callers that already import the SDK.\n *\n * ## Identifiers are canonical and case-sensitive\n *\n * Each ID is the circuit's `name` in its `Nargo.toml`, verbatim. The format is\n * lowercase with underscores. Hyphenated spellings (`coinbase-kyc`,\n * `mdl-kr-age`) are directory or UI route names in other repositories — they\n * are **not** circuit IDs, and passing one produces a nullifier mismatch or a\n * failed on-chain lookup rather than a clear error.\n */\n\n/**\n * Whether a circuit is officially supported today or still on the roadmap.\n *\n * - `supported` — generally available. Safe to build a product on.\n * - `planned` — the identifier is reserved and the circuit exists, but it is\n * not officially supported yet. Availability, inputs and public-input layout\n * may change without a major version bump.\n */\nexport type CircuitSupportStatus = 'supported' | 'experimental' | 'planned';\n\n/**\n * Canonical circuit identifiers, keyed by a stable constant name.\n *\n * Prefer these constants over string literals so a rename is a compile error\n * rather than a runtime surprise.\n *\n * @example\n * ```typescript\n * import { CIRCUIT_IDS } from '@zkproofport-app/sdk/circuits';\n *\n * const relay = await sdk.createRelayRequest(CIRCUIT_IDS.COINBASE_ATTESTATION, {\n * scope: 'myapp.com',\n * });\n * ```\n */\nexport const CIRCUIT_IDS = Object.freeze({\n /** Coinbase KYC attestation. Officially supported. */\n COINBASE_ATTESTATION: 'coinbase_attestation',\n /** Coinbase country attestation (inclusion / exclusion). Officially supported. */\n COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',\n /** OIDC email-domain attestation. Officially supported. */\n OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',\n /** Arc eligibility: exact EIP-712 action authorization. Experimental on Arc Testnet. */\n ARC_ELIGIBILITY: 'arc_eligibility',\n /** GIWA attestation. Planned — not officially supported yet. */\n GIWA_ATTESTATION: 'giwa_attestation',\n /** Korea Mobile ID ownership. Planned — not officially supported yet. */\n MDL_KR_OWNERSHIP: 'mdl_kr_ownership',\n /** Korea Mobile ID age threshold. Planned — not officially supported yet. */\n MDL_KR_AGE: 'mdl_kr_age',\n /** Korea Mobile ID si/do region. Planned — not officially supported yet. */\n MDL_KR_REGION: 'mdl_kr_region',\n} as const);\n\n/**\n * Union of every canonical circuit identifier.\n *\n * `CircuitType` (exported from the package root) is an alias of this type — the\n * two names refer to the same union, and this module is where it is defined.\n */\nexport type CircuitId = (typeof CIRCUIT_IDS)[keyof typeof CIRCUIT_IDS];\n\n/**\n * Support status for every circuit.\n *\n * The record is exhaustive over {@link CircuitId}: adding an identifier without\n * giving it a status is a compile error, so this list cannot drift from\n * {@link CIRCUIT_IDS}.\n *\n * @example\n * ```typescript\n * import { CIRCUIT_SUPPORT_STATUS } from '@zkproofport-app/sdk/circuits';\n *\n * CIRCUIT_SUPPORT_STATUS.coinbase_attestation; // 'supported'\n * CIRCUIT_SUPPORT_STATUS.mdl_kr_age; // 'planned'\n * ```\n */\nexport const CIRCUIT_SUPPORT_STATUS: Readonly<Record<CircuitId, CircuitSupportStatus>> =\n Object.freeze({\n coinbase_attestation: 'supported',\n coinbase_country_attestation: 'supported',\n oidc_domain_attestation: 'supported',\n arc_eligibility: 'experimental',\n // Experimental, not planned: the circuit is built, its verifier is\n // deployed on GIWA Sepolia and the demo requests it. What keeps it out of\n // 'supported' is that it is testnet-only, so the app offers it behind\n // Developer Mode and reads its files from main rather than a release tag.\n giwa_attestation: 'experimental',\n mdl_kr_ownership: 'planned',\n mdl_kr_age: 'planned',\n mdl_kr_region: 'planned',\n } as const);\n\n/**\n * Where each circuit's verification key FILE lives inside the\n * `zkproofport/circuits` repository, relative to its root.\n *\n * Off-chain verification needs the verification key and nothing else — no\n * circuit bytecode, no witness. The key is public by nature: it is what anyone\n * checking a proof must already have, and the Solidity verifiers deployed\n * on-chain embed the same key.\n *\n * NOTE THE TRAILING `/vk` TWICE. In that repository `target/vk` is a DIRECTORY\n * holding `vk`, `vk_hash` and `SHA256SUMS`; the key itself is `target/vk/vk`.\n * These paths were first copied here from the mobile app, which keeps the\n * directory and the filename as two fields and joins them — so taking only the\n * directory produced a URL that answers 404, and the first browser to try\n * off-chain verification said \"Could not fetch the verification key (HTTP 404)\".\n *\n * Exhaustive over {@link CircuitId} on purpose. A new circuit whose key path is\n * not listed here fails to compile rather than failing at verification time,\n * where the only symptom would be a proof that cannot be checked.\n */\nexport const CIRCUIT_VK_PATHS: Readonly<Record<CircuitId, string>> = Object.freeze({\n coinbase_attestation: 'coinbase-attestation/target/vk/vk',\n coinbase_country_attestation: 'coinbase-country-attestation/target/vk/vk',\n oidc_domain_attestation: 'oidc-domain-attestation/target/vk/vk',\n arc_eligibility: 'arc-eligibility/target/vk/vk',\n giwa_attestation: 'giwa-attestation/target/vk/vk',\n mdl_kr_ownership: 'mdl/kr-ownership/target/vk/vk',\n mdl_kr_age: 'mdl/kr-age/target/vk/vk',\n mdl_kr_region: 'mdl/kr-region/target/vk/vk',\n} as const);\n\n/**\n * Every canonical circuit identifier, in declaration order.\n *\n * Includes circuits that are still `planned`. Use {@link SUPPORTED_CIRCUIT_IDS}\n * when you only want the ones that are officially supported today.\n */\nexport const ALL_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n Object.values(CIRCUIT_IDS) as CircuitId[]\n);\n\n/**\n * Circuits that are officially supported today: the two Coinbase circuits and\n * OIDC domain attestation.\n *\n * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.\n */\nexport const SUPPORTED_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'supported')\n);\n\n/**\n * Circuits whose identifiers are reserved but which are not officially\n * supported yet: GIWA attestation and the three Korea Mobile ID circuits.\n *\n * Derived from {@link CIRCUIT_SUPPORT_STATUS}, so it cannot fall out of sync.\n */\n/**\n * Circuits you can prove today, on a testnet, with everything subject to change.\n *\n * `arc_eligibility` is the reason this status exists. Calling it `planned`\n * would be wrong — the circuit compiles, the verifier is deployed and a proof\n * verifies against it — and calling it `supported` would be worse, because the\n * only deployment is Arc Testnet, Circle has not published a mainnet chain id,\n * and the public-input layout is still open. Neither existing word was true, so\n * the honest move was a third word rather than picking the closer lie.\n */\nexport const EXPERIMENTAL_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter(id => CIRCUIT_SUPPORT_STATUS[id] === 'experimental'),\n);\n\nexport const PLANNED_CIRCUIT_IDS: readonly CircuitId[] = Object.freeze(\n ALL_CIRCUIT_IDS.filter((id) => CIRCUIT_SUPPORT_STATUS[id] === 'planned')\n);\n\n/**\n * Narrows an unknown value to a canonical circuit identifier.\n *\n * Returns `false` for hyphenated route names such as `'coinbase-kyc'`, which\n * are not circuit IDs.\n *\n * @example\n * ```typescript\n * isCircuitId('coinbase_attestation'); // true\n * isCircuitId('coinbase-kyc'); // false\n * ```\n */\nexport function isCircuitId(value: unknown): value is CircuitId {\n return typeof value === 'string' && (ALL_CIRCUIT_IDS as readonly string[]).includes(value);\n}\n\n/**\n * Narrows an unknown value to a circuit identifier that is officially supported\n * today. Planned circuits return `false`.\n *\n * @example\n * ```typescript\n * isSupportedCircuitId('oidc_domain_attestation'); // true\n * isSupportedCircuitId('mdl_kr_age'); // false — planned\n * ```\n */\nexport function isSupportedCircuitId(value: unknown): value is CircuitId {\n return isCircuitId(value) && CIRCUIT_SUPPORT_STATUS[value] === 'supported';\n}\n\n/**\n * Returns the support status of a circuit.\n *\n * @param circuit - A canonical circuit identifier.\n * @throws Error when the value is not a canonical circuit identifier.\n *\n * @example\n * ```typescript\n * getCircuitSupportStatus('giwa_attestation'); // 'planned'\n * ```\n */\nexport function getCircuitSupportStatus(circuit: CircuitId): CircuitSupportStatus {\n if (!isCircuitId(circuit)) {\n throw new Error(\n `Unknown circuit '${String(circuit)}'. Expected one of: ${ALL_CIRCUIT_IDS.join(', ')}`\n );\n }\n return CIRCUIT_SUPPORT_STATUS[circuit];\n}\n\n/**\n * Whether a circuit's relay request must carry a wallet signature.\n *\n * The SDK checks this to fail fast — \"Signer not set\" locally beats a 401 from\n * the relay two round trips later, with nothing saying which circuit needed\n * what. The relay declares its OWN copy on purpose and does not import this\n * one: it is a security boundary, and an auth policy that a published package\n * can change is not a policy. A test in the relay asserts the two agree.\n *\n * `satisfies` and not an annotation, so a circuit added above is a compile\n * error here until somebody decides which it is.\n */\n/**\n * Whether a circuit's proof can bind an EIP-712 action, and whether it must.\n *\n * ONE TABLE, because the answer was becoming a chain of `circuit === '...'`\n * tests. It was asked in the deep-link validator and again in\n * `createRelayRequest`, both spelling out `arc_eligibility`, and a second\n * action-capable circuit would have made each of them a two-branch chain that\n * ends on whatever the last test happens to be.\n *\n * 'required' the circuit proves nothing without an action -- the wallet\n * signs the typed data and there is no other message.\n * 'optional' the circuit signs the action when one is supplied and\n * personal_signs its signal hash when one is not. The circuit\n * itself branches; see giwa-attestation/src/main.nr.\n * 'none' the circuit has no action inputs. An action sent anyway is a\n * caller misunderstanding, and is refused rather than dropped:\n * silently ignoring it would let a dapp believe a proof\n * authorized something it never mentioned.\n */\nexport type CircuitActionBinding = 'none' | 'required' | 'optional';\n\nexport const CIRCUIT_ACTION_BINDING = {\n coinbase_attestation: 'none',\n coinbase_country_attestation: 'none',\n // The signature lives inside the identity token; no wallet, no action.\n oidc_domain_attestation: 'none',\n // Both branch in-circuit: with an action the wallet signs the typed data,\n // without one it personal_signs the request's signal hash. Arc read\n // 'required' until 2026-09-22 because its circuit had no second path -- a\n // request without an action built a vector 64 bytes short and died inside\n // the prover. The circuit gained the branch; the table follows it.\n arc_eligibility: 'optional',\n giwa_attestation: 'optional',\n mdl_kr_ownership: 'none',\n mdl_kr_age: 'none',\n mdl_kr_region: 'none',\n} satisfies Record<CircuitId, CircuitActionBinding>;\n\n/*\n * The union is declared above rather than derived from the table's values.\n *\n * Deriving it narrowed the type to whatever the table happens to hold today --\n * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`\n * check impossible. The publish typecheck caught it; `rollup` and `vitest` did\n * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path\n * and nothing else), so the union says so and the table is checked against it.\n */\n\n/**\n * How this circuit treats an action, or an error naming the id that is not one.\n *\n * No default: a circuit nobody has classified must not inherit 'none', which\n * would silently drop an action a dapp meant to bind.\n */\nexport function circuitActionBinding(circuit: string): CircuitActionBinding {\n const binding = (CIRCUIT_ACTION_BINDING as Record<string, CircuitActionBinding | undefined>)[circuit];\n if (!binding) {\n throw new Error(\n `Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`,\n );\n }\n return binding;\n}\n\nexport const CIRCUIT_NEEDS_WALLET_SIGNATURE = {\n coinbase_attestation: true,\n coinbase_country_attestation: true,\n // Same Coinbase-attested wallet as above; what changes is that it signs an\n // EIP-712 action rather than signal_hash.\n arc_eligibility: true,\n // The signature is inside the OIDC identity token, checked by the prover.\n oidc_domain_attestation: false,\n // Web2 (OmniOne CX) flows with no wallet binding at all.\n mdl_kr_ownership: false,\n mdl_kr_age: false,\n mdl_kr_region: false,\n // A GIWA-attested wallet signing an EIP-712 action, the same shape as Arc.\n // This read `false` while the circuit had always verified a wallet signature\n // -- it sat under the Web2 comment above and was never revisited. A request\n // for it would have reached the relay with no signer.\n giwa_attestation: true,\n} satisfies Record<CircuitId, boolean>;\n"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -32,5 +32,7 @@ export { verifyProofOffChain } from './offChainVerifier';
32
32
  export type { OffChainVerifyResult, OffChainVerifyOptions, } from './offChainVerifier';
33
33
  export { COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, MDL_KR_PUBLIC_INPUT_LAYOUT, } from './constants';
34
34
  export type { CircuitId, CircuitSupportStatus } from './circuits';
35
- export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, ArcEligibilityInputs, TypedAction, CoinbaseCountryInputs, OidcDomainInputs, MdlKrOwnershipInputs, MdlKrAgeInputs, MdlKrRegionInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, SDKEnvironment, } from './types';
35
+ export { CIRCUIT_ACTION_BINDING, circuitActionBinding, } from './circuits';
36
+ export type { CircuitActionBinding } from './circuits';
37
+ export type { CircuitType, ProofRequestStatus, CoinbaseKycInputs, ActionBoundInputs, ArcEligibilityInputs, GiwaAttestationInputs, TypedAction, CoinbaseCountryInputs, OidcDomainInputs, MdlKrOwnershipInputs, MdlKrAgeInputs, MdlKrRegionInputs, CircuitInputs, ProofRequest, ProofResponse, QRCodeOptions, VerifierContract, ProofportConfig, ChallengeResponse, WalletSigner, RelayProofRequest, RelayProofResult, SDKEnvironment, } from './types';
36
38
  export { validateTypedAction, parseTypedAction } from './typedAction';
package/dist/index.esm.js CHANGED
@@ -91,16 +91,22 @@ const CIRCUIT_METADATA = {
91
91
  },
92
92
  // Planned circuit — the identifier is reserved and the circuit exists, but it
93
93
  // is not officially supported yet. See CIRCUIT_SUPPORT_STATUS in ./circuits.
94
- // Forked from coinbase_attestation and shares its four public inputs, so the
95
- // count is the byte-flattened total (4 x [u8; 32] = 128), matching the
96
- // COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT indices below. The
97
- // coinbase_attestation entry above reports a legacy logical count of 2 and is
98
- // left as-is for backwards compatibility.
94
+ // Forked from coinbase_attestation, then given the EIP-712 pair and a
95
+ // branch: with an action the wallet signs the typed data and signal_hash is
96
+ // zero; without one it signs signal_hash and the pair is zero. Read off the
97
+ // compiled ABI (6 x [u8; 32] = 192): signal_hash 0-31, domain_separator
98
+ // 32-63, action_hash 64-95, signer_list_merkle_root 96-127, scope 128-159,
99
+ // nullifier 160-191 -- the same order as arc_eligibility.
100
+ //
101
+ // Unlike Arc, signal_hash is NOT part of this circuit's nullifier. That one
102
+ // derives its nullifier from a public input nobody signs and nothing
103
+ // constrains, so one wallet can mint as many nullifiers as it likes; here
104
+ // the secret is the address plus a constant compiled into the circuit.
99
105
  giwa_attestation: {
100
106
  name: 'GIWA Attestation',
101
- description: 'Prove GIWA identity verification (planned — not officially supported yet)',
102
- publicInputsCount: 128,
103
- publicInputNames: ['signal_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
107
+ description: 'Prove GIWA identity verification, optionally authorizing one EIP-712 action (planned — not officially supported yet)',
108
+ publicInputsCount: 192,
109
+ publicInputNames: ['signal_hash', 'domain_separator', 'action_hash', 'signer_list_merkle_root', 'scope', 'nullifier'],
104
110
  },
105
111
  // Korea Mobile ID (mDL) circuits. Planned — not officially supported yet.
106
112
  // Counts are byte-flattened field totals from the compiled circuit ABIs:
@@ -167,6 +173,11 @@ const RPC_ENDPOINTS = {
167
173
  84532: 'https://sepolia.base.org', // Base Sepolia
168
174
  8453: 'https://mainnet.base.org', // Base Mainnet
169
175
  91342: 'https://sepolia-rpc.giwa.io', // GIWA Sepolia
176
+ // Arc Testnet. The arc_eligibility verifier is deployed here and nowhere
177
+ // else, and this line was missing on 2026-09-12 — the circuit shipped, a
178
+ // phone produced a valid proof, and on-chain verification answered "No RPC
179
+ // endpoint configured for chain 5042002", which reads like the proof's fault.
180
+ 5042002: 'https://rpc.testnet.arc.io', // Arc Testnet
170
181
  };
171
182
  /**
172
183
  * Default proof request expiration time in milliseconds.
@@ -374,7 +385,7 @@ const CIRCUIT_IDS = Object.freeze({
374
385
  COINBASE_COUNTRY_ATTESTATION: 'coinbase_country_attestation',
375
386
  /** OIDC email-domain attestation. Officially supported. */
376
387
  OIDC_DOMAIN_ATTESTATION: 'oidc_domain_attestation',
377
- /** Arc eligibility: one EIP-712 action, authorized. Officially supported. */
388
+ /** Arc eligibility: exact EIP-712 action authorization. Experimental on Arc Testnet. */
378
389
  ARC_ELIGIBILITY: 'arc_eligibility',
379
390
  /** GIWA attestation. Planned — not officially supported yet. */
380
391
  GIWA_ATTESTATION: 'giwa_attestation',
@@ -405,7 +416,11 @@ const CIRCUIT_SUPPORT_STATUS = Object.freeze({
405
416
  coinbase_country_attestation: 'supported',
406
417
  oidc_domain_attestation: 'supported',
407
418
  arc_eligibility: 'experimental',
408
- giwa_attestation: 'planned',
419
+ // Experimental, not planned: the circuit is built, its verifier is
420
+ // deployed on GIWA Sepolia and the demo requests it. What keeps it out of
421
+ // 'supported' is that it is testnet-only, so the app offers it behind
422
+ // Developer Mode and reads its files from main rather than a release tag.
423
+ giwa_attestation: 'experimental',
409
424
  mdl_kr_ownership: 'planned',
410
425
  mdl_kr_age: 'planned',
411
426
  mdl_kr_region: 'planned',
@@ -517,18 +532,44 @@ function getCircuitSupportStatus(circuit) {
517
532
  }
518
533
  return CIRCUIT_SUPPORT_STATUS[circuit];
519
534
  }
535
+ const CIRCUIT_ACTION_BINDING = {
536
+ coinbase_attestation: 'none',
537
+ coinbase_country_attestation: 'none',
538
+ // The signature lives inside the identity token; no wallet, no action.
539
+ oidc_domain_attestation: 'none',
540
+ // Both branch in-circuit: with an action the wallet signs the typed data,
541
+ // without one it personal_signs the request's signal hash. Arc read
542
+ // 'required' until 2026-09-22 because its circuit had no second path -- a
543
+ // request without an action built a vector 64 bytes short and died inside
544
+ // the prover. The circuit gained the branch; the table follows it.
545
+ arc_eligibility: 'optional',
546
+ giwa_attestation: 'optional',
547
+ mdl_kr_ownership: 'none',
548
+ mdl_kr_age: 'none',
549
+ mdl_kr_region: 'none',
550
+ };
551
+ /*
552
+ * The union is declared above rather than derived from the table's values.
553
+ *
554
+ * Deriving it narrowed the type to whatever the table happens to hold today --
555
+ * 'none' | 'optional' -- and TypeScript then called every `=== 'required'`
556
+ * check impossible. The publish typecheck caught it; `rollup` and `vitest` did
557
+ * not. 'required' is a state a circuit can legitimately be in (an EIP-712 path
558
+ * and nothing else), so the union says so and the table is checked against it.
559
+ */
520
560
  /**
521
- * Whether a circuit's relay request must carry a wallet signature.
561
+ * How this circuit treats an action, or an error naming the id that is not one.
522
562
  *
523
- * The SDK checks this to fail fast — "Signer not set" locally beats a 401 from
524
- * the relay two round trips later, with nothing saying which circuit needed
525
- * what. The relay declares its OWN copy on purpose and does not import this
526
- * one: it is a security boundary, and an auth policy that a published package
527
- * can change is not a policy. A test in the relay asserts the two agree.
528
- *
529
- * `satisfies` and not an annotation, so a circuit added above is a compile
530
- * error here until somebody decides which it is.
563
+ * No default: a circuit nobody has classified must not inherit 'none', which
564
+ * would silently drop an action a dapp meant to bind.
531
565
  */
566
+ function circuitActionBinding(circuit) {
567
+ const binding = CIRCUIT_ACTION_BINDING[circuit];
568
+ if (!binding) {
569
+ throw new Error(`Unknown circuit '${circuit}'. Expected: ${ALL_CIRCUIT_IDS.join(', ')}`);
570
+ }
571
+ return binding;
572
+ }
532
573
  const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
533
574
  coinbase_attestation: true,
534
575
  coinbase_country_attestation: true,
@@ -541,13 +582,22 @@ const CIRCUIT_NEEDS_WALLET_SIGNATURE = {
541
582
  mdl_kr_ownership: false,
542
583
  mdl_kr_age: false,
543
584
  mdl_kr_region: false,
544
- giwa_attestation: false,
585
+ // A GIWA-attested wallet signing an EIP-712 action, the same shape as Arc.
586
+ // This read `false` while the circuit had always verified a wallet signature
587
+ // -- it sat under the Web2 comment above and was never revisited. A request
588
+ // for it would have reached the relay with no signer.
589
+ giwa_attestation: true,
545
590
  };
546
591
 
547
592
  const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
548
593
  /**
549
594
  * The reason the value is not a usable action, or `null` when it is one.
550
595
  *
596
+ * Structural validation only, not application authorization policy. Consumers
597
+ * (e.g. EligibilityGate) must independently enforce the expected chain, verifying
598
+ * contract, actor, action parameters, nonce and deadline as applicable.
599
+ * EIP-712 signs only fields declared in types, not extra message properties.
600
+ *
551
601
  * A string rather than a boolean because every caller shows it to somebody —
552
602
  * a dapp developer reading a validation error, or a person looking at the
553
603
  * mobile app's demo screen.
@@ -1171,9 +1221,26 @@ function validateProofRequest(request) {
1171
1221
  if (inputs.userAddress && !/^0x[a-fA-F0-9]{40}$/.test(inputs.userAddress)) {
1172
1222
  return { valid: false, error: 'Invalid userAddress format' };
1173
1223
  }
1174
- const actionError = validateTypedAction(inputs.action);
1175
- if (actionError) {
1176
- return { valid: false, error: actionError };
1224
+ }
1225
+ // The action, decided by the circuit's own entry rather than by naming a
1226
+ // circuit here. This used to be part of the arc_eligibility branch above,
1227
+ // which is why a second action-capable circuit had nowhere to be checked.
1228
+ {
1229
+ const action = request.inputs?.action;
1230
+ const binding = circuitActionBinding(request.circuit);
1231
+ if (binding === 'none') {
1232
+ if (action !== undefined) {
1233
+ return {
1234
+ valid: false,
1235
+ error: `${request.circuit} has no action inputs, so an action sent with it would not be proved. Refusing rather than dropping it.`,
1236
+ };
1237
+ }
1238
+ }
1239
+ else if (binding === 'required' || action !== undefined) {
1240
+ const actionError = validateTypedAction(action);
1241
+ if (actionError) {
1242
+ return { valid: false, error: actionError };
1243
+ }
1177
1244
  }
1178
1245
  }
1179
1246
  // Check expiry
@@ -4474,8 +4541,10 @@ function getVerifierChainId(circuit, customVerifier) {
4474
4541
  const PUBLIC_INPUT_OFFSETS = Object.freeze({
4475
4542
  // signal_hash, signer_list_merkle_root, scope, nullifier
4476
4543
  coinbase_attestation: { scope: [64, 95], nullifier: [96, 127] },
4477
- // Same four fields in the same order as Coinbase.
4478
- giwa_attestation: { scope: [64, 95], nullifier: [96, 127] },
4544
+ // No longer Coinbase's layout. The GIWA circuit gained the EIP-712 pair, so
4545
+ // it reads like Arc: signal_hash, domain_separator, action_hash, merkle
4546
+ // root, then scope and nullifier 64 bytes further along than Coinbase.
4547
+ giwa_attestation: { scope: [128, 159], nullifier: [160, 191] },
4479
4548
  // country_list, country_list_length and is_included sit before scope.
4480
4549
  coinbase_country_attestation: { scope: [86, 117], nullifier: [118, 149] },
4481
4550
  // pubkey_modulus_limbs and the bounded domain sit before scope.
@@ -5884,15 +5953,26 @@ class ProofportSDK {
5884
5953
  throw new Error(`${mdlError} (circuit: ${circuit})`);
5885
5954
  }
5886
5955
  }
5887
- // arc_eligibility: the action is what the proof binds to, so a request
5888
- // without a usable one cannot be honoured. Checked here, before a
5889
- // challenge is burned, because the alternative is a refusal from inside
5890
- // the mobile app -- a screen the dapp developer never sees, reached after
5891
- // the person has already scanned a QR code.
5892
- if (circuit === 'arc_eligibility') {
5893
- const actionError = validateTypedAction(inputs.action);
5894
- if (actionError) {
5895
- throw new Error(`${actionError} (circuit: ${circuit})`);
5956
+ // The action, per the circuit's entry in CIRCUIT_ACTION_BINDING. Checked
5957
+ // here, before a challenge is burned, because the alternative is a refusal
5958
+ // from inside the mobile app -- a screen the dapp developer never sees,
5959
+ // reached after the person has already scanned a QR code.
5960
+ //
5961
+ // This named arc_eligibility directly until giwa_attestation became the
5962
+ // second circuit that can carry an action; the id belongs in the table,
5963
+ // not in the check.
5964
+ {
5965
+ const action = inputs.action;
5966
+ const binding = circuitActionBinding(circuit);
5967
+ if (binding === 'none' && action !== undefined) {
5968
+ throw new Error(`${circuit} has no action inputs, so an action sent with it would not be proved. ` +
5969
+ 'Remove it, or use a circuit that binds one.');
5970
+ }
5971
+ if (binding === 'required' || (binding === 'optional' && action !== undefined)) {
5972
+ const actionError = validateTypedAction(action);
5973
+ if (actionError) {
5974
+ throw new Error(`${actionError} (circuit: ${circuit})`);
5975
+ }
5896
5976
  }
5897
5977
  }
5898
5978
  // Return target: validated before we burn a challenge, so a typo surfaces
@@ -6220,5 +6300,5 @@ class ProofportSDK {
6220
6300
  }
6221
6301
  }
6222
6302
 
6223
- export { ALL_CIRCUIT_IDS, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, EXPERIMENTAL_CIRCUIT_IDS, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PLANNED_CIRCUIT_IDS, ProofportSDK, SUPPORTED_CIRCUIT_IDS, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId, parseTypedAction, validateTypedAction, verifyProofOffChain };
6303
+ export { ALL_CIRCUIT_IDS, CIRCUIT_ACTION_BINDING, CIRCUIT_IDS, CIRCUIT_SUPPORT_STATUS, CIRCUIT_VK_PATHS, COINBASE_ATTESTATION_PUBLIC_INPUT_LAYOUT, COINBASE_COUNTRY_PUBLIC_INPUT_LAYOUT, EXPERIMENTAL_CIRCUIT_IDS, MDL_KR_PUBLIC_INPUT_LAYOUT, OIDC_DOMAIN_ATTESTATION_PUBLIC_INPUT_LAYOUT, PLANNED_CIRCUIT_IDS, ProofportSDK, SUPPORTED_CIRCUIT_IDS, circuitActionBinding, ProofportSDK as default, extractDomainFromPublicInputs, extractNullifierFromPublicInputs, extractScopeFromPublicInputs, getCircuitSupportStatus, isCircuitId, isSupportedCircuitId, parseTypedAction, validateTypedAction, verifyProofOffChain };
6224
6304
  //# sourceMappingURL=index.esm.js.map