From 01766ece2e9c6b24e115780518c3e7449cc40d06 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 21 Sep 2026 01:36:48 +0530 Subject: [PATCH 1/5] feat(api): add guild link-token and orchestration steering endpoints Add a new `POST /auth/guild/link-token` route for Discord guild authentication and expose the `GET /orchestration/v1/steering` endpoint as a supplemental public operation. The orchestration route is needed for OpenHuman parity, while the guild link-token completes the auth channel linking flow. Also refactor the sync script to classify service-token operations alongside custom-llm-secret operations, and move the internal Discord routes into the unexposed routes list for consistency. Auto-committed-on: macbook Co-authored-by: Medulla --- api/tinyhumans.backend.json | 28 +++++++++++++++++++-------- scripts/sync-openapi.mjs | 35 +++++++++++++--------------------- src/generated_public_routes.rs | 5 ++++- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index ff28b2e..60bb683 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": 233, - "totalOperationCount": 258, - "operationCount": 206, - "supplementalOperationCount": 13, - "excludedAdminOperationCount": 44, + "pathCount": 235, + "totalOperationCount": 260, + "operationCount": 207, + "supplementalOperationCount": 14, + "excludedAdminOperationCount": 46, "excludedWebhookOperationCount": 12, "servers": [ "https://api.tinyhumans.ai/", @@ -148,7 +148,7 @@ "name": "auth", "basePath": "/auth", "auth": "mixed", - "operationCount": 15, + "operationCount": 16, "tags": [ "Auth" ], @@ -164,6 +164,7 @@ "GET /auth/{provider}/login", "POST /auth/channels/{channel}/link-token", "POST /auth/email/send-link", + "POST /auth/guild/link-token", "POST /auth/integrations/{integrationId}/tokens", "POST /auth/key/grant/{code}/issue", "POST /auth/keys", @@ -311,7 +312,7 @@ "name": "openCompany", "basePath": "/opencompany", "auth": "bearer", - "operationCount": 10, + "operationCount": 9, "tags": [ "OpenCompany" ], @@ -324,10 +325,21 @@ "POST /opencompany/instances/{slug}/custom-domain/verify", "POST /opencompany/instances/{slug}/resume", "POST /opencompany/instances/{slug}/suspend", - "POST /opencompany/instances/{slug}/update", "PUT /opencompany/instances/{slug}/custom-domain" ] }, + { + "name": "orchestration", + "basePath": "/orchestration", + "auth": "bearer", + "operationCount": 1, + "tags": [ + "OpenHuman parity" + ], + "routes": [ + "GET /orchestration/v1/steering" + ] + }, { "name": "payments", "basePath": "/payments", diff --git a/scripts/sync-openapi.mjs b/scripts/sync-openapi.mjs index 50ef3f9..917b9a8 100644 --- a/scripts/sync-openapi.mjs +++ b/scripts/sync-openapi.mjs @@ -33,6 +33,7 @@ const SUPPLEMENTAL_PUBLIC_OPERATIONS = [ ["POST", "/agent-integrations/tinyfish/agent/run"], ["POST", "/agent-integrations/tinyfish/fetch"], ["POST", "/agent-integrations/tinyfish/search"], + ["GET", "/orchestration/v1/steering"], ["PUT", "/teams/{teamId}"], ["DELETE", "/teams/{teamId}/members/{userId}"], ["PUT", "/teams/{teamId}/members/{userId}/role"], @@ -51,19 +52,9 @@ const SUPPLEMENTAL_PUBLIC_OPERATIONS = [ // derived from it. They are declared here so regeneration retains the filter // instead of silently emptying the denylist and opening the raw transport. const RETAINED_UNEXPOSED_ROUTES = [ - // Service-token operations. `isServiceTokenOperation` catches these when the - // spec still describes them -- a `--input` run against a local checkout's RAW - // document. A bare run fetches the DEPLOYED spec, where `publicSwaggerSpec` - // has already stripped them, so there is nothing left to detect and the - // denylist would quietly lose them: exactly the regression the rest of this - // list exists to prevent. Declared here so both paths agree. - ["POST", "/opencompany/instances/{slug}/inference-key"], - ["DELETE", "/opencompany/instances/{slug}/inference-key"], - ["POST", "/opencompany/instances/{slug}/usage"], ["POST", "/admin/announcements"], ["DELETE", "/admin/announcements/{announcementId}"], ["PATCH", "/admin/announcements/{announcementId}"], - ["POST", "/admin/blog-images"], ["POST", "/admin/coupons"], ["DELETE", "/admin/coupons/{couponId}"], ["PATCH", "/admin/coupons/{couponId}"], @@ -97,6 +88,12 @@ const RETAINED_UNEXPOSED_ROUTES = [ ["DELETE", "/invite/campaign/{codeId}"], ["POST", "/webhooks/composio"], ["POST", "/webhooks/discord"], + // Service-token callbacks (see isServiceTokenOperation): hidden from the + // served spec like the admin routes, so retained here the same way. + ["POST", "/internal/discord/link"], + ["DELETE", "/internal/discord/link/{userId}"], + ["POST", "/opencompany/instances/{slug}/inference-key"], + ["DELETE", "/opencompany/instances/{slug}/inference-key"], ["POST", "/webhooks/github"], ["POST", "/webhooks/ingress/{uuid}"], ["POST", "/webhooks/ingress/{uuid}/{path}"], @@ -200,13 +197,11 @@ function isCustomLlmSecretOperation(operation) { return security.some((entry) => Object.hasOwn(entry, "customLlmSecret")); } -/** - * Secured by the shared service token two backend services hold in common - * (`OPENCOMPANY_SERVICE_TOKEN`), not by anything a user of this SDK can obtain. - * Same category as `customLlmSecret` above: a caller of this client cannot - * authenticate to it, so generating a method for it describes a surface that - * can only 401. - */ +// Service-to-service routes (`/internal/*`, the orchestrator callbacks): the +// caller is another backend holding a shared secret, never a user with a +// bearer token, so a client SDK has nothing to send. The backend already hides +// these from its served Swagger; a spec dumped from a checkout still lists +// them, so classify by the security scheme rather than by path. function isServiceTokenOperation(operation) { const security = operation.security ?? []; return security.some((entry) => Object.hasOwn(entry, "serviceToken")); @@ -263,11 +258,7 @@ function buildManifest(spec) { // a user token, so it is not part of the public client surface. Counted // with the admin exclusions below, which are derived from // `excludedOperations` rather than tallied here. - if (isCustomLlmSecretOperation(operation)) { - excludedOperations.push({ method: method.toUpperCase(), path }); - continue; - } - if (isServiceTokenOperation(operation)) { + if (isCustomLlmSecretOperation(operation) || isServiceTokenOperation(operation)) { excludedOperations.push({ method: method.toUpperCase(), path }); continue; } diff --git a/src/generated_public_routes.rs b/src/generated_public_routes.rs index f9d5ed0..9fb4982 100644 --- a/src/generated_public_routes.rs +++ b/src/generated_public_routes.rs @@ -89,6 +89,7 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("POST", "/auth/channels/{channel}/link-token"), ("POST", "/auth/email/send-link"), ("GET", "/auth/email/verify"), + ("POST", "/auth/guild/link-token"), ("GET", "/auth/integrations"), ("DELETE", "/auth/integrations/{integrationId}"), ("POST", "/auth/integrations/{integrationId}/tokens"), @@ -143,8 +144,8 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("POST", "/opencompany/instances/{slug}/custom-domain/verify"), ("POST", "/opencompany/instances/{slug}/resume"), ("POST", "/opencompany/instances/{slug}/suspend"), - ("POST", "/opencompany/instances/{slug}/update"), ("GET", "/opencompany/instances/usage"), + ("GET", "/orchestration/v1/steering"), ("POST", "/payments/coinbase/charge"), ("GET", "/payments/coinbase/charge/{gatewayTransactionId}"), ("GET", "/payments/credits/auto-recharge"), @@ -252,6 +253,8 @@ pub(crate) const UNEXPOSED_ROUTES: &[(&str, &str)] = &[ ("POST", "/feedback/admin/triage/{id}/merge"), ("POST", "/feedback/admin/triage/{id}/reject"), ("POST", "/feedback/admin/triage/{id}/reprocess"), + ("POST", "/internal/discord/link"), + ("DELETE", "/internal/discord/link/{userId}"), ("GET", "/invite/campaign"), ("POST", "/invite/campaign"), ("DELETE", "/invite/campaign/{codeId}"), From c618b2b150fdbc5e037afd3be555619ad6239d10 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 21 Sep 2026 01:37:32 +0530 Subject: [PATCH 2/5] chore: files changed scripts/sync-openapi.mjs Auto-committed-on: macbook Co-authored-by: Medulla --- scripts/sync-openapi.mjs | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/scripts/sync-openapi.mjs b/scripts/sync-openapi.mjs index 917b9a8..50ef3f9 100644 --- a/scripts/sync-openapi.mjs +++ b/scripts/sync-openapi.mjs @@ -33,7 +33,6 @@ const SUPPLEMENTAL_PUBLIC_OPERATIONS = [ ["POST", "/agent-integrations/tinyfish/agent/run"], ["POST", "/agent-integrations/tinyfish/fetch"], ["POST", "/agent-integrations/tinyfish/search"], - ["GET", "/orchestration/v1/steering"], ["PUT", "/teams/{teamId}"], ["DELETE", "/teams/{teamId}/members/{userId}"], ["PUT", "/teams/{teamId}/members/{userId}/role"], @@ -52,9 +51,19 @@ const SUPPLEMENTAL_PUBLIC_OPERATIONS = [ // derived from it. They are declared here so regeneration retains the filter // instead of silently emptying the denylist and opening the raw transport. const RETAINED_UNEXPOSED_ROUTES = [ + // Service-token operations. `isServiceTokenOperation` catches these when the + // spec still describes them -- a `--input` run against a local checkout's RAW + // document. A bare run fetches the DEPLOYED spec, where `publicSwaggerSpec` + // has already stripped them, so there is nothing left to detect and the + // denylist would quietly lose them: exactly the regression the rest of this + // list exists to prevent. Declared here so both paths agree. + ["POST", "/opencompany/instances/{slug}/inference-key"], + ["DELETE", "/opencompany/instances/{slug}/inference-key"], + ["POST", "/opencompany/instances/{slug}/usage"], ["POST", "/admin/announcements"], ["DELETE", "/admin/announcements/{announcementId}"], ["PATCH", "/admin/announcements/{announcementId}"], + ["POST", "/admin/blog-images"], ["POST", "/admin/coupons"], ["DELETE", "/admin/coupons/{couponId}"], ["PATCH", "/admin/coupons/{couponId}"], @@ -88,12 +97,6 @@ const RETAINED_UNEXPOSED_ROUTES = [ ["DELETE", "/invite/campaign/{codeId}"], ["POST", "/webhooks/composio"], ["POST", "/webhooks/discord"], - // Service-token callbacks (see isServiceTokenOperation): hidden from the - // served spec like the admin routes, so retained here the same way. - ["POST", "/internal/discord/link"], - ["DELETE", "/internal/discord/link/{userId}"], - ["POST", "/opencompany/instances/{slug}/inference-key"], - ["DELETE", "/opencompany/instances/{slug}/inference-key"], ["POST", "/webhooks/github"], ["POST", "/webhooks/ingress/{uuid}"], ["POST", "/webhooks/ingress/{uuid}/{path}"], @@ -197,11 +200,13 @@ function isCustomLlmSecretOperation(operation) { return security.some((entry) => Object.hasOwn(entry, "customLlmSecret")); } -// Service-to-service routes (`/internal/*`, the orchestrator callbacks): the -// caller is another backend holding a shared secret, never a user with a -// bearer token, so a client SDK has nothing to send. The backend already hides -// these from its served Swagger; a spec dumped from a checkout still lists -// them, so classify by the security scheme rather than by path. +/** + * Secured by the shared service token two backend services hold in common + * (`OPENCOMPANY_SERVICE_TOKEN`), not by anything a user of this SDK can obtain. + * Same category as `customLlmSecret` above: a caller of this client cannot + * authenticate to it, so generating a method for it describes a surface that + * can only 401. + */ function isServiceTokenOperation(operation) { const security = operation.security ?? []; return security.some((entry) => Object.hasOwn(entry, "serviceToken")); @@ -258,7 +263,11 @@ function buildManifest(spec) { // a user token, so it is not part of the public client surface. Counted // with the admin exclusions below, which are derived from // `excludedOperations` rather than tallied here. - if (isCustomLlmSecretOperation(operation) || isServiceTokenOperation(operation)) { + if (isCustomLlmSecretOperation(operation)) { + excludedOperations.push({ method: method.toUpperCase(), path }); + continue; + } + if (isServiceTokenOperation(operation)) { excludedOperations.push({ method: method.toUpperCase(), path }); continue; } From 7a20534507b81df534841f3aaf84d09693880035 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 21 Sep 2026 01:37:44 +0530 Subject: [PATCH 3/5] chore(api): remove orchestration steering route and add internal Discord routes The orchestration steering endpoint has been removed from the public API and the OpenAPI spec, as it is no longer needed. Two internal Discord callback routes have been added to the retained unexposed routes list, gated by the GUILD_SERVICE_TOKEN, to support the new teeny Discord service integration. Auto-committed-on: macbook Co-authored-by: Medulla --- api/tinyhumans.backend.json | 16 ++-------------- scripts/sync-openapi.mjs | 3 +++ src/generated_public_routes.rs | 1 - 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/api/tinyhumans.backend.json b/api/tinyhumans.backend.json index 60bb683..ddb280c 100644 --- a/api/tinyhumans.backend.json +++ b/api/tinyhumans.backend.json @@ -10,8 +10,8 @@ "version": "1.0.0", "pathCount": 235, "totalOperationCount": 260, - "operationCount": 207, - "supplementalOperationCount": 14, + "operationCount": 206, + "supplementalOperationCount": 13, "excludedAdminOperationCount": 46, "excludedWebhookOperationCount": 12, "servers": [ @@ -328,18 +328,6 @@ "PUT /opencompany/instances/{slug}/custom-domain" ] }, - { - "name": "orchestration", - "basePath": "/orchestration", - "auth": "bearer", - "operationCount": 1, - "tags": [ - "OpenHuman parity" - ], - "routes": [ - "GET /orchestration/v1/steering" - ] - }, { "name": "payments", "basePath": "/payments", diff --git a/scripts/sync-openapi.mjs b/scripts/sync-openapi.mjs index 50ef3f9..036fe4a 100644 --- a/scripts/sync-openapi.mjs +++ b/scripts/sync-openapi.mjs @@ -60,6 +60,9 @@ const RETAINED_UNEXPOSED_ROUTES = [ ["POST", "/opencompany/instances/{slug}/inference-key"], ["DELETE", "/opencompany/instances/{slug}/inference-key"], ["POST", "/opencompany/instances/{slug}/usage"], + // Guild (teeny Discord service) callbacks, gated by GUILD_SERVICE_TOKEN. + ["POST", "/internal/discord/link"], + ["DELETE", "/internal/discord/link/{userId}"], ["POST", "/admin/announcements"], ["DELETE", "/admin/announcements/{announcementId}"], ["PATCH", "/admin/announcements/{announcementId}"], diff --git a/src/generated_public_routes.rs b/src/generated_public_routes.rs index 9fb4982..3be152b 100644 --- a/src/generated_public_routes.rs +++ b/src/generated_public_routes.rs @@ -145,7 +145,6 @@ pub const PUBLIC_ROUTES: &[(&str, &str)] = &[ ("POST", "/opencompany/instances/{slug}/resume"), ("POST", "/opencompany/instances/{slug}/suspend"), ("GET", "/opencompany/instances/usage"), - ("GET", "/orchestration/v1/steering"), ("POST", "/payments/coinbase/charge"), ("GET", "/payments/coinbase/charge/{gatewayTransactionId}"), ("GET", "/payments/credits/auto-recharge"), From 5f7ce972bbe37a7b6641c82403639c15621f9d99 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 21 Sep 2026 01:38:11 +0530 Subject: [PATCH 4/5] fix(test): update unexposed route count for Discord callbacks The assertion for the number of unexposed routes is increased from 56 to 58 to account for two new internal Discord service endpoints that were added. These endpoints use a shared service token rather than a user bearer token, so they are correctly classified as unexposed alongside the other service-to-service callbacks. Auto-committed-on: macbook Co-authored-by: Medulla --- src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 9f046e8..5172a27 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -548,7 +548,13 @@ mod exclusion_tests { // 55 -> 56: `POST /admin/blog-images`, the multipart upload the // dashboard uses for a post's cover and body figures. Same service // token as the other blog writes, so it is blocked alongside them. - assert_eq!(UNEXPOSED_ROUTES.len(), 56); + // + // 56 -> 58: the teeny Discord service (the guild) calls back into the + // backend on `POST /internal/discord/link` and + // `DELETE /internal/discord/link/{userId}`, both gated by a shared + // service token rather than a user bearer, so they are unexposed like + // the orchestrator's inference-key callbacks. + assert_eq!(UNEXPOSED_ROUTES.len(), 58); for (method, template) in UNEXPOSED_ROUTES { let concrete_path = template .split('/') From c600fb46f9d1d0b3d132d96861df3476a696fae1 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Mon, 21 Sep 2026 01:38:41 +0530 Subject: [PATCH 5/5] test(tests): update expected excluded admin operation count The expected count of excluded admin operations is raised from 44 to 46 to account for two newly added internal Discord service routes that are gated by a service token and therefore never appear in the public API surface. Auto-committed-on: macbook Co-authored-by: Medulla --- tests/openapi_sync.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/openapi_sync.rs b/tests/openapi_sync.rs index 06d0b26..b0990cb 100644 --- a/tests/openapi_sync.rs +++ b/tests/openapi_sync.rs @@ -180,7 +180,13 @@ fn generated_rust_routes_match_the_public_manifest() { // // 43 -> 44: `POST /admin/blog-images`, the multipart upload behind a // post's cover and body figures. Same token as the other blog writes. - assert_eq!(manifest["source"]["excludedAdminOperationCount"], 44); + // + // 44 -> 46: `POST /internal/discord/link` and + // `DELETE /internal/discord/link/{userId}`, the teeny Discord service's + // account-link callbacks, gated by GUILD_SERVICE_TOKEN. Service-token + // routes, so they land here and never in the public surface; the + // user-facing half of that flow is `POST /auth/guild/link-token`. + assert_eq!(manifest["source"]["excludedAdminOperationCount"], 46); assert_eq!(manifest["source"]["excludedWebhookOperationCount"], 12); assert_eq!(rust_routes.len(), 206); assert_eq!(rust_routes, manifest_routes);