198 lines
11 KiB
Markdown
198 lines
11 KiB
Markdown
# 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.
|