S3: URL-input adapter, canonicalize all call sites, route contacts through inbox.Cache #13

Open
opened 2026-05-12 20:26:50 +02:00 by arne · 1 comment
Owner

Parent

posta/server#10 — Absorb spec §4.1/§4.2 canonicalization and §9 key-management simplification

What to build

The full canonicalization wave on the input side: build the deep URL-input
adapter module, migrate every site that today calls posta.NormalizeURL to it,
and replace the contacts handler's bespoke actor-doc fetch with the inbox's
shared ActorCache. After this slice, the server has exactly one seam where
"what the user typed" becomes a canonical URL, and exactly one cache that fronts
peer actor-doc fetches.

End-to-end behaviour after this slice:

  • POST /api/v1/contacts with body.url = "alice.example/inbox/" succeeds,
    stores https://alice.example/inbox, and the response body renders
    alice.example/inbox as display form.
  • POST /api/v1/contacts with body.url = "http://x.example" returns 400
    with body field error = "non-https-scheme". Every §4.1 rejection category
    (non-https-scheme, userinfo-present, ip-literal-host, malformed-host,
    malformed-port, malformed-path, query-present, fragment-present) is
    reachable and tested.
  • POST /api/v1/messages canonicalizes the peer URL before enqueuing, so the
    recipient field on the wire envelope is canonical regardless of how the
    client typed it.
  • The per-sender rate limiter (internal/inbox/ratelimit.go) keys on the
    canonical sender URL — two clients hitting the inbox with trailing-slash
    variants share a bucket.
  • posta-server identity add --url alice.example (display form, no scheme)
    prepends https://, canonicalizes, and writes the canonical URL to the
    manifest.
  • internal/api/contacts.go fetchActorName is gone. The contacts handler
    resolves a peer's display name via the inbox's shared ActorCache (through
    a thin new helper such as FetchDoc(senderURL) on the cache or inbox seam).
    Two contact creations within the actor cache's positive TTL trigger exactly
    one upstream HTTPS GET.
  • No call site in the server uses posta.NormalizeURL anymore.

Acceptance criteria

URL-input adapter (the deep module):

  • New module exposes at least: a function that accepts user-supplied URL
    text (with or without https://) and returns either the canonical URL
    or a typed rejection-category value; a DisplayForm(canonical) string
    that returns the §4.2 display form.
  • When the input has no scheme, the adapter prepends https:// before
    delegating to posta.Canonicalize.
  • Rejection categories are surfaced as the stable strings from
    SPEC §4.1 (non-https-scheme, userinfo-present, ip-literal-host,
    malformed-host, malformed-port, malformed-path,
    query-present, fragment-present).
  • A vectors-driven test walks every file under
    posta/spec/testdata/vectors/url-canonical/ and asserts the listed
    canonical for accept vectors and the listed reject category for
    reject vectors. Display-form round-trip is bijective for accept vectors.

Call-site migration:

  • internal/api/contacts.go uses the adapter on every incoming URL
    (contact create, contact patch, read-watermark). On rejection, the
    handler returns 400 with the category string in the response body.
  • internal/api/messages.go uses the adapter on the peer field of
    outbound message creation. Wire recipient is canonical.
  • internal/inbox/ratelimit.go keys its per-sender bucket on the
    canonical sender URL.
  • internal/api/search.go (and any other URL-bearing endpoint) is
    migrated.
  • cmd/posta-server/identity.go (the identity add --url flag and any
    other CLI URL inputs) accept display form and store canonical.
  • Response payloads on GET/POST /api/v1/contacts render the contact's
    url field in §4.2 display form. (Storage stays canonical.)
  • No call to posta.NormalizeURL remains in the server.

Contacts via inbox.Cache:

  • fetchActorName in internal/api/contacts.go is deleted.
  • A new helper on the inbox/cache seam (e.g. Inbox.FetchDoc(senderURL)
    or a method on *posta.ActorCache) returns the validated
    *posta.ActorDoc for a given canonical URL. Contacts uses it.
  • The cache's URLEqual(doc.URL, url) check (or its successor after the
    spec lib tightens DecodeActorDoc) catches actor-doc URL mismatch.
  • A behaviour test wires a counting fake ActorFetcher, performs two
    contact creations for the same peer URL within the cache's positive
    TTL, and asserts exactly one upstream call.

General:

  • go build ./... and go test ./... pass.

Blocked by

posta/spec shipping Canonicalize() and a typed rejection-category error in
pkg/posta. The precondition is documented in posta/spec/TODO.md.

## Parent posta/server#10 — Absorb spec §4.1/§4.2 canonicalization and §9 key-management simplification ## What to build The full canonicalization wave on the input side: build the deep URL-input adapter module, migrate every site that today calls `posta.NormalizeURL` to it, and replace the contacts handler's bespoke actor-doc fetch with the inbox's shared `ActorCache`. After this slice, the server has exactly one seam where "what the user typed" becomes a canonical URL, and exactly one cache that fronts peer actor-doc fetches. End-to-end behaviour after this slice: - `POST /api/v1/contacts` with `body.url = "alice.example/inbox/"` succeeds, stores `https://alice.example/inbox`, and the response body renders `alice.example/inbox` as display form. - `POST /api/v1/contacts` with `body.url = "http://x.example"` returns `400` with body field `error = "non-https-scheme"`. Every §4.1 rejection category (`non-https-scheme`, `userinfo-present`, `ip-literal-host`, `malformed-host`, `malformed-port`, `malformed-path`, `query-present`, `fragment-present`) is reachable and tested. - `POST /api/v1/messages` canonicalizes the peer URL before enqueuing, so the `recipient` field on the wire envelope is canonical regardless of how the client typed it. - The per-sender rate limiter (`internal/inbox/ratelimit.go`) keys on the canonical sender URL — two clients hitting the inbox with trailing-slash variants share a bucket. - `posta-server identity add --url alice.example` (display form, no scheme) prepends `https://`, canonicalizes, and writes the canonical URL to the manifest. - `internal/api/contacts.go fetchActorName` is gone. The contacts handler resolves a peer's display name via the inbox's shared `ActorCache` (through a thin new helper such as `FetchDoc(senderURL)` on the cache or inbox seam). Two contact creations within the actor cache's positive TTL trigger exactly one upstream HTTPS GET. - No call site in the server uses `posta.NormalizeURL` anymore. ## Acceptance criteria **URL-input adapter (the deep module):** - [ ] New module exposes at least: a function that accepts user-supplied URL text (with or without `https://`) and returns either the canonical URL or a typed rejection-category value; a `DisplayForm(canonical) string` that returns the §4.2 display form. - [ ] When the input has no scheme, the adapter prepends `https://` before delegating to `posta.Canonicalize`. - [ ] Rejection categories are surfaced as the stable strings from SPEC §4.1 (`non-https-scheme`, `userinfo-present`, `ip-literal-host`, `malformed-host`, `malformed-port`, `malformed-path`, `query-present`, `fragment-present`). - [ ] A vectors-driven test walks every file under `posta/spec/testdata/vectors/url-canonical/` and asserts the listed `canonical` for accept vectors and the listed reject category for reject vectors. Display-form round-trip is bijective for accept vectors. **Call-site migration:** - [ ] `internal/api/contacts.go` uses the adapter on every incoming URL (contact create, contact patch, read-watermark). On rejection, the handler returns `400` with the category string in the response body. - [ ] `internal/api/messages.go` uses the adapter on the peer field of outbound message creation. Wire `recipient` is canonical. - [ ] `internal/inbox/ratelimit.go` keys its per-sender bucket on the canonical sender URL. - [ ] `internal/api/search.go` (and any other URL-bearing endpoint) is migrated. - [ ] `cmd/posta-server/identity.go` (the `identity add --url` flag and any other CLI URL inputs) accept display form and store canonical. - [ ] Response payloads on `GET`/`POST /api/v1/contacts` render the contact's `url` field in §4.2 display form. (Storage stays canonical.) - [ ] No call to `posta.NormalizeURL` remains in the server. **Contacts via inbox.Cache:** - [ ] `fetchActorName` in `internal/api/contacts.go` is deleted. - [ ] A new helper on the inbox/cache seam (e.g. `Inbox.FetchDoc(senderURL)` or a method on `*posta.ActorCache`) returns the validated `*posta.ActorDoc` for a given canonical URL. Contacts uses it. - [ ] The cache's `URLEqual(doc.URL, url)` check (or its successor after the spec lib tightens `DecodeActorDoc`) catches actor-doc URL mismatch. - [ ] A behaviour test wires a counting fake `ActorFetcher`, performs two contact creations for the same peer URL within the cache's positive TTL, and asserts exactly one upstream call. **General:** - [ ] `go build ./...` and `go test ./...` pass. ## Blocked by `posta/spec` shipping `Canonicalize()` and a typed rejection-category error in `pkg/posta`. The precondition is documented in `posta/spec/TODO.md`.
Author
Owner

This was generated by AI during triage.

Precondition resolved. posta/spec commit 5aa3aa3 lands Canonicalize(s) (string, error), DisplayForm(canonical) string, and *CanonicalizeError{Category CanonicalizeReject} whose category strings are exactly the eight values listed in the acceptance criteria. DecodeActorDoc now rejects non-canonical url. 5c19573 updated the spec's integration tests.

The server's go.mod already uses replace … => ../spec, so no version bump is needed.

Implementation notes for the agent:

  • The URL-input adapter is mostly a thin wrapper: prepend https:// when the input has no scheme, call posta.Canonicalize, type-assert *posta.CanonicalizeError to surface err.Category to HTTP-400 responses.
  • DisplayForm is bijective with canonical for accept vectors — use it for response payload rendering on GET/POST /api/v1/contacts.
  • The vectors-driven test should walk ../spec/testdata/vectors/url-canonical/*.json directly (or via a go:embed if you prefer to detach from the relative path).
  • For contacts via Inbox.Cache: the existing *posta.ActorCache already has Resolve(senderURL, keyID); you'll likely want a FetchDoc(senderURL) shaped helper that returns the validated *posta.ActorDoc without needing a keyID. Plumb it through Inbox.Cache() so the contacts handler shares the inbox's actor cache.

Category: enhancement
State: ready-for-agent

> *This was generated by AI during triage.* Precondition resolved. `posta/spec` commit `5aa3aa3` lands `Canonicalize(s) (string, error)`, `DisplayForm(canonical) string`, and `*CanonicalizeError{Category CanonicalizeReject}` whose category strings are exactly the eight values listed in the acceptance criteria. `DecodeActorDoc` now rejects non-canonical `url`. `5c19573` updated the spec's integration tests. The server's `go.mod` already uses `replace … => ../spec`, so no version bump is needed. Implementation notes for the agent: - The URL-input adapter is mostly a thin wrapper: prepend `https://` when the input has no scheme, call `posta.Canonicalize`, type-assert `*posta.CanonicalizeError` to surface `err.Category` to HTTP-400 responses. - `DisplayForm` is bijective with canonical for accept vectors — use it for response payload rendering on `GET`/`POST /api/v1/contacts`. - The vectors-driven test should walk `../spec/testdata/vectors/url-canonical/*.json` directly (or via a `go:embed` if you prefer to detach from the relative path). - For contacts via `Inbox.Cache`: the existing `*posta.ActorCache` already has `Resolve(senderURL, keyID)`; you'll likely want a `FetchDoc(senderURL)` shaped helper that returns the validated `*posta.ActorDoc` without needing a `keyID`. Plumb it through `Inbox.Cache()` so the contacts handler shares the inbox's actor cache. **Category:** enhancement **State:** ready-for-agent
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
posta/server#13
No description provided.