@muxoai/sdk 1.3.0 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +19 -4
  3. package/.codex/config.toml +9 -2
  4. package/ARCHITECTURE.md +16 -6
  5. package/README.md +69 -24
  6. package/dist/clerk.d.ts +35 -0
  7. package/dist/clerk.js +201 -0
  8. package/dist/cli.js +1677 -103
  9. package/dist/composio-catalog.d.ts +234 -0
  10. package/dist/composio-catalog.js +1894 -0
  11. package/dist/config.d.ts +16 -0
  12. package/dist/config.js +27 -1
  13. package/dist/durable-workflow.d.ts +112 -0
  14. package/dist/durable-workflow.js +284 -0
  15. package/dist/engine.d.ts +377 -6
  16. package/dist/engine.js +1919 -94
  17. package/dist/index.d.ts +3 -0
  18. package/dist/index.js +3 -0
  19. package/dist/mcp-schema.d.ts +286 -0
  20. package/dist/mcp-schema.js +2580 -0
  21. package/dist/mcp.d.ts +2 -25
  22. package/dist/mcp.js +1008 -507
  23. package/dist/memory-graph.d.ts +45 -0
  24. package/dist/memory-graph.js +293 -0
  25. package/dist/nl.js +214 -35
  26. package/dist/oauth.d.ts +32 -0
  27. package/dist/oauth.js +134 -0
  28. package/dist/oss-suite.d.ts +217 -0
  29. package/dist/oss-suite.js +836 -0
  30. package/dist/policy.d.ts +190 -0
  31. package/dist/policy.js +1597 -0
  32. package/dist/providers/composio-instant.d.ts +4 -0
  33. package/dist/providers/composio-instant.js +890 -535
  34. package/dist/providers/composio-sdk.d.ts +126 -5
  35. package/dist/providers/composio-sdk.js +902 -79
  36. package/dist/providers/microsoft-governance.d.ts +89 -3
  37. package/dist/providers/microsoft-governance.js +673 -26
  38. package/dist/providers/stripe-billing.d.ts +42 -0
  39. package/dist/providers/stripe-billing.js +244 -5
  40. package/dist/providers/stripe-projects.d.ts +459 -4
  41. package/dist/providers/stripe-projects.js +2674 -122
  42. package/dist/providers/supermemory.d.ts +102 -6
  43. package/dist/providers/supermemory.js +609 -55
  44. package/dist/provisioning-managed.d.ts +50 -0
  45. package/dist/provisioning-managed.js +155 -0
  46. package/dist/sdk.d.ts +728 -7
  47. package/dist/sdk.js +539 -10
  48. package/dist/server.js +2744 -321
  49. package/dist/store.d.ts +138 -6
  50. package/dist/store.js +932 -58
  51. package/dist/types.d.ts +330 -4
  52. package/dist/ui/console-html.d.ts +62 -0
  53. package/dist/ui/console-html.js +3742 -2228
  54. package/dist/ui/design-tokens.d.ts +177 -0
  55. package/dist/ui/design-tokens.js +210 -0
  56. package/dist/version.d.ts +1 -0
  57. package/dist/version.js +1 -0
  58. package/package.json +6 -5
  59. package/plugins/muxo/.app.json +1 -1
  60. package/plugins/muxo/.claude-plugin/plugin.json +3 -3
  61. package/plugins/muxo/.codex-plugin/plugin.json +19 -10
  62. package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
  63. package/plugins/muxo/.mcp.json +1 -33
  64. package/plugins/muxo/assets/logo.png +0 -0
  65. package/plugins/muxo/hooks/session-start.sh +16 -4
  66. package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
  67. package/plugins/muxo/mcp.json +0 -10
  68. package/plugins/muxo/plugin.json +65 -8
  69. package/plugins/muxo/skills/muxo/SKILL.md +142 -14
  70. package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
  71. package/plugins/muxo/skills/muxo/references/cli.md +9 -3
  72. package/plugins/muxo/skills/muxo/references/mcp.md +22 -10
  73. package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
  74. package/plugins/muxo/skills/setup/SKILL.md +17 -3
  75. package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
  76. package/src/catalogues/stripe-projects-catalog.json +140 -0
package/dist/server.js CHANGED
@@ -5,40 +5,64 @@ import path from "node:path";
5
5
  import { getConfig, ROOT_DIR } from "./config.js";
6
6
  import { MuxoEngine } from "./engine.js";
7
7
  import { getMuxoMcpSkills, handleJsonRpcMcpRequest, MUXO_MCP_SERVER_INSTRUCTIONS, MUXO_MCP_TOOLS, } from "./mcp.js";
8
+ import { APPROVED_PLAN_STATUSES, LATEST_MCP_PROTOCOL_VERSION, MuxoToolError, SUPPORTED_MCP_PROTOCOL_VERSIONS, clampWaitSeconds, errorPayloadFrom, isPublicMcpRequest, validateMcpHttpHeadersAndMeta, workspaceAgentDid, } from "./mcp-schema.js";
8
9
  import { MuxoStore } from "./store.js";
9
- import { renderConsoleHtml, renderLandingHtml } from "./ui/console-html.js";
10
+ import { managedProviderConnectionList, managedProviderConnectionRequest, managedProviderConnectionUnlink, managedResource, managedResourceAction, } from "./provisioning-managed.js";
11
+ import { DEFAULT_WORKSPACE_POLICY, describePolicyInPlainEnglish, sanitizePolicyPatch, } from "./policy.js";
12
+ import { parseNaturalLanguageMemoryInstruction } from "./providers/supermemory.js";
13
+ import { MUXO_VERSION } from "./version.js";
14
+ import { fetchMuxoConnectLinkMetadata, isStripeProjectsExclusiveProvider, normalizeStripeProjectsProviderSlug, parseComposioWebhookEnvelope, submitMuxoConnectLink, verifyComposioWebhookSignature, wrapMuxoConnectUrl, } from "./composio-catalog.js";
15
+ import { isConsolePath, renderApprovalPageHtml, renderConsoleHtml, renderLandingHtml, renderOAuthCallbackPageHtml, renderOAuthConnectPageHtml, renderOAuthConsentHtml, } from "./ui/console-html.js";
16
+ import { fetchClerkProfile, looksLikeJwt, readClerkConfig, verifyClerkSessionToken, } from "./clerk.js";
17
+ import { DEFAULT_OAUTH_SCOPE, OAUTH_ACCESS_TOKEN_TTL_SECONDS, OAUTH_CODE_TTL_MS, oauthConnectionKeyName, oauthRedirectOrigin, oauthSecretToken, parseAuthorizeRequest, parseOAuthFormBody, verifyPkce, } from "./oauth.js";
10
18
  function renderSkillMarkdown(baseUrl) {
11
19
  return `---
12
20
  name: muxo
13
- description: Discover, plan, and execute governed actions across Connected Apps (Composio SDK), Instant Tools (Composio Instant), Cloud Infrastructure (Stripe Projects), Contextual Memory (Supermemory v5), and Cryptographic Governance (Microsoft Agent Governance Toolkit) through Muxo.
21
+ description: Discover, plan, and execute governed actions across Connected Apps (app.*), Instant Tools (instant.*), Cloud Infrastructure (resource.*), Workspace Memory, and Safety Guardrails through Muxo.
14
22
  ---
15
23
 
16
- # Muxo — Unified Agent Execution, Infrastructure, Memory & Governance Platform
24
+ # Muxo — Unified Agent Execution, Infrastructure, Memory & Safety Platform
17
25
 
18
- Muxo provides one governed interface over 4 pillars so agents and customers only ever interact with Muxo:
26
+ Muxo provides one unified interface across 4 capabilities so agents and users only ever interact with Muxo:
19
27
 
20
- 1. **Instant Tools (\`instant.*\`)**: Zero-OAuth tools powered by Composio Instant (\`instant.web.search\`, \`instant.web.scrape\`, \`instant.hackernews.top_stories\`, \`instant.github.repo_intel\`, \`instant.dns.lookup\`).
21
- 2. **Connected Apps (\`app.*\`)**: Managed OAuth SaaS integrations powered by Composio SDK (\`app.github.*\`, \`app.gmail.*\`, \`app.slack.*\`, \`app.notion.*\`, \`app.linear.*\`, etc.).
22
- 3. **Cloud Infrastructure (\`resource.*\`)**: Database, cache, storage, and hosting provisioning powered by Stripe Projects (\`resource.neon.*\`, \`resource.supabase.*\`, \`resource.turso.*\`, \`resource.cloudflare.*\`, \`resource.vercel.*\`, etc.) with AES-256-GCM secret custody in Muxo Vault.
23
- 4. **Supermemory v5 Brain & Microsoft Agent Governance**: Every \`plan_action\` recalls isolated workspace context (\`/ns/muxo-v5-<hash>\`) and runs \`Agent ──► McpSecurityScanner ──► PolicyEngine ──► AgentIdentity (Ed25519 DID) ──► AuditLogger ──► DecisionRecord\`.
28
+ 1. **Instant Tools (\`instant.*\`)**: Zero-OAuth tools ready immediately (\`instant.web.search\`, \`instant.web.scrape\`, \`instant.web.crawl\`, \`instant.seo.audit\`, \`instant.hackernews.top_stories\`, \`instant.github.repo_intel\`, \`instant.dns.lookup\`).
29
+ 2. **Connected Apps (\`app.*\`)**: Managed OAuth SaaS integrations (\`app.github.*\`, \`app.gmail.*\`, \`app.slack.*\`, \`app.notion.*\`, \`app.linear.*\`, etc.).
30
+ 3. **Cloud Infrastructure (\`resource.*\`)**: Database, cache, storage, and hosting provisioning (\`resource.neon.*\`, \`resource.supabase.*\`, \`resource.turso.*\`, \`resource.cloudflare.*\`, \`resource.vercel.*\`, etc.) with AES-256-GCM secret custody in Muxo Vault.
31
+ 4. **Workspace Memory & Safety Guardrails**: Every \`plan_action\` recalls isolated workspace context (\`/ns/muxo-v5-<hash>\`), scans inputs for prompt injection, enforces risk-lane approval policies, and records a verifiable audit log.
24
32
 
25
- ## Installable Muxo Plugin (OpenAI Codex, ChatGPT, Claude Code & Factory Droid)
33
+ ## Installable Muxo Plugin (Claude Code, OpenAI Codex, Claude Desktop & ChatGPT)
34
+
35
+ > **Requirement:** \`@muxoai/cli\` requires **Node.js 22+** (\`node >= 22.0.0\`).
26
36
 
27
37
  \`\`\`bash
28
- # Install Muxo CLI & sign in (or auto-provision an isolated external workspace)
29
- npm install -g muxo-platform
38
+ # Install Muxo CLI & sign in against the hosted workspace (Node.js 22+)
39
+ npm install -g @muxoai/cli@latest
30
40
  muxo login --signup --name "my-agent"
31
41
 
42
+ # Claude Code marketplace (muxo@muxo):
43
+ claude plugin marketplace add ${baseUrl}/.claude-plugin/marketplace.json
44
+ claude plugin install muxo@muxo
45
+
46
+ # OpenAI Codex: add a Git repo or local marketplace directory containing .agents/plugins/marketplace.json,
47
+ # or configure [mcp_servers.muxo] in ~/.codex/config.toml (url = "${baseUrl}/mcp")
48
+
49
+ # Claude Desktop:
50
+ # - Hosted MCP: Settings -> Connectors -> Add custom connector -> ${baseUrl}/mcp
51
+ # - Local stdio server: configure npx -y @muxoai/cli@latest mcp in claude_desktop_config.json
52
+
53
+ # ChatGPT:
54
+ # - Plugins -> + -> Add custom MCP server -> ${baseUrl}/mcp (OAuth supports CIMD or DCR)
55
+
32
56
  # Link OAuth apps with live browser handoff + polling recovery
33
57
  muxo link github --wait
34
58
 
35
59
  # Plugin manifests served by this instance:
36
- # - ${baseUrl}/.agents/plugins/marketplace.json (OpenAI Codex / ChatGPT)
37
- # - ${baseUrl}/.claude-plugin/marketplace.json (Claude Code / Factory Droid)
60
+ # - ${baseUrl}/.agents/plugins/marketplace.json (OpenAI Codex local/Git marketplace manifest)
61
+ # - ${baseUrl}/.claude-plugin/marketplace.json (Claude Code marketplace manifest)
38
62
  # - ${baseUrl}/api/v1/plugin
39
63
  \`\`\`
40
64
 
41
- ## Autonomous Agent 2-Call Signup (Zero Browser Required)
65
+ ## CLI-First Autonomous Agent 2-Call Signup
42
66
 
43
67
  \`\`\`bash
44
68
  # 1. Create an autonomous workspace + mk_live_* API key
@@ -71,9 +95,45 @@ ${MUXO_MCP_TOOLS.map((t, i) => `${i + 1}. \`${t.name}\` — ${t.description}`).j
71
95
  1. **Discover**: Call \`search_tools\` with a keyword or lane filter.
72
96
  2. **Inspect**: Call \`inspect_tool\` to read the tool's \`inputSchema\`, \`riskLevel\`, and \`creditCost\`.
73
97
  3. **Plan**: Call \`plan_action({ tool_slug, input, reason })\` to receive a \`plan_*\` ID.
74
- - If the tool is an unconnected \`app.<provider>.*\` action, \`plan_action\` automatically attaches \`authHandoff.redirectUrl\`. Show that URL to the user, then call \`wait_for_connection({ provider })\` until \`status === "active"\`.
98
+ - If the tool is an unconnected \`app.<provider>.*\` action, \`plan_action\` automatically attaches \`authHandoff.redirectUrl\` and a structured \`humanHandoff\` block. Show \`humanHandoff.url\` to the user, then call \`wait_for_connection({ provider })\` until \`status === "active"\`.
99
+ - If the plan's \`approvalStatus\` is \`pending_approval\`, \`plan_action\` attaches a \`humanHandoff\` of kind \`approval\` whose \`url\` (\`${baseUrl}/approve/<plan_id>\`) opens a one-click **Review & approve** page for the workspace owner. Send that link to the user and stop until they confirm.
100
+ - Always follow \`llmGuidance.nextToolCalls\` for the exact next tool call sequence.
75
101
  4. **Execute**: Call the matching risk lane tool (\`execute_read\`, \`execute_write\`, \`execute_admin\`, \`execute_financial\`, \`execute_provisioning\`, or \`execute_destructive\`) with \`{ plan_id }\`.
76
102
  5. **Verify Receipt**: Every run issues an HMAC-SHA256 chained cryptographic receipt (\`rcpt_*\`) verifiable via \`get_receipt\`.
103
+
104
+ ## Plain-English & Conditional Guardrails (LLM & MCP Harness Native)
105
+
106
+ Users and LLM harnesses can set safety rules in natural language or structured parameter rules; Muxo compiles and enforces them across all tool calls:
107
+
108
+ - "Only allow sending emails to @muxo.ai" → conditional recipient domain guardrail (\`must_end_with: @muxo.ai\`)
109
+ - "Require approval for Stripe refunds over $50, auto-approve under $50" → conditional threshold guardrail (\`max_number: 50\`, \`unit: "usd"\`, \`action: "require_approval"\`, \`elseAction: "auto_approve"\`)
110
+ - "Only post Slack messages to #ops-alerts or #deployments" → channel allowlist guardrail
111
+ - "Never spend more than 25 credits per run" → workspace per-run credit cap
112
+
113
+ Set or inspect them via MCP \`get_run_context({ action: "set_guardrail", prompt: "...", parameter_rules: [...] })\`, dry-run simulate a tool call before planning via \`get_run_context({ action: "check_policy", tool_slug: "...", input: { ... } })\`, reset guardrails via \`get_run_context({ action: "reset_guardrails" })\`, toggle the emergency kill switch via \`get_run_context({ action: "kill_switch", enabled: true, reason: "..." })\`, or scan untrusted inputs for OWASP ASI-06 prompt injection via \`get_run_context({ action: "scan_context", prompt: "..." })\`. Equivalent REST routes: \`POST /api/v1/do\`, \`GET/POST /api/v1/guardrails\`, \`POST /api/v1/guardrails/check\`, and \`POST /api/v1/guardrails/reset\`. Responses always include \`plainEnglishGuardrails\` and a structured \`llmPolicyBrief\` (\`riskLaneMatrix\`, \`parameterRules[].howToComply\`, \`llmSystemDirective\`, and \`agentControlCheatsheet\`).
114
+
115
+ ## MCP Resources & Prompts for AI Harnesses
116
+
117
+ - **MCP Resources (\`resources/list\` & \`resources/read\`)**:
118
+ - \`muxo://policy/brief\` — Structured \`llmPolicyBrief\` (risk-lane approval matrix, parameter compliance rules, learned defaults, and agent control cheatsheet)
119
+ - \`muxo://governance/policy\` — Active guardrails, kill-switch status, trust score, and \`llmPolicyBrief\`
120
+ - \`muxo://context\` — Live workspace context, active guardrails, provisioned resources, and \`llmSystemPrompt\`
121
+ - \`muxo://memory/graph\` — Active Supermemory defaults and bi-temporal knowledge graph facts
122
+ - **MCP Prompts (\`prompts/list\` & \`prompts/get\`)**:
123
+ - \`context\` — Inject workspace memory, active defaults, and guardrail summary
124
+ - \`policy_guardrails\` — Inject the active \`llmSystemDirective\`, risk-lane matrix, and optional dry-run policy simulation for \`tool_slug\`
125
+ - \`governed_execution\` — Step-by-step Plan → Approve → Execute → Receipt system instructions
126
+ - \`security_audit\` — OWASP Agentic Top 10 security and receipt audit workflow
127
+
128
+ ## Active Supermemory (teach Muxo once, auto-fill forever)
129
+
130
+ Teach preferences in plain English and Muxo auto-fills matching tool inputs on every future plan:
131
+
132
+ - "Hey Muxo, remember our default GitHub repo is acme/api and default region is us-east-1"
133
+ - "Hey Muxo, forget our staging redis"
134
+ - "What do you remember?"
135
+
136
+ \`plan_action\` recalls workspace memory before governance checks and reports \`memoryContext.autoFilledInput\` plus \`activeDefaults\` so callers can see exactly which parameters were filled from memory.
77
137
  `;
78
138
  }
79
139
  async function readBody(req) {
@@ -88,8 +148,8 @@ function sendJson(res, status, payload, extraHeaders) {
88
148
  res.writeHead(status, {
89
149
  "Content-Type": "application/json; charset=utf-8",
90
150
  "Access-Control-Allow-Origin": "*",
91
- "Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id",
92
- "Access-Control-Expose-Headers": "Mcp-Session-Id, WWW-Authenticate",
151
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Muxo-Workspace-Id, Mcp-Session-Id, mcp-session-id, MCP-Protocol-Version, mcp-protocol-version, Mcp-Method, mcp-method, Mcp-Name, mcp-name",
152
+ "Access-Control-Expose-Headers": "Mcp-Session-Id, MCP-Protocol-Version, WWW-Authenticate",
93
153
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
94
154
  ...(extraHeaders || {}),
95
155
  });
@@ -117,17 +177,68 @@ export function createMuxoServer(customConfig) {
117
177
  client_id_issued_at: Math.floor(Date.now() / 1000),
118
178
  });
119
179
  const oauthCodes = new Map();
120
- const resolveContext = (req) => {
121
- const authHeader = String(req.headers.authorization || "").trim();
122
- if (authHeader.toLowerCase().startsWith("bearer ")) {
123
- const token = authHeader.slice(7).trim();
124
- const resolved = store.resolveAuthToken(token);
125
- if (resolved)
126
- return resolved;
180
+ const clerkEnv = config.clerkEnv ?? {};
181
+ const clerkConfig = () => readClerkConfig(clerkEnv, config.baseUrl);
182
+ const ownerEmails = () => String(clerkEnv.MUXO_OWNER_EMAILS || "")
183
+ .split(",")
184
+ .map((s) => s.trim().toLowerCase())
185
+ .filter(Boolean);
186
+ const bearerTokenOf = (req) => {
187
+ const header = String(req.headers.authorization || "").trim();
188
+ return /^bearer\s+/i.test(header) ? header.replace(/^bearer\s+/i, "").trim() : null;
189
+ };
190
+ /**
191
+ * Resolves the caller from a workspace API key, a console session token, or a Clerk session
192
+ * JWT. A bearer token that resolves to nothing is "invalid" and must never fall back to a
193
+ * default workspace.
194
+ */
195
+ const authenticate = async (req) => {
196
+ const token = bearerTokenOf(req);
197
+ if (token === null)
198
+ return { kind: "anonymous" };
199
+ if (!token)
200
+ return { kind: "invalid" };
201
+ const wsOverride = String(req.headers["x-muxo-workspace-id"] || "").trim() || null;
202
+ const resolved = store.resolveAuthToken(token, wsOverride);
203
+ if (resolved)
204
+ return { kind: "ok", ctx: resolved, via: resolved.authType };
205
+ const clerk = looksLikeJwt(token) ? clerkConfig() : null;
206
+ if (!clerk)
207
+ return { kind: "invalid" };
208
+ const session = await verifyClerkSessionToken(token, clerk);
209
+ if (!session)
210
+ return { kind: "invalid" };
211
+ let user = store.getClerkLinkedUser(session.userId);
212
+ if (!user) {
213
+ const profile = clerk.secretKey ? await fetchClerkProfile(session.userId, clerk.secretKey) : null;
214
+ const identity = {
215
+ email: profile?.email || session.email || null,
216
+ emailVerified: profile ? profile.emailVerified : Boolean(session.emailVerified),
217
+ name: profile?.name || null,
218
+ };
219
+ const joinsPrimary = Boolean(identity.email && identity.emailVerified && ownerEmails().includes(identity.email.toLowerCase()));
220
+ user = store.provisionClerkUser(session.userId, identity, joinsPrimary ? bootstrap.workspace.id : null);
127
221
  }
128
- // Default local console session fallback to the primary workspace
129
- const ws = store.getWorkspace(bootstrap.workspace.id);
130
- const user = store.getUser(ws.ownerUserId);
222
+ const targetWsId = wsOverride && store.userCanAccessWorkspace(user.id, wsOverride) ? wsOverride : user.workspaceId;
223
+ const workspace = store.getWorkspace(targetWsId) || store.getWorkspace(user.workspaceId);
224
+ if (!workspace)
225
+ return { kind: "invalid" };
226
+ return {
227
+ kind: "ok",
228
+ ctx: { user: { ...user, workspaceId: workspace.id }, workspace, actorId: user.id, authType: "session" },
229
+ via: "clerk",
230
+ clerkUserId: session.userId,
231
+ clerkSessionEmail: session.email && session.emailVerified ? session.email : null,
232
+ };
233
+ };
234
+ /** Without Clerk, a local server serves anonymous callers from its primary workspace. */
235
+ const localFallbackContext = (req) => {
236
+ const wsOverride = String(req.headers["x-muxo-workspace-id"] || "").trim() || null;
237
+ const fallbackWsId = wsOverride && store.userCanAccessWorkspace(bootstrap.user.id, wsOverride)
238
+ ? wsOverride
239
+ : bootstrap.user.workspaceId || bootstrap.workspace.id;
240
+ const ws = store.getWorkspace(fallbackWsId) || store.getWorkspace(bootstrap.workspace.id);
241
+ const user = store.getUser(ws.ownerUserId) || bootstrap.user;
131
242
  return {
132
243
  user,
133
244
  workspace: ws,
@@ -135,11 +246,96 @@ export function createMuxoServer(customConfig) {
135
246
  authType: "session",
136
247
  };
137
248
  };
249
+ const anonymousContext = () => {
250
+ const epoch = new Date(0).toISOString();
251
+ return {
252
+ user: { id: "usr_anonymous", email: "", name: "Anonymous", workspaceId: "ws_public", role: "member", createdAt: epoch },
253
+ workspace: {
254
+ id: "ws_public",
255
+ name: "Public catalog",
256
+ slug: "public",
257
+ ownerUserId: "usr_anonymous",
258
+ tier: "free",
259
+ monthlyActionsLimit: 0,
260
+ monthlyActionsUsed: 0,
261
+ creditsBalance: 0,
262
+ stripeCustomerId: null,
263
+ stripeSubscriptionId: null,
264
+ projectsEnvName: "muxo-public",
265
+ policy: { ...DEFAULT_WORKSPACE_POLICY },
266
+ createdAt: epoch,
267
+ updatedAt: epoch,
268
+ },
269
+ actorId: "usr_anonymous",
270
+ authType: "session",
271
+ };
272
+ };
273
+ const requestOriginOf = (req) => {
274
+ const proto = String(req.headers["x-forwarded-proto"] || "http").split(",")[0].trim();
275
+ return req.headers.host ? `${proto}://${req.headers.host}` : config.baseUrl;
276
+ };
277
+ const sendAuthRequired = (req, res, pathname, message) => {
278
+ const origin = requestOriginOf(req);
279
+ const resourceMetadata = pathname === "/mcp"
280
+ ? `${origin}/.well-known/oauth-protected-resource/mcp`
281
+ : `${origin}/.well-known/oauth-protected-resource`;
282
+ sendJson(res, 401, {
283
+ ok: false,
284
+ code: "auth_required",
285
+ error: message ||
286
+ `Sign in required. Send a Muxo API key (Authorization: Bearer mk_live_...) or sign in at ${origin}/login.`,
287
+ }, { "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadata}"` });
288
+ };
289
+ /** Emails the signed-in person has verified; agent API keys never count as a person. */
290
+ const verifiedEmailsOf = async (auth) => {
291
+ if (auth.kind !== "ok")
292
+ return [];
293
+ if (auth.via === "session")
294
+ return auth.ctx.user.email ? [auth.ctx.user.email.toLowerCase()] : [];
295
+ if (auth.via !== "clerk")
296
+ return [];
297
+ const secretKey = clerkConfig()?.secretKey;
298
+ if (secretKey && auth.clerkUserId) {
299
+ const profile = await fetchClerkProfile(auth.clerkUserId, secretKey);
300
+ if (profile)
301
+ return profile.verifiedEmails;
302
+ }
303
+ return auth.clerkSessionEmail ? [auth.clerkSessionEmail.toLowerCase()] : [];
304
+ };
305
+ const signedInPerson = (auth) => auth.kind === "ok" && auth.via !== "api_key";
306
+ /**
307
+ * Resolves the caller of a protected route, or answers 401 and returns null. Public routes
308
+ * accept anonymous callers with an empty catalog context once Clerk sign-in is configured.
309
+ */
310
+ const requireContext = async (req, res, pathname, publicRoute = false) => {
311
+ const auth = await authenticate(req);
312
+ if (auth.kind === "ok")
313
+ return { ctx: auth.ctx, auth };
314
+ if (auth.kind === "invalid") {
315
+ sendAuthRequired(req, res, pathname);
316
+ return null;
317
+ }
318
+ if (clerkConfig()) {
319
+ if (publicRoute)
320
+ return { ctx: anonymousContext(), auth };
321
+ sendAuthRequired(req, res, pathname);
322
+ return null;
323
+ }
324
+ return { ctx: localFallbackContext(req), auth };
325
+ };
326
+ const consentPageHeaders = {
327
+ "Content-Type": "text/html; charset=utf-8",
328
+ "Cache-Control": "no-store",
329
+ "Content-Security-Policy": "frame-ancestors 'none'",
330
+ "X-Frame-Options": "DENY",
331
+ "Referrer-Policy": "no-referrer",
332
+ };
138
333
  const server = http.createServer(async (req, res) => {
139
334
  if (req.method === "OPTIONS") {
140
335
  res.writeHead(204, {
141
336
  "Access-Control-Allow-Origin": "*",
142
- "Access-Control-Allow-Headers": "Content-Type, Authorization",
337
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Muxo-Workspace-Id, Mcp-Session-Id, mcp-session-id, MCP-Protocol-Version, mcp-protocol-version, Mcp-Method, mcp-method, Mcp-Name, mcp-name",
338
+ "Access-Control-Expose-Headers": "Mcp-Session-Id, MCP-Protocol-Version, WWW-Authenticate",
143
339
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
144
340
  });
145
341
  res.end();
@@ -154,7 +350,7 @@ export function createMuxoServer(customConfig) {
154
350
  ok: true,
155
351
  status: "ready",
156
352
  service: "muxo-platform",
157
- version: "1.3.0",
353
+ version: MUXO_VERSION,
158
354
  environment: "production",
159
355
  live_execution: true,
160
356
  customer_billing: true,
@@ -184,6 +380,20 @@ export function createMuxoServer(customConfig) {
184
380
  });
185
381
  return;
186
382
  }
383
+ if (pathname === "/.well-known/jwks.json" && req.method === "GET") {
384
+ sendJson(res, 200, {
385
+ keys: [
386
+ {
387
+ kty: "OKP",
388
+ crv: "Ed25519",
389
+ use: "sig",
390
+ alg: "EdDSA",
391
+ kid: "muxo-governance-ed25519-v1",
392
+ },
393
+ ],
394
+ });
395
+ return;
396
+ }
187
397
  if ((pathname === "/.well-known/oauth-protected-resource" ||
188
398
  pathname === "/.well-known/oauth-protected-resource/mcp") &&
189
399
  req.method === "GET") {
@@ -198,7 +408,7 @@ export function createMuxoServer(customConfig) {
198
408
  resource,
199
409
  authorization_servers: [requestOrigin],
200
410
  bearer_methods_supported: ["header"],
201
- scopes_supported: ["muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
411
+ scopes_supported: ["mcp:tools", "muxo:mcp", "muxo:read", "muxo:execute", "muxo:provision"],
202
412
  resource_documentation: `${config.docsUrl}/quickstart`,
203
413
  mcp_endpoint: `${requestOrigin}/mcp`,
204
414
  });
@@ -220,6 +430,7 @@ export function createMuxoServer(customConfig) {
220
430
  userinfo_endpoint: `${requestOrigin}/oauth/userinfo`,
221
431
  jwks_uri: `${requestOrigin}/.well-known/jwks.json`,
222
432
  scopes_supported: [
433
+ "mcp:tools",
223
434
  "muxo:mcp",
224
435
  "muxo:read",
225
436
  "muxo:execute",
@@ -229,11 +440,7 @@ export function createMuxoServer(customConfig) {
229
440
  "email",
230
441
  ],
231
442
  response_types_supported: ["code"],
232
- grant_types_supported: [
233
- "authorization_code",
234
- "refresh_token",
235
- "client_credentials",
236
- ],
443
+ grant_types_supported: ["authorization_code", "refresh_token"],
237
444
  token_endpoint_auth_methods_supported: [
238
445
  "none",
239
446
  "client_secret_post",
@@ -241,10 +448,21 @@ export function createMuxoServer(customConfig) {
241
448
  ],
242
449
  code_challenge_methods_supported: ["S256", "plain"],
243
450
  client_id_metadata_document_supported: true,
451
+ authorization_response_iss_parameter_supported: true,
244
452
  service_documentation: `${config.docsUrl}/quickstart`,
245
453
  });
246
454
  return;
247
455
  }
456
+ if (pathname === "/.well-known/openai-apps-challenge" && req.method === "GET") {
457
+ const challenge = process.env.OPENAI_APPS_CHALLENGE ||
458
+ "muxo-openai-apps-domain-verified";
459
+ res.writeHead(200, {
460
+ "Content-Type": "text/plain; charset=utf-8",
461
+ "Access-Control-Allow-Origin": "*",
462
+ });
463
+ res.end(challenge);
464
+ return;
465
+ }
248
466
  if (pathname === "/oauth/register" && req.method === "POST") {
249
467
  const body = JSON.parse((await readBody(req)) || "{}");
250
468
  const clientId = typeof body.client_id === "string" && body.client_id.trim()
@@ -272,148 +490,195 @@ export function createMuxoServer(customConfig) {
272
490
  sendJson(res, 201, clientRecord);
273
491
  return;
274
492
  }
275
- if (pathname === "/oauth/authorize" &&
276
- (req.method === "GET" || req.method === "POST")) {
277
- const bodyParams = req.method === "POST"
278
- ? new URLSearchParams(await readBody(req))
279
- : url.searchParams;
280
- const clientId = bodyParams.get("client_id") ||
281
- url.searchParams.get("client_id") ||
282
- "muxo-openai-plugin";
283
- const redirectUri = bodyParams.get("redirect_uri") ||
284
- url.searchParams.get("redirect_uri") ||
285
- "http://127.0.0.1:3118/oauth/callback";
286
- const stateParam = bodyParams.get("state") || url.searchParams.get("state") || "";
287
- const scope = bodyParams.get("scope") ||
288
- url.searchParams.get("scope") ||
289
- "muxo:mcp muxo:read muxo:execute muxo:provision";
290
- const codeChallenge = bodyParams.get("code_challenge") ||
291
- url.searchParams.get("code_challenge") ||
292
- undefined;
293
- const codeChallengeMethod = bodyParams.get("code_challenge_method") ||
294
- url.searchParams.get("code_challenge_method") ||
295
- "S256";
296
- const code = `muxo_auth_code_${crypto.randomBytes(16).toString("hex")}`;
297
- const issuedKey = bootstrap.defaultApiKey ||
298
- store.createApiKey({
299
- workspaceId: bootstrap.workspace.id,
300
- name: `OAuth Client (${clientId})`,
301
- environment: "live",
302
- }).secretKey;
303
- oauthCodes.set(code, {
304
- code,
305
- clientId,
306
- redirectUri,
307
- scope,
308
- codeChallenge,
309
- codeChallengeMethod,
310
- workspaceId: bootstrap.workspace.id,
311
- apiKey: issuedKey,
312
- expiresAt: Date.now() + 10 * 60 * 1000,
313
- });
314
- const redirectTarget = new URL(redirectUri);
315
- redirectTarget.searchParams.set("code", code);
316
- if (stateParam)
317
- redirectTarget.searchParams.set("state", stateParam);
318
- if (url.searchParams.get("format") === "json" ||
319
- String(req.headers.accept || "").includes("application/json")) {
320
- sendJson(res, 200, {
321
- ok: true,
322
- code,
323
- state: stateParam || null,
324
- client_id: clientId,
325
- redirect_uri: redirectTarget.toString(),
493
+ if (pathname === "/oauth/authorize" && req.method === "GET") {
494
+ const parsed = parseAuthorizeRequest(url.searchParams);
495
+ const wantsJson = url.searchParams.get("format") === "json" ||
496
+ String(req.headers.accept || "").includes("application/json");
497
+ if ("error" in parsed) {
498
+ if (wantsJson) {
499
+ sendJson(res, 400, { error: "invalid_request", error_description: parsed.error }, { "Cache-Control": "no-store" });
500
+ }
501
+ else {
502
+ res.writeHead(400, consentPageHeaders);
503
+ res.end(renderOAuthConsentHtml({ error: parsed.error }));
504
+ }
505
+ return;
506
+ }
507
+ if (wantsJson) {
508
+ sendJson(res, 401, {
509
+ ok: false,
510
+ code: "interactive_sign_in_required",
511
+ error: "Open this authorization URL in a browser to sign in and approve access.",
512
+ authorizeUrl: url.toString(),
326
513
  });
327
514
  return;
328
515
  }
329
- res.writeHead(302, {
330
- Location: redirectTarget.toString(),
331
- "Access-Control-Allow-Origin": "*",
332
- });
333
- res.end();
516
+ const clerk = clerkConfig();
517
+ res.writeHead(200, consentPageHeaders);
518
+ res.end(renderOAuthConsentHtml({
519
+ clientName: parsed.clientName,
520
+ redirectOrigin: oauthRedirectOrigin(parsed.redirectUri),
521
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
522
+ request: {
523
+ client_id: parsed.clientId,
524
+ redirect_uri: parsed.redirectUri,
525
+ state: parsed.state,
526
+ code_challenge: parsed.codeChallenge || "",
527
+ code_challenge_method: parsed.codeChallengeMethod || "",
528
+ scope: parsed.scope,
529
+ },
530
+ }));
334
531
  return;
335
532
  }
336
- if (pathname === "/oauth/token" && req.method === "POST") {
337
- const rawBody = await readBody(req);
533
+ if (pathname === "/oauth/authorize" && req.method === "POST") {
534
+ const auth = await authenticate(req);
535
+ // A person must approve; agent API keys cannot grant themselves new access.
536
+ if (!signedInPerson(auth)) {
537
+ sendJson(res, 401, { ok: false, code: "auth_required", error: "Sign in to approve this connection." });
538
+ return;
539
+ }
540
+ const raw = await readBody(req);
338
541
  const contentType = String(req.headers["content-type"] || "");
339
- const params = {};
340
- if (contentType.includes("application/json")) {
341
- const parsed = JSON.parse(rawBody || "{}");
342
- for (const [k, v] of Object.entries(parsed)) {
343
- if (v !== undefined && v !== null)
344
- params[k] = String(v);
345
- }
542
+ const params = contentType.includes("application/json")
543
+ ? parseOAuthFormBody(raw, "application/json", "")
544
+ : new URLSearchParams(raw);
545
+ const parsed = parseAuthorizeRequest(params);
546
+ if ("error" in parsed) {
547
+ sendJson(res, 400, { ok: false, error: parsed.error });
548
+ return;
346
549
  }
347
- else {
348
- const sp = new URLSearchParams(rawBody);
349
- for (const [k, v] of sp.entries()) {
350
- params[k] = v;
351
- }
550
+ const redirectTarget = new URL(parsed.redirectUri);
551
+ if (parsed.state)
552
+ redirectTarget.searchParams.set("state", parsed.state);
553
+ redirectTarget.searchParams.set("iss", requestOriginOf(req));
554
+ if (params.get("decision") === "deny") {
555
+ redirectTarget.searchParams.set("error", "access_denied");
556
+ sendJson(res, 200, { ok: true, redirect: redirectTarget.toString() });
557
+ return;
352
558
  }
353
- const grantType = params.grant_type || "authorization_code";
354
- const code = params.code || "";
355
- const codeVerifier = params.code_verifier || "";
356
- let accessToken = bootstrap.defaultApiKey || "";
357
- let scope = params.scope || "muxo:mcp muxo:read muxo:execute muxo:provision";
358
- if (grantType === "authorization_code" && code) {
359
- const record = oauthCodes.get(code);
360
- if (!record || record.expiresAt < Date.now()) {
361
- sendJson(res, 400, {
362
- error: "invalid_grant",
363
- error_description: "Authorization code is invalid or expired.",
364
- });
365
- return;
559
+ const now = Date.now();
560
+ for (const [c, rec] of oauthCodes)
561
+ if (rec.expiresAt < now)
562
+ oauthCodes.delete(c);
563
+ const code = oauthSecretToken("muxo_code");
564
+ oauthCodes.set(code, {
565
+ userId: auth.ctx.user.id,
566
+ workspaceId: auth.ctx.workspace.id,
567
+ clientId: parsed.clientId,
568
+ clientName: parsed.clientName,
569
+ redirectUri: parsed.redirectUri,
570
+ scope: parsed.scope,
571
+ codeChallenge: parsed.codeChallenge,
572
+ codeChallengeMethod: parsed.codeChallengeMethod,
573
+ expiresAt: now + OAUTH_CODE_TTL_MS,
574
+ });
575
+ redirectTarget.searchParams.set("code", code);
576
+ sendJson(res, 200, { ok: true, redirect: redirectTarget.toString(), workspaceId: auth.ctx.workspace.id });
577
+ return;
578
+ }
579
+ if (pathname === "/oauth/token" && req.method === "POST") {
580
+ const form = parseOAuthFormBody(await readBody(req), String(req.headers["content-type"] || ""), String(req.headers.authorization || ""));
581
+ const grantType = form.get("grant_type") || "";
582
+ const tokenHeaders = { "Cache-Control": "no-store", Pragma: "no-cache" };
583
+ const oauthError = (error, description) => sendJson(res, 400, { error, error_description: description }, tokenHeaders);
584
+ if (grantType === "authorization_code") {
585
+ const code = form.get("code") || "";
586
+ const rec = oauthCodes.get(code);
587
+ if (!rec)
588
+ return oauthError("invalid_grant", "Authorization code is invalid or was already used.");
589
+ oauthCodes.delete(code);
590
+ if (rec.expiresAt < Date.now())
591
+ return oauthError("invalid_grant", "Authorization code expired.");
592
+ const clientId = form.get("client_id");
593
+ if (clientId && clientId !== rec.clientId)
594
+ return oauthError("invalid_grant", "client_id does not match.");
595
+ const redirectUri = form.get("redirect_uri");
596
+ if (redirectUri && redirectUri !== rec.redirectUri) {
597
+ return oauthError("invalid_grant", "redirect_uri does not match.");
366
598
  }
367
- if (record.codeChallenge) {
368
- if (!codeVerifier) {
369
- sendJson(res, 400, {
370
- error: "invalid_grant",
371
- error_description: "PKCE code_verifier is required.",
372
- });
373
- return;
374
- }
375
- const method = (record.codeChallengeMethod || "S256").toUpperCase();
376
- const computed = method === "S256"
377
- ? crypto
378
- .createHash("sha256")
379
- .update(codeVerifier)
380
- .digest("base64url")
381
- : codeVerifier;
382
- if (computed !== record.codeChallenge) {
383
- sendJson(res, 400, {
384
- error: "invalid_grant",
385
- error_description: "PKCE code_verifier mismatch.",
386
- });
387
- return;
388
- }
599
+ if (rec.codeChallenge) {
600
+ const check = await verifyPkce(form.get("code_verifier") || "", rec.codeChallenge, rec.codeChallengeMethod);
601
+ if (check === "missing")
602
+ return oauthError("invalid_grant", "code_verifier is required.");
603
+ if (check === "mismatch")
604
+ return oauthError("invalid_grant", "code_verifier does not match.");
389
605
  }
390
- accessToken = record.apiKey;
391
- scope = record.scope;
392
- oauthCodes.delete(code);
393
- }
394
- else if (!accessToken) {
395
- accessToken = store.createApiKey({
396
- workspaceId: bootstrap.workspace.id,
397
- name: "OAuth Token Key",
606
+ const ws = store.getWorkspace(rec.workspaceId);
607
+ if (!ws || !store.userCanAccessWorkspace(rec.userId, ws.id)) {
608
+ return oauthError("invalid_grant", "Workspace is no longer available.");
609
+ }
610
+ const minted = store.createApiKey({
611
+ workspaceId: ws.id,
612
+ name: oauthConnectionKeyName(rec.clientName),
398
613
  environment: "live",
399
- }).secretKey;
614
+ });
615
+ const refreshToken = oauthSecretToken("muxo_rt");
616
+ store.saveOAuthRefreshToken({
617
+ refreshToken,
618
+ accessToken: minted.secretKey,
619
+ keyId: minted.record.id,
620
+ workspaceId: ws.id,
621
+ clientId: rec.clientId,
622
+ scope: rec.scope,
623
+ });
624
+ sendJson(res, 200, {
625
+ access_token: minted.secretKey,
626
+ token_type: "Bearer",
627
+ expires_in: OAUTH_ACCESS_TOKEN_TTL_SECONDS,
628
+ refresh_token: refreshToken,
629
+ scope: rec.scope,
630
+ }, tokenHeaders);
631
+ return;
400
632
  }
401
- sendJson(res, 200, {
402
- access_token: accessToken,
403
- token_type: "Bearer",
404
- expires_in: 3600,
405
- refresh_token: `muxo_rt_${crypto.randomBytes(16).toString("hex")}`,
406
- scope,
407
- });
408
- return;
633
+ if (grantType === "refresh_token") {
634
+ const refreshToken = form.get("refresh_token") || "";
635
+ const rec = refreshToken ? store.getOAuthRefreshToken(refreshToken) : null;
636
+ const active = rec ? store.findActiveApiKey(rec.accessToken) : null;
637
+ if (!rec || !active || active.id !== rec.keyId) {
638
+ if (rec)
639
+ store.deleteOAuthRefreshToken(refreshToken);
640
+ return oauthError("invalid_grant", "Refresh token is invalid or its access was revoked.");
641
+ }
642
+ const clientId = form.get("client_id");
643
+ if (clientId && clientId !== rec.clientId)
644
+ return oauthError("invalid_grant", "client_id does not match.");
645
+ // Refresh tokens rotate on every use; the replaced token stops working immediately.
646
+ store.deleteOAuthRefreshToken(refreshToken);
647
+ const rotated = oauthSecretToken("muxo_rt");
648
+ store.saveOAuthRefreshToken({
649
+ refreshToken: rotated,
650
+ accessToken: rec.accessToken,
651
+ keyId: rec.keyId,
652
+ workspaceId: rec.workspaceId,
653
+ clientId: rec.clientId,
654
+ scope: rec.scope,
655
+ });
656
+ sendJson(res, 200, {
657
+ access_token: rec.accessToken,
658
+ token_type: "Bearer",
659
+ expires_in: OAUTH_ACCESS_TOKEN_TTL_SECONDS,
660
+ refresh_token: rotated,
661
+ scope: rec.scope || DEFAULT_OAUTH_SCOPE,
662
+ }, tokenHeaders);
663
+ return;
664
+ }
665
+ return oauthError("unsupported_grant_type", "Use grant_type=authorization_code (with PKCE) or refresh_token.");
409
666
  }
410
667
  if (pathname === "/oauth/userinfo" && req.method === "GET") {
411
- const ctx = resolveContext(req);
668
+ const resolved = await requireContext(req, res, pathname);
669
+ if (!resolved)
670
+ return;
671
+ const { ctx } = resolved;
672
+ const nickname = (ctx.user.email || "").split("@")[0] ||
673
+ ctx.user.name.toLowerCase().replace(/\s+/g, ".") ||
674
+ ctx.user.id;
412
675
  sendJson(res, 200, {
413
676
  sub: ctx.user.id,
414
677
  id: ctx.user.id,
415
678
  name: ctx.user.name,
416
679
  email: ctx.user.email,
680
+ email_verified: Boolean(ctx.user.email),
681
+ nickname,
417
682
  workspace_id: ctx.workspace.id,
418
683
  workspace_name: ctx.workspace.name,
419
684
  tier: ctx.workspace.tier,
@@ -426,8 +691,8 @@ export function createMuxoServer(customConfig) {
426
691
  openapi: "3.1.0",
427
692
  info: {
428
693
  title: "Muxo Unified Agent Execution, Infrastructure, Memory & Governance API",
429
- version: "1.3.0",
430
- description: "One governed boundary over Connected Apps (Composio SDK), Instant Tools (Composio Instant), Cloud Infrastructure (Stripe Projects), Supermemory v5 Brain, and Microsoft Agent Governance Toolkit.",
694
+ version: MUXO_VERSION,
695
+ description: "One governed boundary over Connected Apps, Instant Tools, Cloud Infrastructure, Workspace Memory, and Safety & Approvals.",
431
696
  },
432
697
  servers: [{ url: config.baseUrl }],
433
698
  paths: {
@@ -436,6 +701,12 @@ export function createMuxoServer(customConfig) {
436
701
  "/skill.md": { get: { summary: "Canonical agent skill & onboarding markdown" } },
437
702
  "/api/v1/agents/signup": { post: { summary: "Autonomous 2-call agent workspace & mk_live_* key signup" } },
438
703
  "/api/v1/agents/claim": { post: { summary: "Generate human owner claim link for an agent workspace" } },
704
+ "/api/v1/do": { post: { summary: "Natural-language task routing, planning, and optional execution" } },
705
+ "/api/v1/byok": {
706
+ get: { summary: "List configured BYOK provider keys in the AES-256-GCM vault" },
707
+ post: { summary: "Store or rotate a BYOK provider key (0 credit cost when active)" },
708
+ },
709
+ "/api/v1/byok/{provider}": { delete: { summary: "Delete a BYOK provider key from the vault" } },
439
710
  "/api/v1/tools": { get: { summary: "Search unified catalogue across instant.*, app.*, and resource.*" } },
440
711
  "/api/v1/tools/{slug}": { get: { summary: "Inspect tool schema, risk level, credit cost, and LLM context URL" } },
441
712
  "/api/v1/connections": {
@@ -445,19 +716,68 @@ export function createMuxoServer(customConfig) {
445
716
  "/api/v1/connections/wait": { post: { summary: "Wait/poll for browser OAuth handoff completion" } },
446
717
  "/api/v1/plans": {
447
718
  get: { summary: "List governed action plans" },
448
- post: { summary: "Create governed action plan with Supermemory recall & Ed25519 signature" },
719
+ post: { summary: "Create governed action plan with Workspace Memory recall & Ed25519 signature" },
449
720
  },
450
721
  "/api/v1/plans/{planId}/approve": { post: { summary: "Approve a pending high-risk action plan" } },
451
722
  "/api/v1/plans/{planId}/execute": { post: { summary: "Execute an approved plan and issue HMAC-SHA256 receipt" } },
452
723
  "/api/v1/resources": { get: { summary: "List provisioned cloud infrastructure resources" } },
453
724
  "/api/v1/resources/env": { get: { summary: "Pull decrypted AES-256-GCM workspace environment secrets" } },
725
+ "/api/v1/provisioning/connected_accounts": {
726
+ get: { summary: "Inspect workspace Stripe v2 Connected Account & requirements" },
727
+ post: { summary: "Create a Stripe v2 Connected Account with developer.projects capability" },
728
+ },
729
+ "/api/v1/provisioning/connected_account_links": {
730
+ post: { summary: "Create a Stripe v2 Hosted Account Link (account_onboarding / account_update)" },
731
+ },
732
+ "/api/v1/provisioning/connected_account_sessions": {
733
+ post: { summary: "Create a Stripe Connect Account Session for Accounts v2 Embedded Onboarding" },
734
+ },
735
+ "/api/v1/provisioning/eligibility": { get: { summary: "Check Stripe v2 Provisioning eligibility" } },
736
+ "/api/v1/provisioning/providers": { get: { summary: "List Stripe v2 Provisioning cloud providers" } },
737
+ "/api/v1/provisioning/services": { get: { summary: "List Stripe v2 Provisioning service catalog" } },
738
+ "/api/v1/provisioning/payment_profile": { get: { summary: "Inspect Stripe v2 Provisioning payment profile" } },
739
+ "/api/v1/provisioning/projects": {
740
+ get: { summary: "List Stripe v2 Provisioning projects" },
741
+ post: { summary: "Create a Stripe v2 Provisioning project" },
742
+ },
743
+ "/api/v1/provisioning/resources": {
744
+ get: { summary: "List Stripe v2 Provisioning resources" },
745
+ post: { summary: "Create a Stripe v2 Provisioning resource" },
746
+ },
747
+ "/api/v1/provisioning/resources/{id}/reveal_access_configuration": {
748
+ post: { summary: "Reveal decrypted credentials for a provisioned resource" },
749
+ },
454
750
  "/api/v1/memory": {
455
- get: { summary: "Recall isolated Supermemory v5 workspace brain facts" },
751
+ get: { summary: "Recall isolated Workspace Memory facts and hybrid context" },
456
752
  post: { summary: "Store a workspace memory fact or decision" },
457
753
  },
458
- "/api/v1/governance": { get: { summary: "Inspect Microsoft Agent Governance DID, policy, and Merkle audit chain" } },
459
- "/api/v1/billing/topup": { post: { summary: "Purchase Muxo credits (User pays Muxo -> Muxo pays suppliers)" } },
460
- "/api/v1/billing/settlements": { get: { summary: "Inspect Merchant-of-Record supplier settlement ledger" } },
754
+ "/api/v1/memory/facts": { get: { summary: "Query temporal Workspace Memory facts with lineage" } },
755
+ "/api/v1/memory/forget": { post: { summary: "Delete matching memories and retire matching facts" } },
756
+ "/api/v1/policy": {
757
+ get: { summary: "Inspect workspace safety & approval policy" },
758
+ post: { summary: "Update workspace safety & approval policy" },
759
+ },
760
+ "/api/v1/governance": { get: { summary: "Inspect workspace Safety & Approvals identity, policy, and audit chain" } },
761
+ "/api/v1/governance/kill-switch": { post: { summary: "Pause or resume workspace execution via kill switch" } },
762
+ "/api/v1/governance/scan-context": { post: { summary: "Scan untrusted text for prompt injection before storage or use" } },
763
+ "/api/v1/guardrails": {
764
+ get: { summary: "List active plain-English guardrails, llmPolicyBrief, policy, and learned memory defaults" },
765
+ post: { summary: "Set or reset guardrails in plain English or structured parameterRules" },
766
+ },
767
+ "/api/v1/guardrails/check": {
768
+ post: { summary: "Dry-run evaluate a tool call and arguments against active workspace policy, conditional parameter rules, and memory defaults" },
769
+ },
770
+ "/api/v1/guardrails/reset": {
771
+ post: { summary: "Reset workspace guardrails and parameter rules to Safe Default" },
772
+ },
773
+ "/approve/{planId}": { get: { summary: "One-click human approval handoff page for a pending plan" } },
774
+ "/api/v1/context": {
775
+ get: { summary: "Inspect full workspace run context with a plain-English explanation" },
776
+ post: { summary: "Inspect workspace run context with an optional query" },
777
+ },
778
+ "/api/v1/receipts": { get: { summary: "List recent tamper-proof execution receipts with plain-English explanations" } },
779
+ "/api/v1/billing/topup": { post: { summary: "Purchase Muxo credits" } },
780
+ "/api/v1/billing/settlements": { get: { summary: "Inspect workspace credit activity ledger" } },
461
781
  },
462
782
  });
463
783
  return;
@@ -475,12 +795,12 @@ Or via HTTP:
475
795
  curl -sX POST ${config.baseUrl}/api/v1/agents/signup -H "Content-Type: application/json" -d '{"agentName":"my-agent"}'
476
796
  \`\`\`
477
797
 
478
- ## 2. OAuth Connection Handoff (Composio Plugin Pattern)
798
+ ## 2. OAuth Connection Handoff
479
799
  When \`plan_action\` targets an unconnected \`app.<provider>.*\` tool, Muxo returns \`authHandoff.redirectUrl\`.
480
800
  Open the URL in a browser, complete OAuth (which redirects to \`${config.baseUrl}/oauth/callback\`), and call \`wait_for_connection({ provider })\` or \`muxo link <provider> --wait\`.
481
801
 
482
- ## 3. Merchant-of-Record Credit Settlement
483
- Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing topup 1000\`), and Muxo settles wholesale supplier costs across Composio and Stripe Projects from Muxo's master accounts (\`fundingMode: "muxo_funded"\`).
802
+ ## 3. Credit Billing & BYOK Support
803
+ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing topup 1000\`), and Muxo settles upstream execution costs from Muxo's master accounts (\`fundingMode: "muxo_funded"\`), or users can configure their own BYOK provider keys for 0-credit execution.
484
804
  `);
485
805
  return;
486
806
  }
@@ -507,10 +827,10 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
507
827
  }
508
828
  if (pathname === "/llms.txt" && req.method === "GET") {
509
829
  res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
510
- res.end(`# Muxo\n> Unified execution, cloud provisioning, Supermemory v5 brain, and Microsoft Agent Governance boundary.\n\n- Skill Document: ${config.baseUrl}/skill.md\n- Auth Guide: ${config.baseUrl}/auth.md\n- OpenAPI 3.1 Spec: ${config.baseUrl}/api/openapi.json\n- MCP Endpoint: ${config.baseUrl}/mcp\n- OAuth Callback Handoff: ${config.baseUrl}/oauth/callback\n- Plugin Marketplace (Codex/OpenAI): ${config.baseUrl}/.agents/plugins/marketplace.json\n- Plugin Marketplace (Claude/Droid): ${config.baseUrl}/.claude-plugin/marketplace.json\n- Console (app.muxo.ai): ${config.baseUrl}/\n- Landing & Provider Canvas (muxo.ai): ${config.baseUrl}/landing\n- Documentation (docs.muxo.ai): ${config.docsUrl}\n`);
830
+ res.end(`# Muxo\n> Unified agent execution across Connected Apps, Instant Tools, Cloud Infrastructure, Workspace Memory, and Safety Guardrails.\n\n- Skill Document: ${config.baseUrl}/skill.md\n- Auth Guide: ${config.baseUrl}/auth.md\n- OpenAPI 3.1 Spec: ${config.baseUrl}/api/openapi.json\n- MCP Endpoint: ${config.baseUrl}/mcp\n- OAuth Callback Handoff: ${config.baseUrl}/oauth/callback\n- Plugin Marketplace (Codex/OpenAI): ${config.baseUrl}/.agents/plugins/marketplace.json\n- Plugin Marketplace (Claude/Droid): ${config.baseUrl}/.claude-plugin/marketplace.json\n- Console (app.muxo.ai): ${config.baseUrl}/\n- Landing & Provider Canvas (muxo.ai): ${config.baseUrl}/landing\n- Documentation (docs.muxo.ai): ${config.docsUrl}\n\n## Agent-Native Policy, Context & Governance Interface\n- MCP Control Plane (\`get_run_context\`): \`action: "recall" | "remember" | "forget" | "set_guardrail" | "reset_guardrails" | "check_policy" | "governance_status" | "kill_switch" | "scan_context"\`\n- Strict /v1 Organization Brief: \`get_organization({ company_context: { required: false } })\` returns accepted workspace policy facts + learned defaults in \`company_context.packet\`\n- Dry-Run Policy Simulator: \`POST ${config.baseUrl}/api/v1/guardrails/check\` (\`{ "tool_slug": "...", "input": { ... } }\`) or MCP \`get_run_context({ action: "check_policy", tool_slug, input })\`\n- Guardrails & Policy Brief: \`GET/POST ${config.baseUrl}/api/v1/guardrails\`, \`POST ${config.baseUrl}/api/v1/guardrails/reset\`, \`GET ${config.baseUrl}/api/v1/governance\` (includes \`llmPolicyBrief\`)\n- MCP Resources: \`muxo://policy/brief\`, \`muxo://governance/policy\`, \`muxo://context\`, \`muxo://memory/graph\`\n- MCP Prompts: \`context\`, \`policy_guardrails\`, \`governed_execution\`, \`security_audit\`\n`);
511
831
  return;
512
832
  }
513
- // Serve Installable Muxo Plugin Manifests & Metadata (Portable OpenAI Agent Plugins + Codex + Claude)
833
+ // Serve Installable Muxo Plugin Manifests & Metadata (Portable OpenAI Agent Plugins + Codex + Claude + Cursor)
514
834
  const pluginJsonRoutes = {
515
835
  "/.agents/plugins/marketplace.json": path.join(ROOT_DIR, ".agents", "plugins", "marketplace.json"),
516
836
  "/.claude-plugin/marketplace.json": path.join(ROOT_DIR, ".claude-plugin", "marketplace.json"),
@@ -518,6 +838,7 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
518
838
  "/plugins/muxo/mcp.json": path.join(ROOT_DIR, "plugins", "muxo", "mcp.json"),
519
839
  "/plugins/muxo/.codex-plugin/plugin.json": path.join(ROOT_DIR, "plugins", "muxo", ".codex-plugin", "plugin.json"),
520
840
  "/plugins/muxo/.claude-plugin/plugin.json": path.join(ROOT_DIR, "plugins", "muxo", ".claude-plugin", "plugin.json"),
841
+ "/plugins/muxo/.cursor-plugin/plugin.json": path.join(ROOT_DIR, "plugins", "muxo", ".cursor-plugin", "plugin.json"),
521
842
  "/plugins/muxo/.app.json": path.join(ROOT_DIR, "plugins", "muxo", ".app.json"),
522
843
  "/plugins/muxo/.mcp.json": path.join(ROOT_DIR, "plugins", "muxo", ".mcp.json"),
523
844
  };
@@ -545,6 +866,10 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
545
866
  file: path.join(ROOT_DIR, "plugins", "muxo", "skills", "setup", "SKILL.md"),
546
867
  contentType: "text/markdown; charset=utf-8",
547
868
  },
869
+ "/plugins/muxo/skills/muxo-activity-summary/SKILL.md": {
870
+ file: path.join(ROOT_DIR, "plugins", "muxo", "skills", "muxo-activity-summary", "SKILL.md"),
871
+ contentType: "text/markdown; charset=utf-8",
872
+ },
548
873
  "/.codex/config.toml": {
549
874
  file: path.join(ROOT_DIR, ".codex", "config.toml"),
550
875
  contentType: "text/plain; charset=utf-8",
@@ -565,10 +890,15 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
565
890
  const portableManifest = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, "plugins", "muxo", "plugin.json"), "utf8"));
566
891
  const mcpConfig = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, "plugins", "muxo", "mcp.json"), "utf8"));
567
892
  const codexManifest = JSON.parse(fs.readFileSync(path.join(ROOT_DIR, "plugins", "muxo", ".codex-plugin", "plugin.json"), "utf8"));
893
+ const cursorManifestPath = path.join(ROOT_DIR, "plugins", "muxo", ".cursor-plugin", "plugin.json");
894
+ const cursorManifest = fs.existsSync(cursorManifestPath)
895
+ ? JSON.parse(fs.readFileSync(cursorManifestPath, "utf8"))
896
+ : undefined;
568
897
  sendJson(res, 200, {
569
898
  ok: true,
570
899
  plugin: portableManifest,
571
900
  codexPlugin: codexManifest,
901
+ cursorPlugin: cursorManifest,
572
902
  mcp: mcpConfig,
573
903
  skills: getMuxoMcpSkills().map((s) => ({
574
904
  name: s.name,
@@ -585,71 +915,161 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
585
915
  portablePluginJson: `${config.baseUrl}/plugins/muxo/plugin.json`,
586
916
  portableMcpJson: `${config.baseUrl}/plugins/muxo/mcp.json`,
587
917
  onboardingSkill: `${config.baseUrl}/plugins/muxo/skills/setup/SKILL.md`,
918
+ activitySummarySkill: `${config.baseUrl}/plugins/muxo/skills/muxo-activity-summary/SKILL.md`,
588
919
  codexPluginJson: `${config.baseUrl}/plugins/muxo/.codex-plugin/plugin.json`,
920
+ cursorPluginJson: `${config.baseUrl}/plugins/muxo/.cursor-plugin/plugin.json`,
589
921
  codexConfigToml: `${config.baseUrl}/.codex/config.toml`,
590
922
  },
591
923
  installCommands: {
592
924
  codex: "codex plugin add muxo@muxo",
925
+ cursor: "/add-plugin muxo",
593
926
  cliSignup: "muxo login --signup --name my-agent",
594
927
  oauthHandoff: "muxo link github --wait",
595
928
  },
596
929
  });
597
930
  return;
598
931
  }
599
- // Web Console UI (app.muxo.ai)
600
- if ((pathname === "/" || pathname === "/console") && req.method === "GET") {
932
+ // Web Console UI (app.muxo.ai) & SPA deep-links
933
+ if (isConsolePath(pathname) && req.method === "GET") {
601
934
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
602
935
  res.end(renderConsoleHtml());
603
936
  return;
604
937
  }
938
+ if (pathname === "/downloads/muxo-cli.zip" && req.method === "GET") {
939
+ res.writeHead(302, {
940
+ Location: "https://muxo.ai/plugins/muxo-chatgpt-plugin.zip",
941
+ "Cache-Control": "no-store",
942
+ });
943
+ res.end();
944
+ return;
945
+ }
605
946
  // Built-in Interactive Landing & Provider Canvas (muxo.ai)
606
947
  if (pathname === "/landing" && req.method === "GET") {
607
948
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
608
949
  res.end(renderLandingHtml());
609
950
  return;
610
951
  }
611
- // Autonomous Agent Claim Redemption Route
612
- if (pathname.startsWith("/claim/") && req.method === "GET") {
613
- const claimSlug = decodeURIComponent(pathname.slice("/claim/".length));
614
- const redeemed = store.redeemAgentClaim(claimSlug);
615
- if (!redeemed) {
616
- sendJson(res, 404, { ok: false, error: "Invalid or expired agent claim link" });
952
+ // Public Stripe Projects Hosted Static Sites (GET /sites/:slug and GET /sites/:slug/*)
953
+ const siteMatch = pathname.match(/^\/sites\/([^/]+)(?:\/(.*))?$/);
954
+ if (siteMatch && req.method === "GET") {
955
+ const siteSlug = decodeURIComponent(siteMatch[1]).replace(/\/+$/, "");
956
+ const filePath = siteMatch[2] ? decodeURIComponent(siteMatch[2]) : undefined;
957
+ const htmlContent = engine.getHostedSiteHtml(siteSlug, filePath);
958
+ if (!htmlContent) {
959
+ sendJson(res, 404, {
960
+ ok: false,
961
+ code: "site_not_found",
962
+ error: `Hosted site '${siteSlug}' was not found.`,
963
+ });
617
964
  return;
618
965
  }
966
+ const contentType = filePath && filePath.endsWith(".css")
967
+ ? "text/css; charset=utf-8"
968
+ : filePath && filePath.endsWith(".js")
969
+ ? "application/javascript; charset=utf-8"
970
+ : filePath && filePath.endsWith(".json")
971
+ ? "application/json; charset=utf-8"
972
+ : "text/html; charset=utf-8";
973
+ res.writeHead(200, {
974
+ "Content-Type": contentType,
975
+ "Access-Control-Allow-Origin": "*",
976
+ "Cache-Control": "no-store",
977
+ });
978
+ res.end(htmlContent);
979
+ return;
980
+ }
981
+ // Workspace claim link: read-only; accepting requires the invited person's verified sign-in.
982
+ if (pathname.startsWith("/claim/") && req.method === "GET") {
983
+ const claimSlug = decodeURIComponent(pathname.slice("/claim/".length));
984
+ const claim = store.getOpenAgentClaim(claimSlug);
619
985
  const wantsHtml = url.searchParams.get("format") !== "json" &&
620
986
  String(req.headers.accept || "").includes("text/html");
621
- if (wantsHtml) {
622
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
623
- res.end(`<!DOCTYPE html>
624
- <html lang="en">
625
- <head>
626
- <meta charset="utf-8" />
627
- <title>Muxo Agent Workspace Claimed</title>
628
- <style>
629
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #090a0f; color: #f4f4f6; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
630
- .card { background: #12141f; border: 1px solid #26293d; border-radius: 14px; padding: 32px; max-width: 480px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.45); }
631
- .badge { display: inline-block; background: rgba(45,212,191,0.15); color: #2dd4bf; border: 1px solid rgba(45,212,191,0.35); border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
632
- h1 { font-size: 22px; margin: 0 0 10px; }
633
- p { color: #a1a7bb; font-size: 14px; line-height: 1.5; margin: 0 0 20px; }
634
- code { background: #1c1f30; padding: 3px 7px; border-radius: 6px; color: #e2e8f0; font-size: 12px; }
635
- .btn { display: inline-block; background: #ff5a1f; color: #fff; text-decoration: none; padding: 10px 18px; border-radius: 8px; font-size: 14px; font-weight: 600; }
636
- </style>
637
- </head>
638
- <body>
639
- <div class="card">
640
- <div class="badge">Ownership Bound</div>
641
- <h1>Workspace Claimed</h1>
642
- <p>Workspace <code>${redeemed.workspace.name}</code> (${redeemed.workspace.id}) claimed by <strong>${redeemed.targetEmail}</strong>.</p>
643
- <a class="btn" href="/?claimed=${encodeURIComponent(redeemed.workspace.slug)}">Open Muxo Console →</a>
644
- </div>
645
- </body>
646
- </html>`);
987
+ if (!claim) {
988
+ const error = "This claim link is invalid, expired, or was already used.";
989
+ if (wantsHtml) {
990
+ res.writeHead(410, consentPageHeaders);
991
+ res.end(renderOAuthConsentHtml({
992
+ error,
993
+ claim: { workspaceName: "", targetEmail: "", action: "", cancelUrl: "/" },
994
+ }));
995
+ }
996
+ else {
997
+ sendJson(res, 410, { ok: false, code: "claim_invalid", error });
998
+ }
999
+ return;
1000
+ }
1001
+ if (!wantsHtml) {
1002
+ sendJson(res, 200, {
1003
+ ok: true,
1004
+ code: "sign_in_required",
1005
+ message: `Open this link in a browser and sign in as ${claim.targetEmail} to claim the workspace.`,
1006
+ claim: {
1007
+ workspaceName: claim.workspace.name,
1008
+ targetEmail: claim.targetEmail,
1009
+ status: claim.status,
1010
+ expiresAt: claim.expiresAt,
1011
+ },
1012
+ acceptEndpoint: `${requestOriginOf(req)}/api/v1/claims/${encodeURIComponent(claimSlug)}/accept`,
1013
+ });
1014
+ return;
1015
+ }
1016
+ const clerk = clerkConfig();
1017
+ res.writeHead(200, consentPageHeaders);
1018
+ res.end(renderOAuthConsentHtml({
1019
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
1020
+ claim: {
1021
+ workspaceName: claim.workspace.name,
1022
+ targetEmail: claim.targetEmail,
1023
+ action: `/api/v1/claims/${encodeURIComponent(claimSlug)}/accept`,
1024
+ cancelUrl: "/",
1025
+ },
1026
+ }));
1027
+ return;
1028
+ }
1029
+ const claimAcceptMatch = pathname.match(/^\/api\/v1\/claims\/([^/]+)\/accept$/);
1030
+ if (claimAcceptMatch && req.method === "POST") {
1031
+ const auth = await authenticate(req);
1032
+ // Agents mint claim links with their own key, so a key must never be able to accept one.
1033
+ if (!signedInPerson(auth)) {
1034
+ sendJson(res, 401, {
1035
+ ok: false,
1036
+ code: "auth_required",
1037
+ error: "Sign in as the invited person to claim this workspace.",
1038
+ });
1039
+ return;
1040
+ }
1041
+ const claimSlug = decodeURIComponent(claimAcceptMatch[1]);
1042
+ const claim = store.getOpenAgentClaim(claimSlug);
1043
+ if (!claim) {
1044
+ sendJson(res, 410, {
1045
+ ok: false,
1046
+ code: "claim_invalid",
1047
+ error: "This claim link is invalid, expired, or was already used.",
1048
+ });
1049
+ return;
1050
+ }
1051
+ const emails = await verifiedEmailsOf(auth);
1052
+ if (!emails.includes(claim.targetEmail.toLowerCase())) {
1053
+ sendJson(res, 403, {
1054
+ ok: false,
1055
+ code: "claim_email_mismatch",
1056
+ error: `This workspace was offered to ${claim.targetEmail}. Sign in with that verified email to claim it.`,
1057
+ });
1058
+ return;
1059
+ }
1060
+ const workspace = store.acceptAgentClaim(claimSlug, auth.ctx.user.id);
1061
+ if (!workspace) {
1062
+ sendJson(res, 410, {
1063
+ ok: false,
1064
+ code: "claim_invalid",
1065
+ error: "This claim link is invalid, expired, or was already used.",
1066
+ });
647
1067
  return;
648
1068
  }
649
1069
  sendJson(res, 200, {
650
1070
  ok: true,
651
- message: `Workspace '${redeemed.workspace.name}' claimed by ${redeemed.targetEmail}`,
652
- claim: redeemed,
1071
+ redirect: `${requestOriginOf(req)}/`,
1072
+ workspace: { id: workspace.id, name: workspace.name },
653
1073
  });
654
1074
  return;
655
1075
  }
@@ -665,7 +1085,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
665
1085
  const connectedAccountId = url.searchParams.get("connected_account_id") ||
666
1086
  url.searchParams.get("connectedAccountId") ||
667
1087
  undefined;
668
- const callbackStatus = url.searchParams.get("status") || "success";
1088
+ const oauthError = url.searchParams.get("error") || url.searchParams.get("error_description");
1089
+ const callbackStatus = oauthError ? "failed" : url.searchParams.get("status") || "success";
669
1090
  const handoff = await engine.handleOAuthCallback({
670
1091
  workspaceId,
671
1092
  provider,
@@ -683,42 +1104,342 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
683
1104
  return;
684
1105
  }
685
1106
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
686
- res.end(`<!DOCTYPE html>
687
- <html lang="en">
688
- <head>
689
- <meta charset="utf-8" />
690
- <title>Muxo OAuth Handoff Complete</title>
691
- <style>
692
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #090a0f; color: #f4f4f6; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
693
- .card { background: #12141f; border: 1px solid #26293d; border-radius: 14px; padding: 32px; max-width: 460px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.45); }
694
- .badge { display: inline-block; background: rgba(16,185,129,0.15); color: #34d399; border: 1px solid rgba(16,185,129,0.35); border-radius: 999px; padding: 4px 12px; font-size: 12px; font-weight: 600; margin-bottom: 16px; }
695
- h1 { font-size: 22px; margin: 0 0 10px; }
696
- p { color: #a1a7bb; font-size: 14px; line-height: 1.5; margin: 0 0 20px; }
697
- code { background: #1c1f30; padding: 3px 7px; border-radius: 6px; color: #e2e8f0; font-size: 12px; }
698
- .btn { display: inline-block; background: #6366f1; color: #fff; text-decoration: none; padding: 10px 18px; border-radius: 8px; font-size: 14px; font-weight: 600; }
699
- </style>
700
- </head>
701
- <body>
702
- <div class="card">
703
- <div class="badge">OAuth Handoff Complete (${handoff.status.toUpperCase()})</div>
704
- <h1>${handoff.providerName} Connected to Muxo</h1>
705
- <p>Workspace <code>${handoff.workspaceId}</code> is now linked${handoff.externalAccountId ? ` (<code>${handoff.externalAccountId}</code>)` : ""}. You can close this window and return to ChatGPT, Codex, Claude Code, Droid, or your terminal.</p>
706
- <a class="btn" href="/?connected=${encodeURIComponent(handoff.providerSlug)}">Open Muxo Console</a>
707
- </div>
708
- </body>
709
- </html>`);
710
- return;
711
- }
712
- // Autonomous Agent 2-Call Signup (agents.composio.dev pattern) — Public
713
- if (pathname === "/api/v1/agents/signup" && req.method === "POST") {
1107
+ res.end(renderOAuthCallbackPageHtml({
1108
+ providerSlug: handoff.providerSlug,
1109
+ providerName: handoff.providerName,
1110
+ status: handoff.status,
1111
+ workspaceId: handoff.workspaceId,
1112
+ externalAccountId: handoff.externalAccountId,
1113
+ }));
1114
+ return;
1115
+ }
1116
+ // Public bare /oauth/connect or /connect redirector & zero-OAuth status handler
1117
+ if ((pathname === "/oauth/connect" || pathname === "/connect") && req.method === "GET") {
1118
+ const queryToken = (url.searchParams.get("token") ||
1119
+ url.searchParams.get("link_token") ||
1120
+ "").trim();
1121
+ const directRedirect = (url.searchParams.get("redirect_url") ||
1122
+ url.searchParams.get("url") ||
1123
+ url.searchParams.get("link") ||
1124
+ "").trim();
1125
+ const workspaceId = url.searchParams.get("workspace_id") ||
1126
+ url.searchParams.get("user_id") ||
1127
+ undefined;
1128
+ const rawProvider = url.searchParams.get("provider") ||
1129
+ url.searchParams.get("toolkit") ||
1130
+ "";
1131
+ const cleanProvider = normalizeStripeProjectsProviderSlug(rawProvider) ||
1132
+ rawProvider.trim().toLowerCase().replace(/^(app\.|resource\.|projects\.)/, "");
1133
+ const wantsJson = url.searchParams.get("format") === "json" ||
1134
+ String(req.headers.accept || "").includes("application/json");
1135
+ if (queryToken) {
1136
+ const targetUrl = queryToken.startsWith("s_")
1137
+ ? `https://backend.composio.dev/api/v3/s/${encodeURIComponent(queryToken.slice(2))}`
1138
+ : `https://connect.composio.dev/link/${encodeURIComponent(queryToken)}`;
1139
+ if (wantsJson) {
1140
+ sendJson(res, 200, { ok: true, status: "pending", redirectUrl: targetUrl });
1141
+ return;
1142
+ }
1143
+ res.writeHead(302, { Location: targetUrl, "Cache-Control": "no-store" });
1144
+ res.end();
1145
+ return;
1146
+ }
1147
+ if (directRedirect &&
1148
+ /^https:\/\/(?:connect|backend)\.composio\.dev\//i.test(directRedirect)) {
1149
+ if (wantsJson) {
1150
+ sendJson(res, 200, { ok: true, status: "pending", redirectUrl: directRedirect });
1151
+ return;
1152
+ }
1153
+ res.writeHead(302, { Location: directRedirect, "Cache-Control": "no-store" });
1154
+ res.end();
1155
+ return;
1156
+ }
1157
+ if (cleanProvider && !isStripeProjectsExclusiveProvider(cleanProvider)) {
1158
+ const allWs = [
1159
+ workspaceId
1160
+ ? store.getWorkspace(workspaceId)
1161
+ : store.ensureDefaultWorkspace().workspace,
1162
+ ].filter(Boolean);
1163
+ for (const ws of allWs) {
1164
+ if (!ws)
1165
+ continue;
1166
+ const pending = store
1167
+ .listConnections(ws.id)
1168
+ .find((c) => c.providerSlug.toLowerCase() === cleanProvider &&
1169
+ c.redirectUrl &&
1170
+ !isStripeProjectsExclusiveProvider(c.providerSlug));
1171
+ if (pending?.redirectUrl) {
1172
+ const nativeUrl = wrapMuxoConnectUrl(pending.redirectUrl);
1173
+ if (nativeUrl) {
1174
+ if (wantsJson) {
1175
+ sendJson(res, 200, {
1176
+ ok: true,
1177
+ provider: cleanProvider,
1178
+ status: pending.status,
1179
+ redirectUrl: nativeUrl,
1180
+ });
1181
+ return;
1182
+ }
1183
+ res.writeHead(302, { Location: nativeUrl, "Cache-Control": "no-store" });
1184
+ res.end();
1185
+ return;
1186
+ }
1187
+ }
1188
+ }
1189
+ }
1190
+ else if (!cleanProvider) {
1191
+ const allWs = [
1192
+ workspaceId
1193
+ ? store.getWorkspace(workspaceId)
1194
+ : store.ensureDefaultWorkspace().workspace,
1195
+ ].filter(Boolean);
1196
+ for (const ws of allWs) {
1197
+ if (!ws)
1198
+ continue;
1199
+ const pending = store
1200
+ .listConnections(ws.id)
1201
+ .find((c) => c.status === "pending" &&
1202
+ c.redirectUrl &&
1203
+ !isStripeProjectsExclusiveProvider(c.providerSlug));
1204
+ if (pending?.redirectUrl) {
1205
+ const nativeUrl = wrapMuxoConnectUrl(pending.redirectUrl);
1206
+ if (nativeUrl &&
1207
+ (/^https:\/\/(?:connect|backend)\.composio\.dev\//i.test(nativeUrl) ||
1208
+ nativeUrl.includes("/oauth/connect/"))) {
1209
+ if (wantsJson) {
1210
+ sendJson(res, 200, {
1211
+ ok: true,
1212
+ provider: pending.providerSlug,
1213
+ status: "pending",
1214
+ redirectUrl: nativeUrl,
1215
+ });
1216
+ return;
1217
+ }
1218
+ res.writeHead(302, { Location: nativeUrl, "Cache-Control": "no-store" });
1219
+ res.end();
1220
+ return;
1221
+ }
1222
+ }
1223
+ }
1224
+ }
1225
+ const displayProviderSlug = cleanProvider || "supabase";
1226
+ const displayProviderName = cleanProvider
1227
+ ? cleanProvider.charAt(0).toUpperCase() + cleanProvider.slice(1)
1228
+ : "Supabase & Vercel (Stripe Projects)";
1229
+ if (wantsJson) {
1230
+ sendJson(res, 200, {
1231
+ ok: true,
1232
+ status: "active",
1233
+ lane: "stripe_projects",
1234
+ provider: displayProviderSlug,
1235
+ noOAuthRequired: true,
1236
+ redirectUrl: null,
1237
+ message: "Provisioned Stripe Projects cloud resources (Supabase, Vercel, Cloudflare, Turso, Neon, etc.) are active immediately with zero OAuth required.",
1238
+ });
1239
+ return;
1240
+ }
1241
+ res.writeHead(200, {
1242
+ "Content-Type": "text/html; charset=utf-8",
1243
+ "Cache-Control": "no-store",
1244
+ });
1245
+ res.end(renderOAuthCallbackPageHtml({
1246
+ providerSlug: displayProviderSlug,
1247
+ providerName: displayProviderName,
1248
+ status: "active",
1249
+ workspaceId: workspaceId || "default",
1250
+ externalAccountId: `sp_conn_${displayProviderSlug}`,
1251
+ }));
1252
+ return;
1253
+ }
1254
+ // Muxo OAuth Connection Handoff (GET/POST /oauth/connect/:token or /connect/:token)
1255
+ const connectPageMatch = pathname.match(/^\/(?:oauth\/)?connect\/([^/]+)$/);
1256
+ if (connectPageMatch && (req.method === "GET" || req.method === "POST")) {
1257
+ const token = decodeURIComponent(connectPageMatch[1]);
1258
+ const workspaceId = url.searchParams.get("workspace_id") ||
1259
+ url.searchParams.get("user_id") ||
1260
+ undefined;
1261
+ const providerSlug = url.searchParams.get("provider") ||
1262
+ url.searchParams.get("toolkit") ||
1263
+ undefined;
1264
+ const callbackUrl = url.searchParams.get("callback_url") || undefined;
1265
+ const connectedAccountId = url.searchParams.get("connected_account_id") || undefined;
1266
+ if (req.method === "GET") {
1267
+ const wantsJson = url.searchParams.get("format") === "json" ||
1268
+ String(req.headers.accept || "").includes("application/json");
1269
+ const isTestToken = token.startsWith("lk_test");
1270
+ const forceHtml = url.searchParams.get("render") === "html";
1271
+ // Redirect real Composio link tokens directly to Composio's native hosted OAuth page
1272
+ if (!wantsJson && !isTestToken && !forceHtml && (token.startsWith("lk_") || token.startsWith("s_"))) {
1273
+ const nativeComposioUrl = token.startsWith("s_")
1274
+ ? `https://backend.composio.dev/api/v3/s/${encodeURIComponent(token.slice(2))}`
1275
+ : `https://connect.composio.dev/link/${encodeURIComponent(token)}`;
1276
+ res.writeHead(302, {
1277
+ Location: nativeComposioUrl,
1278
+ "Cache-Control": "no-store",
1279
+ });
1280
+ res.end();
1281
+ return;
1282
+ }
1283
+ const meta = await fetchMuxoConnectLinkMetadata(token, {
1284
+ workspaceId,
1285
+ providerSlug,
1286
+ callbackUrl,
1287
+ connectedAccountId,
1288
+ });
1289
+ if (url.searchParams.get("continue") === "1" ||
1290
+ url.searchParams.get("auto") === "1") {
1291
+ const submitted = await submitMuxoConnectLink(token, {}, {
1292
+ workspaceId,
1293
+ providerSlug: meta.toolkitSlug,
1294
+ callbackUrl: meta.callbackUrl || callbackUrl,
1295
+ connectedAccountId: meta.connectedAccountId,
1296
+ });
1297
+ if (submitted.ok && submitted.redirectUrl) {
1298
+ res.writeHead(302, { Location: submitted.redirectUrl });
1299
+ res.end();
1300
+ return;
1301
+ }
1302
+ }
1303
+ if (wantsJson) {
1304
+ sendJson(res, 200, { ok: true, handoff: meta });
1305
+ return;
1306
+ }
1307
+ res.writeHead(200, {
1308
+ "Content-Type": "text/html; charset=utf-8",
1309
+ "Cache-Control": "no-store",
1310
+ "Referrer-Policy": "no-referrer",
1311
+ });
1312
+ res.end(renderOAuthConnectPageHtml({
1313
+ token: meta.token,
1314
+ toolkitSlug: meta.toolkitSlug,
1315
+ toolkitName: meta.toolkitName,
1316
+ workspaceId,
1317
+ authMode: meta.authMode,
1318
+ status: meta.status,
1319
+ callbackUrl: meta.callbackUrl || callbackUrl,
1320
+ expectedInputFields: meta.expectedInputFields,
1321
+ }));
1322
+ return;
1323
+ }
1324
+ // POST /oauth/connect/:token
1325
+ const rawBody = await readBody(req);
1326
+ let input = {};
1327
+ const contentType = String(req.headers["content-type"] || "");
1328
+ if (contentType.includes("application/x-www-form-urlencoded")) {
1329
+ const formParams = new URLSearchParams(rawBody);
1330
+ for (const [k, v] of formParams.entries()) {
1331
+ if (v !== "")
1332
+ input[k] = v;
1333
+ }
1334
+ }
1335
+ else if (rawBody) {
1336
+ try {
1337
+ const parsed = JSON.parse(rawBody);
1338
+ input =
1339
+ parsed && typeof parsed.input === "object" && parsed.input !== null
1340
+ ? parsed.input
1341
+ : parsed;
1342
+ }
1343
+ catch {
1344
+ input = {};
1345
+ }
1346
+ }
1347
+ const submitted = await submitMuxoConnectLink(token, input, {
1348
+ workspaceId,
1349
+ providerSlug,
1350
+ callbackUrl,
1351
+ connectedAccountId,
1352
+ });
1353
+ const wantsJson = url.searchParams.get("format") === "json" ||
1354
+ String(req.headers.accept || "").includes("application/json") ||
1355
+ contentType.includes("application/json");
1356
+ if (wantsJson) {
1357
+ sendJson(res, submitted.ok ? 200 : 400, {
1358
+ ok: submitted.ok,
1359
+ status: submitted.status,
1360
+ redirectUrl: submitted.redirectUrl,
1361
+ redirect_url: submitted.redirectUrl,
1362
+ connectedAccountId: submitted.connectedAccountId,
1363
+ ...(submitted.error ? { error: submitted.error } : {}),
1364
+ });
1365
+ return;
1366
+ }
1367
+ if (submitted.ok && submitted.redirectUrl) {
1368
+ res.writeHead(302, { Location: submitted.redirectUrl });
1369
+ res.end();
1370
+ return;
1371
+ }
1372
+ const meta = await fetchMuxoConnectLinkMetadata(token, {
1373
+ workspaceId,
1374
+ providerSlug,
1375
+ callbackUrl,
1376
+ connectedAccountId,
1377
+ });
1378
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
1379
+ res.end(renderOAuthConnectPageHtml({
1380
+ token: meta.token,
1381
+ toolkitSlug: meta.toolkitSlug,
1382
+ toolkitName: meta.toolkitName,
1383
+ workspaceId,
1384
+ authMode: meta.authMode,
1385
+ status: meta.status,
1386
+ callbackUrl: meta.callbackUrl || callbackUrl,
1387
+ expectedInputFields: meta.expectedInputFields,
1388
+ error: submitted.error || "Could not start authorization handoff.",
1389
+ }));
1390
+ return;
1391
+ }
1392
+ // One-click Plan Approval Handoff Page (GET /approve/:planId)
1393
+ const approvePageMatch = pathname.match(/^\/approve\/([^/]+)$/);
1394
+ if (approvePageMatch && req.method === "GET") {
1395
+ const planId = decodeURIComponent(approvePageMatch[1]);
1396
+ const clerk = clerkConfig();
1397
+ res.writeHead(200, {
1398
+ "Content-Type": "text/html; charset=utf-8",
1399
+ "Cache-Control": "no-store",
1400
+ "Content-Security-Policy": "frame-ancestors 'none'",
1401
+ "X-Frame-Options": "DENY",
1402
+ "Referrer-Policy": "no-referrer",
1403
+ });
1404
+ res.end(renderApprovalPageHtml({
1405
+ planId,
1406
+ clerk: clerk
1407
+ ? {
1408
+ publishableKey: clerk.publishableKey,
1409
+ frontendApi: clerk.frontendApi,
1410
+ }
1411
+ : null,
1412
+ }));
1413
+ return;
1414
+ }
1415
+ // Autonomous Agent 2-Call Signup — Public
1416
+ if ((pathname === "/api/v1/agents/signup" ||
1417
+ pathname === "/api/v1/auth/signup" ||
1418
+ pathname === "/api/signup") &&
1419
+ req.method === "POST") {
714
1420
  const body = JSON.parse((await readBody(req)) || "{}");
1421
+ const ownerEmail = String(body.ownerEmail || body.email || "").trim().toLowerCase();
1422
+ if (ownerEmail && !/^[^\s@]+@[^\s@]+$/.test(ownerEmail)) {
1423
+ sendJson(res, 400, { ok: false, error: "ownerEmail must be a valid email address" });
1424
+ return;
1425
+ }
715
1426
  const created = store.createAutonomousAgentSignup({
716
1427
  agentName: body.agentName ? String(body.agentName) : undefined,
717
1428
  harness: body.harness ? String(body.harness) : undefined,
718
1429
  });
1430
+ const claim = store.createAgentClaim({
1431
+ workspaceId: created.workspace.id,
1432
+ email: ownerEmail || null,
1433
+ baseUrl: config.baseUrl,
1434
+ });
719
1435
  sendJson(res, 201, {
720
1436
  ok: true,
721
1437
  agentId: created.user.id,
1438
+ agentDid: workspaceAgentDid(created.workspace.id),
1439
+ claim_slug: claim.claimSlug,
1440
+ claimSlug: claim.claimSlug,
1441
+ claimUrl: claim.claimUrl,
1442
+ claim: { ...claim, targetEmail: claim.targetEmail || null },
722
1443
  workspace: {
723
1444
  id: created.workspace.id,
724
1445
  name: created.workspace.name,
@@ -735,8 +1456,14 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
735
1456
  });
736
1457
  return;
737
1458
  }
738
- // Streamable HTTP MCP Endpoint (POST /mcp, GET /mcp, DELETE /mcp)
739
- if (pathname === "/mcp" && req.method === "GET") {
1459
+ // Streamable HTTP MCP Endpoint (POST /mcp, GET /mcp, HEAD /mcp, DELETE /mcp)
1460
+ if (pathname === "/mcp" && (req.method === "GET" || req.method === "HEAD" || req.method === "DELETE")) {
1461
+ if ((await authenticate(req)).kind === "invalid") {
1462
+ sendAuthRequired(req, res, pathname);
1463
+ return;
1464
+ }
1465
+ }
1466
+ if (pathname === "/mcp" && (req.method === "GET" || req.method === "HEAD")) {
740
1467
  const accept = String(req.headers.accept || "");
741
1468
  const sessionId = String(req.headers["mcp-session-id"] || "") ||
742
1469
  `mcp_sess_${crypto.randomBytes(8).toString("hex")}`;
@@ -746,23 +1473,32 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
746
1473
  "Cache-Control": "no-cache",
747
1474
  Connection: "keep-alive",
748
1475
  "Mcp-Session-Id": sessionId,
1476
+ "MCP-Protocol-Version": LATEST_MCP_PROTOCOL_VERSION,
749
1477
  "Access-Control-Allow-Origin": "*",
1478
+ "Access-Control-Expose-Headers": "Mcp-Session-Id, MCP-Protocol-Version",
750
1479
  });
751
- res.write(`event: endpoint\ndata: ${JSON.stringify({ uri: `${config.baseUrl}/mcp`, sessionId })}\n\n`);
1480
+ if (req.method !== "HEAD") {
1481
+ res.write(`event: endpoint\ndata: ${JSON.stringify({ uri: `${config.baseUrl}/mcp`, sessionId })}\n\n`);
1482
+ }
752
1483
  res.end();
753
1484
  return;
754
1485
  }
755
1486
  sendJson(res, 200, {
756
1487
  ok: true,
757
1488
  transport: "streamable-http",
758
- protocolVersion: "2025-06-18",
1489
+ protocolVersion: LATEST_MCP_PROTOCOL_VERSION,
1490
+ supportedVersions: [...SUPPORTED_MCP_PROTOCOL_VERSIONS],
759
1491
  serverInfo: {
760
1492
  name: "muxo-mcp-server",
761
- version: "1.3.0",
1493
+ title: "Muxo Unified Agent Runtime & Governance MCP Server",
1494
+ version: MUXO_VERSION,
762
1495
  },
763
1496
  instructions: MUXO_MCP_SERVER_INSTRUCTIONS,
764
1497
  toolsCount: MUXO_MCP_TOOLS.length,
765
1498
  skillsCount: getMuxoMcpSkills().length,
1499
+ }, {
1500
+ "Mcp-Session-Id": sessionId,
1501
+ "MCP-Protocol-Version": LATEST_MCP_PROTOCOL_VERSION,
766
1502
  });
767
1503
  return;
768
1504
  }
@@ -774,14 +1510,41 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
774
1510
  return;
775
1511
  }
776
1512
  if (pathname === "/mcp" && req.method === "POST") {
777
- const ctx = resolveContext(req);
778
- const sessionId = String(req.headers["mcp-session-id"] || "") ||
779
- `mcp_sess_${ctx.workspace.id}`;
780
1513
  const raw = await readBody(req);
781
1514
  const parsed = JSON.parse(raw || "{}");
782
- if (Array.isArray(parsed)) {
783
- const responses = (await Promise.all(parsed.map((item) => handleJsonRpcMcpRequest(engine, ctx, (item || {}))))).filter((r) => r !== null);
784
- sendJson(res, 200, responses, { "Mcp-Session-Id": sessionId });
1515
+ const headerCheck = !Array.isArray(parsed)
1516
+ ? validateMcpHttpHeadersAndMeta({
1517
+ protocolVersion: String(req.headers["mcp-protocol-version"] || ""),
1518
+ method: String(req.headers["mcp-method"] || ""),
1519
+ name: String(req.headers["mcp-name"] || ""),
1520
+ }, parsed)
1521
+ : { ok: true, protocolVersion: LATEST_MCP_PROTOCOL_VERSION };
1522
+ if (!headerCheck.ok) {
1523
+ const fallbackSessionId = String(req.headers["mcp-session-id"] || "") ||
1524
+ `mcp_sess_${crypto.randomBytes(6).toString("hex")}`;
1525
+ sendJson(res, 200, {
1526
+ jsonrpc: "2.0",
1527
+ id: headerCheck.id,
1528
+ error: headerCheck.error,
1529
+ }, {
1530
+ "Mcp-Session-Id": fallbackSessionId,
1531
+ "MCP-Protocol-Version": LATEST_MCP_PROTOCOL_VERSION,
1532
+ });
1533
+ return;
1534
+ }
1535
+ const resolved = await requireContext(req, res, pathname, !Array.isArray(parsed) && isPublicMcpRequest(parsed));
1536
+ if (!resolved)
1537
+ return;
1538
+ const { ctx } = resolved;
1539
+ const sessionId = String(req.headers["mcp-session-id"] || "") ||
1540
+ `mcp_sess_${ctx.workspace.id}`;
1541
+ const responseHeaders = {
1542
+ "Mcp-Session-Id": sessionId,
1543
+ "MCP-Protocol-Version": headerCheck.protocolVersion,
1544
+ };
1545
+ if (Array.isArray(parsed)) {
1546
+ const responses = (await Promise.all(parsed.map((item) => handleJsonRpcMcpRequest(engine, ctx, (item || {}))))).filter((r) => r !== null);
1547
+ sendJson(res, 200, responses, responseHeaders);
785
1548
  return;
786
1549
  }
787
1550
  const rpc = parsed;
@@ -789,15 +1552,35 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
789
1552
  if (!response) {
790
1553
  res.writeHead(202, {
791
1554
  "Access-Control-Allow-Origin": "*",
792
- "Mcp-Session-Id": sessionId,
1555
+ ...responseHeaders,
793
1556
  });
794
1557
  res.end();
795
1558
  return;
796
1559
  }
797
- sendJson(res, 200, response, { "Mcp-Session-Id": sessionId });
1560
+ sendJson(res, 200, response, responseHeaders);
798
1561
  return;
799
1562
  }
800
1563
  // Auth Endpoints
1564
+ if (pathname === "/api/v1/auth/config" && req.method === "GET") {
1565
+ const clerk = clerkConfig();
1566
+ sendJson(res, 200, {
1567
+ ok: true,
1568
+ provider: clerk ? "clerk" : "password",
1569
+ clerk: clerk ? { publishableKey: clerk.publishableKey, frontendApi: clerk.frontendApi } : null,
1570
+ signInUrl: `${requestOriginOf(req)}/login`,
1571
+ });
1572
+ return;
1573
+ }
1574
+ if ((pathname === "/api/v1/auth/register" || pathname === "/api/v1/auth/login") &&
1575
+ req.method === "POST" &&
1576
+ clerkConfig()) {
1577
+ sendJson(res, 410, {
1578
+ ok: false,
1579
+ code: "use_clerk",
1580
+ error: `Muxo accounts use secure sign-in. Open ${requestOriginOf(req)}/login to sign in or create an account.`,
1581
+ });
1582
+ return;
1583
+ }
801
1584
  if (pathname === "/api/v1/auth/register" && req.method === "POST") {
802
1585
  const body = JSON.parse((await readBody(req)) || "{}");
803
1586
  const created = store.registerUser({
@@ -806,7 +1589,17 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
806
1589
  password: String(body.password || ""),
807
1590
  workspaceName: body.workspaceName ? String(body.workspaceName) : undefined,
808
1591
  });
809
- sendJson(res, 201, { ok: true, ...created });
1592
+ const initialKey = store.createApiKey({
1593
+ workspaceId: created.workspace.id,
1594
+ name: "Default Live Key",
1595
+ environment: "live",
1596
+ });
1597
+ sendJson(res, 201, {
1598
+ ok: true,
1599
+ ...created,
1600
+ token: created.sessionToken,
1601
+ apiKey: initialKey.secretKey,
1602
+ });
810
1603
  return;
811
1604
  }
812
1605
  if (pathname === "/api/v1/auth/login" && req.method === "POST") {
@@ -816,21 +1609,39 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
816
1609
  sendJson(res, 401, { ok: false, error: "Invalid email or password" });
817
1610
  return;
818
1611
  }
819
- sendJson(res, 200, { ok: true, ...loggedIn });
1612
+ sendJson(res, 200, { ok: true, ...loggedIn, token: loggedIn.sessionToken });
820
1613
  return;
821
1614
  }
822
- const ctx = resolveContext(req);
1615
+ const isPublicRoute = ((pathname === "/api/v1/tools" || pathname === "/v1/tools/search") &&
1616
+ (req.method === "GET" || req.method === "POST")) ||
1617
+ (pathname.startsWith("/api/v1/tools/") && req.method === "GET") ||
1618
+ (pathname === "/api/v1/billing/webhook" && req.method === "POST") ||
1619
+ ((pathname === "/webhooks" ||
1620
+ pathname === "/webhooks/composio" ||
1621
+ pathname === "/api/v1/triggers/webhook") &&
1622
+ req.method === "POST");
1623
+ const resolvedCtx = await requireContext(req, res, pathname, isPublicRoute);
1624
+ if (!resolvedCtx)
1625
+ return;
1626
+ const { ctx, auth } = resolvedCtx;
823
1627
  // Console Bootstrap
824
1628
  if (pathname === "/api/v1/bootstrap" && req.method === "GET") {
825
1629
  const org = engine.getOrganization(ctx).organization;
1630
+ const guardrailsSummary = engine.getGuardrailsSummary(ctx);
826
1631
  sendJson(res, 200, {
827
1632
  ok: true,
828
1633
  profile: engine.getProfile(ctx),
829
1634
  organization: org,
830
1635
  governance: engine.governance.getStatus(),
1636
+ llmPolicyBrief: guardrailsSummary.llmPolicyBrief,
1637
+ plainEnglishGuardrails: guardrailsSummary.plainEnglish,
1638
+ activeMemoryDefaults: guardrailsSummary.activeDefaults,
1639
+ activeGuardrails: guardrailsSummary.activeGuardrails,
831
1640
  supermemory: {
832
1641
  enabled: engine.supermemory.isEnabled(),
833
1642
  namespace: engine.supermemory.getWorkspaceNamespace(ctx.workspace.id),
1643
+ activeDefaults: guardrailsSummary.activeDefaults,
1644
+ activeGuardrails: guardrailsSummary.activeGuardrails,
834
1645
  },
835
1646
  surfaces: {
836
1647
  publicWebUrl: config.publicWebUrl,
@@ -839,7 +1650,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
839
1650
  },
840
1651
  apiKeys: store.listApiKeys(ctx.workspace.id),
841
1652
  byokKeys: store.listByokKeys(ctx.workspace.id),
842
- defaultApiKey: bootstrap.defaultApiKey || null,
1653
+ defaultApiKey: ctx.workspace.id === bootstrap.workspace.id ? bootstrap.defaultApiKey || null : null,
1654
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
843
1655
  connections: store.listConnections(ctx.workspace.id),
844
1656
  resources: store.listResources(ctx.workspace.id),
845
1657
  plans: store.listPlans(ctx.workspace.id, 25),
@@ -847,11 +1659,122 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
847
1659
  });
848
1660
  return;
849
1661
  }
1662
+ // Multi-Workspace & Team API Key Onboarding Endpoints
1663
+ if (pathname === "/api/v1/workspaces" && req.method === "GET") {
1664
+ sendJson(res, 200, {
1665
+ ok: true,
1666
+ currentWorkspaceId: ctx.workspace.id,
1667
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
1668
+ });
1669
+ return;
1670
+ }
1671
+ if (pathname === "/api/v1/workspaces" && req.method === "POST") {
1672
+ const body = JSON.parse((await readBody(req)) || "{}");
1673
+ const name = String(body.name || body.workspaceName || "Team Workspace").trim();
1674
+ const teamName = body.teamName ? String(body.teamName).trim() : null;
1675
+ const switchActive = body.switchActive !== false;
1676
+ const created = store.createWorkspaceForUser({
1677
+ userId: ctx.user.id,
1678
+ name,
1679
+ teamName,
1680
+ switchActive,
1681
+ });
1682
+ sendJson(res, 201, {
1683
+ ok: true,
1684
+ ...created,
1685
+ workspaces: store.listUserWorkspaces(ctx.user.id, created.workspace.id),
1686
+ });
1687
+ return;
1688
+ }
1689
+ if (pathname === "/api/v1/workspaces/switch" && req.method === "POST") {
1690
+ const body = JSON.parse((await readBody(req)) || "{}");
1691
+ const workspaceId = String(body.workspaceId || body.workspace_id || "").trim();
1692
+ const switched = store.switchUserWorkspace(ctx.user.id, workspaceId);
1693
+ if (!switched) {
1694
+ sendJson(res, 404, {
1695
+ ok: false,
1696
+ error: "Workspace not found or not linked to your account",
1697
+ });
1698
+ return;
1699
+ }
1700
+ sendJson(res, 200, {
1701
+ ok: true,
1702
+ ...switched,
1703
+ workspaces: store.listUserWorkspaces(ctx.user.id, switched.workspace.id),
1704
+ });
1705
+ return;
1706
+ }
1707
+ if ((pathname === "/api/v1/workspaces/join" || pathname === "/api/v1/auth/key") &&
1708
+ req.method === "POST") {
1709
+ const body = JSON.parse((await readBody(req)) || "{}");
1710
+ const rawKey = String(body.apiKey || body.api_key || body.key || "").trim();
1711
+ const hasExplicitSession = Boolean(String(req.headers.authorization || "").trim()) &&
1712
+ ctx.authType === "session";
1713
+ const joined = store.joinWorkspaceWithApiKey({
1714
+ apiKey: rawKey,
1715
+ userId: hasExplicitSession ? ctx.user.id : null,
1716
+ switchActive: body.switchActive !== false,
1717
+ });
1718
+ if (!joined) {
1719
+ sendJson(res, 401, {
1720
+ ok: false,
1721
+ error: "Invalid or revoked workspace API key (expected mk_live_... or mk_test_...)",
1722
+ });
1723
+ return;
1724
+ }
1725
+ const sessionToken = joined.user && !hasExplicitSession
1726
+ ? store.createSession(joined.user.id, joined.workspace.id)
1727
+ : undefined;
1728
+ sendJson(res, 200, {
1729
+ ok: true,
1730
+ workspace: joined.workspace,
1731
+ keyRecord: joined.keyRecord,
1732
+ user: joined.user,
1733
+ membership: joined.membership,
1734
+ ...(sessionToken ? { sessionToken, token: sessionToken } : {}),
1735
+ workspaces: joined.user
1736
+ ? store.listUserWorkspaces(joined.user.id, joined.workspace.id)
1737
+ : [],
1738
+ });
1739
+ return;
1740
+ }
1741
+ if ((pathname === "/api/v1/workspaces/onboard-team" ||
1742
+ pathname === "/api/v1/teams/onboard") &&
1743
+ req.method === "POST") {
1744
+ const body = JSON.parse((await readBody(req)) || "{}");
1745
+ const teamName = String(body.teamName || body.team_name || body.name || "Engineering Team").trim();
1746
+ const role = (body.role ? String(body.role) : "operator");
1747
+ const mode = body.mode === "dedicated_workspace" ? "dedicated_workspace" : "shared_workspace";
1748
+ const inviteEmail = body.inviteEmail || body.email ? String(body.inviteEmail || body.email).trim() : null;
1749
+ const kit = store.onboardTeam({
1750
+ currentWorkspaceId: ctx.workspace.id,
1751
+ userId: ctx.user.id,
1752
+ userEmail: ctx.user.email,
1753
+ teamName,
1754
+ role,
1755
+ mode,
1756
+ scopes: Array.isArray(body.scopes) ? body.scopes : undefined,
1757
+ inviteEmail,
1758
+ baseUrl: config.baseUrl,
1759
+ });
1760
+ sendJson(res, 201, {
1761
+ ok: true,
1762
+ kit,
1763
+ onboardingKit: kit,
1764
+ ...kit,
1765
+ workspaces: store.listUserWorkspaces(ctx.user.id, ctx.workspace.id),
1766
+ });
1767
+ return;
1768
+ }
850
1769
  // Autonomous Agent Claim Link Creation (Authenticated)
851
1770
  if (pathname === "/api/v1/agents/claim" && req.method === "POST") {
1771
+ if (auth.kind !== "ok") {
1772
+ sendAuthRequired(req, res, pathname);
1773
+ return;
1774
+ }
852
1775
  const body = JSON.parse((await readBody(req)) || "{}");
853
- const email = String(body.email || "").trim();
854
- if (!email || !email.includes("@")) {
1776
+ const email = String(body.email || body.ownerEmail || "").trim().toLowerCase();
1777
+ if (!email || !/^[^\s@]+@[^\s@]+$/.test(email)) {
855
1778
  sendJson(res, 400, { ok: false, error: "Valid target email is required" });
856
1779
  return;
857
1780
  }
@@ -860,34 +1783,286 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
860
1783
  email,
861
1784
  baseUrl: config.baseUrl,
862
1785
  });
863
- sendJson(res, 201, { ok: true, claim, ...claim });
1786
+ sendJson(res, 201, {
1787
+ ok: true,
1788
+ claim: { ...claim, ownerEmail: email },
1789
+ ...claim,
1790
+ claim_slug: claim.claimSlug,
1791
+ ownerEmail: email,
1792
+ });
864
1793
  return;
865
1794
  }
866
1795
  // Microsoft Agent Governance Status & Audit Verification
867
1796
  if (pathname === "/api/v1/governance" && req.method === "GET") {
1797
+ const ws = store.getWorkspace(ctx.workspace.id) || ctx.workspace;
1798
+ const fullReport = engine.governance.getGovernanceStatus(ws);
1799
+ const guardrailsSummary = engine.getGuardrailsSummary(ctx);
868
1800
  sendJson(res, 200, {
869
1801
  ok: true,
870
- governance: engine.governance.getStatus(),
871
- workspacePolicy: ctx.workspace.policy,
1802
+ governance: {
1803
+ ...engine.governance.getStatus(ws),
1804
+ modules: fullReport.modules,
1805
+ trustScore: fullReport.trustScore,
1806
+ killSwitch: fullReport.killSwitch,
1807
+ contextPoisoning: fullReport.contextPoisoning,
1808
+ auditChain: fullReport.auditChain,
1809
+ },
1810
+ workspacePolicy: ws.policy,
1811
+ llmPolicyBrief: guardrailsSummary.llmPolicyBrief,
1812
+ plainEnglishGuardrails: guardrailsSummary.plainEnglish,
1813
+ activeMemoryDefaults: guardrailsSummary.activeDefaults,
1814
+ activeGuardrails: guardrailsSummary.activeGuardrails,
1815
+ });
1816
+ return;
1817
+ }
1818
+ if (pathname === "/api/v1/guardrails" && req.method === "GET") {
1819
+ const summary = engine.getGuardrailsSummary(ctx);
1820
+ sendJson(res, 200, {
1821
+ ok: true,
1822
+ ...summary,
1823
+ });
1824
+ return;
1825
+ }
1826
+ if ((pathname === "/api/v1/guardrails/reset" || pathname === "/api/v1/policy/reset") &&
1827
+ req.method === "POST") {
1828
+ const resetResult = await engine.resetWorkspaceGuardrails(ctx);
1829
+ sendJson(res, 200, {
1830
+ ...resetResult,
1831
+ humanMessage: "Reset workspace guardrails to Safe Default.",
1832
+ });
1833
+ return;
1834
+ }
1835
+ if ((pathname === "/api/v1/guardrails/check" ||
1836
+ pathname === "/api/v1/guardrails/simulate" ||
1837
+ pathname === "/api/v1/policy/check") &&
1838
+ req.method === "POST") {
1839
+ const body = JSON.parse((await readBody(req)) || "{}");
1840
+ const toolSlug = String(body.tool_slug || body.toolSlug || body.slug || body.tool_id || "instant.web.search").trim();
1841
+ const input = (body.input || body.arguments) || {};
1842
+ const result = await engine.checkToolPolicy(ctx, { toolSlug, input });
1843
+ const summary = engine.getGuardrailsSummary(ctx);
1844
+ sendJson(res, 200, {
1845
+ ...result,
1846
+ simulation: result,
1847
+ llmPolicyBrief: summary.llmPolicyBrief,
1848
+ });
1849
+ return;
1850
+ }
1851
+ if (pathname === "/api/v1/guardrails" && req.method === "POST") {
1852
+ const body = JSON.parse((await readBody(req)) || "{}");
1853
+ if (body.reset === true) {
1854
+ const resetResult = await engine.resetWorkspaceGuardrails(ctx);
1855
+ sendJson(res, 200, {
1856
+ ...resetResult,
1857
+ humanMessage: "Reset workspace guardrails to Safe Default.",
1858
+ });
1859
+ return;
1860
+ }
1861
+ const rawParamRules = Array.isArray(body.parameterRules)
1862
+ ? body.parameterRules
1863
+ : Array.isArray(body.parameter_rules)
1864
+ ? body.parameter_rules
1865
+ : undefined;
1866
+ const prompt = String(body.prompt ||
1867
+ body.rule ||
1868
+ body.instruction ||
1869
+ body.naturalLanguageRule ||
1870
+ (rawParamRules && rawParamRules.length > 0
1871
+ ? String(rawParamRules[0]?.description ||
1872
+ "Structured conditional parameter guardrail")
1873
+ : "")).trim();
1874
+ if (!prompt && (!rawParamRules || rawParamRules.length === 0)) {
1875
+ sendJson(res, 400, {
1876
+ ok: false,
1877
+ error: "Provide a plain-English guardrail in 'prompt'/'rule' or structured 'parameterRules' (e.g. \"Only allow sending emails to @muxo.ai\" or \"Require approval for Stripe refunds over $50, auto-approve under $50\").",
1878
+ });
1879
+ return;
1880
+ }
1881
+ const applied = await engine.applyNaturalLanguageGuardrail(ctx, prompt, {
1882
+ parameterRules: rawParamRules,
1883
+ policyPatch: body.policyPatch && typeof body.policyPatch === "object"
1884
+ ? body.policyPatch
1885
+ : undefined,
1886
+ source: rawParamRules ? "mcp_harness" : "deterministic",
1887
+ });
1888
+ sendJson(res, 200, {
1889
+ ...applied,
1890
+ policy: applied.workspace.policy,
1891
+ humanMessage: `Guardrail active: ${applied.guardrail.summary}`,
872
1892
  });
873
1893
  return;
874
1894
  }
1895
+ if (pathname === "/api/v1/governance/kill-switch" && req.method === "POST") {
1896
+ const body = JSON.parse((await readBody(req)) || "{}");
1897
+ const action = String(body.action || "quarantine").toLowerCase();
1898
+ if (action === "resume" || body.active === false) {
1899
+ const out = engine.resumeWorkspaceKillSwitch(ctx);
1900
+ sendJson(res, 200, { ok: true, ...out });
1901
+ return;
1902
+ }
1903
+ const out = await engine.activateWorkspaceKillSwitch(ctx, {
1904
+ reason: String(body.reason || "Emergency kill-switch activated by workspace operator"),
1905
+ action: action === "terminate" || action === "suspend"
1906
+ ? action
1907
+ : "quarantine",
1908
+ });
1909
+ sendJson(res, 200, { ok: true, ...out });
1910
+ return;
1911
+ }
1912
+ if (pathname === "/api/v1/governance/scan-context" && req.method === "POST") {
1913
+ const body = JSON.parse((await readBody(req)) || "{}");
1914
+ const scan = engine.governance.scanMemoryContent({
1915
+ workspaceId: ctx.workspace.id,
1916
+ sessionId: String(body.sessionId || "default"),
1917
+ role: body.role || "user",
1918
+ content: String(body.content || ""),
1919
+ recordInDetector: Boolean(body.recordInDetector),
1920
+ });
1921
+ sendJson(res, 200, { ok: true, scan });
1922
+ return;
1923
+ }
1924
+ // Workspace Context Inspector (natural-language friendly)
1925
+ if ((pathname === "/api/v1/context" || pathname === "/v1/context") &&
1926
+ (req.method === "GET" || req.method === "POST")) {
1927
+ const body = req.method === "POST"
1928
+ ? JSON.parse((await readBody(req)) || "{}")
1929
+ : {};
1930
+ const query = url.searchParams.get("query") ||
1931
+ url.searchParams.get("q") ||
1932
+ (body.query ? String(body.query) : undefined);
1933
+ const runContext = await engine.getRunContext(ctx, query);
1934
+ sendJson(res, 200, { ok: true, ...runContext });
1935
+ return;
1936
+ }
875
1937
  // Supermemory v5 Workspace Brain Endpoints
876
1938
  if (pathname === "/api/v1/memory" && req.method === "GET") {
877
1939
  const query = url.searchParams.get("query") ||
878
1940
  url.searchParams.get("q") ||
879
1941
  undefined;
880
1942
  const overview = await engine.getMemoryOverview(ctx, query);
881
- sendJson(res, 200, { ok: true, ...overview });
1943
+ sendJson(res, 200, {
1944
+ ok: true,
1945
+ ...overview,
1946
+ activeDefaults: overview.context.activeDefaults || {},
1947
+ activeGuardrails: overview.context.activeGuardrails || [],
1948
+ });
1949
+ return;
1950
+ }
1951
+ if (pathname === "/api/v1/memory/graph" &&
1952
+ (req.method === "GET" || req.method === "POST")) {
1953
+ const body = req.method === "POST"
1954
+ ? JSON.parse((await readBody(req)) || "{}")
1955
+ : {};
1956
+ const query = url.searchParams.get("query") ||
1957
+ url.searchParams.get("q") ||
1958
+ (body.query ? String(body.query) : undefined);
1959
+ const asOf = url.searchParams.get("as_of") ||
1960
+ url.searchParams.get("asOf") ||
1961
+ (body.as_of || body.asOf ? String(body.as_of || body.asOf) : null);
1962
+ const focalEntity = url.searchParams.get("focal_entity") ||
1963
+ url.searchParams.get("focalEntity") ||
1964
+ (body.focal_entity || body.focalEntity
1965
+ ? String(body.focal_entity || body.focalEntity)
1966
+ : null);
1967
+ const rawMaxHops = url.searchParams.get("max_hops") ??
1968
+ url.searchParams.get("maxHops") ??
1969
+ body.max_hops ??
1970
+ body.maxHops;
1971
+ const maxHops = rawMaxHops !== undefined && rawMaxHops !== null && rawMaxHops !== ""
1972
+ ? Number(rawMaxHops)
1973
+ : undefined;
1974
+ const graph = await engine.getMemoryGraph(ctx, {
1975
+ query,
1976
+ asOf,
1977
+ focalEntity,
1978
+ maxHops,
1979
+ });
1980
+ sendJson(res, 200, { ok: true, ...graph });
1981
+ return;
1982
+ }
1983
+ if (pathname === "/api/v1/memory/facts" && req.method === "GET") {
1984
+ const query = url.searchParams.get("query") ||
1985
+ url.searchParams.get("q") ||
1986
+ undefined;
1987
+ const includeHistorical = url.searchParams.get("historical") !== "false";
1988
+ const facts = store.listTemporalFacts(ctx.workspace.id, {
1989
+ includeHistorical,
1990
+ subject: url.searchParams.get("subject") || undefined,
1991
+ predicate: url.searchParams.get("predicate") || undefined,
1992
+ query,
1993
+ limit: Number(url.searchParams.get("limit") || 50),
1994
+ });
1995
+ sendJson(res, 200, {
1996
+ ok: true,
1997
+ activeFacts: facts.filter((f) => f.validTo === null),
1998
+ historicalFacts: facts.filter((f) => f.validTo !== null),
1999
+ facts,
2000
+ });
2001
+ return;
2002
+ }
2003
+ if (pathname === "/api/v1/memory/forget" && req.method === "POST") {
2004
+ const body = JSON.parse((await readBody(req)) || "{}");
2005
+ const query = String(body.query || body.q || "").trim();
2006
+ if (!query) {
2007
+ sendJson(res, 400, { ok: false, error: "query is required to forget matching memories" });
2008
+ return;
2009
+ }
2010
+ const pruned = await engine.forgetWorkspaceMemoryMatching(ctx, query);
2011
+ sendJson(res, 200, { ok: true, ...pruned });
882
2012
  return;
883
2013
  }
884
2014
  if (pathname === "/api/v1/memory" && req.method === "POST") {
885
2015
  const body = JSON.parse((await readBody(req)) || "{}");
2016
+ const nlPrompt = String(body.prompt || body.remember || "").trim();
2017
+ if (nlPrompt && !body.content) {
2018
+ const parsed = parseNaturalLanguageMemoryInstruction(nlPrompt);
2019
+ if (parsed.isMemoryInstruction) {
2020
+ const primaryFact = parsed.temporalFacts[0];
2021
+ const record = await engine.addWorkspaceMemory(ctx, {
2022
+ title: parsed.title,
2023
+ content: parsed.content,
2024
+ kind: parsed.kind,
2025
+ subject: primaryFact?.subject,
2026
+ predicate: primaryFact?.predicate,
2027
+ object: primaryFact?.object,
2028
+ metadata: {
2029
+ extractedDefaults: parsed.extractedDefaults,
2030
+ source: "natural_language_api",
2031
+ },
2032
+ });
2033
+ for (const tf of parsed.temporalFacts) {
2034
+ store.upsertTemporalFact({
2035
+ workspaceId: ctx.workspace.id,
2036
+ subject: tf.subject,
2037
+ predicate: tf.predicate,
2038
+ object: tf.object,
2039
+ summary: tf.summary,
2040
+ confidence: 0.98,
2041
+ sourceKind: "preference",
2042
+ sourceReferenceId: record.id,
2043
+ metadata: { source: "natural_language_api" },
2044
+ });
2045
+ }
2046
+ const overview = await engine.getMemoryOverview(ctx);
2047
+ sendJson(res, 201, {
2048
+ ok: true,
2049
+ memory: record,
2050
+ activeDefaults: overview.activeDefaults,
2051
+ explanation: overview.explanation,
2052
+ humanMessage: parsed.humanSummary,
2053
+ });
2054
+ return;
2055
+ }
2056
+ body.content = nlPrompt;
2057
+ }
886
2058
  const record = await engine.addWorkspaceMemory(ctx, {
887
2059
  title: String(body.title || "Workspace Fact"),
888
2060
  content: String(body.content || ""),
889
2061
  kind: body.kind || "fact",
890
2062
  metadata: body.metadata || {},
2063
+ subject: body.subject ? String(body.subject) : undefined,
2064
+ predicate: body.predicate ? String(body.predicate) : undefined,
2065
+ object: body.object ? String(body.object) : undefined,
891
2066
  });
892
2067
  sendJson(res, 201, { ok: true, memory: record });
893
2068
  return;
@@ -899,14 +2074,42 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
899
2074
  return;
900
2075
  }
901
2076
  // API Keys
902
- if (pathname === "/api/v1/api-keys" && req.method === "POST") {
2077
+ if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
2078
+ req.method === "GET") {
2079
+ sendJson(res, 200, {
2080
+ ok: true,
2081
+ apiKeys: store.listApiKeys(ctx.workspace.id),
2082
+ keys: store.listApiKeys(ctx.workspace.id),
2083
+ });
2084
+ return;
2085
+ }
2086
+ if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
2087
+ req.method === "POST") {
903
2088
  const body = JSON.parse((await readBody(req)) || "{}");
904
2089
  const created = store.createApiKey({
905
2090
  workspaceId: ctx.workspace.id,
906
- name: String(body.name || "API Key"),
2091
+ name: String(body.name || (body.teamName ? `${String(body.teamName)} Team Key` : "API Key")),
907
2092
  environment: body.environment === "test" ? "test" : "live",
2093
+ scopes: Array.isArray(body.scopes) ? body.scopes : undefined,
2094
+ teamName: body.teamName ? String(body.teamName) : null,
2095
+ role: (body.role ? String(body.role) : "operator"),
2096
+ createdByEmail: ctx.user.email,
908
2097
  });
909
- sendJson(res, 201, { ok: true, ...created });
2098
+ sendJson(res, 201, { ok: true, apiKey: created.secretKey, ...created });
2099
+ return;
2100
+ }
2101
+ if ((pathname.startsWith("/api/v1/api-keys/") ||
2102
+ pathname.startsWith("/api/v1/keys/")) &&
2103
+ req.method === "DELETE") {
2104
+ const prefix = pathname.startsWith("/api/v1/api-keys/")
2105
+ ? "/api/v1/api-keys/"
2106
+ : "/api/v1/keys/";
2107
+ const keyId = decodeURIComponent(pathname.slice(prefix.length));
2108
+ if (!store.revokeApiKey(ctx.workspace.id, keyId)) {
2109
+ sendJson(res, 404, { ok: false, error: "API key not found" });
2110
+ return;
2111
+ }
2112
+ sendJson(res, 200, { ok: true, revoked: keyId });
910
2113
  return;
911
2114
  }
912
2115
  // Profile & Organization (v1 and api/v1)
@@ -938,78 +2141,759 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
938
2141
  sendJson(res, 200, tool);
939
2142
  return;
940
2143
  }
941
- // Connections
942
- if (pathname === "/api/v1/connections" && req.method === "GET") {
943
- const syncLive = url.searchParams.get("sync") === "true";
944
- const provider = url.searchParams.get("provider") || undefined;
945
- const status = await engine.connectionStatus(ctx, { provider, syncLive });
946
- sendJson(res, 200, { ok: true, ...status });
2144
+ // Connections
2145
+ if (pathname === "/api/v1/connections" && req.method === "GET") {
2146
+ const syncLive = url.searchParams.get("sync") === "true";
2147
+ const provider = url.searchParams.get("provider") || undefined;
2148
+ const status = await engine.connectionStatus(ctx, { provider, syncLive });
2149
+ sendJson(res, 200, { ok: true, ...status });
2150
+ return;
2151
+ }
2152
+ if (pathname === "/api/v1/connections" && req.method === "POST") {
2153
+ const body = JSON.parse((await readBody(req)) || "{}");
2154
+ const connection = await engine.startConnection(ctx, {
2155
+ provider: String(body.provider || ""),
2156
+ lane: body.lane,
2157
+ callbackUrl: body.callbackUrl ? String(body.callbackUrl) : undefined,
2158
+ });
2159
+ sendJson(res, 201, { ok: true, connection });
2160
+ return;
2161
+ }
2162
+ if (pathname === "/api/v1/connections/wait" && req.method === "POST") {
2163
+ const body = JSON.parse((await readBody(req)) || "{}");
2164
+ const provider = String(body.provider || "");
2165
+ if (body.simulateCallback === true || body.simulate_callback === true) {
2166
+ await engine.handleOAuthCallback({
2167
+ workspaceId: ctx.workspace.id,
2168
+ provider,
2169
+ status: "success",
2170
+ });
2171
+ }
2172
+ const result = await engine.waitForConnection(ctx, {
2173
+ provider,
2174
+ timeoutSeconds: clampWaitSeconds(body.timeoutSeconds ?? body.timeout_seconds),
2175
+ });
2176
+ sendJson(res, 200, result);
2177
+ return;
2178
+ }
2179
+ const connectionWaitMatch = pathname.match(/^\/api\/v1\/connections\/([^/]+)\/wait$/);
2180
+ if (connectionWaitMatch && req.method === "GET") {
2181
+ const result = await engine.waitForConnection(ctx, {
2182
+ provider: decodeURIComponent(connectionWaitMatch[1]),
2183
+ timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds") ?? url.searchParams.get("timeoutSeconds")),
2184
+ });
2185
+ sendJson(res, 200, result);
2186
+ return;
2187
+ }
2188
+ // Provisioned Resources & Decrypted Vault Env Pull
2189
+ if (pathname === "/api/v1/resources" && req.method === "GET") {
2190
+ sendJson(res, 200, {
2191
+ ok: true,
2192
+ resources: store.listResources(ctx.workspace.id),
2193
+ });
2194
+ return;
2195
+ }
2196
+ if (pathname === "/api/v1/resources/env" && req.method === "GET") {
2197
+ // Also pull any fresh .env from Stripe Projects workdir if present
2198
+ const workdirSecrets = await engine.stripeProjects
2199
+ .pullEnvVarsFromWorkdir()
2200
+ .catch(() => ({}));
2201
+ const vaultSecrets = store.getDecryptedWorkspaceEnv(ctx.workspace.id);
2202
+ const merged = { ...workdirSecrets, ...vaultSecrets };
2203
+ const dotenvLines = [
2204
+ `# Muxo Encrypted Vault Export — Workspace ${ctx.workspace.slug} (${ctx.workspace.id})`,
2205
+ `# Generated at ${new Date().toISOString()}`,
2206
+ ...Object.entries(merged).map(([k, v]) => `${k}=${JSON.stringify(v)}`),
2207
+ ];
2208
+ sendJson(res, 200, {
2209
+ ok: true,
2210
+ workspaceId: ctx.workspace.id,
2211
+ variables: merged,
2212
+ dotenv: dotenvLines.join("\n") + "\n",
2213
+ });
2214
+ return;
2215
+ }
2216
+ const resourceSecretsMatch = pathname.match(/^\/api\/v1\/resources\/([^/]+)\/secrets$/);
2217
+ if (resourceSecretsMatch && req.method === "GET") {
2218
+ const resourceId = decodeURIComponent(resourceSecretsMatch[1]);
2219
+ const anyRes = store.findResourceAnyWorkspace(resourceId);
2220
+ if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
2221
+ sendJson(res, 403, {
2222
+ ok: false,
2223
+ code: "resource_workspace_mismatch",
2224
+ error: "Resource belongs to a different workspace.",
2225
+ });
2226
+ return;
2227
+ }
2228
+ const ownedRes = store.getResource(ctx.workspace.id, resourceId);
2229
+ if (!ownedRes) {
2230
+ sendJson(res, 404, {
2231
+ ok: false,
2232
+ code: "resource_not_found",
2233
+ error: `Resource '${resourceId}' not found in workspace.`,
2234
+ });
2235
+ return;
2236
+ }
2237
+ const secrets = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
2238
+ sendJson(res, 200, {
2239
+ ok: true,
2240
+ resourceId: ownedRes.id,
2241
+ resourceName: ownedRes.resourceName,
2242
+ workspaceId: ctx.workspace.id,
2243
+ envVarKeys: ownedRes.envVarKeys,
2244
+ secrets,
2245
+ });
2246
+ return;
2247
+ }
2248
+ // Stripe v2 Provisioning API (`Stripe-Version: 2026-09-30.preview`) Routes
2249
+ if (pathname === "/api/v1/provisioning/audit_log" && req.method === "GET") {
2250
+ const limit = Number(url.searchParams.get("limit") || 50);
2251
+ sendJson(res, 200, {
2252
+ ok: true,
2253
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2254
+ entries: engine.stripeProjects.getProvisioningAuditLog(limit),
2255
+ });
2256
+ return;
2257
+ }
2258
+ if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "GET") {
2259
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2260
+ const verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
2261
+ config.stripeConnectedAccountId ||
2262
+ "";
2263
+ if (!verifiedAccount) {
2264
+ sendJson(res, 200, {
2265
+ ok: true,
2266
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2267
+ connected: false,
2268
+ account: null,
2269
+ publishable_key: config.stripePublishableKey || null,
2270
+ });
2271
+ return;
2272
+ }
2273
+ const retrieved = await engine.stripeProjects.retrieveConnectedAccount(verifiedAccount, stripeByok);
2274
+ sendJson(res, 200, {
2275
+ ok: true,
2276
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2277
+ connected: true,
2278
+ account: retrieved,
2279
+ publishable_key: config.stripePublishableKey || null,
2280
+ });
2281
+ return;
2282
+ }
2283
+ if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "POST") {
2284
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2285
+ const body = JSON.parse((await readBody(req)) || "{}");
2286
+ const created = await engine.stripeProjects.createConnectedAccount({
2287
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
2288
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
2289
+ country: body.country ? String(body.country) : "US",
2290
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
2291
+ apiKeyOverride: stripeByok,
2292
+ });
2293
+ if (created.id) {
2294
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
2295
+ }
2296
+ sendJson(res, 201, {
2297
+ ok: true,
2298
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2299
+ ...created,
2300
+ });
2301
+ return;
2302
+ }
2303
+ if (pathname === "/api/v1/provisioning/connected_account_links" && req.method === "POST") {
2304
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2305
+ let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
2306
+ config.stripeConnectedAccountId ||
2307
+ "";
2308
+ const body = JSON.parse((await readBody(req)) || "{}");
2309
+ if (!verifiedAccount && !body.account && body.auto_create === true) {
2310
+ const created = await engine.stripeProjects.createConnectedAccount({
2311
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
2312
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
2313
+ country: body.country ? String(body.country) : "US",
2314
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
2315
+ apiKeyOverride: stripeByok,
2316
+ });
2317
+ if (created.id) {
2318
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
2319
+ verifiedAccount = created.id;
2320
+ }
2321
+ }
2322
+ const accountId = String(verifiedAccount || body.account || "").trim();
2323
+ if (!accountId) {
2324
+ sendJson(res, 400, {
2325
+ ok: false,
2326
+ error: "No verified connected_account_id is mapped to this tenant workspace.",
2327
+ });
2328
+ return;
2329
+ }
2330
+ const link = await engine.stripeProjects.createConnectedAccountLink({
2331
+ account: accountId,
2332
+ type: body.type === "account_update" ? "account_update" : "account_onboarding",
2333
+ fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
2334
+ future_requirements: body.future_requirements === "omit"
2335
+ ? "omit"
2336
+ : body.future_requirements === "include"
2337
+ ? "include"
2338
+ : undefined,
2339
+ refresh_url: String(body.refresh_url || `${config.publicWebUrl}/?onboarding=refresh`),
2340
+ return_url: String(body.return_url || `${config.publicWebUrl}/?onboarding=return`),
2341
+ apiKeyOverride: stripeByok,
2342
+ });
2343
+ sendJson(res, 201, {
2344
+ ok: true,
2345
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2346
+ ...link,
2347
+ });
2348
+ return;
2349
+ }
2350
+ if (pathname === "/api/v1/provisioning/connected_account_sessions" && req.method === "POST") {
2351
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2352
+ let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
2353
+ config.stripeConnectedAccountId ||
2354
+ "";
2355
+ const body = JSON.parse((await readBody(req)) || "{}");
2356
+ if (!verifiedAccount && !body.account && body.auto_create !== false) {
2357
+ const created = await engine.stripeProjects.createConnectedAccount({
2358
+ contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
2359
+ display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
2360
+ country: body.country ? String(body.country) : "US",
2361
+ entity_type: body.entity_type === "individual" ? "individual" : "company",
2362
+ apiKeyOverride: stripeByok,
2363
+ });
2364
+ if (created.id) {
2365
+ store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
2366
+ verifiedAccount = created.id;
2367
+ }
2368
+ }
2369
+ const accountId = String(verifiedAccount || body.account || "").trim();
2370
+ if (!accountId) {
2371
+ sendJson(res, 400, {
2372
+ ok: false,
2373
+ error: "No verified connected_account_id is mapped to this tenant workspace.",
2374
+ });
2375
+ return;
2376
+ }
2377
+ const session = await engine.stripeProjects.createConnectedAccountSession({
2378
+ account: accountId,
2379
+ enableAccountManagement: Boolean(body.enable_account_management ?? body.enableAccountManagement),
2380
+ enableNotificationBanner: body.enable_notification_banner !== undefined
2381
+ ? Boolean(body.enable_notification_banner)
2382
+ : body.enableNotificationBanner !== undefined
2383
+ ? Boolean(body.enableNotificationBanner)
2384
+ : undefined,
2385
+ disableStripeUserAuthentication: body.disable_stripe_user_authentication !== undefined
2386
+ ? Boolean(body.disable_stripe_user_authentication)
2387
+ : body.disableStripeUserAuthentication !== undefined
2388
+ ? Boolean(body.disableStripeUserAuthentication)
2389
+ : undefined,
2390
+ externalAccountCollection: body.external_account_collection !== undefined
2391
+ ? Boolean(body.external_account_collection)
2392
+ : body.externalAccountCollection !== undefined
2393
+ ? Boolean(body.externalAccountCollection)
2394
+ : undefined,
2395
+ apiKeyOverride: stripeByok,
2396
+ });
2397
+ sendJson(res, 201, {
2398
+ ok: true,
2399
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2400
+ publishable_key: config.stripePublishableKey || null,
2401
+ embedded_component: "account-onboarding",
2402
+ collection_options: {
2403
+ fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
2404
+ future_requirements: body.future_requirements === "omit" ? "omit" : "include",
2405
+ },
2406
+ ...session,
2407
+ });
2408
+ return;
2409
+ }
2410
+ const connAccountGetMatch = pathname.match(/^\/api\/v1\/provisioning\/connected_accounts\/([^/]+)$/);
2411
+ if (connAccountGetMatch && req.method === "GET") {
2412
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2413
+ const accountId = decodeURIComponent(connAccountGetMatch[1]);
2414
+ const retrieved = await engine.stripeProjects.retrieveConnectedAccount(accountId, stripeByok);
2415
+ sendJson(res, 200, {
2416
+ ok: true,
2417
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2418
+ ...retrieved,
2419
+ });
2420
+ return;
2421
+ }
2422
+ if (pathname === "/api/v1/provisioning/eligibility" && req.method === "GET") {
2423
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2424
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2425
+ const eligibility = await engine.stripeProjects.checkEligibility(stripeByok, verifiedContext);
2426
+ sendJson(res, 200, {
2427
+ ok: true,
2428
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2429
+ ...eligibility,
2430
+ });
2431
+ return;
2432
+ }
2433
+ if ((pathname === "/api/v1/provisioning/catalog/providers" ||
2434
+ pathname === "/api/v1/provisioning/providers") &&
2435
+ req.method === "GET") {
2436
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2437
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2438
+ const catalog = url.searchParams.get("catalog") || "prod";
2439
+ const limit = Number(url.searchParams.get("limit") || 100);
2440
+ const page = url.searchParams.get("page") || undefined;
2441
+ const page_url = url.searchParams.get("page_url") || undefined;
2442
+ const development = url.searchParams.get("development") !== null
2443
+ ? url.searchParams.get("development") === "true"
2444
+ : undefined;
2445
+ const providers = await engine.stripeProjects.listCatalogProviders({
2446
+ catalog,
2447
+ development,
2448
+ limit,
2449
+ page,
2450
+ page_url,
2451
+ apiKeyOverride: stripeByok,
2452
+ stripeContext: verifiedContext,
2453
+ });
2454
+ sendJson(res, 200, {
2455
+ ok: true,
2456
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2457
+ ...providers,
2458
+ });
2459
+ return;
2460
+ }
2461
+ if ((pathname === "/api/v1/provisioning/catalog/services" ||
2462
+ pathname === "/api/v1/provisioning/services") &&
2463
+ req.method === "GET") {
2464
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2465
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2466
+ const catalog = url.searchParams.get("catalog") || "prod";
2467
+ const limit = Number(url.searchParams.get("limit") || 100);
2468
+ const page = url.searchParams.get("page") || undefined;
2469
+ const page_url = url.searchParams.get("page_url") || undefined;
2470
+ const development = url.searchParams.get("development") !== null
2471
+ ? url.searchParams.get("development") === "true"
2472
+ : undefined;
2473
+ const provider_id = url.searchParams.get("provider_id") || undefined;
2474
+ const provider_name = url.searchParams.get("provider_name") || undefined;
2475
+ const services = await engine.stripeProjects.listCatalogServices({
2476
+ catalog,
2477
+ development,
2478
+ limit,
2479
+ page,
2480
+ page_url,
2481
+ provider_id,
2482
+ provider_name,
2483
+ apiKeyOverride: stripeByok,
2484
+ stripeContext: verifiedContext,
2485
+ });
2486
+ sendJson(res, 200, {
2487
+ ok: true,
2488
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2489
+ ...services,
2490
+ });
2491
+ return;
2492
+ }
2493
+ if (pathname === "/api/v1/provisioning/payment_profile" && req.method === "GET") {
2494
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2495
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2496
+ const provider = url.searchParams.get("provider") || undefined;
2497
+ const livemodeParam = url.searchParams.get("livemode");
2498
+ const livemode = livemodeParam !== null ? livemodeParam === "true" : undefined;
2499
+ const profile = await engine.stripeProjects.getPaymentProfile({
2500
+ provider,
2501
+ livemode,
2502
+ apiKeyOverride: stripeByok,
2503
+ stripeContext: verifiedContext,
2504
+ });
2505
+ sendJson(res, 200, {
2506
+ ok: true,
2507
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2508
+ ...profile,
2509
+ });
2510
+ return;
2511
+ }
2512
+ if (pathname === "/api/v1/provisioning/payment_profile/update_limit" &&
2513
+ req.method === "POST") {
2514
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2515
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2516
+ const body = JSON.parse((await readBody(req)) || "{}");
2517
+ const usageLimits = body.usage_limits || {
2518
+ max_amount: String(body.max_amount ?? "5000"),
2519
+ currency: String(body.currency || "usd"),
2520
+ recurring_interval: String(body.recurring_interval || "month"),
2521
+ };
2522
+ const updated = await engine.stripeProjects.updatePaymentProfileLimit({
2523
+ usage_limits: usageLimits,
2524
+ provider: body.provider ? String(body.provider) : undefined,
2525
+ livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
2526
+ apiKeyOverride: stripeByok,
2527
+ stripeContext: verifiedContext,
2528
+ });
2529
+ sendJson(res, 200, {
2530
+ ok: true,
2531
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2532
+ ...updated,
2533
+ });
2534
+ return;
2535
+ }
2536
+ if (pathname === "/api/v1/provisioning/payment_method_requests" &&
2537
+ req.method === "POST") {
2538
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2539
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
2540
+ const body = JSON.parse((await readBody(req)) || "{}");
2541
+ const created = await engine.stripeProjects.createPaymentMethodRequest({
2542
+ payment_method_owner: body.payment_method_owner === "platform" ||
2543
+ body.payment_method_owner === "provider"
2544
+ ? body.payment_method_owner
2545
+ : undefined,
2546
+ provider: body.provider ? String(body.provider) : undefined,
2547
+ source_account: body.source_account ? String(body.source_account) : undefined,
2548
+ source_customer: body.source_customer ? String(body.source_customer) : undefined,
2549
+ source_payment_method: body.source_payment_method
2550
+ ? String(body.source_payment_method)
2551
+ : undefined,
2552
+ livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
2553
+ usage_limits: body.usage_limits,
2554
+ apiKeyOverride: stripeByok,
2555
+ stripeContext: verifiedContext,
2556
+ });
2557
+ sendJson(res, 201, {
2558
+ ok: true,
2559
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2560
+ ...created,
2561
+ });
2562
+ return;
2563
+ }
2564
+ if (pathname === "/api/v1/provisioning/projects" && req.method === "GET") {
2565
+ sendJson(res, 200, {
2566
+ ok: true,
2567
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2568
+ object: "v2.list",
2569
+ data: [
2570
+ {
2571
+ id: config.stripeProjectId || `proj_muxo_${ctx.workspace.id}`,
2572
+ object: "v2.provisioning.project",
2573
+ name: `${ctx.workspace.name} Cloud Project`,
2574
+ catalog: "prod",
2575
+ created: ctx.workspace.createdAt,
2576
+ livemode: false,
2577
+ },
2578
+ ],
2579
+ next_page_url: null,
2580
+ previous_page_url: null,
2581
+ });
2582
+ return;
2583
+ }
2584
+ if (pathname === "/api/v1/provisioning/projects" && req.method === "POST") {
2585
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2586
+ const body = JSON.parse((await readBody(req)) || "{}");
2587
+ const created = await engine.stripeProjects.createProject({
2588
+ name: String(body.name || "Muxo Cloud Project"),
2589
+ catalog: body.catalog || "prod",
2590
+ project_profile: body.project_profile
2591
+ ? String(body.project_profile)
2592
+ : undefined,
2593
+ apiKeyOverride: stripeByok,
2594
+ });
2595
+ sendJson(res, 201, {
2596
+ ok: true,
2597
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2598
+ ...created,
2599
+ });
2600
+ return;
2601
+ }
2602
+ if (pathname === "/api/v1/provisioning/provider_connections" &&
2603
+ req.method === "GET") {
2604
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2605
+ const limit = Number(url.searchParams.get("limit") || 25);
2606
+ const page = url.searchParams.get("page") || undefined;
2607
+ const provider = url.searchParams.get("provider") || undefined;
2608
+ const connections = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2609
+ ? await engine.stripeProjects.listProviderConnections({
2610
+ limit,
2611
+ page,
2612
+ provider,
2613
+ apiKeyOverride: stripeByok,
2614
+ })
2615
+ : managedProviderConnectionList();
2616
+ sendJson(res, 200, {
2617
+ ok: true,
2618
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2619
+ ...connections,
2620
+ });
2621
+ return;
2622
+ }
2623
+ const provConnUnlinkMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connections\/([^/]+)\/unlink$/);
2624
+ if (provConnUnlinkMatch && req.method === "POST") {
2625
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2626
+ const id = decodeURIComponent(provConnUnlinkMatch[1]);
2627
+ const unlinked = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2628
+ ? await engine.stripeProjects.unlinkProviderConnection(id, stripeByok)
2629
+ : managedProviderConnectionUnlink(id);
2630
+ sendJson(res, 200, {
2631
+ ok: true,
2632
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2633
+ ...unlinked,
2634
+ });
2635
+ return;
2636
+ }
2637
+ if (pathname === "/api/v1/provisioning/provider_connection_requests" &&
2638
+ req.method === "POST") {
2639
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2640
+ const body = JSON.parse((await readBody(req)) || "{}");
2641
+ const reqRes = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2642
+ ? await engine.stripeProjects.createProviderConnectionRequest({
2643
+ provider: String(body.provider || ""),
2644
+ configuration: body.configuration && typeof body.configuration === "object"
2645
+ ? body.configuration
2646
+ : {},
2647
+ project: body.project ? String(body.project) : undefined,
2648
+ apiKeyOverride: stripeByok,
2649
+ })
2650
+ : managedProviderConnectionRequest({ provider: String(body.provider || "") });
2651
+ sendJson(res, 201, {
2652
+ ok: true,
2653
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2654
+ ...reqRes,
2655
+ });
2656
+ return;
2657
+ }
2658
+ const provConnReqSubmitMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)\/submit_information$/);
2659
+ if (provConnReqSubmitMatch && req.method === "POST") {
2660
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2661
+ const id = decodeURIComponent(provConnReqSubmitMatch[1]);
2662
+ const body = JSON.parse((await readBody(req)) || "{}");
2663
+ const submitted = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2664
+ ? await engine.stripeProjects.submitProviderConnectionRequestInformation(id, body.submitted_information || body, stripeByok)
2665
+ : managedProviderConnectionRequest({ id, requestStatus: "complete" });
2666
+ sendJson(res, 200, {
2667
+ ok: true,
2668
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2669
+ ...submitted,
2670
+ });
2671
+ return;
2672
+ }
2673
+ const provConnReqGetMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)$/);
2674
+ if (provConnReqGetMatch && req.method === "GET") {
2675
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2676
+ const id = decodeURIComponent(provConnReqGetMatch[1]);
2677
+ const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2678
+ ? await engine.stripeProjects.retrieveProviderConnectionRequest(id, stripeByok)
2679
+ : managedProviderConnectionRequest({ id });
2680
+ sendJson(res, 200, {
2681
+ ok: true,
2682
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2683
+ ...retrieved,
2684
+ });
2685
+ return;
2686
+ }
2687
+ const revealAccessMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/reveal_access_configuration$/);
2688
+ if (revealAccessMatch && req.method === "POST") {
2689
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2690
+ const resourceId = decodeURIComponent(revealAccessMatch[1]);
2691
+ const anyRes = store.findResourceAnyWorkspace(resourceId);
2692
+ if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
2693
+ sendJson(res, 403, {
2694
+ ok: false,
2695
+ code: "resource_workspace_mismatch",
2696
+ error: "Resource belongs to a different workspace.",
2697
+ });
2698
+ return;
2699
+ }
2700
+ if (engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
2701
+ try {
2702
+ const revealed = await engine.stripeProjects.revealAccessConfiguration(resourceId, stripeByok);
2703
+ sendJson(res, 200, {
2704
+ ok: true,
2705
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2706
+ source: "stripe_v2_api",
2707
+ ...revealed,
2708
+ });
2709
+ return;
2710
+ }
2711
+ catch {
2712
+ // Fall through to workspace vault lookup if Stripe v2 returns error
2713
+ }
2714
+ }
2715
+ const resourceSpecific = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
2716
+ const vaultSecrets = Object.keys(resourceSpecific).length > 0
2717
+ ? resourceSpecific
2718
+ : store.getDecryptedWorkspaceEnv(ctx.workspace.id);
2719
+ sendJson(res, 200, {
2720
+ ok: true,
2721
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2722
+ object: "v2.provisioning.resource.access_configuration",
2723
+ resource: resourceId,
2724
+ configuration: vaultSecrets,
2725
+ created: new Date().toISOString(),
2726
+ expires_at: null,
2727
+ livemode: false,
2728
+ source: "muxo_managed",
2729
+ });
947
2730
  return;
948
2731
  }
949
- if (pathname === "/api/v1/connections" && req.method === "POST") {
2732
+ const resourceActionMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/(remove|unlink|rotate_credentials|submit_information)$/);
2733
+ if (resourceActionMatch && req.method === "POST") {
2734
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2735
+ const resourceId = decodeURIComponent(resourceActionMatch[1]);
2736
+ const action = resourceActionMatch[2];
950
2737
  const body = JSON.parse((await readBody(req)) || "{}");
951
- const connection = await engine.startConnection(ctx, {
952
- provider: String(body.provider || ""),
953
- lane: body.lane,
954
- callbackUrl: body.callbackUrl ? String(body.callbackUrl) : undefined,
2738
+ let actionResult;
2739
+ if (!engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
2740
+ actionResult = managedResourceAction(resourceId, action);
2741
+ }
2742
+ else if (action === "remove") {
2743
+ actionResult = await engine.stripeProjects.removeResource(resourceId, stripeByok);
2744
+ }
2745
+ else if (action === "unlink") {
2746
+ actionResult = await engine.stripeProjects.unlinkResource(resourceId, stripeByok);
2747
+ }
2748
+ else if (action === "rotate_credentials") {
2749
+ actionResult = await engine.stripeProjects.rotateResourceCredentials(resourceId, stripeByok);
2750
+ }
2751
+ else {
2752
+ actionResult = await engine.stripeProjects.submitResourceInformation(resourceId, body.submitted_information || body, stripeByok);
2753
+ }
2754
+ sendJson(res, 200, {
2755
+ ok: true,
2756
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2757
+ ...actionResult,
955
2758
  });
956
- sendJson(res, 201, { ok: true, connection });
957
2759
  return;
958
2760
  }
959
- if (pathname === "/api/v1/connections/wait" && req.method === "POST") {
2761
+ if (pathname === "/api/v1/provisioning/resources/link" && req.method === "POST") {
2762
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
960
2763
  const body = JSON.parse((await readBody(req)) || "{}");
961
- const provider = String(body.provider || "");
962
- if (body.simulateCallback === true || body.simulate_callback === true) {
963
- await engine.handleOAuthCallback({
964
- workspaceId: ctx.workspace.id,
965
- provider,
966
- status: "success",
2764
+ const linked = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2765
+ ? managedResource({
2766
+ provider: String(body.provider || ""),
2767
+ serviceRef: String(body.service_ref || ""),
2768
+ environment: body.environment ? String(body.environment) : "prod",
2769
+ catalog: body.catalog ? String(body.catalog) : "prod",
2770
+ status: "active",
2771
+ })
2772
+ : await engine.stripeProjects.linkResource({
2773
+ provider: String(body.provider || ""),
2774
+ service_ref: String(body.service_ref || ""),
2775
+ provider_resource_id: body.provider_resource_id
2776
+ ? String(body.provider_resource_id)
2777
+ : undefined,
2778
+ environment: body.environment ? String(body.environment) : "prod",
2779
+ catalog: body.catalog || "prod",
2780
+ project: body.project ? String(body.project) : undefined,
2781
+ apiKeyOverride: stripeByok,
967
2782
  });
968
- }
969
- const result = await engine.waitForConnection(ctx, {
970
- provider,
971
- timeoutSeconds: body.timeoutSeconds !== undefined
972
- ? Number(body.timeoutSeconds)
973
- : body.timeout_seconds !== undefined
974
- ? Number(body.timeout_seconds)
975
- : 15,
2783
+ sendJson(res, 201, {
2784
+ ok: true,
2785
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2786
+ ...linked,
976
2787
  });
977
- sendJson(res, 200, { ok: true, ...result });
978
2788
  return;
979
2789
  }
980
- // Provisioned Resources & Decrypted Vault Env Pull
981
- if (pathname === "/api/v1/resources" && req.method === "GET") {
2790
+ if (pathname === "/api/v1/provisioning/resources" && req.method === "GET") {
2791
+ const resources = store.listResources(ctx.workspace.id);
982
2792
  sendJson(res, 200, {
983
2793
  ok: true,
984
- resources: store.listResources(ctx.workspace.id),
2794
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2795
+ object: "v2.list",
2796
+ data: resources,
2797
+ next_page_url: null,
2798
+ previous_page_url: null,
985
2799
  });
986
2800
  return;
987
2801
  }
988
- if (pathname === "/api/v1/resources/env" && req.method === "GET") {
989
- // Also pull any fresh .env from Stripe Projects workdir if present
990
- const workdirSecrets = await engine.stripeProjects
991
- .pullEnvVarsFromWorkdir()
992
- .catch(() => ({}));
993
- const vaultSecrets = store.getDecryptedWorkspaceEnv(ctx.workspace.id);
994
- const merged = { ...workdirSecrets, ...vaultSecrets };
995
- const dotenvLines = [
996
- `# Muxo Encrypted Vault Export — Workspace ${ctx.workspace.slug} (${ctx.workspace.id})`,
997
- `# Generated at ${new Date().toISOString()}`,
998
- ...Object.entries(merged).map(([k, v]) => `${k}=${JSON.stringify(v)}`),
999
- ];
2802
+ if (pathname === "/api/v1/provisioning/resources" && req.method === "POST") {
2803
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2804
+ const body = JSON.parse((await readBody(req)) || "{}");
2805
+ const providerSlug = String(body.provider || "turso").toLowerCase();
2806
+ const serviceRef = String(body.service_ref || "database").toLowerCase();
2807
+ const resourceName = body.name ? String(body.name) : `${providerSlug}-${serviceRef}`;
2808
+ const created = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2809
+ ? managedResource({
2810
+ provider: providerSlug,
2811
+ serviceRef,
2812
+ name: resourceName,
2813
+ environment: body.environment ? String(body.environment) : "prod",
2814
+ catalog: body.catalog ? String(body.catalog) : "prod",
2815
+ })
2816
+ : await engine.stripeProjects.createResource({
2817
+ provider: providerSlug,
2818
+ service_ref: serviceRef,
2819
+ configuration: body.configuration && typeof body.configuration === "object"
2820
+ ? body.configuration
2821
+ : {},
2822
+ name: resourceName,
2823
+ environment: body.environment ? String(body.environment) : "prod",
2824
+ catalog: body.catalog || "prod",
2825
+ project: body.project ? String(body.project) : undefined,
2826
+ apiKeyOverride: stripeByok,
2827
+ });
2828
+ const externalId = String(created.id || `prv_res_${Date.now().toString(36)}`);
2829
+ const envKey = `${providerSlug.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
2830
+ store.upsertResource({
2831
+ workspaceId: ctx.workspace.id,
2832
+ providerSlug,
2833
+ providerName: providerSlug.toUpperCase(),
2834
+ serviceSlug: serviceRef,
2835
+ resourceName,
2836
+ status: "active",
2837
+ externalResourceId: externalId,
2838
+ secrets: { [envKey]: `https://${resourceName}.${providerSlug}.muxo.internal` },
2839
+ metadata: { source: created.source || "muxo_managed" },
2840
+ });
2841
+ sendJson(res, 201, {
2842
+ ok: true,
2843
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2844
+ ...created,
2845
+ });
2846
+ return;
2847
+ }
2848
+ const resourceIdMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)$/);
2849
+ if (resourceIdMatch && (req.method === "GET" || req.method === "POST")) {
2850
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
2851
+ const resourceId = decodeURIComponent(resourceIdMatch[1]);
2852
+ if (req.method === "GET") {
2853
+ const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2854
+ ? await engine.stripeProjects.retrieveResource(resourceId, stripeByok)
2855
+ : managedResource({ id: resourceId });
2856
+ sendJson(res, 200, {
2857
+ ok: true,
2858
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2859
+ ...retrieved,
2860
+ });
2861
+ return;
2862
+ }
2863
+ const body = JSON.parse((await readBody(req)) || "{}");
2864
+ const updated = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
2865
+ ? await engine.stripeProjects.updateResource(resourceId, body.configuration || body, stripeByok)
2866
+ : managedResource({ id: resourceId, status: "active" });
1000
2867
  sendJson(res, 200, {
1001
2868
  ok: true,
1002
- workspaceId: ctx.workspace.id,
1003
- variables: merged,
1004
- dotenv: dotenvLines.join("\n") + "\n",
2869
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
2870
+ ...updated,
1005
2871
  });
1006
2872
  return;
1007
2873
  }
1008
2874
  // Governance Policy
2875
+ if (pathname === "/api/v1/policy" && req.method === "GET") {
2876
+ const summary = engine.getGuardrailsSummary(ctx);
2877
+ sendJson(res, 200, {
2878
+ ok: true,
2879
+ policy: summary.policy,
2880
+ plainEnglish: summary.plainEnglish,
2881
+ explanation: summary.plainEnglish.explanation,
2882
+ activeDefaults: summary.activeDefaults,
2883
+ activeGuardrails: summary.activeGuardrails,
2884
+ });
2885
+ return;
2886
+ }
1009
2887
  if (pathname === "/api/v1/policy" && req.method === "POST") {
1010
2888
  const body = JSON.parse((await readBody(req)) || "{}");
1011
- const updated = store.updateWorkspacePolicy(ctx.workspace.id, body);
1012
- sendJson(res, 200, { ok: true, workspace: updated });
2889
+ const updated = store.updateWorkspacePolicy(ctx.workspace.id, sanitizePolicyPatch(body));
2890
+ const plainEnglish = describePolicyInPlainEnglish(updated.policy);
2891
+ sendJson(res, 200, {
2892
+ ok: true,
2893
+ workspace: updated,
2894
+ plainEnglishGuardrails: plainEnglish,
2895
+ explanation: plainEnglish.explanation,
2896
+ });
1013
2897
  return;
1014
2898
  }
1015
2899
  // Plans & Approvals & Execution
@@ -1022,27 +2906,96 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1022
2906
  }
1023
2907
  if (pathname === "/api/v1/plans" && req.method === "POST") {
1024
2908
  const body = JSON.parse((await readBody(req)) || "{}");
2909
+ const rawFunding = body.fundingMode || body.funding_mode;
2910
+ const fundingMode = rawFunding === "byok" || rawFunding === "customer_funded"
2911
+ ? "byok"
2912
+ : rawFunding === "muxo_funded"
2913
+ ? "muxo_funded"
2914
+ : undefined;
1025
2915
  const planned = await engine.planAction(ctx, {
1026
2916
  toolSlug: String(body.toolSlug || body.tool_slug || ""),
1027
2917
  input: body.input || {},
1028
2918
  reason: body.reason ? String(body.reason) : undefined,
2919
+ fundingMode,
1029
2920
  });
1030
2921
  sendJson(res, 201, { ok: true, ...planned });
1031
2922
  return;
1032
2923
  }
2924
+ const resolvePlanCtx = (planId) => {
2925
+ const found = store.getPlanById(planId);
2926
+ if (found &&
2927
+ found.workspaceId !== ctx.workspace.id &&
2928
+ ctx.authType === "session" &&
2929
+ store.userCanAccessWorkspace(ctx.user.id, found.workspaceId)) {
2930
+ const targetWs = store.getWorkspace(found.workspaceId);
2931
+ if (targetWs) {
2932
+ return { ...ctx, workspace: targetWs };
2933
+ }
2934
+ }
2935
+ return ctx;
2936
+ };
1033
2937
  const planApproveMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/approve$/);
1034
2938
  if (planApproveMatch && req.method === "POST") {
1035
2939
  const planId = decodeURIComponent(planApproveMatch[1]);
1036
- const plan = engine.approvePlan(ctx, planId, "approved");
2940
+ const planCtx = resolvePlanCtx(planId);
2941
+ const body = JSON.parse((await readBody(req)) || "{}");
2942
+ const decision = body.decision === "rejected" || body.decision === "reject"
2943
+ ? "rejected"
2944
+ : "approved";
2945
+ const plan = engine.approvePlan(planCtx, planId, decision);
1037
2946
  sendJson(res, 200, { ok: true, plan });
1038
2947
  return;
1039
2948
  }
2949
+ const planWaitMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/wait$/);
2950
+ if (planWaitMatch && (req.method === "POST" || req.method === "GET")) {
2951
+ const planId = decodeURIComponent(planWaitMatch[1]);
2952
+ const planCtx = resolvePlanCtx(planId);
2953
+ const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
2954
+ const rawTimeout = body.timeoutSeconds ??
2955
+ body.timeout_seconds ??
2956
+ url.searchParams.get("timeout_seconds") ??
2957
+ url.searchParams.get("timeoutSeconds");
2958
+ const waited = await engine.waitForPlanApproval(planCtx, {
2959
+ planId,
2960
+ timeoutSeconds: clampWaitSeconds(rawTimeout, 15, 60),
2961
+ });
2962
+ sendJson(res, 200, waited);
2963
+ return;
2964
+ }
1040
2965
  const planExecuteMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/execute$/);
1041
2966
  if (planExecuteMatch && req.method === "POST") {
1042
2967
  const planId = decodeURIComponent(planExecuteMatch[1]);
1043
- const plan = engine.getPlan(ctx, planId);
1044
- const result = await engine.executePlanForRisk(ctx, planId, plan.riskLevel);
1045
- sendJson(res, 200, { ok: true, ...result });
2968
+ const planCtx = resolvePlanCtx(planId);
2969
+ const plan = engine.getPlan(planCtx, planId);
2970
+ const result = await engine.executePlanForRisk(planCtx, planId, plan.riskLevel);
2971
+ sendJson(res, 200, {
2972
+ ok: true,
2973
+ ...result,
2974
+ settlement: result.receipt?.supplierSettlement || null,
2975
+ });
2976
+ return;
2977
+ }
2978
+ const planGetMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)$/);
2979
+ if (planGetMatch && req.method === "GET") {
2980
+ const planId = decodeURIComponent(planGetMatch[1]);
2981
+ const planCtx = resolvePlanCtx(planId);
2982
+ const wait = url.searchParams.get("wait") === "true" ||
2983
+ url.searchParams.get("wait_for_approval") === "true";
2984
+ if (wait) {
2985
+ const waited = await engine.waitForPlanApproval(planCtx, {
2986
+ planId,
2987
+ timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds"), 15, 60),
2988
+ });
2989
+ sendJson(res, 200, waited);
2990
+ return;
2991
+ }
2992
+ const plan = engine.getPlan(planCtx, planId);
2993
+ sendJson(res, 200, {
2994
+ ok: true,
2995
+ approved: APPROVED_PLAN_STATUSES.has(plan.approvalStatus),
2996
+ timedOut: false,
2997
+ plan,
2998
+ });
1046
2999
  return;
1047
3000
  }
1048
3001
  // Runs & Receipts
@@ -1053,6 +3006,35 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1053
3006
  });
1054
3007
  return;
1055
3008
  }
3009
+ const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
3010
+ if (runCancelMatch && req.method === "POST") {
3011
+ const runId = decodeURIComponent(runCancelMatch[1]);
3012
+ const body = JSON.parse((await readBody(req)) || "{}");
3013
+ const cancelled = engine.cancelRun(ctx, runId, body.reason ? String(body.reason) : undefined);
3014
+ sendJson(res, 200, cancelled);
3015
+ return;
3016
+ }
3017
+ const runGetMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
3018
+ if (runGetMatch && req.method === "GET") {
3019
+ const runId = decodeURIComponent(runGetMatch[1]);
3020
+ const run = engine.getRun(ctx, runId);
3021
+ sendJson(res, 200, { ok: true, run });
3022
+ return;
3023
+ }
3024
+ if (pathname === "/api/v1/receipts" && req.method === "GET") {
3025
+ const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit") || 25)));
3026
+ const list = engine.listReceipts(ctx, limit);
3027
+ sendJson(res, 200, { ok: true, ...list });
3028
+ return;
3029
+ }
3030
+ if ((pathname === "/api/v1/receipts/verify-chain" ||
3031
+ pathname === "/api/v1/receipts/chain") &&
3032
+ req.method === "GET") {
3033
+ const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") || 100)));
3034
+ const chain = engine.verifyReceiptChain(ctx, limit);
3035
+ sendJson(res, 200, chain);
3036
+ return;
3037
+ }
1056
3038
  if (pathname.startsWith("/api/v1/receipts/") && req.method === "GET") {
1057
3039
  const receiptId = decodeURIComponent(pathname.slice("/api/v1/receipts/".length));
1058
3040
  const verified = engine.getReceipt(ctx, receiptId);
@@ -1102,7 +3084,13 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1102
3084
  if ((pathname === "/api/v1/do" || pathname === "/v1/do") &&
1103
3085
  req.method === "POST") {
1104
3086
  const body = JSON.parse((await readBody(req)) || "{}");
1105
- const prompt = String(body.prompt || body.instruction || body.query || body.command || "").trim();
3087
+ const prompt = String(body.prompt ||
3088
+ body.instruction ||
3089
+ body.query ||
3090
+ body.command ||
3091
+ body.toolSlug ||
3092
+ body.tool ||
3093
+ "").trim();
1106
3094
  if (!prompt) {
1107
3095
  sendJson(res, 400, {
1108
3096
  ok: false,
@@ -1110,19 +3098,37 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1110
3098
  });
1111
3099
  return;
1112
3100
  }
3101
+ const dryRun = body.dryRun === true ||
3102
+ body.dry_run === true ||
3103
+ body.planOnly === true ||
3104
+ body.autoExecute === false ||
3105
+ body.auto_execute === false;
3106
+ const autoApprove = body.autoApprove !== false &&
3107
+ body.auto_approve !== false &&
3108
+ body.approveRisky !== false &&
3109
+ body.approve_risky !== false;
3110
+ const rawFunding = body.fundingMode || body.funding_mode;
3111
+ const fundingMode = rawFunding === "byok" || rawFunding === "customer_funded"
3112
+ ? "byok"
3113
+ : rawFunding === "muxo_funded"
3114
+ ? "muxo_funded"
3115
+ : undefined;
1113
3116
  const result = await engine.doNaturalLanguage(ctx, {
1114
3117
  prompt,
1115
3118
  input: body.input && typeof body.input === "object"
1116
3119
  ? body.input
1117
3120
  : undefined,
1118
- autoApprove: body.autoApprove !== false,
1119
- dryRun: body.dryRun === true,
3121
+ autoApprove,
3122
+ dryRun,
3123
+ planOnly: dryRun,
3124
+ fundingMode,
1120
3125
  });
1121
3126
  sendJson(res, 200, result);
1122
3127
  return;
1123
3128
  }
1124
3129
  // Live Stripe Billing, Credit Top-Up & Supplier Settlement Ledger
1125
- if (pathname === "/api/v1/billing/status" && req.method === "GET") {
3130
+ if ((pathname === "/api/v1/billing/status" || pathname === "/api/v1/billing") &&
3131
+ req.method === "GET") {
1126
3132
  const ws = store.getWorkspace(ctx.workspace.id);
1127
3133
  const settlementSummary = store.getSupplierSettlementSummary(ctx.workspace.id);
1128
3134
  const byokKeys = store.listByokKeys(ctx.workspace.id);
@@ -1176,6 +3182,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1176
3182
  sendJson(res, 200, {
1177
3183
  ok: true,
1178
3184
  creditsAdded: credits,
3185
+ creditBalance: updatedWorkspace.creditsBalance,
3186
+ creditsBalance: updatedWorkspace.creditsBalance,
1179
3187
  usdPaid: Number(usdPaid),
1180
3188
  workspace: updatedWorkspace,
1181
3189
  settlementSummary: store.getSupplierSettlementSummary(ctx.workspace.id),
@@ -1247,13 +3255,428 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
1247
3255
  sendJson(res, 200, { ok: true, received: true });
1248
3256
  return;
1249
3257
  }
3258
+ // Stripe v2 Billing Meter Event Stream (`POST /v2/billing/meter_event_session` + `POST /v2/billing/meter_event_stream`)
3259
+ if (pathname === "/api/v1/billing/meter_events" && req.method === "GET") {
3260
+ const limit = Number(url.searchParams.get("limit") || 50);
3261
+ const summary = engine.stripeBilling.listMeterEvents(ctx.workspace.id, limit);
3262
+ sendJson(res, 200, {
3263
+ ok: true,
3264
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
3265
+ ...summary,
3266
+ });
3267
+ return;
3268
+ }
3269
+ if (pathname === "/api/v1/billing/meter_events" && req.method === "POST") {
3270
+ const body = JSON.parse((await readBody(req)) || "{}");
3271
+ const result = await engine.stripeBilling.recordMeterEvent({
3272
+ workspace: ctx.workspace,
3273
+ event_name: body.event_name ? String(body.event_name) : undefined,
3274
+ value: Number(body.value ?? 1),
3275
+ lane: body.lane ? String(body.lane) : undefined,
3276
+ tool_slug: body.tool_slug ? String(body.tool_slug) : undefined,
3277
+ identifier: body.identifier ? String(body.identifier) : undefined,
3278
+ timestamp: body.timestamp ? String(body.timestamp) : undefined,
3279
+ });
3280
+ sendJson(res, 201, {
3281
+ ok: true,
3282
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
3283
+ ...result,
3284
+ });
3285
+ return;
3286
+ }
3287
+ // Stripe v2 Event Destinations & Provisioning Thin Event Webhook Handler
3288
+ if (pathname === "/api/v1/provisioning/event_destinations" &&
3289
+ req.method === "POST") {
3290
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
3291
+ const body = JSON.parse((await readBody(req)) || "{}");
3292
+ const destination = await engine.stripeProjects.createV2EventDestination({
3293
+ name: body.name ? String(body.name) : undefined,
3294
+ url: String(body.url || `${config.baseUrl}/api/v1/provisioning/events`),
3295
+ events: Array.isArray(body.events) ? body.events.map(String) : undefined,
3296
+ apiKeyOverride: stripeByok,
3297
+ });
3298
+ sendJson(res, 201, {
3299
+ ok: true,
3300
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
3301
+ ...destination,
3302
+ });
3303
+ return;
3304
+ }
3305
+ if (pathname === "/api/v1/provisioning/events" && req.method === "POST") {
3306
+ const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
3307
+ const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
3308
+ const body = JSON.parse((await readBody(req)) || "{}");
3309
+ const handled = await engine.stripeProjects.handleV2ProvisioningEvent({
3310
+ event: body,
3311
+ apiKeyOverride: stripeByok,
3312
+ stripeContext: verifiedContext,
3313
+ });
3314
+ if (handled.revealedConfiguration &&
3315
+ Object.keys(handled.revealedConfiguration).length > 0 &&
3316
+ handled.relatedObjectId) {
3317
+ store.upsertResource({
3318
+ workspaceId: ctx.workspace.id,
3319
+ resourceName: String(handled.resource?.name || handled.relatedObjectId),
3320
+ providerSlug: String(handled.resource?.provider || "neon"),
3321
+ providerName: String(handled.resource?.provider || "Neon"),
3322
+ serviceSlug: String(handled.resource?.service_ref || "postgres"),
3323
+ planSlug: "launch",
3324
+ status: "active",
3325
+ secrets: handled.revealedConfiguration,
3326
+ externalResourceId: handled.relatedObjectId,
3327
+ metadata: {
3328
+ region: "aws-us-east-1",
3329
+ },
3330
+ });
3331
+ }
3332
+ sendJson(res, 200, {
3333
+ ok: true,
3334
+ apiVersion: engine.stripeProjects.provisioningApiVersion,
3335
+ ...handled,
3336
+ });
3337
+ return;
3338
+ }
3339
+ // Composio Dynamic Enterprise Toolkits, Scoped Auth Configs, Triggers & Multi-Tool Batch Execution
3340
+ if (pathname === "/api/v1/toolkits" && req.method === "GET") {
3341
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
3342
+ const category = url.searchParams.get("category") || undefined;
3343
+ const search = url.searchParams.get("search") || url.searchParams.get("query") || undefined;
3344
+ const catalog = await engine.composioSdk.listToolkits({
3345
+ category,
3346
+ search,
3347
+ composioApiKey: composioByok,
3348
+ });
3349
+ sendJson(res, 200, { ok: true, ...catalog });
3350
+ return;
3351
+ }
3352
+ if (pathname === "/api/v1/connections/auth-configs" && req.method === "POST") {
3353
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
3354
+ const body = JSON.parse((await readBody(req)) || "{}");
3355
+ const created = await engine.composioSdk.createCustomAuthConfig({
3356
+ workspaceId: ctx.workspace.id,
3357
+ toolkitSlug: String(body.toolkitSlug || body.provider || "github"),
3358
+ name: body.name ? String(body.name) : undefined,
3359
+ authScheme: body.authScheme === "API_KEY" ? "API_KEY" : "OAUTH2",
3360
+ scopeMode: body.scopeMode === "full_access" ? "full_access" : "read_only",
3361
+ customScopes: Array.isArray(body.customScopes)
3362
+ ? body.customScopes.map(String)
3363
+ : undefined,
3364
+ credentials: body.credentials && typeof body.credentials === "object"
3365
+ ? body.credentials
3366
+ : undefined,
3367
+ composioApiKey: composioByok,
3368
+ });
3369
+ sendJson(res, 201, { ok: true, ...created });
3370
+ return;
3371
+ }
3372
+ if (pathname === "/api/v1/triggers" && req.method === "GET") {
3373
+ const provider = url.searchParams.get("provider") || undefined;
3374
+ const triggerSlug = url.searchParams.get("triggerSlug") || url.searchParams.get("trigger_slug") || undefined;
3375
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
3376
+ const available = engine.composioSdk.listTriggerTypes(provider);
3377
+ const subscriptions = engine.composioSdk.listActiveTriggers(ctx.workspace.id);
3378
+ const events = engine.composioSdk.listTriggerEvents(ctx.workspace.id, 25);
3379
+ const triggerType = triggerSlug
3380
+ ? await engine.composioSdk.getTriggerType(triggerSlug, composioByok)
3381
+ : undefined;
3382
+ sendJson(res, 200, {
3383
+ ok: true,
3384
+ availableTriggers: available,
3385
+ activeSubscriptions: subscriptions,
3386
+ recentEvents: events,
3387
+ webhookConfig: engine.composioSdk.getWebhookConfigInfo(),
3388
+ ...(triggerType ? { triggerType } : {}),
3389
+ });
3390
+ return;
3391
+ }
3392
+ if (pathname === "/api/v1/triggers" && req.method === "POST") {
3393
+ const composioByok = store.getByokKey(ctx.workspace.id, "composio");
3394
+ const body = JSON.parse((await readBody(req)) || "{}");
3395
+ const rawConfig = body.triggerConfig && typeof body.triggerConfig === "object"
3396
+ ? body.triggerConfig
3397
+ : body.config && typeof body.config === "object"
3398
+ ? body.config
3399
+ : {};
3400
+ const subscription = await engine.composioSdk.enableTrigger({
3401
+ workspaceId: ctx.workspace.id,
3402
+ userId: body.userId ? String(body.userId) : undefined,
3403
+ triggerSlug: String(body.triggerSlug || body.trigger_slug || "GITHUB_COMMIT_EVENT"),
3404
+ providerSlug: String(body.providerSlug || body.provider || "github"),
3405
+ connectedAccountId: body.connectedAccountId
3406
+ ? String(body.connectedAccountId)
3407
+ : undefined,
3408
+ webhookUrl: body.webhookUrl ? String(body.webhookUrl) : undefined,
3409
+ config: rawConfig,
3410
+ subscribe: body.subscribe === true,
3411
+ composioApiKey: composioByok,
3412
+ });
3413
+ sendJson(res, 201, { ok: true, subscription });
3414
+ return;
3415
+ }
3416
+ if ((pathname === "/api/v1/triggers/webhook" ||
3417
+ pathname === "/webhooks" ||
3418
+ pathname === "/webhooks/composio") &&
3419
+ req.method === "POST") {
3420
+ const rawBody = await readBody(req);
3421
+ const sigHeader = String(req.headers["webhook-signature"] ||
3422
+ req.headers["x-composio-webhook-signature"] ||
3423
+ "").trim();
3424
+ const webhookIdHeader = String(req.headers["webhook-id"] || req.headers["x-composio-webhook-id"] || "").trim();
3425
+ const webhookTimestampHeader = String(req.headers["webhook-timestamp"] ||
3426
+ req.headers["x-composio-webhook-timestamp"] ||
3427
+ "").trim();
3428
+ const webhookVersionHeader = String(req.headers["x-composio-webhook-version"] || "").trim();
3429
+ let signatureVerified = false;
3430
+ if (sigHeader) {
3431
+ const check = await verifyComposioWebhookSignature({
3432
+ rawPayload: rawBody,
3433
+ signatureHeader: sigHeader,
3434
+ webhookIdHeader,
3435
+ webhookTimestampHeader,
3436
+ secret: config.composioWebhookSecret,
3437
+ });
3438
+ if (config.composioWebhookSecret && !check.verified) {
3439
+ sendJson(res, 401, {
3440
+ ok: false,
3441
+ error: `Invalid Composio webhook signature (${check.reason || "verification_failed"})`,
3442
+ });
3443
+ return;
3444
+ }
3445
+ signatureVerified = check.verified;
3446
+ }
3447
+ const body = JSON.parse(rawBody || "{}");
3448
+ const envelope = parseComposioWebhookEnvelope(body, {
3449
+ webhookId: webhookIdHeader || undefined,
3450
+ webhookTimestamp: webhookTimestampHeader || undefined,
3451
+ webhookVersion: webhookVersionHeader || undefined,
3452
+ });
3453
+ let targetWorkspaceId = ctx.workspace.id;
3454
+ if (envelope.userId && store.getWorkspace(envelope.userId)) {
3455
+ targetWorkspaceId = envelope.userId;
3456
+ }
3457
+ if (envelope.eventType === "composio.connected_account.activated" ||
3458
+ envelope.eventType === "composio.connected_account.expired") {
3459
+ const nextStatus = envelope.eventType === "composio.connected_account.activated"
3460
+ ? "active"
3461
+ : "expired";
3462
+ store.upsertConnection({
3463
+ workspaceId: targetWorkspaceId,
3464
+ lane: "composio_sdk",
3465
+ providerSlug: envelope.providerSlug,
3466
+ providerName: envelope.providerSlug.charAt(0).toUpperCase() +
3467
+ envelope.providerSlug.slice(1),
3468
+ status: nextStatus,
3469
+ externalAccountId: envelope.connectedAccountId || null,
3470
+ metadata: {
3471
+ webhookEvent: envelope.eventType,
3472
+ webhookId: envelope.webhookId,
3473
+ updatedViaWebhookAt: new Date().toISOString(),
3474
+ },
3475
+ });
3476
+ if (nextStatus === "expired") {
3477
+ engine.composioSdk.disableTriggersMatching({
3478
+ workspaceId: targetWorkspaceId,
3479
+ connectedAccountId: envelope.connectedAccountId,
3480
+ });
3481
+ }
3482
+ }
3483
+ else if (envelope.eventType === "composio.trigger.disabled") {
3484
+ engine.composioSdk.disableTriggersMatching({
3485
+ workspaceId: targetWorkspaceId,
3486
+ triggerId: envelope.triggerId,
3487
+ triggerSlug: envelope.triggerSlug,
3488
+ connectedAccountId: envelope.connectedAccountId,
3489
+ });
3490
+ }
3491
+ const rawPayloadText = JSON.stringify(envelope.payloadObj);
3492
+ const injectionScan = engine.governance.scanMemoryContent({
3493
+ workspaceId: targetWorkspaceId,
3494
+ content: rawPayloadText,
3495
+ });
3496
+ const disposition = injectionScan.safe ? "dispatched" : "quarantined";
3497
+ const eventRecord = {
3498
+ id: `trgevt_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
3499
+ workspaceId: targetWorkspaceId,
3500
+ eventType: envelope.eventType,
3501
+ webhookVersion: envelope.webhookVersion,
3502
+ signatureVerified,
3503
+ webhookId: envelope.webhookId,
3504
+ triggerId: envelope.triggerId,
3505
+ triggerSlug: envelope.triggerSlug,
3506
+ providerSlug: envelope.providerSlug,
3507
+ connectedAccountId: envelope.connectedAccountId,
3508
+ userId: envelope.userId,
3509
+ projectId: envelope.projectId,
3510
+ orgId: envelope.orgId,
3511
+ receivedAt: new Date().toISOString(),
3512
+ promptInjectionScan: {
3513
+ safe: injectionScan.safe,
3514
+ riskScore: injectionScan.findingsCount * 0.35,
3515
+ findings: injectionScan.findings.map((f) => `${f.patternId}: ${f.description}`),
3516
+ },
3517
+ disposition: disposition,
3518
+ payloadPreview: envelope.payloadObj,
3519
+ };
3520
+ engine.composioSdk.recordTriggerEvent(eventRecord);
3521
+ sendJson(res, injectionScan.safe ? 200 : 202, {
3522
+ ok: true,
3523
+ ...eventRecord,
3524
+ });
3525
+ return;
3526
+ }
3527
+ if (pathname === "/api/v1/plans/batch" && req.method === "POST") {
3528
+ const body = JSON.parse((await readBody(req)) || "{}");
3529
+ const batchResult = await engine.executeBatchPlans(ctx, {
3530
+ items: Array.isArray(body.items) ? body.items : [],
3531
+ // Only a signed-in person may pre-approve pending plans; agent keys never can.
3532
+ autoApprove: body.autoApprove === true && signedInPerson(auth),
3533
+ });
3534
+ sendJson(res, 200, { ok: true, ...batchResult });
3535
+ return;
3536
+ }
3537
+ // Cloudflare Vectorize Hybrid Semantic + Bi-Temporal Recall, Durable Workflows, and R2 WORM Audit Archive
3538
+ if (pathname === "/api/v1/memory/hybrid" && (req.method === "GET" || req.method === "POST")) {
3539
+ const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
3540
+ const query = String(body.query ?? body.q ?? url.searchParams.get("query") ?? url.searchParams.get("q") ?? "");
3541
+ const limit = Number(body.limit ?? url.searchParams.get("limit") ?? 10);
3542
+ const hybrid = engine.searchMemoryHybrid(ctx.workspace.id, query, limit);
3543
+ sendJson(res, 200, { ok: true, ...hybrid });
3544
+ return;
3545
+ }
3546
+ if (pathname === "/api/v1/workflows" && req.method === "GET") {
3547
+ const workflows = engine.listDurableWorkflows(ctx.workspace.id);
3548
+ sendJson(res, 200, { ok: true, workflows });
3549
+ return;
3550
+ }
3551
+ if (pathname === "/api/v1/workflows" && req.method === "POST") {
3552
+ const body = JSON.parse((await readBody(req)) || "{}");
3553
+ const workflow = await engine.startDurableWorkflow(ctx, {
3554
+ provider: body.provider ? String(body.provider) : undefined,
3555
+ serviceRef: body.serviceRef || body.service_ref ? String(body.serviceRef || body.service_ref) : undefined,
3556
+ name: body.name ? String(body.name) : undefined,
3557
+ region: body.region ? String(body.region) : undefined,
3558
+ });
3559
+ sendJson(res, 201, { ok: true, workflow });
3560
+ return;
3561
+ }
3562
+ const wfAdvanceMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/advance$/);
3563
+ if (wfAdvanceMatch && req.method === "POST") {
3564
+ const wfId = decodeURIComponent(wfAdvanceMatch[1]);
3565
+ const body = JSON.parse((await readBody(req)) || "{}");
3566
+ const workflow = await engine.advanceDurableWorkflow(ctx, wfId, body);
3567
+ sendJson(res, 200, { ok: true, workflow });
3568
+ return;
3569
+ }
3570
+ const wfHistoryMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/history$/);
3571
+ if (wfHistoryMatch && req.method === "GET") {
3572
+ const wfId = decodeURIComponent(wfHistoryMatch[1]);
3573
+ const history = engine.getDurableWorkflowHistory(ctx, wfId);
3574
+ sendJson(res, 200, { ok: true, ...history });
3575
+ return;
3576
+ }
3577
+ const wfSignalMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/signal$/);
3578
+ if (wfSignalMatch && req.method === "POST") {
3579
+ const wfId = decodeURIComponent(wfSignalMatch[1]);
3580
+ const body = JSON.parse((await readBody(req)) || "{}");
3581
+ const signalName = String(body.signalName || body.signal_name || body.name || "workflow.signal");
3582
+ const workflow = engine.signalDurableWorkflow(ctx, wfId, {
3583
+ signalName,
3584
+ payload: body.payload && typeof body.payload === "object"
3585
+ ? body.payload
3586
+ : body,
3587
+ });
3588
+ sendJson(res, 200, { ok: true, workflow });
3589
+ return;
3590
+ }
3591
+ const wfRollbackMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/rollback$/);
3592
+ if (wfRollbackMatch && req.method === "POST") {
3593
+ const wfId = decodeURIComponent(wfRollbackMatch[1]);
3594
+ const body = JSON.parse((await readBody(req)) || "{}");
3595
+ const workflow = engine.rollbackDurableWorkflow(ctx, wfId, {
3596
+ reason: body.reason ? String(body.reason) : undefined,
3597
+ failedStep: body.failedStep || body.failed_step
3598
+ ? String(body.failedStep || body.failed_step)
3599
+ : undefined,
3600
+ });
3601
+ sendJson(res, 200, { ok: true, workflow });
3602
+ return;
3603
+ }
3604
+ if ((pathname === "/api/v1/governance/cedar" ||
3605
+ pathname === "/api/v1/policy/cedar") &&
3606
+ req.method === "GET") {
3607
+ const bundle = engine.getPolicyAsCodeBundle(ctx);
3608
+ sendJson(res, 200, { ok: true, ...bundle });
3609
+ return;
3610
+ }
3611
+ if ((pathname === "/api/v1/governance/cedar/evaluate" ||
3612
+ pathname === "/api/v1/policy/cedar/evaluate") &&
3613
+ req.method === "POST") {
3614
+ const body = JSON.parse((await readBody(req)) || "{}");
3615
+ const evaluated = await engine.evaluatePolicyAsCode(ctx, {
3616
+ toolSlug: String(body.toolSlug || body.tool_slug || body.action || "instant.web.search"),
3617
+ input: body.input && typeof body.input === "object"
3618
+ ? body.input
3619
+ : {},
3620
+ riskLevel: (body.riskLevel || body.risk_level),
3621
+ creditCost: typeof body.creditCost === "number"
3622
+ ? body.creditCost
3623
+ : typeof body.credit_cost === "number"
3624
+ ? body.credit_cost
3625
+ : undefined,
3626
+ lane: body.lane ? String(body.lane) : undefined,
3627
+ });
3628
+ sendJson(res, 200, { ok: true, ...evaluated });
3629
+ return;
3630
+ }
3631
+ if ((pathname === "/api/v1/telemetry/otlp" ||
3632
+ pathname === "/api/v1/governance/telemetry") &&
3633
+ req.method === "GET") {
3634
+ const limit = Number(url.searchParams.get("limit") || 25);
3635
+ const otel = engine.exportOtelTraces(ctx, limit);
3636
+ sendJson(res, 200, { ok: true, ...otel });
3637
+ return;
3638
+ }
3639
+ if (pathname === "/api/v1/playbooks" && req.method === "GET") {
3640
+ const playbooks = engine.listCommercialPlaybooks();
3641
+ sendJson(res, 200, {
3642
+ ok: true,
3643
+ count: playbooks.length,
3644
+ playbooks,
3645
+ });
3646
+ return;
3647
+ }
3648
+ const playbookRunMatch = pathname.match(/^\/api\/v1\/playbooks\/([^/]+)\/run$/);
3649
+ if ((playbookRunMatch || pathname === "/api/v1/playbooks/run") &&
3650
+ req.method === "POST") {
3651
+ const body = JSON.parse((await readBody(req)) || "{}");
3652
+ const slug = playbookRunMatch
3653
+ ? decodeURIComponent(playbookRunMatch[1])
3654
+ : String(body.slug || body.playbook || "");
3655
+ const result = engine.runCommercialPlaybook(ctx, {
3656
+ slug,
3657
+ input: body.input && typeof body.input === "object"
3658
+ ? body.input
3659
+ : body,
3660
+ });
3661
+ sendJson(res, 201, result);
3662
+ return;
3663
+ }
3664
+ if ((pathname === "/api/v1/analytics/roi" ||
3665
+ pathname === "/api/v1/billing/roi") &&
3666
+ req.method === "GET") {
3667
+ const roi = engine.getCommercialRoiReport(ctx);
3668
+ sendJson(res, 200, { ok: true, ...roi });
3669
+ return;
3670
+ }
3671
+ if (pathname === "/api/v1/governance/archive" && req.method === "GET") {
3672
+ const archive = engine.exportTamperEvidentArchive(ctx.workspace.id);
3673
+ sendJson(res, 200, { ok: true, ...archive });
3674
+ return;
3675
+ }
1250
3676
  sendJson(res, 404, { ok: false, error: `Route not found: ${req.method} ${pathname}` });
1251
3677
  }
1252
3678
  catch (err) {
1253
- sendJson(res, 400, {
1254
- ok: false,
1255
- error: String(err?.message || err || "Internal error"),
1256
- });
3679
+ sendJson(res, err instanceof MuxoToolError ? err.statusCode : 400, errorPayloadFrom(err, "Internal error"));
1257
3680
  }
1258
3681
  });
1259
3682
  return { server, engine, store, defaultApiKey: bootstrap.defaultApiKey };