Skip to main content

max / makenotwork

server: /changelog/feed.xml alias; promote TOTP at-rest item Mirrors the /changelog page alias in blog.rs so RSS subscribers can follow the canonical /changelog/feed.xml path instead of discovering the underlying /p/changelog/blog/feed.xml. Same CHANGELOG_PROJECT_SLUG lookup; post links point at /changelog/{slug}. todo.md Phase 4 (Ultra Fuzz Run #5) gains the TOTP-seed-encryption-at-rest item, already disclosed in tech/security.md:42-53.
Author: Max Johnson <me@maxj.phd> · 2026-06-04 02:41 UTC
Commit: da2d8e4473a34cfdcf16efd73c07156b7ce95a2b
Parent: e5928a4
2 files changed, +58 insertions, -0 deletions
@@ -9,6 +9,7 @@ use axum::{
9 9 use serde::Deserialize;
10 10
11 11 use crate::{
12 + constants,
12 13 db::{self, Slug, UserId, Username},
13 14 error::{AppError, Result},
14 15 helpers,
@@ -22,6 +23,7 @@ pub fn feed_routes() -> Router<AppState> {
22 23 .route("/u/{username}/rss", get(user_rss_feed))
23 24 .route("/p/{slug}/rss", get(project_rss_feed))
24 25 .route("/p/{slug}/blog/feed.xml", get(project_blog_rss))
26 + .route("/changelog/feed.xml", get(changelog_rss))
25 27 .route("/feed/{user_id}", get(personal_feed))
26 28 }
27 29
@@ -184,6 +186,61 @@ async fn project_blog_rss(
184 186 .into_response())
185 187 }
186 188
189 + /// Platform changelog RSS feed (alias for the "changelog" project blog feed).
190 + ///
191 + /// Mirrors the `/changelog` page alias in `blog.rs`: same project lookup, same
192 + /// rendering, so subscribers can follow the canonical `/changelog/feed.xml`
193 + /// path instead of discovering the underlying `/p/changelog/blog/feed.xml`.
194 + #[tracing::instrument(skip_all, name = "feeds::changelog_rss")]
195 + async fn changelog_rss(State(state): State<AppState>) -> Result<Response> {
196 + // `from_trusted` is safe here because CHANGELOG_PROJECT_SLUG is a
197 + // compile-time constant (`&'static str`), not user input.
198 + let slug = Slug::from_trusted(constants::CHANGELOG_PROJECT_SLUG.to_owned());
199 + let db_project = db::projects::get_public_project_by_slug(&state.db, &slug)
200 + .await?
201 + .ok_or(AppError::NotFound)?;
202 +
203 + let db_user = db::users::get_user_by_id(&state.db, db_project.user_id)
204 + .await?
205 + .ok_or(AppError::NotFound)?;
206 +
207 + if db_user.is_sandbox {
208 + return Err(AppError::NotFound);
209 + }
210 +
211 + let db_posts =
212 + db::blog_posts::get_published_blog_posts_by_project(&state.db, db_project.id).await?;
213 +
214 + let feed_items: Vec<FeedItem> = db_posts
215 + .into_iter()
216 + .map(|post| FeedItem {
217 + title: post.title,
218 + link: format!("{}/changelog/{}", state.config.host_url, post.slug),
219 + description: post.body_markdown.chars().take(300).collect::<String>(),
220 + pub_date: post.published_at.unwrap_or(post.created_at),
221 + guid: post.id.to_string(),
222 + })
223 + .collect();
224 +
225 + let xml = rss::render_blog_feed(
226 + &db_project.title,
227 + &db_project.slug,
228 + db_project.description.as_deref().unwrap_or(""),
229 + &db_user.username,
230 + &feed_items,
231 + &state.config.host_url,
232 + );
233 +
234 + Ok((
235 + [(
236 + axum::http::header::CONTENT_TYPE,
237 + "application/rss+xml; charset=utf-8",
238 + )],
239 + xml,
240 + )
241 + .into_response())
242 + }
243 +
187 244 /// Query parameters for the personal feed.
188 245 #[derive(Debug, Deserialize)]
189 246 struct FeedQuery {
@@ -216,6 +216,7 @@ Full report: `docs/audit_review.md`. 3 CRITICAL, 14 HIGH/SERIOUS. Two-axis regre
216 216 - [ ] Security: `validate_token_consuming` for OAuth POST (`routes/oauth.rs:206`).
217 217 - [ ] Security: `parse_repo_path` rejects lone-dot entries (`git_ssh.rs:162`).
218 218 - [ ] Security: ClamAV INSTREAM 16K cap → fail-closed on truncation (`scanning/clamav.rs:101-108`).
219 + - [ ] Security: TOTP seeds at rest behind an application-level key. Currently unencrypted in the DB; `tech/security.md:42-53` already discloses this and commits to a fix. A database-only compromise yields working second factors today.
219 220 - [ ] UX: validation error messages stop reflecting user input (`wizards/item/mod.rs:176-179`).
220 221 - [ ] UX: CSRF body extraction stops using `from_utf8_lossy` (`csrf.rs:528-543`).
221 222 - [ ] Perf: scan-pipeline 400 MiB worst-case capacity note (`constants.rs:156-157`).