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 |
|
|
|
|
|
|
| --- | --- | --- |
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
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
|
|
|
|
|
|
Kubernetes manifest supplies the scoped credential through the secret boundary;
|
|
|
|
|
|
`tests/kind/lib.sh` provisions equivalent fixture principals, and
|
2026-07-19 15:21:24 +09:00
|
|
|
|
`tests/kind/product-matrix-job.yaml.in` 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.
|