# Whohouse CDN: Complete Service Guide

This is the canonical human-readable document for using and operating Whohouse CDN. The live copy is:

- https://cdn.whohouse.io/guide.md
- https://cdn.whohouse.io/guide (an alias with identical bytes)

Service base URL: **https://cdn.whohouse.io**

Public URL pattern: **https://HOST.cdn.whohouse.io/PATH**

API version: **v1**

Last updated: **2026-09-14**

The downloadable OpenAPI description and dependency-free clients are supplements to this guide:

- https://cdn.whohouse.io/openapi.json
- https://cdn.whohouse.io/sdk/python.py
- https://cdn.whohouse.io/sdk/javascript.mjs

The OpenAPI file is useful for machine discovery. This guide is the source of truth for edge cases, exact-wire behavior, persistence, and operations.

## 1. What the service does

Whohouse CDN lets an authenticated caller publish response bytes at a stable HTTPS URL beneath cdn.whohouse.io. It has three deployment modes:

1. **File mode** accepts body bytes and adds a conventional status line and useful response headers.
2. **Structured mode** constructs a response from a status, reason, first line, ordered headers, and a text or binary body.
3. **Raw mode** treats the upload as the entire response. It performs no parsing or normalization. The stored bytes can begin with HTTP/1.1 999 SWAG, asjidoasdi, an empty line, binary bytes, or anything else.

Every successful deployment creates a new preserved revision. A route is a pointer from host + request method + target to its current revision. Replacing a route moves only that pointer; it does not remove older revisions.

The control plane and admin console require one shared bearer token. Published wildcard-host responses are public and require no token.

### Deliberate boundaries

- The service controls bytes written **after** a valid incoming HTTP/1.x request reaches the wire server.
- It does not let a deployment alter DNS, the TLS handshake, certificate, ALPN, or bytes sent before the request.
- Clients must connect with TLS to port 443 and send a parseable HTTP/1.x request line and headers.
- The Network Load Balancer terminates TLS and negotiates HTTP/1 only. HTTP/2 is deliberately not offered.
- The response itself does not need to be valid HTTP.
- The server serves one request per connection, writes the selected stored object directly, and closes the connection.
- Conventional clients may reject, truncate, reinterpret, or hide deliberately malformed responses. Raw TLS inspection is the authoritative way to compare bytes.

## 2. Authentication and the API key

Use this exact header on every /v1 request:

    Authorization: Bearer TOKEN

The prefix is the case-sensitive text **Bearer** followed by one space. The token is a single global credential: it grants access to every control-plane read, deployment operation, revision download, and admin statistic. Agent names do not create authorization boundaries.

The production token is intentionally not printed in this public guide. Obtain it from an operator and place it in an environment variable:

    export WHOHOUSE_CDN_TOKEN='paste-token-here'

The bundled clients look for WHOHOUSE_CDN_TOKEN and then CDN_TOKEN. Do not put the token in a public URL, query parameter, deployed response, source repository, screenshot, or browser code distributed to untrusted users.

Authentication is not required for:

- public responses at *.cdn.whohouse.io;
- GET /, /admin, and /admin/;
- GET /healthz;
- this guide, OpenAPI, JavaScript, CSS, and SDK assets;
- OPTIONS requests whose path begins /v1/.

Missing or incorrect authentication on other /v1 calls returns 401 with:

    {
      "error": "unauthorized",
      "message": "Use Authorization: Bearer TOKEN."
    }

and WWW-Authenticate: Bearer realm="whohouse-cdn".

## 3. Sixty-second quickstart

### Publish ordinary text

    curl --fail-with-body -X PUT \
      'https://cdn.whohouse.io/v1/file?host=my-agent-demo&path=%2Fhello.txt&agent=quickstart' \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: text/plain; charset=utf-8' \
      --data-binary 'hello from an agent'

The result is HTTP 201 JSON. Open the returned URL or:

    curl -i https://my-agent-demo.cdn.whohouse.io/hello.txt

### Let the service generate a hostname

Omit host:

    curl --fail-with-body -X PUT \
      'https://cdn.whohouse.io/v1/file?path=%2Fresult.json&agent=quickstart&content_type=application%2Fjson' \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      --data-binary '{"answer":42}'

Always save the returned **url**, **host**, and **revision_id**.

### Publish HTTP/1.1 999 SWAG with duplicate headers

    curl --fail-with-body -X POST https://cdn.whohouse.io/v1/deploy \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary '{
        "host": "my-swag-demo",
        "path": "/anything.php.whatever",
        "agent": "quickstart",
        "status": 999,
        "reason": "SWAG",
        "headers": [
          ["Content-Type", "text/plain"],
          ["X-Repeat", "one"],
          ["X-Repeat", "two"]
        ],
        "body": "hello"
      }'

### Publish an exact malformed response

    printf 'asjidoasdi\r\nAnything: yes\r\n\r\nexact bytes' |
      curl --fail-with-body -X PUT \
        'https://cdn.whohouse.io/v1/raw?host=my-raw-demo&path=%2Fraw.bin&agent=quickstart' \
        -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
        -H 'Content-Type: application/octet-stream' \
        --data-binary @-

Inspect it without an HTTP response parser:

    printf 'GET /raw.bin HTTP/1.1\r\nHost: my-raw-demo.cdn.whohouse.io\r\nConnection: close\r\n\r\n' |
      openssl s_client -quiet \
        -connect my-raw-demo.cdn.whohouse.io:443 \
        -servername my-raw-demo.cdn.whohouse.io 2>/dev/null

## 4. Hosts, paths, methods, queries, and route selection

A route key is the exact combination of:

- normalized direct wildcard hostname;
- normalized request method or the wildcard method *;
- stored target string.

### Host rules

The API accepts either a single label, such as **demo-7**, or a full direct hostname, such as **demo-7.cdn.whohouse.io**.

- Input is trimmed, lowercased, and has one trailing dot removed.
- The label must contain 1–63 lowercase ASCII letters, digits, or hyphens.
- A hyphen cannot be first or last.
- Underscores, Unicode labels, dots inside the label, and nested hosts are rejected.
- demo.cdn.whohouse.io is valid.
- a.b.cdn.whohouse.io is invalid and is not covered by the wildcard certificate.
- Omitting host generates **a** followed by 16 lowercase hexadecimal characters.

A generated or validated host is not reserved. POST /v1/hosts reports reserved=false because a host becomes meaningful only when a route is deployed. Any bearer-token holder can deploy to or replace any valid route.

### Path and target rules

- The default path is /.
- A deployment path must begin with /.
- It can be at most 16,384 bytes.
- CR, LF, and NUL are rejected.
- Other characters are preserved; the service does not clean extensions or normalize dot segments.
- Public request parsing uses the escaped path spelling. If a path contains spaces or non-ASCII bytes, store the percent-escaped target that the client will actually send.
- When a path is carried as a control-plane query parameter, percent-encode that query parameter. For example, path=/x becomes path=%2Fx.

Paths may deliberately include a query:

    POST /v1/deploy
    {
      "host": "query-demo",
      "path": "/answer?format=raw&n=1",
      "body": "query-specific"
    }

For an origin-form public request, resolution first tries the exact request target including its query string, then falls back to the escaped path without the query. Therefore:

- a route at /answer?format=raw&n=1 wins for that exact target;
- query order and percent-encoding are significant for the exact-target match;
- if no exact-query route exists, /answer is used as a fallback;
- absolute-form request targets are resolved by path rather than by the complete absolute URI.

Fragments are not normally sent in HTTP requests. Do not use a URL fragment as a route selector.

### Method rules and precedence

- The deployment default is method=*.
- Named methods are uppercased.
- A named method must be an HTTP token, 1–32 characters long, beginning with A–Z.
- For most public requests, an exact method route wins, followed by *.
- HEAD has special lookup order: HEAD, then GET, then *.
- The stored bytes are never changed for HEAD. A GET fallback can therefore include a body even when the incoming method is HEAD.
- There is no automatic OPTIONS behavior on public wildcard hosts; deploy it like any other method if needed.

Examples:

- GET + /x and POST + /x can point to different revisions.
- * + /x is a fallback for any method without a more specific route.
- Replacing GET + /x does not replace * + /x.

### Public-host failures

An undeployed direct wildcard host/path returns a conventional 404 JSON error. A host outside exactly one label beneath cdn.whohouse.io returns unknown_host. Storage lookup and object-read failures return a conventional 503. These fallback errors are generated by the service; successfully selected public responses are delivered exactly as stored and receive no generated headers.

## 5. Common deployment behavior

POST and PUT are both accepted for /v1/deploy, /v1/file, and /v1/raw.

Common inputs:

| Input | Default | Meaning |
| --- | --- | --- |
| host | generated | One label or a full direct wildcard hostname |
| path | / | Exact stored target, beginning with / |
| method | * | Exact uppercase request method or wildcard |
| agent | X-Whohouse-Agent, then anonymous | Self-declared statistics label |
| replace | true | Whether an existing route pointer may move |

Agent input is trimmed. It can be at most 128 bytes and cannot contain CR, LF, or NUL.

For file/raw query parameters, replace is parsed using ordinary boolean spellings such as true, false, 1, 0, t, and f. An empty or unrecognized value falls back to true. Structured JSON requires a JSON boolean when present.

Setting replace=false makes route creation conditional. If the exact host + method + path route already exists, the call returns 409 route_exists and the current route remains unchanged. This is also race-safe when multiple writers try to create the same route.

All successful deployments return HTTP 201 with:

    {
      "ok": true,
      "url": "https://example.cdn.whohouse.io/path",
      "host": "example.cdn.whohouse.io",
      "path": "/path",
      "method": "*",
      "revision_id": "20260914T120000.123456789Z-0123456789abcdef",
      "created_at": "2026-09-14T12:00:00.123456789Z",
      "response_bytes": 112,
      "revision_metadata_url": "https://cdn.whohouse.io/v1/revisions/REVISION_ID"
    }

The URL is ready when 201 is returned. Save the revision ID if exact historical recovery matters.

Uploads can use Content-Length or Transfer-Encoding: chunked. If Expect: 100-continue is supplied, the service sends 100 Continue only after the bearer token has been accepted. A request without Content-Length or chunked encoding is treated as having a zero-byte body.

## 6. File deployment: /v1/file

Use file mode for the easiest ordinary response and for streaming large bodies.

    PUT /v1/file?host=LABEL&path=%2FPATH&method=*&agent=NAME&replace=true&status=200&reason=OK&content_type=text%2Fplain

POST is also accepted. The request body becomes the response body.

### File query parameters

| Name | Default | Details |
| --- | --- | --- |
| host | generated | Common host rules apply |
| path | / | Common path rules apply |
| method | * | Common method rules apply |
| agent | header or anonymous | Common agent rules apply |
| replace | true | Common replace behavior applies |
| status | 200 | Integer from 100 through 999 |
| reason | default for status | Used only when generating the first line |
| content_type | request Content-Type, then application/octet-stream | Stored metadata and generated response header |

### File request headers

| Header | Meaning |
| --- | --- |
| Authorization | Required bearer token |
| Content-Type | Fallback response content type when content_type is absent |
| X-Whohouse-Agent | Fallback agent when agent is absent |
| X-Whohouse-First-Line | Replaces the generated first line |
| X-Whohouse-Response-Headers | Unpadded base64url JSON response headers |
| Expect: 100-continue | Supported after authentication |

X-Whohouse-Response-Headers decodes to either:

- a JSON object whose values are strings or arrays of strings; or
- an ordered JSON array of [name, value] pairs.

Use the pair-array form for duplicate fields and exact field order. Object keys are emitted in lexicographic order.

Python encoding example:

    import base64, json

    pairs = [["X-Repeat", "one"], ["X-Repeat", "two"]]
    encoded = base64.urlsafe_b64encode(
        json.dumps(pairs).encode()
    ).rstrip(b"=").decode()
    print(encoded)

The encoding must be URL-safe Base64 without trailing = padding.

### File response construction

Unless overridden, the first line is:

    HTTP/1.1 STATUS REASON

The numeric status is padded to at least three digits. The service appends:

- Content-Type if no case-insensitive Content-Type was supplied;
- Content-Length if neither Content-Length nor Transfer-Encoding was supplied;
- Connection: close if no Connection field was supplied;
- one CRLF after each header and one empty CRLF line before the body.

Supplied response headers are trusted. The service does not reconcile a supplied Content-Length or Transfer-Encoding with the actual body. Header names must be nonempty and cannot contain colon, CR, or LF. Values cannot contain CR or LF. Use raw mode for anything outside those restrictions.

X-Whohouse-First-Line may be arbitrary text but cannot contain CR or LF. The status query is still validated even when this header is present.

### File size and memory behavior

The production body limit is 8 GiB. The server streams the request to a temporary file, assembles a separate wire file, then streams that file to S3. For very large uploads, use direct HTTP rather than the bundled Python client, because the Python convenience client reads the local input into memory first.

## 7. Structured deployment: /v1/deploy

Use structured mode when JSON is convenient and the complete request is no larger than 32 MiB.

    POST /v1/deploy
    Content-Type: application/json

### Complete JSON field reference

| Field | Type | Default | Details |
| --- | --- | --- | --- |
| host | string | generated | Common host rules |
| path | string | / | Common path rules |
| method | string | * | Common method rules |
| agent | string | header or anonymous | Common agent rules |
| replace | boolean | true | Existing-route behavior |
| status | integer | 200 | 100–999; use raw wire for a nonnumeric status |
| reason | string | mapped reason or Custom | Empty means use the default mapping |
| version | string | HTTP/1.1 | Prefix used for a generated first line |
| first_line | string | generated | Overrides version + status + reason; no CR/LF |
| headers | object or pair array | none | Response fields; pair arrays preserve duplicates/order |
| auto_headers | boolean | true | Adds useful type, length, and connection fields |
| content_type | string | mode-dependent | Default generated type and stored metadata |
| body | string | empty | UTF-8 JSON string bytes |
| body_base64 | string | empty | Standard Base64; arbitrary body bytes |
| wire | string | empty | Complete raw response represented by UTF-8 JSON text |
| wire_base64 | string | empty | Standard Base64 for complete raw response bytes |

Unknown JSON fields are ignored. Type mismatches and malformed JSON are rejected.

### Header JSON forms

Object form:

    {
      "headers": {
        "Content-Type": "text/plain",
        "Set-Cookie": ["a=1", "b=2"]
      }
    }

Object keys are emitted in lexicographic order; array values preserve order within their key.

Ordered pair form:

    {
      "headers": [
        ["X-First", "one"],
        ["X-First", "two"],
        ["X-Last", "three"]
      ]
    }

Header-name matching for automatic fields is case-insensitive. Structured header names must be nonempty and cannot contain colon, CR, or LF. Values cannot contain CR or LF. Other unusual bytes are not normalized.

### Automatic construction

When wire and wire_base64 are empty, the service constructs:

    FIRST-LINE\r\n
    Header-Name: value\r\n
    ...\r\n
    \r\n
    BODY

If first_line is empty:

    VERSION STATUS REASON

Status is formatted with at least three digits. If auto_headers is true:

- Content-Type is added unless already present.
- Its default is application/octet-stream when body_base64 is nonempty.
- Otherwise its default is text/plain; charset=utf-8.
- An explicit content_type changes the generated field.
- Content-Length is added unless Content-Length or Transfer-Encoding already exists.
- Connection: close is added unless Connection already exists.

auto_headers=false suppresses all three automatic fields. The terminal empty line before the body is still added.

body and body_base64 are mutually exclusive when both are nonempty. wire and wire_base64 are also mutually exclusive when both are nonempty.

An empty body is valid. For a completely empty **raw response**, use /v1/raw with Content-Length: 0; an empty wire string is interpreted as structured mode.

### Raw-through-JSON behavior

If wire or wire_base64 is nonempty, it bypasses all structured construction:

- status, reason, version, first_line, headers, auto_headers, content_type, and body do not affect the stored bytes;
- mode metadata is raw;
- the JSON request still has the 32 MiB limit;
- wire text is encoded as UTF-8;
- wire_base64 is the choice for arbitrary bytes.

For larger raw responses or streaming, use /v1/raw.

### Default reason mapping

| Status | Default reason |
| --- | --- |
| 200 | OK |
| 201 | Created |
| 202 | Accepted |
| 204 | No Content |
| 301 | Moved Permanently |
| 302 | Found |
| 307 | Temporary Redirect |
| 308 | Permanent Redirect |
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 409 | Conflict |
| 418 | I'm a teapot |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 503 | Service Unavailable |
| 999 | SWAG |
| every other allowed status | Custom |

Use reason or first_line to override the mapping.

## 8. Exact wire deployment: /v1/raw

Use raw mode when every response byte matters.

    PUT /v1/raw?host=LABEL&path=%2FPATH&method=*&agent=NAME&replace=true

POST is also accepted. The request body is the complete stored response. There is no required first line, header block, CRLF separator, or body. Zero bytes are valid. No bytes are inserted, removed, validated, decoded, compressed, or rewritten by the application.

Query parameters are the common host, path, method, agent, and replace parameters.

Optional request headers affect metadata only:

| Header | Metadata effect |
| --- | --- |
| X-Whohouse-Status-Hint | status_hint; defaults to exact wire bytes |
| X-Whohouse-Content-Type | content_type |
| Content-Type | Describes the upload to the control plane; it is not copied into the public response |

The production raw upload limit is 8 GiB. Content-Length and chunked uploads are supported.

### Byte-for-byte verification in Python

    import socket, ssl

    host = "my-raw-demo.cdn.whohouse.io"
    request = (
        "GET /raw.bin HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Connection: close\r\n\r\n"
    ).encode()

    context = ssl.create_default_context()
    with socket.create_connection((host, 443), timeout=30) as tcp:
        with context.wrap_socket(tcp, server_hostname=host) as tls:
            tls.sendall(request)
            chunks = []
            while True:
                chunk = tls.recv(65536)
                if not chunk:
                    break
                chunks.append(chunk)

    actual = b"".join(chunks)
    print(len(actual), actual.hex())

Use SNI matching the wildcard hostname. Do not use curl, requests, fetch, or a browser to prove equality for malformed data; those tools parse HTTP responses.

## 9. Host generation: /v1/hosts

    POST /v1/hosts
    POST /v1/hosts?host=requested-label

This endpoint validates a requested host or generates one using the same rules as deployment. It does not reserve anything.

Response:

    {
      "host": "a0123456789abcdef.cdn.whohouse.io",
      "base_url": "https://a0123456789abcdef.cdn.whohouse.io",
      "reserved": false,
      "note": "The host becomes visible when its first response is deployed."
    }

You can usually skip this endpoint: omitting host during deployment generates a host and returns it in the 201 result.

## 10. Registry, revisions, agents, and statistics

All endpoints in this section require the bearer token.

### Pagination

These list endpoints accept:

- limit: integer 1–500, default 100;
- cursor: opaque next_cursor returned by the prior page.

Do not inspect, modify, persist as a permanent bookmark, or synthesize a cursor. Omit it on the first request and stop when next_cursor is absent. Results are newest-updated first. DynamoDB index reads used for lists can be eventually consistent, so a new item may take a short time to appear even though its returned public URL and direct revision lookup are ready.

List response:

    {
      "items": [ ... ],
      "next_cursor": "opaque-value-if-more-items-exist"
    }

### GET /v1/routes

Lists current route pointers. There is one current record per exact host + method + path.

Route statistics are cumulative for that route key across replacements. created_at remains the route's original creation time; updated_at describes the current revision. The response metadata fields describe the current revision.

### GET /v1/revisions

Lists every preserved immutable revision. A revision is created for every successful deployment, including replacements.

Revision delivery counters are not updated when the response is served. Route, agent, and total records carry delivery counters.

### GET /v1/agents

Lists self-declared agent aggregates. Agent is metadata, not an authenticated identity.

- deployments counts successful revisions attributed to that label;
- response_bytes is the sum of complete stored response sizes deployed by that label;
- requests and bytes_served are best-effort public delivery statistics;
- last_served_at is updated after public writes.

### GET /v1/revisions/{revision_id}

Returns one revision's metadata using a strongly consistent lookup. A missing ID returns 404 revision_not_found.

### GET /v1/revisions/{revision_id}/wire

Downloads the exact stored complete response as the body of a conventional authenticated HTTP 200 control-plane response.

Generated control response fields include:

- Content-Type: application/octet-stream
- Content-Length: the stored response byte count
- Content-Disposition: attachment; filename="REVISION_ID.wire"

The download wrapper is not part of the stored bytes. The response body is.

Downloading an old revision does not move a public route. There is no rollback endpoint. To restore it, download its wire body and submit that body to /v1/raw at the desired host, path, and method; this creates another preserved revision.

### GET /v1/admin/overview

Returns:

    {
      "total": { ...aggregate record... },
      "recent_revisions": [ ...up to 12... ],
      "agents": [ ...up to 500, sorted by deployments... ],
      "route_count": 0,
      "revision_count": 0,
      "generated_at": "RFC3339 timestamp"
    }

route_count and revision_count are derived from cumulative successful deployment counters. The dashboard uses this endpoint plus the list endpoints.

### Record fields

JSON omits fields that are empty or zero.

| Field | Meaning |
| --- | --- |
| kind | route, revision, agent, or total |
| revision_id | Unique timestamp-plus-random revision identifier |
| host | Full public hostname |
| path | Stored exact target |
| method | Exact method or * |
| agent | Self-declared label |
| mode | file, structured, or raw |
| status_hint | Generated first line or raw metadata hint |
| content_type | Deployment metadata; not a guarantee that raw bytes contain that header |
| url | Public URL built from host and path |
| created_at | RFC3339Nano creation time |
| updated_at | RFC3339Nano last deployment update |
| last_served_at | Best-effort most recent completed/partial public write |
| payload_bytes | File/structured body size; in raw mode, complete wire size |
| response_bytes | Complete wire size for a route/revision, or aggregate deployed bytes for agent/total |
| deployments | Successful revision count for agent/total |
| requests | Best-effort served request count for route/agent/total |
| bytes_served | Actual bytes successfully written, including partial writes on disconnect |
| routes | Number of distinct route keys ever successfully created in total record |

Internal S3 object keys and DynamoDB primary keys are never returned.

### Statistics consistency

Serving statistics update asynchronously after the connection write:

- route, attributed agent, and total each receive one request and the number of bytes actually written;
- errors are deliberately ignored so serving is not blocked by analytics;
- counters can lag, and they are not a billing or security audit log;
- replacing a route preserves its route-level request and byte counters;
- future requests after replacement are attributed to the new revision's agent;
- no per-request history, visitor identity, source IP report, or request-header log is exposed.

## 11. Public and unauthenticated control endpoints

| Method and path | Result |
| --- | --- |
| GET / | Admin SPA shell |
| GET /admin | Admin SPA shell |
| GET /admin/ | Admin SPA shell |
| GET /healthz | JSON health response with service name and current UTC time |
| GET /guide | This Markdown guide |
| GET /guide.md | This Markdown guide |
| GET /openapi.json | OpenAPI 3.1 description |
| GET /sdk/python.py | Dependency-free Python client |
| GET /sdk/javascript.mjs | JavaScript ES module |
| GET /assets/app.js | Admin application |
| GET /assets/styles.css | Admin styles |
| OPTIONS /v1/... | 204 CORS preflight |

Other base-host paths return 404 not_found. Static assets and health are served only when the Host is exactly cdn.whohouse.io.

Generated control-plane responses include:

- Content-Length;
- Connection: close;
- an appropriate Content-Type;
- Access-Control-Allow-Origin: *;
- Access-Control-Allow-Headers: Authorization, Content-Type, X-Whohouse-Agent, X-Whohouse-First-Line, X-Whohouse-Response-Headers;
- Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS.

CORS permits browser code from any origin to use the API if that code has the token. It is not an authorization layer.

## 12. Error contract

Conventional service errors have an HTTP status and:

    {
      "error": "stable_machine_code",
      "message": "human-readable detail"
    }

| HTTP | Error code | Typical cause |
| --- | --- | --- |
| 400 | malformed_request | Invalid request line/header, target, Content-Length, or limits while parsing |
| 400 | invalid_host | Invalid /v1/hosts host |
| 400 | invalid_body | Too large, short body, or body read failure |
| 400 | invalid_json | Malformed JSON or field type mismatch |
| 400 | invalid_response | Conflicting body/wire fields, bad Base64, status, first line, or structured headers |
| 400 | invalid_deployment | Invalid host, path, method, or agent |
| 400 | invalid_status | /v1/file status is not an integer from 100–999 |
| 400 | invalid_headers | Invalid X-Whohouse-Response-Headers or structured file headers |
| 400 | invalid_first_line | File first-line override contains CR/LF |
| 400 | invalid_limit | limit is outside 1–500 |
| 400 | invalid_cursor | Cursor cannot be decoded |
| 401 | unauthorized | Missing or incorrect bearer token |
| 404 | not_found | Unknown control-plane path/method or malformed revision subpath |
| 404 | unknown_host | Public Host is not one direct wildcard label |
| 404 | not_deployed | No matching public route |
| 404 | revision_not_found | Revision ID does not exist |
| 409 | route_exists | replace=false and route already exists |
| 500 | encoding_error | Internal JSON encoding failure |
| 503 | storage_unavailable | DynamoDB/S3 control or route lookup failed |
| 503 | response_unavailable | Selected public S3 object could not be opened |

Error messages can include parser or validation detail; write automation against the error code and HTTP status.

An intentionally malformed deployed response has no service error wrapper. Its bytes are the successful result.

## 13. Python client

Download:

    curl -fsS https://cdn.whohouse.io/sdk/python.py -o whohouse_cdn.py

The client uses only the Python standard library.

    import os
    from whohouse_cdn import CDN

    cdn = CDN(token=os.environ["WHOHOUSE_CDN_TOKEN"])

Constructor:

    CDN(token=None, base_url="https://cdn.whohouse.io")

If token is omitted it reads WHOHOUSE_CDN_TOKEN, then CDN_TOKEN. base_url is useful for local testing.

### serve_file

    cdn.serve_file(
        file,
        host=None,
        path=None,
        agent=None,
        status=200,
        reason=None,
        content_type=None,
        response_headers=None,
        replace=True,
    )

file can be a filesystem path, bytes, or a binary file-like object. A filesystem filename becomes the default public filename and is used for MIME guessing. Other inputs default to /file and application/octet-stream. The implementation reads the entire input into memory.

response_headers accepts the same object or ordered pair-array shape and is encoded into X-Whohouse-Response-Headers.

### deploy

    cdn.deploy(
        body="",
        host=None,
        path="/",
        method="*",
        agent=None,
        status=200,
        reason=None,
        first_line=None,
        headers=None,
        auto_headers=True,
        content_type=None,
        replace=True,
    )

bytes bodies are Base64-encoded into body_base64. String bodies use body.

### raw

    cdn.raw(
        wire,
        host=None,
        path="/",
        method="*",
        agent=None,
        replace=True,
    )

A string is encoded as UTF-8; bytes are sent unchanged.

### host and list helpers

    cdn.new_host(requested=None)
    cdn.list_all(collection="revisions")

collection must be revisions, routes, or agents. list_all follows every cursor with pages of 500.

### Python errors

Non-2xx responses raise CDNError containing the status and message. Network and timeout errors retain their standard urllib exception types. The request timeout is 900 seconds.

The convenience client does not expose every raw field or every read endpoint. Call its _request helper or use direct HTTP when needed.

## 14. JavaScript client

Import directly:

    import CDN from 'https://cdn.whohouse.io/sdk/javascript.mjs';

    const cdn = new CDN({
      token: process.env.WHOHOUSE_CDN_TOKEN,
      agent: 'js-agent'
    });

Constructor:

    new CDN({
      token,
      baseURL = 'https://cdn.whohouse.io',
      agent = ''
    })

In Node, an omitted token falls back to WHOHOUSE_CDN_TOKEN and then CDN_TOKEN. Browsers must pass it explicitly.

Methods:

    cdn.serveFile(file, {
      host, path, status, reason, contentType,
      responseHeaders, replace, agent
    })

    cdn.deploy({
      body, bodyBase64, host, path, method, agent,
      status, reason, firstLine, headers,
      autoHeaders, contentType, replace
    })

    cdn.raw(wire, { host, path, method, agent, replace })
    cdn.newHost(requested)
    cdn.listAll(collection)
    cdn.request(path, fetchOptions)

serveFile accepts any fetch-compatible body; File/Blob objects provide default name/type metadata. raw also accepts any fetch-compatible body. listAll supports revisions, routes, and agents.

Non-2xx responses reject with Error using the API message when available. The client uses the platform's fetch, Headers, URLSearchParams, TextEncoder, and Base64 browser primitives.

Treat a browser token as exposed to that browser user and any script executing in the page.

## 15. Recipes

### Normal JSON

    curl --fail-with-body -X PUT \
      'https://cdn.whohouse.io/v1/file?host=json-demo&path=%2Fdata.json&content_type=application%2Fjson' \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      --data-binary '{"ready":true}'

### Redirect

    curl --fail-with-body -X POST https://cdn.whohouse.io/v1/deploy \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary '{
        "host":"redirect-demo",
        "path":"/",
        "status":302,
        "headers":[["Location","https://example.com/"]],
        "body":""
      }'

Automatic Content-Type, Content-Length, and Connection fields are still added unless auto_headers=false.

### Binary body in structured JSON

    python3 - <<'PY'
    import base64, json, os, urllib.request

    payload = {
        "host": "binary-demo",
        "path": "/bytes",
        "body_base64": base64.b64encode(b"\x00\xff\x10").decode(),
    }
    request = urllib.request.Request(
        "https://cdn.whohouse.io/v1/deploy",
        data=json.dumps(payload).encode(),
        method="POST",
        headers={
            "Authorization": "Bearer " + os.environ["WHOHOUSE_CDN_TOKEN"],
            "Content-Type": "application/json",
        },
    )
    print(urllib.request.urlopen(request).read().decode())
    PY

### A non-HTTP first line with otherwise structured output

    curl --fail-with-body -X POST https://cdn.whohouse.io/v1/deploy \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary '{
        "host":"odd-line-demo",
        "path":"/",
        "first_line":"asjidoasdi",
        "auto_headers":false,
        "headers":[["Anything","yes"]],
        "body":"bytes"
      }'

This stores asjidoasdi, CRLF, the header, an empty CRLF line, then bytes. Use raw mode when even those separators must be controlled.

### Method-specific routes

    curl --fail-with-body -X POST https://cdn.whohouse.io/v1/deploy \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary '{"host":"method-demo","path":"/same","method":"GET","body":"get"}'

    curl --fail-with-body -X POST https://cdn.whohouse.io/v1/deploy \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      -H 'Content-Type: application/json' \
      --data-binary '{"host":"method-demo","path":"/same","method":"POST","body":"post"}'

### Query-specific route with path fallback

Deploy /search as the fallback and /search?q=exact as the exact-target route. In JSON, the question mark is part of path:

    {"host":"query-demo","path":"/search","body":"fallback"}
    {"host":"query-demo","path":"/search?q=exact","body":"exact"}

### Create-only stable URL

    curl --fail-with-body -X PUT \
      'https://cdn.whohouse.io/v1/file?host=claimed-name&path=%2Fresult&replace=false' \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      --data-binary 'first writer wins'

A 201 means the route was created. A 409 means it already existed.

### Download and hash a preserved revision

    curl --fail-with-body \
      -H "Authorization: Bearer $WHOHOUSE_CDN_TOKEN" \
      'https://cdn.whohouse.io/v1/revisions/REVISION_ID/wire' \
      -o revision.wire

    sha256sum revision.wire

### Follow every list cursor

Use a bundled client's list_all/listAll helper, or repeatedly request:

    GET /v1/revisions?limit=500
    GET /v1/revisions?limit=500&cursor=NEXT_CURSOR

## 16. Admin console

Open https://cdn.whohouse.io or https://cdn.whohouse.io/admin.

Enter the same bearer token used by the API. The SPA provides:

- aggregate deployment, live-route, request, and byte counts;
- recent and searchable revision records;
- exact-wire downloads for preserved revisions;
- current route pointers;
- per-agent aggregates;
- interactive file, structured, and raw deployment;
- quick examples, this complete guide, OpenAPI, and SDK downloads.

The token is kept in browser sessionStorage, sent as an Authorization header, and removed by **Lock console** or when the browser session ends. It is not protected from scripts with access to that page/session. The SPA itself is public; data calls remain bearer-protected.

## 17. Limits, timeouts, and protocol behavior

| Item | Production behavior |
| --- | --- |
| Raw/file request body | Maximum 8 GiB |
| Structured JSON request | Maximum 32 MiB, including Base64 expansion |
| Deployment path | Maximum 16,384 bytes |
| Agent | Maximum 128 bytes |
| Method | Maximum 32 token characters |
| Host label | Maximum 63 characters |
| Incoming request line | Maximum 32 KiB |
| Combined incoming header lines | Approximately 1 MiB |
| Connection deadline | 15 minutes for all reads and writes |
| Route lookup storage context | 15 seconds |
| Public statistics update context | 5 seconds, asynchronous |
| List/overview context | 20 seconds |
| Revision metadata/wire-open context | 30 seconds |
| Deployment commit context | 15 minutes |
| List page | Default 100, maximum 500 |
| Public protocol | TLS on 443, HTTP/1 ALPN, one request per connection |
| Plain HTTP | Not exposed |

The NLB and clients can impose their own additional connection and idle timeouts. A maximum is not a throughput or completion guarantee.

The incoming request parser:

- requires exactly METHOD, target, and a token beginning HTTP/ on the request line;
- uppercases the method;
- accepts repeated request headers and uses the first value for service controls;
- strips an optional Host port and trailing dot;
- decodes chunk framing before storing an upload;
- does not provide keep-alive or pipelining.

## 18. Preservation, mutability, and concurrency

On every successful deployment:

1. A unique revision ID and unique date-partitioned S3 key are generated.
2. The complete wire file is uploaded as a private application/octet-stream S3 object using AES-256 server-side encryption.
3. DynamoDB atomically writes the immutable revision record, updates the route pointer, increments the agent aggregate, and increments total aggregates.
4. A new route increments the total route count; a replacement does not.
5. HTTP 201 is returned.

The S3 upload happens before the DynamoDB transaction. A failed metadata transaction can leave an unreferenced private S3 object, but it cannot expose a partially committed route. Only a 201 response confirms a registered revision.

Durability controls:

- S3 bucket versioning is enabled.
- Every response uses a new object key.
- The runtime role has PutObject/GetObject but no S3 delete operation.
- DynamoDB point-in-time recovery and deletion protection are enabled.
- The runtime role has no DynamoDB DeleteItem permission.
- S3, DynamoDB, and the token secret have CloudFormation Retain policies.
- There is no application delete endpoint, TTL, expiration job, response lifecycle, or content cleanup.

Precise qualification: this is application-level append-only preservation, not AWS Object Lock or a legal hold. An AWS account administrator with separate privileges can still change retention controls or delete data. A CloudFormation Retain policy leaves data resources behind rather than making them physically indestructible.

The public URL is stable while its DNS/stack exists. Its bytes remain stable only if no bearer-token holder replaces that exact route. Use replace=false for create-only publication, and keep the revision ID for historical identity.

Concurrent replace=true writers are last successful DynamoDB transaction wins for the live route. Both successful revisions remain preserved.

There is no storage quota or user-facing deletion mechanism. AWS service quotas, available task disk, and account capacity still apply.

## 19. Trust and security model

The deliberately small security model is:

- one shared bearer token protects the control plane;
- TLS protects transport to cdn.whohouse.io;
- public response URLs require no authentication;
- AWS IAM restricts the running task to the required registry/object operations.

There are intentionally no:

- per-agent credentials or permissions;
- route ownership checks;
- content validation, malware scanning, sanitization, or content-type enforcement;
- moderation or allow/deny lists;
- public-response authentication;
- rate limits, user quotas, or abuse throttles in the application;
- expiration or delete API;
- promise that agent metadata identifies a real principal.

Consequences:

- Anyone with the token can read all metadata and exact stored bytes and can replace any route.
- Anyone who knows or discovers a public URL can request its response.
- Generated hostnames are collision-resistant identifiers, not secrets.
- A malicious or mistaken bearer holder can publish deceptive, unsafe, very large, or protocol-hostile bytes.
- Raw responses can exploit bugs in clients that choose to parse them.
- The token should be shared only with agents/operators intended to have full service control.
- Users and operators are responsible for what they publish and for complying with applicable law and AWS policy.

No request body is executed by the service; bytes are stored and later written. That does not make content safe for recipients.

## 20. AWS production architecture

Request path:

    Internet client
        |
        | TLS 1.2/1.3 on 443; base + wildcard ACM certificate
        v
    Internet-facing Network Load Balancer
        |
        | decrypted TCP on 8080; no HTTP response parsing/reconstruction
        v
    ECS Fargate wire server in either of two Availability Zones
        |-- strongly consistent route lookup in DynamoDB
        |-- exact response object read from private versioned S3
        |-- asynchronous statistics update in DynamoDB
        |-- runtime token from Secrets Manager
        +-- operational application logs in CloudWatch

Production account and region:

- AWS account: 076709401358
- Region: us-east-1
- CloudFormation stack: whohouse-cdn
- Base domain: cdn.whohouse.io

Provisioned behavior:

- two public subnets in separate available zones;
- two 0.25-vCPU, 512-MiB Fargate tasks by default;
- 21 GiB task ephemeral storage;
- CPU target tracking at 65%, scaling from the desired count up to six tasks;
- 45-second task health grace period;
- deployment circuit breaker with rollback;
- TCP target health checks and 30-second deregistration delay;
- NLB cross-zone load balancing;
- ACM base/wildcard certificate and Route 53 base/wildcard A aliases;
- DynamoDB on-demand billing, encrypted, with a kind/updated-time index;
- private bucket with public-access blocks, owner-enforced ownership, TLS-only bucket policy, AES256 encryption, and versioning;
- ECR repository with immutable content-derived image tags and AES256 encryption;
- CloudWatch container insights, a 90-day application log group, and an unhealthy-target alarm.

There is no CloudFront, API Gateway, Application Load Balancer, reverse proxy, or managed HTTP cache in the public response path. Those products would parse or reconstruct response bytes.

Serving depends on a live route lookup and S3 read for each request; this is a response registry, not an edge-cached CDN.

## 21. Local development

Requirements:

- Go 1.26 or newer;
- Docker for image builds;
- Python 3 plus boto3 for AWS scripts.

Run formatting, vetting, and tests:

    make check

Run with ephemeral in-memory storage:

    ACCESS_TOKEN=local-development-token-change-me \
      LOCAL_MODE=true \
      go run ./cmd/server

Defaults:

- listen address :8080;
- base domain cdn.whohouse.io;
- raw/file limit 8 GiB;
- JSON limit 32 MiB.

Local control call:

    curl -X POST http://127.0.0.1:8080/v1/deploy \
      -H 'Host: cdn.whohouse.io' \
      -H 'Authorization: Bearer local-development-token-change-me' \
      --data-binary '{"host":"local","path":"/","body":"hello"}'

Local public call:

    curl --http1.1 http://127.0.0.1:8080/ \
      -H 'Host: local.cdn.whohouse.io'

LOCAL_MODE data disappears when the process exits. It does not use S3 or DynamoDB and should not be confused with production preservation.

Runtime environment variables:

| Variable | Required/default |
| --- | --- |
| ACCESS_TOKEN | Required |
| BASE_DOMAIN | cdn.whohouse.io |
| LISTEN_ADDRESS | :8080 |
| MAX_UPLOAD_BYTES | 8589934592 |
| MAX_JSON_BYTES | 33554432 |
| LOCAL_MODE | false; true selects memory storage |
| TABLE_NAME | Required outside LOCAL_MODE |
| BUCKET_NAME | Required outside LOCAL_MODE |

Invalid or nonpositive numeric environment values fall back to defaults.

## 22. AWS deployment and release operations

The deployment script is intentionally pinned to cdn.whohouse.io and validates the expected AWS account before mutation. It reads AWS credentials from:

    /home/sam/mobile/.env

Those credentials are not copied into the image, CloudFormation parameters, project report, or running task.

The service bearer token lives in:

    /home/sam/mobile/whohouse-cdn/.env

That file is excluded from source control and maintained with mode 0600. The same value is placed in the stack's retained Secrets Manager secret and injected into tasks at startup.

### Read-only plan

From /home/sam/mobile/whohouse-cdn:

    ../whohouse-inbox/.venv/bin/python -m scripts.deploy \
      --expected-account 076709401358

The plan:

- verifies STS account identity;
- validates the CloudFormation template;
- finds the public whohouse.io Route 53 zone;
- refuses conflicting base/wildcard DNS not owned by the stack;
- verifies at least two available zones;
- calculates the content-derived image tag;
- writes a token-free private report to .local/aws-plan.json;
- changes no AWS resources unless --execute is present.

### Execute a release

    ../whohouse-inbox/.venv/bin/python -m scripts.deploy \
      --expected-account 076709401358 \
      --execute

Execution:

1. Creates a whcdn_ token if the local private token is missing/short.
2. Creates or reuses the immutable ECR repository.
3. Builds and pushes linux/amd64 only when the content-derived image tag is absent.
4. Creates and reviews a CloudFormation change set.
5. Applies it with IAM capability.
6. Waits for a terminal stack state and relies on the ECS circuit breaker for failed task rollout.
7. Writes token-free deployment outputs to .local/production.json.

The source digest includes Go, HTML, CSS, JavaScript, JSON, Python, MJS, and this Markdown guide beneath internal, plus the Dockerfile and Go module files. This guide is embedded in the server binary, so a guide edit produces a new release image.

Optional script arguments:

| Argument | Default |
| --- | --- |
| --region | us-east-1 |
| --stack | whohouse-cdn |
| --domain | cdn.whohouse.io; other values are refused |
| --desired-count | 2; accepted range 1–6 |

### Verify production

    ../whohouse-inbox/.venv/bin/python -m scripts.verify

The verifier writes a token-free, mode-0600 report to:

    /home/sam/mobile/whohouse-cdn/.local/production-verification.json

It checks:

- stack completion;
- health and authentication;
- admin overview;
- ordinary response bytes;
- 999 SWAG and duplicate headers;
- exact malformed raw bytes and SHA-256;
- TLS base/wildcard names;
- running ECS tasks and NLB target health;
- S3 versioning and delete markers;
- DynamoDB deletion protection and point-in-time recovery.

The current verifier expects the production smoke-test routes launch-file, launch-swag, and launch-raw to remain present. Public verification requests increment best-effort delivery statistics.

### Retrieve the production token locally

Read /home/sam/mobile/whohouse-cdn/.env only on a trusted terminal. Do not copy /home/sam/mobile/.env; that separate file holds AWS credentials, not the CDN bearer token.

### Rotate the bearer token

1. Generate a new random token with the whcdn_ prefix and at least 32 total characters.
2. Replace only ACCESS_TOKEN in the project .env and retain file mode 0600.
3. Run the execute deployment command so CloudFormation updates Secrets Manager.
4. Force a new ECS service deployment, because running tasks resolve a Secrets Manager value only when they start.
5. Wait until the service is stable and both NLB targets are healthy.
6. Verify that the new token succeeds and the old token returns 401.

During a rolling restart, old and new tasks can briefly accept different tokens. Schedule rotation accordingly. Do not delete/recreate the retained secret.

### Stack deletion warning

Deleting the CloudFormation stack is not a content-deletion workflow. Retain policies intentionally leave the response bucket, registry table, and token secret behind; deletion protection can also block table removal. The application has no supported destructive cleanup operation.

### Cost shape

The baseline cost is dominated by two always-running small Fargate tasks and one Network Load Balancer. S3 storage/requests, DynamoDB on-demand operations and backups, ECR, logs, DNS, certificate-related infrastructure, and data transfer add usage-dependent cost. No application quotas cap spend.

## 23. Troubleshooting

### 401 unauthorized

- Use exactly Authorization: Bearer TOKEN.
- Check for newline/whitespace copied into the token.
- Confirm the production token, not the AWS credential file.
- After rotation, ensure every ECS task was restarted.

### 404 unknown_host

- Use exactly one label before .cdn.whohouse.io.
- Ensure the Host header/SNI is the wildcard hostname, not the base host.
- Nested labels are unsupported.

### 404 not_deployed

- Compare host, escaped path, query spelling, and request method.
- Check whether the route was deployed for a named method instead of *.
- List /v1/routes with the token.

### 409 route_exists

The exact route exists and replace=false was requested. Choose another host/path/method or intentionally retry with replace=true.

### 400 invalid_body

- Check the 8 GiB raw/file or 32 MiB JSON limit.
- Ensure Content-Length matches the actual upload.
- For streaming, use proper chunked transfer framing.
- Base64 increases JSON size; use /v1/raw or /v1/file for large data.

### Browser or curl reports a malformed response

This can be the intended result. Fetch the revision wire through the authenticated endpoint and compare it with a raw TLS capture. HTTP parsers are not byte-transparent.

### Browser tries HTTP/2

The production listener advertises HTTP/1 only. For diagnostic clients, explicitly request HTTP/1.1. Exact malformed response behavior is not compatible with HTTP/2 framing.

### New revision is live but missing from a list

Direct route/revision operations use strongly consistent reads; list indexes can lag. Retry the first page after a short interval rather than reusing a stale cursor.

### Statistics do not immediately change

Updates are asynchronous and best effort. They can lag or be lost if DynamoDB rejects the analytics update.

### A previous response must be recovered

Find its revision ID, download /v1/revisions/{id}/wire, and deploy that body through /v1/raw to the desired route. The recovery itself creates a new revision.

### A deployment returned 503

Do not assume the route changed. Check the route/revision lists and retry safely. Use replace=false when duplicate creation must be prevented.

### Health works but public requests fail

- Confirm the wildcard Host and SNI.
- Check the route registry with the token.
- Run scripts.verify.
- Inspect ECS service health, NLB targets, DynamoDB, S3, and the /ecs/whohouse-cdn CloudWatch log group.

## 24. Complete endpoint index

| Authentication | Method | Path |
| --- | --- | --- |
| public | GET | / |
| public | GET | /admin |
| public | GET | /admin/ |
| public | GET | /healthz |
| public | GET | /guide |
| public | GET | /guide.md |
| public | GET | /openapi.json |
| public | GET | /sdk/python.py |
| public | GET | /sdk/javascript.mjs |
| public | GET | /assets/app.js |
| public | GET | /assets/styles.css |
| public | OPTIONS | /v1/* |
| bearer | POST | /v1/hosts |
| bearer | POST or PUT | /v1/deploy |
| bearer | POST or PUT | /v1/file |
| bearer | POST or PUT | /v1/raw |
| bearer | GET | /v1/routes |
| bearer | GET | /v1/revisions |
| bearer | GET | /v1/revisions/{revision_id} |
| bearer | GET | /v1/revisions/{revision_id}/wire |
| bearer | GET | /v1/agents |
| bearer | GET | /v1/admin/overview |
| public response | any parseable method | any deployed target on one-label *.cdn.whohouse.io |

There are no other supported endpoints and no DELETE or PATCH operation.

## 25. Canonical-document policy

This file is the single canonical human guide. The root README only points here. SDK source, OpenAPI, infrastructure templates, and code remain executable/machine-readable artifacts rather than competing prose manuals.

When service behavior changes:

1. update this guide in the same change;
2. update OpenAPI/SDKs when their machine contract changes;
3. run make check;
4. deploy the embedded guide and server together;
5. verify the live /guide.md copy.
