{"openapi":"3.1.0","info":{"title":"Personal Safety App — Cloud Backend (Slice 6 harness)","description":"Upload harness — POST /v1/bundles + health. Slice 7: optional MQTT consumer for contact alerts (log-only lab default). Normative contract: DOC-009 §4 (HTTPS API) and §12 (implementation plan).","version":"0.1.0"},"paths":{"/healthz":{"get":{"tags":["health"],"summary":"Healthz","description":"Liveness — process up.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/readyz":{"get":{"tags":["health"],"summary":"Readyz","description":"Readiness — SQLite + object storage reachable.","operationId":"readyz_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Readyz Readyz Get"}}}}}}},"/v1/bundles":{"post":{"tags":["bundles"],"summary":"Ingest sealed capture session","description":"Ingest one sealed capture session — manifest + encrypted media files — correlating with MQTT by `session_id`. Multipart `files[]` parts are ciphertext blobs (Option A, DOC-009 §4.2).","operationId":"post_bundles_v1_bundles_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_post_bundles_v1_bundles_post"}}}},"responses":{"200":{"description":"Idempotent replay — same session_id + manifest hash","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleAck"}}}},"201":{"description":"First successful ingest","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BundleAck"}}}},"400":{"description":"Malformed multipart or missing parts"},"401":{"description":"Missing or invalid Bearer token"},"403":{"description":"Unknown device"},"413":{"description":"Payload too large"},"422":{"description":"evidence-core validation failed"},"500":{"description":"Storage or internal error — app retries per Slice 8"}},"security":[{"HTTPBearer":[]}]}},"/v1/lab/bundles":{"get":{"tags":["lab"],"summary":"List Lab Bundles","operationId":"list_lab_bundles_v1_lab_bundles_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response List Lab Bundles V1 Lab Bundles Get"}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/lab/bundles/{bundle_id}/files/{filename}":{"get":{"tags":["lab"],"summary":"Download Lab Bundle File","operationId":"download_lab_bundle_file_v1_lab_bundles__bundle_id__files__filename__get","security":[{"HTTPBearer":[]}],"parameters":[{"name":"bundle_id","in":"path","required":true,"schema":{"type":"string","title":"Bundle Id"}},{"name":"filename","in":"path","required":true,"schema":{"type":"string","title":"Filename"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"tags":["lab"],"summary":"Landing Page","operationId":"landing_page__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/lab":{"get":{"tags":["lab"],"summary":"Lab Page","operationId":"lab_page_lab_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"Body_post_bundles_v1_bundles_post":{"properties":{"session_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Session Id"},"device_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"},"manifest":{"anyOf":[{"type":"string","contentMediaType":"application/octet-stream"},{"type":"null"}],"title":"Manifest"},"files[]":{"anyOf":[{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array"},{"type":"null"}],"title":"Files[]"}},"type":"object","title":"Body_post_bundles_v1_bundles_post"},"BundleAck":{"properties":{"bundle_id":{"type":"string","title":"Bundle Id","examples":["bnd_7xk2"]},"session_id":{"type":"string","title":"Session Id","examples":["550e8400-e29b-41d4-a716-446655440000"]},"device_id":{"type":"string","title":"Device Id","examples":["lab-emulator"]},"manifest_hash":{"type":"string","title":"Manifest Hash","examples":["sha256:abc123…"]},"accepted_at":{"type":"string","format":"date-time","title":"Accepted At","examples":["2026-06-24T14:31:00.000Z"]},"schema_version":{"type":"string","title":"Schema Version","default":"0.1.0","examples":["0.1.0"]}},"type":"object","required":["bundle_id","session_id","device_id","manifest_hash","accepted_at"],"title":"BundleAck","description":"Ack JSON returned on first ingest (201) or idempotent replay (200)."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"HTTPBearer":{"type":"http","scheme":"bearer","description":"Pre-provisioned device token (DOC-009 §4.1)"}}}}