# API Contract: MailioReceipt v1

**Date**: 2026-02-04
**Branch**: `001-mailio-receipt-v1`

## Overview

This document defines the public API contract for the `mailio_receipt` library. All public
functions, classes, and types are documented here with their signatures and behavior.

---

## Module: `mailio_receipt`

### Public Exports

```python
from mailio_receipt import (
    # Creation
    create_receipt,
    compute_chain_digest,
    chain_from,

    # Verification
    verify_receipt,
    verify_chain,

    # Execution
    check_execution,
    ExecutionDecision,

    # Models
    MailioReceipt,
    MailioAuditEvent,
    Actors,
    Intent,
    Chain,
    Binding,
    Signature,

    # DID Resolution
    DIDResolver,

    # Errors
    MailioReceiptError,
    SignatureError,
    ChainError,
    DIDResolutionError,
    ExecutionBlockedError,
    ValidationError,
)
```

---

## Receipt Creation

### `create_receipt`

Creates a signed MailioReceipt from event data.

```python
def create_receipt(
    *,
    event: MailioAuditEvent,
        private_key: ec.EllipticCurvePrivateKey | Ed25519PrivateKey,
    signer_did: str,
    role: Literal["user", "agent", "authority"] = "agent",
) -> MailioReceipt:
    """
    Create a signed receipt for an event.

    Args:
        event: The event to receipt (must be fully populated)
        private_key: EC P-256 or Ed25519 private key for signing
        signer_did: DID of the signer (must match key in DID document)
        role: Role of the signer in this event

    Returns:
        MailioReceipt with computed digest and signature

    Raises:
        ValidationError: If event is invalid
        SignatureError: If signing fails

    Example:
        receipt = create_receipt(
            event=event,
            private_key=my_key,
            signer_did="did:web:example.com:agent",
        )
    """
```

### `create_event`

Factory function to create a MailioAuditEvent with defaults.

```python
def create_event(
    *,
    flow_id: str,
    actors: Actors,
    intent: Intent,
    chain: Chain | None = None,
    binding: Binding | None = None,
    ttl: str | None = None,
    meta: dict[str, Any] | None = None,
    event_id: str | None = None,  # Auto-generated if None
    timestamp: datetime | None = None,  # Auto-generated if None
) -> MailioAuditEvent:
    """
    Create a MailioAuditEvent with sensible defaults.

    Args:
        flow_id: Workflow instance ID (UUID string)
        actors: Participants in this event
        intent: What this event represents
        chain: Optional link to previous receipt
        binding: Optional artifact/message bindings
        ttl: Optional ISO8601 duration for expiration
        meta: Optional arbitrary metadata
        event_id: Optional event ID (UUID v4 generated if None)
        timestamp: Optional timestamp (UTC now if None)

    Returns:
        MailioAuditEvent ready for signing
    """
```

### `compute_chain_digest`

Computes the chain digest from a receipt or event. Chaining uses the event digest (not the
full receipt digest).

```python
def compute_chain_digest(source: MailioReceipt | MailioAuditEvent) -> str:
    """
    Compute the chain digest from an event or receipt.
    """
```

### `chain_from`

Creates a `Chain` reference to a prior receipt or event using its event digest.

```python
def chain_from(
    source: MailioReceipt | MailioAuditEvent,
    *,
    prev_receipt_ref: str | None = None,
    reason: str | None = None,
) -> Chain:
    """
    Create a Chain reference from a prior receipt or event.
    """
```

---

## Verification

### `verify_receipt`

Verifies a single receipt's signatures and digest.

```python
async def verify_receipt(
    receipt: MailioReceipt,
    *,
    resolver: DIDResolver | None = None,
) -> VerificationResult:
    """
    Verify a receipt's cryptographic integrity.

    Args:
        receipt: The receipt to verify
        resolver: DID resolver for fetching public keys (default: DIDWebResolver)

    Returns:
        VerificationResult with status and signer details

    Raises:
        SignatureError: If any signature is invalid
        DIDResolutionError: If signer DID cannot be resolved

    Verification steps:
        1. Recompute eventDigest from event
        2. Verify digest matches receipt.eventDigest
        3. For each signature:
           a. Resolve signer DID to get public key
           b. Verify JWS signature over canonicalized event
    """
```

### `verify_chain`

Verifies a chain of receipts.

```python
async def verify_chain(
    receipts: list[MailioReceipt],
    *,
    resolver: DIDResolver | None = None,
) -> ChainVerificationResult:
    """
    Verify a chain of receipts for integrity.

    Args:
        receipts: Receipts in chain order (oldest to newest)
        resolver: DID resolver for fetching public keys

    Returns:
        ChainVerificationResult with chain integrity status

    Raises:
        ChainError: If chain integrity is violated
        SignatureError: If any signature is invalid

    Verification steps:
        1. Verify each receipt individually
        2. For each receipt with chain field:
           a. Find predecessor by prevReceiptDigest
           b. Verify predecessor exists in chain
           c. Verify digest matches
    """
```

---

## Execution Checks

### `check_execution`

Checks if an action can be executed based on approvals and revocations.

```python
def check_execution(
    receipts: list[MailioReceipt],
    *,
    action_digest: str,
    decision_time: datetime | None = None,
) -> ExecutionDecision:
    """
    Check if execution is allowed for an action.

    Args:
        receipts: All receipts in the flow
        action_digest: Digest of the action event to check
        decision_time: Time of execution decision (default: now UTC)

    Returns:
        ExecutionDecision with allowed/blocked status and details

    Decision logic:
        1. Find the action receipt by digest
        2. Collect all approvals referencing the action
        3. For each approval:
           a. Check if expired (ttl)
           b. Check for revocations where revocation.ts <= decision_time
        4. If valid approval exists and no blocking revocation: ALLOWED
        5. Otherwise: BLOCKED with reason
    """
```

### `ExecutionDecision`

```python
@dataclass(frozen=True)
class ExecutionDecision:
    allowed: bool
    reason: str
    approval_digest: str | None  # If allowed, the approval that permits execution
    revocation_digest: str | None  # If blocked by revocation
    blocking_reason: BlockingReason | None  # Enum: MISSING_APPROVAL, EXPIRED, REVOKED, BINDING_MISMATCH
```

---

## DID Resolution

### `DIDResolver` (Protocol)

```python
class DIDResolver(Protocol):
    async def resolve(self, did: str) -> DIDDocument:
        """Resolve a DID to its document."""
        ...

    async def get_verification_key(
        self,
        did: str,
        key_id: str | None = None,
    ) -> ec.EllipticCurvePublicKey:
        """Get a verification public key from a DID document."""
        ...
```

### `DIDWebResolver`

Default implementation for DID:web.

```python
class DIDWebResolver:
    def __init__(
        self,
        *,
        cache_ttl: int = 300,  # seconds
        http_client: httpx.AsyncClient | None = None,
    ):
        """
        Create a DID:web resolver.

        Args:
            cache_ttl: Cache TTL for resolved documents (default: 5 minutes)
            http_client: Optional custom HTTP client
        """
```

---

## Data Models

### `MailioReceipt`

```python
@dataclass(frozen=True)
class MailioReceipt:
    type: Literal["MailioReceipt"]
    v: Literal["v1"]
    event: MailioAuditEvent
    event_digest: str
    signatures: tuple[Signature, ...]

    def to_dict(self) -> dict[str, Any]: ...
    def to_json(self) -> str: ...
    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> "MailioReceipt": ...
    @classmethod
    def from_json(cls, json_str: str) -> "MailioReceipt": ...

    @property
    def digest(self) -> str:
        """Compute digest of entire receipt (identifier)."""
```

### `MailioAuditEvent`

```python
@dataclass(frozen=True)
class MailioAuditEvent:
    type: Literal["MailioAuditEvent"]
    v: Literal["v1"]
    id: str
    flow_id: str
    ts: str
    actors: Actors
    intent: Intent
    chain: Chain | None = None
    binding: Binding | None = None
    ttl: str | None = None
    meta: dict[str, Any] | None = None
```

### `Actors`

```python
@dataclass(frozen=True)
class Actors:
    issuer: str
    subject: str
    authority: str | None = None
    others: tuple[str, ...] | None = None
```

### `Intent`

```python
@dataclass(frozen=True)
class Intent:
    kind: str
    action: str
    category: str
    scope: dict[str, Any]
```

### `Chain`

```python
@dataclass(frozen=True)
class Chain:
    prev_receipt_digest: str
    prev_receipt_ref: str | None = None
    reason: str | None = None
```

`prev_receipt_digest` refers to the predecessor event digest (`eventDigest`), not the full
receipt digest.

### `Binding`

```python
@dataclass(frozen=True)
class Binding:
    artifacts: tuple[ArtifactBinding, ...] | None = None
    message: MessageBinding | None = None

@dataclass(frozen=True)
class ArtifactBinding:
    name: str
    digest: str
    media_type: str | None = None

@dataclass(frozen=True)
class MessageBinding:
    message_id: str
    thread_id: str | None = None
```

### `Signature`

```python
@dataclass(frozen=True)
class Signature:
    role: Literal["user", "agent", "authority"]
    signer: str
    alg: Literal["ES256", "EdDSA"]
    signed: SignedMetadata
    jws: str

@dataclass(frozen=True)
class SignedMetadata:
    canonicalization: Literal["RFC8785"]
    digest: str
    target: Literal["/event"]
```

---

## Errors

```python
class MailioReceiptError(Exception):
    """Base exception for all library errors."""

class ValidationError(MailioReceiptError):
    """Invalid input data."""

class SignatureError(MailioReceiptError):
    """Signature creation or verification failed."""

class InvalidSignatureError(SignatureError):
    """Signature does not match content."""

class SignerNotFoundError(SignatureError):
    """Signer key not found in DID document."""

class ChainError(MailioReceiptError):
    """Chain integrity violation."""

class ChainIntegrityError(ChainError):
    """Chain digest mismatch."""

class MissingPredecessorError(ChainError):
    """Referenced predecessor not found."""

class DIDResolutionError(MailioReceiptError):
    """DID resolution failed."""

class DIDNotFoundError(DIDResolutionError):
    """DID document not found."""

class DIDNetworkError(DIDResolutionError):
    """Network error during resolution (retryable)."""

class ExecutionBlockedError(MailioReceiptError):
    """Execution not allowed."""

class RevocationError(ExecutionBlockedError):
    """Action revoked."""

class ExpiredApprovalError(ExecutionBlockedError):
    """Approval has expired."""

class BindingMismatchError(ExecutionBlockedError):
    """Artifact binding verification failed."""
```

---

## Intent Kind Constants

```python
class IntentKind:
    ACTION_PLANNED = "mailio.workflow.action.planned"
    APPROVAL_REQUESTED = "mailio.workflow.approval.requested"
    APPROVAL_GRANTED = "mailio.workflow.approval.granted"
    APPROVAL_REVOKED = "mailio.workflow.approval.revoked"
    ACTION_EXECUTED = "mailio.workflow.action.executed"
    ACTION_FAILED = "mailio.workflow.action.failed"
    NOTIFY = "mailio.workflow.notify"
```

---

## Serialization Notes

- All `to_dict()` methods use **camelCase** keys for JSON interoperability
- All `from_dict()` methods accept both camelCase and snake_case
- Timestamps are always RFC3339 with timezone
- UUIDs are lowercase with hyphens
- Digests are lowercase hex (64 characters for SHA-256)
