nuthatch/docs/spec/contracts.md

11 KiB
Raw Blame History

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/Browse.hs, src/Nuthatch/Transfer.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 /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.

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

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/lib.sh provisions equivalent fixture principals, and tests/kind/product-matrix-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.