diff --git a/README.md b/README.md index 6ed0616..7358e7b 100755 --- a/README.md +++ b/README.md @@ -141,15 +141,23 @@ wrangler queues create github-events ```bash # Set ChittyOS service tokens -wrangler secret put CHITTY_ID_TOKEN -wrangler secret put CHITTY_AUTH_TOKEN -wrangler secret put CHITTY_CASES_TOKEN -wrangler secret put CHITTY_FINANCE_TOKEN -wrangler secret put CHITTY_EVIDENCE_TOKEN -wrangler secret put CHITTY_SYNC_TOKEN -wrangler secret put CHITTY_CHRONICLE_TOKEN -wrangler secret put CHITTY_CONTEXTUAL_TOKEN -wrangler secret put CHITTY_REGISTRY_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_ID_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_AUTH_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_CASES_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_FINANCE_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_EVIDENCE_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_SYNC_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_CHRONICLE_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_CONTEXTUAL_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_REGISTRY_TOKEN + +# MCP and package auth tokens (VM/runtime) +wrangler secret put CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN +wrangler secret put CHITTYAUTH_ISSUED_NPM_TOKEN + +# VM runtime injection (non-interactive auth handoff) +bash scripts/inject-runtime-auth.sh # Set third-party API keys wrangler secret put NOTION_TOKEN diff --git a/scripts/inject-runtime-auth.sh b/scripts/inject-runtime-auth.sh new file mode 100755 index 0000000..120e79b --- /dev/null +++ b/scripts/inject-runtime-auth.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Runtime auth injector for VM sessions. +# Canonical policy: CHITTYAUTH_ISSUED_* names are authoritative. + +pick_value() { + local current="$1" + shift + if [[ -n "${current:-}" ]]; then + printf "%s" "$current" + return + fi + for candidate in "$@"; do + if [[ -n "${candidate:-}" ]]; then + printf "%s" "$candidate" + return + fi + done +} + +CH1TTY_SMART="$(pick_value "${CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN:-}" "${CH1TTY_SMART_MCP_TOKEN:-}")" +CHITTYMCP="$(pick_value "${CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN:-}" "${CHITTYMCP_TOKEN:-}")" +NPM_ISSUED="$(pick_value "${CHITTYAUTH_ISSUED_NPM_TOKEN:-}" "${NPM_TOKEN:-}" "${NODE_AUTH_TOKEN:-}")" + +if [[ -n "${CH1TTY_SMART:-}" ]]; then + export CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN="$CH1TTY_SMART" +fi + +if [[ -n "${CHITTYMCP:-}" ]]; then + export CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN="$CHITTYMCP" +fi + +if [[ -n "${NPM_ISSUED:-}" ]]; then + export CHITTYAUTH_ISSUED_NPM_TOKEN="$NPM_ISSUED" + export NPM_TOKEN="$NPM_ISSUED" + export NODE_AUTH_TOKEN="$NPM_ISSUED" + + # Non-interactive npm auth for this VM user. + umask 077 + touch "${HOME}/.npmrc" + if grep -q '^//registry.npmjs.org/:_authToken=' "${HOME}/.npmrc" 2>/dev/null; then + sed -i 's#^//registry.npmjs.org/:_authToken=.*#//registry.npmjs.org/:_authToken=${NPM_TOKEN}#' "${HOME}/.npmrc" + else + printf "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}\n" >> "${HOME}/.npmrc" + fi +fi + +echo "runtime_auth_injection=ok" +echo "has_ch1tty_smart_token=$([[ -n "${CH1TTY_SMART:-}" ]] && echo true || echo false)" +echo "has_chittymcp_token=$([[ -n "${CHITTYMCP:-}" ]] && echo true || echo false)" +echo "has_npm_token=$([[ -n "${NPM_ISSUED:-}" ]] && echo true || echo false)" diff --git a/scripts/setup-mcp-profiles.sh b/scripts/setup-mcp-profiles.sh new file mode 100755 index 0000000..bf42672 --- /dev/null +++ b/scripts/setup-mcp-profiles.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Render profile-specific MCP config snippets. +# Purpose: explicit split between ChatGPT MCP endpoints and non-ChatGPT MCP endpoints. + +TARGET="${1:-all}" # chatgpt | claude | codex | all + +render_chatgpt() { + cat <<'EOF' +{ + "chatgpt_mcp": { + "ch1tty_smart_mcp": { + "url": "https://chatgpt.ch1tty.com/mcp", + "bearer_token_env_var": "CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN" + }, + "chittymcp": { + "url": "https://chatgpt.chitty.cc/mcp", + "bearer_token_env_var": "CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN" + } + } +} +EOF +} + +render_claude() { + cat <<'EOF' +{ + "mcpServers": { + "ch1tty-primary": { "url": "https://mcp.ch1tty.com/mcp" }, + "ch1tty-fallback": { "url": "https://mcp.chitty.cc/mcp" } + } +} +EOF +} + +render_codex() { + cat <<'EOF' +[mcp_servers.ch1tty-smart-mcp] +url = "https://chatgpt.ch1tty.com/mcp" +bearer_token_env_var = "CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN" + +[mcp_servers.chittymcp] +url = "https://chatgpt.chitty.cc/mcp" +bearer_token_env_var = "CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN" + +EOF +} + +case "$TARGET" in + chatgpt) + render_chatgpt + ;; + claude) + render_claude + ;; + codex) + render_codex + ;; + all) + echo "# --- ChatGPT profile ---" + render_chatgpt + echo + echo "# --- Claude profile ---" + render_claude + echo + echo "# --- Codex profile ---" + render_codex + ;; + *) + echo "Usage: $0 [chatgpt|claude|codex|all]" >&2 + exit 2 + ;; +esac diff --git a/scripts/setup-mcp.sh b/scripts/setup-mcp.sh index aa26802..67c72c1 100755 --- a/scripts/setup-mcp.sh +++ b/scripts/setup-mcp.sh @@ -96,13 +96,13 @@ CHITTYCONNECT_URL=${CHITTYCONNECT_URL:-https://connect.chitty.cc} # Prompt for auth token echo "" -echo "You need a ChittyAuth token to connect to ChittyConnect." +echo "You need a ChittyAuth-issued token to connect to ChittyConnect." echo "To get a token, visit: https://auth.chitty.cc/register" echo "" -read -sp "ChittyAuth Token: " CHITTY_AUTH_TOKEN +read -sp "ChittyAuth Service Token: " CHITTYAUTH_ISSUED_CONNECT_SERVICE_TOKEN echo "" -if [ -z "$CHITTY_AUTH_TOKEN" ]; then +if [ -z "$CHITTYAUTH_ISSUED_CONNECT_SERVICE_TOKEN" ]; then echo -e "${RED}✗ ChittyAuth token is required${NC}" exit 1 fi @@ -118,7 +118,7 @@ echo -e "${YELLOW}[Step 4/6]${NC} Testing ChittyConnect connection..." # Test API connection HEALTH_CHECK=$(curl -s -o /dev/null -w "%{http_code}" \ - -H "Authorization: Bearer $CHITTY_AUTH_TOKEN" \ + -H "Authorization: Bearer $CHITTYAUTH_ISSUED_CONNECT_SERVICE_TOKEN" \ "$CHITTYCONNECT_URL/health") if [ "$HEALTH_CHECK" != "200" ]; then @@ -168,7 +168,7 @@ MCP_CONFIG=$(cat < "$PROJECT_DIR/.env.example" < { ? servicesData : servicesData?.services || []; - const catalogEntries = getServiceCatalogEntries(env); + const catalogEntries = await getServiceCatalogEntriesDynamic(env); const normalizeService = function(service) { const name = service?.name || service?.id || ""; @@ -113,6 +113,8 @@ discoveryRoutes.get("/chitty.json", async (c) => { mcp: `${mcpBase}/${service.sub}/mcp`, // Direct MCP link: useful for clients that prefer the origin service direct_mcp: `${service.url.replace(/\/$/, "")}/mcp`, + // Explicit service endpoint MCP (e.g., dispute.chitty.cc/mcp) + service_endpoint_mcp: `${service.url.replace(/\/$/, "")}/mcp`, // Legacy API field kept for backward compatibility api: `https://api.chitty.cc/${service.sub}/api`, direct_api: `${service.url.replace(/\/$/, "")}/api`, @@ -145,6 +147,8 @@ discoveryRoutes.get("/chitty.json", async (c) => { session_management: true, oauth_discovery: `${mcpBase}/.well-known/oauth-authorization-server`, + oauth_authority: "https://auth.chitty.cc", + oauth_backend: "neon (via chittyauth facade)", }, api: { openapi_spec: "https://connect.chitty.cc/openapi.json", diff --git a/src/api/routes/services.js b/src/api/routes/services.js index 146267a..25e136f 100755 --- a/src/api/routes/services.js +++ b/src/api/routes/services.js @@ -3,7 +3,7 @@ */ import { Hono } from "hono"; -import { getServiceCatalog } from "../../lib/service-catalog.js"; +import { getServiceCatalogEntriesDynamic } from "../../lib/service-catalog.js"; const servicesRoutes = new Hono(); @@ -13,7 +13,8 @@ const servicesRoutes = new Hono(); */ servicesRoutes.get("/status", async (c) => { try { - const statusChecks = getServiceCatalog(c.env).map(async (service) => { + const catalog = await getServiceCatalogEntriesDynamic(c.env); + const statusChecks = catalog.map(async (service) => { try { const response = await fetch(`${service.url}/health`, { method: "GET", @@ -60,7 +61,8 @@ servicesRoutes.get("/status", async (c) => { servicesRoutes.get("/:serviceId/status", async (c) => { try { const serviceId = c.req.param("serviceId"); - const service = getServiceCatalog(c.env).find((s) => s.id === serviceId); + const catalog = await getServiceCatalogEntriesDynamic(c.env); + const service = catalog.find((s) => s.id === serviceId || s.sub === serviceId); if (!service) { return c.json({ error: "Service not found" }, 404); diff --git a/src/lib/service-catalog.js b/src/lib/service-catalog.js index 5614182..6b48302 100644 --- a/src/lib/service-catalog.js +++ b/src/lib/service-catalog.js @@ -31,6 +31,54 @@ const SERVICE_ENTRIES = [ { id: "chittytask", sub: "tasks" }, ]; +function normalizeMcpServiceCatalog(payload) { + // chittymcp index returns: + // { services: { finance: { path: "/finance/mcp", ... }, ... } } + const servicesObj = payload?.services; + if (!servicesObj || typeof servicesObj !== "object") return []; + + const out = []; + for (const [sub, meta] of Object.entries(servicesObj)) { + const id = `chitty${sub}`; + const domain = "chitty.cc"; + out.push({ + id, + sub, + url: `https://${sub}.${domain}`, + mcpPath: meta?.path || `/${sub}/mcp`, + label: meta?.label || id, + }); + } + return out; +} + +/** + * Resolve service catalog from chittymcp authority when configured. + * Falls back to local static catalog if unavailable. + */ +export async function getServiceCatalogEntriesDynamic(env = {}) { + const authorityUrl = + env.CHITTYMCP_AUTHORITY_URL || "https://mcp.chitty.cc"; + + try { + const response = await fetch(authorityUrl, { + method: "GET", + signal: AbortSignal.timeout(4000), + }); + if (response.ok) { + const payload = await response.json(); + const normalized = normalizeMcpServiceCatalog(payload); + if (normalized.length > 0) return normalized; + } + } catch (error) { + console.warn( + `[service-catalog] chittymcp authority unavailable (${authorityUrl}): ${error.message}`, + ); + } + + return getServiceCatalogEntries(env); +} + /** * Get the full service catalog including subdomain prefixes. * diff --git a/src/middleware/oauth-provider.js b/src/middleware/oauth-provider.js index 210b360..6607da0 100644 --- a/src/middleware/oauth-provider.js +++ b/src/middleware/oauth-provider.js @@ -26,9 +26,9 @@ import { McpConnectAgent } from "../mcp/agent.js"; */ export function createOAuthProvider(honoApp) { return new OAuthProvider({ - // Hostname-specific: only mcp.chitty.cc/mcp is OAuth-protected. - // connect.chitty.cc/mcp/* falls through to defaultHandler (Hono + API key auth). - apiRoute: "https://mcp.chitty.cc/mcp", + // Host-agnostic route so OAuth MCP can operate behind Cloudflare gateway, + // portal, and custom MCP hostnames that terminate to this Worker. + apiRoute: "/mcp", apiHandler: McpConnectAgent.serve("/mcp", { binding: "MCP_AGENT" }), @@ -128,13 +128,15 @@ async function handleAuthorize(request, env) { // @canon: chittycanon://gov/governance#core-types const safeClientId = (oauthReqInfo.clientId || "anonymous").replace(/:/g, "-"); const actorId = `mcp-client-${safeClientId}`; - const { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({ - request: oauthReqInfo, - userId: actorId, - metadata: { - client: clientInfo?.clientName || oauthReqInfo.clientId || "unknown", - authorizedAt: new Date().toISOString(), - }, + const { redirectTo } = await env.OAUTH_PROVIDER.completeAuthorization({ + request: oauthReqInfo, + userId: actorId, + metadata: { + client: clientInfo?.clientName || oauthReqInfo.clientId || "unknown", + oauthAuthority: env.CHITTYAUTH_URL || "https://auth.chitty.cc", + authBackend: env.CHITTYAUTH_PROVIDER_BACKEND || "neon", + authorizedAt: new Date().toISOString(), + }, scope: oauthReqInfo.scope || ["mcp:read", "mcp:write"], props: { userId: actorId, diff --git a/src/services/cloudflare-secrets-client.js b/src/services/cloudflare-secrets-client.js index e1611ac..3ff2efe 100644 --- a/src/services/cloudflare-secrets-client.js +++ b/src/services/cloudflare-secrets-client.js @@ -49,10 +49,10 @@ const PATH_TO_ENV = { // Services "services/chittyauth/jwt_secret": "JWT_SECRET", "services/chittyauth/encryption_key": "ENCRYPTION_KEY", - "services/chittyauth/token_signing_key": "TOKEN_SIGNING_KEY", + "services/chittyauth/token_signing_key": "CHITTYAUTH_ISSUED_MINT_API_KEY", "services/chittyauth/auth_salt": "AUTH_SALT", "services/chittyconnect/service_token": "CHITTYCONNECT_SERVICE_TOKEN", - "services/chittyconnect/mcp_token": "CHITTYCONNECT_TOKEN", + "services/chittyconnect/mcp_token": "CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN", "services/chittyid/service_token": "CHITTY_ID_SERVICE_TOKEN", "services/chittyid/token": "CHITTY_ID_TOKEN", "services/chittyregistry/token": "CHITTY_REGISTRY_TOKEN", @@ -69,6 +69,9 @@ const PATH_TO_ENV = { "services/chittytrack/webhook_secret": "GITHUB_WEBHOOK_SECRET", "services/chittymint/secret": "CHITTYAUTH_ISSUED_MINT_API_KEY", "services/chittymint/service_token": "CHITTYAUTH_ISSUED_MINT_API_KEY", + "services/ch1tty-smart-mcp/token": "CHITTYAUTH_ISSUED_CH1TTY_SMART_MCP_TOKEN", + "services/chittymcp/token": "CHITTYAUTH_ISSUED_CHITTYMCP_TOKEN", + "integrations/npm/token": "CHITTYAUTH_ISSUED_NPM_TOKEN", }; export class CloudflareSecretsClient {