# Parka Webhook Authentication Contract

This document defines the authentication and replay-protection contract for:

- `POST /api/parka/webhook/unlock`

## Required fields

Parka must provide these fields on **every** request:

- `token`
- `timestamp` (UTC epoch seconds)
- `request_id` (unique per request)
- `signature` (lower- or upper-case hex SHA-256 HMAC)

Fields may be sent either:

- in JSON body (`application/json`), or
- via headers:
  - `X-Parka-Token`
  - `X-Parka-Timestamp`
  - `X-Parka-Request-Id` (or `X-Request-Id`)
  - `X-Parka-Signature`

> If both header and body are present for the same field, header value is used.

## Deterministic signature format

Signature base string format is exactly:

`token|timestamp|request_id`

No whitespace, no newline, no extra delimiters.

Example base string:

`pk_live_abc123|1711459200|req_7f5e2f8e`

Compute signature as:

- Algorithm: `HMAC-SHA256`
- Secret: shared secret configured in `PARKA_WEBHOOK_SECRET`
- Digest output: lowercase or uppercase hex accepted

Pseudo-code:

```text
base = token + "|" + timestamp + "|" + request_id
signature = hex(hmac_sha256(base, PARKA_WEBHOOK_SECRET))
```

## Timestamp freshness

- Server enforces tolerance window of `±300` seconds by default.
- Config key: `services.parka.timestamp_tolerance_seconds` (env: `PARKA_WEBHOOK_TIMESTAMP_TOLERANCE`).

Requests outside tolerance are rejected with `PARKA_TIMESTAMP_EXPIRED`.

## Replay protection

- Replay key: `(token_id, request_id)`
- Default replay window: `300` seconds
- Config key: `services.parka.request_id_ttl_seconds` (env: `PARKA_WEBHOOK_REQUEST_ID_TTL`)
- Backward-compatibility fallback key: `services.parka.nonce_ttl_seconds`

If the same `request_id` is re-used for the same token inside the replay window, request is rejected with `PARKA_REPLAY_DETECTED`.

## Success response

HTTP `200`:

```json
{
  "status": "PARKA_UNLOCK_ACCEPTED",
  "message": "Unlock successful",
  "request_id": "req_7f5e2f8e",
  "lock_id": 123456
}
```

## Failure response shape

All failures return JSON with:

```json
{
  "error": "<EXPLICIT_ERROR_CODE>",
  "message": "<human-readable message>"
}
```

## Error codes and meanings

- `PARKA_MISSING_FIELDS` (`422`): Missing or malformed required fields.
- `PARKA_TOKEN_NOT_REGISTERED` (`404`): Token does not exist.
- `PARKA_TOKEN_INACTIVE` (`403`): Token exists but disabled.
- `PARKA_TOKEN_EXPIRED` (`403`): Token expired.
- `PARKA_TIMESTAMP_EXPIRED` (`403`): Timestamp outside allowed tolerance.
- `PARKA_REPLAY_DETECTED` (`409`): Duplicate `request_id` in replay window.
- `PARKA_SECRET_NOT_CONFIGURED` (`403`): Server-side webhook secret missing.
- `PARKA_BAD_SIGNATURE` (`401`): HMAC does not match.
- `PARKA_LOCK_AUTH_UNAVAILABLE` (`503`): Downstream lock auth unavailable.
- `PARKA_UNLOCK_REJECTED` (`403`): Downstream lock provider denied unlock (fallback when no specific mapping matches).
- `PARKA_LOCK_OFFLINE` (`403`): Offline/not-connected failures. `errcode=1` + `failed or means no` returns `Unknown error, likely offline or weak gateway connection.`; `errcode=-2012` or `errcode=-3002` returns `Gateway or lock offline, needs to be checked`.
- `PARKA_LOCK_BUSY` (`403`): Temporary busy failures (for example `errcode=-3003` or `errcode=-3037`); caller should retry.
- `PARKA_REMOTE_UNLOCK_DISABLED` (`403`): Sciener returned `-4043`; enable remote unlock for the lock.

## Example: valid request

```bash
TOKEN="pk_live_abc123"
TS="1711459200"
REQ_ID="req_7f5e2f8e"
SECRET="your_shared_secret"
BASE="$TOKEN|$TS|$REQ_ID"
SIG=$(printf "%s" "$BASE" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST "https://<host>/api/parka/webhook/unlock" \
  -H "Content-Type: application/json" \
  -d "{\"token\":\"$TOKEN\",\"timestamp\":$TS,\"request_id\":\"$REQ_ID\",\"signature\":\"$SIG\"}"
```

## Example: header-based request

```bash
curl -X POST "https://<host>/api/parka/webhook/unlock" \
  -H "X-Parka-Token: pk_live_abc123" \
  -H "X-Parka-Timestamp: 1711459200" \
  -H "X-Parka-Request-Id: req_7f5e2f8e" \
  -H "X-Parka-Signature: <hmac_hex>"
```

## Common failure modes

1. **Clock skew > 5 minutes**  
   Fix NTP/time sync in sender system.
2. **Signing different canonical string**  
   Ensure exactly `token|timestamp|request_id`.
3. **Reusing `request_id` for retries**  
   Generate a new unique `request_id` for each attempt.
4. **Using wrong shared secret**  
   Verify Parka and server secrets match exactly.
5. **Sending milliseconds instead of seconds**  
   `timestamp` must be epoch seconds (UTC).
