11 KiB
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, and the abstract
mutation protocol remains in Nuthatch.tla.
| Contract | Implementation owner | Owning gate |
|---|---|---|
| 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 |
| 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 /login and GET /oidc/callback; authenticated GET /app and /assets/*; and CSRF-protected authenticated POST /logout implement the browser session boundary. |
| 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.
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.
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.
Each delete-batch item contains exactly one typed outcome: a mutation result or
a closed dependency-failure code. Producers and consumers reject items with
both outcomes or neither outcome.
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
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.
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:
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:
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/fixture/target.sh provisions equivalent fixture principals, and
test/AuthProbe.hs 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.