{"openapi":"3.1.0","info":{"title":"Patchwire API","description":"Patchwire — vulnerability management platform","contact":{"name":"Ahmed Anbar","email":"Begnulinux@gmail.com"},"license":{"name":"Proprietary"},"version":"0.1.0"},"servers":[{"url":"http://127.0.0.1:3001","description":"Local dev"},{"url":"https://api.patchwire.app","description":"Production"}],"paths":{"/v1/auth/register":{"post":{"tags":["auth"],"summary":"`POST /v1/auth/register` — public sign-up.","description":"Creates a fresh organization, an admin user inside it, and returns a JWT\n— all in one call so the new user can land in `/dashboard` without a\nsecond round-trip. The org's slug becomes the tenant identifier baked into\nevery subsequent token.\n\nAtomicity: the underlying `db_orgs::create` and `db_users::create` take a\n`&PgPool` rather than a transaction handle, so we can't compose them in a\nsingle SQL transaction without refactoring those signatures. Instead, if\nuser-create fails after org-create succeeds, we best-effort delete the org\nto avoid leaving an orphan empty tenant. Failure of the rollback itself is\nlogged but not surfaced — the user already saw the original error.\n\nFree for everyone today; this is the right place to gate on a future\nsubscription/plan check (return 402 Payment Required if signups are\nmetered).","operationId":"register","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"201":{"description":"Account + organization created, JWT issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Organization slug already taken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/hooks/bitbucket":{"post":{"tags":["webhooks"],"operationId":"handle_bitbucket","parameters":[{"name":"X-Hub-Signature","in":"header","description":"sha256=<hex> HMAC digest (when webhook secret is set)","required":true,"schema":{"type":"string"}},{"name":"X-Event-Key","in":"header","description":"Event type (e.g. \"repo:push\")","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Raw Bitbucket Cloud push event JSON payload","content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"Webhook processed"},"400":{"description":"Malformed payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Invalid HMAC signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/hooks/forgejo":{"post":{"tags":["webhooks"],"operationId":"handle_forgejo","parameters":[{"name":"X-Gitea-Signature","in":"header","description":"HMAC-SHA256 hex digest of the raw body","required":true,"schema":{"type":"string"}},{"name":"X-Gitea-Event","in":"header","description":"Event type (e.g. \"push\")","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Raw Forgejo push event JSON payload","content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"Webhook processed"},"400":{"description":"Malformed payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Invalid HMAC signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/hooks/github":{"post":{"tags":["webhooks"],"operationId":"handle_github","parameters":[{"name":"X-Hub-Signature-256","in":"header","description":"sha256=<hex> HMAC digest of the raw body","required":true,"schema":{"type":"string"}},{"name":"X-GitHub-Event","in":"header","description":"Event type (e.g. \"push\")","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Raw GitHub push event JSON payload","content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"Webhook processed"},"400":{"description":"Malformed payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Invalid HMAC signature","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/hooks/gitlab":{"post":{"tags":["webhooks"],"operationId":"handle_gitlab","parameters":[{"name":"X-Gitlab-Token","in":"header","description":"Plain shared secret (GitLab does not use HMAC)","required":true,"schema":{"type":"string"}},{"name":"X-Gitlab-Event","in":"header","description":"Event type (e.g. \"Push Hook\")","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Raw GitLab push event JSON payload","content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"Webhook processed"},"400":{"description":"Malformed payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/orgs":{"get":{"tags":["orgs"],"summary":"`GET /v1/orgs` — list organizations visible to the caller.","description":"Today this is hard-scoped to the caller's own org: the caller's token\ncarries an `org_slug`, we fetch that one row, and return it as a\nsingle-element array. A super-admin role (future) will bypass the filter\nand see every tenant; until that exists, the list shape is preserved for\nclients while the semantics stay safe.\n\nIf the token's `org_slug` no longer resolves (race between deletion and a\nstill-valid JWT), we return 200 with an empty array rather than 500 —\nthe token references an org that doesn't exist, which is a soft failure.","operationId":"list","responses":{"200":{"description":"Organizations visible to the caller","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgList"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["orgs"],"operationId":"create","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrg"}}},"required":true},"responses":{"201":{"description":"Organization created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Slug already taken","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/orgs/{slug}":{"get":{"tags":["orgs"],"operationId":"retrieve","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"200":{"description":"Organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"delete":{"tags":["orgs"],"operationId":"delete_by_slug","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"204":{"description":"Organization deleted"},"400":{"description":"Invalid slug","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"patch":{"tags":["orgs"],"operationId":"patch","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchOrg"}}},"required":true},"responses":{"200":{"description":"Updated organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Organization"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/auth/login":{"post":{"tags":["auth"],"operationId":"login","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"JWT + user record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponse"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Account is not active","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/v1/orgs/{slug}/auth/me":{"get":{"tags":["auth"],"operationId":"me","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"200":{"description":"Current user + org","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MeResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/auth/password":{"post":{"tags":["auth"],"operationId":"change_password","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordRequest"}}},"required":true},"responses":{"204":{"description":"Password updated"},"400":{"description":"Invalid request (e.g. weak new password)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Current password is wrong or token invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/findings":{"get":{"tags":["findings"],"operationId":"list_org_wide","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"severity","in":"query","description":"Filter by severity","required":false,"schema":{"type":"string"},"example":"critical"}],"responses":{"200":{"description":"Findings across the organization","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DbFinding"}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects":{"get":{"tags":["projects"],"operationId":"list","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"200":{"description":"Projects in the organization","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["projects"],"operationId":"create","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProject"}}},"required":true},"responses":{"201":{"description":"Project created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Slug already taken in this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}":{"get":{"tags":["projects"],"operationId":"retrieve","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Project or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"delete":{"tags":["projects"],"operationId":"delete_by_slug","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Project deleted"},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Project or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"patch":{"tags":["projects"],"operationId":"patch","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchProject"}}},"required":true},"responses":{"200":{"description":"Updated project","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Project or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/run-scan":{"post":{"tags":["scans"],"operationId":"run_now","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RunScanRequest"}}},"required":true},"responses":{"202":{"description":"Scan accepted; the background task will clone + scan + ingest + update the scan row","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scan"}}}},"400":{"description":"Invalid request (e.g. non-HTTP clone URL)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization or project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/scans":{"get":{"tags":["scans"],"operationId":"list","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"}],"responses":{"200":{"description":"Scans in the project","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Scan"}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization or project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["scans"],"operationId":"create","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScan"}}},"required":true},"responses":{"201":{"description":"Scan created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scan"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization or project not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/scans/{scan_id}":{"get":{"tags":["scans"],"operationId":"retrieve","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Scan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scan"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Scan, project, or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"delete":{"tags":["scans"],"operationId":"delete_scan","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Scan deleted"},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Scan, project, or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"patch":{"tags":["scans"],"operationId":"patch","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchScan"}}},"required":true},"responses":{"200":{"description":"Updated scan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Scan"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Scan, project, or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/scans/{scan_id}/findings":{"get":{"tags":["findings"],"operationId":"list_by_scan","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Findings in the scan","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DbFinding"}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization, project, or scan not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["findings"],"operationId":"create","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFinding"}}},"required":true},"responses":{"201":{"description":"Finding created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbFinding"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization, project, or scan not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Duplicate external_id within scan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/scans/{scan_id}/findings/batch":{"post":{"tags":["findings"],"operationId":"create_batch","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchCreateFindings"}}},"required":true},"responses":{"201":{"description":"Findings created","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DbFinding"}}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization, project, or scan not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Duplicate external_id within scan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/projects/{project_slug}/scans/{scan_id}/findings/{finding_id}":{"get":{"tags":["findings"],"operationId":"retrieve","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"finding_id","in":"path","description":"Finding UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Finding","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DbFinding"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Finding, scan, project, or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"delete":{"tags":["findings"],"operationId":"delete_finding","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"project_slug","in":"path","description":"Project slug","required":true,"schema":{"type":"string"},"example":"web"},{"name":"scan_id","in":"path","description":"Scan UUID","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"finding_id","in":"path","description":"Finding UUID","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Finding deleted"},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Finding, scan, project, or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/users":{"get":{"tags":["users"],"operationId":"list","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"200":{"description":"Users in the organization","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"post":{"tags":["users"],"operationId":"create","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUser"}}},"required":true},"responses":{"201":{"description":"User created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Email already taken in this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/users/{user_id}":{"get":{"tags":["users"],"operationId":"retrieve","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"user_id","in":"path","description":"User id (UUID)","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"User or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"delete":{"tags":["users"],"operationId":"delete_by_id","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"user_id","in":"path","description":"User id (UUID)","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"User deleted"},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"User or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]},"patch":{"tags":["users"],"operationId":"patch","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"},{"name":"user_id","in":"path","description":"User id (UUID)","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUser"}}},"required":true},"responses":{"200":{"description":"Updated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"400":{"description":"Invalid request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Token is for a different organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"User or organization not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"409":{"description":"Email already taken in this organization","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}},"/v1/orgs/{slug}/webhook-config":{"get":{"tags":["webhooks"],"operationId":"webhook_config","parameters":[{"name":"slug","in":"path","description":"Organization slug","required":true,"schema":{"type":"string"},"example":"acme"}],"responses":{"200":{"description":"Webhook configuration for Forgejo setup","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigResponse"}}}},"401":{"description":"Missing or invalid token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"403":{"description":"Non-admin or cross-org access","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"500":{"description":"Webhook secret not configured on the server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}},"security":[{"bearer_auth":[]}]}}},"components":{"schemas":{"BatchCreateFindings":{"type":"object","required":["findings"],"properties":{"findings":{"type":"array","items":{"$ref":"#/components/schemas/CreateFinding"}}}},"ChangePasswordRequest":{"type":"object","required":["current_password","new_password"],"properties":{"current_password":{"type":"string","writeOnly":true},"new_password":{"type":"string","writeOnly":true}}},"CreateFinding":{"type":"object","required":["external_id","title","severity","scanner"],"properties":{"description":{"type":["string","null"]},"external_id":{"type":"string"},"location_column":{"type":["integer","null"],"format":"int32"},"location_line":{"type":["integer","null"],"format":"int32"},"location_path":{"type":["string","null"]},"metadata":{},"scanner":{"type":"string"},"severity":{"type":"string"},"title":{"type":"string"}}},"CreateOrg":{"type":"object","required":["slug","name"],"properties":{"name":{"type":"string"},"slug":{"type":"string"}}},"CreateProject":{"type":"object","required":["slug","name"],"properties":{"access_token":{"type":["string","null"],"description":"Plaintext access token for cloning private repos. Encrypted at rest\nvia AES-256-GCM with the server's data key. Never returned in any\nresponse body — clients can only inspect `has_access_token: bool`.","writeOnly":true},"access_token_user":{"type":["string","null"],"description":"Username component of the URL-embedded HTTP Basic credential.\nDefaults to `\"x-access-token\"` (works for GitHub fine-grained PATs).\nSet to `\"oauth2\"` for GitLab project access tokens, or your username\nfor Bitbucket app passwords."},"default_branch":{"type":["string","null"],"description":"Default branch to scan. Server default is `\"main\"` if omitted."},"description":{"type":["string","null"]},"name":{"type":"string"},"repo_url":{"type":["string","null"],"description":"HTTP(S) clone URL. Used to pre-fill the \"Scan now\" form."},"slug":{"type":"string"}}},"CreateScan":{"type":"object","properties":{"scanner_type":{"type":["string","null"]},"target_ref":{"type":["string","null"]}}},"CreateUser":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string","description":"Plaintext — handler hashes before storing.","writeOnly":true},"role":{"type":["string","null"],"description":"Defaults to `\"member\"` when absent."}}},"DbFinding":{"type":"object","description":"Canonical finding record — mirrors the `findings` row shape.\n\nNamed `DbFinding` to avoid collision with `patchwire_findings::Finding`.","required":["id","scan_id","org_id","external_id","title","severity","scanner","metadata","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"description":{"type":["string","null"]},"external_id":{"type":"string"},"id":{"type":"string","format":"uuid"},"location_column":{"type":["integer","null"],"format":"int32"},"location_line":{"type":["integer","null"],"format":"int32"},"location_path":{"type":["string","null"]},"metadata":{},"org_id":{"type":"string","format":"uuid"},"scan_id":{"type":"string","format":"uuid"},"scanner":{"type":"string"},"severity":{"type":"string"},"title":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"ErrorBody":{"type":"object","description":"DTO shape of the JSON body every `ApiError` serializes into. Kept as a\nseparate struct (instead of an ad-hoc `json!` macro call) so the `OpenAPI`\nspec can reference it from every handler's `responses(... body = ErrorBody)`.\n\nThe runtime path still uses `json!` for convenience — this struct is the\ncontract, the macro path is the implementation.","required":["error","message"],"properties":{"error":{"type":"string","description":"Short machine-readable error code (e.g. `not_found`, `bad_request`)."},"message":{"type":"string","description":"Human-readable message."}}},"FindingFilter":{"type":"object","properties":{"severity":{"type":["string","null"]}}},"LoginRequest":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string"},"password":{"type":"string","writeOnly":true}}},"LoginResponse":{"type":"object","required":["token","expires_at","user"],"properties":{"expires_at":{"type":"string","description":"RFC3339 UTC timestamp — clients can use this to know when to\nre-login without decoding the JWT themselves."},"token":{"type":"string"},"user":{"$ref":"#/components/schemas/User"}}},"MeResponse":{"type":"object","description":"Response body shape for `GET /v1/orgs/:slug/auth/me`. Defined as a typed\nDTO (rather than the `serde_json::Value` the handler actually returns) so\nthe `OpenAPI` spec has a concrete schema clients can generate code from.","required":["user","org"],"properties":{"org":{"$ref":"#/components/schemas/Organization"},"user":{"$ref":"#/components/schemas/User"}}},"OrgList":{"type":"object","required":["orgs"],"properties":{"orgs":{"type":"array","items":{"$ref":"#/components/schemas/Organization"}}}},"Organization":{"type":"object","description":"Canonical organization record — mirrors the `organizations` row shape.\n\nSerialized directly as the API response body; the shape is load-bearing\nfor the HTTP contract.","required":["id","slug","name","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"PatchOrg":{"type":"object","properties":{"name":{"type":["string","null"]},"status":{"type":["string","null"]}}},"PatchProject":{"type":"object","description":"Three-state semantics for `access_token` on patch:\n  - field absent       → leave existing token unchanged\n  - empty string `\"\"`  → clear the existing token (NULL out the column)\n  - non-empty string   → re-encrypt and replace","properties":{"access_token":{"type":["string","null"],"writeOnly":true},"access_token_user":{"type":["string","null"]},"default_branch":{"type":["string","null"]},"description":{"type":["string","null"]},"name":{"type":["string","null"]},"repo_url":{"type":["string","null"]},"status":{"type":["string","null"]}}},"PatchScan":{"type":"object","properties":{"status":{"type":["string","null"]},"summary":{}}},"PatchUser":{"type":"object","properties":{"email":{"type":["string","null"]},"role":{"type":["string","null"]},"status":{"type":["string","null"]}}},"Project":{"type":"object","description":"Canonical project record — mirrors the `projects` row shape.\n\nSerialized directly as the API response body; the shape is load-bearing\nfor the HTTP contract. The encrypted access token is **never** exposed to\nAPI clients: the row is fetched in two flavours — `Project` (no token,\nwhat handlers return) and `ProjectWithSecret` (includes the encrypted\nblob, used only by the scan path inside the API).","required":["id","org_id","slug","name","status","default_branch","created_at","updated_at"],"properties":{"access_token_user":{"type":["string","null"],"description":"Username portion of the URL-embedded HTTP Basic credentials. Only\nmeaningful when [`Self::has_access_token`] is `true`. Defaults to\n`\"x-access-token\"` (works for GitHub fine-grained PATs)."},"created_at":{"type":"string","format":"date-time"},"default_branch":{"type":"string","description":"Default branch to scan when none is specified. Defaults to `\"main\"`."},"description":{"type":["string","null"]},"has_access_token":{"type":"boolean","description":"Whether a private-repo access token is configured for this project.\nThe encrypted blob itself is never serialized; clients only see the\nboolean so the UI can render a 🔒 badge."},"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"org_id":{"type":"string","format":"uuid"},"repo_url":{"type":["string","null"],"description":"Optional HTTP(S) git clone URL. When set, the web UI pre-fills the\n\"Scan now\" form and retry/auto-scan features can re-clone without\nasking the user for the URL again."},"slug":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"ProviderWebhookConfig":{"type":"object","description":"One provider's webhook configuration (URL path + auth + event filter).","required":["id","name","path","secret","events","content_type","signature_header","auth_kind"],"properties":{"auth_kind":{"type":"string","description":"How the secret is sent: \"hmac-sha256\" (Forgejo/GitHub) or \"token\" (GitLab)."},"content_type":{"type":"string","description":"Content type the provider must set on the POST body."},"events":{"type":"array","items":{"type":"string"},"description":"Event types the endpoint accepts (provider-specific wording)."},"id":{"type":"string","description":"Short identifier: \"forgejo\" | \"github\" | \"gitlab\"."},"name":{"type":"string","description":"Human-friendly name shown in the setup UI."},"path":{"type":"string","description":"Relative path the webhook must POST to (prepend the API base URL)."},"secret":{"type":"string","description":"HMAC-SHA256 shared secret for Forgejo/GitHub, or the plain token for\nGitLab. Paste into the provider's \"Secret\"/\"Token\" field."},"signature_header":{"type":"string","description":"Header name the provider sends the signature/token in."}}},"RegisterRequest":{"type":"object","required":["org_name","org_slug","email","password"],"properties":{"email":{"type":"string"},"org_name":{"type":"string","description":"Human-readable organization name, e.g. \"Acme Inc.\"."},"org_slug":{"type":"string","description":"URL-safe slug, e.g. \"acme\". Must be unique across all tenants."},"password":{"type":"string","writeOnly":true}}},"RunScanRequest":{"type":"object","description":"Request body for `POST .../scans/run` — the manual-trigger variant that\nruns the full clone → semgrep → gitleaks → ingest pipeline.","required":["clone_url"],"properties":{"branch":{"type":["string","null"],"description":"Git branch to scan. Defaults to `main` if absent."},"clone_url":{"type":"string","description":"HTTP(S) clone URL. SSH and file:// are rejected."}}},"Scan":{"type":"object","description":"Canonical scan record — mirrors the `scans` row shape.\n\nSerialized directly as the API response body; the shape is load-bearing\nfor the HTTP contract.","required":["id","project_id","org_id","status","summary","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"finished_at":{"type":["string","null"],"format":"date-time"},"id":{"type":"string","format":"uuid"},"org_id":{"type":"string","format":"uuid"},"project_id":{"type":"string","format":"uuid"},"scanner_type":{"type":["string","null"]},"started_at":{"type":["string","null"],"format":"date-time"},"status":{"type":"string"},"summary":{},"target_ref":{"type":["string","null"]},"updated_at":{"type":"string","format":"date-time"}}},"User":{"type":"object","description":"Canonical user record. The hashed-password field is present on the\nstruct (so sqlx can populate it from `RETURNING`/`SELECT`) but is\nsuppressed from JSON output via `#[serde(skip_serializing)]` and from the\n`OpenAPI` schema via `#[schema(ignore)]`. Deserialization still accepts it\nfor internal use but the HTTP layer never needs to parse users from JSON.","required":["id","org_id","email","role","status","created_at","updated_at"],"properties":{"created_at":{"type":"string","format":"date-time"},"email":{"type":"string"},"id":{"type":"string","format":"uuid"},"org_id":{"type":"string","format":"uuid"},"role":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"WebhookConfigResponse":{"type":"object","description":"Response shape for `GET /v1/orgs/:slug/webhook-config`.\n\nReturns the set of provider-specific webhook configs so the UI can render\ntabbed instructions. URLs are client-constructed from the API base plus\neach provider's `path` — keeping the backend URL-agnostic lets staging\nand local environments reuse the same code without env gymnastics.","required":["providers"],"properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/ProviderWebhookConfig"}}}}},"securitySchemes":{"bearer_auth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}},"tags":[{"name":"auth","description":"Login + token introspection"},{"name":"orgs","description":"Organization CRUD"},{"name":"projects","description":"Projects within an organization"},{"name":"findings","description":"Scan findings (vulnerabilities/issues)"},{"name":"scans","description":"Security scans within a project"},{"name":"users","description":"User management within an org"},{"name":"webhooks","description":"Forgejo webhook receiver (HMAC-authenticated)"}]}