From 85430dc589926fa22a1a6322a96bda82b5f0c644 Mon Sep 17 00:00:00 2001 From: Ghost Scripter Date: Wed, 9 Sep 2026 17:18:48 +0530 Subject: [PATCH 1/2] feat: expose the public blog routes and block their admin writes Syncs against a backend checkout that adds a blog: two public reads (GET /blog/posts, GET /blog/posts/{slug}) join the client contract, and the three /admin/blog-posts writes the admin dashboard drives join UNEXPOSED_ROUTES. The pinned length moves 51 -> 54 for exactly those three. It is a ratchet against a bare resync quietly shrinking the denylist, so the count is raised deliberately and with the reason, not to make a red test green. --- api/tinyhumans.backend.json | 21 +++++++++++++++++---- src/generated_public_routes.rs | 5 +++++ src/lib.rs | 7 ++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index 35d594a..b31d045 100644 --- a/api/tinyhumans.backend.json +++ b/api/tinyhumans.backend.json @@ -8,11 +8,11 @@ "url": "https://api.tinyhumans.ai/swagger.json", "title": "TinyHumans API", "version": "1.0.0", - "pathCount": 241, - "totalOperationCount": 273, - "operationCount": 227, + "pathCount": 245, + "totalOperationCount": 278, + "operationCount": 229, "supplementalOperationCount": 14, - "excludedAdminOperationCount": 39, + "excludedAdminOperationCount": 42, "excludedWebhookOperationCount": 12, "servers": [ "https://api.tinyhumans.ai/", @@ -164,6 +164,19 @@ "POST /auth/login-token/consume" ] }, + { + "name": "blog", + "basePath": "/blog", + "auth": "none", + "operationCount": 2, + "tags": [ + "Blog" + ], + "routes": [ + "GET /blog/posts", + "GET /blog/posts/{slug}" + ] + }, { "name": "budgets", "basePath": "/budgets", diff --git a/src/generated_public_routes.rs b/src/generated_public_routes.rs index 988fa15..97ae03b 100644 --- a/src/generated_public_routes.rs +++ b/src/generated_public_routes.rs @@ -92,6 +92,8 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("POST", "/auth/integrations/{integrationId}/tokens"), ("POST", "/auth/login-token/consume"), ("GET", "/auth/me"), + ("GET", "/blog/posts"), + ("GET", "/blog/posts/{slug}"), ("GET", "/budgets"), ("POST", "/budgets/seats"), ("DELETE", "/budgets/seats/{seatId}"), @@ -237,6 +239,9 @@ pub(crate) const UNEXPOSED_ROUTES: &[(&str, &str)] = &[ ("POST", "/admin/announcements"), ("DELETE", "/admin/announcements/{announcementId}"), ("PATCH", "/admin/announcements/{announcementId}"), + ("POST", "/admin/blog-posts"), + ("DELETE", "/admin/blog-posts/{blogPostId}"), + ("PATCH", "/admin/blog-posts/{blogPostId}"), ("POST", "/admin/coupons"), ("DELETE", "/admin/coupons/{couponId}"), ("PATCH", "/admin/coupons/{couponId}"), diff --git a/src/lib.rs b/src/lib.rs index 44f21a7..bef950e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -532,7 +532,12 @@ mod exclusion_tests { // `/opencompany/instances/{slug}/inference-key`. The orchestrator calls // them with a shared secret no SDK user holds, so they are excluded // from the client and blocked at the raw transport. - assert_eq!(UNEXPOSED_ROUTES.len(), 51); + // + // 51 -> 54: the three write operations on `/admin/blog-posts`, which + // the admin dashboard drives with the admin service token. The two + // public `/blog/posts` reads that arrived with them are ordinary + // user-facing API and are exposed; only the authoring side is blocked. + assert_eq!(UNEXPOSED_ROUTES.len(), 54); for (method, template) in UNEXPOSED_ROUTES { let concrete_path = template .split('/') From e7fb407762f5fbd91b413cf8e11e917ec303a03e Mon Sep 17 00:00:00 2001 From: Ghost Scripter Date: Wed, 9 Sep 2026 17:30:15 +0530 Subject: [PATCH 2/2] test: move the manifest operation counts with the blog routes The contract has three separate pinned counts, and the first commit moved only the one in src/lib.rs. openapi_sync.rs pins two more against the manifest -- operationCount and excludedAdminOperationCount -- so CI failed on 229 vs 227 where the local run had looked clean. 227 -> 229 is the two public blog reads; 39 -> 42 is the three admin writes that arrived with them. Same change, opposite sides of the line the exclusion filter draws. Each count is recorded with its reason, like the entries above them: the assertions are a ratchet against a resync silently changing the public surface, so they are only worth anything when a bump has to be justified. --- tests/openapi_sync.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/openapi_sync.rs b/tests/openapi_sync.rs index 5213211..4d73e9e 100644 --- a/tests/openapi_sync.rs +++ b/tests/openapi_sync.rs @@ -134,15 +134,21 @@ fn generated_rust_routes_match_the_public_manifest() { .collect::>(); let rust_routes = PUBLIC_ROUTES.iter().copied().collect::>(); - assert_eq!(manifest["source"]["operationCount"], 227); + // 227 -> 229: the two public blog reads, `GET /blog/posts` and + // `GET /blog/posts/{slug}`. + assert_eq!(manifest["source"]["operationCount"], 229); assert_eq!(manifest["source"]["supplementalOperationCount"], 14); // 37 -> 39: the two service-token operations on // `/opencompany/instances/{slug}/inference-key`. They are counted with the // admin exclusions because that tally is derived from `excludedOperations`, // which now holds every non-webhook exclusion including these. - assert_eq!(manifest["source"]["excludedAdminOperationCount"], 39); + // + // 39 -> 42: the three `/admin/blog-posts` writes that arrived with those + // reads. Same change, opposite side of the line: the reads are ordinary + // user-facing API, the writes take the admin service token. + assert_eq!(manifest["source"]["excludedAdminOperationCount"], 42); assert_eq!(manifest["source"]["excludedWebhookOperationCount"], 12); - assert_eq!(rust_routes.len(), 227); + assert_eq!(rust_routes.len(), 229); assert_eq!(rust_routes, manifest_routes); assert!(rust_routes .iter()