# Missive — Patterns Catalog

> Copy-in recipes for `*.api.md` collections. Pick a **Shape** recipe and an **Auth** recipe and drop
> them into the `MISSIVE-TEMPLATE.md` skeleton. Every recipe collapses into the one shape —
> `Request` / `Checks` / `Mock` / `Notebook`, one capability per `##` (see `MISSIVE.md` → Anatomy).
> Secrets are always `{{vault:NAME}}` references, never values; non-secret inputs are `{{var:NAME}}`;
> auth is fired via `scripts/vault.sh exec`.

## Auth recipes (static-header)

Auth lives in the `**Request**` headers (or the fire command). Only `{{vault:NAME}}` for secrets;
non-secret parts (emails, usernames, key names) are `{{var:NAME}}`.

### Bearer token
Most modern APIs.
```http
Authorization: Bearer {{vault:API_TOKEN}}
```

### API key — header
Vendor-specific header name (`X-API-Key`, `apikey`, `Authorization: ApiKey …`).
```http
X-API-Key: {{vault:API_KEY}}
```

### API key — query parameter
```http
GET {{env}}/path?api_key={{vault:API_KEY}}
```
> ⚠️ Query-string secrets leak into server access logs, proxies, and browser history. Prefer a
> header when the API allows it. (The `{{vault:}}` value is still injected at fire-time, never
> written to the file.)

### Basic
The secret is the password. Two forms:

- **Recommended** — vault only the password; keep the username as `{{var:USER}}`; base64 **inside the
  fire subprocess** so the credential pair never lands in the file:
  ```sh
  scripts/vault.sh exec BASIC_PASS -- sh -c \
    'curl -sS -H "Authorization: Basic $(printf "%s:%s" "{{var:USER}}" "$BASIC_PASS" | base64)" "$URL"'
  ```
- **Pre-encoded** — if you must store the whole `base64(user:pass)` blob, treat it as a secret
  (base64 is encoding, not encryption — it decodes back to the credentials):
  ```http
  Authorization: Basic {{vault:BASIC_B64}}
  ```

### Custom multi-header
e.g. Cloudflare's legacy Global Key (**not** an API Token — see the v1 `cloudflare.api.md` caveat).
Non-secret parts are `{{var:}}`; only the secret is `{{vault:}}`.
```http
X-Auth-Email: {{var:CF_EMAIL}}
X-Auth-Key: {{vault:CF_GLOBAL_KEY}}
```

## Shape recipes

Each shape renders into the same four sections; only the `**Request**` fence and the typical
`**Checks**` vary. Pair any shape with an Auth recipe above.

### REST — GET
```http
GET {{env}}/things?per_page={{var:per_page}}
Authorization: Bearer {{vault:API_TOKEN}}
```
Typical **Checks**: `status` == 200 · `.result` is an array · `.result[0].id` is a non-empty string.

### REST — POST (body)
```http
POST {{env}}/things
Authorization: Bearer {{vault:API_TOKEN}}
Content-Type: application/json

{ "name": "{{var:name}}" }
```
Typical **Checks**: `status` == 201 · `.id` is present · `.name` matches the `name` you sent (assert the literal value, not a `{{…}}` reference — Checks never contain references).

### GraphQL
```graphql
query GetThing($id: ID!) { thing(id: $id) { id name } }
```
**Variables**
```json
{ "id": "{{var:THING_ID}}" }
```
Fired as `POST {{env}}` with the auth header + `Content-Type: application/json`.
Typical **Checks**: `status` == 200 · `.errors` == null · `.data.thing.id` is a non-empty string.

### Webhook / Action POST
Fire an event/command; assert acceptance, and the side-effect where it is observable.
```http
POST {{env}}/hooks/order-created
Authorization: Bearer {{vault:WEBHOOK_TOKEN}}
Content-Type: application/json

{ "event": "order.created", "id": "{{var:order_id}}" }
```
Typical **Checks**: `status` == 202 (or 200). If the side-effect is observable, add a **second `##`
endpoint** (e.g. `GET {{env}}/orders/{{var:order_id}}`) to verify it — atomic-per-endpoint still holds.

### Action / tool-shaped
One capability, minimal args — the primitive end of the spectrum. The exact path is vendor-specific
(function-tool / OpenAPI-action platforms differ).
```http
POST {{env}}/tools/search
Authorization: Bearer {{vault:API_TOKEN}}
Content-Type: application/json

{ "query": "{{var:q}}", "limit": {{var:limit}} }
```
Typical **Checks**: `status` == 200 · `.result` is present · `.result` shape matches the tool's contract.

## Advanced auth recipes (runner-driven)

These four schemes are **not** fired by hand — they are computed deterministically by
`scripts/missive-run.sh <file> "<endpoint>" --env <env>` (see `MISSIVE.md` → "Advanced auth
(runner-driven)"). Every recipe below is still `{{vault:NAME}}` / `{{capture:NAME}}`-only: no
literal secret ever appears in the file.

### OAuth2 client-credentials — pre-request + Capture
A **producer** endpoint (an ordinary `##` block) gains a `**Capture**` section; any endpoint that
references `{{capture:NAME}}` in its `**Request**` automatically triggers the producer first — the
dependency is inferred from the reference, not from file order, and a capture cycle is detected
and refused.
```http
POST {{env}}/oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_secret={{vault:CLIENT_SECRET}}
```
**Capture**
- ACCESS_TOKEN <- .access_token

The consuming endpoint then references the captured value — never the vault secret directly:
```http
GET {{env}}/things
Authorization: Bearer {{capture:ACCESS_TOKEN}}
```
Typical **Checks**: `status` == 200 · `.token_type` == "Bearer" (quoted) · `.ok` == true (bare) —
both string forms of the Check RHS work.

A `Capture` source can also pull from a response header instead of the body:
```
NAME <- header X-Session-Id
```

> **Active:** a `(cache .expires_in)` hint after the Capture line reuses the fetched token across runs
> (TTL from the response field, minus a ~10%/60s margin; or `(cache 3300s)` literal, as-written). The
> cache is fingerprint-keyed on the resolving request, so rotating the secret invalidates it; a producer
> using `{{sign:timestamp}}`/another `{{capture:}}` is not cache-safe and refetches. No hint means
> ephemeral (refetch every run), which stays the default.
>
> Producer endpoints fire **unsigned** in v1 scope — put a `**Sign**` block on the endpoint that
> *uses* `{{capture:NAME}}`, not on the token-producing endpoint itself.

### HMAC — `**Sign**` block
Add a `**Sign**` section alongside `**Request**`. `over:` is a string-to-sign template resolved
against the assembled request; it may reference `{method}`, `{path}`, `{query}`, `{body}`,
`{header:Name}` (pulled from the assembled request headers), and `{{sign:timestamp}}` (one instant,
pinned for the whole run — the same value renders wherever else it's referenced, e.g. a header).
```http
POST {{env}}/echo
Content-Type: application/json
X-Timestamp: {{sign:timestamp}}

{"hello":"{{var:greeting}}"}
```
**Sign**
- scheme: hmac-sha256
- over: "{method}\n{path}\n{body}\n{{sign:timestamp}}"
- ts-format: epoch
- encoding: hex
- into: header X-Signature
- secret: {{vault:SIG_SECRET}}

`scheme` is `hmac-sha256` or `hmac-sha1`; `ts-format` is `epoch` (default) \| `rfc1123` \|
`iso8601`; `encoding` is `hex` (default) \| `base64` \| `base64url`; `into` is `header <Name>` or
`query <Name>`. Only `secret:` is a vault reference — the signature itself is computed inside the
runner and never rendered into the file.

### SigV4 — `**Sign**` block
```http
GET {{env}}/ping
```
**Sign**
- scheme: sigv4
- region: us-east-1
- service: execute-api
- key-id: {{vault:AWS_KEY_ID}}
- secret: {{vault:AWS_SECRET}}

Optional fields: `session: {{vault:AWS_SESSION_TOKEN}}` (STS session token — signed via the
`x-amz-security-token` header, not by the field alone) and `unsigned-payload: true` (sets
`x-amz-content-sha256: UNSIGNED-PAYLOAD` instead of hashing the body).

> **Engine caveat:** the signing engine is one global, offline, fail-closed choice — never chosen
> per-request or by inspecting a response. The default (auto) engine is **openssl**, and only after
> it self-certifies against the vendored KAT vectors on this host; the runner refuses to sign at
> all if it can't certify. **curl-native signing is opt-in** (`MR_SIG_ENGINE=curl`) — it's
> argv-clean but not offline-KAT-certifiable, so it is never auto-selected.
>
> **Signing-key argv window:** openssl signing (the default — and the ONLY HMAC path, since LibreSSL's
> `openssl dgst` takes no keyfile) passes the derived key on argv for ~1ms, readable by a same-user/
> root process. That's the same local blast radius as the vault itself, so it's accepted, not a leak
> across a boundary. Set `MR_SIG_ENGINE=curl` to make SigV4 argv-clean where your curl supports it.

### mTLS — `tls:` frontmatter
Client-cert auth is collection-level, set once in the frontmatter (applies to every endpoint fired
from that file), not per-endpoint:
```yaml
tls:
  cert: {{vault:CLIENT_CERT}}
  key: {{vault:CLIENT_KEY}}
  ca: {{var:CA_BUNDLE_PATH}}
```
`cert`/`key` are almost always `{{vault:}}` (the key is the sensitive half of the pair); `ca` is
optional and typically a `{{var:}}` path, not a secret.

> **Backend caveat:** on curl's SecureTransport backend (stock macOS curl, which wants a `.p12`
> pulled from the Keychain rather than a PEM cert/key), mTLS degrades **gracefully** — the runner
> detects the unsupported backend up front and refuses the run with a plain message, rather than
> silently firing over an unauthenticated connection. Nothing secret is written or printed in
> either case.
