diff --git a/render.yaml b/render.yaml index 5131784dd..36ffb0aef 100644 --- a/render.yaml +++ b/render.yaml @@ -97,6 +97,8 @@ services: sync: false - key: TREG_PLATFORM_KEY_HUNTER sync: false + - key: TREG_PLATFORM_KEY_FEEDJOLT # plan-included; BYOK until a per-call rate card exists + sync: false - key: TREG_PLATFORM_KEY_LEADMAGIC sync: false - key: TREG_PLATFORM_KEY_LUSHA diff --git a/src/treg/__init__.py b/src/treg/__init__.py index 2435312ba..cc6a03c08 100644 --- a/src/treg/__init__.py +++ b/src/treg/__init__.py @@ -1,3 +1,6 @@ """tools-registry — a credential-injecting proxy + skill registry.""" __version__ = "0.0.1" + +# Side-effect import: registers Feedjolt on oauth_providers.REGISTRY. +from . import oauth_feedjolt as _oauth_feedjolt # noqa: F401 diff --git a/src/treg/catalog/feedjolt.yaml b/src/treg/catalog/feedjolt.yaml new file mode 100644 index 000000000..41db3b896 --- /dev/null +++ b/src/treg/catalog/feedjolt.yaml @@ -0,0 +1,443 @@ +provider: feedjolt +source: + docs: https://www.feedjolt.com/en/docs/developers + openapi: https://api.feedjolt.com/openapi.json + curated: 2026-09-03 +# BYOK listing. REST+MCP sit in Feedjolt's premium set (Startup $9/mo monthly, Scale $15/mo +# annual). There is no per-call meter and no rate-card endpoint, so treg cannot serve these on +# its own key yet. Authenticated routes are own_account workspace data. +# +# MCP (Streamable HTTP at /mcp/reader and /mcp/writer) is a sibling product, not catalogued here — +# treg relays REST HTTP, not JSON-RPC. +# +# Probe is GET /workspaces. GET /health is unauthenticated (200 with no key) and must not be used. +# Auth: Authorization Bearer fjk_ (X-API-Key also works). The oauth sidecar sends Bearer. + +limits: "plan-included; 429 on abuse. REST API is not on the Growth plan." +pricing_url: https://www.feedjolt.com/en + +proposed_capabilities: + product.feedback.boards.list: "List feedback boards in a product workspace" + product.feedback.posts.list: "List customer feedback posts" + product.feedback.posts.get: "Get one feedback post" + product.feedback.posts.search: "Search customer feedback posts by keyword" + product.feedback.roadmap.get: "Get a product roadmap grouped by status" + product.feedback.changelog.list: "List published changelog entries" + product.feedback.statuses.list: "List feedback statuses in a workspace" + product.feedback.tags.list: "List tags in a feedback workspace" + +endpoints: + - id: feedjolt.public.boards.list + capability: product.feedback.boards.list + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/boards + name: "List public boards" + summary: "List Public Boards" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + test_request: + pathParams: {workspace_slug: "corvidly"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read; no API key required." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.public.posts.list + capability: product.feedback.posts.list + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/boards/{board_slug}/posts + name: "List public posts on a board" + summary: "List Public Posts" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + board_slug: {type: string, required: true, example: "feedback"} + queryParams: + page: {type: integer, required: false, example: 1} + page_size: {type: integer, required: false, example: 3} + sort_by: {type: string, required: false} + q: {type: string, required: false} + status_id: {type: string, required: false} + tag_ids: {type: string, required: false} + test_request: + pathParams: {workspace_slug: "corvidly", board_slug: "feedback"} + queryParams: {page_size: 3} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read; no API key required." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.public.posts.get + capability: product.feedback.posts.get + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/posts/{post_id} + name: "Get a public post" + summary: "Get Public Post" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + post_id: {type: string, required: true, example: "52428c9b-971a-4804-a96e-73ce3da4ab17"} + test_request: + pathParams: {workspace_slug: "corvidly", post_id: "52428c9b-971a-4804-a96e-73ce3da4ab17"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read; no API key required." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.public.posts.search + capability: product.feedback.posts.search + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/search + name: "Search public posts" + summary: "Search Public Posts" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + queryParams: + q: {type: string, required: true, example: "MCP", note: "minLength 2, maxLength 200"} + test_request: + pathParams: {workspace_slug: "corvidly"} + queryParams: {q: "MCP"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read. OpenAPI: q minLength 2, maxLength 200." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.public.roadmap.get + capability: product.feedback.roadmap.get + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/roadmap + name: "Get public roadmap" + summary: "Get Public Roadmap" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + queryParams: + tags: {type: string, required: false, note: "comma-separated tag ids"} + test_request: + pathParams: {workspace_slug: "corvidly"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.public.changelog.list + capability: product.feedback.changelog.list + platform: web + scope: any_account + method: GET + path: /public/{workspace_slug}/changelog + name: "List public changelog" + summary: "List Public Changelog" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "corvidly"} + queryParams: + page: {type: integer, required: false, example: 1} + page_size: {type: integer, required: false, example: 5} + version_id: {type: string, required: false} + test_request: + pathParams: {workspace_slug: "corvidly"} + queryParams: {page_size: 5} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-02" + confidence: documented + note: "Public portal read." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.account.workspaces.list + kind: account + capability: account.usage + platform: account + scope: own_account + method: GET + path: /workspaces + name: "List workspaces for this API key" + summary: "List Workspaces" + input: + note: "no parameters — the key rides in the Authorization Bearer header (X-API-Key also works). This is the registry probe." + test_request: {queryParams: {}} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Probe. Garbage Bearer/X-API-Key returns 401 {\"detail\":\"Invalid or revoked API key\"}. Cookie-missing (no auth) returns 401 {\"detail\":\"Not authenticated\"}. Matching-slug key returns 200 (observed 2026-09-03, workspace luodaint, Bearer and X-API-Key). Cross-workspace slug returns 403 {\"detail\":\"API key is not valid for this workspace\"}." + docs_url: https://www.feedjolt.com/en/docs/developers + + - id: feedjolt.boards.list + capability: product.feedback.boards.list + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/boards + name: "List boards in a workspace" + summary: "List Boards" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + test_request: + pathParams: {workspace_slug: "luodaint"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.posts.list + capability: product.feedback.posts.list + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/posts + name: "List posts in a workspace" + summary: "List Posts" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + queryParams: + page: {type: integer, required: false, example: 1} + page_size: {type: integer, required: false, note: "keep small", example: 5} + board_id: {type: string, required: false} + status_id: {type: string, required: false} + exclude_status_ids: {type: string, required: false} + tag_ids: {type: string, required: false} + q: {type: string, required: false, note: "minLength 2, maxLength 200"} + owner_admin_id: {type: string, required: false} + date_from: {type: string, required: false, note: "YYYY-MM-DD"} + date_to: {type: string, required: false, note: "YYYY-MM-DD"} + sort_by: {type: string, required: false, note: "default newest"} + test_request: + pathParams: {workspace_slug: "luodaint"} + queryParams: {page_size: 5} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.posts.search + capability: product.feedback.posts.search + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/posts/search + name: "Search posts in a workspace" + summary: "Search Posts" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + queryParams: + q: {type: string, required: true, example: "ab", note: "minLength 2, maxLength 200"} + test_request: + pathParams: {workspace_slug: "luodaint"} + queryParams: {q: "ab"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.posts.get + capability: product.feedback.posts.get + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/posts/{post_id} + name: "Get a post" + summary: "Get a post in a workspace" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + post_id: {type: string, required: true, example: "b7e1779e-6c6d-4d07-b916-3dfd3ce5ad15"} + test_request: + pathParams: {workspace_slug: "luodaint", post_id: "b7e1779e-6c6d-4d07-b916-3dfd3ce5ad15"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.roadmap.get + capability: product.feedback.roadmap.get + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/roadmap + name: "Get workspace roadmap" + summary: "Get Workspace Roadmap" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + test_request: + pathParams: {workspace_slug: "luodaint"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.changelog.list + capability: product.feedback.changelog.list + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/changelog + name: "List workspace changelog" + summary: "List Workspace Changelog" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + queryParams: + page: {type: integer, required: false, example: 1} + page_size: {type: integer, required: false, example: 5} + test_request: + pathParams: {workspace_slug: "luodaint"} + queryParams: {page_size: 5} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.statuses.list + capability: product.feedback.statuses.list + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/statuses + name: "List statuses" + summary: "List Statuses" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + test_request: + pathParams: {workspace_slug: "luodaint"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs + + - id: feedjolt.tags.list + capability: product.feedback.tags.list + platform: web + scope: own_account + method: GET + path: /workspaces/{workspace_slug}/tags + name: "List tags" + summary: "List Tags" + input: + pathParams: + workspace_slug: {type: string, required: true, example: "luodaint"} + test_request: + pathParams: {workspace_slug: "luodaint"} + cost: + type: free + value: 0 + currency: USD + unit: call + source: docs + source_url: https://www.feedjolt.com/en + checked: "2026-09-03" + confidence: documented + note: "Included in plan. Auth: Authorization Bearer fjk_ (X-API-Key also works). Live 2xx on 2026-09-03 (slug must match the key). Cross-workspace slug returns 403." + docs_url: https://api.feedjolt.com/docs diff --git a/src/treg/config.py b/src/treg/config.py index 198b55812..cb29985cc 100644 --- a/src/treg/config.py +++ b/src/treg/config.py @@ -149,6 +149,7 @@ def _async_pg_driver(cls, v: str) -> str: platform_key_moz: str = "" # base64 of "access_id:secret_key" (HTTP Basic) platform_key_seranking: str = "" platform_key_hunter: str = "" + platform_key_feedjolt: str = "" # Bearer fjk_…; plan-included REST, no per-call meter platform_key_leadmagic: str = "" platform_key_lusha: str = "" platform_key_pdl: str = "" diff --git a/src/treg/oauth_feedjolt.py b/src/treg/oauth_feedjolt.py new file mode 100644 index 000000000..14feed0ae --- /dev/null +++ b/src/treg/oauth_feedjolt.py @@ -0,0 +1,44 @@ +"""Feedjolt key-provider entry. + +Lives in its own module so the listing can ship without rewriting +the 140k-line oauth_providers.py in one API call. Importing this module +registers FEEDJOLT on oauth_providers.REGISTRY. +""" + +from __future__ import annotations + +from .oauth_providers import OAuthProvider, REGISTRY + +FEEDJOLT = OAuthProvider( + service="feedjolt", + display_name="Feedjolt", + auth_kind="key", + token_label="API key", + token_placeholder="your Feedjolt API key (fjk_…)", + # Published docs use Authorization: Bearer. OpenAPI also lists X-API-Key. + token_header="Authorization", + token_format="Bearer {secret}", + setup_url="https://www.feedjolt.com/en/docs/developers", + setup_action_label="Get your Feedjolt API key", + setup_steps=( + "Sign in to Feedjolt (Startup or Scale; the REST API is in the premium set).", + "Open the workspace dashboard and create an API key.", + "Copy the key. It starts with fjk_.", + ), + setup_note=( + "Main agent auth is OAuth 2.1 on the MCP servers (DCR/PKCE; not this catalog). " + "This listing is REST: API keys (fjk_) also work. REST calls are included in the plan, not billed per call. " + "GET /workspaces is the probe. Do not use GET /health — it is unauthenticated. " + "A garbage Bearer/X-API-Key returns 401 {\"detail\":\"Invalid or revoked API key\"}." + ), + auth_uri="", token_uri="", + scopes={}, + client_id_setting="", client_secret_setting="", + category="Community", + summary="Read customer feedback boards, posts, roadmaps and changelogs for a Feedjolt workspace.", + base_url="https://api.feedjolt.com/api/v1", + docs_url="https://www.feedjolt.com/en/docs/developers", + probe_path="/workspaces", # authenticated; GET /health is public and cannot validate a key +) + +REGISTRY[FEEDJOLT.service] = FEEDJOLT diff --git a/src/treg/web/logos/feedjolt.svg b/src/treg/web/logos/feedjolt.svg new file mode 100644 index 000000000..e026e9d38 --- /dev/null +++ b/src/treg/web/logos/feedjolt.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tests/test_key_providers.py b/tests/test_key_providers.py index ef2c7ec37..c027e8ccd 100644 --- a/tests/test_key_providers.py +++ b/tests/test_key_providers.py @@ -100,7 +100,7 @@ async def test_key_connect_supports_a_query_param_key(clients: AsyncClient, monk async def test_a_plain_text_error_body_is_rejected(clients: AsyncClient, monkeypatch): - """Semrush signals a bad key with HTTP 200 + an "ERROR ..." text body. Storing it anyway just + """Semrush signals a bad key with HTTP 200 + an \"ERROR ...\" text body. Storing it anyway just moves the failure to the first real report call, after the user has left the setup screen.""" monkeypatch.setitem(P.REGISTRY, "semrush", dataclasses.replace( P.REGISTRY["semrush"], base_url="http://upstream", probe_url="", probe_path="/units-bad")) diff --git a/tests/test_oauth_providers_m3.py b/tests/test_oauth_providers_m3.py index 7fd7cbeba..cda678cd6 100644 --- a/tests/test_oauth_providers_m3.py +++ b/tests/test_oauth_providers_m3.py @@ -47,7 +47,7 @@ def test_every_provider_is_registered(): "google-ads", "youtube", "linkedin", "slack", "x", "tiktok", "facebook", "instagram", "meta-ads", # API-key providers (auth_kind="key") - "apollo", "pdl", "akta", "hunter", "crunchbase", "tikhub", "brightdata", "semrush", "justoneapi", + "apollo", "pdl", "akta", "hunter", "feedjolt", "crunchbase", "tikhub", "brightdata", "semrush", "justoneapi", "scrapecreators", "dataforseo", "seranking", "moz", "majestic", "serpstat", "exa", "lusha", "coresignal", "diffbot", "thecompaniesapi", "leadmagic", "fiber-ai",