{"ok":true,"schema":"aenet.public.api.response.v1","requestId":"6bd02560-7e71-4dfc-bf19-49ce073d313a","issuedAt":"2026-10-15T18:34:50.902Z","data":{"schema":"aenet.public.api.policy.v1","note":"Machine-readable service policy for the public API. Served at GET /api/v1/policy; enforced by server/services/api-core.js in both the local server and the Worker. Descriptive text here never grants access that the security boundary forbids.","audience":"Developers and clients of the read-only public surface. Registration is the only write path.","runtime":{"all":"Served by the local server and the production Worker (default).","local":"Served by the local server only; the production Worker does not route it and the discovery index served there omits it."},"authentication":{"required":false,"apiKeys":"optional","accounts":"none","keys":{"what":"Anonymous, revocable, expiring, rate-limited credentials for the read-only API. A key identifies nothing and nobody: no account, email, login, session, IP address or fingerprint.","issue":"Temporarily paused: POST /api/v1/keys returns 503 KEYS_UNAVAILABLE until product readiness is approved. Existing keys and anonymous public reads are unaffected.","issuance":{"enabled":false,"reason":"PRODUCT_NOT_READY","note":"Owner-paused before public launch. Configuration and Turnstile test mode must not reopen issuance. Re-enable only by an explicit owner-approved registry change, verification and release."},"format":"aek_<keyId 16 chars>_<secret 43 chars>","header":"Authorization: Bearer <key>  (or X-AENET-Key). Never accepted in a URL.","scopes":["read"],"ttlDays":{"default":90,"max":365},"inspect":"GET /api/v1/keys/self","revoke":"DELETE /api/v1/keys/self","storedAs":"HMAC-SHA256 keyed with a deployment secret held outside the database; the secret itself is never stored, logged or returned again.","invalidKey":"An unknown, wrong or revoked key is refused with 401 KEY_INVALID and is never silently treated as anonymous."},"note":"Public reads work without a key. A key adds an attributable, revocable quota and is required only where an endpoint declares authentication = api-key. No accounts, no core access."},"versioning":{"scheme":"path-major","current":"v1","additive":"New endpoints and new optional response fields ship inside v1 without notice.","breaking":"Removing or renaming an endpoint or field, changing a type or meaning, or tightening validation ships only as a new major path (/api/v2). The v1 endpoint is deprecated first.","discovery":["GET /api/v1","GET /api/v1/policy","GET /api/v1/openapi.json","GET /api/v1/version"]},"stability":{"stable":"Shape and meaning are covered by the versioning promise.","beta":"Live and usable; fields may still be added and small details adjusted with a CHANGELOG entry. Not yet covered by the deprecation promise.","experimental":"May change or disappear without notice. Never listed as active in the discovery index."},"deprecation":{"minimumNoticeDays":90,"appliesTo":"stable endpoints","signals":["status = deprecated in the registry","Deprecation,  Sunset and Link rel=\"successor-version\" response headers","CHANGELOG entry"],"requires":["sunset","replacement"],"note":"A deprecated endpoint keeps working until its sunset date."},"caching":{"classes":{"static-snapshot":{"cacheControl":"public, max-age=60, stale-while-revalidate=300","description":"Sanitized snapshot published with the site; changes only on a deployment."},"short-lived":{"cacheControl":"public, max-age=0, must-revalidate","description":"Computed per request; clients may store it but must revalidate."},"none":{"cacheControl":"no-store","description":"Writes and every error response."}},"errors":"no-store","validators":"Successful GET responses carry a weak ETag derived from the response data only (not requestId or issuedAt); If-None-Match returns 304."},"cors":{"readEndpoints":{"allowOrigin":"*","allowMethods":["GET","HEAD","OPTIONS"],"allowCredentials":false,"maxAgeSeconds":86400},"writeEndpoints":"No CORS headers: the registration form is same-origin only.","exposeHeaders":["X-Request-Id","X-AENET-API-Version","ETag","Retry-After","Deprecation","Sunset","Link","Content-Digest","X-AENET-Signed-At","X-AENET-Signature","X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset"]},"headers":{"response":["X-Request-Id","X-AENET-API-Version","Cache-Control","ETag","Link","Allow","Retry-After","Deprecation","Sunset","Content-Digest","X-AENET-Signed-At","X-AENET-Signature","X-RateLimit-Limit","X-RateLimit-Remaining","X-RateLimit-Reset","WWW-Authenticate","Vary"],"requestIdNote":"X-Request-Id equals requestId in the body; quote it when reporting a problem. It never carries identity."},"limits":{"maxBodyBytes":8192,"requestContentType":"application/json","responseContentType":"application/json; charset=utf-8"},"rateLimiting":{"registration":{"global":{"limit":300,"periodSeconds":60},"actor":{"limit":5,"periodSeconds":60},"response":"429 RATE_LIMITED with a Retry-After header"},"readEndpoints":"Anonymous reads are not limited in application code; any edge limit is configured in Cloudflare and is not part of this contract.","keyed":{"limit":600,"periodSeconds":60,"window":"fixed","response":"429 KEY_RATE_LIMITED with Retry-After; every keyed response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds)."},"keyIssuance":{"globalPerHour":60,"maxActiveKeys":10000,"note":"No person exists to rate-limit, so issuance is Turnstile-verified and capped globally."},"note":"Raw IP addresses are never used as a key, stored or logged."},"security":{"responseSigning":{"algorithm":"ed25519","headers":["Content-Digest","X-AENET-Signed-At","X-AENET-Signature"],"signedText":"aenet-response-v1 \\n METHOD \\n path+query \\n status \\n signedAt \\n content-digest","binds":"method, URL, status, time and the exact body bytes, so a response cannot be edited, replayed for another URL or fabricated by a mirror.","keyDocument":"GET /api/v1/keys/signing-key","pinning":"Clients that must resist a clone of the origin pin the key id from an independent channel (docs/API_SECURITY.md); the key document alone proves nothing about the origin that served it.","freshness":"Reject signatures older than the cache lifetime (verifier default 900 s).","keyMaterial":"The private key is a deployment secret (API_SIGNING_JWK) and is never in the repository. Without it the Worker serves unsigned responses and the key document returns 503 SIGNING_UNAVAILABLE."},"antiExtraction":["Key secrets and hashes are never returned, listed or logged; issuance shows the secret once.","Unknown, wrong and revoked keys are indistinguishable and cost the same work (no enumeration or timing oracle).","A leaked database cannot be used to recover or forge a key (keyed hash, pepper outside the database).","Keys are accepted only in headers, never in URLs, so they do not reach access logs or referrers.","Per-key quotas and short expiry bound what a leaked key can pull; revoke cuts it immediately.","No endpoint lists keys, holders, requests or usage of other callers."],"limits":"Public data is public: it can be read and copied by anyone who may read it. Signing proves origin and integrity, not secrecy; quotas slow bulk collection, they do not make public data un-copyable."},"errors":[{"code":"INVALID_JSON","status":400,"retryable":false,"message":"Request body must be valid JSON."},{"code":"INVALID_INPUT","status":400,"retryable":false,"message":"The request body is not an acceptable object (unknown field, wrong shape)."},{"code":"INVALID_EMAIL","status":400,"retryable":false,"message":"A valid email address is required."},{"code":"FORBIDDEN_FIELD","status":400,"retryable":false,"message":"The request carries a field that is never accepted."},{"code":"INVALID_LOCALE","status":400,"retryable":false,"message":"Unsupported locale."},{"code":"INVALID_INTERESTS","status":400,"retryable":false,"message":"Interests must be a short array of known interest areas."},{"code":"CONSENT_REQUIRED","status":400,"retryable":false,"message":"Consent is required."},{"code":"INVALID_USE_CASE","status":400,"retryable":false,"message":"Use case must be text within the length limit."},{"code":"INVALID_SESSION_ID","status":400,"retryable":false,"message":"Session id is not acceptable."},{"code":"TURNSTILE_REQUIRED","status":400,"retryable":false,"message":"A Turnstile token is required."},{"code":"TURNSTILE_FAILED","status":403,"retryable":false,"message":"Verification failed."},{"code":"REQUEST_REJECTED","status":403,"retryable":false,"message":"We could not accept this request. The rule that matched is never revealed."},{"code":"PREVIEW_NOT_ENABLED","status":403,"retryable":false,"message":"The preview namespace is not enabled."},{"code":"NOT_FOUND","status":404,"retryable":false,"message":"Public API route not found."},{"code":"VERSION_NOT_FOUND","status":404,"retryable":false,"message":"Version snapshot not found."},{"code":"METHOD_NOT_ALLOWED","status":405,"retryable":false,"message":"The method is not allowed for this route; the Allow header lists the valid methods."},{"code":"PAYLOAD_TOO_LARGE","status":413,"retryable":false,"message":"Request body too large."},{"code":"UNSUPPORTED_MEDIA_TYPE","status":415,"retryable":false,"message":"Content-Type must be application/json."},{"code":"RATE_LIMITED","status":429,"retryable":true,"retryAfterSeconds":60,"message":"Too many requests. Please try again later."},{"code":"SNAPSHOT_UNAVAILABLE","status":503,"retryable":true,"message":"Snapshot is temporarily unavailable."},{"code":"REGISTRATION_UNAVAILABLE","status":503,"retryable":true,"message":"Registration is temporarily unavailable."},{"code":"KEY_REQUIRED","status":401,"retryable":false,"message":"This endpoint needs an API key (Authorization: Bearer aek_…)."},{"code":"KEY_INVALID","status":401,"retryable":false,"message":"The API key is not valid."},{"code":"KEY_EXPIRED","status":401,"retryable":false,"message":"The API key has expired. Issue a new one."},{"code":"KEY_RATE_LIMITED","status":429,"retryable":true,"retryAfterSeconds":60,"message":"This API key exceeded its request quota."},{"code":"KEYS_UNAVAILABLE","status":503,"retryable":true,"message":"API key issuance or verification is temporarily unavailable."},{"code":"SIGNING_UNAVAILABLE","status":503,"retryable":true,"message":"Response signing is not configured."},{"code":"DUPLICATE","status":200,"retryable":false,"message":"This email is already on the early-access list. Returned with HTTP 200 and the error envelope so the list is never revealed by status code."}],"apiVersion":"v1","basePath":"/api/v1","registryVersion":"0.6.0","links":{"index":"/api/v1","openapi":"/api/v1/openapi.json","docs":"/en/api"},"summary":{"endpoints":26,"byStability":{"stable":14,"beta":12},"byStatus":{"active":25,"preview":1},"byCachePolicy":{"static-snapshot":19,"short-lived":3,"none":4}}}}