| @@ -18,8 +18,9 @@ | ||
| 18 | 18 | | `/intros` | DELETE | `includes/seen-intros.php` | logged-in + OpenStation enabled | |
| 19 | 19 | | `/os-settings` | GET / POST | `includes/os-settings.php` | logged-in + OpenStation enabled | |
| 20 | 20 | | `/extended-options` | GET / POST | `includes/extended-options.php` | `manage_options` | |
| 21 | 21 | | `/pwa-state` | GET / POST | `includes/pwa.php` | logged-in + OpenStation enabled | |
| 22 | +| `/feedback/deactivation` | POST | `includes/feedback/rest.php` | `activate_plugins` + `openstation_deactivation_feedback_enabled()`; deliberately not `openstation_rest_require_enabled()` (the person deactivating usually has OpenStation off). No object-level checks: the route stores nothing on the site, it forwards an anonymous payload to the intake on openstation.blog and answers `{ sent }` | | |
| 22 | 23 | | `/debug` | GET | `includes/devtools.php` | `manage_options` (filterable via `openstation_debug_rest_permission`) | |
| 23 | 24 | | `/presence` | GET / POST | `includes/presence.php` | logged-in + OpenStation enabled | |
| 24 | 25 | | `/oauth/start` | POST | `includes/oauth-relay.php` | logged-in | |
| 25 | 26 | | `/oauth/callback` | GET | `includes/oauth-relay.php` | public (validated by the state nonce) | |
| @@ -37,17 +38,16 @@ | ||
| 37 | 38 | | `/comments/bulk` | POST | `apps/comments/parts/rest.php` | `moderate_comments` | |
| 38 | 39 | | `/comments/reply` | POST | `apps/comments/parts/rest.php` | `edit_posts` | |
| 39 | 40 | | `/comments/insights/{email}` | GET | `apps/comments/parts/rest.php` | `moderate_comments` | |
| 40 | 41 | | `/comments/counts` | GET | `apps/comments/parts/rest.php` | `edit_posts` | |
| 41 | -| `/comments/ai-settings` | GET / POST | `apps/comments/parts/ai-moderation.php` | `manage_options` | | |
| 42 | 42 | | `/content-graph/post-types` | GET | `includes/content-graph/rest.php` | `edit_posts` (filterable via `openstation_content_graph_user_can_use`) | |
| 43 | 43 | | `/content-graph/nodes` | GET | `includes/content-graph/rest.php` | `edit_posts` (filterable via `openstation_content_graph_user_can_use`) | |
| 44 | 44 | | `/content-graph/post/{id}` | GET | `includes/content-graph/rest.php` | `edit_posts` (filterable via `openstation_content_graph_user_can_use`) | |
| 45 | 45 | | `/apps/(?P<app>[a-z0-9][a-z0-9_-]*)/dispatch` | POST | `includes/framework/wordpress.php` | Logged in + the app exists + `App::allows()` (its `capabilities()` and `can()` gate). Every `.os.php` window — Code Blue included, behind Developer mode + `manage_options` (`manage_network_options` on multisite), filterable via `openstation_code_blue_user_can_use` — is served by this one route; there are no per-app routes. | |
| 46 | 46 | | `/comment-stats/{id}` | GET | `includes/my-wordpress/comment-stats.php` | `edit_posts` (filterable via `openstation_my_wordpress_user_can_use`) + parent post readable: `read_post`, but `edit_post` when the parent is still sealed by a password (a caller who already entered it reads on `read_post` like anyone else) or when its post type has no readable front end, and `moderate_comments` when the parent is gone | |
| 47 | -| `/term-stats/{taxonomy}/{id}` | GET | `includes/my-wordpress/term-stats.php` | logged-in + `read` | | |
| 48 | -| `/user-stats/{id}` | GET | `includes/my-wordpress/user-stats.php` | logged-in | | |
| 49 | -| `/user-footprint/{id}` | GET | `includes/my-wordpress/user-footprint.php` | logged-in | | |
| 47 | +| `/term-stats/{taxonomy}/{id}` | GET | `includes/my-wordpress/term-stats.php` | `edit_posts` (filterable via `openstation_my_wordpress_user_can_use`), then narrowed per caller: a non-viewable taxonomy answers 400 unless the caller holds its `manage_terms`, and every post-level aggregate (`counts.posts`, `recent`) is scoped to the statuses the caller may read, with `recent` gated per row on `read_post`. The payload is viewer-dependent — never cache it under a term-only key | | |
| 48 | +| `/user-stats/{id}` | GET | `includes/my-wordpress/user-stats.php` | `edit_posts` (filterable via `openstation_my_wordpress_user_can_use`), then tiered in the handler on `list_users` (or the subject viewing their own dossier). Without it: no email / username / registered / roles, and every count and `recent` covers published content only. `counts.cpt` and both comment counts additionally require a viewable post type, since `publish` rows of an internal type have no front end, and the comment counts skip password-protected and deleted parents and ask the `/comment-stats` parent gate of each parent they count, so a plugin's per-post `read_post` filter moves them. Viewer-dependent payload | | |
| 49 | +| `/user-footprint/{id}` | GET | `includes/my-wordpress/user-footprint.php` | `edit_posts` (filterable via `openstation_my_wordpress_user_can_use`), then gated per post rather than per tier. A timeline row needs the caller to see its post: a public status of a viewable type, else `read_post`; `edit_post` for a type with no front end; the `/comment-stats` parent gate for comment rows, sealed and deleted parents included. Every count that can reach those posts (`totals`, each day's `comments` and `updates`, and the streak built from them) asks the same gate of each post it counts, so a count never reports activity the rows withhold, a plugin's per-post capability filter included. `list_users` (or self) only adds `roleLabels` / `registered`. Viewer-dependent payload | | |
| 50 | 50 | | `/media-usage/{id}` | GET | `includes/my-wordpress/media-usage.php` | `read_post` on the attachment | |
| 51 | 51 | | `/recycle-bin/*` | various | `includes/recycle-bin/rest.php` | `delete_posts` (per-route gate) | |
| 52 | 52 | | `/files/*` | various | `includes/desktop-files/rest.php` | logged-in + OpenStation enabled (share routes add a sharing gate) | |
| 53 | 53 | | `/ai/search` | POST | `includes/ai-copilot/search.php` | logged-in + AI feature flag | |