# `X402.Extensions.OfferReceipt.JWS`
[🔗](https://github.com/cardotrejos/x402/blob/v0.9.0/lib/x402/extensions/offer_receipt/jws.ex#L1)

Compact JWS signing and verification for the offer-receipt extension.

Implements the JWS Compact Serialization (`header.payload.signature`,
RFC 7515) used by the x402 offer-and-receipt extension with the two
algorithms the extension names (§3.3):

  * `"ES256K"` — ECDSA over secp256k1 with SHA-256 (RFC 8812), with the
    64-byte `R || S` JOSE signature encoding
  * `"EdDSA"` — Ed25519 (RFC 8037)

Both are implemented with OTP's `:crypto` application — no additional
dependencies. Payloads are canonicalized with the JSON Canonicalization
Scheme (JCS, RFC 8785) before signing, as required by the extension's
security considerations (§10).

## Keys

Keys are raw binaries, not JWKs:

  * `"ES256K"` — a 32-byte secp256k1 private key; the public key is a
    SEC1 point (65-byte uncompressed, 33-byte compressed, or the bare
    64-byte X || Y coordinates)
  * `"EdDSA"` — a 32-byte Ed25519 seed; the public key is the 32-byte
    Ed25519 public key

## Boundaries

  * Key discovery is out of scope: the `kid` header (a DID URL per the
    extension spec) is carried and returned verbatim, but this module never
    resolves it — callers supply the public key for verification and are
    responsible for checking the key is authorized for the resource
    (spec §4.5.1).
  * `canonicalize/1` supports the JSON values that appear in offer and
    receipt payloads (objects, arrays, strings, integers, booleans, null).
    Floats are rejected with `{:error, {:unsupported_json_value, value}}`
    rather than risking a non-canonical number serialization.
  * ECDSA over secp256k1 requires OTP's `:crypto` to be linked against an
    OpenSSL with secp256k1 support (the common case); otherwise `"ES256K"`
    operations return `{:error, {:unsupported_algorithm, "ES256K"}}`.

# `compact`

```elixir
@type compact() :: String.t()
```

A JWS Compact Serialization string (`header.payload.signature`).

# `header`

```elixir
@type header() :: %{optional(String.t()) =&gt; term()}
```

A decoded protected header with at least `alg` and `kid`.

# `sign_error`

```elixir
@type sign_error() ::
  {:unsupported_algorithm, String.t()}
  | {:invalid_key, String.t()}
  | {:unsupported_json_value, term()}
```

# `verify_error`

```elixir
@type verify_error() ::
  :invalid_jws
  | :signature_mismatch
  | {:unsupported_algorithm, term()}
  | {:invalid_key, String.t()}
  | {:missing_header, String.t()}
```

# `canonicalize`
*since 0.6.0* 

```elixir
@spec canonicalize(term()) ::
  {:ok, String.t()} | {:error, {:unsupported_json_value, term()}}
```

Serializes a JSON-representable term with the JSON Canonicalization Scheme.

Implements RFC 8785 for the value domain used by offer and receipt
payloads: object keys are sorted by UTF-16 code units, no insignificant
whitespace is emitted, and strings use ECMAScript's minimal escaping.
Atom keys and values are serialized as their string form (as `Jason`
does); floats are rejected as unsupported.

## Examples

    iex> X402.Extensions.OfferReceipt.JWS.canonicalize(%{"b" => 1, "a" => [true, nil, "x"]})
    {:ok, ~s({"a":[true,null,"x"],"b":1})}

    iex> X402.Extensions.OfferReceipt.JWS.canonicalize(%{"bad" => 1.5})
    {:error, {:unsupported_json_value, 1.5}}

# `peek_header`
*since 0.6.0* 

```elixir
@spec peek_header(compact()) :: {:ok, header()} | {:error, :invalid_jws}
```

Decodes the protected header of a JWS without verifying the signature.

Useful for extracting the `kid` in order to resolve the verification key.

## Examples

    iex> X402.Extensions.OfferReceipt.JWS.peek_header(
    ...>   "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.e30.c2ln"
    ...> )
    {:ok, %{"alg" => "ES256K", "kid" => "did:web:api.example.com#key-1"}}

    iex> X402.Extensions.OfferReceipt.JWS.peek_header("not a jws")
    {:error, :invalid_jws}

# `peek_payload`
*since 0.6.0* 

```elixir
@spec peek_payload(compact()) :: {:ok, term()} | {:error, :invalid_jws}
```

Decodes the payload of a JWS **without verifying the signature**.

Only use the result for display or matching; verified reads must go
through `verify/3`.

## Examples

    iex> X402.Extensions.OfferReceipt.JWS.peek_payload("eyJhbGciOiJFUzI1NksiLCJraWQiOiJrIn0.eyJ2ZXJzaW9uIjoxfQ.c2ln")
    {:ok, %{"version" => 1}}

# `sign`
*since 0.6.0* 

```elixir
@spec sign(
  map(),
  keyword()
) :: {:ok, compact()} | {:error, sign_error()}
```

Signs a JSON payload into a JWS Compact Serialization string.

The protected header is `{"alg": alg, "kid": kid}`; the payload is
JCS-canonicalized before base64url encoding, so signing the same payload
twice produces the same JWS (for a deterministic algorithm like EdDSA).

## Options

* `:alg` - Required. JWS algorithm: `ES256K` or `EdDSA`.

* `:kid` (`t:String.t/0`) - Required. Key identifier placed in the protected header (a DID URL).

* `:key` (`t:String.t/0`) - Required. Raw private key: a 32-byte secp256k1 private key for `"ES256K"`, a
  32-byte Ed25519 seed for `"EdDSA"`.

## Examples

    {:ok, jws} =
      X402.Extensions.OfferReceipt.JWS.sign(
        %{"version" => 1, "resourceUrl" => "https://api.example.com/data"},
        alg: "EdDSA",
        kid: "did:web:api.example.com#key-1",
        key: ed25519_seed
      )

# `verify`
*since 0.6.0* 

```elixir
@spec verify(compact(), binary(), keyword()) ::
  {:ok, %{header: header(), payload: term()}} | {:error, verify_error()}
```

Verifies a JWS Compact Serialization string against a public key.

Returns the decoded header and payload on success. The header must carry
`"alg"` (one of the allowed `:algs`) and `"kid"` (required by the
extension, §3.3). Key authorization for the signed resource is the
caller's responsibility (spec §4.5.1).

## Options

* `:algs` - Algorithms accepted during verification (allowlist). The default value is `["ES256K", "EdDSA"]`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
