2026-07-19 05:58:17 +09:00
# Nuthatch implementation contracts
## Status and ownership
This document defines the durable version-one HTTP, SQL, identity encoding,
content identity, environment, supported-store, and deployment contracts. The
product requirements remain in [`nuthatch.md` ](nuthatch.md ), and the abstract
mutation protocol remains in [`Nuthatch.tla` ](../protocol/Nuthatch.tla ).
| Contract | Implementation owner | Owning gate |
| --- | --- | --- |
2026-07-20 03:20:08 +09:00
| HTTP | `src/Nuthatch/Api.hs` , `src/Nuthatch/App.hs` , `src/Nuthatch/Http/Server.hs` , `src/Nuthatch/Http/Auth.hs` , `src/Nuthatch/Http/Error.hs` , `src/Nuthatch/Http/Contract.hs` , `src/Nuthatch/Browse.hs` , `src/Nuthatch/Transfer.hs` , `web/src/generated/contract.ts` , `web/src/transport.ts` | `make check-product` |
2026-07-19 05:58:17 +09:00
| SQL | `sql/` , `src/Nuthatch/Db.hs` , `src/Nuthatch/Protocol.hs` | `make check-storage-foundation` |
| Identity encoding | `src/Nuthatch/Paths.hs` | `make test` |
| Content identity | `src/Nuthatch/Streaming.hs` , `src/Nuthatch/S3.hs` | `make check-streaming` |
| Environment | `src/Nuthatch/Config.hs` | `make check-runtime` |
| Supported store | `src/Nuthatch/S3.hs` | `make check-protocol` |
| Deployment authority | Deployment object-store IAM/policy; secret injection in `deploy/kubernetes/nuthatch.yaml.in` | `make check-auth` |
`make check-specs` validates this document's structure and links. The aggregate
`make check-v1` gate verifies the implementation owners on the supported
provider matrix.
## HTTP contract
`Nuthatch.Api.Api` is the authoritative route and schema declaration. Servant
performs route, query, header, request-body, response-body, and content-type
decoding and rejects values outside the declared types. Application-defined
failures use closed JSON errors with `error` , `message` , and `details` fields;
framework route and decoding failures retain Servant's response format.
| Surface | Contract |
| --- | --- |
| Runtime | `GET /healthz` reports process liveness. `GET /readyz` reports declared traffic safety. |
2026-07-19 17:49:03 +09:00
| Browser | Public `GET /login` and `GET /oidc/callback` ; authenticated `GET /app` and `/assets/*` ; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. |
2026-07-19 05:58:17 +09:00
| Programmatic product | Authenticated `/api/session` , `/api/browse` , `/api/download` , `/api/upload` , `/api/delete` , `/api/delete-plan` , and `/api/delete-batch` expose the JSON product surface. |
Product paths are valid UTF-8, NFC-normalized, relative, and canonical. The
server decodes each component once and rejects empty segments, dot segments,
backslashes, controls, aliases, traversal, and non-canonical encodings instead
of rewriting them. Root values are the `personal` and `shared` enum values;
clients never submit complete object keys. Operation ids are 1 through 128
URL-safe ASCII characters from the unreserved set.
2026-07-20 03:20:08 +09:00
`Nuthatch.Http.Contract` traverses `ProductApi` and its explicit wire DTO
mappings to generate the checked TypeScript product contract. The handwritten
browser transport implements that generated interface and exclusively owns
fetch, credential, encoding, binary-body, response-header, retry, and
non-success decoding policy.
2026-07-19 05:58:17 +09:00
Upload requests stream `application/octet-stream` , require
`X-Declared-Size` , `X-Content-SHA256` , `X-CSRF-Token` , and an operation id, and
return a typed mutation result with the advertised terminal idempotency window.
Downloads stream through Nuthatch, reject `Range` , use a safe attachment
filename, and set active-content isolation headers. The compiled application
page governs its assets with the application content-security policy, and
`/assets/*` responses prevent content sniffing. HTTPS is the public origin and
cookie security boundary; the application also serves TLS using the configured
certificate and key paths.
## SQL contract
2026-07-19 15:21:24 +09:00
`sql/001_initial_schema.sql` is the authoritative initial schema.
`postgresql-migration` initializes, applies, records, and validates it at
process startup. A fixed PostgreSQL advisory lock serializes migration across
replicas, and startup fails if migration or validation fails. After the first
deployment, schema changes are new ordered migration files and deployed
migration contents do not change.
2026-07-19 05:58:17 +09:00
`pools` owns quota limits, used bytes, and reservations.
`mutations` owns idempotency identity, request fingerprint data, protocol
state, store-settlement timing, and worker leases. `key_locks` provides one
active mutation per canonical object key. `objects_meta` owns the accounted
size and content identity for successful objects. Database constraints enforce
non-negative accounting, valid state enums, complete pending-dispatch timing,
and `used_bytes + reserved_bytes <= limit_bytes` . Mutation admission and
finalization use PostgreSQL transactions and transaction-scoped advisory locks.
## Identity encoding contract
The durable `user_id` is lowercase hexadecimal SHA-256 over these exact bytes:
```text
UTF-8(iss) || 0x00 || UTF-8(sub)
```
OIDC issuer and subject values are used exactly after token validation; display
names are not identity inputs. The NUL separator makes the pair encoding
unambiguous. The result is 64 ASCII characters and forms the personal object
prefix `users/{user_id}/` .
The stable test vector is:
```text
iss: https://issuer.example
sub: subject-α
user_id: e0240ab33521a1882fb647a79f7fd3c74c9b6348b29bdb14e572204517882859
```
## Content identity contract
Content identity is SHA-256 over the exact object-body bytes, encoded as 64
lowercase hexadecimal ASCII characters. Upload clients provide it in
`X-Content-SHA256` ; Nuthatch computes it incrementally while streaming and
rejects a body whose length or digest differs from the declaration.
Nuthatch stores the identity as S3 user metadata named `nuthatch-sha256` and in
PostgreSQL `objects_meta.content_sha256` . S3 carries this metadata as
`x-amz-meta-nuthatch-sha256` , and object header reads expose it when valid.
Protocol observation always streams the full body and computes its digest.
Same-size replacement is decided by content identity, not size alone, and
observation fails closed when it cannot establish the full state.
## Environment contract
`Nuthatch.Config.loadConfig` is the authoritative parser. Required values are:
| Group | Required variables |
| --- | --- |
| Public and session | `NUTHATCH_PUBLIC_BASE_URL` , `NUTHATCH_JWT_SECRET` |
| OIDC | `NUTHATCH_OIDC_ISSUER` , `NUTHATCH_OIDC_CLIENT_ID` , `NUTHATCH_OIDC_CLIENT_SECRET` , `NUTHATCH_OIDC_REDIRECT_URI` |
| PostgreSQL and Redis | `NUTHATCH_PG_CONN` , `NUTHATCH_REDIS_URL` |
| Object store | `NUTHATCH_S3_ENDPOINT` , `NUTHATCH_S3_BUCKET` , `NUTHATCH_S3_ACCESS_KEY` , `NUTHATCH_S3_SECRET_KEY` |
The public base and redirect URI are absolute HTTPS URIs, and the redirect is
exactly `{public-base}/oidc/callback` . The issuer is absolute HTTP or HTTPS.
The S3 endpoint is an HTTP or HTTPS origin without a path, query, or fragment.
The Redis URI uses `redis` , contains at most one database segment in the range
0 through 15, and defaults its port to 6379. URI-valued settings reject embedded
credentials and accept bracketed IPv6 authorities.
`NUTHATCH_JWT_SECRET` contains at least 32 UTF-8 bytes.
| Variable | Default and accepted range |
| --- | --- |
| `NUTHATCH_LISTEN` | `127.0.0.1:8080` ; port 1 through 65535 |
| `NUTHATCH_TLS_CERT_PATH` , `NUTHATCH_TLS_KEY_PATH` | `/tls/tls.crt` , `/tls/tls.key` |
| `NUTHATCH_SESSION_HOURS` | 12; 1 through 168 |
| `NUTHATCH_OIDC_TRANSACTION_MINUTES` | 5; 1 through 60 |
| `NUTHATCH_OIDC_MAX_ID_TOKEN_SECONDS` | 3600; 1 through 86400 |
| `NUTHATCH_S3_REGION` , `NUTHATCH_S3_PATH_STYLE` | `us-east-1` , true |
| `NUTHATCH_PERSONAL_QUOTA_BYTES` , `NUTHATCH_SHARED_QUOTA_BYTES` | 1 GiB, 10 GiB; positive |
| `NUTHATCH_MAX_OBJECT_BYTES` | 100 MiB; positive and within the supported multipart range |
| `NUTHATCH_THROTTLE_BYTES_PER_SEC` , `NUTHATCH_THROTTLE_BURST_BYTES` | 10 MiB, 20 MiB; positive and exactly representable by the Redis script |
| `NUTHATCH_LIST_CACHE_TTL_SEC` | 30; 1 through 86400 |
| `NUTHATCH_BROWSE_PAGE_SIZE` , `NUTHATCH_DELETE_PAGE_SIZE` | 200, 100; 1 through 1000 |
| `NUTHATCH_SEARCH_MAX_KEYS` | 500; 1 through 100000 |
| `NUTHATCH_SEARCH_TIMEOUT_MS` | 2000; 1 through 60000 |
| `NUTHATCH_RECOVERY_GRACE_SEC` , `NUTHATCH_IDEMPOTENCY_WINDOW_SEC` , `NUTHATCH_OBJECT_REQUEST_TIMEOUT_SEC` | 90, 86400, 300; positive |
| `NUTHATCH_STARTUP_ATTEMPTS` , `NUTHATCH_STARTUP_DELAY_MS` | 60, 1000; 1 through 1000 attempts and 1 through 60000 milliseconds |
| `NUTHATCH_AUDIT_OBJECT_KEYS` | false |
`NUTHATCH_COOKIE_NAME` , `NUTHATCH_CSRF_COOKIE_NAME` , and
`NUTHATCH_OIDC_TRANSACTION_COOKIE_NAME` default to `nuthatch_session` ,
`nuthatch_csrf` , and `nuthatch_oidc` . `NUTHATCH_OIDC_ADDITIONAL_AUDIENCES`
defaults to none and, when present, contains unique nonempty values distinct
from the client id. Boolean values accept `1` , `true` , `yes` , `0` , `false` , and
`no` case-insensitively. Secret-bearing configuration uses non-`Show`
application types and sensitive parser values.
## Supported-store contract
A supported object store implements ordinary S3 put, get, head, delete, and
list operations against one configured bucket. A completed single-key put is
an atomic replacement: readers observe the prior body or the complete new
body, never a partial body. Every mutation request reaches an irrevocable
settled outcome within the configured request bound and cannot mutate the key
after settlement. Delete of an absent key is successful.
Correctness does not require versioning, conditional mutation, event delivery,
server-side copy, an administrative API, or transactional store extensions. A
store is unsupported when it cannot provide atomic single-key replacement,
bounded irrevocable settlement, stable read-after-settlement observation, or
the ordinary operations above.
Garage and VersityGW are the implementations exercised by the supported-target
gate. They are not product requirements: another S3-compatible implementation
is supported when it passes the same contract gate.
## Deployment authority contract
The service principal has data-plane authority only for
`NUTHATCH_S3_BUCKET` . It has no bucket-administration authority and no
authority over other buckets. End users and browsers never receive its
credentials.
The deployment's object-store IAM or policy system enforces that scope. The
2026-07-19 23:57:21 +09:00
Kubernetes manifest supplies the scoped credential through the secret boundary.
`tests/kind/fixture/target.sh` provisions equivalent fixture principals, and
`test/AuthProbe.hs` verifies denial against a different bucket.
2026-07-19 05:58:17 +09:00
Nuthatch is the exclusive writer to `users/` and `shared/` in the configured
bucket while the service is enabled. Operators may inspect those prefixes, but
external puts, overwrites, deletes, restores, lifecycle mutation, or replication
into them violate quota accounting, content identity, and recovery guarantees.
Other bucket prefixes are outside Nuthatch ownership.
All replicas share PostgreSQL, Redis-compatible coordination, the configured
bucket, the JWT signing material, and the OIDC client registration. Startup
migration serialization and mutation/recovery leases make overlapping
replicas safe. The public origin is HTTPS, secrets come from the deployment
secret boundary, and structured audit records leave through stdout.