# 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 | | --- | --- | --- | | HTTP | `src/Nuthatch/Api.hs`, `src/Nuthatch/App.hs` | `make check-product` | | 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. | | Browser | Public `GET /nuthatch.js`, `GET /login`, and `GET /oidc/callback`; authenticated `GET /app` and `/assets/*`; and CSRF-protected authenticated `POST /logout` implement the browser session boundary. | | Legacy product | Authenticated `GET /`, `GET /download`, `POST /upload`, `POST /delete`, and `POST /delete-prefix` remain supported. | | 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. 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 The ordered files `sql/001_schema.sql`, `sql/002_pool_limit_guard.sql`, and `sql/003_recovery_settlement.sql` are the authoritative schema history. `postgresql-migration` initializes, applies, records, and validates that history at process startup. A fixed PostgreSQL advisory lock serializes migration across replicas, and startup fails if migration or validation fails. Schema changes are new ordered migration files; deployed migration contents do not change. `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 Kubernetes manifest supplies the scoped credential through the secret boundary; `tests/kind/lib.sh` provisions equivalent fixture principals, and `tests/kind/wp05-job.yaml.in` verifies denial against a different bucket. 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.