Conformance Checklist
Status: Draft v0.1 (2026-08) — the normative requirements of all four spec documents collected into one self-certification sheet. On any discrepancy, the source document wins; each item cites it. IDs are stable once published — new items get new IDs, removed items are struck, never renumbered.
How to use: work through the MUST tables for the roles you implement. Authority (A) = serving your own listings to partners; Consumer (C) = receiving partners’ listings. Most nodes are both. An implementation conforms if every applicable MUST holds. The SHOULD table is not required for conformance but each deviation deserves a written reason.
Sources: federation-protocol.md (FP), yacht-identity.md (ID), api-design.md (API), listing-schema.md (LS).
Identity, discovery, and keys (FP)
| ID | A/C | Requirement |
|---|---|---|
| FP-1 | A+C | Serve GET /.well-known/openyacht over HTTPS with a valid certificate, containing protocol versions, node UUID, name, keys array, and endpoint map. |
| FP-2 | A+C | Never accept a partner’s well-known document or federation traffic over plain HTTP or invalid TLS. |
| FP-3 | A+C | Sign with Ed25519 (RFC 8032); key ID = first 16 hex chars of SHA-256 of the raw 32-byte public key. |
| FP-4 | A+C | Store the private key encrypted at rest; include it in encrypted backups only; never log private keys (or full signatures at debug level) in production. |
| FP-5 | A+C | Generate a node UUID at installation and publish it in the well-known document. |
Request signing and verification (FP)
| ID | A/C | Requirement |
|---|---|---|
| FP-6 | A+C | Sign every request under /openyacht/v1/ except health and capabilities, sending all four X-OpenYacht-* headers (node, key, timestamp, signature). |
| FP-7 | A+C | Build the signing string exactly as specified: method \n path+query \n lowercase host \n timestamp \n lowercase-hex SHA-256 of the raw body (empty-string hash when bodyless). |
| FP-8 | A+C | Reject requests whose timestamp is outside ±300 seconds of server time. |
| FP-9 | A+C | Reject all requests from blocked partners. |
| FP-10 | A+C | On verification failure, refetch the sender’s well-known document once (fresh, rate-limit-respecting) and retry; on second failure reject with 401 and log. |
| FP-11 | A+C | On a node-UUID change for a known domain: downgrade the partner to provisional, reject the request, and notify administrators. |
| FP-12 | A+C | If a partner key is pinned, accept other keys only after administrator confirmation, even when the well-known document serves them. |
| FP-13 | A+C | Treat first contact from an unknown domain as provisional: no data shared beyond what provisional trust allows, until human approval. |
| FP-14 | A+C | Treat all inbound listing content as untrusted input: sanitise HTML, validate media URLs, no unchecked hotlinking into admin interfaces. |
| FP-15 | C | Flag listings from a partner unreachable beyond your staleness threshold (RECOMMENDED: 7 days) as stale in any consuming UI. |
Listing identity, authority, and copies (ID)
| ID | A/C | Requirement |
|---|---|---|
| ID-1 | A | Mint canonical URIs (https://{domain}/openyacht/v1/listings/{uuid}) only for your own listings; the UUID is generated once and never reused; the URI never changes for the life of the listing. |
| ID-2 | C | Store and compare canonical URIs as opaque strings. |
| ID-3 | C | Store a provenance block (canonical URI, authority domain, received_at, signature_verified) on every copy. |
| ID-4 | C | Never present a copy as your own listing: exclude copies from your /listings output, or include them with original provenance intact — never re-minted. |
| ID-5 | C | Never modify the substantive content of a copy (private annotations kept clearly separate are allowed). |
| ID-6 | C | Never re-share a copy onward (no relay in v1); obtain every listing directly from its authority. |
| ID-7 | C | Apply authority updates and withdrawals to copies within 24 hours; remove withdrawn/sold listings from public display within the same period. |
| ID-8 | A | Respect the lifecycle: `draft → active ⇄ under_offer → sold |
| ID-9 | C | When two active listings from different authorities hard-match the same vessel (HIN/IMO): retain both, flag for human review, never auto-resolve. |
| ID-10 | A+C | Honour the usage block; when expires_with_listing is true, cease all use of data and media (including cached media) when the listing ends or the partnership terminates. |
API surface and sync (API)
| ID | A/C | Requirement |
|---|---|---|
| API-1 | A | Serve the federation API under /openyacht/v1/ on the identity domain; all bodies JSON UTF-8; timestamps RFC 3339 UTC. |
| API-2 | A | Support updated_since polling as the baseline sync mechanism, with cursor pagination (meta.next_cursor, absent on last page). |
| API-3 | A | Include tombstones in updated_since results for every listing that became invisible to the requesting partner (withdrawn, sold, or unshared) — a polling consumer must never miss a removal. |
| API-4 | A | Ensure the updated_at a consumer sees reflects the change being reported (cache-invalidate on write); a stale cache in front of updated_since is non-conformant. |
| API-5 | A | Filter every response server-side to the requesting partner’s sharing rules; never send withheld data with “please ignore” semantics. |
| API-6 | A | Serve unsigned GET /openyacht/v1/capabilities (protocol versions, feature flags, limits) and GET /openyacht/v1/health. |
| API-7 | C | Check capabilities.features before using an optional feature; degrade gracefully when absent. |
| API-8 | C | Ignore unknown fields in all payloads. |
| API-9 | A | Use the defined error envelope and codes with their HTTP mappings (SIGNATURE_INVALID 401, PARTNER_BLOCKED 403, GONE 410, RATE_LIMITED 429 + Retry-After, etc.). |
| API-10 | A | If advertising subscriptions: accept POST/DELETE /openyacht/v1/subscriptions, sign every delivery like any federation request, retry with exponential backoff for 24 hours. |
| API-11 | C | If subscribed: deduplicate deliveries on (id, updated_at), and still reconcile by polling — a subscription never replaces updated_since. |
| API-12 | A | Serve monetary amounts as strings of decimal digits with ISO 4217 currency codes — never floats. |
Listing payload (LS)
| ID | A/C | Requirement |
|---|---|---|
| LS-1 | A | Emit every field defined for the listing’s type, with null/[] for unknown or withheld values — consumers must never need to branch on field presence. |
| LS-2 | A | snake_case names; canonical metric units only (_m, _kg, _l, _kn, _nmi, _lph); no imperial values, unit fields, or formatted display strings. |
| LS-3 | A | One currency per amount: the listing currency only; no pre-converted prices anywhere in the payload. |
| LS-4 | A | Restrict rich text to the allowed HTML subset, in descriptions[].content only; no markup in any other field. |
| LS-5 | C | Sanitise descriptions[].content before rendering, regardless of what the authority sent. |
| LS-6 | A | Use closed-enum values exactly as defined; emit null for unmappable values, never invented strings. Private extensions use the x_ prefix. |
| LS-7 | A | Never distribute draft listings. |
| LS-8 | A | media.profile is populated whenever the listing has any imagery, always including a non-null thumbnail_url; a listing with no imagery has profile: null — never a generated placeholder image. |
| LS-9 | C | Use media.profile as the representative image; never fall back to first-gallery-entry when it is present. |
| LS-10 | A | price_history is ordered most-recent-first and its first entry equals the current price; charter listings carry listing.price: null and pricing in charter.rates. |
| LS-11 | A | vessel.builder.slug and specifications.category.slug, when non-null, are slugs present in the vendored builder/category registries — never invented. |
| LS-12 | C | Validate incoming builder and category slugs against the vendored registries; on unknown slug fall back to the name and flag the registry copy for update; never invent a mapping. |
| LS-13 | A | Vendor the builder and category registries; never fetch them (or any third-party resource) as part of validating or serving a listing at request time. |
| LS-14 | A | Apply field-group gating exactly per the gating map (pricing, location_exact, media_original, documents, vessel_identifiers, history) — withheld values are nulled/emptied server-side. |
| LS-15 | A | Distribute charter crew data only while holding an attestation from the charter manager or captain that they are authorised to publish it; crew data inherits the listing’s usage terms including expiry. |
SHOULD / RECOMMENDED (not required for conformance; deviations deserve a written reason)
| Source | Recommendation |
|---|---|
| FP | Choose the identity domain under the business’s recognisable domain (a dedicated subdomain is the recommended pattern); serve federation endpoints only on the identity domain when the node is reachable under multiple hostnames. |
| FP | Cache partners’ well-known documents for 24 h; rate-limit the well-known endpoint (~1 req/min/consumer). |
| FP | Pin keys out of band with high-value partners; reject duplicate (node, timestamp, signature) tuples within the timestamp window. |
| FP | Routine key rotation with ~48 h overlap; back off exponentially on partner failures (cap ~24 h); hide stale partner data from public display after 30 days unreachable (the stale flag itself is FP-15, a MUST). |
| ID | Create listings on the node of the brokerage holding the central agency agreement; retain terminal listings at their canonical URI for ≥12 months before 410 Gone. |
| API | Reconciliation poll daily even when subscribed; default rate limit 500 req/h with a negotiated bulk window for initial sync. |
| LS | Builder data entry as a fixed registry choice with an explicit “unlisted” escape hatch; use well-known descriptions[].section labels (overview, highlights) where they apply. |