@muxoai/sdk 1.3.0 → 1.4.2
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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +19 -4
- package/.codex/config.toml +9 -2
- package/ARCHITECTURE.md +16 -6
- package/README.md +69 -24
- package/dist/clerk.d.ts +35 -0
- package/dist/clerk.js +201 -0
- package/dist/cli.js +1589 -103
- package/dist/composio-catalog.d.ts +234 -0
- package/dist/composio-catalog.js +1894 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +27 -1
- package/dist/durable-workflow.d.ts +45 -0
- package/dist/durable-workflow.js +90 -0
- package/dist/engine.d.ts +315 -6
- package/dist/engine.js +1778 -94
- package/dist/mcp-schema.d.ts +286 -0
- package/dist/mcp-schema.js +2559 -0
- package/dist/mcp.d.ts +2 -25
- package/dist/mcp.js +949 -507
- package/dist/memory-graph.d.ts +21 -0
- package/dist/memory-graph.js +173 -0
- package/dist/nl.js +214 -35
- package/dist/oauth.d.ts +32 -0
- package/dist/oauth.js +134 -0
- package/dist/policy.d.ts +190 -0
- package/dist/policy.js +1597 -0
- package/dist/providers/composio-instant.d.ts +4 -0
- package/dist/providers/composio-instant.js +890 -535
- package/dist/providers/composio-sdk.d.ts +126 -5
- package/dist/providers/composio-sdk.js +902 -79
- package/dist/providers/microsoft-governance.d.ts +89 -3
- package/dist/providers/microsoft-governance.js +673 -26
- package/dist/providers/stripe-billing.d.ts +42 -0
- package/dist/providers/stripe-billing.js +244 -5
- package/dist/providers/stripe-projects.d.ts +459 -4
- package/dist/providers/stripe-projects.js +2674 -122
- package/dist/providers/supermemory.d.ts +102 -6
- package/dist/providers/supermemory.js +609 -55
- package/dist/provisioning-managed.d.ts +50 -0
- package/dist/provisioning-managed.js +155 -0
- package/dist/sdk.d.ts +705 -7
- package/dist/sdk.js +488 -10
- package/dist/server.js +2646 -348
- package/dist/store.d.ts +138 -6
- package/dist/store.js +932 -58
- package/dist/types.d.ts +312 -4
- package/dist/ui/console-html.d.ts +62 -0
- package/dist/ui/console-html.js +3742 -2228
- package/dist/ui/design-tokens.d.ts +177 -0
- package/dist/ui/design-tokens.js +210 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +5 -4
- package/plugins/muxo/.app.json +1 -1
- package/plugins/muxo/.claude-plugin/plugin.json +3 -3
- package/plugins/muxo/.codex-plugin/plugin.json +19 -10
- package/plugins/muxo/.cursor-plugin/plugin.json +30 -0
- package/plugins/muxo/.mcp.json +1 -33
- package/plugins/muxo/assets/logo.png +0 -0
- package/plugins/muxo/hooks/session-start.sh +16 -4
- package/plugins/muxo/hooks/user-prompt-submit.sh +14 -1
- package/plugins/muxo/mcp.json +0 -10
- package/plugins/muxo/plugin.json +65 -8
- package/plugins/muxo/skills/muxo/SKILL.md +142 -14
- package/plugins/muxo/skills/muxo/agents/openai.yaml +12 -0
- package/plugins/muxo/skills/muxo/references/cli.md +9 -3
- package/plugins/muxo/skills/muxo/references/mcp.md +22 -10
- package/plugins/muxo/skills/muxo-activity-summary/SKILL.md +81 -0
- package/plugins/muxo/skills/setup/SKILL.md +17 -3
- package/plugins/muxo/skills/setup/agents/openai.yaml +12 -0
- 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 {
|
|
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 (
|
|
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 &
|
|
24
|
+
# Muxo — Unified Agent Execution, Infrastructure, Memory & Safety Platform
|
|
17
25
|
|
|
18
|
-
Muxo provides one
|
|
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
|
|
21
|
-
2. **Connected Apps (\`app.*\`)**: Managed OAuth SaaS integrations
|
|
22
|
-
3. **Cloud Infrastructure (\`resource.*\`)**: Database, cache, storage, and hosting provisioning
|
|
23
|
-
4. **
|
|
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,
|
|
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
|
|
29
|
-
npm install -g
|
|
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 /
|
|
37
|
-
# - ${baseUrl}/.claude-plugin/marketplace.json (Claude Code
|
|
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
|
|
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
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
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:
|
|
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
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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/
|
|
337
|
-
const
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
|
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:
|
|
430
|
-
description: "One governed boundary over Connected Apps
|
|
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
|
|
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
|
|
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/
|
|
459
|
-
"/api/v1/
|
|
460
|
-
"/api/v1/
|
|
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
|
|
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.
|
|
483
|
-
Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing topup 1000\`), and Muxo settles
|
|
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,
|
|
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
|
|
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
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
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 (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
-
|
|
652
|
-
|
|
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
|
|
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(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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:
|
|
1489
|
+
protocolVersion: LATEST_MCP_PROTOCOL_VERSION,
|
|
1490
|
+
supportedVersions: [...SUPPORTED_MCP_PROTOCOL_VERSIONS],
|
|
759
1491
|
serverInfo: {
|
|
760
1492
|
name: "muxo-mcp-server",
|
|
761
|
-
|
|
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
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
|
|
1555
|
+
...responseHeaders,
|
|
793
1556
|
});
|
|
794
1557
|
res.end();
|
|
795
1558
|
return;
|
|
796
1559
|
}
|
|
797
|
-
sendJson(res, 200, response,
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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,16 +1783,155 @@ 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, {
|
|
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:
|
|
871
|
-
|
|
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}`,
|
|
1892
|
+
});
|
|
1893
|
+
return;
|
|
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),
|
|
872
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 });
|
|
873
1935
|
return;
|
|
874
1936
|
}
|
|
875
1937
|
// Supermemory v5 Workspace Brain Endpoints
|
|
@@ -878,16 +1940,105 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
878
1940
|
url.searchParams.get("q") ||
|
|
879
1941
|
undefined;
|
|
880
1942
|
const overview = await engine.getMemoryOverview(ctx, query);
|
|
881
|
-
sendJson(res, 200, {
|
|
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" && req.method === "GET") {
|
|
1952
|
+
const query = url.searchParams.get("query") ||
|
|
1953
|
+
url.searchParams.get("q") ||
|
|
1954
|
+
undefined;
|
|
1955
|
+
const graph = await engine.getMemoryGraph(ctx, query);
|
|
1956
|
+
sendJson(res, 200, { ok: true, ...graph });
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (pathname === "/api/v1/memory/facts" && req.method === "GET") {
|
|
1960
|
+
const query = url.searchParams.get("query") ||
|
|
1961
|
+
url.searchParams.get("q") ||
|
|
1962
|
+
undefined;
|
|
1963
|
+
const includeHistorical = url.searchParams.get("historical") !== "false";
|
|
1964
|
+
const facts = store.listTemporalFacts(ctx.workspace.id, {
|
|
1965
|
+
includeHistorical,
|
|
1966
|
+
subject: url.searchParams.get("subject") || undefined,
|
|
1967
|
+
predicate: url.searchParams.get("predicate") || undefined,
|
|
1968
|
+
query,
|
|
1969
|
+
limit: Number(url.searchParams.get("limit") || 50),
|
|
1970
|
+
});
|
|
1971
|
+
sendJson(res, 200, {
|
|
1972
|
+
ok: true,
|
|
1973
|
+
activeFacts: facts.filter((f) => f.validTo === null),
|
|
1974
|
+
historicalFacts: facts.filter((f) => f.validTo !== null),
|
|
1975
|
+
facts,
|
|
1976
|
+
});
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
if (pathname === "/api/v1/memory/forget" && req.method === "POST") {
|
|
1980
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1981
|
+
const query = String(body.query || body.q || "").trim();
|
|
1982
|
+
if (!query) {
|
|
1983
|
+
sendJson(res, 400, { ok: false, error: "query is required to forget matching memories" });
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const pruned = await engine.forgetWorkspaceMemoryMatching(ctx, query);
|
|
1987
|
+
sendJson(res, 200, { ok: true, ...pruned });
|
|
882
1988
|
return;
|
|
883
1989
|
}
|
|
884
1990
|
if (pathname === "/api/v1/memory" && req.method === "POST") {
|
|
885
1991
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1992
|
+
const nlPrompt = String(body.prompt || body.remember || "").trim();
|
|
1993
|
+
if (nlPrompt && !body.content) {
|
|
1994
|
+
const parsed = parseNaturalLanguageMemoryInstruction(nlPrompt);
|
|
1995
|
+
if (parsed.isMemoryInstruction) {
|
|
1996
|
+
const primaryFact = parsed.temporalFacts[0];
|
|
1997
|
+
const record = await engine.addWorkspaceMemory(ctx, {
|
|
1998
|
+
title: parsed.title,
|
|
1999
|
+
content: parsed.content,
|
|
2000
|
+
kind: parsed.kind,
|
|
2001
|
+
subject: primaryFact?.subject,
|
|
2002
|
+
predicate: primaryFact?.predicate,
|
|
2003
|
+
object: primaryFact?.object,
|
|
2004
|
+
metadata: {
|
|
2005
|
+
extractedDefaults: parsed.extractedDefaults,
|
|
2006
|
+
source: "natural_language_api",
|
|
2007
|
+
},
|
|
2008
|
+
});
|
|
2009
|
+
for (const tf of parsed.temporalFacts) {
|
|
2010
|
+
store.upsertTemporalFact({
|
|
2011
|
+
workspaceId: ctx.workspace.id,
|
|
2012
|
+
subject: tf.subject,
|
|
2013
|
+
predicate: tf.predicate,
|
|
2014
|
+
object: tf.object,
|
|
2015
|
+
summary: tf.summary,
|
|
2016
|
+
confidence: 0.98,
|
|
2017
|
+
sourceKind: "preference",
|
|
2018
|
+
sourceReferenceId: record.id,
|
|
2019
|
+
metadata: { source: "natural_language_api" },
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
const overview = await engine.getMemoryOverview(ctx);
|
|
2023
|
+
sendJson(res, 201, {
|
|
2024
|
+
ok: true,
|
|
2025
|
+
memory: record,
|
|
2026
|
+
activeDefaults: overview.activeDefaults,
|
|
2027
|
+
explanation: overview.explanation,
|
|
2028
|
+
humanMessage: parsed.humanSummary,
|
|
2029
|
+
});
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
body.content = nlPrompt;
|
|
2033
|
+
}
|
|
886
2034
|
const record = await engine.addWorkspaceMemory(ctx, {
|
|
887
2035
|
title: String(body.title || "Workspace Fact"),
|
|
888
2036
|
content: String(body.content || ""),
|
|
889
2037
|
kind: body.kind || "fact",
|
|
890
2038
|
metadata: body.metadata || {},
|
|
2039
|
+
subject: body.subject ? String(body.subject) : undefined,
|
|
2040
|
+
predicate: body.predicate ? String(body.predicate) : undefined,
|
|
2041
|
+
object: body.object ? String(body.object) : undefined,
|
|
891
2042
|
});
|
|
892
2043
|
sendJson(res, 201, { ok: true, memory: record });
|
|
893
2044
|
return;
|
|
@@ -898,118 +2049,827 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
898
2049
|
sendJson(res, 200, { ok: true, ...deleted });
|
|
899
2050
|
return;
|
|
900
2051
|
}
|
|
901
|
-
// API Keys
|
|
902
|
-
if (pathname === "/api/v1/api-keys"
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
2052
|
+
// API Keys
|
|
2053
|
+
if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
|
|
2054
|
+
req.method === "GET") {
|
|
2055
|
+
sendJson(res, 200, {
|
|
2056
|
+
ok: true,
|
|
2057
|
+
apiKeys: store.listApiKeys(ctx.workspace.id),
|
|
2058
|
+
keys: store.listApiKeys(ctx.workspace.id),
|
|
2059
|
+
});
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
2062
|
+
if ((pathname === "/api/v1/api-keys" || pathname === "/api/v1/keys") &&
|
|
2063
|
+
req.method === "POST") {
|
|
2064
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2065
|
+
const created = store.createApiKey({
|
|
2066
|
+
workspaceId: ctx.workspace.id,
|
|
2067
|
+
name: String(body.name || (body.teamName ? `${String(body.teamName)} Team Key` : "API Key")),
|
|
2068
|
+
environment: body.environment === "test" ? "test" : "live",
|
|
2069
|
+
scopes: Array.isArray(body.scopes) ? body.scopes : undefined,
|
|
2070
|
+
teamName: body.teamName ? String(body.teamName) : null,
|
|
2071
|
+
role: (body.role ? String(body.role) : "operator"),
|
|
2072
|
+
createdByEmail: ctx.user.email,
|
|
2073
|
+
});
|
|
2074
|
+
sendJson(res, 201, { ok: true, apiKey: created.secretKey, ...created });
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
if ((pathname.startsWith("/api/v1/api-keys/") ||
|
|
2078
|
+
pathname.startsWith("/api/v1/keys/")) &&
|
|
2079
|
+
req.method === "DELETE") {
|
|
2080
|
+
const prefix = pathname.startsWith("/api/v1/api-keys/")
|
|
2081
|
+
? "/api/v1/api-keys/"
|
|
2082
|
+
: "/api/v1/keys/";
|
|
2083
|
+
const keyId = decodeURIComponent(pathname.slice(prefix.length));
|
|
2084
|
+
if (!store.revokeApiKey(ctx.workspace.id, keyId)) {
|
|
2085
|
+
sendJson(res, 404, { ok: false, error: "API key not found" });
|
|
2086
|
+
return;
|
|
2087
|
+
}
|
|
2088
|
+
sendJson(res, 200, { ok: true, revoked: keyId });
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
// Profile & Organization (v1 and api/v1)
|
|
2092
|
+
if ((pathname === "/api/v1/profile" || pathname === "/v1/profile") && req.method === "GET") {
|
|
2093
|
+
sendJson(res, 200, { ok: true, ...engine.getProfile(ctx) });
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if ((pathname === "/api/v1/organization" || pathname === "/v1/organization") && req.method === "GET") {
|
|
2097
|
+
sendJson(res, 200, { ok: true, ...engine.getOrganization(ctx) });
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
// Catalog Search & Inspect
|
|
2101
|
+
if ((pathname === "/api/v1/tools" || pathname === "/v1/tools/search") &&
|
|
2102
|
+
(req.method === "GET" || req.method === "POST")) {
|
|
2103
|
+
const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
|
|
2104
|
+
const query = url.searchParams.get("query") || body.query || undefined;
|
|
2105
|
+
const lane = (url.searchParams.get("lane") || body.lane) ||
|
|
2106
|
+
"all";
|
|
2107
|
+
const riskLevel = (url.searchParams.get("risk_level") || body.risk_level) ||
|
|
2108
|
+
undefined;
|
|
2109
|
+
const limit = Number(url.searchParams.get("limit") || body.limit || 30);
|
|
2110
|
+
const results = await engine.searchTools({ query, lane, riskLevel, limit });
|
|
2111
|
+
sendJson(res, 200, { ok: true, ...results });
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
if (pathname.startsWith("/api/v1/tools/") && req.method === "GET") {
|
|
2115
|
+
const slug = decodeURIComponent(pathname.slice("/api/v1/tools/".length));
|
|
2116
|
+
const tool = await engine.inspectTool(slug);
|
|
2117
|
+
sendJson(res, 200, tool);
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
// Connections
|
|
2121
|
+
if (pathname === "/api/v1/connections" && req.method === "GET") {
|
|
2122
|
+
const syncLive = url.searchParams.get("sync") === "true";
|
|
2123
|
+
const provider = url.searchParams.get("provider") || undefined;
|
|
2124
|
+
const status = await engine.connectionStatus(ctx, { provider, syncLive });
|
|
2125
|
+
sendJson(res, 200, { ok: true, ...status });
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
2128
|
+
if (pathname === "/api/v1/connections" && req.method === "POST") {
|
|
2129
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2130
|
+
const connection = await engine.startConnection(ctx, {
|
|
2131
|
+
provider: String(body.provider || ""),
|
|
2132
|
+
lane: body.lane,
|
|
2133
|
+
callbackUrl: body.callbackUrl ? String(body.callbackUrl) : undefined,
|
|
2134
|
+
});
|
|
2135
|
+
sendJson(res, 201, { ok: true, connection });
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
if (pathname === "/api/v1/connections/wait" && req.method === "POST") {
|
|
2139
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2140
|
+
const provider = String(body.provider || "");
|
|
2141
|
+
if (body.simulateCallback === true || body.simulate_callback === true) {
|
|
2142
|
+
await engine.handleOAuthCallback({
|
|
2143
|
+
workspaceId: ctx.workspace.id,
|
|
2144
|
+
provider,
|
|
2145
|
+
status: "success",
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const result = await engine.waitForConnection(ctx, {
|
|
2149
|
+
provider,
|
|
2150
|
+
timeoutSeconds: clampWaitSeconds(body.timeoutSeconds ?? body.timeout_seconds),
|
|
2151
|
+
});
|
|
2152
|
+
sendJson(res, 200, result);
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
const connectionWaitMatch = pathname.match(/^\/api\/v1\/connections\/([^/]+)\/wait$/);
|
|
2156
|
+
if (connectionWaitMatch && req.method === "GET") {
|
|
2157
|
+
const result = await engine.waitForConnection(ctx, {
|
|
2158
|
+
provider: decodeURIComponent(connectionWaitMatch[1]),
|
|
2159
|
+
timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds") ?? url.searchParams.get("timeoutSeconds")),
|
|
2160
|
+
});
|
|
2161
|
+
sendJson(res, 200, result);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
// Provisioned Resources & Decrypted Vault Env Pull
|
|
2165
|
+
if (pathname === "/api/v1/resources" && req.method === "GET") {
|
|
2166
|
+
sendJson(res, 200, {
|
|
2167
|
+
ok: true,
|
|
2168
|
+
resources: store.listResources(ctx.workspace.id),
|
|
2169
|
+
});
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
if (pathname === "/api/v1/resources/env" && req.method === "GET") {
|
|
2173
|
+
// Also pull any fresh .env from Stripe Projects workdir if present
|
|
2174
|
+
const workdirSecrets = await engine.stripeProjects
|
|
2175
|
+
.pullEnvVarsFromWorkdir()
|
|
2176
|
+
.catch(() => ({}));
|
|
2177
|
+
const vaultSecrets = store.getDecryptedWorkspaceEnv(ctx.workspace.id);
|
|
2178
|
+
const merged = { ...workdirSecrets, ...vaultSecrets };
|
|
2179
|
+
const dotenvLines = [
|
|
2180
|
+
`# Muxo Encrypted Vault Export — Workspace ${ctx.workspace.slug} (${ctx.workspace.id})`,
|
|
2181
|
+
`# Generated at ${new Date().toISOString()}`,
|
|
2182
|
+
...Object.entries(merged).map(([k, v]) => `${k}=${JSON.stringify(v)}`),
|
|
2183
|
+
];
|
|
2184
|
+
sendJson(res, 200, {
|
|
2185
|
+
ok: true,
|
|
2186
|
+
workspaceId: ctx.workspace.id,
|
|
2187
|
+
variables: merged,
|
|
2188
|
+
dotenv: dotenvLines.join("\n") + "\n",
|
|
2189
|
+
});
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
const resourceSecretsMatch = pathname.match(/^\/api\/v1\/resources\/([^/]+)\/secrets$/);
|
|
2193
|
+
if (resourceSecretsMatch && req.method === "GET") {
|
|
2194
|
+
const resourceId = decodeURIComponent(resourceSecretsMatch[1]);
|
|
2195
|
+
const anyRes = store.findResourceAnyWorkspace(resourceId);
|
|
2196
|
+
if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
|
|
2197
|
+
sendJson(res, 403, {
|
|
2198
|
+
ok: false,
|
|
2199
|
+
code: "resource_workspace_mismatch",
|
|
2200
|
+
error: "Resource belongs to a different workspace.",
|
|
2201
|
+
});
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
const ownedRes = store.getResource(ctx.workspace.id, resourceId);
|
|
2205
|
+
if (!ownedRes) {
|
|
2206
|
+
sendJson(res, 404, {
|
|
2207
|
+
ok: false,
|
|
2208
|
+
code: "resource_not_found",
|
|
2209
|
+
error: `Resource '${resourceId}' not found in workspace.`,
|
|
2210
|
+
});
|
|
2211
|
+
return;
|
|
2212
|
+
}
|
|
2213
|
+
const secrets = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
|
|
2214
|
+
sendJson(res, 200, {
|
|
2215
|
+
ok: true,
|
|
2216
|
+
resourceId: ownedRes.id,
|
|
2217
|
+
resourceName: ownedRes.resourceName,
|
|
2218
|
+
workspaceId: ctx.workspace.id,
|
|
2219
|
+
envVarKeys: ownedRes.envVarKeys,
|
|
2220
|
+
secrets,
|
|
2221
|
+
});
|
|
2222
|
+
return;
|
|
2223
|
+
}
|
|
2224
|
+
// Stripe v2 Provisioning API (`Stripe-Version: 2026-09-30.preview`) Routes
|
|
2225
|
+
if (pathname === "/api/v1/provisioning/audit_log" && req.method === "GET") {
|
|
2226
|
+
const limit = Number(url.searchParams.get("limit") || 50);
|
|
2227
|
+
sendJson(res, 200, {
|
|
2228
|
+
ok: true,
|
|
2229
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2230
|
+
entries: engine.stripeProjects.getProvisioningAuditLog(limit),
|
|
2231
|
+
});
|
|
2232
|
+
return;
|
|
2233
|
+
}
|
|
2234
|
+
if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "GET") {
|
|
2235
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2236
|
+
const verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
|
|
2237
|
+
config.stripeConnectedAccountId ||
|
|
2238
|
+
"";
|
|
2239
|
+
if (!verifiedAccount) {
|
|
2240
|
+
sendJson(res, 200, {
|
|
2241
|
+
ok: true,
|
|
2242
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2243
|
+
connected: false,
|
|
2244
|
+
account: null,
|
|
2245
|
+
publishable_key: config.stripePublishableKey || null,
|
|
2246
|
+
});
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
const retrieved = await engine.stripeProjects.retrieveConnectedAccount(verifiedAccount, stripeByok);
|
|
2250
|
+
sendJson(res, 200, {
|
|
2251
|
+
ok: true,
|
|
2252
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2253
|
+
connected: true,
|
|
2254
|
+
account: retrieved,
|
|
2255
|
+
publishable_key: config.stripePublishableKey || null,
|
|
2256
|
+
});
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
if (pathname === "/api/v1/provisioning/connected_accounts" && req.method === "POST") {
|
|
2260
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2261
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2262
|
+
const created = await engine.stripeProjects.createConnectedAccount({
|
|
2263
|
+
contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
|
|
2264
|
+
display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
|
|
2265
|
+
country: body.country ? String(body.country) : "US",
|
|
2266
|
+
entity_type: body.entity_type === "individual" ? "individual" : "company",
|
|
2267
|
+
apiKeyOverride: stripeByok,
|
|
2268
|
+
});
|
|
2269
|
+
if (created.id) {
|
|
2270
|
+
store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
|
|
2271
|
+
}
|
|
2272
|
+
sendJson(res, 201, {
|
|
2273
|
+
ok: true,
|
|
2274
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2275
|
+
...created,
|
|
2276
|
+
});
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (pathname === "/api/v1/provisioning/connected_account_links" && req.method === "POST") {
|
|
2280
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2281
|
+
let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
|
|
2282
|
+
config.stripeConnectedAccountId ||
|
|
2283
|
+
"";
|
|
2284
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2285
|
+
if (!verifiedAccount && !body.account && body.auto_create === true) {
|
|
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
|
+
verifiedAccount = created.id;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const accountId = String(verifiedAccount || body.account || "").trim();
|
|
2299
|
+
if (!accountId) {
|
|
2300
|
+
sendJson(res, 400, {
|
|
2301
|
+
ok: false,
|
|
2302
|
+
error: "No verified connected_account_id is mapped to this tenant workspace.",
|
|
2303
|
+
});
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
const link = await engine.stripeProjects.createConnectedAccountLink({
|
|
2307
|
+
account: accountId,
|
|
2308
|
+
type: body.type === "account_update" ? "account_update" : "account_onboarding",
|
|
2309
|
+
fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
|
|
2310
|
+
future_requirements: body.future_requirements === "omit"
|
|
2311
|
+
? "omit"
|
|
2312
|
+
: body.future_requirements === "include"
|
|
2313
|
+
? "include"
|
|
2314
|
+
: undefined,
|
|
2315
|
+
refresh_url: String(body.refresh_url || `${config.publicWebUrl}/?onboarding=refresh`),
|
|
2316
|
+
return_url: String(body.return_url || `${config.publicWebUrl}/?onboarding=return`),
|
|
2317
|
+
apiKeyOverride: stripeByok,
|
|
2318
|
+
});
|
|
2319
|
+
sendJson(res, 201, {
|
|
2320
|
+
ok: true,
|
|
2321
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2322
|
+
...link,
|
|
2323
|
+
});
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
if (pathname === "/api/v1/provisioning/connected_account_sessions" && req.method === "POST") {
|
|
2327
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2328
|
+
let verifiedAccount = store.getByokKey(ctx.workspace.id, "stripe_connected_account") ||
|
|
2329
|
+
config.stripeConnectedAccountId ||
|
|
2330
|
+
"";
|
|
2331
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2332
|
+
if (!verifiedAccount && !body.account && body.auto_create !== false) {
|
|
2333
|
+
const created = await engine.stripeProjects.createConnectedAccount({
|
|
2334
|
+
contact_email: String(body.contact_email || ctx.user?.email || "owner@muxo.ai"),
|
|
2335
|
+
display_name: String(body.display_name || ctx.workspace.name || "Muxo Tenant"),
|
|
2336
|
+
country: body.country ? String(body.country) : "US",
|
|
2337
|
+
entity_type: body.entity_type === "individual" ? "individual" : "company",
|
|
2338
|
+
apiKeyOverride: stripeByok,
|
|
2339
|
+
});
|
|
2340
|
+
if (created.id) {
|
|
2341
|
+
store.setByokKey(ctx.workspace.id, "stripe_connected_account", created.id);
|
|
2342
|
+
verifiedAccount = created.id;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
const accountId = String(verifiedAccount || body.account || "").trim();
|
|
2346
|
+
if (!accountId) {
|
|
2347
|
+
sendJson(res, 400, {
|
|
2348
|
+
ok: false,
|
|
2349
|
+
error: "No verified connected_account_id is mapped to this tenant workspace.",
|
|
2350
|
+
});
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
const session = await engine.stripeProjects.createConnectedAccountSession({
|
|
2354
|
+
account: accountId,
|
|
2355
|
+
enableAccountManagement: Boolean(body.enable_account_management ?? body.enableAccountManagement),
|
|
2356
|
+
enableNotificationBanner: body.enable_notification_banner !== undefined
|
|
2357
|
+
? Boolean(body.enable_notification_banner)
|
|
2358
|
+
: body.enableNotificationBanner !== undefined
|
|
2359
|
+
? Boolean(body.enableNotificationBanner)
|
|
2360
|
+
: undefined,
|
|
2361
|
+
disableStripeUserAuthentication: body.disable_stripe_user_authentication !== undefined
|
|
2362
|
+
? Boolean(body.disable_stripe_user_authentication)
|
|
2363
|
+
: body.disableStripeUserAuthentication !== undefined
|
|
2364
|
+
? Boolean(body.disableStripeUserAuthentication)
|
|
2365
|
+
: undefined,
|
|
2366
|
+
externalAccountCollection: body.external_account_collection !== undefined
|
|
2367
|
+
? Boolean(body.external_account_collection)
|
|
2368
|
+
: body.externalAccountCollection !== undefined
|
|
2369
|
+
? Boolean(body.externalAccountCollection)
|
|
2370
|
+
: undefined,
|
|
2371
|
+
apiKeyOverride: stripeByok,
|
|
2372
|
+
});
|
|
2373
|
+
sendJson(res, 201, {
|
|
2374
|
+
ok: true,
|
|
2375
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2376
|
+
publishable_key: config.stripePublishableKey || null,
|
|
2377
|
+
embedded_component: "account-onboarding",
|
|
2378
|
+
collection_options: {
|
|
2379
|
+
fields: body.fields === "eventually_due" ? "eventually_due" : "currently_due",
|
|
2380
|
+
future_requirements: body.future_requirements === "omit" ? "omit" : "include",
|
|
2381
|
+
},
|
|
2382
|
+
...session,
|
|
2383
|
+
});
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const connAccountGetMatch = pathname.match(/^\/api\/v1\/provisioning\/connected_accounts\/([^/]+)$/);
|
|
2387
|
+
if (connAccountGetMatch && req.method === "GET") {
|
|
2388
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2389
|
+
const accountId = decodeURIComponent(connAccountGetMatch[1]);
|
|
2390
|
+
const retrieved = await engine.stripeProjects.retrieveConnectedAccount(accountId, stripeByok);
|
|
2391
|
+
sendJson(res, 200, {
|
|
2392
|
+
ok: true,
|
|
2393
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2394
|
+
...retrieved,
|
|
2395
|
+
});
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (pathname === "/api/v1/provisioning/eligibility" && req.method === "GET") {
|
|
2399
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2400
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
2401
|
+
const eligibility = await engine.stripeProjects.checkEligibility(stripeByok, verifiedContext);
|
|
2402
|
+
sendJson(res, 200, {
|
|
2403
|
+
ok: true,
|
|
2404
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2405
|
+
...eligibility,
|
|
2406
|
+
});
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
if ((pathname === "/api/v1/provisioning/catalog/providers" ||
|
|
2410
|
+
pathname === "/api/v1/provisioning/providers") &&
|
|
2411
|
+
req.method === "GET") {
|
|
2412
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2413
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
2414
|
+
const catalog = url.searchParams.get("catalog") || "prod";
|
|
2415
|
+
const limit = Number(url.searchParams.get("limit") || 100);
|
|
2416
|
+
const page = url.searchParams.get("page") || undefined;
|
|
2417
|
+
const page_url = url.searchParams.get("page_url") || undefined;
|
|
2418
|
+
const development = url.searchParams.get("development") !== null
|
|
2419
|
+
? url.searchParams.get("development") === "true"
|
|
2420
|
+
: undefined;
|
|
2421
|
+
const providers = await engine.stripeProjects.listCatalogProviders({
|
|
2422
|
+
catalog,
|
|
2423
|
+
development,
|
|
2424
|
+
limit,
|
|
2425
|
+
page,
|
|
2426
|
+
page_url,
|
|
2427
|
+
apiKeyOverride: stripeByok,
|
|
2428
|
+
stripeContext: verifiedContext,
|
|
2429
|
+
});
|
|
2430
|
+
sendJson(res, 200, {
|
|
2431
|
+
ok: true,
|
|
2432
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2433
|
+
...providers,
|
|
2434
|
+
});
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if ((pathname === "/api/v1/provisioning/catalog/services" ||
|
|
2438
|
+
pathname === "/api/v1/provisioning/services") &&
|
|
2439
|
+
req.method === "GET") {
|
|
2440
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2441
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
2442
|
+
const catalog = url.searchParams.get("catalog") || "prod";
|
|
2443
|
+
const limit = Number(url.searchParams.get("limit") || 100);
|
|
2444
|
+
const page = url.searchParams.get("page") || undefined;
|
|
2445
|
+
const page_url = url.searchParams.get("page_url") || undefined;
|
|
2446
|
+
const development = url.searchParams.get("development") !== null
|
|
2447
|
+
? url.searchParams.get("development") === "true"
|
|
2448
|
+
: undefined;
|
|
2449
|
+
const provider_id = url.searchParams.get("provider_id") || undefined;
|
|
2450
|
+
const provider_name = url.searchParams.get("provider_name") || undefined;
|
|
2451
|
+
const services = await engine.stripeProjects.listCatalogServices({
|
|
2452
|
+
catalog,
|
|
2453
|
+
development,
|
|
2454
|
+
limit,
|
|
2455
|
+
page,
|
|
2456
|
+
page_url,
|
|
2457
|
+
provider_id,
|
|
2458
|
+
provider_name,
|
|
2459
|
+
apiKeyOverride: stripeByok,
|
|
2460
|
+
stripeContext: verifiedContext,
|
|
2461
|
+
});
|
|
2462
|
+
sendJson(res, 200, {
|
|
2463
|
+
ok: true,
|
|
2464
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2465
|
+
...services,
|
|
2466
|
+
});
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
if (pathname === "/api/v1/provisioning/payment_profile" && req.method === "GET") {
|
|
2470
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2471
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
2472
|
+
const provider = url.searchParams.get("provider") || undefined;
|
|
2473
|
+
const livemodeParam = url.searchParams.get("livemode");
|
|
2474
|
+
const livemode = livemodeParam !== null ? livemodeParam === "true" : undefined;
|
|
2475
|
+
const profile = await engine.stripeProjects.getPaymentProfile({
|
|
2476
|
+
provider,
|
|
2477
|
+
livemode,
|
|
2478
|
+
apiKeyOverride: stripeByok,
|
|
2479
|
+
stripeContext: verifiedContext,
|
|
2480
|
+
});
|
|
2481
|
+
sendJson(res, 200, {
|
|
2482
|
+
ok: true,
|
|
2483
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2484
|
+
...profile,
|
|
2485
|
+
});
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
if (pathname === "/api/v1/provisioning/payment_profile/update_limit" &&
|
|
2489
|
+
req.method === "POST") {
|
|
2490
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2491
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
2492
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2493
|
+
const usageLimits = body.usage_limits || {
|
|
2494
|
+
max_amount: String(body.max_amount ?? "5000"),
|
|
2495
|
+
currency: String(body.currency || "usd"),
|
|
2496
|
+
recurring_interval: String(body.recurring_interval || "month"),
|
|
2497
|
+
};
|
|
2498
|
+
const updated = await engine.stripeProjects.updatePaymentProfileLimit({
|
|
2499
|
+
usage_limits: usageLimits,
|
|
2500
|
+
provider: body.provider ? String(body.provider) : undefined,
|
|
2501
|
+
livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
|
|
2502
|
+
apiKeyOverride: stripeByok,
|
|
2503
|
+
stripeContext: verifiedContext,
|
|
2504
|
+
});
|
|
2505
|
+
sendJson(res, 200, {
|
|
2506
|
+
ok: true,
|
|
2507
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2508
|
+
...updated,
|
|
2509
|
+
});
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (pathname === "/api/v1/provisioning/payment_method_requests" &&
|
|
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 created = await engine.stripeProjects.createPaymentMethodRequest({
|
|
2518
|
+
payment_method_owner: body.payment_method_owner === "platform" ||
|
|
2519
|
+
body.payment_method_owner === "provider"
|
|
2520
|
+
? body.payment_method_owner
|
|
2521
|
+
: undefined,
|
|
2522
|
+
provider: body.provider ? String(body.provider) : undefined,
|
|
2523
|
+
source_account: body.source_account ? String(body.source_account) : undefined,
|
|
2524
|
+
source_customer: body.source_customer ? String(body.source_customer) : undefined,
|
|
2525
|
+
source_payment_method: body.source_payment_method
|
|
2526
|
+
? String(body.source_payment_method)
|
|
2527
|
+
: undefined,
|
|
2528
|
+
livemode: body.livemode !== undefined ? Boolean(body.livemode) : undefined,
|
|
2529
|
+
usage_limits: body.usage_limits,
|
|
2530
|
+
apiKeyOverride: stripeByok,
|
|
2531
|
+
stripeContext: verifiedContext,
|
|
2532
|
+
});
|
|
2533
|
+
sendJson(res, 201, {
|
|
2534
|
+
ok: true,
|
|
2535
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2536
|
+
...created,
|
|
2537
|
+
});
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
if (pathname === "/api/v1/provisioning/projects" && req.method === "GET") {
|
|
2541
|
+
sendJson(res, 200, {
|
|
2542
|
+
ok: true,
|
|
2543
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2544
|
+
object: "v2.list",
|
|
2545
|
+
data: [
|
|
2546
|
+
{
|
|
2547
|
+
id: config.stripeProjectId || `proj_muxo_${ctx.workspace.id}`,
|
|
2548
|
+
object: "v2.provisioning.project",
|
|
2549
|
+
name: `${ctx.workspace.name} Cloud Project`,
|
|
2550
|
+
catalog: "prod",
|
|
2551
|
+
created: ctx.workspace.createdAt,
|
|
2552
|
+
livemode: false,
|
|
2553
|
+
},
|
|
2554
|
+
],
|
|
2555
|
+
next_page_url: null,
|
|
2556
|
+
previous_page_url: null,
|
|
2557
|
+
});
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
if (pathname === "/api/v1/provisioning/projects" && req.method === "POST") {
|
|
2561
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2562
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2563
|
+
const created = await engine.stripeProjects.createProject({
|
|
2564
|
+
name: String(body.name || "Muxo Cloud Project"),
|
|
2565
|
+
catalog: body.catalog || "prod",
|
|
2566
|
+
project_profile: body.project_profile
|
|
2567
|
+
? String(body.project_profile)
|
|
2568
|
+
: undefined,
|
|
2569
|
+
apiKeyOverride: stripeByok,
|
|
2570
|
+
});
|
|
2571
|
+
sendJson(res, 201, {
|
|
2572
|
+
ok: true,
|
|
2573
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2574
|
+
...created,
|
|
2575
|
+
});
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
if (pathname === "/api/v1/provisioning/provider_connections" &&
|
|
2579
|
+
req.method === "GET") {
|
|
2580
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2581
|
+
const limit = Number(url.searchParams.get("limit") || 25);
|
|
2582
|
+
const page = url.searchParams.get("page") || undefined;
|
|
2583
|
+
const provider = url.searchParams.get("provider") || undefined;
|
|
2584
|
+
const connections = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2585
|
+
? await engine.stripeProjects.listProviderConnections({
|
|
2586
|
+
limit,
|
|
2587
|
+
page,
|
|
2588
|
+
provider,
|
|
2589
|
+
apiKeyOverride: stripeByok,
|
|
2590
|
+
})
|
|
2591
|
+
: managedProviderConnectionList();
|
|
2592
|
+
sendJson(res, 200, {
|
|
2593
|
+
ok: true,
|
|
2594
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2595
|
+
...connections,
|
|
2596
|
+
});
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const provConnUnlinkMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connections\/([^/]+)\/unlink$/);
|
|
2600
|
+
if (provConnUnlinkMatch && req.method === "POST") {
|
|
2601
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2602
|
+
const id = decodeURIComponent(provConnUnlinkMatch[1]);
|
|
2603
|
+
const unlinked = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2604
|
+
? await engine.stripeProjects.unlinkProviderConnection(id, stripeByok)
|
|
2605
|
+
: managedProviderConnectionUnlink(id);
|
|
2606
|
+
sendJson(res, 200, {
|
|
2607
|
+
ok: true,
|
|
2608
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2609
|
+
...unlinked,
|
|
908
2610
|
});
|
|
909
|
-
sendJson(res, 201, { ok: true, ...created });
|
|
910
2611
|
return;
|
|
911
2612
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
2613
|
+
if (pathname === "/api/v1/provisioning/provider_connection_requests" &&
|
|
2614
|
+
req.method === "POST") {
|
|
2615
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2616
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2617
|
+
const reqRes = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2618
|
+
? await engine.stripeProjects.createProviderConnectionRequest({
|
|
2619
|
+
provider: String(body.provider || ""),
|
|
2620
|
+
configuration: body.configuration && typeof body.configuration === "object"
|
|
2621
|
+
? body.configuration
|
|
2622
|
+
: {},
|
|
2623
|
+
project: body.project ? String(body.project) : undefined,
|
|
2624
|
+
apiKeyOverride: stripeByok,
|
|
2625
|
+
})
|
|
2626
|
+
: managedProviderConnectionRequest({ provider: String(body.provider || "") });
|
|
2627
|
+
sendJson(res, 201, {
|
|
2628
|
+
ok: true,
|
|
2629
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2630
|
+
...reqRes,
|
|
2631
|
+
});
|
|
915
2632
|
return;
|
|
916
2633
|
}
|
|
917
|
-
|
|
918
|
-
|
|
2634
|
+
const provConnReqSubmitMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)\/submit_information$/);
|
|
2635
|
+
if (provConnReqSubmitMatch && req.method === "POST") {
|
|
2636
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2637
|
+
const id = decodeURIComponent(provConnReqSubmitMatch[1]);
|
|
2638
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2639
|
+
const submitted = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2640
|
+
? await engine.stripeProjects.submitProviderConnectionRequestInformation(id, body.submitted_information || body, stripeByok)
|
|
2641
|
+
: managedProviderConnectionRequest({ id, requestStatus: "complete" });
|
|
2642
|
+
sendJson(res, 200, {
|
|
2643
|
+
ok: true,
|
|
2644
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2645
|
+
...submitted,
|
|
2646
|
+
});
|
|
919
2647
|
return;
|
|
920
2648
|
}
|
|
921
|
-
|
|
922
|
-
if (
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
const
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
2649
|
+
const provConnReqGetMatch = pathname.match(/^\/api\/v1\/provisioning\/provider_connection_requests\/([^/]+)$/);
|
|
2650
|
+
if (provConnReqGetMatch && req.method === "GET") {
|
|
2651
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2652
|
+
const id = decodeURIComponent(provConnReqGetMatch[1]);
|
|
2653
|
+
const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2654
|
+
? await engine.stripeProjects.retrieveProviderConnectionRequest(id, stripeByok)
|
|
2655
|
+
: managedProviderConnectionRequest({ id });
|
|
2656
|
+
sendJson(res, 200, {
|
|
2657
|
+
ok: true,
|
|
2658
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2659
|
+
...retrieved,
|
|
2660
|
+
});
|
|
933
2661
|
return;
|
|
934
2662
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
2663
|
+
const revealAccessMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/reveal_access_configuration$/);
|
|
2664
|
+
if (revealAccessMatch && req.method === "POST") {
|
|
2665
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2666
|
+
const resourceId = decodeURIComponent(revealAccessMatch[1]);
|
|
2667
|
+
const anyRes = store.findResourceAnyWorkspace(resourceId);
|
|
2668
|
+
if (anyRes && anyRes.workspaceId !== ctx.workspace.id) {
|
|
2669
|
+
sendJson(res, 403, {
|
|
2670
|
+
ok: false,
|
|
2671
|
+
code: "resource_workspace_mismatch",
|
|
2672
|
+
error: "Resource belongs to a different workspace.",
|
|
2673
|
+
});
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
|
|
2677
|
+
try {
|
|
2678
|
+
const revealed = await engine.stripeProjects.revealAccessConfiguration(resourceId, stripeByok);
|
|
2679
|
+
sendJson(res, 200, {
|
|
2680
|
+
ok: true,
|
|
2681
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2682
|
+
source: "stripe_v2_api",
|
|
2683
|
+
...revealed,
|
|
2684
|
+
});
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
catch {
|
|
2688
|
+
// Fall through to workspace vault lookup if Stripe v2 returns error
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
const resourceSpecific = store.getDecryptedResourceSecrets(ctx.workspace.id, resourceId);
|
|
2692
|
+
const vaultSecrets = Object.keys(resourceSpecific).length > 0
|
|
2693
|
+
? resourceSpecific
|
|
2694
|
+
: store.getDecryptedWorkspaceEnv(ctx.workspace.id);
|
|
2695
|
+
sendJson(res, 200, {
|
|
2696
|
+
ok: true,
|
|
2697
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2698
|
+
object: "v2.provisioning.resource.access_configuration",
|
|
2699
|
+
resource: resourceId,
|
|
2700
|
+
configuration: vaultSecrets,
|
|
2701
|
+
created: new Date().toISOString(),
|
|
2702
|
+
expires_at: null,
|
|
2703
|
+
livemode: false,
|
|
2704
|
+
source: "muxo_managed",
|
|
2705
|
+
});
|
|
939
2706
|
return;
|
|
940
2707
|
}
|
|
941
|
-
|
|
942
|
-
if (
|
|
943
|
-
const
|
|
944
|
-
const
|
|
945
|
-
const
|
|
946
|
-
|
|
2708
|
+
const resourceActionMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)\/(remove|unlink|rotate_credentials|submit_information)$/);
|
|
2709
|
+
if (resourceActionMatch && req.method === "POST") {
|
|
2710
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2711
|
+
const resourceId = decodeURIComponent(resourceActionMatch[1]);
|
|
2712
|
+
const action = resourceActionMatch[2];
|
|
2713
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2714
|
+
let actionResult;
|
|
2715
|
+
if (!engine.stripeProjects.hasStripeV2ApiKey(stripeByok)) {
|
|
2716
|
+
actionResult = managedResourceAction(resourceId, action);
|
|
2717
|
+
}
|
|
2718
|
+
else if (action === "remove") {
|
|
2719
|
+
actionResult = await engine.stripeProjects.removeResource(resourceId, stripeByok);
|
|
2720
|
+
}
|
|
2721
|
+
else if (action === "unlink") {
|
|
2722
|
+
actionResult = await engine.stripeProjects.unlinkResource(resourceId, stripeByok);
|
|
2723
|
+
}
|
|
2724
|
+
else if (action === "rotate_credentials") {
|
|
2725
|
+
actionResult = await engine.stripeProjects.rotateResourceCredentials(resourceId, stripeByok);
|
|
2726
|
+
}
|
|
2727
|
+
else {
|
|
2728
|
+
actionResult = await engine.stripeProjects.submitResourceInformation(resourceId, body.submitted_information || body, stripeByok);
|
|
2729
|
+
}
|
|
2730
|
+
sendJson(res, 200, {
|
|
2731
|
+
ok: true,
|
|
2732
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2733
|
+
...actionResult,
|
|
2734
|
+
});
|
|
947
2735
|
return;
|
|
948
2736
|
}
|
|
949
|
-
if (pathname === "/api/v1/
|
|
2737
|
+
if (pathname === "/api/v1/provisioning/resources/link" && req.method === "POST") {
|
|
2738
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
950
2739
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
2740
|
+
const linked = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2741
|
+
? managedResource({
|
|
2742
|
+
provider: String(body.provider || ""),
|
|
2743
|
+
serviceRef: String(body.service_ref || ""),
|
|
2744
|
+
environment: body.environment ? String(body.environment) : "prod",
|
|
2745
|
+
catalog: body.catalog ? String(body.catalog) : "prod",
|
|
2746
|
+
status: "active",
|
|
2747
|
+
})
|
|
2748
|
+
: await engine.stripeProjects.linkResource({
|
|
2749
|
+
provider: String(body.provider || ""),
|
|
2750
|
+
service_ref: String(body.service_ref || ""),
|
|
2751
|
+
provider_resource_id: body.provider_resource_id
|
|
2752
|
+
? String(body.provider_resource_id)
|
|
2753
|
+
: undefined,
|
|
2754
|
+
environment: body.environment ? String(body.environment) : "prod",
|
|
2755
|
+
catalog: body.catalog || "prod",
|
|
2756
|
+
project: body.project ? String(body.project) : undefined,
|
|
2757
|
+
apiKeyOverride: stripeByok,
|
|
2758
|
+
});
|
|
2759
|
+
sendJson(res, 201, {
|
|
2760
|
+
ok: true,
|
|
2761
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2762
|
+
...linked,
|
|
955
2763
|
});
|
|
956
|
-
sendJson(res, 201, { ok: true, connection });
|
|
957
2764
|
return;
|
|
958
2765
|
}
|
|
959
|
-
if (pathname === "/api/v1/
|
|
2766
|
+
if (pathname === "/api/v1/provisioning/resources" && req.method === "GET") {
|
|
2767
|
+
const resources = store.listResources(ctx.workspace.id);
|
|
2768
|
+
sendJson(res, 200, {
|
|
2769
|
+
ok: true,
|
|
2770
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2771
|
+
object: "v2.list",
|
|
2772
|
+
data: resources,
|
|
2773
|
+
next_page_url: null,
|
|
2774
|
+
previous_page_url: null,
|
|
2775
|
+
});
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
if (pathname === "/api/v1/provisioning/resources" && req.method === "POST") {
|
|
2779
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
960
2780
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
2781
|
+
const providerSlug = String(body.provider || "turso").toLowerCase();
|
|
2782
|
+
const serviceRef = String(body.service_ref || "database").toLowerCase();
|
|
2783
|
+
const resourceName = body.name ? String(body.name) : `${providerSlug}-${serviceRef}`;
|
|
2784
|
+
const created = !engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2785
|
+
? managedResource({
|
|
2786
|
+
provider: providerSlug,
|
|
2787
|
+
serviceRef,
|
|
2788
|
+
name: resourceName,
|
|
2789
|
+
environment: body.environment ? String(body.environment) : "prod",
|
|
2790
|
+
catalog: body.catalog ? String(body.catalog) : "prod",
|
|
2791
|
+
})
|
|
2792
|
+
: await engine.stripeProjects.createResource({
|
|
2793
|
+
provider: providerSlug,
|
|
2794
|
+
service_ref: serviceRef,
|
|
2795
|
+
configuration: body.configuration && typeof body.configuration === "object"
|
|
2796
|
+
? body.configuration
|
|
2797
|
+
: {},
|
|
2798
|
+
name: resourceName,
|
|
2799
|
+
environment: body.environment ? String(body.environment) : "prod",
|
|
2800
|
+
catalog: body.catalog || "prod",
|
|
2801
|
+
project: body.project ? String(body.project) : undefined,
|
|
2802
|
+
apiKeyOverride: stripeByok,
|
|
967
2803
|
});
|
|
968
|
-
}
|
|
969
|
-
const
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
2804
|
+
const externalId = String(created.id || `prv_res_${Date.now().toString(36)}`);
|
|
2805
|
+
const envKey = `${providerSlug.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_URL`;
|
|
2806
|
+
store.upsertResource({
|
|
2807
|
+
workspaceId: ctx.workspace.id,
|
|
2808
|
+
providerSlug,
|
|
2809
|
+
providerName: providerSlug.toUpperCase(),
|
|
2810
|
+
serviceSlug: serviceRef,
|
|
2811
|
+
resourceName,
|
|
2812
|
+
status: "active",
|
|
2813
|
+
externalResourceId: externalId,
|
|
2814
|
+
secrets: { [envKey]: `https://${resourceName}.${providerSlug}.muxo.internal` },
|
|
2815
|
+
metadata: { source: created.source || "muxo_managed" },
|
|
2816
|
+
});
|
|
2817
|
+
sendJson(res, 201, {
|
|
2818
|
+
ok: true,
|
|
2819
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2820
|
+
...created,
|
|
976
2821
|
});
|
|
977
|
-
sendJson(res, 200, { ok: true, ...result });
|
|
978
2822
|
return;
|
|
979
2823
|
}
|
|
980
|
-
|
|
981
|
-
if (
|
|
2824
|
+
const resourceIdMatch = pathname.match(/^\/api\/v1\/provisioning\/resources\/([^/]+)$/);
|
|
2825
|
+
if (resourceIdMatch && (req.method === "GET" || req.method === "POST")) {
|
|
2826
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
2827
|
+
const resourceId = decodeURIComponent(resourceIdMatch[1]);
|
|
2828
|
+
if (req.method === "GET") {
|
|
2829
|
+
const retrieved = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2830
|
+
? await engine.stripeProjects.retrieveResource(resourceId, stripeByok)
|
|
2831
|
+
: managedResource({ id: resourceId });
|
|
2832
|
+
sendJson(res, 200, {
|
|
2833
|
+
ok: true,
|
|
2834
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2835
|
+
...retrieved,
|
|
2836
|
+
});
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2840
|
+
const updated = engine.stripeProjects.hasStripeV2ApiKey(stripeByok)
|
|
2841
|
+
? await engine.stripeProjects.updateResource(resourceId, body.configuration || body, stripeByok)
|
|
2842
|
+
: managedResource({ id: resourceId, status: "active" });
|
|
982
2843
|
sendJson(res, 200, {
|
|
983
2844
|
ok: true,
|
|
984
|
-
|
|
2845
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
2846
|
+
...updated,
|
|
985
2847
|
});
|
|
986
2848
|
return;
|
|
987
2849
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const
|
|
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
|
-
];
|
|
2850
|
+
// Governance Policy
|
|
2851
|
+
if (pathname === "/api/v1/policy" && req.method === "GET") {
|
|
2852
|
+
const summary = engine.getGuardrailsSummary(ctx);
|
|
1000
2853
|
sendJson(res, 200, {
|
|
1001
2854
|
ok: true,
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
2855
|
+
policy: summary.policy,
|
|
2856
|
+
plainEnglish: summary.plainEnglish,
|
|
2857
|
+
explanation: summary.plainEnglish.explanation,
|
|
2858
|
+
activeDefaults: summary.activeDefaults,
|
|
2859
|
+
activeGuardrails: summary.activeGuardrails,
|
|
1005
2860
|
});
|
|
1006
2861
|
return;
|
|
1007
2862
|
}
|
|
1008
|
-
// Governance Policy
|
|
1009
2863
|
if (pathname === "/api/v1/policy" && req.method === "POST") {
|
|
1010
2864
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1011
|
-
const updated = store.updateWorkspacePolicy(ctx.workspace.id, body);
|
|
1012
|
-
|
|
2865
|
+
const updated = store.updateWorkspacePolicy(ctx.workspace.id, sanitizePolicyPatch(body));
|
|
2866
|
+
const plainEnglish = describePolicyInPlainEnglish(updated.policy);
|
|
2867
|
+
sendJson(res, 200, {
|
|
2868
|
+
ok: true,
|
|
2869
|
+
workspace: updated,
|
|
2870
|
+
plainEnglishGuardrails: plainEnglish,
|
|
2871
|
+
explanation: plainEnglish.explanation,
|
|
2872
|
+
});
|
|
1013
2873
|
return;
|
|
1014
2874
|
}
|
|
1015
2875
|
// Plans & Approvals & Execution
|
|
@@ -1022,27 +2882,96 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1022
2882
|
}
|
|
1023
2883
|
if (pathname === "/api/v1/plans" && req.method === "POST") {
|
|
1024
2884
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2885
|
+
const rawFunding = body.fundingMode || body.funding_mode;
|
|
2886
|
+
const fundingMode = rawFunding === "byok" || rawFunding === "customer_funded"
|
|
2887
|
+
? "byok"
|
|
2888
|
+
: rawFunding === "muxo_funded"
|
|
2889
|
+
? "muxo_funded"
|
|
2890
|
+
: undefined;
|
|
1025
2891
|
const planned = await engine.planAction(ctx, {
|
|
1026
2892
|
toolSlug: String(body.toolSlug || body.tool_slug || ""),
|
|
1027
2893
|
input: body.input || {},
|
|
1028
2894
|
reason: body.reason ? String(body.reason) : undefined,
|
|
2895
|
+
fundingMode,
|
|
1029
2896
|
});
|
|
1030
2897
|
sendJson(res, 201, { ok: true, ...planned });
|
|
1031
2898
|
return;
|
|
1032
2899
|
}
|
|
2900
|
+
const resolvePlanCtx = (planId) => {
|
|
2901
|
+
const found = store.getPlanById(planId);
|
|
2902
|
+
if (found &&
|
|
2903
|
+
found.workspaceId !== ctx.workspace.id &&
|
|
2904
|
+
ctx.authType === "session" &&
|
|
2905
|
+
store.userCanAccessWorkspace(ctx.user.id, found.workspaceId)) {
|
|
2906
|
+
const targetWs = store.getWorkspace(found.workspaceId);
|
|
2907
|
+
if (targetWs) {
|
|
2908
|
+
return { ...ctx, workspace: targetWs };
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
return ctx;
|
|
2912
|
+
};
|
|
1033
2913
|
const planApproveMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/approve$/);
|
|
1034
2914
|
if (planApproveMatch && req.method === "POST") {
|
|
1035
2915
|
const planId = decodeURIComponent(planApproveMatch[1]);
|
|
1036
|
-
const
|
|
2916
|
+
const planCtx = resolvePlanCtx(planId);
|
|
2917
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2918
|
+
const decision = body.decision === "rejected" || body.decision === "reject"
|
|
2919
|
+
? "rejected"
|
|
2920
|
+
: "approved";
|
|
2921
|
+
const plan = engine.approvePlan(planCtx, planId, decision);
|
|
1037
2922
|
sendJson(res, 200, { ok: true, plan });
|
|
1038
2923
|
return;
|
|
1039
2924
|
}
|
|
2925
|
+
const planWaitMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/wait$/);
|
|
2926
|
+
if (planWaitMatch && (req.method === "POST" || req.method === "GET")) {
|
|
2927
|
+
const planId = decodeURIComponent(planWaitMatch[1]);
|
|
2928
|
+
const planCtx = resolvePlanCtx(planId);
|
|
2929
|
+
const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
|
|
2930
|
+
const rawTimeout = body.timeoutSeconds ??
|
|
2931
|
+
body.timeout_seconds ??
|
|
2932
|
+
url.searchParams.get("timeout_seconds") ??
|
|
2933
|
+
url.searchParams.get("timeoutSeconds");
|
|
2934
|
+
const waited = await engine.waitForPlanApproval(planCtx, {
|
|
2935
|
+
planId,
|
|
2936
|
+
timeoutSeconds: clampWaitSeconds(rawTimeout, 15, 60),
|
|
2937
|
+
});
|
|
2938
|
+
sendJson(res, 200, waited);
|
|
2939
|
+
return;
|
|
2940
|
+
}
|
|
1040
2941
|
const planExecuteMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)\/execute$/);
|
|
1041
2942
|
if (planExecuteMatch && req.method === "POST") {
|
|
1042
2943
|
const planId = decodeURIComponent(planExecuteMatch[1]);
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
|
|
2944
|
+
const planCtx = resolvePlanCtx(planId);
|
|
2945
|
+
const plan = engine.getPlan(planCtx, planId);
|
|
2946
|
+
const result = await engine.executePlanForRisk(planCtx, planId, plan.riskLevel);
|
|
2947
|
+
sendJson(res, 200, {
|
|
2948
|
+
ok: true,
|
|
2949
|
+
...result,
|
|
2950
|
+
settlement: result.receipt?.supplierSettlement || null,
|
|
2951
|
+
});
|
|
2952
|
+
return;
|
|
2953
|
+
}
|
|
2954
|
+
const planGetMatch = pathname.match(/^\/api\/v1\/plans\/([^/]+)$/);
|
|
2955
|
+
if (planGetMatch && req.method === "GET") {
|
|
2956
|
+
const planId = decodeURIComponent(planGetMatch[1]);
|
|
2957
|
+
const planCtx = resolvePlanCtx(planId);
|
|
2958
|
+
const wait = url.searchParams.get("wait") === "true" ||
|
|
2959
|
+
url.searchParams.get("wait_for_approval") === "true";
|
|
2960
|
+
if (wait) {
|
|
2961
|
+
const waited = await engine.waitForPlanApproval(planCtx, {
|
|
2962
|
+
planId,
|
|
2963
|
+
timeoutSeconds: clampWaitSeconds(url.searchParams.get("timeout_seconds"), 15, 60),
|
|
2964
|
+
});
|
|
2965
|
+
sendJson(res, 200, waited);
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
const plan = engine.getPlan(planCtx, planId);
|
|
2969
|
+
sendJson(res, 200, {
|
|
2970
|
+
ok: true,
|
|
2971
|
+
approved: APPROVED_PLAN_STATUSES.has(plan.approvalStatus),
|
|
2972
|
+
timedOut: false,
|
|
2973
|
+
plan,
|
|
2974
|
+
});
|
|
1046
2975
|
return;
|
|
1047
2976
|
}
|
|
1048
2977
|
// Runs & Receipts
|
|
@@ -1053,6 +2982,35 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1053
2982
|
});
|
|
1054
2983
|
return;
|
|
1055
2984
|
}
|
|
2985
|
+
const runCancelMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)\/cancel$/);
|
|
2986
|
+
if (runCancelMatch && req.method === "POST") {
|
|
2987
|
+
const runId = decodeURIComponent(runCancelMatch[1]);
|
|
2988
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
2989
|
+
const cancelled = engine.cancelRun(ctx, runId, body.reason ? String(body.reason) : undefined);
|
|
2990
|
+
sendJson(res, 200, cancelled);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
const runGetMatch = pathname.match(/^\/api\/v1\/runs\/([^/]+)$/);
|
|
2994
|
+
if (runGetMatch && req.method === "GET") {
|
|
2995
|
+
const runId = decodeURIComponent(runGetMatch[1]);
|
|
2996
|
+
const run = engine.getRun(ctx, runId);
|
|
2997
|
+
sendJson(res, 200, { ok: true, run });
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
if (pathname === "/api/v1/receipts" && req.method === "GET") {
|
|
3001
|
+
const limit = Math.min(100, Math.max(1, Number(url.searchParams.get("limit") || 25)));
|
|
3002
|
+
const list = engine.listReceipts(ctx, limit);
|
|
3003
|
+
sendJson(res, 200, { ok: true, ...list });
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
if ((pathname === "/api/v1/receipts/verify-chain" ||
|
|
3007
|
+
pathname === "/api/v1/receipts/chain") &&
|
|
3008
|
+
req.method === "GET") {
|
|
3009
|
+
const limit = Math.min(200, Math.max(1, Number(url.searchParams.get("limit") || 100)));
|
|
3010
|
+
const chain = engine.verifyReceiptChain(ctx, limit);
|
|
3011
|
+
sendJson(res, 200, chain);
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
1056
3014
|
if (pathname.startsWith("/api/v1/receipts/") && req.method === "GET") {
|
|
1057
3015
|
const receiptId = decodeURIComponent(pathname.slice("/api/v1/receipts/".length));
|
|
1058
3016
|
const verified = engine.getReceipt(ctx, receiptId);
|
|
@@ -1102,7 +3060,13 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1102
3060
|
if ((pathname === "/api/v1/do" || pathname === "/v1/do") &&
|
|
1103
3061
|
req.method === "POST") {
|
|
1104
3062
|
const body = JSON.parse((await readBody(req)) || "{}");
|
|
1105
|
-
const prompt = String(body.prompt ||
|
|
3063
|
+
const prompt = String(body.prompt ||
|
|
3064
|
+
body.instruction ||
|
|
3065
|
+
body.query ||
|
|
3066
|
+
body.command ||
|
|
3067
|
+
body.toolSlug ||
|
|
3068
|
+
body.tool ||
|
|
3069
|
+
"").trim();
|
|
1106
3070
|
if (!prompt) {
|
|
1107
3071
|
sendJson(res, 400, {
|
|
1108
3072
|
ok: false,
|
|
@@ -1110,19 +3074,37 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1110
3074
|
});
|
|
1111
3075
|
return;
|
|
1112
3076
|
}
|
|
3077
|
+
const dryRun = body.dryRun === true ||
|
|
3078
|
+
body.dry_run === true ||
|
|
3079
|
+
body.planOnly === true ||
|
|
3080
|
+
body.autoExecute === false ||
|
|
3081
|
+
body.auto_execute === false;
|
|
3082
|
+
const autoApprove = body.autoApprove !== false &&
|
|
3083
|
+
body.auto_approve !== false &&
|
|
3084
|
+
body.approveRisky !== false &&
|
|
3085
|
+
body.approve_risky !== false;
|
|
3086
|
+
const rawFunding = body.fundingMode || body.funding_mode;
|
|
3087
|
+
const fundingMode = rawFunding === "byok" || rawFunding === "customer_funded"
|
|
3088
|
+
? "byok"
|
|
3089
|
+
: rawFunding === "muxo_funded"
|
|
3090
|
+
? "muxo_funded"
|
|
3091
|
+
: undefined;
|
|
1113
3092
|
const result = await engine.doNaturalLanguage(ctx, {
|
|
1114
3093
|
prompt,
|
|
1115
3094
|
input: body.input && typeof body.input === "object"
|
|
1116
3095
|
? body.input
|
|
1117
3096
|
: undefined,
|
|
1118
|
-
autoApprove
|
|
1119
|
-
dryRun
|
|
3097
|
+
autoApprove,
|
|
3098
|
+
dryRun,
|
|
3099
|
+
planOnly: dryRun,
|
|
3100
|
+
fundingMode,
|
|
1120
3101
|
});
|
|
1121
3102
|
sendJson(res, 200, result);
|
|
1122
3103
|
return;
|
|
1123
3104
|
}
|
|
1124
3105
|
// Live Stripe Billing, Credit Top-Up & Supplier Settlement Ledger
|
|
1125
|
-
if (pathname === "/api/v1/billing/status"
|
|
3106
|
+
if ((pathname === "/api/v1/billing/status" || pathname === "/api/v1/billing") &&
|
|
3107
|
+
req.method === "GET") {
|
|
1126
3108
|
const ws = store.getWorkspace(ctx.workspace.id);
|
|
1127
3109
|
const settlementSummary = store.getSupplierSettlementSummary(ctx.workspace.id);
|
|
1128
3110
|
const byokKeys = store.listByokKeys(ctx.workspace.id);
|
|
@@ -1176,6 +3158,8 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1176
3158
|
sendJson(res, 200, {
|
|
1177
3159
|
ok: true,
|
|
1178
3160
|
creditsAdded: credits,
|
|
3161
|
+
creditBalance: updatedWorkspace.creditsBalance,
|
|
3162
|
+
creditsBalance: updatedWorkspace.creditsBalance,
|
|
1179
3163
|
usdPaid: Number(usdPaid),
|
|
1180
3164
|
workspace: updatedWorkspace,
|
|
1181
3165
|
settlementSummary: store.getSupplierSettlementSummary(ctx.workspace.id),
|
|
@@ -1247,13 +3231,327 @@ Users purchase Muxo Credits (\`POST /api/v1/billing/topup\` or \`muxo billing to
|
|
|
1247
3231
|
sendJson(res, 200, { ok: true, received: true });
|
|
1248
3232
|
return;
|
|
1249
3233
|
}
|
|
3234
|
+
// Stripe v2 Billing Meter Event Stream (`POST /v2/billing/meter_event_session` + `POST /v2/billing/meter_event_stream`)
|
|
3235
|
+
if (pathname === "/api/v1/billing/meter_events" && req.method === "GET") {
|
|
3236
|
+
const limit = Number(url.searchParams.get("limit") || 50);
|
|
3237
|
+
const summary = engine.stripeBilling.listMeterEvents(ctx.workspace.id, limit);
|
|
3238
|
+
sendJson(res, 200, {
|
|
3239
|
+
ok: true,
|
|
3240
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
3241
|
+
...summary,
|
|
3242
|
+
});
|
|
3243
|
+
return;
|
|
3244
|
+
}
|
|
3245
|
+
if (pathname === "/api/v1/billing/meter_events" && req.method === "POST") {
|
|
3246
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3247
|
+
const result = await engine.stripeBilling.recordMeterEvent({
|
|
3248
|
+
workspace: ctx.workspace,
|
|
3249
|
+
event_name: body.event_name ? String(body.event_name) : undefined,
|
|
3250
|
+
value: Number(body.value ?? 1),
|
|
3251
|
+
lane: body.lane ? String(body.lane) : undefined,
|
|
3252
|
+
tool_slug: body.tool_slug ? String(body.tool_slug) : undefined,
|
|
3253
|
+
identifier: body.identifier ? String(body.identifier) : undefined,
|
|
3254
|
+
timestamp: body.timestamp ? String(body.timestamp) : undefined,
|
|
3255
|
+
});
|
|
3256
|
+
sendJson(res, 201, {
|
|
3257
|
+
ok: true,
|
|
3258
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
3259
|
+
...result,
|
|
3260
|
+
});
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
// Stripe v2 Event Destinations & Provisioning Thin Event Webhook Handler
|
|
3264
|
+
if (pathname === "/api/v1/provisioning/event_destinations" &&
|
|
3265
|
+
req.method === "POST") {
|
|
3266
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
3267
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3268
|
+
const destination = await engine.stripeProjects.createV2EventDestination({
|
|
3269
|
+
name: body.name ? String(body.name) : undefined,
|
|
3270
|
+
url: String(body.url || `${config.baseUrl}/api/v1/provisioning/events`),
|
|
3271
|
+
events: Array.isArray(body.events) ? body.events.map(String) : undefined,
|
|
3272
|
+
apiKeyOverride: stripeByok,
|
|
3273
|
+
});
|
|
3274
|
+
sendJson(res, 201, {
|
|
3275
|
+
ok: true,
|
|
3276
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
3277
|
+
...destination,
|
|
3278
|
+
});
|
|
3279
|
+
return;
|
|
3280
|
+
}
|
|
3281
|
+
if (pathname === "/api/v1/provisioning/events" && req.method === "POST") {
|
|
3282
|
+
const stripeByok = store.getByokKey(ctx.workspace.id, "stripe");
|
|
3283
|
+
const verifiedContext = store.getByokKey(ctx.workspace.id, "stripe_connected_account") || undefined;
|
|
3284
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3285
|
+
const handled = await engine.stripeProjects.handleV2ProvisioningEvent({
|
|
3286
|
+
event: body,
|
|
3287
|
+
apiKeyOverride: stripeByok,
|
|
3288
|
+
stripeContext: verifiedContext,
|
|
3289
|
+
});
|
|
3290
|
+
if (handled.revealedConfiguration &&
|
|
3291
|
+
Object.keys(handled.revealedConfiguration).length > 0 &&
|
|
3292
|
+
handled.relatedObjectId) {
|
|
3293
|
+
store.upsertResource({
|
|
3294
|
+
workspaceId: ctx.workspace.id,
|
|
3295
|
+
resourceName: String(handled.resource?.name || handled.relatedObjectId),
|
|
3296
|
+
providerSlug: String(handled.resource?.provider || "neon"),
|
|
3297
|
+
providerName: String(handled.resource?.provider || "Neon"),
|
|
3298
|
+
serviceSlug: String(handled.resource?.service_ref || "postgres"),
|
|
3299
|
+
planSlug: "launch",
|
|
3300
|
+
status: "active",
|
|
3301
|
+
secrets: handled.revealedConfiguration,
|
|
3302
|
+
externalResourceId: handled.relatedObjectId,
|
|
3303
|
+
metadata: {
|
|
3304
|
+
region: "aws-us-east-1",
|
|
3305
|
+
},
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
sendJson(res, 200, {
|
|
3309
|
+
ok: true,
|
|
3310
|
+
apiVersion: engine.stripeProjects.provisioningApiVersion,
|
|
3311
|
+
...handled,
|
|
3312
|
+
});
|
|
3313
|
+
return;
|
|
3314
|
+
}
|
|
3315
|
+
// Composio Dynamic Enterprise Toolkits, Scoped Auth Configs, Triggers & Multi-Tool Batch Execution
|
|
3316
|
+
if (pathname === "/api/v1/toolkits" && req.method === "GET") {
|
|
3317
|
+
const composioByok = store.getByokKey(ctx.workspace.id, "composio");
|
|
3318
|
+
const category = url.searchParams.get("category") || undefined;
|
|
3319
|
+
const search = url.searchParams.get("search") || url.searchParams.get("query") || undefined;
|
|
3320
|
+
const catalog = await engine.composioSdk.listToolkits({
|
|
3321
|
+
category,
|
|
3322
|
+
search,
|
|
3323
|
+
composioApiKey: composioByok,
|
|
3324
|
+
});
|
|
3325
|
+
sendJson(res, 200, { ok: true, ...catalog });
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
if (pathname === "/api/v1/connections/auth-configs" && req.method === "POST") {
|
|
3329
|
+
const composioByok = store.getByokKey(ctx.workspace.id, "composio");
|
|
3330
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3331
|
+
const created = await engine.composioSdk.createCustomAuthConfig({
|
|
3332
|
+
workspaceId: ctx.workspace.id,
|
|
3333
|
+
toolkitSlug: String(body.toolkitSlug || body.provider || "github"),
|
|
3334
|
+
name: body.name ? String(body.name) : undefined,
|
|
3335
|
+
authScheme: body.authScheme === "API_KEY" ? "API_KEY" : "OAUTH2",
|
|
3336
|
+
scopeMode: body.scopeMode === "full_access" ? "full_access" : "read_only",
|
|
3337
|
+
customScopes: Array.isArray(body.customScopes)
|
|
3338
|
+
? body.customScopes.map(String)
|
|
3339
|
+
: undefined,
|
|
3340
|
+
credentials: body.credentials && typeof body.credentials === "object"
|
|
3341
|
+
? body.credentials
|
|
3342
|
+
: undefined,
|
|
3343
|
+
composioApiKey: composioByok,
|
|
3344
|
+
});
|
|
3345
|
+
sendJson(res, 201, { ok: true, ...created });
|
|
3346
|
+
return;
|
|
3347
|
+
}
|
|
3348
|
+
if (pathname === "/api/v1/triggers" && req.method === "GET") {
|
|
3349
|
+
const provider = url.searchParams.get("provider") || undefined;
|
|
3350
|
+
const triggerSlug = url.searchParams.get("triggerSlug") || url.searchParams.get("trigger_slug") || undefined;
|
|
3351
|
+
const composioByok = store.getByokKey(ctx.workspace.id, "composio");
|
|
3352
|
+
const available = engine.composioSdk.listTriggerTypes(provider);
|
|
3353
|
+
const subscriptions = engine.composioSdk.listActiveTriggers(ctx.workspace.id);
|
|
3354
|
+
const events = engine.composioSdk.listTriggerEvents(ctx.workspace.id, 25);
|
|
3355
|
+
const triggerType = triggerSlug
|
|
3356
|
+
? await engine.composioSdk.getTriggerType(triggerSlug, composioByok)
|
|
3357
|
+
: undefined;
|
|
3358
|
+
sendJson(res, 200, {
|
|
3359
|
+
ok: true,
|
|
3360
|
+
availableTriggers: available,
|
|
3361
|
+
activeSubscriptions: subscriptions,
|
|
3362
|
+
recentEvents: events,
|
|
3363
|
+
webhookConfig: engine.composioSdk.getWebhookConfigInfo(),
|
|
3364
|
+
...(triggerType ? { triggerType } : {}),
|
|
3365
|
+
});
|
|
3366
|
+
return;
|
|
3367
|
+
}
|
|
3368
|
+
if (pathname === "/api/v1/triggers" && req.method === "POST") {
|
|
3369
|
+
const composioByok = store.getByokKey(ctx.workspace.id, "composio");
|
|
3370
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3371
|
+
const rawConfig = body.triggerConfig && typeof body.triggerConfig === "object"
|
|
3372
|
+
? body.triggerConfig
|
|
3373
|
+
: body.config && typeof body.config === "object"
|
|
3374
|
+
? body.config
|
|
3375
|
+
: {};
|
|
3376
|
+
const subscription = await engine.composioSdk.enableTrigger({
|
|
3377
|
+
workspaceId: ctx.workspace.id,
|
|
3378
|
+
userId: body.userId ? String(body.userId) : undefined,
|
|
3379
|
+
triggerSlug: String(body.triggerSlug || body.trigger_slug || "GITHUB_COMMIT_EVENT"),
|
|
3380
|
+
providerSlug: String(body.providerSlug || body.provider || "github"),
|
|
3381
|
+
connectedAccountId: body.connectedAccountId
|
|
3382
|
+
? String(body.connectedAccountId)
|
|
3383
|
+
: undefined,
|
|
3384
|
+
webhookUrl: body.webhookUrl ? String(body.webhookUrl) : undefined,
|
|
3385
|
+
config: rawConfig,
|
|
3386
|
+
subscribe: body.subscribe === true,
|
|
3387
|
+
composioApiKey: composioByok,
|
|
3388
|
+
});
|
|
3389
|
+
sendJson(res, 201, { ok: true, subscription });
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
if ((pathname === "/api/v1/triggers/webhook" ||
|
|
3393
|
+
pathname === "/webhooks" ||
|
|
3394
|
+
pathname === "/webhooks/composio") &&
|
|
3395
|
+
req.method === "POST") {
|
|
3396
|
+
const rawBody = await readBody(req);
|
|
3397
|
+
const sigHeader = String(req.headers["webhook-signature"] ||
|
|
3398
|
+
req.headers["x-composio-webhook-signature"] ||
|
|
3399
|
+
"").trim();
|
|
3400
|
+
const webhookIdHeader = String(req.headers["webhook-id"] || req.headers["x-composio-webhook-id"] || "").trim();
|
|
3401
|
+
const webhookTimestampHeader = String(req.headers["webhook-timestamp"] ||
|
|
3402
|
+
req.headers["x-composio-webhook-timestamp"] ||
|
|
3403
|
+
"").trim();
|
|
3404
|
+
const webhookVersionHeader = String(req.headers["x-composio-webhook-version"] || "").trim();
|
|
3405
|
+
let signatureVerified = false;
|
|
3406
|
+
if (sigHeader) {
|
|
3407
|
+
const check = await verifyComposioWebhookSignature({
|
|
3408
|
+
rawPayload: rawBody,
|
|
3409
|
+
signatureHeader: sigHeader,
|
|
3410
|
+
webhookIdHeader,
|
|
3411
|
+
webhookTimestampHeader,
|
|
3412
|
+
secret: config.composioWebhookSecret,
|
|
3413
|
+
});
|
|
3414
|
+
if (config.composioWebhookSecret && !check.verified) {
|
|
3415
|
+
sendJson(res, 401, {
|
|
3416
|
+
ok: false,
|
|
3417
|
+
error: `Invalid Composio webhook signature (${check.reason || "verification_failed"})`,
|
|
3418
|
+
});
|
|
3419
|
+
return;
|
|
3420
|
+
}
|
|
3421
|
+
signatureVerified = check.verified;
|
|
3422
|
+
}
|
|
3423
|
+
const body = JSON.parse(rawBody || "{}");
|
|
3424
|
+
const envelope = parseComposioWebhookEnvelope(body, {
|
|
3425
|
+
webhookId: webhookIdHeader || undefined,
|
|
3426
|
+
webhookTimestamp: webhookTimestampHeader || undefined,
|
|
3427
|
+
webhookVersion: webhookVersionHeader || undefined,
|
|
3428
|
+
});
|
|
3429
|
+
let targetWorkspaceId = ctx.workspace.id;
|
|
3430
|
+
if (envelope.userId && store.getWorkspace(envelope.userId)) {
|
|
3431
|
+
targetWorkspaceId = envelope.userId;
|
|
3432
|
+
}
|
|
3433
|
+
if (envelope.eventType === "composio.connected_account.activated" ||
|
|
3434
|
+
envelope.eventType === "composio.connected_account.expired") {
|
|
3435
|
+
const nextStatus = envelope.eventType === "composio.connected_account.activated"
|
|
3436
|
+
? "active"
|
|
3437
|
+
: "expired";
|
|
3438
|
+
store.upsertConnection({
|
|
3439
|
+
workspaceId: targetWorkspaceId,
|
|
3440
|
+
lane: "composio_sdk",
|
|
3441
|
+
providerSlug: envelope.providerSlug,
|
|
3442
|
+
providerName: envelope.providerSlug.charAt(0).toUpperCase() +
|
|
3443
|
+
envelope.providerSlug.slice(1),
|
|
3444
|
+
status: nextStatus,
|
|
3445
|
+
externalAccountId: envelope.connectedAccountId || null,
|
|
3446
|
+
metadata: {
|
|
3447
|
+
webhookEvent: envelope.eventType,
|
|
3448
|
+
webhookId: envelope.webhookId,
|
|
3449
|
+
updatedViaWebhookAt: new Date().toISOString(),
|
|
3450
|
+
},
|
|
3451
|
+
});
|
|
3452
|
+
if (nextStatus === "expired") {
|
|
3453
|
+
engine.composioSdk.disableTriggersMatching({
|
|
3454
|
+
workspaceId: targetWorkspaceId,
|
|
3455
|
+
connectedAccountId: envelope.connectedAccountId,
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
else if (envelope.eventType === "composio.trigger.disabled") {
|
|
3460
|
+
engine.composioSdk.disableTriggersMatching({
|
|
3461
|
+
workspaceId: targetWorkspaceId,
|
|
3462
|
+
triggerId: envelope.triggerId,
|
|
3463
|
+
triggerSlug: envelope.triggerSlug,
|
|
3464
|
+
connectedAccountId: envelope.connectedAccountId,
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
const rawPayloadText = JSON.stringify(envelope.payloadObj);
|
|
3468
|
+
const injectionScan = engine.governance.scanMemoryContent({
|
|
3469
|
+
workspaceId: targetWorkspaceId,
|
|
3470
|
+
content: rawPayloadText,
|
|
3471
|
+
});
|
|
3472
|
+
const disposition = injectionScan.safe ? "dispatched" : "quarantined";
|
|
3473
|
+
const eventRecord = {
|
|
3474
|
+
id: `trgevt_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
|
3475
|
+
workspaceId: targetWorkspaceId,
|
|
3476
|
+
eventType: envelope.eventType,
|
|
3477
|
+
webhookVersion: envelope.webhookVersion,
|
|
3478
|
+
signatureVerified,
|
|
3479
|
+
webhookId: envelope.webhookId,
|
|
3480
|
+
triggerId: envelope.triggerId,
|
|
3481
|
+
triggerSlug: envelope.triggerSlug,
|
|
3482
|
+
providerSlug: envelope.providerSlug,
|
|
3483
|
+
connectedAccountId: envelope.connectedAccountId,
|
|
3484
|
+
userId: envelope.userId,
|
|
3485
|
+
projectId: envelope.projectId,
|
|
3486
|
+
orgId: envelope.orgId,
|
|
3487
|
+
receivedAt: new Date().toISOString(),
|
|
3488
|
+
promptInjectionScan: {
|
|
3489
|
+
safe: injectionScan.safe,
|
|
3490
|
+
riskScore: injectionScan.findingsCount * 0.35,
|
|
3491
|
+
findings: injectionScan.findings.map((f) => `${f.patternId}: ${f.description}`),
|
|
3492
|
+
},
|
|
3493
|
+
disposition: disposition,
|
|
3494
|
+
payloadPreview: envelope.payloadObj,
|
|
3495
|
+
};
|
|
3496
|
+
engine.composioSdk.recordTriggerEvent(eventRecord);
|
|
3497
|
+
sendJson(res, injectionScan.safe ? 200 : 202, {
|
|
3498
|
+
ok: true,
|
|
3499
|
+
...eventRecord,
|
|
3500
|
+
});
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
if (pathname === "/api/v1/plans/batch" && req.method === "POST") {
|
|
3504
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3505
|
+
const batchResult = await engine.executeBatchPlans(ctx, {
|
|
3506
|
+
items: Array.isArray(body.items) ? body.items : [],
|
|
3507
|
+
// Only a signed-in person may pre-approve pending plans; agent keys never can.
|
|
3508
|
+
autoApprove: body.autoApprove === true && signedInPerson(auth),
|
|
3509
|
+
});
|
|
3510
|
+
sendJson(res, 200, { ok: true, ...batchResult });
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
// Cloudflare Vectorize Hybrid Semantic + Bi-Temporal Recall, Durable Workflows, and R2 WORM Audit Archive
|
|
3514
|
+
if (pathname === "/api/v1/memory/hybrid" && (req.method === "GET" || req.method === "POST")) {
|
|
3515
|
+
const body = req.method === "POST" ? JSON.parse((await readBody(req)) || "{}") : {};
|
|
3516
|
+
const query = String(body.query ?? body.q ?? url.searchParams.get("query") ?? url.searchParams.get("q") ?? "");
|
|
3517
|
+
const limit = Number(body.limit ?? url.searchParams.get("limit") ?? 10);
|
|
3518
|
+
const hybrid = engine.searchMemoryHybrid(ctx.workspace.id, query, limit);
|
|
3519
|
+
sendJson(res, 200, { ok: true, ...hybrid });
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
if (pathname === "/api/v1/workflows" && req.method === "GET") {
|
|
3523
|
+
const workflows = engine.listDurableWorkflows(ctx.workspace.id);
|
|
3524
|
+
sendJson(res, 200, { ok: true, workflows });
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
if (pathname === "/api/v1/workflows" && req.method === "POST") {
|
|
3528
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3529
|
+
const workflow = await engine.startDurableWorkflow(ctx, {
|
|
3530
|
+
provider: body.provider ? String(body.provider) : undefined,
|
|
3531
|
+
serviceRef: body.serviceRef || body.service_ref ? String(body.serviceRef || body.service_ref) : undefined,
|
|
3532
|
+
name: body.name ? String(body.name) : undefined,
|
|
3533
|
+
region: body.region ? String(body.region) : undefined,
|
|
3534
|
+
});
|
|
3535
|
+
sendJson(res, 201, { ok: true, workflow });
|
|
3536
|
+
return;
|
|
3537
|
+
}
|
|
3538
|
+
const wfAdvanceMatch = pathname.match(/^\/api\/v1\/workflows\/([^/]+)\/advance$/);
|
|
3539
|
+
if (wfAdvanceMatch && req.method === "POST") {
|
|
3540
|
+
const wfId = decodeURIComponent(wfAdvanceMatch[1]);
|
|
3541
|
+
const body = JSON.parse((await readBody(req)) || "{}");
|
|
3542
|
+
const workflow = await engine.advanceDurableWorkflow(ctx, wfId, body);
|
|
3543
|
+
sendJson(res, 200, { ok: true, workflow });
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
if (pathname === "/api/v1/governance/archive" && req.method === "GET") {
|
|
3547
|
+
const archive = engine.exportTamperEvidentArchive(ctx.workspace.id);
|
|
3548
|
+
sendJson(res, 200, { ok: true, ...archive });
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
1250
3551
|
sendJson(res, 404, { ok: false, error: `Route not found: ${req.method} ${pathname}` });
|
|
1251
3552
|
}
|
|
1252
3553
|
catch (err) {
|
|
1253
|
-
sendJson(res, 400,
|
|
1254
|
-
ok: false,
|
|
1255
|
-
error: String(err?.message || err || "Internal error"),
|
|
1256
|
-
});
|
|
3554
|
+
sendJson(res, err instanceof MuxoToolError ? err.statusCode : 400, errorPayloadFrom(err, "Internal error"));
|
|
1257
3555
|
}
|
|
1258
3556
|
});
|
|
1259
3557
|
return { server, engine, store, defaultApiKey: bootstrap.defaultApiKey };
|