{"id":31943198,"url":"https://github.com/zksecurity/mina-attestations","last_synced_at":"2025-10-14T09:50:02.806Z","repository":{"id":257952673,"uuid":"858078314","full_name":"zksecurity/mina-attestations","owner":"zksecurity","description":"Private Attestations for Mina wallets","archived":false,"fork":false,"pushed_at":"2025-09-24T08:50:15.000Z","size":1514,"stargazers_count":16,"open_issues_count":9,"forks_count":6,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-10-12T17:12:36.991Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zksecurity.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-09-16T09:12:23.000Z","updated_at":"2025-09-24T08:50:19.000Z","dependencies_parsed_at":"2024-11-18T14:49:19.953Z","dependency_job_id":"0ad9279e-cc64-4ea7-912b-80e8c5fba1ac","html_url":"https://github.com/zksecurity/mina-attestations","commit_stats":null,"previous_names":["zksecurity/mina-credentials","zksecurity/mina-attestations"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/zksecurity/mina-attestations","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zksecurity%2Fmina-attestations","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zksecurity%2Fmina-attestations/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zksecurity%2Fmina-attestations/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zksecurity%2Fmina-attestations/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zksecurity","download_url":"https://codeload.github.com/zksecurity/mina-attestations/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zksecurity%2Fmina-attestations/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279018555,"owners_count":26086404,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-14T02:00:06.444Z","response_time":60,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-10-14T09:49:58.821Z","updated_at":"2025-10-14T09:50:02.788Z","avatar_url":"https://github.com/zksecurity.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mina Attestations \u0026nbsp; [![npm version](https://img.shields.io/npm/v/mina-attestations.svg?style=flat)](https://www.npmjs.com/package/mina-attestations)\n\nThis is a TypeScript library that implements _private attestations_: A cryptographic protocol that allows you to selectively disclose facts about yourself, using zero-knowledge proofs.\n\nThe library is available on npm and designed for all modern JS runtimes.\n\n```\nnpm i mina-attestations\n```\n\n## What are private attestations? 🧑‍🎓\n\nThe attestation flow involves three parties: _issuer_, _user_ and _verifier_. They exchange two kinds of digital objects: _credentials_ and _presentations_.\n\n1. an **issuer** makes a statement about you and hands you a certificate of that statement: a **credential**.\n\n\u003e Example: Your passport is a credential issued by a government agency. It contains information such as your name, birth date and citizenship.\n\n\u003c!-- - A credential derives its value from the credibility of the issuer: Third parties will trust the information on your passport, because they trust your government.\n- To be usable, a credential has to carry a _digital signature_ by the issuer. (For modern passports in most countries, this is the case!) --\u003e\n\n2. the **verifier** is interested in some particular fact about you (that is contained in a credential).\n\n\u003e Example: To sign up users, a crypto exchange must check that they are not US citizens. The exchange acts as a verifier.\n\n3. the **user** owns credentials. They can create **presentations** of a credential, that only disclose the information a verifier needs to know.\n\n\u003e Example: Prompted by the crypto exchange's request, you create a presentation, proving that your passport comes from a non-US country.\n\u003e The crypto exchange verifies that this is true, without learning anything else about you.\n\n\u003c!-- TODO: add diagram? --\u003e\n\nTo summarize, roughly, in cryptographic terms: credentials are signed data, and presentations are zero-knowledge proofs about credentials.\n\n_Private attestations_ refers to the entire protocol sketched above. A synonymous term from the academic literature is [anonymous credentials](https://www.sciencedirect.com/topics/computer-science/anonymous-credential).\n\n## Features 💫\n\nMina Attestations helps you implement all parts of the private attestation flow.\n\n- ✅ Supports [issuing credentials](#creating-credentials) as well as [requesting](#requesting-presentations),\n  [creating](#creating-presentations) and [verifying](#verifying-presentations) presentations\n- 🪪 [Import real-world credentials](#credential-kinds), like passports or emails, by wrapping them in a zk proof\n- 💡 Selective disclosure logic is defined with the embedded [`Operation` DSL](#operations-dsl) that is feature-rich, yet simple enough for non-technical users to understand what data they share\n- 🔒 Designed for integration in crypto wallets, to store credentials and authorize presentations by a signature\n  - Integration in the [Pallad](https://pallad.co) wallet is underway\n- 🧠 The cryptographic protocol is carefully designed to provide strong safety guarantees:\n  - **Ownership**: Credentials are tied to their owner, a Mina public key, and become invalid when changing the owner.\n  - **Unforgeability**: Presentations can only be created with access to their underlying credentials and an owner signature. So, credentials can even be stored with third parties without risking impersonation (if giving up privacy to those parties is acceptable).\n  - **Privacy**: Presentations do not leak any data from the input credential or the owner, apart from the specific public statement they were designed to encode.\n  - **Unlinkability**: Two different presentations of the same credential, or by the same user, cannot be linked (apart from out-of-band correlations like the user's IP address)\n  - **Context-binding**: Presentations are bound to a specific context such as the origin of the requesting website, so that the verifier cannot man-in-the-middle and impersonate users at a third party.\n\nZero-knowledge proofs are implemented using [o1js](https://github.com/o1-labs/o1js), a general-purpose zk framework.\n\n## Documentation\n\nThe remainder of this README contains documentation aimed at developers, starting from high-level examples and concepts and then moving to detailed API docs.\n\n- [Code example: Defining a private attestation ](#operations-dsl)\n- [What credentials are supported? ](#credential-kinds)\n- [API](#api)\n- Bonus:\n  - [`mina-attestations/dynamic`](#bonus-mina-attestationsdynamic)\n  - [`mina-attestations/rsa`](#bonus-mina-attestationsrsa)\n\nApart from reading the docs, have a look at our full code examples:\n\n- [examples/unique-hash.eg.ts](https://github.com/zksecurity/mina-attestations/blob/main/examples/unique-hash.eg.ts) - A good introduction, this example simulates the entire flow between issuer, user wallet and verifier within a single script, that is heavily commented to explain the individual steps.\n- [examples/zkapp-example.eg.ts](https://github.com/zksecurity/mina-attestations/blob/main/examples/zkapp-example.eg.ts) - Similar example as the one above, but where the presentation is verified in a Mina zkApp\n- [examples/web-demo](https://github.com/zksecurity/mina-attestations/blob/main/examples/web-demo) - Source code for [mina-attestations-demo.zksecurity.xyz](https://mina-attestations-demo.zksecurity.xyz). It includes both frontend and backend and can be useful as a reference for integrating `mina-attestations` in a real application. Caveat: The example mixes two different entities, issuer and verifier, in a single web app.\n\n\u003e 🧑‍🎓 In the docs that follow, we occasionally assume familiarity with zk programming concepts. If you don't know what a circuit or a \"public input\" are, we recommend checking out the [o1js docs](https://docs.minaprotocol.com/zkapps/o1js) or a similar resource, to build background understanding. Nonetheless, our library should be easy to use even without that understanding.\n\n## Code example: Defining a private attestation \u003ca id=\"operations-dsl\"\u003e\u003c/a\u003e\n\nLet's look at how a verifier might specify their conditions on the user's credential, using `mina-attestations`:\n\n```ts\nimport {\n  Claim,\n  Credential,\n  DynamicString,\n  Operation,\n  PresentationSpec,\n} from 'mina-attestations';\nimport { UInt64 } from 'o1js';\n\nconst String = DynamicString({ maxLength: 100 });\n\n// define expected credential schema\nlet credential = Credential.Native({\n  name: String,\n  nationality: String,\n  expiresAt: UInt64,\n});\n\nlet spec = PresentationSpec(\n  // inputs: credential and an additional \"claim\" (public input)\n  { credential, createdAt: Claim(UInt64) },\n  // logic\n  ({ credential, createdAt }) =\u003e ({\n    // we make two assertions:\n    assert: [\n      // 1. not from the United States\n      Operation.not(\n        Operation.equals(\n          Operation.property(credential, 'nationality'),\n          Operation.constant(String.from('United States'))\n        )\n      ),\n\n      // 2. credential is not expired\n      Operation.lessThanEq(\n        createdAt,\n        Operation.property(credential, 'expiresAt')\n      ),\n    ],\n    // we expose the credential's issuer, for the verifier to check\n    outputClaim: Operation.issuer(credential),\n  })\n);\n```\n\nThere's much to unpack in this example, but the main thing we want to highlight is how custom logic for a presentation is defined, the _presentation spec_. This spec is created using a declarative API that specifies a custom zk circuit.\n\nThe first parameter to `PresentationSpec()` specifies the inputs to the presentation circuit: `credential` and `createdAt`.\n\n- `credential` defines what _type_ of credential we expect, including the data layout. Here, we expect a \"native\" credential defined with `Credential.Native()` (see [credential kinds](#credential-kinds)).\n- `createdAt` is a so-called \"claim\", which means a _public input_ to this circuit. By contrast, the credential is a _private_ input.\n\nNote: The input name \"credential\" in this example is arbitrary and picked by the developer. You could also have multiple credentials as inputs, and make a statement that combines their properties. Similarly, you can have many claims.\n\nThe second parameter to `PresentationSpec()` defines the circuit logic, as a function from the inputs, using our `Operation` DSL. `Operation` is, essentially, a radically simplified language for writing zk circuits, tailored to the use case of making statements about user data. It contains common operations like testing equality, comparisons, arithmetic, conditionals, hashing, etc.\n\nThere are two outputs, `assert` and `outputClaim`, both of which contain `Operation` nodes.\n\n- `assert` tells us which conditions on the credential are proven to hold\n- `outputClaim` specifies the _public output_: credential data the user directly exposes to the verifier. In this example, we expose the credential's `issuer` (hash of a public key), so that the verifier can check that the credential was issued by a legitimate entity.\n\nThe assertion logic should be easy to read for you: We check that the `nationality` doesn't equal `\"United States\"`. We also check a condition on the credential's `expiresAt` attribute. The idea is that the verifier can pass in the _current date_ as `createdAt`, and this check ensures the credential hasn't expired without leaking the exact expiry date.\n\n\u003e 🤓 By interacting with this code in your editor, you might appreciate that all our library interfaces are richly typed, using generic types to preserve as much information as possible. For example, the inferred type of `credential`, which is passed as an input to `PresentationSpec`, is carried into the callback. There, `Operation.property(credential, 'nationality')` is correctly inferred to be a `String`. This, in turn, ensures that a `String` is also passed to the `Operation.constant()`, because `Operation.equals()` requires its inputs to be of equal type.\n\nBehind the scenes, the circuit created from a presentation spec contains more than the `assert` and `outputClaim` logic. It also verifies the authorization on all input credentials, and in addition verifies a signature by the credential owner. The latter ensures that nobody but the owner can present a credential.\n\n### From spec to presentation request\n\nIn a typical flow, the code above would be called once in the verifier's application, and used to precompile the circuit for later verification. Then, for every user that wants to authenticate with a presentation, we would create a new _presentation request_ from the `spec`:\n\n```ts\n// VERIFIER\nlet request = PresentationRequest.https(\n  spec,\n  { createdAt: UInt64.from(Date.now()) },\n  { action: 'my-app:authenticate' }\n);\nlet requestJson = PresentationRequest.toJSON(request);\n// now send request to user wallet\n```\n\nThis highlights an important point: The target receiver of a presentation request is generic software, like a web3 wallet, that doesn't know about the specific attestation being proved. Therefore, we had to ensure that the serialized JSON request **fully specifies the circuit**.\n\nThe request also has to contain the input claims (here: `createdAt`), as there is no way for a wallet to come up with these custom values. The only inputs required on the wallet side to create a proof from this are the actual credential, and a user signature.\n\nAnother point is that the user, when approving the request, should be able to understand what data they share. To make this possible, we implemented a pretty-printer that converts presentation specs into human-readable pseudo-code:\n\n\u003c!-- TODO would be nice to show a screenshot of the Pallad prompt here --\u003e\n\n```\ncredential.nationality ≠ \"United States\"\n```\n\nThese points imply that the representation of a circuit has to be simple, and deserializable without concerns about malicious code execution.\n\nSimplicity is the core advantage that `Operation` has over a general-purpose zk framework like o1js. It explains why we aren't using o1js as the circuit-writing interface directly.\n\nThe best part is that, by being easy to read and understand, presentation specs are also really easy to write for developers!\n\n## What credentials are supported? \u003ca id=\"credential-kinds\"\u003e\u003c/a\u003e\n\nConceptually, credentials are data authorized by a signature. When using credentials in a presentation, we have to verify that signature inside our circuit. If the signature uses Mina's native signature scheme (Schnorr over the Pallas curve), this is efficient.\n\nHowever, most credentials that exist out there were not created with Mina in mind, and verifying their signatures is expensive in terms of circuit size, and usually complicated to implement.\n\nTo support both cases well, our library distinguishes two different kinds of credentials:\n\n1. **Native credentials** are authorized by a Mina signature.\n2. **Imported credentials** are authorized by a _zero-knowledge proof_.\n\nFor an imported credential, our presentation uses recursion and verifies the attached proof inside the circuit. For native credentials, we just verify the signature.\n\nSince arbitrary logic can be encoded in a zk proof, imported credentials can cover a wide variety of existing credentials: You just need someone to implement an o1js circuit that verifies them. The only thing required from proofs to make them usable as an imported credentials is that their public output follows the structure `{ owner, data }`, where `owner` is the public key of the credential's owner.\n\nFor example, to \"import\" a passport as a credential, we need a circuit that proves it has a valid passport, and exposes the passport data in `data`. A user with their passport at hand can wrap it in that proof to get an imported credential.\n\nThere are cool examples for what we could \"import\" as a credential, that go beyond the traditional concept of a credentials. Everything you can prove in zk can be a credential!\n\nFor example, [zk-email](https://prove.email/) proves the DKIM signature on emails to support the statement \"I received this particular email from this domain\", which has very interesting applications.\nThe imported credential version of that would simply expose the entire email as `data`: Subject, \"from\" address and body text. Only when doing presentations, we care about hiding the content and making specific assertions about it.\n\n### Why not do everything in one proof?\n\nThe process of first importing a credential, and then using it for a presentation, means that _two_ proofs have to be created by a user. Why not do both in one proof, if possible?\n\nOne reason for preferring separate steps is that the importing proof is usually very big, and takes a lot of time. On the other hand, presentation proofs are small. Also, presentations are one-off and designed to be used exactly once, so you really _want_ those proofs to be small. On the other hand, credentials are designed to be stored long-term, so separating them saves a lot of proof generation time if credentials can be reused.\n\nAnother reason is that modeling imported credentials as recursive proofs keeps our core library agnostic about the inner verification logic. That way, we avoid the burden of supporting all possible credentials within the library itself. Anyone can write their own \"import\" circuit, and still be compatible with the standard!\n\n### What imported credentials are available now?\n\n- ECDSA credential that wraps an Ethereum-style signature\n\n```ts\nimport { EcdsaEthereum } from 'mina-attestations/imported';\n```\n\n- [ZkPass](https://zkpass.org) credential that wraps a ZkPass validator signature, which attests to a web interaction created by the ZkPass TransGate browser extension.\n\n```ts\nimport { ZkPass, type ZkPassResponseItem } from 'mina-attestations/imported';\n```\n\n- [WIP](https://github.com/zksecurity/mina-attestations/tree/main/src/email): zk-email\n- [WIP](https://github.com/piconbello/zk-passport-o1js-lib) (by another team): zk passport\n\n## API\n\nTable of contents:\n\n- [Data types](#data-types)\n  - [`CredentialSpec`](#credentialspec)\n  - [Native Credential example](#native-credential-example)\n  - [Imported Credential example](#imported-credential-example)\n  - [Unsigned Credential example](#unsigned-credential-example)\n  - [`StoredCredential`](#storedcredential)\n  - [Credential types](#credential-types)\n  - [`Credential`](#credential)\n  - [Credential example](#credential-example)\n- [Creating credentials](#creating-credentials)\n  - [Native Credential](#native-credential)\n  - [Unsigned Credential](#unsigned-credential)\n  - [Imported Credential](#imported-credential)\n- [Defining presentation logic](#defining-presentation-logic)\n  - [`Spec`](#spec)\n  - [`Operation`](#operation)\n  - [Optional Logic in Specs](#optional-logic-in-specs)\n- [Requesting presentations](#requesting-presentations)\n  - [Creating requests](#creating-requests)\n  - [Context](#context)\n    - [HTTPS Context](#https-context)\n    - [zkApp Context](#zkapp-context)\n    - [Context Derivation](#context-derivation)\n  - [Serialization](#serialization)\n  - [Examples](#examples)\n- [Creating presentations](#creating-presentations)\n  - [Precompilation and Compilation](#precompilation-and-compilation)\n  - [Creating a Presentation](#creating-a-presentation)\n  - [Serialization](#serialization-1)\n  - [Credential matching](#credential-matching)\n  - [Full Example](#full-example)\n- [Verifying presentations](#verifying-presentations)\n- [Defining new imported credentials](#defining-new-imported-credentials)\n\n### Data types\n\n\u003c!-- highlight how to serialize every type --\u003e\n\n#### `CredentialSpec`\n\nA `CredentialSpec` defines the structure and verification logic for a credential.\n\n```ts\ntype CredentialSpec\u003cWitness = unknown, Data = unknown\u003e = {\n  credentialType: CredentialType;\n  data: NestedProvableFor\u003cData\u003e;\n  witness: WitnessSpec;\n\n  witnessType(type: WitnessSpec): NestedProvableFor\u003cWitness\u003e;\n\n  verify(witness: Witness, credHash: Field): void;\n  issuer(witness: Witness): Field;\n  validate(witness: Witness, credHash: Field): Promise\u003cvoid\u003e;\n\n  matchesSpec(witness: Witness): boolean;\n};\n```\n\nIt specifies:\n\n- The credential type (native, imported, unsigned)\n- The data schema\n- A \"witness\" type for private parameters\n- A function `verify(...)` that verifies the credential inside a ZkProgram circuit\n- A function `validate(...)` that verifies the credential in normal JS\n- A function `issuer(...)` that derives a commitment to the \"issuer\" of the credential, e.g. a public key for signed credentials\n- A function `matchesSpec(...)` that decides whether a stored credential's witness matches the spec\n\nThe serialization and deserialization of a `CredentialSpec` is handled through methods in the `Credential` namespace using:\n\n- `Credential.specToJSON(spec: CredentialSpec): CredentialSpecJSON`\n- `Credential.specFromJSON(json: CredentialSpecJSON): CredentialSpec\u003cany, any\u003e`.\n\n#### Native Credential example\n\n```ts\nimport { Field, Bytes } from 'o1js';\nimport { Credential } from 'mina-attestations';\n\n// Define schema with a fixed-size Bytes type\nconst Bytes32 = Bytes(32);\nconst InputData = {\n  age: Field,\n  name: Bytes32,\n};\n\n// Create native credential spec\nconst SignedData = Credential.Native(InputData);\n\n// Issue a credential using the spec\nconst data = {\n  age: Field(25),\n  name: Bytes32.fromString('Alice'),\n};\nconst signedCredential = Credential.sign(issuerKey, { owner, data });\n\n// Validate the credential\nawait Credential.validate(signedCredential);\n```\n\n#### Imported Credential example\n\n```ts\nimport { Field, UInt64 } from 'o1js';\nimport { Credential, DynamicString } from 'mina-attestations';\n\nconst Nationality = DynamicString({ maxLength: 50 });\n\n// Create imported credential spec\nconst PassportCredential = await Credential.Imported.fromMethod(\n  {\n    name: 'passport',\n    publicInput: { issuer: Field },\n    privateInput: {\n      nationality: Nationality,\n      expiresAt: UInt64,\n    },\n    data: {\n      nationality: Nationality,\n      expiresAt: UInt64,\n    },\n  },\n  async ({ privateInput }) =\u003e {\n    // here, you can put any o1js circuit\n    // (to make this example real, we would need a circuit that verifies a passport)\n    return privateInput;\n  }\n);\n\n// Create a credential with proof\nconst passport = await PassportCredential.create({\n  owner,\n  publicInput: { issuer: 1001 },\n  privateInput: {\n    expiresAt: UInt64.from(Date.UTC(2027, 1, 1)),\n    nationality: 'Austria',\n  },\n});\n\n// Validate the credential\nawait Credential.validate(passport);\n```\n\n#### Unsigned Credential example\n\n```ts\nimport { Field } from 'o1js';\nimport { Credential } from 'mina-attestations';\n\n// Define simple schema\nconst InputData = { value: Field };\n\n// Create unsigned credential spec\nconst UnsignedSpec = Credential.Unsigned(InputData);\n\n// Create an unsigned credential\nconst unsignedCredential = Credential.unsigned({\n  value: Field(123),\n});\n```\n\n#### `StoredCredential`\n\nA `StoredCredential` represents a credential in its stored form, containing all necessary data for verification and usage in presentations.\n\n```ts\ntype StoredCredential\u003cData = unknown, Witness = unknown\u003e = {\n  version: 'v0';\n  witness: Witness;\n  metadata: Json | undefined;\n  credential: Credential\u003cData\u003e;\n};\n```\n\nIt specifies:\n\n- A version identifier for future compatibility\n- A witness that provides verification data that proves the credential's authenticity\n  - For native credentials it contains the issuer's public key and signature\n  - For imported credentials it contains a verification key and a proof\n  - For unsigned credentials it is undefined, since unsigned credentials don't prove their authenticity\n- An optional metadata associated with the credential\n  - Can store any JSON-serializable data\n- A credential which is the core credential data containing:\n  - The owner's public key\n  - The data that contains the actual credential attributes\n\nThe serialization and deserialization of a `StoredCredential` is handled through methods in the `Credential` namespace using `Credential.toJSON(...)` and `Credential.fromJSON(...)`.\n\n#### Credential types\n\n```ts\n// Native credential type\ntype Native\u003cData\u003e = StoredCredential\u003c\n  Data,\n  {\n    type: 'native';\n    issuer: PublicKey;\n    issuerSignature: Signature;\n  }\n\u003e;\n\n// Imported credential type\ntype Imported\u003cData, Input\u003e = StoredCredential\u003c\n  Data,\n  {\n    type: 'imported';\n    vk: VerificationKey;\n    proof: DynamicProof\u003cInput, Credential\u003e;\n  }\n\u003e;\n\n// Unsigned credential type\ntype Unsigned\u003cData\u003e = StoredCredential\u003cData, undefined\u003e;\n```\n\n#### `Credential`\n\nThe `Credential` namespace provides the main interface for working with credentials. It has the following static properties:\n\n- `Native\u003cDataType extends NestedProvable\u003e(\n  dataType: DataType\n): CredentialSpec\u003cNativeWitness, InferNestedProvable\u003cDataType\u003e\u003e`: Factory for creating native credential specifications\n- `Imported`: Namespace for creating credential specifications that use zero-knowledge proofs for verification.\n- `Unsigned\u003cDataType extends NestedProvable\u003e(\n  data: DataType\n): CredentialSpec\u003cundefined, InferNestedProvable\u003cDataType\u003e\u003e`: Factory for creating unsigned credential specifications\n\nIt also specifies the following methods:\n\n- `sign\u003cData\u003e(issuerPrivateKey: PrivateKey, credentialInput: Credential\u003cData\u003e | string, metadata?: Json): Native\u003cData\u003e`\n\n  - Creates a new native credential signed by the issuer\n  - Parameters:\n    - `issuerPrivateKey`: The private key of the issuer\n    - `credentialInput`: Either a credential object of its JSON string representation\n    - `metadata`: Optional metadata to attach to the credential\n  - Returns:\n    - A new native credential\n\n- `unsigned\u003cData\u003e(data: Data, metadata?: Json): Unsigned\u003cData\u003e`\n  - Creates a new unsigned, dummy credential with no owner and no signature\n  - Parameters:\n    - `data`: The credential data\n    - `metadata`: Optional metadata to attach to the credential\n  - Returns:\n    - A new unsigned credential\n\n\u003e ⚠️ Unsigned credentials use a dummy owner key and should only be used for testing!\n\n- `toJSON(credential: StoredCredential): string`\n\n  - Serializes a credential to JSON format\n  - Parameters:\n    - `credential`: The credential to serialize\n  - Returns:\n    - JSON string representation\n\n- `fromJSON(json: string): Promise\u003cStoredCredential\u003e`\n\n  - Deserializes a credential from JSON format\n  - Parameters:\n    - `json`: JSON string representing a credential\n  - Returns:\n    - Promise resolving to the deserialized credential\n\n- `validate(credential: StoredCredential): Promise\u003cvoid\u003e`\n\n  - Validates a credential's authenticity\n  - Parameters:\n    - `credential`: The credential to validate\n  - Returns:\n    - Promise that resolves if validation succeeds, rejects if validation fails\n\n- `dataToJSON\u003cData\u003e(credential: Credential\u003cData\u003e): string`\n\n  - Serialize the data input to a `signCredential()` call.\n    The resulting string is accepted as input to `Credential.sign()`\n  - Parameters:\n    - `credential`: Credential data to serialize\n  - Returns:\n    - JSON string representation of credential data\n  - Example:\n\n    ```ts\n    let credentialData = { owner: publicKey, data: { name: 'Alice' } };\n    let credentialDataJson = Credential.dataToJSON(credentialData);\n\n    let credential = Credential.sign(privateKey, credentialDataJson);\n    ```\n\n#### Credential example\n\n```ts\nimport { Field, PrivateKey } from 'o1js';\nimport { Credential } from 'mina-attestations';\n\n// Create a native credential spec\nconst spec = Credential.Native({ age: Field });\n\n// Issue a credential\nconst issuerKey = PrivateKey.random();\nconst owner = PrivateKey.random().toPublicKey();\nconst data = { age: Field(25) };\n\nconst credential = Credential.sign(\n  issuerKey,\n  { owner, data },\n  {\n    issuedAt: Date.now(), // optional metadata\n  }\n);\n\n// Serialize\nconst json = Credential.toJSON(credential);\n\n// Deserialize and validate\nconst recovered = await Credential.fromJSON(json);\nawait Credential.validate(recovered);\n```\n\n### Creating credentials\n\nThe `Credential` namespace provides several methods to help create different types of credentials:\n\n#### Native Credential\n\n`Credential.sign\u003cData\u003e(issuerPrivateKey: PrivateKey, credentialInput: Credential\u003cData\u003e | string, metadata?: Json): Native\u003cData\u003e`\n\n- Creates a new native credential signed by the issuer\n- Parameters:\n  - `issuerPrivateKey`: The private key of the issuer\n  - `credentialInput`: Either a credential object of its JSON string representation\n  - `metadata`: Optional metadata to attach to the credential\n- Returns:\n  - A new native credential\n\nExample:\n\n```ts\nlet data = { age: Field(18), name: Bytes32.fromString('Alice') };\nlet signedData = Credential.sign(issuerKey, { owner, data });\n```\n\n#### Unsigned Credential\n\n`Credential.unsigned\u003cData\u003e(data: Data, metadata?: Json): Unsigned\u003cData\u003e`\n\n- Creates a new unsigned, dummy credential with no owner and no signature\n- Parameters:\n  - `data`: The credential data\n  - `metadata`: Optional metadata to attach to the credential\n- Returns:\n  - A new unsigned credential\n\nExample:\n\n```ts\nconst unsignedCredential = Credential.unsigned({\n  value: Field(123),\n});\n```\n\n#### Imported Credential\n\nTo import credentials we have to use the `Credential.Imported` namespace and follow the following pattern:\n\n1. First create the specification using fromMethod or fromProgram\n2. Get back an object that contains both the spec and helper functions\n3. Use the create function that came with that object to create actual credentials\n\nMethods to create specifications:\n\n`Credential.Imported.fromProgram(program)`\n\nCreates an imported credential specification from an existing o1js ZkProgram.\n\n```ts\nasync function fromProgram\n  DataType extends ProvableType,\n  InputType extends ProvableType,\n  Data extends InferProvable\u003cDataType\u003e,\n  Input extends InferProvable\u003cInputType\u003e,\n  AllInputs extends any[]\n\u003e(program: {\n  publicInputType: InputType;            // Type of public inputs\n  publicOutputType: ProvableType\u003cCredential\u003cData\u003e\u003e; // Output must be a credential\n  analyzeMethods(): Promise\u003cRecord\u003cstring, any\u003e\u003e;   // For analysis\n  maxProofsVerified(): Promise\u003c0 | 1 | 2\u003e;         // Max proofs to verify\n  compile(options?: {                    // Compilation\n    cache?: Cache;\n    forceRecompile?: boolean;\n    proofsEnabled?: boolean;\n  }): Promise\u003c{ verificationKey: VerificationKey }\u003e;\n\n  run(...inputs: AllInputs): Promise\u003c{   // Program execution\n    proof: Proof\u003cInput, Credential\u003cData\u003e\u003e;\n    auxiliaryOutput: undefined;\n  }\u003e;\n}): Promise\u003c{\n  spec: CredentialSpec;          // The credential specification\n  program: Program;             // The original program\n  compile(): Promise\u003cVerificationKey\u003e;  // Compile the program\n  create(...inputs: AllInputs): Promise\u003cImported\u003cData, Input\u003e\u003e;  // Create credential\n  fromProof(proof: Proof\u003cInput, Credential\u003cData\u003e\u003e, vk: VerificationKey): Promise\u003cImported\u003cData, Input\u003e\u003e;  // Create from existing proof\n  dummy(credential: Credential\u003cFrom\u003cDataType\u003e\u003e): Promise\u003cImported\u003cData, Input\u003e\u003e;  // Create dummy credential\n}\u003e;\n```\n\nExample:\n\n```ts\nimport { Field, Bytes, PublicKey, ZkProgram, Struct, Proof } from 'o1js';\nimport { Credential } from 'mina-attestations';\nimport { owner } from './test-utils.ts'; // dummy owner used for testing\n\nconst Bytes32 = Bytes(32);\nconst InputData = { age: Field, name: Bytes32 };\n\n// Create a program that outputs a credential with InputData\nconst program = ZkProgram({\n  name: 'importedCredential',\n  publicInput: {\n    // Matches the Spec claims\n    inputOwner: PublicKey,\n    data: InputData,\n  },\n  publicOutput: Struct({\n    // Matches Operation.record output\n    owner: PublicKey,\n    data: InputData,\n  }),\n  methods: {\n    create: {\n      privateInputs: [], // No private inputs needed\n      method(publicInput: { inputOwner: PublicKey; data: typeof InputData }) {\n        // Simply pass through the input as credential\n        return {\n          publicOutput: {\n            owner: publicInput.inputOwner,\n            data: publicInput.data,\n          },\n        };\n      },\n    },\n  },\n});\n\n// Create imported credential specification from program\nconst Imported = await Credential.Imported.fromProgram(program);\n\n// Create verification key\nconst vk = await Imported.compile();\n\n// Create credential data\nlet data = {\n  age: Field(18),\n  name: Bytes32.fromString('Alice'),\n};\n\n// First create a proof using the program directly\nconst { proof } = await program.create({\n  inputOwner: owner,\n  data: data,\n});\n\n// Create a credential using fromProof\nlet provedData = await Imported.fromProof(proof, vk);\n\n// Create a credential using create\nlet provedData2 = await Imported.create({\n  inputOwner: owner, // Public inputs match program's publicInput type\n  data: data,\n});\n```\n\n`Credential.Imported.fromMethod(config, method)`\n\nCreates an imported credential spec from a configuration object and method.\n\n```ts\nasync function fromMethod\u003c\n  Config extends {\n    name: string;\n    publicInput?: NestedProvable; // Optional public input type\n    privateInput?: NestedProvable; // Optional private input type\n    data: NestedProvable; // Credential data type\n  }\n\u003e(\n  spec: Config,\n  method: (inputs: {\n    publicInput: PublicInput\u003cConfig\u003e;\n    privateInput: PrivateInput\u003cConfig\u003e;\n    owner: PublicKey;\n  }) =\u003e Promise\u003cData\u003cConfig\u003e\u003e\n): Promise\u003c{\n  spec: CredentialSpec;\n  create(inputs: {\n    publicInput: From\u003cPublicInputType\u003e;\n    privateInput: From\u003cPrivateInputType\u003e;\n    owner: PublicKey;\n  }): Promise\u003cImported\u003cData, Input\u003e\u003e;\n}\u003e;\n```\n\nExample:\n\n```ts\nimport { Field, UInt64 } from 'o1js';\nimport { Credential, DynamicString } from 'mina-attestations';\n\nconst Nationality = DynamicString({ maxLength: 50 });\n\nlet PassportCredential_ = await Credential.Imported.fromMethod(\n  {\n    name: 'passport',\n    publicInput: { issuer: Field },\n    privateInput: { nationality: Nationality, expiresAt: UInt64 },\n    data: { nationality: Nationality, expiresAt: UInt64 },\n  },\n  async ({ privateInput }) =\u003e {\n    return privateInput;\n  }\n);\nlet PassportCredential = Object.assign(PassportCredential_, { Nationality });\nlet vk = await PassportCredential.compile();\n\n// user \"imports\" their passport into a credential, by creating a PassportCredential proof\nlet cred = await PassportCredential.create({\n  owner,\n  publicInput: { issuer: 1001 },\n  privateInput: {\n    expiresAt: UInt64.from(Date.UTC(2027, 1, 1)),\n    nationality: 'Austria',\n  },\n});\n```\n\n`Credential.Imported.create(data, witness)`\n\nCreates an imported credential specification by directly specifying the data type and witness specification\n\n```ts\nfunction create\u003cDataType extends NestedProvable, InputType extends ProvableType\u003e({\n  data: DataType;                // Schema for the credential data\n  witness: ImportedWitnessSpec;  // Specification for the witness/proof\n}): CredentialSpec\u003cImportedWitness\u003cInput\u003e, Data\u003e\n```\n\nExample:\n\n```ts\nCredential.Imported.create({\n  data: Field,\n  witness: ProofSpec,\n});\n```\n\n\u003e This is a lower-level method compared to `fromMethod` or `fromProgram`. It's used internally by those methods but can also be used directly when you need full control over the witness specification.\n\n### Defining presentation logic\n\nThe `Spec` function and `Operation` namespace provide the core functionality for defining the logic of presentations - what should be proven about credentials and what data should be revealed.\n\n#### `Spec`\n\n```ts\ntype Spec\u003c\n  Output = unknown,\n  Inputs extends Record\u003cstring, Input\u003e = Record\u003cstring, Input\u003e\n\u003e = {\n  inputs: Inputs;\n  assert: Node\u003cBool\u003e;\n  outputClaim: Node\u003cOutput\u003e;\n};\n```\n\nThe `Spec` function specifies a ZkProgram that verifies and selectively discloses data.\n\n```ts\nfunction Spec\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n  inputs: Inputs,\n  spec: (inputs: {\n    [K in keyof Inputs]: InputToNode\u003cInputs[K]\u003e;\n  }) =\u003e {\n    assert?: Node\u003cBool\u003e | Node\u003cBool\u003e[];\n    outputClaim: Node\u003cOutput\u003e;\n  }\n): Spec\u003cOutput, Inputs\u003e;\n\n// variant without data output\nfunction Spec\u003cInputs extends Record\u003cstring, Input\u003e\u003e(\n  inputs: Inputs,\n  spec: (inputs: {\n    [K in keyof Inputs]: InputToNode\u003cInputs[K]\u003e;\n  }) =\u003e {\n    assert?: Node\u003cBool\u003e | Node\u003cBool\u003e[];\n  }\n): Spec\u003cundefined, Inputs\u003e;\n```\n\nA presentation specification consists of:\n\n- Input definitions - This can either be a [`CredentialSpec`](#credentialspec), a `Constant` or a `Claim`\n  - `Constant`: Defined at the time of creating the `Spec`\n    ```ts\n    function Constant\u003cDataType extends ProvableType\u003e(\n      data: DataType,\n      value: From\u003cDataType\u003e\n    ): Constant\u003cInferProvable\u003cDataType\u003e\u003e;\n    ```\n  - `Claim`: Public inputs to the ZkProgram\n    ```ts\n    function Claim\u003cDataType extends NestedProvable\u003e(\n      data: DataType\n    ): Claim\u003cInferNestedProvable\u003cDataType\u003e\u003e;\n    ```\n- Assertion logic - What to prove about the inputs\n  - A `Node` or an array of `Node`s evaluating to Bool values\n- Output logic - What data to reveal publicly\n  - A `Node` of the generic `Output` type\n\n\u003e The `Node` type represents an operation or value in a presentation's circuit. It's the foundational type used throughout the Operation DSL. It is a discriminated union type with a `type` field identifying the operation. Nodes are evaluated when creating or verifying a presentation. The `Node.eval` function handles this internally. While you typically don't create Node objects directly, understanding their structure helps when working with the Operation DSL. Each Operation function returns a Node that represents that operation or value in the circuit.\n\n#### `Operation`\n\nThe `Operation` namespace provides a DSL for writing circuit logic.\n\n```ts\nconst Operation = {\n  owner: { type: 'owner' } as Node\u003cPublicKey\u003e,\n  constant\u003cData\u003e(data: Data): Node\u003cData\u003e {\n    return { type: 'constant', data };\n  },\n\n  issuer,\n  issuerPublicKey,\n  verificationKeyHash,\n  publicInput,\n\n  property,\n  record,\n  equals,\n  equalsOneOf,\n  lessThan,\n  lessThanEq,\n  add,\n  sub,\n  mul,\n  div,\n  and,\n  or,\n  not,\n  hash,\n  hashWithPrefix,\n  ifThenElse,\n  compute,\n};\n```\n\n- Comparisons:\n\n  - `Operation.equals(left: Node, right: Node)` - Assert equality\n  - `Operation.equalsOneOf(input: Node\u003cData\u003e, options: Node\u003cData\u003e[] | Node\u003cData[]\u003e | Node\u003cDynamicArray\u003cData\u003e\u003e)` - Asserts if a value equals one of several options\n  - `Operation.lessThan(left: Node, right: Node)` - Asserts if `left` is less than `right`\n  - `Operation.lessThanEq(left: Node, right: Node)` - Asserts if `left` is less than or equal to `right`\n\n- Boolean Logic\n\n  - `Operation.not(inner: Node\u003cBool\u003e)` - Logical NOT\n  - `Operation.and(...inputs: Node\u003cBool\u003e[])` - Logical AND\n  - `Operation.or(left: Node\u003cBool\u003e, right: Node\u003cBool\u003e)` - Logical OR\n\n- Arithmetic:\n\n  - `Operation.add(left: Node, right: Node)` - Addition\n  - `Operation.sub(left: Node, right: Node)` - Subtraction\n  - `Operation.mul(left: Node, right: Node)` - Multiplication\n  - `Operation.div(left: Node, right: Node)` - Division\n\n- Data access:\n\n  - `Operation.property(node: Node, key: string)` - Access object property\n  - `Operation.record(data: Record\u003cstring, Node\u003e)` - Create record from nodes\n  - `Operation.constant(data: T)` - Create constant value\n\n- Credential-specific:\n\n  - `Operation.owner` - Access credential owner\n  - `Operation.issuer(credential: CredentialNode)` - Get credential issuer\n  - `Operation.issuerPublicKey(credential: CredentialNode)` - Get issuer's public key\n  - `Operation.verificationKeyHash(credential: CredentialNode)` - Get verification key hash\n  - `Operation.publicInput(credential: CredentialNode)` - Get credential's public input\n\n- Conditional logic:\n\n  - `Operation.ifThenElse( condition: Node\u003cBool\u003e, thenNode: Node, elseNode: Node)`\n  - Allows for branching logic based on a condition\n    - Example:\n      ```ts\n      let result = Operation.ifThenElse(\n        Operation.lessThan(age, threshold),\n        Operation.constant(Field(0)), // If age \u003c threshold\n        Operation.property(data, 'age') // Otherwise return actual age\n      );\n      ```\n\n- Custom computation:\n\n  - `Operation.compute\u003cInputs extends readonly Node[], Output\u003e(\n  inputs: [...Inputs],\n  outputType: ProvableType\u003cOutput\u003e,\n  computation: (...args: Inputs) =\u003e Output\n): Node\u003cOutput\u003e`\n  - Enables defining custom computations on input values\n    - Example:\n      ```ts\n      Operation.compute(\n        [\n          Operation.property(position, 'x'),\n          Operation.property(position, 'y'),\n          Operation.property(center, 'x'),\n          Operation.property(center, 'y'),\n        ],\n        Field,\n        (px, py, cx, cy) =\u003e {\n          const dx = px.sub(cx);\n          const dy = py.sub(cy);\n          return dx.mul(dx).add(dy.mul(dy));\n        }\n      );\n      ```\n\n- Hashing:\n  - `Operation.hash(...inputs: Node[]): Node\u003cField\u003e` - Hash one or more values\n  - `Operation.hashWithPrefix(prefix: string, ...inputs: Node[]): Node\u003cField\u003e` - Hash with a domain separator prefix\n\nExample:\n\n```ts\nlet spec = PresentationSpec(\n  { passport: PassportCredential.spec, createdAt: Claim(UInt64) },\n  ({ passport, createdAt }) =\u003e ({\n    assert: [\n      // not from the United States\n      Operation.not(\n        Operation.equals(\n          Operation.property(passport, 'nationality'),\n          Operation.constant(\n            PassportCredential.Nationality.from('United States')\n          )\n        )\n      ),\n\n      // passport is not expired\n      Operation.lessThanEq(\n        createdAt,\n        Operation.property(passport, 'expiresAt')\n      ),\n\n      // hard-code passport verification key\n      Operation.equals(\n        Operation.verificationKeyHash(passport),\n        Operation.constant(vk.hash)\n      ),\n    ],\n    // return public input (passport issuer hash) for verification\n    outputClaim: Operation.publicInput(passport),\n  })\n);\n```\n\n#### Optional Logic in Specs\n\nBoth `assert` and `outputClaim` are optional in a presentation spec. When defining simple presentations or when you only need part of the functionality, you can omit either or both:\n\n```ts\n// Spec with no custom logic\nlet spec = Spec(\n  { credential: Credential.Native(someSchema) },\n  () =\u003e ({}) // Empty logic - no assertions, no output\n);\n\n// Spec with only assertions\nlet assertOnlySpec = Spec(\n  { credential: Credential.Native(someSchema) },\n  ({ credential }) =\u003e ({\n    assert: Operation.lessThan(\n      Operation.property(credential, 'age'),\n      Operation.constant(Field(18))\n    ),\n    // No outputClaim - nothing is revealed\n  })\n);\n\n// Spec with only output\nlet outputOnlySpec = Spec(\n  { credential: Credential.Native(someSchema) },\n  ({ credential }) =\u003e ({\n    // No assert - no conditions to verify\n    outputClaim: Operation.property(credential, 'publicInfo'),\n  })\n);\n```\n\nWhen parts are omitted:\n\n- If `assert` is omitted, it defaults to `Operation.constant(Bool(true))`\n- If `outputClaim` is omitted, it defaults to `Operation.constant(undefined)`\n- If both are omitted, the presentation will just verify the authenticity of the credentials without making any additional claims\n\n\u003e Even without custom logic, the presentation still verifies the validity of all input credentials and the owner's signature authorizing the presentation\n\n### Requesting presentations\n\nThe `PresentationRequest` namespace provides functionality for creating and managing presentation requests. A presentation request combines a presentation specification with specific input values and context information.\n\n**Core Types**\n\n```ts\ntype PresentationRequest\u003c\n  RequestType extends PresentationRequestType = PresentationRequestType,\n  Output = any,\n  Inputs extends Record\u003cstring, Input\u003e = Record\u003cstring, Input\u003e,\n  InputContext = any,\n  WalletContext = any\n\u003e = {\n  type: RequestType;\n  spec: Spec\u003cOutput, Inputs\u003e;\n  claims: Claims\u003cInputs\u003e;\n  inputContext: InputContext;\n  program?: unknown;\n  verificationKey?: VerificationKey;\n\n  // Derive final context hash from inputs\n  deriveContext(\n    inputContext: InputContext, // From request\n    walletContext: WalletContext, // From wallet\n    derivedContext: WalletDerivedContext // Auto-generated\n  ): Field;\n};\n\n// Available request types\ntype PresentationRequestType = 'no-context' | 'zk-app' | 'https';\n\n// Wallet-derived context values\ntype WalletDerivedContext = {\n  vkHash: Field; // Verification key hash\n  claims: Field; // Hash of claims\n  clientNonce: Field; // Random nonce\n};\n\n// Base context shared by all context types\ntype BaseInputContext = {\n  serverNonce: Field; // Random nonce generated by server\n};\n\n// Context for HTTPS requests\ntype HttpsInputContext = BaseInputContext \u0026 {\n  type: 'https';\n  action: string; // HTTP action like \"POST /api/verify\"\n};\n\n// Context for zkApp requests\ntype ZkAppInputContext = BaseInputContext \u0026 {\n  type: 'zk-app';\n  action: Field; // Method ID + args hash\n};\n\n// Final context used for derivation\ntype Context = {\n  type: ContextType; // 'zk-app' | 'https'\n  vkHash: Field;\n  clientNonce: Field;\n  serverNonce: Field;\n  claims: Field;\n  verifierIdentity: PublicKey | string;\n  action: Field | string;\n};\n```\n\nThere are three types of presentation requests, specified by `PresentationRequestType`:\n\n- `no-context` - Basic request without context binding\n- `zk-app` - Request bound to a zkApp context\n- `https` - Request bound to an HTTPS endpoint\n\n#### Creating requests\n\nThe `PresentationRequest` namespace provides several methods for creating requests:\n\n```ts\nconst PresentationRequest = {\n  // Create HTTPS request\n  https\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n    spec: Spec\u003cOutput, Inputs\u003e,\n    claims: Claims\u003cInputs\u003e,\n    context: { action: string }\n  ): HttpsRequest\u003cOutput, Inputs\u003e;\n\n  // Create from precompiled HTTPS spec\n  httpsFromCompiled\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n    compiled: CompiledRequest\u003cOutput, Inputs\u003e,\n    claims: Claims\u003cInputs\u003e,\n    context: { action: string }\n  ): HttpsRequest\u003cOutput, Inputs\u003e;\n\n  // Create zkApp request\n  zkApp\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n    spec: Spec\u003cOutput, Inputs\u003e,\n    claims: Claims\u003cInputs\u003e,\n    context: { action: Field }\n  ): ZkAppRequest\u003cOutput, Inputs\u003e;\n\n  // Create from precompiled zkApp spec\n  zkAppFromCompiled\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n    compiled: CompiledRequest\u003cOutput, Inputs\u003e,\n    claims: Claims\u003cInputs\u003e,\n    context: { action: Field }\n  ): ZkAppRequest\u003cOutput, Inputs\u003e;\n\n  // Create no-context request\n  noContext\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n    spec: Spec\u003cOutput, Inputs\u003e,\n    claims: Claims\u003cInputs\u003e\n  ): NoContextRequest\u003cOutput, Inputs\u003e;\n};\n```\n\nEach request type has its own context structure:\n\n```ts\n// HTTPS request context\ntype HttpsInputContext = {\n  type: 'https';\n  action: string;\n  serverNonce: Field;\n};\n\n// zkApp request context\ntype ZkAppInputContext = {\n  type: 'zk-app';\n  action: Field;\n  serverNonce: Field;\n};\n```\n\n#### Context\n\nRequest contexts provide security by binding presentations to a specific verifier and action. Each type of request has its own context structure that helps prevent misuse of presentations.\n\n\u003e For testing or special cases, you can create requests without context binding. However, no-context requests should generally be avoided in production as they lack the security guarantees provided by proper context binding.\n\n##### HTTPS Context\n\nThe `HttpsRequest` type bind presentations to a web verifier:\n\n```ts\ntype HttpsRequest\n  Output = any,\n  Inputs extends Record\u003cstring, Input\u003e = Record\u003cstring, Input\u003e\n\u003e = PresentationRequest\n  'https',\n  Output,\n  Inputs,\n  HttpsInputContext,\n  { verifierIdentity: string }\n\u003e;\n\ntype HttpsInputContext = {\n  type: 'https';\n  action: string;         // e.g. \"POST /api/verify\"\n  serverNonce: Field;     // Random nonce to prevent replay\n};\n```\n\n##### zkApp Context\n\nThe `zkAppRequest` type binds presentations to a zkApp verifier:\n\n```ts\ntype ZkAppRequest\n  Output = any,\n  Inputs extends Record\u003cstring, Input\u003e = Record\u003cstring, Input\u003e\n\u003e = PresentationRequest\n  'zk-app',\n  Output,\n  Inputs,\n  ZkAppInputContext,\n  { verifierIdentity: PublicKey }\n\u003e;\n\ntype ZkAppInputContext = {\n  type: 'zk-app';\n  action: Field;          // Method ID + args hash\n  serverNonce: Field;     // Random nonce\n};\n```\n\n##### Context Derivation\n\nThe final context hash is derived from multiple components to ensure security:\n\n```ts\n// Base context shared by all context types\ntype BaseInputContext = {\n  serverNonce: Field; // Random nonce generated by server\n};\n\n// Context for HTTPS requests\ntype HttpsInputContext = BaseInputContext \u0026 {\n  type: 'https';\n  action: string; // HTTP action like \"POST /api/verify\"\n};\n\n// Context for zkApp requests\ntype ZkAppInputContext = BaseInputContext \u0026 {\n  type: 'zk-app';\n  action: Field; // Method ID + args hash\n};\n\n// Context information derived by wallet\ntype WalletDerivedContext = {\n  vkHash: Field; // Hash of verification key\n  claims: Field; // Hash of input claims\n  clientNonce: Field; // Random client nonce\n};\n\n// Final context used for derivation\ntype Context = {\n  type: ContextType; // 'zk-app' | 'https'\n  vkHash: Field;\n  clientNonce: Field;\n  serverNonce: Field;\n  claims: Field;\n  verifierIdentity: PublicKey | string;\n  action: Field | string;\n};\n```\n\nThe context is derived in two steps:\n\n1. First compute the context object:\n\n```ts\nconst context = computeContext({\n  ...inputContext, // From request\n  ...walletContext, // From wallet\n  ...derivedContext, // Generated values\n});\n```\n\n2. Then generate the final context hash:\n\n```ts\nconst contextHash = generateContext(context);\n```\n\n#### Serialization\n\nRequests can be serialized for transmission between verifier and wallet:\n\n```ts\nconst PresentationRequest = {\n  // Serialize to JSON\n  toJSON(request: PresentationRequest): string;\n\n  // Deserialize from JSON with type checking\n  fromJSON\u003cR extends RequestFromType\u003cK\u003e, K extends PresentationRequestType\u003e(\n    expectedType: K,\n    json: string\n  ): R;\n};\n```\n\n#### Examples\n\n**HTTPS Presentation Request Example**\n\n```ts\nimport { Field, Bytes } from 'o1js';\nimport {\n  Spec,\n  Claim,\n  Credential,\n  Operation,\n  PresentationRequest,\n} from 'mina-attestations';\n\n// Define schema with bytes for fixed-size strings\nconst Bytes32 = Bytes(32);\n\n// Create the specification\nconst spec = Spec(\n  {\n    signedData: Credential.Native({\n      age: Field,\n      name: Bytes32,\n    }),\n    targetAge: Claim(Field),\n    targetName: Constant(Bytes32, Bytes32.fromString('Alice')),\n  },\n  ({ signedData, targetAge, targetName }) =\u003e ({\n    assert: Operation.and(\n      Operation.equals(Operation.property(signedData, 'age'), targetAge),\n      Operation.equals(Operation.property(signedData, 'name'), targetName)\n    ),\n    outputClaim: Operation.property(signedData, 'age'),\n  })\n);\n\n// Create HTTPS presentation request\nlet request = PresentationRequest.https(\n  spec,\n  { targetAge: Field(18) }, // Claims\n  { action: 'POST /api/verify' } // Context\n);\n```\n\n**zkApp Presentation Request Example**\n\n```ts\nimport { Field, Bytes, Proof } from 'o1js';\nimport {\n  Spec,\n  Claim,\n  Credential,\n  Operation,\n  PresentationRequest,\n  Presentation,\n} from 'mina-attestations';\n\n// Define schema\nconst Bytes32 = Bytes(32);\nconst InputData = {\n  age: Field,\n  name: Bytes32,\n};\n\n// Create specification\nconst spec = Spec(\n  {\n    signedData: Credential.Native(InputData),\n    targetAge: Claim(Field),\n    targetName: Constant(Bytes32, Bytes32.fromString('Alice')),\n  },\n  ({ signedData, targetAge, targetName }) =\u003e ({\n    assert: Operation.and(\n      Operation.equals(Operation.property(signedData, 'age'), targetAge),\n      Operation.equals(Operation.property(signedData, 'name'), targetName)\n    ),\n    outputClaim: Operation.property(signedData, 'age'),\n  })\n);\n\n// Create zkApp presentation request\nlet request = PresentationRequest.zkApp(\n  spec,\n  { targetAge: Field(18) }, // Claims\n  { action: Field(123) } // Method ID + args hash\n);\n```\n\n### Creating presentations\n\nThe `Presentation` namespace provides functionality for creating, preparing, and finalizing presentations in response to presentation requests. It handles the compilation of circuits, proof generation, and credential verification.\n\n**Core types**\n\n```ts\ntype Presentation\n  Output = any,\n  Inputs extends Record\u003cstring, Input\u003e = Record\u003cstring, Input\u003e\n\u003e = {\n  version: 'v0';\n  claims: Claims\u003cInputs\u003e;\n  outputClaim: Output;\n  serverNonce: Field;\n  clientNonce: Field;\n  proof: {\n    proof: string;\n    maxProofsVerified: number;\n  };\n};\n\n// Compiled request used for creating presentations\ntype CompiledRequest\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e = {\n  spec: Spec\u003cOutput, Inputs\u003e;\n  program: Program\u003cOutput, Inputs\u003e;\n  verificationKey: VerificationKey;\n};\n```\n\n#### Precompilation and Compilation\n\nThese methods provide two levels of circuit compilation needed for creating and verifying presentations. The key difference is that `precompile()` works with a raw specification while `compile()` works with a complete presentation request.\n\n**Precompile**\n\n```ts\nasync function precompile\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n  spec: Spec\u003cOutput, Inputs\u003e\n): Promise\u003cCompiledRequest\u003cOutput, Inputs\u003e\u003e;\n```\n\nThe `precompile()` function:\n\n- Takes a raw presentation specification and compiles it into a reusable format\n- Parameters:\n  - `spec`: The presentation specification to compile\n- Returns:\n  - Promise resolving to a `CompiledRequest` containing the spec, program and verification key\n- Generates the ZK program and verification key that will be needed for creating proofs\n- Should be used when you plan to create multiple presentation requests with the same specification\n- Is typically called once during application startup or deployment\n- Can significantly improve performance\n\nExample:\n\n```ts\n// Define the specification\nconst spec = Spec(\n  {\n    passport: PassportCredential.spec,\n    createdAt: Claim(UInt64),\n  },\n  ({ passport, createdAt }) =\u003e ({\n    assert: Operation.lessThanEq(\n      createdAt,\n      Operation.property(passport, 'expiresAt')\n    ),\n    outputClaim: Operation.publicInput(passport),\n  })\n);\n\n// Precompile the spec - do this once and save the result\nconst compiledSpec = await Presentation.precompile(spec);\n\n// Later, when creating requests:\nconst request1 = PresentationRequest.httpsFromCompiled(\n  compiledSpec,\n  { createdAt: UInt64.from(Date.now()) },\n  { action: 'verify-passport' }\n);\n\n// Can reuse the same compiled spec for multiple requests\nconst request2 = PresentationRequest.httpsFromCompiled(\n  compiledSpec,\n  { createdAt: UInt64.from(Date.now()) },\n  { action: 'another-verification' }\n);\n```\n\n**Compile**\n\n```ts\nasync function compile\u003cR extends PresentationRequest\u003e(\n  request: R\n): Promise\u003c\n  Omit\u003cR, 'program'\u003e \u0026 {\n    program: Program\u003cOutput\u003cR\u003e, Inputs\u003cR\u003e\u003e;\n    verificationKey: VerificationKey;\n  }\n\u003e;\n```\n\nThe `compile()` function:\n\n- Takes a complete presentation request and compiles its underlying specification\n- Parameters:\n  - `request`: The presentation request to compile\n- Returns:\n  - Promise resolving to the request with added compiled program and verification key\n- Used when you have a single request that needs to be compiled\n- Is typically used when creating one-off presentation requests\n- Can be used by wallets to compile requests they receive\n\nExample:\n\n```ts\n// Create a presentation request\nconst request = PresentationRequest.https(\n  spec,\n  { createdAt: UInt64.from(Date.now()) },\n  { action: 'verify-passport' }\n);\n\n// Compile the request before creating a presentation\nconst compiledRequest = await Presentation.compile(request);\n\n// Now we can create a presentation using the compiled request\nconst presentation = await Presentation.create(ownerKey, {\n  request: compiledRequest,\n  credentials: [passport],\n  context: { verifierIdentity: 'passport-verifier.com' },\n});\n```\n\n#### Creating a Presentation\n\n```ts\nasync function create\u003cR extends PresentationRequest\u003e(\n  ownerKey: PrivateKey,\n  params: {\n    request: R;\n    context: WalletContext\u003cR\u003e;\n    credentials: (StoredCredential \u0026 { key?: string })[];\n  }\n): Promise\u003cPresentation\u003cOutput\u003cR\u003e, Inputs\u003cR\u003e\u003e\u003e;\n```\n\nCreates a complete presentation from a request and credentials\nParameters:\n\n- `ownerKey`: Private key of the credential owner\n- `params`: Object containing:\n  - `request`: The presentation request\n  - `context`: Wallet-specific context\n  - `credentials`: Array of credentials to use\n\nReturns:\n\n- Promise resolving to the generated presentation\n\nEven though this works, and in some cases it might be viable to pass in the owner's private key directly, in most cases a user would generate a presentation through their wallet so for that scenario the presentation creation is split into preparation and finalization.\n\n**Prepare**\n\n```ts\nasync function prepare\u003cR extends PresentationRequest\u003e(params: {\n  request: R;\n  context: WalletContext\u003cR\u003e;\n  credentials: (StoredCredential \u0026 { key?: string })[];\n}): Promise\u003c{\n  context: Field;\n  messageFields: string[];\n  credentialsUsed: Record\u003cstring, StoredCredential\u003e;\n  serverNonce: Field;\n  clientNonce: Field;\n  compiledRequest: CompiledRequest\u003cOutput\u003cR\u003e, Inputs\u003cR\u003e\u003e;\n}\u003e;\n```\n\nPrepares the presentation data for signing\nParameters:\n\n- `params`: Same as `create()` but without owner key because the signing will be handled by the wallet.\n  Returns:\n- Promise resolving to prepared presentation data\n\n**Finalize**\n\n```ts\nasync function finalize\u003cR extends PresentationRequest\u003e(\n  request: R,\n  ownerSignature: Signature,\n  preparedData: {\n    serverNonce: Field;\n    clientNonce: Field;\n    context: Field;\n    credentialsUsed: Record\u003cstring, StoredCredential\u003e;\n    compiledRequest: { program: Program\u003cOutput\u003cR\u003e, Inputs\u003cR\u003e\u003e };\n  }\n): Promise\u003cPresentation\u003cOutput\u003cR\u003e, Inputs\u003cR\u003e\u003e\u003e;\n```\n\nFinalizes a presentation using prepared data and the owner signature\nParameters:\n\n- `request`: The presentation request\n- `ownerSignature`: Signature from the credential owner\n- `preparedData`: Data from `prepare()`\n\nReturns:\n\n- Promise resolving to the completed presentation\n\n#### Serialization\n\n```ts\nfunction toJSON\u003cOutput, Inputs extends Record\u003cstring, Input\u003e\u003e(\n  presentation: Presentation\u003cOutput, Inputs\u003e\n): string;\n```\n\nSerializes a presentation to JSON format\nParameters:\n\n- `presentation`: The presentation to serialize\n\nReturns:\n\n- JSON string representation\n\n```ts\nfunction fromJSON(presentationJson: string): Presentation;\n```\n\nDeserializes a presentation from JSON format\nParameters:\n\n- `presentationJson`: JSON string representing a presentation\n\nReturns:\n\n- The deserialized presentation\n\n#### Credential matching\n\nWhen creating a presentation, credentials are passed as an array along with optional keys:\n\n```ts\ntype CredentialInput = StoredCredential \u0026 { key?: string };\n\n// In Presentation.create():\ncredentials: CredentialInput[]\n```\n\nEach credential can optionally include a `key` property that explicitly maps it to an input in the presentation spec.\n\nThe system matches credentials to the inputs in your presentation spec in two ways:\n\n1. Explicit Matching (using keys):\n\n```ts\n// Presentation spec with two credential inputs\nconst spec = Spec({\n  passport: PassportCredential.spec,\n  driverLicense: LicenseCredential.spec,\n  // ...other inputs\n});\n\n// Explicitly map credentials to inputs using keys\nconst presentation = await Presentation.create(ownerKey, {\n  request,\n  credentials: [\n    { ...passportCred, key: 'passport' },\n    { ...licenseCred, key: 'driverLicense' },\n  ],\n  context,\n});\n```\n\n2. Automatic Matching (without keys):\n\n```ts\n// Credentials will be matched automatically based on different criteria\nconst presentation = await Presentation.create(ownerKey, {\n  request,\n  credentials: [passportCred, licenseCred],\n  context,\n});\n```\n\n**Key Points About Credential Selection**\n\n1. Order Independence with Keys:\n\n   - When using explicit keys, the order of credentials in the array doesn't matter\n\n     ```ts\n     // These are equivalent:\n     credentials: [\n       { ...licenseCred, key: 'driverLicense' },\n       { ...passportCred, key: 'passport' },\n     ];\n\n     credentials: [\n       { ...passportCred, key: 'passport' },\n       { ...licenseCred, key: 'driverLicense' },\n     ];\n     ```\n\n2. Type Matching\n\n   - Without keys, credentials are matched based on their type compatibility with the spec\n   - The credential matches a spec input if it passes `credentialMatchesSpec(spec, credential)`\n   - This checks:\n     - Credential version matches\n     - Witness type matches (native/imported/unsigned)\n     - Data schema matches\n\n3. Mixed Approach\n\n   - You can mix keyed and unkeyed credentials in the same array\n   - Keyed credentials are matched first, then remaining slots are filled by matching unkeyed credentials\n\n4. Validation\n\n   ```ts\n   // This will throw if credentials can't be matched\n   await Presentation.create(ownerKey, {\n     request,\n     credentials: [wrongTypeCred], // Error: Missing credentials: \"passport\", \"driverLicense\"\n     context,\n   });\n   ```\n\n#### Full Example\n\n```ts\nimport { Field, Bytes } from 'o1js';\nimport {\n  Spec,\n  Credential,\n  Presentation,\n  PresentationRequest,\n  Operation,\n} from 'mina-attestations';\n\n// 1. Set up credential and request\nconst Bytes32 = Bytes(32);\nconst InputData = { age: Field, name: Bytes32 };\n\n// Create the specification\nconst spec = Spec(\n  {\n    signedData: Credential.Native(InputData),\n    targetAge: Claim(Field),\n    targetName: Constant(Bytes32, Bytes32.fromString('Alice')),\n  },\n  ({ signedData, targetAge, targetName }) =\u003e ({\n    assert: Operation.and(\n      Operation.equals(Operation.property(signedData, 'age'), targetAge),\n      Operation.equals(Operation.property(signedData, 'name'), targetName)\n    ),\n    outputClaim: Operation.property(signedData, 'age'),\n  })\n);\n\n// Create sample credential\nconst data = {\n  age: Field(18),\n  name: Bytes32.fromString('Alice'),\n};\nconst signedData = Credential.sign(issuerKey, { owner, data });\n\n// Create request\nlet request = PresentationRequest.https(\n  spec,\n  { targetAge: Field(18) },\n  { action: 'POST /api/verify' }\n);\n\n// 2. Two-step presentation creation\n// Step 1: Prepare the presentation\nconst prepared = await Presentation.prepare({\n  request,\n  credentials: [signedData],\n  context: { verifierIdentity: 'my-app.xyz' },\n});\n\n// Step 2: Create signature and finalize\n// This would typically happen in the wallet\nconst ownerSignature = Signature.create(\n  ownerKey,\n  prepared.messageFields.map(Field.from)\n);\n\n// Create final presentation\nconst presentation = await Presentation.finalize(\n  request,\n  ownerSignature,\n  prepared\n);\n```\n\n### Verifying presentations\n\nThe `Presentation` namespace provides functionality to verify presentations against their original requests.\n\n```ts\nasync function verify\u003cR extends PresentationRequest\u003e(\n  request: R,\n  presentation: Presentation\u003cany, Record\u003cstring, any\u003e\u003e,\n  context: WalletContext\u003cR\u003e\n): Promise\u003cOutput\u003cR\u003e\u003e;\n```\n\nVerifies a presentation against a request and context. This function:\n\n- Verifies the zero-knowledge proof contained in the presentation\n- Checks that the presentation was created for this specific request\n- Checks that the presentation is bound to the expected context\n- Returns the verified output claim, which can be used for further application-specific validation\n\nParameters:\n\n- `request`: The original presentation request\n- `presentation`: The presentation to verify\n- `context`: The verifier's identity\n  - For HTTPS requests: `{ verifierIdentity: string }`\n  - For zkApp requests: `{ verifierIdentity: PublicKey }`\n\nReturns:\n\n- Promise resolving to the verifies output claim specified in the presentation spec\n\nThe verification process includes several key steps:\n\n1. Recomputing the context hash using the request's `inputContext`, provided `context`, and original `claims`\n2. Verifying the proof against the request's verification key\n3. Checking that the proof's public inputs match the recomputed context and claims\n4. Extracting and returning the verified output claim\n\nExample from [`examples/unique-hash.eg.ts`](./examples/unique-hash.eg.ts):\n\n```ts\n// VERIFIER: verify the presentation\nlet output = await Presentation.verify(\n  request,\n  Presentation.fromJSON(presentationJson),\n  { verifierIdentity: 'my-app.xyz' }\n);\n\n// After verifying the proof itself, perform application-specific checks.\n// In this case, verify that the passport was issued by a legitimate authority\n// by checking if the issuer (exposed in the presentation's output claim)\n// is in our list of accepted issuers\nlet acceptedIssuers = [1001n, 1203981n, 21380123n]; // list of accepted issuers\nassert(acceptedIssuers.includes(output.issuer.toBigInt()), 'Invalid issuer');\n```\n\n### Defining new imported credentials\n\nSee [imported credential example](#imported-credential-example) for an example.\n\n## Bonus: `mina-attestations/dynamic`\n\n\u003c!-- TODO Rename the lib to `o1js-dynamic` and publish as its own npm package, to make it look less mina-attestations specific and more likely to be adopted everywhere --\u003e\n\nUnder the sub-import `mina-attestations/dynamic`, we export an entire library of dynamic data types and hashes with o1js.\n\nFeatures:\n\n- `DynamicSHA2` for hashing dynamic-length inputs with SHA2-256, -224, -384 or -512\n- `DynamicSHA3` for hashing dynamic-length inputs with Keccak256\n- `DynamicString` and `DynamicBytes` for representing strings and bytes, with many useful methods for manipulating strings in a circuit\n- `DynamicArray`, a generalization of the above types to an arbitrary element type\n- `StaticArray`, which provides an API consistent with `DynamicArray` but for fixed-length arrays\n- `DynamicRecord`, a wrapper for objects that you don't necessarily know the exact layout of, but can be hashed and accessed properties of inside a circuit\n- `hashDynamic()`, for Poseidon-hashing pretty much any input (including plain strings, records, o1js types etc) in a way which is compatible to in-circuit hashing of padded data types like `DynamicRecord` and `DynamicArray`\n- `Numeric` for performing arithmetic or comparisons on any combination of numberic types (e.g., check if a `UInt32` is less than a `Int64`)\n- `toDecimalString()`, a gadget to compute the variable-length decimal string from a `Field`\n\nThe sub-library is intended to help with importing **real-world credentials** into the Mina ecosystem: For example, to \"import\" your passport, you have to verify the passport authority's signature on your passport data. The signature relies one of several hashing and signature schemes such as ECDSA, RSA and SHA2-256, SHA2-384, SHA2-512. Also, the signature will be over a dynamic-length string.\n\nExample of SHA-512-hashing a dynamic-length string:\n\n```ts\nimport { Bytes, ZkProgram } from 'o1js';\nimport { DynamicSHA2, DynamicString } from 'mina-attestations/dynamic';\n\n// allow strings up to length 100 as input\nconst String = DynamicString({ maxLength: 100 });\n\nlet sha512Program = ZkProgram({\n  name: 'sha512',\n  publicOutput: Bytes(64) // 64 bytes == 512 bits\n\n  methods: {\n    run: {\n      privateInputs: [String],\n      async method(string: DynamicString) {\n        let publicOutput = DynamicSHA2.hash(512, string);\n        return { publicOutput };\n      },\n    },\n  },\n});\n\nawait sha512Program.compile();\n\nlet result = await sha512Program.run('Hello, world!');\nlet provenHash: Bytes = result.proof.publicOutput;\n\nconsole.log(provenHash.toHex());\n```\n\n## Bonus: `mina-attestations/rsa`\n\nThe sub-import `mina-attestations/rsa` provides provable methods for verifying RSA signatures. For more details, refer to [the subfolder README](https://github.com/zksecurity/mina-attestations/blob/main/src/rsa/README.md).\n\n\u003c!-- ## Further resources and background\n\nTODO: references to various md docs and papers and examples --\u003e\n\n## Acknowledgement\n\nWe thank [Mina Foundation](https://www.minafoundation.com/) for funding this work with a grant, and for providing us with valuable feedback and direction throughout. Link to the original grant proposal: https://github.com/MinaFoundation/Core-Grants/issues/35#issuecomment-2318685738\n\nWe thank o1Labs for creating and open-sourcing [o1js](https://github.com/o1-labs/o1js). Some of our code, such as the SHA2, Keccak and RSA gadgets, were seeded by copying code from the o1js repo and modifying it to fit our needs.\n\nWe thank the [zk-email project](https://github.com/zkemail) for creating and open-sourcing zk-email. We took great inspiration for our own (unfinished) zk-email implementation. Our TS code that prepares emails for in-circuit verification was seeded by copying over files from [zk-email-verify](https://github.com/zkemail/zk-email-verify); some parts of it still exist in our code almost unchanged.\n\n## License\n\n[Apache-2.0](LICENSE)\n\nCopyright 2024-2025 zkSecurity\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzksecurity%2Fmina-attestations","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzksecurity%2Fmina-attestations","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzksecurity%2Fmina-attestations/lists"}