{"openapi":"3.1.1","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"Done for Tonight public content","version":"1.0.0","description":"Read-only English routine/help copy. No account, credentials, user records, selected-app tokens or uploads. Production deployment is not configured. Unknown or repeated parameters are rejected. Browser origins must exactly match the configured Pages origin; native requests without Origin work.","license":{"name":"All rights reserved"}},"servers":[{"url":"http://localhost:8787","description":"Local Wrangler development only; no production service is implied."}],"security":[],"tags":[{"name":"Public content","description":"Optional copy refresh; local app behavior works offline."}],"paths":{"/v1/content":{"get":{"operationId":"getPublicContent","summary":"Get published routine and help copy","tags":["Public content"],"security":[],"description":"Locale defaults to en-US. Only one optional locale=en-US query parameter is accepted. Immutable R2 bundles and their D1 index are validated before cache/response. Clients retain local copy on failures and adopt valid updates for future unfrozen nights. Rate limits use the edge client IP, so shared mobile networks may share a budget.","parameters":[{"name":"locale","in":"query","required":false,"description":"The single supported MVP locale. No duplicate or unknown query parameters.","schema":{"type":"string","enum":["en-US"],"default":"en-US"},"example":"en-US"}],"responses":{"200":{"description":"Validated published public copy.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"description":"Public response freshness; payload cache is versioned and never stores tracing or CORS headers.","schema":{"type":"string","const":"public, max-age=300, stale-while-revalidate=3600"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentBundle"},"example":{"schemaVersion":1,"version":"2026-10-13.1","locale":"en-US","routine":[{"id":"wash-up","title":"Wash up","body":"Brush your teeth, wash your face, and take care of the small things before bed."},{"id":"phone-away","title":"Put your phone out of reach","body":"Choose a place you won't keep reaching for it. Leave calls, music and podcasts available by keeping those apps out of your limits."},{"id":"get-into-bed","title":"Get into bed","body":"Let the day end here. When you're ready, tap Done for tonight and put the phone down."}],"help":{"essentialApps":"Choose distracting apps individually. Leave calling, messaging, music and podcast apps unselected. The app cannot identify every essential app automatically.","limits":"Screen Time access is optional. App limits begin 15 minutes before bedtime and end at your chosen time. Completing the routine does not end limits. Open Done for Tonight from the Home Screen to end tonight's limits sooner.","reminders":"One reminder when wind-down starts and one at bedtime. Notification settings and Focus can affect delivery. Reminders and app limits are scheduled ahead. Open the app occasionally to keep the plan refreshed.","privacy":"Your plan, explicit starts, self-reported completions, manual releases and selected-app tokens stay on this device. Optional public content requests send no night records or app choices. Technical network requests may create server access logs."}}}}},"400":{"description":"Invalid query, disallowed browser origin, or unsupported method/route.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"INVALID_PARAMETERS","message":"Use only one optional locale parameter with the value en-US.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}},"429":{"description":"Abuse-reduction budget exceeded; response is never cached.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Conservative delay in seconds. Rate limit counters are per location and permissive.","required":true,"schema":{"type":"string","const":"60"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"RATE_LIMITED","message":"Too many requests. Try again later.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}},"503":{"description":"Publication, binding or content temporarily unavailable. Retain local copy.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"CONTENT_UNAVAILABLE","message":"Public content is temporarily unavailable. Keep using local copy.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}}}},"head":{"operationId":"headPublicContent","summary":"Get public copy headers without a body","tags":["Public content"],"security":[],"description":"Hono derives this safe operation from GET, with identical validation, publication lookup, rate limits and headers, and strips every response body. The optional locale parameter follows the same rules as GET.","parameters":[{"name":"locale","in":"query","required":false,"description":"The single supported MVP locale. No duplicate or unknown query parameters.","schema":{"type":"string","enum":["en-US"],"default":"en-US"},"example":"en-US"}],"responses":{"200":{"description":"Validated published public-copy headers, with no response body.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Content-Type":{"schema":{"type":"string","const":"application/json"}},"Cache-Control":{"schema":{"type":"string","const":"public, max-age=300, stale-while-revalidate=3600"}}}},"400":{"description":"Invalid parameters or disallowed origin, without a body.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}}},"429":{"description":"Rate limit exceeded, without a body.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Conservative delay in seconds. Rate limit counters are per location and permissive.","required":true,"schema":{"type":"string","const":"60"},"example":"60"}}},"503":{"description":"Content or bindings unavailable, without a body.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}}}}},"options":{"operationId":"preflightPublicContent","summary":"Check an allowed browser GET request","tags":["Public content"],"security":[],"description":"Exact-origin preflight allows GET without additional request headers. It has a fresh request ID and is not cached.","parameters":[{"name":"Origin","in":"header","required":true,"schema":{"type":"string"},"description":"Must exactly match the configured Pages origin."},{"name":"Access-Control-Request-Method","in":"header","required":true,"schema":{"type":"string","const":"GET"}}],"responses":{"204":{"description":"Allowed preflight, with no body.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Access-Control-Allow-Methods":{"schema":{"type":"string","const":"GET"}}}},"400":{"description":"Disallowed origin or unsupported preflight.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"UNSUPPORTED_REQUEST","message":"Use GET /v1/content to request public copy.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}},"429":{"description":"Rate limit exceeded.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Conservative delay in seconds. Rate limit counters are per location and permissive.","required":true,"schema":{"type":"string","const":"60"},"example":"60"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"RATE_LIMITED","message":"Too many requests. Try again later.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}},"503":{"description":"Rate limiter or configuration unavailable.","headers":{"X-Request-Id":{"description":"Fresh server-generated UUID for this response; never read from cached payloads.","required":true,"schema":{"type":"string","format":"uuid"},"example":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"},"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"CONTENT_UNAVAILABLE","message":"Public content is temporarily unavailable. Keep using local copy.","requestId":"812f0af2-a9c5-4c76-8a95-935ce9dcb78d"}}}}}}}},"components":{"schemas":{"ContentBundle":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"schemaVersion":{"type":"number","const":1},"version":{"type":"string","minLength":1,"maxLength":64,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$"},"locale":{"type":"string","const":"en-US"},"routine":{"type":"array","prefixItems":[{"type":"object","properties":{"id":{"type":"string","const":"wash-up"},"title":{"type":"string","minLength":1,"maxLength":120,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"body":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"}},"required":["id","title","body"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","const":"phone-away"},"title":{"type":"string","minLength":1,"maxLength":120,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"body":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"}},"required":["id","title","body"],"additionalProperties":false},{"type":"object","properties":{"id":{"type":"string","const":"get-into-bed"},"title":{"type":"string","minLength":1,"maxLength":120,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"body":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"}},"required":["id","title","body"],"additionalProperties":false}],"items":false,"minItems":3,"maxItems":3},"help":{"type":"object","properties":{"essentialApps":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"limits":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"reminders":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"},"privacy":{"type":"string","minLength":1,"maxLength":1000,"pattern":"^(?=[\\s\\S]*\\S)[^<>\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F]+$"}},"required":["essentialApps","limits","reminders","privacy"],"additionalProperties":false}},"required":["schemaVersion","version","locale","routine","help"],"additionalProperties":false},"Error":{"type":"object","additionalProperties":false,"required":["code","message","requestId"],"properties":{"code":{"type":"string","enum":["INVALID_PARAMETERS","ORIGIN_NOT_ALLOWED","RATE_LIMITED","CONTENT_UNAVAILABLE","UNSUPPORTED_REQUEST"]},"message":{"type":"string","minLength":1,"maxLength":200},"requestId":{"type":"string","format":"uuid"}}}}}}
