diff --git a/distribution/all-in-one/docker-compose.yaml b/distribution/all-in-one/docker-compose.yaml index df6724706c..06a26b8e95 100644 --- a/distribution/all-in-one/docker-compose.yaml +++ b/distribution/all-in-one/docker-compose.yaml @@ -51,9 +51,18 @@ services: # platform-api signs its tokens with RS256; the devportal verifies them # against jwt_public.pem from the shared keypair volume mounted below # (auth.local.jwt_public_key in devportal-config.toml). There is no shared - # HMAC secret. tls_skip_verify covers platform-api's self-signed cert. + # HMAC secret. - APIP_AP_AUTH_LOCAL_PLATFORM_API_URL=https://platform-api:9243 - - APIP_AP_AUTH_LOCAL_TLS_SKIP_VERIFY=true + # Trust platform-api's self-signed cert at Node's trust-store level instead of + # skipping verification per client. NODE_EXTRA_CA_CERTS *appends* to Node's + # built-in CAs (public CAs keep working) and applies process-wide, so it covers + # BOTH the auth login client (authController's https.Agent passes no custom + # `ca`, so it honours this) AND the webhook delivery worker + # (src/services/webhooks/deliveryWorker.js), which POSTs events such as + # apikey.generated over Node's default TLS and has no tls_skip_verify hatch of + # its own. That is why APIP_AP_AUTH_LOCAL_TLS_SKIP_VERIFY is no longer set here: + # verification stays ON for every platform-api call, self-signed cert and all. + - NODE_EXTRA_CA_CERTS=/etc/api-portal/certs/cert.pem # Required — devportal fails closed at startup if either doesn't resolve # to a 64-char hex string. Set both before `docker compose up`, e.g.: # export APIP_AP_SECURITY_ENCRYPTION_KEY=$(openssl rand -hex 32) @@ -70,11 +79,24 @@ services: # Same RS256 keypair volume platform-api signs with — the devportal reads # only jwt_public.pem (0644) from it to verify those tokens. - platform-api-jwt-keys:/etc/api-portal/keys:ro + # platform-api's self-signed TLS cert, trusted via NODE_EXTRA_CA_CERTS above. + # Subpath mount exposes only the public cert.pem — never the private key.pem + # that also lives in this volume (same pattern as the e2e compose). + - type: volume + source: platform-api-certs + target: /etc/api-portal/certs/cert.pem + read_only: true + volume: + subpath: cert.pem depends_on: postgres: condition: service_healthy platform-api: condition: service_healthy + # cert.pem must exist in the certs volume before Node reads NODE_EXTRA_CA_CERTS + # at startup (a missing file is silently ignored with a warning). + platform-api-certgen: + condition: service_completed_successfully platform-api-jwtkeygen: condition: service_completed_successfully @@ -85,11 +107,20 @@ services: entrypoint: ["/bin/sh", "-c"] command: - | - [ -f /certs/cert.pem ] && [ -f /certs/key.pem ] && exit 0 - openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \ - -keyout /certs/key.pem -out /certs/cert.pem \ - -subj "/O=WSO2 API Platform/CN=platform-api" \ - -addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1" + set -e + if [ ! -f /certs/cert.pem ] || [ ! -f /certs/key.pem ]; then + openssl req -x509 -newkey rsa:2048 -sha256 -days 365 -nodes \ + -keyout /certs/key.pem -out /certs/cert.pem \ + -subj "/O=WSO2 API Platform/CN=platform-api" \ + -addext "subjectAltName=DNS:localhost,DNS:platform-api,IP:127.0.0.1" + fi + # platform-api runs as uid 10001 and reads both files. Keep the private key + # owned by that uid and non-world-readable; keep the public cert world-readable + # so api-portal can trust it via NODE_EXTRA_CA_CERTS. Applied every run (like + # platform-api-jwtkeygen) so a cached certs volume gets the same permissions. + chown 10001 /certs/key.pem + chmod 0600 /certs/key.pem + chmod 0644 /certs/cert.pem volumes: - platform-api-certs:/certs diff --git a/portals/api-portal/src/controllers/apiContentController.js b/portals/api-portal/src/controllers/apiContentController.js index 1e9eb08c38..bd1bc82c3b 100644 --- a/portals/api-portal/src/controllers/apiContentController.js +++ b/portals/api-portal/src/controllers/apiContentController.js @@ -587,6 +587,12 @@ function requestOrigin(req) { return `${protocol}://${req.get('host')}`; } +const TRYOUT_CAPABLE_TYPES = new Set([ + constants.API_TYPE.WS, + constants.API_TYPE.WEBSUB, + constants.API_TYPE.GRAPHQL, +]); + const loadDocument = async (req, res, next) => { const { orgName, apiHandle, viewName, docType, docName } = req.params; @@ -599,11 +605,14 @@ const loadDocument = async (req, res, next) => { isAPIDefinition: false, isWebSocketTryout: false, isGraphQLTryout: false, + isTryout: false, }; templateContent.apiType = definitionResponse.apiType; + templateContent.supportsTryout = TRYOUT_CAPABLE_TYPES.has(definitionResponse.apiType); if (isSpecPage && definitionResponse.swagger) { const specType = definitionResponse.apiType; - const tryoutEnabled = !!req.query.tryout; + const tryoutEnabled = req.query.tryout === true || req.query.tryout === 'true'; + templateContent.isTryout = templateContent.supportsTryout && tryoutEnabled; if (specType === constants.API_TYPE.WS || specType === constants.API_TYPE.WEBSUB) { templateContent.asyncapi = JSON.stringify(parseApiDefinitionContent(definitionResponse.swagger)); templateContent.isWebSocketTryout = tryoutEnabled; @@ -653,12 +662,20 @@ const loadDocument = async (req, res, next) => { let templateContent = { "isAPIDefinition": false, "isWebSocketTryout": false, - "isGraphQLTryout": false + "isGraphQLTryout": false, + "isTryout": false }; const definitionResponse = await getAPIDefinition(orgName, viewName, apiHandle); templateContent.apiType = definitionResponse.apiType; - - const tryoutEnabled = req.query.tryout ? true : false; + + // Only an explicit tryout=true opts in, and only on the specification page — + // the tryout payload (asyncapi/GraphQL introspection) is loaded only there, so a + // non-spec document must never render the tryout console. Any other query value + // (e.g. tryout=false) is a truthy string and must not enable it. + const isSpecPage = req.originalUrl.includes(constants.FILE_NAME.API_SPECIFICATION_PATH); + const tryoutEnabled = isSpecPage && (req.query.tryout === true || req.query.tryout === 'true'); + templateContent.supportsTryout = TRYOUT_CAPABLE_TYPES.has(definitionResponse.apiType); + templateContent.isTryout = templateContent.supportsTryout && tryoutEnabled; if (definitionResponse.apiType === constants.API_TYPE.WS || definitionResponse.apiType === constants.API_TYPE.WEBSUB) { templateContent.isWebSocketTryout = tryoutEnabled; } else if (definitionResponse.apiType === constants.API_TYPE.GRAPHQL) { @@ -667,7 +684,7 @@ const loadDocument = async (req, res, next) => { let apiMetadata = definitionResponse.metaData; //load API definition - if (req.originalUrl.includes(constants.FILE_NAME.API_SPECIFICATION_PATH)) { + if (isSpecPage) { if (definitionResponse.apiType === constants.API_TYPE.MCP) { // The playground reads its server URL from servers[0].url. A diff --git a/portals/api-portal/src/controllers/apiKeysPageController.js b/portals/api-portal/src/controllers/apiKeysPageController.js index 6d623620ec..4a3671285e 100644 --- a/portals/api-portal/src/controllers/apiKeysPageController.js +++ b/portals/api-portal/src/controllers/apiKeysPageController.js @@ -51,7 +51,7 @@ const loadAPIApiKeys = async (req, res, next) => { const images = metaData.apiImageMetadata; if (images) { for (const key in images) { - images[key] = `${constants.API_PORTAL_API.orgPath(orgId)}${constants.ROUTE.API_FILE_PATH}${apiId}${constants.API_TEMPLATE_FILE_NAME}${images[key]}`; + images[key] = `${constants.API_PORTAL_API.orgPath(orgId)}${constants.ROUTE.API_FILE_PATH}${metaData.id}${constants.API_TEMPLATE_FILE_NAME}${images[key]}`; } } } else { diff --git a/portals/api-portal/src/controllers/apiWorkflowsController.js b/portals/api-portal/src/controllers/apiWorkflowsController.js index 6c2d4eb8c0..798841529f 100644 --- a/portals/api-portal/src/controllers/apiWorkflowsController.js +++ b/portals/api-portal/src/controllers/apiWorkflowsController.js @@ -23,7 +23,7 @@ const apiWorkflowService = require('../services/apiWorkflowService'); const { config } = require('../config/configLoader'); const logger = require('../config/logger'); const util = require('../utils/util'); -const { loadLayoutFromAPI, renderGivenTemplate, renderTemplateFromAPI, rewriteViewStyles, isAiDisabledForPortal } = require('../utils/util'); +const { loadLayoutFromAPI, renderGivenTemplate, renderTemplateFromAPI, rewriteViewStyles, isAiDisabledForPortal, resolveAiEnabled } = require('../utils/util'); const constants = require('../utils/constants'); const fs = require('fs'); const path = require('path'); @@ -114,6 +114,11 @@ const loadAPIWorkflows = async (req, res, next) => { viewName, baseUrl: `/${orgName}/views/${viewName}`, profile, + // Gate the AI Ready chips on the real AI toggle, not the artifact type. + // Set explicitly so the custom-view renderGivenTemplate path (which hardcodes + // aiEnabled: true) also respects it, matching renderTemplateFromAPI. Uses the + // fail-open helper so a transient config-read error doesn't reject the page. + aiEnabled: await resolveAiEnabled(orgId, viewName), }; const dbLayout = await loadLayoutFromAPI(orgId, viewName); @@ -194,6 +199,11 @@ const loadAPIWorkflowDetail = async (req, res, next) => { viewName, baseUrl: `/${orgName}/views/${viewName}`, profile, + // Gate the "Try with AI" button/modal on the real AI toggle. Set explicitly so + // the custom-view renderGivenTemplate path (which hardcodes aiEnabled: true) also + // respects it, matching renderTemplateFromAPI. Uses the fail-open helper so a + // transient config-read error doesn't reject the page. + aiEnabled: await resolveAiEnabled(orgId, viewName), }; const dbLayout = await loadLayoutFromAPI(orgId, viewName); diff --git a/portals/api-portal/src/controllers/viewConfigureController.js b/portals/api-portal/src/controllers/viewConfigureController.js index d4c97a1562..593f70fc98 100644 --- a/portals/api-portal/src/controllers/viewConfigureController.js +++ b/portals/api-portal/src/controllers/viewConfigureController.js @@ -142,6 +142,12 @@ const loadSettingsPage = async (req, res) => { } templateContent.orgLabels = orgLabels; + const labelNameByHandle = new Map(orgLabels.map(l => [l.id, l.displayName])); + templateContent.views = views.map(view => ({ + ...view, + labelNames: (view.labels || []).map(handle => labelNameByHandle.get(handle) || handle), + })); + let orgPlans = []; try { const plansRaw = await subscriptionPlanDao.list(orgId); diff --git a/portals/api-portal/src/defaultContent/images/graphql-icon.svg b/portals/api-portal/src/defaultContent/images/graphql-icon.svg new file mode 100644 index 0000000000..ebf6590ba5 --- /dev/null +++ b/portals/api-portal/src/defaultContent/images/graphql-icon.svg @@ -0,0 +1 @@ +GraphQL diff --git a/portals/api-portal/src/defaultContent/images/websocket-icon.svg b/portals/api-portal/src/defaultContent/images/websocket-icon.svg new file mode 100644 index 0000000000..faae2d35b4 --- /dev/null +++ b/portals/api-portal/src/defaultContent/images/websocket-icon.svg @@ -0,0 +1 @@ +WebSocket diff --git a/portals/api-portal/src/defaultContent/pages/api-landing/page.hbs b/portals/api-portal/src/defaultContent/pages/api-landing/page.hbs index cab115e649..f75d27ea23 100644 --- a/portals/api-portal/src/defaultContent/pages/api-landing/page.hbs +++ b/portals/api-portal/src/defaultContent/pages/api-landing/page.hbs @@ -58,7 +58,7 @@ {{> alert }} {{!-- ── AGENT PROMPT MODAL ── --}} -{{#if showApiWorkflowsNav}} +{{#if aiEnabled}} {{#unless (eq apiMetadata.agentVisibility "HIDDEN")}} {{> api-agent-prompt-modal}} {{/unless}} diff --git a/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-detail-banner.hbs b/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-detail-banner.hbs index 763b70da8a..f314c72732 100644 --- a/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-detail-banner.hbs +++ b/portals/api-portal/src/defaultContent/pages/api-landing/partials/api-detail-banner.hbs @@ -18,14 +18,16 @@ {{#if (eq apiMetadata.type "GRAPHQL")}}dp-badge--graphql {{else if (eq apiMetadata.type "WS")}}dp-badge--ws {{else if (eq apiMetadata.type "SOAP")}}dp-badge--soap + {{else if (eq apiMetadata.type "WebSubApi")}}dp-badge--ws {{else}}dp-badge--rest{{/if}}"> - {{#if (eq apiMetadata.type "GRAPHQL")}} GraphQL - {{else if (eq apiMetadata.type "WS")}} WebSocket + {{#if (eq apiMetadata.type "GRAPHQL")}} GraphQL + {{else if (eq apiMetadata.type "WS")}} WebSocket + {{else if (eq apiMetadata.type "WebSubApi")}} WebSub {{else if (eq apiMetadata.type "SOAP")}} SOAP - {{else}} {{apiMetadata.type}}{{/if}} + {{else}} REST{{/if}} {{/if}} - {{#if showApiWorkflowsNav}} + {{#if aiEnabled}} {{#unless (eq apiMetadata.agentVisibility "HIDDEN")}} AI Ready @@ -68,7 +70,7 @@ {{/if}} {{/in}} - {{#if showApiWorkflowsNav}} + {{#if aiEnabled}} {{#unless (eq apiMetadata.agentVisibility "HIDDEN")}} {{/unless}} + {{/if}} @@ -58,7 +60,11 @@ {{> alert}} {{!-- ── AGENT PROMPT MODAL ── --}} + {{#if aiEnabled}} + {{#unless (eq flow.agentVisibility "HIDDEN")}} {{> agent-prompt-modal}} + {{/unless}} + {{/if}} {{!-- Hidden container for spec content --}} diff --git a/portals/api-portal/src/defaultContent/pages/api-workflows/page.hbs b/portals/api-portal/src/defaultContent/pages/api-workflows/page.hbs index 51435a6a84..18d02645d2 100644 --- a/portals/api-portal/src/defaultContent/pages/api-workflows/page.hbs +++ b/portals/api-portal/src/defaultContent/pages/api-workflows/page.hbs @@ -10,13 +10,6 @@

API Workflows

Pre-configured workflows let your AI agents reference these best practices to use your APIs correctly, every time — no hallucination, no manual setup.

- {{#if apiWorkflows.length}} - {{#if profile.isAdmin}} - - Manage workflows - - {{/if}} - {{/if}} {{#if apiWorkflows.length}} @@ -31,12 +24,7 @@

No workflows yet

-

Create a workflow to encode a multi-step API sequence your AI agents can follow. Published workflows will appear here for every consumer.

- {{#if profile.isAdmin}} - - Create workflow - - {{/if}} +

There are no published workflows available right now. Once they're published, they'll appear here for your AI agents to follow.

{{/if}} diff --git a/portals/api-portal/src/defaultContent/pages/api-workflows/partials/workflow-card.hbs b/portals/api-portal/src/defaultContent/pages/api-workflows/partials/workflow-card.hbs index 52522c94c7..6c712e6d60 100644 --- a/portals/api-portal/src/defaultContent/pages/api-workflows/partials/workflow-card.hbs +++ b/portals/api-portal/src/defaultContent/pages/api-workflows/partials/workflow-card.hbs @@ -17,10 +17,12 @@ {{/if}} diff --git a/portals/api-portal/src/defaultContent/pages/apis/partials/api-listing.hbs b/portals/api-portal/src/defaultContent/pages/apis/partials/api-listing.hbs index 4f021dfedc..01150283bc 100644 --- a/portals/api-portal/src/defaultContent/pages/apis/partials/api-listing.hbs +++ b/portals/api-portal/src/defaultContent/pages/apis/partials/api-listing.hbs @@ -101,13 +101,13 @@ {{else if (eq type "SOAP")}}dp-badge--soap {{else if (eq type "WebSubApi")}}dp-badge--ws {{else}}dp-badge--rest{{/if}}"> - {{#if (eq type "GRAPHQL")}} GraphQL - {{else if (eq type "WS")}} WebSocket + {{#if (eq type "GRAPHQL")}} GraphQL + {{else if (eq type "WS")}} WebSocket {{else if (eq type "WebSubApi")}} WebSub {{else if (eq type "SOAP")}} SOAP {{else}} REST{{/if}} - {{#if ../showApiWorkflowsNav}} + {{#if ../aiEnabled}} {{#unless (eq agentVisibility "HIDDEN")}} AI Ready @@ -136,7 +136,7 @@ + diff --git a/portals/api-portal/src/defaultContent/pages/docs/partials/docs-nav.hbs b/portals/api-portal/src/defaultContent/pages/docs/partials/docs-nav.hbs index d1294efd6a..35b834096a 100644 --- a/portals/api-portal/src/defaultContent/pages/docs/partials/docs-nav.hbs +++ b/portals/api-portal/src/defaultContent/pages/docs/partials/docs-nav.hbs @@ -6,22 +6,13 @@
SPECIFICATION
+ class="adoc-nav-item doc-link{{#if ../isAPIDefinition}}{{#unless ../isTryout}} adoc-nav-item--active{{/unless}}{{/if}}"> {{#if (eq ../apiType "Mcp")}}MCP Playground{{else}}API Definition{{/if}} - {{#if (eq ../apiType "WS")}} - - Tryout - - {{/if}} - {{#if (eq ../apiType "WebSubApi")}} - - Tryout - - {{/if}} - {{#if (eq ../apiType "GRAPHQL")}} - + {{#if ../supportsTryout}} + Tryout {{/if}} diff --git a/portals/api-portal/src/defaultContent/pages/home/page.hbs b/portals/api-portal/src/defaultContent/pages/home/page.hbs index 060ee37e60..9f2f9acabc 100644 --- a/portals/api-portal/src/defaultContent/pages/home/page.hbs +++ b/portals/api-portal/src/defaultContent/pages/home/page.hbs @@ -24,4 +24,4 @@ {{#pageScripts}} {{/pageScripts}} -{{> home showApiWorkflowsNav=showApiWorkflowsNav}} +{{> home aiEnabled=aiEnabled}} diff --git a/portals/api-portal/src/defaultContent/pages/home/partials/home.hbs b/portals/api-portal/src/defaultContent/pages/home/partials/home.hbs index 56b281d7ce..0c6d259832 100644 --- a/portals/api-portal/src/defaultContent/pages/home/partials/home.hbs +++ b/portals/api-portal/src/defaultContent/pages/home/partials/home.hbs @@ -5,11 +5,11 @@
- Ready for AI agents + {{#if aiEnabled}}Ready for AI agents{{else}}Ready for developers{{/if}}

- {{artifactTypesLabel}}
built for Developers and AI Agents. + {{artifactTypesLabel}}
built for Developers{{#if aiEnabled}} and AI Agents{{/if}}.

@@ -27,7 +27,7 @@ Browse our MCP servers {{/if}} - {{#if showApiWorkflowsNav}} + {{#if aiEnabled}} @@ -37,7 +37,7 @@ -{{#if showApiWorkflowsNav}} +{{#if aiEnabled}}